refactor(cli): migrate to citty — --help generated from flag definitions
Replace manual switch + HELP string with citty defineCommand/runMain. Flag definitions in index.ts are now the single source of truth for --help output. Remove parseArgs() from launch.ts; accept citty-parsed flags + rawArgs (-- passthrough to claude preserved). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "1.27.1",
|
"@modelcontextprotocol/sdk": "1.27.1",
|
||||||
|
"citty": "0.2.2",
|
||||||
"libsodium-wrappers": "0.7.15",
|
"libsodium-wrappers": "0.7.15",
|
||||||
"ws": "8.20.0",
|
"ws": "8.20.0",
|
||||||
"zod": "4.1.13"
|
"zod": "4.1.13"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* `claudemesh launch` — spawn `claude` with peer mesh identity.
|
* `claudemesh launch` — spawn `claude` with peer mesh identity.
|
||||||
*
|
*
|
||||||
|
* Flags are defined in index.ts (citty command) — that is the source of
|
||||||
|
* truth. This file receives already-parsed flags and rawArgs.
|
||||||
|
*
|
||||||
* Flow:
|
* Flow:
|
||||||
* 1. Parse --name, --join, --mesh, --quiet flags
|
* 1. Receive parsed flags from citty + rawArgs for -- passthrough
|
||||||
* 2. If --join: run join flow first (accepts token or URL)
|
* 2. If --join: run join flow first
|
||||||
* 3. Load config → pick mesh (auto if 1, interactive picker if >1)
|
* 3. Load config → pick mesh (auto if 1, interactive picker if >1)
|
||||||
* 4. Write per-session config to tmpdir (isolates mesh selection)
|
* 4. Write per-session config to tmpdir (isolates mesh selection)
|
||||||
* 5. Spawn claude with CLAUDEMESH_CONFIG_DIR + CLAUDEMESH_DISPLAY_NAME
|
* 5. Spawn claude with CLAUDEMESH_CONFIG_DIR + CLAUDEMESH_DISPLAY_NAME
|
||||||
@@ -18,85 +21,17 @@ import { createInterface } from "node:readline";
|
|||||||
import { loadConfig, getConfigPath } from "../state/config";
|
import { loadConfig, getConfigPath } from "../state/config";
|
||||||
import type { Config, JoinedMesh, GroupEntry } from "../state/config";
|
import type { Config, JoinedMesh, GroupEntry } from "../state/config";
|
||||||
|
|
||||||
// --- Arg parsing ---
|
// Flags as parsed by citty (index.ts is the source of truth for definitions).
|
||||||
|
export interface LaunchFlags {
|
||||||
interface LaunchArgs {
|
name?: string;
|
||||||
name: string | null;
|
role?: string;
|
||||||
role: string | null;
|
groups?: string;
|
||||||
groups: string | null; // comma-separated, e.g. "frontend:lead,reviewers:member"
|
join?: string;
|
||||||
joinLink: string | null;
|
mesh?: string;
|
||||||
meshSlug: string | null;
|
"message-mode"?: string;
|
||||||
messageMode: "push" | "inbox" | "off" | null;
|
"system-prompt"?: string;
|
||||||
systemPrompt: string | null;
|
yes?: boolean;
|
||||||
quiet: boolean;
|
quiet?: boolean;
|
||||||
skipPermConfirm: boolean;
|
|
||||||
claudeArgs: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(argv: string[]): LaunchArgs {
|
|
||||||
const result: LaunchArgs = {
|
|
||||||
name: null,
|
|
||||||
role: null,
|
|
||||||
groups: null,
|
|
||||||
joinLink: null,
|
|
||||||
meshSlug: null,
|
|
||||||
messageMode: null,
|
|
||||||
systemPrompt: null,
|
|
||||||
quiet: false,
|
|
||||||
skipPermConfirm: false,
|
|
||||||
claudeArgs: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
while (i < argv.length) {
|
|
||||||
const arg = argv[i]!;
|
|
||||||
if (arg === "--name" && i + 1 < argv.length) {
|
|
||||||
result.name = argv[++i]!;
|
|
||||||
} else if (arg.startsWith("--name=")) {
|
|
||||||
result.name = arg.slice("--name=".length);
|
|
||||||
} else if (arg === "--role" && i + 1 < argv.length) {
|
|
||||||
result.role = argv[++i]!;
|
|
||||||
} else if (arg.startsWith("--role=")) {
|
|
||||||
result.role = arg.slice("--role=".length);
|
|
||||||
} else if (arg === "--groups" && i + 1 < argv.length) {
|
|
||||||
result.groups = argv[++i]!;
|
|
||||||
} else if (arg.startsWith("--groups=")) {
|
|
||||||
result.groups = arg.slice("--groups=".length);
|
|
||||||
} else if (arg === "--join" && i + 1 < argv.length) {
|
|
||||||
result.joinLink = argv[++i]!;
|
|
||||||
} else if (arg.startsWith("--join=")) {
|
|
||||||
result.joinLink = arg.slice("--join=".length);
|
|
||||||
} else if (arg === "--mesh" && i + 1 < argv.length) {
|
|
||||||
result.meshSlug = argv[++i]!;
|
|
||||||
} else if (arg.startsWith("--mesh=")) {
|
|
||||||
result.meshSlug = arg.slice("--mesh=".length);
|
|
||||||
} else if (arg === "--message-mode" && i + 1 < argv.length) {
|
|
||||||
const mode = argv[++i]! as "push" | "inbox" | "off";
|
|
||||||
if (["push", "inbox", "off"].includes(mode)) result.messageMode = mode;
|
|
||||||
} else if (arg.startsWith("--message-mode=")) {
|
|
||||||
const mode = arg.slice("--message-mode=".length) as "push" | "inbox" | "off";
|
|
||||||
if (["push", "inbox", "off"].includes(mode)) result.messageMode = mode;
|
|
||||||
} else if (arg === "--system-prompt" && i + 1 < argv.length) {
|
|
||||||
result.systemPrompt = argv[++i]!;
|
|
||||||
} else if (arg.startsWith("--system-prompt=")) {
|
|
||||||
result.systemPrompt = arg.slice("--system-prompt=".length);
|
|
||||||
} else if (arg === "--inbox") {
|
|
||||||
result.messageMode = "inbox";
|
|
||||||
} else if (arg === "--no-messages") {
|
|
||||||
result.messageMode = "off";
|
|
||||||
} else if (arg === "--quiet") {
|
|
||||||
result.quiet = true;
|
|
||||||
} else if (arg === "-y" || arg === "--yes") {
|
|
||||||
result.skipPermConfirm = true;
|
|
||||||
} else if (arg === "--") {
|
|
||||||
result.claudeArgs.push(...argv.slice(i + 1));
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
result.claudeArgs.push(arg);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Interactive mesh picker ---
|
// --- Interactive mesh picker ---
|
||||||
@@ -218,8 +153,26 @@ function printBanner(name: string, meshSlug: string, role: string | null, groups
|
|||||||
|
|
||||||
// --- Main ---
|
// --- Main ---
|
||||||
|
|
||||||
export async function runLaunch(extraArgs: string[]): Promise<void> {
|
export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<void> {
|
||||||
const args = parseArgs(extraArgs);
|
// Extract args that follow "--" — passed straight through to claude.
|
||||||
|
const dashIdx = rawArgs.indexOf("--");
|
||||||
|
const claudePassthrough = dashIdx >= 0 ? rawArgs.slice(dashIdx + 1) : [];
|
||||||
|
|
||||||
|
// Normalise flags into the internal shape used below.
|
||||||
|
const args = {
|
||||||
|
name: flags.name ?? null,
|
||||||
|
role: flags.role ?? null,
|
||||||
|
groups: flags.groups ?? null,
|
||||||
|
joinLink: flags.join ?? null,
|
||||||
|
meshSlug: flags.mesh ?? null,
|
||||||
|
messageMode: (["push", "inbox", "off"].includes(flags["message-mode"] ?? "")
|
||||||
|
? flags["message-mode"] as "push" | "inbox" | "off"
|
||||||
|
: null),
|
||||||
|
systemPrompt: flags["system-prompt"] ?? null,
|
||||||
|
quiet: flags.quiet ?? false,
|
||||||
|
skipPermConfirm: flags.yes ?? false,
|
||||||
|
claudeArgs: claudePassthrough,
|
||||||
|
};
|
||||||
|
|
||||||
// 1. If --join, run join flow first.
|
// 1. If --join, run join flow first.
|
||||||
if (args.joinLink) {
|
if (args.joinLink) {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* claudemesh-cli entry point.
|
* claudemesh-cli entry point.
|
||||||
*
|
*
|
||||||
|
* Uses citty to define commands and flags. --help is generated from
|
||||||
|
* the command definitions — the flag list here IS the documentation.
|
||||||
|
*
|
||||||
* Dispatches between two modes:
|
* Dispatches between two modes:
|
||||||
* - `claudemesh mcp` → MCP server (stdio transport)
|
* - `claudemesh mcp` → MCP server (stdio transport)
|
||||||
* - `claudemesh <subcommand>` → CLI subcommand
|
* - `claudemesh <subcommand>` → CLI subcommand
|
||||||
*
|
|
||||||
* Claude Code invokes the `mcp` mode via stdio. Humans use all others.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { defineCommand, runMain } from "citty";
|
||||||
import { startMcpServer } from "./mcp/server";
|
import { startMcpServer } from "./mcp/server";
|
||||||
import { runInstall, runUninstall } from "./commands/install";
|
import { runInstall, runUninstall } from "./commands/install";
|
||||||
import { runJoin } from "./commands/join";
|
import { runJoin } from "./commands/join";
|
||||||
@@ -21,119 +23,152 @@ import { runDoctor } from "./commands/doctor";
|
|||||||
import { runWelcome } from "./commands/welcome";
|
import { runWelcome } from "./commands/welcome";
|
||||||
import { VERSION } from "./version";
|
import { VERSION } from "./version";
|
||||||
|
|
||||||
const HELP = `claudemesh v${VERSION} — peer mesh for Claude Code sessions
|
const launch = defineCommand({
|
||||||
|
meta: {
|
||||||
Usage:
|
name: "launch",
|
||||||
claudemesh <command> [args]
|
description: "Launch Claude Code connected to a mesh with real-time peer messaging",
|
||||||
|
},
|
||||||
Commands:
|
args: {
|
||||||
install Register MCP server + status hooks with Claude Code
|
name: {
|
||||||
--no-hooks Register MCP only, skip hooks
|
type: "string",
|
||||||
uninstall Remove MCP server and hooks
|
description: "Display name for this session",
|
||||||
launch [opts] Launch Claude Code connected to a mesh
|
},
|
||||||
join <url> Join a mesh via invite URL
|
role: {
|
||||||
list Show joined meshes and identities
|
type: "string",
|
||||||
leave <slug> Leave a mesh
|
description: "Role tag (dev, lead, analyst — free-form)",
|
||||||
status Check broker reachability for each joined mesh
|
},
|
||||||
doctor Diagnose install, config, keypairs, and PATH
|
groups: {
|
||||||
mcp Start MCP server (stdio — Claude Code only)
|
type: "string",
|
||||||
--help, -h Show this help
|
description: 'Groups to join: "group:role,group2" — colon sets role. Hierarchy via slash: "eng/frontend:lead"',
|
||||||
--version, -v Show version
|
},
|
||||||
|
mesh: {
|
||||||
launch options:
|
type: "string",
|
||||||
--name <name> Display name for this session
|
description: "Select mesh by slug (interactive picker if omitted and >1 joined)",
|
||||||
--role <role> Role tag (dev, lead, analyst — free-form)
|
},
|
||||||
--groups <spec> Groups to join: "g1:role,g2" (colon = role)
|
join: {
|
||||||
--mesh <slug> Select mesh by slug (interactive if omitted)
|
type: "string",
|
||||||
--join <url> Join a mesh before launching
|
description: "Join a mesh via invite URL before launching",
|
||||||
--message-mode <mode> push (default) | inbox | off
|
},
|
||||||
push — peer messages arrive in real time
|
"message-mode": {
|
||||||
inbox — held until you call check_messages
|
type: "string",
|
||||||
off — no messages; use tools only
|
description: "push (default) | inbox | off — controls how peer messages are delivered",
|
||||||
--system-prompt <text> Set Claude's system prompt for this session
|
},
|
||||||
-y, --yes Skip permission confirmation
|
"system-prompt": {
|
||||||
--quiet Skip banner and all interactive prompts
|
type: "string",
|
||||||
-- <args> Pass remaining args directly to claude
|
description: "Set Claude's system prompt for this session",
|
||||||
|
},
|
||||||
Full non-interactive launch:
|
yes: {
|
||||||
claudemesh launch \\
|
type: "boolean",
|
||||||
--name Worker --mesh myteam --role analyst \\
|
alias: "y",
|
||||||
--groups "myteam/docs:member" \\
|
description: "Skip permission confirmation",
|
||||||
--message-mode push \\
|
default: false,
|
||||||
--system-prompt "You are a documentation analyst..." \\
|
},
|
||||||
-y --quiet
|
quiet: {
|
||||||
|
type: "boolean",
|
||||||
Groups support hierarchy (slash-separated):
|
description: "Skip banner and all interactive prompts",
|
||||||
--groups "eng/frontend:lead,eng/reviewers"
|
default: false,
|
||||||
@eng delivers to members of @eng, @eng/frontend, @eng/reviewers, etc.
|
},
|
||||||
|
},
|
||||||
Environment:
|
run({ args, rawArgs }) {
|
||||||
CLAUDEMESH_BROKER_URL Override broker URL (default: wss://ic.claudemesh.com/ws)
|
// Forward to the existing launch runner, preserving -- passthrough to claude.
|
||||||
CLAUDEMESH_CONFIG_DIR Override config directory (default: ~/.claudemesh/)
|
return runLaunch(args, rawArgs);
|
||||||
CLAUDEMESH_DISPLAY_NAME Override display name (set automatically by launch)
|
},
|
||||||
CLAUDEMESH_ROLE Override role tag (set automatically by launch)
|
|
||||||
CLAUDEMESH_DEBUG=1 Verbose logging
|
|
||||||
`;
|
|
||||||
|
|
||||||
const cmd = process.argv[2];
|
|
||||||
const args = process.argv.slice(3);
|
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
|
||||||
switch (cmd) {
|
|
||||||
case "mcp":
|
|
||||||
await startMcpServer();
|
|
||||||
return;
|
|
||||||
case "install":
|
|
||||||
runInstall(args);
|
|
||||||
return;
|
|
||||||
case "uninstall":
|
|
||||||
runUninstall();
|
|
||||||
return;
|
|
||||||
case "hook":
|
|
||||||
await runHook(args);
|
|
||||||
return;
|
|
||||||
case "launch":
|
|
||||||
await runLaunch(args);
|
|
||||||
return;
|
|
||||||
case "join":
|
|
||||||
await runJoin(args);
|
|
||||||
return;
|
|
||||||
case "list":
|
|
||||||
runList();
|
|
||||||
return;
|
|
||||||
case "leave":
|
|
||||||
runLeave(args);
|
|
||||||
return;
|
|
||||||
case "status":
|
|
||||||
await runStatus();
|
|
||||||
return;
|
|
||||||
case "doctor":
|
|
||||||
await runDoctor();
|
|
||||||
return;
|
|
||||||
case "seed-test-mesh":
|
|
||||||
runSeedTestMesh(args);
|
|
||||||
return;
|
|
||||||
case "--version":
|
|
||||||
case "-v":
|
|
||||||
case "version":
|
|
||||||
console.log(VERSION);
|
|
||||||
return;
|
|
||||||
case "--help":
|
|
||||||
case "-h":
|
|
||||||
case "help":
|
|
||||||
console.log(HELP);
|
|
||||||
return;
|
|
||||||
case undefined:
|
|
||||||
runWelcome();
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
console.error(`Unknown command: ${cmd}`);
|
|
||||||
console.error("Run `claudemesh --help` for usage.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((e) => {
|
|
||||||
console.error(`claudemesh: ${e instanceof Error ? e.message : String(e)}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const install = defineCommand({
|
||||||
|
meta: {
|
||||||
|
name: "install",
|
||||||
|
description: "Register MCP server + status hooks with Claude Code",
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
"no-hooks": {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Register MCP server only, skip hooks",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run({ rawArgs }) {
|
||||||
|
runInstall(rawArgs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const join = defineCommand({
|
||||||
|
meta: {
|
||||||
|
name: "join",
|
||||||
|
description: "Join a mesh via invite URL",
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
url: {
|
||||||
|
type: "positional",
|
||||||
|
description: "Invite URL (https://claudemesh.com/join/...)",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run({ args }) {
|
||||||
|
return runJoin([args.url]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const leave = defineCommand({
|
||||||
|
meta: {
|
||||||
|
name: "leave",
|
||||||
|
description: "Leave a joined mesh",
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
slug: {
|
||||||
|
type: "positional",
|
||||||
|
description: "Mesh slug to leave",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run({ args }) {
|
||||||
|
runLeave([args.slug]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const main = defineCommand({
|
||||||
|
meta: {
|
||||||
|
name: "claudemesh",
|
||||||
|
version: VERSION,
|
||||||
|
description: "Peer mesh for Claude Code sessions",
|
||||||
|
},
|
||||||
|
subCommands: {
|
||||||
|
launch,
|
||||||
|
install,
|
||||||
|
uninstall: defineCommand({
|
||||||
|
meta: { name: "uninstall", description: "Remove MCP server and hooks" },
|
||||||
|
run() { runUninstall(); },
|
||||||
|
}),
|
||||||
|
join,
|
||||||
|
list: defineCommand({
|
||||||
|
meta: { name: "list", description: "Show joined meshes and identities" },
|
||||||
|
run() { runList(); },
|
||||||
|
}),
|
||||||
|
leave,
|
||||||
|
status: defineCommand({
|
||||||
|
meta: { name: "status", description: "Check broker reachability for each joined mesh" },
|
||||||
|
async run() { await runStatus(); },
|
||||||
|
}),
|
||||||
|
doctor: defineCommand({
|
||||||
|
meta: { name: "doctor", description: "Diagnose install, config, keypairs, and PATH" },
|
||||||
|
async run() { await runDoctor(); },
|
||||||
|
}),
|
||||||
|
mcp: defineCommand({
|
||||||
|
meta: { name: "mcp", description: "Start MCP server (stdio — invoked by Claude Code, not users)" },
|
||||||
|
async run() { await startMcpServer(); },
|
||||||
|
}),
|
||||||
|
"seed-test-mesh": defineCommand({
|
||||||
|
meta: { name: "seed-test-mesh", description: "Dev only: inject a mesh into config (skips invite flow)" },
|
||||||
|
run({ rawArgs }) { runSeedTestMesh(rawArgs); },
|
||||||
|
}),
|
||||||
|
hook: defineCommand({
|
||||||
|
meta: { name: "hook", description: "Internal hook handler (invoked by Claude Code hooks)" },
|
||||||
|
async run({ rawArgs }) { await runHook(rawArgs); },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
run() {
|
||||||
|
runWelcome();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
runMain(main);
|
||||||
|
|||||||
Reference in New Issue
Block a user