diff --git a/scripts/installation/install-qwen-standalone.ps1 b/scripts/installation/install-qwen-standalone.ps1 index 122a6ee46..b75d4df35 100644 --- a/scripts/installation/install-qwen-standalone.ps1 +++ b/scripts/installation/install-qwen-standalone.ps1 @@ -91,6 +91,146 @@ function Get-ParentProcessName { } } +function Get-NormalizedPath { + param([string]$PathValue) + + if ([string]::IsNullOrEmpty($PathValue)) { + return $null + } + + $trimmed = $PathValue.Trim().Trim('"') + if ([string]::IsNullOrEmpty($trimmed)) { + return $null + } + + try { + return [IO.Path]::GetFullPath($trimmed).TrimEnd('\') + } catch { + return $trimmed.TrimEnd('\') + } +} + +function Test-PathContainsDirectory { + param([string]$PathValue, [string]$Directory) + + $target = Get-NormalizedPath -PathValue $Directory + if ([string]::IsNullOrEmpty($target)) { + return $false + } + + foreach ($entry in @($PathValue -split ';')) { + $normalizedEntry = Get-NormalizedPath -PathValue $entry + if ([string]::Equals($normalizedEntry, $target, [StringComparison]::OrdinalIgnoreCase)) { + return $true + } + } + + return $false +} + +function Test-WritableDirectory { + param([string]$Directory) + + if ([string]::IsNullOrEmpty($Directory)) { + return $false + } + + if (-not (Test-Path -LiteralPath $Directory -PathType Container)) { + return $false + } + + $probe = Join-Path $Directory ('.qwen-write-test-' + [IO.Path]::GetRandomFileName()) + try { + [IO.File]::WriteAllText($probe, '') + Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue + return $true + } catch { + Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue + return $false + } +} + +function Add-PathCandidate { + param( + [System.Collections.Generic.List[string]]$Candidates, + [string]$Directory + ) + + $normalizedDirectory = Get-NormalizedPath -PathValue $Directory + if ([string]::IsNullOrEmpty($normalizedDirectory)) { + return + } + + foreach ($candidate in $Candidates) { + $normalizedCandidate = Get-NormalizedPath -PathValue $candidate + if ([string]::Equals($normalizedCandidate, $normalizedDirectory, [StringComparison]::OrdinalIgnoreCase)) { + return + } + } + + [void]$Candidates.Add($Directory.Trim().Trim('"')) +} + +function Install-CurrentCmdPathShim { + param([string]$QwenCommand, [string]$PathValue) + + $pathEntries = @($PathValue -split ';' | Where-Object { -not [string]::IsNullOrEmpty($_) }) + $candidates = [System.Collections.Generic.List[string]]::new() + $preferredDirectories = @() + + if (-not [string]::IsNullOrEmpty($env:LOCALAPPDATA)) { + $preferredDirectories += Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps' + } + if (-not [string]::IsNullOrEmpty($env:APPDATA)) { + $preferredDirectories += Join-Path $env:APPDATA 'npm' + } + if (-not [string]::IsNullOrEmpty($env:USERPROFILE)) { + $preferredDirectories += Join-Path $env:USERPROFILE '.bun\bin' + } + + foreach ($preferredDirectory in $preferredDirectories) { + $preferredNormalized = Get-NormalizedPath -PathValue $preferredDirectory + foreach ($entry in $pathEntries) { + $entryNormalized = Get-NormalizedPath -PathValue $entry + if ([string]::Equals($entryNormalized, $preferredNormalized, [StringComparison]::OrdinalIgnoreCase)) { + Add-PathCandidate -Candidates $candidates -Directory $entry + } + } + } + + $userRoot = Get-NormalizedPath -PathValue $env:USERPROFILE + foreach ($entry in $pathEntries) { + $entryNormalized = Get-NormalizedPath -PathValue $entry + if ( + -not [string]::IsNullOrEmpty($userRoot) -and + -not [string]::IsNullOrEmpty($entryNormalized) -and + $entryNormalized.StartsWith($userRoot, [StringComparison]::OrdinalIgnoreCase) + ) { + Add-PathCandidate -Candidates $candidates -Directory $entry + } + } + + foreach ($candidate in $candidates) { + if (-not (Test-WritableDirectory -Directory $candidate)) { + continue + } + + $shimPath = Join-Path $candidate 'qwen.cmd' + if (Test-Path -LiteralPath $shimPath -PathType Leaf) { + $existingShim = Get-Content -LiteralPath $shimPath -Raw -ErrorAction SilentlyContinue + if ($existingShim -notmatch 'Qwen Code current-session shim') { + continue + } + } + + $shim = "@echo off`r`nREM Qwen Code current-session shim. Generated by install-qwen-standalone.ps1.`r`ncall `"$QwenCommand`" %*`r`n" + [IO.File]::WriteAllText($shimPath, $shim, [Text.UTF8Encoding]::new($false)) + return $shimPath + } + + return $null +} + function Update-CurrentShell { $qwenInstallBinDir = Get-QwenInstallBinDir $qwenCommandPath = Join-Path $qwenInstallBinDir 'qwen.cmd' @@ -98,15 +238,32 @@ function Update-CurrentShell { return } + $inheritedPath = $env:Path Update-CurrentSessionPath -BinDir $qwenInstallBinDir Write-Output "Run: qwen" $parentProcessName = Get-ParentProcessName if ($parentProcessName -ieq 'cmd.exe') { + if (Test-PathContainsDirectory -PathValue $inheritedPath -Directory $qwenInstallBinDir) { + Write-Output "qwen is ready to use after this installer command returns." + return + } + + $shimPath = Install-CurrentCmdPathShim -QwenCommand $qwenCommandPath -PathValue $inheritedPath + if (-not [string]::IsNullOrEmpty($shimPath)) { + Write-Output "INFO: Added qwen.cmd to a directory already on this cmd.exe PATH:" + Write-Output "INFO: ${shimPath}" + Write-Output "qwen is ready to use after this installer command returns." + return + } + + Write-Output "WARNING: Windows does not allow this PowerShell child process to update the parent cmd.exe PATH directly." Write-Output "Or, for this cmd.exe window, run:" Write-Output " set `"PATH=${qwenInstallBinDir};%PATH%`"" return } + + Write-Output "qwen is ready to use in this PowerShell session." } $qwenDefaultInstallerUrl = 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.bat' diff --git a/scripts/tests/install-script.test.js b/scripts/tests/install-script.test.js index 47b800dab..df2b4605e 100644 --- a/scripts/tests/install-script.test.js +++ b/scripts/tests/install-script.test.js @@ -603,6 +603,35 @@ describe('standalone release packaging', () => { expect(installPowerShellSource).toContain('@args'); }); + it('PowerShell hosted entrypoint refreshes the current Windows shell', () => { + const installPowerShellSource = readScript( + 'scripts/installation/install-qwen-standalone.ps1', + ); + const installBatchSource = readScript( + 'scripts/installation/install-qwen-standalone.bat', + ); + + expect(installPowerShellSource).toContain('Update-CurrentSessionPath'); + expect(installPowerShellSource).toContain('Install-CurrentCmdPathShim'); + expect(installPowerShellSource).toContain('Test-WritableDirectory'); + expect(installPowerShellSource).toContain('Qwen Code current-session shim'); + expect(installPowerShellSource).not.toContain('doskey.exe'); + expect(installPowerShellSource).toContain( + 'qwen is ready to use in this PowerShell session.', + ); + expect(installPowerShellSource).toContain( + 'Added qwen.cmd to a directory already on this cmd.exe PATH:', + ); + expect(installPowerShellSource).toContain( + 'Windows does not allow this PowerShell child process to update the parent cmd.exe PATH directly.', + ); + + expect(installBatchSource).toContain('QWEN_INSTALLER_PARENT_POWERSHELL'); + expect(installBatchSource).toContain( + 'Final PATH refresh is handled by the PowerShell entrypoint.', + ); + }); + it('stages hosted installation assets with checksums', async () => { const { HOSTED_INSTALLATION_ASSET_NAMES,