Files
msd-core/get-shit-done/bin/lib/runtime-homes.cjs
Cristian Uibar 05316369ae fix(3583): normalize retired colon-form commands in generated Claude/Qwen/Hermes SKILL.md bodies (#3629)
* Add first-class grok runtime support (maps to ~/.agents); wire installer, runtime-homes.cjs and sync-skills; update Grok Build engine in local ~/.agents to latest; record session progress in discussion doc

* Normalize gsd colon references to hyphen in generated Claude SKILL.md bodies using the shared transformer. Fixes #3583.

* Refine #3583 implementation after review: cache command names, improve tests, clean up comments

* Harden gsd colon-to-hyphen transformer with bidirectional word boundaries and body-only regression guard

* Track quick-wins batch status and local session notes for #3583/#3579 handoff

* Port installer robustness (hoist copyLibDir + selective Codex hooks) from 3579 to make Codex tests pass on this branch. Fixes ReferenceError and prevents extra hook pollution in Codex installs.

* Restore #3583 transformer wiring and Codex .sh GSD_VERSION branch lost in 50ff8f17 port

Commit 50ff8f17 ('Port installer robustness from #3579') accidentally reverted:
- the top-level require of transformContentToHyphen/readGsdCommandNames
- the body normalization inside convertClaudeCommandToClaudeSkill
- the Codex hook loop's .sh branch with {{GSD_VERSION}} substitution

These were the actual #3583 fix and the Codex half of the #2136 invariant.
Failing tests fixed: bug-2808-skill-hyphen-name, claude-skills-migration #3583
case, bug-2136 Codex .sh substitution.

* Exempt 'sync-skills' slug from docs-parity check (skill dir name in path references)

gsd-sync-skills is an installed Claude skill name and a workflow file but
not a registered slash command. The docs-parity regex catches /gsd-sync-skills
from filesystem path references like ~/.agents/skills/gsd-sync-skills/ in
docs/discussions/grok-build-support-2026-05.md.

Adding to INTERNAL_COMPONENT_SLUGS matches the existing exemption pattern
for 'statusline', 'workspaces', 'graphify-update', etc.

* Restrict hooks/lib/ install to hook-enabled runtimes and managed allowlist

Codex/Copilot/Cursor/Windsurf/Trae/Cline already skip the hooks block but were still copying hooks/lib/ helpers, contradicting the downstream Codex comment. Gate the call on the same runtime check and pass GSD_HOOK_LIB_FILES so install scope matches the uninstall/manifest scope.
2026-05-16 13:09:58 -04:00

186 lines
9.3 KiB
JavaScript

'use strict';
/**
* runtime-homes.cjs — canonical runtime → global config/skills directory mapping.
*
* Single source of truth for resolving the global config base directory and
* the correct global skills directory for every GSD-supported runtime.
*
* Mirrors the logic in bin/install.js getGlobalDir() but as a pure,
* side-effect-free module safe to require() at any point without triggering
* the installer. bin/install.js is the authoritative source — keep in sync.
*
* Runtime-specific notes:
* hermes — GSD skills nest under skills/gsd/<skillName>/ (not the flat
* skills/<skillName>/ layout used by all other runtimes). This
* collapses 86 skill entries into one category in Hermes' system
* prompt (#2841).
* cline — Rules-based; commands are embedded in .clinerules. Cline does
* not use a skills/ directory. getGlobalSkillDir() returns null
* for cline so the caller can emit an appropriate warning.
*/
const os = require('os');
const path = require('path');
/**
* Expand a leading ~ to os.homedir().
* @param {string} p
* @returns {string}
*/
function expandTilde(p) {
if (!p) return p;
if (p.startsWith('~/') || p === '~') return path.join(os.homedir(), p.slice(1));
return p;
}
/**
* Return the global config base directory for the given runtime.
* Respects the same env-var overrides as bin/install.js getGlobalDir().
*
* @param {string} runtime
* @returns {string} Absolute path to the runtime's global config directory
*/
function getGlobalConfigDir(runtime) {
const home = os.homedir();
const env = process.env;
switch (runtime) {
// ── Claude Code ──────────────────────────────────────────────────────────
case 'claude':
return env.CLAUDE_CONFIG_DIR ? expandTilde(env.CLAUDE_CONFIG_DIR) : path.join(home, '.claude');
// ── Cursor ───────────────────────────────────────────────────────────────
case 'cursor':
return env.CURSOR_CONFIG_DIR ? expandTilde(env.CURSOR_CONFIG_DIR) : path.join(home, '.cursor');
// ── Gemini CLI ───────────────────────────────────────────────────────────
case 'gemini':
return env.GEMINI_CONFIG_DIR ? expandTilde(env.GEMINI_CONFIG_DIR) : path.join(home, '.gemini');
// ── Codex ────────────────────────────────────────────────────────────────
case 'codex':
return env.CODEX_HOME ? expandTilde(env.CODEX_HOME) : path.join(home, '.codex');
// ── Grok Build ───────────────────────────────────────────────────────────
// Uses the unified ~/.agents layout (skills + agents + engine) shared with
// Codex-style harnesses. This is the pragmatic primary target for users
// running GSD inside Grok Build.
case 'grok':
return env.GROK_AGENTS_HOME ? expandTilde(env.GROK_AGENTS_HOME) : path.join(home, '.agents');
// ── Copilot (VS Code) ────────────────────────────────────────────────────
case 'copilot':
return env.COPILOT_CONFIG_DIR ? expandTilde(env.COPILOT_CONFIG_DIR) : path.join(home, '.copilot');
// ── Antigravity ──────────────────────────────────────────────────────────
case 'antigravity':
return env.ANTIGRAVITY_CONFIG_DIR
? expandTilde(env.ANTIGRAVITY_CONFIG_DIR)
: path.join(home, '.gemini', 'antigravity');
// ── Windsurf ─────────────────────────────────────────────────────────────
case 'windsurf':
return env.WINDSURF_CONFIG_DIR
? expandTilde(env.WINDSURF_CONFIG_DIR)
: path.join(home, '.codeium', 'windsurf');
// ── Augment ──────────────────────────────────────────────────────────────
case 'augment':
return env.AUGMENT_CONFIG_DIR ? expandTilde(env.AUGMENT_CONFIG_DIR) : path.join(home, '.augment');
// ── Trae ─────────────────────────────────────────────────────────────────
case 'trae':
return env.TRAE_CONFIG_DIR ? expandTilde(env.TRAE_CONFIG_DIR) : path.join(home, '.trae');
// ── Qwen Code ────────────────────────────────────────────────────────────
case 'qwen':
return env.QWEN_CONFIG_DIR ? expandTilde(env.QWEN_CONFIG_DIR) : path.join(home, '.qwen');
// ── Hermes Agent ─────────────────────────────────────────────────────────
// Note: skills use a nested layout (skills/gsd/<skill>/) — see getGlobalSkillDir().
case 'hermes':
return env.HERMES_HOME ? expandTilde(env.HERMES_HOME) : path.join(home, '.hermes');
// ── CodeBuddy ────────────────────────────────────────────────────────────
case 'codebuddy':
return env.CODEBUDDY_CONFIG_DIR ? expandTilde(env.CODEBUDDY_CONFIG_DIR) : path.join(home, '.codebuddy');
// ── Cline ────────────────────────────────────────────────────────────────
// Note: Cline is rules-based (.clinerules) — no skills/ directory.
// getGlobalSkillDir() returns null for cline.
case 'cline':
return env.CLINE_CONFIG_DIR ? expandTilde(env.CLINE_CONFIG_DIR) : path.join(home, '.cline');
// ── OpenCode (XDG) ───────────────────────────────────────────────────────
case 'opencode': {
if (env.OPENCODE_CONFIG_DIR) return expandTilde(env.OPENCODE_CONFIG_DIR);
if (env.XDG_CONFIG_HOME) return path.join(expandTilde(env.XDG_CONFIG_HOME), 'opencode');
return path.join(home, '.config', 'opencode');
}
// ── Kilo (XDG) ───────────────────────────────────────────────────────────
case 'kilo': {
if (env.KILO_CONFIG_DIR) return expandTilde(env.KILO_CONFIG_DIR);
if (env.XDG_CONFIG_HOME) return path.join(expandTilde(env.XDG_CONFIG_HOME), 'kilo');
return path.join(home, '.config', 'kilo');
}
// ── Default (Claude fallback) ─────────────────────────────────────────────
default:
return env.CLAUDE_CONFIG_DIR ? expandTilde(env.CLAUDE_CONFIG_DIR) : path.join(home, '.claude');
}
}
/**
* Return the global skills base directory for the given runtime.
* Most runtimes: <configDir>/skills
* Hermes: <configDir>/skills/gsd (nested category layout — #2841)
* Cline: null (rules-based, no skills directory)
*
* @param {string} runtime
* @returns {string|null}
*/
function getGlobalSkillsBase(runtime) {
if (runtime === 'cline') return null;
const configDir = getGlobalConfigDir(runtime);
if (runtime === 'hermes') return path.join(configDir, 'skills', 'gsd');
return path.join(configDir, 'skills');
}
/**
* Return the full path to a specific skill's directory for the given runtime.
* Returns null for runtimes that don't use a skills directory (cline).
*
* @param {string} runtime
* @param {string} skillName - e.g. 'gsd-executor'
* @returns {string|null}
*/
function getGlobalSkillDir(runtime, skillName) {
const base = getGlobalSkillsBase(runtime);
if (base === null) return null;
return path.join(base, skillName);
}
/**
* Return a human-readable display path for a global skill (for log messages).
*
* @param {string} runtime
* @param {string} skillName
* @returns {string}
*/
function getGlobalSkillDisplayPath(runtime, skillName) {
const dir = getGlobalSkillDir(runtime, skillName);
if (!dir) return `(${runtime} does not use a skills directory)`;
// Replace homedir prefix with ~ for readability
const home = os.homedir();
return dir.startsWith(home) ? '~' + dir.slice(home.length) : dir;
}
module.exports = {
getGlobalConfigDir,
getGlobalSkillsBase,
getGlobalSkillDir,
getGlobalSkillDisplayPath,
};