added scripts

This commit is contained in:
Nicholai Vogel 2026-01-24 23:28:43 -07:00
parent c1ea14975e
commit cec499fbb6
5 changed files with 628 additions and 0 deletions

265
cli.js Normal file
View File

@ -0,0 +1,265 @@
#!/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);
}

168
install.sh Normal file
View File

@ -0,0 +1,168 @@
#!/bin/bash
# vfx-skills installer
# Symlinks VFX skills to your AI agent's skills directory
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILLS_DIR="$SCRIPT_DIR/skills"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
print_header() {
echo ""
echo "vfx-skills installer"
echo "===================="
echo ""
}
detect_agents() {
local agents=()
if [ -d "$HOME/.claude" ]; then
agents+=("claude-code:$HOME/.claude/skills")
fi
if [ -d "$HOME/.config/opencode" ]; then
agents+=("opencode:$HOME/.config/opencode/skills")
fi
if [ -d "$HOME/.cursor" ]; then
agents+=("cursor:$HOME/.cursor/skills")
fi
if [ -d "$HOME/.gemini" ]; then
agents+=("gemini:$HOME/.gemini/skills")
fi
echo "${agents[@]}"
}
install_skills() {
local target_dir="$1"
local agent_name="$2"
echo -e "installing to ${GREEN}$agent_name${NC} ($target_dir)..."
# Create target directory if it doesn't exist
mkdir -p "$target_dir"
local installed=0
local skipped=0
for skill_dir in "$SKILLS_DIR"/*/; do
if [ -f "$skill_dir/SKILL.md" ]; then
skill_name=$(basename "$skill_dir")
target="$target_dir/$skill_name"
if [ -L "$target" ]; then
# Remove existing symlink
rm "$target"
elif [ -e "$target" ]; then
echo -e " ${YELLOW}skip${NC}: $skill_name (already exists, not a symlink)"
((skipped++))
continue
fi
ln -s "$skill_dir" "$target"
((installed++))
fi
done
echo -e " done: ${GREEN}$installed installed${NC}, $skipped skipped"
echo ""
}
# Parse arguments
TARGET_AGENT=""
LOCAL_INSTALL=false
while [[ $# -gt 0 ]]; do
case $1 in
--agent=*)
TARGET_AGENT="${1#*=}"
shift
;;
--local)
LOCAL_INSTALL=true
shift
;;
--help|-h)
echo "usage: ./install.sh [options]"
echo ""
echo "options:"
echo " --agent=NAME Install for specific agent (claude-code, opencode, cursor, gemini)"
echo " --local Install to current directory instead of global"
echo " --help Show this help"
exit 0
;;
*)
echo "unknown option: $1"
exit 1
;;
esac
done
print_header
# Check skills directory exists
if [ ! -d "$SKILLS_DIR" ]; then
echo -e "${RED}error${NC}: skills directory not found at $SKILLS_DIR"
exit 1
fi
# Count available skills
skill_count=$(find "$SKILLS_DIR" -maxdepth 2 -name "SKILL.md" | wc -l)
echo "found $skill_count skills"
if $LOCAL_INSTALL; then
# Install to current directory
echo "installing to current project..."
install_skills ".claude/skills" "local project"
else
# Detect and install to agents
if [ -n "$TARGET_AGENT" ]; then
case $TARGET_AGENT in
claude-code)
install_skills "$HOME/.claude/skills" "Claude Code"
;;
opencode)
install_skills "$HOME/.config/opencode/skills" "OpenCode"
;;
cursor)
install_skills "$HOME/.cursor/skills" "Cursor"
;;
gemini)
install_skills "$HOME/.gemini/skills" "Gemini CLI"
;;
*)
echo -e "${RED}error${NC}: unknown agent '$TARGET_AGENT'"
echo "available agents: claude-code, opencode, cursor, gemini"
exit 1
;;
esac
else
# Auto-detect agents
agents=$(detect_agents)
if [ -z "$agents" ]; then
# Default to claude-code if nothing detected
echo "no agents detected, defaulting to Claude Code..."
install_skills "$HOME/.claude/skills" "Claude Code"
else
for agent in $agents; do
name="${agent%%:*}"
path="${agent#*:}"
install_skills "$path" "$name"
done
fi
fi
fi
echo -e "${GREEN}installation complete!${NC}"
echo "restart your agent to load the new skills."
echo ""

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "vfx-skills",
"version": "1.0.0",
"description": "Claude Code skills for VFX workflows - Nuke, ACES, ffmpeg, EXR wrangling",
"keywords": [
"vfx",
"nuke",
"compositing",
"claude-code",
"ai-skills",
"aces",
"ffmpeg",
"exr"
],
"author": "Biohazard VFX",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/biohazard-vfx/vfx-skills.git"
},
"bin": {
"vfx-skills": "./cli.js"
},
"files": [
"cli.js",
"skills/**/*"
],
"engines": {
"node": ">=18"
}
}

60
scripts/generate_index.py Normal file
View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
Generate skills_index.json from all skills in the repository.
"""
import os
import json
import re
def generate_index(skills_dir, output_file):
print(f"generating index from: {skills_dir}\n")
skills = []
for entry in sorted(os.listdir(skills_dir)):
skill_path = os.path.join(skills_dir, entry)
skill_file = os.path.join(skill_path, "SKILL.md")
if not os.path.isdir(skill_path) or not os.path.exists(skill_file):
continue
with open(skill_file, 'r', encoding='utf-8') as f:
content = f.read()
skill_info = {
"id": entry,
"name": entry.replace("-", " ").title(),
"description": "",
"path": f"skills/{entry}"
}
# Extract from frontmatter
fm_match = re.search(r'^---\s*\n(.*?)\n---', content, re.DOTALL)
if fm_match:
fm_content = fm_match.group(1)
name_match = re.search(r'^name:\s*(.+)$', fm_content, re.MULTILINE)
if name_match:
skill_info["name"] = name_match.group(1).strip()
desc_match = re.search(r'^description:\s*(.+)$', fm_content, re.MULTILINE)
if desc_match:
skill_info["description"] = desc_match.group(1).strip()
skills.append(skill_info)
print(f" {skill_info['id']}: {skill_info['description'][:60]}...")
# Write index
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(skills, f, indent=2)
print(f"\ngenerated index with {len(skills)} skills at: {output_file}")
return skills
if __name__ == "__main__":
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
skills_path = os.path.join(base_dir, "skills")
output_path = os.path.join(base_dir, "skills_index.json")
generate_index(skills_path, output_path)

104
scripts/validate.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
Validate all skills in the repository.
Checks for required frontmatter fields and proper structure.
"""
import os
import re
import sys
def validate_skills(skills_dir):
print(f"validating skills in: {skills_dir}\n")
errors = []
warnings = []
skill_count = 0
for entry in sorted(os.listdir(skills_dir)):
skill_path = os.path.join(skills_dir, entry)
skill_file = os.path.join(skill_path, "SKILL.md")
if not os.path.isdir(skill_path):
continue
if not os.path.exists(skill_file):
errors.append(f"{entry}: missing SKILL.md")
continue
skill_count += 1
with open(skill_file, 'r', encoding='utf-8') as f:
content = f.read()
# Check for frontmatter
if not content.strip().startswith("---"):
errors.append(f"{entry}: missing frontmatter (must start with ---)")
continue
# Extract frontmatter
fm_match = re.search(r'^---\s*\n(.*?)\n---', content, re.DOTALL)
if not fm_match:
errors.append(f"{entry}: malformed frontmatter")
continue
fm_content = fm_match.group(1)
# Check required fields
name_match = re.search(r'^name:\s*(.+)$', fm_content, re.MULTILINE)
desc_match = re.search(r'^description:\s*(.+)$', fm_content, re.MULTILINE)
if not name_match:
errors.append(f"{entry}: frontmatter missing 'name' field")
else:
name = name_match.group(1).strip()
# Validate name format
if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', name):
errors.append(f"{entry}: name must be lowercase alphanumeric with hyphens")
if name != entry:
warnings.append(f"{entry}: name '{name}' doesn't match directory name")
if not desc_match:
errors.append(f"{entry}: frontmatter missing 'description' field")
else:
desc = desc_match.group(1).strip()
if len(desc) < 10:
warnings.append(f"{entry}: description seems too short")
if len(desc) > 1024:
errors.append(f"{entry}: description exceeds 1024 characters")
# Check for content after frontmatter
body = content[fm_match.end():].strip()
if len(body) < 50:
warnings.append(f"{entry}: skill content seems too short")
# Print results
print(f"checked {skill_count} skills\n")
if warnings:
print("warnings:")
for w in warnings:
print(f" {w}")
print()
if errors:
print("errors:")
for e in errors:
print(f" {e}")
print()
print(f"validation failed with {len(errors)} errors")
return False
print("all skills passed validation!")
return True
if __name__ == "__main__":
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
skills_path = os.path.join(base_dir, "skills")
if not os.path.exists(skills_path):
print(f"error: skills directory not found at {skills_path}")
sys.exit(1)
success = validate_skills(skills_path)
sys.exit(0 if success else 1)