Skip to content

feat: non-TTY mode for agent-friendly CLI#75

Open
nicknisi wants to merge 11 commits intomainfrom
nicknisi/non-tty
Open

feat: non-TTY mode for agent-friendly CLI#75
nicknisi wants to merge 11 commits intomainfrom
nicknisi/non-tty

Conversation

@nicknisi
Copy link
Member

Summary

The WorkOS CLI currently assumes a human at a terminal — 26+ interactive prompts (via clack), browser-based OAuth, and chalk-formatted tables make it unusable by coding agents like Claude Code, Codex, or Cursor. Agents must either avoid the CLI entirely or rely on users to copy-paste credentials and command output, defeating the purpose of automation.

This PR adds comprehensive non-TTY support following the gh CLI model: automatic non-TTY detection, structured JSON output, standardized exit codes, and a headless installer adapter. Zero breaking changes for humans — all existing interactive behavior is preserved.

What changed

Core infrastructure

  • Output mode system (src/utils/output.ts) — OutputMode resolved once at startup. Auto-detects non-TTY → JSON. Global --json flag for explicit control. Shared helpers: outputJson(), outputSuccess(), outputError(), outputTable(), exitWithError()
  • Exit codes (src/utils/exit-codes.ts) — Standardized following gh convention: 0=success, 1=general error, 2=cancelled, 4=auth required
  • Environment variablesWORKOS_NO_PROMPT=1 forces non-interactive mode (suppresses all prompts + switches to JSON). WORKOS_FORCE_TTY=1 forces interactive mode even when piped

Management commands (env, organization, user)

  • All commands produce structured JSON when piped or with --json flag
  • Non-interactive guards for env add (requires name + apiKey args) and env switch (requires name arg)
  • List commands return { "data": [...] } with proper empty states
  • Structured JSON errors to stderr with error codes
  • API keys redacted in env list JSON output (only hasApiKey boolean exposed)
  • Added org alias for organization command

Agent-discoverable help

  • --help --json outputs machine-readable command tree (commands, options, positionals, types, defaults)
  • Subcommand scoping: workos env --help --json returns only the env subtree
  • Improved all command descriptions for agent readability

Headless installer

  • HeadlessAdapter (src/lib/adapters/headless-adapter.ts) — third adapter alongside CLI and Dashboard. Subscribes to same state machine events, auto-resolves all interactive decisions with sensible defaults
  • NDJSON streaming (src/utils/ndjson.ts) — progress events streamed as newline-delimited JSON to stdout (detection, auth, git status, agent progress, validation, completion)
  • Flag overrides: --no-branch, --no-commit, --create-pr, --no-git-check
  • --api-key and --client-id now visible flags (were hidden)
  • Removed non-TTY error block in install command — routes to headless adapter instead

Auth improvements

  • Non-TTY auth guard: ensureAuthenticated() exits code 4 instead of opening browser when no TTY
  • WORKOS_API_KEY env var respected for management commands (bypasses OAuth)
  • resolveApiKey() uses structured error output instead of throwing raw errors
  • Stale credential clearing: clearCredentials() called on all refresh failure paths (invalid_grant, network errors, no refresh token). Prevents the loop where expired tokens cause repeated browser opens

Flag naming fix

  • Renamed --no-validate option to validate (default: true) so yargs --no-validate negation works correctly with strict mode. Same for --no-branchbranch, --no-commitcommit, --no-git-checkgit-check. The --no-* flags still work as before from user perspective.

Environment variables

Variable Effect
WORKOS_NO_PROMPT=1 Force non-interactive mode + JSON output
WORKOS_FORCE_TTY=1 Force interactive mode even when piped
WORKOS_API_KEY API key for management commands (bypasses OAuth)

Exit codes

Code Meaning
0 Success
1 General error
2 Cancelled
4 Authentication required

Add foundational layer for non-interactive CLI usage following gh CLI
patterns. Agents and CI pipelines can now use the CLI without TTY.

- Add output mode system (src/utils/output.ts) with JSON/human modes,
  auto-detecting non-TTY and supporting --json global flag
- Add standardized exit codes (0=success, 1=error, 2=cancelled, 4=auth)
- Add WORKOS_NO_PROMPT and WORKOS_FORCE_TTY env var support
- Guard ensure-auth.ts to exit code 4 instead of opening browser in
  non-TTY mode (silent token refresh still works)
- Update handleApiError in org/user commands to use structured errors
- Add ideation specs for remaining phases (management commands, help,
  headless installer)
… installer

Phase 2 — Management commands JSON output:
- env/org/user commands produce structured JSON in non-TTY mode
- Non-interactive guards for env add (requires args) and env switch
  (requires name)
- Empty list states return valid JSON ({ data: [] })

Phase 3 — Agent-discoverable help:
- --help --json outputs machine-readable command tree with types,
  defaults, and positional schemas
- Subcommand scoping (e.g. workos env --help --json)
- Improved all command descriptions for agent readability

Phase 4 — Headless installer with NDJSON streaming:
- HeadlessAdapter auto-resolves all interactive decisions with
  sensible defaults
- Streams progress as NDJSON events to stdout
- Flag overrides: --no-branch, --no-commit, --create-pr, --no-git-check
- --api-key and --client-id now visible flags
- Removed non-TTY error block in install command
When the refresh token is expired or invalidated (e.g., after rotation),
the old credentials remained in the keyring. This caused a loop where
every CLI invocation found dead creds, tried to refresh, failed, and
opened the browser for login.

Now clearCredentials() is called before triggering login on all failure
paths: invalid_grant, network/server errors, no refresh token, and
invalid credentials file. The next run sees "no credentials" and
prompts a clean login instead of retrying with stale tokens.
- env list --json no longer leaks full API keys; outputs hasApiKey and
  hasClientId booleans instead
- WORKOS_FORCE_TTY now requires explicit '1' or 'true' value, matching
  WORKOS_NO_PROMPT behavior (setting WORKOS_FORCE_TTY=false no longer
  incorrectly forces TTY mode)
- Change outputSuccess data param from Record<string, unknown> to
  object, eliminating as unknown as Record<string, unknown> double
  casts in org/user commands
- Remove redundant section divider comment in headless-adapter
- Extract duplicated handleApiError from organization.ts and user.ts
  into shared createApiErrorHandler() factory in lib/api-error-handler.ts
- Cache hideBin(process.argv) result to avoid double-parsing in bin.ts
- Simplify flag inversion mapping in run.ts (remove verbose ternaries)
- Lazy-import HeadlessAdapter in run-with-core.ts to avoid loading it
  in interactive mode
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