Files
get-shit-done/hooks/gsd-check-update.js
Tom Boucher 50f61bfd9a fix(hooks): complete stale-hooks false-positive fix — stamp .sh version headers + fix detector regex (#2224)
* fix(hooks): stamp gsd-hook-version in .sh hooks and fix stale detection regex (#2136, #2206)

Three-part fix for the persistent "⚠ stale hooks — run /gsd-update" false
positive that appeared on every session after a fresh install.

Root cause: the stale-hook detector (gsd-check-update.js) could only match
the JS comment syntax // in its version regex — never the bash # syntax used
in .sh hooks. And the bash hooks had no version header at all, so they always
landed in the "unknown / stale" branch regardless.

Neither partial fix (PR #2207 regex only, PR #2215 install stamping only) was
sufficient alone:
  - Regex fix without install stamping: hooks install with literal
    "{{GSD_VERSION}}", the {{-guard silently skips them, bash hook staleness
    permanently undetectable after future updates.
  - Install stamping without regex fix: hooks are stamped correctly with
    "# gsd-hook-version: 1.36.0" but the detector's // regex can't read it;
    still falls to the unknown/stale branch on every session.

Fix:
  1. Add "# gsd-hook-version: {{GSD_VERSION}}" header to
     gsd-phase-boundary.sh, gsd-session-state.sh, gsd-validate-commit.sh
  2. Extend install.js (both bundled and Codex paths) to substitute
     {{GSD_VERSION}} in .sh files at install time (same as .js hooks)
  3. Extend gsd-check-update.js versionMatch regex to handle bash "#"
     comment syntax: /(?:\/\/|#) gsd-hook-version:\s*(.+)/

Tests: 11 new assertions across 5 describe blocks covering all three fix
parts independently plus an E2E install+detect round-trip. 3885/3885 pass.

Approach credit: PR #2207 (j2h4u / Maxim Brashenko) for the regex fix;
PR #2215 (nitsan2dots) for the install.js substitution approach.

Closes #2136, #2206, #2209, #2210, #2212

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(hooks): extract check-update worker to dedicated file, eliminating template-literal regex escaping

Move stale-hook detection logic from inline `node -e '<template literal>'` subprocess
to a standalone gsd-check-update-worker.js. Benefits:
- Regex is plain JS with no double-escaping (root cause of the (?:\\/\\/|#) confusion)
- Worker is independently testable and can be read directly by tests
- Uses execFileSync (array args) to satisfy security hook that blocks execSync
- MANAGED_HOOKS now includes gsd-check-update-worker.js itself

Update tests to read worker file instead of main hook for regex/configDir assertions.
All 3886 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:57:38 -04:00

65 lines
2.4 KiB
JavaScript
Executable File

#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Check for GSD updates in background, write result to cache
// Called by SessionStart hook - runs once per session
const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
const homeDir = os.homedir();
const cwd = process.cwd();
// Detect runtime config directory (supports Claude, OpenCode, Kilo, Gemini)
// Respects CLAUDE_CONFIG_DIR for custom config directory setups
function detectConfigDir(baseDir) {
// Check env override first (supports multi-account setups)
const envDir = process.env.CLAUDE_CONFIG_DIR;
if (envDir && fs.existsSync(path.join(envDir, 'get-shit-done', 'VERSION'))) {
return envDir;
}
for (const dir of ['.claude', '.gemini', '.config/kilo', '.kilo', '.config/opencode', '.opencode']) {
if (fs.existsSync(path.join(baseDir, dir, 'get-shit-done', 'VERSION'))) {
return path.join(baseDir, dir);
}
}
return envDir || path.join(baseDir, '.claude');
}
const globalConfigDir = detectConfigDir(homeDir);
const projectConfigDir = detectConfigDir(cwd);
// Use a shared, tool-agnostic cache directory to avoid multi-runtime
// resolution mismatches where check-update writes to one runtime's cache
// but statusline reads from another (#1421).
const cacheDir = path.join(homeDir, '.cache', 'gsd');
const cacheFile = path.join(cacheDir, 'gsd-update-check.json');
// VERSION file locations (check project first, then global)
const projectVersionFile = path.join(projectConfigDir, 'get-shit-done', 'VERSION');
const globalVersionFile = path.join(globalConfigDir, 'get-shit-done', 'VERSION');
// Ensure cache directory exists
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// Run check in background via a dedicated worker script.
// Spawning a file (rather than node -e '<inline code>') keeps the worker logic
// in plain JS with no template-literal regex-escaping concerns, and makes the
// worker independently testable.
const workerPath = path.join(__dirname, 'gsd-check-update-worker.js');
const child = spawn(process.execPath, [workerPath], {
stdio: 'ignore',
windowsHide: true,
detached: true, // Required on Windows for proper process detachment
env: {
...process.env,
GSD_CACHE_FILE: cacheFile,
GSD_PROJECT_VERSION_FILE: projectVersionFile,
GSD_GLOBAL_VERSION_FILE: globalVersionFile,
},
});
child.unref();