diff --git a/sdks/cli-shared/package.json b/sdks/cli-shared/package.json index 896c536..35e005d 100644 --- a/sdks/cli-shared/package.json +++ b/sdks/cli-shared/package.json @@ -15,7 +15,7 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", + "import": "./dist/index.js", "require": "./dist/index.cjs" } }, diff --git a/sdks/cli-shared/src/index.ts b/sdks/cli-shared/src/index.ts index 3696364..da2773f 100644 --- a/sdks/cli-shared/src/index.ts +++ b/sdks/cli-shared/src/index.ts @@ -10,18 +10,54 @@ export type NonExecutableBinaryMessageOptions = { genericInstallCommands?: string[]; }; +export type FsSubset = { + accessSync: (path: string, mode?: number) => void; + chmodSync: (path: string, mode: number) => void; + constants: { X_OK: number }; +}; + export function isBunRuntime(): boolean { - if (typeof process?.versions?.bun === "string") return true; - const userAgent = process?.env?.npm_config_user_agent || ""; - return userAgent.includes("bun/"); + if (typeof process?.versions?.bun === "string") return true; + const userAgent = process?.env?.npm_config_user_agent || ""; + return userAgent.includes("bun/"); } const PERMISSION_ERRORS = new Set(["EACCES", "EPERM", "ENOEXEC"]); -export function isPermissionError(error: unknown): boolean { - if (!error || typeof error !== "object") return false; - const code = (error as { code?: unknown }).code; - return typeof code === "string" && PERMISSION_ERRORS.has(code); +function isPermissionError(error: unknown): boolean { + if (!error || typeof error !== "object") return false; + const code = (error as { code?: unknown }).code; + return typeof code === "string" && PERMISSION_ERRORS.has(code); +} + +/** + * Checks if a binary is executable and attempts to make it executable if not. + * Returns true if the binary is (or was made) executable, false if it couldn't + * be made executable due to permission errors. Throws for other errors. + * + * Requires fs to be passed in to avoid static imports that break browser builds. + */ +export function assertExecutable(binPath: string, fs: FsSubset): boolean { + if (process.platform === "win32") { + return true; + } + + try { + fs.accessSync(binPath, fs.constants.X_OK); + return true; + } catch { + // Not executable, try to fix + } + + try { + fs.chmodSync(binPath, 0o755); + return true; + } catch (error) { + if (isPermissionError(error)) { + return false; + } + throw error; + } } export function formatNonExecutableBinaryMessage( diff --git a/sdks/cli/bin/sandbox-agent b/sdks/cli/bin/sandbox-agent index 93cac38..66c1aac 100755 --- a/sdks/cli/bin/sandbox-agent +++ b/sdks/cli/bin/sandbox-agent @@ -1,8 +1,8 @@ #!/usr/bin/env node const { execFileSync } = require("child_process"); const { + assertExecutable, formatNonExecutableBinaryMessage, - isPermissionError, } = require("@sandbox-agent/cli-shared"); const fs = require("fs"); const path = require("path"); @@ -10,29 +10,27 @@ const path = require("path"); const TRUST_PACKAGES = "@sandbox-agent/cli-linux-x64 @sandbox-agent/cli-darwin-arm64 @sandbox-agent/cli-darwin-x64 @sandbox-agent/cli-win32-x64"; -function printExecutableHint(binPath) { - console.error( - formatNonExecutableBinaryMessage({ - binPath, - trustPackages: TRUST_PACKAGES, - bunInstallBlocks: [ - { - label: "Project install", - commands: [ - `bun pm trust ${TRUST_PACKAGES}`, - "bun add @sandbox-agent/cli", - ], - }, - { - label: "Global install", - commands: [ - `bun pm -g trust ${TRUST_PACKAGES}`, - "bun add -g @sandbox-agent/cli", - ], - }, - ], - }), - ); +function formatHint(binPath) { + return formatNonExecutableBinaryMessage({ + binPath, + trustPackages: TRUST_PACKAGES, + bunInstallBlocks: [ + { + label: "Project install", + commands: [ + `bun pm trust ${TRUST_PACKAGES}`, + "bun add @sandbox-agent/cli", + ], + }, + { + label: "Global install", + commands: [ + `bun pm -g trust ${TRUST_PACKAGES}`, + "bun add -g @sandbox-agent/cli", + ], + }, + ], + }); } const PLATFORMS = { @@ -53,29 +51,13 @@ try { const pkgPath = require.resolve(`${pkg}/package.json`); const bin = process.platform === "win32" ? "sandbox-agent.exe" : "sandbox-agent"; const binPath = path.join(path.dirname(pkgPath), "bin", bin); - if (process.platform !== "win32") { - try { - fs.accessSync(binPath, fs.constants.X_OK); - } catch (error) { - try { - fs.chmodSync(binPath, 0o755); - } catch (chmodError) { - if (isPermissionError(chmodError)) { - printExecutableHint(binPath); - } - console.error(`Failed to make ${binPath} executable.`); - throw chmodError; - } - } - } - try { - execFileSync(binPath, process.argv.slice(2), { stdio: "inherit" }); - } catch (execError) { - if (isPermissionError(execError)) { - printExecutableHint(binPath); - } - throw execError; + + if (!assertExecutable(binPath, fs)) { + console.error(formatHint(binPath)); + process.exit(1); } + + execFileSync(binPath, process.argv.slice(2), { stdio: "inherit" }); } catch (e) { if (e.status !== undefined) process.exit(e.status); throw e; diff --git a/sdks/typescript/src/spawn.ts b/sdks/typescript/src/spawn.ts index c879786..6323e08 100644 --- a/sdks/typescript/src/spawn.ts +++ b/sdks/typescript/src/spawn.ts @@ -1,8 +1,8 @@ import type { ChildProcess } from "node:child_process"; import type { AddressInfo } from "node:net"; import { + assertExecutable, formatNonExecutableBinaryMessage, - isPermissionError, } from "@sandbox-agent/cli-shared"; export type SandboxAgentSpawnLogMode = "inherit" | "pipe" | "silent"; @@ -73,29 +73,29 @@ export async function spawnSandboxAgent( throw new Error("sandbox-agent binary not found. Install @sandbox-agent/cli or set SANDBOX_AGENT_BIN."); } - if (process.platform !== "win32") { - try { - fs.accessSync(binaryPath, fs.constants.X_OK); - } catch (error) { - if (isPermissionError(error)) { - throw new Error( - formatNonExecutableBinaryMessage({ - binPath: binaryPath, - trustPackages: TRUST_PACKAGES, - bunInstallBlocks: [ - { - label: "Project install", - commands: [ - `bun pm trust ${TRUST_PACKAGES}`, - "bun add sandbox-agent", - ], - }, + if (!assertExecutable(binaryPath, fs)) { + throw new Error( + formatNonExecutableBinaryMessage({ + binPath: binaryPath, + trustPackages: TRUST_PACKAGES, + bunInstallBlocks: [ + { + label: "Project install", + commands: [ + `bun pm trust ${TRUST_PACKAGES}`, + "bun add sandbox-agent", ], - }), - ); - } - throw error; - } + }, + { + label: "Global install", + commands: [ + `bun pm -g trust ${TRUST_PACKAGES}`, + "bun add -g sandbox-agent", + ], + }, + ], + }), + ); } const stdio = logMode === "inherit" ? "inherit" : logMode === "silent" ? "ignore" : "pipe";