Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions src/__tests__/providers/cliProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ describe('CliProvider', () => {
});

describe('arguments', () => {
it('passes correct args to spawn', async () => {
it('passes correct args to spawn (non-Windows)', async () => {
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });

const provider = new CliProvider('/my/cwd');
const token = createMockCancellationToken();

Expand All @@ -88,6 +90,35 @@ describe('CliProvider', () => {
stdio: ['pipe', 'pipe', 'pipe'],
}
);

Object.defineProperty(process, 'platform', { value: process.platform, configurable: true });
});

it('routes through cmd /c on Windows so the npm .cmd shim is resolved', async () => {
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });

const provider = new CliProvider('/my/cwd');
const token = createMockCancellationToken();

const promise = provider.generateMessage('my instruction', 'ctx', token);
mockProcess.emitClose(0);
await promise;

expect(mockSpawn).toHaveBeenCalledWith(
'cmd',
[
'/c', 'claude',
'-p', 'my instruction',
'--model', 'sonnet',
'--system-prompt', expect.any(String),
],
{
cwd: '/my/cwd',
stdio: ['pipe', 'pipe', 'pipe'],
}
);

Object.defineProperty(process, 'platform', { value: process.platform, configurable: true });
});

it('default model is sonnet when no options', async () => {
Expand All @@ -99,7 +130,7 @@ describe('CliProvider', () => {
await promise;

expect(mockSpawn).toHaveBeenCalledWith(
'claude',
expect.any(String),
expect.arrayContaining(['--model', 'sonnet']),
expect.any(Object)
);
Expand All @@ -114,12 +145,8 @@ describe('CliProvider', () => {
await promise;

expect(mockSpawn).toHaveBeenCalledWith(
'claude',
[
'-p', 'inst',
'--model', 'opus',
'--system-prompt', expect.any(String),
],
expect.any(String),
expect.arrayContaining(['-p', 'inst', '--model', 'opus', '--system-prompt', expect.any(String)]),
expect.any(Object)
);
});
Expand All @@ -137,7 +164,7 @@ describe('CliProvider', () => {
await promise;

expect(mockSpawn).toHaveBeenCalledWith(
'claude',
expect.any(String),
expect.arrayContaining(['--model', 'sonnet']),
expect.any(Object)
);
Expand Down
19 changes: 13 additions & 6 deletions src/providers/cliProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ export class CliProvider implements CommitMessageProvider {
return;
}

// On Windows, npm wraps the claude binary as claude.cmd which
// Node's spawn cannot execute without a shell. Route through
// cmd /c so Windows resolves the .cmd shim correctly while
// still passing each argument as a discrete array element
// (avoiding the quoting/newline pitfalls of shell:true).
const isWindows = process.platform === 'win32';
const command = isWindows ? 'cmd' : 'claude';
const args = isWindows
? ['/c', 'claude', '-p', instruction, '--model', model, '--system-prompt', buildSystemPrompt()]
: ['-p', instruction, '--model', model, '--system-prompt', buildSystemPrompt()];

const child: ChildProcess = spawn(
'claude',
[
'-p', instruction,
'--model', model,
'--system-prompt', buildSystemPrompt(),
],
command,
args,
{
cwd: this.cwd,
stdio: ['pipe', 'pipe', 'pipe'],
Expand Down