feat(cli): stateful welcome screen + v0.1.4 bump
Running \`claudemesh\` with no args now detects install state and prints context-appropriate guidance: suggests \`install\` if MCP not registered, \`join\` if no meshes, \`launch\` if ready. Replaces the static HELP dump with a first-run wizard that meets users where they are. Static HELP still available via --help. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
111
apps/cli/src/commands/welcome.ts
Normal file
111
apps/cli/src/commands/welcome.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Stateful welcome screen — shown when the user runs `claudemesh`
|
||||||
|
* with no arguments. Detects install state + joined meshes + prints
|
||||||
|
* the next action they should take.
|
||||||
|
*
|
||||||
|
* States, in priority order:
|
||||||
|
* 1. MCP not registered in ~/.claude.json → run install
|
||||||
|
* 2. Config dir exists but no meshes joined → run join
|
||||||
|
* 3. Meshes joined, all reachable → run launch
|
||||||
|
* 4. Meshes joined, broker unreachable → run status / doctor
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { loadConfig } from "../state/config";
|
||||||
|
import { VERSION } from "../version";
|
||||||
|
|
||||||
|
type State = "no-install" | "no-meshes" | "ready" | "broken-config";
|
||||||
|
|
||||||
|
function detectState(): State {
|
||||||
|
// 1. MCP registered?
|
||||||
|
const claudeConfig = join(homedir(), ".claude.json");
|
||||||
|
let mcpRegistered = false;
|
||||||
|
if (existsSync(claudeConfig)) {
|
||||||
|
try {
|
||||||
|
const cfg = JSON.parse(readFileSync(claudeConfig, "utf-8")) as {
|
||||||
|
mcpServers?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
mcpRegistered = Boolean(cfg.mcpServers?.["claudemesh"]);
|
||||||
|
} catch {
|
||||||
|
/* treat parse errors as not-registered */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mcpRegistered) return "no-install";
|
||||||
|
|
||||||
|
// 2. Config parseable + has meshes?
|
||||||
|
try {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
return cfg.meshes.length === 0 ? "no-meshes" : "ready";
|
||||||
|
} catch {
|
||||||
|
return "broken-config";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runWelcome(): void {
|
||||||
|
const useColor =
|
||||||
|
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||||
|
const bold = (s: string): string => (useColor ? `\x1b[1m${s}\x1b[22m` : s);
|
||||||
|
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 yellow = (s: string): string => (useColor ? `\x1b[33m${s}\x1b[39m` : s);
|
||||||
|
|
||||||
|
console.log(bold(`claudemesh v${VERSION}`) + dim(" — peer mesh for Claude Code"));
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
|
||||||
|
const state = detectState();
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "no-install":
|
||||||
|
console.log("Welcome. Let's get you set up.");
|
||||||
|
console.log("");
|
||||||
|
console.log(bold("Step 1:") + " register the MCP server + status hooks");
|
||||||
|
console.log(` ${green("$")} claudemesh install`);
|
||||||
|
console.log("");
|
||||||
|
console.log(dim("Step 2 (after restart): claudemesh join <invite-url>"));
|
||||||
|
console.log(dim("Step 3: claudemesh launch"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "no-meshes":
|
||||||
|
console.log(green("✓") + " MCP registered. Now join a mesh.");
|
||||||
|
console.log("");
|
||||||
|
console.log(bold("Step 2:") + " join a mesh");
|
||||||
|
console.log(` ${green("$")} claudemesh join https://claudemesh.com/join/<token>`);
|
||||||
|
console.log("");
|
||||||
|
console.log(
|
||||||
|
dim(" Don't have an invite? Create one at ") +
|
||||||
|
bold("https://claudemesh.com") +
|
||||||
|
dim(" or ask a mesh owner."),
|
||||||
|
);
|
||||||
|
console.log("");
|
||||||
|
console.log(dim("Step 3 (after joining): claudemesh launch"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ready": {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const meshNames = cfg.meshes.map((m) => m.slug).join(", ");
|
||||||
|
console.log(green("✓") + " MCP registered.");
|
||||||
|
console.log(green("✓") + ` ${cfg.meshes.length} mesh(es) joined: ${meshNames}`);
|
||||||
|
console.log("");
|
||||||
|
console.log(bold("You're ready.") + " Launch Claude Code with real-time peer messages:");
|
||||||
|
console.log(` ${green("$")} claudemesh launch`);
|
||||||
|
console.log("");
|
||||||
|
console.log(dim(" (Plain `claude` works too — messages pull-only via check_messages.)"));
|
||||||
|
console.log("");
|
||||||
|
console.log(dim("Health check: claudemesh status"));
|
||||||
|
console.log(dim("Diagnostics: claudemesh doctor"));
|
||||||
|
console.log(dim("All commands: claudemesh --help"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "broken-config":
|
||||||
|
console.log(yellow("⚠") + " Your ~/.claudemesh/config.json is unreadable.");
|
||||||
|
console.log("");
|
||||||
|
console.log("Run diagnostics to see what's wrong:");
|
||||||
|
console.log(` ${green("$")} claudemesh doctor`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { runHook } from "./commands/hook";
|
|||||||
import { runLaunch } from "./commands/launch";
|
import { runLaunch } from "./commands/launch";
|
||||||
import { runStatus } from "./commands/status";
|
import { runStatus } from "./commands/status";
|
||||||
import { runDoctor } from "./commands/doctor";
|
import { runDoctor } from "./commands/doctor";
|
||||||
|
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 HELP = `claudemesh v${VERSION} — peer mesh for Claude Code sessions
|
||||||
@@ -94,9 +95,11 @@ async function main(): Promise<void> {
|
|||||||
case "--help":
|
case "--help":
|
||||||
case "-h":
|
case "-h":
|
||||||
case "help":
|
case "help":
|
||||||
case undefined:
|
|
||||||
console.log(HELP);
|
console.log(HELP);
|
||||||
return;
|
return;
|
||||||
|
case undefined:
|
||||||
|
runWelcome();
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown command: ${cmd}`);
|
console.error(`Unknown command: ${cmd}`);
|
||||||
console.error("Run `claudemesh --help` for usage.");
|
console.error("Run `claudemesh --help` for usage.");
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export async function startMcpServer(): Promise<void> {
|
|||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
{ name: "claudemesh", version: "0.1.3" },
|
{ name: "claudemesh", version: "0.1.4" },
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
experimental: { "claude/channel": {} },
|
experimental: { "claude/channel": {} },
|
||||||
|
|||||||
Reference in New Issue
Block a user