fix(install): template hook paths for OpenCode/Gemini runtimes (#585)
Hooks had hardcoded `.claude` paths that broke OpenCode users. The installer now templates `.js` hooks with runtime-specific config dirs, same as it already does for `.md` files. Also added `./.claude/` replacement for local install paths in workflows. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,27 @@ function getDirName(runtime) {
|
||||
return '.claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config directory path relative to home directory for a runtime
|
||||
* Used for templating hooks that use path.join(homeDir, '<configDir>', ...)
|
||||
* @param {string} runtime - 'claude', 'opencode', or 'gemini'
|
||||
* @param {boolean} isGlobal - Whether this is a global install
|
||||
*/
|
||||
function getConfigDirFromHome(runtime, isGlobal) {
|
||||
if (!isGlobal) {
|
||||
// Local installs use the same dir name pattern
|
||||
return getDirName(runtime);
|
||||
}
|
||||
// Global installs - OpenCode uses XDG path structure
|
||||
if (runtime === 'opencode') {
|
||||
// OpenCode: ~/.config/opencode -> '.config', 'opencode'
|
||||
// Return as comma-separated for path.join() replacement
|
||||
return "'.config', 'opencode'";
|
||||
}
|
||||
if (runtime === 'gemini') return "'.gemini'";
|
||||
return "'.claude'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global config directory for OpenCode
|
||||
* OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/
|
||||
@@ -626,9 +647,11 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
||||
const destPath = path.join(destDir, destName);
|
||||
|
||||
let content = fs.readFileSync(srcPath, 'utf8');
|
||||
const claudeDirRegex = /~\/\.claude\//g;
|
||||
const globalClaudeRegex = /~\/\.claude\//g;
|
||||
const localClaudeRegex = /\.\/\.claude\//g;
|
||||
const opencodeDirRegex = /~\/\.opencode\//g;
|
||||
content = content.replace(claudeDirRegex, pathPrefix);
|
||||
content = content.replace(globalClaudeRegex, pathPrefix);
|
||||
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
||||
content = content.replace(opencodeDirRegex, pathPrefix);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
content = convertClaudeToOpencodeFrontmatter(content);
|
||||
@@ -665,10 +688,12 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
|
||||
if (entry.isDirectory()) {
|
||||
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime);
|
||||
} else if (entry.name.endsWith('.md')) {
|
||||
// Always replace ~/.claude/ as it is the source of truth in the repo
|
||||
// Replace ~/.claude/ and ./.claude/ with runtime-appropriate paths
|
||||
let content = fs.readFileSync(srcPath, 'utf8');
|
||||
const claudeDirRegex = /~\/\.claude\//g;
|
||||
content = content.replace(claudeDirRegex, pathPrefix);
|
||||
const globalClaudeRegex = /~\/\.claude\//g;
|
||||
const localClaudeRegex = /\.\/\.claude\//g;
|
||||
content = content.replace(globalClaudeRegex, pathPrefix);
|
||||
content = content.replace(localClaudeRegex, `./${dirName}/`);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
|
||||
// Convert frontmatter for opencode compatibility
|
||||
@@ -1429,16 +1454,25 @@ function install(isGlobal, runtime = 'claude') {
|
||||
console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`);
|
||||
|
||||
// Copy hooks from dist/ (bundled with dependencies)
|
||||
// Template paths for the target runtime (replaces '.claude' with correct config dir)
|
||||
const hooksSrc = path.join(src, 'hooks', 'dist');
|
||||
if (fs.existsSync(hooksSrc)) {
|
||||
const hooksDest = path.join(targetDir, 'hooks');
|
||||
fs.mkdirSync(hooksDest, { recursive: true });
|
||||
const hookEntries = fs.readdirSync(hooksSrc);
|
||||
const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
|
||||
for (const entry of hookEntries) {
|
||||
const srcFile = path.join(hooksSrc, entry);
|
||||
if (fs.statSync(srcFile).isFile()) {
|
||||
const destFile = path.join(hooksDest, entry);
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
// Template .js files to replace '.claude' with runtime-specific config dir
|
||||
if (entry.endsWith('.js')) {
|
||||
let content = fs.readFileSync(srcFile, 'utf8');
|
||||
content = content.replace(/'\.claude'/g, configDirReplacement);
|
||||
fs.writeFileSync(destFile, content);
|
||||
} else {
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (verifyInstalled(hooksDest, 'hooks')) {
|
||||
|
||||
@@ -14,11 +14,11 @@ After a GSD update wipes and reinstalls files, this command merges user's previo
|
||||
Check for local patches directory:
|
||||
|
||||
```bash
|
||||
# Global install
|
||||
PATCHES_DIR="${HOME}/.claude/gsd-local-patches"
|
||||
# Global install (path templated at install time)
|
||||
PATCHES_DIR=~/.claude/gsd-local-patches
|
||||
# Local install fallback
|
||||
if [ ! -d "$PATCHES_DIR" ]; then
|
||||
PATCHES_DIR="./.claude/gsd-local-patches"
|
||||
PATCHES_DIR=./.claude/gsd-local-patches
|
||||
fi
|
||||
```
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ Detect whether GSD is installed locally or globally by checking both locations:
|
||||
|
||||
```bash
|
||||
# Check local first (takes priority)
|
||||
if [ -f "./.claude/get-shit-done/VERSION" ]; then
|
||||
cat "./.claude/get-shit-done/VERSION"
|
||||
# Paths templated at install time for runtime compatibility
|
||||
if [ -f ./.claude/get-shit-done/VERSION ]; then
|
||||
cat ./.claude/get-shit-done/VERSION
|
||||
echo "LOCAL"
|
||||
elif [ -f ~/.claude/get-shit-done/VERSION ]; then
|
||||
cat ~/.claude/get-shit-done/VERSION
|
||||
@@ -167,6 +168,7 @@ rm -f ./.claude/cache/gsd-update-check.json
|
||||
```bash
|
||||
rm -f ~/.claude/cache/gsd-update-check.json
|
||||
```
|
||||
(Paths are templated at install time for runtime compatibility)
|
||||
</step>
|
||||
|
||||
<step name="display_result">
|
||||
|
||||
Reference in New Issue
Block a user