Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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
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: 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
4 changes: 3 additions & 1 deletion scripts/installation/install-qwen-with-source.bat
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ exit /b 1

:ValidateVersion
if /i "!VERSION!"=="latest" 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
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
26 changes: 26 additions & 0 deletions scripts/installation/install-qwen-with-source.sh
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,22 @@ validate_archive_entry_path() {
esac
}

archive_contains_symlinks() {
local archive_path="$1"

case "${archive_path}" in
*.zip)
unzip -Z -v "${archive_path}" 2>/dev/null | grep -E 'Unix file attributes \(12[0-7]{4} octal\)' >/dev/null
;;
*.tar.gz|*.tgz|*.tar.xz)
tar -tvf "${archive_path}" 2>/dev/null | awk '$1 ~ /^l/ { found=1 } END { exit found ? 0 : 1 }'
Comment thread
yiliang114 marked this conversation as resolved.
Outdated
;;
*)
return 1
;;
esac
}

validate_archive_contents() {
local archive_path="$1"
local entries
Expand Down Expand Up @@ -652,6 +668,16 @@ validate_archive_contents() {
;;
esac

if [[ -z "${entries}" ]]; then
log_error "Archive is empty: ${archive_path}"
return 1
fi

if archive_contains_symlinks "${archive_path}"; then
log_error "Archive contains symlinks; refusing to install."
return 1
fi

while IFS= read -r entry; do
validate_archive_entry_path "${entry}" || return 1
done <<< "${entries}"
Comment thread
yiliang114 marked this conversation as resolved.
Expand Down
157 changes: 156 additions & 1 deletion scripts/tests/install-script.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@
expect(script).toContain('validate_https_url "${NPM_REGISTRY}"');
expect(script).toContain('qwen-code/node/bin/node');
expect(script).toContain('Archive contains symlinks; refusing to install');
expect(script).toContain('Archive is empty');

Check failure on line 130 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

scripts/tests/install-script.test.js > installation scripts > supports code-server-style standalone install on Linux/macOS

AssertionError: expected '#!/usr/bin/env bash\n\n# Qwen Code In…' to contain 'Archive is empty' - Expected + Received - Archive is empty + #!/usr/bin/env bash + + # Qwen Code Installation Script + # Installs Qwen Code from a standalone archive when available, with npm fallback. + # This script intentionally does not install Node.js or change npm config. + # + # Usage: + # install-qwen-standalone.sh --source [github|npm|internal|local-build] + # install-qwen-standalone.sh --method [detect|standalone|npm] + + if [ -z "${BASH_VERSION}" ] && [ -z "${__QWEN_INSTALL_REEXEC:-}" ]; then + if command -v bash >/dev/null 2>&1; then + if [ -f "${0}" ]; then + export __QWEN_INSTALL_REEXEC=1 + exec bash -- "${0}" "$@" + fi + + echo "Error: This script requires bash. Run the installer with: curl ... | bash" + exit 1 + fi + + echo "Error: This script requires bash. Please install bash first." + exit 1 + fi + + set -eo pipefail + + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' + + log_info() { + printf '%bINFO:%b %s\n' "${BLUE}" "${NC}" "$1" + } + + log_success() { + printf '%bSUCCESS:%b %s\n' "${GREEN}" "${NC}" "$1" + } + + log_warning() { + printf '%bWARNING:%b %s\n' "${YELLOW}" "${NC}" "$1" + } + + log_error() { + printf '%bERROR:%b %s\n' "${RED}" "${NC}" "$1" >&2 + } + + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + TEMP_DIRS=() + + cleanup_temp_dirs() { + local temp_dir + for temp_dir in "${TEMP_DIRS[@]}"; do + if [[ -n "${temp_dir}" ]]; then + rm -rf "${temp_dir}" + fi + done + } + + register_temp_dir() { + local temp_dir="$1" + TEMP_DIRS+=("${temp_dir}") + } + + shell_quote() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" + } + + display_install_version() { + if [[ "${VERSION}" == "latest" ]]; then + echo "latest" + return 0 + fi + + echo "${VERSION#v}" + } + + trap cleanup_temp_dirs EXIT + trap 'cleanup_temp_dirs; exit 130' INT + trap 'cleanup_temp_dirs; exit 143' TERM + + print_usage() { + cat <<EOF + Qwen Code Installer + + Usage: $0 [OPTIONS] + + Options: + -s, --source SOURCE Record the installation source. + --method METHOD Install method: detect, standalone, or npm. + Defaults to QWEN_INSTALL_METHOD or detect. + --mirror MIRROR Standalone archive mirror: auto, github, or aliyun. + Defaults to QWEN_INSTALL_MIRROR or auto, which picks + whichever responds first via a HEAD probe. + --base-url URL Override standalone archive base URL. + --archive PATH Install from a local standalone archive. + --version VERSION Standalone release version. Defaults to latest. + --registry REGISTRY npm registry to use for npm fallback. + Defaults to QWEN_NPM_REGISTRY or https://registry.npmmirror.com + --no-modify-path Do not append PATH to the user's shell rc file even + when a shadowing 'qwen' is detected. + -h, --help Show this help message. + + Examples: + curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash + curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash -s -- --source github + curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash -s -- --method standalone + ./install-qwen-standalone.sh --archive ./qwen-code-linux-x64.tar.gz + EOF + } + + SOURCE="unknown" + METHOD="${QWEN_INSTALL_METHOD:-}" + MIRROR="${QWEN_INSTALL_MIRROR:-auto}" + BASE_URL="${QWEN_INSTALL_BASE_URL:-}" + ARCHIVE_PATH="${QWEN_INSTALL_ARCHIVE:-}" + VERSION="${QWEN_INSTALL_VERSION:-latest}" + NO_MODIFY_PATH="${QWEN_NO_MODIFY_PATH:-0}" + NPM_REGISTRY="${QWEN_NPM_REGISTRY:-https://registry.npmmirror.com}" + I

Check failure on line 130 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (macos-latest, Node 22.x)

scripts/tests/install-script.test.js > installation scripts > supports code-server-style standalone install on Linux/macOS

AssertionError: expected '#!/usr/bin/env bash\n\n# Qwen Code In…' to contain 'Archive is empty' - Expected + Received - Archive is empty + #!/usr/bin/env bash + + # Qwen Code Installation Script + # Installs Qwen Code from a standalone archive when available, with npm fallback. + # This script intentionally does not install Node.js or change npm config. + # + # Usage: + # install-qwen-standalone.sh --source [github|npm|internal|local-build] + # install-qwen-standalone.sh --method [detect|standalone|npm] + + if [ -z "${BASH_VERSION}" ] && [ -z "${__QWEN_INSTALL_REEXEC:-}" ]; then + if command -v bash >/dev/null 2>&1; then + if [ -f "${0}" ]; then + export __QWEN_INSTALL_REEXEC=1 + exec bash -- "${0}" "$@" + fi + + echo "Error: This script requires bash. Run the installer with: curl ... | bash" + exit 1 + fi + + echo "Error: This script requires bash. Please install bash first." + exit 1 + fi + + set -eo pipefail + + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' + + log_info() { + printf '%bINFO:%b %s\n' "${BLUE}" "${NC}" "$1" + } + + log_success() { + printf '%bSUCCESS:%b %s\n' "${GREEN}" "${NC}" "$1" + } + + log_warning() { + printf '%bWARNING:%b %s\n' "${YELLOW}" "${NC}" "$1" + } + + log_error() { + printf '%bERROR:%b %s\n' "${RED}" "${NC}" "$1" >&2 + } + + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + TEMP_DIRS=() + + cleanup_temp_dirs() { + local temp_dir + for temp_dir in "${TEMP_DIRS[@]}"; do + if [[ -n "${temp_dir}" ]]; then + rm -rf "${temp_dir}" + fi + done + } + + register_temp_dir() { + local temp_dir="$1" + TEMP_DIRS+=("${temp_dir}") + } + + shell_quote() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" + } + + display_install_version() { + if [[ "${VERSION}" == "latest" ]]; then + echo "latest" + return 0 + fi + + echo "${VERSION#v}" + } + + trap cleanup_temp_dirs EXIT + trap 'cleanup_temp_dirs; exit 130' INT + trap 'cleanup_temp_dirs; exit 143' TERM + + print_usage() { + cat <<EOF + Qwen Code Installer + + Usage: $0 [OPTIONS] + + Options: + -s, --source SOURCE Record the installation source. + --method METHOD Install method: detect, standalone, or npm. + Defaults to QWEN_INSTALL_METHOD or detect. + --mirror MIRROR Standalone archive mirror: auto, github, or aliyun. + Defaults to QWEN_INSTALL_MIRROR or auto, which picks + whichever responds first via a HEAD probe. + --base-url URL Override standalone archive base URL. + --archive PATH Install from a local standalone archive. + --version VERSION Standalone release version. Defaults to latest. + --registry REGISTRY npm registry to use for npm fallback. + Defaults to QWEN_NPM_REGISTRY or https://registry.npmmirror.com + --no-modify-path Do not append PATH to the user's shell rc file even + when a shadowing 'qwen' is detected. + -h, --help Show this help message. + + Examples: + curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash + curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash -s -- --source github + curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash -s -- --method standalone + ./install-qwen-standalone.sh --archive ./qwen-code-linux-x64.tar.gz + EOF + } + + SOURCE="unknown" + METHOD="${QWEN_INSTALL_METHOD:-}" + MIRROR="${QWEN_INSTALL_MIRROR:-auto}" + BASE_URL="${QWEN_INSTALL_BASE_URL:-}" + ARCHIVE_PATH="${QWEN_INSTALL_ARCHIVE:-}" + VERSION="${QWEN_INSTALL_VERSION:-latest}" + NO_MODIFY_PATH="${QWEN_NO_MODIFY_PATH:-0}" + NPM_REGISTRY="${QWEN_NPM_REGISTRY:-https://registry.npmmirror.com}" + I
expect(script).toContain('archive_contains_symlinks()');
expect(script).toContain('not a Qwen Code standalone install');
expect(script).toContain(
'Return 2 only when a standalone archive is unavailable',
Expand Down Expand Up @@ -288,6 +290,13 @@
expect(script).toContain('if "!INSTALL_DIR:~1,2!"==":/"');
expect(script).toContain('if "!INSTALL_BIN_DIR:~1,2!"==":/"');
expect(script).toContain(':ValidateVersion');
expect(script).toContain(

Check failure on line 293 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

scripts/tests/install-script.test.js > installation scripts > supports code-server-style standalone install on Windows

AssertionError: expected '@echo off\r\nREM Qwen Code Installati…' to contain 'findstr /R /C:"^[0-9][0-9]*\.[0-9][0-…' - Expected + Received - findstr /R /C:"^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[A-Za-z0-9.-]*$" + @echo off + REM Qwen Code Installation Script + REM Installs Qwen Code from a standalone archive when available, with npm fallback. + REM This script intentionally does not install Node.js or change npm config. + + setlocal enabledelayedexpansion + + call :ValidateRawEnvironmentOptions + if %ERRORLEVEL% NEQ 0 exit /b 1 + + set "SOURCE=unknown" + set "METHOD=" + if defined QWEN_INSTALL_METHOD set "METHOD=!QWEN_INSTALL_METHOD!" + set "MIRROR=auto" + if defined QWEN_INSTALL_MIRROR set "MIRROR=!QWEN_INSTALL_MIRROR!" + set "NO_MODIFY_PATH=0" + if defined QWEN_NO_MODIFY_PATH set "NO_MODIFY_PATH=!QWEN_NO_MODIFY_PATH!" + set "BASE_URL=" + if defined QWEN_INSTALL_BASE_URL set "BASE_URL=!QWEN_INSTALL_BASE_URL!" + set "ARCHIVE_PATH=" + if defined QWEN_INSTALL_ARCHIVE set "ARCHIVE_PATH=!QWEN_INSTALL_ARCHIVE!" + set "VERSION=latest" + if defined QWEN_INSTALL_VERSION set "VERSION=!QWEN_INSTALL_VERSION!" + set "NPM_REGISTRY=https://registry.npmmirror.com" + if defined QWEN_NPM_REGISTRY set "NPM_REGISTRY=!QWEN_NPM_REGISTRY!" + if defined LOCALAPPDATA ( + set "INSTALL_BASE=!LOCALAPPDATA!\qwen-code" + ) else ( + set "INSTALL_BASE=!USERPROFILE!\AppData\Local\qwen-code" + ) + if defined QWEN_INSTALL_ROOT set "INSTALL_BASE=!QWEN_INSTALL_ROOT!" + set "INSTALL_DIR=!INSTALL_BASE!\qwen-code" + if defined QWEN_INSTALL_LIB_DIR set "INSTALL_DIR=!QWEN_INSTALL_LIB_DIR!" + set "INSTALL_BIN_DIR=!INSTALL_BASE!\bin" + if defined QWEN_INSTALL_BIN_DIR set "INSTALL_BIN_DIR=!QWEN_INSTALL_BIN_DIR!" + + REM Parse flags before any network or filesystem work. + :parse_args + if "%~1"=="" goto end_parse + set "ARG_RAW=%~1" + set "ARG_KEY=%~1" + set "ARG_VALUE=" + set "ARG_HAS_INLINE_VALUE=0" + for /f "tokens=1,* delims==" %%A in ("%~1") do ( + set "ARG_KEY=%%~A" + set "ARG_VALUE=%%~B" + ) + if not "!ARG_KEY!"=="!ARG_RAW!" set "ARG_HAS_INLINE_VALUE=1" + if /i "!ARG_KEY!"=="--source" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --source requires a value + exit /b 1 + ) + set "SOURCE=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --source requires a value + exit /b 1 + ) + set "SOURCE=%~2" + shift + shift + goto parse_args + ) + if /i "%~1"=="-s" ( + if "%~2"=="" ( + echo ERROR: -s requires a value + exit /b 1 + ) + set "SOURCE=%~2" + shift + shift + goto parse_args + ) + if /i "!ARG_KEY!"=="--method" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --method requires a value + exit /b 1 + ) + set "METHOD=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --method requires a value + exit /b 1 + ) + set "METHOD=%~2" + shift + shift + goto parse_args + ) + if /i "!ARG_KEY!"=="--mirror" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --mirror requires a value + exit /b 1 + ) + set "MIRROR=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --mirror requires a value + exit /b 1 + ) + set "MIRROR=%~2" + shift + shift + goto parse_args + ) + if /i "!ARG_KEY!"=="--base-url" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --base-url requires a value + exit /b 1 + ) + set "BASE_URL=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --base-url requires a

Check failure on line 293 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (macos-latest, Node 22.x)

scripts/tests/install-script.test.js > installation scripts > supports code-server-style standalone install on Windows

AssertionError: expected '@echo off\r\nREM Qwen Code Installati…' to contain 'findstr /R /C:"^[0-9][0-9]*\.[0-9][0-…' - Expected + Received - findstr /R /C:"^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[A-Za-z0-9.-]*$" + @echo off + REM Qwen Code Installation Script + REM Installs Qwen Code from a standalone archive when available, with npm fallback. + REM This script intentionally does not install Node.js or change npm config. + + setlocal enabledelayedexpansion + + call :ValidateRawEnvironmentOptions + if %ERRORLEVEL% NEQ 0 exit /b 1 + + set "SOURCE=unknown" + set "METHOD=" + if defined QWEN_INSTALL_METHOD set "METHOD=!QWEN_INSTALL_METHOD!" + set "MIRROR=auto" + if defined QWEN_INSTALL_MIRROR set "MIRROR=!QWEN_INSTALL_MIRROR!" + set "NO_MODIFY_PATH=0" + if defined QWEN_NO_MODIFY_PATH set "NO_MODIFY_PATH=!QWEN_NO_MODIFY_PATH!" + set "BASE_URL=" + if defined QWEN_INSTALL_BASE_URL set "BASE_URL=!QWEN_INSTALL_BASE_URL!" + set "ARCHIVE_PATH=" + if defined QWEN_INSTALL_ARCHIVE set "ARCHIVE_PATH=!QWEN_INSTALL_ARCHIVE!" + set "VERSION=latest" + if defined QWEN_INSTALL_VERSION set "VERSION=!QWEN_INSTALL_VERSION!" + set "NPM_REGISTRY=https://registry.npmmirror.com" + if defined QWEN_NPM_REGISTRY set "NPM_REGISTRY=!QWEN_NPM_REGISTRY!" + if defined LOCALAPPDATA ( + set "INSTALL_BASE=!LOCALAPPDATA!\qwen-code" + ) else ( + set "INSTALL_BASE=!USERPROFILE!\AppData\Local\qwen-code" + ) + if defined QWEN_INSTALL_ROOT set "INSTALL_BASE=!QWEN_INSTALL_ROOT!" + set "INSTALL_DIR=!INSTALL_BASE!\qwen-code" + if defined QWEN_INSTALL_LIB_DIR set "INSTALL_DIR=!QWEN_INSTALL_LIB_DIR!" + set "INSTALL_BIN_DIR=!INSTALL_BASE!\bin" + if defined QWEN_INSTALL_BIN_DIR set "INSTALL_BIN_DIR=!QWEN_INSTALL_BIN_DIR!" + + REM Parse flags before any network or filesystem work. + :parse_args + if "%~1"=="" goto end_parse + set "ARG_RAW=%~1" + set "ARG_KEY=%~1" + set "ARG_VALUE=" + set "ARG_HAS_INLINE_VALUE=0" + for /f "tokens=1,* delims==" %%A in ("%~1") do ( + set "ARG_KEY=%%~A" + set "ARG_VALUE=%%~B" + ) + if not "!ARG_KEY!"=="!ARG_RAW!" set "ARG_HAS_INLINE_VALUE=1" + if /i "!ARG_KEY!"=="--source" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --source requires a value + exit /b 1 + ) + set "SOURCE=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --source requires a value + exit /b 1 + ) + set "SOURCE=%~2" + shift + shift + goto parse_args + ) + if /i "%~1"=="-s" ( + if "%~2"=="" ( + echo ERROR: -s requires a value + exit /b 1 + ) + set "SOURCE=%~2" + shift + shift + goto parse_args + ) + if /i "!ARG_KEY!"=="--method" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --method requires a value + exit /b 1 + ) + set "METHOD=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --method requires a value + exit /b 1 + ) + set "METHOD=%~2" + shift + shift + goto parse_args + ) + if /i "!ARG_KEY!"=="--mirror" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --mirror requires a value + exit /b 1 + ) + set "MIRROR=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --mirror requires a value + exit /b 1 + ) + set "MIRROR=%~2" + shift + shift + goto parse_args + ) + if /i "!ARG_KEY!"=="--base-url" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --base-url requires a value + exit /b 1 + ) + set "BASE_URL=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --base-url requires a
'findstr /R /C:"^[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*[A-Za-z0-9.-]*$"',
);
expect(script).toContain(
'findstr /R /C:"^v[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*[A-Za-z0-9.-]*$"',
);
expect(script).not.toContain('/C:"^v*[0-9]');
expect(script).toContain(
'call :ValidateHttpsUrlVar "NPM_REGISTRY" "--registry"',
);
Expand Down Expand Up @@ -664,7 +673,7 @@
['scripts/build-hosted-installation-assets.js', '--help'],
{ encoding: 'utf8' },
);
const verifierOutput = execFileSync(

Check failure on line 676 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

scripts/tests/install-script.test.js > standalone release packaging > loads the hosted installation release helpers

Error: Command failed: /opt/hostedtoolcache/node/22.22.3/x64/bin/node scripts/verify-installation-release.js --help file:///home/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145 } ^ SyntaxError: Unexpected token '}' at compileSourceTextModule (node:internal/modules/esm/utils:346:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:110:18) at #translate (node:internal/modules/esm/loader:559:20) at afterLoad (node:internal/modules/esm/loader:612:29) at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:617:12) at #createModuleJob (node:internal/modules/esm/loader:640:36) at #getJobFromResolveResult (node:internal/modules/esm/loader:353:34) at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:321:41) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:680:25) Node.js v22.22.3 ❯ scripts/tests/install-script.test.js:676:28 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { status: 1, signal: null, output: [ null, '', 'file:///home/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145\n}\n^\n\nSyntaxError: Unexpected token \'}\'\n at compileSourceTextModule (node:internal/modules/esm/utils:346:16)\n at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:110:18)\n at #translate (node:internal/modules/esm/loader:559:20)\n at afterLoad (node:internal/modules/esm/loader:612:29)\n at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:617:12)\n at #createModuleJob (node:internal/modules/esm/loader:640:36)\n at #getJobFromResolveResult (node:internal/modules/esm/loader:353:34)\n at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:321:41)\n at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:680:25)\n\nNode.js v22.22.3\n' ], pid: 31182, stdout: '', stderr: 'file:///home/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145\n}\n^\n\nSyntaxError: Unexpected token \'}\'\n at compileSourceTextModule (node:internal/modules/esm/utils:346:16)\n at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:110:18)\n at #translate (node:internal/modules/esm/loader:559:20)\n at afterLoad (node:internal/modules/esm/loader:612:29)\n at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:617:12)\n at #createModuleJob (node:internal/modules/esm/loader:640:36)\n at #getJobFromResolveResult (node:internal/modules/esm/loader:353:34)\n at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:321:41)\n at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:680:25)\n\nNode.js v22.22.3\n' }

Check failure on line 676 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (macos-latest, Node 22.x)

scripts/tests/install-script.test.js > standalone release packaging > loads the hosted installation release helpers

Error: Command failed: /Users/runner/hostedtoolcache/node/22.22.2/arm64/bin/node scripts/verify-installation-release.js --help file:///Users/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145 } ^ SyntaxError: Unexpected token '}' at compileSourceTextModule (node:internal/modules/esm/utils:346:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:107:18) at #translate (node:internal/modules/esm/loader:546:20) at afterLoad (node:internal/modules/esm/loader:596:29) at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:601:12) at #createModuleJob (node:internal/modules/esm/loader:624:36) at #getJobFromResolveResult (node:internal/modules/esm/loader:343:34) at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:311:41) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:664:25) Node.js v22.22.2 ❯ scripts/tests/install-script.test.js:676:28 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { status: 1, signal: null, output: [ null, '', 'file:///Users/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145\n}\n^\n\nSyntaxError: Unexpected token \'}\'\n at compileSourceTextModule (node:internal/modules/esm/utils:346:16)\n at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:107:18)\n at #translate (node:internal/modules/esm/loader:546:20)\n at afterLoad (node:internal/modules/esm/loader:596:29)\n at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:601:12)\n at #createModuleJob (node:internal/modules/esm/loader:624:36)\n at #getJobFromResolveResult (node:internal/modules/esm/loader:343:34)\n at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:311:41)\n at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:664:25)\n\nNode.js v22.22.2\n' ], pid: 56299, stdout: '', stderr: 'file:///Users/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145\n}\n^\n\nSyntaxError: Unexpected token \'}\'\n at compileSourceTextModule (node:internal/modules/esm/utils:346:16)\n at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:107:18)\n at #translate (node:internal/modules/esm/loader:546:20)\n at afterLoad (node:internal/modules/esm/loader:596:29)\n at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:601:12)\n at #createModuleJob (node:internal/modules/esm/loader:624:36)\n at #getJobFromResolveResult (node:internal/modules/esm/loader:343:34)\n at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:311:41)\n at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:664:25)\n\nNode.js v22.22.2\n' }
process.execPath,
['scripts/verify-installation-release.js', '--help'],
{ encoding: 'utf8' },
Expand Down Expand Up @@ -696,7 +705,7 @@
caughtError?.stdout?.toString(),
caughtError?.stderr?.toString(),
].join('\n'),
).toMatch(expectedOutput);

Check failure on line 708 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

scripts/tests/install-script.test.js > standalone release packaging > rejects invalid installation release verification CLI arguments

AssertionError: expected 'Command failed: /opt/hostedtoolcache/…' to match /Unknown option: --unknown/ - Expected: /Unknown option: --unknown/ + Received: "Command failed: /opt/hostedtoolcache/node/22.22.3/x64/bin/node scripts/verify-installation-release.js --unknown file:///home/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145 } ^ SyntaxError: Unexpected token '}' at compileSourceTextModule (node:internal/modules/esm/utils:346:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:110:18) at #translate (node:internal/modules/esm/loader:559:20) at afterLoad (node:internal/modules/esm/loader:612:29) at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:617:12) at #createModuleJob (node:internal/modules/esm/loader:640:36) at #getJobFromResolveResult (node:internal/modules/esm/loader:353:34) at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:321:41) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:680:25) Node.js v22.22.3 file:///home/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145 } ^ SyntaxError: Unexpected token '}' at compileSourceTextModule (node:internal/modules/esm/utils:346:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:110:18) at #translate (node:internal/modules/esm/loader:559:20) at afterLoad (node:internal/modules/esm/loader:612:29) at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:617:12) at #createModuleJob (node:internal/modules/esm/loader:640:36) at #getJobFromResolveResult (node:internal/modules/esm/loader:353:34) at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:321:41) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:680:25) Node.js v22.22.3 " ❯ expectFail scripts/tests/install-script.test.js:708:9 ❯ scripts/tests/install-script.test.js:711:5

Check failure on line 708 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (macos-latest, Node 22.x)

scripts/tests/install-script.test.js > standalone release packaging > rejects invalid installation release verification CLI arguments

AssertionError: expected 'Command failed: /Users/runner/hostedt…' to match /Unknown option: --unknown/ - Expected: /Unknown option: --unknown/ + Received: "Command failed: /Users/runner/hostedtoolcache/node/22.22.2/arm64/bin/node scripts/verify-installation-release.js --unknown file:///Users/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145 } ^ SyntaxError: Unexpected token '}' at compileSourceTextModule (node:internal/modules/esm/utils:346:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:107:18) at #translate (node:internal/modules/esm/loader:546:20) at afterLoad (node:internal/modules/esm/loader:596:29) at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:601:12) at #createModuleJob (node:internal/modules/esm/loader:624:36) at #getJobFromResolveResult (node:internal/modules/esm/loader:343:34) at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:311:41) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:664:25) Node.js v22.22.2 file:///Users/runner/work/qwen-code/qwen-code/scripts/verify-installation-release.js:145 } ^ SyntaxError: Unexpected token '}' at compileSourceTextModule (node:internal/modules/esm/utils:346:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:107:18) at #translate (node:internal/modules/esm/loader:546:20) at afterLoad (node:internal/modules/esm/loader:596:29) at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:601:12) at #createModuleJob (node:internal/modules/esm/loader:624:36) at #getJobFromResolveResult (node:internal/modules/esm/loader:343:34) at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:311:41) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:664:25) Node.js v22.22.2 " ❯ expectFail scripts/tests/install-script.test.js:708:9 ❯ scripts/tests/install-script.test.js:711:5
};

expectFail(
Expand Down Expand Up @@ -1201,6 +1210,32 @@
await expect(verifyReleaseDirectory(tmpDir)).rejects.toThrow(
/Unexpected release asset checksum: qwen-code-extra\.tar\.gz/,
);

writeStandaloneReleaseAssets(tmpDir, EXPECTED_STANDALONE_ARCHIVE_NAMES);
writeStandaloneReleaseChecksums(
tmpDir,
EXPECTED_STANDALONE_ARCHIVE_NAMES.slice(1),
);
await expect(verifyReleaseDirectory(tmpDir)).rejects.toThrow(
/Missing release asset checksum: qwen-code-/,
);
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
});

it('rejects unexpected files in a release directory', async () => {
const { EXPECTED_STANDALONE_ARCHIVE_NAMES, verifyReleaseDirectory } =
await import(installationReleaseVerificationScriptUrl);
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-release-verify-'));

try {
writeStandaloneReleaseAssets(tmpDir, EXPECTED_STANDALONE_ARCHIVE_NAMES);
writeFileSync(path.join(tmpDir, '.DS_Store'), 'finder metadata\n');

await expect(verifyReleaseDirectory(tmpDir)).rejects.toThrow(
/Unexpected file\(s\) in release directory: \.DS_Store/,
);
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
Expand Down Expand Up @@ -1278,11 +1313,62 @@
).rejects.toThrow(/Checksum mismatch for qwen-code-/);
});

it('rejects remote SHA256SUMS responses that are unavailable', async () => {
const { verifyReleaseBaseUrl } = await import(
installationReleaseVerificationScriptUrl
);

await expect(
verifyReleaseBaseUrl('https://example.com/qwen-code/v0.0.0', {
fetchImpl: async () => new Response('missing', { status: 404 }),
}),
).rejects.toThrow(/Failed to download .*SHA256SUMS: 404/);
});

it('rejects remote SHA256SUMS with missing or extra archive entries', async () => {
const { EXPECTED_STANDALONE_ARCHIVE_NAMES, verifyReleaseBaseUrl } =
await import(installationReleaseVerificationScriptUrl);

await expect(
verifyReleaseBaseUrl('https://example.com/qwen-code/v0.0.0', {
fetchImpl: async (url) => {
if (url.endsWith('/SHA256SUMS')) {
return new Response(
placeholderChecksumContent(
EXPECTED_STANDALONE_ARCHIVE_NAMES.slice(1),
),
);
}
return new Response(null, { status: 200 });
},
}),
).rejects.toThrow(/Missing release asset checksum: qwen-code-/);

await expect(
verifyReleaseBaseUrl('https://example.com/qwen-code/v0.0.0', {
fetchImpl: async (url) => {
if (url.endsWith('/SHA256SUMS')) {
return new Response(
placeholderChecksumContent([
...EXPECTED_STANDALONE_ARCHIVE_NAMES,
'qwen-code-extra.tar.gz',
]),
);
}
return new Response(null, { status: 200 });
},
}),
).rejects.toThrow(
/Unexpected release asset checksum: qwen-code-extra\.tar\.gz/,
);
});

it('rejects a release base URL that is not https', async () => {
const { verifyReleaseBaseUrl } = await import(
installationReleaseVerificationScriptUrl
);

// file:// must be rejected as a URL the verifier cannot reach safely.
await expect(verifyReleaseBaseUrl('file:///tmp/release/')).rejects.toThrow(
/--base-url must use https/,
);
Expand Down Expand Up @@ -1811,7 +1897,7 @@
expect(guide).toContain('ALIYUN_OSS_ACCESS_KEY_SECRET');
expect(guide).toContain('ALIYUN_OSS_BUCKET');
expect(guide).toContain('ALIYUN_OSS_ENDPOINT');
expect(guide).toContain('Public installation documentation');

Check failure on line 1900 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

scripts/tests/install-script.test.js > standalone release packaging > documents optional native module parity for standalone installs

AssertionError: expected '# Installation Guide for Qwen Code wi…' to contain 'Public installation documentation' - Expected + Received - Public installation documentation + # Installation Guide for Qwen Code with Source Tracking + + This guide describes the source-tracking installation scripts for Qwen Code. + The scripts prefer standalone release archives and can fall back to npm when a + standalone archive is not available. + + ## Overview + + The installers are intentionally lightweight: + + - They try a standalone archive first by default. + - They do not install Node.js, NVM, or any other Node version manager. + - They do not edit npm config. Standalone installs may update the shell profile + or user PATH so the generated `qwen` shim is discoverable. + - They do not start `qwen` automatically after installation. + - They store source information in `~/.qwen/source.json` or + `%USERPROFILE%\.qwen\source.json` when `--source` is provided. + + Standalone archives include a private Node.js runtime, so users do not need a + local Node.js installation on the standalone path. Node.js 22 or newer and npm + are only required when the installer falls back to npm or when + `--method npm` is used. + + ## Installation Scripts + + - Linux/macOS: `install-qwen-standalone.sh` + - Windows: `install-qwen-standalone.ps1` + - Linux/macOS uninstall: `uninstall-qwen-standalone.sh` + - Windows uninstall: `uninstall-qwen-standalone.ps1` + + ## Release Artifacts + + GitHub releases publish these standalone archives: + + - `qwen-code-darwin-arm64.tar.gz` + - `qwen-code-darwin-x64.tar.gz` + - `qwen-code-linux-arm64.tar.gz` + - `qwen-code-linux-x64.tar.gz` + - `qwen-code-win-x64.zip` + - `SHA256SUMS` + + The new standalone-first installer scripts (`install-qwen-standalone.sh`, + `install-qwen-standalone.ps1`) are not republished per release. They are served + from a hosted installation endpoint and accept `--version` to pin a specific + standalone release. The `standalone` suffix intentionally avoids overwriting the + existing production `install-qwen.sh` / `install-qwen.bat` OSS objects during + the staged rollout. + + Hosted installer assets are staged separately from GitHub Release archives: + + - `install-qwen-standalone.sh` is the Linux/macOS hosted entrypoint. + - `install-qwen-standalone.ps1` is the Windows hosted entrypoint for `irm | iex`. + - `install-qwen-standalone.bat` is the Windows installer implementation used by + `install-qwen-standalone.ps1` and can also be downloaded and run directly. + - `uninstall-qwen-standalone.sh` removes Linux/macOS standalone installs. + - `uninstall-qwen-standalone.ps1` removes Windows standalone installs. + + The global standalone-suffixed OSS entrypoints are maintained under + `installation/install-qwen-standalone.sh`, + `installation/install-qwen-standalone.ps1`, + `installation/install-qwen-standalone.bat`, + `installation/uninstall-qwen-standalone.sh`, and + `installation/uninstall-qwen-standalone.ps1`. + + Build them with: + + ```bash + npm run package:hosted-installation -- --out-dir dist/installation + ``` + + The staged `install-qwen-standalone.sh`, `install-qwen-standalone.ps1`, + `install-qwen-standalone.bat`, `uninstall-qwen-standalone.sh`, and + `uninstall-qwen-standalone.ps1` files map to the standalone-suffixed hosted URLs + shown above. The staging command also writes `SHA256SUMS` for upload + verification. During a non-dry-run stable release, the publish workflow uploads + a byte-for-byte snapshot to `installation/vX.Y.Z/` for audit and rollback, and + also refreshes the global `installation/` entrypoint objects so `curl | bash` + links keep resolving without a version segment. The versioned snapshot lets you + roll back by repointing the global objects to a previous tag if a regression is + caught after publish. The hosted + installers intentionally default to `latest`; on Aliyun OSS this means reading + `releases/qwen-code/latest/VERSION` first, then downloading the matching + versioned release directory. Use `--version` or `QWEN_INSTALL_VERSION` to pin a + standalone re

Check failure on line 1900 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (macos-latest, Node 22.x)

scripts/tests/install-script.test.js > standalone release packaging > documents optional native module parity for standalone installs

AssertionError: expected '# Installation Guide for Qwen Code wi…' to contain 'Public installation documentation' - Expected + Received - Public installation documentation + # Installation Guide for Qwen Code with Source Tracking + + This guide describes the source-tracking installation scripts for Qwen Code. + The scripts prefer standalone release archives and can fall back to npm when a + standalone archive is not available. + + ## Overview + + The installers are intentionally lightweight: + + - They try a standalone archive first by default. + - They do not install Node.js, NVM, or any other Node version manager. + - They do not edit npm config. Standalone installs may update the shell profile + or user PATH so the generated `qwen` shim is discoverable. + - They do not start `qwen` automatically after installation. + - They store source information in `~/.qwen/source.json` or + `%USERPROFILE%\.qwen\source.json` when `--source` is provided. + + Standalone archives include a private Node.js runtime, so users do not need a + local Node.js installation on the standalone path. Node.js 22 or newer and npm + are only required when the installer falls back to npm or when + `--method npm` is used. + + ## Installation Scripts + + - Linux/macOS: `install-qwen-standalone.sh` + - Windows: `install-qwen-standalone.ps1` + - Linux/macOS uninstall: `uninstall-qwen-standalone.sh` + - Windows uninstall: `uninstall-qwen-standalone.ps1` + + ## Release Artifacts + + GitHub releases publish these standalone archives: + + - `qwen-code-darwin-arm64.tar.gz` + - `qwen-code-darwin-x64.tar.gz` + - `qwen-code-linux-arm64.tar.gz` + - `qwen-code-linux-x64.tar.gz` + - `qwen-code-win-x64.zip` + - `SHA256SUMS` + + The new standalone-first installer scripts (`install-qwen-standalone.sh`, + `install-qwen-standalone.ps1`) are not republished per release. They are served + from a hosted installation endpoint and accept `--version` to pin a specific + standalone release. The `standalone` suffix intentionally avoids overwriting the + existing production `install-qwen.sh` / `install-qwen.bat` OSS objects during + the staged rollout. + + Hosted installer assets are staged separately from GitHub Release archives: + + - `install-qwen-standalone.sh` is the Linux/macOS hosted entrypoint. + - `install-qwen-standalone.ps1` is the Windows hosted entrypoint for `irm | iex`. + - `install-qwen-standalone.bat` is the Windows installer implementation used by + `install-qwen-standalone.ps1` and can also be downloaded and run directly. + - `uninstall-qwen-standalone.sh` removes Linux/macOS standalone installs. + - `uninstall-qwen-standalone.ps1` removes Windows standalone installs. + + The global standalone-suffixed OSS entrypoints are maintained under + `installation/install-qwen-standalone.sh`, + `installation/install-qwen-standalone.ps1`, + `installation/install-qwen-standalone.bat`, + `installation/uninstall-qwen-standalone.sh`, and + `installation/uninstall-qwen-standalone.ps1`. + + Build them with: + + ```bash + npm run package:hosted-installation -- --out-dir dist/installation + ``` + + The staged `install-qwen-standalone.sh`, `install-qwen-standalone.ps1`, + `install-qwen-standalone.bat`, `uninstall-qwen-standalone.sh`, and + `uninstall-qwen-standalone.ps1` files map to the standalone-suffixed hosted URLs + shown above. The staging command also writes `SHA256SUMS` for upload + verification. During a non-dry-run stable release, the publish workflow uploads + a byte-for-byte snapshot to `installation/vX.Y.Z/` for audit and rollback, and + also refreshes the global `installation/` entrypoint objects so `curl | bash` + links keep resolving without a version segment. The versioned snapshot lets you + roll back by repointing the global objects to a previous tag if a regression is + caught after publish. The hosted + installers intentionally default to `latest`; on Aliyun OSS this means reading + `releases/qwen-code/latest/VERSION` first, then downloading the matching + versioned release directory. Use `--version` or `QWEN_INSTALL_VERSION` to pin a + standalone re
expect(guide).toContain('node-pty');
expect(guide).toContain('clipboard');
});
Expand Down Expand Up @@ -1869,7 +1955,7 @@
const archive = packageFakeStandalone(tmpDir);
const installRoot = path.join(tmpDir, 'install');
const home = path.join(tmpDir, 'home');
const output = runUnixInstaller(archive, installRoot, home).toString();
runUnixInstaller(archive, installRoot, home);

expect(existsSync(path.join(installRoot, 'bin', 'qwen'))).toBe(true);
expect(
Expand All @@ -1887,26 +1973,26 @@
.toString()
.trim();
expect(version).toBe('0.0.0-smoke');
expect(output).toContain('Installing Qwen Code version: latest');

Check failure on line 1976 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined

Check failure on line 1976 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

scripts/tests/install-script.test.js > Linux/macOS installer end-to-end > installs a local standalone archive with checksum verification

ReferenceError: output is not defined ❯ scripts/tests/install-script.test.js:1976:16

Check failure on line 1976 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (macos-latest, Node 22.x)

scripts/tests/install-script.test.js > Linux/macOS installer end-to-end > installs a local standalone archive with checksum verification

ReferenceError: output is not defined ❯ scripts/tests/install-script.test.js:1976:16
expect(output).toContain('QWEN CODE');

Check failure on line 1977 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
expect(output).toContain(

Check failure on line 1978 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
'Qwen Code 0.0.0-smoke installed successfully.',
);
expect(output).toContain('To start:\n cd <project>\n qwen');

Check failure on line 1981 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
expect(output).toContain(

Check failure on line 1982 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
`Installed to:\n ${path.join(installRoot, 'lib', 'qwen-code')}`,
);
expect(output).toContain('Uninstall:');

Check failure on line 1985 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
expect(output).toContain(

Check failure on line 1986 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/uninstall-qwen-standalone.sh',
);
expect(output).toContain(

Check failure on line 1989 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
`QWEN_INSTALL_LIB_DIR='${path.join(installRoot, 'lib', 'qwen-code')}'`,
);
expect(output).toContain(

Check failure on line 1992 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
`QWEN_INSTALL_BIN_DIR='${path.join(installRoot, 'bin')}'`,
);
expect(output).not.toContain('rm -rf');

Check failure on line 1995 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Lint

'output' is not defined
} finally {
rmSync(tmpDir, { recursive: true, force: true });
restoreMinimalDist(createdDist);
Expand Down Expand Up @@ -2630,6 +2716,64 @@
}
});

itOnUnix('rejects archive symlinks before extraction', () => {
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-install-test-'));

try {
const archive = createSymlinkStandaloneArchive(tmpDir);
const tarWrapperDir = path.join(tmpDir, 'bin');
const marker = path.join(tmpDir, 'tar-extraction-attempted');
mkdirSync(tarWrapperDir, { recursive: true });
writeFileSync(
path.join(tarWrapperDir, 'tar'),
[
'#!/usr/bin/env bash',
'if [[ "$1" == "-xzf" || "$1" == "-xf" ]]; then',
' touch "$QWEN_TAR_EXTRACT_MARKER"',
'fi',
'exec "$QWEN_REAL_TAR" "$@"',
'',
].join('\n'),
);
chmodSync(path.join(tarWrapperDir, 'tar'), 0o755);

expect(() =>
runUnixInstaller(
archive,
path.join(tmpDir, 'install'),
path.join(tmpDir, 'home'),
'standalone',
{
PATH: `${tarWrapperDir}${path.delimiter}${process.env.PATH}`,
QWEN_REAL_TAR: execFileSync('which', ['tar']).toString().trim(),
QWEN_TAR_EXTRACT_MARKER: marker,
},
),
).toThrow(/Archive contains symlinks/);
expect(existsSync(marker)).toBe(false);

Check failure on line 2753 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

scripts/tests/install-script.test.js > Linux/macOS installer end-to-end > rejects archive symlinks before extraction

AssertionError: expected true to be false // Object.is equality - Expected + Received - false + true ❯ scripts/tests/install-script.test.js:2753:34

Check failure on line 2753 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (macos-latest, Node 22.x)

scripts/tests/install-script.test.js > Linux/macOS installer end-to-end > rejects archive symlinks before extraction

AssertionError: expected true to be false // Object.is equality - Expected + Received - false + true ❯ scripts/tests/install-script.test.js:2753:34
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
});

itOnUnix('rejects empty standalone archives with a clear error', () => {
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-install-test-'));

try {
const archive = createEmptyStandaloneArchive(tmpDir);

expect(() =>
runUnixInstaller(
archive,
path.join(tmpDir, 'install'),
path.join(tmpDir, 'home'),
),
).toThrow(/Archive is empty/);

Check failure on line 2771 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

scripts/tests/install-script.test.js > Linux/macOS installer end-to-end > rejects empty standalone archives with a clear error

AssertionError: expected [Function] to throw error matching /Archive is empty/ but got 'Command failed: bash scripts/installa…' - Expected: /Archive is empty/ + Received: "Command failed: bash scripts/installation/install-qwen-standalone.sh --method standalone --archive /tmp/qwen-install-test-FPfHyG/out/qwen-code-linux-x64.tar.gz --source smoke ERROR: Archive contains unsafe path: <empty> Installing Qwen Code version: latest SUCCESS: Checksum verified for qwen-code-linux-x64.tar.gz. ERROR: Archive contains unsafe path: <empty> " ❯ scripts/tests/install-script.test.js:2771:9

Check failure on line 2771 in scripts/tests/install-script.test.js

View workflow job for this annotation

GitHub Actions / Test (macos-latest, Node 22.x)

scripts/tests/install-script.test.js > Linux/macOS installer end-to-end > rejects empty standalone archives with a clear error

AssertionError: expected [Function] to throw error matching /Archive is empty/ but got 'Command failed: bash scripts/installa…' - Expected: /Archive is empty/ + Received: "Command failed: bash scripts/installation/install-qwen-standalone.sh --method standalone --archive /var/folders/tb/y368xp_x10s3ty1b_mtl5mxr0000gn/T/qwen-install-test-e20K1k/out/qwen-code-linux-x64.tar.gz --source smoke ERROR: Archive contains unsafe path: <empty> Installing Qwen Code version: latest SUCCESS: Checksum verified for qwen-code-linux-x64.tar.gz. ERROR: Archive contains unsafe path: <empty> " ❯ scripts/tests/install-script.test.js:2771:9
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
});

itOnUnix(
'rejects standalone archives containing path traversal entries',
() => {
Expand Down Expand Up @@ -3802,6 +3946,17 @@
return archive;
}

function createEmptyStandaloneArchive(tmpDir) {
const outDir = path.join(tmpDir, 'out');
Comment thread
yiliang114 marked this conversation as resolved.
Outdated
mkdirSync(outDir, { recursive: true });
const archive = path.join(outDir, 'qwen-code-linux-x64.tar.gz');
execFileSync('tar', ['-czf', archive, '-T', '/dev/null'], {
stdio: 'ignore',
});
writeChecksumFile(outDir, path.basename(archive));
return archive;
}

function createTraversalStandaloneArchive(tmpDir) {
const maliciousRoot = path.join(tmpDir, 'malicious');
const packageRoot = path.join(maliciousRoot, 'qwen-code');
Expand Down
Loading
Loading