diff --git a/bin/install.js b/bin/install.js index b62071f65..53cb8af31 100755 --- a/bin/install.js +++ b/bin/install.js @@ -365,6 +365,7 @@ const { } = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs')); const { createRuntimeArtifactInstallPlan, + createRuntimeArtifactUninstallPlan, } = require(path.join(_gsdLibDir, 'runtime-artifact-install-plan.cjs')); const { planLegacyCleanup, @@ -7189,9 +7190,14 @@ function uninstallRuntimeArtifacts(runtime, configDir, scope) { const savedLegacyArtifacts = _runLegacyUninstallCleanup(runtime, configDir, scope); const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope); - for (const kind of layout.kinds) { - const dest = path.join(layout.configDir, kind.destSubpath); - _removeGsdEntries(dest, kind); + const plan = createRuntimeArtifactUninstallPlan(layout); + const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind])); + for (const item of plan.items) { + const kind = kindsByName.get(item.kind); + if (!kind) { + throw new Error(`Runtime artifact uninstall plan referenced unknown kind: ${item.kind}`); + } + _removeGsdEntries(item.destDir, kind); } // Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove diff --git a/gsd-core/bin/lib/runtime-artifact-install-plan.cjs b/gsd-core/bin/lib/runtime-artifact-install-plan.cjs index b4178e9ca..9a89d8e6d 100644 --- a/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +++ b/gsd-core/bin/lib/runtime-artifact-install-plan.cjs @@ -66,4 +66,12 @@ function createRuntimeArtifactInstallPlan(args) { } return { ok: true, plan: { items, cleanupDirs } }; } -module.exports = { createRuntimeArtifactInstallPlan }; +function createRuntimeArtifactUninstallPlan(layout) { + return { + items: layout.kinds.map((kind) => ({ + kind: kind.kind, + destDir: path.join(layout.configDir, kind.destSubpath), + })), + }; +} +module.exports = { createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan }; diff --git a/src/runtime-artifact-install-plan.cts b/src/runtime-artifact-install-plan.cts index 4c0ef08bd..d6b2e6903 100644 --- a/src/runtime-artifact-install-plan.cts +++ b/src/runtime-artifact-install-plan.cts @@ -24,6 +24,7 @@ interface ResolvedProfile { interface ArtifactKind { kind: ArtifactKindName; destSubpath: string; + prefix?: string; stage: (resolvedProfile: ResolvedProfile) => string; } @@ -64,6 +65,15 @@ interface InstallPlan { cleanupDirs: string[]; } +interface UninstallPlanItem { + kind: ArtifactKindName; + destDir: string; +} + +interface UninstallPlan { + items: UninstallPlanItem[]; +} + type InstallPlanResult = | { ok: true; plan: InstallPlan } | { ok: false; kind: 'stage_failed' | 'rewrite_failed'; message: string; cleanupDirs: string[]; failedKind?: ArtifactKindName }; @@ -143,4 +153,13 @@ function createRuntimeArtifactInstallPlan(args: CreateRuntimeArtifactInstallPlan return { ok: true, plan: { items, cleanupDirs } }; } -export = { createRuntimeArtifactInstallPlan }; +function createRuntimeArtifactUninstallPlan(layout: Layout): UninstallPlan { + return { + items: layout.kinds.map((kind) => ({ + kind: kind.kind, + destDir: path.join(layout.configDir, kind.destSubpath), + })), + }; +} + +export = { createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan }; diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index 6573070f3..0377c4a60 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -47,18 +47,25 @@ const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR); const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST }); function loadFreshInstallerWithInstallPlanStub(stub) { + return loadFreshInstallerWithPlanStubs({ installStub: stub }); +} + +function loadFreshInstallerWithPlanStubs({ installStub, uninstallStub }) { const installPath = require.resolve('../bin/install.js'); const planPath = require.resolve('../gsd-core/bin/lib/runtime-artifact-install-plan.cjs'); const planModule = require(planPath); - const original = planModule.createRuntimeArtifactInstallPlan; - planModule.createRuntimeArtifactInstallPlan = stub; + const originalInstall = planModule.createRuntimeArtifactInstallPlan; + const originalUninstall = planModule.createRuntimeArtifactUninstallPlan; + if (installStub) planModule.createRuntimeArtifactInstallPlan = installStub; + if (uninstallStub) planModule.createRuntimeArtifactUninstallPlan = uninstallStub; delete require.cache[installPath]; const installer = require('../bin/install.js'); return { installer, restore() { - planModule.createRuntimeArtifactInstallPlan = original; + planModule.createRuntimeArtifactInstallPlan = originalInstall; + planModule.createRuntimeArtifactUninstallPlan = originalUninstall; delete require.cache[installPath]; }, }; @@ -401,6 +408,39 @@ describe('installOpencodeFamilySkills — emits skills//SKILL.md (#784)', // ─── Section 7: uninstallRuntimeArtifacts — all runtimes ───────────────────── +describe('uninstallRuntimeArtifacts — consumes Runtime Artifact Uninstall Plan Module', () => { + test('removes returned plan destinations with layout kind metadata', (t) => { + const configDir = createTempDir('gsd-uninstall-plan-adapter-'); + t.after(() => cleanup(configDir)); + + const commandsDir = path.join(configDir, 'custom-commands'); + fs.mkdirSync(commandsDir, { recursive: true }); + fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), '# remove\n'); + fs.writeFileSync(path.join(commandsDir, 'user-custom.md'), '# keep\n'); + + let planLayout; + const { installer, restore } = loadFreshInstallerWithPlanStubs({ + uninstallStub(layout) { + planLayout = layout; + return { + items: [ + { kind: 'commands', destDir: commandsDir }, + ], + }; + }, + }); + t.after(restore); + + installer.uninstallRuntimeArtifacts('gemini', configDir, 'global'); + + assert.strictEqual(planLayout.runtime, 'gemini'); + assert.strictEqual(planLayout.configDir, configDir); + assert.strictEqual(planLayout.scope, 'global'); + assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md'))); + assert.ok(fs.existsSync(path.join(commandsDir, 'user-custom.md'))); + }); +}); + describe('uninstallRuntimeArtifacts — removes gsd-owned entries, preserves foreign', () => { for (const runtime of ALL_RUNTIMES_LAYOUT) { test(`${runtime}: gsd entries removed, foreign preserved`, (t) => {