Ostiary — удалённый запуск скриптов
Консольный клиент с текстовым интерфейсом (TUI) на Rust. Загружает иерархическое меню с сервера и позволяет запускать bash-скрипты, скачивать файлы и выполнять HTTP-запросы прямо из терминале. Все диалоги (выбор из списка, подтверждение, ввод текста) реализованы внутри интерфейса — никаких сырых read -p в терминале.
Навигация
Главное меню
| Клавиша | Действие |
|---|---|
↑ / ↓ или k / j |
Перемещение по списку |
Enter или l |
Войти в категорию / запустить действие |
Esc или h |
Выйти из категории |
q |
Выйти из приложения |
R |
Перезагрузить меню с сервера |
Popup запущенного скрипта
| Клавиша | Действие |
|---|---|
↑ / ↓ или k / j |
Прокрутка лога / навигация по селектору |
PgUp / PgDn |
Прокрутка лога на 10 строк |
Y / N или l / h |
Ответ на подтверждение (без Enter) |
Enter или l |
Выбрать пункт селектора / подтвердить |
Esc или h |
Прервать скрипт и закрыть popup |
Vim-motions (
j/k/l/h) отключаются автоматически во время текстового ввода — символы идут в поле ввода как обычно.
Конфигурация
При первом запуске клиент запрашивает URL сервера прямо в терминале — до входа в TUI:
╔══════════════════════════════════╗
║ Ostiary — первый запуск ║
╚══════════════════════════════════╝
Конфиг будет сохранён в:
/home/user/.config/ostiary/config.toml
URL сервера меню: https://your-server/api/menu
После ввода создаётся ~/.config/ostiary/config.toml. Его можно редактировать вручную:
server_url = "https://your-server/api/menu"
timeout_sec = 10
update_api = "https://git.example.com/api/v1/repos/user/ostiary"
[theme]
selected_bg = "blue"
| Параметр | Описание |
|---|---|
server_url |
URL эндпоинта GET /api/menu |
timeout_sec |
Таймаут HTTP-запросов в секундах |
update_api |
Базовый URL Gitea API для проверки обновлений (опционально) |
Автообновление
Если задан update_api, при каждом запуске клиент в фоне проверяет наличие нового релиза в Gitea. Если найдена новая стабильная версия — показывает диалог:
Доступно обновление
1.0.0 → 1.1.0
Размер: 4.2 МБ
[Y / l] Обновить [N / h / Esc] Пропустить
При подтверждении скачивает бинарник, атомарно заменяет текущий исполняемый файл и перезапускает приложение через execv (процесс не пересоздаётся, PID остаётся тем же).
Naming convention ассетов в Gitea
Файлы релиза должны называться по шаблону {name}-{os}-{arch}:
| Файл | Платформа |
|---|---|
ostiary-linux-x86_64 |
Linux 64-bit |
ostiary-linux-aarch64 |
Linux ARM64 |
ostiary-macos-aarch64 |
macOS Apple Silicon |
ostiary-macos-x86_64 |
macOS Intel |
Формат меню (JSON)
Сервер отдаёт GET /api/menu с Content-Type: application/json.
Корневая структура
{
"version": "1.0",
"menu": [ ...пункты... ]
}
Категория
Содержит вложенные пункты. Отображается с иконкой 📁.
{
"id": "bitrix",
"title": "1С Битрикс",
"description": "Инструменты администрирования",
"children": [
...пункты меню...
]
}
Действие
Отображается с иконкой ▶.
{
"id": "db_manage",
"title": "Управление БД",
"description": "Дамп, импорт, подключение к базе",
"action": {
"type": "bash",
"script": "#!/bin/bash\necho hello",
"interaction": "structured",
"confirm": false
}
}
confirm и confirm_message
Если confirm: true, перед запуском появится диалог подтверждения.
confirm_message переопределяет текст вопроса.
{
"id": "drop_db",
"title": "Удалить базу данных",
"action": {
"type": "bash",
"script": "...",
"interaction": "structured",
"confirm": true,
"confirm_message": "База данных будет удалена без возможности восстановления. Продолжить?"
}
}
Типы действий
bash — запуск скрипта
{
"type": "bash",
"script": "#!/bin/bash\n...",
"interaction": "structured",
"confirm": false
}
| Параметр | Значения | Описание |
|---|---|---|
script |
строка | Полный текст bash-скрипта включая shebang |
interaction |
"structured" / "terminal" |
Режим взаимодействия |
confirm |
true / false |
Запросить подтверждение перед запуском |
confirm_message |
строка | Кастомный текст диалога подтверждения |
interaction: "terminal" — скрипт запускается дочерним процессом без захвата вывода.
interaction: "structured" — весь вывод отображается в прокручиваемом логе с ANSI-цветами, скрипт может запрашивать ввод через протокол команд.
download — скачивание файла
{
"type": "download",
"url": "https://example.com/file.tar.gz",
"filename": "backup.tar.gz",
"target_dir": "/var/backups",
"confirm": false
}
download_and_run — скачать и выполнить
{
"type": "download_and_run",
"url": "https://example.com/installer.sh",
"run_args": ["--silent", "--prefix=/opt"],
"keep_file": false,
"confirm": true
}
http_request — HTTP-запрос
{
"type": "http_request",
"method": "POST",
"url": "https://api.example.com/deploy",
"headers": { "Authorization": "Bearer token123" },
"body": "{\"env\": \"production\"}",
"confirm": true
}
Интерактивные скрипты (structured)
При interaction: "structured" скрипт общается с клиентом через протокол команд. Команды отправляются в stderr (>&2), ответы приходят в stdin.
Базовые хелперы
#!/bin/bash
# Отправить команду и получить ответ от пользователя
ask() {
printf 'CMD:%s\n' "$1" >&2
read -r _response
printf '%s' "$_response"
}
# Отправить уведомление (не ждёт ответа, не блокирует скрипт)
notify() {
printf 'CMD:%s\n' "$1" >&2
}
Ввод текста (input)
{"type":"input","prompt":"Введите имя пользователя:","default":"admin","secret":false}
| Поле | Описание |
|---|---|
prompt |
Заголовок поля ввода |
default |
Значение по умолчанию (опционально) |
secret |
Если true — символы заменяются на • |
username=$(ask '{"type":"input","prompt":"Имя пользователя:","default":"admin"}')
password=$(ask '{"type":"input","prompt":"Пароль:","secret":true}')
Выбор из списка (menu)
Список с навигацией ↑/↓/j/k, выбор по Enter/l. Возвращает id выбранного пункта.
{
"type": "menu",
"prompt": "Выберите действие:",
"options": [
{"id": "dump", "label": "Создать дамп базы"},
{"id": "import", "label": "Импортировать дамп"},
{"id": "connect","label": "Подключиться к БД"}
]
}
action=$(ask '{"type":"menu","prompt":"Выберите действие:","options":[
{"id":"dump","label":"Создать дамп"},
{"id":"import","label":"Импортировать"},
{"id":"exit","label":"Отмена"}
]}')
case $action in
dump) echo "Создаю дамп..." ;;
import) echo "Импортирую..." ;;
exit) exit 0 ;;
esac
Динамический список (из массива bash):
files=($(ls *.sql 2>/dev/null))
options='['
for i in "${!files[@]}"; do
[ $i -gt 0 ] && options+=','
escaped=$(printf '%s' "${files[$i]}" | sed 's/"/\\"/g')
options+="{\"id\":\"$i\",\"label\":\"${escaped}\"}"
done
options+=']'
idx=$(ask "{\"type\":\"menu\",\"prompt\":\"Выберите файл:\",\"options\":${options}}")
selected="${files[$idx]}"
Подтверждение (confirm)
Показывает вопрос с кнопками [Y]/[N]. Клавиши Y/N/l/h обрабатываются без Enter.
{"type":"confirm","prompt":"Данные будут перезаписаны. Продолжить?"}
Ответ: "y" или "n".
answer=$(ask '{"type":"confirm","prompt":"Удалить временные файлы?"}')
if [[ "$answer" =~ ^[Yy]$ ]]; then
rm -rf /tmp/myapp_*
fi
Сообщение (message)
Показывает сообщение с цветом. Скрипт продолжается после нажатия Enter.
{"type":"message","level":"info","text":"Операция завершена успешно!"}
level |
Цвет |
|---|---|
"info" |
Зелёный |
"warn" |
Жёлтый |
"error" |
Красный |
if ! command -v mysqldump &> /dev/null; then
ask '{"type":"message","level":"error","text":"mysqldump не найден."}' > /dev/null
exit 1
fi
> /dev/null— сбрасывает пустой ответ клиента чтобы он не попал в переменную.
Прогресс (progress)
Обновляет прогресс-бар не блокируя скрипт (используйте notify, не ask).
{"type":"progress","percent":45,"message":"Копирование файлов..."}
notify '{"type":"progress","percent":0,"message":"Начало..."}'
mysqldump -h "$host" -u "$user" "$db" > backup.sql
notify '{"type":"progress","percent":70,"message":"Сжимаю..."}'
gzip backup.sql
notify '{"type":"progress","percent":100,"message":"Готово!"}'
Запуск внешнего приложения (exec)
Временно передаёт терминал внешней программе (mysql, vim, htop и др.). Клиент скрывает TUI, программа работает на полном экране, после завершения TUI восстанавливается. Возвращает код завершения.
{"type":"exec","shell":"mysql -h localhost -u root mydb"}
# Интерактивная сессия mysql
exit_code=$(ask "{\"type\":\"exec\",\"shell\":\"mysql -h \\\"$host\\\" -u \\\"$user\\\" -p\\\"$pass\\\" \\\"$db\\\"\"}")
echo "Сессия завершена (код: $exit_code)"
# Редактирование конфига
ask '{"type":"exec","shell":"vim /etc/nginx/nginx.conf"}' > /dev/null
systemctl reload nginx
# Мониторинг ресурсов
ask '{"type":"exec","shell":"htop"}' > /dev/null
Полный пример скрипта
#!/bin/bash
ask() { printf 'CMD:%s\n' "$1" >&2; read -r _r; printf '%s' "$_r"; }
notify() { printf 'CMD:%s\n' "$1" >&2; }
host=$(ask '{"type":"input","prompt":"Хост:","default":"localhost"}')
db=$(ask '{"type":"input","prompt":"База данных:"}')
user=$(ask '{"type":"input","prompt":"Пользователь:","default":"root"}')
pass=$(ask '{"type":"input","prompt":"Пароль:","secret":true}')
action=$(ask "{\"type\":\"menu\",\"prompt\":\"Выберите действие:\",\"options\":[
{\"id\":\"dump\", \"label\":\"Создать дамп\"},
{\"id\":\"shell\", \"label\":\"Открыть mysql-консоль\"},
{\"id\":\"cancel\",\"label\":\"Отмена\"}
]}")
case $action in
dump)
confirm=$(ask '{"type":"confirm","prompt":"Создать дамп базы данных?"}')
[[ ! "$confirm" =~ ^[Yy]$ ]] && exit 0
backup="backup_${db}_$(date +%Y%m%d_%H%M%S).sql"
notify '{"type":"progress","percent":10,"message":"Создание дампа..."}'
if mysqldump -h "$host" -u "$user" -p"$pass" "$db" > "$backup"; then
notify '{"type":"progress","percent":100,"message":"Готово!"}'
echo "Дамп: $backup ($(du -h "$backup" | cut -f1))"
else
ask '{"type":"message","level":"error","text":"Ошибка при создании дампа!"}' > /dev/null
exit 1
fi
;;
shell)
code=$(ask "{\"type\":\"exec\",\"shell\":\"mysql -h \\\"$host\\\" -u \\\"$user\\\" -p\\\"$pass\\\" \\\"$db\\\"\"}")
echo "Сессия завершена с кодом $code"
;;
cancel) exit 0 ;;
esac
Структура сервера
server/
├── index.php # Собирает JSON: menu_structure.json + scripts/
├── menu_structure.json # Иерархия меню (script_file вместо script)
└── scripts/
├── bitrix_db_manage.sh
├── bitrix_monitoring.sh
├── network_suspicious_connections.sh
└── wordpress_db_backup.sh
В menu_structure.json вместо поля "script" используется "script_file":
{
"id": "db_manage",
"title": "Управление БД",
"action": {
"type": "bash",
"script_file": "bitrix_db_manage.sh",
"interaction": "structured",
"confirm": false
}
}
index.php читает файл скрипта при каждом запросе — скрипты редактируются без изменения JSON.
Добавление нового пункта меню
- Создать
server/scripts/category_action.sh - Добавить пункт в
menu_structure.json:
{
"id": "my_action",
"title": "Название в меню",
"description": "Описание для нижней панели",
"action": {
"type": "bash",
"script_file": "category_action.sh",
"interaction": "structured",
"confirm": false
}
}
Соглашение по именованию файлов: {категория}_{подкатегория}_{действие}.sh
Сборка
Скрипт release.sh собирает бинарники для всех платформ сразу:
cd Ostiary
./release.sh
# → dist/ostiary-linux-x86_64
# → dist/ostiary-linux-aarch64
# → dist/ostiary-macos-aarch64
Вручную — Linux:
cargo build --release
# → target/release/ostiary
Вручную — кросс-компиляция macOS → Linux x86_64 (musl, статический):
rustup target add x86_64-unknown-linux-musl
brew install FiloSottile/musl-cross/musl-cross
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \
cargo build --release --target x86_64-unknown-linux-musl
# → target/x86_64-unknown-linux-musl/release/ostiary
Вручную — ARM64 Linux:
rustup target add aarch64-unknown-linux-musl
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc \
cargo build --release --target aarch64-unknown-linux-musl
# → target/aarch64-unknown-linux-musl/release/ostiary
Бинарник собирается статически (musl) и не требует никаких зависимостей на целевом сервере.