- Add chmod +x for .sh hooks during install (fixes #1755 permission denied) - Fix Codex hook: wrong path (get-shit-done/hooks/) and inverted filename (gsd-update-check.js → gsd-check-update.js) - Fix cache invalidation path from ~/cache/ to ~/.cache/gsd/ - Track .sh hooks in writeManifest so saveLocalPatches detects modifications - Add gsd-workflow-guard.js to uninstall file cleanup list - Add community hooks (session-state, validate-commit, phase-boundary) to uninstall settings.json cleanup - Remove phantom gsd-check-update.sh from uninstall list - Remove dead isCursor/isWindsurf branches in uninstall (already handled by combined branch) - Warn when expected .sh hooks are missing after verifyInstalled - Add 15 regression tests in install-hooks-copy.test.cjs - Update codex-config.test.cjs assertions for corrected hook filename Fixes #1755
This commit is contained in:
@@ -4260,40 +4260,6 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Antigravity skills`);
|
||||
}
|
||||
}
|
||||
} else if (isCursor) {
|
||||
// Cursor: remove skills/gsd-*/ directories (same layout as Codex skills)
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
let skillCount = 0;
|
||||
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
||||
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
||||
skillCount++;
|
||||
}
|
||||
}
|
||||
if (skillCount > 0) {
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Cursor skills`);
|
||||
}
|
||||
}
|
||||
} else if (isWindsurf) {
|
||||
// Windsurf: remove skills/gsd-*/ directories (same layout as Cursor skills)
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
let skillCount = 0;
|
||||
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
||||
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
||||
skillCount++;
|
||||
}
|
||||
}
|
||||
if (skillCount > 0) {
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Windsurf skills`);
|
||||
}
|
||||
}
|
||||
} else if (isGemini) {
|
||||
// Gemini: still uses commands/gsd/
|
||||
const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
|
||||
@@ -4403,7 +4369,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
// 4. Remove GSD hooks
|
||||
const hooksDir = path.join(targetDir, 'hooks');
|
||||
if (fs.existsSync(hooksDir)) {
|
||||
const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh', 'gsd-context-monitor.js', 'gsd-prompt-guard.js', 'gsd-read-guard.js', 'gsd-session-state.sh', 'gsd-validate-commit.sh', 'gsd-phase-boundary.sh'];
|
||||
const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-context-monitor.js', 'gsd-prompt-guard.js', 'gsd-read-guard.js', 'gsd-workflow-guard.js', 'gsd-session-state.sh', 'gsd-validate-commit.sh', 'gsd-phase-boundary.sh'];
|
||||
let hookCount = 0;
|
||||
for (const hook of gsdHooks) {
|
||||
const hookPath = path.join(hooksDir, hook);
|
||||
@@ -4459,7 +4425,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
if (entry.hooks && Array.isArray(entry.hooks)) {
|
||||
// Filter out GSD hooks
|
||||
const hasGsdHook = entry.hooks.some(h =>
|
||||
h.command && (h.command.includes('gsd-check-update') || h.command.includes('gsd-statusline'))
|
||||
h.command && (h.command.includes('gsd-check-update') || h.command.includes('gsd-statusline') || h.command.includes('gsd-session-state'))
|
||||
);
|
||||
return !hasGsdHook;
|
||||
}
|
||||
@@ -4482,7 +4448,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
settings.hooks[eventName] = settings.hooks[eventName].filter(entry => {
|
||||
if (entry.hooks && Array.isArray(entry.hooks)) {
|
||||
const hasGsdHook = entry.hooks.some(h =>
|
||||
h.command && h.command.includes('gsd-context-monitor')
|
||||
h.command && (h.command.includes('gsd-context-monitor') || h.command.includes('gsd-phase-boundary'))
|
||||
);
|
||||
return !hasGsdHook;
|
||||
}
|
||||
@@ -4505,7 +4471,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
settings.hooks[eventName] = settings.hooks[eventName].filter(entry => {
|
||||
if (entry.hooks && Array.isArray(entry.hooks)) {
|
||||
const hasGsdHook = entry.hooks.some(h =>
|
||||
h.command && (h.command.includes('gsd-prompt-guard') || h.command.includes('gsd-read-guard'))
|
||||
h.command && (h.command.includes('gsd-prompt-guard') || h.command.includes('gsd-read-guard') || h.command.includes('gsd-validate-commit'))
|
||||
);
|
||||
return !hasGsdHook;
|
||||
}
|
||||
@@ -4971,7 +4937,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
const hooksDir = path.join(configDir, 'hooks');
|
||||
if (fs.existsSync(hooksDir)) {
|
||||
for (const file of fs.readdirSync(hooksDir)) {
|
||||
if (file.startsWith('gsd-') && file.endsWith('.js')) {
|
||||
if (file.startsWith('gsd-') && (file.endsWith('.js') || file.endsWith('.sh'))) {
|
||||
manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
|
||||
}
|
||||
}
|
||||
@@ -5392,11 +5358,22 @@ function install(isGlobal, runtime = 'claude') {
|
||||
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
|
||||
} else {
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
// Ensure .sh hook files are executable (mirrors chmod in build-hooks.js)
|
||||
if (entry.endsWith('.sh')) {
|
||||
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (verifyInstalled(hooksDest, 'hooks')) {
|
||||
console.log(` ${green}✓${reset} Installed hooks (bundled)`);
|
||||
// Warn if expected community .sh hooks are missing (non-fatal)
|
||||
const expectedShHooks = ['gsd-session-state.sh', 'gsd-validate-commit.sh', 'gsd-phase-boundary.sh'];
|
||||
for (const sh of expectedShHooks) {
|
||||
if (!fs.existsSync(path.join(hooksDest, sh))) {
|
||||
console.warn(` ${yellow}⚠${reset} Missing expected hook: ${sh}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
failures.push('hooks');
|
||||
}
|
||||
@@ -5404,8 +5381,8 @@ function install(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
|
||||
// Clear stale update cache so next session re-evaluates hook versions
|
||||
// targetDir is e.g. ~/.claude/get-shit-done/, parent is the config dir
|
||||
const updateCacheFile = path.join(path.dirname(targetDir), 'cache', 'gsd-update-check.json');
|
||||
// Cache lives at ~/.cache/gsd/ (see hooks/gsd-check-update.js line 35-36)
|
||||
const updateCacheFile = path.join(os.homedir(), '.cache', 'gsd', 'gsd-update-check.json');
|
||||
try { fs.unlinkSync(updateCacheFile); } catch (e) { /* cache may not exist yet */ }
|
||||
|
||||
if (failures.length > 0) {
|
||||
@@ -5484,14 +5461,14 @@ function install(isGlobal, runtime = 'claude') {
|
||||
configContent = setManagedCodexHooksOwnership(codexHooksFeature.content, codexHooksFeature.ownership);
|
||||
|
||||
// Add SessionStart hook for update checking
|
||||
const updateCheckScript = path.resolve(targetDir, 'get-shit-done', 'hooks', 'gsd-update-check.js').replace(/\\/g, '/');
|
||||
const updateCheckScript = path.resolve(targetDir, 'hooks', 'gsd-check-update.js').replace(/\\/g, '/');
|
||||
const hookBlock =
|
||||
`${eol}# GSD Hooks${eol}` +
|
||||
`[[hooks]]${eol}` +
|
||||
`event = "SessionStart"${eol}` +
|
||||
`command = "node ${updateCheckScript}"${eol}`;
|
||||
|
||||
if (hasEnabledCodexHooksFeature(configContent) && !configContent.includes('gsd-update-check')) {
|
||||
if (hasEnabledCodexHooksFeature(configContent) && !configContent.includes('gsd-check-update')) {
|
||||
configContent += hookBlock;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user