fix(installer): make Windows standalone shim available in cmd

This commit is contained in:
yiliang114 2026-05-14 17:00:03 +08:00
parent 564f899359
commit 74130fc79e
2 changed files with 186 additions and 0 deletions

View file

@ -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'

View file

@ -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,