Files
reverse-skills-max/vendor-new/rev-skills/bin/install.mjs

131 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// bin/install.mjs — rev-skills 多工具安装器
import { cpSync, rmSync, symlinkSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createInterface } from 'node:readline';
import { convertAll } from './convert.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
export const SKILLS_SRC = join(__dirname, '..', '.claude', 'skills');
const TARGETS = {
claude: { mode: 'native', places: { global: '~/.claude/skills', project: '.claude/skills' } },
gemini: { mode: 'native', places: { global: '~/.gemini/skills', project: '.gemini/skills' } },
cline: { mode: 'native', places: { project: '.claude/skills' } },
codex: { mode: 'native', places: { global: '~/.codex/skills', project: '.codex/skills' } },
dsh: { mode: 'native', places: { global: '~/.dsh/skills', project: '.dsh/skills' } },
cursor: { mode: 'rule', places: { project: '.cursor/rules' }, ruleType: 'cursor' },
copilot: { mode: 'rule', places: { project: '.github' }, ruleType: 'copilot' },
windsurf: { mode: 'rule', places: { project: '.windsurf/rules' }, ruleType: 'windsurf' },
};
// 每个 native 目标的自检提示:技能根路径 + 该运行时里「看到技能」的方式
const VERIFY = {
claude: d => `Claude Code 中运行 /skills 查看,或 ls ${d}`,
gemini: d => `Gemini CLI 中查看技能目录,或 ls ${d}`,
cline: d => `Cline 中查看技能目录,或 ls ${d}`,
codex: d => `Codex 中查看技能目录,或 ls ${d}`,
dsh: d => `技能根 ${d};新建会话后 \`skill\` 工具目录可见(也可 ls ${d} 核对技能数)`,
};
function expandHome(p) { return p.replace(/^~/, process.env.HOME ?? process.env.USERPROFILE ?? '.'); }
function ask(q) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise(res => rl.question(q, a => { rl.close(); res(a.trim()); }));
}
export function planFor(target, scope, opts) {
const t = TARGETS[target];
if (!t) throw new Error(`unknown target: ${target} (valid: ${Object.keys(TARGETS).join(', ')}, all)`);
if (t.mode === 'native') {
if (!t.places[scope]) throw new Error(`target ${target} does not support --${scope}`);
const dest = expandHome(t.places[scope]);
const names = readdirSync(SKILLS_SRC);
return { mode: 'native', dest, names, link: opts.link };
}
if (t.mode === 'rule') {
if (scope !== 'project') throw new Error(`target ${target} only supports --project`);
return { mode: 'rule', dest: expandHome(t.places.project), ruleType: t.ruleType };
}
}
async function installTarget(target, scope, opts) {
const plan = planFor(target, scope, opts);
if (plan.mode === 'native') {
const conflicts = plan.names.filter(n => existsSync(join(plan.dest, n)));
const selfDestructive = resolve(plan.dest) === resolve(SKILLS_SRC);
if (!opts.dryRun) {
mkdirSync(plan.dest, { recursive: true });
for (const n of plan.names) {
const src = join(SKILLS_SRC, n), dst = join(plan.dest, n);
if (existsSync(dst)) {
if (opts.force && selfDestructive) {
console.log(`WARN: dest == source, skipping destructive overwrite for ${n}`);
continue;
}
if (opts.force) rmSync(dst, { recursive: true, force: true });
else { console.log(`SKIP ${n}: exists at ${dst} (use --force to overwrite)`); continue; }
}
if (plan.link && !opts.dryRun) {
try { symlinkSync(src, dst, 'dir'); }
catch { cpSync(src, dst, { recursive: true }); console.log(`LINK failed for ${n}, copied instead`); }
} else cpSync(src, dst, { recursive: true });
}
}
console.log(`[${target}] plan: ${plan.names.length} skills → ${plan.dest}${opts.link ? ' (symlink)' : ''}${conflicts.length ? `, ${conflicts.length} conflict(s): ${conflicts.join(', ')}` : ''}`);
console.log(`[${target}] verify: ${(VERIFY[target] ?? (d => `技能根 ${d}`))(plan.dest)}`);
} else {
const files = opts.dryRun ? [] : convertAll(plan.ruleType, plan.dest);
console.log(`[${target}] plan: convert ${readdirSync(SKILLS_SRC).length} skills → ${plan.dest}`);
if (files.length) console.log(`[${target}] generated: ${files.join(', ')}`);
}
}
async function main() {
const args = process.argv.slice(2);
const opts = {
target: 'claude', scope: null, dryRun: false, link: false, force: false,
yes: false, uninstall: args[0] === 'uninstall',
};
for (let i = opts.uninstall ? 1 : 0; i < args.length; i++) {
const a = args[i];
if (a === '--global') opts.scope = 'global';
else if (a === '--project') opts.scope = 'project';
else if (a === '--target') opts.target = args[++i] ?? 'claude';
else if (a === '--dry-run') opts.dryRun = true;
else if (a === '--link') opts.link = true;
else if (a === '--force') opts.force = true;
else if (a === '--yes') opts.yes = true;
}
const targets = opts.target === 'all' ? Object.keys(TARGETS) : [opts.target];
if (opts.uninstall) {
for (const t of targets) {
const scope = opts.scope ?? 'project';
const plan = planFor(t, scope, opts);
if (plan.mode === 'rule') { console.log(`[${t}] uninstall: remove ${plan.dest} manually (converted rules are plain files)`); continue; }
if (!opts.dryRun) { for (const n of readdirSync(SKILLS_SRC)) rmSync(join(plan.dest, n), { recursive: true, force: true }); }
console.log(`[${t}] removed skills from ${plan.dest}`);
}
return;
}
for (const t of targets) {
if (!TARGETS[t]) throw new Error(`unknown target: ${t} (valid: ${Object.keys(TARGETS).join(', ')}, all)`);
if (opts.scope === 'global' && !('global' in TARGETS[t].places)) {
console.log(`[${t}] skip: target 仅支持 --project,已跳过(--target all --global 组合)`);
continue;
}
let scope = opts.scope;
if (!scope) {
const tDef = TARGETS[t];
const hasGlobal = 'global' in tDef.places, hasProject = 'project' in tDef.places;
scope = hasGlobal && hasProject ? (opts.yes ? 'global' : await ask(`[${t}] 安装到全局还是项目?(global/project) `)) : (hasGlobal ? 'global' : 'project');
}
await installTarget(t, scope, opts);
}
}
if (process.argv[1] && process.argv[1].endsWith('install.mjs')) {
main().catch(e => { console.error(`ERROR: ${e.message}`); process.exit(1); });
}