added scripts
This commit is contained in:
parent
c1ea14975e
commit
cec499fbb6
265
cli.js
Normal file
265
cli.js
Normal 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
168
install.sh
Normal 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
31
package.json
Normal 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
60
scripts/generate_index.py
Normal 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
104
scripts/validate.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user