diff --git a/src/cli.test.ts b/src/cli.test.ts index 2cbc6620..632fb13e 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'fs'; +import { readFileSync, mkdtempSync } from 'fs'; import { join } from 'path'; -import { runCliOutput, stripLogo, hasLogo } from './test-utils.ts'; +import { tmpdir } from 'os'; +import { runCli, runCliOutput, stripLogo, hasLogo } from './test-utils.ts'; describe('skills CLI', () => { describe('--help', () => { @@ -14,6 +15,7 @@ describe('skills CLI', () => { expect(output).toContain('check'); expect(output).toContain('update'); expect(output).toContain('Add Options:'); + expect(output).toContain('Check Options:'); expect(output).toContain('-g, --global'); expect(output).toContain('-a, --agent'); expect(output).toContain('-s, --skill'); @@ -67,6 +69,28 @@ describe('skills CLI', () => { }); }); + describe('check --json', () => { + it('should output valid JSON with no skills', () => { + const tempState = mkdtempSync(join(tmpdir(), 'skills-check-test-')); + const result = runCli(['check', '--json'], undefined, { XDG_STATE_HOME: tempState }); + const parsed = JSON.parse(result.stdout.trim()); + expect(parsed).toEqual([]); + }); + + it('should output an array', () => { + const tempState = mkdtempSync(join(tmpdir(), 'skills-check-test-')); + const result = runCli(['check', '--json'], undefined, { XDG_STATE_HOME: tempState }); + const parsed = JSON.parse(result.stdout.trim()); + expect(Array.isArray(parsed)).toBe(true); + }); + + it('should not contain ANSI codes', () => { + const tempState = mkdtempSync(join(tmpdir(), 'skills-check-test-')); + const result = runCli(['check', '--json'], undefined, { XDG_STATE_HOME: tempState }); + expect(result.stdout).not.toMatch(/\x1b\[/); + }); + }); + describe('logo display', () => { it('should not display logo for list command', () => { const output = runCliOutput(['list']); diff --git a/src/cli.ts b/src/cli.ts index 30282096..8ad1a2ba 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -150,6 +150,9 @@ ${BOLD}List Options:${RESET} -a, --agent Filter by specific agents --json Output as JSON (machine-readable, no ANSI codes) +${BOLD}Check Options:${RESET} + --json Output as JSON (machine-readable, no ANSI codes) + ${BOLD}Options:${RESET} --help, -h Show this help message --version, -v Show version number @@ -169,6 +172,7 @@ ${BOLD}Examples:${RESET} ${DIM}$${RESET} skills find ${DIM}# interactive search${RESET} ${DIM}$${RESET} skills find typescript ${DIM}# search by keyword${RESET} ${DIM}$${RESET} skills check + ${DIM}$${RESET} skills check --json ${DIM}# JSON output${RESET} ${DIM}$${RESET} skills update ${DIM}$${RESET} skills experimental_install ${DIM}# restore from skills-lock.json${RESET} ${DIM}$${RESET} skills init my-skill @@ -397,13 +401,21 @@ function printSkippedSkills(skipped: SkippedSkill[]): void { } async function runCheck(args: string[] = []): Promise { - console.log(`${TEXT}Checking for skill updates...${RESET}`); - console.log(); + const json = args.includes('--json'); + + if (!json) { + console.log(`${TEXT}Checking for skill updates...${RESET}`); + console.log(); + } const lock = readSkillLock(); const skillNames = Object.keys(lock.skills); if (skillNames.length === 0) { + if (json) { + console.log(JSON.stringify([], null, 2)); + return; + } console.log(`${DIM}No skills tracked in lock file.${RESET}`); console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add ${RESET}`); return; @@ -433,12 +445,18 @@ async function runCheck(args: string[] = []): Promise { const totalSkills = skillNames.length - skipped.length; if (totalSkills === 0) { + if (json) { + console.log(JSON.stringify([], null, 2)); + return; + } console.log(`${DIM}No GitHub skills to check.${RESET}`); printSkippedSkills(skipped); return; } - console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`); + if (!json) { + console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`); + } const updates: Array<{ name: string; source: string }> = []; const errors: Array<{ name: string; source: string; error: string }> = []; @@ -467,6 +485,17 @@ async function runCheck(args: string[] = []): Promise { } } + if (json) { + console.log( + JSON.stringify( + updates.map((u) => ({ name: u.name, source: u.source })), + null, + 2 + ) + ); + return; + } + console.log(); if (updates.length === 0) {