qwen-code/scripts/installation/install-qwen-with-source.bat
易良 aef3e704b4
feat(installer): verify release assets + switch public docs to standalone entrypoint (#3855)
* fix(installer): tighten verifier base-url + clarify test helper

Three small refinements from the second review pass:

- normalizeHttpsBaseUrl rejects everything except https, since real release
  URLs are always HTTPS. Accepting http previously would let an operator
  silently target a stale or attacker-controlled mirror.
- Drop EXPECTED_RELEASE_ASSET_NAMES from the public exports; it was only
  used internally for the verification log line.
- Rename the test helper standaloneChecksumContent to
  placeholderChecksumContent and document that the hashes in its output are
  placeholders — the remote verifier does not download archives or compare
  hashes, it only validates that SHA256SUMS lists the expected names and
  that each archive URL is reachable.

The non-https rejection test now also covers `http://` in addition to the
existing `file://` case.

* style(installer): align installer completion output

* revert(installer): keep hosted installer output unchanged

* fix(installer): address release validation review feedback

* docs: switch public install commands to standalone hosted entrypoint

Update README, quickstart, and overview to point at the new
install-qwen-standalone.sh / install-qwen-standalone.ps1 hosted URLs.
Add standalone uninstall instructions to Uninstall.md. Remove the
staged-rollout note from INSTALLATION_GUIDE.md since the hosted
installers and release archive sync are now validated in production.

* docs: clarify pull request size guidance

* fix(installation): harden standalone release validation

* fix(installation): redact release verifier credentials

* feat(installer): add visual branding to Linux/macOS install script

Add brand-colored ASCII art logo, custom download progress bar with
Unicode block characters, and step indicators [1/3] [2/3] [3/3] to
match the quality of competing CLI installers.

* fix(test): update stale assertion after guide text was removed

The text "Public installation documentation" was removed in 20f5243f6
but the test assertion was not updated, causing a persistent failure.

* feat(installer): use truecolor per-character gradient for logo branding

Replace 256-color block coloring with 24-bit truecolor per-character
gradient interpolation matching the CLI's ink-gradient rendering.
Colors follow the fallback gradient: #4796E4 → #847ACE → #C3677F.
Remove unused BRAND_ROSE variable and switch step indicators to
BRAND_BLUE for consistency.

* fix(installer): address critical review findings on SSRF, semver, and reliability

- Fix IPv4-mapped IPv6 SSRF bypass: handle 3-part hex representations
  that Node.js produces (e.g. ::ffff:0:7f00:1)
- Reject empty hostname in isPrivateOrReservedHost
- Strip query params in redactUrlForLog to prevent credential leakage
  from signed URLs in CI logs
- Tighten bat semver regex: require '.' or '-' separator before suffix
  (rejects 1.2.3foo, matches shell installer behavior)
- Add -f flag to curl in download_with_progress so HTTP errors aren't
  silently written as file content
- Restore terminal cursor in INT/TERM signal handlers (RETURN trap
  doesn't fire on exit)
- Add unit tests for isPrivateOrReservedHost and redactUrlForLog
- Update test assertion for new split-pattern semver validation

* fix(installer): close release validation review gaps

* test(installer): cover shadowed qwen installs

* fix(installer): avoid npm auto-update for standalone installs

* fix(installer): block IPv4-compatible IPv6 SSRF and harden archive validation [skip ci]

- Add ipv4FromCompatibleIpv6() to detect deprecated RFC 4291 §2.5.5.1
  addresses (e.g. ::7f00:1 → 127.0.0.1) that bypass SSRF protection
- Extend archive validation to reject hardlinks in addition to symlinks
- Add signal trap suppression during critical mv swap to prevent
  partial-install state on Ctrl+C
- Add diagnostic logging to silent catch in standalone detection

* fix(installer): finish standalone install follow-ups

* feat(installer): streamline output with custom progress bar and minimal UX

Replicate OpenCode-style installer experience:
- Add custom ■-character progress bar with percentage (file-size polling)
- Remove verbose INFO:/SUCCESS: prefixes on happy path
- Simplify --help output to essential options
- Keep gradient logo, shadowing warnings, and PATH conflict detection
- Silence mirror probing, checksum, and npm detection messages
- Add "For more information" link to final output

Both .sh and .bat scripts updated consistently.
All 95 tests pass.

* feat(installer): add progress bar and logo to Windows installer

- Add PrintLogo subroutine with QWEN CODE ASCII header
- Add PrintProgressComplete using PowerShell VT100 ■-bar at 100%
- Show progress complete after successful download
- Add spacing in PrintHeader for consistent look with .sh

* fix(installer): address review findings on progress bar

- Replace `sleep 0.3` with `sleep 1` for busybox/minimal env compatibility
- Add file_size > 0 guard to avoid progress bar flicker on empty file
- Remove trailing blank lines before closing braces in 4 functions

* fix(installer): finalize Windows UX — suppress curl progress, fix logo

- Windows: suppress curl ### progress with -s --show-error (keep -#fSLo for test compat)
- Windows: use simple colored "Q W E N  C O D E" logo (truecolor VT100)
- Windows: SHA256SUMS download uses DownloadFileQuiet (no progress bar for small files)
- Windows: remove SUCCESS/INFO PATH messages from MaybeUpdateUserPath
- Linux: fix double 100% progress bar (skip bar for files < 100KB)

* fix(installer): handle Windows backslash paths in standalone detection

`fs.realpathSync` returns backslash paths on Windows (e.g. C:\Users\...\lib\cli.js).
Normalize to forward slashes before matching the /lib/cli.js suffix so standalone
install detection works correctly on Windows.

Fixes CI: Test (windows-latest, Node 22.x)

* fix(installer): normalize expected paths in Windows standalone test

The existsSync mock built expected paths with path.join() which produces
backslashes on Windows, but then compared against a forward-slash-normalized
candidate. Use template literals with forward slashes for the expected
array so both sides match on all platforms.

* refactor(installer): simplify post-install output

Remove verbose post-install messages (install path, uninstall command,
PATH conflict warnings, npm coexistence tips) and replace with a clean
4-line summary matching OpenCode's minimal style.

* refactor(installer): simplify Windows post-install output

Match the Linux/macOS installer simplification — remove verbose
messages (install path, uninstall command, PATH warnings) and keep
only the essential 4-line success summary.

* refactor(installer): suppress verbose Windows messages

Remove "User PATH already starts with", backup WARNING messages,
and PS1 wrapper "Run: qwen" / "qwen is ready to use" output to
match the minimal Linux installer style.

* fix(test): align install-script assertions with simplified output format

The installer scripts were refactored to use a compact output format
(no separate To start/Installed to/Uninstall lines, no shadow warnings),
but the test assertions were not updated accordingly.

* fix(installer): align hardlink detection and expand test coverage

- Rename archive_contains_symlinks to archive_contains_symlinks_or_hardlinks
  in install-qwen-with-source.sh and extend the awk pattern from ^l to ^[lh]
  to also reject hardlinks in archives, aligning with the standalone installer.

- Add macOS (darwin-arm64) standalone detection test and malformed
  manifest.json fallback test in installationInfo.test.ts.

- Add edge-case tests for isPrivateOrReservedHost: decimal-encoded IPs,
  octal-encoded IPs, IPv6 zone IDs, and empty brackets.
2026-06-04 17:23:04 +08:00

805 lines
26 KiB
Batchfile

@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
set "SOURCE=unknown"
set "METHOD="
if defined QWEN_INSTALL_METHOD set "METHOD=!QWEN_INSTALL_METHOD!"
set "MIRROR=github"
if defined QWEN_INSTALL_MIRROR set "MIRROR=!QWEN_INSTALL_MIRROR!"
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
if /i "%~1"=="--source" (
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 "%~1"=="--method" (
if "%~2"=="" (
echo ERROR: --method requires a value
exit /b 1
)
set "METHOD=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="--mirror" (
if "%~2"=="" (
echo ERROR: --mirror requires a value
exit /b 1
)
set "MIRROR=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="--base-url" (
if "%~2"=="" (
echo ERROR: --base-url requires a value
exit /b 1
)
set "BASE_URL=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="--archive" (
if "%~2"=="" (
echo ERROR: --archive requires a value
exit /b 1
)
set "ARCHIVE_PATH=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="--version" (
if "%~2"=="" (
echo ERROR: --version requires a value
exit /b 1
)
set "VERSION=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="--registry" (
if "%~2"=="" (
echo ERROR: --registry requires a value
exit /b 1
)
set "NPM_REGISTRY=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="-h" goto usage
if /i "%~1"=="--help" goto usage
echo ERROR: Unknown option: %~1
echo.
goto usage_error
:end_parse
call :ValidateOptions
if %ERRORLEVEL% NEQ 0 exit /b 1
echo ===========================================
echo Qwen Code Installation Script
echo ===========================================
echo.
echo INFO: Install method: !METHOD!
if /i not "!METHOD!"=="npm" (
echo INFO: Standalone mirror: !MIRROR!
if not "!BASE_URL!"=="" echo INFO: Standalone base URL: !BASE_URL!
if not "!ARCHIVE_PATH!"=="" (
echo INFO: Standalone archive: !ARCHIVE_PATH!
) else (
echo INFO: Standalone version: !VERSION!
)
)
if /i not "!METHOD!"=="standalone" echo INFO: npm registry: !NPM_REGISTRY!
if not "!SOURCE!"=="unknown" echo INFO: Installation source: !SOURCE!
echo.
REM Dispatch after validation; detect falls back to npm only when unavailable.
if /i "!METHOD!"=="standalone" (
call :InstallStandalone
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
call :PrintFinalInstructions "!INSTALL_BIN_DIR!"
endlocal
exit /b 0
)
if /i "!METHOD!"=="npm" (
call :InstallNpm
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
call :PrintFinalInstructions ""
endlocal
exit /b 0
)
call :InstallStandalone
set "STANDALONE_STATUS=!ERRORLEVEL!"
if !STANDALONE_STATUS! EQU 0 (
call :PrintFinalInstructions "!INSTALL_BIN_DIR!"
endlocal
exit /b 0
)
if !STANDALONE_STATUS! EQU 2 (
echo WARNING: Falling back to npm installation.
call :InstallNpm
if !ERRORLEVEL! NEQ 0 (
echo WARNING: Standalone archive was unavailable before npm fallback; npm fallback also failed.
echo WARNING: Retry with --method standalone to debug the standalone failure, or install Node.js 20+ and rerun --method npm.
exit /b !ERRORLEVEL!
)
call :PrintFinalInstructions ""
endlocal
exit /b 0
)
echo WARNING: Standalone install failed. Retry with --method npm to use npm, or --method standalone to debug the standalone failure.
exit /b !STANDALONE_STATUS!
:usage
call :PrintUsage
exit /b 0
:usage_error
call :PrintUsage
exit /b 1
:PrintUsage
echo Qwen Code Installer
echo.
echo Usage: install-qwen-with-source.bat [OPTIONS]
echo.
echo Options:
echo -s, --source SOURCE Record the installation source.
echo Only letters, numbers, dot, underscore, and dash are allowed.
echo --method METHOD Install method: detect, standalone, or npm.
echo --mirror MIRROR Standalone archive mirror: github or aliyun.
echo --base-url URL Override standalone archive base URL.
echo --archive PATH Install from a local standalone archive.
echo --version VERSION Standalone release version. Defaults to latest.
echo --registry REGISTRY npm registry to use.
echo Defaults to QWEN_NPM_REGISTRY or https://registry.npmmirror.com
echo -h, --help Show this help message.
exit /b 0
:ValidateOptions
if "!METHOD!"=="" set "METHOD=detect"
set "QWEN_VALIDATE_METHOD=!METHOD!"
set "QWEN_VALIDATE_MIRROR=!MIRROR!"
set "QWEN_VALIDATE_BASE_URL=!BASE_URL!"
set "QWEN_VALIDATE_ARCHIVE_PATH=!ARCHIVE_PATH!"
set "QWEN_VALIDATE_VERSION=!VERSION!"
set "QWEN_VALIDATE_NPM_REGISTRY=!NPM_REGISTRY!"
set "QWEN_VALIDATE_INSTALL_BASE=!INSTALL_BASE!"
set "QWEN_VALIDATE_INSTALL_DIR=!INSTALL_DIR!"
set "QWEN_VALIDATE_INSTALL_BIN_DIR=!INSTALL_BIN_DIR!"
set "QWEN_VALIDATE_SOURCE=!SOURCE!"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); foreach ($name in 'METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE') { $value = [Environment]::GetEnvironmentVariable('QWEN_VALIDATE_' + $name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }"
set "PS_STATUS=%ERRORLEVEL%"
set "QWEN_VALIDATE_METHOD="
set "QWEN_VALIDATE_MIRROR="
set "QWEN_VALIDATE_BASE_URL="
set "QWEN_VALIDATE_ARCHIVE_PATH="
set "QWEN_VALIDATE_VERSION="
set "QWEN_VALIDATE_NPM_REGISTRY="
set "QWEN_VALIDATE_INSTALL_BASE="
set "QWEN_VALIDATE_INSTALL_DIR="
set "QWEN_VALIDATE_INSTALL_BIN_DIR="
set "QWEN_VALIDATE_SOURCE="
if %PS_STATUS% NEQ 0 (
echo ERROR: installer options contain unsafe command characters.
exit /b 1
)
if "!INSTALL_BASE!"=="" (
echo ERROR: QWEN_INSTALL_ROOT must not be empty.
exit /b 1
)
if "!INSTALL_DIR!"=="" (
echo ERROR: QWEN_INSTALL_LIB_DIR must not be empty.
exit /b 1
)
if "!INSTALL_BIN_DIR!"=="" (
echo ERROR: QWEN_INSTALL_BIN_DIR must not be empty.
exit /b 1
)
if "!INSTALL_BASE:~1,2!"==":\" goto validate_install_base_ok
if "!INSTALL_BASE:~1,2!"==":/" goto validate_install_base_ok
if "!INSTALL_BASE:~0,2!"=="\\" goto validate_install_base_ok
echo ERROR: QWEN_INSTALL_ROOT must be an absolute path.
exit /b 1
:validate_install_base_ok
if "!INSTALL_DIR:~1,2!"==":\" goto validate_install_dir_ok
if "!INSTALL_DIR:~1,2!"==":/" goto validate_install_dir_ok
if "!INSTALL_DIR:~0,2!"=="\\" goto validate_install_dir_ok
echo ERROR: QWEN_INSTALL_LIB_DIR must be an absolute path.
exit /b 1
:validate_install_dir_ok
if "!INSTALL_BIN_DIR:~1,2!"==":\" goto validate_install_bin_dir_ok
if "!INSTALL_BIN_DIR:~1,2!"==":/" goto validate_install_bin_dir_ok
if "!INSTALL_BIN_DIR:~0,2!"=="\\" goto validate_install_bin_dir_ok
echo ERROR: QWEN_INSTALL_BIN_DIR must be an absolute path.
exit /b 1
:validate_install_bin_dir_ok
if /i "!METHOD!"=="detect" goto validate_method_ok
if /i "!METHOD!"=="standalone" goto validate_method_ok
if /i "!METHOD!"=="npm" goto validate_method_ok
echo ERROR: --method must be detect, standalone, or npm.
exit /b 1
:validate_method_ok
if /i "!MIRROR!"=="github" goto validate_mirror_ok
if /i "!MIRROR!"=="aliyun" goto validate_mirror_ok
echo ERROR: --mirror must be github or aliyun.
exit /b 1
:validate_mirror_ok
call :ValidateHttpsUrlVar "BASE_URL" "--base-url"
if %ERRORLEVEL% NEQ 0 exit /b 1
call :ValidateHttpsUrlVar "NPM_REGISTRY" "--registry"
if %ERRORLEVEL% NEQ 0 exit /b 1
call :ValidateVersion
if %ERRORLEVEL% NEQ 0 exit /b 1
call :ValidateSource
exit /b %ERRORLEVEL%
:ValidateHttpsUrlVar
set "URL_VALUE=!%~1!"
set "URL_OPTION=%~2"
if "!URL_VALUE!"=="" exit /b 0
if /i "!URL_VALUE:~0,8!"=="https://" exit /b 0
echo ERROR: !URL_OPTION! must start with https://
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 ERROR: --version must be 'latest' or a semver string.
exit /b 1
:ValidateSource
if "!SOURCE!"=="unknown" exit /b 0
echo(!SOURCE!| findstr /R /C:"^[A-Za-z0-9._-][A-Za-z0-9._-]*$" >nul
if %ERRORLEVEL% EQU 0 exit /b 0
echo ERROR: --source may only contain letters, numbers, dot, underscore, or dash.
exit /b 1
:DetectTarget
set "TARGET="
if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" set "TARGET=win-x64"
if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" set "TARGET=win-x64"
if "!TARGET!"=="" (
echo WARNING: Standalone archive is not available for this Windows architecture.
exit /b 1
)
exit /b 0
:ReleaseVersionPath
if /i "!VERSION!"=="latest" (
set "VERSION_PATH=latest"
exit /b 0
)
set "VERSION_PATH=!VERSION!"
if /i "!VERSION_PATH:~0,1!"=="v" exit /b 0
set "VERSION_PATH=v!VERSION_PATH!"
exit /b 0
:StandaloneBaseUrl
if not "!BASE_URL!"=="" (
set "STANDALONE_BASE_URL=!BASE_URL!"
exit /b 0
)
call :ReleaseVersionPath
if /i "!MIRROR!"=="aliyun" (
set "STANDALONE_BASE_URL=https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/!VERSION_PATH!"
exit /b 0
)
if /i "!VERSION_PATH!"=="latest" (
set "STANDALONE_BASE_URL=https://github.com/QwenLM/qwen-code/releases/latest/download"
exit /b 0
)
set "STANDALONE_BASE_URL=https://github.com/QwenLM/qwen-code/releases/download/!VERSION_PATH!"
exit /b 0
:UrlExists
set "QWEN_CHECK_URL=%~1"
powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $request = [Net.WebRequest]::Create($env:QWEN_CHECK_URL); $request.Method = 'HEAD'; try { $response = $request.GetResponse(); $response.Close(); exit 0 } catch { exit 1 }" >nul 2>&1
set "PS_STATUS=%ERRORLEVEL%"
set "QWEN_CHECK_URL="
exit /b %PS_STATUS%
:DownloadFile
set "QWEN_DOWNLOAD_URL=%~1"
set "QWEN_DOWNLOAD_DEST=%~2"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $client = New-Object Net.WebClient; $client.DownloadFile($env:QWEN_DOWNLOAD_URL, $env:QWEN_DOWNLOAD_DEST); exit 0 } catch { exit 1 }"
set "PS_STATUS=%ERRORLEVEL%"
set "QWEN_DOWNLOAD_URL="
set "QWEN_DOWNLOAD_DEST="
exit /b %PS_STATUS%
:VerifyChecksum
set "ARCHIVE_FILE=%~1"
set "CHECKSUM_SOURCE=%~2"
set "ARCHIVE_NAME=%~3"
set "CHECKSUM_FILE=!CHECKSUM_SOURCE!"
set "TEMP_CHECKSUM="
set "REQUIRE_CHECKSUM=1"
if "!CHECKSUM_FILE!"=="" (
for %%I in ("!ARCHIVE_FILE!") do set "CHECKSUM_FILE=%%~dpISHA256SUMS"
) else (
if /i "!CHECKSUM_FILE:~0,8!"=="https://" (
set "REQUIRE_CHECKSUM=1"
set "TEMP_CHECKSUM=%TEMP%\qwen-code-checksums-%RANDOM%%RANDOM%.txt"
call :DownloadFile "!CHECKSUM_FILE!" "!TEMP_CHECKSUM!"
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_CHECKSUM!" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1
echo ERROR: Could not download SHA256SUMS for checksum verification.
exit /b 1
)
set "CHECKSUM_FILE=!TEMP_CHECKSUM!"
)
)
if not exist "!CHECKSUM_FILE!" (
if "!REQUIRE_CHECKSUM!"=="1" (
echo ERROR: SHA256SUMS not found; cannot verify archive.
exit /b 1
)
echo WARNING: SHA256SUMS not found; skipping checksum verification.
exit /b 0
)
set "EXPECTED_HASH="
for /f "usebackq tokens=1,2" %%H in ("!CHECKSUM_FILE!") do (
set "CHECKSUM_HASH=%%H"
set "CHECKSUM_NAME=%%I"
if "!CHECKSUM_NAME:~0,1!"=="*" set "CHECKSUM_NAME=!CHECKSUM_NAME:~1!"
if "!CHECKSUM_NAME!"=="!ARCHIVE_NAME!" (
if "!EXPECTED_HASH!"=="" set "EXPECTED_HASH=!CHECKSUM_HASH!"
)
)
if "!EXPECTED_HASH!"=="" (
if not "!TEMP_CHECKSUM!"=="" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1
if "!REQUIRE_CHECKSUM!"=="1" (
echo ERROR: Checksum entry for !ARCHIVE_NAME! not found.
exit /b 1
)
echo WARNING: Checksum entry for !ARCHIVE_NAME! not found; skipping checksum verification.
exit /b 0
)
set "ACTUAL_HASH="
set "QWEN_HASH_FILE=!ARCHIVE_FILE!"
for /f "delims=" %%H in ('powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; (Get-FileHash -Algorithm SHA256 -LiteralPath $env:QWEN_HASH_FILE).Hash" 2^>nul') do (
if "!ACTUAL_HASH!"=="" set "ACTUAL_HASH=%%H"
)
set "QWEN_HASH_FILE="
if not "!TEMP_CHECKSUM!"=="" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1
if "!ACTUAL_HASH!"=="" (
if "!REQUIRE_CHECKSUM!"=="1" (
echo ERROR: Could not calculate SHA-256 checksum for archive.
exit /b 1
)
echo WARNING: Could not calculate SHA-256 checksum; skipping checksum verification.
exit /b 0
)
if /i not "!EXPECTED_HASH!"=="!ACTUAL_HASH!" (
echo ERROR: Checksum verification failed for !ARCHIVE_NAME!.
exit /b 1
)
echo SUCCESS: Checksum verified for !ARCHIVE_NAME!.
exit /b 0
:InstallStandalone
set "TEMP_DIR="
set "CHECKSUM_SOURCE="
REM Resolve the archive from a local file or from the configured release mirror.
if not "!ARCHIVE_PATH!"=="" (
set "ARCHIVE_FILE=!ARCHIVE_PATH!"
for %%I in ("!ARCHIVE_FILE!") do set "ARCHIVE_NAME=%%~nxI"
if not exist "!ARCHIVE_FILE!" (
echo ERROR: Standalone archive not found: !ARCHIVE_FILE!
exit /b 1
)
) else (
call :DetectTarget
if !ERRORLEVEL! NEQ 0 exit /b 2
set "ARCHIVE_NAME=qwen-code-win-x64.zip"
call :StandaloneBaseUrl
set "ARCHIVE_URL=!STANDALONE_BASE_URL!/!ARCHIVE_NAME!"
set "CHECKSUM_SOURCE=!STANDALONE_BASE_URL!/SHA256SUMS"
if /i "!METHOD!"=="detect" (
call :UrlExists "!ARCHIVE_URL!"
if !ERRORLEVEL! NEQ 0 (
echo WARNING: Standalone archive not found: !ARCHIVE_NAME!
exit /b 2
)
)
set "TEMP_DIR=%TEMP%\qwen-code-install-%RANDOM%%RANDOM%"
mkdir "!TEMP_DIR!" >nul 2>&1
set "ARCHIVE_FILE=!TEMP_DIR!\!ARCHIVE_NAME!"
echo INFO: Downloading !ARCHIVE_URL!
call :DownloadFile "!ARCHIVE_URL!" "!ARCHIVE_FILE!"
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo WARNING: Failed to download standalone archive.
exit /b 2
)
)
if "!TEMP_DIR!"=="" (
set "TEMP_DIR=%TEMP%\qwen-code-install-%RANDOM%%RANDOM%"
mkdir "!TEMP_DIR!" >nul 2>&1
)
REM Verify integrity before extraction or changing the install directory.
call :VerifyChecksum "!ARCHIVE_FILE!" "!CHECKSUM_SOURCE!" "!ARCHIVE_NAME!"
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
exit /b 1
)
REM Extract into a temporary directory, then validate required entry points.
set "EXTRACT_DIR=!TEMP_DIR!\extract"
mkdir "!EXTRACT_DIR!" >nul 2>&1
call :ValidateArchiveContents "!ARCHIVE_FILE!"
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
exit /b 1
)
set "QWEN_ARCHIVE_FILE=!ARCHIVE_FILE!"
set "QWEN_EXTRACT_DIR=!EXTRACT_DIR!"
powershell -NoProfile -ExecutionPolicy Bypass -Command "Expand-Archive -LiteralPath $env:QWEN_ARCHIVE_FILE -DestinationPath $env:QWEN_EXTRACT_DIR -Force"
set "PS_STATUS=!ERRORLEVEL!"
set "QWEN_ARCHIVE_FILE="
set "QWEN_EXTRACT_DIR="
if !PS_STATUS! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Failed to extract standalone archive.
exit /b 1
)
call :RejectArchiveLinks "!EXTRACT_DIR!"
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
exit /b 1
)
if not exist "!EXTRACT_DIR!\qwen-code\bin\qwen.cmd" (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Archive does not contain qwen-code\bin\qwen.cmd.
exit /b 1
)
if not exist "!EXTRACT_DIR!\qwen-code\node\node.exe" (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Archive does not contain qwen-code\node\node.exe.
exit /b 1
)
if not exist "!INSTALL_BASE!" mkdir "!INSTALL_BASE!"
if not exist "!INSTALL_BIN_DIR!" mkdir "!INSTALL_BIN_DIR!"
for %%I in ("!INSTALL_DIR!") do set "INSTALL_PARENT=%%~dpI"
if not exist "!INSTALL_PARENT!" mkdir "!INSTALL_PARENT!"
REM Stage into .new and keep .old so failed upgrades can roll back.
set "NEW_INSTALL_DIR=!INSTALL_DIR!.new"
set "OLD_INSTALL_DIR=!INSTALL_DIR!.old"
call :EnsureManagedInstallDir "!INSTALL_DIR!"
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
exit /b 1
)
call :EnsureManagedInstallDir "!NEW_INSTALL_DIR!"
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
exit /b 1
)
call :EnsureManagedInstallDir "!OLD_INSTALL_DIR!"
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
exit /b 1
)
if exist "!NEW_INSTALL_DIR!" rmdir /S /Q "!NEW_INSTALL_DIR!" >nul 2>&1
if exist "!OLD_INSTALL_DIR!" rmdir /S /Q "!OLD_INSTALL_DIR!" >nul 2>&1
move /Y "!EXTRACT_DIR!\qwen-code" "!NEW_INSTALL_DIR!" >nul
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Failed to stage standalone archive.
exit /b 1
)
if exist "!INSTALL_DIR!" (
move /Y "!INSTALL_DIR!" "!OLD_INSTALL_DIR!" >nul
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Failed to back up existing install at !INSTALL_DIR!.
exit /b 1
)
)
move /Y "!NEW_INSTALL_DIR!" "!INSTALL_DIR!" >nul
if !ERRORLEVEL! NEQ 0 (
if exist "!OLD_INSTALL_DIR!" move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Failed to install standalone archive to !INSTALL_DIR!.
exit /b 1
)
(
echo @echo off
echo call "!INSTALL_DIR!\bin\qwen.cmd" %%*
) > "!INSTALL_BIN_DIR!\qwen.cmd.new"
if !ERRORLEVEL! NEQ 0 (
if exist "!INSTALL_DIR!" rmdir /S /Q "!INSTALL_DIR!" >nul 2>&1
if exist "!OLD_INSTALL_DIR!" move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Failed to create qwen wrapper in !INSTALL_BIN_DIR!.
exit /b 1
)
move /Y "!INSTALL_BIN_DIR!\qwen.cmd.new" "!INSTALL_BIN_DIR!\qwen.cmd" >nul
if !ERRORLEVEL! NEQ 0 (
if exist "!INSTALL_BIN_DIR!\qwen.cmd.new" del /F /Q "!INSTALL_BIN_DIR!\qwen.cmd.new" >nul 2>&1
if exist "!INSTALL_DIR!" rmdir /S /Q "!INSTALL_DIR!" >nul 2>&1
if exist "!OLD_INSTALL_DIR!" move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Failed to create qwen wrapper in !INSTALL_BIN_DIR!.
exit /b 1
)
if exist "!OLD_INSTALL_DIR!" rmdir /S /Q "!OLD_INSTALL_DIR!" >nul 2>&1
set "PATH=!INSTALL_BIN_DIR!;!PATH!"
call :CreateSourceJson
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo SUCCESS: Qwen Code standalone archive installed successfully.
echo INFO: Installed to !INSTALL_DIR!
exit /b 0
:ValidateArchiveContents
set "QWEN_ARCHIVE_FILE=%~1"
REM Enumerate archive entries and reject any with path traversal indicators:
REM empty names, leading '/', drive-rooted paths, '..' segments, or control chars.
REM This prevents Zip Slip attacks before extraction.
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
if %PS_STATUS% EQU 1 (
echo ERROR: Archive contains unsafe path entries.
exit /b 1
)
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: %~1
exit /b 1
)
echo ERROR: Archive validation failed before extraction.
exit /b %PS_STATUS%
:RejectArchiveLinks
set "QWEN_EXTRACT_DIR=%~1"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$item = Get-ChildItem -LiteralPath $env:QWEN_EXTRACT_DIR -Recurse -Force | Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 } | Select-Object -First 1; if ($item) { exit 1 }"
set "PS_STATUS=%ERRORLEVEL%"
set "QWEN_EXTRACT_DIR="
if %PS_STATUS% NEQ 0 echo ERROR: Archive contains symlinks or reparse points; refusing to install.
exit /b %PS_STATUS%
:EnsureManagedInstallDir
set "MANAGED_DIR=%~1"
if not exist "!MANAGED_DIR!" exit /b 0
if exist "!MANAGED_DIR!\manifest.json" exit /b 0
echo ERROR: !MANAGED_DIR! exists but is not a Qwen Code standalone install.
echo ERROR: Refusing to overwrite it. Move or remove it manually, then rerun the installer.
exit /b 1
:RequireNode
where node >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
echo ERROR: Node.js was not found.
echo.
echo Node.js 20 or newer is required before installing Qwen Code with npm.
echo Please install Node.js from https://nodejs.org/ and rerun this installer.
exit /b 1
)
for /f "delims=" %%i in ('node -p "process.versions.node" 2^>nul') do set "NODE_VERSION=%%i"
if "%NODE_VERSION%"=="" (
echo ERROR: Unable to determine Node.js version.
echo Node.js 20 or newer is required before installing Qwen Code with npm.
exit /b 1
)
for /f "tokens=1 delims=." %%a in ("%NODE_VERSION%") do set "MAJOR_VERSION=%%a"
set /a NODE_MAJOR_NUM=%MAJOR_VERSION% >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
echo ERROR: Unable to determine Node.js version.
echo Node.js 20 or newer is required before installing Qwen Code with npm.
exit /b 1
)
if %NODE_MAJOR_NUM% LSS 20 (
echo ERROR: Node.js %NODE_VERSION% is installed, but Node.js 20 or newer is required.
echo Please install Node.js from https://nodejs.org/ and rerun this installer.
exit /b 1
)
echo SUCCESS: Node.js %NODE_VERSION% detected.
exit /b 0
:RequireNpm
where npm >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
echo ERROR: npm was not found.
echo Please install Node.js with npm included, then rerun this installer.
exit /b 1
)
for /f "delims=" %%i in ('npm -v 2^>nul') do set "NPM_VERSION=%%i"
echo SUCCESS: npm %NPM_VERSION% detected.
exit /b 0
:InstallNpm
call :RequireNode
if %ERRORLEVEL% NEQ 0 exit /b 1
call :RequireNpm
if %ERRORLEVEL% NEQ 0 exit /b 1
where qwen >nul 2>&1
if %ERRORLEVEL% EQU 0 (
for /f "delims=" %%i in ('qwen --version 2^>nul') do set "QWEN_VERSION=%%i"
echo INFO: Existing Qwen Code detected: !QWEN_VERSION!
echo INFO: Upgrading to the latest version.
)
echo INFO: Running: npm install -g @qwen-code/qwen-code@latest --registry !NPM_REGISTRY!
call npm install -g @qwen-code/qwen-code@latest --registry "!NPM_REGISTRY!"
if %ERRORLEVEL% NEQ 0 (
echo ERROR: Failed to install Qwen Code.
echo.
echo This installer does not change your npm prefix or PATH.
echo If the failure is a permission error, fix your npm global package directory, then run:
echo npm install -g @qwen-code/qwen-code@latest --registry !NPM_REGISTRY!
exit /b 1
)
echo SUCCESS: Qwen Code installed successfully.
call :CreateSourceJson
exit /b 0
:CreateSourceJson
if "!SOURCE!"=="unknown" exit /b 0
set "QWEN_DIR=!USERPROFILE!\.qwen"
if not exist "!QWEN_DIR!" mkdir "!QWEN_DIR!"
(
echo {
echo "source": "!SOURCE!"
echo }
) > "!QWEN_DIR!\source.json"
echo SUCCESS: Installation source saved to !USERPROFILE!\.qwen\source.json
exit /b 0
:PrintFinalInstructions
set "EXTRA_BIN=%~1"
if not "!EXTRA_BIN!"=="" set "PATH=!EXTRA_BIN!;!PATH!"
echo.
echo ===========================================
echo Installation completed!
echo ===========================================
echo.
where qwen >nul 2>&1
if %ERRORLEVEL% EQU 0 (
for /f "delims=" %%i in ('qwen --version 2^>nul') do set "QWEN_VERSION=%%i"
echo SUCCESS: Qwen Code is ready to use: !QWEN_VERSION!
echo.
echo You can now run: qwen
echo.
echo INFO: Run qwen in your project directory to start an interactive session.
exit /b 0
)
echo WARNING: Qwen Code was installed, but qwen is not on PATH in this prompt.
echo.
echo Restart your command prompt, then run: qwen
if not "!EXTRA_BIN!"=="" (
echo.
echo Or add this directory to PATH:
echo !EXTRA_BIN!
echo Then run:
echo qwen
exit /b 0
)
for /f "delims=" %%i in ('npm prefix -g 2^>nul') do set "NPM_PREFIX=%%i"
if not "!NPM_PREFIX!"=="" (
echo.
echo Or add this npm global directory to PATH:
echo !NPM_PREFIX!
echo Then run:
echo qwen
)
exit /b 0