refactor(#56): retire legacy runtime directory helpers into runtime-homes projection (#802)

* refactor(#56): retire legacy runtime directory helpers into runtime-homes projection

Consolidate per-runtime global config-dir resolution onto the single
canonical projection runtime-homes:getGlobalConfigDir. Extend it with the
explicitDir override (CLI --config-dir) and the opencode/kilo
OPENCODE_CONFIG/KILO_CONFIG file-path precedence the installer helpers had,
making it byte-for-behavior equivalent to the old getGlobalDir across all
15 install runtimes.

Delete bin/install.js's getGlobalDir/getOpencodeGlobalDir/getKiloGlobalDir
(and the orphaned local expandTilde), repoint all 9 call-sites, and remove
getGlobalDir from module.exports (net -242 lines in the installer). Migrate
the 5 test importers to the canonical projection; harden default/XDG
assertions against ambient *_CONFIG env vars. getAgentsDir now respects
OPENCODE_CONFIG/KILO_CONFIG consistently with the installer (intentional
convergence). Update CONTEXT.md Installer Module entry.

Completes the installer-refactor chain #58 -> #60 -> #56.

Closes #56

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#56): add changeset for runtime directory helper retirement

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-07 12:24:04 -04:00
committed by GitHub
parent f66c4a082c
commit 7e1112c28f
11 changed files with 207 additions and 297 deletions

View File

@@ -30,6 +30,7 @@ const {
} = require(path.join(__dirname, '..', 'scripts', 'fix-slash-commands.cjs'));
const {
resolveAntigravityGlobalDir,
getGlobalConfigDir,
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
const {
applyWorktreeBaseRef,
@@ -408,218 +409,6 @@ function getConfigDirFromHome(runtime, isGlobal) {
return "'.claude'";
}
/**
* Get the global config directory for OpenCode
* OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/
* Priority: OPENCODE_CONFIG_DIR > dirname(OPENCODE_CONFIG) > XDG_CONFIG_HOME/opencode > ~/.config/opencode
*/
function getOpencodeGlobalDir() {
// 1. Explicit OPENCODE_CONFIG_DIR env var
if (process.env.OPENCODE_CONFIG_DIR) {
return expandTilde(process.env.OPENCODE_CONFIG_DIR);
}
// 2. OPENCODE_CONFIG env var (use its directory)
if (process.env.OPENCODE_CONFIG) {
return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
}
// 3. XDG_CONFIG_HOME/opencode
if (process.env.XDG_CONFIG_HOME) {
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
}
// 4. Default: ~/.config/opencode (XDG default)
return path.join(os.homedir(), '.config', 'opencode');
}
/**
* Get the global config directory for Kilo
* Kilo follows XDG Base Directory spec and uses ~/.config/kilo/
* Priority: KILO_CONFIG_DIR > dirname(KILO_CONFIG) > XDG_CONFIG_HOME/kilo > ~/.config/kilo
*/
function getKiloGlobalDir() {
// 1. Explicit KILO_CONFIG_DIR env var
if (process.env.KILO_CONFIG_DIR) {
return expandTilde(process.env.KILO_CONFIG_DIR);
}
// 2. KILO_CONFIG env var (use its directory)
if (process.env.KILO_CONFIG) {
return path.dirname(expandTilde(process.env.KILO_CONFIG));
}
// 3. XDG_CONFIG_HOME/kilo
if (process.env.XDG_CONFIG_HOME) {
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'kilo');
}
// 4. Default: ~/.config/kilo (XDG default)
return path.join(os.homedir(), '.config', 'kilo');
}
/**
* Get the global config directory for a runtime
* @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot'
* @param {string|null} explicitDir - Explicit directory from --config-dir flag
*/
function getGlobalDir(runtime, explicitDir = null) {
if (runtime === 'opencode') {
// For OpenCode, --config-dir overrides env vars
if (explicitDir) {
return expandTilde(explicitDir);
}
return getOpencodeGlobalDir();
}
if (runtime === 'kilo') {
// For Kilo, --config-dir overrides env vars
if (explicitDir) {
return expandTilde(explicitDir);
}
return getKiloGlobalDir();
}
if (runtime === 'gemini') {
// Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.GEMINI_CONFIG_DIR) {
return expandTilde(process.env.GEMINI_CONFIG_DIR);
}
return path.join(os.homedir(), '.gemini');
}
if (runtime === 'codex') {
// Codex: --config-dir > CODEX_HOME > ~/.codex
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.CODEX_HOME) {
return expandTilde(process.env.CODEX_HOME);
}
return path.join(os.homedir(), '.codex');
}
if (runtime === 'copilot') {
// Copilot: --config-dir > COPILOT_CONFIG_DIR > ~/.copilot
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.COPILOT_CONFIG_DIR) {
return expandTilde(process.env.COPILOT_CONFIG_DIR);
}
return path.join(os.homedir(), '.copilot');
}
if (runtime === 'antigravity') {
// Antigravity: --config-dir > ANTIGRAVITY_CONFIG_DIR > auto-detected
// ~/.gemini/{antigravity,antigravity-ide,antigravity-cli}
if (explicitDir) {
return expandTilde(explicitDir);
}
return resolveAntigravityGlobalDir();
}
if (runtime === 'cursor') {
// Cursor: --config-dir > CURSOR_CONFIG_DIR > ~/.cursor
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.CURSOR_CONFIG_DIR) {
return expandTilde(process.env.CURSOR_CONFIG_DIR);
}
return path.join(os.homedir(), '.cursor');
}
if (runtime === 'windsurf') {
// Windsurf: --config-dir > WINDSURF_CONFIG_DIR > ~/.codeium/windsurf
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.WINDSURF_CONFIG_DIR) {
return expandTilde(process.env.WINDSURF_CONFIG_DIR);
}
return path.join(os.homedir(), '.codeium', 'windsurf');
}
if (runtime === 'augment') {
// Augment: --config-dir > AUGMENT_CONFIG_DIR > ~/.augment
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.AUGMENT_CONFIG_DIR) {
return expandTilde(process.env.AUGMENT_CONFIG_DIR);
}
return path.join(os.homedir(), '.augment');
}
if (runtime === 'trae') {
// Trae: --config-dir > TRAE_CONFIG_DIR > ~/.trae
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.TRAE_CONFIG_DIR) {
return expandTilde(process.env.TRAE_CONFIG_DIR);
}
return path.join(os.homedir(), '.trae');
}
if (runtime === 'qwen') {
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.QWEN_CONFIG_DIR) {
return expandTilde(process.env.QWEN_CONFIG_DIR);
}
return path.join(os.homedir(), '.qwen');
}
if (runtime === 'hermes') {
// Hermes Agent: --config-dir > HERMES_HOME > ~/.hermes
// Honors HERMES_HOME which Hermes users set for profile mode / Docker
// deploys (docs: https://hermes-agent.nousresearch.com/docs).
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.HERMES_HOME) {
return expandTilde(process.env.HERMES_HOME);
}
return path.join(os.homedir(), '.hermes');
}
if (runtime === 'codebuddy') {
// CodeBuddy: --config-dir > CODEBUDDY_CONFIG_DIR > ~/.codebuddy
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.CODEBUDDY_CONFIG_DIR) {
return expandTilde(process.env.CODEBUDDY_CONFIG_DIR);
}
return path.join(os.homedir(), '.codebuddy');
}
if (runtime === 'cline') {
// Cline: --config-dir > CLINE_CONFIG_DIR > ~/.cline
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.CLINE_CONFIG_DIR) {
return expandTilde(process.env.CLINE_CONFIG_DIR);
}
return path.join(os.homedir(), '.cline');
}
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.CLAUDE_CONFIG_DIR) {
return expandTilde(process.env.CLAUDE_CONFIG_DIR);
}
return path.join(os.homedir(), '.claude');
}
const banner = '\n' +
cyan + ' ██████╗ ███████╗██████╗\n' +
' ██╔════╝ ██╔════╝██╔══██╗\n' +
@@ -692,16 +481,6 @@ if (hasHelp) {
process.exit(0);
}
/**
* Expand ~ to home directory (shell doesn't expand in env vars passed to node)
*/
function expandTilde(filePath) {
if (filePath && filePath.startsWith('~/')) {
return path.join(os.homedir(), filePath.slice(2));
}
return filePath;
}
/**
* Compute the path prefix used for `@file` references in installed command/skill
* markdown. For global installs into a runtime config dir under $HOME, we
@@ -1774,11 +1553,11 @@ function getCommitAttribution(runtime) {
const resolveConfigPath = runtime === 'opencode'
? resolveOpencodeConfigPath
: resolveKiloConfigPath;
const config = readSettings(resolveConfigPath(getGlobalDir(runtime, null)));
const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null)));
result = (config && config.disable_ai_attribution === true) ? null : undefined;
} else if (runtime === 'gemini') {
// Gemini: check gemini settings.json for attribution config
const settings = readSettings(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json'));
const settings = readSettings(path.join(getGlobalConfigDir('gemini', explicitConfigDir), 'settings.json'));
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
result = undefined;
} else if (settings.attribution.commit === '') {
@@ -1788,7 +1567,7 @@ function getCommitAttribution(runtime) {
}
} else if (runtime === 'claude') {
// Claude Code
const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
const settings = readSettings(path.join(getGlobalConfigDir('claude', explicitConfigDir), 'settings.json'));
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
result = undefined;
} else if (settings.attribution.commit === '') {
@@ -7019,7 +6798,7 @@ function uninstall(isGlobal, runtime = 'claude') {
// Get the target directory based on runtime and install type
const targetDir = isGlobal
? getGlobalDir(runtime, explicitConfigDir)
? getGlobalConfigDir(runtime, explicitConfigDir)
: path.join(process.cwd(), dirName);
const locationLabel = isGlobal
@@ -7532,7 +7311,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
// For local installs, use ./.opencode/
// For global installs, use ~/.config/opencode/
const opencodeConfigDir = configDir || (isGlobal
? getGlobalDir('opencode', explicitConfigDir)
? getGlobalConfigDir('opencode', explicitConfigDir)
: path.join(process.cwd(), '.opencode'));
// Ensure config directory exists
fs.mkdirSync(opencodeConfigDir, { recursive: true });
@@ -7612,7 +7391,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
// For local installs, use ./.kilo/
// For global installs, use ~/.config/kilo/
const kiloConfigDir = configDir || (isGlobal
? getGlobalDir('kilo', explicitConfigDir)
? getGlobalConfigDir('kilo', explicitConfigDir)
: path.join(process.cwd(), '.kilo'));
// Ensure config directory exists
fs.mkdirSync(kiloConfigDir, { recursive: true });
@@ -8293,7 +8072,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
// Cline local installs write to the project root (like Claude Code) — .clinerules
// lives at the root, not inside a .cline/ subdirectory.
const targetDir = isGlobal
? getGlobalDir(runtime, explicitConfigDir)
? getGlobalConfigDir(runtime, explicitConfigDir)
: isCline
? process.cwd()
: path.join(process.cwd(), dirName);
@@ -10614,7 +10393,7 @@ function promptLocation(runtimes) {
});
const pathExamples = runtimes.map(r => {
const globalPath = getGlobalDir(r, explicitConfigDir);
const globalPath = getGlobalConfigDir(r, explicitConfigDir);
return globalPath.replace(os.homedir(), '~');
}).join(', ');
@@ -11023,7 +10802,6 @@ module.exports = {
GSD_CODEX_MARKER,
CODEX_AGENT_SANDBOX,
getDirName,
getGlobalDir,
getConfigDirFromHome,
resolveKiloConfigPath,
configureKiloPermissions,
@@ -11114,7 +10892,7 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
console.error('Usage: node install.js --skills-root <runtime>');
process.exit(1);
}
const globalDir = getGlobalDir(runtimeArg, null);
const globalDir = getGlobalConfigDir(runtimeArg, null);
// Hermes nests GSD skills under skills/gsd/ as a single category (#2841).
// Other runtimes use a flat skills/ root.
const skillsRoot = runtimeArg === 'hermes'