933 lines · 34.0 KB · TypeScript
RawDownload
import { cp, mkdir, rm, writeFile, chmod } from "node:fs/promises";import { existsSync } from "node:fs";import { createRequire } from "node:module";import { resolve, join, dirname } from "node:path";import { fileURLToPath } from "node:url";import { parseArgs } from "node:util";import { $ } from "bun"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");const cliRoot = resolve(repoRoot, "apps/cli");const webRoot = resolve(repoRoot, "apps/local");const distDir = resolve(cliRoot, "dist"); const resolveQuickJsWasmPath = (): string => {  const req = createRequire(join(repoRoot, "packages/kernel/runtime-quickjs/package.json"));  const quickJsPkg = req.resolve("quickjs-emscripten/package.json");  const wasmPath = resolve(    dirname(quickJsPkg),    "../@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.wasm",  );  if (!existsSync(wasmPath)) throw new Error(`QuickJS WASM not found at ${wasmPath}`);  return wasmPath;}; // ---------------------------------------------------------------------------// Metadata// --------------------------------------------------------------------------- const readMetadata = async () => {  const rootPkg = await Bun.file(join(repoRoot, "package.json")).json();  const cliPkg = await Bun.file(join(cliRoot, "package.json")).json();  return {    name: "executor",    version: process.env.EXECUTOR_VERSION ?? cliPkg.version ?? rootPkg.version ?? "0.0.0",    description:      rootPkg.description ?? "Local AI executor with a CLI, local API server, and web UI.",    keywords: rootPkg.keywords ?? [],    homepage: rootPkg.homepage,    bugs: rootPkg.bugs,    repository: rootPkg.repository,    license: rootPkg.license ?? "MIT",  };}; // ---------------------------------------------------------------------------// Targets// --------------------------------------------------------------------------- type Target = {  os: "linux" | "darwin" | "win32";  arch: "x64" | "arm64";  abi?: "musl";}; const ALL_TARGETS: Target[] = [  { os: "linux", arch: "x64" },  { os: "linux", arch: "arm64" },  { os: "linux", arch: "x64", abi: "musl" },  { os: "linux", arch: "arm64", abi: "musl" },  { os: "darwin", arch: "x64" },  { os: "darwin", arch: "arm64" },  { os: "win32", arch: "x64" },  { os: "win32", arch: "arm64" },]; const platformName = (t: Target) => (t.os === "win32" ? "windows" : t.os); const targetPackageName = (t: Target) =>  ["executor", platformName(t), t.arch, t.abi].filter(Boolean).join("-"); const bunTarget = (t: Target) => ["bun", platformName(t), t.arch, t.abi].filter(Boolean).join("-"); const binaryName = (t: Target) => (t.os === "win32" ? "executor.exe" : "executor"); const isCurrentPlatform = (t: Target) =>  t.os === process.platform && t.arch === process.arch && !t.abi; /** Resolve the platform-specific secure-exec-v8 binary for a given target. */const resolveSecureExecV8 = (t: Target): string | null => {  const platformMap: Record<string, string> = {    "darwin-arm64": "@secure-exec/v8-darwin-arm64",    "darwin-x64": "@secure-exec/v8-darwin-x64",    "linux-arm64": "@secure-exec/v8-linux-arm64-gnu",    "linux-x64": "@secure-exec/v8-linux-x64-gnu",  };  const key = `${t.os}-${t.arch}`;  const pkg = platformMap[key];  if (!pkg) return null;  try {    // Resolve from @secure-exec/v8 which has these as optional deps    const req = createRequire(join(repoRoot, "node_modules", "secure-exec", "package.json"));    const pkgJson = req.resolve(`${pkg}/package.json`);    return join(dirname(pkgJson), "secure-exec-v8");  } catch {    // Try bun's flat node_modules layout    const bunPath = join(      repoRoot,      `node_modules/.bun/${pkg.replace("/", "+")}@0.2.1/node_modules/${pkg}/secure-exec-v8`,    );    if (existsSync(bunPath)) return bunPath;    return null;  }}; // ---------------------------------------------------------------------------// Build mode// --------------------------------------------------------------------------- type BuildMode = "production" | "development"; // ---------------------------------------------------------------------------// Build web app// --------------------------------------------------------------------------- const buildWeb = async (mode: BuildMode) => {  const webDist = join(webRoot, "dist");  await rm(webDist, { recursive: true, force: true });   console.log(`Building web app (${mode})...`);  const proc = Bun.spawn(["bun", "run", "build", "--mode", mode], {    cwd: webRoot,    stdio: ["ignore", "inherit", "inherit"],    env: { ...process.env, NODE_ENV: mode },  });  if ((await proc.exited) !== 0) throw new Error("Web build failed");  return webDist;}; // ---------------------------------------------------------------------------// Embedded web UI — generates a virtual module that imports all web assets// using `with { type: "file" }` so Bun bakes them into the compiled binary.// --------------------------------------------------------------------------- const createEmbeddedWebUISource = async (mode: BuildMode) => {  const webDist = await buildWeb(mode);  const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: webDist })))    .map((f) => f.replaceAll("\\", "/"))    .sort();   const imports = files.map((file, i) => {    const spec = join(webDist, file).replaceAll("\\", "/");    return `import file_${i} from ${JSON.stringify(spec)} with { type: "file" };`;  });   const entries = files.map((file, i) => `  ${JSON.stringify(file)}: file_${i},`);   return [    "// Auto-generated — maps web UI paths to embedded file references",    ...imports,    "export default {",    ...entries,    "} as Record<string, string>;",  ].join("\n");}; // ---------------------------------------------------------------------------// Embedded drizzle migrations — inlined as text imports so drizzle's// `migrate()` (which reads a folder from disk) can be given a tmpdir// populated from the inlined contents at runtime.// --------------------------------------------------------------------------- const createEmbeddedMigrationsSource = async () => {  const migrationsDir = resolve(webRoot, "drizzle");  const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: migrationsDir })))    .map((f) => f.replaceAll("\\", "/"))    .sort();   const imports = files.map((file, i) => {    const spec = join(migrationsDir, file).replaceAll("\\", "/");    return `import file_${i} from ${JSON.stringify(spec)} with { type: "text" };`;  });   const entries = files.map((file, i) => `  ${JSON.stringify(file)}: file_${i},`);   return [    "// Auto-generated — maps migration paths to inlined file contents",    ...imports,    "export default {",    ...entries,    "} as Record<string, string>;",  ].join("\n");}; // ---------------------------------------------------------------------------// Bun.build plugin for @secure-exec bundling issues// --------------------------------------------------------------------------- /** * Plugin that fixes two issues with @secure-exec in compiled binaries: * * 1. node-stdlib-browser / web-streams-polyfill eagerly call require.resolve() *    at import time, which fails in bunfs. We stub these out since our code *    never uses the polyfill functions. * * 2. bridge-loader.js reads bridge.js from disk via fs.readFileSync using *    __dirname. In a compiled binary __dirname points into bunfs where the *    file doesn't exist. We replace bridge-loader with a version that embeds *    bridge.js content directly. */const secureExecBundlePlugin = async (): Promise<import("bun").BunPlugin> => {  // Read files at build time so we can inline them into the compiled binary.  // bridge.js is loaded via fs.readFileSync at runtime, which fails in bunfs.  // bridgeAttach comes from @secure-exec/core's generated isolate-runtime.  const secureExecNodejs = join(    repoRoot,    "node_modules/.bun/@secure-exec+nodejs@0.2.1/node_modules/@secure-exec/nodejs",  );  const secureExecCore = join(    repoRoot,    "node_modules/.bun/@secure-exec+core@0.2.1/node_modules/@secure-exec/core",  );  const bridgeCode = await Bun.file(join(secureExecNodejs, "dist/bridge.js")).text();  const isolateRuntime = await import(join(secureExecCore, "dist/generated/isolate-runtime.js"));  const bridgeAttachCode = isolateRuntime.ISOLATE_RUNTIME_SOURCES.bridgeAttach;  const polyfillCodeMap = (await import(join(secureExecCore, "dist/generated/polyfills.js")))    .POLYFILL_CODE_MAP as Record<string, string>;   return {    name: "secure-exec-bundle-fixes",    setup(build) {      // Stub polyfill modules that fail at import time in compiled binaries      const stubTargets = /node-stdlib-browser|web-streams-polyfill/;      build.onResolve({ filter: stubTargets }, (args) => ({        path: args.path,        namespace: "stub",      }));      // Replace @secure-exec/nodejs polyfills (which use node-stdlib-browser + esbuild      // at runtime) with a shim that serves from pre-bundled POLYFILL_CODE_MAP.      build.onResolve({ filter: /polyfills/ }, (args) => {        if (          args.importer.includes("@secure-exec/nodejs") ||          args.importer.includes("@secure-exec+nodejs")        ) {          return { path: args.path, namespace: "polyfills-shim" };        }      });      build.onLoad({ filter: /.*/, namespace: "polyfills-shim" }, () => ({        contents: `const POLYFILL_CODE_MAP = ${JSON.stringify(polyfillCodeMap)};const polyfillCache = new Map();export default {};export async function bundlePolyfill(moduleName) {  const cached = polyfillCache.get(moduleName);  if (cached) return cached;  const code = POLYFILL_CODE_MAP[moduleName];  if (!code) throw new Error("No polyfill available for module: " + moduleName);  polyfillCache.set(moduleName, code);  return code;}export function getAvailableStdlib() { return Object.keys(POLYFILL_CODE_MAP); }export function hasPolyfill(name) { return name.replace(/^node:/, "") in POLYFILL_CODE_MAP; }export async function prebundleAllPolyfills() { return { ...POLYFILL_CODE_MAP }; }        `,        loader: "js",      }));      build.onLoad({ filter: /.*/, namespace: "stub" }, () => ({        contents: "export default {};",        loader: "js",      }));       // Replace bridge-loader with pre-read content      build.onResolve({ filter: /bridge-loader/ }, (args) => {        if (args.importer.includes("secure-exec")) {          return { path: args.path, namespace: "bridge" };        }      });      build.onLoad({ filter: /.*/, namespace: "bridge" }, () => ({        contents: `const bridgeCode = ${JSON.stringify(bridgeCode)};const bridgeAttachCode = ${JSON.stringify(bridgeAttachCode)};export function getRawBridgeCode() { return bridgeCode; }export function getBridgeAttachCode() { return bridgeAttachCode; }        `,        loader: "js",      }));    },  };}; // ---------------------------------------------------------------------------// Build platform binaries// --------------------------------------------------------------------------- const EMBEDDED_WEB_UI_STUB = `const files: Record<string, string> | null = null;\n\nexport default files;\n`;const EMBEDDED_MIGRATIONS_STUB = `const migrations: Record<string, string> | null = null;\n\nexport default migrations;\n`; const buildBinaries = async (targets: Target[], mode: BuildMode) => {  const meta = await readMetadata();  const binaries: Record<string, string> = {};  const embeddedWebUIPath = join(cliRoot, "src/embedded-web-ui.gen.ts");  const embeddedMigrationsPath = join(webRoot, "src/server/embedded-migrations.gen.ts");   await rm(distDir, { recursive: true, force: true });   console.log(`Generating embedded web UI bundle (${mode})...`);  const embeddedWebUI = await createEmbeddedWebUISource(mode);  await writeFile(embeddedWebUIPath, `${embeddedWebUI}\n`);   console.log("Generating embedded drizzle migrations...");  const embeddedMigrations = await createEmbeddedMigrationsSource();  await writeFile(embeddedMigrationsPath, `${embeddedMigrations}\n`);   const quickJsWasmPath = resolveQuickJsWasmPath();   try {    for (const target of targets) {      const name = targetPackageName(target);      const outDir = join(distDir, name);      const binDir = join(outDir, "bin");      await mkdir(binDir, { recursive: true });       console.log(`Building ${name}...`);       await Bun.build({        entrypoints: [join(cliRoot, "src/main.ts")],        minify: mode === "production",        compile: {          // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Bun compile target string is dynamically constructed          target: bunTarget(target) as any,          outfile: join(binDir, binaryName(target)),        },        plugins: [await secureExecBundlePlugin()],      });       // Copy QuickJS WASM next to binary — loaded at runtime by the server      await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm"));       // Copy secure-exec-v8 binary next to executor — needed for code execution      const secureExecBin = resolveSecureExecV8(target);      if (secureExecBin && existsSync(secureExecBin)) {        const destName = target.os === "win32" ? "secure-exec-v8.exe" : "secure-exec-v8";        await cp(secureExecBin, join(binDir, destName));        await chmod(join(binDir, destName), 0o755);      }       // Smoke test on current platform      if (isCurrentPlatform(target)) {        const bin = join(binDir, binaryName(target));        console.log(`  Smoke test: ${bin} --version`);        const version = await $`${bin} --version`.text();        console.log(`  OK: ${version.trim()}`);      }       // Platform package.json      await writeFile(        join(outDir, "package.json"),        JSON.stringify(          {            name,            version: meta.version,            os: [target.os],            cpu: [target.arch],            bin: { executor: `bin/${binaryName(target)}` },          },          null,          2,        ) + "\n",      );       binaries[name] = meta.version;    }     return binaries;  } finally {    await writeFile(embeddedWebUIPath, EMBEDDED_WEB_UI_STUB);    await writeFile(embeddedMigrationsPath, EMBEDDED_MIGRATIONS_STUB);  }}; // ---------------------------------------------------------------------------// Build wrapper npm package// --------------------------------------------------------------------------- const buildWrapperPackage = async (binaries: Record<string, string>) => {  const meta = await readMetadata();  const wrapperDir = join(distDir, meta.name);  const binDir = join(wrapperDir, "bin");   await mkdir(binDir, { recursive: true });   // Node.js shim that spawns the downloaded platform binary  await writeFile(join(binDir, "executor"), NODE_SHIM);  await chmod(join(binDir, "executor"), 0o755);   // Postinstall downloads the matching release asset from GitHub Releases  await writeFile(join(wrapperDir, "postinstall.cjs"), POSTINSTALL_SCRIPT);   // Package.json  await writeFile(    join(wrapperDir, "package.json"),    JSON.stringify(      {        name: meta.name,        version: meta.version,        description: meta.description,        keywords: meta.keywords,        homepage: meta.homepage,        bugs: meta.bugs,        repository: meta.repository,        license: meta.license,        bin: { executor: "bin/executor" },        scripts: {          postinstall: "node ./postinstall.cjs",        },        engines: {          node: ">=20",        },      },      null,      2,    ) + "\n",  );   // README  const readmePath = join(repoRoot, "README.md");  if (existsSync(readmePath)) {    await cp(readmePath, join(wrapperDir, "README.md"));  }   console.log(`\nWrapper package: ${wrapperDir}`);  console.log(`  ${meta.name}@${meta.version}`);  console.log(`  release assets: ${Object.keys(binaries).join(", ")}`);}; // ---------------------------------------------------------------------------// Preview wrapper — a slim npm package for pkg.pr.new previews that fetches// the platform binary from our R2 bucket at install time.//// The release wrapper points postinstall at GitHub Releases. For previews// there's no release, so we upload per-PR tarballs to R2 (keyed by commit// SHA) and have a dedicated postinstall download from there.//// CI splits this in two://   1. A matrix job per platform runs `buildPreviewTarballs` to produce//      dist/previews/<platform>.tar.gz, which it uploads to R2.//   2. A single publish job runs `buildPreviewWrapperPackage` with an//      explicit list of platforms and hands the wrapper to pkg.pr.new.//// Both paths require EXECUTOR_PREVIEW_CDN_URL and EXECUTOR_PREVIEW_SHA so// the postinstall URL embeds the right commit.// --------------------------------------------------------------------------- const buildPreviewTarballs = async (binaries: Record<string, string>) => {  const previewDir = join(distDir, "previews");  await mkdir(previewDir, { recursive: true });  for (const platformPkg of Object.keys(binaries)) {    const srcBinDir = join(distDir, platformPkg, "bin");    const tarPath = join(previewDir, `${platformPkg}.tar.gz`);    await $`tar -czf ${tarPath} .`.cwd(srcBinDir).quiet();    console.log(`Preview tarball: ${tarPath}`);  }}; const resolveTargetsFromEnv = (env: string | undefined): Target[] => {  if (!env) throw new Error("EXECUTOR_PREVIEW_TARGETS must be set (comma-separated package names)");  const names = env.split(",").map((s) => s.trim()).filter(Boolean);  const resolved = names.map((name) => {    const match = ALL_TARGETS.find((t) => targetPackageName(t) === name);    if (!match) throw new Error(`Unknown preview target: ${name}`);    return match;  });  if (resolved.length === 0) throw new Error("EXECUTOR_PREVIEW_TARGETS resolved to an empty list");  return resolved;}; const buildPreviewWrapperPackage = async (targets: Target[]) => {  const meta = await readMetadata();  const cdnUrl = process.env.EXECUTOR_PREVIEW_CDN_URL?.replace(/\/+$/, "");  const sha = process.env.EXECUTOR_PREVIEW_SHA;  if (!cdnUrl || !sha) {    throw new Error(      "preview build requires EXECUTOR_PREVIEW_CDN_URL and EXECUTOR_PREVIEW_SHA to be set",    );  }   const wrapperDir = join(distDir, meta.name);  const binDir = join(wrapperDir, "bin");  await mkdir(binDir, { recursive: true });   await writeFile(join(binDir, "executor"), NODE_SHIM);  await chmod(join(binDir, "executor"), 0o755);   const postinstall = PREVIEW_POSTINSTALL_SCRIPT.replaceAll(    "__CDN_BASE_URL__",    `${cdnUrl}/${sha}`,  );  await writeFile(join(wrapperDir, "postinstall.cjs"), postinstall);   // Restrict os/cpu to platforms we actually built this run so npm refuses  // the install on anything else — a clear error beats a cryptic 404 from  // postinstall on an unbuilt platform.  const osList = Array.from(new Set(targets.map((t) => t.os)));  const cpuList = Array.from(new Set(targets.map((t) => t.arch)));   await writeFile(    join(wrapperDir, "package.json"),    JSON.stringify(      {        name: meta.name,        version: meta.version,

Lines 1–500 of 933.