#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const os = require('os'); const SKILLS_DIR = path.join(__dirname, 'skills'); // Agent configurations const AGENTS = { 'claude-code': { name: 'Claude Code', global: path.join(os.homedir(), '.claude', 'skills'), local: '.claude/skills', detect: () => fs.existsSync(path.join(os.homedir(), '.claude')) }, 'opencode': { name: 'OpenCode', global: path.join(os.homedir(), '.config', 'opencode', 'skills'), local: '.opencode/skills', detect: () => fs.existsSync(path.join(os.homedir(), '.config', 'opencode')) }, 'cursor': { name: 'Cursor', global: path.join(os.homedir(), '.cursor', 'skills'), local: '.cursor/skills', detect: () => fs.existsSync(path.join(os.homedir(), '.cursor')) }, 'gemini': { name: 'Gemini CLI', global: path.join(os.homedir(), '.gemini', 'skills'), local: '.gemini/skills', detect: () => fs.existsSync(path.join(os.homedir(), '.gemini')) } }; function getAvailableSkills() { if (!fs.existsSync(SKILLS_DIR)) return []; return fs.readdirSync(SKILLS_DIR).filter(name => { const skillPath = path.join(SKILLS_DIR, name, 'SKILL.md'); return fs.existsSync(skillPath); }); } function detectAgents() { return Object.entries(AGENTS) .filter(([_, config]) => config.detect()) .map(([id, config]) => ({ id, ...config })); } function ensureDir(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } function createSymlink(source, target) { if (fs.existsSync(target)) { const stats = fs.lstatSync(target); if (stats.isSymbolicLink()) { fs.unlinkSync(target); } else { console.log(` skip: ${path.basename(target)} (already exists, not a symlink)`); return false; } } fs.symlinkSync(source, target); return true; } function install(args) { const skills = getAvailableSkills(); const detectedAgents = detectAgents(); // Parse arguments let targetSkills = []; let targetAgent = null; let useGlobal = true; for (const arg of args) { if (arg === '--all') { targetSkills = skills; } else if (arg === '--local') { useGlobal = false; } else if (arg.startsWith('--agent=')) { targetAgent = arg.split('=')[1]; } else if (!arg.startsWith('-')) { if (skills.includes(arg)) { targetSkills.push(arg); } else { console.log(`warning: skill '${arg}' not found, skipping`); } } } // Default to all skills if none specified if (targetSkills.length === 0) { targetSkills = skills; } // Determine which agents to install to let agents = []; if (targetAgent) { if (AGENTS[targetAgent]) { agents = [{ id: targetAgent, ...AGENTS[targetAgent] }]; } else { console.log(`error: unknown agent '${targetAgent}'`); console.log(`available agents: ${Object.keys(AGENTS).join(', ')}`); process.exit(1); } } else if (detectedAgents.length > 0) { agents = detectedAgents; } else { // Default to claude-code if nothing detected agents = [{ id: 'claude-code', ...AGENTS['claude-code'] }]; } console.log(`\nvfx-skills installer\n`); console.log(`skills to install: ${targetSkills.length}`); console.log(`target agents: ${agents.map(a => a.name).join(', ')}`); console.log(`install type: ${useGlobal ? 'global' : 'local'}\n`); for (const agent of agents) { const targetDir = useGlobal ? agent.global : path.join(process.cwd(), agent.local); console.log(`installing to ${agent.name} (${targetDir})...`); ensureDir(targetDir); let installed = 0; let skipped = 0; for (const skill of targetSkills) { const source = path.join(SKILLS_DIR, skill); const target = path.join(targetDir, skill); if (createSymlink(source, target)) { installed++; } else { skipped++; } } console.log(` done: ${installed} installed, ${skipped} skipped\n`); } console.log('installation complete!'); console.log('restart your agent to load the new skills.\n'); } function list() { const skills = getAvailableSkills(); console.log(`\navailable skills (${skills.length}):\n`); for (const skill of skills) { const skillPath = path.join(SKILLS_DIR, skill, 'SKILL.md'); const content = fs.readFileSync(skillPath, 'utf-8'); // Extract description from frontmatter const match = content.match(/^---\s*\n[\s\S]*?description:\s*(.+)\n[\s\S]*?---/); const desc = match ? match[1].trim() : '(no description)'; console.log(` ${skill}`); console.log(` ${desc}\n`); } } function uninstall(args) { const detectedAgents = detectAgents(); const skills = args.filter(a => !a.startsWith('-')); if (skills.length === 0) { console.log('usage: vfx-skills uninstall [skill-name...]'); process.exit(1); } let targetAgent = null; for (const arg of args) { if (arg.startsWith('--agent=')) { targetAgent = arg.split('=')[1]; } } let agents = targetAgent ? [{ id: targetAgent, ...AGENTS[targetAgent] }] : detectedAgents.length > 0 ? detectedAgents : [{ id: 'claude-code', ...AGENTS['claude-code'] }]; for (const agent of agents) { console.log(`removing from ${agent.name}...`); for (const skill of skills) { const target = path.join(agent.global, skill); if (fs.existsSync(target)) { const stats = fs.lstatSync(target); if (stats.isSymbolicLink()) { fs.unlinkSync(target); console.log(` removed: ${skill}`); } else { console.log(` skip: ${skill} (not a symlink)`); } } else { console.log(` skip: ${skill} (not found)`); } } } console.log('\ndone!'); } function help() { console.log(` vfx-skills - VFX skills for Claude Code and other AI agents usage: npx vfx-skills install [skills...] [options] npx vfx-skills uninstall npx vfx-skills list commands: install Install skills (default: all skills, global install) uninstall Remove installed skills list List available skills install options: --all Install all skills (default if no skills specified) --local Install to current project instead of global --agent= Target specific agent (claude-code, opencode, cursor, gemini) examples: npx vfx-skills install # Install all skills globally npx vfx-skills install nuke-scripting # Install specific skill npx vfx-skills install --local # Install to current project npx vfx-skills install --agent=opencode # Install for OpenCode only npx vfx-skills list # Show available skills npx vfx-skills uninstall aces-vfx # Remove a skill `); } // Main const [,, command, ...args] = process.argv; switch (command) { case 'install': install(args); break; case 'uninstall': case 'remove': uninstall(args); break; case 'list': case 'ls': list(); break; case 'help': case '--help': case '-h': case undefined: help(); break; default: console.log(`unknown command: ${command}`); help(); process.exit(1); }