@@ -146,13 +146,157 @@ function Check-Git {
146146 }
147147}
148148
149- function Require-Git {
149+ function Add-ToProcessPath {
150+ param (
151+ [Parameter (Mandatory = $true )]
152+ [string ]$PathEntry
153+ )
154+
155+ if ([string ]::IsNullOrWhiteSpace($PathEntry )) {
156+ return
157+ }
158+
159+ $currentEntries = @ ($env: Path -split " ;" | Where-Object { -not [string ]::IsNullOrWhiteSpace($_ ) })
160+ if ($currentEntries | Where-Object { $_ -ieq $PathEntry }) {
161+ return
162+ }
163+
164+ $env: Path = " $PathEntry ;$env: Path "
165+ }
166+
167+ function Get-PortableGitRoot {
168+ $base = Join-Path $env: LOCALAPPDATA " OpenClaw\deps"
169+ return (Join-Path $base " portable-git" )
170+ }
171+
172+ function Get-PortableGitCommandPath {
173+ $root = Get-PortableGitRoot
174+ foreach ($candidate in @ (
175+ (Join-Path $root " mingw64\bin\git.exe" ),
176+ (Join-Path $root " cmd\git.exe" ),
177+ (Join-Path $root " bin\git.exe" ),
178+ (Join-Path $root " git.exe" )
179+ )) {
180+ if (Test-Path $candidate ) {
181+ return $candidate
182+ }
183+ }
184+ return $null
185+ }
186+
187+ function Use-PortableGitIfPresent {
188+ $gitExe = Get-PortableGitCommandPath
189+ if (-not $gitExe ) {
190+ return $false
191+ }
192+
193+ $portableRoot = Get-PortableGitRoot
194+ foreach ($pathEntry in @ (
195+ (Join-Path $portableRoot " mingw64\bin" ),
196+ (Join-Path $portableRoot " usr\bin" ),
197+ (Split-Path - Parent $gitExe )
198+ )) {
199+ if (Test-Path $pathEntry ) {
200+ Add-ToProcessPath $pathEntry
201+ }
202+ }
203+ if (Check- Git) {
204+ return $true
205+ }
206+ return $false
207+ }
208+
209+ function Resolve-PortableGitDownload {
210+ $releaseApi = " https://api.github.com/repos/git-for-windows/git/releases/latest"
211+ $headers = @ {
212+ " User-Agent" = " openclaw-installer"
213+ " Accept" = " application/vnd.github+json"
214+ }
215+ $release = Invoke-RestMethod - Uri $releaseApi - Headers $headers
216+ if (-not $release -or -not $release.assets ) {
217+ throw " Could not resolve latest git-for-windows release metadata."
218+ }
219+
220+ $asset = $release.assets |
221+ Where-Object { $_.name -match ' ^MinGit-.*-64-bit\.zip$' -and $_.name -notmatch ' busybox' } |
222+ Select-Object - First 1
223+
224+ if (-not $asset ) {
225+ throw " Could not find a MinGit zip asset in the latest git-for-windows release."
226+ }
227+
228+ return @ {
229+ Tag = $release.tag_name
230+ Name = $asset.name
231+ Url = $asset.browser_download_url
232+ }
233+ }
234+
235+ function Install-PortableGit {
236+ if (Use-PortableGitIfPresent ) {
237+ $portableVersion = (& git -- version 2> $null )
238+ if ($portableVersion ) {
239+ Write-Host " [OK] User-local Git already available: $portableVersion " - ForegroundColor Green
240+ }
241+ return
242+ }
243+
244+ Write-Host " [*] Git not found; bootstrapping user-local portable Git..." - ForegroundColor Yellow
245+
246+ $download = Resolve-PortableGitDownload
247+ $portableRoot = Get-PortableGitRoot
248+ $portableParent = Split-Path - Parent $portableRoot
249+ $tmpZip = Join-Path $env: TEMP $download.Name
250+ $tmpExtract = Join-Path $env: TEMP (" openclaw-portable-git-" + [guid ]::NewGuid().ToString(" N" ))
251+
252+ New-Item - ItemType Directory - Force - Path $portableParent | Out-Null
253+ if (Test-Path $portableRoot ) {
254+ Remove-Item - Recurse - Force $portableRoot
255+ }
256+ if (Test-Path $tmpExtract ) {
257+ Remove-Item - Recurse - Force $tmpExtract
258+ }
259+ New-Item - ItemType Directory - Force - Path $tmpExtract | Out-Null
260+
261+ try {
262+ Write-Host " Downloading $ ( $download.Tag ) ..." - ForegroundColor Gray
263+ Invoke-WebRequest - Uri $download.Url - OutFile $tmpZip
264+ Expand-Archive - Path $tmpZip - DestinationPath $tmpExtract - Force
265+ Move-Item - Path (Join-Path $tmpExtract " *" ) - Destination $portableRoot - Force
266+ } finally {
267+ if (Test-Path $tmpZip ) {
268+ Remove-Item - Force $tmpZip
269+ }
270+ if (Test-Path $tmpExtract ) {
271+ Remove-Item - Recurse - Force $tmpExtract
272+ }
273+ }
274+
275+ if (-not (Use-PortableGitIfPresent )) {
276+ throw " Portable Git bootstrap completed, but git is still unavailable."
277+ }
278+
279+ $portableVersion = (& git -- version 2> $null )
280+ Write-Host " [OK] User-local Git ready: $portableVersion " - ForegroundColor Green
281+ }
282+
283+ function Ensure-Git {
150284 if (Check- Git) { return }
285+ if (Use-PortableGitIfPresent ) { return }
286+ try {
287+ Install-PortableGit
288+ if (Check- Git) {
289+ return
290+ }
291+ } catch {
292+ Write-Host " [!] Portable Git bootstrap failed: $ ( $_.Exception.Message ) " - ForegroundColor Yellow
293+ }
294+
151295 Write-Host " "
152296 Write-Host " Error: Git is required to install OpenClaw." - ForegroundColor Red
153- Write-Host " Install Git for Windows:" - ForegroundColor Yellow
297+ Write-Host " Auto-bootstrap of user-local Git did not succeed." - ForegroundColor Yellow
298+ Write-Host " Install Git for Windows manually, then re-run this installer:" - ForegroundColor Yellow
154299 Write-Host " https://git-scm.com/download/win" - ForegroundColor Cyan
155- Write-Host " Then re-run this installer." - ForegroundColor Yellow
156300 exit 1
157301}
158302
@@ -184,6 +328,38 @@ function Invoke-OpenClawCommand {
184328 & $commandPath @Arguments
185329}
186330
331+ function Resolve-CommandPath {
332+ param (
333+ [Parameter (Mandatory = $true )]
334+ [string []]$Candidates
335+ )
336+
337+ foreach ($candidate in $Candidates ) {
338+ $command = Get-Command $candidate - ErrorAction SilentlyContinue
339+ if ($command -and $command.Source ) {
340+ return $command.Source
341+ }
342+ }
343+
344+ return $null
345+ }
346+
347+ function Get-NpmCommandPath {
348+ $path = Resolve-CommandPath - Candidates @ (" npm.cmd" , " npm.exe" , " npm" )
349+ if (-not $path ) {
350+ throw " npm not found on PATH."
351+ }
352+ return $path
353+ }
354+
355+ function Get-CorepackCommandPath {
356+ return (Resolve-CommandPath - Candidates @ (" corepack.cmd" , " corepack.exe" , " corepack" ))
357+ }
358+
359+ function Get-PnpmCommandPath {
360+ return (Resolve-CommandPath - Candidates @ (" pnpm.cmd" , " pnpm.exe" , " pnpm" ))
361+ }
362+
187363function Get-NpmGlobalBinCandidates {
188364 param (
189365 [string ]$NpmPrefix
@@ -208,7 +384,7 @@ function Ensure-OpenClawOnPath {
208384
209385 $npmPrefix = $null
210386 try {
211- $npmPrefix = (npm config get prefix 2> $null ).Trim()
387+ $npmPrefix = (& ( Get-NpmCommandPath ) config get prefix 2> $null ).Trim()
212388 } catch {
213389 $npmPrefix = $null
214390 }
@@ -242,14 +418,15 @@ function Ensure-OpenClawOnPath {
242418}
243419
244420function Ensure-Pnpm {
245- if (Get-Command pnpm - ErrorAction SilentlyContinue ) {
421+ if (Get-PnpmCommandPath ) {
246422 return
247423 }
248- if (Get-Command corepack - ErrorAction SilentlyContinue) {
424+ $corepackCommand = Get-CorepackCommandPath
425+ if ($corepackCommand ) {
249426 try {
250- corepack enable | Out-Null
251- corepack prepare pnpm@latest -- activate | Out-Null
252- if (Get-Command pnpm - ErrorAction SilentlyContinue ) {
427+ & $corepackCommand enable | Out-Null
428+ & $corepackCommand prepare pnpm@latest -- activate | Out-Null
429+ if (Get-PnpmCommandPath ) {
253430 Write-Host " [OK] pnpm installed via corepack" - ForegroundColor Green
254431 return
255432 }
@@ -261,7 +438,7 @@ function Ensure-Pnpm {
261438 $prevScriptShell = $env: NPM_CONFIG_SCRIPT_SHELL
262439 $env: NPM_CONFIG_SCRIPT_SHELL = " cmd.exe"
263440 try {
264- npm install - g pnpm
441+ & ( Get-NpmCommandPath ) install - g pnpm
265442 } finally {
266443 $env: NPM_CONFIG_SCRIPT_SHELL = $prevScriptShell
267444 }
@@ -273,7 +450,7 @@ function Install-OpenClaw {
273450 if ([string ]::IsNullOrWhiteSpace($Tag )) {
274451 $Tag = " latest"
275452 }
276- Require - Git
453+ Ensure - Git
277454
278455 # Use openclaw package for beta, openclaw for stable
279456 $packageName = " openclaw"
@@ -286,13 +463,15 @@ function Install-OpenClaw {
286463 $prevFund = $env: NPM_CONFIG_FUND
287464 $prevAudit = $env: NPM_CONFIG_AUDIT
288465 $prevScriptShell = $env: NPM_CONFIG_SCRIPT_SHELL
466+ $prevNodeLlamaSkipDownload = $env: NODE_LLAMA_CPP_SKIP_DOWNLOAD
289467 $env: NPM_CONFIG_LOGLEVEL = " error"
290468 $env: NPM_CONFIG_UPDATE_NOTIFIER = " false"
291469 $env: NPM_CONFIG_FUND = " false"
292470 $env: NPM_CONFIG_AUDIT = " false"
293471 $env: NPM_CONFIG_SCRIPT_SHELL = " cmd.exe"
472+ $env: NODE_LLAMA_CPP_SKIP_DOWNLOAD = " 1"
294473 try {
295- $npmOutput = npm install - g " $packageName @$Tag " 2>&1
474+ $npmOutput = & ( Get-NpmCommandPath ) install - g " $packageName @$Tag " 2>&1
296475 if ($LASTEXITCODE -ne 0 ) {
297476 Write-Host " [!] npm install failed" - ForegroundColor Red
298477 if ($npmOutput -match " spawn git" -or $npmOutput -match " ENOENT.*git" ) {
@@ -312,6 +491,7 @@ function Install-OpenClaw {
312491 $env: NPM_CONFIG_FUND = $prevFund
313492 $env: NPM_CONFIG_AUDIT = $prevAudit
314493 $env: NPM_CONFIG_SCRIPT_SHELL = $prevScriptShell
494+ $env: NODE_LLAMA_CPP_SKIP_DOWNLOAD = $prevNodeLlamaSkipDownload
315495 }
316496 Write-Host " [OK] OpenClaw installed" - ForegroundColor Green
317497}
@@ -322,7 +502,7 @@ function Install-OpenClawFromGit {
322502 [string ]$RepoDir ,
323503 [switch ]$SkipUpdate
324504 )
325- Require - Git
505+ Ensure - Git
326506 Ensure- Pnpm
327507
328508 $repoUrl = " https://github.com/openclaw/openclaw.git"
@@ -345,13 +525,17 @@ function Install-OpenClawFromGit {
345525 Remove-LegacySubmodule - RepoDir $RepoDir
346526
347527 $prevPnpmScriptShell = $env: NPM_CONFIG_SCRIPT_SHELL
528+ $pnpmCommand = Get-PnpmCommandPath
529+ if (-not $pnpmCommand ) {
530+ throw " pnpm not found after installation."
531+ }
348532 $env: NPM_CONFIG_SCRIPT_SHELL = " cmd.exe"
349533 try {
350- pnpm - C $RepoDir install
351- if (-not (pnpm - C $RepoDir ui:build)) {
534+ & $pnpmCommand - C $RepoDir install
535+ if (-not (& $pnpmCommand - C $RepoDir ui:build)) {
352536 Write-Host " [!] UI build failed; continuing (CLI may still work)" - ForegroundColor Yellow
353537 }
354- pnpm - C $RepoDir build
538+ & $pnpmCommand - C $RepoDir build
355539 } finally {
356540 $env: NPM_CONFIG_SCRIPT_SHELL = $prevPnpmScriptShell
357541 }
@@ -522,7 +706,7 @@ function Main {
522706 }
523707 if (-not $installedVersion ) {
524708 try {
525- $npmList = npm list - g -- depth 0 -- json 2> $null | ConvertFrom-Json
709+ $npmList = & ( Get-NpmCommandPath ) list - g -- depth 0 -- json 2> $null | ConvertFrom-Json
526710 if ($npmList -and $npmList.dependencies -and $npmList.dependencies.openclaw -and $npmList.dependencies.openclaw.version ) {
527711 $installedVersion = $npmList.dependencies.openclaw.version
528712 }
0 commit comments