Files
msd-core/get-shit-done/bin/lib/runtime-artifact-layout.cjs
Tom Boucher 4d578394eb refactor(3663): migrate applySurface to layout-driven loop; add skills kind to _syncGsdDir
- applySurface(runtimeConfigDir, commandsDir, agentsDir, manifest, clusterMap) → applySurface(runtimeConfigDir, layout, manifest, clusterMap)
- _syncGsdDir extended to handle skills kind (dirs not files, prefix-gated removal)
- _findInstallSource/_findAgentsSource deleted; listSurface now uses findInstallSourceRoot() from runtime-artifact-layout.cjs
- findInstallSourceRoot exported from runtime-artifact-layout.cjs
- test(3663): update surface-apply.test.cjs to new layout-passing shape + add skills kind test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 01:12:00 -04:00

232 lines
6.8 KiB
JavaScript

'use strict';
/**
* Runtime artifact layout module — resolves the artifact directory shapes
* (commands, agents, skills) for each supported runtime.
*
* grok is intentionally absent: it is in runtime-homes.cjs but not wired
* here. The TypeError on unknown runtime is the loud-fail signal that a
* runtime was added to the homes list without a layout entry.
*/
const path = require('path');
const fs = require('fs');
const {
stageSkillsForProfile,
stageAgentsForProfile,
stageSkillsForRuntimeAsSkills,
} = require('./install-profiles.cjs');
// Load converters from bin/install.js in test-safe way (GSD_TEST_MODE skips main logic)
process.env.GSD_TEST_MODE = process.env.GSD_TEST_MODE || '1';
const {
convertClaudeCommandToClaudeSkill,
convertClaudeCommandToCursorSkill,
convertClaudeCommandToCodexSkill,
convertClaudeCommandToCopilotSkill,
convertClaudeCommandToAntigravitySkill,
convertClaudeCommandToWindsurfSkill,
convertClaudeCommandToAugmentSkill,
convertClaudeCommandToTraeSkill,
convertClaudeCommandToCodebuddySkill,
} = require('../../../bin/install.js');
/**
* @typedef {'commands'|'agents'|'skills'} ArtifactKindName
* @typedef {Object} ArtifactKind
* @property {ArtifactKindName} kind
* @property {string} destSubpath
* @property {string} prefix
* @property {(resolvedProfile: Object) => string} stage
* @typedef {Object} Layout
* @property {string} runtime
* @property {string} configDir
* @property {ArtifactKind[]} kinds
*/
// ---------------------------------------------------------------------------
// Source root finders
// ---------------------------------------------------------------------------
/**
* Walk up from __dirname to find commands/gsd. Walk-up-only for Phase 1;
* the .gsd-source marker check (which requires runtimeConfigDir) is deferred.
*
* @param {string} [overrideRoot] optional override for testability
* @returns {string}
*/
function findInstallSourceRoot(overrideRoot) {
if (overrideRoot) return overrideRoot;
let dir = __dirname;
for (let i = 0; i < 6; i++) {
const candidate = path.join(dir, 'commands', 'gsd');
if (fs.existsSync(candidate)) return candidate;
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return path.join(__dirname, '..', '..', '..', 'commands', 'gsd');
}
/**
* Walk up from __dirname to find agents/. Walk-up-only for Phase 1.
*
* @param {string} [overrideRoot] optional override for testability
* @returns {string|null}
*/
function findAgentsSourceRoot(overrideRoot) {
if (overrideRoot) return overrideRoot;
let dir = __dirname;
for (let i = 0; i < 6; i++) {
const candidate = path.join(dir, 'agents');
if (fs.existsSync(candidate)) return candidate;
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}
// ---------------------------------------------------------------------------
// Allowlisted runtimes
// ---------------------------------------------------------------------------
const ALLOWED_RUNTIMES = new Set([
'claude', 'cursor', 'gemini', 'codex', 'copilot', 'antigravity',
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy',
'cline', 'opencode', 'kilo',
]);
// ---------------------------------------------------------------------------
// Layout table builders
// ---------------------------------------------------------------------------
function commandsKind(destSubpath, prefix, srcOverride) {
return {
kind: 'commands',
destSubpath,
prefix,
stage: (resolved) => stageSkillsForProfile(findInstallSourceRoot(srcOverride), resolved),
};
}
function agentsKind(destSubpath, prefix, srcOverride) {
return {
kind: 'agents',
destSubpath,
prefix,
stage: (resolved) => stageAgentsForProfile(findAgentsSourceRoot(srcOverride), resolved),
};
}
function skillsKind(destSubpath, prefix, converter, srcOverride) {
return {
kind: 'skills',
destSubpath,
prefix,
stage: (resolved) => stageSkillsForRuntimeAsSkills(findInstallSourceRoot(srcOverride), resolved, converter, prefix),
};
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Resolve the artifact layout for a given runtime and config directory.
*
* @param {string} runtime
* @param {string} configDir
* @param {'local'|'global'} [scope]
* @returns {Layout}
*/
function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
if (typeof configDir !== 'string' || configDir === '') {
throw new TypeError('configDir must be a non-empty string');
}
if (scope !== 'local' && scope !== 'global') {
throw new TypeError('scope must be "local" or "global"');
}
if (!ALLOWED_RUNTIMES.has(runtime)) {
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
}
let kinds;
switch (runtime) {
case 'claude':
if (scope === 'local') {
kinds = [
commandsKind('commands/gsd', 'gsd-'),
agentsKind('agents', 'gsd-'),
];
} else {
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToClaudeSkill)];
}
break;
case 'cursor':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToCursorSkill)];
break;
case 'gemini':
kinds = [commandsKind('commands/gsd', 'gsd-')];
break;
case 'codex':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToCodexSkill)];
break;
case 'copilot':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToCopilotSkill)];
break;
case 'antigravity':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToAntigravitySkill)];
break;
case 'windsurf':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToWindsurfSkill)];
break;
case 'augment':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToAugmentSkill)];
break;
case 'trae':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToTraeSkill)];
break;
case 'qwen':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToClaudeSkill)];
break;
case 'hermes':
kinds = [skillsKind('skills/gsd', '', convertClaudeCommandToClaudeSkill)];
break;
case 'codebuddy':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToCodebuddySkill)];
break;
case 'cline':
kinds = [];
break;
case 'opencode':
kinds = [commandsKind('command', 'gsd-')];
break;
case 'kilo':
kinds = [commandsKind('command', 'gsd-')];
break;
default:
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
}
return { runtime, configDir, kinds };
}
module.exports = { resolveRuntimeArtifactLayout, findInstallSourceRoot };