Files
ostiary/README.md
2026-05-22 15:23:30 +07:00

597 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Ostiary — удалённый запуск скриптов
Консольный клиент с текстовым интерфейсом (TUI) на Rust. Загружает иерархическое меню с сервера и позволяет запускать bash-скрипты, скачивать файлы и выполнять HTTP-запросы прямо из терминала. Все диалоги (выбор из списка, подтверждение, ввод текста) реализованы внутри интерфейса — никаких сырых `read -p` в терминале.
---
## Установка
### Быстрая установка (Linux / macOS)
```bash
curl -fsSL https://vainend.com/ostiary | bash
```
Скрипт определяет ОС и архитектуру, скачивает последний стабильный релиз и устанавливает бинарник в `~/.local/bin/ostiary`. Если этого пути нет в `$PATH` — автоматически добавляет строку в `~/.bashrc` (или `~/.bash_profile` / `~/.profile`). Не требует `sudo`.
### Ручная установка
Скачать нужный бинарник со страницы [Releases](https://git.vainend.com/admin/ostiary/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 |
```bash
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
```
### Удаление
```bash
curl -fsSL https://vainend.com/ostiary-remove | bash
```
Или вручную:
```bash
rm ~/.local/bin/ostiary
rm -rf ~/.config/ostiary
```
### Переменная окружения
| Переменная | Описание |
|---|---|
| `OSTIARY_API` | Базовый URL Gitea API (если используете собственный инстанс) |
Пример с кастомным каталогом:
```bash
INSTALL_DIR=~/.local/bin bash install.sh
```
---
## Навигация
### Главное меню
| Клавиша | Действие |
|---|---|
| `↑` / `↓` или `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`. Его можно редактировать вручную:
```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`.
### Корневая структура
```json
{
"version": "1.0",
"menu": [ ...пункты... ]
}
```
### Категория
Содержит вложенные пункты. Отображается с иконкой `📁`.
```json
{
"id": "bitrix",
"title": "1С Битрикс",
"description": "Инструменты администрирования",
"children": [
...пункты меню...
]
}
```
### Действие
Отображается с иконкой `▶`.
```json
{
"id": "db_manage",
"title": "Управление БД",
"description": "Дамп, импорт, подключение к базе",
"action": {
"type": "bash",
"script": "#!/bin/bash\necho hello",
"interaction": "structured",
"confirm": false
}
}
```
#### `confirm` и `confirm_message`
Если `confirm: true`, перед запуском появится диалог подтверждения.
`confirm_message` переопределяет текст вопроса.
```json
{
"id": "drop_db",
"title": "Удалить базу данных",
"action": {
"type": "bash",
"script": "...",
"interaction": "structured",
"confirm": true,
"confirm_message": "База данных будет удалена без возможности восстановления. Продолжить?"
}
}
```
---
## Типы действий
### `bash` — запуск скрипта
```json
{
"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` — скачивание файла
```json
{
"type": "download",
"url": "https://example.com/file.tar.gz",
"filename": "backup.tar.gz",
"target_dir": "/var/backups",
"confirm": false
}
```
### `download_and_run` — скачать и выполнить
```json
{
"type": "download_and_run",
"url": "https://example.com/installer.sh",
"run_args": ["--silent", "--prefix=/opt"],
"keep_file": false,
"confirm": true
}
```
### `http_request` — HTTP-запрос
```json
{
"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**.
### Базовые хелперы
```bash
#!/bin/bash
# Отправить команду и получить ответ от пользователя
ask() {
printf 'CMD:%s\n' "$1" >&2
read -r _response
printf '%s' "$_response"
}
# Отправить уведомление (не ждёт ответа, не блокирует скрипт)
notify() {
printf 'CMD:%s\n' "$1" >&2
}
```
---
### Ввод текста (`input`)
```json
{"type":"input","prompt":"Введите имя пользователя:","default":"admin","secret":false}
```
| Поле | Описание |
|---|---|
| `prompt` | Заголовок поля ввода |
| `default` | Значение по умолчанию (опционально) |
| `secret` | Если `true` — символы заменяются на `•` |
```bash
username=$(ask '{"type":"input","prompt":"Имя пользователя:","default":"admin"}')
password=$(ask '{"type":"input","prompt":"Пароль:","secret":true}')
```
---
### Выбор из списка (`menu`)
Список с навигацией `↑`/`↓`/`j`/`k`, выбор по `Enter`/`l`. Возвращает `id` выбранного пункта.
```json
{
"type": "menu",
"prompt": "Выберите действие:",
"options": [
{"id": "dump", "label": "Создать дамп базы"},
{"id": "import", "label": "Импортировать дамп"},
{"id": "connect","label": "Подключиться к БД"}
]
}
```
```bash
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):
```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.
```json
{"type":"confirm","prompt":"Данные будут перезаписаны. Продолжить?"}
```
Ответ: `"y"` или `"n"`.
```bash
answer=$(ask '{"type":"confirm","prompt":"Удалить временные файлы?"}')
if [[ "$answer" =~ ^[Yy]$ ]]; then
rm -rf /tmp/myapp_*
fi
```
---
### Сообщение (`message`)
Показывает сообщение с цветом. Скрипт продолжается после нажатия `Enter`.
```json
{"type":"message","level":"info","text":"Операция завершена успешно!"}
```
| `level` | Цвет |
|---|---|
| `"info"` | Зелёный |
| `"warn"` | Жёлтый |
| `"error"` | Красный |
```bash
if ! command -v mysqldump &> /dev/null; then
ask '{"type":"message","level":"error","text":"mysqldump не найден."}' > /dev/null
exit 1
fi
```
> `> /dev/null` — сбрасывает пустой ответ клиента чтобы он не попал в переменную.
---
### Прогресс (`progress`)
Обновляет прогресс-бар не блокируя скрипт (используйте `notify`, не `ask`).
```json
{"type":"progress","percent":45,"message":"Копирование файлов..."}
```
```bash
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 восстанавливается. Возвращает код завершения.
```json
{"type":"exec","shell":"mysql -h localhost -u root mydb"}
```
```bash
# Интерактивная сессия 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
```
---
## Полный пример скрипта
```bash
#!/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"`:
```json
{
"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`:
```json
{
"id": "my_action",
"title": "Название в меню",
"description": "Описание для нижней панели",
"action": {
"type": "bash",
"script_file": "category_action.sh",
"interaction": "structured",
"confirm": false
}
}
```
Соглашение по именованию файлов: `{категория}_{подкатегория}_{действие}.sh`
---
## Сборка релиза
Скрипт `release.sh` собирает бинарники для всех платформ сразу и кладёт их в `dist/`:
```bash
cd Ostiary
./release.sh
# → dist/ostiary-linux-x86_64
# → dist/ostiary-linux-aarch64
# → dist/ostiary-macos-aarch64
```
После сборки загрузите файлы из `dist/` в [новый релиз Gitea](https://git.vainend.com/admin/ostiary/releases/new). Имена файлов должны точно совпадать с тем, что скачивает `install.sh`.
**Вручную — Linux:**
```bash
cargo build --release
# → target/release/ostiary
```
**Вручную — кросс-компиляция macOS → Linux x86_64 (musl, статический):**
```bash
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:**
```bash
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) и не требует никаких зависимостей на целевом сервере.