Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b5cfff2
feat: allow overriding CLI auth client ID via WORKOS_CLIENT_ID
nicknisi Jun 15, 2026
6941e43
feat: point the CLI at a non-prod API via WORKOS_API_URL
nicknisi Jun 22, 2026
98a28a4
feat: add `workos whoami` command
nicknisi Jun 23, 2026
db29bbb
feat: vendor Management catalog snapshot + loader
nicknisi Jun 25, 2026
e88d3fe
feat(catalog): add justification manifest, curation, and reused confi…
nicknisi Jun 25, 2026
c1e491c
feat: add account-plane lifecycle commands (environment, project, team)
nicknisi Jun 25, 2026
d018558
feat: add authkit app config commands (redirect URIs, CORS, logout UR…
nicknisi Jun 25, 2026
e167c14
docs(catalog): note deferred userland->user ops and retained test fix…
nicknisi Jul 10, 2026
368d05b
feat: command-path auth guard with silent token refresh
nicknisi Jul 22, 2026
76a9a93
feat: target the active environment on dashboard-plane commands
nicknisi Jul 22, 2026
d398bc9
feat!: migrate organization + user commands to the dashboard account …
nicknisi Jul 22, 2026
331b084
feat!: migrate membership, invitation, session to the dashboard accou…
nicknisi Jul 22, 2026
65de301
feat!: migrate role, permission, feature-flag to the dashboard accoun…
nicknisi Jul 23, 2026
1e55062
feat!: migrate event + org-domain to the dashboard account plane; dir…
nicknisi Jul 23, 2026
5a0edeb
feat!: migrate webhook, portal, config to the dashboard account plane
nicknisi Jul 23, 2026
43e8434
chore: remove dead REST-plane code and align docs with dashboard auth
nicknisi Jul 23, 2026
9e198c1
test: add live smoke script for dashboard-plane resource commands
nicknisi Jul 23, 2026
64fb027
chore: formatting
nicknisi Jul 23, 2026
77a06ef
chore: remove unused declarations flagged by oxlint
nicknisi Jul 23, 2026
725b86a
chore: formatting
nicknisi Jul 23, 2026
d86c6e0
fix: address review feedback on client-id override, 403 handling, and…
nicknisi Jul 23, 2026
cf00bdd
refactor: extract dashboard-operation executor seam and shared resour…
nicknisi Jul 23, 2026
ef6139f
feat: add branding image upload to the CLI
nicknisi Jul 25, 2026
fecd0ca
fix: point remaining clack imports at the ui facade
nicknisi Jul 25, 2026
dd18ef7
feat!: move branding to a top-level command with a positional form
nicknisi Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Typecheck
run: pnpm typecheck

- name: Justification check
run: pnpm justification:check

- name: Build
run: pnpm build

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ WorkOS CLI for installing AuthKit integrations and managing WorkOS resources (or
## Non-TTY Behavior

- **Output**: Auto-switches to JSON when piped or `--json` flag. `WORKOS_FORCE_TTY=1` overrides.
- **Auth**: Exits code 4 instead of opening browser. Requires prior `workos auth login` or `WORKOS_API_KEY` env var.
- **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_KEY` applies only to `workos api` and the still-REST commands (`connection`, `directory`, `audit-log`, `api-key`, `vault`, plus the workflow/debug commands `seed`, `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 `gh` CLI convention)
- **Headless flags**: `--no-branch`, `--no-commit`, `--create-pr`, `--no-git-check`. CI mode (`WORKOS_MODE=ci`) auto-continues past a dirty tree without `--no-git-check`; agent mode requires the flag.
Expand Down
97 changes: 55 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Workflows:
debug-sync Diagnose directory sync issues
```

All management commands support `--json` for structured output (auto-enabled in non-TTY) and `--api-key` to override the active environment's key.
All management commands support `--json` for structured output (auto-enabled in non-TTY). Still-REST commands (`connection`, `directory`, `audit-log`, `api-key`, `vault`, `workos api`, and the workflow/debug commands) also accept `--api-key` to override the active environment's key; the other resource commands authenticate with your `workos auth login` session (see [Resource Management](#resource-management)).

### Unclaimed Environments

Expand Down Expand Up @@ -255,7 +255,11 @@ API keys are stored in the system keychain via `@napi-rs/keyring`, with a JSON f

### Resource Management

All resource commands follow the same pattern: `workos <resource> <action> [args] [--options]`. API keys resolve via: `--api-key` flag → `WORKOS_API_KEY` env var → active environment's stored key.
All resource commands follow the same pattern: `workos <resource> <action> [args] [--options]`.

Most resource commands — organization, user, role, permission, membership, invitation, session, event, feature-flag, org-domain, portal, webhook, config — authenticate with your WorkOS dashboard session from `workos auth login` and target the active environment (see `workos env`). Pass `--environment-id <id>` on any of them to target a different environment for a single invocation. Access tokens refresh automatically while the session is valid, so a logged-in machine keeps working headlessly; a dead session exits with code 4.

The remaining commands (`connection`, `directory`, `audit-log`, `api-key`, `vault`) still use the REST plane, as does the raw escape hatch `workos api`. Those resolve an API key via: `--api-key` flag → `WORKOS_API_KEY` env var → active environment's stored key.

#### organization

Expand All @@ -264,68 +268,72 @@ workos organization create <name> [domain:state ...]
workos organization update <orgId> <name> [domain] [state]
workos organization get <orgId>
workos organization list [--domain] [--limit] [--before] [--after] [--order]
workos organization delete <orgId>
workos organization delete <orgId> [--yes]
```

#### user

```bash
workos user get <userId>
workos user list [--email] [--organization] [--limit]
workos user update <userId> [--first-name] [--last-name] [--email-verified] [--password] [--external-id]
workos user delete <userId>
workos user list [--email] [--limit] [--before] [--after] [--order]
workos user update <userId> [--first-name] [--last-name] [--email] [--locale] [--external-id]
workos user delete <userId> [--yes]
```

#### role

Role mutations change the privilege surface; non-interactive callers must pass `--yes`.

```bash
workos role list [--org <orgId>]
workos role get <slug> [--org <orgId>]
workos role create --slug <slug> --name <name> [--org <orgId>]
workos role update <slug> [--name] [--description] [--org <orgId>]
workos role delete <slug> --org <orgId>
workos role set-permissions <slug> --permissions <slugs> [--org <orgId>]
workos role add-permission <slug> <permissionSlug> [--org <orgId>]
workos role remove-permission <slug> <permissionSlug> --org <orgId>
workos role create --slug <slug> --name <name> [--description] [--org <orgId>] [--yes]
workos role update <slug> [--name] [--description] [--org <orgId>] [--yes]
workos role delete <slug> --org <orgId> [--yes]
workos role set-permissions <slug> --permissions <slugs> [--org <orgId>] [--yes]
workos role add-permission <slug> <permissionSlug> [--org <orgId>] [--yes]
workos role remove-permission <slug> <permissionSlug> --org <orgId> [--yes]
```

#### permission

```bash
workos permission list [--limit]
workos permission list
workos permission get <slug>
workos permission create --slug <slug> --name <name> [--description]
workos permission update <slug> [--name] [--description]
workos permission delete <slug>
workos permission create --slug <slug> --name <name> [--description] [--yes]
workos permission update <slug> [--name] [--description] [--yes]
workos permission delete <slug> [--yes]
```

#### membership

`--org` and `--user` are mutually exclusive on `list`; pagination flags apply to `--org` listings only.

```bash
workos membership list [--org] [--user] [--limit]
workos membership list (--org <orgId> | --user <userId>) [--limit] [--before] [--after] [--order]
workos membership get <id>
workos membership create --org <orgId> --user <userId> [--role]
workos membership update <id> [--role]
workos membership delete <id>
workos membership deactivate <id>
workos membership update <id> [--role] [--yes]
workos membership delete <id> [--yes]
workos membership deactivate <id> [--yes]
workos membership reactivate <id>
```

#### invitation

```bash
workos invitation list [--org] [--email] [--limit]
workos invitation list [--org] [--email] [--limit] [--before] [--after]
workos invitation get <id>
workos invitation send --email <email> [--org] [--role] [--expires-in-days]
workos invitation revoke <id>
workos invitation revoke <id> [--yes]
workos invitation resend <id>
```

#### session

```bash
workos session list <userId> [--limit]
workos session revoke <sessionId>
workos session list <userId> [--limit] [--before] [--after]
workos session revoke <sessionId> [--yes]
```

#### connection
Expand All @@ -349,7 +357,7 @@ workos directory list-groups --directory <id> [--limit]
#### event

```bash
workos event list --events <types> [--org] [--range-start] [--range-end] [--limit]
workos event list --events <types> [--range-start] [--range-end] [--after] [--limit]
```

#### audit-log
Expand All @@ -366,7 +374,7 @@ workos audit-log get-retention <orgId>
#### feature-flag

```bash
workos feature-flag list [--limit]
workos feature-flag list [--limit] [--before] [--after] [--order]
workos feature-flag get <slug>
workos feature-flag enable <slug>
workos feature-flag disable <slug>
Expand All @@ -379,7 +387,7 @@ workos feature-flag remove-target <slug> <targetId>
```bash
workos webhook list
workos webhook create --url <endpoint> --events <types>
workos webhook delete <id>
workos webhook delete <id> [--yes]
```

#### config
Expand All @@ -393,9 +401,11 @@ workos config homepage-url set <url>
#### portal

```bash
workos portal generate-link --intent <intent> --org <orgId> [--return-url] [--success-url]
workos portal generate-link --intent <intent> --org <orgId>
```

Supported intents: `sso`, `dsync`, `log_streams`, `domain_verification`, `certificate_renewal`. Generating a link expires prior links of the same intent.

#### vault

```bash
Expand Down Expand Up @@ -425,7 +435,7 @@ workos api-key delete <id>
workos org-domain get <id>
workos org-domain create <domain> --org <orgId>
workos org-domain verify <id>
workos org-domain delete <id>
workos org-domain delete <id> [--yes]
```

### Installer Options
Expand Down Expand Up @@ -464,11 +474,11 @@ mkdir my-app && cd my-app && npx workos@latest install
# With visual dashboard (experimental)
npx workos@latest dashboard

# JSON output (explicit)
workos org list --json --api-key sk_test_xxx
# JSON output (explicit; requires a prior `workos auth login`)
workos org list --json

# Pipe-friendly (auto-detects non-TTY)
workos org list --api-key sk_test_xxx | jq '.data[].name'
workos org list | jq '.data[].name'

# Machine-readable command discovery
workos --help --json | jq '.commands[].name'
Expand All @@ -494,7 +504,7 @@ The CLI also auto-detects non-TTY environments (piped output, CI, coding agents)
All commands produce structured JSON when piped or with `--json`:

```bash
workos org list --api-key sk_test_xxx | jq .
workos org list | jq .
# → { "data": [...], "list_metadata": { "before": null, "after": "..." } }

workos env list --json
Expand All @@ -504,8 +514,8 @@ workos env list --json
Errors go to stderr as structured JSON:

```bash
workos org list 2>&1
# → { "error": { "code": "no_api_key", "message": "No API key configured..." } }
workos org list 2>&1 # not logged in → exit code 4
# → { "error": { "code": "auth_required", "message": "Not logged in..." } }
```

### Agent Mode
Expand Down Expand Up @@ -555,13 +565,16 @@ workos install --api-key sk_test_xxx --client-id client_xxx --no-commit 2>/dev/n

### Environment Variables

| Variable | Effect |
| ------------------------ | --------------------------------------------------------- |
| `WORKOS_API_KEY` | API key for management commands (bypasses stored config) |
| `WORKOS_API_BASE_URL` | Override API base URL (set automatically by `workos dev`) |
| `WORKOS_MODE` | Interaction mode: `human`, `agent`, or `ci` |
| `WORKOS_FORCE_TTY=1` | Force human (non-JSON) **output** mode even when piped |
| `WORKOS_TELEMETRY=false` | Disable telemetry |
| Variable | Effect |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `WORKOS_API_KEY` | API key for `workos api`, the still-REST commands (connection, directory, audit-log, api-key, vault), and workflow/debug commands. Resource commands use the `workos auth login` session instead |
| `WORKOS_API_URL` | Override the API base URL for all CLI commands (e.g. a local API) |
| `WORKOS_API_BASE_URL` | Accepted alias for `WORKOS_API_URL` (what `workos dev` sets) |
| `WORKOS_MODE` | Interaction mode: `human`, `agent`, or `ci` |
| `WORKOS_FORCE_TTY=1` | Force human (non-JSON) **output** mode even when piped |
| `WORKOS_TELEMETRY=false` | Disable telemetry |

> `WORKOS_API_URL` controls where the **CLI's own** commands (`workos user`, `workos api`, etc.) send requests — set it to point the CLI at a locally-running API. This is distinct from `workos dev`, which sets API vars for your **app's** dev process so it talks to the in-process emulator.

### Command Discovery

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@
"eval:diff": "tsx tests/evals/index.ts diff",
"eval:prune": "tsx tests/evals/index.ts prune",
"eval:logs": "tsx tests/evals/index.ts logs",
"eval:show": "tsx tests/evals/index.ts show"
"eval:show": "tsx tests/evals/index.ts show",
"catalog:vendor": "tsx scripts/vendor-catalog.ts",
"justification:check": "tsx scripts/check-justification.ts"
},
"author": "WorkOS",
"license": "MIT"
Expand Down
39 changes: 39 additions & 0 deletions scripts/check-justification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env tsx
/**
* CI gate for the command justification manifest.
*
* Loads the curated manifest and the vendored Management catalog, runs
* `validateManifest`, and exits non-zero if any entry is incomplete or maps to
* an operation the catalog does not contain. Wired to `pnpm justification:check`
* and run in CI so an unjustified or drifted command cannot ship.
*
* Usage:
* pnpm justification:check
*/

import { getManifest } from '../src/catalog/manifest.js';
import { loadManagementCatalog } from '../src/catalog/loader.js';
import { validateManifest } from '../src/catalog/justification.js';

function main(): void {
const manifest = getManifest();
// Validate against the full catalog (including feature-flag-gated ops): a
// manifest entry may legitimately target a flagged operation, and we only
// want to fail on genuinely-unknown `mapsTo` values, not on visibility.
const catalog = loadManagementCatalog(undefined, { includeFeatureFlagged: true });

const { ok, errors } = validateManifest(manifest, catalog);

if (ok) {
console.log(`justification:check — OK (${manifest.length} command(s) validated)`);
process.exit(0);
}

console.error(`justification:check — FAILED (${errors.length} error(s)):`);
for (const error of errors) {
console.error(` - ${error}`);
}
process.exit(1);
}

main();
Loading