mirror of
https://github.com/jnMetaCode/superpowers-zh.git
synced 2026-09-02 22:54:06 +08:00
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:
@@ -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 下我们装过的 agent(agents 源是平铺的文件,例如 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user