Fix Windows HUD npm root discovery

The HUD wrapper needs npm root discovery to keep global installs reachable when cache and marketplace paths are absent. Node 20.12+ rejects direct npm.cmd execFileSync calls on Windows unless shell execution is enabled, so limit shell:true to the Windows npm.cmd probe while preserving the existing non-Windows npm exec behavior.

Constraint: Node 20.12+ requires shell execution for Windows .cmd/.bat child_process calls.

Rejected: Use execSync('npm root -g') everywhere | broader behavior change for non-Windows wrapper runtime.

Confidence: high

Scope-risk: narrow

Tested: npm test -- --run src/installer/__tests__/hud-wrapper-env.test.ts src/__tests__/hud-windows.test.ts src/__tests__/hud-wrapper-template-sync.test.ts src/__tests__/hud-marketplace-resolution.test.ts

Tested: npx tsc
This commit is contained in:
Codex Review
2026-04-18 09:12:30 +00:00
parent c90aadab90
commit 56513f28a2
7 changed files with 40 additions and 3 deletions

8
dist/__tests__/hud-windows.test.js generated vendored
View File

@@ -53,6 +53,14 @@ describe('HUD Windows Compatibility', () => {
const content = readFileSync(templatePath, 'utf-8');
expect(content).toContain('pathToFileURL(pluginPath).href');
});
it('shared HUD wrapper template uses shell:true only for Windows npm root discovery', () => {
const templatePath = join(packageRoot, 'scripts', 'lib', 'hud-wrapper-template.txt');
const content = readFileSync(templatePath, 'utf-8');
expect(content).toContain('const isWin = process.platform === "win32";');
expect(content).toContain('const npmCommand = isWin ? "npm.cmd" : "npm";');
expect(content).toContain('shell: isWin');
expect(content).not.toContain('shell: true');
});
it('pathToFileURL should correctly convert Unix paths', () => {
const unixPath = '/home/user/test.js';
expect(pathToFileURL(unixPath).href).toBe(process.platform === 'win32'

File diff suppressed because one or more lines are too long

View File

@@ -229,6 +229,13 @@ describe('HUD wrapper — OMC_PLUGIN_ROOT resolution', () => {
expect(txt).not.toContain('Workspace/oh-my-claudecode');
expect(txt).not.toContain('projects/oh-my-claudecode');
});
it('uses shell:true only for Windows npm root discovery', () => {
const txt = readFileSync(TEMPLATE_TXT, 'utf8');
expect(txt).toContain('const isWin = process.platform === "win32";');
expect(txt).toContain('const npmCommand = isWin ? "npm.cmd" : "npm";');
expect(txt).toContain('shell: isWin');
expect(txt).not.toContain('shell: true');
});
});
describe('HUD wrapper — fixture sanity', () => {
it('the template txt file exists in the repo', () => {

File diff suppressed because one or more lines are too long

View File

@@ -46,11 +46,15 @@ function getGlobalNodeModuleRoots() {
}
try {
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
const isWin = process.platform === "win32";
const npmCommand = isWin ? "npm.cmd" : "npm";
const npmRoot = String(execFileSync(npmCommand, ["root", "-g"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 1500,
// Node 20.12+ rejects direct .cmd/.bat spawns without a shell on Windows.
// Keep non-Windows behavior unchanged by only enabling shell there.
shell: isWin,
})).trim();
if (npmRoot) roots.unshift(npmRoot);
} catch { /* continue */ }

View File

@@ -63,6 +63,15 @@ describe('HUD Windows Compatibility', () => {
expect(content).toContain('pathToFileURL(pluginPath).href');
});
it('shared HUD wrapper template uses shell:true only for Windows npm root discovery', () => {
const templatePath = join(packageRoot, 'scripts', 'lib', 'hud-wrapper-template.txt');
const content = readFileSync(templatePath, 'utf-8');
expect(content).toContain('const isWin = process.platform === "win32";');
expect(content).toContain('const npmCommand = isWin ? "npm.cmd" : "npm";');
expect(content).toContain('shell: isWin');
expect(content).not.toContain('shell: true');
});
it('pathToFileURL should correctly convert Unix paths', () => {
const unixPath = '/home/user/test.js';
expect(pathToFileURL(unixPath).href).toBe(

View File

@@ -302,6 +302,15 @@ describe('HUD wrapper — OMC_PLUGIN_ROOT resolution', () => {
expect(txt).not.toContain('Workspace/oh-my-claudecode');
expect(txt).not.toContain('projects/oh-my-claudecode');
});
it('uses shell:true only for Windows npm root discovery', () => {
const txt = readFileSync(TEMPLATE_TXT, 'utf8');
expect(txt).toContain('const isWin = process.platform === "win32";');
expect(txt).toContain('const npmCommand = isWin ? "npm.cmd" : "npm";');
expect(txt).toContain('shell: isWin');
expect(txt).not.toContain('shell: true');
});
});
describe('HUD wrapper — fixture sanity', () => {