diff --git a/.changeset/fix-windows-upgrade-spawn.md b/.changeset/fix-windows-upgrade-spawn.md new file mode 100644 index 0000000000..c4a8678b79 --- /dev/null +++ b/.changeset/fix-windows-upgrade-spawn.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix `kimi upgrade` failing on Windows with a spawn error when installing the new version. diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts index 1ad8aefa31..fe388882f4 100644 --- a/apps/kimi-code/src/cli/update/preflight.ts +++ b/apps/kimi-code/src/cli/update/preflight.ts @@ -488,7 +488,14 @@ export async function installUpdate( ): Promise { const { cmd, args } = spawnForSource(source, version, platform); await new Promise((resolve, reject) => { - const child = spawn(cmd, [...args], { stdio: 'inherit' }); + // Windows package managers (npm/pnpm/yarn) are .cmd shims. Since the + // CVE-2024-27980 fix, Node throws EINVAL when spawning a .cmd/.bat without + // a shell, so run through the shell on win32. The version is a validated + // semver and the package name is a constant, so args are shell-safe. + const child = spawn(cmd, [...args], { + stdio: 'inherit', + shell: platform === 'win32' ? true : undefined, + }); child.once('error', reject); child.once('exit', (code, signal) => { if (code === 0) { @@ -596,7 +603,11 @@ async function startBackgroundInstall( }); }; - const child = spawn(cmd, [...args], { detached: true, stdio: 'ignore' }); + const child = spawn(cmd, [...args], { + detached: true, + stdio: 'ignore', + shell: platform === 'win32' ? true : undefined, + }); child.once('error', () => { finish(false); }); child.once('exit', (code) => { finish(code === 0); }); child.unref(); diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts index 0a56905660..664bc49820 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -419,6 +419,28 @@ describe('runUpdatePreflight', () => { ); }); + it('pnpm-global on win32: spawns pnpm.cmd through a shell', async () => { + disableAutoInstall(); + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('pnpm-global'); + mocks.promptForInstallChoice.mockResolvedValue('install'); + mockSpawnExit(0); + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + try { + const { options } = captureOutput(); + await runUpdatePreflight('0.4.0', options); + expect(mocks.spawn).toHaveBeenCalledWith( + 'pnpm.cmd', + ['add', '-g', '@moonshot-ai/kimi-code@0.5.0'], + { stdio: 'inherit', shell: true }, + ); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); + it('yarn-global: spawns yarn global add', async () => { disableAutoInstall(); mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0'));