Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/refresh-update-prompt-target.md
Original file line number Diff line number Diff line change
@@ -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.
67 changes: 58 additions & 9 deletions apps/kimi-code/src/cli/update/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Logger, 'info' | 'warn'>;

Expand Down Expand Up @@ -191,6 +192,30 @@ function refreshAndMaybeInstallInBackground(
})().catch(() => {});
}

async function refreshUserVisibleUpdateTarget(
currentVersion: string,
fallbackTarget: UpdateTarget,
): Promise<UpdateTarget | null> {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
const refresh = refreshUpdateCache()
.then((refreshed) => selectUpdateTarget(currentVersion, refreshed.latest))
.catch(() => fallbackTarget);
Comment thread
liruifengv marked this conversation as resolved.
const fallback = new Promise<UpdateTarget>((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();
}
Expand Down Expand Up @@ -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,
Expand All @@ -556,27 +582,50 @@ export async function runUpdatePreflight(
options.track,
logger,
)
) {
refreshInBackground();
return 'continue';
}

const userVisibleTarget = await refreshUserVisibleUpdateTarget(currentVersion, target);
Comment thread
liruifengv marked this conversation as resolved.
if (userVisibleTarget === null) return 'continue';
if (
await tryStartAutomaticBackgroundInstall(
installState,
currentVersion,
userVisibleTarget,
source,
platform,
options.track,
logger,
)
) {
return 'continue';
}

trackUpdatePrompted(options.track, currentVersion, target, source, decision);
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';
Expand Down
51 changes: 51 additions & 0 deletions apps/kimi-code/test/cli/update/preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
Loading