Skip to content

Evolving the CLI Without Breaking Its Contracts

What to preserve when separating Barn’s command tree, application orchestration, and output handling.

Barn already uses Cobra. The useful part of the 2026-11-04 CLI proposal is its separation of command syntax, application orchestration, and observable behavior. This essay retains that design, checked against v1.1.0 on 2026-11-09. The package split and broader command reorganization remain proposals; current usage belongs in the CLI reference.

Keep the existing boundaries

cmd/barn/command_*.go defines the command families. Each execution constructs a fresh Cobra tree, avoiding flag values leaking between calls. Command handlers collect a commandOutcome; a shared renderer handles text or structured results, while typed failures retain their exit code, reason, recovery hint, and any partial result. Linux lifecycle, image verification, and native Mac behavior remain in their domain packages.

These boundaries already protect several contracts:

Contract What a refactor must preserve
Deployment selection One Linux deployment under BARN_HOME; named Mac VMs remain under its mac/ subtree
Configuration source Desired-state commands resolve inventory; start, stop, status, and access use applied state
Machine-readable output Existing payload fields and types, JSON/YAML parity, and useful partial results on failure
Interactive access Real terminal descriptors, child-process signals, and remote exit status
Help and completion Existing aliases, local flags, platform scope, and offline completion

Moving a command must not turn an invalid inventory into a fallback to old state, reinterpret init -o FILE as an output format, or collapse up, restart, reload, and recreate into one operation. Their differences describe which configuration is read and which resources may change.

Resolve presentation without parsing twice

The current prepareOutput adapter scans presentation options before Cobra, using commandFlagValues to distinguish flags from values. A future simplification should let the same command tree drive normal parsing, help, completion, and exported command descriptions. It still needs a narrow way to choose error presentation when parsing itself fails.

The invariant is explicit CLI value → selected environment default → built-in default. Validate the selected value after resolving precedence. Barn 1.0.0 already fixed valid CLI overrides being rejected by an invalid environment default. Preserve that fix, explicit boolean false, and values that resemble flags: -o --json may name a file, while anything after the remote -- boundary belongs to the guest command.

Human help remains text with --json --help. Bare barn prints full help in text mode and retains actions[] in structured mode. A proposed machine-readable help schema would be a separate, explicit interface generated from the command tree.

Separate results from streams

Single-result JSON/YAML commands emit one result document on stdout. Progress, warnings, and diagnostics use stderr. Preserve command-specific payloads such as .nodes and .results; wrapping every response in a new data object would break existing automation.

SSH and followed logs need streaming paths. They must retain empty arguments, quoting, a second --, cancellation, and the guest’s nonzero exit code. Structured interactive SSH sends session output to stderr and the final report to stdout; followed logs emit NDJSON or a YAML document stream. A renderer cannot safely buffer an interactive session into an ordinary result object. Deprecation notices must also stay out of structured stdout.

Move responsibility before changing behavior

The proposed structure has three layers:

Layer Responsibility
cmd/barn Process entry, signals, and exit status
Proposed internal/cli Cobra tree, argument validation, presentation, and stream wiring
Application orchestration and existing domain packages Typed requests and reports, lifecycle work, network setup, images, and Mac VMs

Inject the streams, environment reader, prompter, and runners actually needed by each operation. Keep domain services independent of Cobra and avoid a generic dispatcher that erases request types. Confirmation and SSH must use the injected input stream throughout; setting a Cobra input stream while reading global os.Stdin elsewhere leaves tests and embedded calls inconsistent.

The proposal moves side effects after validation. In the locked Cobra 1.10.2, pre-run hooks precede required-flag and flag-group validation, so they should perform only normalization and checks. Current lifecycle commands intentionally record a begin event before some configuration checks to make early failures traceable; changing that ordering needs an explicit audit-policy decision. A new --dry-run option needs a real read-only planner with the same scope as execution; calling a mutator and suppressing its final write is insufficient.

Confirmation, permission, and execution are separate facts. Existing network and hosts commands can successfully return a plan with applied=false; automation checks that field or passes --yes. Confirmation never bypasses identity, ownership, or integrity checks. Likewise, status can reconcile interrupted runtime records, so a query-like command name alone does not establish a read-only contract.

Reconcile the proposal with shipped behavior

The original proposal contains both durable contracts and dated product choices. At the 2026-11-09 review:

  • 1.0.0 already includes full bare-command help, structured actions[], confirmation aliases on applicable destructive commands, and the output-environment precedence fix.
  • CLI code still lives in cmd/barn/. The proposed internal/cli extraction, --format, image update, and ssh-config show/install/uninstall command tree have not landed.
  • Current Barn 1.1.0 adds --image/-I and --arch/-A overrides to applicable inventory operations, unifies result and live-progress tables (freezing in-place TTY redraws before prompts or errors), standardizes plain ASCII terminal output (->, :, ;, [DONE]/[FAILED]/[CANCELLED]), makes barn purge execute immediately by default with --ask for interactive confirmation, and upgrades YAML parsing to go.yaml.in/yaml/v4.

Further behavior changes need their own decision and migration notes.

Validate the contract at each step

First extract responsibilities with command behavior and state formats unchanged. Then add compatible entry points, followed by separately reviewed changes to defaults or selection scope. Compare old and new executables sequentially against controlled fixtures; help, completion, and invalid-input tests should check for unwanted files, subprocesses, or network access as well as output.

Regression coverage must include CLI/environment precedence, stdout/stderr separation, partial failures, remote argument boundaries, real PTY signals, and confirmation scope. Reuse cli_test.go, output_test.go, remote_command_test.go, and signal_test.go. Moving tests into a new package also requires updating CI package paths and build targets. VM or host-network behavior changes need the corresponding physical-host acceptance.

Table reader