Skip to content

feat(cli): agent contract — exit codes, error envelope, confirmation without a terminal, dry run - #26

Merged
Domantas merged 5 commits into
mainfrom
feat/agent-native-cli
Oct 3, 2026
Merged

Domantas merged 5 commits into
mainfrom
feat/agent-native-cli

Conversation

@Domantas

@Domantas Domantas commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

PR 1 of 3 for "Agent-native CLI" (docs/tasks/agent-native-cli.md). PR 2 (feat/agent-native-cli-2, discovery + dry-run everywhere) and PR 3 (feat/agent-native-cli-3, startup + agent docs + norbix ai init) are stacked on top of this branch so each stays reviewable; merge this one first.

Breaking: destructive commands (delete, stop, regenerate, block, disable, db update --many, db delete --id) now exit 3 and send nothing when there is no terminal and no --yes — before, norbix users delete x </dev/null (or | tee) deleted silently. users block, scheduler disable, payments disable, webhooks disable, push integration disable ask for confirmation now. Error text moved to stderr as Error: … / Hint: …; exit codes are the documented 0–9 table instead of oclif's 1/2.

What changed

  • Exit codes (src/lib/exit-codes.ts): the documented table 0–9, the same with and without --json. No oclif import — the MCP server can share it.
  • One error envelope (src/lib/cli-error.ts, BaseCommand.catch): every failure — SDK errors, parser errors, bad JSON input, prompts, bugs, unknown commands — becomes {"error": {code, message, status, exit, fieldErrors, url, traceId, hint, docs}} on stdout with --json, or Error: … / Hint: … on stderr. oclif's own JSON error path (the 22 000-line parse-context dump) never runs. Unknown commands go through a command_not_found hook. No oclif import in the lib either.
  • Confirmation without a terminal (BaseCommand.confirmOrFail): destructive commands ask in a terminal as before; in a non-interactive shell (stdout or stdin not a TTY, CI set, or --json) they exit 3 with a one-line message naming --yes, and the request is never sent. "No" at the prompt exits 9 instead of printing Cancelled. and exiting 0.
  • --dry-run on every destructive command and on hub/api: the SDK builds the request with the real auth, region and project; a transport middleware stops it just before fetch; the SDK call and the HTTP request (method, URL, headers with the token redacted, body) are printed; exit 0. A dry run with a missing region or key fails like the real call would.
  • login / configure: every prompt guarded; non-interactive → exit 2 with the three flag/env alternatives, no stack trace, no unsettled-await warning.
  • No ANSI codes with --json, NO_COLOR or a pipe (init hook; there is still no colour library).
  • README: new "Errors and exit codes" section.

Tests

tests/agent-mode.test.ts runs the built CLI as a child process (pipes everywhere, like an agent sees it) against a fake gateway on a free port: every destructive command exits 3 without --yes and sends nothing, and runs with --yes; --dry-run prints the call and sends nothing; the envelope and exit code for 404 / 400 with fieldErrors / 401 / 500 / network / usage / unknown command / bad JSON input / unknown profile / login without a terminal — same exit code with and without --json, stdout one JSON document, stderr empty. tests/cli-error.test.ts unit-tests the mapping. 165 tests green.

Reproduction (the §6 harness, after)

users delete abc123 --profile x                        exit=3 stdout_lines=0   (nothing sent)
db update orders --many ... --profile x                 exit=3 stdout_lines=0   (nothing sent)
users get missing --profile x --json                    exit=5 stdout_lines=12  (status, code, traceId, hint)
users get bad --profile x --json                        exit=6 stdout_lines=16  (fieldErrors.email)
users list --profile y --json                           exit=7 stdout_lines=10  (url)
db update orders --profile x --json                     exit=2 stdout_lines=9   (was 26 879 lines)
nothing --json                                          exit=2 stdout_lines=9   (envelope)
users delete abc123 --profile x --dry-run --json        exit=0 stdout_lines=17  (request shown, nothing sent)

🤖 Generated with Claude Code

Domantas and others added 3 commits October 2, 2026 12:36
…without a terminal, dry run

Plan step [1] of docs/tasks/agent-native-cli.md.

- src/lib/exit-codes.ts: the documented exit-code table (0–9), identical
  with and without --json. No oclif import, so the MCP server can share it.
- src/lib/cli-error.ts: CliError + toEnvelope, one shape for every failure
  (SDK errors, parser errors, JSON input, prompts, bugs): code, message,
  status, exit, fieldErrors, url, traceId, hint, docs. No oclif import.
- BaseCommand.catch prints the envelope (stdout with --json, stderr as text)
  and exits with the documented code; oclif's own JSON error path (22k-line
  parse-context dump) never runs. Unknown commands go through a
  command_not_found hook and get the same envelope.
- confirmOrFail: destructive commands (delete, stop, regenerate, block,
  disable, --many updates) ask in a terminal; without a terminal and without
  --yes they exit 3 and send nothing; "no" at the prompt exits 9. The old
  `isTTY` check let `norbix users delete x </dev/null` (or `| tee`) delete
  silently.
- --dry-run on destructive commands: the SDK builds the request with the
  real auth/region/project and a transport middleware stops it before fetch;
  the SDK call and the HTTP request are printed, exit 0.
- login / configure: every prompt is guarded; a non-interactive shell gets
  exit 2 with the flag-based alternatives instead of a stack trace.
- hub/api: --yes / --dry-run are oclif flags now; context is resolved before
  the confirmation or the dry run.
- No ANSI codes with --json, NO_COLOR or a pipe (init hook).
- README: "Errors and exit codes".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ive sweep

Plan step [1] of docs/tasks/agent-native-cli.md.

The built CLI runs as a child process (pipes everywhere, like an agent sees
it) against a fake gateway on a free port. Covers: every destructive command
exits 3 without --yes and sends nothing, runs with --yes; --dry-run prints
the call and sends nothing, and still fails on a missing region or key; the
JSON envelope and exit code for 404, 400 with fieldErrors, 401, 500, network,
usage, unknown command, bad JSON input, unknown profile and login without a
terminal — the same exit code with and without --json, stdout one JSON
document, stderr empty; no ANSI codes. Plus unit tests for toEnvelope.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The handover (target behaviour, verified findings, specification, PR order,
reproduction harness, acceptance tests) plus the work log kept while
implementing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Domantas and others added 2 commits October 3, 2026 16:59
Each of the three tests spawns the CLI once per destructive command, four
at a time. Windows runners take ~3 s per spawn, so they ran past vitest's
20 s default, and the timed-out pool kept sending requests into the next
test (the --dry-run test then saw DELETE/PUT hits it never made).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Domantas
Domantas merged commit 2555549 into main Oct 3, 2026
11 checks passed
@Domantas
Domantas deleted the feat/agent-native-cli branch October 3, 2026 14:21
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.6.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant