drop the orphaned bridge tier (~600 LoC). client/server/protocol files deleted; tryBridge had returned null in production for seven releases since the 1.24.0 mcp shim rewrite stopped opening the sockets. each verb now has two paths: daemon (with 1.27.3's auto-spawn) → cold ws. add per-process daemon policy: --strict (error instead of cold fallback) and --no-daemon (skip daemon entirely). enforcement at withMesh so a single chokepoint covers every verb. env equivalents CLAUDEMESH_STRICT_DAEMON / CLAUDEMESH_NO_DAEMON. flag wins. net -394 loc; the daemon-up case ships ~600 loc lighter and the fallback story is one tier simpler. first sprint A drop; per-session ipc tokens and the wizard refactors follow in 1.29.0+. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
3.6 KiB
TypeScript
110 lines
3.6 KiB
TypeScript
/**
|
|
* Short-lived WS connection helper for CLI commands (peers, send, inbox, state).
|
|
*
|
|
* Opens a connection to one mesh, runs a callback, then closes cleanly.
|
|
* The caller never deals with connect/close lifecycle.
|
|
*/
|
|
|
|
import { hostname } from "node:os";
|
|
import { createInterface } from "node:readline";
|
|
import { BrokerClient } from "~/services/broker/facade.js";
|
|
import { readConfig } from "~/services/config/facade.js";
|
|
import type { JoinedMesh } from "~/services/config/facade.js";
|
|
import { getDaemonPolicy } from "~/services/daemon/policy.js";
|
|
|
|
export interface ConnectOpts {
|
|
/** Mesh slug to connect to. Auto-selects if only one mesh joined. */
|
|
meshSlug?: string | null;
|
|
/** Display name for this session. Defaults to hostname-pid. */
|
|
displayName?: string;
|
|
/** Connect to all meshes and run fn for each. */
|
|
all?: boolean;
|
|
}
|
|
|
|
async function pickMesh(meshes: JoinedMesh[]): Promise<JoinedMesh> {
|
|
console.log("\n Select mesh:");
|
|
meshes.forEach((m, i) => {
|
|
console.log(` ${i + 1}) ${m.slug}`);
|
|
});
|
|
console.log("");
|
|
|
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
return new Promise((resolve) => {
|
|
rl.question(" Choice [1]: ", (answer) => {
|
|
rl.close();
|
|
const idx = parseInt(answer || "1", 10) - 1;
|
|
if (idx >= 0 && idx < meshes.length) {
|
|
resolve(meshes[idx]!);
|
|
} else {
|
|
console.error(" Invalid choice, using first mesh.");
|
|
resolve(meshes[0]!);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function withMesh<T>(
|
|
opts: ConnectOpts,
|
|
fn: (client: BrokerClient, mesh: JoinedMesh) => Promise<T>,
|
|
): Promise<T> {
|
|
// --strict gate: every cold-path verb funnels through here, so a single
|
|
// policy check covers the whole CLI surface. The daemon-routing helpers
|
|
// already returned null (auto-spawn failed); under --strict we refuse
|
|
// the cold-path fallback and exit loudly instead.
|
|
if (getDaemonPolicy().mode === "strict") {
|
|
console.error(
|
|
"\n ✘ daemon not reachable — --strict refuses cold-path fallback.\n" +
|
|
" run `claudemesh daemon up` (or `claudemesh doctor`) and retry.\n",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const config = readConfig();
|
|
if (config.meshes.length === 0) {
|
|
console.error("No meshes joined. Run `claudemesh join <url>` first.");
|
|
process.exit(1);
|
|
}
|
|
|
|
let mesh: JoinedMesh;
|
|
if (opts.meshSlug) {
|
|
const found = config.meshes.find((m) => m.slug === opts.meshSlug);
|
|
if (!found) {
|
|
console.error(
|
|
`Mesh "${opts.meshSlug}" not found. Joined: ${config.meshes.map((m) => m.slug).join(", ")}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
mesh = found;
|
|
} else if (config.meshes.length === 1) {
|
|
mesh = config.meshes[0]!;
|
|
} else {
|
|
mesh = await pickMesh(config.meshes);
|
|
}
|
|
|
|
const displayName = opts.displayName ?? config.displayName ?? `${hostname()}-${process.pid}`;
|
|
const client = new BrokerClient(mesh, { displayName, quiet: true });
|
|
|
|
try {
|
|
await client.connect();
|
|
const result = await fn(client, mesh);
|
|
return result;
|
|
} catch (e) {
|
|
// Terminal close from the broker (banned / kicked). Give the user
|
|
// a clear message instead of the low-level ws error.
|
|
if (client.terminalClose) {
|
|
const { code, reason } = client.terminalClose;
|
|
if (code === 4002) {
|
|
console.error(`\n ✘ ${reason}\n`);
|
|
} else if (code === 4001) {
|
|
console.error(`\n ✘ Kicked from this mesh. Run \`claudemesh\` to rejoin.\n`);
|
|
} else {
|
|
console.error(`\n ✘ Broker closed connection: ${reason}\n`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
throw e;
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|