openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
160 lines (159 loc) • 5.57 kB
JavaScript
import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { isAbsolute, join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
//#region scripts/lib/bun-cli-launcher.mjs
const HEADER = "#!/bin/sh\n# OpenClaw Bun launcher\n";
const BODY = `bun= entry= records=0 malformed=
while IFS= read -r line; do
case $line in
'#openclaw-bun='*)
[ "$records" -eq 0 ] || malformed=1
bun=\${line#'#openclaw-bun='}
records=1 ;;
'#openclaw-entry='*)
[ "$records" -eq 1 ] || malformed=1
entry=\${line#'#openclaw-entry='}
records=2 ;;
*) [ "$records" -eq 0 ] || malformed=1 ;;
esac
done < "$0"
if [ "$records" -ne 2 ] || [ -n "$malformed" ] || [ ! -x "$bun" ] || [ ! -f "$entry" ]; then
printf 'openclaw: Bun launcher target not found (runtime: %s, entry: %s). Run "openclaw doctor" with Bun to repair.\\n' "$bun" "$entry" >&2
exit 127
fi
exec "$bun" "$entry" "$@"
exit 127
`;
/** @param {{bunPath: string, entryPath: string}} target @returns {string | null} */
function getBunCliLauncherPathIssue(target) {
const unsupported = [["\n", "a newline"], ["\r", "a carriage return"]];
for (const [label, value] of [["Bun executable path", target.bunPath], ["Install path", target.entryPath]]) {
if (!isAbsolute(value) || value.includes("\0")) return "Bun CLI launcher requires absolute, single-line runtime and entry paths";
for (const [character, name] of unsupported) if (value.includes(character)) return `${label} contains ${name}, which cannot be stored in a launcher data line`;
}
return null;
}
/** @param {{bunPath: string, entryPath: string}} target */
function renderBunCliLauncher(target) {
const issue = getBunCliLauncherPathIssue(target);
if (issue) throw new Error(issue);
return `${HEADER}${BODY}#openclaw-bun=${target.bunPath}\n#openclaw-entry=${target.entryPath}\n`;
}
/** @param {string} content */
function parseBunCliLauncher(content) {
if (!content.startsWith(HEADER)) return null;
try {
const match = /\n#openclaw-bun=([^\r\n]*)\n#openclaw-entry=([^\r\n]*)\n$/u.exec(content);
if (!match) return null;
const target = {
bunPath: match[1],
entryPath: match[2]
};
return renderBunCliLauncher(target) === content ? target : null;
} catch {
return null;
}
}
/** @param {{bunPath: string, env?: NodeJS.ProcessEnv, cwd?: string}} params */
function resolveBunGlobalBinDir(params) {
if (!isAbsolute(params.bunPath)) throw new Error("Bun CLI launcher requires an absolute Bun executable");
const result = spawnSync(params.bunPath, [
"pm",
"bin",
"-g"
], {
cwd: params.cwd,
env: params.env ?? process.env,
encoding: "utf8",
timeout: 1e4,
stdio: [
"ignore",
"pipe",
"pipe"
]
});
const binDir = result.stdout?.trim();
if (result.error || result.status !== 0 || !binDir || !isAbsolute(binDir) || /[\r\n]/u.test(binDir)) throw new Error("Could not resolve the owning Bun global bin directory with bun pm bin -g");
return binDir;
}
/** @param {string} left @param {string} right */
function samePath(left, right) {
if (resolve(left) === resolve(right)) return true;
try {
return realpathSync(left) === realpathSync(right);
} catch {
return false;
}
}
/** @param {{packageRoot: string, bunPath: string, binDir: string}} params */
function inspectBunCliLauncher(params) {
const target = {
bunPath: params.bunPath,
entryPath: join(params.packageRoot, "openclaw.mjs")
};
const content = renderBunCliLauncher(target);
const path = join(params.binDir, "openclaw");
let stat;
try {
stat = lstatSync(path);
} catch (error) {
if (error.code === "ENOENT") return {
path,
state: "missing"
};
throw error;
}
if (stat.isSymbolicLink()) return {
path,
state: samePath(path, target.entryPath) ? "stale" : "conflict"
};
if (!stat.isFile()) return {
path,
state: "conflict"
};
const existing = readFileSync(path, "utf8");
const recorded = parseBunCliLauncher(existing);
if (!recorded || !samePath(recorded.entryPath, target.entryPath)) return {
path,
state: "conflict"
};
return {
path,
state: existing === content && (stat.mode & 73) === 73 ? "current" : "stale"
};
}
/** Caller owns the selected package/bin pair; never follows the bin symlink when writing.
* @param {{packageRoot: string, bunPath: string, binDir: string}} params
*/
function installBunCliLauncher(params) {
const inspected = inspectBunCliLauncher(params);
if (inspected.state === "conflict") throw new Error(`Bun CLI launcher ${inspected.path} belongs to another command; left unchanged`);
if (inspected.state === "current") return inspected;
const content = renderBunCliLauncher({
bunPath: params.bunPath,
entryPath: join(params.packageRoot, "openclaw.mjs")
});
mkdirSync(params.binDir, { recursive: true });
const temporary = mkdtempSync(join(params.binDir, ".openclaw-launcher-"));
try {
const file = join(temporary, "openclaw");
writeFileSync(file, content, {
mode: 493,
flag: "wx"
});
chmodSync(file, 493);
if (inspectBunCliLauncher(params).state === "conflict") throw new Error(`Bun CLI launcher ${inspected.path} changed; left unchanged`);
renameSync(file, inspected.path);
} finally {
rmSync(temporary, {
recursive: true,
force: true
});
}
return {
path: inspected.path,
state: "current"
};
}
//#endregion
export { renderBunCliLauncher as a, parseBunCliLauncher as i, inspectBunCliLauncher as n, resolveBunGlobalBinDir as o, installBunCliLauncher as r, getBunCliLauncherPathIssue as t };