From 5d703954c99bdfff07083c1aa6abbb2363e88d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Sun, 15 Mar 2026 11:44:25 -0600 Subject: [PATCH] fix: use absolute paths for gsd-tools.cjs in all install types (#820) (#1062) Local installs wrote $HOME/.claude/get-shit-done/bin/gsd-tools.cjs into workflow files, which breaks when GSD is installed outside $HOME (e.g. external drives, symlinked projects) and when spawned subagents have an empty $HOME environment variable. - pathPrefix now always resolves to an absolute path via path.resolve() - All $HOME/.claude/ replacements use the absolute prefix directly - Codex installer uses absolute path for get-shit-done prefix - Removed unused toHomePrefix() function Tested: 535/535 existing tests pass, verified local install produces correct absolute paths, verified global install unchanged, verified empty $HOME scenario resolves correctly. Closes #820 Made-with: Cursor Co-authored-by: ralberts3 --- CHANGELOG.md | 1 + bin/install.js | 50 ++++++++++++++------------------------------------ 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bca5dc742..8086e8765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - OpenCode agent frontmatter conversion — agents now get correct `name:`, `model: inherit`, `mode: subagent` instead of broken command-style conversion that stripped name and kept `color:`/`skills:`/`tools:` +- 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.23.0] - 2026-03-15 diff --git a/bin/install.js b/bin/install.js index af7ab4e95..45dffc24a 100755 --- a/bin/install.js +++ b/bin/install.js @@ -115,25 +115,6 @@ Then re-run: npx get-shit-done-cc@latest } } -/** - * 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); - } - // Convert tilde-based paths to $HOME-based paths for bash code blocks - if (normalized.startsWith('~/')) { - return '$HOME' + normalized.slice(1); - } - // 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 === 'copilot') return '.github'; @@ -941,15 +922,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 - // Use tilde-based path to avoid baking absolute paths into templates - const codexPathPrefix = `${targetDir.replace(/\\/g, '/').replace(os.homedir().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') || ''; @@ -1306,7 +1286,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)); @@ -1367,7 +1347,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)); @@ -1466,7 +1446,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)); @@ -2259,13 +2239,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 tilde-based path (~/.claude/) to avoid baking - // absolute paths (containing OS username) into templates - // For local installs: use relative - const pathPrefix = isGlobal - ? `${targetDir.replace(/\\/g, '/').replace(os.homedir().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'; @@ -2374,7 +2352,7 @@ function install(isGlobal, runtime = 'claude') { const homeDirRegex = /\$HOME\/\.claude\//g; if (!isCopilot) { 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 (agents need different handling)