Skip to content

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

Description

@Bccorb

Problem

#153 added requireInteractive (src/core/tty.ts) and wired it through every
prompt in the init flow, so a run with no TTY on stdin fails naming the flag that answers the
question. It was deliberately scoped to init. Every other command that prompts still does the old
thing: renders a prompt nobody can answer and waits forever, so a CI step or a scripted run hangs
until its job times out instead of failing.

The prompts

Where Prompt Kind
src/core/interactiveLogin.ts:29 "Email or phone" input
src/core/interactiveLogin.ts:48 "Enter the code we sent you" input
src/commands/profile.ts:85 "Profile name" input
src/commands/profile.ts:105 "Instance URL" input
src/commands/users.ts:84 "Permanently delete user ...?" destructive confirm
src/commands/users.ts:161 "Prepare device replacement for ...?" destructive confirm
src/commands/sessions.ts:52 "Revoke every session, including this one?" destructive confirm
src/commands/sessions.ts:86 "This is your current session..." destructive confirm
src/commands/org.ts:249 "Remove user ... from organization ...?" destructive confirm
src/commands/config.ts:183 "Apply N changes to ...?" destructive confirm
src/commands/config.ts:317 "Remove OAuth provider ...?" destructive confirm

config oauth-providers remove already takes --yes; nothing else does.

(src/prompts/oauthSetup.ts also prompts, but its only caller is init, which already guards it.)

Change

Two halves, and the second is the design question.

1. Guard every call site. Front each prompt with requireInteractive(question, remedy), the way
the init flow does. This alone converts a hang into an immediate non-zero exit with a message,
which is the bulk of the value and carries no behavioral risk for interactive use.

2. Give the confirms a way to be answered. A guard with no remedy to point at is only half an
answer: it still means the command cannot run unattended. The inputs mostly have flags already
(login --identifier, profile add --instance-url), so their remedy is just naming those. The
destructive confirms mostly do not.

The decision worth making deliberately: which of those confirms should take a skip flag at all.
init settled on the rule that --yes answers ordinary questions and --force is required for
anything destructive, and these are all in the second category. Options:

  • Standardize on --force for the destructive confirms, keeping --yes for ordinary questions, so
    the rule matches init. config oauth-providers remove --yes already ships, so it would need an
    alias or a deprecation.
  • Standardize on --yes everywhere, matching the flag that already exists, and accept that it
    diverges from what init means by --yes.
  • Guard only, and add no skip flags: deleting a user or revoking every session stays something you
    cannot do unattended, by design.

Worth deciding before the implementation, since it sets the convention for every command added
later. My inclination is the first, with --yes kept as an accepted alias on
config oauth-providers remove so nothing breaks.

3. Emit the width warning (warnOnUnusableWidth) wherever prompts are about to run, not just in
init.

Acceptance

  • Every prompt in the table refuses to render without a TTY and exits non-zero with a message.
  • Each message names the flag or the alternative that answers the question, once [Feature]: Create Seamless Init #2 is settled.
  • A run that supplies every answer by flag behaves the same on a pipe as on a terminal.
  • Interactive runs are unchanged.
  • AGENTS.md drops the "not guarded yet" caveat once this lands.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions