Skip to content

Pipeline Design 4

Seth Ford edited this page Feb 12, 2026 · 2 revisions

Design: Add --json output flag to shipwright status command

Context

The shipwright status command (scripts/sw-status.sh, 605 lines) renders a rich terminal dashboard with 8 sections: tmux windows, team configs, task lists, daemon pipelines, issue tracker, heartbeats, remote machines, and connected developers. Currently it only produces ANSI-colored human-readable output.

Users and automated tooling (the web dashboard, CI scripts, sw-connect.sh) need machine-readable access to the same data. The codebase already has --json flag precedents in sw-cost.sh, sw-fleet.sh, and sw-pipeline-vitals.sh — all using the "early-exit separate code path" pattern where JSON collection runs before display code, assembles via jq -n, prints, and exits.

Constraints:

  • Bash 3.2 compatible (no associative arrays, no readarray, no ${var,,})
  • set -euo pipefail throughout
  • jq is the only JSON tool available (already a dependency across the project)
  • The existing 600-line display code path must remain completely untouched to avoid regression
  • Output helpers (info(), success(), warn(), error()) write to stderr, which naturally keeps JSON stdout clean

Decision

Separate early-exit code path, matching the established pattern from sw-cost.sh:550-670.

Data Flow

  1. Parse --json flag in the existing argument loop at the top of sw-status.sh
  2. When --json is set, run a dedicated collection block that gathers each section into shell variables holding JSON strings (e.g., WINDOWS_JSON, TEAMS_JSON)
  3. Each collector function reads the same files/commands the display code does (tmux list-windows, daemon-state.json, heartbeat files, etc.) but produces JSON fragments via jq
  4. Assemble all fragments into a single object with jq -n --argjson ... and print to stdout
  5. exit 0 before the display code path ever executes

JSON Schema (top-level keys)

{
  "version": "string",
  "timestamp": "ISO-8601 UTC",
  "tmux_windows": [{"name": "string", "panes": N, "active": bool}],
  "teams": [{"name": "string", "template": "string", "agents": N}],
  "task_lists": [{"team": "string", "total": N, "completed": N, "tasks": [...]}],
  "daemon": {"running": bool, "pid": N|null, "active_jobs": [...], "queued": [...], "recent_completions": [...]},
  "issue_tracker": {"provider": "string|null", "url": "string|null"},
  "heartbeats": [{"agent": "string", "last_seen": "ISO-8601", "status": "string"}],
  "remote_machines": [{"name": "string", "host": "string", "status": "string"}],
  "connected_developers": {"reachable": bool, "total_online": N, "developers": [...]}
}

Missing/unavailable sections produce null or [] — never omitted keys. This guarantees consumers can always reference any top-level key without existence checks.

Error Handling

  • If jq is not installed, emit error "jq is required for --json output" to stderr and exit 1 — check happens immediately after flag parsing, before any collection work
  • If tmux is not running, tmux_windows becomes [] (not an error)
  • If daemon-state.json is missing or malformed, daemon.running is false with remaining fields null/empty
  • Each collector is wrapped in a guard: if the source data is absent, emit the empty/null default. No collector failure should abort the entire JSON output

Flag Parsing

JSON_OUTPUT="false"
while [[ $# -gt 0 ]]; do
    case "$1" in
        --json) JSON_OUTPUT="true"; shift ;;
        --help|-h) usage; exit 0 ;;
        *) error "Unknown option: $1"; usage; exit 1 ;;
    esac
done

CLI Router Update

The help text in scripts/sw line ~70 updates to: status [--json] Show dashboard of running teams and agents

Alternatives Considered

  1. Refactor into collect/render pairs — Pros: Cleaner separation, each section gets collect_X() and render_X() functions, the JSON path calls collect_* only. Cons: Touching all 600 lines of existing display code to refactor into render functions creates significant regression risk. The plan initially proposed this but the "early exit" approach achieves the same result with zero changes to existing code. Refactoring can happen later as a separate PR.

  2. Inline JSON into each display section (interleaved) — Pros: Single code path, no duplication. Cons: Mixes display and data concerns, every section gets if $JSON; then ... fi blocks that double the complexity of every section. Much harder to maintain. Fragile — any display change risks breaking JSON output.

  3. External wrapper script — Pros: Zero changes to sw-status.sh. Cons: Would need to screen-scrape ANSI output or duplicate all the data collection logic in a new file. Not maintainable. Violates the "source of truth" principle.

Implementation Plan

Files to modify

  • scripts/sw-status.sh — Add --json flag parsing, jq check, 8 collector blocks, JSON assembly, early exit
  • scripts/sw — Update help text for status subcommand (~line 70)
  • completions/shipwright.bash — Add --json to status completions
  • completions/_shipwright — Add --json to status completions (zsh)
  • completions/shipwright.fish — Add --json to status completions (fish)
  • package.json — Register new test suite

Files to create

  • scripts/sw-status-test.sh — Test suite (mock environment, 10+ test cases, PASS/FAIL counters, ERR trap)

Dependencies

  • None new. jq is already required by the project (used in 20+ scripts)

Risk Areas

  • jq --argjson with large daemon state: If daemon-state.json contains hundreds of completed jobs, the assembled JSON could be large. Mitigate by limiting recent_completions to the last 20 entries (matching the dashboard's visual limit).
  • tmux not available: The tmux list-windows call will fail if tmux isn't running. The collector must handle this gracefully (return []).
  • Shell variable size: Each JSON fragment is stored in a bash variable. Extremely large task lists could theoretically hit shell limits. In practice, shipwright task lists are small (< 100 items). No mitigation needed for v1.
  • Pipefail + jq chains: Under set -eo pipefail, a jq parse error in a collector could abort the entire script. Each collector should use || echo '[]' / || echo 'null' fallbacks.

Validation Criteria

  • shipwright status (no flags) produces byte-identical output to the current version
  • shipwright status --json outputs valid JSON (jq empty exits 0)
  • JSON output contains zero ANSI escape sequences (grep -P '\x1b\[' finds nothing)
  • All 10 top-level keys present: version, timestamp, tmux_windows, teams, task_lists, daemon, issue_tracker, heartbeats, remote_machines, connected_developers
  • Empty state (no tmux, no daemon, no teams) produces valid JSON with []/null — not errors, not missing keys
  • shipwright status --json | jq .daemon.active_jobs returns a valid array (subsections independently queryable)
  • sw-status-test.sh passes all cases (valid JSON, key presence, empty state, active daemon, heartbeats, human-readable section headers preserved)
  • Test suite registered in package.json and runs via npm test
  • No Bash 3.2 incompatibilities (shellcheck clean, no associative arrays)
  • --json without jq installed prints clear error to stderr and exits 1
  • Tab completion works for shipwright status --json in bash, zsh, and fish

Clone this wiki locally