feat(M4): Reusable code abstractions and dead-code removal
This commit is contained in:
@@ -532,6 +532,24 @@ export async function buildOperationPlan({ selections, repoRoot = process.cwd(),
|
||||
return { operations, prompts, reportRows, assumeYes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the target of an operation (skill, helper, or superpowers).
|
||||
*
|
||||
* Validates that the target is within the skills root before removing.
|
||||
* Handles both regular directories and symbolic links.
|
||||
* Idempotent: succeeds even when the target does not exist.
|
||||
*
|
||||
* @param {object} op - Operation object with at least `target` and `skillsRoot`.
|
||||
* @returns {Promise<object>} Operation with `status: "ok"`.
|
||||
*/
|
||||
export async function removeTarget(op) {
|
||||
await validateRemoveTarget(op.target, op.skillsRoot, { repoRoot: REPO_ROOT });
|
||||
const info = existsSync(op.target) ? await lstat(op.target) : null;
|
||||
if (info?.isSymbolicLink()) await unlink(op.target);
|
||||
else await rm(op.target, { recursive: true, force: true });
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
|
||||
export async function validateRemoveTarget(target, skillsRoot, { repoRoot = process.cwd() } = {}) {
|
||||
const resolvedRoot = path.resolve(skillsRoot);
|
||||
const resolvedTarget = path.resolve(target);
|
||||
@@ -599,8 +617,6 @@ export async function executeOperation(op) {
|
||||
if (op.kind === "package-skill") return { ...op, status: "included" };
|
||||
if (op.kind === "sync-pi-package") {
|
||||
// Use the canonical generator (pnpm run sync:pi / node scripts/generate-skills.mjs).
|
||||
// The legacy sync-pi-package-skills.sh is retired in M3; it bypassed the
|
||||
// generator and copied skills/*/pi into pi-package directly, corrupting manifests.
|
||||
runCommand(process.execPath, [path.join(op.repoRoot, "scripts", "generate-skills.mjs")], { cwd: op.repoRoot });
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
@@ -610,33 +626,19 @@ export async function executeOperation(op) {
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
if (op.kind === "skill") {
|
||||
if (op.action === "remove") {
|
||||
await validateRemoveTarget(op.target, op.skillsRoot, { repoRoot: REPO_ROOT });
|
||||
const info = existsSync(op.target) ? await lstat(op.target) : null;
|
||||
if (info?.isSymbolicLink()) await unlink(op.target);
|
||||
else await rm(op.target, { recursive: true, force: true });
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
if (op.action === "remove") return removeTarget(op);
|
||||
await copyDirectoryReplacing(op.source, op.target);
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
if (op.kind === "helper") {
|
||||
if (op.action === "remove") {
|
||||
await validateRemoveTarget(op.target, op.skillsRoot, { repoRoot: REPO_ROOT });
|
||||
const info = existsSync(op.target) ? await lstat(op.target) : null;
|
||||
if (info?.isSymbolicLink()) await unlink(op.target);
|
||||
else await rm(op.target, { recursive: true, force: true });
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
if (op.action === "remove") return removeTarget(op);
|
||||
await installHelperAllowlist(op);
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
if (op.kind === "superpowers") {
|
||||
if (op.action === "remove") return removeTarget(op);
|
||||
await mkdir(path.dirname(op.target), { recursive: true });
|
||||
if (op.action === "remove") {
|
||||
await validateRemoveTarget(op.target, op.skillsRoot, { repoRoot: REPO_ROOT });
|
||||
await rm(op.target, { recursive: true, force: true });
|
||||
} else if (op.mode === "copy") {
|
||||
if (op.mode === "copy") {
|
||||
await copyDirectoryReplacing(op.source, op.target);
|
||||
} else {
|
||||
await rm(op.target, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user