126 lines
4.9 KiB
JavaScript
126 lines
4.9 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
// gsd-hook-version: 1.30.0
|
||
|
|
// Check for GSD updates in background, write result to cache
|
||
|
|
// Called by SessionStart hook - runs once per session
|
||
|
|
//
|
||
|
|
// SHARED CANONICAL FILE - hardlinked into all harness hooks/ directories.
|
||
|
|
// Harness is auto-detected from __dirname (e.g. .claude/hooks -> .claude).
|
||
|
|
// Do NOT add harness-specific fallbacks here; detection is fully dynamic.
|
||
|
|
|
||
|
|
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();
|
||
|
|
|
||
|
|
// Derive the harness config dir name from this file's location.
|
||
|
|
// e.g. /home/user/.claude/hooks/gsd-check-update.js -> '.claude'
|
||
|
|
// Falls back to the CLAUDE_CONFIG_DIR env var, then a full filesystem search.
|
||
|
|
const harnessDir = path.basename(path.dirname(path.dirname(__filename)));
|
||
|
|
|
||
|
|
// Detect runtime config directory (supports Claude, OpenCode, Gemini, Agent, etc.)
|
||
|
|
// 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;
|
||
|
|
}
|
||
|
|
// Check harness-derived dir first (most specific), then common alternates
|
||
|
|
const searchDirs = [harnessDir, '.config/opencode', '.opencode', '.gemini', '.claude', '.agent'];
|
||
|
|
for (const dir of searchDirs) {
|
||
|
|
if (fs.existsSync(path.join(baseDir, dir, 'get-shit-done', 'VERSION'))) {
|
||
|
|
return path.join(baseDir, dir);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return envDir || path.join(baseDir, harnessDir);
|
||
|
|
}
|
||
|
|
|
||
|
|
const globalConfigDir = detectConfigDir(homeDir);
|
||
|
|
const projectConfigDir = detectConfigDir(cwd);
|
||
|
|
const cacheDir = path.join(globalConfigDir, 'cache');
|
||
|
|
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 (spawn background process, windowsHide prevents console flash)
|
||
|
|
const child = spawn(process.execPath, ['-e', `
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
const { execSync } = require('child_process');
|
||
|
|
|
||
|
|
const cacheFile = ${JSON.stringify(cacheFile)};
|
||
|
|
const projectVersionFile = ${JSON.stringify(projectVersionFile)};
|
||
|
|
const globalVersionFile = ${JSON.stringify(globalVersionFile)};
|
||
|
|
|
||
|
|
// Check project directory first (local install), then global
|
||
|
|
let installed = '0.0.0';
|
||
|
|
let configDir = '';
|
||
|
|
try {
|
||
|
|
if (fs.existsSync(projectVersionFile)) {
|
||
|
|
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
|
||
|
|
configDir = path.dirname(path.dirname(projectVersionFile));
|
||
|
|
} else if (fs.existsSync(globalVersionFile)) {
|
||
|
|
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
|
||
|
|
configDir = path.dirname(path.dirname(globalVersionFile));
|
||
|
|
}
|
||
|
|
} catch (e) {}
|
||
|
|
|
||
|
|
// Check for stale hooks - compare hook version headers against installed VERSION
|
||
|
|
// Hooks live inside get-shit-done/hooks/, not configDir/hooks/
|
||
|
|
let staleHooks = [];
|
||
|
|
if (configDir) {
|
||
|
|
const hooksDir = path.join(configDir, 'get-shit-done', 'hooks');
|
||
|
|
try {
|
||
|
|
if (fs.existsSync(hooksDir)) {
|
||
|
|
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.startsWith('gsd-') && f.endsWith('.js'));
|
||
|
|
for (const hookFile of hookFiles) {
|
||
|
|
try {
|
||
|
|
const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8');
|
||
|
|
const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/);
|
||
|
|
if (versionMatch) {
|
||
|
|
const hookVersion = versionMatch[1].trim();
|
||
|
|
if (hookVersion !== installed && !hookVersion.includes('{{')) {
|
||
|
|
staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// No version header at all - definitely stale (pre-version-tracking)
|
||
|
|
staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed });
|
||
|
|
}
|
||
|
|
} catch (e) {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (e) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
let latest = null;
|
||
|
|
try {
|
||
|
|
latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
|
||
|
|
} catch (e) {}
|
||
|
|
|
||
|
|
const result = {
|
||
|
|
update_available: latest && installed !== latest,
|
||
|
|
installed,
|
||
|
|
latest: latest || 'unknown',
|
||
|
|
checked: Math.floor(Date.now() / 1000),
|
||
|
|
stale_hooks: staleHooks.length > 0 ? staleHooks : undefined
|
||
|
|
};
|
||
|
|
|
||
|
|
fs.writeFileSync(cacheFile, JSON.stringify(result));
|
||
|
|
`], {
|
||
|
|
stdio: 'ignore',
|
||
|
|
windowsHide: true,
|
||
|
|
detached: true // Required on Windows for proper process detachment
|
||
|
|
});
|
||
|
|
|
||
|
|
child.unref();
|