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 <noreply@anthropic.com>
This commit is contained in:
Jiayuan 2026-01-30 15:22:53 +08:00
parent 4f50b3b399
commit 7f25378e4b
4 changed files with 86 additions and 0 deletions

1
.gitignore vendored
View file

@ -8,6 +8,7 @@ dist
out out
.turbo .turbo
build build
bin
dist-electron dist-electron
release release
*.tsbuildinfo *.tsbuildinfo

View file

@ -4,6 +4,12 @@
"description": "", "description": "",
"type": "module", "type": "module",
"main": "dist/index.js", "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": { "scripts": {
"dev": "tsx src/index.ts", "dev": "tsx src/index.ts",
"agent:cli": "tsx src/agent/cli.ts", "agent:cli": "tsx src/agent/cli.ts",
@ -15,6 +21,7 @@
"dev:desktop": "pnpm --filter @multica/desktop dev", "dev:desktop": "pnpm --filter @multica/desktop dev",
"build": "turbo build", "build": "turbo build",
"build:sdk": "pnpm --filter @multica/sdk build", "build:sdk": "pnpm --filter @multica/sdk build",
"build:cli": "node scripts/build-cli.js",
"start": "node dist/index.js", "start": "node dist/index.js",
"typecheck": "turbo typecheck", "typecheck": "turbo typecheck",
"test": "vitest run", "test": "vitest run",
@ -30,6 +37,7 @@
"@types/turndown": "^5.0.6", "@types/turndown": "^5.0.6",
"@types/uuid": "^11.0.0", "@types/uuid": "^11.0.0",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"esbuild": "^0.27.2",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"turbo": "^2.3.4", "turbo": "^2.3.4",
"typescript": "catalog:", "typescript": "catalog:",

3
pnpm-lock.yaml generated
View file

@ -120,6 +120,9 @@ importers:
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: ^4.0.18 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)) 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: tsx:
specifier: ^4.21.0 specifier: ^4.21.0
version: 4.21.0 version: 4.21.0

74
scripts/build-cli.js Normal file
View file

@ -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);
});