Skip to content

Commit 4f9af29

Browse files
authored
Merge pull request #19 from ncode/ncode/v2migration
Migrate Bubbletea and Bubbles to v2
2 parents a3b1b9f + 5c61dbd commit 4f9af29

11 files changed

Lines changed: 498 additions & 108 deletions

File tree

AGENTS.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# AGENTS.md
2+
3+
Guidance for agentic coding assistants working in `github.com/ncode/pretty`.
4+
5+
## Quick Start for Agents
6+
7+
Run these first from repo root:
8+
9+
1. `go env -w GOTOOLCHAIN=go1.25.0+auto`
10+
2. `gofmt -w .`
11+
3. `go test -v ./...`
12+
13+
When narrowing scope:
14+
15+
- Single package: `go test -v ./internal/shell`
16+
- Single test: `go test -v ./internal/shell -run '^TestParseCommandAsync$'`
17+
- Subtest: `go test -v ./cmd -run '^TestParseHostSpec$/host_with_port$'`
18+
19+
## Project Snapshot
20+
21+
- Language: Go (`go 1.25` in `go.mod`)
22+
- Binary entrypoint: `main.go`
23+
- CLI layer: `cmd/`
24+
- Core packages: `internal/shell`, `internal/sshConn`, `internal/jobs`
25+
- Main user docs: `README.md`
26+
27+
## Toolchain and Environment
28+
29+
- Use Go 1.25.
30+
- Match CI behavior when possible:
31+
- `go env -w GOTOOLCHAIN=go1.25.0+auto`
32+
- A `Makefile` is provided. Run `make` to build, `make test` for tests, `make demo` for the full testbed workflow.
33+
- No dedicated linter config (`.golangci.yml`) is present.
34+
35+
## Build / Lint / Test Commands
36+
37+
Run from repository root.
38+
39+
### Build
40+
41+
- Build all packages: `go build -v ./...`
42+
- Build binary only: `go build -o pretty .`
43+
44+
### Test (standard)
45+
46+
- Full suite: `go test -v ./...`
47+
- Full suite with race detector: `go test -race ./...`
48+
- CI-like coverage command:
49+
- `go test -coverpkg=./... ./... -race -coverprofile=coverage.out -covermode=atomic`
50+
51+
### Test (single package / file / test)
52+
53+
- Single package:
54+
- `go test -v ./internal/shell`
55+
- Single test function:
56+
- `go test -v ./internal/shell -run '^TestParseCommandAsync$'`
57+
- Single test in another package:
58+
- `go test -v ./cmd -run '^TestParseHostSpec$'`
59+
- Subtest target (table-driven tests):
60+
- `go test -v ./cmd -run '^TestParseHostSpec$/host_with_port$'`
61+
- Repeat a flaky test deterministically:
62+
- `go test -v ./internal/sshConn -run '^TestResolveHostPatternMatchWildcard$' -count=1`
63+
64+
### Lint / Static checks
65+
66+
Because no project-specific linter config exists, use baseline Go checks:
67+
68+
- Format check and rewrite: `gofmt -w .`
69+
- Vet all packages: `go vet ./...`
70+
- Optional stronger check (if installed): `staticcheck ./...`
71+
72+
If a change touches many files, run at least:
73+
74+
1. `gofmt -w .`
75+
2. `go test -v ./...`
76+
77+
If concurrency/networking code changes, also run:
78+
79+
3. `go test -race ./...`
80+
81+
## Workflow Expectations for Agents
82+
83+
- Prefer small, focused diffs.
84+
- Do not introduce new dependencies unless clearly necessary.
85+
- Preserve existing CLI behavior and command semantics.
86+
- Keep public behavior aligned with `README.md`.
87+
- Add or update tests when behavior changes.
88+
89+
## Code Style Guidelines
90+
91+
These reflect patterns already used in this repository.
92+
93+
### Formatting and file layout
94+
95+
- Always use `gofmt` formatting.
96+
- Keep package names short and lowercase; follow existing names (including `sshConn`).
97+
- Keep functions cohesive and avoid deep nesting.
98+
- Prefer early returns to reduce indentation.
99+
100+
### Imports
101+
102+
- Group imports in three blocks when needed:
103+
1. Go standard library
104+
2. Third-party libraries
105+
3. Internal project imports (`github.com/ncode/pretty/...`)
106+
- Keep imports sorted as `gofmt` outputs.
107+
- Use import aliases only when needed for clarity or conflicts (for example `tea`, `homedir`).
108+
109+
### Naming
110+
111+
- Exported identifiers: PascalCase.
112+
- Unexported identifiers: camelCase.
113+
- Constants:
114+
- exported: PascalCase if part of API
115+
- internal/private: camelCase (`defaultPrompt`, `maxOutputLines`)
116+
- Test names: `TestXxx` with descriptive suffixes.
117+
118+
### Types and data structures
119+
120+
- Prefer concrete structs for domain state (`Manager`, `HostSpec`, `ResolvedHost`).
121+
- Use pointers for shared/mutable state.
122+
- Initialize slices/maps with capacity when size is known.
123+
- Keep zero-value behavior sensible.
124+
125+
### Error handling
126+
127+
- In library/internal packages:
128+
- return errors to caller
129+
- wrap with context using `%w` when rethrowing (`fmt.Errorf("...: %w", err)`).
130+
- In CLI command execution paths (`cmd/`), current pattern is:
131+
- print user-facing error
132+
- exit non-zero (`os.Exit(1)`).
133+
- Avoid panics for expected runtime errors.
134+
135+
### Concurrency and synchronization
136+
137+
- Guard shared mutable state with `sync.Mutex`/atomics as currently done.
138+
- Keep lock scope tight; avoid blocking operations while holding locks.
139+
- For goroutines in tests/helpers, use `t.Cleanup` to shut down resources.
140+
141+
### Testing conventions
142+
143+
- Prefer table-driven tests for parser/validation behavior.
144+
- Use `t.Run` with stable, descriptive case names.
145+
- Use `t.Helper()` in test helpers.
146+
- Use `t.Fatalf` for fatal assertions; include expected vs got details.
147+
- Prefer deterministic tests; avoid sleeps unless absolutely required.
148+
149+
### Comments and docs
150+
151+
- Add comments for non-obvious logic, invariants, or protocol details.
152+
- Do not add redundant comments that restate code.
153+
- Keep exported APIs understandable from names and function signatures.
154+
155+
## Validation Checklist Before Finishing
156+
157+
- Code is `gofmt`-formatted.
158+
- Relevant package tests pass.
159+
- Full `go test -v ./...` passes for non-trivial changes.
160+
- `go test -race ./...` run when touching concurrent code.
161+
- `README.md` updated if CLI behavior or config format changed.
162+
163+
## Repository-Specific Rules Files
164+
165+
Checked paths:
166+
167+
- `.cursor/rules/`
168+
- `.cursorrules`
169+
- `.github/copilot-instructions.md`
170+
171+
Current status in this repo: none of the above files exist.
172+
173+
If any are added later, treat their guidance as authoritative and merge it into this document.

CLAUDE.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
`pretty` is a parallel remote execution TTY (parallel SSH shell) written in Go. It opens persistent SSH shell sessions to multiple hosts, fans commands out to all of them, and displays color-prefixed output in an interactive terminal UI built on Bubbletea v2.
8+
9+
See `AGENTS.md` for build/test/lint commands and code style guidelines.
10+
11+
## Common Development Commands
12+
13+
```bash
14+
go build -v ./... # build all packages
15+
go build -o pretty . # build binary
16+
go test -v ./... # full test suite
17+
go test -v ./internal/shell # single package
18+
go test -v ./internal/shell -run '^TestParseCommandAsync$' # single test
19+
go test -race ./... # race detection (run when touching concurrent code)
20+
go test -coverpkg=./... ./... -race -coverprofile=coverage.out -covermode=atomic # CI coverage
21+
gofmt -w . && go vet ./... # format + lint
22+
```
23+
24+
Go 1.26 required (see `go.mod`). CI runs on Ubuntu with `go-version: '1.26'`.
25+
26+
## Architecture
27+
28+
### Data Flow
29+
30+
```
31+
CLI (cmd/root.go)
32+
-> parse host specs, resolve SSH config, build HostList
33+
-> shell.Spawn(hostList)
34+
-> start Broker goroutine (fan-out)
35+
-> start Bubbletea TUI program
36+
37+
User input -> model.Update -> CommandRequest -> broker channel
38+
-> per-host worker goroutine -> SSH stdin
39+
40+
SSH stdout -> ProxyWriter -> OutputEvent channel
41+
-> model.Update (outputMsg) -> viewport
42+
```
43+
44+
### Key Packages
45+
46+
- **`cmd/`** -- Cobra CLI. Host spec parsing (`hosts.go`), SSH config resolution, group/file loading, color assignment. Entry point calls `shell.Spawn`.
47+
- **`internal/shell/`** -- Bubbletea v2 TUI. The `model` struct owns the text input, viewport, output buffer, command history, and job manager. Commands are parsed in `command.go` (`:async`, `:status`, `:list`, `:scroll`, `:bye`, `exit`, or plain text = run).
48+
- **`internal/sshConn/`** -- SSH connection lifecycle. `Broker` fans a single `CommandRequest` channel out to per-host `worker` goroutines. Each worker holds a persistent SSH shell session. `RunCommand` opens a fresh session for async jobs. `config.go` resolves SSH config (Host/Match patterns, ProxyJump, IdentityFile) via `ncode/ssh_config`.
49+
- **`internal/jobs/`** -- Job tracking. `Manager` maintains the latest normal job and last 2 async jobs. Thread-safe via mutex + snapshot cloning for reads. `sentinel.go` injects/extracts `__PRETTY_EXIT__<jobID>:<exitCode>` markers to capture per-host exit codes from shell output.
50+
51+
### Concurrency Model
52+
53+
- **Broker pattern**: One goroutine reads from the shared `CommandRequest` channel and forwards to each connected host's private channel.
54+
- **Per-host workers**: Each host has a dedicated goroutine holding an SSH shell session, reading from its private channel.
55+
- **Async jobs**: Each `:async` command spawns N goroutines (one per connected host), each opening a fresh SSH session via `RunCommand`.
56+
- **Connection state**: `Host.IsConnected` and `Host.IsWaiting` are `int32` atomics.
57+
- **Job snapshots**: `Manager` uses copy-on-read (`cloneJob`) so the Bubbletea model never holds a lock while rendering.
58+
59+
### Sentinel Protocol
60+
61+
Commands sent to remote shells are wrapped with a trailing `printf '__PRETTY_EXIT__<jobID>:%d\n' $?`. The `ExtractSentinel` function in `internal/jobs/sentinel.go` parses these markers from output lines to determine per-host exit codes and mark jobs complete.
62+
63+
### TUI (Bubbletea v2)
64+
65+
Uses `charm.land/bubbletea/v2` and `charm.land/bubbles/v2` (not the old `github.com/charmbracelet` import paths). The view is an alt-screen with a viewport (output) + text input (prompt). Scroll mode (`:scroll`) disables auto-follow and lets the user scroll the viewport; `esc` returns to the prompt.
66+
67+
## Testing
68+
69+
- `internal/sshConn` uses function variables (`connectionFunc`, `sessionFunc`, `workerRunner`) to stub SSH calls in tests.
70+
- `cmd/root.go` uses `loadSSHConfigFunc`, `resolveHostFunc`, and `spawnShellFunc` for the same purpose.
71+
- Table-driven tests throughout; use `t.Run` with descriptive subtest names.
72+
73+
## Local SSHD Testbed
74+
75+
For integration testing against real SSH servers:
76+
```bash
77+
make demo # setup + docker + build + run (full workflow)
78+
make clean # tear down containers and remove .pretty-test/
79+
```
80+
81+
Or step by step:
82+
```bash
83+
export PRETTY_AUTHORIZED_KEY="$(ssh-add -L | grep 'my-key' | head -n1)" # optional
84+
make testbed # setup + docker + host key scan + build
85+
make run # launch pretty against testbed
86+
make testbed-down # stop containers
87+
```

Makefile

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
BINARY := pretty
2+
COMPOSE := docker compose -f docker-compose.sshd.yml
3+
SETUP := ./scripts/ssh-testbed-setup.sh
4+
CONFIG := .pretty-test/pretty.yaml
5+
GO_FILES := $(shell find . -name '*.go' -not -path './.worktrees/*')
6+
7+
.PHONY: all build test test-race coverage vet \
8+
testbed testbed-setup testbed-up testbed-scan testbed-down \
9+
run demo clean
10+
11+
all: build
12+
13+
# --- Build ---
14+
15+
build: $(BINARY)
16+
17+
$(BINARY): $(GO_FILES) go.mod go.sum
18+
go build -o $(BINARY) .
19+
20+
# --- Test / Lint ---
21+
22+
test:
23+
go test -v ./...
24+
25+
test-race:
26+
go test -race ./...
27+
28+
coverage:
29+
go test -coverpkg=./... ./... -race -coverprofile=coverage.out -covermode=atomic
30+
31+
vet:
32+
@bad=$$(gofmt -l . | grep -v '^\.worktrees/'); test -z "$$bad" || { echo "$$bad"; echo "gofmt needed"; exit 1; }
33+
go vet ./...
34+
35+
# --- Testbed ---
36+
37+
testbed-setup: .pretty-test/sshd.env
38+
39+
.pretty-test/sshd.env:
40+
PRETTY_AUTHORIZED_KEY="$$(ssh-add -L)" $(SETUP)
41+
42+
testbed-up: testbed-setup
43+
$(COMPOSE) up -d --build
44+
@echo "Waiting for SSHD containers..."
45+
@for port in 2221 2222 2223; do \
46+
for i in 1 2 3 4 5 6 7 8 9 10; do \
47+
ssh-keyscan -p $$port localhost >/dev/null 2>&1 && break; \
48+
sleep 1; \
49+
done; \
50+
done
51+
52+
testbed-scan: testbed-up
53+
$(SETUP)
54+
55+
testbed: testbed-scan build
56+
57+
# --- Run ---
58+
59+
run: $(BINARY)
60+
./$(BINARY) --config $(CONFIG) -G testbed
61+
62+
demo: testbed run
63+
64+
# --- Cleanup ---
65+
66+
testbed-down:
67+
-$(COMPOSE) down
68+
69+
clean: testbed-down
70+
rm -rf .pretty-test
71+
rm -f $(BINARY) coverage.out

go.mod

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/ncode/pretty
33
go 1.26
44

55
require (
6-
github.com/charmbracelet/bubbles v1.0.0
7-
github.com/charmbracelet/bubbletea v1.3.10
6+
charm.land/bubbles/v2 v2.1.0
7+
charm.land/bubbletea/v2 v2.0.2
88
github.com/fatih/color v1.18.0
99
github.com/mitchellh/go-homedir v1.1.0
1010
github.com/ncode/ssh_config v0.0.0-20260207174636-b38c9e3f09f0
@@ -14,27 +14,27 @@ require (
1414
)
1515

1616
require (
17+
charm.land/lipgloss/v2 v2.0.2 // indirect
1718
github.com/atotto/clipboard v0.1.4 // indirect
18-
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
19+
github.com/aymanbagabas/go-udiff v0.4.1 // indirect
1920
github.com/charmbracelet/colorprofile v0.4.2 // indirect
20-
github.com/charmbracelet/lipgloss v1.1.0 // indirect
21+
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
2122
github.com/charmbracelet/x/ansi v0.11.6 // indirect
22-
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
23+
github.com/charmbracelet/x/exp/golden v0.0.0-20251109135125-8916d276318f // indirect
24+
github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260323091123-df7b1bcffcca // indirect
2325
github.com/charmbracelet/x/term v0.2.2 // indirect
24-
github.com/clipperhouse/displaywidth v0.10.0 // indirect
25-
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
26-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
26+
github.com/charmbracelet/x/termios v0.1.1 // indirect
27+
github.com/charmbracelet/x/windows v0.2.2 // indirect
28+
github.com/clipperhouse/displaywidth v0.11.0 // indirect
29+
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
2730
github.com/fsnotify/fsnotify v1.9.0 // indirect
2831
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
2932
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3033
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
3134
github.com/mattn/go-colorable v0.1.14 // indirect
3235
github.com/mattn/go-isatty v0.0.20 // indirect
33-
github.com/mattn/go-localereader v0.0.1 // indirect
34-
github.com/mattn/go-runewidth v0.0.19 // indirect
35-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
36+
github.com/mattn/go-runewidth v0.0.21 // indirect
3637
github.com/muesli/cancelreader v0.2.2 // indirect
37-
github.com/muesli/termenv v0.16.0 // indirect
3838
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
3939
github.com/rivo/uniseg v0.4.7 // indirect
4040
github.com/sagikazarmark/locafero v0.12.0 // indirect
@@ -44,6 +44,7 @@ require (
4444
github.com/subosito/gotenv v1.6.0 // indirect
4545
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
4646
go.yaml.in/yaml/v3 v3.0.4 // indirect
47+
golang.org/x/sync v0.20.0 // indirect
4748
golang.org/x/sys v0.42.0 // indirect
4849
golang.org/x/text v0.35.0 // indirect
4950
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect

0 commit comments

Comments
 (0)