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
95 changes: 68 additions & 27 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,38 +226,79 @@ yargs(rawArgs)
);
return yargs.demandCommand(1, 'Please specify an auth subcommand').strict();
})
.command(
'install-skill',
'Install bundled AuthKit skills to coding agents (Claude Code, Codex, Cursor, Goose)',
(yargs) => {
return yargs
.option('list', {
alias: 'l',
type: 'boolean',
description: 'List available skills without installing',
})
.option('skill', {
alias: 's',
type: 'array',
string: true,
description: 'Install specific skill(s)',
})
.option('agent', {
.command('skills', 'Manage WorkOS skills for coding agents (Claude Code, Codex, Cursor, Goose)', (yargs) => {
registerSubcommand(
yargs,
'install',
'Install bundled AuthKit skills to coding agents',
(y) =>
y
.option('skill', {
alias: 's',
type: 'array',
string: true,
description: 'Install specific skill(s) by name',
})
.option('agent', {
alias: 'a',
type: 'array',
string: true,
description: 'Target specific agent(s): claude-code, codex, cursor, goose',
}),
async (argv) => {
const { runInstallSkill } = await import('./commands/install-skill.js');
await runInstallSkill({
skill: argv.skill as string[] | undefined,
agent: argv.agent as string[] | undefined,
});
},
);
registerSubcommand(
yargs,
'uninstall',
'Remove installed WorkOS skills from coding agents',
(y) =>
y
.option('skill', {
alias: 's',
type: 'array',
string: true,
description: 'Remove specific skill(s) by name',
})
.option('agent', {
alias: 'a',
type: 'array',
string: true,
description: 'Target specific agent(s): claude-code, codex, cursor, goose',
}),
async (argv) => {
const { runUninstallSkill } = await import('./commands/uninstall-skill.js');
await runUninstallSkill({
skill: argv.skill as string[] | undefined,
agent: argv.agent as string[] | undefined,
});
},
);
registerSubcommand(
yargs,
'list',
'List available and installed skills',
(y) =>
y.option('agent', {
alias: 'a',
type: 'array',
string: true,
description: 'Target specific agent(s): claude-code, codex, cursor, goose',
}),
async (argv) => {
const { runListSkills } = await import('./commands/list-skills.js');
await runListSkills({
agent: argv.agent as string[] | undefined,
});
},
withAuth(async (argv) => {
const { runInstallSkill } = await import('./commands/install-skill.js');
await runInstallSkill({
list: argv.list as boolean | undefined,
skill: argv.skill as string[] | undefined,
agent: argv.agent as string[] | undefined,
});
}),
)
},
);
return yargs.demandCommand(1, 'Please specify a skills subcommand').strict();
})
.command(
'doctor',
'Diagnose WorkOS AuthKit integration issues in the current project',
Expand Down
10 changes: 0 additions & 10 deletions src/commands/install-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export function createAgents(home: string): Record<string, AgentConfig> {
}

export interface InstallSkillOptions {
list?: boolean;
skill?: string[];
agent?: string[];
}
Expand Down Expand Up @@ -97,15 +96,6 @@ export async function runInstallSkill(options: InstallSkillOptions): Promise<voi
const skillsDir = getSkillsDir();
const skills = await discoverSkills(skillsDir);

if (options.list) {
console.log(chalk.bold('\nAvailable Skills:\n'));
for (const skill of skills) {
console.log(` ${chalk.cyan(skill)}`);
}
console.log();
return;
}

const targetSkills = options.skill ? skills.filter((s) => options.skill!.includes(s)) : skills;

if (targetSkills.length === 0) {
Expand Down
109 changes: 109 additions & 0 deletions src/commands/list-skills.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import type { AgentConfig } from './install-skill.js';

describe('runListSkills', () => {
let testDir: string;
let homeDir: string;
let skillsDir: string;
let agentSkillsDir: string;

beforeEach(() => {
testDir = mkdtempSync(join(tmpdir(), 'list-skills-test-'));
homeDir = join(testDir, 'home');
skillsDir = join(testDir, 'skills');
agentSkillsDir = join(homeDir, '.test/skills');
mkdirSync(homeDir);
mkdirSync(skillsDir);
});

afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
rmSync(testDir, { recursive: true, force: true });
});

function makeTestAgent(): AgentConfig {
return { name: 'test', displayName: 'Test', globalSkillsDir: agentSkillsDir, detect: () => true };
}

async function importMocked() {
vi.resetModules();
vi.doMock('./install-skill.js', async (importOriginal) => {
const mod = await importOriginal<typeof import('./install-skill.js')>();
return {
...mod,
getSkillsDir: () => skillsDir,
createAgents: () => ({ test: makeTestAgent() }),
detectAgents: () => [makeTestAgent()],
};
});
const { runListSkills } = await import('./list-skills.js');
return { runListSkills };
}

async function importMockedWithJsonMode() {
vi.resetModules();
vi.doMock('./install-skill.js', async (importOriginal) => {
const mod = await importOriginal<typeof import('./install-skill.js')>();
return {
...mod,
getSkillsDir: () => skillsDir,
createAgents: () => ({ test: makeTestAgent() }),
detectAgents: () => [makeTestAgent()],
};
});
const output = await import('../utils/output.js');
output.setOutputMode('json');
const { runListSkills } = await import('./list-skills.js');
return { runListSkills, resetMode: () => output.setOutputMode('human') };
}

it('lists available and installed skills', async () => {
mkdirSync(join(skillsDir, 'skill-a'), { recursive: true });
writeFileSync(join(skillsDir, 'skill-a', 'SKILL.md'), '# A');
mkdirSync(join(skillsDir, 'skill-b'), { recursive: true });
writeFileSync(join(skillsDir, 'skill-b', 'SKILL.md'), '# B');

mkdirSync(join(agentSkillsDir, 'skill-a'), { recursive: true });
writeFileSync(join(agentSkillsDir, 'skill-a', 'SKILL.md'), '# A');

const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const { runListSkills } = await importMocked();
await runListSkills({});

const output = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
expect(output).toContain('skill-a');
expect(output).toContain('skill-b');
consoleSpy.mockRestore();
});

it('outputs structured JSON in JSON mode', async () => {
mkdirSync(join(skillsDir, 'skill-a'), { recursive: true });
writeFileSync(join(skillsDir, 'skill-a', 'SKILL.md'), '# A');

mkdirSync(join(agentSkillsDir, 'skill-a'), { recursive: true });
writeFileSync(join(agentSkillsDir, 'skill-a', 'SKILL.md'), '# A');

const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const { runListSkills, resetMode } = await importMockedWithJsonMode();
await runListSkills({});

const jsonOutput = consoleSpy.mock.calls.find((call) => {
try {
JSON.parse(call[0] as string);
return true;
} catch {
return false;
}
});
expect(jsonOutput).toBeDefined();
const parsed = JSON.parse(jsonOutput![0] as string);
expect(parsed).toEqual([{ agent: 'Test', available: ['skill-a'], installed: ['skill-a'] }]);

consoleSpy.mockRestore();
resetMode();
});
});
61 changes: 61 additions & 0 deletions src/commands/list-skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { homedir } from 'os';
import chalk from 'chalk';
import { logError } from '../utils/debug.js';
import { exitWithError, isJsonMode, outputJson } from '../utils/output.js';
import { createAgents, detectAgents, discoverSkills, getSkillsDir } from './install-skill.js';
import { findInstalledSkills } from './uninstall-skill.js';

export interface ListSkillsOptions {
agent?: string[];
}

export async function runListSkills(options: ListSkillsOptions): Promise<void> {
const home = homedir();
const agents = createAgents(home);
const skillsDir = getSkillsDir();

let knownSkills: string[];
try {
knownSkills = await discoverSkills(skillsDir);
} catch (error) {
logError('Failed to read skills directory:', error);
exitWithError({
code: 'SKILLS_DIR_READ_FAILED',
message: `Could not read skills directory at ${skillsDir}. Your WorkOS CLI installation may be corrupted. Try reinstalling with \`npm install -g @workos-inc/cli\`.`,
});
}

const targetAgents = detectAgents(agents, options.agent);

const listData: Array<{ agent: string; available: string[]; installed: string[] }> = [];
for (const agent of targetAgents) {
const installed = findInstalledSkills(knownSkills, agent);
listData.push({ agent: agent.displayName, available: knownSkills, installed });
}

if (isJsonMode()) {
outputJson(listData);
return;
}

console.log(chalk.bold('\nWorkOS Skills:\n'));
console.log(` ${chalk.bold('Available:')} ${knownSkills.map((s) => chalk.cyan(s)).join(', ')}\n`);

if (targetAgents.length === 0) {
console.log(chalk.dim(' No coding agents detected.\n'));
return;
}

console.log(chalk.bold(' Installed per agent:\n'));
for (const entry of listData) {
console.log(` ${chalk.bold(entry.agent)}:`);
if (entry.installed.length === 0) {
console.log(` ${chalk.dim('(none)')}`);
} else {
for (const skill of entry.installed) {
console.log(` ${chalk.cyan(skill)}`);
}
}
}
console.log();
}
Loading