fix: Windows hook support via Node.js conversion

- Convert statusline.sh and gsd-check-update.sh to Node.js
- Remove gsd-notify.sh (blocking popups, not true toasts)
- Remove --force-notify and --no-notify flags from installer
- Use `node` command for cross-platform hook execution

Fixes #114

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson
2026-01-16 22:56:26 -06:00
parent 7c60722b71
commit 967734df21
6 changed files with 148 additions and 246 deletions

View File

@@ -55,8 +55,6 @@ function parseConfigDirArg() {
const explicitConfigDir = parseConfigDirArg();
const hasHelp = args.includes('--help') || args.includes('-h');
const forceStatusline = args.includes('--force-statusline');
const forceNotify = args.includes('--force-notify');
const noNotify = args.includes('--no-notify');
console.log(banner);
@@ -70,8 +68,6 @@ if (hasHelp) {
${cyan}-c, --config-dir <path>${reset} Specify custom Claude config directory
${cyan}-h, --help${reset} Show this help message
${cyan}--force-statusline${reset} Replace existing statusline config
${cyan}--force-notify${reset} Replace existing notification hook
${cyan}--no-notify${reset} Skip notification hook installation
${yellow}Examples:${reset}
${dim}# Install to default ~/.claude directory${reset}
@@ -221,10 +217,6 @@ function install(isGlobal) {
const srcFile = path.join(hooksSrc, entry);
const destFile = path.join(hooksDest, entry);
fs.copyFileSync(srcFile, destFile);
// Make shell scripts executable
if (entry.endsWith('.sh')) {
fs.chmodSync(destFile, 0o755);
}
}
console.log(` ${green}✓${reset} Installed hooks`);
}
@@ -233,14 +225,11 @@ function install(isGlobal) {
const settingsPath = path.join(claudeDir, 'settings.json');
const settings = readSettings(settingsPath);
const statuslineCommand = isGlobal
? '$HOME/.claude/hooks/statusline.sh'
: '.claude/hooks/statusline.sh';
? 'node "$HOME/.claude/hooks/statusline.js"'
: 'node .claude/hooks/statusline.js';
const updateCheckCommand = isGlobal
? '$HOME/.claude/hooks/gsd-check-update.sh'
: '.claude/hooks/gsd-check-update.sh';
const notifyCommand = isGlobal
? '$HOME/.claude/hooks/gsd-notify.sh'
: '.claude/hooks/gsd-notify.sh';
? 'node "$HOME/.claude/hooks/gsd-check-update.js"'
: 'node .claude/hooks/gsd-check-update.js';
// Configure SessionStart hook for update checking
if (!settings.hooks) {
@@ -267,13 +256,13 @@ function install(isGlobal) {
console.log(` ${green}✓${reset} Configured update check hook`);
}
return { settingsPath, settings, statuslineCommand, notifyCommand };
return { settingsPath, settings, statuslineCommand };
}
/**
* Apply statusline and notification config, then print completion message
* Apply statusline config, then print completion message
*/
function finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, shouldInstallStatusline, shouldInstallNotify) {
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline) {
if (shouldInstallStatusline) {
settings.statusLine = {
type: 'command',
@@ -282,25 +271,6 @@ function finishInstall(settingsPath, settings, statuslineCommand, notifyCommand,
console.log(` ${green}✓${reset} Configured statusline`);
}
if (shouldInstallNotify) {
if (!settings.hooks.Stop) {
settings.hooks.Stop = [];
}
// Remove any existing GSD notify hook first
settings.hooks.Stop = settings.hooks.Stop.filter(entry =>
!(entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-notify')))
);
settings.hooks.Stop.push({
hooks: [
{
type: 'command',
command: notifyCommand
}
]
});
console.log(` ${green}✓${reset} Configured completion notifications`);
}
// Always write settings (hooks were already configured in install())
writeSettings(settingsPath, settings);
@@ -365,66 +335,6 @@ function handleStatusline(settings, isInteractive, callback) {
});
}
/**
* Handle notification hook configuration with optional prompt
*/
function handleNotifications(settings, isInteractive, callback) {
// Check if --no-notify flag was passed
if (noNotify) {
callback(false);
return;
}
// Check if GSD notify hook already exists
const hasExisting = settings.hooks?.Stop?.some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-notify'))
);
// No existing - just install it
if (!hasExisting) {
callback(true);
return;
}
// Has existing and --force-notify flag
if (forceNotify) {
callback(true);
return;
}
// Has existing, non-interactive mode - skip
if (!isInteractive) {
console.log(` ${yellow}⚠${reset} Skipping notifications (already configured)`);
console.log(` Use ${cyan}--force-notify${reset} to replace\n`);
callback(false);
return;
}
// Has existing, interactive mode - prompt user
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log(`
${yellow}⚠${reset} Existing notification hook detected
GSD includes completion notifications that alert you when:
• A phase completes planning or execution
• Claude stops and needs your input
• Works on Mac, Linux, and Windows
${cyan}1${reset}) Keep existing
${cyan}2${reset}) Replace with GSD notifications
`);
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
rl.close();
const choice = answer.trim() || '1';
callback(choice === '2');
});
}
/**
* Prompt for install location
*/
@@ -448,12 +358,10 @@ function promptLocation() {
rl.close();
const choice = answer.trim() || '1';
const isGlobal = choice !== '2';
const { settingsPath, settings, statuslineCommand, notifyCommand } = install(isGlobal);
const { settingsPath, settings, statuslineCommand } = install(isGlobal);
// Interactive mode - prompt for optional features
handleStatusline(settings, true, (shouldInstallStatusline) => {
handleNotifications(settings, true, (shouldInstallNotify) => {
finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, shouldInstallStatusline, shouldInstallNotify);
});
finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
});
});
}
@@ -466,20 +374,16 @@ if (hasGlobal && hasLocal) {
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
process.exit(1);
} else if (hasGlobal) {
const { settingsPath, settings, statuslineCommand, notifyCommand } = install(true);
const { settingsPath, settings, statuslineCommand } = install(true);
// Non-interactive - respect flags
handleStatusline(settings, false, (shouldInstallStatusline) => {
handleNotifications(settings, false, (shouldInstallNotify) => {
finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, shouldInstallStatusline, shouldInstallNotify);
});
finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
});
} else if (hasLocal) {
const { settingsPath, settings, statuslineCommand, notifyCommand } = install(false);
const { settingsPath, settings, statuslineCommand } = install(false);
// Non-interactive - respect flags
handleStatusline(settings, false, (shouldInstallStatusline) => {
handleNotifications(settings, false, (shouldInstallNotify) => {
finishInstall(settingsPath, settings, statuslineCommand, notifyCommand, shouldInstallStatusline, shouldInstallNotify);
});
finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
});
} else {
promptLocation();