/** * skill command * Manage the SpecVerse Claude skill (reference docs + workflow templates bundled for Claude Code / Claude Desktop) * Generated from SpecVerse specification */ import { Command } from 'commander'; import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, cpSync } from 'fs'; import { resolve, dirname, join } from 'path'; import { homedir } from 'os'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; /** * Register the skill command on the program. */ export function registerSkillCommand(program: Command): void { const cmd = program .command('skill') .description('Manage the SpecVerse Claude skill (reference docs + workflow templates bundled for Claude Code / Claude Desktop)'); cmd .command('install') .description('Install the SpecVerse skill to a Claude skills directory (copies SKILL.md + reference docs + workflow templates)') .option('--global', 'Install to ~/.claude/skills/specverse/ (personal, all projects)', false) .option('--project', 'Install to /.claude/skills/specverse/ (project-local, committable)', false) .option('--target ', 'Install to an explicit directory (overrides --global / --project)') .action(async (options: any) => { try { const requireResolve = createRequire(import.meta.url); // 1. Resolve target directory. let targetDir: string; if (options.target) { targetDir = resolve(options.target); } else if (options.global) { targetDir = join(homedir(), '.claude', 'skills', 'specverse'); } else { // Default: project-local (current working directory). targetDir = join(process.env.SPECVERSE_USER_CWD || process.cwd(), '.claude', 'skills', 'specverse'); } mkdirSync(targetDir, { recursive: true }); mkdirSync(join(targetDir, 'reference'), { recursive: true }); mkdirSync(join(targetDir, 'workflows'), { recursive: true }); // 2. Helpers to resolve into installed packages. // For @specverse/self specifically, we fall back to walking up from // import.meta.url — handy when running the in-repo bootstrap CLI where // self can't resolve its own package.json via require. const selfRoot = (() => { try { const pkgJson = requireResolve.resolve('@specverse/self/package.json'); return dirname(pkgJson); } catch { let dir = dirname(fileURLToPath(import.meta.url)); for (let i = 0; i < 8; i++) { const candidate = join(dir, 'package.json'); if (existsSync(candidate)) { try { const pkg = JSON.parse(readFileSync(candidate, 'utf8')); if (pkg?.name === '@specverse/self') return dir; } catch { /* continue */ } } const parent = dirname(dir); if (parent === dir) break; dir = parent; } return null; } })(); const resolvePkg = (pkg: string, rel: string): string | null => { if (pkg === '@specverse/self') { return selfRoot ? join(selfRoot, rel) : null; } try { const pkgJson = requireResolve.resolve(pkg + '/package.json'); return join(dirname(pkgJson), rel); } catch { return null; } }; // 3. Copy SKILL.md from @specverse/self. const skillMdSrc = resolvePkg('@specverse/self', 'skills/specverse/SKILL.md'); if (!skillMdSrc || !existsSync(skillMdSrc)) { console.error('Cannot locate SKILL.md in @specverse/self — install failed.'); process.exit(1); } writeFileSync(join(targetDir, 'SKILL.md'), readFileSync(skillMdSrc, 'utf8')); // 4. Copy reference files (schema + ai-guidance + minimal-example). Per-task grounding // comes from workflows/ (the composed prompts), not a monolithic guide — see // specverse-self proposal 2026-06-06-RETIRE-COMPLETE-GUIDE-GROUNDING. const refCopies: [string, string, string][] = [ ['@specverse/entities', 'schema/SPECVERSE-SCHEMA.json', 'reference/schema.json'], ['@specverse/entities', 'schema/SPECVERSE-SCHEMA-AI.yaml', 'reference/ai-guidance.yaml'], ['@specverse/entities', 'schema/MINIMAL-SYNTAX-REFERENCE.specly', 'reference/minimal-example.specly'], ]; let refCopied = 0; for (const [pkg, rel, dest] of refCopies) { const src = resolvePkg(pkg, rel); if (src && existsSync(src)) { writeFileSync(join(targetDir, dest), readFileSync(src, 'utf8')); refCopied++; } else { console.warn(' (skipped ' + dest + ' — ' + pkg + '/' + rel + ' not found)'); } } // 5. Build cli-reference.md from the running CLI's own `--help`. // Minimal content — a pointer to the spv help and a static summary. const cliRef = [ '# SpecVerse CLI Reference', '', 'Run `spv --help` for flag details on any command. Common subcommands:', '', '| Command | Purpose |', '|---|---|', '| `spv init ` | Scaffold a new project (templates: default / full-stack / backend-only / frontend-only) |', '| `spv validate ` | Parse + schema-validate a .specly file |', '| `spv validate-bundle ` | Validate an entity bundle (for engine extenders) |', '| `spv infer ` | Expand a minimal spec to full architecture (controllers / services / events / views) |', '| `spv realize all ` | Generate production code from inferred spec + manifest |', '| `spv gen diagrams ` | Emit mermaid diagrams (ER, lifecycle, architecture) |', '| `spv ai template ` | Dump a canonical workflow prompt filled in for a spec |', '| `spv smoke` | Verify the `spv` install end-to-end |', '| `spv skill install [--global\\|--project]` | Reinstall / refresh this skill |', '', 'For CI invocations, see the `SPECVERSE-TOOLING.md` guide in `@specverse/self/docs/guides/`.', '', ].join('\n'); writeFileSync(join(targetDir, 'reference', 'cli-reference.md'), cliRef); // 6. Emit one workflow markdown per prompt YAML in @specverse/engines/assets/prompts/core/standard/default. const yamlLib = await import('js-yaml').catch(() => null) as any; const workflowsDir = resolvePkg('@specverse/engines', 'assets/prompts/core/standard/default'); let workflowCount = 0; if (yamlLib && workflowsDir && existsSync(workflowsDir)) { const entries = readdirSync(workflowsDir).filter((f: string) => f.endsWith('.prompt.yaml')); for (const entry of entries) { try { const parsed = yamlLib.load(readFileSync(join(workflowsDir, entry), 'utf8')) as any; if (!parsed?.name) continue; const md = buildWorkflowMarkdown(parsed); writeFileSync(join(targetDir, 'workflows', parsed.name + '.md'), md); workflowCount++; } catch (err: any) { console.warn(' (skipped workflow ' + entry + ' — ' + err.message + ')'); } } } else { console.warn(' (no workflows emitted — js-yaml or @specverse/engines prompts unavailable)'); } // 7. Copy the build-generated family skills (analyse / create / verify / // behavior / manifest) from @specverse/assets. These carry the AI-op // grounding that the claude-cli delivery path auto-loads (Phase 2b of // the prompt-partial-composition proposal): with the family skill // installed, `spv ai ` on claude-cli sends a thin prompt + lets the // CLI load the grounding from the skill (cached). They install as // SIBLINGS of the specverse skill at the skills root, using the same // tag the runtime loads (`SPECVERSE_PROMPT_TAG`, default `current`). const skillsRoot = dirname(targetDir); const promptTag = process.env.SPECVERSE_PROMPT_TAG || 'current'; const familySkillsDir = resolvePkg('@specverse/assets', 'prompts/_composed/' + promptTag + '/skills'); let familySkillCount = 0; const familyNames: string[] = []; if (familySkillsDir && existsSync(familySkillsDir)) { for (const family of readdirSync(familySkillsDir)) { const srcFamily = join(familySkillsDir, family); if (!existsSync(join(srcFamily, 'SKILL.md'))) continue; // only real skill dirs cpSync(srcFamily, join(skillsRoot, family), { recursive: true }); familySkillCount++; familyNames.push(family); } } else { console.warn(' (no family skills — @specverse/assets has no _composed/' + promptTag + '/skills; pre-Phase-2b assets)'); } // 8. Report. console.log('Installed SpecVerse skill → ' + targetDir); console.log(' SKILL.md + ' + refCopied + ' reference file(s) + ' + workflowCount + ' workflow(s)'); if (familySkillCount > 0) { console.log(' + ' + familySkillCount + ' family skill(s) [' + familyNames.join(', ') + '] → ' + skillsRoot); } if (!options.global && !options.target) { console.log(''); console.log('Tip: use --global to install to ~/.claude/skills/ for cross-project use.'); } function buildWorkflowMarkdown(parsed: any): string { const parts: string[] = []; parts.push('# SpecVerse workflow: ' + parsed.name); parts.push(''); if (parsed.description) { parts.push('> ' + parsed.description); parts.push(''); } const vars = Array.isArray(parsed?.user?.variables) ? parsed.user.variables : []; if (vars.length > 0) { parts.push('## Inputs'); parts.push(''); parts.push('| Variable | Required | Description |'); parts.push('|---|---|---|'); for (const v of vars) { const req = v.required === false ? 'no' : 'yes'; const desc = (v.description || '').replace(/\|/g, '\\|'); parts.push('| `' + v.name + '` | ' + req + ' | ' + desc + ' |'); } parts.push(''); } if (parsed?.system?.role) { parts.push('## Role'); parts.push(''); parts.push(String(parsed.system.role).trim()); parts.push(''); } if (parsed?.system?.context) { parts.push('## Instructions'); parts.push(''); parts.push(String(parsed.system.context).trim()); parts.push(''); } if (parsed?.user?.template) { parts.push('## Task template'); parts.push(''); parts.push('When invoking this workflow, fill the variables above into the template below:'); parts.push(''); parts.push('```'); parts.push(String(parsed.user.template).trim()); parts.push('```'); parts.push(''); } return parts.join('\n'); } } catch (error: any) { console.error('Error:', error.message); process.exit(1); } }); }