fix: 加 --uninstall 子命令、home 目录守护、计数显示修复(#17)

- 修复在 ~ 直接安装会污染主目录的问题:默认拒绝,需 --force
- 新增 npx superpowers-zh --uninstall:清理 skills、bootstrap 文件,
  保留用户在 CLAUDE.md/HERMES.md 中的原有内容
- 修复多工具检测时计数叠加显示(21/23/101 → 全部按源数 20 显示)
- copyDirSync 跳过 .DS_Store,停止散播 macOS 元数据
This commit is contained in:
AI不止语
2026-05-05 18:01:43 +08:00
parent 8298e9f865
commit 85fd3aeda2

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env node
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, copyFileSync, lstatSync, realpathSync } from 'fs';
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, copyFileSync, lstatSync, realpathSync, rmSync } from 'fs';
import { resolve, dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { homedir } from 'os';
// 手动递归复制:跨 Node 版本和操作系统行为一致
// 不使用 cpSync —— 在 Windows + npx 缓存(含 junction+ Node 16.7-18 下不稳定
@@ -14,6 +15,7 @@ function copyDirSync(src, dest) {
mkdirSync(dest, { recursive: true });
const entries = readdirSync(realSrc, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === '.DS_Store') continue;
const srcPath = join(realSrc, entry.name);
const destPath = join(dest, entry.name);
let stat;
@@ -380,6 +382,8 @@ function showHelp() {
用法:
npx superpowers-zh 自动检测工具并安装
npx superpowers-zh --tool cursor 指定工具安装(检测不到时使用)
npx superpowers-zh --uninstall 卸载当前目录下的 superpowers-zh
npx superpowers-zh --force 允许在用户主目录(~)安装(默认拒绝)
npx superpowers-zh --help 显示帮助
npx superpowers-zh --version 显示版本
@@ -392,6 +396,9 @@ function showHelp() {
npx superpowers-zh --tool cursor
npx superpowers-zh --tool trae
误装到主目录可以这样清理:
cd ~ && npx superpowers-zh --uninstall
项目https://github.com/jnMetaCode/superpowers-zh
`);
}
@@ -401,8 +408,8 @@ function installForTarget(target) {
const srcCount = countDirs(SKILLS_SRC);
mkdirSync(dest, { recursive: true });
copyDirSync(SKILLS_SRC, dest);
const count = countDirs(dest);
if (srcCount > 0 && count === 0) {
const totalAfter = countDirs(dest);
if (srcCount > 0 && totalAfter === 0) {
throw new Error(
`复制 skills 失败:源目录 ${SKILLS_SRC}${srcCount} 个 skill但目标 ${dest} 为空。` +
`\n 这通常是 npx 缓存目录权限或路径问题。请尝试:\n` +
@@ -411,7 +418,7 @@ function installForTarget(target) {
` 3. 或手动克隆复制: 见 https://github.com/jnMetaCode/superpowers-zh#方式二手动安装`
);
}
console.log(`${target.name}: ${count} 个 skills -> ${dest}`);
console.log(`${target.name}: ${srcCount} 个 skills -> ${dest}`);
if (target.name === 'Trae') {
generateTraeBootstrapRule(PROJECT_DIR);
@@ -443,7 +450,139 @@ function installForTarget(target) {
}
}
function install(forceToolName) {
function isHomeDir(p) {
try {
const a = realpathSync(p);
const b = realpathSync(homedir());
return a === b;
} catch { return resolve(p) === resolve(homedir()); }
}
// 卸载支持:完整删除的 bootstrap 文件、需要清理段落的 bootstrap 文件
const BOOTSTRAP_DELETE = [
'.trae/rules/superpowers-zh.md',
'.antigravity/rules.md',
];
const BOOTSTRAP_CLEAN_SECTION = [
'CLAUDE.md',
'GEMINI.md',
'HERMES.md',
'CONVENTIONS.md',
];
const BOOTSTRAP_SECTION_MARKERS = [
'# Superpowers-ZH 中文增强版',
'# Superpowers-ZH 工作方法论',
];
function cleanBootstrapSection(filePath) {
if (!existsSync(filePath)) return false;
const content = readFileSync(filePath, 'utf8');
let idx = -1;
for (const marker of BOOTSTRAP_SECTION_MARKERS) {
const i = content.indexOf(marker);
if (i !== -1 && (idx === -1 || i < idx)) idx = i;
}
if (idx === -1) return false;
// 段落结束:下一个一级标题(行首 "# ")或文件末尾
let end = content.length;
const after = content.indexOf('\n# ', idx + 1);
if (after !== -1) end = after + 1;
const cleaned = (content.slice(0, idx) + content.slice(end)).replace(/\s+$/, '') + '\n';
if (cleaned.trim().length === 0) {
rmSync(filePath);
} else {
writeFileSync(filePath, cleaned, 'utf8');
}
return true;
}
function uninstallForTarget(target, srcSkillNames) {
const dest = resolve(PROJECT_DIR, target.dir);
if (!existsSync(dest)) return 0;
let removed = 0;
for (const entry of readdirSync(dest, { withFileTypes: true })) {
if (entry.isDirectory() && srcSkillNames.has(entry.name)) {
rmSync(resolve(dest, entry.name), { recursive: true, force: true });
removed++;
}
}
// 如果目录已空(或仅剩 .DS_Store顺手清掉避免留下空骨架
try {
if (existsSync(dest)) {
const left = readdirSync(dest).filter(n => n !== '.DS_Store');
if (left.length === 0) rmSync(dest, { recursive: true, force: true });
}
} catch {}
return removed;
}
function uninstall() {
console.log(`\n superpowers-zh v${PKG.version} — 卸载\n`);
console.log(` 目标项目: ${PROJECT_DIR}\n`);
if (!existsSync(SKILLS_SRC)) {
console.error(' ❌ 错误skills 源目录不存在,无法识别要卸载的 skill 名单。');
process.exit(1);
}
const srcSkillNames = new Set(
readdirSync(SKILLS_SRC, { withFileTypes: true })
.filter(e => e.isDirectory())
.map(e => e.name)
);
let totalSkills = 0;
for (const target of TARGETS) {
const removed = uninstallForTarget(target, srcSkillNames);
if (removed > 0) {
console.log(`${target.name}: 移除 ${removed} 个 skills <- ${resolve(PROJECT_DIR, target.dir)}`);
totalSkills += removed;
}
}
// 顺带清理 .claude/agents 下我们装过的 agentagents 源是平铺的文件,例如 code-reviewer.md
const agentsDest = resolve(PROJECT_DIR, '.claude', 'agents');
if (existsSync(AGENTS_SRC) && existsSync(agentsDest)) {
const srcAgentNames = new Set(readdirSync(AGENTS_SRC).filter(n => n !== '.DS_Store'));
let agentsRemoved = 0;
for (const entry of readdirSync(agentsDest)) {
if (srcAgentNames.has(entry)) {
rmSync(resolve(agentsDest, entry), { recursive: true, force: true });
agentsRemoved++;
}
}
if (agentsRemoved > 0) console.log(` ✅ Claude Code agents: 移除 ${agentsRemoved} 个 -> ${agentsDest}`);
try {
const left = readdirSync(agentsDest).filter(n => n !== '.DS_Store');
if (left.length === 0) rmSync(agentsDest, { recursive: true, force: true });
} catch {}
}
let bootstrapsRemoved = 0;
for (const rel of BOOTSTRAP_DELETE) {
const full = resolve(PROJECT_DIR, rel);
if (existsSync(full)) {
rmSync(full);
console.log(` ✅ 删除 bootstrap: ${full}`);
bootstrapsRemoved++;
}
}
for (const rel of BOOTSTRAP_CLEAN_SECTION) {
const full = resolve(PROJECT_DIR, rel);
if (cleanBootstrapSection(full)) {
console.log(` ✅ 清理 bootstrap: ${full}`);
bootstrapsRemoved++;
}
}
if (totalSkills === 0 && bootstrapsRemoved === 0) {
console.log(' ⚠️ 未在当前目录找到 superpowers-zh 安装痕迹。');
} else {
console.log(`\n 卸载完成。共移除 ${totalSkills} 个 skill 目录、${bootstrapsRemoved} 个 bootstrap 文件。\n`);
}
}
function install(forceToolName, force) {
try {
console.log(`\n superpowers-zh v${PKG.version} — AI 编程超能力中文版\n`);
@@ -452,6 +591,27 @@ function install(forceToolName) {
process.exit(1);
}
if (!force && isHomeDir(PROJECT_DIR)) {
console.error(
` ⚠️ 当前目录是用户主目录: ${PROJECT_DIR}
superpowers-zh 应该装到具体项目目录,而不是 ~/。
在主目录安装会把 skills 和 bootstrap 文件CLAUDE.md / HERMES.md 等)
写入你的 home污染所有项目。
请先 cd 到项目目录:
cd /path/to/your/project
npx superpowers-zh
如果你确实要在主目录安装(不推荐),加 --force
npx superpowers-zh --force
如果你已经在主目录误装过,可以用 --uninstall 清理:
npx superpowers-zh --uninstall
`);
process.exit(1);
}
console.log(` 源: ${countDirs(SKILLS_SRC)} 个 skills`);
console.log(` 目标项目: ${PROJECT_DIR}\n`);
@@ -512,11 +672,16 @@ const args = process.argv.slice(2);
const helpIdx = args.findIndex(a => a === '--help' || a === '-h');
const versionIdx = args.findIndex(a => a === '--version' || a === '-v');
const toolIdx = args.findIndex(a => a === '--tool' || a === '-t');
const uninstallIdx = args.findIndex(a => a === '--uninstall' || a === '-u');
const forceIdx = args.findIndex(a => a === '--force' || a === '-f');
const force = forceIdx !== -1;
if (helpIdx !== -1) {
showHelp();
} else if (versionIdx !== -1) {
console.log(PKG.version);
} else if (uninstallIdx !== -1) {
uninstall();
} else if (toolIdx !== -1) {
const toolArg = args[toolIdx + 1];
if (!toolArg) {
@@ -530,11 +695,11 @@ if (helpIdx !== -1) {
console.error(` 支持的工具: ${Object.keys(TOOL_ALIASES).join(', ')}\n`);
process.exit(1);
}
install(toolName);
} else if (args.length > 0 && args[0].startsWith('-')) {
install(toolName, force);
} else if (args.length > 0 && args[0].startsWith('-') && forceIdx === -1) {
console.warn(` 未知参数: ${args[0]}\n`);
showHelp();
process.exit(1);
} else {
install();
install(undefined, force);
}