diff --git a/CHANGELOG.md b/CHANGELOG.md index 4923bd115b..f35f933c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed +- Local install: workflow and reference files now get absolute path to `gsd-tools.cjs` instead of `$HOME/.claude/get-shit-done/`, so local installs work when GSD is outside `$HOME` and spawned subagents with empty `$HOME` still resolve the path (#820) + ## [1.22.4] - 2026-03-03 ### Added diff --git a/bin/install.js b/bin/install.js index b7f11e4cf6..235c7c9a9f 100755 --- a/bin/install.js +++ b/bin/install.js @@ -58,21 +58,6 @@ if (hasAll) { if (hasCodex) selectedRuntimes.push('codex'); } -/** - * Convert a pathPrefix (which uses absolute paths for global installs) to a - * $HOME-relative form for replacing $HOME/.claude/ references in bash code blocks. - * Preserves $HOME as a shell variable so paths remain portable across machines. - */ -function toHomePrefix(pathPrefix) { - const home = os.homedir().replace(/\\/g, '/'); - const normalized = pathPrefix.replace(/\\/g, '/'); - if (normalized.startsWith(home)) { - return '$HOME' + normalized.slice(home.length); - } - // For relative paths or paths not under $HOME, return as-is - return normalized; -} - // Helper to get directory name for a runtime (used for local/project installs) function getDirName(runtime) { if (runtime === 'opencode') return '.opencode'; @@ -715,14 +700,14 @@ function installCodexConfig(targetDir, agentsSrc) { const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md')); const agents = []; - // Compute the Codex pathPrefix for replacing .claude paths - const codexPathPrefix = `${targetDir.replace(/\\/g, '/')}/`; + // Compute the Codex GSD install path (absolute, so subagents with empty $HOME work — #820) + const codexGsdPath = `${path.resolve(targetDir, 'get-shit-done').replace(/\\/g, '/')}/`; for (const file of agentEntries) { let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8'); - // Replace .claude paths before generating TOML (source files use ~/.claude and $HOME/.claude) - content = content.replace(/~\/\.claude\//g, codexPathPrefix); - content = content.replace(/\$HOME\/\.claude\//g, toHomePrefix(codexPathPrefix)); + // Replace full .claude/get-shit-done prefix so path resolves to codex GSD install + content = content.replace(/~\/\.claude\/get-shit-done\//g, codexGsdPath); + content = content.replace(/\$HOME\/\.claude\/get-shit-done\//g, codexGsdPath); const { frontmatter } = extractFrontmatterAndBody(content); const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', ''); const description = extractFrontmatterField(frontmatter, 'description') || ''; @@ -1032,7 +1017,7 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) { const localClaudeRegex = /\.\/\.claude\//g; const opencodeDirRegex = /~\/\.opencode\//g; content = content.replace(globalClaudeRegex, pathPrefix); - content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix)); + content = content.replace(globalClaudeHomeRegex, pathPrefix); content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`); content = content.replace(opencodeDirRegex, pathPrefix); content = processAttribution(content, getCommitAttribution(runtime)); @@ -1093,7 +1078,7 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim const localClaudeRegex = /\.\/\.claude\//g; const codexDirRegex = /~\/\.codex\//g; content = content.replace(globalClaudeRegex, pathPrefix); - content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix)); + content = content.replace(globalClaudeHomeRegex, pathPrefix); content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`); content = content.replace(codexDirRegex, pathPrefix); content = processAttribution(content, getCommitAttribution(runtime)); @@ -1140,7 +1125,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand const globalClaudeHomeRegex = /\$HOME\/\.claude\//g; const localClaudeRegex = /\.\/\.claude\//g; content = content.replace(globalClaudeRegex, pathPrefix); - content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix)); + content = content.replace(globalClaudeHomeRegex, pathPrefix); content = content.replace(localClaudeRegex, `./${dirName}/`); content = processAttribution(content, getCommitAttribution(runtime)); @@ -1888,12 +1873,11 @@ function install(isGlobal, runtime = 'claude') { ? targetDir.replace(os.homedir(), '~') : targetDir.replace(process.cwd(), '.'); - // Path prefix for file references in markdown content - // For global installs: use full path - // For local installs: use relative - const pathPrefix = isGlobal - ? `${targetDir.replace(/\\/g, '/')}/` - : `./${dirName}/`; + // Path prefix for file references in markdown content (e.g. gsd-tools.cjs). + // Replaces $HOME/.claude/ or ~/.claude/ so the result is get-shit-done/bin/... + // Always use absolute path so: (1) local installs work when GSD is outside $HOME, + // (2) spawned subagents with empty $HOME still resolve the path (fixes #820). + const pathPrefix = `${path.resolve(targetDir).replace(/\\/g, '/')}/`; let runtimeLabel = 'Claude Code'; if (isOpencode) runtimeLabel = 'OpenCode'; @@ -1985,7 +1969,7 @@ function install(isGlobal, runtime = 'claude') { const dirRegex = /~\/\.claude\//g; const homeDirRegex = /\$HOME\/\.claude\//g; content = content.replace(dirRegex, pathPrefix); - content = content.replace(homeDirRegex, toHomePrefix(pathPrefix)); + content = content.replace(homeDirRegex, pathPrefix); content = processAttribution(content, getCommitAttribution(runtime)); // Convert frontmatter for runtime compatibility if (isOpencode) {