Skip to content

fix: unblock agent/CI non-interactive CLI paths#187

Closed
nicknisi wants to merge 1 commit into
nicknisi/install-uxfrom
nicknisi/agent-ci-path
Closed

fix: unblock agent/CI non-interactive CLI paths#187
nicknisi wants to merge 1 commit into
nicknisi/install-uxfrom
nicknisi/agent-ci-path

Conversation

@nicknisi

@nicknisi nicknisi commented Jul 8, 2026

Copy link
Copy Markdown
Member

What

  • Bare $0 in non-TTY: npx workos with no args now emits the full machine-readable command tree as JSON on stdout (exit 0) instead of a degenerate two-option help dump — the previous handler called yargs(rawArgs).showHelp() on a fresh yargs instance with no commands registered. Human/TTY mode shows the real configured help via the parser closure.
  • Prompt guard: abortIfCancelled now fails fast with a structured non_interactive_prompt error when prompts are disallowed (agent/ci mode), instead of hanging forever on input that will never come. This is a global invariant: any future unguarded prompt dies loudly.
  • Ambiguous router detection: new --router app|pages flag (flag always wins). Without the flag, non-interactive mode defaults to the app router with a warning instead of blocking on a prompt; human mode prompts as before. Same graceful-default treatment for React Router's ambiguous branches and the upload-env-variables prompt.
  • WORKOS_MODE=ci bridges --ci: the env var now enforces the same validation and downstream behavior as the hidden --ci flag (dirty-tree auto-continue, package-manager auto-select) — previously these were two disconnected notions of "CI".

Why

From the friction log: these are the first things every CI pipeline and AI agent hits. Bare npx workos@latest in a non-TTY shell dead-ended with no discoverable subcommands, and WORKOS_MODE=ci installs still blocked forever on the interactive router prompt.

Testing

  • New subprocess integration spec bin-default-command.integration.spec.ts (mirrors the telemetry integration harness): agent/JSON, CI, WORKOS_FORCE_TTY human, and --help --json regression paths.
  • New unit specs: clack-utils.spec.ts (guard), nextjs/utils.spec.ts + react-router/utils.spec.ts (ambiguous fixtures across human/ci/flag paths), upload-environment-variables/index.spec.ts.
  • Full gate green (2,245 tests at this point in the stack) plus end-to-end smoke tests on the built binary.

Stack 3/7 (akshay-friction-log): merge after #186, then rebase onto main.

Removes the two hard dead-ends an AI agent or CI pipeline hits first when
driving the CLI non-interactively:

- Bare `workos` in a non-TTY shell now emits the machine-readable command
  tree (JSON) or the fully-configured help, instead of a degenerate
  two-line block on stderr with empty stdout. Agents and CI can now
  discover `install`.
- Non-interactive installs no longer hang on interactive prompts.
  `abortIfCancelled` fails fast with a structured `non_interactive_prompt`
  error (Layer 1), and Next.js, React Router, and upload-env apply
  graceful per-site defaults so installs succeed (Layer 2).
- `--router app|pages` deterministically selects the Next.js router,
  winning over detection.

WORKOS_MODE=ci now enforces the same required-arg validation as the hidden
--ci flag and bridges to the downstream `options.ci` paths (git checks
auto-continue, package-manager auto-select). Behavior change: `WORKOS_MODE=ci
workos install` without --api-key/--client-id/--install-dir now errors with
a structured missing_args message instead of falling through to
auto-provisioning.

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread src/utils/clack-utils.ts
Comment on lines +64 to +84
if (!isPromptAllowed()) {
// Never await `input` in non-interactive mode — awaiting a never-resolving
// prompt is exactly the hang this guard fixes. Fail fast with a structured
// error instead. Placed before analytics.shutdown('cancelled') so a
// non-interactive block is not mislabeled as a user cancel.
exitWithError({
code: 'non_interactive_prompt',
message:
`This step requires interactive input${integration ? ` for ${integration}` : ''}, but the CLI is running ` +
`in a non-interactive mode (agent/CI/non-TTY). Pass the required flags (e.g. --router app|pages for Next.js) ` +
`or run in an interactive terminal.`,
recovery: {
hints: [
{
description:
'Re-run in an interactive terminal, or pass the flags that answer this prompt (e.g. --router).',
},
],
},
});
}

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.

🔍 Behavioral change: all unguarded prompts now hard-fail in agent mode instead of clean-exiting

The new guard in abortIfCancelled (src/utils/clack-utils.ts:64-84) changes the behavior for ALL callers that reach it in non-interactive mode. Previously, clack prompts in non-TTY environments would typically resolve with a cancel symbol, causing abortIfCancelled to call process.exit(0) (a clean cancellation). Now, any unguarded prompt path throws CliExit with error code non_interactive_prompt and a non-zero exit code (from resolveErrorCode). This is intentional but affects callers like installPackage (line 317 — the 'already installed, update?' confirm), ensurePackageIsInstalled (line 437), and confirmContinueIfPackageVersionNotSupported (line 253) which don't have their own isPromptAllowed() or options.ci guards. In agent mode, hitting any of these paths will now produce a structured error instead of silently exiting 0.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/utils/clack-utils.ts
});
}

await analytics.shutdown('cancelled');

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.

🔍 Pre-existing: analytics.shutdown('cancelled') runs on every successful abortIfCancelled call

At src/utils/clack-utils.ts:86, await analytics.shutdown('cancelled') executes unconditionally before resolving the input — even on the happy path where the user doesn't cancel. This means analytics is shut down with a 'cancelled' reason on every prompt interaction. This is a pre-existing issue (not introduced by this PR) but is now more visible since the new guard draws attention to this function. It likely works because analytics.shutdown is idempotent or a no-op after the first call, but it's semantically misleading.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes non-interactive CLI runs deterministic. The main changes are:

  • Bare workos in agent/CI paths now emits the command tree as JSON or configured help.
  • Prompt helpers now fail fast when prompts are disallowed.
  • WORKOS_MODE=ci now follows the same install validation and downstream behavior as --ci.
  • Ambiguous Next.js and React Router detection now uses explicit flags or non-interactive defaults.
  • Environment variable upload prompts are skipped in non-interactive mode.
  • New integration and unit tests cover the updated agent/CI paths.

Confidence Score: 5/5

Safe to merge with minimal risk.

The changes are focused on CLI mode handling and prompt fallbacks, with tests covering the main non-interactive paths. No blocking functional or security issues were identified in the reviewed changes.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex generated the CLI default command validation script and used it to exercise the built CLI and validate the JSON shape of its output.
  • The CLI validation log captured the direct command transcript, stdout and stderr, exit codes, and shows that there were no failures.
  • A focused Vitest run for the default command validation completed with all tests passing.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/bin.ts Adds --router install option and changes bare non-interactive $0 to emit JSON command tree or configured help.
src/commands/install.ts Bridges environment-derived CI mode into install validation before invoking the installer.
src/run.ts Propagates environment-derived CI mode and router into downstream installer options.
src/utils/clack-utils.ts Adds global non-interactive prompt guard that emits structured non_interactive_prompt errors.
src/integrations/nextjs/utils.ts Adds --router override handling and non-interactive app-router fallback for ambiguous detection.
src/integrations/react-router/utils.ts Defaults ambiguous non-interactive React Router detection to v7 Framework mode with a warning.
src/steps/upload-environment-variables/index.ts Skips upload-environment-variable prompt in non-interactive mode and records analytics.
src/bin-default-command.integration.spec.ts Adds subprocess coverage for bare non-interactive CLI output, CI detection, forced human help, and --help --json.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User as Agent/CI or Human
participant Bin as src/bin.ts
participant Mode as interaction/output mode
participant Install as install handler
participant Prompt as prompt helpers

User->>Bin: Invoke `workos` or `workos install`
Bin->>Mode: Resolve WORKOS_MODE/CI/non-TTY and output mode
alt bare `$0` and prompts disallowed
    Bin->>User: Emit command tree JSON or configured help
else install path
    Bin->>Install: Pass argv including `router`
    Install->>Mode: "Treat `WORKOS_MODE=ci` like `--ci`"
    Install->>Prompt: Run installer steps
    alt prompt needed but disallowed
        Prompt->>User: Structured `non_interactive_prompt` error or deterministic default/skip
    else human prompt allowed
        Prompt->>User: Interactive selection
    end
end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User as Agent/CI or Human
participant Bin as src/bin.ts
participant Mode as interaction/output mode
participant Install as install handler
participant Prompt as prompt helpers

User->>Bin: Invoke `workos` or `workos install`
Bin->>Mode: Resolve WORKOS_MODE/CI/non-TTY and output mode
alt bare `$0` and prompts disallowed
    Bin->>User: Emit command tree JSON or configured help
else install path
    Bin->>Install: Pass argv including `router`
    Install->>Mode: "Treat `WORKOS_MODE=ci` like `--ci`"
    Install->>Prompt: Run installer steps
    alt prompt needed but disallowed
        Prompt->>User: Structured `non_interactive_prompt` error or deterministic default/skip
    else human prompt allowed
        Prompt->>User: Interactive selection
    end
end
Loading

Reviews (1): Last reviewed commit: "fix: unblock agent/CI non-interactive CL..." | Re-trigger Greptile

@nicknisi

nicknisi commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Closing in favor of a single combined PR from nicknisi/akshay.

@nicknisi nicknisi closed this Jul 8, 2026
@nicknisi nicknisi deleted the nicknisi/agent-ci-path branch July 8, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant