Skip to content

Commit 92ffcb6

Browse files
committed
fix: prefer powershell cli shims on Windows
1 parent 67c2a2c commit 92ffcb6

6 files changed

Lines changed: 113 additions & 2 deletions

File tree

dist/engines/shared.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/engines/shared.js

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/engines/shared.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__tests__/shared-spawn.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,55 @@ describe('engine shared spawn helpers', () => {
9292
options,
9393
);
9494
});
95+
96+
it('spawnCommand prefers a sibling PowerShell shim on Windows when available', async () => {
97+
const originalPlatform = process.platform;
98+
Object.defineProperty(process, 'platform', { value: 'win32' });
99+
const crossSpawnMock = vi.fn();
100+
try {
101+
vi.doMock('cross-spawn', () => ({
102+
default: crossSpawnMock,
103+
}));
104+
vi.doMock('node:child_process', async (importOriginal) => {
105+
const original = await importOriginal<typeof import('node:child_process')>();
106+
return {
107+
...original,
108+
execFileSync: vi.fn((cmd: string, args: string[]) => {
109+
if (cmd === 'where' && args[0] === 'claude') return 'C:\\Users\\me\\AppData\\Roaming\\npm\\claude.cmd\r\n';
110+
if (cmd === 'where' && args[0] === 'powershell.exe')
111+
return 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\r\n';
112+
return '';
113+
}),
114+
};
115+
});
116+
vi.doMock('node:fs', async (importOriginal) => {
117+
const original = await importOriginal<typeof import('node:fs')>();
118+
return {
119+
...original,
120+
existsSync: vi.fn((path: string) => path === 'C:\\Users\\me\\AppData\\Roaming\\npm\\claude.ps1'),
121+
};
122+
});
123+
124+
const { spawnCommand } = await import('../engines/shared.js');
125+
const options = { stdio: 'ignore' as const };
126+
spawnCommand('claude', ['--version'], options);
127+
128+
expect(crossSpawnMock).toHaveBeenCalledWith(
129+
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
130+
[
131+
'-NoLogo',
132+
'-NoProfile',
133+
'-NonInteractive',
134+
'-ExecutionPolicy',
135+
'Bypass',
136+
'-File',
137+
'C:\\Users\\me\\AppData\\Roaming\\npm\\claude.ps1',
138+
'--version',
139+
],
140+
options,
141+
);
142+
} finally {
143+
Object.defineProperty(process, 'platform', { value: originalPlatform });
144+
}
145+
});
95146
});

src/__tests__/windows-cli-launch.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,46 @@ describe('Windows CLI launch smoke', () => {
5454
expect(events.some((event) => event.type === 'done' && event.sessionId === 'win-shim')).toBe(true);
5555
expect(events.some((event) => event.type === 'error')).toBe(false);
5656
});
57+
58+
itWindows('prefers claude.ps1 over claude.cmd when both exist', async () => {
59+
const workspace = await mkdtemp(join(tmpdir(), 'golem-win-workspace-'));
60+
const fakeBin = await mkdtemp(join(tmpdir(), 'golem-win-bin-'));
61+
tempDirs.push(workspace, fakeBin);
62+
63+
await mkdir(join(workspace, 'skills', 'general'), { recursive: true });
64+
await writeFile(join(workspace, 'golem.yaml'), 'name: windows-smoke\nengine: claude-code\n', 'utf-8');
65+
await writeFile(
66+
join(workspace, 'skills', 'general', 'SKILL.md'),
67+
'---\nname: general\ndescription: General assistant\n---\n# General\n',
68+
'utf-8',
69+
);
70+
71+
await writeFile(
72+
join(fakeBin, 'claude.cmd'),
73+
['@echo off', 'echo plain text from cmd shim', ''].join('\r\n'),
74+
'utf-8',
75+
);
76+
await writeFile(
77+
join(fakeBin, 'claude.ps1'),
78+
[
79+
'Write-Output \'{"type":"assistant","message":{"content":[{"type":"text","text":"ps1 ok"}]}}\'',
80+
'Write-Output \'{"type":"result","is_error":false,"session_id":"win-ps1"}\'',
81+
'',
82+
].join('\r\n'),
83+
'utf-8',
84+
);
85+
86+
originalPath = process.env.PATH;
87+
process.env.PATH = [fakeBin, originalPath].filter(Boolean).join(delimiter);
88+
89+
const assistant = createAssistant({ dir: workspace, timeoutMs: 5_000 });
90+
const events: StreamEvent[] = [];
91+
for await (const event of assistant.chat('hello from windows')) {
92+
events.push(event);
93+
}
94+
95+
expect(events).toContainEqual({ type: 'text', content: 'ps1 ok' });
96+
expect(events.some((event) => event.type === 'done' && event.sessionId === 'win-ps1')).toBe(true);
97+
expect(events.some((event) => event.type === 'error')).toBe(false);
98+
});
5799
});

src/engines/shared.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ export function prependPathEntries(
4747

4848
export function spawnCommand(command: string, args: string[], options: SpawnOptions) {
4949
const resolved = resolveOnPath(command) || command;
50+
if (process.platform === 'win32' && /\.cmd$/i.test(resolved)) {
51+
const ps1Path = resolved.replace(/\.cmd$/i, '.ps1');
52+
const powershell = resolveOnPath('powershell.exe') || resolveOnPath('pwsh.exe');
53+
if (existsSync(ps1Path) && powershell) {
54+
return crossSpawn(
55+
powershell,
56+
['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ps1Path, ...args],
57+
options,
58+
);
59+
}
60+
}
5061
return crossSpawn(resolved, args, options);
5162
}
5263

0 commit comments

Comments
 (0)