From 7f25378e4bf34e79f5d41a73b5f6b60ce977195d Mon Sep 17 00:00:00 2001 From: Jiayuan Date: Fri, 30 Jan 2026 15:22:53 +0800 Subject: [PATCH] feat(cli): add binary build support for interactive CLI Add esbuild-based build script to bundle CLI tools into standalone binaries that can be run directly with node. The built binaries externalize dependencies to keep bundle size small and avoid bundling issues with native modules. Co-Authored-By: Claude Opus 4.5 --- .gitignore | 1 + package.json | 8 +++++ pnpm-lock.yaml | 3 ++ scripts/build-cli.js | 74 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 scripts/build-cli.js diff --git a/.gitignore b/.gitignore index 2feed4f0..f08a7248 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist out .turbo build +bin dist-electron release *.tsbuildinfo diff --git a/package.json b/package.json index dc40264f..8070e86b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,12 @@ "description": "", "type": "module", "main": "dist/index.js", + "bin": { + "multica": "./bin/multica-interactive.mjs", + "multica-interactive": "./bin/multica-interactive.mjs", + "multica-cli": "./bin/multica-cli.mjs", + "multica-profile": "./bin/multica-profile.mjs" + }, "scripts": { "dev": "tsx src/index.ts", "agent:cli": "tsx src/agent/cli.ts", @@ -15,6 +21,7 @@ "dev:desktop": "pnpm --filter @multica/desktop dev", "build": "turbo build", "build:sdk": "pnpm --filter @multica/sdk build", + "build:cli": "node scripts/build-cli.js", "start": "node dist/index.js", "typecheck": "turbo typecheck", "test": "vitest run", @@ -30,6 +37,7 @@ "@types/turndown": "^5.0.6", "@types/uuid": "^11.0.0", "@vitest/coverage-v8": "^4.0.18", + "esbuild": "^0.27.2", "tsx": "^4.21.0", "turbo": "^2.3.4", "typescript": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2efb22a7..c06ab117 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: '@vitest/coverage-v8': specifier: ^4.0.18 version: 4.0.18(vitest@4.0.18(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.10)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2)) + esbuild: + specifier: ^0.27.2 + version: 0.27.2 tsx: specifier: ^4.21.0 version: 4.21.0 diff --git a/scripts/build-cli.js b/scripts/build-cli.js new file mode 100644 index 00000000..85f814eb --- /dev/null +++ b/scripts/build-cli.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +import * as esbuild from "esbuild"; +import { fileURLToPath } from "url"; +import { dirname, resolve } from "path"; +import { readFileSync, chmodSync } from "fs"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = resolve(__dirname, ".."); + +// Read package.json to get all dependencies +const pkg = JSON.parse(readFileSync(resolve(rootDir, "package.json"), "utf8")); +const allDeps = [ + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.devDependencies || {}), +]; + +// Plugin to strip shebangs from source files (they get bundled otherwise) +const stripShebangPlugin = { + name: "strip-shebang", + setup(build) { + build.onLoad({ filter: /\.ts$/ }, async (args) => { + const source = readFileSync(args.path, "utf8"); + // Remove shebang if present + const contents = source.replace(/^#!.*\n/, ""); + return { contents, loader: "ts" }; + }); + }, +}; + +async function build() { + const entryPoints = [ + { entry: "src/agent/interactive-cli.ts", outfile: "bin/multica-interactive.mjs" }, + { entry: "src/agent/cli.ts", outfile: "bin/multica-cli.mjs" }, + { entry: "src/agent/profile-cli.ts", outfile: "bin/multica-profile.mjs" }, + ]; + + for (const { entry, outfile } of entryPoints) { + console.log(`Building ${entry} -> ${outfile}...`); + + await esbuild.build({ + entryPoints: [resolve(rootDir, entry)], + outfile: resolve(rootDir, outfile), + bundle: true, + platform: "node", + target: "node20", + format: "esm", + banner: { + js: "#!/usr/bin/env node", + }, + plugins: [stripShebangPlugin], + sourcemap: true, + minify: false, + // Externalize all dependencies - they will be loaded from node_modules at runtime + external: allDeps, + }); + + // Make executable + chmodSync(resolve(rootDir, outfile), 0o755); + console.log(` ✓ ${outfile}`); + } + + console.log("\nBuild complete! Binaries are in ./bin/"); + console.log("\nUsage:"); + console.log(" node bin/multica-interactive.mjs # Interactive CLI"); + console.log(" node bin/multica-cli.mjs # Non-interactive CLI"); + console.log(" node bin/multica-profile.mjs # Profile management"); + console.log("\nNote: The built binaries require node_modules to be present."); + console.log("Run 'pnpm install --prod' to install only production dependencies."); +} + +build().catch((err) => { + console.error(err); + process.exit(1); +});