sync-skills.js 3.7 KB
#!/usr/bin/env node

/**
 * sync-skills.js — 将 skills/ 目录下的共享 Skill 同步到各 CLI 配置目录
 *
 * 源目录: skills/
 * 目标目录:
 *   - .claude/skills/  (SKILL.md,原样复制)
 *
 * 用法:
 *   node scripts/sync-skills.js           # 同步所有 skill
 *   node scripts/sync-skills.js innovator # 只同步指定 skill
 */

const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const SOURCE_DIR = path.join(ROOT, 'skills');

// 同步目标配置
const TARGETS = [
    {
        name: 'claude',
        dir: path.join(ROOT, '.claude', 'skills'),
        // Claude Code: SKILL.md 原样保留
        renameMap: {},
    },
];

/**
 * 递归复制目录,支持文件名重命名映射
 * @param {Set} [skipDirs] - 要跳过的子目录名集合
 */
function copyDirSync(src, dest, renameMap = {}, skipDirs = new Set()) {
    fs.mkdirSync(dest, { recursive: true });

    for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
        const srcPath = path.join(src, entry.name);
        const destName = renameMap[entry.name] || entry.name;
        const destPath = path.join(dest, destName);

        if (entry.isDirectory()) {
            if (skipDirs.has(entry.name)) continue; // 跳过内部目录
            copyDirSync(srcPath, destPath, renameMap, skipDirs);
        } else {
            fs.copyFileSync(srcPath, destPath);
        }
    }
}

/**
 * 递归删除目录
 */
function rmDirSync(dir) {
    if (fs.existsSync(dir)) {
        fs.rmSync(dir, { recursive: true, force: true });
    }
}

// Innovator 内部目录,不同步到 CLI
const INTERNAL_DIRS = new Set(['hunter', 'digest', 'evolve']);

/**
 * 同步指定的 skill 到所有目标
 */
function syncSkill(skillName) {
    const srcDir = path.join(SOURCE_DIR, skillName);
    if (!fs.existsSync(srcDir)) {
        console.error(`  ✗ 源目录不存在: skills/${skillName}/`);
        return false;
    }

    for (const target of TARGETS) {
        const destDir = path.join(target.dir, skillName);

        // 清空目标目录(确保删除旧文件)
        rmDirSync(destDir);

        // 复制(跳过 hunter/digest/evolve 内部目录,带重命名映射)
        copyDirSync(srcDir, destDir, target.renameMap, INTERNAL_DIRS);

        const renames = Object.entries(target.renameMap)
            .map(([from, to]) => `${from}→${to}`)
            .join(', ');
        const renameNote = renames ? ` (${renames})` : '';
        console.log(`  ✓ .${target.name}/skills/${skillName}/${renameNote}`);
    }

    return true;
}

// ─── Main ───

function main() {
    console.log('[sync-skills] 同步共享 Skill 到 CLI 配置目录\n');

    // 检查源目录
    if (!fs.existsSync(SOURCE_DIR)) {
        console.error(`源目录不存在: ${SOURCE_DIR}`);
        process.exit(1);
    }

    // 确定要同步的 skill 列表
    const filterSkills = process.argv.slice(2);
    const allSkills = fs.readdirSync(SOURCE_DIR, { withFileTypes: true })
        .filter(d => d.isDirectory())
        .map(d => d.name);

    const skillsToSync = filterSkills.length > 0
        ? filterSkills.filter(s => allSkills.includes(s))
        : allSkills;

    if (skillsToSync.length === 0) {
        console.log('没有需要同步的 Skill。');
        return;
    }

    console.log(`源: skills/ (${allSkills.length} 个 skill)`);
    console.log(`目标: .claude/skills/`);
    console.log(`同步: ${skillsToSync.join(', ')}\n`);

    let synced = 0;
    for (const skillName of skillsToSync) {
        console.log(`[${skillName}]`);
        if (syncSkill(skillName)) synced++;
    }

    console.log(`\n✅ 同步完成: ${synced} 个 skill → ${TARGETS.length} 个目标`);
}

main();