From b998e35d178a4b7d81221fdbe3bea08d3c7ce44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Wed, 15 Apr 2026 03:06:13 +0100 Subject: [PATCH] fix(cli): auto-inject VERSION from package.json at build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit alpha.28-30 binaries all reported 'v1.0.0-alpha.27' from a hardcoded constant in src/constants/urls.ts — my bump sed only matched package.json's 'version' key, not the TypeScript literal. build.ts now reads package.json version and injects it via Bun's `define` (source-text replacement, equivalent to esbuild --define). urls.ts reads the injected symbol with a runtime fallback for `bun src/...` dev mode. Version drift can't recur. + peers + status migrated to the render.ts unified renderer. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/cli-v2/build.ts | 8 ++++ apps/cli-v2/package.json | 2 +- apps/cli-v2/src/commands/peers.ts | 68 +++++++++++++----------------- apps/cli-v2/src/commands/status.ts | 56 ++++++++++++------------ apps/cli-v2/src/constants/urls.ts | 6 ++- 5 files changed, 71 insertions(+), 69 deletions(-) diff --git a/apps/cli-v2/build.ts b/apps/cli-v2/build.ts index 0ac9701..f65e895 100644 --- a/apps/cli-v2/build.ts +++ b/apps/cli-v2/build.ts @@ -3,6 +3,11 @@ import { gzipSync } from "node:zlib"; const MAX_GZIPPED_BYTES = 1.2 * 1024 * 1024; // 1.2 MB +// Inject the version from package.json at build time so VERSION can never +// drift from what's published. Bun's `define` is a source-text replacement, +// equivalent to `--define` in esbuild / a webpack DefinePlugin. +const pkgVersion = ((await Bun.file("package.json").json()) as { version: string }).version; + const result = await Bun.build({ entrypoints: [ "src/entrypoints/cli.ts", @@ -13,6 +18,9 @@ const result = await Bun.build({ format: "esm", splitting: false, sourcemap: "external", + define: { + __CLAUDEMESH_VERSION__: JSON.stringify(pkgVersion), + }, external: [ "libsodium-wrappers", "ws", diff --git a/apps/cli-v2/package.json b/apps/cli-v2/package.json index 8da0c31..0bd0d4e 100644 --- a/apps/cli-v2/package.json +++ b/apps/cli-v2/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli-v2", - "version": "1.0.0-alpha.30", + "version": "1.0.0-alpha.31", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "keywords": [ "claude-code", diff --git a/apps/cli-v2/src/commands/peers.ts b/apps/cli-v2/src/commands/peers.ts index ab23c7e..52baae8 100644 --- a/apps/cli-v2/src/commands/peers.ts +++ b/apps/cli-v2/src/commands/peers.ts @@ -6,6 +6,8 @@ import { withMesh } from "./connect.js"; import { readConfig } from "~/services/config/facade.js"; +import { render } from "~/ui/render.js"; +import { bold, dim, green, yellow } from "~/ui/styles.js"; export interface PeersFlags { mesh?: string; @@ -13,22 +15,12 @@ export interface PeersFlags { } export async function runPeers(flags: PeersFlags): Promise { - const useColor = - !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY; - const dim = (s: string) => (useColor ? `\x1b[2m${s}\x1b[22m` : s); - const bold = (s: string) => (useColor ? `\x1b[1m${s}\x1b[22m` : s); - const green = (s: string) => (useColor ? `\x1b[32m${s}\x1b[39m` : s); - const yellow = (s: string) => (useColor ? `\x1b[33m${s}\x1b[39m` : s); - const config = readConfig(); - - // If --mesh specified, show only that one. Otherwise show all. - const slugs = flags.mesh - ? [flags.mesh] - : config.meshes.map(m => m.slug); + const slugs = flags.mesh ? [flags.mesh] : config.meshes.map((m) => m.slug); if (slugs.length === 0) { - console.error("No meshes joined. Run `claudemesh join ` first."); + render.err("No meshes joined."); + render.hint("claudemesh # join + launch"); process.exit(1); } @@ -44,39 +36,39 @@ export async function runPeers(flags: PeersFlags): Promise { return; } - console.log(bold(`Peers on ${mesh.slug}`) + dim(` (${peers.length})`)); - console.log(""); + render.section(`peers on ${mesh.slug} (${peers.length})`); if (peers.length === 0) { - console.log(dim(" No peers connected.")); - } else { - for (const p of peers) { - const groups = p.groups.length - ? " [" + p.groups.map((g: { name: string; role?: string }) => - `@${g.name}${g.role ? `:${g.role}` : ""}`).join(", ") + "]" - : ""; - const statusIcon = p.status === "working" ? yellow("●") : green("●"); - const name = bold(p.displayName); - const meta: string[] = []; - if (p.peerType) meta.push(p.peerType); - if (p.channel) meta.push(p.channel); - if (p.model) meta.push(p.model); - const metaStr = meta.length ? dim(` (${meta.join(", ")})`) : ""; - const cwdStr = p.cwd ? dim(` cwd: ${p.cwd}`) : ""; - const summary = p.summary ? dim(` ${p.summary}`) : ""; - console.log(` ${statusIcon} ${name}${groups}${metaStr}${summary}`); - if (cwdStr) console.log(` ${cwdStr}`); - } + render.info(dim(" (no peers connected)")); + return; + } + + for (const p of peers) { + const groups = p.groups.length + ? " [" + + p.groups + .map((g: { name: string; role?: string }) => `@${g.name}${g.role ? `:${g.role}` : ""}`) + .join(", ") + + "]" + : ""; + const statusDot = p.status === "working" ? yellow("●") : green("●"); + const name = bold(p.displayName); + const meta: string[] = []; + if (p.peerType) meta.push(p.peerType); + if (p.channel) meta.push(p.channel); + if (p.model) meta.push(p.model); + const metaStr = meta.length ? dim(` (${meta.join(", ")})`) : ""; + const summary = p.summary ? dim(` — ${p.summary}`) : ""; + render.info(`${statusDot} ${name}${groups}${metaStr}${summary}`); + if (p.cwd) render.info(dim(` cwd: ${p.cwd}`)); } - console.log(""); }); } catch (e) { - console.error(dim(` Could not connect to ${slug}: ${e instanceof Error ? e.message : String(e)}`)); - console.log(""); + render.err(`${slug}: ${e instanceof Error ? e.message : String(e)}`); } } if (flags.json) { - console.log(JSON.stringify(slugs.length === 1 ? allJson[0]?.peers : allJson, null, 2)); + process.stdout.write(JSON.stringify(slugs.length === 1 ? allJson[0]?.peers : allJson, null, 2) + "\n"); } } diff --git a/apps/cli-v2/src/commands/status.ts b/apps/cli-v2/src/commands/status.ts index aa17def..d0631d0 100644 --- a/apps/cli-v2/src/commands/status.ts +++ b/apps/cli-v2/src/commands/status.ts @@ -10,6 +10,7 @@ import { statSync, existsSync } from "node:fs"; import WebSocket from "ws"; import { readConfig, getConfigPath } from "~/services/config/facade.js"; import { VERSION } from "~/constants/urls.js"; +import { render } from "~/ui/render.js"; interface MeshStatus { slug: string; @@ -17,10 +18,12 @@ interface MeshStatus { pubkey: string; reachable: boolean; error?: string; + latencyMs?: number; } -async function probeBroker(url: string, timeoutMs = 4000): Promise<{ ok: boolean; error?: string }> { +async function probeBroker(url: string, timeoutMs = 4000): Promise<{ ok: boolean; error?: string; latencyMs?: number }> { return new Promise((resolve) => { + const started = Date.now(); const ws = new WebSocket(url); const timer = setTimeout(() => { try { ws.terminate(); } catch { /* noop */ } @@ -28,8 +31,9 @@ async function probeBroker(url: string, timeoutMs = 4000): Promise<{ ok: boolean }, timeoutMs); ws.on("open", () => { clearTimeout(timer); + const latency = Date.now() - started; try { ws.close(); } catch { /* noop */ } - resolve({ ok: true }); + resolve({ ok: true, latencyMs: latency }); }); ws.on("error", (err) => { clearTimeout(timer); @@ -39,65 +43,59 @@ async function probeBroker(url: string, timeoutMs = 4000): Promise<{ ok: boolean } export async function runStatus(): Promise { - const useColor = - !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY; - const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s); - const green = (s: string): string => (useColor ? `\x1b[32m${s}\x1b[39m` : s); - const red = (s: string): string => (useColor ? `\x1b[31m${s}\x1b[39m` : s); - - console.log(`claudemesh status (v${VERSION})`); - console.log("─".repeat(60)); + render.section(`status (v${VERSION})`); const configPath = getConfigPath(); - let configPerms = "missing"; + let configPermsNote = "missing"; if (existsSync(configPath)) { - const st = statSync(configPath); - const mode = (st.mode & 0o777).toString(8).padStart(4, "0"); - configPerms = mode === "0600" ? `${mode} ✓` : `${mode} ⚠ (expected 0600)`; + const mode = (statSync(configPath).mode & 0o777).toString(8).padStart(4, "0"); + configPermsNote = mode === "0600" ? `${mode}` : `${mode} — expected 0600`; } - console.log(`Config: ${configPath} (${configPerms})`); + render.kv([["config", configPath], ["perms", configPermsNote]]); const config = readConfig(); if (config.meshes.length === 0) { - console.log(""); - console.log(dim("No meshes joined. Run `claudemesh join ` to get started.")); + render.blank(); + render.info("No meshes joined."); + render.hint("claudemesh # join + launch"); process.exit(0); } - console.log(""); - console.log(`Meshes (${config.meshes.length}):`); + render.blank(); + render.heading(`meshes (${config.meshes.length})`); const results: MeshStatus[] = []; for (const m of config.meshes) { - process.stdout.write(` ${m.slug.padEnd(20)} probing ${m.brokerUrl}… `); const probe = await probeBroker(m.brokerUrl); - results.push({ + const entry: MeshStatus = { slug: m.slug, brokerUrl: m.brokerUrl, pubkey: m.pubkey, reachable: probe.ok, error: probe.error, - }); + latencyMs: probe.latencyMs, + }; + results.push(entry); if (probe.ok) { - console.log(green("reachable")); + render.ok(`${m.slug}`, `${probe.latencyMs}ms → ${m.brokerUrl}`); } else { - console.log(red(`unreachable (${probe.error})`)); + render.err(`${m.slug}`, `unreachable (${probe.error})`); } } - console.log(""); + render.blank(); for (const r of results) { - console.log(dim(` ${r.slug}: pubkey ${r.pubkey.slice(0, 16)}…`)); + render.kv([[r.slug, `${r.pubkey.slice(0, 16)}…`]]); } const allOk = results.every((r) => r.reachable); - console.log(""); + render.blank(); if (allOk) { - console.log(green("All meshes reachable.")); + render.ok("all meshes reachable"); process.exit(0); } else { const broken = results.filter((r) => !r.reachable).length; - console.log(red(`${broken} of ${results.length} mesh(es) unreachable.`)); + render.err(`${broken} of ${results.length} mesh(es) unreachable`); process.exit(1); } } diff --git a/apps/cli-v2/src/constants/urls.ts b/apps/cli-v2/src/constants/urls.ts index 359560d..8140e41 100644 --- a/apps/cli-v2/src/constants/urls.ts +++ b/apps/cli-v2/src/constants/urls.ts @@ -5,7 +5,11 @@ export const URLS = { NPM_REGISTRY: "https://registry.npmjs.org/claudemesh-cli", } as const; -export const VERSION = "1.0.0-alpha.27"; +// Injected at build time from package.json#version via `bun build --define` +// (see build.ts). Falls back to a dev sentinel when running from source. +declare const __CLAUDEMESH_VERSION__: string; +export const VERSION: string = + typeof __CLAUDEMESH_VERSION__ !== "undefined" ? __CLAUDEMESH_VERSION__ : "0.0.0-dev"; export const env = { CLAUDEMESH_BROKER_URL: URLS.BROKER,