Skip to content
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"fast-glob": "^3.3.3",
"hono": "^4",
"ink": "^6.8.0",
"jsonc-parser": "^3.3.1",
"open": "^11.0.0",
"react": "^19.2.4",
"semver": "^7.7.4",
Expand Down
2,262 changes: 797 additions & 1,465 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,36 @@ async function runCli(): Promise<void> {
await applyInsecureStorage(argv.insecureStorage as boolean | undefined);
await maybeWarnUnclaimed();
})
.middleware(async (argv) => {
// One-time MCP banner (lowest-priority startup notice — runs after the
// telemetry notice + unclaimed warning so they win the one-per-run slot).
// Skip commands that manage MCP/agents directly or where the nudge is
// noise, mirroring + extending maybeWarnUnclaimed's list. Self-guarded and
// never throws.
const command = String(argv._?.[0] ?? '');
if (
[
'mcp',
'install',
'doctor',
'skills',
'auth',
'env',
'claim',
'debug',
'dashboard',
'emulate',
'dev',
'migrations',
'telemetry',
'completion',
'',
].includes(command)
)
return;
const { maybeShowMcpNotice } = await import('./lib/mcp-notice.js');
await maybeShowMcpNotice();
})
.command('auth', 'Manage authentication (login, logout, status)', (yargs) => {
yargs.options(insecureStorageOption);
registerSubcommand(
Expand Down Expand Up @@ -466,6 +496,51 @@ async function runCli(): Promise<void> {
);
return yargs.demandCommand(1, 'Please specify a skills subcommand').strict();
})
.command('mcp', 'Manage the WorkOS MCP server in coding agents (Claude Code, Codex, Cursor)', (yargs) => {
registerSubcommand(
yargs,
'install',
'Add the WorkOS MCP server to detected coding agents',
(y) =>
y.option('agent', {
alias: 'a',
type: 'array',
string: true,
description: 'Target specific agent(s): claude-code, codex, cursor',
}),
async (argv) => {
const { runMcpInstall } = await import('./commands/mcp.js');
await runMcpInstall({ agent: argv.agent as string[] | undefined });
},
);
registerSubcommand(
yargs,
'remove',
'Remove the WorkOS MCP server from coding agents',
(y) =>
y.option('agent', {
alias: 'a',
type: 'array',
string: true,
description: 'Target specific agent(s): claude-code, codex, cursor',
}),
async (argv) => {
const { runMcpRemove } = await import('./commands/mcp.js');
await runMcpRemove({ agent: argv.agent as string[] | undefined });
},
);
registerSubcommand(
yargs,
'status',
'Show which coding agents have the WorkOS MCP server configured',
(y) => y,
async () => {
const { runMcpStatus } = await import('./commands/mcp.js');
await runMcpStatus();
},
);
return yargs.demandCommand(1, 'Please specify an mcp subcommand').strict();
})
.command(
'doctor',
'Diagnose WorkOS AuthKit integration issues in the current project',
Expand Down
17 changes: 17 additions & 0 deletions src/commands/install.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ vi.mock('./install-skill.js', () => ({
autoInstallSkills: vi.fn(),
}));

vi.mock('../lib/mcp-notice.js', () => ({
maybeOfferMcpInstall: vi.fn(),
}));

vi.mock('../utils/clack.js', () => ({
default: {
log: { info: vi.fn(), error: vi.fn() },
Expand All @@ -25,6 +29,7 @@ vi.mock('../utils/debug.js', () => ({

const { runInstaller } = await import('../run.js');
const { autoInstallSkills } = await import('./install-skill.js');
const { maybeOfferMcpInstall } = await import('../lib/mcp-notice.js');
const clack = (await import('../utils/clack.js')).default;
const { isJsonMode } = await import('../utils/output.js');
const { CliExit } = await import('../utils/cli-exit.js');
Expand All @@ -51,6 +56,18 @@ describe('handleInstall', () => {
expect(autoInstallOrder).toBeGreaterThan(runInstallerOrder);
});

it('offers the MCP install after skills, on the install-flow entry point', async () => {
vi.mocked(runInstaller).mockResolvedValue(undefined as any);
vi.mocked(autoInstallSkills).mockResolvedValue(null);

await expect(handleInstall({ _: ['install'], $0: 'workos' } as any)).resolves.toBeUndefined();

expect(maybeOfferMcpInstall).toHaveBeenCalledWith({ entryPoint: 'install-flow' });
const autoInstallOrder = vi.mocked(autoInstallSkills).mock.invocationCallOrder[0];
const mcpOfferOrder = vi.mocked(maybeOfferMcpInstall).mock.invocationCallOrder[0];
expect(mcpOfferOrder).toBeGreaterThan(autoInstallOrder);
});

it('prints an info line when skills were installed in a TTY session', async () => {
vi.mocked(runInstaller).mockResolvedValue(undefined as any);
vi.mocked(autoInstallSkills).mockResolvedValue({
Expand Down
5 changes: 5 additions & 0 deletions src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { exitWithError, isJsonMode } from '../utils/output.js';
import { ExitCode, exitWithCode } from '../utils/exit-codes.js';
import type { ArgumentsCamelCase } from 'yargs';
import { autoInstallSkills } from './install-skill.js';
import { maybeOfferMcpInstall } from '../lib/mcp-notice.js';

/**
* Handle install command execution.
Expand Down Expand Up @@ -37,6 +38,10 @@ export async function handleInstall(argv: ArgumentsCamelCase<InstallerArgs>): Pr
`Installed ${skillResult.skills.length} WorkOS ${skillWord} for ${skillResult.agents.join(', ')}. Your coding agent now has up-to-date WorkOS guidance.`,
);
}

// Offer to connect the user's coding agent to WorkOS via MCP. Self-gating
// (human/TTY-only, decline-respecting) and best-effort — never fails install.
await maybeOfferMcpInstall({ entryPoint: 'install-flow' });
} catch (err) {
const { getLogFilePath } = await import('../utils/debug.js');
const logPath = getLogFilePath();
Expand Down
Loading