diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0b6d39a..2312aa8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -364,6 +364,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: AxeForging/reviewforge@main + continue-on-error: true with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AI_PROVIDER: gemini diff --git a/.structlint.yaml b/.structlint.yaml index a935e05..7a36bd5 100644 --- a/.structlint.yaml +++ b/.structlint.yaml @@ -38,6 +38,7 @@ file_naming_pattern: - ".gitignore" - ".goreleaser.yml" - "*.gif" + - "*.png" - "LICENSE" disallowed: - "*.env*" diff --git a/README.md b/README.md index d831f11..1d1be9a 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,22 @@

yoink

-Background process manager with interactive I/O. Spawn commands in the background, check their output, send input to hanging processes, and reattach with full terminal control. +Run interactive commands in the background, inspect their output, send input, capture TUI screens, wait on them from scripts, and reattach when you need full control. + +`yoink` is a lightweight PTY process manager for developers, CI jobs, automation scripts, and AI-agent workflows. It fills the space between shell background jobs, `tmux`, `expect`, and full process supervisors: every process gets a real terminal, a name, logs, input control, lifecycle state, and a way back in. ![yoink demo](docs/demo.gif) +## Why yoink? + +- Keep long-running commands alive without dedicating a terminal tab. +- Drive interactive CLIs from scripts with `send`, `send --key`, and `send-redacted`. +- Read only new output with `log --new`, which is useful for polling and test reports. +- Render TUI apps with `snapshot` instead of raw escape-sequence logs. +- Coordinate dev servers, test runners, deploys, tunnels, model servers, and AI coding agents. + +See [Why yoink?](docs/WHY.md) for the deeper positioning and comparisons. + ## Install ```bash @@ -19,95 +31,55 @@ curl -Lo yoink.tar.gz https://github.com/AxeForging/yoink/releases/latest/downlo tar xzf yoink.tar.gz && sudo mv yoink /usr/local/bin/ ``` -## Usage +## Quickstart ```bash -# Spawn a command in the background -yoink run "make deploy-staging" - -# Spawn with an alias for easier reference +# Start a command under a managed PTY yoink run --alias deploy "make deploy-staging" -# List all managed processes +# Check state and output yoink ls - -# Check output of a process (by ID or alias) -yoink log 1 -yoink log deploy --lines 100 - -# Show only new lines since last --new call yoink log deploy --new -# Render the current screen of any process — works correctly for TUI apps -# (yoink log gives raw escape garbage for TUIs; snapshot renders the actual frame) -yoink snapshot htop -yoink snap htop # short alias - -# Send input to a process waiting for confirmation +# Answer a prompt without reattaching yoink send deploy "y" -# Send input character by character (simulates typing — great for asciinema recordings) -yoink send deploy "hello world" --type - -# Control typing speed with --delay (default 50ms) -yoink send deploy "hello world" --type --delay 100 - -# Send without pressing Enter (useful for prompts that read a single key) -yoink send deploy "y" --no-enter - -# Submit text with carriage-return Enter (useful for TUIs like Claude) -yoink send claude "say only OK" --submit - -# Send a named key sequence (arrow keys, ctrl combos, function keys, etc.) -yoink send deploy --key up -yoink send deploy --key ctrl+c -yoink send deploy --key pagedown -yoink send deploy --key f5 - -# Send sensitive input (passwords, tokens) — hidden from logs -yoink send-redacted deploy "my-secret-token" - -# Flags work on send-redacted too -yoink send-redacted deploy "my-secret" --type --delay 80 - -# Full interactive reattach (Ctrl+] to detach) -yoink attach deploy - -# Wait for a process to finish (streams output, exits with process exit code) +# Wait from a script, streaming new output yoink wait deploy --timeout 300 --poll 5 -# Wait for multiple processes at once (fail-fast on first failure) -yoink wait build install runner-startup --timeout 300 - -# Wait with custom timeout message -yoink wait build --timeout 60 --message "Build exceeded 1min budget" - -# Kill a process +# Reattach later, or clean up +yoink attach deploy # Ctrl+] detaches yoink kill deploy -yoink kill 1 --force # SIGKILL - -# Remove finished processes yoink clean ``` -> **More examples?** See [EXAMPLES.md](EXAMPLES.md) for real-world use cases — CI pipelines, interactive prompts, TUI monitoring, secret handling, and more. - -## How it works - -yoink runs a lightweight daemon that holds PTY (pseudo-terminal) sessions open for each background process. The CLI client communicates with the daemon over a Unix socket. - -- **run**: Spawns the command under a PTY in the daemon, returns an ID. Use `--alias` to name it -- **ls**: Lists all processes with state (running/done/failed), PID, exit code -- **log**: Shows buffered output (last 1000 lines kept per process). Use `--new` to see only lines since the last `--new` call -- **wait**: Polls a process until it finishes, streaming new output to stdout on each cycle. Exits 0 on success, propagates the process exit code on failure, or exits 124 on timeout (matches GNU `timeout` convention). Flags: `--timeout` (seconds, 0=forever), `--poll` (seconds, default 2), `--message` (custom timeout error) -- **snapshot** (`snap`): Renders the current VT100 screen of a process as plain text. Unlike `log`, this correctly handles TUI applications (htop, vim, Claude, etc.) by replaying their escape sequences through a VT100 emulator and returning only the rendered content. Works on both running and finished processes -- **send**: Writes text to the process stdin (fire-and-forget). Flags: `--type` (character by character), `--delay` (ms between chars, default 50), `--no-enter` (omit trailing newline), `--submit` (append carriage-return Enter for TUIs like Claude), `--key` (send a named key sequence — e.g. `up`, `down`, `ctrl+c`, `f1`, `pageup`; implies `--no-enter`) -- **send-redacted**: Same as send, but input is masked in logs. Supports the same flags -- **attach**: Bridges your terminal to the process PTY for full interactive control -- **kill**: Sends SIGTERM (or SIGKILL with --force) -- **clean**: Removes finished processes from the list - -The daemon auto-starts when needed and stores its socket at `~/.yoink/yoink.sock`. +## Common Commands + +| Task | Command | +|------|---------| +| Run in background | `yoink run --alias name "command"` | +| List managed processes | `yoink ls` | +| Read recent output | `yoink log name --lines 100` | +| Read only new output | `yoink log name --new` | +| Send input | `yoink send name "text"` | +| Send a key | `yoink send name --key ctrl+c` | +| Send secret input | `yoink send-redacted name "$TOKEN"` | +| Render a TUI screen | `yoink snapshot name` | +| Wait with timeout | `yoink wait name --timeout 300` | +| Reattach interactively | `yoink attach name` | +| Stop a process | `yoink kill name` | + +## Documentation + +- [Why yoink?](docs/WHY.md): what problem it solves and how it compares to common alternatives. +- [Killer examples](docs/KILLER_EXAMPLES.md): compact real-world CI and automation cases where yoink removes painful glue code. +- [Command reference](docs/COMMANDS.md): commands, aliases, flags, and behavior notes. +- [Examples](EXAMPLES.md): real-world workflows for CI, prompts, TUIs, tunnels, AI agents, model servers, E2E tests, and more. +- [Architecture](docs/README.md): daemon/client design, PTY handling, snapshots, protocol, and platform notes. + +## How It Works + +yoink runs a lightweight daemon that holds PTY sessions open for background processes. The CLI talks to the daemon over a Unix socket at `~/.yoink/yoink.sock`, which auto-starts when needed. ## Platform Support diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md new file mode 100644 index 0000000..d0004d0 --- /dev/null +++ b/docs/COMMANDS.md @@ -0,0 +1,205 @@ +# Command Reference + +This page covers the command surface. For longer workflows, see [Examples](../EXAMPLES.md). + +## Process Lifecycle + +### `yoink run` + +Spawn a command under a managed PTY. + +```bash +yoink run "make deploy-staging" +yoink run --alias deploy "make deploy-staging" +``` + +Aliases: `r` + +Flags: + +| Flag | Description | +|------|-------------| +| `--alias ` | Assign a stable name for later commands | + +### `yoink ls` + +List managed processes with state, PID, aliases, and exit information. + +```bash +yoink ls +``` + +Aliases: `list`, `ps` + +### `yoink kill` + +Terminate a managed process. + +```bash +yoink kill deploy +yoink kill deploy --force +``` + +Aliases: `k` + +Flags: + +| Flag | Description | +|------|-------------| +| `--force`, `-f` | Send SIGKILL instead of SIGTERM | + +### `yoink clean` + +Remove finished or failed processes from the process list. + +```bash +yoink clean +``` + +## Output + +### `yoink log` + +Show buffered output from a process. The daemon keeps the recent output for each managed process. + +```bash +yoink log deploy +yoink log deploy --lines 100 +yoink log deploy --new +``` + +Aliases: `l` + +Flags: + +| Flag | Description | +|------|-------------| +| `--lines`, `-n` | Number of output lines to show. Default: `50` | +| `--new` | Show only lines not yet seen by a previous `--new` call | + +Use `log` for normal line-oriented programs. Use `snapshot` for TUIs. + +### `yoink snapshot` + +Render the current VT100 screen of a process as plain text. + +```bash +yoink snapshot htop +yoink snap htop +``` + +Aliases: `snap` + +This is useful for TUI apps such as `htop`, `vim`, `k9s`, chat CLIs, and AI coding agents. `log` shows raw terminal bytes for those programs; `snapshot` shows the rendered screen. + +## Input + +### `yoink send` + +Send text or key sequences to a running process. + +```bash +yoink send deploy "y" +yoink send repl "hello world" --type --delay 80 +yoink send claude "say only OK" --submit +yoink send tui --key down +yoink send tui --key ctrl+c +``` + +Aliases: `s` + +Flags: + +| Flag | Description | +|------|-------------| +| `--type` | Send input character by character | +| `--delay`, `-d` | Milliseconds between characters with `--type`. Default: `50` | +| `--no-enter` | Do not send a trailing newline | +| `--submit` | Submit text with carriage-return Enter, useful for TUIs | +| `--key`, `-k` | Send a named key sequence instead of text | + +Common keys include `up`, `down`, `left`, `right`, `enter`, `tab`, `escape`, `ctrl+c`, `f1`, and `pageup`. + +### `yoink send-redacted` + +Send sensitive input while masking it in yoink logs. + +```bash +yoink send-redacted deploy "$DB_PASSWORD" +yoink send-redacted deploy "$TOKEN" --type --delay 80 +``` + +Flags: + +| Flag | Description | +|------|-------------| +| `--type` | Send input character by character | +| `--delay`, `-d` | Milliseconds between characters with `--type`. Default: `50` | +| `--no-enter` | Do not send a trailing newline | +| `--submit` | Submit text with carriage-return Enter, useful for TUIs | + +## Waiting And Reattaching + +### `yoink wait` + +Wait for one or more processes to finish. Output is streamed while waiting. + +```bash +yoink wait deploy --timeout 300 --poll 5 +yoink wait build test lint --timeout 600 +yoink wait migrate --timeout 600 --message "Migration exceeded 10min budget" +``` + +Aliases: `w` + +Flags: + +| Flag | Description | +|------|-------------| +| `--timeout`, `-t` | Maximum seconds to wait. `0` means forever | +| `--poll`, `-p` | Seconds between state checks. Default: `2` | +| `--message`, `-m` | Custom error message on timeout | + +Exit behavior: + +| Result | Exit code | +|--------|-----------| +| All processes succeeded | `0` | +| A process failed | That process exit code | +| Timeout | `124` | + +### `yoink attach` + +Bridge your terminal to the process PTY for full interactive control. + +```bash +yoink attach deploy +``` + +Aliases: `a` + +Detach with `Ctrl+]`. + +## Other Commands + +### `yoink version` + +Show version information. + +```bash +yoink version +``` + +### `yoink daemon` + +Start the daemon. This is normally auto-started by other commands. + +```bash +yoink daemon +``` + +## Global Flags + +| Flag | Description | +|------|-------------| +| `--verbose` | Enable verbose logging | diff --git a/docs/KILLER_EXAMPLES.md b/docs/KILLER_EXAMPLES.md new file mode 100644 index 0000000..af74df2 --- /dev/null +++ b/docs/KILLER_EXAMPLES.md @@ -0,0 +1,211 @@ +# Killer Examples + +These are compact workflows where `yoink` replaces brittle shell glue with named, scriptable PTY sessions. + +For longer versions, see [Examples](../EXAMPLES.md). + +## CI: Dev Server + E2E Tests + +CI often needs a web server running while Playwright or Cypress tests execute. Plain `npm run dev &` makes it awkward to know whether the server is ready, capture the right logs, and clean up reliably. + +```bash +yoink run --alias app "npm run dev" + +ready=0 +for i in $(seq 1 60); do + if yoink log app --new | grep -q "Local:.*http://localhost:3000"; then + ready=1 + break + fi + sleep 1 +done + +if [ "$ready" -ne 1 ]; then + yoink log app + yoink kill app + exit 1 +fi + +yoink run --alias e2e "npx playwright test --reporter=list" +yoink wait e2e --timeout 300 --poll 5 +status=$? + +if [ "$status" -ne 0 ]; then + yoink log app --new +fi + +yoink kill app +yoink clean +exit "$status" +``` + +Why it hurts without yoink: readiness checks, server logs, test logs, and cleanup usually become separate ad hoc scripts. + +## CI: Parallel Build Matrix With Labeled Logs + +Shell background jobs can parallelize work, but debugging failures is painful when logs interleave or disappear. + +```bash +yoink run --alias build-api "make build-api" +yoink run --alias build-web "make build-web" +yoink run --alias build-worker "make build-worker" + +yoink wait build-api build-web build-worker --timeout 600 --poll 5 +``` + +Why it hurts without yoink: `wait` only tells you that something failed; yoink keeps each command named, stateful, and inspectable. + +## CI: Start Slow Setup Early, Join Later + +Some CI steps are slow because they download dependencies, pull models, warm caches, build assets, or boot local services. If later steps do not need them immediately, start them with `yoink`, keep doing other work, then wait only when the pipeline actually depends on the result. + +```yaml +name: test + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Install yoink + run: | + go install github.com/AxeForging/yoink@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Start slow setup + run: | + yoink run --alias pg-image "docker pull postgres:16" + yoink run --alias redis-image "docker pull redis:7" + yoink run --alias web-deps "cd web && npm ci" + + - name: Do independent checks while setup runs + run: | + test -z "$(gofmt -l .)" + go vet ./... + go test ./internal/... + + - name: Join slow setup + run: | + yoink wait pg-image redis-image web-deps --timeout 900 --poll 10 + + - name: Run tests that need the setup + run: | + docker compose up -d postgres redis + (cd web && npm test) + (cd web && npx playwright test) +``` + +Why it hurts without yoink: shell `&` can start work in the background, but later steps lose a clean handle for logs, state, exit codes, and timeout behavior. yoink turns those slow setup tasks into named CI resources you can inspect and join later. + +## CI: Interactive Infrastructure Approval + +Infrastructure tools sometimes need a plan review before confirmation. That is a bad fit for fully headless scripts and a bad fit for manual terminal babysitting. + +```bash +yoink run --alias infra "pulumi up --stack prod" +sleep 10 + +yoink log infra --new +yoink send infra "details" +sleep 5 +yoink log infra --new + +yoink send infra "yes" +yoink wait infra --timeout 900 --poll 15 \ + --message "Infrastructure apply exceeded 15min" +``` + +Why it hurts without yoink: `yes | pulumi up` skips review, while a manual run cannot be controlled cleanly by CI. + +## CI: Secret Entry For PTY-Aware Prompts + +Some tools demand secrets through an interactive prompt and will not behave correctly with a normal stdin pipe. + +```bash +yoink run --alias release "vendor-cli deploy --prod" +sleep 2 + +yoink send-redacted release "$DEPLOY_TOKEN" +yoink wait release --timeout 300 +``` + +Why it hurts without yoink: `echo "$TOKEN" | command` can fail with PTY-aware prompts and can leak secrets through logs or shell history. + +## Local Dev: Port-Forward Stack + +Multiple `kubectl port-forward` processes are easy to start and easy to lose. + +```bash +yoink run --alias pf-api "kubectl port-forward svc/api 8080:80" +yoink run --alias pf-db "kubectl port-forward svc/postgres 5432:5432" +yoink run --alias pf-grafana "kubectl port-forward svc/grafana 3000:3000" + +yoink ls +yoink log pf-api --new + +yoink kill pf-api +yoink kill pf-db +yoink kill pf-grafana +yoink clean +``` + +Why it hurts without yoink: when a port-forward drops, a background shell job rarely tells you clearly what died or why. + +## AI Agents: Multiple Interactive Workers + +AI coding agents are long-running, interactive, and often TUI-based. They need real terminals, but you still want to coordinate them from scripts. + +```bash +yoink run --alias agent-api "cd worktree-api && claude" +yoink run --alias agent-tests "cd worktree-tests && claude" + +yoink send agent-api "Add pagination to GET /users" --submit +yoink send agent-tests "Wait for the API shape, then write tests" --submit + +yoink snapshot agent-api +yoink wait agent-api --timeout 900 --poll 10 + +yoink send agent-tests "API is done. Use query params page and limit." --submit +yoink snapshot agent-tests +``` + +Why it hurts without yoink: terminal multiplexers are manual; yoink gives scripts a way to observe, steer, and reattach. + +## TUI Monitoring: Render The Screen, Not Escape Codes + +TUIs write terminal control sequences, so logs are usually unreadable. `snapshot` gives you the rendered screen. + +```bash +yoink run --alias gpu "watch -n 5 nvidia-smi" +yoink snapshot gpu + +yoink run --alias train "python train.py" +yoink log train --new +``` + +Why it hurts without yoink: raw PTY logs are not the same thing as the screen the user sees. + +## Stdio Server Harness + +Stdio-based tools such as MCP servers need stdin/stdout held open while you send structured messages and inspect responses. + +```bash +yoink run --alias mcp "node dist/mcp-server.js --stdio" +sleep 1 + +yoink send mcp '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' +yoink log mcp --new + +yoink send mcp '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search","arguments":{"query":"test"}}}' +yoink log mcp --new +``` + +Why it hurts without yoink: you otherwise end up writing a custom harness just to keep the process alive and talk to it. diff --git a/docs/README.md b/docs/README.md index 5b91bcd..e8e8791 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,12 @@ # yoink Documentation +## Guides + +- [Why yoink?](WHY.md): positioning, use cases, and comparisons with common alternatives. +- [Killer examples](KILLER_EXAMPLES.md): compact real-world CI and automation cases where yoink removes painful glue code. +- [Command reference](COMMANDS.md): commands, aliases, flags, and behavior notes. +- [Examples](../EXAMPLES.md): practical workflows for CI, prompts, TUIs, tunnels, AI agents, model servers, and more. + ## Architecture yoink uses a daemon/client architecture: diff --git a/docs/WHY.md b/docs/WHY.md new file mode 100644 index 0000000..0c15f2d --- /dev/null +++ b/docs/WHY.md @@ -0,0 +1,65 @@ +# Why yoink? + +`yoink` is for commands that are too interactive for plain background jobs and too automation-heavy for a manual terminal multiplexer. + +It gives every managed command a real PTY, a stable name, buffered output, lifecycle state, input control, timeout-aware waiting, rendered TUI snapshots, and an escape hatch back into full interactive control. + +## The Problem + +Developers constantly run commands that do not fit neatly into one terminal session: + +- deploys that ask for confirmation halfway through +- dev servers that need to stay alive while tests run +- migrations, model pulls, builds, training jobs, and backfills that take a long time +- `kubectl port-forward`, SSH tunnels, and local daemons that fail silently +- TUI programs where raw logs are useless +- AI coding agents and chat CLIs that need a real terminal + +The common workaround is a pile of terminal tabs, shell `&`, `nohup`, `tmux`, `screen`, `expect`, and ad hoc scripts. Those tools are useful, but the seams show when you want to script the workflow: start something, check only new output, send a key, wait with a timeout, inspect a rendered screen, then reattach if needed. + +## What yoink Adds + +`yoink` treats interactive processes as named, scriptable terminal sessions: + +```bash +yoink run --alias app "npm run dev" +yoink log app --new +yoink send app --key ctrl+c +yoink wait app --timeout 300 +yoink attach app +``` + +The important part is that the process still thinks it is attached to a terminal. That means prompts, TUIs, colored output, terminal control sequences, and REPLs behave much closer to how they behave in a real shell. + +## Compared To Alternatives + +| Tool | Great for | Where yoink fits | +|------|-----------|------------------| +| Shell `&` / `jobs` | Quick one-off backgrounding | Adds names, logs, process state, input, timeouts, and reattach | +| `nohup` | Keeping non-interactive commands alive | Keeps a real PTY and supports later interaction | +| `tmux` / `screen` | Manual long-lived terminal sessions | Provides script-friendly commands around PTY sessions | +| `expect` | Automating known prompt sequences | Lets scripts inspect, react, send keys, and debug incrementally | +| `timeout` | Killing commands after a deadline | Streams output while waiting and preserves process logs | +| Process supervisors | Running services reliably | Targets developer workflows, prompts, TUIs, and temporary commands | +| Docker Compose | Container service groups | Also manages local binaries, CLIs, tunnels, and test runners | + +`yoink` does not replace these tools. It gives you a small CLI vocabulary for the annoying middle ground: interactive programs that need to be backgrounded, observed, poked, waited on, and occasionally reattached. + +## Best Fits + +- CI scripts that need to run several commands and keep useful logs. +- E2E test setup where a dev server must be started, checked, and torn down. +- Deploys and infrastructure applies that require approval gates. +- Long-running builds, migrations, model pulls, or ML training jobs. +- Port-forward and tunnel management during debugging. +- TUI monitoring with `snapshot`. +- AI agent orchestration where each agent runs in its own PTY. +- Demo recording with typed input via `send --type --delay`. + +## Mental Model + +Think of `yoink` as: + +> named, scriptable PTY sessions with logs, input, snapshots, waits, and reattach. + +That is the core value. It is intentionally smaller than a terminal multiplexer and more interactive than a process supervisor.