mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:50:43 +00:00
fix(installer): preserve PowerShell host on failure
This commit is contained in:
@@ -78,6 +78,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/model runs: keep `openclaw infer model run` on explicit OpenRouter models from loading the full provider catalog or inheriting chat-agent silent-reply policy, restoring non-empty one-shot probe output. Fixes #68791. Thanks @limpredator.
|
||||
- Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming `node@24` was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io.
|
||||
- Installer: load nvm before Node.js detection so `curl | bash` installs respect nvm-managed Node instead of stale system Node. Fixes #49556. Thanks @heavenlxj.
|
||||
- Installer/Windows: route PowerShell install failures through a top-level handler so `iwr ... | iex` returns control to the current shell while direct script-file runs still exit non-zero. Fixes #38054. Thanks @PwrSrg.
|
||||
- CLI/Volta: respawn raw `openclaw` CLI runs through the named `node` shim when the current Node executable resolves to `volta-shim`, avoiding direct shim execution failures in non-interactive shells. Fixes #68672. Thanks @sanchezm86.
|
||||
- Installer: warn when multiple npm global roots contain OpenClaw installs, showing active Node/npm/openclaw plus each install path and version so stale version-manager installs are visible. Fixes #40839. Thanks @zhixianio.
|
||||
- Cron/tasks: recover completed cron task ledger records from durable run logs and job state before marking them `lost`, reducing false `backing session missing` audit errors for isolated cron runs and keeping offline CLI audit from treating its empty local cron active-job set as authoritative. Fixes #71963.
|
||||
|
||||
@@ -292,6 +292,9 @@ by default, plus git-checkout installs under the same prefix flow.
|
||||
- Refreshes a loaded gateway service best-effort (`openclaw gateway install --force`, then restart)
|
||||
- Runs `openclaw doctor --non-interactive` on upgrades and git installs (best effort)
|
||||
</Step>
|
||||
<Step title="Handle failures">
|
||||
`iwr ... | iex` and scriptblock installs report a terminating error without closing the current PowerShell session. Direct `powershell -File` / `pwsh -File` installs still exit non-zero for automation.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Examples (install.ps1)
|
||||
|
||||
@@ -384,6 +384,29 @@ function Add-ToPath {
|
||||
}
|
||||
}
|
||||
|
||||
$script:InstallExitCode = 0
|
||||
|
||||
function Fail-Install {
|
||||
param([int]$Code = 1)
|
||||
|
||||
$script:InstallExitCode = $Code
|
||||
return $false
|
||||
}
|
||||
|
||||
function Complete-Install {
|
||||
param([bool]$Succeeded)
|
||||
|
||||
if ($Succeeded) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($PSCommandPath) {
|
||||
exit $script:InstallExitCode
|
||||
}
|
||||
|
||||
throw "OpenClaw installation failed with exit code $($script:InstallExitCode)."
|
||||
}
|
||||
|
||||
# Main
|
||||
function Main {
|
||||
Write-Banner
|
||||
@@ -394,16 +417,16 @@ function Main {
|
||||
if (!(Ensure-ExecutionPolicy)) {
|
||||
Write-Host ""
|
||||
Write-Host "Installation cannot continue due to execution policy restrictions" -Level error
|
||||
exit 1
|
||||
return (Fail-Install)
|
||||
}
|
||||
|
||||
if (!(Ensure-Node)) {
|
||||
exit 1
|
||||
return (Fail-Install)
|
||||
}
|
||||
|
||||
if ($InstallMethod -eq "git") {
|
||||
if (!(Ensure-Git)) {
|
||||
exit 1
|
||||
return (Fail-Install)
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
@@ -421,7 +444,7 @@ function Main {
|
||||
Write-Host "[DRY RUN] Would install OpenClaw via npm ($((Resolve-PackageInstallSpec -Target $Tag)))" -Level info
|
||||
} else {
|
||||
if (!(Install-OpenClawNpm -Target $Tag)) {
|
||||
exit 1
|
||||
return (Fail-Install)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,6 +469,8 @@ function Main {
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🦞 OpenClaw installed successfully!" -Level success
|
||||
return $true
|
||||
}
|
||||
|
||||
Main
|
||||
$installSucceeded = Main
|
||||
Complete-Install -Succeeded:$installSucceeded
|
||||
|
||||
113
test/scripts/install-ps1.test.ts
Normal file
113
test/scripts/install-ps1.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createScriptTestHarness } from "./test-helpers";
|
||||
|
||||
const SCRIPT_PATH = "scripts/install.ps1";
|
||||
|
||||
function extractFunctionBody(source: string, name: string): string {
|
||||
const match = source.match(
|
||||
new RegExp(`^function ${name} \\{\\r?\\n([\\s\\S]*?)^\\}\\r?\\n`, "m"),
|
||||
);
|
||||
expect(match?.[1]).toBeDefined();
|
||||
return match![1];
|
||||
}
|
||||
|
||||
function findPowerShell(): string | undefined {
|
||||
for (const candidate of ["pwsh", "powershell"]) {
|
||||
const result = spawnSync(
|
||||
candidate,
|
||||
["-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
if (result.status === 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function toPowerShellSingleQuotedLiteral(value: string): string {
|
||||
return `'${value.replaceAll("'", "''")}'`;
|
||||
}
|
||||
|
||||
function createFailingNodeFixture(source: string): string {
|
||||
const scriptWithoutEntryPoint = source.replace(
|
||||
/\r?\n\$installSucceeded = Main\r?\nComplete-Install -Succeeded:\$installSucceeded\s*$/m,
|
||||
"",
|
||||
);
|
||||
expect(scriptWithoutEntryPoint).not.toBe(source);
|
||||
|
||||
return [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"function Write-Banner { }",
|
||||
"function Ensure-ExecutionPolicy { return $true }",
|
||||
"function Ensure-Node { return $false }",
|
||||
"",
|
||||
"$installSucceeded = Main",
|
||||
"Complete-Install -Succeeded:$installSucceeded",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
describe("install.ps1 failure handling", () => {
|
||||
const harness = createScriptTestHarness();
|
||||
const source = readFileSync(SCRIPT_PATH, "utf8");
|
||||
const powershell = findPowerShell();
|
||||
const runIfPowerShell = powershell ? it : it.skip;
|
||||
|
||||
it("does not exit directly from inside Main", () => {
|
||||
const mainBody = extractFunctionBody(source, "Main");
|
||||
expect(mainBody).not.toMatch(/\bexit\b/i);
|
||||
expect(mainBody).toContain("return (Fail-Install)");
|
||||
});
|
||||
|
||||
it("keeps failure termination in the top-level completion handler", () => {
|
||||
const completeInstallBody = extractFunctionBody(source, "Complete-Install");
|
||||
expect(completeInstallBody).toMatch(/\$PSCommandPath/);
|
||||
expect(completeInstallBody).toMatch(/\bexit \$script:InstallExitCode\b/);
|
||||
expect(completeInstallBody).toMatch(/\bthrow "OpenClaw installation failed with exit code/);
|
||||
});
|
||||
|
||||
runIfPowerShell("exits non-zero when run as a script file", () => {
|
||||
const tempDir = harness.createTempDir("openclaw-install-ps1-");
|
||||
const scriptPath = join(tempDir, "install.ps1");
|
||||
writeFileSync(scriptPath, createFailingNodeFixture(source));
|
||||
chmodSync(scriptPath, 0o755);
|
||||
|
||||
const result = spawnSync(
|
||||
powershell!,
|
||||
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
});
|
||||
|
||||
runIfPowerShell("throws without killing the caller when run as a scriptblock", () => {
|
||||
const tempDir = harness.createTempDir("openclaw-install-ps1-");
|
||||
const scriptPath = join(tempDir, "install.ps1");
|
||||
writeFileSync(scriptPath, createFailingNodeFixture(source));
|
||||
chmodSync(scriptPath, 0o755);
|
||||
|
||||
const command = [
|
||||
"try {",
|
||||
` & ([scriptblock]::Create((Get-Content -LiteralPath ${toPowerShellSingleQuotedLiteral(scriptPath)} -Raw)))`,
|
||||
"} catch {",
|
||||
' Write-Output "caught=$($_.Exception.Message)"',
|
||||
"}",
|
||||
'Write-Output "alive-after-install"',
|
||||
].join("\n");
|
||||
const result = spawnSync(powershell!, ["-NoLogo", "-NoProfile", "-Command", command], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("caught=OpenClaw installation failed with exit code 1.");
|
||||
expect(result.stdout).toContain("alive-after-install");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user