Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4206590
fix: align standalone-update RC markers with install script, add vers…
liziwl Jun 23, 2026
8b1eaaa
i18n: add update command translations for all 8 non-English locales
liziwl Jun 23, 2026
d6bc34d
Merge branch 'main' into main
yiliang114 Jun 24, 2026
10b0912
fix update command review feedback
liziwl Jun 24, 2026
81a0e07
Merge branch 'main' into main
liziwl Jun 24, 2026
10d3f36
address update command review followups
liziwl Jun 24, 2026
b4f7f5f
fix update command test args type
liziwl Jun 24, 2026
6a62bee
address update review hardlink and fallback feedback
liziwl Jun 24, 2026
af5d733
fix update tar filter typing and sdk bundle guard
liziwl Jun 24, 2026
3af0409
Merge branch 'main' into main
liziwl Jun 24, 2026
b1b6cfa
Merge branch 'main' into main
liziwl Jun 24, 2026
25903a1
fix(cli): address update review feedback
liziwl Jun 24, 2026
0188035
address update review followups
liziwl Jun 25, 2026
96e4e1b
restore windows archive traversal scan
liziwl Jun 25, 2026
6f4df40
address update review observations
liziwl Jun 25, 2026
edeac9a
address latest update review findings
liziwl Jun 26, 2026
2b22391
Merge branch 'main' into main
liziwl Jun 26, 2026
a52696e
fix latest update review regressions
liziwl Jun 26, 2026
ca219f4
fix(cli): localize update command output
liziwl Jun 27, 2026
1a5025d
fix(cli): localize update install guidance
liziwl Jun 27, 2026
3a8e419
fix(cli): harden update review follow-ups
liziwl Jun 27, 2026
7e4ef88
fix(cli): address latest update review comments
liziwl Jun 27, 2026
10bc0a1
fix(cli): honor explicit update requests
liziwl Jun 27, 2026
b510231
fix(cli): address update command review feedback
liziwl Jun 29, 2026
8518e46
fix(cli): address update slash command review feedback
liziwl Jun 29, 2026
a6f1d59
Merge branch 'main' into main
wenshao Jul 3, 2026
78e0c03
Merge branch 'main' into main
liziwl Jul 4, 2026
cbb1ea2
fix(cli): address latest update review feedback
liziwl Jul 4, 2026
d0fae3e
fix(cli): address update review follow-ups
liziwl Jul 4, 2026
3efeacb
add code
liziwl Jul 4, 2026
a6e4c31
fix(cli): add .deferred marker to prevent Windows deferred update race
liziwl Jul 4, 2026
29fcb2d
fix(cli): harden update review edge cases
liziwl Jul 5, 2026
8025c64
fix(cli): address PR #5780 review feedback on update engine
liziwl Jul 5, 2026
8b3c992
merge: resolve conflicts between update feature and main branch changes
qwen-code-dev-bot Jul 9, 2026
7530c3d
Merge branch 'main' into main
yiliang114 Jul 9, 2026
c6816ec
test(cli): fix update check test import
yiliang114 Jul 9, 2026
30a6b70
fix(cli): avoid duplicate startup update checks
yiliang114 Jul 9, 2026
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
1 change: 1 addition & 0 deletions packages/cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ describe('bootstrap import boundaries', () => {
['reviewCommand', 'review'],
['serveCommand', 'serve'],
['sessionsCommand', 'sessions'],
['updateCommand', 'update'],
]);
const registeredIdentifiers = [
...configSource.matchAll(/\.command\((\w+Command)\)/g),
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const TOP_LEVEL_COMMANDS = [
'Run Qwen Code as a local HTTP daemon (Stage 1 experimental: --http-bridge)',
],
['sessions <command>', 'Manage Qwen Code sessions'],
['update', 'Check for Qwen Code updates and install if available'],
] as const;

export const MCP_COMMANDS = [
Expand Down
258 changes: 258 additions & 0 deletions packages/cli/src/commands/update.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { ArgumentsCamelCase } from 'yargs';

const loadSettings = vi.fn();
const checkForUpdatesDetailed = vi.fn();
const getInstallationInfo = vi.fn();
const resolveUpdateCommand = vi.fn(
(updateCommand: string, latestVersion: string) =>
updateCommand.replace('@latest', `@${latestVersion}`),
);
const formatUpdateInstructions = vi.fn(
(
installationInfo: {
updateMessage?: string;
updateCommand?: string;
isStandalone?: boolean;
},
latestVersion: string,
) => {
if (installationInfo.updateMessage && !installationInfo.updateCommand) {
return [installationInfo.updateMessage];
}
if (installationInfo.updateCommand) {
return [
'Run the following to update:',
` ${resolveUpdateCommand(installationInfo.updateCommand, latestVersion)}`,
];
}
return ['Manual update required. Please reinstall Qwen Code.'];
},
);
const performStandaloneUpdate = vi.fn();
const getPackageJson = vi.fn();
const writeStdoutLine = vi.fn();
const writeStderrLine = vi.fn();
const initializeI18n = vi.fn();
const resolveLanguageSetting = vi.fn((language?: string) => language || 'auto');

vi.mock('../config/settings.js', () => ({ loadSettings }));
vi.mock('../ui/utils/updateCheck.js', () => ({ checkForUpdatesDetailed }));
vi.mock('../utils/installationInfo.js', () => ({
formatUpdateInstructions,
getInstallationInfo,
resolveUpdateCommand,
}));
vi.mock('../utils/standalone-update.js', () => ({ performStandaloneUpdate }));
vi.mock('../utils/package.js', () => ({ getPackageJson }));
vi.mock('../utils/stdioHelpers.js', () => ({
writeStdoutLine,
writeStderrLine,
}));
vi.mock('../i18n/index.js', () => ({
initializeI18n,
resolveLanguageSetting,
t: (key: string, params?: Record<string, string>) =>
key.replace(
/\{\{(\w+)\}\}/g,
(_, param: string) => params?.[param] ?? `{{${param}}}`,
),
}));

const { updateCommand } = await import('./update.js');

const updateArgs: ArgumentsCamelCase<object> = {
_: [],
$0: 'qwen',
};

function settings(enableAutoUpdate?: boolean) {
return {
merged: {
general: { enableAutoUpdate, language: 'zh' },
},
};
}

describe('update command', () => {
beforeEach(() => {
vi.clearAllMocks();
process.exitCode = undefined;
loadSettings.mockReturnValue(settings(undefined));
checkForUpdatesDetailed.mockResolvedValue({
status: 'update',
info: {
message: 'Update available: 1.2.3',
update: { latest: '1.2.3' },
},
});
getInstallationInfo.mockReturnValue({
isStandalone: false,
updateCommand: 'npm install -g @qwen-code/qwen-code@latest',
});
});

it('prints the package-manager update command even when auto-update is disabled', async () => {
loadSettings.mockReturnValue(settings(false));

await updateCommand.handler(updateArgs);

expect(resolveLanguageSetting).toHaveBeenCalledWith('zh');
expect(initializeI18n).toHaveBeenCalledWith('zh');
expect(getInstallationInfo).toHaveBeenCalledWith(expect.any(String), true);
expect(writeStdoutLine).toHaveBeenCalledWith('Update available: 1.2.3');
expect(writeStdoutLine).toHaveBeenCalledWith(
'Run the following to update:',
);
expect(writeStdoutLine).toHaveBeenCalledWith(
' npm install -g @qwen-code/qwen-code@1.2.3',
);
});

Comment thread
liziwl marked this conversation as resolved.
it('sets a non-zero exit code when a standalone update fails', async () => {
getInstallationInfo.mockReturnValue({
isStandalone: true,
standaloneDir: '/tmp/qwen-code',
});
performStandaloneUpdate.mockRejectedValue(new Error('boom'));

await updateCommand.handler(updateArgs);

expect(performStandaloneUpdate).toHaveBeenCalledWith(
'/tmp/qwen-code',
'1.2.3',
);
expect(writeStdoutLine).toHaveBeenCalledWith('Downloading update...');
expect(writeStderrLine).toHaveBeenCalledWith('Update failed: boom');
expect(process.exitCode).toBe(1);
});

it('updates standalone installs even when auto-update is disabled', async () => {
loadSettings.mockReturnValue(settings(false));
getInstallationInfo.mockReturnValue({
isStandalone: true,
standaloneDir: '/tmp/qwen-code',
});
performStandaloneUpdate.mockResolvedValue('done');

await updateCommand.handler(updateArgs);

expect(getInstallationInfo).toHaveBeenCalledWith(expect.any(String), true);
expect(performStandaloneUpdate).toHaveBeenCalledWith(
'/tmp/qwen-code',
'1.2.3',
);
expect(writeStdoutLine).toHaveBeenCalledWith(
'Update successful! The new version will be used on your next run.',
);
});

it('does not print generic fallback when installation info has updateMessage', async () => {
getInstallationInfo.mockReturnValue({
isStandalone: false,
updateMessage: 'Running via npx, update not applicable.',
});

await updateCommand.handler(updateArgs);

expect(writeStdoutLine).toHaveBeenCalledWith(
'Running via npx, update not applicable.',
);
expect(writeStdoutLine).not.toHaveBeenCalledWith(
'Manual update required. Please reinstall Qwen Code.',
);
});

it('prints the update message when no update command is available', async () => {
getInstallationInfo.mockReturnValue({
updateMessage:
'Running from a local git clone. Please update with "git pull".',
});

await updateCommand.handler(updateArgs);

expect(writeStdoutLine).toHaveBeenCalledWith(
'Running from a local git clone. Please update with "git pull".',
);
});

it('prints success message on standalone update', async () => {
getInstallationInfo.mockReturnValue({
isStandalone: true,
standaloneDir: '/tmp/qwen-code',
});
performStandaloneUpdate.mockResolvedValue('done');

await updateCommand.handler(updateArgs);

expect(writeStdoutLine).toHaveBeenCalledWith('Downloading update...');
expect(writeStdoutLine).toHaveBeenCalledWith(
'Update successful! The new version will be used on your next run.',
);
});

it('prints deferred message on standalone update', async () => {
getInstallationInfo.mockReturnValue({
isStandalone: true,
standaloneDir: '/tmp/qwen-code',
});
performStandaloneUpdate.mockResolvedValue('deferred');

await updateCommand.handler(updateArgs);

expect(writeStdoutLine).toHaveBeenCalledWith('Downloading update...');
expect(writeStdoutLine).toHaveBeenCalledWith(
'Update downloaded. It will be applied after you exit this session.',
);
});

it('prints the current version when no update is available', async () => {
checkForUpdatesDetailed.mockResolvedValue({
status: 'up-to-date',
currentVersion: '1.0.0',
});

await updateCommand.handler(updateArgs);

expect(writeStdoutLine).toHaveBeenCalledWith(
'Qwen Code 1.0.0 is up to date!',
);
expect(getInstallationInfo).not.toHaveBeenCalled();
});

it('sets a non-zero exit code when the update check fails', async () => {
checkForUpdatesDetailed.mockResolvedValue({
status: 'error',
error: new Error('registry unavailable'),
});

await updateCommand.handler(updateArgs);

expect(writeStderrLine).toHaveBeenCalledWith(
'Failed to check for updates. Please check your network or registry configuration.',
);
expect(process.exitCode).toBe(1);
expect(getInstallationInfo).not.toHaveBeenCalled();
});

it('sets a non-zero exit code when the update check is skipped', async () => {
checkForUpdatesDetailed.mockResolvedValue({
status: 'skipped',
reason: 'development mode',
});

await updateCommand.handler(updateArgs);

expect(writeStderrLine).toHaveBeenCalledWith(
'Unable to check for updates: development mode',
);
expect(process.exitCode).toBe(1);
expect(getInstallationInfo).not.toHaveBeenCalled();
});
});
123 changes: 123 additions & 0 deletions packages/cli/src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
Comment thread
liziwl marked this conversation as resolved.
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type { CommandModule } from 'yargs';
import { initializeI18n, resolveLanguageSetting, t } from '../i18n/index.js';

export const updateCommand: CommandModule = {
command: 'update',
get describe() {
return t('Check for Qwen Code updates and install if available');
},
handler: async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The qwen update CLI handler calls performStandaloneUpdate(...) directly but never imports updateEventEmitter or registers listeners on it.

performStandaloneUpdate emits progress/warning events on the shared updateEventEmitter — in particular surfaceBinWrapperWarnings() (standalone-update.ts:839) emits 'update-info' events for wrapper-install warnings. The interactive UI (startInteractiveUI.tsx) and the slash command (handleAutoUpdate.ts) both wire up listeners, so they surface these warnings. The CLI command does not — wrapper warnings emitted during a qwen update invocation are silently dropped.

Either register a lightweight listener here to pipe 'update-info' events through writeStdoutLine, or have performStandaloneUpdate return warnings alongside the result so the caller can choose how to display them. The latter avoids coupling the CLI command to the shared emitter.

const [
{ loadSettings },
{ checkForUpdatesDetailed },
installationInfoModule,
standaloneUpdate,
stdioHelpers,
{ updateEventEmitter },
] = await Promise.all([
import('../config/settings.js'),
import('../ui/utils/updateCheck.js'),
import('../utils/installationInfo.js'),
import('../utils/standalone-update.js'),
import('../utils/stdioHelpers.js'),
import('../utils/updateEventEmitter.js'),
]);

const { formatUpdateInstructions, getInstallationInfo } =
installationInfoModule;
const { performStandaloneUpdate } = standaloneUpdate;
const { writeStdoutLine, writeStderrLine } = stdioHelpers;

const cwd = process.cwd();
const settings = loadSettings(cwd, false);
Comment thread
liziwl marked this conversation as resolved.
await initializeI18n(
resolveLanguageSetting(settings.merged.general?.language as string),
);

const updateCheck = await checkForUpdatesDetailed();

Comment thread
liziwl marked this conversation as resolved.
if (updateCheck.status === 'up-to-date') {
writeStdoutLine(
t('Qwen Code {{version}} is up to date!', {
version: updateCheck.currentVersion,
}),
);
return;
}

if (updateCheck.status === 'error') {
writeStderrLine(
t(
'Failed to check for updates. Please check your network or registry configuration.',
),
);
process.exitCode = 1;
return;
}

if (updateCheck.status === 'skipped') {
writeStderrLine(
t('Unable to check for updates: {{reason}}', {
Comment thread
liziwl marked this conversation as resolved.
reason: updateCheck.reason,
}),
);
process.exitCode = 1;
return;
}

const info = updateCheck.info;
writeStdoutLine(info.message);

const installationInfo = getInstallationInfo(cwd, true);

if (installationInfo.isStandalone && installationInfo.standaloneDir) {
const handleUpdateInfo = (data: { message: string }) => {
writeStdoutLine(data.message);
};
updateEventEmitter.on('update-info', handleUpdateInfo);
try {
writeStdoutLine(t('Downloading update...'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Duplicate "Downloading update..." output. The handleUpdateInfo listener registered at line 83 writes every update-info event to stdout. This direct write at line 85 prints t('Downloading update...') before calling performStandaloneUpdate, which internally emits the same message via updateEventEmitter.emit('update-info', { message: t('Downloading update...') }) (standalone-update.ts:1006). The listener catches it and prints it again — the user sees the message twice.

Suggested change
writeStdoutLine(t('Downloading update...'));
updateEventEmitter.on('update-info', handleUpdateInfo);
try {
const result = await performStandaloneUpdate(

(Remove line 85 — the message is already emitted by performStandaloneUpdate and caught by the listener.)

— qwen3.7-max via Qwen Code /review

const result = await performStandaloneUpdate(
installationInfo.standaloneDir,
info.update.latest,
);
if (result === 'done') {
writeStdoutLine(
t(
'Update successful! The new version will be used on your next run.',
),
);
} else {
writeStdoutLine(
t(
'Update downloaded. It will be applied after you exit this session.',
),
);
}
} catch (err) {
Comment thread
liziwl marked this conversation as resolved.
writeStderrLine(
t('Update failed: {{error}}', {
error: err instanceof Error ? err.message : String(err),
}),
);
process.exitCode = 1;
} finally {
updateEventEmitter.off('update-info', handleUpdateInfo);
}
return;
}

for (const line of formatUpdateInstructions(
installationInfo,
info.update.latest,
)) {
writeStdoutLine(t(line));
}
},
};
Loading
Loading