From 1f9e5559798a360b34cdb2f481995cee418ebd17 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Thu, 4 Jun 2026 12:24:36 +0800 Subject: [PATCH] fix(update): start automatic installs after fresh version checks --- .changeset/fresh-update-background-install.md | 5 + apps/kimi-code/src/cli/update/preflight.ts | 100 ++++++++++++++---- .../test/cli/update/preflight.test.ts | 49 ++++++++- 3 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 .changeset/fresh-update-background-install.md diff --git a/.changeset/fresh-update-background-install.md b/.changeset/fresh-update-background-install.md new file mode 100644 index 0000000000..4cf574cbc6 --- /dev/null +++ b/.changeset/fresh-update-background-install.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Start automatic background updates as soon as startup's fresh update check finds a newer version. diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts index 98148075ff..5ab3722232 100644 --- a/apps/kimi-code/src/cli/update/preflight.ts +++ b/apps/kimi-code/src/cli/update/preflight.ts @@ -165,6 +165,32 @@ function refreshInBackground(): void { void refreshUpdateCache().catch(() => {}); } +function refreshAndMaybeInstallInBackground( + currentVersion: string, + isInteractive: boolean, + installState: UpdateInstallState, + platform: NodeJS.Platform, + track: RunUpdatePreflightOptions['track'], + logger: UpdateLogger, +): void { + void (async () => { + const refreshed = await refreshUpdateCache(); + if (!isInteractive) return; + const target = selectUpdateTarget(currentVersion, refreshed.latest); + if (target === null) return; + const source = await detectInstallSource().catch(() => 'unsupported' as const); + await tryStartAutomaticBackgroundInstall( + installState, + currentVersion, + target, + source, + platform, + track, + logger, + ); + })().catch(() => {}); +} + function nowIso(): string { return new Date().toISOString(); } @@ -433,6 +459,35 @@ async function startBackgroundInstall( } } +async function tryStartAutomaticBackgroundInstall( + installState: UpdateInstallState, + currentVersion: string, + target: UpdateTarget, + source: InstallSource, + platform: NodeJS.Platform, + track: RunUpdatePreflightOptions['track'], + logger: UpdateLogger, +): Promise { + const sourceCanAutoInstall = canAutoInstall(source, platform); + const autoInstallUpdates = sourceCanAutoInstall ? await shouldAutoInstallUpdates() : false; + if (!autoInstallUpdates || !sourceCanAutoInstall) return false; + if (failureAttemptsFor(installState, target) >= AUTO_INSTALL_FAILURE_PROMPT_THRESHOLD) { + return false; + } + if (!hasFreshActiveInstall(installState, target)) { + await startBackgroundInstall( + installState, + currentVersion, + target, + source, + platform, + track, + logger, + ).catch(() => {}); + } + return true; +} + export function decideUpdateAction( target: UpdateTarget | null, isInteractive: boolean, @@ -469,33 +524,40 @@ export async function runUpdatePreflight( const cache = await readUpdateCache().catch(() => null); const latest = cache?.latest ?? null; const target = selectUpdateTarget(currentVersion, latest); + if (target === null) { + refreshAndMaybeInstallInBackground( + currentVersion, + isInteractive, + installState, + platform, + options.track, + logger, + ); + return 'continue'; + } + refreshInBackground(); const source: InstallSource = - target === null || !isInteractive + !isInteractive ? 'unsupported' : await detectInstallSource().catch(() => 'unsupported' as const); const decision = decideUpdateAction(target, isInteractive, source, platform); - if (decision === 'none' || target === null) return 'continue'; + if (decision === 'none') return 'continue'; const installCommand = installCommandFor(source, target.version, platform); - const sourceCanAutoInstall = canAutoInstall(source, platform); - const autoInstallUpdates = sourceCanAutoInstall ? await shouldAutoInstallUpdates() : false; - if (autoInstallUpdates && sourceCanAutoInstall) { - if (failureAttemptsFor(installState, target) < AUTO_INSTALL_FAILURE_PROMPT_THRESHOLD) { - if (!hasFreshActiveInstall(installState, target)) { - await startBackgroundInstall( - installState, - currentVersion, - target, - source, - platform, - options.track, - logger, - ).catch(() => {}); - } - return 'continue'; - } + if ( + await tryStartAutomaticBackgroundInstall( + installState, + currentVersion, + target, + source, + platform, + options.track, + logger, + ) + ) { + return 'continue'; } trackUpdatePrompted(options.track, currentVersion, target, source, decision); diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts index ad24577d4b..9665dec978 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -185,15 +185,42 @@ describe('runUpdatePreflight', () => { afterEach(() => { vi.clearAllMocks(); }); - it('continues on first launch with empty cache, still refreshes in background', async () => { + it('starts an automatic update from the first fresh check when the cache is empty', async () => { mocks.readUpdateCache.mockResolvedValue(emptyUpdateCache()); - mocks.refreshUpdateCache.mockResolvedValue(emptyUpdateCache()); + mocks.readUpdateInstallState.mockResolvedValue(installState()); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('npm-global'); + mockSpawnExit(0); const { options } = captureOutput(); await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + await flushBackgroundInstall(); + expect(readUpdateCache).toHaveBeenCalledTimes(1); expect(refreshUpdateCache).toHaveBeenCalledTimes(1); - expect(detectInstallSource).not.toHaveBeenCalled(); + expect(promptForInstallChoice).not.toHaveBeenCalled(); + expect(detectInstallSource).toHaveBeenCalledTimes(1); + expect(mocks.spawn).toHaveBeenCalledWith( + expect.stringMatching(/^npm(\.cmd)?$/), + ['install', '-g', '@moonshot-ai/kimi-code@0.5.0'], + { detached: true, stdio: 'ignore' }, + ); + }); + + it('does not start a fresh-check background install when automatic updates are disabled', async () => { + disableAutoInstall(); + mocks.readUpdateCache.mockResolvedValue(emptyUpdateCache()); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('npm-global'); + const { options } = captureOutput(); + + await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + await flushBackgroundInstall(); + + expect(refreshUpdateCache).toHaveBeenCalledTimes(1); + expect(detectInstallSource).toHaveBeenCalledTimes(1); + expect(promptForInstallChoice).not.toHaveBeenCalled(); + expect(mocks.spawn).not.toHaveBeenCalled(); }); it('skips when non-interactive', async () => { @@ -206,6 +233,22 @@ describe('runUpdatePreflight', () => { expect(detectInstallSource).not.toHaveBeenCalled(); }); + it('does not start a fresh-check background install when non-interactive', async () => { + mocks.readUpdateCache.mockResolvedValue(emptyUpdateCache()); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + const { options } = captureOutput(); + + await expect( + runUpdatePreflight('0.4.0', { ...options, isTTY: false }), + ).resolves.toBe('continue'); + await flushBackgroundInstall(); + + expect(refreshUpdateCache).toHaveBeenCalledTimes(1); + expect(detectInstallSource).not.toHaveBeenCalled(); + expect(promptForInstallChoice).not.toHaveBeenCalled(); + expect(mocks.spawn).not.toHaveBeenCalled(); + }); + it('npm-global: prompts and spawns npm install -g when automatic updates are disabled', async () => { disableAutoInstall(); mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0'));