src/
├── bin.ts # CLI entry point (yargs command routing)
├── cli.config.ts # App configuration (model, URLs)
├── run.ts # Installer orchestration entry point
├── lib/
│ ├── agent-runner.ts # Core agent execution
│ ├── agent-interface.ts # Claude Agent SDK interface
│ ├── installer-core.ts # Headless installer core (XState)
│ ├── config.ts # Framework detection config
│ ├── constants.ts # Integration types, shared constants
│ ├── credential-store.ts # OAuth credential storage (keyring + file fallback)
│ ├── config-store.ts # Environment config storage (keyring + file fallback)
│ ├── api-key.ts # API key resolution (env var → flag → config)
│ ├── workos-api.ts # Generic WorkOS REST API client
│ ├── credential-proxy.ts # Token refresh proxy for long sessions
│ ├── ensure-auth.ts # Startup auth guard
│ └── adapters/ # CLI, full-screen (TUI), and headless adapters
├── commands/
│ ├── env.ts # workos env (add/remove/switch/list)
│ ├── organization.ts # workos organization (create/update/get/list/delete)
│ ├── user.ts # workos user (get/list/update/delete)
│ ├── install.ts # workos install
│ ├── install-skill.ts # workos install-skill
│ ├── auth-status.ts # workos auth status
│ ├── login.ts # workos auth login
│ └── logout.ts # workos auth logout
├── tui/ # Full-screen installer (Ink/React)
│ ├── content/ # Swappable tips, news, walkthrough copy (see README there)
│ └── model/ # Event-driven run model (no Ink)
├── nextjs/ # Next.js installer agent
├── react/ # React SPA installer agent
├── react-router/ # React Router installer agent
├── tanstack-start/ # TanStack Start installer agent
├── vanilla-js/ # Vanilla JS installer agent
└── utils/
├── table.ts # Terminal table formatter
├── clack-utils.ts # CLI prompts
├── debug.ts # Logging with redaction
├── redact.ts # Credential redaction
└── ... # Additional utilities
# Install dependencies
bun install
# Build
bun run build# Run the TypeScript source in watch mode
bun run dev
# Test installer in another project
cd /path/to/test/nextjs-app
workos install
# Test management commands
workos env add sandbox sk_test_xxx
workos organization list
workos user list# Build
bun run build
# Clean and rebuild
bun run clean && bun run build
# Format code
bun run format
# Check types
bun run typecheck
# Run tests
bun run test
bun run test:watch- Target: ES2022
- Module: NodeNext (ESM)
- Strict mode enabled
The CLI separates two axes:
| Axis | Question | API |
|---|---|---|
| Output mode | How should output be formatted? | isJsonMode() from src/utils/output.ts |
| Interaction mode | Who is driving the CLI? | isHumanMode(), isAgentMode(), isCiMode(), isPromptAllowed() from src/utils/interaction-mode.ts |
Guidelines for new code:
- Use
isJsonMode()only to choose between structured JSON and human-formatted output. Do not use it to decide whether to prompt, open a browser, or skip a confirmation. - Use
isPromptAllowed()(==isHumanMode()) before any clack prompt or interactive flow. - Use
isAgentMode()to add agent-specific recovery hints, manual-fallback wording, or host-execution warnings. - Use
isCiMode()to refuse browser-based flows and to prefer terse failures over recovery handoff text. - For destructive operations, require an explicit
--yes/--forceflag whenever!isPromptAllowed()regardless of output mode. - For
auth_requiredand other deterministic failures, attach recovery metadata viasrc/utils/recovery-hints.tsso agents can parseerror.recovery.hints[].
Mode resolution — do not regress these:
WORKOS_MODE=agentmaps to agent interaction behavior and JSON output (viaresolveEffectiveOutputMode). The oldWORKOS_NO_PROMPTalias has been removed.WORKOS_FORCE_TTY=1only affects output mode (forces human). It must not change interaction mode.- Non-TTY stdout still defaults output to JSON and interaction to agent.
isNonInteractiveEnvironment()fromsrc/utils/environment.tsis a thin wrapper over!isHumanMode()kept for backward compatibility. Prefer the explicit interaction-mode predicates in new code.
The full backwards-compat matrix lives in src/utils/mode-compatibility.spec.ts.
- Create
src/integrations/your-framework/index.ts - Export a
FrameworkConfigasconfigand an installer function asrun - Run
bun run generateto refresh the static integration manifest - Add detection and validation coverage
See src/integrations/nextjs/index.ts as a reference.
bun run generate produces three manifests (it runs automatically before
build, test, and typecheck, and after bun install):
src/integrations/_manifest.ts— static imports for every integration (committed; CI fails if it drifts from the directory listing)src/generated/skills-manifest.ts— embeds every file of the@workos/skillsplugin tree into the binary (gitignored: contains absolute paths)src/generated/agent-sdk-manifest.ts— pins the target platform's native Claude Agent SDKclaudeexecutable: version, npm tarball URL, and sha256 (gitignored: target-specific). The executable is not embedded; a compiled binary downloads it on first agent use, verifies the checksum, and caches it under~/.workos/cache/agent-sdk/
Set WORKOS_BUILD_TARGET (e.g. bun-linux-x64-baseline) to generate/build
for a non-host platform; the same value must be used for both generate and
the compile, which bun run build (via scripts/build.ts + the prebuild
hook) guarantees.
Nothing in src/ imports react-devtools-core, but it is required to
build, not to run. The full-screen installer (src/tui/) uses ink, whose
reconciler does a runtime-gated await import('./devtools.js') that only fires
when DEV=true; devtools.js then statically imports react-devtools-core.
bun build --compile follows that static import at bundle time and cannot prove
the DEV branch is dead, so removing the devDependency fails the compile with
error: Could not resolve: "react-devtools-core" (do not "clean it up"). Do
not try to trim it with --external react-devtools-core: that compiles, but
bun resolves the external eagerly and the standalone binary then crashes on
every command (even --version) with Cannot find package 'react-devtools-core'.
The installer prompt in agent-runner.ts tells Claude to:
- Fetch live docs from workos.com
- Fetch SDK README from GitHub/npm
- Follow official documentation
To change instructions, edit buildIntegrationPrompt() in lib/agent-runner.ts.
Credential redaction is in utils/redact.ts. Add patterns:
export function redactCredentials(obj: any): any {
// Add new patterns here
const redacted = JSON.stringify(obj).replace(/sk_test_[a-zA-Z0-9]+/g, (match) => `sk_test_...${match.slice(-3)}`);
return JSON.parse(redacted);
}Manual testing:
- Run installer in a test app:
workos install(full-screen), andworkos install --no-tui(plain) - Check logs at
~/.workos/logs/workos-{timestamp}.log - Verify integration works in test app
Full-screen installer: the view lives in src/tui/. Its text (tips, news,
walkthrough copy, task labels) is data in src/tui/content/installer-content.json;
edit that and run bun run test src/tui/content. The adapter
(src/lib/adapters/tui-adapter.ts) wraps the CLI adapter, so prompt behavior
never forks: it routes ui output and prompts into the view through
setUiHost(). Render tests draw into fake streams
(src/tui/ink-streams.test-utils.ts); check a real terminal at about 80×24 and
in a large window, and confirm the terminal is restored after success, failure,
and ctrl-c.
What to test:
- Framework detection
- API key masking (should show
*****) - Log redaction (keys show as
sk_test_...X6Y) - SDK installation
- File creation
- Environment variables
- UI components
Smoke testing the npm distribution:
scripts/npm-dist-smoke.ts publishes the generated npm packages to a
throwaway local registry (Verdaccio) and drives the real user flows —
npx workos, npm install -g workos, platform selection, and the
launcher's no-binary error path — from a hermetic environment (fresh
HOME/cache/prefix, sanitized PATH, no uplinks so nothing can leak to the
real registry). CI runs it on every PR and the release pipeline runs it as a
pre-publish gate. Locally:
bun run build
bun run ./scripts/npm-dist-smoke.ts # generates a host-only dist/npm if absentSmoke testing the command contract:
scripts/command-smoke.sh executes real commands against a compiled binary
and asserts the non-TTY contract: exit codes (0 success, 1 error, 4 auth
required), structured JSON errors on stderr, and JSON output. It is
offline-safe — CI runs it in a --network none container, and the release
pipeline runs it against every platform binary on native hardware. Locally:
bun run build
sh scripts/command-smoke.sh ./dist/workosWith WORKOS_API_KEY set to a staging-environment key, it also runs an
authenticated section (organization list + create → get → delete round-trip).
CI provides this via the WORKOS_SMOKE_API_KEY repository secret; fork PRs
receive no secrets and skip it.
Automated eval framework for testing installer skills across frameworks and project states.
bun run eval # Run all scenarios
bun run eval --framework=nextjs # Single framework
bun run eval --quality # Include LLM quality grading
bun run eval:history # List recent runs
bun run eval:diff <id1> <id2> # Compare runsSee tests/evals/README.md for full documentation.
Verbose logs:
workos --debugCheck logs:
tail -f ~/.workos/logs/workos-{timestamp}.logReleases are fully automated via release-please + GitHub Releases (there is no npm publish; users download platform binaries):
- Merging to
mainupdates the release-please PR; merging that PR creates a draft GitHub release and pushes its tag immediately (force-tag-creation). release.ymlcross-compiles all eight platform binaries (macOS arm64/x64, Linux glibc + musl on x64/arm64, Windows x64/arm64), smoke tests each one on native hardware for its platform (includingworkos internal verify-assets, which checks the keyring native binding loaded, downloads the pinned Agent SDK executable, verifies its checksum, and spawns it), attaches them to the draft, and only then publishes it.releases/latestnever points at a partial or untested release.- After the GitHub release publishes,
publish-npmregenerates the npm distribution (scripts/gen-npm-packages.ts): a thinworkoslauncher package plus one@workos/cli-<platform>-<arch>package per binary, published via npm trusted publishing (OIDC). npm is a secondary channel — it never leads GitHub Releases, and a failed npm publish is re-runnable in isolation.
Operational notes:
- A leg failed: the release stays an invisible draft and the previous
release remains
latest. Fix the problem, then either "Re-run failed jobs" on the same run or triggerrelease.ymlmanually viaworkflow_dispatchwith the tag name. - Abandoning a draft: delete BOTH the draft release and its tag, or release-please will treat that version as shipped.
- Bumping the pinned Bun version (
ci.yml,release.yml,packageManager) is a smoke-gated event: Bun has shipped releases that broke cross-compiled macOS code signatures (oven-sh/bun#29120 — binaries SIGKILL on Apple Silicon). The native macOS smoke leg is the regression gate; never bypass it.
See README for user-facing docs.