Files
msd-core/get-shit-done/bin/lib/runtime-artifact-layout.cjs
Tom Boucher 67dd09e509 fix(3663): restore .gsd-source marker resolution in findInstallSourceRoot
Both findInstallSourceRoot and findAgentsSourceRoot now accept an optional
runtimeConfigDir. When provided, they first check <runtimeConfigDir>/.gsd-source:
read the stored path, verify it exists, and return it. Fall through to the
path.dirname walk-up only when the marker is absent or points to a missing path.

The factory functions (commandsKind, agentsKind, skillsKind) now receive configDir
from the switch arms and pass it through to the finders. listSurface in surface.cjs
passes its runtimeConfigDir argument to findInstallSourceRoot so installed-from-source
layouts use the marker rather than the walk-up.

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

269 lines
8.3 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
// ---------------------------------------------------------------------------
/**
* Locate the GSD commands/gsd source directory.
*
* Resolution order:
* 1. If runtimeConfigDir provided, check <runtimeConfigDir>/.gsd-source marker.
* 2. Walk up from __dirname using path.dirname (no literal .. segments).
* 3. Throw a descriptive error if neither succeeds.
*
* @param {string} [runtimeConfigDir] optional runtime config directory
* @returns {string}
*/
function findInstallSourceRoot(runtimeConfigDir) {
// Step 1: marker check
if (runtimeConfigDir) {
const markerPath = path.join(runtimeConfigDir, '.gsd-source');
if (fs.existsSync(markerPath)) {
try {
const src = fs.readFileSync(markerPath, 'utf8').trim();
if (src && fs.existsSync(src)) return src;
} catch { /* fall through */ }
}
}
// Step 2: walk up from __dirname
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;
}
throw new Error(`findInstallSourceRoot: could not locate commands/gsd from ${__dirname}`);
}
/**
* Locate the GSD agents source directory.
*
* Resolution order:
* 1. If runtimeConfigDir provided, check <runtimeConfigDir>/.gsd-source marker.
* 2. Walk up from __dirname using path.dirname (no literal .. segments).
* 3. Throw a descriptive error if neither succeeds.
*
* @param {string} [runtimeConfigDir] optional runtime config directory
* @returns {string}
*/
function findAgentsSourceRoot(runtimeConfigDir) {
// Step 1: marker check
if (runtimeConfigDir) {
const markerPath = path.join(runtimeConfigDir, '.gsd-source');
if (fs.existsSync(markerPath)) {
try {
const src = fs.readFileSync(markerPath, 'utf8').trim();
if (src && fs.existsSync(src)) {
// Marker points to commands/gsd; agents/ is a sibling of commands/
const agentsCandidate = path.resolve(path.dirname(src), '..', 'agents');
if (fs.existsSync(agentsCandidate)) return agentsCandidate;
}
} catch { /* fall through */ }
}
}
// Step 2: walk up from __dirname
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;
}
throw new Error(`findAgentsSourceRoot: could not locate agents/ from ${__dirname}`);
}
// ---------------------------------------------------------------------------
// 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, configDir) {
return {
kind: 'commands',
destSubpath,
prefix,
stage: (resolved) => stageSkillsForProfile(findInstallSourceRoot(configDir), resolved),
};
}
function agentsKind(destSubpath, prefix, configDir) {
return {
kind: 'agents',
destSubpath,
prefix,
stage: (resolved) => stageAgentsForProfile(findAgentsSourceRoot(configDir), resolved),
};
}
function skillsKind(destSubpath, prefix, converter, configDir) {
return {
kind: 'skills',
destSubpath,
prefix,
stage: (resolved) => stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), 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-', configDir),
agentsKind('agents', 'gsd-', configDir),
];
} else {
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToClaudeSkill, configDir)];
}
break;
case 'cursor':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToCursorSkill, configDir)];
break;
case 'gemini':
kinds = [commandsKind('commands/gsd', 'gsd-', configDir)];
break;
case 'codex':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToCodexSkill, configDir)];
break;
case 'copilot':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToCopilotSkill, configDir)];
break;
case 'antigravity':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToAntigravitySkill, configDir)];
break;
case 'windsurf':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToWindsurfSkill, configDir)];
break;
case 'augment':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToAugmentSkill, configDir)];
break;
case 'trae':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToTraeSkill, configDir)];
break;
case 'qwen':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToClaudeSkill, configDir)];
break;
case 'hermes':
kinds = [skillsKind('skills/gsd', '', convertClaudeCommandToClaudeSkill, configDir)];
break;
case 'codebuddy':
kinds = [skillsKind('skills', 'gsd-', convertClaudeCommandToCodebuddySkill, configDir)];
break;
case 'cline':
kinds = [];
break;
case 'opencode':
kinds = [commandsKind('command', 'gsd-', configDir)];
break;
case 'kilo':
kinds = [commandsKind('command', 'gsd-', configDir)];
break;
default:
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
}
return { runtime, configDir, kinds };
}
module.exports = { resolveRuntimeArtifactLayout, findInstallSourceRoot };