diff --git a/scripts/build-hosted-installation-assets.js b/scripts/build-hosted-installation-assets.js index 732e2f5548..881e85e7b8 100644 --- a/scripts/build-hosted-installation-assets.js +++ b/scripts/build-hosted-installation-assets.js @@ -71,6 +71,22 @@ const HOSTED_INSTALLER_BEHAVIOR_PATTERNS = { name: '--version parser', pattern: /ARG_KEY!"=="--version"|"%~1"=="--version"/, }, + { + name: '--repair-path parser', + pattern: /if \/i "%~1"=="--repair-path"/, + }, + { + name: '--path-scope parser', + pattern: /if \/i "!ARG_KEY!"=="--path-scope"/, + }, + { + name: 'QWEN_INSTALL_REPAIR_PATH', + pattern: /QWEN_INSTALL_REPAIR_PATH/, + }, + { + name: 'QWEN_INSTALL_PATH_SCOPE', + pattern: /QWEN_INSTALL_PATH_SCOPE/, + }, ], 'install-qwen-standalone.ps1': [ { @@ -111,7 +127,7 @@ const HOSTED_INSTALLER_BEHAVIOR_PATTERNS = { }, { name: 'PATH cleanup', - pattern: /Remove-UserPathEntry/, + pattern: /Remove-PathEntryFromAllScopes/, }, { name: 'current cmd shim cleanup', diff --git a/scripts/installation/INSTALLATION_GUIDE.md b/scripts/installation/INSTALLATION_GUIDE.md index b92075e49b..f7ff9a1a70 100644 --- a/scripts/installation/INSTALLATION_GUIDE.md +++ b/scripts/installation/INSTALLATION_GUIDE.md @@ -11,7 +11,7 @@ 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. + or user/machine 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. @@ -133,6 +133,14 @@ install-qwen-standalone.bat --method standalone install-qwen-standalone.bat --method npm ``` +Repair PATH for an existing standalone Windows install without downloading or +reinstalling Qwen Code: + +```bat +install-qwen-standalone.bat --repair-path +install-qwen-standalone.bat --repair-path --path-scope machine +``` + ## Optional Native Modules The standalone archives bundle Qwen Code and a private Node.js runtime. They do @@ -211,6 +219,18 @@ Standalone installs to: Override with `QWEN_INSTALL_ROOT`, `QWEN_INSTALL_LIB_DIR`, or `QWEN_INSTALL_BIN_DIR` when needed. +For self-hosted Windows runners, install under the runner account once, then +repair machine-level PATH without reinstalling: + +```powershell +$env:QWEN_INSTALL_REPAIR_PATH = '1' +$env:QWEN_INSTALL_PATH_SCOPE = 'machine' +irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex +``` + +Restart the runner service or machine after updating machine-level PATH so jobs +inherit the refreshed environment. + Restart the terminal if `qwen` is not immediately available on PATH. Uninstall a standalone Windows install: @@ -220,10 +240,11 @@ powershell -ExecutionPolicy Bypass -c "irm https://qwen-code-assets.oss-cn-hangz ``` The uninstaller removes only the standalone runtime, generated `qwen.cmd` -wrapper, user PATH entry, and the current-session `cmd.exe` shim created by the -hosted PowerShell installer. It preserves `%USERPROFILE%\.qwen` by default. Set -`QWEN_UNINSTALL_PURGE=1` to remove `%USERPROFILE%\.qwen\source.json`; other -config and auth files are still preserved. +wrapper, user or machine PATH entry, and the current-session `cmd.exe` shim +created by the hosted PowerShell installer. It preserves `%USERPROFILE%\.qwen` +by default. Set `QWEN_UNINSTALL_PURGE=1` to remove +`%USERPROFILE%\.qwen\source.json`; other config and auth files are still +preserved. ## Mirrors and Overrides @@ -236,6 +257,8 @@ Options: - `--version VERSION` - `--registry REGISTRY` - `--source SOURCE` +- `--repair-path` +- `--path-scope user|machine` Environment variables: @@ -245,6 +268,8 @@ Environment variables: - `QWEN_INSTALL_ARCHIVE` - `QWEN_INSTALL_VERSION` - `QWEN_NPM_REGISTRY` +- `QWEN_INSTALL_REPAIR_PATH` +- `QWEN_INSTALL_PATH_SCOPE` Use `--base-url` for private mirrors. The URL must contain `qwen-code-` archives and `SHA256SUMS` in the same directory. Custom diff --git a/scripts/installation/install-qwen-standalone.bat b/scripts/installation/install-qwen-standalone.bat index 875b59c5ab..e698c23119 100644 --- a/scripts/installation/install-qwen-standalone.bat +++ b/scripts/installation/install-qwen-standalone.bat @@ -15,6 +15,16 @@ 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 "REPAIR_PATH=0" +if defined QWEN_INSTALL_REPAIR_PATH set "REPAIR_PATH=!QWEN_INSTALL_REPAIR_PATH!" +set "PATH_SCOPE=user" +REM Auto-detect SYSTEM account and default to machine scope so that new sessions +REM spawned by service processes (SSM agent, WinRM, scheduled tasks) inherit PATH. +if not defined QWEN_INSTALL_PATH_SCOPE ( + whoami /user 2>nul | findstr /i "S-1-5-18" >nul 2>&1 + if !ERRORLEVEL! EQU 0 set "PATH_SCOPE=machine" +) +if defined QWEN_INSTALL_PATH_SCOPE set "PATH_SCOPE=!QWEN_INSTALL_PATH_SCOPE!" set "BASE_URL=" if defined QWEN_INSTALL_BASE_URL set "BASE_URL=!QWEN_INSTALL_BASE_URL!" set "ARCHIVE_PATH=" @@ -194,6 +204,30 @@ if /i "%~1"=="--no-modify-path" ( shift goto parse_args ) +if /i "%~1"=="--repair-path" ( + set "REPAIR_PATH=1" + shift + goto parse_args +) +if /i "!ARG_KEY!"=="--path-scope" ( + if "!ARG_HAS_INLINE_VALUE!"=="1" ( + if "!ARG_VALUE!"=="" ( + echo ERROR: --path-scope requires a value + exit /b 1 + ) + set "PATH_SCOPE=!ARG_VALUE!" + shift + goto parse_args + ) + if "%~2"=="" ( + echo ERROR: --path-scope requires a value + exit /b 1 + ) + set "PATH_SCOPE=%~2" + shift + shift + goto parse_args +) if /i "%~1"=="-h" goto usage if /i "%~1"=="--help" goto usage @@ -206,6 +240,13 @@ goto usage_error call :ValidateOptions if %ERRORLEVEL% NEQ 0 exit /b 1 +if /i "!REPAIR_PATH!"=="1" ( + call :RepairPath + if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL! + endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%" + exit /b 0 +) + call :PrintHeader REM Discover all qwen executables on disk BEFORE we install. We can't @@ -302,7 +343,9 @@ echo --base-url URL Override standalone archive base URL echo --archive PATH Install from a local standalone archive echo --version VERSION Release version (default: latest) echo --registry URL npm registry (default: https://registry.npmmirror.com) -echo --no-modify-path Do not modify user PATH +echo --no-modify-path Do not modify PATH +echo --repair-path Repair PATH for an existing standalone install +echo --path-scope SCOPE PATH scope: user or machine (default: user, auto machine for SYSTEM) echo -s, --source SOURCE Record installation source echo -h, --help Show this help message exit /b 0 @@ -329,7 +372,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "$esc = [char]27; $bar = exit /b 0 :ValidateRawEnvironmentOptions -powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); $rawNames = @('QWEN_INSTALL_METHOD','QWEN_INSTALL_MIRROR','QWEN_NO_MODIFY_PATH','QWEN_INSTALL_BASE_URL','QWEN_INSTALL_ARCHIVE','QWEN_INSTALL_VERSION','QWEN_NPM_REGISTRY','QWEN_INSTALL_ROOT','QWEN_INSTALL_LIB_DIR','QWEN_INSTALL_BIN_DIR','QWEN_INSTALL_GITHUB_REPO','QWEN_INSTALL_CURL_EXE'); foreach ($name in $rawNames) { $value = [Environment]::GetEnvironmentVariable($name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }; exit 0" +powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); $rawNames = @('QWEN_INSTALL_METHOD','QWEN_INSTALL_MIRROR','QWEN_NO_MODIFY_PATH','QWEN_INSTALL_REPAIR_PATH','QWEN_INSTALL_PATH_SCOPE','QWEN_INSTALL_BASE_URL','QWEN_INSTALL_ARCHIVE','QWEN_INSTALL_VERSION','QWEN_NPM_REGISTRY','QWEN_INSTALL_ROOT','QWEN_INSTALL_LIB_DIR','QWEN_INSTALL_BIN_DIR','QWEN_INSTALL_GITHUB_REPO','QWEN_INSTALL_CURL_EXE'); foreach ($name in $rawNames) { $value = [Environment]::GetEnvironmentVariable($name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }; exit 0" if %ERRORLEVEL% EQU 0 exit /b 0 echo ERROR: installer options contain unsafe command characters. exit /b 1 @@ -347,11 +390,12 @@ 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!" +set "QWEN_VALIDATE_PATH_SCOPE=!PATH_SCOPE!" call :CreateTempFile "qwen-validate-options" ".ps1" if !ERRORLEVEL! NEQ 0 exit /b 1 set "QWEN_VALIDATE_OPTIONS_SCRIPT=!TEMP_FILE!" > "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124) ->> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $names = @('METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE') +>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $names = @('METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE','PATH_SCOPE') >> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo foreach ($name in $names) { >> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $value = [Environment]::GetEnvironmentVariable('QWEN_VALIDATE_' + $name) >> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } @@ -371,6 +415,7 @@ set "QWEN_VALIDATE_INSTALL_BASE=" set "QWEN_VALIDATE_INSTALL_DIR=" set "QWEN_VALIDATE_INSTALL_BIN_DIR=" set "QWEN_VALIDATE_SOURCE=" +set "QWEN_VALIDATE_PATH_SCOPE=" if %PS_STATUS% NEQ 0 ( echo ERROR: installer options contain unsafe command characters. exit /b 1 @@ -421,6 +466,18 @@ echo ERROR: --mirror must be auto, github, or aliyun. exit /b 1 :validate_mirror_ok +if "!REPAIR_PATH!"=="0" goto validate_repair_path_ok +if "!REPAIR_PATH!"=="1" goto validate_repair_path_ok +echo ERROR: QWEN_INSTALL_REPAIR_PATH must be 0 or 1. +exit /b 1 + +:validate_repair_path_ok +if /i "!PATH_SCOPE!"=="user" goto validate_path_scope_ok +if /i "!PATH_SCOPE!"=="machine" goto validate_path_scope_ok +echo ERROR: --path-scope must be user or machine. +exit /b 1 + +:validate_path_scope_ok call :ValidateHttpsUrlVar "BASE_URL" "--base-url" if %ERRORLEVEL% NEQ 0 exit /b 1 @@ -592,17 +649,19 @@ set "GITHUB_FALLBACK_BASE_URL=" set "MIRROR=github" exit /b 0 -:MaybeUpdateUserPath +:MaybeUpdatePath rem args: %~1=install_bin_dir -rem Prepend the install dir to the user-level PATH (HKCU\Environment) via +rem Prepend the install dir to the selected PATH scope via rem [Environment]::SetEnvironmentVariable. Idempotent: skips if the dir is -rem already on the user PATH. Uses PowerShell rather than `setx` because setx +rem already on PATH. Uses PowerShell rather than `setx` because setx rem truncates PATH at 1024 chars, which can silently mangle long PATHs. set "QWEN_NEW_BIN=%~1" if "!QWEN_NEW_BIN!"=="" exit /b 0 -powershell -NoProfile -ExecutionPolicy Bypass -Command "$bin = $env:QWEN_NEW_BIN; $userPath = [Environment]::GetEnvironmentVariable('Path', 'User'); if ([string]::IsNullOrEmpty($userPath)) { $userPath = '' }; $entries = @($userPath -split ';' | Where-Object { $_ -ne '' }); $remaining = @($entries | Where-Object { $_ -ne $bin }); if ($entries.Count -gt 0 -and $entries[0] -eq $bin -and $remaining.Count -eq ($entries.Count - 1)) { exit 0 }; $newPath = (@($bin) + $remaining) -join ';'; [Environment]::SetEnvironmentVariable('Path', $newPath, 'User'); exit 0" +set "QWEN_PATH_SCOPE=!PATH_SCOPE!" +powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; try { $bin = $env:QWEN_NEW_BIN; $scope = $env:QWEN_PATH_SCOPE; $target = if ($scope -ieq 'machine') { 'Machine' } else { 'User' }; $pathValue = [Environment]::GetEnvironmentVariable('Path', $target); if ([string]::IsNullOrEmpty($pathValue)) { $pathValue = '' }; $entries = @($pathValue -split ';' | Where-Object { $_ -ne '' }); $remaining = @($entries | Where-Object { -not [string]::Equals($_, $bin, [StringComparison]::OrdinalIgnoreCase) }); if ($entries.Count -gt 0 -and [string]::Equals($entries[0], $bin, [StringComparison]::OrdinalIgnoreCase) -and $remaining.Count -eq ($entries.Count - 1)) { exit 0 }; $newPath = (@($bin) + $remaining) -join ';'; [Environment]::SetEnvironmentVariable('Path', $newPath, $target); exit 0 } catch { exit 1 }" set "PS_STATUS=%ERRORLEVEL%" set "QWEN_NEW_BIN=" +set "QWEN_PATH_SCOPE=" exit /b %PS_STATUS% :UrlExists @@ -1014,6 +1073,35 @@ if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 REM Standalone archive installed to !INSTALL_DIR! exit /b 0 +:RepairPath +set "INSTALLED_BIN=!INSTALL_BIN_DIR!\qwen.cmd" +if not exist "!INSTALLED_BIN!" ( + echo ERROR: Qwen Code standalone wrapper was not found: + echo !INSTALLED_BIN! + echo ERROR: Set QWEN_INSTALL_ROOT or QWEN_INSTALL_BIN_DIR to the existing install, or install Qwen Code first. + exit /b 1 +) + +set "PATH=!INSTALL_BIN_DIR!;!PATH!" +if /i not "!NO_MODIFY_PATH!"=="1" ( + call :MaybeUpdatePath "!INSTALL_BIN_DIR!" + if !ERRORLEVEL! NEQ 0 ( + echo ERROR: Failed to update !PATH_SCOPE! PATH. Run as administrator for --path-scope machine, or add this directory manually: + echo !INSTALL_BIN_DIR! + exit /b 1 + ) +) + +set "INSTALLED_VERSION=unknown" +for /f "delims=" %%i in ('"!INSTALLED_BIN!" --version 2^>nul') do set "INSTALLED_VERSION=%%i" + +echo. +echo Qwen Code !INSTALLED_VERSION! is already installed. +echo PATH repaired for !PATH_SCOPE! scope: +echo !INSTALL_BIN_DIR! +echo. +exit /b 0 + :CreateTempDir set "TEMP_DIR=" for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $dir = Join-Path $env:TEMP ('qwen-code-install-' + [IO.Path]::GetRandomFileName()); New-Item -ItemType Directory -Path $dir -ErrorAction Stop | Out-Null; [Console]::Write($dir)"`) do set "TEMP_DIR=%%I" @@ -1239,11 +1327,11 @@ if not "!INSTALLED_BIN!"=="" if exist "!INSTALLED_BIN!" ( for /f "delims=" %%i in ('"!INSTALLED_BIN!" --version 2^>nul') do set "INSTALLED_VERSION=%%i" ) -rem Persist the install bin to user PATH unless --no-modify-path is set. +rem Persist the install bin to PATH unless --no-modify-path is set. if not "!EXTRA_BIN!"=="" if /i not "!NO_MODIFY_PATH!"=="1" ( - call :MaybeUpdateUserPath "!EXTRA_BIN!" + call :MaybeUpdatePath "!EXTRA_BIN!" if !ERRORLEVEL! NEQ 0 ( - echo WARNING: Failed to update user PATH. Add the directory manually: + echo WARNING: Failed to update !PATH_SCOPE! PATH. Add the directory manually: echo !EXTRA_BIN! ) else ( set "PATH_UPDATE_APPLIED=1" diff --git a/scripts/installation/uninstall-qwen-standalone.ps1 b/scripts/installation/uninstall-qwen-standalone.ps1 index cee6854bef..f152226ff5 100644 --- a/scripts/installation/uninstall-qwen-standalone.ps1 +++ b/scripts/installation/uninstall-qwen-standalone.ps1 @@ -150,22 +150,25 @@ function Test-QwenStandaloneInstallDir { return $true } -function Remove-UserPathEntry { - param([string]$BinDir) +function Remove-PathEntry { + param( + [string]$BinDir, + [string]$Scope + ) $target = Get-NormalizedPath -PathValue $BinDir if ([string]::IsNullOrEmpty($target)) { return } - $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') - if ([string]::IsNullOrEmpty($userPath)) { + $pathValue = [Environment]::GetEnvironmentVariable('Path', $Scope) + if ([string]::IsNullOrEmpty($pathValue)) { return } $kept = New-Object System.Collections.Generic.List[string] $removed = $false - foreach ($entry in @($userPath -split ';')) { + foreach ($entry in @($pathValue -split ';')) { if ([string]::IsNullOrEmpty($entry)) { continue } @@ -179,9 +182,25 @@ function Remove-UserPathEntry { } if ($removed) { - [Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), 'User') - Write-Success "Removed $BinDir from user PATH." + try { + [Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), $Scope) + Write-Success "Removed $BinDir from $Scope PATH." + } catch { + Write-WarningMessage "Could not remove $BinDir from $Scope PATH: $($_.Exception.Message)" + } } +} + +function Remove-PathEntryFromAllScopes { + param([string]$BinDir) + + $target = Get-NormalizedPath -PathValue $BinDir + if ([string]::IsNullOrEmpty($target)) { + return + } + + Remove-PathEntry -BinDir $target -Scope 'User' + Remove-PathEntry -BinDir $target -Scope 'Machine' $current = New-Object System.Collections.Generic.List[string] foreach ($entry in @($env:Path -split ';')) { @@ -360,7 +379,7 @@ if ($installWasManaged) { Write-Info "Leaving $(Join-Path $installBinDir 'qwen.cmd') unchanged because no managed standalone runtime was removed." } -Remove-UserPathEntry -BinDir $installBinDir +Remove-PathEntryFromAllScopes -BinDir $installBinDir Remove-SourceMarker if ([string]::IsNullOrEmpty($env:QWEN_INSTALL_BIN_DIR)) { Remove-EmptyDirectory -Directory $installBinDir diff --git a/scripts/tests/install-script.test.js b/scripts/tests/install-script.test.js index 8d9ea31b3f..8659e29d74 100644 --- a/scripts/tests/install-script.test.js +++ b/scripts/tests/install-script.test.js @@ -346,7 +346,9 @@ describe('installation scripts', () => { expect(script).toContain('qwen-code\\node\\node.exe'); expect(script).toContain('Archive contains symlinks or reparse points'); expect(script).toContain('unsafe path with control character'); - expect(script).toContain('Failed to update user PATH'); + expect(script).toContain('Failed to update !PATH_SCOPE! PATH'); + expect(script).toContain("$ErrorActionPreference = 'Stop'; try"); + expect(script).toContain('catch { exit 1 }'); expect(script).toContain('PRE_INSTALL_QWENS_LIST'); expect(script).toContain('QWEN_INSTALL_ROOT'); expect(script).toContain('npm fallback also failed'); @@ -574,6 +576,35 @@ describe('release-script-utils', () => { }); }); +const STUB_BAT_CONTENT = + '@echo off\r\n' + + 'set "VERSION=%QWEN_INSTALL_VERSION%"\r\n' + + 'set "REPAIR_PATH=%QWEN_INSTALL_REPAIR_PATH%"\r\n' + + 'set "PATH_SCOPE=%QWEN_INSTALL_PATH_SCOPE%"\r\n' + + 'if "%VERSION%"=="" set "VERSION=latest"\r\n' + + 'set "VERSION=latest"\r\n' + + 'if "%~1"=="--version" set "VERSION=%~2"\r\n' + + 'if /i "%~1"=="--repair-path" set "REPAIR_PATH=1"\r\n' + + 'set "ARG_KEY=%~1"\r\n' + + 'if /i "!ARG_KEY!"=="--path-scope" set "PATH_SCOPE=%~2"\r\n'; + +const STUB_SH_CONTENT = + '#!/usr/bin/env bash\n' + + 'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' + + 'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n'; + +const STUB_UNINSTALL_SH_CONTENT = + '#!/usr/bin/env bash\n' + + 'is_qwen_standalone_install_dir() { return 0; }\n' + + 'remove_shell_path_entry() { :; }\n' + + 'QWEN_UNINSTALL_PURGE=""\n'; + +const STUB_UNINSTALL_PS1_CONTENT = + 'function Test-QwenStandaloneInstallDir { return $true }\n' + + 'function Remove-PathEntryFromAllScopes { }\n' + + 'function Remove-CurrentCmdPathShim { }\n' + + '$env:QWEN_UNINSTALL_PURGE = ""\n'; + describe('standalone release packaging', () => { it('defines a standalone packaging script', () => { const packageJson = JSON.parse(readScript('package.json')); @@ -1049,13 +1080,11 @@ describe('standalone release packaging', () => { mkdirSync(sourceDir, { recursive: true }); writeFileSync( path.join(sourceDir, 'install-qwen-standalone.sh'), - '#!/usr/bin/env bash\n' + - 'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' + - 'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n', + STUB_SH_CONTENT, ); writeFileSync( path.join(sourceDir, 'install-qwen-standalone.bat'), - '@echo off\r\nset "VERSION=%QWEN_INSTALL_VERSION%"\r\nif "%VERSION%"=="" set "VERSION=latest"\r\nset "VERSION=latest"\r\nif "%~1"=="--version" set "VERSION=%~2"\r\n', + STUB_BAT_CONTENT, ); // The ps1 shim has every required behavior pattern but also contains // a hardcoded $env:QWEN_INSTALL_VERSION assignment, which must be @@ -1070,17 +1099,11 @@ describe('standalone release packaging', () => { ); writeFileSync( path.join(sourceDir, 'uninstall-qwen-standalone.sh'), - '#!/usr/bin/env bash\n' + - 'is_qwen_standalone_install_dir() { return 0; }\n' + - 'remove_shell_path_entry() { :; }\n' + - 'QWEN_UNINSTALL_PURGE=""\n', + STUB_UNINSTALL_SH_CONTENT, ); writeFileSync( path.join(sourceDir, 'uninstall-qwen-standalone.ps1'), - 'function Test-QwenStandaloneInstallDir { return $true }\n' + - 'function Remove-UserPathEntry { }\n' + - 'function Remove-CurrentCmdPathShim { }\n' + - '$env:QWEN_UNINSTALL_PURGE = ""\n', + STUB_UNINSTALL_PS1_CONTENT, ); await expect( @@ -1106,13 +1129,11 @@ describe('standalone release packaging', () => { mkdirSync(sourceDir, { recursive: true }); writeFileSync( path.join(sourceDir, 'install-qwen-standalone.sh'), - '#!/usr/bin/env bash\n' + - 'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' + - 'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n', + STUB_SH_CONTENT, ); writeFileSync( path.join(sourceDir, 'install-qwen-standalone.bat'), - '@echo off\r\nset "VERSION=%QWEN_INSTALL_VERSION%"\r\nif "%VERSION%"=="" set "VERSION=latest"\r\nset "VERSION=latest"\r\nif "%~1"=="--version" set "VERSION=%~2"\r\n', + STUB_BAT_CONTENT, ); // ps1 contains the exact docstring shipped in production // ("$env:QWEN_INSTALL_VERSION = 'vX.Y.Z'") as a `#` comment; the @@ -1128,17 +1149,11 @@ describe('standalone release packaging', () => { ); writeFileSync( path.join(sourceDir, 'uninstall-qwen-standalone.sh'), - '#!/usr/bin/env bash\n' + - 'is_qwen_standalone_install_dir() { return 0; }\n' + - 'remove_shell_path_entry() { :; }\n' + - 'QWEN_UNINSTALL_PURGE=""\n', + STUB_UNINSTALL_SH_CONTENT, ); writeFileSync( path.join(sourceDir, 'uninstall-qwen-standalone.ps1'), - 'function Test-QwenStandaloneInstallDir { return $true }\n' + - 'function Remove-UserPathEntry { }\n' + - 'function Remove-CurrentCmdPathShim { }\n' + - '$env:QWEN_UNINSTALL_PURGE = ""\n', + STUB_UNINSTALL_PS1_CONTENT, ); // Build should succeed (only resolves; throws would fail the test). @@ -1804,7 +1819,7 @@ describe('standalone release packaging', () => { expect(releaseWorkflow).not.toContain('verify_node_checksum()'); expect(releaseWorkflow).not.toContain('download_node()'); const createReleaseStepIndex = releaseWorkflow.indexOf( - "name: 'Create GitHub Release and Tag'", + "- name: 'Create GitHub Release and Tag'", ); expect(createReleaseStepIndex).toBeGreaterThanOrEqual(0); const createReleaseStep = releaseWorkflow.slice(createReleaseStepIndex); @@ -1844,21 +1859,25 @@ describe('standalone release packaging', () => { ); const syncStepIndex = ossWorkflow.indexOf( - "name: 'Sync Release Assets to Aliyun OSS'", + "- name: 'Sync Release Assets to Aliyun OSS'", + ); + const packageHostedStepIndex = ossWorkflow.indexOf( + "- name: 'Package Hosted Installation Assets'", ); const verifyStepIndex = ossWorkflow.indexOf( - "name: 'Verify Aliyun OSS Release Assets'", + "- name: 'Verify Aliyun OSS Release Assets'", ); const publishLatestStepIndex = ossWorkflow.indexOf( - "name: 'Publish Aliyun OSS Latest VERSION'", + "- name: 'Publish Aliyun OSS Latest VERSION'", ); const syncHostedStepIndex = ossWorkflow.indexOf( - "name: 'Sync Hosted Installation Assets to Aliyun OSS'", + "- name: 'Sync Hosted Installation Assets to Aliyun OSS'", ); const verifyHostedStepIndex = ossWorkflow.indexOf( - "name: 'Verify Aliyun OSS Hosted Installation Assets'", + "- name: 'Verify Aliyun OSS Hosted Installation Assets'", ); expect(syncStepIndex).toBeGreaterThanOrEqual(0); + expect(packageHostedStepIndex).toBeGreaterThanOrEqual(0); expect(verifyStepIndex).toBeGreaterThan(syncStepIndex); expect(syncHostedStepIndex).toBeGreaterThan(verifyStepIndex); expect(verifyHostedStepIndex).toBeGreaterThan(syncHostedStepIndex); @@ -1988,7 +2007,9 @@ describe('standalone release packaging', () => { expect(uninstallPowerShellSource).toContain( 'Test-QwenStandaloneInstallDir', ); - expect(uninstallPowerShellSource).toContain('Remove-UserPathEntry'); + expect(uninstallPowerShellSource).toContain( + 'Remove-PathEntryFromAllScopes', + ); expect(uninstallPowerShellSource).toContain('Remove-CurrentCmdPathShim'); expect(uninstallPowerShellSource).toContain( 'Remove-RecordedCurrentCmdPathShim',