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