WorkOS CLI for installing AuthKit integrations and managing WorkOS resources (organizations, users, environments).
- Three adapters subscribe to
InstallerEventEmitterstate machine events, chosen byselectInstallerAdapter(): Headless in JSON mode; the full-screen TUI (src/tui/, Ink) for a human at an interactive terminal of at least 80×24 unless--no-tui; CLI otherwise. The TUI adapter wraps the CLI adapter and redirectsuioutput/prompts viasetUiHost(), so prompt logic lives in one place - Full-screen installer copy (tips, announcements, walkthrough, task labels) is data in
src/tui/content/installer-content.json, validated by tests OutputMode(human/json) resolved once at startup inbin.ts, drives all formattinginstallerCanUseTool()inagent-interface.tsrestricts Bash to safe commands only- Config/credentials stored in system keyring with file fallback
- Output: Auto-switches to JSON when piped or
--jsonflag.WORKOS_FORCE_TTY=1overrides. - Auth: Exits code 4 instead of opening browser. Resource commands (organization, user, role, permission, membership, invitation, session, event, feature-flag, org-domain, portal, webhook, config) use the dashboard session from a prior
workos auth login; expired access tokens refresh silently while the stored refresh token is valid, so only a truly dead session exits 4.WORKOS_API_KEYapplies only toworkos apiand the still-REST commands (connection,directory,audit-log,api-key,vault, plus the workflow/debug commandsseed,setup-org,onboard-user,verify-login,debug-sso,debug-sync,migrations). - Errors: Structured JSON to stderr:
{ "error": { "code": "...", "message": "..." } } - Exit codes: 0=success, 1=error, 2=cancelled, 4=auth required (follows
ghCLI convention) - Headless flags:
--no-branch,--no-git-check. CI mode (WORKOS_MODE=ci) auto-continues past a dirty tree without--no-git-check; agent mode requires the flag. - Installer Git policy: Generated changes stay unstaged/uncommitted; pre-existing staging is preserved. No installer-controlled staging, commits, pushes, PR creation, or commit/PR text generation.
--commit,--no-commit, and--create-prare deprecated compatibility-only no-ops, with human-only notices. Branch prompts/--no-branchremain unchanged; branches do not isolate uncommitted work. Change reporting is scoped toinstallDirand may include pre-existing changes.
--json output is a public API: users script against it with jq and in CI. The
backend's vocabulary is an implementation detail and must never leak through
untranslated, or the next backend migration becomes another user-visible break.
Route every enum and metadata field through src/utils/output-conventions.ts
rather than hand-normalizing per command.
- Keys are camelCase.
- Enum values are lowercase. Backends emit assorted casings (
Verified,PENDING,Active); the CLI emits one convention. UseenumOut(). - Enum input is case-insensitive. Whatever the CLI prints for a field it
accepts for that field ("forgiving in, canonical out"). Use
enumIn(). stateis the lifecycle-state key on every resource, notstatus.metadatais an object map, not GraphQL's array of pairs, so.metadata.fooresolves in jq. UsemetadataToMap().- Internal/backend-only fields are dropped from curated shapes.
When a spec mocks a backend response, feed it the backend's real casing
('Verified') and assert the lowercase output. A mock that feeds already-correct
values never exercises the normalization, which is exactly how a casing bug
shipped once already.
scripts/parity-smoke.ts compares this branch against ../main and fails on any
unexpected field divergence. Its ACCEPTED map lists deliberate curations only;
a casing-only difference appearing there is a bug, not an accepted divergence.
- Bun only; the shipped CLI is a Bun-compiled standalone binary
- Runtime assets must be statically imported or materialized from the compiled binary. Exception: the Agent SDK
claudeexecutable is downloaded on first agent use — pinned by version + sha256 in the generated manifest — and cached under~/.workos/cache/agent-sdk/
Conventional Commits — release-please auto-generates changelog. Use ! suffix for breaking changes (e.g., feat!:).
bun run build # Build the standalone binary
bun run dev # Run source in watch mode
bun run test # Run tests
bun run typecheck # Type check- Create
src/integrations/{framework}/index.tsexportingconfigandrun - Run
bun run generateto refreshsrc/integrations/_manifest.ts - Add or update detection and validation tests for the integration
- Create
src/commands/{resource}.ts+{resource}.spec.ts(follow patterns inorganization.ts) - Register in
src/bin.tsand updatesrc/utils/help-json.tscommand registry - Include JSON mode tests in spec file
All commands automatically emit a command telemetry event with name, duration, and success/failure. The centralized lifecycle in bin.ts (runCli()) handles this — no manual wrapping required.
Subcommands via registerSubcommand() — auto-tracked. Just write the handler:
.command('user', 'Manage users', (yargs) => {
registerSubcommand(yargs, 'reset-password', '...', (y) => y,
async (argv) => { await runResetPassword(argv); },
);
})Top-level .command() with inline handler — also auto-tracked:
.command(
'migrate',
'Migrate from another provider',
(yargs) => yargs.options({...}),
async (argv) => {
await runMigrate(argv);
},
)Exiting with errors: Use exitWithError() or exitWithCode() from handlers — they throw CliExit which the lifecycle catches, classifies, and records.
Skip list: Commands in SKIP_TELEMETRY_COMMANDS (command-telemetry.ts) are excluded from command-level telemetry because they have their own session-based telemetry. Currently: install, root (the default $0 handler).
Aliases: if you register a command with multiple names (e.g., ['organization', 'org']), add the alias to src/lib/command-aliases.ts so metrics don't fragment.
Do:
- Follow the adapter pattern (
CLI,TUI,Headless) insrc/integrations/when adding framework installers - Use
InstallerEventEmitterfor state machine events -- see existing adapters for examples - Add both human and JSON output modes -- check
OutputModeusage insrc/bin.ts - Follow existing command patterns in
src/commands/organization.tswhen adding resource commands - Write
.spec.tstests alongside every command file
Don't:
- Use Node-specific sync APIs (crypto, fs sync) unless necessary
- Add runtime filesystem discovery or import.meta.url-relative package asset reads
- Skip JSON mode tests in spec files
- Forget to wire up new frameworks in
src/run.tsswitch statement
-
bun run buildpasses -
bun run testpasses -
bun run typecheckpasses - Conventional Commit message format used (
feat:,fix:,feat!:for breaking) - New commands include JSON mode support and tests