reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
130
vendor-new/rev-skills/bin/install.mjs
Normal file
130
vendor-new/rev-skills/bin/install.mjs
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/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); });
|
||||
}
|
||||
Reference in New Issue
Block a user