2026-01-24 23:28:43 -07:00

266 lines
7.1 KiB
JavaScript

#!/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> [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 <skills...>
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=<name> 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);
}