fix(factory-plugin): honor GITNEXUS_HOOK_CLI_PATH so augment runs on Windows

This commit is contained in:
joseph02 2026-07-21 11:42:20 -07:00
parent 2bfb276eb6
commit b813d39666
2 changed files with 29 additions and 10 deletions

View file

@ -137,12 +137,18 @@ function extractPattern(toolName, toolInput) {
* fd level, making it unusable in subprocess contexts). Tries a PATH-installed
* binary first, then falls back to npx.
*
* Honors GITNEXUS_HOOK_CLI_PATH first, same as the Claude adapter: it runs the
* CLI as `node <path>`, which is the only branch that works on Windows, where
* Node refuses to spawn the `.cmd` launcher shims below without a shell
* (CVE-2024-27980). Falls back to a PATH binary, then npx.
*
* SECURITY: `pattern` is passed after the `--` end-of-options marker and never
* through a shell — the Windows npx fallback invokes `npx.cmd` directly rather
* than `shell: true`, so a pattern like `-rf` or `$(...)` is inert.
*/
function runAugment(pattern, cwd) {
const isWin = process.platform === 'win32';
const args = ['augment', '--', pattern];
const spawnOpts = {
encoding: 'utf-8',
timeout: 8000,
@ -151,12 +157,21 @@ function runAugment(pattern, cwd) {
windowsHide: true,
};
const hookCli = process.env.GITNEXUS_HOOK_CLI_PATH;
if (hookCli && String(hookCli).trim() && fs.existsSync(String(hookCli))) {
try {
const child = spawnSync(process.execPath, [String(hookCli), ...args], spawnOpts);
if (!child.error && child.status === 0 && child.stderr && child.stderr.trim()) {
return child.stderr;
}
} catch {
/* graceful failure */
}
return '';
}
try {
const child = spawnSync(
isWin ? 'gitnexus.cmd' : 'gitnexus',
['augment', '--', pattern],
spawnOpts,
);
const child = spawnSync(isWin ? 'gitnexus.cmd' : 'gitnexus', args, spawnOpts);
if (!child.error && child.status === 0 && child.stderr && child.stderr.trim()) {
return child.stderr;
}
@ -165,11 +180,7 @@ function runAugment(pattern, cwd) {
}
try {
const child = spawnSync(
isWin ? 'npx.cmd' : 'npx',
['-y', 'gitnexus', 'augment', '--', pattern],
spawnOpts,
);
const child = spawnSync(isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args], spawnOpts);
if (!child.error && child.status === 0 && child.stderr && child.stderr.trim()) {
return child.stderr;
}

View file

@ -155,6 +155,14 @@ describe('Factory hook source regressions', () => {
expect(source).toContain('npx.cmd');
});
// Windows regression: Node refuses to spawn the .cmd launcher shims without a
// shell (CVE-2024-27980), so `node <cliPath>` is the only branch that runs
// there. Same escape hatch the Claude adapter honors.
it('prefers GITNEXUS_HOOK_CLI_PATH via process.execPath', () => {
expect(source).toContain('GITNEXUS_HOOK_CLI_PATH');
expect(source).toMatch(/spawnSync\(\s*process\.execPath/);
});
it('passes the pattern after the -- end-of-options marker', () => {
expect(source).toMatch(/'augment',\s*'--',\s*pattern/);
});