Skip to content

feat(cli): non-interactive / agent-friendly auth + region flags#569

Merged
coderdan merged 8 commits into
mainfrom
agent-friendly-cli
Jul 8, 2026
Merged

feat(cli): non-interactive / agent-friendly auth + region flags#569
coderdan merged 8 commits into
mainfrom
agent-friendly-cli

Conversation

@coderdan

@coderdan coderdan commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Why

Driving the CLI as an agent (or in CI / over a pipe) hits one hard wall: the
region picker in stash auth login — and, transitively, stash init — is an
interactive p.select with no non-interactive escape hatch, so it blocks.
Everything else in init already has one (--database-url/DATABASE_URL,
--proxy/--no-proxy, --target), so region was the last thing standing
between an agent and a scripted stash init.

This PR closes that gap additively — nothing changes for an interactive
human (the picker still renders in a real terminal); we only add flags, env
vars, and a machine-readable output mode.

Scope: every change is under packages/cli. No existing default behaviour is
altered.

What changed

1. Region without a prompt — --region / STASH_REGION

  • Added to both stash auth login and stash init.
  • Accepts the short slug (us-east-1) or canonical form (us-east-1.aws),
    case-insensitively.
  • Resolution mirrors the existing DATABASE_URL resolver: --region
    STASH_REGION → interactive picker (only when stdin.isTTY and not
    CI) → clean exit(1) with an actionable message (never a hang).
  • Lives in a new native-free module src/commands/auth/region.ts so the pure
    helpers (normalizeRegion, regionSlugs) and the resolver policy get fast
    unit coverage without loading the @cipherstash/auth binary. login.ts
    re-exports the names, so existing call sites are untouched.

2. Agent-triggerable device-code auth — stash auth login --json

The device-code flow is the natural fit for "agent triggers, human
completes
". With --json, auth login emits newline-delimited events:

{"status":"authorization_required","userCode":"ABCD-1234","verificationUri":"https://…/activate","verificationUriComplete":"https://…/activate?user_code=ABCD-1234","expiresIn":899}
// … blocks polling until a human authorizes in the browser …
{"status":"authorized","expiresAt":1751990400,"expiresAtIso":"2025-07-08T12:00:00.000Z"}
{"status":"device_bound"}

The intended agent recipe: run it in the background, read the first line,
surface verificationUriComplete to the user, let them finish in the browser.
The agent never completes auth. --no-open suppresses the browser launch for
headless boxes. Errors are {"status":"error","code":"…","message":"…"} + a
non-zero exit; a missing region in --json mode returns code:"region_required"
immediately rather than hanging.

3. Docs + help

--help (top-level and auth), the package README, and AGENTS.md all
document the new flags, the env var, and the agent auth recipe.

Verification

  • Unit: pnpm --filter stash test → 349 passed (15 new for the region module).
  • E2E: pnpm --filter stash test:e2e → 35 passed (6 new non-interactive
    cases via the piped/non-TTY harness; the interactive-cancel test still
    exercises the picker).
  • Lint: pnpm --filter stash lint → clean.
  • Live trigger (real auth server, --no-open, bounded to 12s):
    stash auth login --region us-east-1 --json --no-open emitted a real
    authorization_required event and then blocked on polling exactly as
    designed (only a human can complete it).

Notes for the maintainer

  • No changeset included — a changeset lives in root .changeset/, outside
    packages/cli, and I was asked to confine changes to the CLI. Please add one
    before release:
    ---
    'stash': minor
    ---
    
    Add non-interactive / agent-friendly flags: `--region` / `STASH_REGION` for
    `auth login` and `init`, and `--json` / `--no-open` for `auth login`
    (machine-readable device-code flow). All additive; interactive behaviour is
    unchanged.

Summary by CodeRabbit

  • New Features

    • Added non-interactive support for region selection with a new region flag/environment option.
    • Introduced machine-readable JSON output for auth login and region listing.
    • Added a command to view available auth regions.
  • Bug Fixes

    • Improved behavior in non-TTY and CI environments so commands fail fast with clear messages instead of hanging.
    • Enhanced login flow to support device-code auth with clearer success and error reporting.
  • Documentation

    • Updated CLI help, README, and guidance for non-interactive usage and region selection.

@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f55226a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
stash Minor
@cipherstash/e2e Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@coderdan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f9e7ba67-c9a9-4f99-acdd-1f0360b08875

📥 Commits

Reviewing files that changed from the base of the PR and between fba4b02 and f55226a.

📒 Files selected for processing (27)
  • .changeset/stash-cli-manifest.md
  • .changeset/stash-non-interactive-agent-cli.md
  • packages/cli/AGENTS.md
  • packages/cli/README.md
  • packages/cli/src/bin/main.ts
  • packages/cli/src/cli/__tests__/manifest.test.ts
  • packages/cli/src/cli/manifest.ts
  • packages/cli/src/cli/registry.ts
  • packages/cli/src/commands/auth/__tests__/index.test.ts
  • packages/cli/src/commands/auth/__tests__/login.test.ts
  • packages/cli/src/commands/auth/__tests__/region.test.ts
  • packages/cli/src/commands/auth/events.ts
  • packages/cli/src/commands/auth/index.ts
  • packages/cli/src/commands/auth/login.ts
  • packages/cli/src/commands/auth/region.ts
  • packages/cli/src/commands/index.ts
  • packages/cli/src/commands/init/__tests__/init-command.test.ts
  • packages/cli/src/commands/init/index.ts
  • packages/cli/src/commands/init/steps/authenticate.ts
  • packages/cli/src/commands/init/types.ts
  • packages/cli/src/commands/manifest/index.ts
  • packages/cli/src/config/database-url.ts
  • packages/cli/src/config/tty.ts
  • packages/cli/src/messages.ts
  • packages/cli/tests/e2e/auth-login-cancel.e2e.test.ts
  • packages/cli/tests/e2e/auth-non-interactive.e2e.test.ts
  • packages/cli/tests/e2e/manifest.e2e.test.ts
📝 Walkthrough

Walkthrough

This PR adds non-interactive/agent-friendly behavior to stash init and stash auth login: a --region/STASH_REGION mechanism with CI-aware resolution, NDJSON event emission for device-code login and binding, a new stash auth regions subcommand, plus supporting messages, documentation, and tests.

Changes

Non-interactive CLI region and auth JSON support

Layer / File(s) Summary
Shared CI detection helper
packages/cli/src/config/tty.ts, packages/cli/src/config/database-url.ts
Adds isCiEnv() to parse CI env var, and reuses it in database URL resolution.
Region resolution module
packages/cli/src/commands/auth/region.ts, packages/cli/src/messages.ts, packages/cli/src/commands/auth/__tests__/region.test.ts
Adds normalizeRegion, regionList, selectRegion, resolveRegion, failRegion, new region error messages, and unit tests covering precedence and CI/TTY gating.
JSON NDJSON event emission
packages/cli/src/commands/auth/events.ts, packages/cli/src/commands/auth/login.ts, packages/cli/src/commands/auth/__tests__/login.test.ts
Adds emitJsonEvent/emitJsonError, rewrites login/bindDevice to support json/open options with structured events, and adds tests.
Auth command dispatch
packages/cli/src/commands/auth/index.ts, packages/cli/src/commands/auth/__tests__/index.test.ts
Threads values into authCommand, validates --region, resolves region and forwards options, adds regions subcommand with printRegions, updated help text, and tests.
Init command region threading
packages/cli/src/commands/init/index.ts, packages/cli/src/commands/init/types.ts, packages/cli/src/commands/init/steps/authenticate.ts, packages/cli/src/commands/init/__tests__/init-command.test.ts
Adds regionFlag to InitState, threads --region through initCommand, and switches authenticateStep to use resolveRegion.
CLI entrypoint wiring
packages/cli/src/bin/main.ts
Updates HELP text/examples and wires parsed values into initCommand/authCommand calls.
Docs and E2E tests
packages/cli/README.md, packages/cli/AGENTS.md, .changeset/stash-non-interactive-agent-cli.md, packages/cli/tests/e2e/*
Documents new flags/behaviors and adds/updates end-to-end tests for non-interactive region resolution, regions listing, help text, and cancel-flow CI override.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User as CLI User/Agent
  participant CLI as stash CLI
  participant Region as resolveRegion
  participant Auth as AuthAPI (device code)
  participant Events as events.ts

  User->>CLI: stash auth login --region us-east-1 --json
  CLI->>Region: resolveRegion({regionFlag, json})
  Region-->>CLI: normalized region or failRegion(exit 1)
  CLI->>Auth: beginDeviceCodeFlow(region)
  Auth-->>CLI: pending device code + verification URL
  CLI->>Events: emitJsonEvent(authorization_required)
  CLI->>Auth: poll for authorization
  Auth-->>CLI: authorized token
  CLI->>Events: emitJsonEvent(authorized)
  CLI->>Auth: bindDevice()
  Auth-->>CLI: bound
  CLI->>Events: emitJsonEvent(device_bound)
Loading

Possibly related issues

Possibly related PRs

  • cipherstash/stack#336: Both PRs modify authCommand/login/region handling in packages/cli/src/commands/auth.
  • cipherstash/stack#368: Both PRs modify packages/cli/src/commands/init/steps/authenticate.ts login invocation flow.
  • cipherstash/stack#371: Both PRs modify packages/cli/src/messages.ts for auth/region-picker user-facing text.

Suggested reviewers: auxesis, calvinbrewer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main CLI auth and region flag changes for non-interactive, agent-friendly use.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent-friendly-cli

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderdan coderdan left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage review — PR #569 (non-interactive / agent-friendly auth CLI)

Verdict: Solid coverage on the parts that were factored out to be testable. region.ts (normalizeRegion, regionSlugs, regionList, resolveRegion) is exercised across happy paths, precedence, invalid/missing regions, TTY/CI/json branches — nicely done. The E2E suite covers auth regions, help text, and the pre-network region-resolution failures.

The one real gap is the new JSON-event emission and error-mapping logic in login.ts (login() and bindDevice()). The E2E header explicitly (and reasonably) skips the login happy path because it hits the network — but the JSON serialization, the expiresAt→ISO conversion, and the begin_failed/poll_failed/bind_failed error mapping are all pure logic that can be unit-tested by mocking @cipherstash/auth the same way region.test.ts mocks @clack/prompts (the mock replaces the module before load, so no native binary is pulled into the fast suite). Two inline sketches below cover it.

Additional coverage gaps not posted inline

  • authErrorCode fallback (login.ts:45) — the ?? 'begin_failed' / poll_failed / bind_failed defaults kick in only when the thrown error lacks a .code. The two sketches below should assert both an AuthError-shaped error ({ code: 'INVALID_CLIENT' } → payload code echoes it) and a plain Error (→ the per-call default), so the branch in authErrorCode is actually exercised. Classic lopsided-negative: right now neither the code-present nor code-absent arm is covered.
  • --no-open / openInBrowser() (login.ts:96) — the open flag and the !opened && !json warn branch are untested. Lower value (browser-opening is UI-adjacent), but a mocked openInBrowser: () => false in non-json mode would confirm the warn fires and stays silent in json mode.
  • init --region threading (init/index.ts:80, authenticate.ts:48)initCommand now copies values.region into state.regionFlag, consumed by resolveRegion in the authenticate step. No test asserts this wiring; it's integration-level (full init flow) so likely not worth a unit test, but worth a mention.

No crypto/security concerns noted in the diff.

Comment thread packages/cli/src/commands/auth/login.ts
Comment thread packages/cli/src/commands/auth/login.ts
@coderdan

coderdan commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the code-review findings and the test-gap review in 8b4ff7cd.

Code review fixes

  • --json no longer auto-opens a browser on the agent host — open now defaults to !json; help/README updated.
  • Valueless --region fails with an actionable "needs a value" message instead of a misleading region_required.
  • Empty/whitespace --region falls back to STASH_REGION (matches init's if (values.region) guard).
  • Extracted a shared isCiEnv() into config/tty.ts, deduped against the DATABASE_URL resolver.
  • Removed the login.ts re-export shim; region symbols are imported from region.js directly.
  • Shared the NDJSON error envelope via commands/auth/events.ts (emitJsonEvent / emitJsonError).
  • regionSlugs() now derives from regionList().

Test gaps

  • New login.test.ts covers the login/bindDevice --json paths, the expiresAtexpiresAtIso conversion, the begin_failed/poll_failed/bind_failed mapping, and authErrorCode both with and without .code.
  • Added unit coverage for the empty-flag fallback and e2e coverage for the bare---region guard.

Verification: 360/360 unit tests and 12/12 auth e2e tests pass; typecheck introduces no new errors (the pre-existing @cipherstash/auth@0.41.0 wasm-types errors are unchanged and tracked separately).

@coderdan coderdan marked this pull request as ready for review July 8, 2026 03:27
@coderdan coderdan requested a review from a team as a code owner July 8, 2026 03:27
@coderdan coderdan requested a review from auxesis July 8, 2026 03:28

@auxesis auxesis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test-coverage review (consolidated)

Solid PR. Coverage on the new non-interactive surface is genuinely strong: region.test.ts exercises resolveRegion precedence/gating exhaustively, login.test.ts covers the JSON event stream on both error axes, and the E2E suite guards the anti-hang regression end-to-end. The remaining gaps are all on the interactive (non-json) arms of login.ts and on the command-wrapper plumbing (authCommand / initCommand), neither of which is exercised today. None are correctness bugs — this is a COMMENT, not a change request — but each is a branch a future refactor could silently break.

Both reviewers independently landed on the interactive browser-open gap; the rest are single-source but verified against the diff.

Review stats

Source Raw findings Survived
codex (gpt-5.5) [test-gap] 3 3
claude (claude-opus-4-8) [test-gap] 3 3
  • Kept findings: 5 (the two login.ts:80 findings merged into one)
  • Cross-model corroboration: 1 kept finding (interactive browser-open branch) was raised by 2+ models.
  • Dropped as unverifiable: 0

Comment thread packages/cli/src/commands/auth/login.ts
Comment thread packages/cli/src/commands/auth/login.ts Outdated
Comment thread packages/cli/src/commands/auth/login.ts Outdated
Comment thread packages/cli/src/commands/auth/index.ts
Comment thread packages/cli/src/commands/init/index.ts

@auxesis auxesis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvements, thanks @coderdan.

Have flagged a bunch of test gaps that ideally are fixed before merge, but can be addressed as follow up PRs if this needs to be merged urgently.

coderdan added a commit that referenced this pull request Jul 8, 2026
Addresses 5 gaps Lindsay flagged:
- login: interactive (non-json) browser-open branch — opens once, respects
  open:false, warns when openInBrowser() returns false
- login: non-json rethrow arms for begin + poll failures propagate the original
  error and do NOT call process.exit
- bindDevice: code-present error branch (authErrorCode pass-through), matching
  login's code-present/absent coverage
- authCommand: new index.test.ts asserts values.region / json / open:
  !json && !--no-open / referrer are forwarded into resolveRegion, login,
  bindDevice (+ valueless --region fails fast before login)
- initCommand: new init-command.test.ts asserts state.regionFlag is seeded from
  values.region before the authenticate step runs

login.test.ts's clack mock is hoisted so the spinner + p.log.warn are observable.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/cli/src/commands/auth/region.ts (1)

21-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider making regions immutable.

The regions array is exported and mutable. Since regionList() and regionSlugs() derive from it, accidental mutation would silently corrupt downstream behavior. Adding as const (or at minimum readonly) would prevent this at compile time.

♻️ Optional: harden the regions array
-export const regions = [
+export const regions = [
+  { value: 'us-east-1.aws', label: 'us-east-1 (Virginia, USA)' },
+  { value: 'us-east-2.aws', label: 'us-east-2 (Ohio, USA)' },
+  { value: 'us-west-1.aws', label: 'us-west-1 (California, USA)' },
+  { value: 'us-west-2.aws', label: 'us-west-2 (Oregon, USA)' },
+  { value: 'ap-southeast-2.aws', label: 'ap-southeast-2 (Sydney, Australia)' },
+  { value: 'eu-central-1.aws', label: 'eu-central-1 (Frankfurt, Germany)' },
+  { value: 'eu-west-1.aws', label: 'eu-west-1 (Dublin, Ireland)' },
+] as const
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/auth/region.ts` around lines 21 - 29, The exported
regions list is mutable, which can let accidental changes affect regionList()
and regionSlugs() behavior. Make the regions declaration immutable in region.ts
by using a readonly/const assertion on the regions array so its entries cannot
be modified at compile time, and keep the existing regionList and regionSlugs
helpers reading from that immutable source.
packages/cli/src/commands/auth/login.ts (1)

83-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the warn string to messages.ts — it's asserted in tests.

'Could not open browser — please visit the URL above manually.' is a user-facing string asserted via expect.stringContaining('Could not open browser') in login.test.ts:233-234. Per coding guidelines, assertion-stable user-facing strings should live in src/messages.ts so both production code and tests consume the same copy. As per coding guidelines: "Put assertion-stable user-facing strings in src/messages.ts instead of hard-coding new wording directly in tests; update the constant there so both production code and tests consume the same copy."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/auth/login.ts` around lines 83 - 85, Move the
hard-coded browser warning in login’s warn call to src/messages.ts and have
login.ts reference that shared constant instead. The string is assertion-stable
in login.test.ts, so define it in messages.ts with the existing user-facing
wording, then update the login flow to import and use that message so production
code and tests consume the same copy.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/cli/src/commands/auth/login.ts`:
- Around line 83-85: Move the hard-coded browser warning in login’s warn call to
src/messages.ts and have login.ts reference that shared constant instead. The
string is assertion-stable in login.test.ts, so define it in messages.ts with
the existing user-facing wording, then update the login flow to import and use
that message so production code and tests consume the same copy.

In `@packages/cli/src/commands/auth/region.ts`:
- Around line 21-29: The exported regions list is mutable, which can let
accidental changes affect regionList() and regionSlugs() behavior. Make the
regions declaration immutable in region.ts by using a readonly/const assertion
on the regions array so its entries cannot be modified at compile time, and keep
the existing regionList and regionSlugs helpers reading from that immutable
source.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1f9a7ff-ac45-48d7-8623-08a8c5cfec8b

📥 Commits

Reviewing files that changed from the base of the PR and between 7f67e11 and fba4b02.

📒 Files selected for processing (20)
  • .changeset/stash-non-interactive-agent-cli.md
  • packages/cli/AGENTS.md
  • packages/cli/README.md
  • packages/cli/src/bin/main.ts
  • packages/cli/src/commands/auth/__tests__/index.test.ts
  • packages/cli/src/commands/auth/__tests__/login.test.ts
  • packages/cli/src/commands/auth/__tests__/region.test.ts
  • packages/cli/src/commands/auth/events.ts
  • packages/cli/src/commands/auth/index.ts
  • packages/cli/src/commands/auth/login.ts
  • packages/cli/src/commands/auth/region.ts
  • packages/cli/src/commands/init/__tests__/init-command.test.ts
  • packages/cli/src/commands/init/index.ts
  • packages/cli/src/commands/init/steps/authenticate.ts
  • packages/cli/src/commands/init/types.ts
  • packages/cli/src/config/database-url.ts
  • packages/cli/src/config/tty.ts
  • packages/cli/src/messages.ts
  • packages/cli/tests/e2e/auth-login-cancel.e2e.test.ts
  • packages/cli/tests/e2e/auth-non-interactive.e2e.test.ts

@coderdan

coderdan commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Nice improvements, thanks @coderdan.

Have flagged a bunch of test gaps that ideally are fixed before merge, but can be addressed as follow up PRs if this needs to be merged urgently.

Thanks! All addressed.

coderdan added 7 commits July 8, 2026 15:09
Additive escape hatches so `stash init` and `stash auth login` can run
without a TTY (agents, CI, pipes). Nothing changes for interactive humans —
the region picker still renders in a real terminal.

- `--region <slug>` + `STASH_REGION` on `auth login` and `init` skip the
  interactive region picker. An unknown or missing region in a non-TTY
  context now exits with an actionable message instead of hanging on the
  picker (previously the only prompt with no escape hatch).
- `stash auth login --json` emits newline-delimited device-code events. The
  first event (`authorization_required`) carries the verification URL so an
  agent can *trigger* auth and hand the browser step to a human — only a
  human completes it in the browser. `--no-open` suppresses the browser
  launch for headless contexts.
- Region resolution mirrors the DATABASE_URL resolver (TTY + !CI gate) and
  lives in a native-free module (`src/commands/auth/region.ts`) so its pure
  helpers and policy have fast unit coverage. New E2E covers the
  non-interactive paths; the interactive-cancel E2E now overrides CI to keep
  exercising the picker.

Docs updated in packages/cli README + AGENTS.md.
First-contact affordance so an agent (or human) can discover valid
`--region` / `STASH_REGION` values up front instead of learning them
reactively from an error. Grouped under `auth` (region is an auth concern,
next to `stash auth login`), matching how AWS/gcloud/Fly group region lists
under their domain.

- `stash auth regions` prints the region labels (plain stdout, pipe-friendly).
- `stash auth regions --json` emits `[{ slug, label }]` for agents.
- Backed by a new `regionList()` in the native-free region module; unit + E2E
  covered. Docs + help updated.
Code review fixes:
- --json no longer auto-opens a browser on the agent host (open defaults
  to !json; help/README updated)
- valueless --region now fails with an actionable message instead of a
  misleading region_required
- empty/whitespace --region falls back to STASH_REGION (matches init's guard)
- extract shared isCiEnv() into config/tty.ts (dedup vs database-url resolver)
- remove login.ts re-export shim; import region symbols from region.js directly
- share the NDJSON error envelope via commands/auth/events.ts
- derive regionSlugs() from regionList()

Test gaps:
- new login.test.ts covers login/bindDevice json paths + authErrorCode branches
- unit coverage for the empty-flag fallback; e2e coverage for the bare --region guard
Addresses 5 gaps Lindsay flagged:
- login: interactive (non-json) browser-open branch — opens once, respects
  open:false, warns when openInBrowser() returns false
- login: non-json rethrow arms for begin + poll failures propagate the original
  error and do NOT call process.exit
- bindDevice: code-present error branch (authErrorCode pass-through), matching
  login's code-present/absent coverage
- authCommand: new index.test.ts asserts values.region / json / open:
  !json && !--no-open / referrer are forwarded into resolveRegion, login,
  bindDevice (+ valueless --region fails fast before login)
- initCommand: new init-command.test.ts asserts state.regionFlag is seeded from
  values.region before the authenticate step runs

login.test.ts's clack mock is hoisted so the spinner + p.log.warn are observable.
…-3431)

Phase 1 of docs/plans/cli-help-and-manifest.md: a single source of truth for
command metadata so help/docs can't drift from the real command set.

- src/cli/registry.ts: Flag/CommandDescriptor/CommandGroup types + the full
  command surface (groups, summaries, flags; long/examples for key commands),
  populated from the existing HELP surface and per-command flag parsing.
- src/cli/manifest.ts: buildManifest(version) → { name, version, groups[] },
  the exact contract the docs generator (cipherstash/docs#45) targets. version
  comes from the CLI's package.json.
- `stash manifest --json` emits the structured surface; `stash manifest` prints
  a grouped human-readable list. Pure metadata — no native binary required.

Additive and non-breaking; the top-level HELP string is untouched. Rendering
help from the registry (phases 2–4) is the documented follow-on.

Tests: unit coverage for buildManifest shape/version/hidden-exclusion; e2e for
`manifest --json`, `manifest`, and `--help` listing the command.
Registry-vs-reality fixes (the drift this PR exists to prevent):
- encrypt status/plan: drop --table/--column — both dispatch to zero-arg
  commands (statusCommand()/planCommand()) that ignore them
- eql install: add the real --name/--out flags (forwarded to the drizzle path)
- db migrate: mark summary "(not yet implemented)" and drop the --database-url
  flag it never reads (dispatch only prints a not-implemented warning)

Cleanup / correctness:
- manifest: toManifestCommand now defensively copies examples/flags so the
  manifest can't alias the registry's shared flag singletons (mutation safety)
- registry: extract TABLE/COLUMN/DRY_RUN/EXCLUDE_OPERATOR_FAMILY/SUPABASE_COMPAT
  shared flag consts (were copy-pasted 3-5x), matching the DATABASE_URL_FLAG
  pattern
- manifest: drop the single-use `ManifestFlag = Flag` alias
- soften the "single source of truth" claim (registry docstring + changeset):
  it's additive; HELP stays authoritative for --help until phase 2

Tests:
- buildManifest gains a `groups` injection seam so the hidden-command filter is
  driven by a stub (was vacuously green against the real registry)
- add positive assertions for examples passthrough and the defensive-copy fix
@coderdan coderdan force-pushed the agent-friendly-cli branch from 7b0bf68 to 4a523f1 Compare July 8, 2026 05:15
Rebase follow-up: login.ts was merged onto @cipherstash/auth 0.41's Result API
(begin/poll/openInBrowser/bindClientDevice return { data }/{ failure }, and
non-json failures p.log.error + exit(1) instead of throwing). The login tests
still used the old throwing-API mocks — rewrite them to the Result shape
(failure code from AuthFailure.type) and flip the two non-json cases from
rethrow to error+exit(1).
@coderdan coderdan merged commit 6eddeb4 into main Jul 8, 2026
9 checks passed
@coderdan coderdan deleted the agent-friendly-cli branch July 8, 2026 05:23
@freshtonic freshtonic mentioned this pull request Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants