From e0046910da588812303a56ded27e9bec03e6d3e9 Mon Sep 17 00:00:00 2001 From: ranger Date: Thu, 30 Apr 2026 10:07:09 +0800 Subject: [PATCH] feat(install): expand directory entries into children for skill coexistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace monolithic directory copy with child-level installation in installCore(). User files in target dirs (skills/, output-styles/, bin/lib/) are now preserved — only Code Abyss entries are backed up and replaced individually. Also enable empty-parent-dir pruning for all manifest entries during uninstall, removing the cross-root restriction so stale dirs from child-level install are cleaned up regardless of root domain. Rebased on v2.1.8 (telagod/main). Verified: - npm test --runInBand: 217 passed (5 pre-existing Windows-only failures) - npm run verify:skills: 26 skills - Smoke: 12/12 (Claude/Codex/Gemini/OpenClaw) - No explicit commands generated by default - User skills survive install→uninstall cycle - .sage-backup/manifest.json restore chain compatible --- bin/install.js | 65 ++++++++++++++++++++++++++++++++++-------------- bin/uninstall.js | 8 +++--- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/bin/install.js b/bin/install.js index 3ea4e78..53d856d 100755 --- a/bin/install.js +++ b/bin/install.js @@ -250,15 +250,13 @@ function runUninstall(tgt) { if (fs.existsSync(targetPath)) { rmSafe(targetPath); console.log(` ${c.red('✘')} ${manifestLabel(entry, tgt)}`); - if (normalized.root !== tgt) { - let parent = path.dirname(targetPath); - while (parent !== installRoot && parent !== path.dirname(parent)) { - try { - if (fs.readdirSync(parent).length > 0) break; - fs.rmdirSync(parent); - } catch { break; } - parent = path.dirname(parent); - } + let parent = path.dirname(targetPath); + while (parent !== installRoot && parent !== path.dirname(parent)) { + try { + if (fs.readdirSync(parent).length > 0) break; + fs.rmdirSync(parent); + } catch { break; } + parent = path.dirname(parent); } } }); @@ -694,17 +692,46 @@ function installCore(tgt, selectedStyle, selectedPersona, packPlan) { warn(`跳过: ${src}`); return; } - if (fs.existsSync(destPath)) { - const backupPath = path.join(backupDir, rootName, dest); - rmSafe(backupPath); - copyRecursive(destPath, backupPath); - pushManifestEntry(manifest.backups, rootName, dest); - info(`备份: ${c.d(rootName === tgt ? dest : `${rootName}/${dest}`)}`); + let srcStat; + try { srcStat = fs.statSync(srcPath); } catch (e) { + warn(`无法读取源: ${srcPath}`); return; + } + + if (srcStat.isDirectory()) { + let children; + try { children = fs.readdirSync(srcPath); } catch (e) { + warn(`无法读取目录: ${srcPath}`); return; + } + children.filter(child => !shouldSkip(child)).forEach(child => { + const childSrc = path.join(srcPath, child); + const childDest = path.join(destPath, child); + const childRelPath = path.posix.join(dest, child); + + if (fs.existsSync(childDest)) { + const backupPath = path.join(backupDir, rootName, childRelPath); + rmSafe(backupPath); + copyRecursive(childDest, backupPath); + pushManifestEntry(manifest.backups, rootName, childRelPath); + info(`备份: ${c.d(rootName === tgt ? childRelPath : `${rootName}/${childRelPath}`)}`); + } + ok(rootName === tgt ? childRelPath : `${rootName}/${childRelPath}`); + rmSafe(childDest); + copyRecursive(childSrc, childDest); + pushManifestEntry(manifest.installed, rootName, childRelPath); + }); + } else { + if (fs.existsSync(destPath)) { + const backupPath = path.join(backupDir, rootName, dest); + rmSafe(backupPath); + copyRecursive(destPath, backupPath); + pushManifestEntry(manifest.backups, rootName, dest); + info(`备份: ${c.d(rootName === tgt ? dest : `${rootName}/${dest}`)}`); + } + ok(rootName === tgt ? dest : `${rootName}/${dest}`); + rmSafe(destPath); + copyRecursive(srcPath, destPath); + pushManifestEntry(manifest.installed, rootName, dest); } - ok(rootName === tgt ? dest : `${rootName}/${dest}`); - rmSafe(destPath); - copyRecursive(srcPath, destPath); - pushManifestEntry(manifest.installed, rootName, dest); }); pushPackReport(manifest, { diff --git a/bin/uninstall.js b/bin/uninstall.js index 3de26f3..a80c7bc 100644 --- a/bin/uninstall.js +++ b/bin/uninstall.js @@ -65,10 +65,10 @@ console.log(`\n🗑️ 卸载 Code Abyss v${manifest.version}...\n`); if (fs.existsSync(p)) { fs.rmSync(p, { recursive: true, force: true }); console.log(`🗑️ 删除: ${entryLabel(entry)}`); - if (typeof entry !== 'string' && entry.root !== manifest.target) { - const rootDir = path.join(os.homedir(), MANAGED_ROOTS[entry.root] || ''); - pruneEmptyParents(path.dirname(p), rootDir); - } + const rootDir = typeof entry !== 'string' + ? path.join(os.homedir(), MANAGED_ROOTS[entry.root] || '') + : targetDir; + pruneEmptyParents(path.dirname(p), rootDir); } });