refactor: consolidate executable check into assertExecutable helper

- Add assertExecutable() to cli-shared that checks and attempts chmod
- Simplify CLI and SDK spawn code to use the shared helper
- Fix cli-shared package.json exports (.js not .mjs)
- Add global install instructions to SDK error message
This commit is contained in:
Nathan Flurry 2026-02-02 18:38:06 -08:00
parent 048dcc5693
commit 7f07428621
4 changed files with 95 additions and 77 deletions

View file

@ -15,7 +15,7 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},

View file

@ -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(