mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 22:42:31 +00:00
fix(installer): tolerate WSL UNC launch cwd
This commit is contained in:
@@ -93,6 +93,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/discovery: strip `-plugin` package suffixes when deriving plugin id hints so package names line up with manifest ids. (#85170) Thanks @JulyanXu.
|
||||
- Tlon: stop advertising a non-existent agent tool contract in the plugin manifest.
|
||||
- Telegram: preserve fenced code block languages through Markdown rendering so Telegram receives `language-*` code classes. (#85209) Thanks @leno23.
|
||||
- Windows installer: run npm and Corepack command shims from a Windows-local directory so installs launched from WSL2 UNC paths do not fail before OpenClaw is installed.
|
||||
- Windows updates: roll back git-backed updates to the previous checkout when dependency install, build, UI build, or doctor repair fails.
|
||||
- Windows installer: persist user-local portable Git on PATH and activate the repo-pinned pnpm version for git-backed installs and updates.
|
||||
- Windows installer: bootstrap a user-local portable Node.js when native Windows has no Node and no winget, Chocolatey, or Scoop, so first-run installs can continue on raw hosts.
|
||||
|
||||
@@ -623,6 +623,53 @@ function Get-PnpmCommandPath {
|
||||
return (Resolve-CommandPath -Candidates @("pnpm.cmd", "pnpm.exe", "pnpm"))
|
||||
}
|
||||
|
||||
function Get-WindowsCommandSafeDirectory {
|
||||
$userHome = [Environment]::GetFolderPath("UserProfile")
|
||||
if (-not [string]::IsNullOrWhiteSpace($userHome) -and (Test-Path $userHome)) {
|
||||
return $userHome
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:TEMP) -and (Test-Path $env:TEMP)) {
|
||||
return $env:TEMP
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-CommandFromWindowsSafeDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CommandPath,
|
||||
[string[]]$Arguments = @()
|
||||
)
|
||||
|
||||
$safeDir = Get-WindowsCommandSafeDirectory
|
||||
$pushedLocation = $false
|
||||
try {
|
||||
if (-not [string]::IsNullOrWhiteSpace($safeDir)) {
|
||||
Push-Location -LiteralPath $safeDir
|
||||
$pushedLocation = $true
|
||||
}
|
||||
& $CommandPath @Arguments
|
||||
} finally {
|
||||
if ($pushedLocation) {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-NpmCommand {
|
||||
param([string[]]$Arguments = @())
|
||||
Invoke-CommandFromWindowsSafeDirectory -CommandPath (Get-NpmCommandPath) -Arguments $Arguments
|
||||
}
|
||||
|
||||
function Invoke-CorepackCommand {
|
||||
param([string[]]$Arguments = @())
|
||||
$corepackCommand = Get-CorepackCommandPath
|
||||
if (-not $corepackCommand) {
|
||||
throw "corepack not found on PATH."
|
||||
}
|
||||
Invoke-CommandFromWindowsSafeDirectory -CommandPath $corepackCommand -Arguments $Arguments
|
||||
}
|
||||
|
||||
function Get-NpmGlobalBinCandidates {
|
||||
param(
|
||||
[string]$NpmPrefix
|
||||
@@ -647,7 +694,7 @@ function Ensure-OpenClawOnPath {
|
||||
|
||||
$npmPrefix = $null
|
||||
try {
|
||||
$npmPrefix = (& (Get-NpmCommandPath) config get prefix 2>$null).Trim()
|
||||
$npmPrefix = (Invoke-NpmCommand -Arguments @("config", "get", "prefix") 2>$null).Trim()
|
||||
} catch {
|
||||
$npmPrefix = $null
|
||||
}
|
||||
@@ -751,8 +798,8 @@ function Ensure-Pnpm {
|
||||
$corepackCommand = Get-CorepackCommandPath
|
||||
if ($corepackCommand) {
|
||||
try {
|
||||
& $corepackCommand enable | Out-Null
|
||||
& $corepackCommand prepare $pnpmSpec --activate | Out-Null
|
||||
Invoke-CorepackCommand -Arguments @("enable") | Out-Null
|
||||
Invoke-CorepackCommand -Arguments @("prepare", $pnpmSpec, "--activate") | Out-Null
|
||||
if (Test-PnpmCommandMatchesVersion -PnpmVersion $pnpmVersion -RepoDir $RepoDir) {
|
||||
Write-Host "[OK] pnpm installed via corepack ($pnpmSpec)" -ForegroundColor Green
|
||||
return
|
||||
@@ -765,7 +812,7 @@ function Ensure-Pnpm {
|
||||
$prevScriptShell = $env:NPM_CONFIG_SCRIPT_SHELL
|
||||
$env:NPM_CONFIG_SCRIPT_SHELL = "cmd.exe"
|
||||
try {
|
||||
& (Get-NpmCommandPath) install -g $pnpmSpec
|
||||
Invoke-NpmCommand -Arguments @("install", "-g", $pnpmSpec)
|
||||
} finally {
|
||||
$env:NPM_CONFIG_SCRIPT_SHELL = $prevScriptShell
|
||||
}
|
||||
@@ -776,6 +823,52 @@ function Ensure-Pnpm {
|
||||
}
|
||||
|
||||
# Install OpenClaw
|
||||
function Resolve-LocalNpmPackagePath {
|
||||
param([string]$PackagePath)
|
||||
|
||||
try {
|
||||
return (Resolve-Path -LiteralPath $PackagePath -ErrorAction Stop).ProviderPath
|
||||
} catch {
|
||||
return [System.IO.Path]::GetFullPath($PackagePath)
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-LocalNpmPackageInstallSpec {
|
||||
param([string]$InstallSpec)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($InstallSpec)) {
|
||||
return $InstallSpec
|
||||
}
|
||||
if ($InstallSpec -match '^file:(?<path>.+)$') {
|
||||
$filePath = $Matches["path"]
|
||||
if (
|
||||
$filePath -match '^/' -or
|
||||
$filePath -match '^\\\\' -or
|
||||
$filePath -match '^[A-Za-z]:[\\/]'
|
||||
) {
|
||||
return $InstallSpec
|
||||
}
|
||||
return ([System.Uri](Resolve-LocalNpmPackagePath -PackagePath $filePath)).AbsoluteUri
|
||||
}
|
||||
if (
|
||||
$InstallSpec -match '^https?:' -or
|
||||
$InstallSpec -match '^(git\+|github:)' -or
|
||||
$InstallSpec -match '^[A-Za-z]:[\\/]' -or
|
||||
$InstallSpec -match '^\\\\'
|
||||
) {
|
||||
return $InstallSpec
|
||||
}
|
||||
if ($InstallSpec -notmatch '^\.\.?[\\/]' -and $InstallSpec -notmatch '\.tgz$') {
|
||||
return $InstallSpec
|
||||
}
|
||||
|
||||
try {
|
||||
return (Resolve-LocalNpmPackagePath -PackagePath $InstallSpec)
|
||||
} catch {
|
||||
return $InstallSpec
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-NpmOpenClawInstallSpec {
|
||||
param(
|
||||
[string]$PackageName,
|
||||
@@ -795,7 +888,7 @@ function Resolve-NpmOpenClawInstallSpec {
|
||||
$trimmedTag -match '^\.\.?[\\/]' -or
|
||||
$trimmedTag -match '\.tgz($|[?#])'
|
||||
) {
|
||||
return $trimmedTag
|
||||
return (Resolve-LocalNpmPackageInstallSpec -InstallSpec $trimmedTag)
|
||||
}
|
||||
|
||||
return "$PackageName@$trimmedTag"
|
||||
@@ -852,9 +945,9 @@ function Install-OpenClaw {
|
||||
$installSpec = Resolve-NpmOpenClawInstallSpec -PackageName $packageName -RequestedTag $Tag
|
||||
Write-Host "[*] Installing OpenClaw ($installSpec)..." -ForegroundColor Yellow
|
||||
$freshnessArgs = @("--min-release-age=0")
|
||||
$minReleaseAge = (& (Get-NpmCommandPath) config get min-release-age 2>$null)
|
||||
$minReleaseAge = (Invoke-NpmCommand -Arguments @("config", "get", "min-release-age") 2>$null)
|
||||
if ($LASTEXITCODE -ne 0 -or -not $minReleaseAge -or $minReleaseAge.Trim() -eq "null" -or $minReleaseAge.Trim() -eq "undefined") {
|
||||
$beforeValue = (& (Get-NpmCommandPath) config get before 2>$null)
|
||||
$beforeValue = (Invoke-NpmCommand -Arguments @("config", "get", "before") 2>$null)
|
||||
if ($LASTEXITCODE -eq 0 -and $beforeValue -and $beforeValue.Trim() -ne "null" -and $beforeValue.Trim() -ne "undefined") {
|
||||
$freshnessArgs = @("--before=$((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"))")
|
||||
}
|
||||
@@ -876,7 +969,7 @@ function Install-OpenClaw {
|
||||
Remove-Item Env:NPM_CONFIG_BEFORE -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:NPM_CONFIG_MIN_RELEASE_AGE -ErrorAction SilentlyContinue
|
||||
try {
|
||||
$npmOutput = & (Get-NpmCommandPath) install -g @freshnessArgs "$installSpec" 2>&1
|
||||
$npmOutput = Invoke-NpmCommand -Arguments (@("install", "-g") + $freshnessArgs + @("$installSpec")) 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[!] npm install failed" -ForegroundColor Red
|
||||
if ($npmOutput -match "spawn git" -or $npmOutput -match "ENOENT.*git") {
|
||||
@@ -1104,7 +1197,7 @@ function Main {
|
||||
try {
|
||||
$npmCommand = Get-NpmCommandPath
|
||||
if ($npmCommand) {
|
||||
& $npmCommand uninstall -g openclaw 2>$null | Out-Null
|
||||
Invoke-NpmCommand -Arguments @("uninstall", "-g", "openclaw") 2>$null | Out-Null
|
||||
Write-Host "[OK] Removed npm global install if present" -ForegroundColor Green
|
||||
}
|
||||
} catch { }
|
||||
@@ -1144,7 +1237,7 @@ function Main {
|
||||
}
|
||||
if (-not $installedVersion) {
|
||||
try {
|
||||
$npmList = & (Get-NpmCommandPath) list -g --depth 0 --json 2>$null | ConvertFrom-Json
|
||||
$npmList = Invoke-NpmCommand -Arguments @("list", "-g", "--depth", "0", "--json") 2>$null | ConvertFrom-Json
|
||||
if ($npmList -and $npmList.dependencies -and $npmList.dependencies.openclaw -and $npmList.dependencies.openclaw.version) {
|
||||
$installedVersion = $npmList.dependencies.openclaw.version
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("install.ps1 failure handling", () => {
|
||||
|
||||
it("runs npm install through the resolved command with quiet CI defaults", () => {
|
||||
const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw");
|
||||
expect(npmInstallBody).toContain("$npmOutput = & (Get-NpmCommandPath) install -g");
|
||||
expect(npmInstallBody).toContain("$npmOutput = Invoke-NpmCommand -Arguments");
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_LOGLEVEL = "error"');
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_UPDATE_NOTIFIER = "false"');
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_FUND = "false"');
|
||||
@@ -95,7 +95,10 @@ describe("install.ps1 failure handling", () => {
|
||||
expect(npmInstallBody).toContain("Remove-Item Env:NPM_CONFIG_MIN_RELEASE_AGE");
|
||||
expect(npmInstallBody).toContain('$env:NODE_LLAMA_CPP_SKIP_DOWNLOAD = "1"');
|
||||
expect(npmInstallBody).toContain(
|
||||
'$npmOutput = & (Get-NpmCommandPath) install -g @freshnessArgs "$installSpec"',
|
||||
[
|
||||
"$npmOutput = Invoke-NpmCommand -Arguments",
|
||||
'(@("install", "-g") + $freshnessArgs + @("$installSpec"))',
|
||||
].join(" "),
|
||||
);
|
||||
expect(npmInstallBody).toContain("$env:NPM_CONFIG_LOGLEVEL = $prevLogLevel");
|
||||
expect(npmInstallBody).toContain("$env:NPM_CONFIG_BEFORE = $prevBefore");
|
||||
@@ -104,6 +107,35 @@ describe("install.ps1 failure handling", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("runs Windows command shims from a Windows-local cwd", () => {
|
||||
const commandSafeBody = extractFunctionBody(source, "Invoke-CommandFromWindowsSafeDirectory");
|
||||
const npmCommandBody = extractFunctionBody(source, "Invoke-NpmCommand");
|
||||
const corepackCommandBody = extractFunctionBody(source, "Invoke-CorepackCommand");
|
||||
const openClawPathBody = extractFunctionBody(source, "Ensure-OpenClawOnPath");
|
||||
const ensurePnpmBody = extractFunctionBody(source, "Ensure-Pnpm");
|
||||
const mainBody = extractFunctionBody(source, "Main");
|
||||
|
||||
expect(commandSafeBody).toContain("Get-WindowsCommandSafeDirectory");
|
||||
expect(commandSafeBody).toContain("Push-Location -LiteralPath $safeDir");
|
||||
expect(commandSafeBody).toContain("& $CommandPath @Arguments");
|
||||
expect(commandSafeBody).toContain("Pop-Location");
|
||||
expect(npmCommandBody).toContain("Invoke-CommandFromWindowsSafeDirectory");
|
||||
expect(corepackCommandBody).toContain("Invoke-CommandFromWindowsSafeDirectory");
|
||||
expect(openClawPathBody).toContain(
|
||||
'Invoke-NpmCommand -Arguments @("config", "get", "prefix")',
|
||||
);
|
||||
expect(ensurePnpmBody).toContain(
|
||||
'Invoke-CorepackCommand -Arguments @("prepare", $pnpmSpec, "--activate")',
|
||||
);
|
||||
expect(ensurePnpmBody).toContain('Invoke-NpmCommand -Arguments @("install", "-g", $pnpmSpec)');
|
||||
expect(mainBody).toContain(
|
||||
'Invoke-NpmCommand -Arguments @("uninstall", "-g", "openclaw")',
|
||||
);
|
||||
expect(mainBody).toContain(
|
||||
'Invoke-NpmCommand -Arguments @("list", "-g", "--depth", "0", "--json")',
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects OpenClaw GitHub source targets for npm installs", () => {
|
||||
const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw");
|
||||
const sourceTargetBody = extractFunctionBody(source, "Test-OpenClawSourcePackageInstallSpec");
|
||||
@@ -114,6 +146,23 @@ describe("install.ps1 failure handling", () => {
|
||||
expect(npmInstallBody).toContain("-InstallMethod git -Tag main");
|
||||
});
|
||||
|
||||
it("preserves caller-relative local tarball install specs before safe-cwd npm calls", () => {
|
||||
const resolveSpecBody = extractFunctionBody(source, "Resolve-NpmOpenClawInstallSpec");
|
||||
const localSpecBody = extractFunctionBody(source, "Resolve-LocalNpmPackageInstallSpec");
|
||||
const localPathBody = extractFunctionBody(source, "Resolve-LocalNpmPackagePath");
|
||||
|
||||
expect(resolveSpecBody).toContain(
|
||||
"Resolve-LocalNpmPackageInstallSpec -InstallSpec $trimmedTag",
|
||||
);
|
||||
expect(localSpecBody).toContain("$InstallSpec -match '^file:(?<path>.+)$'");
|
||||
expect(localSpecBody).toContain("Resolve-LocalNpmPackagePath -PackagePath $filePath");
|
||||
expect(localSpecBody).toContain(").AbsoluteUri");
|
||||
expect(localSpecBody).toContain("$InstallSpec -notmatch '^\\.\\.?[\\\\/]'");
|
||||
expect(localSpecBody).toContain("$InstallSpec -notmatch '\\.tgz$'");
|
||||
expect(localPathBody).toContain("Resolve-Path -LiteralPath $PackagePath");
|
||||
expect(localPathBody).toContain("[System.IO.Path]::GetFullPath($PackagePath)");
|
||||
});
|
||||
|
||||
it("falls back to a user-local portable Node.js bootstrap when package managers are absent", () => {
|
||||
const installNodeBody = extractFunctionBody(source, "Install-Node");
|
||||
const portableNodeBody = extractFunctionBody(source, "Install-PortableNode");
|
||||
@@ -188,8 +237,10 @@ describe("install.ps1 failure handling", () => {
|
||||
expect(ensurePnpmBody).toContain(
|
||||
"Test-PnpmCommandMatchesVersion -PnpmVersion $pnpmVersion -RepoDir $RepoDir",
|
||||
);
|
||||
expect(ensurePnpmBody).toContain("& $corepackCommand prepare $pnpmSpec --activate");
|
||||
expect(ensurePnpmBody).toContain("& (Get-NpmCommandPath) install -g $pnpmSpec");
|
||||
expect(ensurePnpmBody).toContain(
|
||||
'Invoke-CorepackCommand -Arguments @("prepare", $pnpmSpec, "--activate")',
|
||||
);
|
||||
expect(ensurePnpmBody).toContain('Invoke-NpmCommand -Arguments @("install", "-g", $pnpmSpec)');
|
||||
expect(gitInstallBody.indexOf("git clone $repoUrl $RepoDir")).toBeLessThan(
|
||||
gitInstallBody.indexOf("Ensure-Pnpm -RepoDir $RepoDir"),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user