This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
See AGENTS.md for build/test/lint commands and code style guidelines.
go build -v ./... # build all packages
go build -o pretty . # build binary
go test -v ./... # full test suite
go test -v ./internal/shell # single package
go test -v ./internal/shell -run '^TestParseCommandAsync$' # single test
go test -race ./... # race detection (run when touching concurrent code)
go test -coverpkg=./... ./... -race -coverprofile=coverage.out -covermode=atomic # CI coverage
gofmt -w . && go vet ./... # format + lintGo 1.26 required (see go.mod). CI runs on Ubuntu with go-version: '1.26'.
CLI (cmd/root.go)
-> parse host specs, resolve SSH config, build HostList
-> shell.Spawn(hostList)
-> start Broker goroutine (fan-out)
-> start Bubbletea TUI program
User input -> model.Update -> CommandRequest -> broker channel
-> per-host worker goroutine -> SSH stdin
SSH stdout -> ProxyWriter -> OutputEvent channel
-> model.Update (outputMsg) -> viewport
cmd/-- Cobra CLI. Host spec parsing (hosts.go), SSH config resolution, group/file loading, color assignment. Entry point callsshell.Spawn.internal/shell/-- Bubbletea v2 TUI. Themodelstruct owns the text input, viewport, output buffer, command history, and job manager. Commands are parsed incommand.go(:async,:status,:list,:scroll,:bye,exit, or plain text = run).internal/sshConn/-- SSH connection lifecycle.Brokerfans a singleCommandRequestchannel out to per-hostworkergoroutines. Each worker holds a persistent SSH shell session.RunCommandopens a fresh session for async jobs.config.goresolves SSH config (Host/Match patterns, ProxyJump, IdentityFile) viancode/ssh_config.internal/jobs/-- Job tracking.Managermaintains the latest normal job and last 2 async jobs. Thread-safe via mutex + snapshot cloning for reads.sentinel.goinjects/extracts__PRETTY_EXIT__<jobID>:<exitCode>markers to capture per-host exit codes from shell output.
- Broker pattern: One goroutine reads from the shared
CommandRequestchannel and forwards to each connected host's private channel. - Per-host workers: Each host has a dedicated goroutine holding an SSH shell session, reading from its private channel.
- Async jobs: Each
:asynccommand spawns N goroutines (one per connected host), each opening a fresh SSH session viaRunCommand. - Connection state:
Host.IsConnectedandHost.IsWaitingareint32atomics. - Job snapshots:
Manageruses copy-on-read (cloneJob) so the Bubbletea model never holds a lock while rendering.
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.
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.
internal/sshConnuses function variables (connectionFunc,sessionFunc,workerRunner) to stub SSH calls in tests.cmd/root.gousesloadSSHConfigFunc,resolveHostFunc, andspawnShellFuncfor the same purpose.- Table-driven tests throughout; use
t.Runwith descriptive subtest names.
For integration testing against real SSH servers:
make demo # setup + docker + build + run (full workflow)
make clean # tear down containers and remove .pretty-test/Or step by step:
export PRETTY_AUTHORIZED_KEY="$(ssh-add -L | grep 'my-key' | head -n1)" # optional
make testbed # setup + docker + host key scan + build
make run # launch pretty against testbed
make testbed-down # stop containers