@mushi-mushi/cli 0.13.0 → 0.14.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.
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var MUSHI_CLI_VERSION = true ? "0.14.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ MUSHI_CLI_VERSION
6
+ };
package/dist/index.js CHANGED
@@ -598,7 +598,7 @@ function getFrameworkFromPkg(pkg) {
598
598
  }
599
599
 
600
600
  // src/version.ts
601
- var MUSHI_CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
601
+ var MUSHI_CLI_VERSION = true ? "0.14.0" : "0.0.0-dev";
602
602
 
603
603
  // src/init.ts
604
604
  var ENV_FILES = [".env.local", ".env"];
@@ -1430,10 +1430,11 @@ var IngestSetupHttpError = class extends Error {
1430
1430
  };
1431
1431
  var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
1432
1432
  async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
1433
+ const safeKey = config.apiKey.replace(/[\r\n]/g, "");
1433
1434
  const res = await doFetch(`${config.endpoint}/v1/sync/ingest-setup`, {
1434
1435
  headers: {
1435
- Authorization: `Bearer ${config.apiKey}`,
1436
- "X-Mushi-Api-Key": config.apiKey,
1436
+ Authorization: `Bearer ${safeKey}`,
1437
+ "X-Mushi-Api-Key": safeKey,
1437
1438
  ...config.projectId ? { "X-Mushi-Project": config.projectId } : {}
1438
1439
  },
1439
1440
  signal: AbortSignal.timeout(8e3)
@@ -1575,7 +1576,7 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
1575
1576
  return serverChecks.map((sc) => ({
1576
1577
  name: `[server] ${sc.label}`,
1577
1578
  ok: sc.ready,
1578
- detail: sc.hint
1579
+ detail: sc.ready ? "" : sc.hint
1579
1580
  }));
1580
1581
  }
1581
1582
  const text2 = await res.text().catch(() => "");
@@ -1613,7 +1614,7 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
1613
1614
  const checks = steps.filter((s) => s.required).map((s) => ({
1614
1615
  name: `[ingest] ${s.label}`,
1615
1616
  ok: s.complete,
1616
- detail: s.hint ?? ""
1617
+ detail: s.complete ? "" : s.hint ?? ""
1617
1618
  }));
1618
1619
  const diag = data.diagnostic;
1619
1620
  if (diag?.last_sdk_seen_at) {
@@ -1629,6 +1630,94 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
1629
1630
  return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
1630
1631
  }
1631
1632
  }
1633
+ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
1634
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
1635
+ return [
1636
+ {
1637
+ name: "QA stories health",
1638
+ ok: false,
1639
+ detail: "Need projectId, apiKey, and endpoint for QA story checks."
1640
+ }
1641
+ ];
1642
+ }
1643
+ const checks = [];
1644
+ try {
1645
+ const storiesRes = await doFetch(
1646
+ `${config.endpoint}/v1/admin/projects/${config.projectId}/qa-coverage`,
1647
+ {
1648
+ headers: {
1649
+ Authorization: `Bearer ${config.apiKey}`,
1650
+ "X-Mushi-Api-Key": config.apiKey,
1651
+ "X-Mushi-Project": config.projectId
1652
+ },
1653
+ signal: AbortSignal.timeout(8e3)
1654
+ }
1655
+ );
1656
+ if (!storiesRes.ok) {
1657
+ checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
1658
+ return checks;
1659
+ }
1660
+ const storiesBody = await storiesRes.json();
1661
+ const stories2 = storiesBody.data?.coverage ?? [];
1662
+ const enabled = stories2.filter((s) => s.enabled);
1663
+ if (enabled.length === 0) {
1664
+ checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
1665
+ return checks;
1666
+ }
1667
+ checks.push({
1668
+ name: "[qa] Enabled QA stories",
1669
+ ok: true,
1670
+ detail: `${enabled.length} enabled story/stories configured`
1671
+ });
1672
+ const slackRes = await doFetch(
1673
+ `${config.endpoint}/v1/admin/projects/${config.projectId}/integrations/probe/slack`,
1674
+ {
1675
+ method: "POST",
1676
+ headers: {
1677
+ Authorization: `Bearer ${config.apiKey}`,
1678
+ "X-Mushi-Api-Key": config.apiKey,
1679
+ "X-Mushi-Project": config.projectId
1680
+ },
1681
+ signal: AbortSignal.timeout(6e3)
1682
+ }
1683
+ );
1684
+ const slackBody = slackRes.ok ? await slackRes.json() : null;
1685
+ const slackOk = slackBody?.status === "ok";
1686
+ checks.push({
1687
+ name: "[qa] Slack notifications configured",
1688
+ ok: slackOk,
1689
+ 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."
1690
+ });
1691
+ const fcRes = await doFetch(
1692
+ `${config.endpoint}/v1/admin/projects/${config.projectId}/integrations/probe/firecrawl`,
1693
+ {
1694
+ method: "POST",
1695
+ headers: {
1696
+ Authorization: `Bearer ${config.apiKey}`,
1697
+ "X-Mushi-Api-Key": config.apiKey,
1698
+ "X-Mushi-Project": config.projectId
1699
+ },
1700
+ signal: AbortSignal.timeout(6e3)
1701
+ }
1702
+ );
1703
+ const fcBody = fcRes.ok ? await fcRes.json() : null;
1704
+ const hasFirecrawlStories = enabled.some(
1705
+ (s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
1706
+ );
1707
+ if (hasFirecrawlStories) {
1708
+ const fcOk = fcBody?.status === "ok";
1709
+ checks.push({
1710
+ name: "[qa] Firecrawl API key configured",
1711
+ ok: fcOk,
1712
+ 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."
1713
+ });
1714
+ }
1715
+ } catch (err) {
1716
+ const msg = err instanceof Error ? err.message : String(err);
1717
+ checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
1718
+ }
1719
+ return checks;
1720
+ }
1632
1721
  async function runDoctor(config, options = {}) {
1633
1722
  const doFetch = options.fetch ?? globalThis.fetch;
1634
1723
  const checks = [];
@@ -1646,6 +1735,10 @@ async function runDoctor(config, options = {}) {
1646
1735
  const ingestChecks = await checkIngestSetup(config, doFetch);
1647
1736
  checks.push(...ingestChecks);
1648
1737
  }
1738
+ if (options.qaStories) {
1739
+ const qaChecks = await checkQaStoriesHealth(config, doFetch);
1740
+ checks.push(...qaChecks);
1741
+ }
1649
1742
  return { checks, ready: checks.every((c) => c.ok) };
1650
1743
  }
1651
1744
  function formatDoctorResult(result) {
@@ -1654,7 +1747,7 @@ function formatDoctorResult(result) {
1654
1747
  const lines = [];
1655
1748
  for (const c of result.checks) {
1656
1749
  lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
1657
- lines.push(` ${c.detail}`);
1750
+ if (c.detail) lines.push(` ${c.detail}`);
1658
1751
  }
1659
1752
  const failed = result.checks.filter((c) => !c.ok);
1660
1753
  if (failed.length === 0) {
@@ -1763,7 +1856,6 @@ async function runUpgrade(opts = {}) {
1763
1856
 
1764
1857
  // src/connect.ts
1765
1858
  import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
1766
- import { existsSync as existsSync5 } from "fs";
1767
1859
  import { join as join6, resolve as resolve3 } from "path";
1768
1860
  function envKeyPresent(content, key) {
1769
1861
  const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1774,11 +1866,15 @@ async function mergeEnvFile(path, lines) {
1774
1866
  # Mushi \u2014 added by mushi connect
1775
1867
  ${lines.join("\n")}
1776
1868
  `;
1777
- if (existsSync5(path)) {
1778
- const content = await readFile2(path, "utf8");
1869
+ let existing = null;
1870
+ try {
1871
+ existing = await readFile2(path, "utf8");
1872
+ } catch {
1873
+ }
1874
+ if (existing !== null) {
1779
1875
  const needs = lines.filter((line) => {
1780
1876
  const key = line.split("=")[0];
1781
- return !envKeyPresent(content, key);
1877
+ return !envKeyPresent(existing, key);
1782
1878
  });
1783
1879
  if (needs.length === 0) return false;
1784
1880
  await appendFile(path, `
@@ -1793,13 +1889,17 @@ ${needs.join("\n")}
1793
1889
  async function ensureMcpJsonGitignored(cwd, messages) {
1794
1890
  const gitignorePath = join6(cwd, ".gitignore");
1795
1891
  const patterns = [".cursor/mcp.json", ".cursor/"];
1796
- if (!existsSync5(gitignorePath)) {
1892
+ let content = null;
1893
+ try {
1894
+ content = await readFile2(gitignorePath, "utf8");
1895
+ } catch {
1896
+ }
1897
+ if (content === null) {
1797
1898
  messages.push(
1798
1899
  "\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
1799
1900
  );
1800
1901
  return;
1801
1902
  }
1802
- const content = await readFile2(gitignorePath, "utf8");
1803
1903
  const covered = patterns.some((p3) => content.split("\n").some((line) => line.trim() === p3 || line.trim() === `${p3}/`));
1804
1904
  if (covered) return;
1805
1905
  await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
@@ -1843,11 +1943,9 @@ async function runConnect(opts, baseConfig = {}) {
1843
1943
  }
1844
1944
  };
1845
1945
  let merged = { mcpServers: {} };
1846
- if (existsSync5(mcpPath)) {
1847
- try {
1848
- merged = JSON.parse(await readFile2(mcpPath, "utf8"));
1849
- } catch {
1850
- }
1946
+ try {
1947
+ merged = JSON.parse(await readFile2(mcpPath, "utf8"));
1948
+ } catch {
1851
1949
  }
1852
1950
  const servers = merged.mcpServers ?? {};
1853
1951
  servers[serverName] = mcpServerBlock;
@@ -2139,7 +2237,7 @@ Examples:
2139
2237
  mushi config endpoint https://... # set endpoint
2140
2238
  mushi config projectId <uuid> # set project`).action((key, value) => {
2141
2239
  const config = loadConfig();
2142
- const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
2240
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId", "consoleUrl"]);
2143
2241
  if (key && value) {
2144
2242
  if (!ALLOWED_KEYS.has(key)) {
2145
2243
  process.stderr.write(`error: unknown config key "${key}". Allowed: ${[...ALLOWED_KEYS].join(", ")}
@@ -2644,7 +2742,7 @@ Typical first-time flow:
2644
2742
  # CLI writes .env.local and .cursor/mcp.json
2645
2743
  # mushi whoami to confirm`).action(async (opts) => {
2646
2744
  const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
2647
- const { existsSync: existsSync6 } = await import("fs");
2745
+ const { existsSync: existsSync5 } = await import("fs");
2648
2746
  const nodePath = await import("path");
2649
2747
  const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
2650
2748
  const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
@@ -2691,7 +2789,7 @@ Typical first-time flow:
2691
2789
  `MUSHI_API_KEY=${apiKey}`,
2692
2790
  ""
2693
2791
  ];
2694
- const envExisting = existsSync6(envPath);
2792
+ const envExisting = existsSync5(envPath);
2695
2793
  await writeFile2(envPath, envLines.join("\n"), "utf8");
2696
2794
  console.log(`
2697
2795
  \u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
@@ -2711,7 +2809,7 @@ Typical first-time flow:
2711
2809
  }
2712
2810
  }
2713
2811
  };
2714
- const mcpExisting = existsSync6(mcpPath);
2812
+ const mcpExisting = existsSync5(mcpPath);
2715
2813
  if (mcpExisting) {
2716
2814
  try {
2717
2815
  const { readFile: readFile3 } = await import("fs/promises");
@@ -2745,7 +2843,7 @@ Supported IDEs:
2745
2843
 
2746
2844
  The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
2747
2845
  const { writeFile: writeFile2, mkdir: mkdir2, readFile: readFile3 } = await import("fs/promises");
2748
- const { existsSync: existsSync6 } = await import("fs");
2846
+ const { existsSync: existsSync5 } = await import("fs");
2749
2847
  const nodePath = await import("path");
2750
2848
  const os = await import("os");
2751
2849
  const config = requireConfig({ needsProject: true });
@@ -2777,7 +2875,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2777
2875
  const configPath = nodePath.join(configDir, ideEntry.file);
2778
2876
  if (ideEntry.format === "mcp-json") {
2779
2877
  let merged = { mcpServers: {} };
2780
- if (existsSync6(configPath)) {
2878
+ if (existsSync5(configPath)) {
2781
2879
  try {
2782
2880
  const raw = await readFile3(configPath, "utf8");
2783
2881
  merged = JSON.parse(raw);
@@ -2798,7 +2896,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2798
2896
  }
2799
2897
  } else if (ideEntry.format === "zed") {
2800
2898
  let settings = {};
2801
- if (existsSync6(configPath)) {
2899
+ if (existsSync5(configPath)) {
2802
2900
  try {
2803
2901
  const raw = await readFile3(configPath, "utf8");
2804
2902
  settings = JSON.parse(raw);
@@ -3059,9 +3157,12 @@ program.command("doctor").description(
3059
3157
  ).option(
3060
3158
  "--ingest",
3061
3159
  "Also call GET /v1/sync/ingest-setup for the 4 required ingest steps (API key, SDK heartbeat, first report). Composable with --server."
3160
+ ).option(
3161
+ "--qa-stories",
3162
+ "Check enabled QA stories for common setup issues: missing Firecrawl key, missing target URL, Slack not connected. Requires --server credentials."
3062
3163
  ).action(async (opts) => {
3063
3164
  const config = loadConfig();
3064
- const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest });
3165
+ const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories });
3065
3166
  const { checks } = result;
3066
3167
  if (opts.json) {
3067
3168
  console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
@@ -3373,6 +3474,373 @@ keys.command("add").description("Add a new API key to the pool").requiredOption(
3373
3474
  }
3374
3475
  console.log(`\u2713 Key added \u2014 id: ${res.data.id}`);
3375
3476
  });
3477
+ var integrations = program.command("integrations").description("Manage service integrations");
3478
+ integrations.command("list").description("List all configured integrations and their current health status").option("--json", "Machine-readable output").action(async (opts) => {
3479
+ const config = loadConfig();
3480
+ if (!config.apiKey) {
3481
+ console.error("Run `mushi login` first");
3482
+ process.exit(2);
3483
+ }
3484
+ if (!config.projectId) {
3485
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3486
+ process.exit(2);
3487
+ }
3488
+ const result = await apiCall(
3489
+ `/v1/admin/projects/${config.projectId}/integrations`,
3490
+ config
3491
+ );
3492
+ const rawResult = result;
3493
+ if (!rawResult.integrations && !result.ok) {
3494
+ console.error("Failed:", result.error);
3495
+ process.exit(1);
3496
+ }
3497
+ if (opts.json) {
3498
+ console.log(JSON.stringify(rawResult, null, 2));
3499
+ return;
3500
+ }
3501
+ const rows = rawResult.integrations ?? [];
3502
+ if (rows.length === 0) {
3503
+ console.log("No integrations configured. Visit the Integrations page to connect services.");
3504
+ return;
3505
+ }
3506
+ const icons = {
3507
+ slack: "\u{1F514}",
3508
+ github: "\u{1F419}",
3509
+ sentry: "\u{1FAB2}",
3510
+ langfuse: "\u{1F52D}",
3511
+ discord: "\u{1F4AC}",
3512
+ linear: "\u{1F4D0}",
3513
+ jira: "\u{1F5C2}\uFE0F",
3514
+ cursor_cloud: "\u{1F5B1}\uFE0F",
3515
+ claude_code_agent: "\u{1F916}"
3516
+ };
3517
+ console.log("\nIntegrations:\n");
3518
+ for (const row of rows) {
3519
+ const icon = icons[row.kind] ?? "\u{1F50C}";
3520
+ const statusIcon = row.status === "ok" ? "\u2705" : row.status === "error" ? "\u274C" : "\u26AA";
3521
+ console.log(` ${icon} ${row.kind.padEnd(20)} ${statusIcon} ${row.detail ?? ""}`);
3522
+ }
3523
+ console.log();
3524
+ });
3525
+ integrations.command("test <kind>").description(
3526
+ "Run a health probe for a specific integration (e.g. slack, sentry, github, langfuse, discord, cursor_cloud, claude_code_agent)"
3527
+ ).option("--json", "Machine-readable output").action(async (kind, opts) => {
3528
+ const config = loadConfig();
3529
+ if (!config.apiKey) {
3530
+ console.error("Run `mushi login` first");
3531
+ process.exit(2);
3532
+ }
3533
+ if (!config.projectId) {
3534
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3535
+ process.exit(2);
3536
+ }
3537
+ const result = await apiCall(
3538
+ `/v1/admin/projects/${config.projectId}/integrations/probe/${kind}`,
3539
+ config,
3540
+ { method: "POST" }
3541
+ );
3542
+ if (!result.ok) {
3543
+ console.error("Request failed:", result.error);
3544
+ process.exit(1);
3545
+ }
3546
+ if (opts.json) {
3547
+ console.log(JSON.stringify(result.data, null, 2));
3548
+ return;
3549
+ }
3550
+ const probeOk = result.data?.status === "ok";
3551
+ console.log(
3552
+ probeOk ? `\u2705 ${kind} integration is healthy${result.data.detail ? ": " + result.data.detail : ""}` : `\u274C ${kind} integration check failed${result.data.detail ? ": " + result.data.detail : ""}`
3553
+ );
3554
+ if (!probeOk) process.exit(1);
3555
+ });
3556
+ var slack = program.command("slack").description("Slack integration commands");
3557
+ slack.command("status").description("Show whether Slack is connected and which channel receives notifications").option("--json", "Machine-readable output").action(async (opts) => {
3558
+ const config = loadConfig();
3559
+ if (!config.apiKey) {
3560
+ console.error("Run `mushi login` first");
3561
+ process.exit(2);
3562
+ }
3563
+ if (!config.projectId) {
3564
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3565
+ process.exit(2);
3566
+ }
3567
+ const result = await apiCall(
3568
+ `/v1/admin/projects/${config.projectId}/integrations/probe/slack`,
3569
+ config,
3570
+ { method: "POST" }
3571
+ );
3572
+ if (!result.ok) {
3573
+ console.error("Request failed:", result.error);
3574
+ process.exit(1);
3575
+ }
3576
+ if (opts.json) {
3577
+ console.log(JSON.stringify(result.data, null, 2));
3578
+ return;
3579
+ }
3580
+ if (result.data?.status === "ok") {
3581
+ console.log("\u2705 Slack connected");
3582
+ if (result.data.detail) console.log(` ${result.data.detail}`);
3583
+ console.log("\n To change the channel or notification prefs, visit /integrations in the Mushi console.");
3584
+ } else {
3585
+ console.log("\u26AA Slack not connected");
3586
+ console.log(' Visit /integrations in the Mushi console and click "Add to Slack".');
3587
+ }
3588
+ });
3589
+ slack.command("test").description("Send a test Slack notification to confirm the current channel is working").option("--json", "Machine-readable output").action(async (opts) => {
3590
+ const config = loadConfig();
3591
+ if (!config.apiKey) {
3592
+ console.error("Run `mushi login` first");
3593
+ process.exit(2);
3594
+ }
3595
+ if (!config.projectId) {
3596
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3597
+ process.exit(2);
3598
+ }
3599
+ const result = await apiCall(
3600
+ `/v1/admin/projects/${config.projectId}/integrations/slack/test`,
3601
+ config,
3602
+ { method: "POST" }
3603
+ );
3604
+ if (!result.ok) {
3605
+ console.error("Request failed:", result.error);
3606
+ process.exit(1);
3607
+ }
3608
+ if (opts.json) {
3609
+ console.log(JSON.stringify(result.data, null, 2));
3610
+ return;
3611
+ }
3612
+ if (result.data?.ok) {
3613
+ console.log("\u2705 Test message sent! Check your Slack channel.");
3614
+ } else {
3615
+ console.error("\u274C Test failed:", result.data?.error ?? "unknown error");
3616
+ process.exit(1);
3617
+ }
3618
+ });
3619
+ var qa = program.command("qa").description("QA story management");
3620
+ qa.command("stories").description("List QA stories for the current project").option("--json", "Machine-readable output").option("-n, --limit <n>", "Max stories to return (not applied server-side; all stories returned)", "20").action(async (opts) => {
3621
+ const config = loadConfig();
3622
+ if (!config.apiKey) {
3623
+ console.error("Run `mushi login` first");
3624
+ process.exit(2);
3625
+ }
3626
+ if (!config.projectId) {
3627
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3628
+ process.exit(2);
3629
+ }
3630
+ const result = await apiCall(
3631
+ `/v1/admin/projects/${config.projectId}/qa-coverage`,
3632
+ config
3633
+ );
3634
+ if (!result.ok) {
3635
+ console.error("Failed:", result.error);
3636
+ process.exit(1);
3637
+ }
3638
+ if (opts.json) {
3639
+ console.log(JSON.stringify(result.data, null, 2));
3640
+ return;
3641
+ }
3642
+ const stories2 = result.data?.coverage ?? [];
3643
+ if (stories2.length === 0) {
3644
+ console.log("No QA stories yet. Create one at /qa-coverage in the Mushi console.");
3645
+ return;
3646
+ }
3647
+ console.log(`
3648
+ QA Stories (${stories2.length}):
3649
+ `);
3650
+ for (const s of stories2) {
3651
+ const statusIcon = s.last_run_status === "passed" ? "\u2705" : s.last_run_status === "failed" ? "\u274C" : s.last_run_status === "error" ? "\u{1F6A8}" : "\u26AA";
3652
+ const enabled = s.enabled ? "" : " [disabled]";
3653
+ const sid = s.story_id ?? s.id ?? "\u2014";
3654
+ console.log(` ${statusIcon} ${s.name.slice(0, 50).padEnd(52)} ${sid}${enabled}`);
3655
+ }
3656
+ console.log(`
3657
+ Use 'mushi qa runs <storyId>' to see recent runs for a story.`);
3658
+ console.log();
3659
+ });
3660
+ qa.command("runs <storyId>").description("Show recent runs for a QA story, including error heads").option("--json", "Machine-readable output").option("-n, --limit <n>", "Max runs to return", "10").action(async (storyId, opts) => {
3661
+ const config = loadConfig();
3662
+ if (!config.apiKey) {
3663
+ console.error("Run `mushi login` first");
3664
+ process.exit(2);
3665
+ }
3666
+ if (!config.projectId) {
3667
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3668
+ process.exit(2);
3669
+ }
3670
+ const limit = parseInt(opts.limit ?? "10", 10);
3671
+ const result = await apiCall(
3672
+ `/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/runs?limit=${limit}`,
3673
+ config
3674
+ );
3675
+ if (!result.ok) {
3676
+ console.error("Failed:", result.error);
3677
+ process.exit(1);
3678
+ }
3679
+ if (opts.json) {
3680
+ console.log(JSON.stringify(result.data, null, 2));
3681
+ return;
3682
+ }
3683
+ const runs = result.data?.runs ?? [];
3684
+ if (runs.length === 0) {
3685
+ console.log("No runs yet for this story. Trigger one with `mushi qa run <storyId>`.");
3686
+ return;
3687
+ }
3688
+ console.log(`
3689
+ Recent runs for story ${storyId.slice(0, 8)}\u2026:
3690
+ `);
3691
+ for (const r of runs) {
3692
+ const statusIcon = r.status === "passed" ? "\u2705" : r.status === "failed" ? "\u274C" : r.status === "error" ? "\u{1F6A8}" : "\u23F3";
3693
+ const ts = r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace("T", " ") : "\u2014";
3694
+ const latency = r.latency_ms ? ` (${(r.latency_ms / 1e3).toFixed(1)}s)` : "";
3695
+ console.log(` ${statusIcon} ${ts}${latency} ${r.id.slice(0, 8)}`);
3696
+ if (r.error_message) {
3697
+ console.log(` Error: ${r.error_message.slice(0, 120)}`);
3698
+ }
3699
+ if (r.assertion_failures?.length) {
3700
+ for (const af of r.assertion_failures.slice(0, 3)) {
3701
+ console.log(` \xB7 ${String(af).slice(0, 100)}`);
3702
+ }
3703
+ }
3704
+ }
3705
+ const consoleUrl = config.consoleUrl ?? "https://app.mushi.ai";
3706
+ console.log(`
3707
+ Open in console: ${consoleUrl}/qa-coverage?story=${storyId}`);
3708
+ console.log(` Tip: run 'mushi config consoleUrl http://localhost:6464' to set your local console URL`);
3709
+ console.log();
3710
+ });
3711
+ qa.command("run <storyId>").description("Manually trigger a QA story run (fire-and-forget; check results with `mushi qa runs <id>`)").option("--json", "Machine-readable output").action(async (storyId, opts) => {
3712
+ const config = loadConfig();
3713
+ if (!config.apiKey) {
3714
+ console.error("Run `mushi login` first");
3715
+ process.exit(2);
3716
+ }
3717
+ if (!config.projectId) {
3718
+ console.error("No projectId. Run `mushi config projectId <uuid>`");
3719
+ process.exit(2);
3720
+ }
3721
+ const result = await apiCall(
3722
+ `/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/run`,
3723
+ config,
3724
+ { method: "POST" }
3725
+ );
3726
+ if (!result.ok) {
3727
+ console.error("Failed:", result.error);
3728
+ process.exit(1);
3729
+ }
3730
+ if (opts.json) {
3731
+ console.log(JSON.stringify(result.data, null, 2));
3732
+ return;
3733
+ }
3734
+ const runId = result.data?.run_id;
3735
+ if (runId) {
3736
+ console.log(`\u25B6 Run triggered: ${runId.slice(0, 8)}\u2026`);
3737
+ console.log(` Check results: mushi qa runs ${storyId}`);
3738
+ } else {
3739
+ console.error("\u274C Trigger failed: no run_id in response", JSON.stringify(result.data));
3740
+ process.exit(1);
3741
+ }
3742
+ });
3743
+ program.command("audit").description("Run a full-stack health audit for the current project").option("--json", "Machine-readable JSON output").option("--project-id <id>", "Project ID to audit (defaults to MUSHI_PROJECT_ID from config)").addHelpText("after", `
3744
+ Description:
3745
+ Fans out to the Mushi backend to run a full-stack health audit:
3746
+ \u2022 DB schema + Supabase advisors (requires Supabase PAT in API Keys)
3747
+ \u2022 Recent backend error logs
3748
+ \u2022 Tables without RLS enabled
3749
+ \u2022 Gate results: API contract (G3), spec drift (G6), orphan endpoints (G7),
3750
+ unknown frontend calls (G8), schema drift, status claim (G5)
3751
+
3752
+ Returns a PM-readable scorecard with severity-ranked findings.
3753
+
3754
+ Prerequisites:
3755
+ 1. Configure your Supabase PAT: mushi settings set supabase-pat <token>
3756
+ 2. Set supabase_project_ref in Admin \u2192 Settings \u2192 Project.
3757
+
3758
+ Examples:
3759
+ mushi audit
3760
+ mushi audit --json
3761
+ mushi audit --project-id abc123`).action(async (opts) => {
3762
+ const config = requireConfig();
3763
+ const projectId = opts.projectId ?? config.projectId;
3764
+ if (!projectId) {
3765
+ process.stderr.write("error: project ID required. Run `mushi login` or pass --project-id\n");
3766
+ process.exit(1);
3767
+ }
3768
+ const headers = {
3769
+ "Content-Type": "application/json",
3770
+ "X-Mushi-Project-Id": projectId
3771
+ };
3772
+ const jwt = config.jwt ?? null;
3773
+ const apiKey = config.apiKey ?? null;
3774
+ if (jwt) {
3775
+ headers["Authorization"] = `Bearer ${jwt}`;
3776
+ } else if (apiKey) {
3777
+ headers["X-Mushi-Api-Key"] = apiKey;
3778
+ } else {
3779
+ process.stderr.write("error: no credentials found. Run `mushi login` first.\n");
3780
+ process.exit(1);
3781
+ }
3782
+ if (!opts.json) process.stdout.write("Running full-stack audit\u2026 ");
3783
+ try {
3784
+ const controller = new AbortController();
3785
+ const timer = setTimeout(() => controller.abort(), 3e4);
3786
+ const res = await fetch(
3787
+ `${config.endpoint}/v1/admin/projects/${projectId}/audit`,
3788
+ { method: "POST", headers, body: "{}", signal: controller.signal }
3789
+ );
3790
+ clearTimeout(timer);
3791
+ const body = await res.json();
3792
+ if (!res.ok || !body.ok) {
3793
+ if (opts.json) {
3794
+ console.log(JSON.stringify(body));
3795
+ process.exit(1);
3796
+ }
3797
+ process.stdout.write("FAIL\n");
3798
+ process.stderr.write(`error: ${body.error?.message ?? `HTTP ${res.status}`}
3799
+ `);
3800
+ process.exit(1);
3801
+ }
3802
+ if (opts.json) {
3803
+ console.log(JSON.stringify(body.data, null, 2));
3804
+ return;
3805
+ }
3806
+ const data = body.data;
3807
+ const overallGlyph = data.summary.overall === "fail" ? "\u274C" : data.summary.overall === "warn" ? "\u26A0\uFE0F " : "\u2705";
3808
+ process.stdout.write(`${overallGlyph}
3809
+
3810
+ `);
3811
+ console.log(`Full-Stack Audit \u2014 ${new Date(data.audit_at).toLocaleString()}`);
3812
+ console.log(`Backend linked: ${data.backend_linked ? "yes" : "no (configure Supabase PAT + project ref)"}`);
3813
+ console.log(`Summary: ${data.summary.error_count} error(s) \xB7 ${data.summary.warn_count} warning(s)
3814
+ `);
3815
+ if (data.findings.length === 0) {
3816
+ console.log(" \u2713 No findings. Your project looks healthy.");
3817
+ } else {
3818
+ for (const f of data.findings) {
3819
+ const icon = f.severity === "error" ? "\u{1F534}" : f.severity === "warn" ? "\u{1F7E1}" : "\u2139\uFE0F ";
3820
+ console.log(` ${icon} ${f.title}`);
3821
+ console.log(` ${f.detail.slice(0, 120)}${f.detail.length > 120 ? "\u2026" : ""}`);
3822
+ }
3823
+ }
3824
+ if (data.gate_runs.length > 0) {
3825
+ console.log("\nGate Results:");
3826
+ for (const run of data.gate_runs) {
3827
+ const g = run.status === "pass" ? "\u2713" : run.status === "fail" ? "\u2717" : "~";
3828
+ console.log(` ${g} ${run.gate.padEnd(22)} ${run.status} (${run.findings_count} finding${run.findings_count !== 1 ? "s" : ""})`);
3829
+ }
3830
+ }
3831
+ if (data.summary.overall === "fail") process.exit(1);
3832
+ } catch (err) {
3833
+ const msg = err instanceof Error ? err.message : String(err);
3834
+ if (opts.json) {
3835
+ console.log(JSON.stringify({ ok: false, error: msg }));
3836
+ } else {
3837
+ process.stdout.write("ERROR\n");
3838
+ process.stderr.write(`error: ${msg}
3839
+ `);
3840
+ }
3841
+ process.exit(1);
3842
+ }
3843
+ });
3376
3844
  program.parseAsync().catch((err) => {
3377
3845
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
3378
3846
  process.exit(1);
package/dist/init.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-NYPX5KXR.js";
9
9
  import {
10
10
  MUSHI_CLI_VERSION
11
- } from "./chunk-IMDLL4EO.js";
11
+ } from "./chunk-4W6VOIQT.js";
12
12
 
13
13
  // src/init.ts
14
14
  import * as p from "@clack/prompts";
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-IMDLL4EO.js";
3
+ } from "./chunk-4W6VOIQT.js";
4
4
  export {
5
5
  MUSHI_CLI_VERSION
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mushi-mushi/cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "license": "MIT",
5
5
  "description": "CLI for Mushi Mushi — `mushi init` wizard installs the right SDK for your framework, plus report triage and pipeline health commands",
6
6
  "bin": {
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var MUSHI_CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
3
-
4
- export {
5
- MUSHI_CLI_VERSION
6
- };