Prepare public release docs

This commit is contained in:
mirivlad 2026-05-29 10:52:03 +08:00
parent c2b0e57f3a
commit d66e103c73
10 changed files with 47 additions and 1700 deletions

21
LICENSE Normal file
View File

@ -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.

View File

@ -19,7 +19,7 @@ starts the system `ssh` client with the right options.
## Install ## Install
```bash ```bash
git clone https://git.mirv.top/mirivlad/sshkeeper.git git clone https://github.com/mirivlad/sshkeeper.git
cd sshkeeper cd sshkeeper
go build -o ~/.local/bin/sshkeeper . go build -o ~/.local/bin/sshkeeper .
``` ```
@ -89,6 +89,22 @@ storing the secret.
Running `sshkeeper` without arguments opens the TUI. 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 | | Key | Action |
| --- | --- | | --- | --- |
| Enter | Connect to selected server | | 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 password themselves because they need to decrypt the vault in the current
process. 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 ## Data Locations
`sshkeeper` uses XDG-style app directories: `sshkeeper` uses XDG-style app directories:
@ -181,4 +204,4 @@ sshkeeper/
## License ## License
MIT MIT. See [LICENSE](LICENSE).

View File

@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@ -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.

View File

@ -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.

File diff suppressed because it is too large Load Diff