From d81dfaf143745d9fa647e7d7c20eef1466b55f22 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Fri, 16 Jan 2026 12:54:56 +0800 Subject: [PATCH] fix: add cross-platform support for hook installation (#82) - Add PlatformUtils module for platform detection (Windows/macOS/Linux) - Add escapeForShell() for platform-specific shell escaping - Add checkCompatibility() to warn about incompatible hooks before install - Add getVariant() to support platform-specific template variants - Fix node -e commands: use double quotes on Windows, single quotes on Unix --- .../dashboard-js/components/hook-manager.js | 120 ++++++++++++++++++ .../dashboard-js/views/hook-manager.js | 32 +++-- 2 files changed, 144 insertions(+), 8 deletions(-) diff --git a/ccw/src/templates/dashboard-js/components/hook-manager.js b/ccw/src/templates/dashboard-js/components/hook-manager.js index 37e8b4bd..87d987ac 100644 --- a/ccw/src/templates/dashboard-js/components/hook-manager.js +++ b/ccw/src/templates/dashboard-js/components/hook-manager.js @@ -1,6 +1,103 @@ // Hook Manager Component // Manages Claude Code hooks configuration from settings.json +// ========== Platform Detection ========== +const PlatformUtils = { + // Detect current platform + detect() { + if (typeof navigator !== 'undefined') { + const platform = navigator.platform.toLowerCase(); + if (platform.includes('win')) return 'windows'; + if (platform.includes('mac')) return 'macos'; + return 'linux'; + } + if (typeof process !== 'undefined') { + if (process.platform === 'win32') return 'windows'; + if (process.platform === 'darwin') return 'macos'; + return 'linux'; + } + return 'unknown'; + }, + + isWindows() { + return this.detect() === 'windows'; + }, + + isUnix() { + const platform = this.detect(); + return platform === 'macos' || platform === 'linux'; + }, + + // Get default shell for platform + getShell() { + return this.isWindows() ? 'cmd' : 'bash'; + }, + + // Check if template is compatible with current platform + checkCompatibility(template) { + const platform = this.detect(); + const issues = []; + + // bash commands require Unix or Git Bash on Windows + if (template.command === 'bash' && platform === 'windows') { + issues.push({ + level: 'warning', + message: 'bash command may not work on Windows without Git Bash or WSL' + }); + } + + // Check for Unix-specific shell features in args + if (template.args && Array.isArray(template.args)) { + const argStr = template.args.join(' '); + + if (platform === 'windows') { + // Unix shell features that won't work in cmd + if (argStr.includes('$HOME') || argStr.includes('${HOME}')) { + issues.push({ level: 'warning', message: 'Uses $HOME - use %USERPROFILE% on Windows' }); + } + if (argStr.includes('$(') || argStr.includes('`')) { + issues.push({ level: 'warning', message: 'Uses command substitution - not supported in cmd' }); + } + if (argStr.includes(' | ')) { + issues.push({ level: 'info', message: 'Uses pipes - works in cmd but syntax may differ' }); + } + } + } + + return { + compatible: issues.filter(i => i.level === 'error').length === 0, + issues + }; + }, + + // Get platform-specific command variant if available + getVariant(template) { + const platform = this.detect(); + + // Check if template has platform-specific variants + if (template.variants && template.variants[platform]) { + return { ...template, ...template.variants[platform] }; + } + + return template; + }, + + // Escape script for specific shell type + escapeForShell(script, shell) { + if (shell === 'bash' || shell === 'sh') { + // Unix: use single quotes, escape internal single quotes + return script.replace(/'/g, "'\\''"); + } else if (shell === 'cmd') { + // Windows cmd: escape double quotes and special chars + return script.replace(/"/g, '\\"').replace(/%/g, '%%'); + } else if (shell === 'powershell') { + // PowerShell: escape single quotes by doubling + return script.replace(/'/g, "''"); + } + return script; + } +}; + // ========== Hook State ========== let hookConfig = { global: { hooks: {} }, @@ -394,6 +491,29 @@ function convertToClaudeCodeFormat(hookData) { }); commandStr += ' ' + additionalArgs.join(' '); } + } else if (commandStr === 'node' && hookData.args.length >= 2 && hookData.args[0] === '-e') { + // Special handling for node -e commands using PlatformUtils + const script = hookData.args[1]; + + if (PlatformUtils.isWindows()) { + // Windows: use double quotes, escape internal quotes + const escapedScript = PlatformUtils.escapeForShell(script, 'cmd'); + commandStr = `node -e "${escapedScript}"`; + } else { + // Unix: use single quotes to prevent shell interpretation + const escapedScript = PlatformUtils.escapeForShell(script, 'bash'); + commandStr = `node -e '${escapedScript}'`; + } + // Handle any additional args after the script + if (hookData.args.length > 2) { + const additionalArgs = hookData.args.slice(2).map(arg => { + if (arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) { + return `"${arg.replace(/"/g, '\\"')}"`; + } + return arg; + }); + commandStr += ' ' + additionalArgs.join(' '); + } } else { // Default handling for other commands const quotedArgs = hookData.args.map(arg => { diff --git a/ccw/src/templates/dashboard-js/views/hook-manager.js b/ccw/src/templates/dashboard-js/views/hook-manager.js index e709926b..1fc82211 100644 --- a/ccw/src/templates/dashboard-js/views/hook-manager.js +++ b/ccw/src/templates/dashboard-js/views/hook-manager.js @@ -524,16 +524,32 @@ async function installHookTemplate(templateId, scope) { return; } - const hookData = { - command: template.command, - args: template.args - }; - - if (template.matcher) { - hookData.matcher = template.matcher; + // Platform compatibility check + const compatibility = PlatformUtils.checkCompatibility(template); + if (compatibility.issues.length > 0) { + const warnings = compatibility.issues.filter(i => i.level === 'warning'); + if (warnings.length > 0) { + const platform = PlatformUtils.detect(); + const warningMsg = warnings.map(w => w.message).join('; '); + console.warn(`[Hook Install] Platform: ${platform}, Warnings: ${warningMsg}`); + // Show warning but continue installation + showRefreshToast(`Warning: ${warningMsg}`, 'warning', 5000); + } } - await saveHook(scope, template.event, hookData); + // Get platform-specific variant if available + const adaptedTemplate = PlatformUtils.getVariant(template); + + const hookData = { + command: adaptedTemplate.command, + args: adaptedTemplate.args + }; + + if (adaptedTemplate.matcher) { + hookData.matcher = adaptedTemplate.matcher; + } + + await saveHook(scope, adaptedTemplate.event, hookData); } async function uninstallHookTemplate(templateId) {