diff --git a/scripts/hooks/plugin-hook-bootstrap.js b/scripts/hooks/plugin-hook-bootstrap.js index 6f6152c8..aef94aab 100644 --- a/scripts/hooks/plugin-hook-bootstrap.js +++ b/scripts/hooks/plugin-hook-bootstrap.js @@ -31,6 +31,20 @@ function passthrough(raw, result) { } } +function normalizePluginRootForPlatform(rootDir, platform = process.platform) { + if (platform !== 'win32' || typeof rootDir !== 'string') { + return rootDir; + } + + const match = rootDir.match(/^\/([a-zA-Z])(?:\/(.*))?$/); + if (!match) { + return rootDir; + } + + const [, driveLetter, rest = ''] = match; + return `${driveLetter.toUpperCase()}:/${rest}`; +} + function resolveTarget(rootDir, relPath) { const resolvedRoot = path.resolve(rootDir); const resolvedTarget = path.resolve(rootDir, relPath); @@ -110,7 +124,9 @@ function spawnShell(rootDir, relPath, raw, args) { function main() { const [, , mode, relPath, ...args] = process.argv; const raw = readStdinRaw(); - const rootDir = process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT; + const rootDir = normalizePluginRootForPlatform( + process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT + ); if (!mode || !relPath || !rootDir) { process.stdout.write(raw); @@ -150,4 +166,11 @@ function main() { process.exit(Number.isInteger(result.status) ? result.status : 0); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { + main, + normalizePluginRootForPlatform, +}; diff --git a/tests/hooks/plugin-hook-bootstrap.test.js b/tests/hooks/plugin-hook-bootstrap.test.js index 0b54f3c7..b5a913d3 100644 --- a/tests/hooks/plugin-hook-bootstrap.test.js +++ b/tests/hooks/plugin-hook-bootstrap.test.js @@ -11,6 +11,7 @@ const path = require('path'); const { spawnSync } = require('child_process'); const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'plugin-hook-bootstrap.js'); +const { normalizePluginRootForPlatform } = require(SCRIPT); function createTempDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'plugin-hook-bootstrap-')); @@ -68,6 +69,50 @@ function runTests() { assert.strictEqual(result.stderr, ''); })) passed++; else failed++; + if (test('normalizes Windows Git Bash POSIX drive roots', () => { + assert.strictEqual( + normalizePluginRootForPlatform('/c/Users/x/.claude/plugins/ecc', 'win32'), + 'C:/Users/x/.claude/plugins/ecc' + ); + assert.strictEqual( + normalizePluginRootForPlatform('/z/Work/ECC/scripts/hooks/check-console-log.js', 'win32'), + 'Z:/Work/ECC/scripts/hooks/check-console-log.js' + ); + })) passed++; else failed++; + + if (test('leaves already-Windows roots unchanged', () => { + assert.strictEqual( + normalizePluginRootForPlatform('C:/Users/x/.claude/plugins/ecc', 'win32'), + 'C:/Users/x/.claude/plugins/ecc' + ); + assert.strictEqual( + normalizePluginRootForPlatform('D:\\Users\\x\\.claude\\plugins\\ecc', 'win32'), + 'D:\\Users\\x\\.claude\\plugins\\ecc' + ); + })) passed++; else failed++; + + if (test('leaves POSIX-looking roots unchanged off Windows', () => { + assert.strictEqual( + normalizePluginRootForPlatform('/c/Users/x/.claude/plugins/ecc', 'darwin'), + '/c/Users/x/.claude/plugins/ecc' + ); + assert.strictEqual( + normalizePluginRootForPlatform('/c/Users/x/.claude/plugins/ecc', 'linux'), + '/c/Users/x/.claude/plugins/ecc' + ); + })) passed++; else failed++; + + if (test('does not mangle UNC or non-drive absolute paths on Windows', () => { + assert.strictEqual( + normalizePluginRootForPlatform('\\\\server\\share\\ecc', 'win32'), + '\\\\server\\share\\ecc' + ); + assert.strictEqual( + normalizePluginRootForPlatform('/workspace/ecc', 'win32'), + '/workspace/ecc' + ); + })) passed++; else failed++; + if (test('node mode runs target script with plugin root environment', () => { const root = createTempDir(); try {