Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: pnpm lint

- name: Format
run: pnpm format
run: pnpm format:check

- name: Typecheck
run: pnpm typecheck
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
"build": "pnpm tsc",
"postbuild": "chmod +x ./dist/bin.js && cp -r scripts/** dist",
"lint": "oxlint",
"format": "oxfmt --check .",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"try": "tsx dev.ts",
"dev": "pnpm build && pnpm link --global && pnpm build:watch",
"test": "vitest run",
Expand Down
95 changes: 76 additions & 19 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,22 @@ if (!satisfies(process.version, NODE_VERSION_RANGE)) {
}

import { isNonInteractiveEnvironment } from './utils/environment.js';
import { resolveOutputMode, setOutputMode, outputJson, exitWithError } from './utils/output.js';
import clack from './utils/clack.js';

// Resolve output mode early from raw argv (before yargs parses)
const rawArgs = hideBin(process.argv);
const hasJsonFlag = rawArgs.includes('--json');
setOutputMode(resolveOutputMode(hasJsonFlag));

// Intercept --help --json before yargs parses (yargs exits on --help)
if (hasJsonFlag && (rawArgs.includes('--help') || rawArgs.includes('-h'))) {
const { buildCommandTree } = await import('./utils/help-json.js');
const command = rawArgs.find((a) => !a.startsWith('-'));
outputJson(buildCommandTree(command));
process.exit(0);
}

/** Apply insecure storage flag if set */
async function applyInsecureStorage(insecureStorage?: boolean): Promise<void> {
if (insecureStorage) {
Expand Down Expand Up @@ -94,11 +108,11 @@ const installerOptions = {
},
'api-key': {
type: 'string' as const,
hidden: true,
describe: 'WorkOS API key (required in non-interactive mode)',
},
'client-id': {
type: 'string' as const,
hidden: true,
describe: 'WorkOS client ID (required in non-interactive mode)',
},
inspect: {
default: false,
Expand All @@ -114,9 +128,9 @@ const installerOptions = {
describe: 'Redirect URI for WorkOS callback (defaults to framework convention)',
type: 'string' as const,
},
'no-validate': {
default: false,
describe: 'Skip post-installation validation (includes build check)',
validate: {
default: true,
describe: 'Run post-installation validation (use --no-validate to skip)',
type: 'boolean' as const,
},
'install-dir': {
Expand All @@ -138,27 +152,53 @@ const installerOptions = {
describe: 'Run with visual dashboard mode',
type: 'boolean' as const,
},
branch: {
default: true,
describe: 'Create a new branch for changes (use --no-branch to skip)',
type: 'boolean' as const,
},
commit: {
default: true,
describe: 'Auto-commit after installation (use --no-commit to skip)',
type: 'boolean' as const,
},
'create-pr': {
default: false,
describe: 'Auto-create pull request after installation',
type: 'boolean' as const,
},
'git-check': {
default: true,
describe: 'Check for dirty working tree (use --no-git-check to skip)',
type: 'boolean' as const,
},
};

// Check for updates (blocks up to 500ms)
await checkForUpdates();

yargs(hideBin(process.argv))
yargs(rawArgs)
.env('WORKOS_INSTALLER')
.command('login', 'Authenticate with WorkOS', insecureStorageOption, async (argv) => {
.option('json', {
type: 'boolean',
default: false,
describe: 'Output results as JSON (auto-enabled in non-TTY)',
global: true,
})
.command('login', 'Authenticate with WorkOS via browser-based OAuth', insecureStorageOption, async (argv) => {
await applyInsecureStorage(argv.insecureStorage);
const { runLogin } = await import('./commands/login.js');
await runLogin();
process.exit(0);
})
.command('logout', 'Remove stored credentials', insecureStorageOption, async (argv) => {
.command('logout', 'Remove stored WorkOS credentials and tokens', insecureStorageOption, async (argv) => {
await applyInsecureStorage(argv.insecureStorage);
const { runLogout } = await import('./commands/logout.js');
await runLogout();
})
.command(
'install-skill',
'Install bundled AuthKit skills to coding agents',
'Install bundled AuthKit skills to coding agents (Claude Code, Codex, Cursor, Goose)',
(yargs) => {
return yargs
.option('list', {
Expand Down Expand Up @@ -190,7 +230,7 @@ yargs(hideBin(process.argv))
)
.command(
'doctor',
'Diagnose WorkOS integration issues',
'Diagnose WorkOS AuthKit integration issues in the current project',
(yargs) =>
yargs.options({
verbose: {
Expand Down Expand Up @@ -229,7 +269,8 @@ yargs(hideBin(process.argv))
await handleDoctor(argv);
},
)
.command('env', 'Manage environment configurations', (yargs) =>
// NOTE: When adding commands here, also update src/utils/help-json.ts
.command('env', 'Manage environment configurations (API keys, endpoints, active environment)', (yargs) =>
yargs
.options(insecureStorageOption)
.command(
Expand Down Expand Up @@ -267,6 +308,12 @@ yargs(hideBin(process.argv))
'Switch active environment',
(yargs) => yargs.positional('name', { type: 'string', describe: 'Environment name' }),
async (argv) => {
if (!argv.name && isNonInteractiveEnvironment()) {
exitWithError({
code: 'missing_args',
message: 'Environment name required. Usage: workos env switch <name>',
});
}
await applyInsecureStorage(argv.insecureStorage);
const { runEnvSwitch } = await import('./commands/env.js');
await runEnvSwitch(argv.name);
Expand All @@ -285,19 +332,26 @@ yargs(hideBin(process.argv))
.demandCommand(1, 'Please specify an env subcommand')
.strict(),
)
.command('organization', 'Manage organizations', (yargs) =>
.command(['organization', 'org'], 'Manage WorkOS organizations (create, update, get, list, delete)', (yargs) =>
yargs
.options({
...insecureStorageOption,
'api-key': { type: 'string' as const, describe: 'WorkOS API key (overrides environment config)' },
'api-key': {
type: 'string' as const,
describe: 'WorkOS API key (overrides environment config). Format: sk_live_* or sk_test_*',
},
})
.command(
'create <name> [domains..]',
'Create a new organization',
'Create a new organization with optional verified domains',
(yargs) =>
yargs
.positional('name', { type: 'string', demandOption: true, describe: 'Organization name' })
.positional('domains', { type: 'string', array: true, describe: 'Domains as domain:state' }),
.positional('domains', {
type: 'string',
array: true,
describe: 'Domains in format domain:state (state defaults to verified)',
}),
async (argv) => {
await applyInsecureStorage(argv.insecureStorage);
const { resolveApiKey, resolveApiBaseUrl } = await import('./lib/api-key.js');
Expand Down Expand Up @@ -373,11 +427,14 @@ yargs(hideBin(process.argv))
.demandCommand(1, 'Please specify an organization subcommand')
.strict(),
)
.command('user', 'Manage users', (yargs) =>
.command('user', 'Manage WorkOS users (get, list, update, delete)', (yargs) =>
yargs
.options({
...insecureStorageOption,
'api-key': { type: 'string' as const, describe: 'WorkOS API key (overrides environment config)' },
'api-key': {
type: 'string' as const,
describe: 'WorkOS API key (overrides environment config). Format: sk_live_* or sk_test_*',
},
})
.command(
'get <userId>',
Expand Down Expand Up @@ -465,7 +522,7 @@ yargs(hideBin(process.argv))
)
.command(
'install',
'Install WorkOS AuthKit into your project',
'Install WorkOS AuthKit into your project (interactive framework detection and setup)',
(yargs) => yargs.options(installerOptions),
withAuth(async (argv) => {
const { handleInstall } = await import('./commands/install.js');
Expand All @@ -488,7 +545,7 @@ yargs(hideBin(process.argv))
async (argv) => {
// Non-TTY: show help
if (isNonInteractiveEnvironment()) {
yargs(hideBin(process.argv)).showHelp();
yargs(rawArgs).showHelp();
return;
}

Expand Down
67 changes: 67 additions & 0 deletions src/commands/env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ vi.mock('node:os', async (importOriginal) => {

const { getConfig, setInsecureConfigStorage, clearConfig } = await import('../lib/config-store.js');
const { runEnvAdd, runEnvRemove, runEnvSwitch, runEnvList } = await import('./env.js');
const { setOutputMode } = await import('../utils/output.js');
const clack = (await import('../utils/clack.js')).default;

// Spy on process.exit
Expand Down Expand Up @@ -160,4 +161,70 @@ describe('env commands', () => {
await expect(runEnvList()).resolves.not.toThrow();
});
});

describe('JSON output mode', () => {
let consoleOutput: string[];

beforeEach(() => {
setOutputMode('json');
consoleOutput = [];
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
consoleOutput.push(args.map(String).join(' '));
});
});

afterEach(() => {
setOutputMode('human');
});

it('runEnvAdd outputs JSON success', async () => {
await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' });
const output = JSON.parse(consoleOutput[0]);
expect(output.status).toBe('ok');
expect(output.message).toBe('Environment added');
expect(output.name).toBe('prod');
expect(output.type).toBe('production');
expect(output.active).toBe(true);
});

it('runEnvRemove outputs JSON success', async () => {
await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' });
consoleOutput = [];
await runEnvRemove('prod');
const output = JSON.parse(consoleOutput[0]);
expect(output.status).toBe('ok');
expect(output.message).toBe('Environment removed');
expect(output.name).toBe('prod');
});

it('runEnvSwitch outputs JSON success', async () => {
await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' });
await runEnvAdd({ name: 'sandbox', apiKey: 'sk_test_abc' });
consoleOutput = [];
await runEnvSwitch('sandbox');
const output = JSON.parse(consoleOutput[0]);
expect(output.status).toBe('ok');
expect(output.message).toBe('Switched environment');
expect(output.name).toBe('sandbox');
});

it('runEnvList outputs JSON with data array', async () => {
await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' });
await runEnvAdd({ name: 'sandbox', apiKey: 'sk_test_abc' });
consoleOutput = [];
await runEnvList();
const output = JSON.parse(consoleOutput[0]);
expect(output.data).toHaveLength(2);
expect(output.data[0].name).toBe('prod');
expect(output.data[0].active).toBe(true);
expect(output.data[1].name).toBe('sandbox');
expect(output.data[1].active).toBe(false);
});

it('runEnvList outputs empty data array when no environments', async () => {
await runEnvList();
const output = JSON.parse(consoleOutput[0]);
expect(output.data).toEqual([]);
});
});
});
Loading