diff --git a/src/bin.ts b/src/bin.ts index 5891d0f..a3ae927 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -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', diff --git a/src/commands/install-skill.ts b/src/commands/install-skill.ts index 02f4ac3..f475778 100644 --- a/src/commands/install-skill.ts +++ b/src/commands/install-skill.ts @@ -42,7 +42,6 @@ export function createAgents(home: string): Record { } export interface InstallSkillOptions { - list?: boolean; skill?: string[]; agent?: string[]; } @@ -97,15 +96,6 @@ export async function runInstallSkill(options: InstallSkillOptions): Promise options.skill!.includes(s)) : skills; if (targetSkills.length === 0) { diff --git a/src/commands/list-skills.spec.ts b/src/commands/list-skills.spec.ts new file mode 100644 index 0000000..1a9027b --- /dev/null +++ b/src/commands/list-skills.spec.ts @@ -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(); + 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(); + 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(); + }); +}); diff --git a/src/commands/list-skills.ts b/src/commands/list-skills.ts new file mode 100644 index 0000000..952a499 --- /dev/null +++ b/src/commands/list-skills.ts @@ -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 { + 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(); +} diff --git a/src/commands/uninstall-skill.spec.ts b/src/commands/uninstall-skill.spec.ts new file mode 100644 index 0000000..36cf116 --- /dev/null +++ b/src/commands/uninstall-skill.spec.ts @@ -0,0 +1,290 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { createAgents, type AgentConfig } from './install-skill.js'; +import { findInstalledSkills, uninstallSkill } from './uninstall-skill.js'; + +describe('uninstall-skill', () => { + let testDir: string; + let homeDir: string; + + beforeEach(() => { + testDir = mkdtempSync(join(tmpdir(), 'uninstall-skill-test-')); + homeDir = join(testDir, 'home'); + mkdirSync(homeDir); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + describe('findInstalledSkills', () => { + let agent: AgentConfig; + + beforeEach(() => { + agent = { + name: 'test-agent', + displayName: 'Test Agent', + globalSkillsDir: join(homeDir, '.test-agent/skills'), + detect: () => true, + }; + }); + + it('returns empty array when no skills are installed', () => { + mkdirSync(agent.globalSkillsDir, { recursive: true }); + const result = findInstalledSkills(['skill-one', 'skill-two'], agent); + expect(result).toEqual([]); + }); + + it('returns empty array when globalSkillsDir does not exist', () => { + const result = findInstalledSkills(['skill-one', 'skill-two'], agent); + expect(result).toEqual([]); + }); + + it('returns only skills that exist in agent directory', () => { + const skillDir = join(agent.globalSkillsDir, 'skill-one'); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), '# Skill One'); + + const result = findInstalledSkills(['skill-one', 'skill-two'], agent); + expect(result).toEqual(['skill-one']); + }); + + it('returns all matching skills when multiple are installed', () => { + for (const name of ['skill-one', 'skill-two']) { + const skillDir = join(agent.globalSkillsDir, name); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), `# ${name}`); + } + + const result = findInstalledSkills(['skill-one', 'skill-two', 'skill-three'], agent); + expect(result).toEqual(['skill-one', 'skill-two']); + }); + + it('ignores directories without SKILL.md', () => { + const skillDir = join(agent.globalSkillsDir, 'skill-one'); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'README.md'), '# Not a skill'); + + const result = findInstalledSkills(['skill-one'], agent); + expect(result).toEqual([]); + }); + + it('does not detect skills not in the known list', () => { + const skillDir = join(agent.globalSkillsDir, 'custom-skill'); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), '# Custom'); + + const result = findInstalledSkills(['workos-skill'], agent); + expect(result).toEqual([]); + }); + }); + + describe('uninstallSkill', () => { + let agent: AgentConfig; + + beforeEach(() => { + agent = { + name: 'test-agent', + displayName: 'Test Agent', + globalSkillsDir: join(homeDir, '.test-agent/skills'), + detect: () => true, + }; + }); + + it('removes skill directory', async () => { + const skillDir = join(agent.globalSkillsDir, 'test-skill'); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), '# Test'); + + const result = await uninstallSkill('test-skill', agent); + + expect(result.success).toBe(true); + expect(existsSync(skillDir)).toBe(false); + }); + + it('succeeds when directory does not exist', async () => { + const result = await uninstallSkill('nonexistent-skill', agent); + + expect(result.success).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it('removes directory and all contents', async () => { + const skillDir = join(agent.globalSkillsDir, 'test-skill'); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), '# Test'); + writeFileSync(join(skillDir, 'extra-file.txt'), 'extra'); + + const result = await uninstallSkill('test-skill', agent); + + expect(result.success).toBe(true); + expect(existsSync(skillDir)).toBe(false); + }); + }); + + describe('createAgents integration', () => { + it('uses correct skill paths for uninstall detection', () => { + mkdirSync(join(homeDir, '.claude'), { recursive: true }); + const agents = createAgents(homeDir); + const claudeAgent = agents['claude-code']; + + const skillDir = join(claudeAgent.globalSkillsDir, 'workos-test'); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), '# Test'); + + const installed = findInstalledSkills(['workos-test', 'workos-other'], claudeAgent); + expect(installed).toEqual(['workos-test']); + }); + }); +}); + +describe('runUninstallSkill', () => { + let testDir: string; + let homeDir: string; + let skillsDir: string; + let agentSkillsDir: string; + let mockExit: ReturnType; + + beforeEach(() => { + testDir = mkdtempSync(join(tmpdir(), 'uninstall-run-test-')); + homeDir = join(testDir, 'home'); + skillsDir = join(testDir, 'skills'); + agentSkillsDir = join(homeDir, '.test/skills'); + mkdirSync(homeDir); + mkdirSync(skillsDir); + mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + }); + + afterEach(() => { + mockExit.mockRestore(); + 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(); + return { + ...mod, + getSkillsDir: () => skillsDir, + createAgents: () => ({ test: makeTestAgent() }), + detectAgents: () => [makeTestAgent()], + }; + }); + const { runUninstallSkill } = await import('./uninstall-skill.js'); + return { runUninstallSkill }; + } + + it('exits with error when --skill contains only unknown names', async () => { + mkdirSync(join(skillsDir, 'authkit-setup'), { recursive: true }); + writeFileSync(join(skillsDir, 'authkit-setup', 'SKILL.md'), '# AuthKit'); + + const { runUninstallSkill } = await importMocked(); + await runUninstallSkill({ skill: ['nonexistent-skill'] }); + + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('does not uninstall all skills when --skill filter partially matches', async () => { + // Set up known skills + for (const name of ['skill-a', 'skill-b']) { + mkdirSync(join(skillsDir, name), { recursive: true }); + writeFileSync(join(skillsDir, name, 'SKILL.md'), `# ${name}`); + } + + // Set up installed skills for the agent + for (const name of ['skill-a', 'skill-b']) { + mkdirSync(join(agentSkillsDir, name), { recursive: true }); + writeFileSync(join(agentSkillsDir, name, 'SKILL.md'), `# ${name}`); + } + + const { runUninstallSkill } = await importMocked(); + await runUninstallSkill({ skill: ['skill-a', 'typo-skill'] }); + + // skill-a should be removed, skill-b should be untouched + expect(existsSync(join(agentSkillsDir, 'skill-a', 'SKILL.md'))).toBe(false); + expect(existsSync(join(agentSkillsDir, 'skill-b', 'SKILL.md'))).toBe(true); + }); + + describe('JSON mode', () => { + async function importMockedWithJsonMode() { + vi.resetModules(); + vi.doMock('./install-skill.js', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + getSkillsDir: () => skillsDir, + createAgents: () => ({ test: makeTestAgent() }), + detectAgents: () => [makeTestAgent()], + }; + }); + // Import output.js from the fresh module graph and set JSON mode + const output = await import('../utils/output.js'); + output.setOutputMode('json'); + const { runUninstallSkill } = await import('./uninstall-skill.js'); + return { runUninstallSkill, resetMode: () => output.setOutputMode('human') }; + } + + it('outputs structured JSON results for uninstall', async () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + // Set up a known skill + mkdirSync(join(skillsDir, 'test-skill'), { recursive: true }); + writeFileSync(join(skillsDir, 'test-skill', 'SKILL.md'), '# Test'); + + // Set up installed skill for the agent + mkdirSync(join(agentSkillsDir, 'test-skill'), { recursive: true }); + writeFileSync(join(agentSkillsDir, 'test-skill', 'SKILL.md'), '# Test'); + + const { runUninstallSkill, resetMode } = await importMockedWithJsonMode(); + await runUninstallSkill({}); + + const jsonOutput = consoleSpy.mock.calls.find((call) => { + try { + const parsed = JSON.parse(call[0] as string); + return parsed.removed !== undefined; + } catch { + return false; + } + }); + expect(jsonOutput).toBeDefined(); + const parsed = JSON.parse(jsonOutput![0] as string); + expect(parsed.removed).toHaveLength(1); + expect(parsed.removed[0].skill).toBe('test-skill'); + + consoleSpy.mockRestore(); + resetMode(); + }); + + it('outputs structured JSON error for unknown skills', async () => { + const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + mkdirSync(join(skillsDir, 'authkit-setup'), { recursive: true }); + writeFileSync(join(skillsDir, 'authkit-setup', 'SKILL.md'), '# AuthKit'); + + const { runUninstallSkill, resetMode } = await importMockedWithJsonMode(); + await runUninstallSkill({ skill: ['nonexistent'] }); + + const jsonError = stderrSpy.mock.calls.find((call) => { + try { + const parsed = JSON.parse(call[0] as string); + return parsed.error?.code === 'SKILL_NOT_FOUND'; + } catch { + return false; + } + }); + expect(jsonError).toBeDefined(); + + stderrSpy.mockRestore(); + resetMode(); + }); + }); +}); diff --git a/src/commands/uninstall-skill.ts b/src/commands/uninstall-skill.ts new file mode 100644 index 0000000..64e4f1f --- /dev/null +++ b/src/commands/uninstall-skill.ts @@ -0,0 +1,151 @@ +import { existsSync } from 'fs'; +import { rm } from 'fs/promises'; +import { homedir } from 'os'; +import { join } from 'path'; +import chalk from 'chalk'; +import { logError, logInfo, logWarn } from '../utils/debug.js'; +import { exitWithError, isJsonMode, outputJson } from '../utils/output.js'; +import { createAgents, detectAgents, discoverSkills, getSkillsDir, type AgentConfig } from './install-skill.js'; + +export interface UninstallSkillOptions { + skill?: string[]; + agent?: string[]; +} + +export function findInstalledSkills(knownSkills: string[], agent: AgentConfig): string[] { + return knownSkills.filter((name) => existsSync(join(agent.globalSkillsDir, name, 'SKILL.md'))); +} + +export async function uninstallSkill( + skillName: string, + agent: AgentConfig, +): Promise<{ success: boolean; error?: string }> { + const targetDir = join(agent.globalSkillsDir, skillName); + try { + await rm(targetDir, { recursive: true, force: true }); + return { success: true }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + logError(`Failed to remove skill "${skillName}" for ${agent.displayName} at ${targetDir}:`, message); + return { success: false, error: message }; + } +} + +export async function runUninstallSkill(options: UninstallSkillOptions): Promise { + 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); + + if (targetAgents.length === 0) { + const message = options.agent ? 'Specified agents not found.' : 'No coding agents detected.'; + logWarn(message, 'Supported agents:', Object.keys(agents).join(', ')); + exitWithError({ + code: 'NO_AGENTS_FOUND', + message: `${message} Supported agents: ${Object.keys(agents).join(', ')}`, + }); + } + + const targetSkillNames = options.skill ? knownSkills.filter((s) => options.skill!.includes(s)) : knownSkills; + + if (options.skill) { + const unrecognized = options.skill.filter((s) => !knownSkills.includes(s)); + if (unrecognized.length > 0) { + logWarn('Unrecognized skill names requested for uninstall:', unrecognized); + if (!isJsonMode()) { + console.warn(chalk.yellow(`Unknown skills (ignored): ${unrecognized.join(', ')}`)); + } + } + } + + if (options.skill && targetSkillNames.length === 0) { + logError('No matching skills found. Known skills:', knownSkills.join(', ')); + exitWithError({ + code: 'SKILL_NOT_FOUND', + message: `No matching skills found. Known skills: ${knownSkills.join(', ')}`, + }); + } + + logInfo( + 'Uninstalling skills:', + targetSkillNames.join(', '), + 'for agents:', + targetAgents.map((a) => a.displayName).join(', '), + ); + + if (!isJsonMode()) { + console.log(chalk.bold('\nUninstalling skills...\n')); + } + + const results: Array<{ + skill: string; + agent: string; + success: boolean; + skipped: boolean; + error?: string; + }> = []; + + for (const skill of targetSkillNames) { + for (const agent of targetAgents) { + const installed = findInstalledSkills([skill], agent); + if (installed.length === 0) { + results.push({ skill, agent: agent.displayName, success: true, skipped: true }); + continue; + } + const result = await uninstallSkill(skill, agent); + results.push({ + skill, + agent: agent.displayName, + skipped: false, + ...result, + }); + } + } + + const removed = results.filter((r) => r.success && !r.skipped); + const skipped = results.filter((r) => r.skipped); + const failed = results.filter((r) => !r.success); + + if (isJsonMode()) { + outputJson({ removed, skipped, failed }); + if (failed.length > 0) { + process.exit(1); + } + return; + } + + if (removed.length > 0) { + logInfo(`Removed ${removed.length} skill(s)`); + console.log(chalk.green(`āœ“ Removed ${removed.length} skill(s):\n`)); + for (const r of removed) { + console.log(` ${chalk.cyan(r.skill)} ← ${chalk.dim(r.agent)}`); + } + } + + if (skipped.length > 0 && removed.length === 0 && failed.length === 0) { + console.log(chalk.dim('No WorkOS skills were installed.')); + } + + if (failed.length > 0) { + logError(`Failed to remove ${failed.length} skill(s)`); + console.log(chalk.red(`\nāœ— Failed to remove ${failed.length}:\n`)); + for (const r of failed) { + console.log(` ${r.skill} ← ${r.agent}: ${chalk.dim(r.error)}`); + } + process.exit(1); + } + + console.log(chalk.green('\nDone!')); +} diff --git a/src/utils/help-json.spec.ts b/src/utils/help-json.spec.ts index 4744301..e548ec2 100644 --- a/src/utils/help-json.spec.ts +++ b/src/utils/help-json.spec.ts @@ -32,7 +32,7 @@ describe('help-json', () => { 'auth login', 'auth logout', 'auth status', - 'install-skill', + 'skills', 'doctor', 'env', 'organization', diff --git a/src/utils/help-json.ts b/src/utils/help-json.ts index f8a44cb..c1f6837 100644 --- a/src/utils/help-json.ts +++ b/src/utils/help-json.ts @@ -111,32 +111,66 @@ const commands: CommandSchema[] = [ options: [insecureStorageOpt], }, { - name: 'install-skill', - description: 'Install bundled AuthKit skills to coding agents (Claude Code, Codex, Cursor, Goose)', - options: [ + name: 'skills', + description: 'Manage WorkOS skills for coding agents (Claude Code, Codex, Cursor, Goose)', + commands: [ { - name: 'list', - type: 'boolean', - description: 'List available skills without installing', - required: false, - alias: 'l', - hidden: false, + name: 'install', + description: 'Install bundled AuthKit skills to coding agents', + options: [ + { + name: 'skill', + type: 'array', + description: 'Install specific skill(s) by name', + required: false, + alias: 's', + hidden: false, + }, + { + name: 'agent', + type: 'array', + description: 'Target specific agent(s): claude-code, codex, cursor, goose', + required: false, + alias: 'a', + hidden: false, + }, + ], }, { - name: 'skill', - type: 'array', - description: 'Install specific skill(s) by name', - required: false, - alias: 's', - hidden: false, + name: 'uninstall', + description: 'Remove installed WorkOS skills from coding agents', + options: [ + { + name: 'skill', + type: 'array', + description: 'Remove specific skill(s) by name', + required: false, + alias: 's', + hidden: false, + }, + { + name: 'agent', + type: 'array', + description: 'Target specific agent(s): claude-code, codex, cursor, goose', + required: false, + alias: 'a', + hidden: false, + }, + ], }, { - name: 'agent', - type: 'array', - description: 'Target specific agent(s): claude-code, codex, cursor, goose', - required: false, - alias: 'a', - hidden: false, + name: 'list', + description: 'List available and installed skills', + options: [ + { + name: 'agent', + type: 'array', + description: 'Target specific agent(s): claude-code, codex, cursor, goose', + required: false, + alias: 'a', + hidden: false, + }, + ], }, ], },