Merge remote-tracking branch 'upstream/next' into kimi-runtime-support
# Conflicts: # bin/install.js
This commit is contained in:
287
bin/install.js
287
bin/install.js
@@ -37,6 +37,7 @@ const {
|
||||
applyWorktreeBaseRef,
|
||||
readBaseRefFromSettings,
|
||||
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
|
||||
const { resolveRuntimeConfigIntent } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
|
||||
|
||||
/**
|
||||
* Runtimes that register hyphen-form `name:` per #2808 AND copy agent bodies
|
||||
@@ -402,224 +403,12 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Compatibility seam for tests and older installer consumers.
|
||||
* Runtime home resolution now lives in runtime-homes.cjs.
|
||||
*/
|
||||
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');
|
||||
}
|
||||
|
||||
if (runtime === 'kimi') {
|
||||
if (explicitDir) {
|
||||
return expandTilde(explicitDir);
|
||||
}
|
||||
return getGlobalConfigDir('kimi');
|
||||
}
|
||||
|
||||
// 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');
|
||||
return getGlobalConfigDir(runtime, explicitDir);
|
||||
}
|
||||
|
||||
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 === '') {
|
||||
@@ -7420,7 +7199,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
|
||||
@@ -7934,7 +7713,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 });
|
||||
@@ -8014,7 +7793,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 });
|
||||
@@ -8668,6 +8447,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
const isHermes = runtime === 'hermes';
|
||||
const isCodebuddy = runtime === 'codebuddy';
|
||||
const isCline = runtime === 'cline';
|
||||
const configIntent = resolveRuntimeConfigIntent(runtime);
|
||||
const dirName = getDirName(runtime);
|
||||
const src = path.join(__dirname, '..');
|
||||
|
||||
@@ -8722,7 +8502,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);
|
||||
@@ -9668,7 +9448,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
throw _earlyInstallErr;
|
||||
}
|
||||
|
||||
if (isCodex && !isMinimalMode(_effectiveInstallMode)) {
|
||||
if (configIntent.installSurface === 'codex-toml' && !isMinimalMode(_effectiveInstallMode)) {
|
||||
// Capture pre-install snapshots before ANY GSD mutation
|
||||
// (#2760 fix 3). On post-write schema-validation failure OR any throw
|
||||
// during the mutation sequence (write failure, merge throw, etc.) we
|
||||
@@ -10014,7 +9794,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isCopilot) {
|
||||
if (configIntent.installSurface === 'copilot-instructions') {
|
||||
// Generate copilot-instructions.md
|
||||
const templatePath = path.join(targetDir, 'gsd-core', 'templates', 'copilot-instructions.md');
|
||||
const instructionsPath = path.join(targetDir, 'copilot-instructions.md');
|
||||
@@ -10028,32 +9808,13 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isCursor) {
|
||||
// Cursor uses skills — no config.toml, no settings.json hooks needed
|
||||
if (configIntent.installSurface === 'profile-marker-only') {
|
||||
// Cursor/Windsurf/Trae/Kimi use skills/agents — no config.toml, no settings.json hooks needed
|
||||
persistActiveProfileMarker();
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isWindsurf) {
|
||||
// Windsurf uses skills — no config.toml, no settings.json hooks needed
|
||||
persistActiveProfileMarker();
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isTrae) {
|
||||
// Trae uses skills — no settings.json hooks needed
|
||||
persistActiveProfileMarker();
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isKimi) {
|
||||
// Kimi uses Agent Skills plus explicit custom agent YAML files. It does
|
||||
// not own settings.json, hooks, rules, or update-banner/statusline config.
|
||||
persistActiveProfileMarker();
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
|
||||
}
|
||||
|
||||
if (isCline) {
|
||||
if (configIntent.installSurface === 'cline-rules') {
|
||||
// Cline uses .clinerules — generate a rules file with GSD system instructions
|
||||
const clinerulesDest = path.join(targetDir, '.clinerules');
|
||||
const clinerules = [
|
||||
@@ -10671,9 +10432,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
const isWindsurf = runtime === 'windsurf';
|
||||
const isTrae = runtime === 'trae';
|
||||
const isCline = runtime === 'cline';
|
||||
const isKimi = runtime === 'kimi';
|
||||
const configIntent = resolveRuntimeConfigIntent(runtime);
|
||||
|
||||
if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isKimi) {
|
||||
if (shouldInstallStatusline && configIntent.writesSharedSettings && !isOpencode) {
|
||||
if (!isGlobal && !forceStatusline) {
|
||||
// Local installs skip statusLine by default: repo settings.json takes precedence over
|
||||
// profile-level settings.json in Claude Code, so writing here would silently clobber
|
||||
@@ -10699,7 +10460,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
// settings.json hooks block — opencode/kilo/codex/cursor/windsurf/trae/
|
||||
// cline either lack the surface or use a different config schema.
|
||||
const { shouldInstallBanner, bannerCommand } = bannerOpts;
|
||||
if (shouldInstallBanner && settings && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) {
|
||||
if (shouldInstallBanner && settings && configIntent.writesSharedSettings && !isOpencode) {
|
||||
if (!bannerCommand) {
|
||||
console.warn(` ${yellow}⚠${reset} Skipped update banner registration — Node executable path unavailable. See #2979 / #3002.`);
|
||||
} else {
|
||||
@@ -10731,17 +10492,17 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
// {type: 'command', command: null} items that the runtime hook schema
|
||||
// rejects at parse time. validateHookFields filters those out so the file
|
||||
// we write is always schema-valid.
|
||||
if (settingsPath && settings && !isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) {
|
||||
if (settingsPath && settings && configIntent.writesSharedSettings) {
|
||||
writeSettings(settingsPath, validateHookFields(settings));
|
||||
}
|
||||
|
||||
// Configure OpenCode permissions
|
||||
if (isOpencode && !process.env.GSD_TEST_MODE) {
|
||||
if (configIntent.finishPermissionWriter === 'opencode' && !process.env.GSD_TEST_MODE) {
|
||||
configureOpencodePermissions(isGlobal, configDir);
|
||||
}
|
||||
|
||||
// Configure Kilo permissions
|
||||
if (isKilo) {
|
||||
if (configIntent.finishPermissionWriter === 'kilo') {
|
||||
configureKiloPermissions(isGlobal, configDir);
|
||||
}
|
||||
|
||||
@@ -11099,7 +10860,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(', ');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user