* Distribute platform binaries via optionalDependencies The npm wrapper used to download the matching platform binary from GitHub Releases in a postinstall step. That broke `bun i -g executor` two ways: bun blocks postinstalls by default, and the launcher's `#!/usr/bin/env node` shebang fails outright on machines without node. Mirror the pattern opencode and the esbuild/swc family use: - Each per-platform package (executor-linux-x64, executor-darwin-arm64, ...) is published to npm with `os`/`cpu` fields. The wrapper lists them all under `optionalDependencies`, so npm/bun's filtering only fetches the matching one. - The launcher walks parent `node_modules` for the platform package and execs its binary, with a glibc/musl preference chain on Linux. - Postinstall is a fast-path: it hardlinks the resolved binary to `bin/.executor` so subsequent launches skip the walk. It always exits 0 — failures are non-fatal because the launcher does the same walk at runtime, which also covers the bun-blocks-postinstalls case. - The legacy `bin/runtime/` lookup is kept as a fallback so the pkg.pr.new preview wrapper (which downloads per-PR builds from R2) keeps working unchanged. - `publish` now ships every platform package in parallel before the wrapper, so optionalDependencies always resolve to packages that exist on npm. For the no-node-at-all case (e.g. `bun i -g` without node installed), add `scripts/install.sh`: a POSIX installer that downloads the matching release tarball/zip into `~/.executor/bin` and PATH-appends. Same pattern opencode uses for its curl-installer path. Smoke test rewritten to exercise the new flow: build wrapper + current-platform package, set up the install layout `<root>/executor/node_modules/executor-<plat>-<arch>`, run the launcher, boot `executor web`, and verify the bundled UI serves. * install.sh: drop dead bin/ branch, the tarball is flat * Drop postinstall, async signal forwarding, remove secure-exec Codex-style cleanups on top of the optionalDependencies switch: - Drop the postinstall script entirely. The launcher already does the same require.resolve at runtime, so the bin/.executor cache only saves ~1ms per launch and adds surface area. Also removes the bun-blocks- postinstalls failure mode — there's no postinstall to block. - Switch the launcher from spawnSync to async spawn with explicit forwarding of SIGINT/SIGTERM/SIGHUP, so Ctrl-C reaches the binary cleanly during long-running commands like `executor web`. - Detect bun vs npm in the launcher and tell the user the right reinstall command if the platform binary is missing. Remove secure-exec, which is no longer used: - @executor-js/runtime-secure-exec had zero consumers in the codebase (only its own tests imported it). Delete the package, drop its entry from .changeset/config.json. - Remove resolveSecureExecV8 and secureExecBundlePlugin from build.ts; the binary no longer copies a secure-exec-v8 sidecar or runs the esbuild plugin that inlined @secure-exec/nodejs polyfills + bridge code. - Drop the secure-exec mention from main.ts's PATH-augmentation comment and from install.sh's sidecar comment.
Showing the changes to apps/cli/src (2 of 12 files). Show all
Unified · Split
| @@ -106,33 +106,6 @@ const binaryName = (t: Target) => (t.os === "win32" ? "executor.exe" : "executor · 105 unmodified lines | ||
| 106 | 106 | const isCurrentPlatform = (t: Target) => |
| 107 | 107 | t.os === process.platform && t.arch === process.arch && !t.abi; |
| 108 | 108 | |
| 109 | −/** Resolve the platform-specific secure-exec-v8 binary for a given target. */ | |
| 110 | −const resolveSecureExecV8 = (t: Target): string | null => { | |
| 111 | − const platformMap: Record<string, string> = { | |
| 112 | − "darwin-arm64": "@secure-exec/v8-darwin-arm64", | |
| 113 | − "darwin-x64": "@secure-exec/v8-darwin-x64", | |
| 114 | − "linux-arm64": "@secure-exec/v8-linux-arm64-gnu", | |
| 115 | − "linux-x64": "@secure-exec/v8-linux-x64-gnu", | |
| 116 | − }; | |
| 117 | − const key = `${t.os}-${t.arch}`; | |
| 118 | − const pkg = platformMap[key]; | |
| 119 | − if (!pkg) return null; | |
| 120 | − try { | |
| 121 | − // Resolve from @secure-exec/v8 which has these as optional deps | |
| 122 | − const req = createRequire(join(repoRoot, "node_modules", "secure-exec", "package.json")); | |
| 123 | − const pkgJson = req.resolve(`${pkg}/package.json`); | |
| 124 | − return join(dirname(pkgJson), "secure-exec-v8"); | |
| 125 | − } catch { | |
| 126 | − // Try bun's flat node_modules layout | |
| 127 | − const bunPath = join( | |
| 128 | − repoRoot, | |
| 129 | − `node_modules/.bun/${pkg.replace("/", "+")}@0.2.1/node_modules/${pkg}/secure-exec-v8`, | |
| 130 | − ); | |
| 131 | − if (existsSync(bunPath)) return bunPath; | |
| 132 | − return null; | |
| 133 | − } | |
| 134 | −}; | |
| 135 | − | |
| 136 | 109 | /** |
| 137 | 110 | * Resolve the platform-specific @napi-rs/keyring native binding for a target. |
| 138 | 111 | * |
| @@ -260,102 +233,6 @@ const createEmbeddedMigrationsSource = async () => { · 121 unmodified lines | ||
| 260 | 233 | ].join("\n"); |
| 261 | 234 | }; |
| 262 | 235 | |
| 263 | −// --------------------------------------------------------------------------- | |
| 264 | −// Bun.build plugin for @secure-exec bundling issues | |
| 265 | −// --------------------------------------------------------------------------- | |
| 266 | − | |
| 267 | −/** | |
| 268 | − * Plugin that fixes two issues with @secure-exec in compiled binaries: | |
| 269 | − * | |
| 270 | − * 1. node-stdlib-browser / web-streams-polyfill eagerly call require.resolve() | |
| 271 | − * at import time, which fails in bunfs. We stub these out since our code | |
| 272 | − * never uses the polyfill functions. | |
| 273 | − * | |
| 274 | − * 2. bridge-loader.js reads bridge.js from disk via fs.readFileSync using | |
| 275 | − * __dirname. In a compiled binary __dirname points into bunfs where the | |
| 276 | − * file doesn't exist. We replace bridge-loader with a version that embeds | |
| 277 | − * bridge.js content directly. | |
| 278 | − */ | |
| 279 | −const secureExecBundlePlugin = async (): Promise<import("bun").BunPlugin> => { | |
| 280 | − // Read files at build time so we can inline them into the compiled binary. | |
| 281 | − // bridge.js is loaded via fs.readFileSync at runtime, which fails in bunfs. | |
| 282 | − // bridgeAttach comes from @secure-exec/core's generated isolate-runtime. | |
| 283 | − const secureExecNodejs = join( | |
| 284 | − repoRoot, | |
| 285 | − "node_modules/.bun/@secure-exec+nodejs@0.2.1/node_modules/@secure-exec/nodejs", | |
| 286 | − ); | |
| 287 | − const secureExecCore = join( | |
| 288 | − repoRoot, | |
| 289 | − "node_modules/.bun/@secure-exec+core@0.2.1/node_modules/@secure-exec/core", | |
| 290 | − ); | |
| 291 | − const bridgeCode = await Bun.file(join(secureExecNodejs, "dist/bridge.js")).text(); | |
| 292 | − const isolateRuntime = await import(join(secureExecCore, "dist/generated/isolate-runtime.js")); | |
| 293 | − const bridgeAttachCode = isolateRuntime.ISOLATE_RUNTIME_SOURCES.bridgeAttach; | |
| 294 | − const polyfillCodeMap = (await import(join(secureExecCore, "dist/generated/polyfills.js"))) | |
| 295 | − .POLYFILL_CODE_MAP as Record<string, string>; | |
| 296 | − | |
| 297 | − return { | |
| 298 | − name: "secure-exec-bundle-fixes", | |
| 299 | − setup(build) { | |
| 300 | − // Stub polyfill modules that fail at import time in compiled binaries | |
| 301 | − const stubTargets = /node-stdlib-browser|web-streams-polyfill/; | |
| 302 | − build.onResolve({ filter: stubTargets }, (args) => ({ | |
| 303 | − path: args.path, | |
| 304 | − namespace: "stub", | |
| 305 | − })); | |
| 306 | − // Replace @secure-exec/nodejs polyfills (which use node-stdlib-browser + esbuild | |
| 307 | − // at runtime) with a shim that serves from pre-bundled POLYFILL_CODE_MAP. | |
| 308 | − build.onResolve({ filter: /polyfills/ }, (args) => { | |
| 309 | − if ( | |
| 310 | − args.importer.includes("@secure-exec/nodejs") || | |
| 311 | − args.importer.includes("@secure-exec+nodejs") | |
| 312 | − ) { | |
| 313 | − return { path: args.path, namespace: "polyfills-shim" }; | |
| 314 | − } | |
| 315 | − }); | |
| 316 | − build.onLoad({ filter: /.*/, namespace: "polyfills-shim" }, () => ({ | |
| 317 | − contents: ` | |
| 318 | −const POLYFILL_CODE_MAP = ${JSON.stringify(polyfillCodeMap)}; | |
| 319 | −const polyfillCache = new Map(); | |
| 320 | −export default {}; | |
| 321 | −export async function bundlePolyfill(moduleName) { | |
| 322 | − const cached = polyfillCache.get(moduleName); | |
| 323 | − if (cached) return cached; | |
| 324 | − const code = POLYFILL_CODE_MAP[moduleName]; | |
| 325 | − if (!code) throw new Error("No polyfill available for module: " + moduleName); | |
| 326 | − polyfillCache.set(moduleName, code); | |
| 327 | − return code; | |
| 328 | −} | |
| 329 | −export function getAvailableStdlib() { return Object.keys(POLYFILL_CODE_MAP); } | |
| 330 | −export function hasPolyfill(name) { return name.replace(/^node:/, "") in POLYFILL_CODE_MAP; } | |
| 331 | −export async function prebundleAllPolyfills() { return { ...POLYFILL_CODE_MAP }; } | |
| 332 | − `, | |
| 333 | − loader: "js", | |
| 334 | − })); | |
| 335 | − build.onLoad({ filter: /.*/, namespace: "stub" }, () => ({ | |
| 336 | − contents: "export default {};", | |
| 337 | − loader: "js", | |
| 338 | − })); | |
| 339 | − | |
| 340 | − // Replace bridge-loader with pre-read content | |
| 341 | − build.onResolve({ filter: /bridge-loader/ }, (args) => { | |
| 342 | − if (args.importer.includes("secure-exec")) { | |
| 343 | − return { path: args.path, namespace: "bridge" }; | |
| 344 | − } | |
| 345 | − }); | |
| 346 | − build.onLoad({ filter: /.*/, namespace: "bridge" }, () => ({ | |
| 347 | − contents: ` | |
| 348 | −const bridgeCode = ${JSON.stringify(bridgeCode)}; | |
| 349 | −const bridgeAttachCode = ${JSON.stringify(bridgeAttachCode)}; | |
| 350 | −export function getRawBridgeCode() { return bridgeCode; } | |
| 351 | −export function getBridgeAttachCode() { return bridgeAttachCode; } | |
| 352 | − `, | |
| 353 | − loader: "js", | |
| 354 | − })); | |
| 355 | − }, | |
| 356 | − }; | |
| 357 | −}; | |
| 358 | − | |
| 359 | 236 | // --------------------------------------------------------------------------- |
| 360 | 237 | // Build platform binaries |
| 361 | 238 | // --------------------------------------------------------------------------- |
| @@ -372,10 +249,9 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { · 10 unmodified lines | ||
| 372 | 249 | await rm(distDir, { recursive: true, force: true }); |
| 373 | 250 | |
| 374 | 251 | // Cross-platform builds need every target's optional native packages |
| 375 | − // (e.g. @napi-rs/keyring-darwin-arm64, @secure-exec/v8-win32-x64) so we | |
| 376 | − // can copy the right .node / binary next to each target's executor. | |
| 377 | − // `bun install --frozen-lockfile --cpu=* --os=*` extracts them all | |
| 378 | − // without modifying the lockfile. | |
| 252 | + // (e.g. @napi-rs/keyring-darwin-arm64) so we can copy the right .node next | |
| 253 | + // to each target's executor. `bun install --frozen-lockfile --cpu=* --os=*` | |
| 254 | + // extracts them all without modifying the lockfile. | |
| 379 | 255 | const needsCrossPlatform = targets.some((t) => !isCurrentPlatform(t)); |
| 380 | 256 | if (needsCrossPlatform) { |
| 381 | 257 | console.log("Installing optional native deps for all platforms..."); |
| @@ -414,20 +290,11 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { · 32 unmodified lines | ||
| 414 | 290 | target: bunTarget(target), |
| 415 | 291 | outfile: join(binDir, binaryName(target)), |
| 416 | 292 | }, |
| 417 | − plugins: [await secureExecBundlePlugin()], | |
| 418 | 293 | }); |
| 419 | 294 | |
| 420 | 295 | // Copy QuickJS WASM next to binary — loaded at runtime by the server |
| 421 | 296 | await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm")); |
| 422 | 297 | |
| 423 | − // Copy secure-exec-v8 binary next to executor — needed for code execution | |
| 424 | − const secureExecBin = resolveSecureExecV8(target); | |
| 425 | − if (secureExecBin && existsSync(secureExecBin)) { | |
| 426 | − const destName = target.os === "win32" ? "secure-exec-v8.exe" : "secure-exec-v8"; | |
| 427 | − await cp(secureExecBin, join(binDir, destName)); | |
| 428 | − await chmod(join(binDir, destName), 0o755); | |
| 429 | − } | |
| 430 | − | |
| 431 | 298 | // Copy @napi-rs/keyring native binding next to executor — bun --compile |
| 432 | 299 | // doesn't bundle .node files, so the loader needs to find it on disk |
| 433 | 300 | // via NAPI_RS_NATIVE_LIBRARY_PATH (set in main.ts). |
| @@ -444,16 +311,23 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { · 10 unmodified lines | ||
| 444 | 311 | console.log(` OK: ${version.trim()}`); |
| 445 | 312 | } |
| 446 | 313 | |
| 447 | − // Platform package.json | |
| 314 | + // Platform package.json. No `bin` field on purpose — the wrapper's | |
| 315 | + // launcher walks node_modules to locate `bin/executor` directly. Adding | |
| 316 | + // a bin here would race with the wrapper for the global `executor` | |
| 317 | + // entry under flat installs. | |
| 448 | 318 | await writeFile( |
| 449 | 319 | join(outDir, "package.json"), |
| 450 | 320 | JSON.stringify( |
| 451 | 321 | { |
| 452 | 322 | name, |
| 453 | 323 | version: meta.version, |
| 324 | + description: `${meta.description} (${name})`, | |
| 454 | 325 | os: [target.os], |
| 455 | 326 | cpu: [target.arch], |
| 456 | − bin: { executor: `bin/${binaryName(target)}` }, | |
| 327 | + homepage: meta.homepage, | |
| 328 | + bugs: meta.bugs, | |
| 329 | + repository: meta.repository, | |
| 330 | + license: meta.license, | |
| 457 | 331 | }, |
| 458 | 332 | null, |
| 459 | 333 | 2, |
| @@ -481,14 +355,14 @@ const buildWrapperPackage = async (binaries: Record<string, string>) => { · 21 unmodified lines | ||
| 481 | 355 | |
| 482 | 356 | await mkdir(binDir, { recursive: true }); |
| 483 | 357 | |
| 484 | − // Node.js shim that spawns the downloaded platform binary | |
| 358 | + // Node.js launcher — resolves the platform binary via require.resolve | |
| 359 | + // against optionalDependencies and execs it. No postinstall: the binary | |
| 360 | + // ships as an os/cpu-filtered optional dep, the launcher resolves it at | |
| 361 | + // runtime. This works whether or not the package manager runs postinstalls | |
| 362 | + // (bun blocks them by default). | |
| 485 | 363 | await writeFile(join(binDir, "executor"), NODE_SHIM); |
| 486 | 364 | await chmod(join(binDir, "executor"), 0o755); |
| 487 | 365 | |
| 488 | − // Postinstall downloads the matching release asset from GitHub Releases | |
| 489 | − await writeFile(join(wrapperDir, "postinstall.cjs"), POSTINSTALL_SCRIPT); | |
| 490 | − | |
| 491 | − // Package.json | |
| 492 | 366 | await writeFile( |
| 493 | 367 | join(wrapperDir, "package.json"), |
| 494 | 368 | JSON.stringify( |
| @@ -502,9 +376,9 @@ const buildWrapperPackage = async (binaries: Record<string, string>) => { · 7 unmodified lines | ||
| 502 | 376 | repository: meta.repository, |
| 503 | 377 | license: meta.license, |
| 504 | 378 | bin: { executor: "bin/executor" }, |
| 505 | − scripts: { | |
| 506 | − postinstall: "node ./postinstall.cjs", | |
| 507 | − }, | |
| 379 | + // Per-platform compiled binaries. npm/bun install only the entry | |
| 380 | + // matching the current os/cpu; everything else is a no-op. | |
| 381 | + optionalDependencies: binaries, | |
| 508 | 382 | engines: { |
| 509 | 383 | node: ">=20", |
| 510 | 384 | }, |
| @@ -514,7 +388,6 @@ const buildWrapperPackage = async (binaries: Record<string, string>) => { · 3 unmodified lines | ||
| 514 | 388 | ) + "\n", |
| 515 | 389 | ); |
| 516 | 390 | |
| 517 | − // README | |
| 518 | 391 | const readmePath = join(repoRoot, "README.md"); |
| 519 | 392 | if (existsSync(readmePath)) { |
| 520 | 393 | await cp(readmePath, join(wrapperDir, "README.md")); |
| @@ -522,7 +395,7 @@ const buildWrapperPackage = async (binaries: Record<string, string>) => { · 1 unmodified line | ||
| 522 | 395 | |
| 523 | 396 | console.log(`\nWrapper package: ${wrapperDir}`); |
| 524 | 397 | console.log(` ${meta.name}@${meta.version}`); |
| 525 | − console.log(` release assets: ${Object.keys(binaries).join(", ")}`); | |
| 398 | + console.log(` optionalDependencies: ${Object.keys(binaries).join(", ")}`); | |
| 526 | 399 | }; |
| 527 | 400 | |
| 528 | 401 | // --------------------------------------------------------------------------- |
| @@ -669,9 +542,20 @@ const publishPackedPackage = async (pkgDir: string, channel: string) => { · 140 unmodified lines | ||
| 669 | 542 | const publish = async (channel: string) => { |
| 670 | 543 | const meta = await readMetadata(); |
| 671 | 544 | |
| 672 | − // Publish only the wrapper npm package. Platform binaries are distributed via GitHub release assets. | |
| 545 | + // Publish per-platform packages first so the wrapper's | |
| 546 | + // optionalDependencies resolve to packages that already exist on npm | |
| 547 | + // by the time anyone installs the wrapper. | |
| 548 | + const platformDirs: string[] = []; | |
| 549 | + for (const entry of new Bun.Glob("executor-*/package.json").scanSync({ cwd: distDir })) { | |
| 550 | + platformDirs.push(join(distDir, dirname(entry))); | |
| 551 | + } | |
| 552 | + platformDirs.sort(); | |
| 553 | + | |
| 554 | + console.log(`Publishing ${platformDirs.length} platform package(s)...`); | |
| 555 | + await Promise.all(platformDirs.map((dir) => publishPackedPackage(dir, channel))); | |
| 556 | + | |
| 673 | 557 | const wrapperDir = join(distDir, meta.name); |
| 674 | − console.log(`Publishing ${wrapperDir}...`); | |
| 558 | + console.log(`Publishing wrapper ${wrapperDir}...`); | |
| 675 | 559 | await publishPackedPackage(wrapperDir, channel); |
| 676 | 560 | }; |
| 677 | 561 | |
| @@ -707,107 +591,48 @@ const createReleaseAssets = async () => { · 29 unmodified lines | ||
| 707 | 591 | }; |
| 708 | 592 | |
| 709 | 593 | // --------------------------------------------------------------------------- |
| 710 | −// Node.js shim — finds the right platform binary and spawns it | |
| 594 | +// Node.js launcher — resolves the platform binary shipped as an | |
| 595 | +// optionalDependency and execs it. Resolution order: | |
| 596 | +// 1. EXECUTOR_BIN_PATH override | |
| 597 | +// 2. require.resolve("executor-<platform>-<arch>/package.json") | |
| 598 | +// 3. bin/runtime/<binary> (preview wrapper compat — pkg.pr.new previews | |
| 599 | +// download the binary here at install time instead of via | |
| 600 | +// optionalDependencies) | |
| 601 | +// | |
| 602 | +// Signals (SIGINT/SIGTERM/SIGHUP) are forwarded to the child so long-running | |
| 603 | +// commands like `executor web` shut down cleanly under Ctrl-C. | |
| 711 | 604 | // --------------------------------------------------------------------------- |
| 712 | 605 | |
| 713 | 606 | const NODE_SHIM = `#!/usr/bin/env node |
| 714 | 607 | const childProcess = require("child_process"); |
| 715 | 608 | const fs = require("fs"); |
| 716 | 609 | const path = require("path"); |
| 610 | +const os = require("os"); | |
| 717 | 611 | |
| 718 | −function run(target) { | |
| 719 | − const result = childProcess.spawnSync(target, process.argv.slice(2), { stdio: "inherit" }); | |
| 720 | − if (result.error) { | |
| 721 | − console.error(result.error.message); | |
| 722 | − process.exit(1); | |
| 723 | − } | |
| 724 | − process.exit(typeof result.status === "number" ? result.status : 0); | |
| 725 | −} | |
| 726 | − | |
| 727 | −if (process.env.EXECUTOR_BIN_PATH) run(process.env.EXECUTOR_BIN_PATH); | |
| 728 | − | |
| 729 | −const scriptDir = path.dirname(fs.realpathSync(__filename)); | |
| 730 | −const binary = process.platform === "win32" ? "executor.exe" : "executor"; | |
| 731 | −const runtimeBinary = path.join(scriptDir, "runtime", binary); | |
| 732 | −const legacyCached = path.join(scriptDir, process.platform === "win32" ? ".executor.exe" : ".executor"); | |
| 733 | − | |
| 734 | −const resolveInstalledBinary = () => { | |
| 735 | − if (fs.existsSync(runtimeBinary)) { | |
| 736 | − return runtimeBinary; | |
| 737 | − } | |
| 738 | − if (fs.existsSync(legacyCached)) { | |
| 739 | − return legacyCached; | |
| 740 | − } | |
| 741 | − return null; | |
| 742 | −}; | |
| 743 | − | |
| 744 | −const installIfNeeded = () => { | |
| 745 | − const existing = resolveInstalledBinary(); | |
| 746 | − if (existing) { | |
| 747 | − return existing; | |
| 748 | − } | |
| 749 | − | |
| 750 | − const installer = path.resolve(scriptDir, "..", "postinstall.cjs"); | |
| 751 | − if (!fs.existsSync(installer)) { | |
| 752 | − return null; | |
| 753 | − } | |
| 754 | − | |
| 755 | − console.error("executor binary is missing; downloading release asset..."); | |
| 756 | − const result = childProcess.spawnSync(process.execPath, [installer], { | |
| 757 | − stdio: "inherit", | |
| 758 | − env: process.env, | |
| 612 | +function spawnAndExit(target) { | |
| 613 | + const child = childProcess.spawn(target, process.argv.slice(2), { stdio: "inherit" }); | |
| 614 | + child.on("error", (err) => { console.error(err.message); process.exit(1); }); | |
| 615 | + const forward = (signal) => { if (!child.killed) { try { child.kill(signal); } catch {} } }; | |
| 616 | + ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => process.on(sig, () => forward(sig))); | |
| 617 | + child.on("exit", (code, signal) => { | |
| 618 | + if (signal) process.kill(process.pid, signal); | |
| 619 | + else process.exit(typeof code === "number" ? code : 0); | |
| 759 | 620 | }); |
| 760 | − | |
| 761 | − if (result.error) { | |
| 762 | − console.error(result.error.message); | |
| 763 | − process.exit(1); | |
| 764 | − } | |
| 765 | − | |
| 766 | − if (typeof result.status === "number" && result.status !== 0) { | |
| 767 | − process.exit(result.status); | |
| 768 | − } | |
| 769 | − | |
| 770 | − return resolveInstalledBinary(); | |
| 771 | −}; | |
| 772 | − | |
| 773 | −const installedBinary = installIfNeeded(); | |
| 774 | −if (!installedBinary) { | |
| 775 | − console.error("executor binary is missing. Reinstall the package or run 'npm rebuild executor'."); | |
| 776 | − process.exit(1); | |
| 777 | 621 | } |
| 778 | 622 | |
| 779 | −run(installedBinary); | |
| 780 | −`; | |
| 781 | − | |
| 782 | −// --------------------------------------------------------------------------- | |
| 783 | −// Postinstall — hardlink/copy the platform binary for fast startup | |
| 784 | −// --------------------------------------------------------------------------- | |
| 785 | − | |
| 786 | −const POSTINSTALL_SCRIPT = `#!/usr/bin/env node | |
| 787 | −const childProcess = require("child_process"); | |
| 788 | −const fs = require("fs"); | |
| 789 | −const path = require("path"); | |
| 790 | −const os = require("os"); | |
| 791 | − | |
| 792 | −const packageDir = path.dirname(fs.realpathSync(__filename)); | |
| 793 | −const binDir = path.join(packageDir, "bin"); | |
| 794 | −const runtimeDir = path.join(binDir, "runtime"); | |
| 795 | −const packageJson = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8")); | |
| 623 | +if (process.env.EXECUTOR_BIN_PATH) { | |
| 624 | + spawnAndExit(process.env.EXECUTOR_BIN_PATH); | |
| 625 | + return; | |
| 626 | +} | |
| 796 | 627 | |
| 797 | −const repositoryUrl = typeof packageJson.repository === "string" | |
| 798 | − ? packageJson.repository | |
| 799 | − : packageJson.repository && packageJson.repository.url; | |
| 800 | −const githubBase = String(packageJson.homepage || repositoryUrl || "https://github.com/RhysSullivan/executor") | |
| 801 | − .replace(/^git[+]/, "") | |
| 802 | − .replace(/.git$/, ""); | |
| 803 | −const version = packageJson.version; | |
| 628 | +const isWin = process.platform === "win32"; | |
| 629 | +const binary = isWin ? "executor.exe" : "executor"; | |
| 804 | 630 | |
| 631 | +// Package names use "windows" (not "win32") to match what the build script emits. | |
| 805 | 632 | const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" }; |
| 633 | +const archMap = { x64: "x64", arm64: "arm64" }; | |
| 806 | 634 | const platform = platformMap[os.platform()] || os.platform(); |
| 807 | −const arch = os.arch() === "arm64" ? "arm64" : "x64"; | |
| 808 | −const binary = platform === "windows" ? "executor.exe" : "executor"; | |
| 809 | −const cachedBinary = path.join(runtimeDir, binary); | |
| 810 | −const legacyCachedBinary = path.join(binDir, platform === "windows" ? ".executor.exe" : ".executor"); | |
| 635 | +const arch = archMap[os.arch()] || os.arch(); | |
| 811 | 636 | |
| 812 | 637 | const isMusl = (() => { |
| 813 | 638 | if (platform !== "linux") return false; |
| @@ -819,79 +644,61 @@ const isMusl = (() => { · 5 unmodified lines | ||
| 819 | 644 | return false; |
| 820 | 645 | })(); |
| 821 | 646 | |
| 822 | −const assetBase = (() => { | |
| 647 | +// Candidate package names in preference order. On linux a system might match | |
| 648 | +// glibc or musl; try musl first if detected, glibc first otherwise. | |
| 649 | +const candidates = (() => { | |
| 823 | 650 | const base = "executor-" + platform + "-" + arch; |
| 824 | − if (platform === "linux" && isMusl) return base + "-musl"; | |
| 825 | − return base; | |
| 826 | −})(); | |
| 827 | −const archiveName = platform === "linux" ? assetBase + ".tar.gz" : assetBase + ".zip"; | |
| 828 | −const downloadUrl = githubBase + "/releases/download/v" + version + "/" + archiveName; | |
| 829 | −const archivePath = path.join(packageDir, archiveName); | |
| 830 | − | |
| 831 | −const run = (command, args) => { | |
| 832 | − const result = childProcess.spawnSync(command, args, { stdio: "inherit" }); | |
| 833 | − if (result.error) throw result.error; | |
| 834 | − if (typeof result.status === "number" && result.status !== 0) { | |
| 835 | − throw new Error(command + " exited with code " + result.status); | |
| 836 | − } | |
| 837 | −}; | |
| 838 | − | |
| 839 | −const download = async () => { | |
| 840 | − const response = await fetch(downloadUrl, { redirect: "follow" }); | |
| 841 | − if (!response.ok) { | |
| 842 | − throw new Error("Failed to download " + downloadUrl + " (status " + response.status + ")"); | |
| 843 | − } | |
| 844 | − const arrayBuffer = await response.arrayBuffer(); | |
| 845 | − fs.writeFileSync(archivePath, Buffer.from(arrayBuffer)); | |
| 846 | −}; | |
| 847 | − | |
| 848 | −const extract = () => { | |
| 849 | − fs.mkdirSync(binDir, { recursive: true }); | |
| 850 | − fs.rmSync(runtimeDir, { recursive: true, force: true }); | |
| 851 | − fs.mkdirSync(runtimeDir, { recursive: true }); | |
| 852 | − | |
| 853 | 651 | if (platform === "linux") { |
| 854 | − run("tar", ["-xzf", archivePath, "-C", runtimeDir]); | |
| 855 | − return; | |
| 856 | − } | |
| 857 | − | |
| 858 | − if (platform === "darwin") { | |
| 859 | − run("unzip", ["-o", archivePath, "-d", runtimeDir]); | |
| 860 | − return; | |
| 652 | + return isMusl ? [base + "-musl", base] : [base, base + "-musl"]; | |
| 861 | 653 | } |
| 654 | + return [base]; | |
| 655 | +})(); | |
| 862 | 656 | |
| 863 | − const psCommand = [ | |
| 864 | − "$ErrorActionPreference = 'Stop'", | |
| 865 | − "Expand-Archive -LiteralPath '" + archivePath.replace(/'/g, "''") + "' -DestinationPath '" + runtimeDir.replace(/'/g, "''") + "' -Force", | |
| 866 | − ].join("; "); | |
| 867 | − const psArgs = ["-NoLogo", "-NoProfile", "-Command", psCommand]; | |
| 868 | − // Prefer pwsh (PowerShell 7+) which reliably has Expand-Archive; fall back to powershell.exe | |
| 869 | − const pwshResult = childProcess.spawnSync("pwsh", psArgs, { stdio: "inherit" }); | |
| 870 | − if (pwshResult.error || (typeof pwshResult.status === "number" && pwshResult.status !== 0)) { | |
| 871 | − run("powershell.exe", psArgs); | |
| 657 | +function detectPackageManager() { | |
| 658 | + const ua = process.env.npm_config_user_agent || ""; | |
| 659 | + if (/\\bbun\\//.test(ua)) return "bun"; | |
| 660 | + const execPath = process.env.npm_execpath || ""; | |
| 661 | + if (execPath.includes("bun")) return "bun"; | |
| 662 | + if (__dirname.includes(".bun/install/global") || __dirname.includes(".bun\\\\install\\\\global")) { | |
| 663 | + return "bun"; | |
| 872 | 664 | } |
| 873 | −}; | |
| 665 | + return ua ? "npm" : null; | |
| 666 | +} | |
| 874 | 667 | |
| 875 | −(async () => { | |
| 668 | +for (const name of candidates) { | |
| 876 | 669 | try { |
| 877 | − await download(); | |
| 878 | − extract(); | |
| 879 | − | |
| 880 | − if (!fs.existsSync(cachedBinary)) { | |
| 881 | − throw new Error("Expected extracted binary at " + cachedBinary); | |
| 882 | − } | |
| 883 | − | |
| 884 | − if (fs.existsSync(legacyCachedBinary)) { | |
| 885 | − fs.unlinkSync(legacyCachedBinary); | |
| 670 | + const pkgJson = require.resolve(name + "/package.json"); | |
| 671 | + const candidate = path.join(path.dirname(pkgJson), "bin", binary); | |
| 672 | + if (fs.existsSync(candidate)) { | |
| 673 | + spawnAndExit(candidate); | |
| 674 | + return; | |
| 886 | 675 | } |
| 887 | − if (platform !== "windows") fs.chmodSync(cachedBinary, 0o755); | |
| 888 | − fs.rmSync(archivePath, { force: true }); | |
| 889 | − console.log("executor: installed " + assetBase + " from GitHub Releases"); | |
| 890 | − } catch (error) { | |
| 891 | − console.error("executor postinstall failed:", error && error.message ? error.message : error); | |
| 892 | − process.exit(1); | |
| 676 | + } catch { | |
| 677 | + // package not installed for this platform; try next candidate | |
| 893 | 678 | } |
| 894 | −})(); | |
| 679 | +} | |
| 680 | + | |
| 681 | +// Preview wrapper compat: pkg.pr.new previews download the platform binary | |
| 682 | +// to bin/runtime/ rather than via optionalDependencies. | |
| 683 | +const scriptDir = path.dirname(fs.realpathSync(__filename)); | |
| 684 | +const previewBinary = path.join(scriptDir, "runtime", binary); | |
| 685 | +if (fs.existsSync(previewBinary)) { | |
| 686 | + spawnAndExit(previewBinary); | |
| 687 | + return; | |
| 688 | +} | |
| 689 | + | |
| 690 | +const pm = detectPackageManager(); | |
| 691 | +const reinstall = pm === "bun" | |
| 692 | + ? "bun install -g executor" | |
| 693 | + : pm === "npm" | |
| 694 | + ? "npm install -g executor" | |
| 695 | + : "reinstall executor"; | |
| 696 | +console.error( | |
| 697 | + "executor: could not locate a platform binary for " + os.platform() + "-" + os.arch() + ".\\n" + | |
| 698 | + "Tried optionalDependencies: " + candidates.map((n) => '"' + n + '"').join(", ") + "\\n" + | |
| 699 | + "To fix: " + reinstall | |
| 700 | +); | |
| 701 | +process.exit(1); | |
| 895 | 702 | `; |
| 896 | 703 | |
| 897 | 704 | // --------------------------------------------------------------------------- |
| @@ -1,6 +1,7 @@ | ||
| 1 | −// Ensure binaries next to the executor (e.g. secure-exec-v8) are on $PATH | |
| 2 | 1 | import { randomUUID } from "node:crypto"; |
| 3 | 2 | import { dirname, join, resolve } from "node:path"; |
| 3 | +// Make sibling binaries (if any are added later) discoverable on $PATH so | |
| 4 | +// child processes spawned without an absolute path still find them. | |
| 4 | 5 | const execDir = dirname(process.execPath); |
| 5 | 6 | if (process.env.PATH && !process.env.PATH.includes(execDir)) { |
| 6 | 7 | process.env.PATH = `${execDir}:${process.env.PATH}`; |