feat(cli): 1.28.0 — bridge deletion + daemon-policy flags
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>
This commit is contained in:
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
|
||||
import { withMesh } from "./connect.js";
|
||||
import { readConfig } from "~/services/config/facade.js";
|
||||
import { tryBridge } from "~/services/bridge/client.js";
|
||||
import { tryForgetViaDaemon } from "~/services/bridge/daemon-route.js";
|
||||
import { render } from "~/ui/render.js";
|
||||
import { bold, clay, dim } from "~/ui/styles.js";
|
||||
@@ -26,14 +24,6 @@ import { validateMessageId, renderValidationError } from "~/cli/validators.js";
|
||||
type StateFlags = { mesh?: string; json?: boolean };
|
||||
type PeerStatus = "idle" | "working" | "dnd";
|
||||
|
||||
/** Resolve unambiguous mesh slug for warm-path bridging. Returns null if
|
||||
* the user has multiple joined meshes and didn't pick one. */
|
||||
function unambiguousMesh(opts: StateFlags): string | null {
|
||||
if (opts.mesh) return opts.mesh;
|
||||
const config = readConfig();
|
||||
return config.meshes.length === 1 ? config.meshes[0]!.slug : null;
|
||||
}
|
||||
|
||||
// --- status ---
|
||||
|
||||
export async function runStatusSet(state: string, opts: StateFlags): Promise<number> {
|
||||
@@ -43,21 +33,9 @@ export async function runStatusSet(state: string, opts: StateFlags): Promise<num
|
||||
return EXIT.INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Warm path
|
||||
const meshSlug = unambiguousMesh(opts);
|
||||
if (meshSlug) {
|
||||
const bridged = await tryBridge(meshSlug, "status_set", { status: state });
|
||||
if (bridged !== null) {
|
||||
if (bridged.ok) {
|
||||
if (opts.json) console.log(JSON.stringify({ status: state }));
|
||||
else render.ok(`status set to ${bold(state)}`);
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
render.err(bridged.error);
|
||||
return EXIT.INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// Bridge tier deleted in 1.28.0 (dead code; the orphaned warm-path
|
||||
// socket was never opened by anyone). Daemon route would belong here;
|
||||
// adding it for status/summary/visible is queued for 1.29.0.
|
||||
await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
|
||||
await client.setStatus(state as PeerStatus);
|
||||
});
|
||||
@@ -74,21 +52,6 @@ export async function runSummary(text: string, opts: StateFlags): Promise<number
|
||||
return EXIT.INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Warm path
|
||||
const meshSlug = unambiguousMesh(opts);
|
||||
if (meshSlug) {
|
||||
const bridged = await tryBridge(meshSlug, "summary", { summary: text });
|
||||
if (bridged !== null) {
|
||||
if (bridged.ok) {
|
||||
if (opts.json) console.log(JSON.stringify({ summary: text }));
|
||||
else render.ok("summary set", dim(text));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
render.err(bridged.error);
|
||||
return EXIT.INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
|
||||
await client.setSummary(text);
|
||||
});
|
||||
@@ -108,21 +71,6 @@ export async function runVisible(value: string | undefined, opts: StateFlags): P
|
||||
return EXIT.INVALID_ARGS;
|
||||
}
|
||||
|
||||
// Warm path
|
||||
const meshSlug = unambiguousMesh(opts);
|
||||
if (meshSlug) {
|
||||
const bridged = await tryBridge(meshSlug, "visible", { visible });
|
||||
if (bridged !== null) {
|
||||
if (bridged.ok) {
|
||||
if (opts.json) console.log(JSON.stringify({ visible }));
|
||||
else render.ok(visible ? "you are now visible to peers" : "you are now hidden", visible ? undefined : "direct messages still reach you");
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
render.err(bridged.error);
|
||||
return EXIT.INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
|
||||
await client.setVisible(visible);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ 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. */
|
||||
@@ -46,6 +47,18 @@ 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.");
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
import { withMesh } from "./connect.js";
|
||||
import { readConfig } from "~/services/config/facade.js";
|
||||
import { tryBridge } from "~/services/bridge/client.js";
|
||||
import { render } from "~/ui/render.js";
|
||||
import { bold, dim, green, yellow } from "~/ui/styles.js";
|
||||
|
||||
@@ -68,7 +67,10 @@ async function listPeersForMesh(slug: string): Promise<PeerRecord[]> {
|
||||
const selfMemberPubkey = joined?.pubkey ?? null;
|
||||
|
||||
// Daemon path — preferred when running. Same routing pattern as send.ts:
|
||||
// ~1 ms IPC round-trip; broker WS already warm in the daemon.
|
||||
// ~1 ms IPC round-trip; broker WS already warm in the daemon. The
|
||||
// lifecycle helper inside tryListPeersViaDaemon auto-spawns the
|
||||
// daemon if it's down and probes it for liveness — no separate bridge
|
||||
// tier is needed any more (1.28.0).
|
||||
try {
|
||||
const { tryListPeersViaDaemon } = await import("~/services/bridge/daemon-route.js");
|
||||
const dr = await tryListPeersViaDaemon();
|
||||
@@ -77,13 +79,8 @@ async function listPeersForMesh(slug: string): Promise<PeerRecord[]> {
|
||||
}
|
||||
} catch { /* daemon route helper not available; fall through */ }
|
||||
|
||||
// Try warm bridge path next.
|
||||
const bridged = await tryBridge(slug, "peers");
|
||||
if (bridged && bridged.ok) {
|
||||
const peers = bridged.result as PeerRecord[];
|
||||
return peers.map((p) => annotateSelf(p, selfMemberPubkey, null));
|
||||
}
|
||||
// Cold path — open our own WS.
|
||||
// Cold path — open our own WS. Reached only when the lifecycle helper
|
||||
// could not bring the daemon up.
|
||||
let result: PeerRecord[] = [];
|
||||
await withMesh({ meshSlug: slug }, async (client) => {
|
||||
const all = (await client.listPeers()) as unknown as PeerRecord[];
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
import { withMesh } from "./connect.js";
|
||||
import { readConfig } from "~/services/config/facade.js";
|
||||
import { tryBridge } from "~/services/bridge/client.js";
|
||||
import { trySendViaDaemon } from "~/services/bridge/daemon-route.js";
|
||||
import type { Priority } from "~/services/broker/facade.js";
|
||||
import { render } from "~/ui/render.js";
|
||||
@@ -82,34 +81,12 @@ export async function runSend(flags: SendFlags, to: string, message: string): Pr
|
||||
else render.err(`send failed (daemon): ${dr.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
// dr === null → daemon not running; fall through to bridge.
|
||||
// dr === null → daemon not running and lifecycle couldn't auto-
|
||||
// spawn it; fall through to cold path. The orphaned bridge tier
|
||||
// was removed in 1.28.0.
|
||||
}
|
||||
|
||||
// Warm path — only when mesh is unambiguous.
|
||||
if (meshSlug) {
|
||||
const bridged = await tryBridge(meshSlug, "send", { to, message, priority });
|
||||
if (bridged !== null) {
|
||||
if (bridged.ok) {
|
||||
const r = bridged.result as { messageId?: string };
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify({ ok: true, messageId: r.messageId, target: to }));
|
||||
} else {
|
||||
render.ok(`sent to ${to}`, r.messageId ? dim(r.messageId.slice(0, 8)) : undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Bridge reachable but op failed — surface error, don't fall through.
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify({ ok: false, error: bridged.error }));
|
||||
} else {
|
||||
render.err(`send failed: ${bridged.error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
// bridged === null → bridge unreachable, fall through to cold path
|
||||
}
|
||||
|
||||
// Cold path
|
||||
// Cold path — open our own WS, encrypt locally, fire envelope.
|
||||
await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
|
||||
let targetSpec = to;
|
||||
if (to.startsWith("#") && !/^#[0-9a-z_-]{20,}$/i.test(to)) {
|
||||
|
||||
Reference in New Issue
Block a user