From c321a6b475d40ab710c29d6bf5e71ffddba90553 Mon Sep 17 00:00:00 2001 From: Aalhad Date: Fri, 31 Jul 2026 15:33:31 +0530 Subject: [PATCH] fix(vscode): use shell mode for spawn on Windows to detect npm correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, child_process.spawn cannot find npm.cmd / npx.cmd without shell: true, causing the VS Code extension to falsely report 'npm not detected' and disable CLI features. probeCommand now hardcodes ['--version'] and adds shell: true only on win32. runRaw is intentionally left unchanged — it receives user-controlled args and must not use shell: true for security. Fixes #453 --- .../src/extension/services/CliService.ts | 11 ++-- .../services/__tests__/CliService.test.ts | 57 +++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/extensions/vscode/src/extension/services/CliService.ts b/extensions/vscode/src/extension/services/CliService.ts index 2f39f9ce..c04d6203 100644 --- a/extensions/vscode/src/extension/services/CliService.ts +++ b/extensions/vscode/src/extension/services/CliService.ts @@ -30,9 +30,10 @@ export class CliService { return env.ocr.ok; } - private probeCommand(bin: string, args: string[]): Promise<{ ok: boolean; version?: string }> { + private probeCommand(bin: string): Promise<{ ok: boolean; version?: string }> { return new Promise((resolve) => { - const proc = spawn(resolveBin(bin), args, { env: getShellEnv() }); + // shell: true is safe here because args are hardcoded ['--version'] — no user input. + const proc = spawn(resolveBin(bin), ['--version'], { env: getShellEnv(), shell: process.platform === 'win32' }); let stdout = ''; let errored = false; proc.stdout?.on('data', (d) => { stdout += d.toString(); }); @@ -53,10 +54,10 @@ export class CliService { const cached = this.getCachedEnvironment(); if (cached) return cached; } - const node = await this.probeCommand('node', ['--version']); - const npm = node.ok ? await this.probeCommand('npm', ['--version']) : { ok: false }; + const node = await this.probeCommand('node'); + const npm = node.ok ? await this.probeCommand('npm') : { ok: false }; const ocr = node.ok && npm.ok - ? await this.probeCommand(this.cliPath, ['--version']) + ? await this.probeCommand(this.cliPath) : { ok: false }; const env = { node, npm, ocr }; this.envCache = { env, at: Date.now() }; diff --git a/extensions/vscode/src/extension/services/__tests__/CliService.test.ts b/extensions/vscode/src/extension/services/__tests__/CliService.test.ts index 42ae24da..a72d302b 100644 --- a/extensions/vscode/src/extension/services/__tests__/CliService.test.ts +++ b/extensions/vscode/src/extension/services/__tests__/CliService.test.ts @@ -13,6 +13,63 @@ describe('CliService.isAvailable', () => { }); }); +describe('CliService probe shell option', () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + let spawnSpy: jest.SpyInstance; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + spawnSpy = jest.spyOn(require('child_process'), 'spawn'); + }); + + afterEach(() => { + spawnSpy.mockRestore(); + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform); + } + }); + + it('Windows 上 probeCommand 应传入 shell: true', async () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + const mockProc = { + stdout: { on: jest.fn() }, + on: jest.fn((event: string, cb: (code: number) => void) => { + if (event === 'close') cb(0); + }), + }; + spawnSpy.mockReturnValue(mockProc as any); + + const svc = new CliService('node'); + await (svc as any).probeCommand('npm'); + + expect(spawnSpy).toHaveBeenCalledWith( + 'npm', + ['--version'], + expect.objectContaining({ shell: true }), + ); + }); + + it('非 Windows 上 probeCommand 不应传入 shell', async () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + const mockProc = { + stdout: { on: jest.fn() }, + on: jest.fn((event: string, cb: (code: number) => void) => { + if (event === 'close') cb(0); + }), + }; + spawnSpy.mockReturnValue(mockProc as any); + + const svc = new CliService('node'); + await (svc as any).probeCommand('npm'); + + expect(spawnSpy).toHaveBeenCalledWith( + 'npm', + ['--version'], + expect.objectContaining({ shell: false }), + ); + }); +}); + describe('CliService.runRaw', () => { it('收集 stdout 并在结束时 resolve', async () => { // 用 node 打印一段 JSON 模拟 ocr