refactor(3664): migrate bin/install.js install/uninstall to Runtime Artifact Layout Module (#3674)
Phase 2 of #3660 / ADR-3660. Routes both lifecycle verbs through the Runtime Artifact Layout Module landed in Phase 1 (#3663): - Add installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) and uninstallRuntimeArtifacts(runtime, configDir, scope) as the public orchestrators. Both pre-prune stale gsd-* entries before staged copy; installRuntimeArtifacts brackets the prune+copy with preserveUserArtifacts / restoreUserArtifacts so user-owned content (e.g. gsd-dev-preferences) survives wipe-and-replace for claude/qwen/hermes runtimes. - Add applyRuntimeContentRewritesInPlace as the per-runtime path/branding post-stage step (preserves byte-output equivalence with the legacy copyCommandsAs* pipeline, including Qwen/Hermes branding rewrites). - Add _copyStaged, _removeGsdEntries kind-aware filesystem helpers. - Add _runLegacyInstallMigrations, _runLegacyUninstallCleanup as thin dispatchers over existing ADR-0008 legacy migrations (Hermes flat->nested per #2841, dev-preferences-as-skill per #2973). For Hermes, also clean up the intermediate skills/gsd/gsd-*/ layout that pre-Phase-2 installs left on disk. - Delete the 9 copyCommandsAs*Skills functions (Codex / Cursor / Windsurf / Trae / CodeBuddy / Copilot / Claude / Antigravity / Augment) and the _copyCommandsAsSkillsViaConverter helper. All test entry points migrated to call installRuntimeArtifacts directly through the unified seam. - Collapse the 9-branch uninstall ladder to one uninstallRuntimeArtifacts call plus preserved non-layout side-effects (Codex TOML, Copilot instructions, hooks). - Unify install dispatcher: a single _isSkillsRuntime gate routes all 11 skills runtimes through installRuntimeArtifacts for both full and core/ minimal profiles. Removes 11 per-runtime if-else branches (3 minimal-mode shim branches + 8 dead after-the-gate branches). Net delta on bin/install.js: 11,495 -> 11,174 (-321 LOC). New tests: - tests/install-uninstall-layout-loop.test.cjs (34 tests) - per-runtime fixture assertions on install/uninstall/legacy-migration ordering. - tests/install-hermes-regressions.test.cjs (6 tests) - covers the six defects surfaced by iterative review: Hermes upgrade leaves stale dirs, --hermes --profile=core fall-through, --qwen --profile=core fall-through, minimal-mode dev-preferences migration skipped (Hermes/Qwen/Claude-global), and ordering bug in _runLegacyInstallMigrations. Existing tests (10,038 prior + 40 new) all green: 10,078/10,078 pass. Refs #3664 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -109,18 +109,20 @@ describe('Hermes Agent local install/uninstall', () => {
|
||||
|
||||
// Nested layout per spec #2841: all GSD skills collapse into a single
|
||||
// skills/gsd/ category so Hermes' system prompt sees one entry, not 86.
|
||||
assert.ok(fs.existsSync(path.join(targetDir, 'skills', 'gsd', 'gsd-help', 'SKILL.md')));
|
||||
// Skills use bare stem names (no gsd- prefix) inside the category dir,
|
||||
// per the layout module's prefix: '' design for Hermes (#3664).
|
||||
assert.ok(fs.existsSync(path.join(targetDir, 'skills', 'gsd', 'help', 'SKILL.md')));
|
||||
assert.ok(fs.existsSync(path.join(targetDir, 'skills', 'gsd', 'DESCRIPTION.md')),
|
||||
'DESCRIPTION.md exists at category root');
|
||||
assert.ok(fs.existsSync(path.join(targetDir, 'get-shit-done', 'VERSION')));
|
||||
assert.ok(fs.existsSync(path.join(targetDir, 'agents')));
|
||||
|
||||
const manifest = writeManifest(targetDir, 'hermes');
|
||||
assert.ok(Object.keys(manifest.files).some(file => file.startsWith('skills/gsd/gsd-help/')), manifest);
|
||||
assert.ok(Object.keys(manifest.files).some(file => file.startsWith('skills/gsd/help/')), JSON.stringify(manifest.files));
|
||||
|
||||
uninstall(false, 'hermes');
|
||||
|
||||
assert.ok(!fs.existsSync(path.join(targetDir, 'skills', 'gsd', 'gsd-help')), 'Hermes skill directory removed');
|
||||
assert.ok(!fs.existsSync(path.join(targetDir, 'skills', 'gsd', 'help')), 'Hermes skill directory removed');
|
||||
assert.ok(!fs.existsSync(path.join(targetDir, 'skills', 'gsd')), 'Hermes gsd category dir removed');
|
||||
assert.ok(!fs.existsSync(path.join(targetDir, 'get-shit-done')), 'get-shit-done removed');
|
||||
});
|
||||
@@ -128,13 +130,15 @@ describe('Hermes Agent local install/uninstall', () => {
|
||||
test('installed SKILL.md frontmatter conforms to Hermes spec', () => {
|
||||
install(false, 'hermes');
|
||||
const targetDir = path.join(tmpDir, '.hermes');
|
||||
// Nested layout: skills live under skills/gsd/gsd-*/SKILL.md.
|
||||
// Nested layout: skills live under skills/gsd/<stem>/SKILL.md.
|
||||
// The layout module uses prefix: '' for Hermes so skill dirs have bare
|
||||
// stem names (help, plan, …) inside the category dir (#3664).
|
||||
const categoryDir = path.join(targetDir, 'skills', 'gsd');
|
||||
const skillDirs = fs.readdirSync(categoryDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'))
|
||||
.filter(e => e.isDirectory() && e.name !== 'DESCRIPTION.md')
|
||||
.map(e => e.name);
|
||||
|
||||
assert.ok(skillDirs.length > 0, 'at least one gsd-* skill installed');
|
||||
assert.ok(skillDirs.length > 0, 'at least one skill installed');
|
||||
|
||||
// Parse every SKILL.md and assert structural shape required by Hermes.
|
||||
for (const dir of skillDirs) {
|
||||
@@ -203,7 +207,7 @@ describe('E2E: Hermes Agent uninstall skills cleanup', () => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('removes all gsd-* skill directories on --hermes --uninstall', () => {
|
||||
test('removes all skill directories on --hermes --uninstall', () => {
|
||||
const targetDir = path.join(tmpDir, '.hermes');
|
||||
install(false, 'hermes');
|
||||
|
||||
@@ -211,9 +215,10 @@ describe('E2E: Hermes Agent uninstall skills cleanup', () => {
|
||||
const categoryDir = path.join(skillsDir, 'gsd');
|
||||
assert.ok(fs.existsSync(categoryDir), 'skills/gsd/ category dir exists after install');
|
||||
|
||||
// Layout uses prefix: '' for Hermes — skill dirs have bare stem names (no gsd- prefix)
|
||||
const installedSkills = fs.readdirSync(categoryDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
assert.ok(installedSkills.length > 0, `found ${installedSkills.length} gsd-* skill dirs before uninstall`);
|
||||
.filter(e => e.isDirectory());
|
||||
assert.ok(installedSkills.length > 0, `found ${installedSkills.length} skill dirs before uninstall`);
|
||||
|
||||
uninstall(false, 'hermes');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user