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
This commit is contained in:
catlog22
2026-01-16 12:54:56 +08:00
parent d7e5ee44cc
commit d81dfaf143
2 changed files with 144 additions and 8 deletions

View File

@@ -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 => {

View File

@@ -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) {