#!/usr/bin/env node import chalk from "chalk" import { execFileSync, execSync } from "child_process" import fsSync from "fs" import * as fs from "fs/promises" import { globby } from "globby" import { createRequire } from "module" import os from "os" import * as path from "path" import { rmrf } from "./file-utils.mjs" import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs" import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs" const require = createRequire(import.meta.url) const isWindows = process.platform === "win32" // Resolve the grpc-tools package root via its package.json (stable regardless of `main`), so we // can both locate the bundled protoc and re-run its install script when the binary is missing. const GRPC_TOOLS_DIR = path.dirname(require.resolve("grpc-tools/package.json")) const GRPC_TOOLS_PROTOC = path.join(GRPC_TOOLS_DIR, "bin", isWindows ? "protoc.exe" : "protoc") // Legacy compatibility: some older/local Windows setups provision protoc into tmp-protoc. // Prefer that path when present, but fall back to the grpc-tools bundled binary used by CI/npm installs. const LEGACY_WINDOWS_PROTOC = path.resolve("tmp-protoc/bin/protoc.exe") const PROTOC = isWindows && fsSync.existsSync(LEGACY_WINDOWS_PROTOC) ? LEGACY_WINDOWS_PROTOC : GRPC_TOOLS_PROTOC // `bun install` skips grpc-tools' `install` lifecycle script (`node-pre-gyp install`), so the prebuilt // protoc is never downloaded into bin/. When it's missing, run that same command here to fetch it. // grpc-tools depends on @mapbox/node-pre-gyp, which exposes the `node-pre-gyp` CLI. function resolveNodePreGypCli() { const candidates = ["@mapbox/node-pre-gyp/bin/node-pre-gyp", "node-pre-gyp/bin/node-pre-gyp"] for (const candidate of candidates) { try { // Resolve from the grpc-tools package (its direct dependency). return require.resolve(candidate, { paths: [GRPC_TOOLS_DIR] }) } catch { // Fall back to resolving from this script's location (covers hoisted installs). try { return require.resolve(candidate) } catch { // try the next candidate } } } return null } function ensureProtocBinary() { console.warn(chalk.yellow(`protoc not found at ${GRPC_TOOLS_PROTOC}; downloading the grpc-tools prebuilt binary...`)) const nodePreGypCli = resolveNodePreGypCli() if (!nodePreGypCli) { console.error( chalk.red( `Could not resolve the node-pre-gyp CLI from ${GRPC_TOOLS_DIR}. Run \`bun install\`, then retry \`bun run protos\`.`, ), ) process.exit(1) } try { // Mirrors grpc-tools' `scripts.install` ("node-pre-gyp install"): downloads the prebuilt // protoc for the current platform/arch into grpc-tools/bin. execFileSync(process.execPath, [nodePreGypCli, "install"], { cwd: GRPC_TOOLS_DIR, stdio: "inherit" }) } catch (error) { console.error(chalk.red(`Failed to download protoc via node-pre-gyp: ${error?.message ?? error}`)) process.exit(1) } if (!fsSync.existsSync(GRPC_TOOLS_PROTOC)) { console.error(chalk.red(`protoc still not found at ${GRPC_TOOLS_PROTOC} after node-pre-gyp install.`)) process.exit(1) } console.log(chalk.green("✓ protoc binary installed.")) } if (!fsSync.existsSync(PROTOC)) { // PROTOC only differs from GRPC_TOOLS_PROTOC when the legacy Windows path exists, so a missing // PROTOC always means the grpc-tools-bundled protoc needs to be fetched. ensureProtocBinary() } const PROTO_DIR = path.resolve("proto") const TS_OUT_DIR = path.resolve("src/shared/proto") const GRPC_JS_OUT_DIR = path.resolve("src/generated/grpc-js") const NICE_JS_OUT_DIR = path.resolve("src/generated/nice-grpc") const DESCRIPTOR_OUT_DIR = path.resolve("dist-standalone/proto") // protoc invokes the ts-proto plugin as a child process, so it needs a path it can // directly execute. On POSIX the package's JS bin (with its shebang) works. On // Windows protoc cannot exec a bare .js or bun's `.bunx` shim ("%1 is not a valid // Win32 application"), and the package manager's `.cmd` shim location/name varies // (npm vs bun's hoisted store). To be package-manager-agnostic, generate a tiny // .cmd wrapper that runs the resolved plugin JS via `node`. function resolveTsProtoPlugin() { const pluginJs = require.resolve("ts-proto/protoc-gen-ts_proto") if (!isWindows) { return pluginJs } const wrapperDir = path.resolve("dist-standalone") fsSync.mkdirSync(wrapperDir, { recursive: true }) const wrapperPath = path.join(wrapperDir, "protoc-gen-ts_proto.cmd") // %* forwards protoc's plugin args/stdio to the JS entry run under node. fsSync.writeFileSync(wrapperPath, `@echo off\r\nnode "${pluginJs}" %*\r\n`) return wrapperPath } const TS_PROTO_PLUGIN = resolveTsProtoPlugin() const TS_PROTO_OPTIONS = [ "env=both", "esModuleInterop=true", "outputServices=generic-definitions", // output generic ServiceDefinitions "outputIndex=true", // output an index file for each package which exports all protos in the package. "useOptionals=none", // scalar and message fields are required unless they are marked as optional. "useDate=false", // Timestamp fields will not be automatically converted to Date. ] async function main() { await cleanup() await compileProtos() await generateProtoBusSetup() await generateHostBridgeClient() } async function compileProtos() { console.log(chalk.bold.blue("Compiling Protocol Buffers...")) // Check for Apple Silicon compatibility before proceeding checkAppleSiliconCompatibility() // Create output directories if they don't exist for (const dir of [TS_OUT_DIR, GRPC_JS_OUT_DIR, NICE_JS_OUT_DIR, DESCRIPTOR_OUT_DIR]) { await fs.mkdir(dir, { recursive: true }) } // Process all proto files const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR, realpath: true }) console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), PROTO_DIR) tsProtoc(TS_OUT_DIR, protoFiles, TS_PROTO_OPTIONS) // grpc-js is used to generate service impls for the ProtoBus service. tsProtoc(GRPC_JS_OUT_DIR, protoFiles, ["outputServices=grpc-js", ...TS_PROTO_OPTIONS]) // nice-js is used for the Host Bridge client impls because it uses promises. tsProtoc(NICE_JS_OUT_DIR, protoFiles, ["outputServices=nice-grpc,useExactTypes=false", ...TS_PROTO_OPTIONS]) const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb") const descriptorProtocArgs = [ `--proto_path=${PROTO_DIR}`, `--descriptor_set_out=${descriptorFile}`, "--include_imports", ...protoFiles, ] try { log_verbose(chalk.cyan("Generating descriptor set...")) log_verbose(`${PROTOC} ${descriptorProtocArgs.join(" ")}`) execFileSync(PROTOC, descriptorProtocArgs, { stdio: "inherit" }) } catch (error) { console.error(chalk.red("Error generating descriptor set for proto file:"), error) process.exit(1) } log_verbose(chalk.green("Protocol Buffer code generation completed successfully.")) log_verbose(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`)) } function tsProtoc(outDir, protoFiles, protoOptions) { const args = [ `--proto_path=${PROTO_DIR}`, `--plugin=protoc-gen-ts_proto=${TS_PROTO_PLUGIN}`, `--ts_proto_out=${outDir}`, `--ts_proto_opt=${protoOptions.join(",")}`, ...protoFiles, ] try { log_verbose(chalk.cyan(`Generating TypeScript code in ${outDir} for:\n${protoFiles.join("\n")}...`)) log_verbose(`${PROTOC} ${args.join(" ")}`) execFileSync(PROTOC, args, { stdio: "inherit" }) } catch (error) { console.error(chalk.red("Error generating TypeScript for proto files:"), error) process.exit(1) } } async function cleanup() { // Clean up existing generated files log_verbose(chalk.cyan("Cleaning up existing generated TypeScript files...")) await rmrf(TS_OUT_DIR) await rmrf("src/generated") // Clean up generated files that were moved. await rmrf("src/standalone/services/host-grpc-client.ts") await rmrf("src/standalone/server-setup.ts") await rmrf("src/hosts/vscode/host-grpc-service-config.ts") await rmrf("src/core/controller/grpc-service-config.ts") const oldhostbridgefiles = [ "src/hosts/vscode/workspace/methods.ts", "src/hosts/vscode/workspace/index.ts", "src/hosts/vscode/diff/methods.ts", "src/hosts/vscode/diff/index.ts", "src/hosts/vscode/env/methods.ts", "src/hosts/vscode/env/index.ts", "src/hosts/vscode/window/methods.ts", "src/hosts/vscode/window/index.ts", "src/hosts/vscode/watch/methods.ts", "src/hosts/vscode/watch/index.ts", "src/hosts/vscode/uri/methods.ts", "src/hosts/vscode/uri/index.ts", ] const oldprotobusfiles = [ "src/core/controller/account/index.ts", "src/core/controller/account/methods.ts", "src/core/controller/browser/index.ts", "src/core/controller/browser/methods.ts", "src/core/controller/checkpoints/index.ts", "src/core/controller/checkpoints/methods.ts", "src/core/controller/file/index.ts", "src/core/controller/file/methods.ts", "src/core/controller/mcp/index.ts", "src/core/controller/mcp/methods.ts", "src/core/controller/models/index.ts", "src/core/controller/models/methods.ts", "src/core/controller/slash/index.ts", "src/core/controller/slash/methods.ts", "src/core/controller/state/index.ts", "src/core/controller/state/methods.ts", "src/core/controller/task/index.ts", "src/core/controller/task/methods.ts", "src/core/controller/ui/index.ts", "src/core/controller/ui/methods.ts", "src/core/controller/web/index.ts", "src/core/controller/web/methods.ts", ] for (const file of [...oldhostbridgefiles, ...oldprotobusfiles]) { await rmrf(file) } } // Check for Apple Silicon compatibility function checkAppleSiliconCompatibility() { // Only run check on macOS if (process.platform !== "darwin") { return } // Check if running on Apple Silicon const cpuArchitecture = os.arch() if (cpuArchitecture === "arm64") { try { // Check if Rosetta is installed const rosettaCheck = execSync('/usr/bin/pgrep oahd || echo "NOT_INSTALLED"').toString().trim() if (rosettaCheck === "NOT_INSTALLED") { console.log(chalk.yellow("Detected Apple Silicon (ARM64) architecture.")) console.log( chalk.red("Rosetta 2 is NOT installed. The npm version of protoc is not compatible with Apple Silicon."), ) console.log(chalk.cyan("Please install Rosetta 2 using the following command:")) console.log(chalk.cyan(" softwareupdate --install-rosetta --agree-to-license")) console.log(chalk.red("Aborting build process.")) process.exit(1) } } catch (_error) { console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway.")) } } } function log_verbose(s) { if (process.argv.includes("-v") || process.argv.includes("--verbose")) { console.log(s) } } // Run the main function main().catch((error) => { console.error(chalk.red("Error:"), error) process.exit(1) })