chore(cli): bundle for node, prep for npm publish
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled

Makes @claudemesh/cli installable globally via npm without requiring
bun on user machines. (Bun stays the dev runtime; bundled output is
node-compatible.)

- bun build --target=node --outfile dist/index.js produces a 2.69MB
  standalone bundle with node-shebang banner
- package.json: add description/keywords/author/license/homepage/
  repository, set bin to ./dist/index.js, files=[dist, README, LICENSE],
  publishConfig.access=public, engines.node >=20
- prepublishOnly auto-runs the build
- pin zod from catalog: to 4.1.13 (npm rejects catalog: refs)
- swap Bun.spawnSync → node:child_process.spawnSync in install.ts
  (the only Bun-global usage in the package)
- strip shebang from src/index.ts (banner supplies it post-bundle)

install command now runs in two modes:
- BUNDLED (npm i -g): detects dist/index.js path, writes MCP entry
  with command "claudemesh" (relies on the global bin shim on PATH)
- SOURCE (bun src/index.ts, dev): preflights bun, writes MCP entry
  with command "bun <absolute-path> mcp"

verified end-to-end:
- node dist/index.js --help prints usage ✓
- node dist/index.js install writes correct ~/.claude.json ✓
- node dist/index.js mcp | tools/list returns all 5 tools ✓
- bun src/index.ts install (dev mode) still works ✓

NOT PUBLISHED YET — @claudemesh/cli is owned by an unrelated project
on npm. Awaiting user decision on alternative name (claudemesh-cli,
@alezmad/claudemesh-cli, or new org scope). Bundle is name-agnostic
and will reuse regardless.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-05 14:31:27 +01:00
parent dfb53b6ac2
commit 714d82e4e7
3 changed files with 70 additions and 13 deletions

View File

@@ -27,6 +27,7 @@ import {
import { homedir, platform } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
const MCP_NAME = "claudemesh";
const CLAUDE_CONFIG = join(homedir(), ".claude.json");
@@ -64,22 +65,44 @@ function writeClaudeConfig(obj: Record<string, unknown>): void {
}
}
/** Check `bun` is on PATH — OS-agnostic. */
/** Check `bun` is on PATH — OS-agnostic, node:child_process. */
function bunAvailable(): boolean {
const which =
const res =
platform() === "win32"
? Bun.spawnSync(["where", "bun"])
: Bun.spawnSync(["sh", "-c", "command -v bun"]);
return which.exitCode === 0;
? spawnSync("where", ["bun"])
: spawnSync("sh", ["-c", "command -v bun"]);
return res.status === 0;
}
/** Absolute path to this CLI's entry file. */
function resolveEntry(): string {
const here = fileURLToPath(import.meta.url);
// When bundled (dist/index.js), this file IS the entry → return self.
// When running from source (src/index.ts via bun), walk up to the
// dir + resolve index.ts.
if (here.endsWith("/dist/index.js") || here.endsWith("\\dist\\index.js")) {
return here;
}
return resolve(dirname(here), "..", "index.ts");
}
/**
* Build the MCP server entry for Claude Code's config.
*
* Two modes:
* - Installed globally (npm i -g @claudemesh/cli): use `claudemesh`
* as the command, relies on it being on PATH.
* - Local dev (bun apps/cli/src/index.ts): use `bun <absolute-path>`.
*/
function buildMcpEntry(entryPath: string): McpEntry {
const isBundled = entryPath.endsWith("/dist/index.js") ||
entryPath.endsWith("\\dist\\index.js");
if (isBundled) {
return {
command: "claudemesh",
args: ["mcp"],
};
}
return {
command: "bun",
args: [entryPath, "mcp"],
@@ -97,14 +120,18 @@ export function runInstall(): void {
console.log("claudemesh install");
console.log("------------------");
if (!bunAvailable()) {
const entry = resolveEntry();
const isBundled = entry.endsWith("/dist/index.js") ||
entry.endsWith("\\dist\\index.js");
// Dev mode (running from src/) requires bun on PATH; bundled mode
// (npm install -g) just uses node + the claudemesh bin shim.
if (!isBundled && !bunAvailable()) {
console.error(
"✗ `bun` is not on PATH. Install Bun first: https://bun.com",
);
process.exit(1);
}
const entry = resolveEntry();
if (!existsSync(entry)) {
console.error(`✗ MCP entry not found at ${entry}`);
process.exit(1);
@@ -142,7 +169,9 @@ export function runInstall(): void {
console.log(`✓ MCP server "${MCP_NAME}" ${action}`);
console.log(` config: ${CLAUDE_CONFIG}`);
console.log(` command: bun ${entry} mcp`);
console.log(
` command: ${desired.command}${desired.args?.length ? " " + desired.args.join(" ") : ""}`,
);
console.log("");
console.log("Restart Claude Code to load the MCP server.");
console.log("Then join a mesh:");