fix(openclaw): resolve installation failure on Windows with spaces in path (#12977)

### What this PR does

Before this PR:
- OpenClaw installation/uninstallation failed on Windows when the npm
command path contained spaces (e.g., "C:\Program Files\nodejs\npm.cmd").

After this PR:
- OpenClawService.install() and OpenClawService.uninstall() use spawn
with shell: true when the npm path contains spaces, fixing the Windows
installation failure.

Fixes #12945, #12935, #12963

### Why we need it and why it was done in this way

- Using spawn with shell:true for npm paths containing spaces ensures
the command is executed correctly on Windows without changing
crossPlatformSpawn or other callers.
- Scope limited to install() and uninstall() to minimize impact.

### Breaking changes

- None.

### Special notes for your reviewer

- Change is minimal and targeted to OpenClawService install/uninstall
logic.

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [ ] Refactor: Left the code cleaner than you found it
- [ ] Upgrade: Impact of this change on upgrade flows was considered
- [x] Documentation: No user-guide update required

Release note

```release-note
fix(openclaw): handle npm paths with spaces on Windows during install/uninstall
```
This commit is contained in:
George·Dong
2026-02-21 14:13:59 +08:00
committed by GitHub
parent 38c29c194e
commit 9c44d4562c

View File

@@ -247,14 +247,18 @@ class OpenClawService {
// Keep the command string for logging and sudo retry
const npmCommand = `"${npmPath}" install -g ${packageName} ${registryArg}`.trim()
logger.info(`Installing OpenClaw with command: ${npmPath} ${npmArgs.join(' ')}`)
this.sendInstallProgress(`Running: ${npmPath} ${npmArgs.join(' ')}`)
// On Windows, wrap npm path in quotes if it contains spaces and is not already quoted
const needsQuotes = isWin && npmPath.includes(' ') && !npmPath.startsWith('"')
const processedNpmPath = needsQuotes ? `"${npmPath}"` : npmPath
logger.info(`Installing OpenClaw with command: ${processedNpmPath} ${npmArgs.join(' ')}`)
this.sendInstallProgress(`Running: ${processedNpmPath} ${npmArgs.join(' ')}`)
const spawnEnv = await getShellEnv()
return new Promise((resolve) => {
try {
const installProcess = crossPlatformSpawn(npmPath, npmArgs, { env: spawnEnv })
const installProcess = crossPlatformSpawn(processedNpmPath, npmArgs, { env: spawnEnv })
let stderr = ''
@@ -346,14 +350,18 @@ class OpenClawService {
// Keep the command string for logging and sudo retry
const npmCommand = `"${npmPath}" uninstall -g openclaw @qingchencloud/openclaw-zh`
logger.info(`Uninstalling OpenClaw with command: ${npmPath} ${npmArgs.join(' ')}`)
this.sendInstallProgress(`Running: ${npmPath} ${npmArgs.join(' ')}`)
// On Windows, wrap npm path in quotes if it contains spaces and is not already quoted
const needsQuotes = isWin && npmPath.includes(' ') && !npmPath.startsWith('"')
const processedNpmPath = needsQuotes ? `"${npmPath}"` : npmPath
logger.info(`Uninstalling OpenClaw with command: ${processedNpmPath} ${npmArgs.join(' ')}`)
this.sendInstallProgress(`Running: ${processedNpmPath} ${npmArgs.join(' ')}`)
const shellEnv = await getShellEnv()
return new Promise((resolve) => {
try {
const uninstallProcess = crossPlatformSpawn(npmPath, npmArgs, { env: shellEnv })
const uninstallProcess = crossPlatformSpawn(processedNpmPath, npmArgs, { env: shellEnv })
let stderr = ''