Skip to content

Commit 7d7b610

Browse files
committed
fix: handle bin-only runtime deps
1 parent 0ac1a07 commit 7d7b610

4 files changed

Lines changed: 119 additions & 10 deletions

File tree

scripts/install.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,5 +485,6 @@ function Main {
485485
return $true
486486
}
487487

488-
$installSucceeded = Main
488+
$mainResults = @(Main)
489+
$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true
489490
Complete-Install -Succeeded:$installSucceeded

src/plugins/bundled-runtime-deps-materialization.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ function hasInstalledRuntimeDepExportFiles(packageDir: string, rawExports: unkno
213213
}
214214

215215
function hasInstalledRuntimeDepEntryFiles(packageDir: string, packageJson: JsonObject): boolean {
216+
if (hasInstalledRuntimeDepBinFiles(packageDir, packageJson.bin)) {
217+
return true;
218+
}
216219
if (packageJson.exports !== undefined) {
217220
return hasInstalledRuntimeDepExportFiles(packageDir, packageJson.exports);
218221
}
@@ -223,6 +226,23 @@ function hasInstalledRuntimeDepEntryFiles(packageDir: string, packageJson: JsonO
223226
return hasRuntimeDepEntryFile(packageDir, "index");
224227
}
225228

229+
function collectRuntimeDepBinTargets(rawBin: unknown): string[] {
230+
if (typeof rawBin === "string" && rawBin.trim() !== "") {
231+
return [rawBin];
232+
}
233+
if (!isJsonObject(rawBin)) {
234+
return [];
235+
}
236+
return Object.values(rawBin).filter(
237+
(value): value is string => typeof value === "string" && value.trim() !== "",
238+
);
239+
}
240+
241+
function hasInstalledRuntimeDepBinFiles(packageDir: string, rawBin: unknown): boolean {
242+
const targets = collectRuntimeDepBinTargets(rawBin);
243+
return targets.some((target) => hasRuntimeDepEntryFile(packageDir, target));
244+
}
245+
226246
function isRuntimeDepSatisfied(rootDir: string, dep: { name: string; version: string }): boolean {
227247
const installed = readInstalledRuntimeDepPackage(rootDir, dep.name);
228248
if (!installed) {

src/plugins/bundled-runtime-deps.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,32 @@ describe("createBundledRuntimeDepsPackagePlan config policy", () => {
14581458
).not.toThrow();
14591459
});
14601460

1461+
it("accepts staged runtime deps that expose a package bin entry", () => {
1462+
const installRoot = makeTempDir();
1463+
const packageDir = path.join(installRoot, "node_modules", "@zed-industries", "codex-acp");
1464+
fs.mkdirSync(path.join(packageDir, "bin"), { recursive: true });
1465+
fs.writeFileSync(
1466+
path.join(packageDir, "package.json"),
1467+
JSON.stringify({
1468+
name: "@zed-industries/codex-acp",
1469+
version: "0.12.0",
1470+
bin: {
1471+
"codex-acp": "bin/codex-acp.js",
1472+
},
1473+
}),
1474+
"utf8",
1475+
);
1476+
fs.writeFileSync(path.join(packageDir, "bin", "codex-acp.js"), "#!/usr/bin/env node\n");
1477+
writeGeneratedRuntimeDepsManifest(installRoot, ["@zed-industries/codex-acp@0.12.0"]);
1478+
1479+
expect(isRuntimeDepsPlanMaterialized(installRoot, ["@zed-industries/codex-acp@0.12.0"])).toBe(
1480+
true,
1481+
);
1482+
expect(() =>
1483+
assertBundledRuntimeDepsInstalled(installRoot, ["@zed-industries/codex-acp@0.12.0"]),
1484+
).not.toThrow();
1485+
});
1486+
14611487
it("accepts staged runtime deps with exported package entry files", () => {
14621488
const installRoot = makeTempDir();
14631489
const packageDir = path.join(installRoot, "node_modules", "alpha-runtime");
@@ -1645,6 +1671,29 @@ describe("createBundledRuntimeDepsPackagePlan config policy", () => {
16451671
);
16461672
});
16471673

1674+
it("reports staged runtime deps as missing when a package bin entry is absent", () => {
1675+
const installRoot = makeTempDir();
1676+
const packageDir = path.join(installRoot, "node_modules", "alpha-runtime");
1677+
fs.mkdirSync(packageDir, { recursive: true });
1678+
fs.writeFileSync(
1679+
path.join(packageDir, "package.json"),
1680+
JSON.stringify({
1681+
name: "alpha-runtime",
1682+
version: "1.0.0",
1683+
bin: {
1684+
"alpha-runtime": "bin/alpha-runtime.js",
1685+
},
1686+
}),
1687+
"utf8",
1688+
);
1689+
writeGeneratedRuntimeDepsManifest(installRoot, ["alpha-runtime@1.0.0"]);
1690+
1691+
expect(isRuntimeDepsPlanMaterialized(installRoot, ["alpha-runtime@1.0.0"])).toBe(false);
1692+
expect(() => assertBundledRuntimeDepsInstalled(installRoot, ["alpha-runtime@1.0.0"])).toThrow(
1693+
/alpha-runtime@1\.0\.0/,
1694+
);
1695+
});
1696+
16481697
it("reports staged runtime deps as missing when a declared entry file is absent", () => {
16491698
const packageRoot = setupPolicyPackageRoot();
16501699
const env = { OPENCLAW_PLUGIN_STAGE_DIR: makeTempDir() };

test/scripts/install-ps1.test.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { describe, expect, it } from "vitest";
55
import { createScriptTestHarness } from "./test-helpers";
66

77
const SCRIPT_PATH = "scripts/install.ps1";
8+
const ENTRYPOINT_RE =
9+
/\r?\n\$mainResults = @\(Main\)\r?\n\$installSucceeded = \$mainResults\.Count -gt 0 -and \$mainResults\[-1\] -eq \$true\r?\nComplete-Install -Succeeded:\$installSucceeded\s*$/m;
810

911
function extractFunctionBody(source: string, name: string): string {
1012
const match = source.match(
@@ -35,10 +37,7 @@ function toPowerShellSingleQuotedLiteral(value: string): string {
3537
}
3638

3739
function createFailingNodeFixture(source: string): string {
38-
const scriptWithoutEntryPoint = source.replace(
39-
/\r?\n\$installSucceeded = Main\r?\nComplete-Install -Succeeded:\$installSucceeded\s*$/m,
40-
"",
41-
);
40+
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
4241
expect(scriptWithoutEntryPoint).not.toBe(source);
4342

4443
return [
@@ -48,7 +47,8 @@ function createFailingNodeFixture(source: string): string {
4847
"function Ensure-ExecutionPolicy { return $true }",
4948
"function Ensure-Node { return $false }",
5049
"",
51-
"$installSucceeded = Main",
50+
"$mainResults = @(Main)",
51+
"$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true",
5252
"Complete-Install -Succeeded:$installSucceeded",
5353
"",
5454
].join("\n");
@@ -114,10 +114,7 @@ describe("install.ps1 failure handling", () => {
114114
runIfPowerShell("keeps npm chatter out of Main's success return value", () => {
115115
const tempDir = harness.createTempDir("openclaw-install-ps1-");
116116
const scriptPath = join(tempDir, "install.ps1");
117-
const scriptWithoutEntryPoint = source.replace(
118-
/\r?\n\$installSucceeded = Main\r?\nComplete-Install -Succeeded:\$installSucceeded\s*$/m,
119-
"",
120-
);
117+
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
121118
writeFileSync(
122119
scriptPath,
123120
[
@@ -149,4 +146,46 @@ describe("install.ps1 failure handling", () => {
149146
expect(result.status).toBe(0);
150147
expect(result.stderr).toBe("");
151148
});
149+
150+
runIfPowerShell("uses Main's final boolean result when helper output precedes success", () => {
151+
const tempDir = harness.createTempDir("openclaw-install-ps1-");
152+
const scriptPath = join(tempDir, "install.ps1");
153+
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
154+
writeFileSync(
155+
scriptPath,
156+
[
157+
scriptWithoutEntryPoint,
158+
"",
159+
"function Write-Banner { }",
160+
"function Ensure-ExecutionPolicy { return $true }",
161+
"function Ensure-Node { return $true }",
162+
"function Ensure-Git { return $true }",
163+
"function Add-ToPath { param([string]$Path) }",
164+
"function Install-OpenClawNpm {",
165+
" param([string]$Target = 'latest')",
166+
" Write-Output 'native chatter'",
167+
" return $true",
168+
"}",
169+
"function Invoke-NativeCommandCapture {",
170+
" param([string]$FilePath, [string[]]$Arguments)",
171+
" return @{ ExitCode = 0; Stdout = 'npm prefix'; Stderr = '' }",
172+
"}",
173+
"$NoOnboard = $true",
174+
"$mainResults = @(Main)",
175+
"$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true",
176+
"Complete-Install -Succeeded:$installSucceeded",
177+
"",
178+
].join("\n"),
179+
);
180+
chmodSync(scriptPath, 0o755);
181+
182+
const result = spawnSync(
183+
powershell!,
184+
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
185+
{ encoding: "utf8" },
186+
);
187+
188+
expect(result.status).toBe(0);
189+
expect(result.stderr).toBe("");
190+
});
152191
});

0 commit comments

Comments
 (0)