Skip to content

Guard sources create/update against non-interactive terminals - #155

Merged
grimicorn merged 2 commits into
mainfrom
agent/tty-guard-create-update
Sep 8, 2026
Merged

Guard sources create/update against non-interactive terminals#155
grimicorn merged 2 commits into
mainfrom
agent/tty-guard-create-update

Conversation

@grimicorn-agent

@grimicorn-agent grimicorn-agent commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

What changed

Extends the both-streams TTY guard that sources delete already uses (added in a prior PR) to two more subcommands that can hang or silently no-op under a pipe/cron job:

  • sources create — always prompts for source type/name/route folder/provider, with no non-interactive escape hatch.
  • sources update — always ends by prompting for the route folder, whether the target was resolved from an explicit uuid or picked interactively (no uuid opens a picker first).

Both now fail fast with a clear error under a non-interactive terminal (either stream not a TTY) instead of hanging or being silently swallowed by the ExitPromptError abort path.

Implementation notes

  • The TTY check itself (process.stdin.isTTY && process.stdout.isTTY, computed once in runSourcesCommand) is unchanged and reused as-is — no duplicated TTY-detection logic.
  • Refactored usageErrorFor's single delete-only branch into a small interactiveGuardMessageFor(subcommand, skipConfirm) helper so the !isInteractive condition lives in exactly one place in usageErrorFor, with per-subcommand messaging factored out (rule of three: this is now the third subcommand needing this check).
  • Promoted the 'create'/'update' string literals to CREATE_SUBCOMMAND/UPDATE_SUBCOMMAND constants (matching the existing LIST_SUBCOMMAND/DELETE_SUBCOMMAND pattern), and used them in both the handler map and the new guard messages.
  • sources rotate-secret is deliberately not guarded by this change — see the @todo in src/commands/sources.ts and the follow-up suggestion below. It's out of scope for this issue and left in the same state create/update were left in by the original delete-only guard.

Tests

Added, mirroring delete's existing non-TTY coverage:

  • create refuses on a non-TTY stdin and on a redirected stdout (both fail fast, no prompt rendered, no API call).
  • update refuses on a non-TTY stdin/redirected stdout with no uuid (picker path) and with an explicit uuid (direct route-folder-prompt path), since both end at an unanswerable prompt.
  • A regression test pinning that list (the primary scripted/piped use case) still works over a non-TTY — the widened !isInteractive check must not sweep it in.
  • A regression test pinning that rotate-secret still works by uuid over a non-TTY — documents the current, deliberate gap so a future change can't silently narrow or widen it without a test noticing.

Full suite: 844 tests passing. npm run lint:fix and npm run typecheck both clean.

Independent code review

Ran the required 3-round review loop; findings addressed each round (message specificity, a missing list-on-non-TTY regression test, a stale/optimistic comment about coverage, constant interpolation consistency). See PR comment for the full trail.

Viewable at src/commands/sources.ts (interactiveGuardMessageFor, usageErrorFor) and tests/commands/sources.test.ts.

Closes #148

Follow-up suggestions

  • Guard sources rotate-secret against non-interactive terminals — with no uuid it opens the same kind of picker as update, and a manual-secret (stripe) provider's password prompt also needs a TTY check inside collectRotateInput once the provider is known; both currently hang/no-op the same way delete and now create/update did before their guards (suggested: P3, effort: S, evidence: src/commands/sources.ts rotateSecretCommand/collectRotateInput)

Extends the both-streams TTY guard that sources delete already uses
to sources create (always prompts) and sources update (always ends
by prompting for the route folder, whether the target came from an
explicit uuid or the interactive picker). Both now fail fast with a
clear message under a non-interactive terminal instead of hanging.

Closes #148
…secret gap in tests

- Assert the distinguishing half of each new TTY-guard message
  (create/update) instead of the shared generic substring, so a
  reordered branch can't slip past the tests with misleading advice.
- Interpolate CREATE_SUBCOMMAND/UPDATE_SUBCOMMAND in their own
  messages, matching the delete message's existing pattern.
- Add a regression test pinning that 'list' still works over a
  redirected/non-TTY stream (the primary scripted use case).
- Add a regression test documenting that rotate-secret is
  deliberately left unguarded for now (tracked via @todo + follow-up
  suggestion), so a future change can't silently widen or narrow that
  gap without a test noticing.
- Note the rotate-secret gap in the README so scripters don't assume
  it's covered by this change.
@grimicorn-agent grimicorn-agent added the has-suggestions PR carries follow-up suggestions for the improvement digest label Sep 7, 2026
@grimicorn-agent

Copy link
Copy Markdown
Collaborator Author

Independent code review trail

Ran the required 3-round claude --model opus review loop against git diff origin/main...HEAD.

Round 1 — flagged 4 items:

  1. rotate-secret still hangs/leaks under a pipe, and the guard comment reads as if coverage is complete. Fixed (partially): kept rotate-secret out of scope (it's not in issue Guard create/update against non-interactive terminals #148's stated scope of create/update), but rewrote the comment to explicitly call it out as a deliberate, tracked gap (@todo), documented the gap in the README, and added a regression test pinning current behavior. Filed as a follow-up suggestion (see PR body) rather than silently expanding this PR's scope.
  2. No test pinned that list still passes the widened !isInteractive guard. Fixed: added still lists on a non-TTY (neither stdin nor stdout is a terminal).
  3. README only documented the TTY requirement for delete. Fixed: extended the README row to cover create/update, and added the rotate-secret caveat from finding 1.
  4. Comment on interactiveGuardMessageFor was stale/over-long relative to the three simple branches. Fixed: trimmed and corrected it.

Round 2 — re-flagged the rotate-secret gap plus 3 new items:

  1. rotate-secret gap (see above — same disposition, now with the README caveat it asked for).
  2. The 5 new non-TTY tests all asserted the same generic 'needs an interactive terminal' substring, which doesn't distinguish messages — a reordered branch could ship broken advice with green tests. Fixed: create tests now assert 'always prompts for the source details'; update tests assert 'prompts for the route folder'; also strengthened the two pre-existing delete non-TTY tests to assert '--yes' specifically, since they're now checked against a 3-branch lookup instead of a single condition.
  3. No test pinned that rotate-secret deliberately still works by uuid on a non-TTY. Fixed: added still rotates by uuid on a non-TTY (deliberately unguarded for now).
  4. 'rotate-secret' was the only bare string literal left in the handler map after promoting the other four to constants. Fixed: added ROTATE_SECRET_SUBCOMMAND and used it in the map.

Round 3 — re-flagged the rotate-secret gap again, plus 2 items:

  1. rotate-secret gap — same disposition as rounds 1–2 (deliberately out of scope, now documented via comment + @todo + README + regression test + follow-up suggestion). Not re-actioned as code in this PR.
  2. Suggested an additional list/delete --yes non-TTY regression test. Partially addressed: the list non-TTY test from round 1's fix already covers the primary case this was about; skipped adding a redundant redirected-stdout variant of the pre-existing delete --yes non-TTY test since that code path isn't touched by this diff.
  3. The new create/update guard messages hardcoded `sources create`/`sources update` instead of interpolating the CREATE_SUBCOMMAND/UPDATE_SUBCOMMAND constants, unlike the existing delete message. Fixed: now interpolates both.

Unresolved after 3 rounds: none outstanding as code changes — the one recurring item (rotate-secret) is a deliberate scope decision (issue #148 only asked for create/update), tracked via @todo, README note, a pinning test, and a has-suggestions follow-up for the improvement digest.

Skipped: none beyond the rotate-secret scope call above.

@grimicorn
grimicorn merged commit 030f803 into main Sep 8, 2026
2 of 3 checks passed
@grimicorn
grimicorn deleted the agent/tty-guard-create-update branch September 8, 2026 00:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

has-suggestions PR carries follow-up suggestions for the improvement digest

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Guard create/update against non-interactive terminals

2 participants