2026-05-29 11:35:20 +07:00
2026-05-29 05:54:10 +07:00
2026-05-29 05:54:10 +07:00
2026-05-29 05:54:10 +07:00
2026-05-29 11:35:20 +07:00
2026-05-22 01:00:57 +07:00

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

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


Установка

Быстрая установка (Linux / macOS)

curl -fsSL https://vainend.com/ostiary | bash

Скрипт определяет ОС и архитектуру, скачивает последний стабильный релиз и устанавливает бинарник в ~/.local/bin/ostiary. Если этого пути нет в $PATH — автоматически добавляет строку в ~/.bashrc (или ~/.bash_profile / ~/.profile). Не требует sudo.

Ручная установка

Скачать нужный бинарник со страницы Releases:

Файл Платформа
ostiary-linux-x86_64 Linux x86-64
ostiary-linux-aarch64 Linux ARM64
ostiary-macos-aarch64 macOS Apple Silicon
ostiary-macos-x86_64 macOS Intel
mkdir -p ~/.local/bin
chmod +x ostiary-linux-x86_64
mv ostiary-linux-x86_64 ~/.local/bin/ostiary

# Добавить в PATH если нужно
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Удаление

curl -fsSL https://vainend.com/ostiary-remove | bash

Или вручную:

rm ~/.local/bin/ostiary
rm -rf ~/.config/ostiary

Переменная окружения

Переменная Описание
OSTIARY_API Базовый URL Gitea API (если используете собственный инстанс)

Пример с кастомным каталогом:

INSTALL_DIR=~/.local/bin bash install.sh

Навигация

Главное меню

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

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

Клавиша Контекст Действие
/ или k / j Лог / селектор / форма Прокрутка лога или навигация между элементами
PgUp / PgDn Лог Прокрутка на 10 строк
Space Форма Переключить чекбокс / выбрать радиокнопку
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":"Готово!"}'

Форма с чекбоксами и радиокнопками (form)

Отображает список полей с возможностью навигации и переключения. Возвращает строку с ID всех включённых/выбранных полей через пробел.

{
  "type": "form",
  "prompt": "Параметры поиска:",
  "fields": [
    {"id": "verbose",  "label": "Подробный вывод",   "field_type": "checkbox", "default": false},
    {"id": "php",      "label": "PHP файлы (*.php)",  "field_type": "radio",    "group": "ftype", "default": true},
    {"id": "js",       "label": "JS файлы (*.js)",    "field_type": "radio",    "group": "ftype", "default": false}
  ]
}
Поле Описание
id Идентификатор, возвращается если поле включено
label Отображаемый текст
field_type "checkbox" или "radio"
default Начальное состояние
group Группа радиокнопок — одновременно активна только одна в группе

Ответ: строка с ID активных полей через пробел: "verbose php"

Клавиши внутри формы: //j/k — навигация, Space — переключить, Enter/l — применить, Esc — отмена.

Важно: весь JSON должен быть на одной строке — протокол читает строки, не блоки.

result=$(ask '{"type":"form","prompt":"Параметры:","fields":[{"id":"verbose","label":"Подробный вывод","field_type":"checkbox","default":false},{"id":"php","label":"PHP файлы","field_type":"radio","group":"t","default":true},{"id":"js","label":"JS файлы","field_type":"radio","group":"t","default":false}]}')

is_on() { [[ " $result " == *" $1 "* ]]; }

is_on "verbose" && FLAGS+=("-v")
is_on "php"     && INCLUDE="--include=*.php"
is_on "js"      && INCLUDE="--include=*.js"

Запуск внешнего приложения (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.

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

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

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


Сборка релиза

Скрипт release.sh собирает бинарники для всех платформ сразу и кладёт их в dist/:

cd Ostiary
./release.sh
# → dist/ostiary-linux-x86_64
# → dist/ostiary-linux-aarch64
# → dist/ostiary-macos-aarch64

После сборки загрузите файлы из dist/ в новый релиз Gitea. Имена файлов должны точно совпадать с тем, что скачивает install.sh.

Вручную — 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) и не требует никаких зависимостей на целевом сервере.

Description
No description provided
Readme 843 KiB
Languages
Rust 94%
Shell 6%