Files
ostiary/README.md
2026-05-21 21:41:10 +07:00

18 KiB
Raw Blame History

Ostiary — удалённый запуск скриптов

Консольный клиент с текстовым интерфейсом (TUI) на Rust. Загружает иерархическое меню с сервера и позволяет запускать bash-скрипты, скачивать файлы и выполнять HTTP-запросы прямо из терминала. Все диалоги (выбор из списка, подтверждение, ввод текста) реализованы внутри интерфейса — никаких сырых read -p в терминале.


Навигация

Клавиша Действие
/ Перемещение по списку
Enter Войти в категорию / запустить действие
Esc Выйти из категории / закрыть popup / отменить скрипт
q Выйти из приложения
R Перезагрузить меню с сервера

В попапе запущенного скрипта:

Клавиша Действие
/ Прокрутка лога (если нет активного запроса) / навигация по селектору
PgUp / PgDn Прокрутка лога на 10 строк
Y / N Быстрый ответ на подтверждение
Esc Прервать скрипт и закрыть popup

Конфигурация

При первом запуске создаётся файл ~/.config/tui-client/config.toml:

server_url = "http://your-server/api/menu"
timeout_sec = 10

[theme]
selected_bg = "blue"

Формат меню (JSON)

Сервер должен отдавать GET /api/menu с Content-Type: application/json.

Корневая структура

{
  "version": "1.0",
  "menu": [ ...пункты... ]
}

Категория

Содержит вложенные пункты. Отображается с иконкой 📁.

{
  "id": "bitrix",
  "title": "1С Битрикс",
  "description": "Инструменты администрирования",
  "children": [
    ...пункты меню...
  ]
}
Поле Обязательное Описание
id Уникальный идентификатор
title Отображаемое название
description Текст в нижней панели при выборе
children Вложенные категории и действия

Действие

Отображается с иконкой .

{
  "id": "db_manage",
  "title": "Управление БД",
  "description": "Дамп, импорт, подключение к базе",
  "action": {
    "type": "bash",
    "script": "#!/bin/bash\necho hello",
    "interaction": "structured",
    "confirm": false
  }
}
Поле Обязательное Описание
id Уникальный идентификатор
title Отображаемое название
description Текст в нижней панели
action Объект действия

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
}
Параметр Описание
url Адрес файла
filename Имя сохраняемого файла (по умолчанию — из URL)
target_dir Директория сохранения (по умолчанию — текущая)

download_and_run — скачать и выполнить

{
  "type": "download_and_run",
  "url": "https://example.com/installer.sh",
  "run_args": ["--silent", "--prefix=/opt"],
  "keep_file": false,
  "confirm": true
}
Параметр Описание
run_args Аргументы командной строки
keep_file Оставить файл после выполнения (по умолчанию false)

http_request — HTTP-запрос

{
  "type": "http_request",
  "method": "POST",
  "url": "https://api.example.com/deploy",
  "headers": {
    "Authorization": "Bearer token123",
    "Content-Type": "application/json"
  },
  "body": "{\"env\": \"production\"}",
  "confirm": true
}

Интерактивные скрипты (structured)

При interaction: "structured" скрипт может запрашивать у пользователя данные через специальный протокол. Клиент показывает соответствующий элемент интерфейса ratatui и ждёт ответа.

Базовые хелперы

Добавьте эти функции в начало каждого интерактивного скрипта:

#!/bin/bash

# Отправить команду и получить ответ от пользователя
ask() {
    printf 'CMD:%s\n' "$1" >&2
    read -r _response
    printf '%s' "$_response"
}

# Отправить уведомление (не ждёт ответа)
notify() {
    printf 'CMD:%s\n' "$1" >&2
}

Команды отправляются в stderr (>&2), ответы приходят в stdin через read. Это позволяет использовать $(ask '...') для захвата результата.


Ввод текста (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}')
echo "Логин: $username"

Выбор из списка (menu)

Показывает список с навигацией / и выбором по Enter. Возвращает id выбранного пункта.

Команда:

{
  "type": "menu",
  "prompt": "Выберите действие:",
  "options": [
    {"id": "dump",   "label": "Создать дамп базы"},
    {"id": "import", "label": "Импортировать дамп"},
    {"id": "connect","label": "Подключиться к БД"}
  ]
}
Поле Описание
prompt Заголовок списка
options Массив объектов {"id": "...", "label": "..."}

Ответ: строка id выбранного пункта.

Пример:

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

Динамический список (например, файлы на диске):

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]}"
echo "Выбран: $selected"

Подтверждение (confirm)

Показывает вопрос с кнопками [Y] Да и [N] Нет. Нажатия Y/N обрабатываются без Enter.

Команда:

{"type":"confirm","prompt":"Данные будут перезаписаны. Продолжить?"}

Ответ: "y" или "n".

Пример:

answer=$(ask '{"type":"confirm","prompt":"Удалить временные файлы?"}')

if [[ "$answer" =~ ^[Yy]$ ]]; then
    rm -rf /tmp/myapp_*
    echo "Файлы удалены"
else
    echo "Отменено"
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 не найден. Установите пакет mysql-client."}' > /dev/null
    exit 1
fi

ask '{"type":"message","level":"warn","text":"База данных занимает более 10 ГБ. Дамп может занять несколько минут."}' > /dev/null

echo "Начинаю создание дампа..."

> /dev/null — сбрасывает пустой ответ клиента, чтобы он не попал в переменную.


Прогресс (progress)

Обновляет прогресс-бар без остановки скрипта. Скрипт продолжает выполнение немедленно.

Команда:

{"type":"progress","percent":45,"message":"Копирование файлов..."}
Поле Описание
percent Число от 0 до 100
message Подпись под прогресс-баром (опционально)

Пример:

notify '{"type":"progress","percent":0,"message":"Начало..."}'

mysqldump -h "$host" -u "$user" "$db" > backup.sql
notify '{"type":"progress","percent":60,"message":"Дамп создан, сжимаю..."}'

gzip backup.sql
notify '{"type":"progress","percent":100,"message":"Готово!"}'
echo "Файл: backup.sql.gz ($(du -h backup.sql.gz | cut -f1))"

Запуск внешнего приложения (exec)

Временно передаёт терминал внешней программе (mysql, vim, htop и т.д.). Клиент скрывает ratatui, программа работает на полном экране, после завершения TUI восстанавливается. Возвращает код завершения.

Команда:

{"type":"exec","shell":"mysql -h localhost -u root mydb"}
Поле Описание
shell Команда, которая будет выполнена через bash -c

Ответ: код завершения программы ("0", "1" и т.д.).

Примеры:

# Интерактивная сессия 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
notify '{"type":"progress","percent":0,"message":"Перезагружаю nginx..."}'
systemctl reload nginx

# Мониторинг ресурсов
ask '{"type":"exec","shell":"htop"}' > /dev/null

Полный пример скрипта

#!/bin/bash

ask() {
    printf 'CMD:%s\n' "$1" >&2
    read -r _response
    printf '%s' "$_response"
}

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}')

echo "Подключаюсь к $db на $host..."

# Выбор действия
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)
    echo "Открываю mysql-консоль..."
    code=$(ask "{\"type\":\"exec\",\"shell\":\"mysql -h \\\"$host\\\" -u \\\"$user\\\" -p\\\"$pass\\\" \\\"$db\\\"\"}")
    echo "Сессия завершена с кодом $code"
    ;;

  cancel)
    echo "Отменено"
    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
    └── 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.

Добавление нового пункта меню

  1. Создать скрипт server/scripts/myapp_action.sh
  2. Добавить пункт в menu_structure.json:
{
  "id": "myapp_action",
  "title": "Название в меню",
  "description": "Описание для нижней панели",
  "action": {
    "type": "bash",
    "script_file": "myapp_action.sh",
    "interaction": "structured",
    "confirm": false
  }
}

Именование файлов: {категория}_{подкатегория}_{действие}.sh


Сборка

Linux:

cd Ratatui
cargo build --release
# → target/release/tui-client

Кросс-компиляция с macOS → Linux (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/tui-client

Бинарный файл собирается статически и не требует никаких зависимостей на целевом сервере.