@mushi-mushi/cli 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -78,6 +78,23 @@ var MushiCliError = class extends Error {
78
78
  };
79
79
  }
80
80
  };
81
+ function printAndExit(err, asJson = false) {
82
+ const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
83
+ "E_INTERNAL",
84
+ err instanceof Error ? err.message : String(err),
85
+ "this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
86
+ err
87
+ );
88
+ if (asJson) {
89
+ process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
90
+ } else {
91
+ process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
92
+ `);
93
+ if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
94
+ `);
95
+ }
96
+ process.exit(cliErr.exitCode);
97
+ }
81
98
 
82
99
  // src/signals.ts
83
100
  var installed = false;
@@ -110,7 +127,7 @@ function getAbortSignal(external) {
110
127
  }
111
128
 
112
129
  // src/version.ts
113
- var MUSHI_CLI_VERSION = true ? "0.21.0" : "0.0.0-dev";
130
+ var MUSHI_CLI_VERSION = true ? "0.22.0" : "0.0.0-dev";
114
131
 
115
132
  // src/init.ts
116
133
  import * as p from "@clack/prompts";
@@ -237,6 +254,20 @@ async function openInBrowser(url) {
237
254
 
238
255
  // src/cli-shared.ts
239
256
  var API_TIMEOUT_MS = 15e3;
257
+ async function probeEndpointHealth(endpoint) {
258
+ const t0 = Date.now();
259
+ const controller = new AbortController();
260
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
261
+ try {
262
+ const res = await fetch(`${endpoint}/health`, { signal: controller.signal });
263
+ clearTimeout(timer);
264
+ const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
265
+ return { ok: res.ok, status: res.status, latencyMs: Date.now() - t0, body };
266
+ } catch (err) {
267
+ clearTimeout(timer);
268
+ throw err;
269
+ }
270
+ }
240
271
  async function apiCall(path, config, options = {}) {
241
272
  const endpoint = config.endpoint;
242
273
  if (!endpoint) {
@@ -337,19 +368,21 @@ function die(result, exitCode = 1) {
337
368
  function requireConfig(opts = {}) {
338
369
  const config = loadConfig();
339
370
  if (!config.apiKey) {
340
- process.stderr.write(
341
- "error: API key not configured.\n Run: mushi login --api-key <key>\n Or: export MUSHI_API_KEY=<key>\n"
342
- );
343
- process.exit(2);
371
+ printAndExit(new MushiCliError(
372
+ "E_AUTH_MISSING",
373
+ "API key not configured.",
374
+ "Run: mushi login --api-key <key> Or: export MUSHI_API_KEY=<key>"
375
+ ));
344
376
  }
345
377
  if (!config.endpoint) {
346
378
  config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
347
379
  }
348
380
  if (opts.needsProject && !config.projectId) {
349
- process.stderr.write(
350
- "error: Project ID not configured.\n Run: mushi login --project-id <uuid>\n Or: export MUSHI_PROJECT_ID=<uuid>\n Find your project ID: " + consoleUrl(resolveConsoleUrlSync(), "/projects") + "\n"
351
- );
352
- process.exit(2);
381
+ printAndExit(new MushiCliError(
382
+ "E_PROJECT_MISSING",
383
+ "Project ID not configured.",
384
+ `Run: mushi login --project-id <uuid> Or find your project ID at ${consoleUrl(resolveConsoleUrlSync(), "/projects")}`
385
+ ));
353
386
  }
354
387
  return config;
355
388
  }
@@ -716,7 +749,7 @@ var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
716
749
  async function runInit(options = {}) {
717
750
  const cwd = options.cwd ?? process.cwd();
718
751
  ensureInteractiveOrBailOut(options);
719
- p.intro("\u{1F41B} Mushi Mushi setup wizard");
752
+ p.intro("Mushi setup");
720
753
  await printFreshnessHint();
721
754
  warnIfWorkspaceRoot(cwd);
722
755
  const pkg = readPackageJson(cwd);
@@ -750,7 +783,7 @@ async function runInit(options = {}) {
750
783
  printNextSteps(framework, consoleBase, enableRewards);
751
784
  await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
752
785
  await maybeOfferConnect(credentials, options, consoleBase);
753
- p.outro("Setup complete. Happy bug squashing \u{1F41B}");
786
+ p.outro("Setup complete.");
754
787
  }
755
788
  function ensureInteractiveOrBailOut(options) {
756
789
  const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -1657,24 +1690,19 @@ Useful after 'mushi login' to confirm credentials are correct.`).action(async (o
1657
1690
  const config = requireConfig();
1658
1691
  const t0 = Date.now();
1659
1692
  try {
1660
- const controller = new AbortController();
1661
- const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1662
- const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
1663
- clearTimeout(timer);
1664
- const latency = Date.now() - t0;
1693
+ const probe = await probeEndpointHealth(config.endpoint);
1665
1694
  if (opts.json) {
1666
- console.log(JSON.stringify({ ok: res.ok, status: res.status, latency_ms: latency }));
1695
+ console.log(JSON.stringify({ ok: probe.ok, status: probe.status, latency_ms: probe.latencyMs }));
1667
1696
  } else {
1668
- const symbol = res.ok ? "\u2713" : "\u2717";
1669
- console.log(`${symbol} ${res.ok ? "OK" : "FAIL"} \u2014 ${res.status} (${latency}ms)`);
1670
- if (!res.ok) process.exit(1);
1697
+ console.log(`${probe.ok ? "OK" : "FAIL"} \u2014 ${probe.status} (${probe.latencyMs}ms)`);
1698
+ if (!probe.ok) process.exit(1);
1671
1699
  }
1672
1700
  } catch (err) {
1673
1701
  const msg = err instanceof Error ? err.message : String(err);
1674
1702
  if (opts.json) {
1675
1703
  console.log(JSON.stringify({ ok: false, error: msg, latency_ms: Date.now() - t0 }));
1676
1704
  } else {
1677
- process.stderr.write(`\u2717 Unreachable: ${msg}
1705
+ process.stderr.write(`FAIL Unreachable: ${msg}
1678
1706
  `);
1679
1707
  }
1680
1708
  process.exit(1);
@@ -1739,22 +1767,16 @@ function registerDeployCommands(program2) {
1739
1767
  const deploy = program2.command("deploy").description("Deployment management");
1740
1768
  deploy.command("check").description("Check edge function health and measure latency").option("--json", "Machine-readable JSON output").action(async (opts) => {
1741
1769
  const config = requireConfig();
1742
- const t0 = Date.now();
1743
1770
  try {
1744
- const controller = new AbortController();
1745
- const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1746
- const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
1747
- clearTimeout(timer);
1748
- const latency = Date.now() - t0;
1749
- const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
1771
+ const probe = await probeEndpointHealth(config.endpoint);
1750
1772
  if (opts.json) {
1751
- console.log(JSON.stringify({ ok: res.ok, status: res.status, latency_ms: latency, ...body }));
1773
+ console.log(JSON.stringify({ ok: probe.ok, status: probe.status, latency_ms: probe.latencyMs, ...probe.body }));
1752
1774
  } else {
1753
- console.log(`Health: ${res.status === 200 ? "OK" : "FAIL"} (${res.status}) \u2014 ${latency}ms`);
1754
- if (body["version"]) console.log(` Version: ${body["version"]}`);
1755
- if (body["region"]) console.log(` Region: ${body["region"]}`);
1775
+ console.log(`Health: ${probe.status === 200 ? "OK" : "FAIL"} (${probe.status}) \u2014 ${probe.latencyMs}ms`);
1776
+ if (probe.body["version"]) console.log(` Version: ${probe.body["version"]}`);
1777
+ if (probe.body["region"]) console.log(` Region: ${probe.body["region"]}`);
1756
1778
  }
1757
- if (!res.ok) process.exit(1);
1779
+ if (!probe.ok) process.exit(1);
1758
1780
  } catch (err) {
1759
1781
  const msg = err instanceof Error ? err.message : String(err);
1760
1782
  if (opts.json) {
@@ -2158,11 +2180,11 @@ AI code review context.
2158
2180
  Typical CI usage:
2159
2181
  MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
2160
2182
  npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
2161
- const { writeFile, mkdir } = await import("fs/promises");
2162
- const nodePath = await import("path");
2183
+ const { writeFile: writeFile2, mkdir } = await import("fs/promises");
2184
+ const nodePath2 = await import("path");
2163
2185
  const config = requireConfig();
2164
2186
  const cwd = opts.cwd ?? process.cwd();
2165
- const target = nodePath.join(cwd, ".mushi", "lessons.json");
2187
+ const target = nodePath2.join(cwd, ".mushi", "lessons.json");
2166
2188
  const result = await apiCall("/v1/sync/lessons?limit=500", config);
2167
2189
  if (!result.ok) die(result);
2168
2190
  const rows = Array.isArray(result.data) ? result.data : [];
@@ -2185,8 +2207,8 @@ Typical CI usage:
2185
2207
  console.log(JSON.stringify(output, null, 2));
2186
2208
  return;
2187
2209
  }
2188
- await mkdir(nodePath.dirname(target), { recursive: true });
2189
- await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
2210
+ await mkdir(nodePath2.dirname(target), { recursive: true });
2211
+ await writeFile2(target, JSON.stringify(output, null, 2) + "\n", "utf8");
2190
2212
  if (opts.json) {
2191
2213
  console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
2192
2214
  } else {
@@ -2382,34 +2404,34 @@ Examples:
2382
2404
  mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
2383
2405
  const config = requireConfig({ needsProject: true });
2384
2406
  const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
2385
- const nodePath = await import("path");
2407
+ const nodePath2 = await import("path");
2386
2408
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
2387
2409
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
2388
2410
  const MAX_FILE_BYTES = 5e5;
2389
2411
  async function* walk(dir) {
2390
2412
  const entries = await readdir2(dir, { withFileTypes: true });
2391
2413
  for (const e of entries) {
2392
- const full = nodePath.join(dir, e.name);
2414
+ const full = nodePath2.join(dir, e.name);
2393
2415
  if (SKIP.test(full)) continue;
2394
2416
  if (e.isDirectory()) yield* walk(full);
2395
- else if (EXTS.has(nodePath.extname(e.name))) yield full;
2417
+ else if (EXTS.has(nodePath2.extname(e.name))) yield full;
2396
2418
  }
2397
2419
  }
2398
2420
  let count = 0;
2399
2421
  let bytes = 0;
2400
2422
  let errors = 0;
2401
- const root = nodePath.resolve(path);
2423
+ const root = nodePath2.resolve(path);
2402
2424
  for await (const file of walk(root)) {
2403
- const lang = nodePath.extname(file).slice(1);
2425
+ const lang = nodePath2.extname(file).slice(1);
2404
2426
  if (opts.language && opts.language !== lang) continue;
2405
2427
  const stats = await stat(file);
2406
2428
  if (stats.size > MAX_FILE_BYTES) {
2407
- if (!opts.json) process.stdout.write(` skip ${nodePath.relative(root, file)} (>${MAX_FILE_BYTES / 1e3}KB)
2429
+ if (!opts.json) process.stdout.write(` skip ${nodePath2.relative(root, file)} (>${MAX_FILE_BYTES / 1e3}KB)
2408
2430
  `);
2409
2431
  continue;
2410
2432
  }
2411
2433
  const source = await readFile2(file, "utf8");
2412
- const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
2434
+ const relative2 = nodePath2.relative(root, file).replaceAll("\\", "/");
2413
2435
  count++;
2414
2436
  bytes += source.length;
2415
2437
  if (opts.dryRun) {
@@ -2455,6 +2477,37 @@ Examples:
2455
2477
  });
2456
2478
  }
2457
2479
 
2480
+ // src/project-bootstrap.ts
2481
+ import { writeFile } from "fs/promises";
2482
+ import { existsSync as existsSync4 } from "fs";
2483
+ import nodePath from "path";
2484
+ async function writeProjectBootstrapFiles(opts) {
2485
+ const cwd = opts.cwd ?? process.cwd();
2486
+ const envPath = nodePath.join(cwd, ".env.local");
2487
+ const envLines = [
2488
+ "# Mushi MCP \u2014 drop into .env.local (gitignored). The MCP binary picks these up on spawn.",
2489
+ `MUSHI_API_ENDPOINT=${opts.endpoint}`,
2490
+ `MUSHI_PROJECT_ID=${opts.projectId}`,
2491
+ `MUSHI_API_KEY=${opts.apiKey}`,
2492
+ ""
2493
+ ];
2494
+ const envUpdated = existsSync4(envPath);
2495
+ await writeFile(envPath, envLines.join("\n"), "utf8");
2496
+ const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
2497
+ const serverName = buildMcpServerName({ legacy: true });
2498
+ const serverBlock = buildMcpServerBlock({
2499
+ endpoint: opts.endpoint,
2500
+ projectId: opts.projectId,
2501
+ apiKey: opts.apiKey
2502
+ });
2503
+ const { created: mcpCreated } = await writeMcpServerEntry({
2504
+ configPath: mcpPath,
2505
+ serverName,
2506
+ serverBlock
2507
+ });
2508
+ return { envUpdated, mcpUpdated: !mcpCreated };
2509
+ }
2510
+
2458
2511
  // src/commands/project.ts
2459
2512
  function registerProjectCommands(program2) {
2460
2513
  const project = program2.command("project").description("Project management");
@@ -2469,9 +2522,6 @@ Typical first-time flow:
2469
2522
  # Browser opens \u2192 click Approve \u2192 pick or create a project
2470
2523
  # CLI writes .env.local and .cursor/mcp.json
2471
2524
  # mushi whoami to confirm`).action(async (opts) => {
2472
- const { writeFile } = await import("fs/promises");
2473
- const { existsSync: existsSync4 } = await import("fs");
2474
- const nodePath = await import("path");
2475
2525
  const savedConfig = loadConfig();
2476
2526
  const endpoint = resolveCloudEndpoint(
2477
2527
  opts.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? savedConfig.endpoint
@@ -2568,24 +2618,14 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
2568
2618
  config.projectId = projectId;
2569
2619
  config.consoleUrl = consoleBase;
2570
2620
  saveConfig(config);
2571
- const cwd = process.cwd();
2572
- const envPath = nodePath.join(cwd, ".env.local");
2573
- const envLines = [
2574
- "# Mushi MCP \u2014 drop into .env.local (gitignored). The MCP binary picks these up on spawn.",
2575
- `MUSHI_API_ENDPOINT=${endpoint}`,
2576
- `MUSHI_PROJECT_ID=${projectId}`,
2577
- `MUSHI_API_KEY=${apiKey}`,
2578
- ""
2579
- ];
2580
- const envExisting = existsSync4(envPath);
2581
- await writeFile(envPath, envLines.join("\n"), "utf8");
2621
+ const { envUpdated, mcpUpdated } = await writeProjectBootstrapFiles({
2622
+ endpoint,
2623
+ projectId,
2624
+ apiKey
2625
+ });
2582
2626
  console.log(`
2583
- \u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
2584
- const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
2585
- const serverName = buildMcpServerName({ legacy: true });
2586
- const serverBlock = buildMcpServerBlock({ endpoint, projectId, apiKey });
2587
- const { created: mcpCreated } = await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
2588
- console.log(` \u2713 ${mcpCreated ? "Created" : "Updated"} .cursor/mcp.json`);
2627
+ OK ${envUpdated ? "Updated" : "Created"} .env.local`);
2628
+ console.log(` OK ${mcpUpdated ? "Updated" : "Created"} .cursor/mcp.json`);
2589
2629
  console.log("");
2590
2630
  console.log(' Done! Restart Cursor and ask: "list mushi tools"');
2591
2631
  console.log(" Run `mushi whoami` to verify the connection.");
@@ -2609,9 +2649,9 @@ Supported IDEs:
2609
2649
  zed \u2014 writes ~/.config/zed/settings.json mcpServers block
2610
2650
 
2611
2651
  The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
2612
- const { writeFile, mkdir, readFile: readFile2 } = await import("fs/promises");
2613
- const { existsSync: existsSync4 } = await import("fs");
2614
- const nodePath = await import("path");
2652
+ const { writeFile: writeFile2, mkdir, readFile: readFile2 } = await import("fs/promises");
2653
+ const { existsSync: existsSync5 } = await import("fs");
2654
+ const nodePath2 = await import("path");
2615
2655
  const os = await import("os");
2616
2656
  const config = requireConfig({ needsProject: true });
2617
2657
  const redactKeyForDisplay = (text2) => {
@@ -2654,7 +2694,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2654
2694
  cursor: { dir: ".cursor", file: "mcp.json", format: "mcp-json" },
2655
2695
  claude: { dir: ".claude", file: "mcp.json", format: "mcp-json" },
2656
2696
  continue: { dir: ".continue", file: "mcp.json", format: "mcp-json" },
2657
- zed: { dir: nodePath.join(os.homedir(), ".config", "zed"), file: "settings.json", format: "zed" }
2697
+ zed: { dir: nodePath2.join(os.homedir(), ".config", "zed"), file: "settings.json", format: "zed" }
2658
2698
  };
2659
2699
  const ideEntry = IDE_CONFIG[opts.ide];
2660
2700
  if (!ideEntry) {
@@ -2690,8 +2730,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2690
2730
  MUSHI_API_KEY: config.apiKey
2691
2731
  }
2692
2732
  };
2693
- const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir : nodePath.join(cwd, ideEntry.dir);
2694
- const configPath = nodePath.join(configDir, ideEntry.file);
2733
+ const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir : nodePath2.join(cwd, ideEntry.dir);
2734
+ const configPath = nodePath2.join(configDir, ideEntry.file);
2695
2735
  if (ideEntry.format === "mcp-json") {
2696
2736
  if (allProjectsList && allProjectsList.length > 0) {
2697
2737
  for (const p3 of allProjectsList) {
@@ -2722,7 +2762,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2722
2762
  }
2723
2763
  } else if (ideEntry.format === "zed") {
2724
2764
  let settings = {};
2725
- if (existsSync4(configPath)) {
2765
+ if (existsSync5(configPath)) {
2726
2766
  try {
2727
2767
  const raw = await readFile2(configPath, "utf8");
2728
2768
  settings = JSON.parse(raw);
@@ -2749,7 +2789,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2749
2789
  console.log(redactKeyForDisplay(output));
2750
2790
  } else {
2751
2791
  await mkdir(configDir, { recursive: true });
2752
- await writeFile(configPath, output, "utf8");
2792
+ await writeFile2(configPath, output, "utf8");
2753
2793
  console.log(`\u2713 Written ${configPath}`);
2754
2794
  }
2755
2795
  }
@@ -2764,7 +2804,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2764
2804
  "## Before writing a fix",
2765
2805
  "",
2766
2806
  "1. Call `get_fix_context` (MCP) for the report \u2014 get root cause + blast radius first.",
2767
- "2. Call `lessons.query` (MCP) or read .mushi/lessons.json \u2014 apply every matching rule.",
2807
+ "2. Call `list_lessons` (MCP) or read .mushi/lessons.json \u2014 apply every matching rule.",
2768
2808
  "3. Prefer the smallest change that makes the test pass. Don't refactor unrelated code.",
2769
2809
  "",
2770
2810
  "## After writing a fix",
@@ -2779,21 +2819,21 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2779
2819
  ""
2780
2820
  ].join("\n");
2781
2821
  if (opts.ide === "cursor") {
2782
- const rulesPath = nodePath.join(cwd, ".cursorrules");
2822
+ const rulesPath = nodePath2.join(cwd, ".cursorrules");
2783
2823
  if (opts.dryRun) {
2784
2824
  console.log(`[dry-run] Would write ${rulesPath}`);
2785
2825
  } else {
2786
- await writeFile(rulesPath, rulesContent, "utf8");
2826
+ await writeFile2(rulesPath, rulesContent, "utf8");
2787
2827
  console.log(`\u2713 Written .cursorrules`);
2788
2828
  }
2789
2829
  } else if (opts.ide === "claude") {
2790
- const rulesDir = nodePath.join(cwd, ".claude", "rules");
2791
- const rulesPath = nodePath.join(rulesDir, "mushi.md");
2830
+ const rulesDir = nodePath2.join(cwd, ".claude", "rules");
2831
+ const rulesPath = nodePath2.join(rulesDir, "mushi.md");
2792
2832
  if (opts.dryRun) {
2793
2833
  console.log(`[dry-run] Would write ${rulesPath}`);
2794
2834
  } else {
2795
2835
  await mkdir(rulesDir, { recursive: true });
2796
- await writeFile(rulesPath, rulesContent, "utf8");
2836
+ await writeFile2(rulesPath, rulesContent, "utf8");
2797
2837
  console.log(`\u2713 Written .claude/rules/mushi.md`);
2798
2838
  }
2799
2839
  }
@@ -2852,393 +2892,809 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2852
2892
  if (!opts.withRules) {
2853
2893
  console.log(`Tip: run with --with-rules to also write the lesson-library coding hook.`);
2854
2894
  }
2855
- const configRelPath = ideEntry.dir.startsWith("/") ? configPath : nodePath.relative(cwd, configPath);
2895
+ const configRelPath = ideEntry.dir.startsWith("/") ? configPath : nodePath2.relative(cwd, configPath);
2856
2896
  console.log(`
2857
2897
  Note: ${configRelPath} contains your Mushi API key \u2014 add it to .gitignore if this is a shared repo.`);
2858
2898
  }
2859
2899
  });
2860
2900
  }
2861
2901
 
2862
- // src/nudge.ts
2863
- var PRESETS = {
2864
- // Alpha: noisy is OK because there are very few users and you need
2865
- // signal yesterday. Show all triggers, short cooldowns.
2866
- alpha: {
2867
- maxProactivePerSession: 3,
2868
- dismissCooldownHours: 6,
2869
- suppressAfterDismissals: 5,
2870
- pageDwellMinutes: 3,
2871
- firstSessionSeconds: 30,
2872
- rageClick: true,
2873
- longTask: true,
2874
- apiCascade: true,
2875
- errorBoundary: true,
2876
- betaMode: true,
2877
- featureRequestCard: true
2878
- },
2879
- // Beta: most apps spend the longest here. Conservative cadence so
2880
- // we don't poison the well, but every trigger still on.
2881
- beta: {
2882
- maxProactivePerSession: 2,
2883
- dismissCooldownHours: 24,
2884
- suppressAfterDismissals: 3,
2885
- pageDwellMinutes: 5,
2886
- firstSessionSeconds: 45,
2887
- rageClick: true,
2888
- longTask: true,
2889
- apiCascade: true,
2890
- errorBoundary: true,
2891
- betaMode: true,
2892
- featureRequestCard: true
2893
- },
2894
- // GA / production: only the technical signals (error boundary, rage
2895
- // click). No page-dwell, no welcome — those are explicitly beta-only
2896
- // patterns. Feature-request card still on because it's user-initiated.
2897
- ga: {
2898
- maxProactivePerSession: 1,
2899
- dismissCooldownHours: 168,
2900
- suppressAfterDismissals: 2,
2901
- pageDwellMinutes: 0,
2902
- firstSessionSeconds: 0,
2903
- rageClick: true,
2904
- longTask: false,
2905
- apiCascade: true,
2906
- errorBoundary: true,
2907
- betaMode: false,
2908
- featureRequestCard: true
2909
- }
2910
- };
2911
- function renderNudgeSnippet(opts) {
2912
- const p3 = { ...PRESETS[opts.phase], ...opts.overrides };
2913
- const dwellMs = p3.pageDwellMinutes > 0 ? `${p3.pageDwellMinutes} * 60 * 1000` : null;
2914
- const firstMs = p3.firstSessionSeconds > 0 ? `${p3.firstSessionSeconds} * 1000` : null;
2915
- const proactiveLines = [];
2916
- if (p3.rageClick) proactiveLines.push(` rageClick: true,`);
2917
- if (p3.longTask) proactiveLines.push(` longTask: true,`);
2918
- if (p3.apiCascade) proactiveLines.push(` apiCascade: true,`);
2919
- if (p3.errorBoundary) proactiveLines.push(` errorBoundary: true,`);
2920
- if (dwellMs) proactiveLines.push(` pageDwell: { thresholdMs: ${dwellMs} }, // ${p3.pageDwellMinutes}min on the same route`);
2921
- if (firstMs) proactiveLines.push(` firstSession: { delayMs: ${firstMs} }, // welcome new users after ${p3.firstSessionSeconds}s`);
2922
- proactiveLines.push(` cooldown: {`);
2923
- proactiveLines.push(` maxProactivePerSession: ${p3.maxProactivePerSession},`);
2924
- proactiveLines.push(` dismissCooldownHours: ${p3.dismissCooldownHours},`);
2925
- proactiveLines.push(` suppressAfterDismissals: ${p3.suppressAfterDismissals},`);
2926
- proactiveLines.push(` },`);
2927
- const widgetLines = [];
2928
- if (p3.featureRequestCard) widgetLines.push(` featureRequestCard: true,`);
2929
- if (p3.betaMode) {
2930
- widgetLines.push(` betaMode: {`);
2931
- widgetLines.push(` enabled: true,`);
2932
- widgetLines.push(` label: 'BETA',`);
2933
- widgetLines.push(` // changelogItems: [{ version: 'v0.42', date: '${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}', items: ['\u2026'] }],`);
2934
- widgetLines.push(` },`);
2935
- }
2936
- return [
2937
- `// Generated by \`mushi nudge --phase ${opts.phase}\``,
2938
- `// Phase: ${opts.phase} \u2014 tune via --max, --cooldown, --dwell, --welcome.`,
2939
- `Mushi.init({`,
2940
- ` projectId: process.env.MUSHI_PROJECT_ID!,`,
2941
- ` apiKey: process.env.MUSHI_API_KEY!,`,
2942
- ` proactive: {`,
2943
- ...proactiveLines,
2944
- ` },`,
2945
- ...widgetLines.length ? [` widget: {`, ...widgetLines, ` },`] : [],
2946
- `})`,
2947
- ``
2948
- ].join("\n");
2949
- }
2950
- function renderNudgeExplainer(phase) {
2951
- const p3 = PRESETS[phase];
2952
- const lines = [];
2953
- lines.push(``);
2954
- lines.push(`Nudge preset for "${phase}" phase:`);
2955
- lines.push(` - max ${p3.maxProactivePerSession} proactive prompts per session`);
2956
- lines.push(` - ${p3.dismissCooldownHours}h cooldown after a dismissal`);
2957
- lines.push(` - suppress permanently after ${p3.suppressAfterDismissals} consecutive dismissals`);
2958
- if (p3.pageDwellMinutes > 0) lines.push(` - fire page-dwell trigger after ${p3.pageDwellMinutes} continuous minutes on a route`);
2959
- if (p3.firstSessionSeconds > 0) lines.push(` - welcome new users with a button pulse ${p3.firstSessionSeconds}s after init`);
2960
- lines.push(` - signals enabled: ${[
2961
- p3.rageClick && "rage-click",
2962
- p3.longTask && "long-task",
2963
- p3.apiCascade && "api-cascade",
2964
- p3.errorBoundary && "error-boundary"
2965
- ].filter(Boolean).join(", ")}`);
2966
- lines.push(` - feature-request card: ${p3.featureRequestCard ? "shown" : "hidden"}`);
2967
- lines.push(` - beta-mode UI: ${p3.betaMode ? "on" : "off"}`);
2968
- lines.push(``);
2969
- return lines.join("\n");
2970
- }
2902
+ // src/commands/fix.ts
2903
+ function registerFixCommands(program2) {
2904
+ const fixCmd = program2.command("fix").description("Dispatch an agentic fix for a report");
2905
+ fixCmd.argument("<reportId>", "Report UUID to fix").option(
2906
+ "--agent <name>",
2907
+ "Agent adapter: claude_code (default), cursor_cloud, codex, mcp",
2908
+ "claude_code"
2909
+ ).option(
2910
+ "--model <slug>",
2911
+ "Model override for cursor_cloud (e.g. composer-latest)"
2912
+ ).option(
2913
+ "--no-auto-pr",
2914
+ "For cursor_cloud: skip automatic PR creation (branch only)"
2915
+ ).option(
2916
+ "--wait",
2917
+ "Poll until terminal state and exit non-zero on error/cancelled (CI-friendly)"
2918
+ ).option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--project-id <id>", "Project ID (overrides MUSHI_PROJECT_ID)").addHelpText("after", `
2919
+ Examples:
2920
+ mushi fix abc123 --agent cursor_cloud --wait
2921
+ mushi fix abc123 --agent cursor_cloud --model composer-latest --no-auto-pr
2922
+ mushi fix abc123 --agent claude_code
2971
2923
 
2972
- // src/doctor.ts
2973
- function checkCliConfig(config) {
2974
- return [
2975
- {
2976
- name: "CLI config file",
2977
- ok: Boolean(config.endpoint),
2978
- detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint \u2014 set MUSHI_API_ENDPOINT, run `mushi connect`, or `mushi config endpoint <url>`"
2979
- },
2980
- {
2981
- name: "API key configured",
2982
- ok: Boolean(config.apiKey),
2983
- detail: config.apiKey ? `apiKey=${config.apiKey.slice(0, 8)}\u2026${config.apiKey.slice(-4)}` : "No API key set \u2014 run `mushi login --api-key <key>`"
2984
- },
2985
- {
2986
- name: "Project ID configured",
2987
- ok: Boolean(config.projectId),
2988
- detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
2989
- }
2990
- ];
2991
- }
2992
- async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
2993
- try {
2994
- const safeEndpoint = sanitizeEndpoint(endpoint);
2995
- const res = await doFetch(`${safeEndpoint}/health`, {
2996
- signal: AbortSignal.timeout(5e3)
2997
- });
2998
- return {
2999
- name: "Endpoint reachable",
3000
- ok: res.status === 200,
3001
- detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
2924
+ # CI: fail the pipeline if the fix errors
2925
+ mushi fix $REPORT_ID --agent cursor_cloud --wait && echo "Fix PR opened"`).action(async (reportId, opts) => {
2926
+ const cfg = loadConfig();
2927
+ if (opts.endpoint) cfg.endpoint = opts.endpoint;
2928
+ if (opts.apiKey) cfg.apiKey = opts.apiKey;
2929
+ if (opts.projectId) cfg.projectId = opts.projectId;
2930
+ const isTTY = process.stdout.isTTY;
2931
+ const emitEvent = (type, data) => {
2932
+ if (isTTY) {
2933
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2934
+ console.log(`[${ts}] ${type}`, JSON.stringify(data));
2935
+ } else {
2936
+ process.stdout.write(JSON.stringify({ type, ...data }) + "\n");
2937
+ }
3002
2938
  };
3003
- } catch (err) {
3004
- const msg = err instanceof Error ? err.message : String(err);
3005
- return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
3006
- }
3007
- }
3008
- async function checkSdkInstall(cwd) {
3009
- try {
3010
- const { readFile: readFile2 } = await import("fs/promises");
3011
- const { join: join5, resolve: resolve3 } = await import("path");
3012
- const root = resolve3(cwd);
3013
- const pkgPath = join5(root, "package.json");
3014
- const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
3015
- const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
3016
- const sdks = [
3017
- "@mushi-mushi/react",
3018
- "@mushi-mushi/web",
3019
- "@mushi-mushi/core",
3020
- "@mushi-mushi/react-native"
3021
- ];
3022
- const installed2 = sdks.filter((s) => deps[s]);
3023
- return {
3024
- name: "SDK installed in this repo",
3025
- ok: installed2.length > 0,
3026
- detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
2939
+ emitEvent("dispatch.start", { reportId, agent: opts.agent, model: opts.model ?? null });
2940
+ const body = {
2941
+ reportId,
2942
+ projectId: cfg.projectId,
2943
+ agent: opts.agent
3027
2944
  };
3028
- } catch {
3029
- return null;
3030
- }
3031
- }
3032
- async function checkServerPreflight(config, doFetch = globalThis.fetch) {
3033
- if (!config.projectId || !config.apiKey || !config.endpoint) {
3034
- return [
3035
- {
3036
- name: "Server preflight",
3037
- ok: false,
3038
- detail: "Need projectId, apiKey, and endpoint. Run `mushi login` and `mushi config projectId <uuid>`."
3039
- }
3040
- ];
3041
- }
3042
- try {
3043
- const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
3044
- const res = await doFetch(
3045
- `${endpoint}/v1/admin/projects/${projectId}/preflight`,
3046
- {
3047
- headers: apiKeyHeaders(apiKey, projectId),
3048
- signal: AbortSignal.timeout(8e3)
3049
- }
3050
- );
3051
- if (res.ok) {
3052
- const body = await res.json();
3053
- const serverChecks = body.data?.checks ?? [];
3054
- return serverChecks.map((sc) => ({
3055
- name: `[server] ${sc.label}`,
3056
- ok: sc.ready,
3057
- detail: sc.ready ? "" : sc.hint
3058
- }));
2945
+ if (opts.agent === "cursor_cloud") {
2946
+ if (opts.model) body.cursorModel = opts.model;
2947
+ if (!opts.autoPr) body.cursorAutoCreatePR = false;
3059
2948
  }
3060
- if (res.status === 403) {
3061
- let errCode;
3062
- try {
3063
- const body = await res.json();
3064
- errCode = body?.error?.code;
3065
- } catch {
3066
- }
3067
- if (errCode === "INSUFFICIENT_SCOPE") {
3068
- return [
3069
- {
3070
- name: "Server preflight",
3071
- ok: true,
3072
- detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and unlock admin checks."
3073
- }
3074
- ];
3075
- }
2949
+ const result = await apiCall("/v1/admin/fixes/dispatch", cfg, {
2950
+ method: "POST",
2951
+ headers: { "Content-Type": "application/json" },
2952
+ body: JSON.stringify(body)
2953
+ });
2954
+ if (!result.ok) {
2955
+ console.error("Error dispatching fix:", result.error.message);
2956
+ process.exit(1);
3076
2957
  }
3077
- const text2 = await res.text().catch(() => "");
3078
- return [
3079
- {
3080
- name: "Server preflight",
3081
- ok: false,
3082
- detail: `HTTP ${res.status}: ${text2.slice(0, 120)}`
3083
- }
3084
- ];
3085
- } catch (err) {
3086
- const msg = err instanceof Error ? err.message : String(err);
3087
- return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
3088
- }
3089
- }
3090
- async function checkIngestSetup(config, doFetch = globalThis.fetch) {
3091
- if (!config.apiKey || !config.endpoint) {
3092
- return [
3093
- {
3094
- name: "Ingest setup",
3095
- ok: false,
3096
- detail: "Need apiKey and endpoint. Run `mushi connect`."
3097
- }
3098
- ];
3099
- }
3100
- try {
3101
- const data = await fetchIngestSetup(
3102
- { endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
3103
- doFetch
3104
- );
3105
- if (!data) {
3106
- return [{ name: "Ingest setup", ok: false, detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload" }];
2958
+ const { fixId, status, agentId, runId, prUrl } = result.data;
2959
+ emitEvent("dispatch.ok", { fixId, status, agentId, runId, prUrl });
2960
+ if (!opts.wait) {
2961
+ process.exit(0);
3107
2962
  }
3108
- const steps = data.steps ?? [];
3109
- const checks = steps.filter((s) => s.required).map((s) => ({
3110
- name: `[ingest] ${s.label}`,
3111
- ok: s.complete,
3112
- detail: s.complete ? "" : s.hint ?? ""
3113
- }));
3114
- const diag = data.diagnostic;
3115
- if (diag?.last_sdk_seen_at) {
3116
- checks.push({
3117
- name: "[ingest] Last SDK heartbeat",
3118
- ok: true,
3119
- detail: `${diag.last_sdk_seen_at}${diag.last_sdk_endpoint_host ? ` @ ${diag.last_sdk_endpoint_host}` : ""}`
3120
- });
2963
+ if (!fixId) {
2964
+ console.error("No fixId returned \u2014 cannot poll.");
2965
+ process.exit(1);
3121
2966
  }
3122
- return checks.length > 0 ? checks : [{ name: "Ingest setup", ok: false, detail: "Empty response from /v1/sync/ingest-setup" }];
3123
- } catch (err) {
3124
- const msg = err instanceof Error ? err.message : String(err);
3125
- return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
3126
- }
3127
- }
3128
- async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
3129
- if (!config.projectId || !config.apiKey || !config.endpoint) {
3130
- return [
3131
- {
3132
- name: "QA stories health",
3133
- ok: false,
3134
- detail: "Need projectId, apiKey, and endpoint for QA story checks."
2967
+ const POLL_MS = 5e3;
2968
+ const MAX_POLLS = 120;
2969
+ const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "error", "cancelled", "skipped", "skipped_unsupported_agent", "skipped_no_sandbox"]);
2970
+ for (let i = 0; i < MAX_POLLS; i++) {
2971
+ await new Promise((r) => setTimeout(r, POLL_MS));
2972
+ const pollResult = await apiCall(
2973
+ `/v1/admin/fixes/${fixId}`,
2974
+ cfg
2975
+ );
2976
+ if (!pollResult.ok) {
2977
+ emitEvent("poll.error", { error: pollResult.error.message });
2978
+ continue;
3135
2979
  }
3136
- ];
3137
- }
3138
- const checks = [];
3139
- try {
3140
- const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
3141
- const headers = apiKeyHeaders(apiKey, projectId);
3142
- const storiesRes = await doFetch(
3143
- `${endpoint}/v1/admin/projects/${projectId}/qa-coverage`,
3144
- {
3145
- headers,
3146
- signal: AbortSignal.timeout(8e3)
2980
+ const s = pollResult.data.status;
2981
+ emitEvent("fix.status", { status: s, pr_url: pollResult.data.pr_url, cursor_agent_id: pollResult.data.cursor_agent_id });
2982
+ if (s && TERMINAL.has(s)) {
2983
+ const success = s === "completed";
2984
+ if (!success) {
2985
+ console.error(`Fix ended with status: ${s}${pollResult.data.error ? ` \u2014 ${pollResult.data.error}` : ""}`);
2986
+ process.exit(1);
2987
+ }
2988
+ process.exit(0);
3147
2989
  }
3148
- );
3149
- if (!storiesRes.ok) {
3150
- checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
3151
- return checks;
3152
2990
  }
3153
- const storiesBody = await storiesRes.json();
3154
- const stories = storiesBody.data?.coverage ?? [];
3155
- const enabled = stories.filter((s) => s.enabled);
3156
- if (enabled.length === 0) {
3157
- checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
3158
- return checks;
2991
+ console.error("Polling timed out after 10 minutes. The fix may still be running.");
2992
+ process.exit(1);
2993
+ });
2994
+ const fixes = program2.command("fixes").description("Fix dispatch management");
2995
+ fixes.command("tail").description(
2996
+ "Stream SSE dispatch events for a report in real time. Useful for headless debugging without opening the browser."
2997
+ ).requiredOption("--report-id <id>", "Report ID to follow").action(async (opts) => {
2998
+ const config = loadConfig();
2999
+ if (!config.apiKey) {
3000
+ console.error("Run `mushi login` first");
3001
+ process.exit(1);
3159
3002
  }
3160
- checks.push({
3161
- name: "[qa] Enabled QA stories",
3162
- ok: true,
3163
- detail: `${enabled.length} enabled story/stories configured`
3164
- });
3165
- const slackRes = await doFetch(
3166
- `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
3167
- {
3168
- method: "POST",
3169
- headers,
3170
- signal: AbortSignal.timeout(6e3)
3171
- }
3172
- );
3173
- const slackBody = slackRes.ok ? await slackRes.json() : null;
3174
- const slackOk = slackBody?.status === "ok";
3175
- checks.push({
3176
- name: "[qa] Slack notifications configured",
3177
- ok: slackOk,
3178
- detail: slackOk ? "Slack connected \u2014 failures will notify your channel" : "Slack not connected \u2014 you won't be notified when stories fail. Visit /integrations \u2192 Add to Slack."
3179
- });
3180
- const fcRes = await doFetch(
3181
- `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
3182
- {
3183
- method: "POST",
3184
- headers,
3185
- signal: AbortSignal.timeout(6e3)
3186
- }
3187
- );
3188
- const fcBody = fcRes.ok ? await fcRes.json() : null;
3189
- const hasFirecrawlStories = enabled.some(
3190
- (s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
3191
- );
3192
- if (hasFirecrawlStories) {
3193
- const fcOk = fcBody?.status === "ok";
3194
- checks.push({
3195
- name: "[qa] Firecrawl API key configured",
3196
- ok: fcOk,
3197
- detail: fcOk ? "Firecrawl key is resolvable \u2014 stories will run without Unauthorized errors" : "No Firecrawl key found \u2014 enabled stories using firecrawl_actions will 401. Add a key at /integrations \u2192 BYOK keys."
3198
- });
3003
+ if (!config.endpoint) {
3004
+ console.error("No endpoint configured. Run `mushi init`");
3005
+ process.exit(1);
3199
3006
  }
3200
- } catch (err) {
3201
- const msg = err instanceof Error ? err.message : String(err);
3202
- checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
3203
- }
3204
- return checks;
3205
- }
3206
- async function checkHostAppWiring(cwd) {
3207
- const checks = [];
3208
- try {
3209
- const { readFile: readFile2, access } = await import("fs/promises");
3210
- const { join: join5, resolve: resolve3 } = await import("path");
3211
- const root = resolve3(cwd);
3212
- const pkgPath = join5(root, "package.json");
3213
- const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
3214
- const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
3215
- const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
3216
- const envCandidates = [".env.local", ".env"];
3217
- let envContent = "";
3218
- for (const f of envCandidates) {
3219
- try {
3220
- envContent = await readFile2(join5(root, f), "utf8");
3221
- break;
3222
- } catch {
3007
+ const url = `${config.endpoint}/v1/admin/reports/${opts.reportId}/dispatch/stream`;
3008
+ console.log(`Tailing dispatch stream for report ${opts.reportId}\u2026`);
3009
+ console.log(`(Ctrl-C to stop)
3010
+ `);
3011
+ const res = await fetch(url, {
3012
+ headers: {
3013
+ "Authorization": `Bearer ${config.apiKey}`,
3014
+ "X-Mushi-Api-Key": config.apiKey,
3015
+ "X-Mushi-Project": config.projectId ?? "",
3016
+ "Accept": "text/event-stream"
3223
3017
  }
3224
- }
3225
- const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
3226
- const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(envContent);
3227
- checks.push({
3228
- name: "[host] Mushi env vars in .env.local",
3229
- ok: hasProjectId && hasApiKey,
3230
- detail: hasProjectId && hasApiKey ? "VITE_/MUSHI_ project id + API key found" : "Run `mushi connect --write-env` or add VITE_MUSHI_PROJECT_ID + VITE_MUSHI_API_KEY"
3231
3018
  });
3232
- let mcpPresent = false;
3233
- try {
3234
- await access(join5(root, ".cursor", "mcp.json"));
3235
- mcpPresent = true;
3236
- } catch {
3019
+ if (!res.ok || !res.body) {
3020
+ console.error(`Failed to connect: HTTP ${res.status}`);
3021
+ const text2 = await res.text().catch(() => "");
3022
+ if (text2) console.error(text2.slice(0, 300));
3023
+ process.exit(1);
3237
3024
  }
3238
- checks.push({
3239
- name: "[host] Cursor MCP config",
3240
- ok: mcpPresent,
3241
- detail: mcpPresent ? ".cursor/mcp.json present" : "Run `mushi connect` to wire MCP for two-way reporter replies"
3025
+ const decoder = new TextDecoder();
3026
+ const reader = res.body.getReader();
3027
+ let done = false;
3028
+ process.on("SIGINT", () => {
3029
+ done = true;
3030
+ void reader.cancel();
3031
+ console.log("\nDisconnected.");
3032
+ process.exit(0);
3033
+ });
3034
+ while (!done) {
3035
+ const { value, done: streamDone } = await reader.read();
3036
+ if (streamDone) break;
3037
+ const chunk = decoder.decode(value, { stream: true });
3038
+ for (const line of chunk.split("\n")) {
3039
+ if (line.startsWith("data: ")) {
3040
+ const raw = line.slice(6).trim();
3041
+ if (raw === "[DONE]") {
3042
+ console.log("\n[stream ended]");
3043
+ process.exit(0);
3044
+ }
3045
+ try {
3046
+ const event = JSON.parse(raw);
3047
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3048
+ const type = event.type ?? event.event ?? "event";
3049
+ const status = event.status ?? event.data ?? "";
3050
+ console.log(`${ts} ${type.padEnd(24)} ${status}`);
3051
+ } catch {
3052
+ console.log(line);
3053
+ }
3054
+ } else if (line.startsWith("event: ")) {
3055
+ } else if (line && !line.startsWith(":")) {
3056
+ console.log(line);
3057
+ }
3058
+ }
3059
+ }
3060
+ });
3061
+ fixes.command("merge <fixId>").description(
3062
+ "Squash-merge the fix PR on GitHub and mark the report Fixed (same as console merge)"
3063
+ ).option(
3064
+ "--method <method>",
3065
+ "GitHub merge method: squash (default), merge, or rebase",
3066
+ "squash"
3067
+ ).option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
3068
+ const config = loadConfig();
3069
+ if (!config.apiKey) {
3070
+ console.error("Run `mushi login` first");
3071
+ process.exit(1);
3072
+ }
3073
+ if (!config.endpoint) {
3074
+ console.error("No endpoint configured. Run `mushi init`");
3075
+ process.exit(1);
3076
+ }
3077
+ const method = opts.method;
3078
+ if (!["squash", "merge", "rebase"].includes(method)) {
3079
+ console.error("--method must be squash, merge, or rebase");
3080
+ process.exit(1);
3081
+ }
3082
+ const result = await apiCall(`/v1/admin/fixes/${fixId}/merge`, config, {
3083
+ method: "POST",
3084
+ body: JSON.stringify({ mergeMethod: method })
3085
+ });
3086
+ if (!result.ok) {
3087
+ if (opts.json) {
3088
+ console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
3089
+ } else {
3090
+ console.error("Merge failed:", result.error.message);
3091
+ }
3092
+ process.exit(1);
3093
+ }
3094
+ const data = result.data ?? {};
3095
+ if (opts.json) {
3096
+ console.log(JSON.stringify({ ok: true, ...data }, null, 2));
3097
+ return;
3098
+ }
3099
+ if (data.alreadyMerged) {
3100
+ console.log(`PR was already merged. Report status: ${data.reportStatus ?? "unknown"}`);
3101
+ } else {
3102
+ console.log(`Merged successfully. Report status: ${data.reportStatus ?? "fixed"}`);
3103
+ }
3104
+ });
3105
+ fixes.command("refresh-ci <fixId>").description("Pull latest GitHub Actions check-run status for a fix attempt (on-demand ci-sync)").option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
3106
+ const config = loadConfig();
3107
+ if (!config.apiKey) {
3108
+ console.error("Run `mushi login` first");
3109
+ process.exit(1);
3110
+ }
3111
+ if (!config.endpoint) {
3112
+ console.error("No endpoint configured. Run `mushi init`");
3113
+ process.exit(1);
3114
+ }
3115
+ const result = await apiCall(`/v1/admin/fixes/${fixId}/refresh-ci`, config, { method: "POST" });
3116
+ if (!result.ok) {
3117
+ if (opts.json) {
3118
+ console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
3119
+ } else {
3120
+ console.error("CI refresh failed:", result.error.message);
3121
+ }
3122
+ process.exit(1);
3123
+ }
3124
+ const data = result.data ?? {};
3125
+ if (opts.json) {
3126
+ console.log(JSON.stringify({ ok: true, ...data }, null, 2));
3127
+ return;
3128
+ }
3129
+ const status = data.check_run_status ?? "unknown";
3130
+ const conclusion = data.check_run_conclusion ?? "\u2014";
3131
+ const updated = data.check_run_updated_at ? new Date(data.check_run_updated_at).toISOString() : "\u2014";
3132
+ console.log(`CI status: ${status} \xB7 conclusion: ${conclusion} \xB7 updated: ${updated}`);
3133
+ });
3134
+ }
3135
+
3136
+ // src/nudge.ts
3137
+ var PRESETS = {
3138
+ // Alpha: noisy is OK because there are very few users and you need
3139
+ // signal yesterday. Show all triggers, short cooldowns.
3140
+ alpha: {
3141
+ maxProactivePerSession: 3,
3142
+ dismissCooldownHours: 6,
3143
+ suppressAfterDismissals: 5,
3144
+ pageDwellMinutes: 3,
3145
+ firstSessionSeconds: 30,
3146
+ rageClick: true,
3147
+ longTask: true,
3148
+ apiCascade: true,
3149
+ errorBoundary: true,
3150
+ betaMode: true,
3151
+ featureRequestCard: true
3152
+ },
3153
+ // Beta: most apps spend the longest here. Conservative cadence so
3154
+ // we don't poison the well, but every trigger still on.
3155
+ beta: {
3156
+ maxProactivePerSession: 2,
3157
+ dismissCooldownHours: 24,
3158
+ suppressAfterDismissals: 3,
3159
+ pageDwellMinutes: 5,
3160
+ firstSessionSeconds: 45,
3161
+ rageClick: true,
3162
+ longTask: true,
3163
+ apiCascade: true,
3164
+ errorBoundary: true,
3165
+ betaMode: true,
3166
+ featureRequestCard: true
3167
+ },
3168
+ // GA / production: only the technical signals (error boundary, rage
3169
+ // click). No page-dwell, no welcome — those are explicitly beta-only
3170
+ // patterns. Feature-request card still on because it's user-initiated.
3171
+ ga: {
3172
+ maxProactivePerSession: 1,
3173
+ dismissCooldownHours: 168,
3174
+ suppressAfterDismissals: 2,
3175
+ pageDwellMinutes: 0,
3176
+ firstSessionSeconds: 0,
3177
+ rageClick: true,
3178
+ longTask: false,
3179
+ apiCascade: true,
3180
+ errorBoundary: true,
3181
+ betaMode: false,
3182
+ featureRequestCard: true
3183
+ }
3184
+ };
3185
+ function renderNudgeSnippet(opts) {
3186
+ const p3 = { ...PRESETS[opts.phase], ...opts.overrides };
3187
+ const dwellMs = p3.pageDwellMinutes > 0 ? `${p3.pageDwellMinutes} * 60 * 1000` : null;
3188
+ const firstMs = p3.firstSessionSeconds > 0 ? `${p3.firstSessionSeconds} * 1000` : null;
3189
+ const proactiveLines = [];
3190
+ if (p3.rageClick) proactiveLines.push(` rageClick: true,`);
3191
+ if (p3.longTask) proactiveLines.push(` longTask: true,`);
3192
+ if (p3.apiCascade) proactiveLines.push(` apiCascade: true,`);
3193
+ if (p3.errorBoundary) proactiveLines.push(` errorBoundary: true,`);
3194
+ if (dwellMs) proactiveLines.push(` pageDwell: { thresholdMs: ${dwellMs} }, // ${p3.pageDwellMinutes}min on the same route`);
3195
+ if (firstMs) proactiveLines.push(` firstSession: { delayMs: ${firstMs} }, // welcome new users after ${p3.firstSessionSeconds}s`);
3196
+ proactiveLines.push(` cooldown: {`);
3197
+ proactiveLines.push(` maxProactivePerSession: ${p3.maxProactivePerSession},`);
3198
+ proactiveLines.push(` dismissCooldownHours: ${p3.dismissCooldownHours},`);
3199
+ proactiveLines.push(` suppressAfterDismissals: ${p3.suppressAfterDismissals},`);
3200
+ proactiveLines.push(` },`);
3201
+ const widgetLines = [];
3202
+ if (p3.featureRequestCard) widgetLines.push(` featureRequestCard: true,`);
3203
+ if (p3.betaMode) {
3204
+ widgetLines.push(` betaMode: {`);
3205
+ widgetLines.push(` enabled: true,`);
3206
+ widgetLines.push(` label: 'BETA',`);
3207
+ widgetLines.push(` // changelogItems: [{ version: '1.0.0', date: '${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}', items: ['Fixed login timeout on slow networks'] }],`);
3208
+ widgetLines.push(` },`);
3209
+ }
3210
+ return [
3211
+ `// Generated by \`mushi nudge --phase ${opts.phase}\``,
3212
+ `// Phase: ${opts.phase} \u2014 tune via --max, --cooldown, --dwell, --welcome.`,
3213
+ `Mushi.init({`,
3214
+ ` projectId: process.env.MUSHI_PROJECT_ID!,`,
3215
+ ` apiKey: process.env.MUSHI_API_KEY!,`,
3216
+ ` proactive: {`,
3217
+ ...proactiveLines,
3218
+ ` },`,
3219
+ ...widgetLines.length ? [` widget: {`, ...widgetLines, ` },`] : [],
3220
+ `})`,
3221
+ ``
3222
+ ].join("\n");
3223
+ }
3224
+ function renderNudgeExplainer(phase) {
3225
+ const p3 = PRESETS[phase];
3226
+ const lines = [];
3227
+ lines.push(``);
3228
+ lines.push(`Nudge preset for "${phase}" phase:`);
3229
+ lines.push(` - max ${p3.maxProactivePerSession} proactive prompts per session`);
3230
+ lines.push(` - ${p3.dismissCooldownHours}h cooldown after a dismissal`);
3231
+ lines.push(` - suppress permanently after ${p3.suppressAfterDismissals} consecutive dismissals`);
3232
+ if (p3.pageDwellMinutes > 0) lines.push(` - fire page-dwell trigger after ${p3.pageDwellMinutes} continuous minutes on a route`);
3233
+ if (p3.firstSessionSeconds > 0) lines.push(` - welcome new users with a button pulse ${p3.firstSessionSeconds}s after init`);
3234
+ lines.push(` - signals enabled: ${[
3235
+ p3.rageClick && "rage-click",
3236
+ p3.longTask && "long-task",
3237
+ p3.apiCascade && "api-cascade",
3238
+ p3.errorBoundary && "error-boundary"
3239
+ ].filter(Boolean).join(", ")}`);
3240
+ lines.push(` - feature-request card: ${p3.featureRequestCard ? "shown" : "hidden"}`);
3241
+ lines.push(` - beta-mode UI: ${p3.betaMode ? "on" : "off"}`);
3242
+ lines.push(``);
3243
+ return lines.join("\n");
3244
+ }
3245
+
3246
+ // src/commands/nudge.ts
3247
+ function registerNudgeCommand(program2) {
3248
+ program2.command("nudge").description(
3249
+ "Generate a Mushi.init() snippet tuned for your release phase (alpha, beta, ga). Customises proactive triggers, cooldowns, feature-request card, and beta-mode UI."
3250
+ ).option("--phase <phase>", "Release phase: alpha | beta | ga", "beta").option("--explain", "Print a human-readable summary of what the preset does").option("--max <n>", "Override maxProactivePerSession").option("--cooldown <hours>", "Override dismissCooldownHours").option("--dwell <minutes>", "Override page-dwell threshold (0 disables)").option("--welcome <seconds>", "Override first-session welcome delay (0 disables)").action((opts) => {
3251
+ const validPhases = ["alpha", "beta", "ga"];
3252
+ if (!validPhases.includes(opts.phase)) {
3253
+ console.error(`Unknown phase "${opts.phase}". Use one of: ${validPhases.join(", ")}`);
3254
+ process.exit(1);
3255
+ }
3256
+ const phase = opts.phase;
3257
+ const overrides = {};
3258
+ const parseNumericFlag = (flag, raw, min) => {
3259
+ const n = Number(raw);
3260
+ if (!Number.isFinite(n) || n < min) {
3261
+ console.error(
3262
+ `error: --${flag} must be a finite number >= ${min} (got "${raw}")`
3263
+ );
3264
+ process.exit(1);
3265
+ }
3266
+ return n;
3267
+ };
3268
+ if (opts.max !== void 0) overrides.maxProactivePerSession = parseNumericFlag("max", opts.max, 1);
3269
+ if (opts.cooldown !== void 0) overrides.dismissCooldownHours = parseNumericFlag("cooldown", opts.cooldown, 0);
3270
+ if (opts.dwell !== void 0) overrides.pageDwellMinutes = parseNumericFlag("dwell", opts.dwell, 0);
3271
+ if (opts.welcome !== void 0) overrides.firstSessionSeconds = parseNumericFlag("welcome", opts.welcome, 0);
3272
+ if (opts.explain) {
3273
+ console.log(renderNudgeExplainer(phase));
3274
+ }
3275
+ console.log(renderNudgeSnippet({ phase, overrides }));
3276
+ });
3277
+ }
3278
+
3279
+ // src/upgrade.ts
3280
+ import { resolve as resolve2 } from "path";
3281
+ import { execSync } from "child_process";
3282
+ var SAFE_NPM_VERSION = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
3283
+ var MUSHI_PACKAGES = [
3284
+ "@mushi-mushi/web",
3285
+ "@mushi-mushi/core",
3286
+ "@mushi-mushi/react",
3287
+ "@mushi-mushi/react-native",
3288
+ "@mushi-mushi/capacitor",
3289
+ "@mushi-mushi/node",
3290
+ "@mushi-mushi/cli"
3291
+ ];
3292
+ async function planUpgrade(cwd) {
3293
+ const root = resolve2(cwd);
3294
+ const pkg = readPackageJson(root);
3295
+ const pm = detectPackageManager(root);
3296
+ const deps = {
3297
+ ...pkg?.dependencies ?? {},
3298
+ ...pkg?.devDependencies ?? {}
3299
+ };
3300
+ const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
3301
+ const entries = await Promise.all(
3302
+ installed2.map(async (name) => {
3303
+ const current = deps[name] ?? "";
3304
+ const currentCore = current.replace(/^[\^~>=<]*/, "");
3305
+ if (!/^\d/.test(currentCore)) {
3306
+ return { name, current, latest: null, willUpgrade: false };
3307
+ }
3308
+ const freshness = await checkFreshness(name, currentCore, {
3309
+ timeoutMs: 4e3,
3310
+ ignoreOptOut: true
3311
+ });
3312
+ const latest = freshness?.latest ?? null;
3313
+ const safeLatest = latest && SAFE_NPM_VERSION.test(latest) ? latest : null;
3314
+ const willUpgrade = Boolean(freshness?.isOutdated && safeLatest);
3315
+ return {
3316
+ name,
3317
+ current,
3318
+ latest: safeLatest,
3319
+ willUpgrade,
3320
+ migrateToWeb: name === "@mushi-mushi/react" && willUpgrade
3321
+ };
3322
+ })
3323
+ );
3324
+ const toBump = entries.filter((e) => e.willUpgrade && e.latest).map((e) => `${e.name}@${e.latest}`);
3325
+ return {
3326
+ cwd: root,
3327
+ packageManager: pm,
3328
+ entries,
3329
+ installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
3330
+ };
3331
+ }
3332
+ async function runUpgrade(opts = {}) {
3333
+ const plan = await planUpgrade(opts.cwd ?? process.cwd());
3334
+ if (plan.entries.length === 0) {
3335
+ return {
3336
+ plan,
3337
+ upgraded: false,
3338
+ message: "No @mushi-mushi/* packages in package.json \u2014 run `mushi init` first."
3339
+ };
3340
+ }
3341
+ if (!plan.installCmd) {
3342
+ const semverEntries = plan.entries.filter((e) => /\d/.test(e.current));
3343
+ const allChecksFailed = semverEntries.length > 0 && semverEntries.every((e) => e.latest === null);
3344
+ return {
3345
+ plan,
3346
+ upgraded: false,
3347
+ message: allChecksFailed ? "Could not reach the npm registry to check for updates \u2014 try again in a moment." : "All installed Mushi packages are already at the latest stable version."
3348
+ };
3349
+ }
3350
+ if (opts.dryRun) {
3351
+ return { plan, upgraded: false, message: `[dry-run] Would run: ${plan.installCmd}` };
3352
+ }
3353
+ try {
3354
+ execSync(plan.installCmd, { cwd: plan.cwd, stdio: "inherit", env: process.env });
3355
+ } catch {
3356
+ return {
3357
+ plan,
3358
+ upgraded: false,
3359
+ message: `Install command failed \u2014 fix the error above or run it manually:
3360
+ ${plan.installCmd}`
3361
+ };
3362
+ }
3363
+ const reactEntry = plan.entries.find((e) => e.migrateToWeb);
3364
+ const migrateNote = reactEntry ? "\nNote: @mushi-mushi/react is legacy \u2014 prefer @mushi-mushi/web for Vite/Capacitor/SPA apps." : "";
3365
+ return {
3366
+ plan,
3367
+ upgraded: true,
3368
+ message: `Upgraded Mushi SDK packages.
3369
+ ${plan.installCmd}${migrateNote}`
3370
+ };
3371
+ }
3372
+
3373
+ // src/commands/upgrade-cli.ts
3374
+ function registerUpgradeCommand(program2) {
3375
+ program2.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--dry-run", "Print the install command without running it").option("--json", "Machine-readable plan + result").addHelpText("after", `
3376
+ Examples:
3377
+ mushi upgrade
3378
+ mushi upgrade --dry-run
3379
+ mushi upgrade --cwd ../glot.it`).action(async (opts) => {
3380
+ const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
3381
+ if (opts.json) {
3382
+ console.log(JSON.stringify(result, null, 2));
3383
+ } else {
3384
+ console.log(result.message);
3385
+ for (const e of result.plan.entries) {
3386
+ const tag = e.willUpgrade && e.latest ? `\u2192 v${e.latest}` : "(current)";
3387
+ console.log(` ${e.name}@${e.current} ${tag}`);
3388
+ }
3389
+ }
3390
+ if (!result.upgraded && result.plan.entries.some((e) => e.willUpgrade) && !opts.dryRun) {
3391
+ process.exit(1);
3392
+ }
3393
+ if (result.plan.entries.length === 0) process.exit(1);
3394
+ });
3395
+ }
3396
+
3397
+ // src/commands/connect-cli.ts
3398
+ function registerConnectCommand(program2) {
3399
+ program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "Force-write SDK env vars to .env.local (default; overrides --no-env)").option("--wire-ide", "Force-wire Cursor MCP into .cursor/mcp.json (default; overrides --no-ide)").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
3400
+ Examples:
3401
+ MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
3402
+ mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
3403
+ const apiKey = process.env.MUSHI_API_KEY ?? opts.apiKey;
3404
+ if (!apiKey) {
3405
+ console.error("Provide the API key via the MUSHI_API_KEY env var (recommended) or --api-key <key>.");
3406
+ process.exit(1);
3407
+ }
3408
+ const result = await runConnect({
3409
+ apiKey,
3410
+ projectId: opts.projectId,
3411
+ endpoint: opts.endpoint,
3412
+ cwd: opts.cwd,
3413
+ writeEnv: opts.writeEnv === true ? true : opts.env !== false,
3414
+ wireIde: opts.wireIde === true ? true : opts.ide !== false,
3415
+ wait: opts.wait,
3416
+ waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
3417
+ json: opts.json
3418
+ });
3419
+ if (opts.json) {
3420
+ console.log(JSON.stringify(result, null, 2));
3421
+ } else {
3422
+ for (const line of result.messages) console.log(line);
3423
+ }
3424
+ if (!result.ok) process.exit(1);
3425
+ });
3426
+ }
3427
+
3428
+ // src/doctor.ts
3429
+ function checkCliConfig(config) {
3430
+ return [
3431
+ {
3432
+ name: "CLI config file",
3433
+ ok: Boolean(config.endpoint),
3434
+ detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint \u2014 set MUSHI_API_ENDPOINT, run `mushi connect`, or `mushi config endpoint <url>`"
3435
+ },
3436
+ {
3437
+ name: "API key configured",
3438
+ ok: Boolean(config.apiKey),
3439
+ detail: config.apiKey ? `apiKey=${config.apiKey.slice(0, 8)}\u2026${config.apiKey.slice(-4)}` : "No API key set \u2014 run `mushi login --api-key <key>`"
3440
+ },
3441
+ {
3442
+ name: "Project ID configured",
3443
+ ok: Boolean(config.projectId),
3444
+ detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
3445
+ }
3446
+ ];
3447
+ }
3448
+ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
3449
+ try {
3450
+ const safeEndpoint = sanitizeEndpoint(endpoint);
3451
+ const res = await doFetch(`${safeEndpoint}/health`, {
3452
+ signal: AbortSignal.timeout(5e3)
3453
+ });
3454
+ return {
3455
+ name: "Endpoint reachable",
3456
+ ok: res.status === 200,
3457
+ detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
3458
+ };
3459
+ } catch (err) {
3460
+ const msg = err instanceof Error ? err.message : String(err);
3461
+ return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
3462
+ }
3463
+ }
3464
+ async function checkSdkInstall(cwd) {
3465
+ try {
3466
+ const { readFile: readFile2 } = await import("fs/promises");
3467
+ const { join: join5, resolve: resolve3 } = await import("path");
3468
+ const root = resolve3(cwd);
3469
+ const pkgPath = join5(root, "package.json");
3470
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
3471
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
3472
+ const sdks = [
3473
+ "@mushi-mushi/react",
3474
+ "@mushi-mushi/web",
3475
+ "@mushi-mushi/core",
3476
+ "@mushi-mushi/react-native"
3477
+ ];
3478
+ const installed2 = sdks.filter((s) => deps[s]);
3479
+ return {
3480
+ name: "SDK installed in this repo",
3481
+ ok: installed2.length > 0,
3482
+ detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
3483
+ };
3484
+ } catch {
3485
+ return null;
3486
+ }
3487
+ }
3488
+ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
3489
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
3490
+ return [
3491
+ {
3492
+ name: "Server preflight",
3493
+ ok: false,
3494
+ detail: "Need projectId, apiKey, and endpoint. Run `mushi login` and `mushi config projectId <uuid>`."
3495
+ }
3496
+ ];
3497
+ }
3498
+ try {
3499
+ const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
3500
+ const res = await doFetch(
3501
+ `${endpoint}/v1/admin/projects/${projectId}/preflight`,
3502
+ {
3503
+ headers: apiKeyHeaders(apiKey, projectId),
3504
+ signal: AbortSignal.timeout(8e3)
3505
+ }
3506
+ );
3507
+ if (res.ok) {
3508
+ const body = await res.json();
3509
+ const serverChecks = body.data?.checks ?? [];
3510
+ return serverChecks.map((sc) => ({
3511
+ name: `[server] ${sc.label}`,
3512
+ ok: sc.ready,
3513
+ detail: sc.ready ? "" : sc.hint
3514
+ }));
3515
+ }
3516
+ if (res.status === 403) {
3517
+ let errCode;
3518
+ try {
3519
+ const body = await res.json();
3520
+ errCode = body?.error?.code;
3521
+ } catch {
3522
+ }
3523
+ if (errCode === "INSUFFICIENT_SCOPE") {
3524
+ return [
3525
+ {
3526
+ name: "Server preflight",
3527
+ ok: true,
3528
+ detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and run admin checks."
3529
+ }
3530
+ ];
3531
+ }
3532
+ }
3533
+ const text2 = await res.text().catch(() => "");
3534
+ return [
3535
+ {
3536
+ name: "Server preflight",
3537
+ ok: false,
3538
+ detail: `HTTP ${res.status}: ${text2.slice(0, 120)}`
3539
+ }
3540
+ ];
3541
+ } catch (err) {
3542
+ const msg = err instanceof Error ? err.message : String(err);
3543
+ return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
3544
+ }
3545
+ }
3546
+ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
3547
+ if (!config.apiKey || !config.endpoint) {
3548
+ return [
3549
+ {
3550
+ name: "Ingest setup",
3551
+ ok: false,
3552
+ detail: "Need apiKey and endpoint. Run `mushi connect`."
3553
+ }
3554
+ ];
3555
+ }
3556
+ try {
3557
+ const data = await fetchIngestSetup(
3558
+ { endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
3559
+ doFetch
3560
+ );
3561
+ if (!data) {
3562
+ return [{ name: "Ingest setup", ok: false, detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload" }];
3563
+ }
3564
+ const steps = data.steps ?? [];
3565
+ const checks = steps.filter((s) => s.required).map((s) => ({
3566
+ name: `[ingest] ${s.label}`,
3567
+ ok: s.complete,
3568
+ detail: s.complete ? "" : s.hint ?? ""
3569
+ }));
3570
+ const diag = data.diagnostic;
3571
+ if (diag?.last_sdk_seen_at) {
3572
+ checks.push({
3573
+ name: "[ingest] Last SDK heartbeat",
3574
+ ok: true,
3575
+ detail: `${diag.last_sdk_seen_at}${diag.last_sdk_endpoint_host ? ` @ ${diag.last_sdk_endpoint_host}` : ""}`
3576
+ });
3577
+ }
3578
+ return checks.length > 0 ? checks : [{ name: "Ingest setup", ok: false, detail: "Empty response from /v1/sync/ingest-setup" }];
3579
+ } catch (err) {
3580
+ const msg = err instanceof Error ? err.message : String(err);
3581
+ return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
3582
+ }
3583
+ }
3584
+ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
3585
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
3586
+ return [
3587
+ {
3588
+ name: "QA stories health",
3589
+ ok: false,
3590
+ detail: "Need projectId, apiKey, and endpoint for QA story checks."
3591
+ }
3592
+ ];
3593
+ }
3594
+ const checks = [];
3595
+ try {
3596
+ const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
3597
+ const headers = apiKeyHeaders(apiKey, projectId);
3598
+ const storiesRes = await doFetch(
3599
+ `${endpoint}/v1/admin/projects/${projectId}/qa-coverage`,
3600
+ {
3601
+ headers,
3602
+ signal: AbortSignal.timeout(8e3)
3603
+ }
3604
+ );
3605
+ if (!storiesRes.ok) {
3606
+ checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
3607
+ return checks;
3608
+ }
3609
+ const storiesBody = await storiesRes.json();
3610
+ const stories = storiesBody.data?.coverage ?? [];
3611
+ const enabled = stories.filter((s) => s.enabled);
3612
+ if (enabled.length === 0) {
3613
+ checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
3614
+ return checks;
3615
+ }
3616
+ checks.push({
3617
+ name: "[qa] Enabled QA stories",
3618
+ ok: true,
3619
+ detail: `${enabled.length} enabled story/stories configured`
3620
+ });
3621
+ const slackRes = await doFetch(
3622
+ `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
3623
+ {
3624
+ method: "POST",
3625
+ headers,
3626
+ signal: AbortSignal.timeout(6e3)
3627
+ }
3628
+ );
3629
+ const slackBody = slackRes.ok ? await slackRes.json() : null;
3630
+ const slackOk = slackBody?.status === "ok";
3631
+ checks.push({
3632
+ name: "[qa] Slack notifications configured",
3633
+ ok: slackOk,
3634
+ detail: slackOk ? "Slack connected \u2014 failures will notify your channel" : "Slack not connected \u2014 you won't be notified when stories fail. Visit /integrations \u2192 Add to Slack."
3635
+ });
3636
+ const fcRes = await doFetch(
3637
+ `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
3638
+ {
3639
+ method: "POST",
3640
+ headers,
3641
+ signal: AbortSignal.timeout(6e3)
3642
+ }
3643
+ );
3644
+ const fcBody = fcRes.ok ? await fcRes.json() : null;
3645
+ const hasFirecrawlStories = enabled.some(
3646
+ (s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
3647
+ );
3648
+ if (hasFirecrawlStories) {
3649
+ const fcOk = fcBody?.status === "ok";
3650
+ checks.push({
3651
+ name: "[qa] Firecrawl API key configured",
3652
+ ok: fcOk,
3653
+ detail: fcOk ? "Firecrawl key is resolvable \u2014 stories will run without Unauthorized errors" : "No Firecrawl key found \u2014 enabled stories using firecrawl_actions will 401. Add a key at /integrations \u2192 BYOK keys."
3654
+ });
3655
+ }
3656
+ } catch (err) {
3657
+ const msg = err instanceof Error ? err.message : String(err);
3658
+ checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
3659
+ }
3660
+ return checks;
3661
+ }
3662
+ async function checkHostAppWiring(cwd) {
3663
+ const checks = [];
3664
+ try {
3665
+ const { readFile: readFile2, access } = await import("fs/promises");
3666
+ const { join: join5, resolve: resolve3 } = await import("path");
3667
+ const root = resolve3(cwd);
3668
+ const pkgPath = join5(root, "package.json");
3669
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
3670
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
3671
+ const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
3672
+ const envCandidates = [".env.local", ".env"];
3673
+ let envContent = "";
3674
+ for (const f of envCandidates) {
3675
+ try {
3676
+ envContent = await readFile2(join5(root, f), "utf8");
3677
+ break;
3678
+ } catch {
3679
+ }
3680
+ }
3681
+ const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
3682
+ const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(envContent);
3683
+ checks.push({
3684
+ name: "[host] Mushi env vars in .env.local",
3685
+ ok: hasProjectId && hasApiKey,
3686
+ detail: hasProjectId && hasApiKey ? "VITE_/MUSHI_ project id + API key found" : "Run `mushi connect --write-env` or add VITE_MUSHI_PROJECT_ID + VITE_MUSHI_API_KEY"
3687
+ });
3688
+ let mcpPresent = false;
3689
+ try {
3690
+ await access(join5(root, ".cursor", "mcp.json"));
3691
+ mcpPresent = true;
3692
+ } catch {
3693
+ }
3694
+ checks.push({
3695
+ name: "[host] Cursor MCP config",
3696
+ ok: mcpPresent,
3697
+ detail: mcpPresent ? ".cursor/mcp.json present" : "Run `mushi connect` to wire MCP for two-way reporter replies"
3242
3698
  });
3243
3699
  if (isCapHybrid) {
3244
3700
  const hasWebSdk = Boolean(deps["@mushi-mushi/web"] || deps["@mushi-mushi/react"]);
@@ -3388,449 +3844,187 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
3388
3844
  });
3389
3845
  }
3390
3846
  }
3391
- return checks;
3392
- }
3393
- var FIX_HINTS = {
3394
- "CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
3395
- "API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
3396
- "Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
3397
- "Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
3398
- "[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
3399
- "[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
3400
- "[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
3401
- };
3402
- function fixHintForCheck(name) {
3403
- if (FIX_HINTS[name]) return FIX_HINTS[name];
3404
- if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
3405
- if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
3406
- if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
3407
- return void 0;
3408
- }
3409
- async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = globalThis.fetch) {
3410
- if (!config.apiKey || !config.projectId || !config.endpoint) {
3411
- if (config.apiKey && config.endpoint) {
3412
- return {
3413
- nextAction: "Select or create a project: run `mushi project create` or pick one with `mushi project list`",
3414
- ctaPath: "/onboarding?tab=steps&setup=cli",
3415
- done: false
3416
- };
3417
- }
3418
- return {
3419
- nextAction: "Sign in to Mushi: run `npx mushi-mushi` (browser opens automatically \u2014 do NOT type the code in the terminal)",
3420
- ctaPath: "/onboarding?tab=steps&setup=cli",
3421
- done: false
3422
- };
3423
- }
3424
- try {
3425
- const { readFile: readFile2 } = await import("fs/promises");
3426
- const { join: join5, resolve: resolve3 } = await import("path");
3427
- const root = resolve3(cwd);
3428
- let envContent = "";
3429
- for (const f of [".env.local", ".env"]) {
3430
- try {
3431
- envContent = await readFile2(join5(root, f), "utf8");
3432
- break;
3433
- } catch {
3434
- }
3435
- }
3436
- const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
3437
- if (!hasEnvVars) {
3438
- return {
3439
- nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
3440
- ctaPath: "/onboarding?tab=sdk",
3441
- done: false
3442
- };
3443
- }
3444
- } catch {
3445
- }
3446
- try {
3447
- const sdkCheck = await checkSdkInstall(cwd);
3448
- if (sdkCheck && !sdkCheck.ok) {
3449
- return {
3450
- nextAction: `Install the Mushi SDK: ${sdkCheck.detail}`,
3451
- ctaPath: "/onboarding?tab=sdk",
3452
- done: false
3453
- };
3454
- }
3455
- } catch {
3456
- }
3457
- try {
3458
- const data = await fetchIngestSetup(
3459
- { endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
3460
- doFetch
3461
- );
3462
- if (data) {
3463
- const steps = data.steps ?? [];
3464
- const incomplete = steps.find((s) => s.required && !s.complete);
3465
- if (incomplete) {
3466
- const tabMap = {
3467
- project_created: "/onboarding?tab=steps&setup=cli",
3468
- api_key_generated: "/onboarding?tab=verify",
3469
- sdk_installed: "/onboarding?tab=sdk",
3470
- first_report_received: "/onboarding?tab=verify"
3471
- };
3472
- const tab = tabMap[incomplete.id] ?? "/onboarding?tab=verify";
3473
- return {
3474
- nextAction: incomplete.hint ?? `Complete setup step: ${incomplete.label}`,
3475
- ctaPath: tab,
3476
- done: false
3477
- };
3478
- }
3479
- return {
3480
- nextAction: "All 4 required setup steps are complete! Open the console to explore.",
3481
- ctaPath: "/reports",
3482
- done: true
3483
- };
3484
- }
3485
- } catch {
3486
- }
3487
- return {
3488
- nextAction: "Open the onboarding wizard in the console to check your setup status.",
3489
- ctaPath: "/onboarding",
3490
- done: false
3491
- };
3492
- }
3493
- async function runDoctor(config, options = {}) {
3494
- const doFetch = options.fetch ?? globalThis.fetch;
3495
- const checks = [];
3496
- const isFull = options.full === true;
3497
- const runServer = isFull || options.server !== false;
3498
- const runIngest = isFull || options.ingest !== false;
3499
- checks.push(...checkCliConfig(config));
3500
- if (config.endpoint) {
3501
- checks.push(await checkEndpointReachability(config.endpoint, doFetch));
3502
- }
3503
- const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
3504
- if (sdkCheck) checks.push(sdkCheck);
3505
- if (runServer) {
3506
- const serverChecks = await checkServerPreflight(config, doFetch);
3507
- checks.push(...serverChecks);
3508
- }
3509
- if (runIngest) {
3510
- const ingestChecks = await checkIngestSetup(config, doFetch);
3511
- checks.push(...ingestChecks);
3512
- }
3513
- if (isFull || options.qaStories) {
3514
- const qaChecks = await checkQaStoriesHealth(config, doFetch);
3515
- checks.push(...qaChecks);
3516
- }
3517
- if (isFull || options.hostApp) {
3518
- const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
3519
- checks.push(...hostChecks);
3520
- }
3521
- if (isFull || options.mcp) {
3522
- const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
3523
- checks.push(...mcpChecks);
3524
- }
3525
- const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
3526
- if (heartbeatPassed) {
3527
- const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
3528
- if (idx >= 0) {
3529
- checks[idx] = {
3530
- ...checks[idx],
3531
- ok: true,
3532
- warn: true,
3533
- detail: "No @mushi-mushi/* found in cwd package.json \u2014 but live heartbeats confirm the SDK is active. Run from your app repo or install with `mushi init`."
3534
- };
3535
- }
3536
- }
3537
- return { checks, ready: checks.every((c) => c.ok) };
3538
- }
3539
- function formatDoctorResult(result) {
3540
- const PASS = "\u2713";
3541
- const WARN = "\u26A0";
3542
- const FAIL = "\u2717";
3543
- const lines = [];
3544
- for (const c of result.checks) {
3545
- const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
3546
- lines.push(`${icon} ${c.name}`);
3547
- if (c.detail) lines.push(` ${c.detail}`);
3548
- if (!c.ok) {
3549
- const hint = fixHintForCheck(c.name);
3550
- if (hint) lines.push(` \u2192 Fix: ${hint}`);
3551
- }
3552
- }
3553
- const failed = result.checks.filter((c) => !c.ok);
3554
- const warned = result.checks.filter((c) => c.ok && c.warn);
3555
- if (failed.length === 0) {
3556
- if (warned.length > 0) {
3557
- lines.push(`
3558
- All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
3559
- } else {
3560
- lines.push("\nAll checks passed. The CLI is ready.");
3561
- }
3562
- } else {
3563
- lines.push(`
3564
- ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
3565
- lines.push("Fix the items above and re-run `mushi doctor`.");
3566
- }
3567
- return lines.join("\n");
3568
- }
3569
-
3570
- // src/upgrade.ts
3571
- import { resolve as resolve2 } from "path";
3572
- import { execSync } from "child_process";
3573
- var SAFE_NPM_VERSION = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
3574
- var MUSHI_PACKAGES = [
3575
- "@mushi-mushi/web",
3576
- "@mushi-mushi/core",
3577
- "@mushi-mushi/react",
3578
- "@mushi-mushi/react-native",
3579
- "@mushi-mushi/capacitor",
3580
- "@mushi-mushi/node",
3581
- "@mushi-mushi/cli"
3582
- ];
3583
- async function planUpgrade(cwd) {
3584
- const root = resolve2(cwd);
3585
- const pkg = readPackageJson(root);
3586
- const pm = detectPackageManager(root);
3587
- const deps = {
3588
- ...pkg?.dependencies ?? {},
3589
- ...pkg?.devDependencies ?? {}
3590
- };
3591
- const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
3592
- const entries = await Promise.all(
3593
- installed2.map(async (name) => {
3594
- const current = deps[name] ?? "";
3595
- const currentCore = current.replace(/^[\^~>=<]*/, "");
3596
- if (!/^\d/.test(currentCore)) {
3597
- return { name, current, latest: null, willUpgrade: false };
3598
- }
3599
- const freshness = await checkFreshness(name, currentCore, {
3600
- timeoutMs: 4e3,
3601
- ignoreOptOut: true
3602
- });
3603
- const latest = freshness?.latest ?? null;
3604
- const safeLatest = latest && SAFE_NPM_VERSION.test(latest) ? latest : null;
3605
- const willUpgrade = Boolean(freshness?.isOutdated && safeLatest);
3847
+ return checks;
3848
+ }
3849
+ var FIX_HINTS = {
3850
+ "CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
3851
+ "API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
3852
+ "Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
3853
+ "Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
3854
+ "[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
3855
+ "[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
3856
+ "[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
3857
+ };
3858
+ function fixHintForCheck(name) {
3859
+ if (FIX_HINTS[name]) return FIX_HINTS[name];
3860
+ if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
3861
+ if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
3862
+ if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
3863
+ return void 0;
3864
+ }
3865
+ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = globalThis.fetch) {
3866
+ if (!config.apiKey || !config.projectId || !config.endpoint) {
3867
+ if (config.apiKey && config.endpoint) {
3606
3868
  return {
3607
- name,
3608
- current,
3609
- latest: safeLatest,
3610
- willUpgrade,
3611
- migrateToWeb: name === "@mushi-mushi/react" && willUpgrade
3869
+ nextAction: "Select or create a project: run `mushi project create` or pick one with `mushi project list`",
3870
+ ctaPath: "/onboarding?tab=steps&setup=cli",
3871
+ done: false
3612
3872
  };
3613
- })
3614
- );
3615
- const toBump = entries.filter((e) => e.willUpgrade && e.latest).map((e) => `${e.name}@${e.latest}`);
3616
- return {
3617
- cwd: root,
3618
- packageManager: pm,
3619
- entries,
3620
- installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
3621
- };
3622
- }
3623
- async function runUpgrade(opts = {}) {
3624
- const plan = await planUpgrade(opts.cwd ?? process.cwd());
3625
- if (plan.entries.length === 0) {
3626
- return {
3627
- plan,
3628
- upgraded: false,
3629
- message: "No @mushi-mushi/* packages in package.json \u2014 run `mushi init` first."
3630
- };
3631
- }
3632
- if (!plan.installCmd) {
3633
- const semverEntries = plan.entries.filter((e) => /\d/.test(e.current));
3634
- const allChecksFailed = semverEntries.length > 0 && semverEntries.every((e) => e.latest === null);
3873
+ }
3635
3874
  return {
3636
- plan,
3637
- upgraded: false,
3638
- message: allChecksFailed ? "Could not reach the npm registry to check for updates \u2014 try again in a moment." : "All installed Mushi packages are already at the latest stable version."
3875
+ nextAction: "Sign in to Mushi: run `npx mushi-mushi` (browser opens automatically \u2014 do NOT type the code in the terminal)",
3876
+ ctaPath: "/onboarding?tab=steps&setup=cli",
3877
+ done: false
3639
3878
  };
3640
3879
  }
3641
- if (opts.dryRun) {
3642
- return { plan, upgraded: false, message: `[dry-run] Would run: ${plan.installCmd}` };
3643
- }
3644
3880
  try {
3645
- execSync(plan.installCmd, { cwd: plan.cwd, stdio: "inherit", env: process.env });
3646
- } catch {
3647
- return {
3648
- plan,
3649
- upgraded: false,
3650
- message: `Install command failed \u2014 fix the error above or run it manually:
3651
- ${plan.installCmd}`
3652
- };
3653
- }
3654
- const reactEntry = plan.entries.find((e) => e.migrateToWeb);
3655
- const migrateNote = reactEntry ? "\nNote: @mushi-mushi/react is legacy \u2014 prefer @mushi-mushi/web for Vite/Capacitor/SPA apps." : "";
3656
- return {
3657
- plan,
3658
- upgraded: true,
3659
- message: `Upgraded Mushi SDK packages.
3660
- ${plan.installCmd}${migrateNote}`
3661
- };
3662
- }
3663
-
3664
- // src/commands/fix.ts
3665
- function registerFixCommands(program2) {
3666
- const fixCmd = program2.command("fix").description("Dispatch an agentic fix for a report");
3667
- fixCmd.argument("<reportId>", "Report UUID to fix").option(
3668
- "--agent <name>",
3669
- "Agent adapter: claude_code (default), cursor_cloud, codex, mcp",
3670
- "claude_code"
3671
- ).option(
3672
- "--model <slug>",
3673
- "Model override for cursor_cloud (e.g. composer-latest)"
3674
- ).option(
3675
- "--no-auto-pr",
3676
- "For cursor_cloud: skip automatic PR creation (branch only)"
3677
- ).option(
3678
- "--wait",
3679
- "Poll until terminal state and exit non-zero on error/cancelled (CI-friendly)"
3680
- ).option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--project-id <id>", "Project ID (overrides MUSHI_PROJECT_ID)").addHelpText("after", `
3681
- Examples:
3682
- mushi fix abc123 --agent cursor_cloud --wait
3683
- mushi fix abc123 --agent cursor_cloud --model composer-latest --no-auto-pr
3684
- mushi fix abc123 --agent claude_code
3685
-
3686
- # CI: fail the pipeline if the fix errors
3687
- mushi fix $REPORT_ID --agent cursor_cloud --wait && echo "Fix PR opened"`).action(async (reportId, opts) => {
3688
- const cfg = loadConfig();
3689
- if (opts.endpoint) cfg.endpoint = opts.endpoint;
3690
- if (opts.apiKey) cfg.apiKey = opts.apiKey;
3691
- if (opts.projectId) cfg.projectId = opts.projectId;
3692
- const isTTY = process.stdout.isTTY;
3693
- const emitEvent = (type, data) => {
3694
- if (isTTY) {
3695
- const ts = (/* @__PURE__ */ new Date()).toISOString();
3696
- console.log(`[${ts}] ${type}`, JSON.stringify(data));
3697
- } else {
3698
- process.stdout.write(JSON.stringify({ type, ...data }) + "\n");
3699
- }
3700
- };
3701
- emitEvent("dispatch.start", { reportId, agent: opts.agent, model: opts.model ?? null });
3702
- const body = {
3703
- reportId,
3704
- projectId: cfg.projectId,
3705
- agent: opts.agent
3706
- };
3707
- if (opts.agent === "cursor_cloud") {
3708
- if (opts.model) body.cursorModel = opts.model;
3709
- if (!opts.autoPr) body.cursorAutoCreatePR = false;
3710
- }
3711
- const result = await apiCall("/v1/admin/fixes/dispatch", cfg, {
3712
- method: "POST",
3713
- headers: { "Content-Type": "application/json" },
3714
- body: JSON.stringify(body)
3715
- });
3716
- if (!result.ok) {
3717
- console.error("Error dispatching fix:", result.error.message);
3718
- process.exit(1);
3719
- }
3720
- const { fixId, status, agentId, runId, prUrl } = result.data;
3721
- emitEvent("dispatch.ok", { fixId, status, agentId, runId, prUrl });
3722
- if (!opts.wait) {
3723
- process.exit(0);
3724
- }
3725
- if (!fixId) {
3726
- console.error("No fixId returned \u2014 cannot poll.");
3727
- process.exit(1);
3728
- }
3729
- const POLL_MS = 5e3;
3730
- const MAX_POLLS = 120;
3731
- const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "error", "cancelled", "skipped", "skipped_unsupported_agent", "skipped_no_sandbox"]);
3732
- for (let i = 0; i < MAX_POLLS; i++) {
3733
- await new Promise((r) => setTimeout(r, POLL_MS));
3734
- const pollResult = await apiCall(
3735
- `/v1/admin/fixes/${fixId}`,
3736
- cfg
3737
- );
3738
- if (!pollResult.ok) {
3739
- emitEvent("poll.error", { error: pollResult.error.message });
3740
- continue;
3741
- }
3742
- const s = pollResult.data.status;
3743
- emitEvent("fix.status", { status: s, pr_url: pollResult.data.pr_url, cursor_agent_id: pollResult.data.cursor_agent_id });
3744
- if (s && TERMINAL.has(s)) {
3745
- const success = s === "completed";
3746
- if (!success) {
3747
- console.error(`Fix ended with status: ${s}${pollResult.data.error ? ` \u2014 ${pollResult.data.error}` : ""}`);
3748
- process.exit(1);
3749
- }
3750
- process.exit(0);
3881
+ const { readFile: readFile2 } = await import("fs/promises");
3882
+ const { join: join5, resolve: resolve3 } = await import("path");
3883
+ const root = resolve3(cwd);
3884
+ let envContent = "";
3885
+ for (const f of [".env.local", ".env"]) {
3886
+ try {
3887
+ envContent = await readFile2(join5(root, f), "utf8");
3888
+ break;
3889
+ } catch {
3751
3890
  }
3752
3891
  }
3753
- console.error("Polling timed out after 10 minutes. The fix may still be running.");
3754
- process.exit(1);
3755
- });
3756
- program2.command("nudge").description(
3757
- "Generate a Mushi.init() snippet tuned for your release phase (alpha, beta, ga). Customises proactive triggers, cooldowns, feature-request card, and beta-mode UI."
3758
- ).option("--phase <phase>", "Release phase: alpha | beta | ga", "beta").option("--explain", "Print a human-readable summary of what the preset does").option("--max <n>", "Override maxProactivePerSession").option("--cooldown <hours>", "Override dismissCooldownHours").option("--dwell <minutes>", "Override page-dwell threshold (0 disables)").option("--welcome <seconds>", "Override first-session welcome delay (0 disables)").action((opts) => {
3759
- const validPhases = ["alpha", "beta", "ga"];
3760
- if (!validPhases.includes(opts.phase)) {
3761
- console.error(`Unknown phase "${opts.phase}". Use one of: ${validPhases.join(", ")}`);
3762
- process.exit(1);
3892
+ const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
3893
+ if (!hasEnvVars) {
3894
+ return {
3895
+ nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
3896
+ ctaPath: "/onboarding?tab=sdk",
3897
+ done: false
3898
+ };
3763
3899
  }
3764
- const phase = opts.phase;
3765
- const overrides = {};
3766
- const parseNumericFlag = (flag, raw, min) => {
3767
- const n = Number(raw);
3768
- if (!Number.isFinite(n) || n < min) {
3769
- console.error(
3770
- `error: --${flag} must be a finite number >= ${min} (got "${raw}")`
3771
- );
3772
- process.exit(1);
3773
- }
3774
- return n;
3775
- };
3776
- if (opts.max !== void 0) overrides.maxProactivePerSession = parseNumericFlag("max", opts.max, 1);
3777
- if (opts.cooldown !== void 0) overrides.dismissCooldownHours = parseNumericFlag("cooldown", opts.cooldown, 0);
3778
- if (opts.dwell !== void 0) overrides.pageDwellMinutes = parseNumericFlag("dwell", opts.dwell, 0);
3779
- if (opts.welcome !== void 0) overrides.firstSessionSeconds = parseNumericFlag("welcome", opts.welcome, 0);
3780
- if (opts.explain) {
3781
- console.log(renderNudgeExplainer(phase));
3900
+ } catch {
3901
+ }
3902
+ try {
3903
+ const sdkCheck = await checkSdkInstall(cwd);
3904
+ if (sdkCheck && !sdkCheck.ok) {
3905
+ return {
3906
+ nextAction: `Install the Mushi SDK: ${sdkCheck.detail}`,
3907
+ ctaPath: "/onboarding?tab=sdk",
3908
+ done: false
3909
+ };
3782
3910
  }
3783
- console.log(renderNudgeSnippet({ phase, overrides }));
3784
- });
3785
- program2.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--dry-run", "Print the install command without running it").option("--json", "Machine-readable plan + result").addHelpText("after", `
3786
- Examples:
3787
- mushi upgrade
3788
- mushi upgrade --dry-run
3789
- mushi upgrade --cwd ../glot.it`).action(async (opts) => {
3790
- const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
3791
- if (opts.json) {
3792
- console.log(JSON.stringify(result, null, 2));
3793
- } else {
3794
- console.log(result.message);
3795
- for (const e of result.plan.entries) {
3796
- const tag = e.willUpgrade && e.latest ? `\u2192 v${e.latest}` : "(current)";
3797
- console.log(` ${e.name}@${e.current} ${tag}`);
3911
+ } catch {
3912
+ }
3913
+ try {
3914
+ const data = await fetchIngestSetup(
3915
+ { endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
3916
+ doFetch
3917
+ );
3918
+ if (data) {
3919
+ const steps = data.steps ?? [];
3920
+ const incomplete = steps.find((s) => s.required && !s.complete);
3921
+ if (incomplete) {
3922
+ const tabMap = {
3923
+ project_created: "/onboarding?tab=steps&setup=cli",
3924
+ api_key_generated: "/onboarding?tab=verify",
3925
+ sdk_installed: "/onboarding?tab=sdk",
3926
+ first_report_received: "/onboarding?tab=verify"
3927
+ };
3928
+ const tab = tabMap[incomplete.id] ?? "/onboarding?tab=verify";
3929
+ return {
3930
+ nextAction: incomplete.hint ?? `Complete setup step: ${incomplete.label}`,
3931
+ ctaPath: tab,
3932
+ done: false
3933
+ };
3798
3934
  }
3935
+ return {
3936
+ nextAction: "All 4 required setup steps are complete! Open the console to explore.",
3937
+ ctaPath: "/reports",
3938
+ done: true
3939
+ };
3799
3940
  }
3800
- if (!result.upgraded && result.plan.entries.some((e) => e.willUpgrade) && !opts.dryRun) {
3801
- process.exit(1);
3941
+ } catch {
3942
+ }
3943
+ return {
3944
+ nextAction: "Open the onboarding wizard in the console to check your setup status.",
3945
+ ctaPath: "/onboarding",
3946
+ done: false
3947
+ };
3948
+ }
3949
+ async function runDoctor(config, options = {}) {
3950
+ const doFetch = options.fetch ?? globalThis.fetch;
3951
+ const checks = [];
3952
+ const isFull = options.full === true;
3953
+ const runServer = isFull || options.server !== false;
3954
+ const runIngest = isFull || options.ingest !== false;
3955
+ checks.push(...checkCliConfig(config));
3956
+ if (config.endpoint) {
3957
+ checks.push(await checkEndpointReachability(config.endpoint, doFetch));
3958
+ }
3959
+ const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
3960
+ if (sdkCheck) checks.push(sdkCheck);
3961
+ if (runServer) {
3962
+ const serverChecks = await checkServerPreflight(config, doFetch);
3963
+ checks.push(...serverChecks);
3964
+ }
3965
+ if (runIngest) {
3966
+ const ingestChecks = await checkIngestSetup(config, doFetch);
3967
+ checks.push(...ingestChecks);
3968
+ }
3969
+ if (isFull || options.qaStories) {
3970
+ const qaChecks = await checkQaStoriesHealth(config, doFetch);
3971
+ checks.push(...qaChecks);
3972
+ }
3973
+ if (isFull || options.hostApp) {
3974
+ const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
3975
+ checks.push(...hostChecks);
3976
+ }
3977
+ if (isFull || options.mcp) {
3978
+ const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
3979
+ checks.push(...mcpChecks);
3980
+ }
3981
+ const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
3982
+ if (heartbeatPassed) {
3983
+ const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
3984
+ if (idx >= 0) {
3985
+ checks[idx] = {
3986
+ ...checks[idx],
3987
+ ok: true,
3988
+ warn: true,
3989
+ detail: "No @mushi-mushi/* found in cwd package.json \u2014 but live heartbeats confirm the SDK is active. Run from your app repo or install with `mushi init`."
3990
+ };
3802
3991
  }
3803
- if (result.plan.entries.length === 0) process.exit(1);
3804
- });
3805
- program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "Force-write SDK env vars to .env.local (default; overrides --no-env)").option("--wire-ide", "Force-wire Cursor MCP into .cursor/mcp.json (default; overrides --no-ide)").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
3806
- Examples:
3807
- MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
3808
- mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
3809
- const apiKey = process.env.MUSHI_API_KEY ?? opts.apiKey;
3810
- if (!apiKey) {
3811
- console.error("Provide the API key via the MUSHI_API_KEY env var (recommended) or --api-key <key>.");
3812
- process.exit(1);
3992
+ }
3993
+ return { checks, ready: checks.every((c) => c.ok) };
3994
+ }
3995
+ function formatDoctorResult(result) {
3996
+ const PASS = "OK";
3997
+ const WARN = "WARN";
3998
+ const FAIL = "FAIL";
3999
+ const lines = [];
4000
+ for (const c of result.checks) {
4001
+ const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
4002
+ lines.push(`${icon} ${c.name}`);
4003
+ if (c.detail) lines.push(` ${c.detail}`);
4004
+ if (!c.ok) {
4005
+ const hint = fixHintForCheck(c.name);
4006
+ if (hint) lines.push(` \u2192 Fix: ${hint}`);
3813
4007
  }
3814
- const result = await runConnect({
3815
- apiKey,
3816
- projectId: opts.projectId,
3817
- endpoint: opts.endpoint,
3818
- cwd: opts.cwd,
3819
- // Explicit --write-env / --wire-ide force the action on even if --no-env /
3820
- // --no-ide was also passed; otherwise writing is the default.
3821
- writeEnv: opts.writeEnv === true ? true : opts.env !== false,
3822
- wireIde: opts.wireIde === true ? true : opts.ide !== false,
3823
- wait: opts.wait,
3824
- waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
3825
- json: opts.json
3826
- });
3827
- if (opts.json) {
3828
- console.log(JSON.stringify(result, null, 2));
4008
+ }
4009
+ const failed = result.checks.filter((c) => !c.ok);
4010
+ const warned = result.checks.filter((c) => c.ok && c.warn);
4011
+ if (failed.length === 0) {
4012
+ if (warned.length > 0) {
4013
+ lines.push(`
4014
+ All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
3829
4015
  } else {
3830
- for (const line of result.messages) console.log(line);
4016
+ lines.push("\nAll checks passed. The CLI is ready.");
3831
4017
  }
3832
- if (!result.ok) process.exit(1);
3833
- });
4018
+ } else {
4019
+ lines.push(`
4020
+ ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
4021
+ lines.push("Fix the items above and re-run `mushi doctor`.");
4022
+ }
4023
+ return lines.join("\n");
4024
+ }
4025
+
4026
+ // src/commands/doctor-cli.ts
4027
+ function registerDoctorCommand(program2) {
3834
4028
  program2.command("doctor").description(
3835
4029
  "Run pre-flight checks: CLI config, endpoint reachability, SDK install, ingest readiness (API key \u2192 heartbeat \u2192 first report), and dispatch readiness (GitHub, index, BYOK, autofix). Ingest + server checks run by default; pass --no-server or --no-ingest to skip."
3836
4030
  ).option("--cwd <path>", "Run package detection from a different directory").option("--json", "Machine-readable output").option("--no-server", "Skip dispatch-readiness /preflight checks").option("--no-ingest", "Skip ingest-setup checks (SDK heartbeat, first report)").option(
@@ -3859,11 +4053,11 @@ Examples:
3859
4053
  const status = await checkOnboardingStatus(config, consoleBase, cwd);
3860
4054
  const base = consoleBase.replace(/\/$/, "");
3861
4055
  if (status.done) {
3862
- console.log(`\u2713 Setup complete. ${status.nextAction}`);
3863
- console.log(` ${base}${status.ctaPath}`);
4056
+ console.log(`OK Setup complete. ${status.nextAction}`);
4057
+ console.log(` ${base}${status.ctaPath}`);
3864
4058
  } else {
3865
4059
  console.log(`\u2192 Next: ${status.nextAction}`);
3866
- console.log(` Open: ${base}${status.ctaPath}`);
4060
+ console.log(` Open: ${base}${status.ctaPath}`);
3867
4061
  }
3868
4062
  if (!status.done) process.exit(1);
3869
4063
  return;
@@ -3899,6 +4093,10 @@ Examples:
3899
4093
  console.log(formatDoctorResult(result));
3900
4094
  if (!result.ready) process.exit(1);
3901
4095
  });
4096
+ }
4097
+
4098
+ // src/commands/reset-cli.ts
4099
+ function registerResetCommand(program2) {
3902
4100
  program2.command("reset [projectId]").description(
3903
4101
  "Archive a project and wipe its test data (codebase_files, fix_attempts, reports). Speeds up re-running the full onboarding flow from scratch. Requires `--confirm` to prevent accidents."
3904
4102
  ).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (projectId, opts) => {
@@ -3933,146 +4131,6 @@ Re-run with --confirm to proceed.`
3933
4131
  process.exit(1);
3934
4132
  }
3935
4133
  });
3936
- const fixes = program2.command("fixes").description("Fix dispatch management");
3937
- fixes.command("tail").description(
3938
- "Stream SSE dispatch events for a report in real time. Useful for headless debugging without opening the browser."
3939
- ).requiredOption("--report-id <id>", "Report ID to follow").action(async (opts) => {
3940
- const config = loadConfig();
3941
- if (!config.apiKey) {
3942
- console.error("Run `mushi login` first");
3943
- process.exit(1);
3944
- }
3945
- if (!config.endpoint) {
3946
- console.error("No endpoint configured. Run `mushi init`");
3947
- process.exit(1);
3948
- }
3949
- const url = `${config.endpoint}/v1/admin/reports/${opts.reportId}/dispatch/stream`;
3950
- console.log(`Tailing dispatch stream for report ${opts.reportId}\u2026`);
3951
- console.log(`(Ctrl-C to stop)
3952
- `);
3953
- const res = await fetch(url, {
3954
- headers: {
3955
- "Authorization": `Bearer ${config.apiKey}`,
3956
- "X-Mushi-Api-Key": config.apiKey,
3957
- "X-Mushi-Project": config.projectId ?? "",
3958
- "Accept": "text/event-stream"
3959
- }
3960
- });
3961
- if (!res.ok || !res.body) {
3962
- console.error(`Failed to connect: HTTP ${res.status}`);
3963
- const text2 = await res.text().catch(() => "");
3964
- if (text2) console.error(text2.slice(0, 300));
3965
- process.exit(1);
3966
- }
3967
- const decoder = new TextDecoder();
3968
- const reader = res.body.getReader();
3969
- let done = false;
3970
- process.on("SIGINT", () => {
3971
- done = true;
3972
- void reader.cancel();
3973
- console.log("\nDisconnected.");
3974
- process.exit(0);
3975
- });
3976
- while (!done) {
3977
- const { value, done: streamDone } = await reader.read();
3978
- if (streamDone) break;
3979
- const chunk = decoder.decode(value, { stream: true });
3980
- for (const line of chunk.split("\n")) {
3981
- if (line.startsWith("data: ")) {
3982
- const raw = line.slice(6).trim();
3983
- if (raw === "[DONE]") {
3984
- console.log("\n[stream ended]");
3985
- process.exit(0);
3986
- }
3987
- try {
3988
- const event = JSON.parse(raw);
3989
- const ts = (/* @__PURE__ */ new Date()).toISOString();
3990
- const type = event.type ?? event.event ?? "event";
3991
- const status = event.status ?? event.data ?? "";
3992
- console.log(`${ts} ${type.padEnd(24)} ${status}`);
3993
- } catch {
3994
- console.log(line);
3995
- }
3996
- } else if (line.startsWith("event: ")) {
3997
- } else if (line && !line.startsWith(":")) {
3998
- console.log(line);
3999
- }
4000
- }
4001
- }
4002
- });
4003
- fixes.command("merge <fixId>").description(
4004
- "Squash-merge the fix PR on GitHub and mark the report Fixed (same as console merge)"
4005
- ).option(
4006
- "--method <method>",
4007
- "GitHub merge method: squash (default), merge, or rebase",
4008
- "squash"
4009
- ).option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
4010
- const config = loadConfig();
4011
- if (!config.apiKey) {
4012
- console.error("Run `mushi login` first");
4013
- process.exit(1);
4014
- }
4015
- if (!config.endpoint) {
4016
- console.error("No endpoint configured. Run `mushi init`");
4017
- process.exit(1);
4018
- }
4019
- const method = opts.method;
4020
- if (!["squash", "merge", "rebase"].includes(method)) {
4021
- console.error("--method must be squash, merge, or rebase");
4022
- process.exit(1);
4023
- }
4024
- const result = await apiCall(`/v1/admin/fixes/${fixId}/merge`, config, {
4025
- method: "POST",
4026
- body: JSON.stringify({ mergeMethod: method })
4027
- });
4028
- if (!result.ok) {
4029
- if (opts.json) {
4030
- console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
4031
- } else {
4032
- console.error("Merge failed:", result.error.message);
4033
- }
4034
- process.exit(1);
4035
- }
4036
- const data = result.data ?? {};
4037
- if (opts.json) {
4038
- console.log(JSON.stringify({ ok: true, ...data }, null, 2));
4039
- return;
4040
- }
4041
- if (data.alreadyMerged) {
4042
- console.log(`PR was already merged. Report status: ${data.reportStatus ?? "unknown"}`);
4043
- } else {
4044
- console.log(`Merged successfully. Report status: ${data.reportStatus ?? "fixed"}`);
4045
- }
4046
- });
4047
- fixes.command("refresh-ci <fixId>").description("Pull latest GitHub Actions check-run status for a fix attempt (on-demand ci-sync)").option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
4048
- const config = loadConfig();
4049
- if (!config.apiKey) {
4050
- console.error("Run `mushi login` first");
4051
- process.exit(1);
4052
- }
4053
- if (!config.endpoint) {
4054
- console.error("No endpoint configured. Run `mushi init`");
4055
- process.exit(1);
4056
- }
4057
- const result = await apiCall(`/v1/admin/fixes/${fixId}/refresh-ci`, config, { method: "POST" });
4058
- if (!result.ok) {
4059
- if (opts.json) {
4060
- console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
4061
- } else {
4062
- console.error("CI refresh failed:", result.error.message);
4063
- }
4064
- process.exit(1);
4065
- }
4066
- const data = result.data ?? {};
4067
- if (opts.json) {
4068
- console.log(JSON.stringify({ ok: true, ...data }, null, 2));
4069
- return;
4070
- }
4071
- const status = data.check_run_status ?? "unknown";
4072
- const conclusion = data.check_run_conclusion ?? "\u2014";
4073
- const updated = data.check_run_updated_at ? new Date(data.check_run_updated_at).toISOString() : "\u2014";
4074
- console.log(`CI status: ${status} \xB7 conclusion: ${conclusion} \xB7 updated: ${updated}`);
4075
- });
4076
4134
  }
4077
4135
 
4078
4136
  // src/commands/tdd.ts
@@ -4329,7 +4387,7 @@ function registerIntegrationsCommands(program2) {
4329
4387
  console.log("\nIntegrations:\n");
4330
4388
  for (const row of rows) {
4331
4389
  const icon = icons[row.kind] ?? "\u{1F50C}";
4332
- const statusIcon = row.status === "ok" ? "\u2705" : row.status === "error" ? "\u274C" : "\u26AA";
4390
+ const statusIcon = row.status === "ok" ? "OK" : row.status === "error" ? "FAIL" : "SKIP";
4333
4391
  console.log(` ${icon} ${row.kind.padEnd(20)} ${statusIcon} ${row.detail ?? ""}`);
4334
4392
  }
4335
4393
  console.log();
@@ -4361,7 +4419,7 @@ function registerIntegrationsCommands(program2) {
4361
4419
  }
4362
4420
  const probeOk = result.data?.status === "ok";
4363
4421
  console.log(
4364
- probeOk ? `\u2705 ${kind} integration is healthy${result.data.detail ? ": " + result.data.detail : ""}` : `\u274C ${kind} integration check failed${result.data.detail ? ": " + result.data.detail : ""}`
4422
+ probeOk ? `OK ${kind} integration is healthy${result.data.detail ? ": " + result.data.detail : ""}` : `FAIL ${kind} integration check failed${result.data.detail ? ": " + result.data.detail : ""}`
4365
4423
  );
4366
4424
  if (!probeOk) process.exit(1);
4367
4425
  });
@@ -4390,11 +4448,11 @@ function registerIntegrationsCommands(program2) {
4390
4448
  return;
4391
4449
  }
4392
4450
  if (result.data?.status === "ok") {
4393
- console.log("\u2705 Slack connected");
4451
+ console.log("OK Slack connected");
4394
4452
  if (result.data.detail) console.log(` ${result.data.detail}`);
4395
4453
  console.log("\n To change the channel or notification prefs, visit /integrations in the Mushi console.");
4396
4454
  } else {
4397
- console.log("\u26AA Slack not connected");
4455
+ console.log("SKIP Slack not connected");
4398
4456
  console.log(' Visit /integrations in the Mushi console and click "Add to Slack".');
4399
4457
  }
4400
4458
  });
@@ -4422,9 +4480,9 @@ function registerIntegrationsCommands(program2) {
4422
4480
  return;
4423
4481
  }
4424
4482
  if (result.data?.ok) {
4425
- console.log("\u2705 Test message sent! Check your Slack channel.");
4483
+ console.log("OK Test message sent. Check your Slack channel.");
4426
4484
  } else {
4427
- console.error("\u274C Test failed:", result.data?.error ?? "unknown error");
4485
+ console.error("FAIL Test failed:", result.data?.error ?? "unknown error");
4428
4486
  process.exit(1);
4429
4487
  }
4430
4488
  });
@@ -4464,7 +4522,7 @@ function registerQaCommands(program2) {
4464
4522
  QA Stories (${stories.length}):
4465
4523
  `);
4466
4524
  for (const s of stories) {
4467
- const statusIcon = s.last_run_status === "passed" ? "\u2705" : s.last_run_status === "failed" ? "\u274C" : s.last_run_status === "error" ? "\u{1F6A8}" : "\u26AA";
4525
+ const statusIcon = s.last_run_status === "passed" ? "PASS" : s.last_run_status === "failed" ? "FAIL" : s.last_run_status === "error" ? "ERROR" : "SKIP";
4468
4526
  const enabled = s.enabled ? "" : " [disabled]";
4469
4527
  const sid = s.story_id ?? s.id ?? "\u2014";
4470
4528
  console.log(` ${statusIcon} ${s.name.slice(0, 50).padEnd(52)} ${sid}${enabled}`);
@@ -4505,7 +4563,7 @@ QA Stories (${stories.length}):
4505
4563
  Recent runs for story ${storyId.slice(0, 8)}\u2026:
4506
4564
  `);
4507
4565
  for (const r of runs) {
4508
- const statusIcon = r.status === "passed" ? "\u2705" : r.status === "failed" ? "\u274C" : r.status === "error" ? "\u{1F6A8}" : "\u23F3";
4566
+ const statusIcon = r.status === "passed" ? "PASS" : r.status === "failed" ? "FAIL" : r.status === "error" ? "ERROR" : "PEND";
4509
4567
  const ts = r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace("T", " ") : "\u2014";
4510
4568
  const latency = r.latency_ms ? ` (${(r.latency_ms / 1e3).toFixed(1)}s)` : "";
4511
4569
  console.log(` ${statusIcon} ${ts}${latency} ${r.id.slice(0, 8)}`);
@@ -4518,7 +4576,7 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
4518
4576
  }
4519
4577
  }
4520
4578
  }
4521
- const consoleUrl2 = config.consoleUrl ?? "https://app.mushi.ai";
4579
+ const consoleUrl2 = config.consoleUrl ?? resolveConsoleUrlSync();
4522
4580
  console.log(`
4523
4581
  Open in console: ${consoleUrl2}/qa-coverage?story=${storyId}`);
4524
4582
  console.log(` Tip: run 'mushi config consoleUrl <url>' to override (e.g. http://localhost:6464 for local dev)`);
@@ -4549,10 +4607,10 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
4549
4607
  }
4550
4608
  const runId = result.data?.run_id;
4551
4609
  if (runId) {
4552
- console.log(`\u25B6 Run triggered: ${runId.slice(0, 8)}\u2026`);
4553
- console.log(` Check results: mushi qa runs ${storyId}`);
4610
+ console.log(`RUN Triggered: ${runId.slice(0, 8)}\u2026`);
4611
+ console.log(` Check results: mushi qa runs ${storyId}`);
4554
4612
  } else {
4555
- console.error("\u274C Trigger failed: no run_id in response", JSON.stringify(result.data));
4613
+ console.error("FAIL Trigger failed: no run_id in response", JSON.stringify(result.data));
4556
4614
  process.exit(1);
4557
4615
  }
4558
4616
  });
@@ -4569,10 +4627,10 @@ Description:
4569
4627
  \u2022 Gate results: API contract (G3), spec drift (G6), orphan endpoints (G7),
4570
4628
  unknown frontend calls (G8), schema drift, status claim (G5)
4571
4629
 
4572
- Returns a PM-readable scorecard with severity-ranked findings.
4630
+ Returns a human-readable summary with severity-ranked findings.
4573
4631
 
4574
4632
  Prerequisites:
4575
- 1. Configure your Supabase PAT: mushi settings set supabase-pat <token>
4633
+ 1. Add your Supabase PAT in Admin \u2192 Settings \u2192 API Keys
4576
4634
  2. Set supabase_project_ref in Admin \u2192 Settings \u2192 Project.
4577
4635
 
4578
4636
  Examples:
@@ -4632,8 +4690,8 @@ Examples:
4632
4690
  return;
4633
4691
  }
4634
4692
  const data = body.data;
4635
- const overallGlyph = data.summary.overall === "fail" ? "\u274C" : data.summary.overall === "warn" ? "\u26A0\uFE0F " : "\u2705";
4636
- process.stdout.write(`${overallGlyph}
4693
+ const overallLabel = data.summary.overall === "fail" ? "FAIL" : data.summary.overall === "warn" ? "WARN" : "OK";
4694
+ process.stdout.write(`${overallLabel}
4637
4695
 
4638
4696
  `);
4639
4697
  console.log(`Full-Stack Audit \u2014 ${new Date(data.audit_at).toLocaleString()}`);
@@ -4641,18 +4699,18 @@ Examples:
4641
4699
  console.log(`Summary: ${data.summary.error_count} error(s) \xB7 ${data.summary.warn_count} warning(s)
4642
4700
  `);
4643
4701
  if (data.findings.length === 0) {
4644
- console.log(" \u2713 No findings. Your project looks healthy.");
4702
+ console.log(" OK No findings. Your project looks healthy.");
4645
4703
  } else {
4646
4704
  for (const f of data.findings) {
4647
- const icon = f.severity === "error" ? "\u{1F534}" : f.severity === "warn" ? "\u{1F7E1}" : "\u2139\uFE0F ";
4648
- console.log(` ${icon} ${f.title}`);
4705
+ const icon = f.severity === "error" ? "FAIL" : f.severity === "warn" ? "WARN" : "INFO";
4706
+ console.log(` ${icon} ${f.title}`);
4649
4707
  console.log(` ${f.detail.slice(0, 120)}${f.detail.length > 120 ? "\u2026" : ""}`);
4650
4708
  }
4651
4709
  }
4652
4710
  if (data.gate_runs.length > 0) {
4653
4711
  console.log("\nGate Results:");
4654
4712
  for (const run of data.gate_runs) {
4655
- const g = run.status === "pass" ? "\u2713" : run.status === "fail" ? "\u2717" : "~";
4713
+ const g = run.status === "pass" ? "OK" : run.status === "fail" ? "FAIL" : "SKIP";
4656
4714
  console.log(` ${g} ${run.gate.padEnd(22)} ${run.status} (${run.findings_count} finding${run.findings_count !== 1 ? "s" : ""})`);
4657
4715
  }
4658
4716
  }
@@ -5053,7 +5111,7 @@ Examples:
5053
5111
 
5054
5112
  // src/index.ts
5055
5113
  installSignalHandlers();
5056
- var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION).addHelpText("after", `
5114
+ var program = new Command().name("mushi").description("Mushi CLI \u2014 set up the SDK, triage a report, fix from your editor").version(MUSHI_CLI_VERSION).addHelpText("after", `
5057
5115
  Environment variables:
5058
5116
  MUSHI_API_KEY SDK ingest key (report:write scope \u2014 from Onboarding \u2192 Verify in the console)
5059
5117
  MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
@@ -5076,6 +5134,11 @@ registerLessonsCommands(program);
5076
5134
  registerDiagnosticsCommands(program);
5077
5135
  registerProjectCommands(program);
5078
5136
  registerSetupCommands(program);
5137
+ registerNudgeCommand(program);
5138
+ registerUpgradeCommand(program);
5139
+ registerConnectCommand(program);
5140
+ registerDoctorCommand(program);
5141
+ registerResetCommand(program);
5079
5142
  registerFixCommands(program);
5080
5143
  registerTddCommands(program);
5081
5144
  registerKeysCommands(program);