diff --git a/.changeset/refresh-update-prompt-target.md b/.changeset/refresh-update-prompt-target.md new file mode 100644 index 000000000..11cf842f2 --- /dev/null +++ b/.changeset/refresh-update-prompt-target.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Refresh the update target before showing foreground update prompts so the displayed version matches the install. diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts index 5ab372223..7b8d57976 100644 --- a/apps/kimi-code/src/cli/update/preflight.ts +++ b/apps/kimi-code/src/cli/update/preflight.ts @@ -42,6 +42,7 @@ export interface RunUpdatePreflightOptions { const AUTO_INSTALL_FAILURE_PROMPT_THRESHOLD = 2; const AUTO_INSTALL_ACTIVE_TTL_MS = 6 * 60 * 60 * 1000; +const USER_VISIBLE_UPDATE_REFRESH_TIMEOUT_MS = 1_000; type UpdateLogger = Pick; @@ -191,6 +192,30 @@ function refreshAndMaybeInstallInBackground( })().catch(() => {}); } +async function refreshUserVisibleUpdateTarget( + currentVersion: string, + fallbackTarget: UpdateTarget, +): Promise { + let timeout: ReturnType | undefined; + try { + const refresh = refreshUpdateCache() + .then((refreshed) => selectUpdateTarget(currentVersion, refreshed.latest)) + .catch(() => fallbackTarget); + const fallback = new Promise((resolve) => { + timeout = setTimeout(() => { + resolve(fallbackTarget); + }, USER_VISIBLE_UPDATE_REFRESH_TIMEOUT_MS); + }); + return await Promise.race([refresh, fallback]); + } catch { + return fallbackTarget; + } finally { + if (timeout !== undefined) { + clearTimeout(timeout); + } + } +} + function nowIso(): string { return new Date().toISOString(); } @@ -536,16 +561,17 @@ export async function runUpdatePreflight( return 'continue'; } - refreshInBackground(); const source: InstallSource = !isInteractive ? 'unsupported' : await detectInstallSource().catch(() => 'unsupported' as const); const decision = decideUpdateAction(target, isInteractive, source, platform); - if (decision === 'none') return 'continue'; + if (decision === 'none') { + refreshInBackground(); + return 'continue'; + } - const installCommand = installCommandFor(source, target.version, platform); if ( await tryStartAutomaticBackgroundInstall( installState, @@ -557,26 +583,49 @@ export async function runUpdatePreflight( logger, ) ) { + refreshInBackground(); return 'continue'; } - trackUpdatePrompted(options.track, currentVersion, target, source, decision); + const userVisibleTarget = await refreshUserVisibleUpdateTarget(currentVersion, target); + if (userVisibleTarget === null) return 'continue'; + if ( + await tryStartAutomaticBackgroundInstall( + installState, + currentVersion, + userVisibleTarget, + source, + platform, + options.track, + logger, + ) + ) { + return 'continue'; + } + + const installCommand = installCommandFor(source, userVisibleTarget.version, platform); + trackUpdatePrompted(options.track, currentVersion, userVisibleTarget, source, decision); if (decision === 'manual-command') { - stdout.write(renderManualUpdateMessage(currentVersion, target, source, installCommand)); + stdout.write(renderManualUpdateMessage( + currentVersion, + userVisibleTarget, + source, + installCommand, + )); return 'continue'; } - const choice = await promptInstall(currentVersion, target, source, installCommand); + const choice = await promptInstall(currentVersion, userVisibleTarget, source, installCommand); if (choice === 'skip') return 'continue'; try { - await installUpdate(source, target.version, platform); - stdout.write(renderInstallSuccessMessage(target)); + await installUpdate(source, userVisibleTarget.version, platform); + stdout.write(renderInstallSuccessMessage(userVisibleTarget)); return 'exit'; } catch (error) { stderr.write( - `warning: failed to install ${NPM_PACKAGE_NAME}@${target.version}: ` + + `warning: failed to install ${NPM_PACKAGE_NAME}@${userVisibleTarget.version}: ` + `${formatErrorMessage(error)}\n`, ); return 'continue'; diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts index 9665dec97..0e0d1f276 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -273,6 +273,57 @@ describe('runUpdatePreflight', () => { expect(stdout.join('')).toContain('Updated @moonshot-ai/kimi-code to 0.5.0'); }); + it('refreshes a stale cached target before showing the foreground install prompt', async () => { + disableAutoInstall(); + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.6.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.7.0')); + mocks.detectInstallSource.mockResolvedValue('npm-global'); + mocks.promptForInstallChoice.mockResolvedValue('install'); + mockSpawnExit(0); + const { stdout, options } = captureOutput(); + + await expect(runUpdatePreflight('0.5.0', options)).resolves.toBe('exit'); + + expect(refreshUpdateCache).toHaveBeenCalledTimes(1); + expect(mocks.promptForInstallChoice).toHaveBeenCalledWith( + expect.objectContaining({ + target: { version: '0.7.0' }, + installCommand: 'npm install -g @moonshot-ai/kimi-code@0.7.0', + }), + ); + expect(mocks.spawn).toHaveBeenCalledWith( + expect.stringMatching(/^npm(\.cmd)?$/), + ['install', '-g', '@moonshot-ai/kimi-code@0.7.0'], + { stdio: 'inherit' }, + ); + expect(stdout.join('')).toContain('Updated @moonshot-ai/kimi-code to 0.7.0'); + }); + + it('falls back to the cached foreground prompt target when the refresh hangs', async () => { + vi.useFakeTimers(); + try { + disableAutoInstall(); + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.6.0')); + mocks.refreshUpdateCache.mockReturnValue(new Promise(() => {})); + mocks.detectInstallSource.mockResolvedValue('npm-global'); + mocks.promptForInstallChoice.mockResolvedValue('skip'); + const { options } = captureOutput(); + + const result = runUpdatePreflight('0.5.0', options); + await vi.advanceTimersByTimeAsync(1_000); + + await expect(result).resolves.toBe('continue'); + expect(mocks.promptForInstallChoice).toHaveBeenCalledWith( + expect.objectContaining({ + target: { version: '0.6.0' }, + installCommand: 'npm install -g @moonshot-ai/kimi-code@0.6.0', + }), + ); + } finally { + vi.useRealTimers(); + } + }); + it('pnpm-global: spawns pnpm add -g', async () => { disableAutoInstall(); mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0'));