Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
98d2be7
fix(installer): tighten verifier base-url + clarify test helper
yiliang114 May 7, 2026
05e79e7
style(installer): align installer completion output
yiliang114 May 15, 2026
e6a1459
revert(installer): keep hosted installer output unchanged
yiliang114 May 15, 2026
3eb5c49
fix(installer): address release validation review feedback
yiliang114 May 17, 2026
20f5243
docs: switch public install commands to standalone hosted entrypoint
yiliang114 May 21, 2026
0bbb5e2
Merge remote-tracking branch 'origin/main' into codex/pr-3855-review-fix
yiliang114 May 22, 2026
523e03e
docs: clarify pull request size guidance
yiliang114 May 22, 2026
c6e4244
fix(installation): harden standalone release validation
yiliang114 May 22, 2026
67cae75
fix(installation): redact release verifier credentials
yiliang114 May 22, 2026
ad9cdd1
merge: sync with main branch
yiliang114 May 25, 2026
a6f4bda
feat(installer): add visual branding to Linux/macOS install script
yiliang114 May 25, 2026
5481da5
fix(test): update stale assertion after guide text was removed
yiliang114 May 25, 2026
178f090
feat(installer): use truecolor per-character gradient for logo branding
yiliang114 May 25, 2026
5ea2bdf
fix(installer): address critical review findings on SSRF, semver, and…
yiliang114 May 25, 2026
4a57ea3
fix(installer): close release validation review gaps
yiliang114 May 26, 2026
b268a40
test(installer): cover shadowed qwen installs
yiliang114 May 26, 2026
59680ea
fix(installer): avoid npm auto-update for standalone installs
yiliang114 May 27, 2026
74b3ea2
fix(installer): block IPv4-compatible IPv6 SSRF and harden archive va…
yiliang114 May 27, 2026
54d7397
fix(installer): finish standalone install follow-ups
yiliang114 May 27, 2026
aa3b79c
feat(installer): streamline output with custom progress bar and minim…
yiliang114 May 28, 2026
895b5e7
feat(installer): add progress bar and logo to Windows installer
yiliang114 May 28, 2026
c48174b
fix(installer): address review findings on progress bar
yiliang114 May 28, 2026
1f42ca3
fix(installer): finalize Windows UX — suppress curl progress, fix logo
yiliang114 May 28, 2026
6953e7a
fix(installer): handle Windows backslash paths in standalone detection
yiliang114 May 29, 2026
d2dd787
fix(installer): normalize expected paths in Windows standalone test
yiliang114 May 29, 2026
0c90351
refactor(installer): simplify post-install output
yiliang114 May 29, 2026
134ab59
refactor(installer): simplify Windows post-install output
yiliang114 May 29, 2026
07c2261
refactor(installer): suppress verbose Windows messages
yiliang114 May 29, 2026
ed8636d
fix(test): align install-script assertions with simplified output format
yiliang114 Jun 1, 2026
05191b2
fix(installer): align hardlink detection and expand test coverage
yiliang114 Jun 3, 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
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ We favor small, atomic PRs that address a single issue or add a single, self-con
- **Do:** Create a PR that fixes one specific bug or adds one specific feature.
- **Don't:** Bundle multiple unrelated changes (e.g., a bug fix, a new feature, and a refactor) into a single PR.

Large changes should be broken down into a series of smaller, logical PRs that can be reviewed and merged independently.
As a rule of thumb, start splitting a PR once it exceeds about 1,200 changed
lines. PRs above about 2,000 changed lines should either be split into a series
of smaller, logical PRs that can be reviewed and merged independently, or
explain in the PR description why the change needs to land together.

#### 3. Use Draft PRs for Work in Progress

Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,13 @@ Qwen Code is an open-source AI agent for the terminal, optimized for Qwen series
#### Linux / macOS

```bash
bash -c "$(curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh)"
curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash
```

#### Windows (Run as Administrator)
#### Windows

Works in both Command Prompt and PowerShell:

```cmd
powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"
```powershell
irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex
```

> **Note**: It's recommended to restart your terminal after installation to ensure environment variables take effect.
Expand Down
5 changes: 4 additions & 1 deletion docs/developers/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ We favor small, atomic PRs that address a single issue or add a single, self-con
- **Do:** Create a PR that fixes one specific bug or adds one specific feature.
- **Don't:** Bundle multiple unrelated changes (e.g., a bug fix, a new feature, and a refactor) into a single PR.

Large changes should be broken down into a series of smaller, logical PRs that can be reviewed and merged independently.
As a rule of thumb, start splitting a PR once it exceeds about 1,200 changed
lines. PRs above about 2,000 changed lines should either be split into a series
of smaller, logical PRs that can be reviewed and merged independently, or
explain in the PR description why the change needs to land together.

#### 3. Use Draft PRs for Work in Progress

Expand Down
8 changes: 4 additions & 4 deletions docs/users/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
### Install Qwen Code:

The recommended installer uses a standalone archive when one is available for
your platform. If it falls back to npm, Node.js 20 or later with npm must be
your platform. If it falls back to npm, Node.js 22 or later with npm must be
available on PATH.

**Linux / macOS**

```sh
curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh | bash
curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash
```

**Windows**

```cmd
powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"
```powershell
irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex
```

> [!note]
Expand Down
8 changes: 4 additions & 4 deletions docs/users/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ To install Qwen Code, use one of the following methods:
**Linux / macOS**

```sh
curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh | bash
curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash
```

**Windows (Run as Administrator)**
**Windows**

```cmd
powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"
```powershell
irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex
```

> [!note]
Expand Down
20 changes: 19 additions & 1 deletion docs/users/support/Uninstall.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Uninstall

Your uninstall method depends on how you ran the CLI. Follow the instructions for either npx or a global npm installation.
Your uninstall method depends on how you installed the CLI.

## Method 1: Using npx

Expand Down Expand Up @@ -40,3 +40,21 @@ npm uninstall -g @qwen-code/qwen-code
```

This command completely removes the package from your system.

## Method 3: Standalone Install

If you installed via the standalone installer (`curl ... | bash` or `irm ... | iex`), use the dedicated uninstall script.

**Linux / macOS**

```bash
curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/uninstall-qwen-standalone.sh | bash
```

**Windows**

```powershell
irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/uninstall-qwen-standalone.ps1 | iex
```

The uninstaller removes the standalone runtime, generated `qwen` wrapper, and installer-managed PATH changes. Your Qwen Code configuration (`~/.qwen`) is preserved by default.
5 changes: 1 addition & 4 deletions packages/cli/src/ui/commands/directoryCommand.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,7 @@ describe('getDirPathCompletions', () => {
fs.mkdirSync(path.join(tempTestDir, 'sub1', 'deep'), { recursive: true });
// Add some non-directory files (should be filtered out)
fs.writeFileSync(path.join(tempTestDir, 'file.txt'), '');
fs.writeFileSync(
path.join(tempTestDir, 'sub1', 'nested.txt'),
'',
);
fs.writeFileSync(path.join(tempTestDir, 'sub1', 'nested.txt'), '');
});

afterAll(() => {
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/ui/commands/directoryCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type { SlashCommand, CommandContext, CommandCompletionItem } from './types.js';
import type {
SlashCommand,
CommandContext,
CommandCompletionItem,
} from './types.js';
import { CommandKind } from './types.js';
import { MessageType } from '../types.js';
import * as fs from 'node:fs';
Expand Down Expand Up @@ -58,7 +62,9 @@ function findExistingWorkspaceDirectory(
* Returns directory path completions for the given partial argument.
* Supports comma-separated paths by completing only the last segment.
*/
export function getDirPathCompletions(partialArg: string): CommandCompletionItem[] {
export function getDirPathCompletions(
partialArg: string,
): CommandCompletionItem[] {
const lastComma = partialArg.lastIndexOf(',');
const prefix = lastComma >= 0 ? partialArg.substring(0, lastComma + 1) : '';
const partial =
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/ui/hooks/useCommandCompletion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,11 @@ describe('useCommandCompletion', () => {
it('should not append trailing space for directory completions', async () => {
setupMocks({
atSuggestions: [
{ label: 'src/components/', value: 'src/components/', isDirectory: true },
{
label: 'src/components/',
value: 'src/components/',
isDirectory: true,
},
],
});

Expand Down Expand Up @@ -652,9 +656,7 @@ describe('useCommandCompletion', () => {
result.current.handleAutocomplete(0);
});

expect(result.current.textBuffer.text).toBe(
'@src/components/ is a dir',
);
expect(result.current.textBuffer.text).toBe('@src/components/ is a dir');
});
});

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/ui/hooks/useCommandCompletion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,10 @@ export function useCommandCompletion(
const lineCodePoints = toCodePoints(buffer.lines[cursorRow] || '');
const charAfterCompletion = lineCodePoints[end];
const isDirectory = suggestions[indexToUse].isDirectory;
if (charAfterCompletion !== ' ' && !(isDirectory && !charAfterCompletion)) {
if (
charAfterCompletion !== ' ' &&
!(isDirectory && !charAfterCompletion)
) {
suggestionText += ' ';
}

Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/ui/hooks/useSlashCompletion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1125,10 +1125,12 @@ describe('useSlashCompletion', () => {

describe('isDirectory propagation', () => {
it('should propagate isDirectory from CommandCompletionItem to Suggestion', async () => {
const mockCompletionFn = vi.fn().mockResolvedValue([
{ value: '/tmp/workspace/', isDirectory: true },
{ value: '/tmp/file.txt' },
]);
const mockCompletionFn = vi
.fn()
.mockResolvedValue([
{ value: '/tmp/workspace/', isDirectory: true },
{ value: '/tmp/file.txt' },
]);

const slashCommands = [
createTestCommand({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,7 @@ export class DashScopeOpenAICompatibleProvider extends DefaultOpenAICompatiblePr
}

return (
isDashscopeOrigin ||
isTokenPlanOrigin ||
isInternalOrigin ||
isProxyMatch
isDashscopeOrigin || isTokenPlanOrigin || isInternalOrigin || isProxyMatch
);
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/create-standalone-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,4 +608,4 @@ function fail(message) {
throw new Error(`Error: ${message}`);
}

export { writeSha256Sums };
export { TARGETS, writeSha256Sums };
5 changes: 0 additions & 5 deletions scripts/installation/INSTALLATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ standalone release. The `standalone` suffix intentionally avoids overwriting the
existing production `install-qwen.sh` / `install-qwen.bat` OSS objects during
the staged rollout.

Public installation documentation intentionally continues to use the existing
production installer in this PR. Update README and other public quick-install
instructions in a follow-up after the standalone-suffixed hosted installers and
release archive sync have been validated in production.

Hosted installer assets are staged separately from GitHub Release archives:

- `install-qwen-standalone.sh` is the Linux/macOS hosted entrypoint.
Expand Down
15 changes: 9 additions & 6 deletions scripts/installation/install-qwen-standalone.bat
Original file line number Diff line number Diff line change
Expand Up @@ -439,11 +439,10 @@ exit /b 1

:ValidateVersion
if /i "!VERSION!"=="latest" exit /b 0
set "QWEN_VERSION_VALUE=!VERSION!"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$value = $env:QWEN_VERSION_VALUE; if ($value -match '^v?[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*$') { exit 0 }; exit 1"
set "PS_STATUS=%ERRORLEVEL%"
set "QWEN_VERSION_VALUE="
if %PS_STATUS% EQU 0 exit /b 0
echo(!VERSION!| findstr /R /C:"^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[A-Za-z0-9.-]*$" >nul
Comment thread
yiliang114 marked this conversation as resolved.
Outdated
if %ERRORLEVEL% EQU 0 exit /b 0
echo(!VERSION!| findstr /R /C:"^v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[A-Za-z0-9.-]*$" >nul
if %ERRORLEVEL% EQU 0 exit /b 0
echo ERROR: --version must be 'latest' or a semver string.
exit /b 1

Expand Down Expand Up @@ -1050,7 +1049,7 @@ REM with backslash separators even though the ZIP spec requires '/'. We
REM accept either separator and reject only entries that, after
REM normalization, are empty, absolute, drive-rooted, or contain a '..'
REM segment.
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $archive = $null; try { Add-Type -AssemblyName System.IO.Compression.FileSystem; $archive = [IO.Compression.ZipFile]::OpenRead($env:QWEN_ARCHIVE_FILE); foreach ($entry in $archive.Entries) { $raw = $entry.FullName; if ($raw.IndexOfAny([char[]](10,13)) -ge 0) { [Console]::Error.WriteLine('Archive contains unsafe path with control character: ' + $raw); exit 1 }; $name = $raw -replace '\\', '/'; while ($name.StartsWith('./')) { $name = $name.Substring(2) }; if ($name -eq '' -or $name.StartsWith('/') -or $name -match '^[A-Za-z]:' -or $name -match '(^|/)\.\.(/|$)') { [Console]::Error.WriteLine('Archive contains unsafe path: ' + $entry.FullName); exit 1 } } } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 } finally { if ($null -ne $archive) { $archive.Dispose() } }"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $archive = $null; try { Add-Type -AssemblyName System.IO.Compression.FileSystem; $archive = [IO.Compression.ZipFile]::OpenRead($env:QWEN_ARCHIVE_FILE); if ($archive.Entries.Count -eq 0) { [Console]::Error.WriteLine('Archive is empty: ' + $env:QWEN_ARCHIVE_FILE); exit 3 }; foreach ($entry in $archive.Entries) { $raw = $entry.FullName; if ($raw.IndexOfAny([char[]](10,13)) -ge 0) { [Console]::Error.WriteLine('Archive contains unsafe path with control character: ' + $raw); exit 1 }; $name = $raw -replace '\\', '/'; while ($name.StartsWith('./')) { $name = $name.Substring(2) }; if ($name -eq '' -or $name.StartsWith('/') -or $name -match '^[A-Za-z]:' -or $name -match '(^|/)\.\.(/|$)') { [Console]::Error.WriteLine('Archive contains unsafe path: ' + $entry.FullName); exit 1 } } } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 } finally { if ($null -ne $archive) { $archive.Dispose() } }"
set "PS_STATUS=%ERRORLEVEL%"
set "QWEN_ARCHIVE_FILE="
if %PS_STATUS% EQU 0 exit /b 0
Expand All @@ -1062,6 +1061,10 @@ if %PS_STATUS% EQU 2 (
echo ERROR: Archive could not be inspected before extraction.
exit /b 1
)
if %PS_STATUS% EQU 3 (
echo ERROR: Archive is empty: !ARCHIVE_FILE!
Comment thread
yiliang114 marked this conversation as resolved.
Outdated
exit /b 1
)
echo ERROR: Archive validation failed before extraction.
exit /b %PS_STATUS%

Expand Down
Loading
Loading