diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e8337be --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 mirivlad + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f50bf4f..722e9e3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ starts the system `ssh` client with the right options. ## Install ```bash -git clone https://git.mirv.top/mirivlad/sshkeeper.git +git clone https://github.com/mirivlad/sshkeeper.git cd sshkeeper go build -o ~/.local/bin/sshkeeper . ``` @@ -89,6 +89,22 @@ storing the secret. Running `sshkeeper` without arguments opens the TUI. +## Screenshots + +### Main Window + +![sshkeeper main window](docs/screenshots/screen_1.png) + +### Edit Server + +![sshkeeper edit server form](docs/screenshots/screen_2.png) + +![sshkeeper group picker](docs/screenshots/screen_3.png) + +### Template Manager + +![sshkeeper template manager](docs/screenshots/screen_4.png) + | Key | Action | | --- | --- | | Enter | Connect to selected server | @@ -142,6 +158,13 @@ sshkeeper vault change-password password themselves because they need to decrypt the vault in the current process. +## Security + +`sshkeeper` stores SSH passwords and key passphrases in an encrypted local vault +and avoids passing secrets through command-line arguments. The project has not +had an independent security audit; review the implementation and threat model +before using it for high-risk environments. + ## Data Locations `sshkeeper` uses XDG-style app directories: @@ -181,4 +204,4 @@ sshkeeper/ ## License -MIT +MIT. See [LICENSE](LICENSE). diff --git a/docs/screenshots/.gitkeep b/docs/screenshots/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/screenshots/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/screenshots/screen_1.png b/docs/screenshots/screen_1.png new file mode 100644 index 0000000..5ceac17 Binary files /dev/null and b/docs/screenshots/screen_1.png differ diff --git a/docs/screenshots/screen_2.png b/docs/screenshots/screen_2.png new file mode 100644 index 0000000..f33ef51 Binary files /dev/null and b/docs/screenshots/screen_2.png differ diff --git a/docs/screenshots/screen_3.png b/docs/screenshots/screen_3.png new file mode 100644 index 0000000..8ccb00e Binary files /dev/null and b/docs/screenshots/screen_3.png differ diff --git a/docs/screenshots/screen_4.png b/docs/screenshots/screen_4.png new file mode 100644 index 0000000..a93dfdc Binary files /dev/null and b/docs/screenshots/screen_4.png differ diff --git a/docs/superpowers/plans/2026-05-28-server-list-scalability.md b/docs/superpowers/plans/2026-05-28-server-list-scalability.md deleted file mode 100644 index 8b64c3f..0000000 --- a/docs/superpowers/plans/2026-05-28-server-list-scalability.md +++ /dev/null @@ -1,295 +0,0 @@ -# Server List Scalability Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Keep the server list usable when the saved server count is larger than the terminal height. - -**Architecture:** The list screen should render a bounded table viewport instead of rendering every server row. Selection remains owned by the existing `bubbles/list.Model`, while `viewServerList` derives a visible row range around the selected item and keeps the selected-server detail panel and footer visible. - -**Tech Stack:** Go, Bubble Tea, Bubbles list, Lip Gloss, existing TUI tests in `internal/tui/app_test.go`. - ---- - -### Task 1: Add Regression Coverage For Long Server Lists - -**Files:** -- Modify: `internal/tui/app_test.go` - -- [x] **Step 1: Add a test for a constrained terminal height** - -Add a test that creates more servers than can fit on screen, sets a small terminal size, renders the list, and verifies the selected details and footer are still visible. - -```go -func TestServerListViewKeepsDetailsVisibleWithManyServers(t *testing.T) { - servers := make([]*model.Server, 45) - for i := range servers { - servers[i] = &model.Server{ - Alias: fmt.Sprintf("server-%02d", i+1), - DisplayName: fmt.Sprintf("Server %02d", i+1), - Host: fmt.Sprintf("host-%02d.example.org", i+1), - Port: 22, - User: "mirivlad", - AuthMethod: model.AuthKey, - LastTestStatus: model.TestUnknown, - } - } - - m := New(servers) - updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 18}) - model := updated.(*tuiModel) - - view := model.View() - if !strings.Contains(view, "Server 01") { - t.Fatalf("expected first selected server to be visible:\n%s", view) - } - if !strings.Contains(view, "Selected") { - t.Fatalf("expected selected server details to remain visible:\n%s", view) - } - if !strings.Contains(view, "Enter connect") { - t.Fatalf("expected footer to remain visible:\n%s", view) - } - if count := strings.Count(view, "server-"); count >= len(servers) { - t.Fatalf("expected bounded row rendering, rendered %d server aliases", count) - } -} -``` - -- [x] **Step 2: Run the focused test and confirm it fails** - -Run: - -```bash -env GOCACHE=/tmp/sshkeeper-go-cache go test ./internal/tui -run TestServerListViewKeepsDetailsVisibleWithManyServers -count=1 -``` - -Expected: FAIL because the current table renders every server and can push the detail panel/footer below the visible terminal area. - -### Task 2: Compute A Visible Row Window - -**Files:** -- Modify: `internal/tui/app.go` -- Modify: `internal/tui/app_test.go` - -- [x] **Step 1: Add focused tests for visible range calculation** - -Add tests for a helper that computes the inclusive start and exclusive end indexes for rendered rows. - -```go -func TestVisibleServerRangeKeepsSelectionInsideWindow(t *testing.T) { - tests := []struct { - name string - total int - selected int - available int - wantStart int - wantEnd int - }{ - {name: "first page", total: 40, selected: 0, available: 10, wantStart: 0, wantEnd: 10}, - {name: "middle page", total: 40, selected: 20, available: 10, wantStart: 11, wantEnd: 21}, - {name: "last page", total: 40, selected: 39, available: 10, wantStart: 30, wantEnd: 40}, - {name: "all fit", total: 5, selected: 3, available: 10, wantStart: 0, wantEnd: 5}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - start, end := visibleServerRange(tt.total, tt.selected, tt.available) - if start != tt.wantStart || end != tt.wantEnd { - t.Fatalf("visibleServerRange() = %d, %d; want %d, %d", start, end, tt.wantStart, tt.wantEnd) - } - }) - } -} -``` - -- [x] **Step 2: Implement `visibleServerRange`** - -Add a small helper near `selectedServer`. - -```go -func visibleServerRange(total, selected, available int) (int, int) { - if total <= 0 || available <= 0 { - return 0, 0 - } - if available >= total { - return 0, total - } - if selected < 0 { - selected = 0 - } - if selected >= total { - selected = total - 1 - } - - start := selected - available + 1 - if start < 0 { - start = 0 - } - end := start + available - if end > total { - end = total - start = end - available - } - return start, end -} -``` - -- [x] **Step 3: Run helper tests** - -Run: - -```bash -env GOCACHE=/tmp/sshkeeper-go-cache go test ./internal/tui -run TestVisibleServerRangeKeepsSelectionInsideWindow -count=1 -``` - -Expected: PASS. - -### Task 3: Render Only Rows That Fit - -**Files:** -- Modify: `internal/tui/app.go` -- Modify: `internal/tui/app_test.go` - -- [x] **Step 1: Reserve terminal space for fixed UI blocks** - -Add a helper that decides how many server rows may be rendered while keeping the selected details and footer visible. - -```go -func (m *tuiModel) visibleServerRows() int { - if m.height <= 0 { - return len(m.servers) - } - - const fixedRows = 16 - rows := m.height - fixedRows - if rows < 3 { - return 3 - } - return rows -} -``` - -- [x] **Step 2: Use the visible range in `viewServerList`** - -In `viewServerList`, replace the loop over all servers with a bounded loop: - -```go -selectedIndex := m.list.Index() -start, end := visibleServerRange(len(m.servers), selectedIndex, m.visibleServerRows()) -for _, server := range m.servers[start:end] { - // existing row rendering body stays unchanged -} -``` - -Then render a compact range hint when rows are hidden: - -```go -if len(m.servers) > end-start { - b.WriteString(helpStyle.Render(fmt.Sprintf(" Showing %d-%d of %d", start+1, end, len(m.servers)))) - b.WriteString("\n") -} -``` - -- [x] **Step 3: Run long-list regression test** - -Run: - -```bash -env GOCACHE=/tmp/sshkeeper-go-cache go test ./internal/tui -run TestServerListViewKeepsDetailsVisibleWithManyServers -count=1 -``` - -Expected: PASS. - -### Task 4: Verify Navigation Still Works - -**Files:** -- Modify: `internal/tui/app_test.go` - -- [x] **Step 1: Add a test for moving selection beyond the first window** - -Use the existing `m.list.Update` path by sending `tea.KeyDown` messages and confirm the rendered window follows the selected server. - -```go -func TestServerListViewScrollsWithSelection(t *testing.T) { - servers := make([]*model.Server, 45) - for i := range servers { - servers[i] = &model.Server{ - Alias: fmt.Sprintf("server-%02d", i+1), - DisplayName: fmt.Sprintf("Server %02d", i+1), - Host: fmt.Sprintf("host-%02d.example.org", i+1), - Port: 22, - User: "mirivlad", - AuthMethod: model.AuthKey, - } - } - - m := New(servers) - updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 18}) - model := updated.(*tuiModel) - for i := 0; i < 20; i++ { - updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown}) - model = updated.(*tuiModel) - } - - view := model.View() - if !strings.Contains(view, "Server 21") { - t.Fatalf("expected selected server to be visible after navigation:\n%s", view) - } - if !strings.Contains(view, "Showing") { - t.Fatalf("expected range hint for long server list:\n%s", view) - } -} -``` - -- [x] **Step 2: Run TUI tests** - -Run: - -```bash -env GOCACHE=/tmp/sshkeeper-go-cache go test ./internal/tui -count=1 -``` - -Expected: PASS. - -### Task 5: Final Verification And Build - -**Files:** -- No source edits expected. - -- [x] **Step 1: Run the full test suite** - -Run: - -```bash -env GOCACHE=/tmp/sshkeeper-go-cache go test ./... -``` - -Expected: all packages pass. - -- [x] **Step 2: Rebuild the project binary** - -Run: - -```bash -env GOCACHE=/tmp/sshkeeper-go-cache go build -o bin/sshkeeper . -``` - -Expected: exit code 0 and updated `bin/sshkeeper`. - -- [x] **Step 3: Commit the implementation** - -Run: - -```bash -git add internal/tui/app.go internal/tui/app_test.go bin/sshkeeper -git commit -m "fix: keep server list usable with many servers" -``` - -Expected: commit succeeds. - ---- - -## Self-Review - -- Spec coverage: the plan covers the known failure mode for 40+ servers, keeps selected details visible, keeps the footer visible, and preserves existing `bubbles/list` navigation. -- Placeholder scan: no `TBD`, `TODO`, or open-ended implementation placeholders remain. -- Type consistency: helper names and files match the current TUI code shape. diff --git a/docs/superpowers/plans/2026-05-28-tui-tags-global-templates.md b/docs/superpowers/plans/2026-05-28-tui-tags-global-templates.md deleted file mode 100644 index 13c6460..0000000 --- a/docs/superpowers/plans/2026-05-28-tui-tags-global-templates.md +++ /dev/null @@ -1,51 +0,0 @@ -# TUI Tags And Global Templates Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add full TUI support for tags and global command templates, including multi-server template execution. - -**Architecture:** Move command templates to global entities while preserving legacy server-scoped rows as importable data. Add server `startup_command`, richer tag CRUD helpers, and TUI screens for tag/template management. Template execution uses existing OpenSSH command construction, with foreground execution returning control to the terminal and background execution collecting per-server results in a TUI results screen. - -**Tech Stack:** Go, Bubble Tea, Bubbles list/textinput, SQLite, existing Cobra CLI and TUI package. - ---- - -### Task 1: Data Model And CLI - -- [x] Add `StartupCommand` to `model.Server`. -- [x] Add DB schema migration helpers for `servers.startup_command` and global `global_command_templates`. -- [x] Add DB CRUD for global templates and tag management. -- [x] Update CLI `template` commands to manage global templates. -- [x] Update `run-template` to run a global template on a server. -- [x] Add focused DB and CLI tests. - -### Task 2: TUI Tags - -- [x] Add tags to server form as comma-separated input. -- [x] Persist tags on add/edit. -- [x] Show tags in selected-server details. -- [x] Add a tag management screen with list, add, rename, delete, assign/remove selected servers. -- [x] Add tests for tag rendering and callbacks. - -### Task 3: TUI Templates And Selection - -- [x] Add multi-selection state toggled by Insert. -- [x] Show selected markers and selected count in list footer. -- [x] Add global template picker opened by Shift+Enter. -- [x] Add template manager screen with list/add/edit/delete. -- [x] Add startup command field to server form. -- [x] Add tests for selection and template screen state. - -### Task 4: Template Execution - -- [x] Add foreground template execution result path that exits TUI and lets caller run SSH. -- [x] Add background execution callback and results screen. -- [x] Run background template on selected servers, collecting stdout/stderr/status. -- [x] Add tests for run mode selection and result rendering. - -### Task 5: Verification - -- [x] Run `env GOCACHE=/tmp/sshkeeper-go-cache go test ./...`. -- [x] Run `env GOCACHE=/tmp/sshkeeper-go-cache go build -o bin/sshkeeper .`. -- [ ] Smoke-test CLI template CRUD on temporary XDG paths. -- [ ] Commit final implementation. diff --git a/sshkeeper_tz.md b/sshkeeper_tz.md deleted file mode 100644 index d5c52e1..0000000 --- a/sshkeeper_tz.md +++ /dev/null @@ -1,1352 +0,0 @@ -# ТЗ для ИИ-кодера: sshkeeper — консольный менеджер SSH-подключений для Linux - -## 1. Назначение проекта - -Нужно разработать консольное приложение для Linux, которое работает как менеджер SSH-серверов и учётных данных. - -Приложение **не должно реализовывать собственный SSH-клиент с нуля**. Оно должно использовать системный OpenSSH (`/usr/bin/ssh`) как реальный транспорт подключения, а само приложение должно быть удобным слоем управления над: - -- списком серверов; -- пользователями; -- портами; -- SSH-ключами; -- SSH-паролями; -- группами; -- тегами; -- заметками; -- bastion/proxyjump; -- локальными настройками подключения; -- encrypted vault для секретов. - -Рабочее название приложения: **sshkeeper**. - -Главная идея: - -> `sshkeeper` не заменяет OpenSSH. -> `sshkeeper` управляет профилями подключений, секретами и удобным запуском SSH-сессий. - ---- - -## 2. Целевая платформа - -Основная целевая платформа: - -- Linux x86_64; -- Arch Linux; -- Debian/Ubuntu; -- Fedora-compatible дистрибутивы. - -На первом этапе Windows и macOS не обязательны. - ---- - -## 3. Основной стек - -Использовать: - -- Go; -- Cobra для CLI; -- Bubble Tea для TUI; -- Bubbles для TUI-компонентов; -- SQLite для локальной базы профилей; -- `modernc.org/sqlite` как SQLite-драйвер без CGO; -- `golang.org/x/crypto/argon2` для Argon2id KDF; -- `golang.org/x/crypto/chacha20poly1305` для XChaCha20-Poly1305; -- `github.com/creack/pty` для запуска OpenSSH через PTY; -- системный `/usr/bin/ssh`. - -Не использовать: - -- Electron; -- GUI; -- внешние password managers как обязательную зависимость; -- GNOME Keyring/KWallet/libsecret как обязательную зависимость; -- хранение паролей в plaintext; -- передачу пароля через аргументы командной строки; -- передачу пароля через environment variables. - ---- - -## 4. Основные пользовательские сценарии - -### 4.1. Добавить сервер - -Пользователь запускает: - -```bash -sshkeeper add -``` - -Приложение открывает интерактивную форму в терминале. - -Поля: - -- Alias; -- Display name; -- Host; -- Port; -- User; -- Auth method: - - `password`; - - `key`; - - `key+passphrase`; - - `agent`; -- Identity file, если используется ключ; -- Password, если используется пароль; -- Key passphrase, если используется ключ с passphrase; -- Group; -- Tags; -- Notes; -- ProxyJump, опционально; -- Local forwards, опционально; -- Remote forwards, опционально. - -В конце формы должны быть две основные кнопки: - -```text -[Test] [Save] -``` - ---- - -### 4.2. Кнопка Test - -Кнопка `Test` проверяет текущие введённые данные. - -Важно: - -- `Test` **не сохраняет** профиль. -- `Test` **не сохраняет** новые секреты в постоянный vault. -- `Test` использует введённые данные только из текущей формы. -- Если подключение успешно — показать `Connection OK`. -- Если подключение неуспешно — показать ошибку. -- После теста пользователь остаётся в форме. -- Все введённые значения должны сохраниться в форме. -- Пользователь сам решает, нажимать ли потом `Save`. - -Пример успешного теста: - -```text -Connection OK. -``` - -Пример ошибки пароля: - -```text -Connection failed: - -Permission denied, please try again. -``` - -Пример ошибки ключа: - -```text -Connection failed: - -Identity file ~/.ssh/prod_ed25519 not found. -``` - -Пример сетевой ошибки: - -```text -Connection failed: - -connect to host 10.0.0.11 port 22: No route to host. -``` - ---- - -### 4.3. Кнопка Save - -Кнопка `Save` сохраняет профиль как есть. - -Важно: - -- `Save` **не обязан** выполнять тест подключения. -- `Save` **не должен** блокировать сохранение, если сервер недоступен. -- `Save` **не должен** показывать раздражающие предупреждения про опасность SSH-паролей. -- Если пользователь выбрал `password`, пароль сохраняется в собственный encrypted vault. -- Если пользователь выбрал `key+passphrase`, passphrase сохраняется в encrypted vault. -- Обычные данные профиля сохраняются в SQLite. -- Секреты не должны попадать в SQLite в открытом виде. - -После сохранения показать: - -```text -Saved. -``` - ---- - -## 5. Пароли как штатный режим - -Парольная авторизация должна быть полноценным штатным режимом. - -Не нужно делать предупреждения вида: - -```text -WARNING! Password auth is insecure! -``` - -Такие предупреждения не нужны. - -Логика: - -- `auth=password` — нормальный режим. -- Пароль хранится в собственном encrypted vault. -- Пароль не хранится в YAML/TOML/SQLite открытым текстом. -- Пароль не передаётся в argv. -- Пароль не передаётся через env. -- Пароль не пишется в логи. -- Пароль не показывается в интерфейсе после ввода. -- В списке серверов можно показывать тип авторизации: `password`, `key`, `agent`, `key+passphrase`. - ---- - -## 6. Структура хранения данных - -Использовать XDG-совместимые пути. - -База данных: - -```text -~/.local/share/sshkeeper/sshkeeper.db -``` - -Vault: - -```text -~/.local/share/sshkeeper/vault.bin -``` - -Конфиг приложения: - -```text -~/.config/sshkeeper/config.toml -``` - -Сгенерированный OpenSSH config: - -```text -~/.ssh/config.d/sshkeeper.conf -``` - -Пользовательский `~/.ssh/config` может содержать: - -```sshconfig -Include ~/.ssh/config.d/*.conf -``` - -Если такой строки нет, приложение должно уметь добавить её отдельной командой: - -```bash -sshkeeper ssh-config install-include -``` - -Делать это автоматически без явного действия пользователя не нужно. - ---- - -## 7. SQLite-схема MVP - -Минимальные таблицы: - ---- - -### 7.1. `servers` - -Поля: - -- `id`; -- `alias`; -- `display_name`; -- `host`; -- `port`; -- `user`; -- `auth_method`; -- `identity_file`; -- `proxy_jump`; -- `group_name`; -- `notes`; -- `created_at`; -- `updated_at`; -- `last_connected_at`; -- `last_test_at`; -- `last_test_status`; -- `last_test_error`. - -`auth_method` значения: - -- `password`; -- `key`; -- `key_passphrase`; -- `agent`. - -`last_test_status` значения: - -- `unknown`; -- `ok`; -- `failed`. - ---- - -### 7.2. `tags` - -Поля: - -- `id`; -- `name`. - ---- - -### 7.3. `server_tags` - -Поля: - -- `server_id`; -- `tag_id`. - ---- - -### 7.4. `forwards` - -Поля: - -- `id`; -- `server_id`; -- `type`; -- `local_addr`; -- `local_port`; -- `remote_addr`; -- `remote_port`. - -`type` значения: - -- `local`; -- `remote`; -- `dynamic`. - ---- - -### 7.5. `command_templates` - -Поля: - -- `id`; -- `server_id`; -- `name`; -- `command`. - -Примеры command templates: - -- `logs` → `journalctl -xe`; -- `docker` → `docker ps`; -- `nginx-test` → `nginx -t`. - ---- - -## 8. Vault - -Нужен собственный encrypted vault. - ---- - -### 8.1. Master password - -При первом запуске: - -```bash -sshkeeper init -``` - -Приложение спрашивает: - -```text -Create master password: -Repeat master password: -``` - -Master password не хранится. - -Из master password через Argon2id выводится master key. - ---- - -### 8.2. KDF - -Использовать Argon2id. - -Начальные параметры: - -```text -memory: 64 MiB -iterations: 3 -parallelism: 1 -salt: random 16 или 32 bytes -key length: 32 bytes -``` - -Параметры KDF должны храниться в metadata vault, чтобы в будущем можно было менять настройки. - ---- - -### 8.3. Шифрование - -Использовать XChaCha20-Poly1305. - -Каждая запись vault должна иметь отдельный случайный nonce. - -Формат vault можно сделать JSON или бинарный. Для MVP допустим JSON с base64-полями. - -Пример структуры: - -```json -{ - "version": 1, - "kdf": { - "name": "argon2id", - "memory_kib": 65536, - "iterations": 3, - "parallelism": 1, - "salt": "base64..." - }, - "records": [ - { - "id": "server:old-router:ssh-password", - "type": "ssh_password", - "nonce": "base64...", - "ciphertext": "base64..." - } - ] -} -``` - ---- - -### 8.4. Типы секретов - -Поддержать типы: - -- `ssh_password`; -- `key_passphrase`; -- `sudo_password`; -- `custom_secret`. - -Для MVP обязательны: - -- `ssh_password`; -- `key_passphrase`. - ---- - -### 8.5. Secret references - -В SQLite хранить только ссылки на секреты. - -Примеры: - -```text -server:old-router:ssh-password -server:prod-web-1:key-passphrase -server:prod-web-1:sudo-password -``` - -Секреты должны лежать только в vault. - ---- - -## 9. Подключение к серверу - -Команда: - -```bash -sshkeeper connect -``` - -Короткий алиас: - -```bash -sshkeeper c -``` - -Логика: - -1. Найти профиль сервера в SQLite. -2. Если нужен vault — запросить master password, если vault ещё не разблокирован. -3. Сформировать команду `ssh`. -4. Запустить системный `/usr/bin/ssh`. -5. Если используется пароль — запустить SSH через PTY-wrapper. -6. Дождаться password prompt. -7. Отправить пароль в PTY. -8. Передать управление пользователю. -9. После завершения сессии обновить `last_connected_at`. - ---- - -## 10. PTY-wrapper для паролей - -Для `auth=password` нельзя передавать пароль через аргументы командной строки. - -Нужен PTY-wrapper. - -Примерная логика: - -```text -start ssh through PTY -read PTY output -detect password prompt -write password + "\n" -after login, bridge stdin/stdout/stderr between user terminal and PTY -handle terminal resize -restore terminal state on exit -``` - -Prompt detection должен учитывать варианты: - -```text -password: -Password: -user@host's password: -Enter password: -``` - -Для MVP достаточно английских вариантов. - -Позже можно добавить расширяемые regex-шаблоны в config. - ---- - -## 11. Проверка подключения - -Команда: - -```bash -sshkeeper test -``` - -В форме сервера кнопка: - -```text -[Test] -``` - -Проверка должна выполнять короткую безопасную команду: - -```bash -echo SSHKEEPER_OK -``` - -Ожидаемый вывод: - -```text -SSHKEEPER_OK -``` - -Если команда выполнилась и вывод получен — тест успешен. - -Для нестандартных систем можно добавить fallback: - -```bash -exit -``` - -Но для MVP достаточно `echo SSHKEEPER_OK`. - -Важно: - -- Тест из формы не сохраняет профиль. -- Тест существующего профиля обновляет: - - `last_test_at`; - - `last_test_status`; - - `last_test_error`. -- Тест должен иметь timeout. -- Начальный timeout: 10 секунд. -- Timeout должен настраиваться в config. - ---- - -## 12. CLI-команды MVP - -Обязательные команды: - -```bash -sshkeeper init -sshkeeper add -sshkeeper list -sshkeeper show -sshkeeper edit -sshkeeper delete -sshkeeper connect -sshkeeper c -sshkeeper test -sshkeeper search -sshkeeper vault lock -sshkeeper vault unlock -sshkeeper vault status -sshkeeper vault change-password -sshkeeper config path -sshkeeper ssh-config generate -sshkeeper ssh-config install-include -``` - -Дополнительные команды, если останется время: - -```bash -sshkeeper import ~/.ssh/config -sshkeeper export -sshkeeper run -sshkeeper run-template