Skip to content

feat: extend the no-terminal guard to every command that prompts - #160

Merged
Bccorb merged 1 commit into
mainfrom
feat/require-tty-everywhere
Aug 1, 2026
Merged

feat: extend the no-terminal guard to every command that prompts#160
Bccorb merged 1 commit into
mainfrom
feat/require-tty-everywhere

Conversation

@Bccorb

@Bccorb Bccorb commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Closes #159. Follow-up to #153 / #158, which shipped the helper and the init coverage.

seamless init stopped rendering prompts to a pipe in #158. Every other command still hung there until its job timed out. This closes that gap and settles the flag convention you picked: --force, with --yes and -y as accepted aliases.

Guarded

Every prompt is now fronted by requireInteractive, so a run without a TTY on stdin fails naming the flag that answers the question:

Command Prompt Answered by
login, profile login Email or phone --identifier, or positional
login, profile login One-time code nothing (see below)
profile add Profile name positional
profile add Instance URL --instance-url
users delete Permanently delete user? --force
users prepare-device-replacement Prepare replacement? --force
sessions revoke --all / current Revoke? --force
org members remove Remove member? --force
config apply Apply N changes? --force
config oauth-providers remove Remove provider? --force

The one-time code is the honest exception: it does not exist until the code is sent, so no flag can supply it ahead of time. That step reports that it genuinely needs a terminal rather than pretending otherwise.

--force

A guard with nothing to point at only converts a hang into a dead end, so the destructive confirmations get a way to be answered. They share confirmDestructive (src/core/confirmAction.ts), which answers itself when forced and otherwise asks.

seamless users delete <id> --force
seamless sessions revoke --all --force
seamless org members remove <orgId> <userId> --force
seamless config apply config.json --force

hasForceFlag also accepts --yes and -y, so config oauth-providers remove --yes keeps working. That subcommand previously wrapped its confirm in if (!skipConfirm); the wrapper is gone and the flag flows through the helper instead.

Two behaviors deliberately preserved: --force does not override --dry-run (config apply --dry-run --force still changes nothing), and cancelling a confirmation reads as declining rather than raising, which is what all six call sites already did.

Tests

The guard would otherwise fail every existing test that exercises a prompt, since vitest has no TTY. Rather than patching each file, src/testSetup.ts marks stdin a TTY before each test, and the no-terminal tests opt back out. That also let me drop the per-file setup #158 added to three test files.

Checks

npm run build and npm test pass (815 passed, 4 skipped). confirmAction.ts and tty.ts are at 100% coverage, as are users.ts, sessions.ts, and org.ts. Verified against the built CLI that login, profile add, and a fully flagged profile add behave correctly on a pipe with the right exit codes, and that the new flags show up in --help.

seamless init stopped rendering prompts to a pipe in #158; every other command
still hung there until its job timed out. Every prompt is now fronted by
requireInteractive, so a run without a TTY on stdin fails naming the flag that
answers the question: login, profile add, users delete,
users prepare-device-replacement, sessions revoke, org members remove,
config apply, and config oauth-providers remove.

A guard with nothing to point at would only convert a hang into a dead end, so
the destructive confirmations now take --force, matching what init means by it.
They share confirmDestructive, which answers itself when forced and otherwise
asks. hasForceFlag also accepts --yes and -y, because
config oauth-providers remove --yes shipped before the convention existed.
--force does not override --dry-run, and cancelling still reads as declining
rather than as an error.

The one-time code at login has no flag form (it does not exist until the code is
sent), so that step reports that it genuinely needs a terminal.

Tests get a setup file that marks stdin a TTY, since the guard would otherwise
fail every test that exercises a prompt; the no-terminal tests opt back out.

Closes #159
@Bccorb
Bccorb merged commit 3b45a45 into main Aug 1, 2026
2 checks passed
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.

feat: extend the no-terminal guard to every command that prompts

1 participant