mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 22:43:28 +08:00
Merge pull request #1445 from affaan-m/fix/plugin-installed-hook-root-resolution
fix: resolve plugin-installed hook root on marketplace installs
This commit is contained in:
153
scripts/hooks/plugin-hook-bootstrap.js
Normal file
153
scripts/hooks/plugin-hook-bootstrap.js
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
function readStdinRaw() {
|
||||
try {
|
||||
return fs.readFileSync(0, 'utf8');
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function writeStderr(stderr) {
|
||||
if (typeof stderr === 'string' && stderr.length > 0) {
|
||||
process.stderr.write(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
function passthrough(raw, result) {
|
||||
const stdout = typeof result?.stdout === 'string' ? result.stdout : '';
|
||||
if (stdout) {
|
||||
process.stdout.write(stdout);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(result?.status) || result.status === 0) {
|
||||
process.stdout.write(raw);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTarget(rootDir, relPath) {
|
||||
const resolvedRoot = path.resolve(rootDir);
|
||||
const resolvedTarget = path.resolve(rootDir, relPath);
|
||||
if (
|
||||
resolvedTarget !== resolvedRoot &&
|
||||
!resolvedTarget.startsWith(resolvedRoot + path.sep)
|
||||
) {
|
||||
throw new Error(`Path traversal rejected: ${relPath}`);
|
||||
}
|
||||
return resolvedTarget;
|
||||
}
|
||||
|
||||
function findShellBinary() {
|
||||
const candidates = [];
|
||||
if (process.env.BASH && process.env.BASH.trim()) {
|
||||
candidates.push(process.env.BASH.trim());
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
candidates.push('bash.exe', 'bash');
|
||||
} else {
|
||||
candidates.push('bash', 'sh');
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const probe = spawnSync(candidate, ['-c', ':'], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
});
|
||||
if (!probe.error) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function spawnNode(rootDir, relPath, raw, args) {
|
||||
return spawnSync(process.execPath, [resolveTarget(rootDir, relPath), ...args], {
|
||||
input: raw,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_PLUGIN_ROOT: rootDir,
|
||||
ECC_PLUGIN_ROOT: rootDir,
|
||||
},
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
windowsHide: true,
|
||||
});
|
||||
}
|
||||
|
||||
function spawnShell(rootDir, relPath, raw, args) {
|
||||
const shell = findShellBinary();
|
||||
if (!shell) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: '',
|
||||
stderr: '[Hook] shell runtime unavailable; skipping shell-backed hook\n',
|
||||
};
|
||||
}
|
||||
|
||||
return spawnSync(shell, [resolveTarget(rootDir, relPath), ...args], {
|
||||
input: raw,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_PLUGIN_ROOT: rootDir,
|
||||
ECC_PLUGIN_ROOT: rootDir,
|
||||
},
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
windowsHide: true,
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [, , mode, relPath, ...args] = process.argv;
|
||||
const raw = readStdinRaw();
|
||||
const rootDir = process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT;
|
||||
|
||||
if (!mode || !relPath || !rootDir) {
|
||||
process.stdout.write(raw);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
if (mode === 'node') {
|
||||
result = spawnNode(rootDir, relPath, raw, args);
|
||||
} else if (mode === 'shell') {
|
||||
result = spawnShell(rootDir, relPath, raw, args);
|
||||
} else {
|
||||
writeStderr(`[Hook] unknown bootstrap mode: ${mode}\n`);
|
||||
process.stdout.write(raw);
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
writeStderr(`[Hook] bootstrap resolution failed: ${error.message}\n`);
|
||||
process.stdout.write(raw);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
passthrough(raw, result);
|
||||
writeStderr(result.stderr);
|
||||
|
||||
if (result.error || result.signal || result.status === null) {
|
||||
const reason = result.error
|
||||
? result.error.message
|
||||
: result.signal
|
||||
? `terminated by signal ${result.signal}`
|
||||
: 'missing exit status';
|
||||
writeStderr(`[Hook] bootstrap execution failed: ${reason}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.exit(Number.isInteger(result.status) ? result.status : 0);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user