@mushi-mushi/cli 0.26.1 → 0.27.1

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
@@ -17,10 +17,15 @@ import {
17
17
  fmtDate,
18
18
  installCommand,
19
19
  installSignalHandlers,
20
+ listProfiles,
20
21
  loadConfig,
22
+ maybeShowTelemetryNotice,
21
23
  openInBrowser,
24
+ outputIsJson,
22
25
  pad,
23
26
  printAndExit,
27
+ printKeyExportHint,
28
+ printResult,
24
29
  probeEndpointHealth,
25
30
  projectIdHint,
26
31
  readPackageJson,
@@ -29,24 +34,29 @@ import {
29
34
  requireUuid,
30
35
  resolveConsoleUrl,
31
36
  resolveConsoleUrlSync,
37
+ resolveProfileName,
32
38
  runConnect,
33
39
  sanitizeApiKey,
34
40
  sanitizeCliCredentials,
35
41
  sanitizeEndpoint,
36
42
  sanitizeProjectId,
37
43
  saveConfig,
44
+ setActiveProfile,
45
+ setGlobalOutputFormat,
46
+ trySaveKeyToKeychain,
38
47
  writeMcpServerEntry
39
- } from "./chunk-2ZLYNNA3.js";
48
+ } from "./chunk-NNB2NONG.js";
40
49
  import {
41
50
  MUSHI_CLI_VERSION,
42
51
  MUSHI_MCP_PIN_SPEC
43
- } from "./chunk-6XYPFV23.js";
52
+ } from "./chunk-DBAEB56N.js";
44
53
  import {
45
54
  TEST_REPORT_FETCH_TIMEOUT_MS,
46
55
  assertEndpoint,
47
56
  normalizeEndpoint,
48
57
  resolveCloudEndpoint
49
58
  } from "./chunk-LDPFTQLE.js";
59
+ import "./chunk-PDX44BCA.js";
50
60
 
51
61
  // src/index.ts
52
62
  import { Command } from "commander";
@@ -272,6 +282,38 @@ async function mintProjectKey(endpoint, cliToken, projectId, opts = {}) {
272
282
  res.status
273
283
  );
274
284
  }
285
+ async function revokeProjectKey(endpoint, cliToken, projectId, keyPrefix, newKeyId) {
286
+ const base = trimTrailingSlash(endpoint);
287
+ const url = `${base}/v1/cli/projects/${encodeURIComponent(projectId)}/keys/${encodeURIComponent(keyPrefix)}/revoke`;
288
+ let res;
289
+ try {
290
+ res = await deviceFetch(url, {
291
+ method: "DELETE",
292
+ headers: {
293
+ ...cliAuthHeaders(cliToken),
294
+ "Content-Type": "application/json"
295
+ },
296
+ body: newKeyId ? JSON.stringify({ newKeyId }) : void 0
297
+ });
298
+ } catch (err) {
299
+ throw new DeviceAuthRequestError(
300
+ "revoke_key",
301
+ `Could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`
302
+ );
303
+ }
304
+ const json = await res.json().catch(() => null);
305
+ if (res.ok && json?.ok) {
306
+ return {
307
+ revoked: json.revoked ?? 0,
308
+ revokedAt: json.revokedAt
309
+ };
310
+ }
311
+ throw new DeviceAuthRequestError(
312
+ "revoke_key",
313
+ json?.error?.message ?? `Could not revoke key (HTTP ${res.status}).`,
314
+ res.status
315
+ );
316
+ }
275
317
 
276
318
  // src/freshness.ts
277
319
  var REGISTRY = "https://registry.npmjs.org";
@@ -861,6 +903,71 @@ async function checkHostAppWiring(cwd) {
861
903
  }
862
904
  return checks;
863
905
  }
906
+ var HASH_ROUTER_PATTERNS = [
907
+ /\bHashRouter\b/,
908
+ /\bcreateHashRouter\b/,
909
+ /location\.hash\b/,
910
+ /addEventListener\(['"]hashchange['"]/
911
+ ];
912
+ var HASH_TEMPLATE_PATTERN = /['"`]#\//;
913
+ async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
914
+ const { readdir: readdir2, readFile: readFile3 } = await import("fs/promises");
915
+ const { join: join4 } = await import("path");
916
+ let count = 0;
917
+ async function walk(d, depth) {
918
+ if (depth > maxDepth || count > maxFiles) return null;
919
+ let entries = [];
920
+ try {
921
+ entries = await readdir2(d, { withFileTypes: true });
922
+ } catch {
923
+ return null;
924
+ }
925
+ for (const e of entries) {
926
+ if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue;
927
+ const full = join4(d, e.name);
928
+ if (e.isDirectory()) {
929
+ const found = await walk(full, depth + 1);
930
+ if (found) return found;
931
+ } else if (/\.[cm]?[jt]sx?$/.test(e.name)) {
932
+ count++;
933
+ try {
934
+ const src = await readFile3(full, "utf8");
935
+ if (patterns.some((p3) => p3.test(src))) return full;
936
+ } catch {
937
+ }
938
+ }
939
+ }
940
+ return null;
941
+ }
942
+ const { resolve: resolve3 } = await import("path");
943
+ for (const sub of ["src", "app", "pages", "lib", "components"]) {
944
+ const found = await walk(join4(resolve3(dir), sub), 0);
945
+ if (found) return found;
946
+ }
947
+ return walk(resolve3(dir), 0);
948
+ }
949
+ async function checkHashRouterCapture(cwd) {
950
+ const hashFile = await scanDirForPattern(cwd, HASH_ROUTER_PATTERNS);
951
+ if (!hashFile) return [];
952
+ const templateFile = await scanDirForPattern(cwd, [HASH_TEMPLATE_PATTERN]);
953
+ if (templateFile) {
954
+ return [
955
+ {
956
+ name: "[host] Hash-router route capture",
957
+ ok: true,
958
+ detail: `Hash-router detected (${hashFile}) and /#/ route templates found in Mushi config \u2014 routes will be captured correctly.`
959
+ }
960
+ ];
961
+ }
962
+ return [
963
+ {
964
+ name: "[host] Hash-router route capture",
965
+ ok: true,
966
+ warn: true,
967
+ detail: `Hash-router detected (${hashFile}) but no /#/ route templates found in your Mushi init call. Add discoverInventory.routeTemplates with /#/-prefixed patterns (e.g. ['/#/article/[slug]']) so Mushi captures parametric hash routes correctly \u2014 without these, all hash navigations appear as a single "/#/" route in the inventory.`
968
+ }
969
+ ];
970
+ }
864
971
  async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
865
972
  const checks = [];
866
973
  const { readFile: readFile3 } = await import("fs/promises");
@@ -1096,7 +1203,7 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
1096
1203
  }
1097
1204
  async function checkVersionDrift(cwd, doFetch) {
1098
1205
  const checks = [];
1099
- const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-5DCYWDRK.js");
1206
+ const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-AYMUW3UQ.js");
1100
1207
  if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
1101
1208
  const parse2 = (v) => {
1102
1209
  const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
@@ -1201,7 +1308,7 @@ async function runDoctor(config, options = {}) {
1201
1308
  const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
1202
1309
  if (sdkCheck) checks.push(sdkCheck);
1203
1310
  if (sdkCheck?.ok && !sdkCheck.warn) {
1204
- const { findSdkImport } = await import("./snippet-inject-LPRWTQDE.js");
1311
+ const { findSdkImport } = await import("./snippet-inject-CCXILWYZ.js");
1205
1312
  const imported = await findSdkImport(options.cwd ?? process.cwd());
1206
1313
  checks.push(
1207
1314
  imported ? { name: "SDK init snippet wired", ok: true, detail: `Found SDK usage in ${imported.file}` } : {
@@ -1229,6 +1336,8 @@ async function runDoctor(config, options = {}) {
1229
1336
  if (isFull || options.hostApp) {
1230
1337
  const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
1231
1338
  checks.push(...hostChecks);
1339
+ const hashChecks = await checkHashRouterCapture(options.cwd ?? process.cwd());
1340
+ checks.push(...hashChecks);
1232
1341
  }
1233
1342
  if (isFull || options.mcp) {
1234
1343
  const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
@@ -1413,6 +1522,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
1413
1522
  };
1414
1523
  }
1415
1524
  const existing = loadConfig();
1525
+ maybeShowTelemetryNotice(existing);
1416
1526
  if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
1417
1527
  const reuse = options.yes ? true : await p.confirm({
1418
1528
  message: "Use the Mushi credentials saved from your last sign-in?",
@@ -1803,10 +1913,10 @@ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
1803
1913
  }
1804
1914
  async function maybeInjectSnippet(cwd, framework, options) {
1805
1915
  if (options.yes || !process.stdin.isTTY) return;
1806
- const { ENTRY_CANDIDATES, injectSnippet, MUSHI_MARKER_START } = await import("./snippet-inject-LPRWTQDE.js");
1916
+ const { ENTRY_CANDIDATES, injectSnippet, MUSHI_MARKER_START } = await import("./snippet-inject-CCXILWYZ.js");
1807
1917
  const candidates = ENTRY_CANDIDATES[framework.id];
1808
1918
  if (!candidates) return;
1809
- const { readFile: readFile3, writeFile: writeFile2 } = await import("fs/promises");
1919
+ const { readFile: readFile3, writeFile: writeFile3 } = await import("fs/promises");
1810
1920
  const nodePath2 = await import("path");
1811
1921
  for (const rel of candidates) {
1812
1922
  const abs = nodePath2.join(cwd, rel);
@@ -1821,7 +1931,7 @@ async function maybeInjectSnippet(cwd, framework, options) {
1821
1931
  message: alreadyInjected ? `Refresh the Mushi init block in ${rel}?` : `Add the Mushi init snippet to ${rel}? (wrapped in markers; re-runs update in place)`
1822
1932
  });
1823
1933
  if (p.isCancel(confirmed) || !confirmed) return;
1824
- await writeFile2(abs, injectSnippet(source, framework.snippet()), "utf8");
1934
+ await writeFile3(abs, injectSnippet(source, framework.snippet()), "utf8");
1825
1935
  p.log.success(`${alreadyInjected ? "Updated" : "Added"} Mushi init block in ${rel}`);
1826
1936
  return;
1827
1937
  }
@@ -1859,7 +1969,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
1859
1969
  if (p.isCancel(answer) || !answer) return;
1860
1970
  const endpoint = resolveCloudEndpoint(options.endpoint);
1861
1971
  try {
1862
- const { runConnect: runConnect2 } = await import("./connect-2LCLLN7R.js");
1972
+ const { runConnect: runConnect2 } = await import("./connect-TWO2ZNFB.js");
1863
1973
  await runConnect2({
1864
1974
  apiKey: credentials.apiKey,
1865
1975
  projectId: credentials.projectId,
@@ -2058,7 +2168,7 @@ async function resolveProjectChoice(projectsList, ask) {
2058
2168
  return { kind: "create", name: newName };
2059
2169
  }
2060
2170
  async function runLogin(opts = {}) {
2061
- const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
2171
+ const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-TEBF4IH6.js");
2062
2172
  const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
2063
2173
  const consoleBase = await resolveConsoleUrl();
2064
2174
  let apiKey = opts.apiKey;
@@ -2180,6 +2290,10 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
2180
2290
  if (chosenProjectId) config.projectId = chosenProjectId;
2181
2291
  if (apiKey) config.apiKey = apiKey;
2182
2292
  saveConfig(config);
2293
+ if (apiKey) {
2294
+ const profile = resolveProfileName(void 0, config.activeProfile);
2295
+ trySaveKeyToKeychain(apiKey, profile);
2296
+ }
2183
2297
  console.log("");
2184
2298
  if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
2185
2299
  if (apiKey) {
@@ -2510,7 +2624,7 @@ function registerDeployCommands(program2) {
2510
2624
  // src/commands/reports.ts
2511
2625
  function registerReportsCommands(program2) {
2512
2626
  const reports = program2.command("reports").description("Manage bug reports");
2513
- reports.command("list").description("List recent reports for the current project").option("--limit <n>", "Max results (1\u2013100)", "20").option("--status <status>", "Filter by status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "Filter by severity: critical|high|medium|low").option("--search <query>", "Full-text search in summary and description").option("--json", "Machine-readable JSON output").addHelpText("after", `
2627
+ reports.command("list").description("List recent reports for the current project").option("--limit <n>", "Max results (1\u2013100)", "20").option("--status <status>", "Filter by status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "Filter by severity: critical|high|medium|low").option("--search <query>", "Full-text search in summary and description").option("--json", "Output as JSON (alias for -o json)").addHelpText("after", `
2514
2628
  Examples:
2515
2629
  mushi reports list
2516
2630
  mushi reports list --status new --severity critical
@@ -2523,29 +2637,28 @@ Examples:
2523
2637
  if (opts.search) params.set("search", opts.search);
2524
2638
  const result = await apiCall(`/v1/sync/reports?${params}`, config);
2525
2639
  if (!result.ok) die(result);
2526
- if (opts.json) {
2527
- console.log(JSON.stringify(result.data, null, 2));
2528
- return;
2529
- }
2530
- const rows = result.data.reports;
2531
- if (rows.length === 0) {
2532
- console.log("No reports found.");
2533
- return;
2534
- }
2535
- console.log(`${pad("ID", 38)} ${pad("SEV", 9)} ${pad("STATUS", 12)} ${pad("CREATED", 17)} SUMMARY`);
2536
- console.log("\u2500".repeat(110));
2537
- for (const r of rows) {
2538
- const sev = r.severity ?? "unset";
2539
- const status = r.status ?? "new";
2540
- const summary = (r.summary ?? r.description ?? "").slice(0, 50);
2541
- console.log(`${pad(r.id, 38)} ${pad(sev, 9)} ${pad(status, 12)} ${pad(fmtDate(r.created_at), 17)} ${summary}`);
2542
- }
2543
- if (result.data.total > rows.length) {
2544
- console.log(`
2545
- \u2026 ${result.data.total - rows.length} more. Use --limit to see more.`);
2546
- }
2640
+ printResult(result.data, {
2641
+ json: opts.json,
2642
+ render(d) {
2643
+ const rows = d.reports;
2644
+ if (rows.length === 0) {
2645
+ console.log("No reports found.");
2646
+ return;
2647
+ }
2648
+ console.log(`${pad("ID", 38)} ${pad("SEV", 9)} ${pad("STATUS", 12)} ${pad("CREATED", 17)} SUMMARY`);
2649
+ console.log("\u2500".repeat(110));
2650
+ for (const r of rows) {
2651
+ const sev = r.severity ?? "unset";
2652
+ const status = r.status ?? "new";
2653
+ const summary = (r.summary ?? r.description ?? "").slice(0, 50);
2654
+ console.log(`${pad(r.id, 38)} ${pad(sev, 9)} ${pad(status, 12)} ${pad(fmtDate(r.created_at), 17)} ${summary}`);
2655
+ }
2656
+ if (d.total > rows.length) console.log(`
2657
+ \u2026 ${d.total - rows.length} more. Use --limit to see more.`);
2658
+ }
2659
+ });
2547
2660
  });
2548
- reports.command("thread <id>").description("Show unified timeline for a report (comments, fixes, QA, pipelines)").option("--json", "Machine-readable JSON output").option("--watch", "Poll for new timeline entries (TTY only)").action(async (id, opts) => {
2661
+ reports.command("thread <id>").description("Show unified timeline for a report (comments, fixes, QA, pipelines)").option("--json", "Output as JSON (alias for -o json)").option("--watch", "Poll for new timeline entries (TTY only)").action(async (id, opts) => {
2549
2662
  const config = requireConfig();
2550
2663
  const fetchTimeline = async () => {
2551
2664
  const result = await apiCall(`/v1/sync/reports/${id}/timeline`, config);
@@ -2560,7 +2673,7 @@ Examples:
2560
2673
  return result.data;
2561
2674
  };
2562
2675
  const render = (data) => {
2563
- if (opts.json) {
2676
+ if (outputIsJson(opts.json)) {
2564
2677
  console.log(JSON.stringify(data, null, 2));
2565
2678
  return;
2566
2679
  }
@@ -2585,7 +2698,7 @@ Examples:
2585
2698
  }
2586
2699
  render(await fetchTimeline());
2587
2700
  });
2588
- reports.command("show <id>").description("Show full details for a single report").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
2701
+ reports.command("show <id>").description("Show full details for a single report").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
2589
2702
  const config = requireConfig();
2590
2703
  const result = await apiCall(`/v1/sync/reports/${id}`, config);
2591
2704
  if (!result.ok) {
@@ -2596,30 +2709,28 @@ Examples:
2596
2709
  }
2597
2710
  die(result);
2598
2711
  }
2599
- if (opts.json) {
2600
- console.log(JSON.stringify(result.data, null, 2));
2601
- return;
2602
- }
2603
- const r = result.data;
2604
- console.log(`Report: ${r.id}`);
2605
- console.log(` Status: ${r.status ?? "new"}`);
2606
- console.log(` Severity: ${r.severity ?? "unset"}`);
2607
- console.log(` Category: ${r.category ?? "\u2014"}`);
2608
- console.log(` Created: ${fmtDate(r.created_at)}`);
2609
- if (r.summary) console.log(` Summary: ${r.summary}`);
2610
- if (r.description) {
2611
- console.log(` Description:`);
2612
- console.log(` ${r.description.replace(/\n/g, "\n ")}`);
2613
- }
2614
- if (r.environment?.url) console.log(` URL: ${r.environment.url}`);
2615
- if (r.component) console.log(` Component: ${r.component}`);
2616
- if (r.sentry_event_id) console.log(` Sentry: ${r.sentry_event_id}`);
2617
- if (r.fix_id) console.log(` Fix: ${r.fix_id}`);
2618
- if (r.tags && Object.keys(r.tags).length > 0) {
2619
- console.log(` Tags: ${JSON.stringify(r.tags)}`);
2620
- }
2712
+ printResult(result.data, {
2713
+ json: opts.json,
2714
+ render(r) {
2715
+ console.log(`Report: ${r.id}`);
2716
+ console.log(` Status: ${r.status ?? "new"}`);
2717
+ console.log(` Severity: ${r.severity ?? "unset"}`);
2718
+ console.log(` Category: ${r.category ?? "\u2014"}`);
2719
+ console.log(` Created: ${fmtDate(r.created_at)}`);
2720
+ if (r.summary) console.log(` Summary: ${r.summary}`);
2721
+ if (r.description) {
2722
+ console.log(` Description:`);
2723
+ console.log(` ${r.description.replace(/\n/g, "\n ")}`);
2724
+ }
2725
+ if (r.environment?.url) console.log(` URL: ${r.environment.url}`);
2726
+ if (r.component) console.log(` Component: ${r.component}`);
2727
+ if (r.sentry_event_id) console.log(` Sentry: ${r.sentry_event_id}`);
2728
+ if (r.fix_id) console.log(` Fix: ${r.fix_id}`);
2729
+ if (r.tags && Object.keys(r.tags).length > 0) console.log(` Tags: ${JSON.stringify(r.tags)}`);
2730
+ }
2731
+ });
2621
2732
  });
2622
- reports.command("triage <id>").description("Update the status and/or severity of a report").option("--status <status>", "New status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "New severity: critical|high|medium|low").option("--note <text>", "Internal triage note").option("--json", "Machine-readable JSON output").addHelpText("after", `
2733
+ reports.command("triage <id>").description("Update the status and/or severity of a report").option("--status <status>", "New status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "New severity: critical|high|medium|low").option("--note <text>", "Internal triage note").option("--json", "Output as JSON (alias for -o json)").addHelpText("after", `
2623
2734
  Examples:
2624
2735
  mushi reports triage <id> --status triaged --severity high
2625
2736
  mushi reports triage <id> --status in_progress --note "assigned to @alice"`).action(async (id, opts) => {
@@ -2644,16 +2755,17 @@ Examples:
2644
2755
  }
2645
2756
  die(result);
2646
2757
  }
2647
- if (opts.json) {
2648
- console.log(JSON.stringify(result.data, null, 2));
2649
- } else {
2650
- console.log(`\u2713 Updated report ${id}`);
2651
- if (opts.status) console.log(` Status: ${opts.status}`);
2652
- if (opts.severity) console.log(` Severity: ${opts.severity}`);
2653
- if (opts.note) console.log(` Note: ${opts.note}`);
2654
- }
2758
+ printResult(result.data, {
2759
+ json: opts.json,
2760
+ render() {
2761
+ console.log(`\u2713 Updated report ${id}`);
2762
+ if (opts.status) console.log(` Status: ${opts.status}`);
2763
+ if (opts.severity) console.log(` Severity: ${opts.severity}`);
2764
+ if (opts.note) console.log(` Note: ${opts.note}`);
2765
+ }
2766
+ });
2655
2767
  });
2656
- reports.command("resolve <id>").description("Mark a report as resolved (shorthand for triage --status resolved)").option("--note <text>", "Resolution note").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
2768
+ reports.command("resolve <id>").description("Mark a report as resolved (shorthand for triage --status resolved)").option("--note <text>", "Resolution note").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
2657
2769
  const config = requireConfig();
2658
2770
  const body = { status: "resolved" };
2659
2771
  if (opts.note) body["note"] = opts.note;
@@ -2669,14 +2781,15 @@ Examples:
2669
2781
  }
2670
2782
  die(result);
2671
2783
  }
2672
- if (opts.json) {
2673
- console.log(JSON.stringify(result.data, null, 2));
2674
- } else {
2675
- console.log(`\u2713 Resolved report ${id}`);
2676
- if (opts.note) console.log(` Note: ${opts.note}`);
2677
- }
2784
+ printResult(result.data, {
2785
+ json: opts.json,
2786
+ render() {
2787
+ console.log(`\u2713 Resolved report ${id}`);
2788
+ if (opts.note) console.log(` Note: ${opts.note}`);
2789
+ }
2790
+ });
2678
2791
  });
2679
- reports.command("reopen <id>").description("Reopen a resolved or dismissed report").option("--note <text>", "Note explaining the reopen").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
2792
+ reports.command("reopen <id>").description("Reopen a resolved or dismissed report").option("--note <text>", "Note explaining the reopen").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
2680
2793
  const config = requireConfig();
2681
2794
  const body = { status: "reopened" };
2682
2795
  if (opts.note) body["note"] = opts.note;
@@ -2692,13 +2805,14 @@ Examples:
2692
2805
  }
2693
2806
  die(result);
2694
2807
  }
2695
- if (opts.json) {
2696
- console.log(JSON.stringify(result.data, null, 2));
2697
- } else {
2698
- console.log(`\u2713 Reopened report ${id}`);
2699
- }
2808
+ printResult(result.data, {
2809
+ json: opts.json,
2810
+ render() {
2811
+ console.log(`\u2713 Reopened report ${id}`);
2812
+ }
2813
+ });
2700
2814
  });
2701
- reports.command("verify <id>").description("Mark a fixed report as verified by the reporter (operator shortcut)").option("--note <text>", "Optional audit note").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
2815
+ reports.command("verify <id>").description("Mark a fixed report as verified by the reporter (operator shortcut)").option("--note <text>", "Optional audit note").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
2702
2816
  const config = requireConfig();
2703
2817
  const body = { status: "verified" };
2704
2818
  if (opts.note) body["note"] = opts.note;
@@ -2707,10 +2821,14 @@ Examples:
2707
2821
  body: JSON.stringify(body)
2708
2822
  });
2709
2823
  if (!result.ok) die(result);
2710
- if (opts.json) console.log(JSON.stringify(result.data, null, 2));
2711
- else console.log(`\u2713 Verified report ${id}`);
2824
+ printResult(result.data, {
2825
+ json: opts.json,
2826
+ render() {
2827
+ console.log(`\u2713 Verified report ${id}`);
2828
+ }
2829
+ });
2712
2830
  });
2713
- reports.command("dismiss <id>").description("Dismiss a report (not a real bug / out of scope)").option("--note <text>", "Reason for dismissal").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
2831
+ reports.command("dismiss <id>").description("Dismiss a report (not a real bug / out of scope)").option("--note <text>", "Reason for dismissal").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
2714
2832
  const config = requireConfig();
2715
2833
  const body = { status: "dismissed" };
2716
2834
  if (opts.note) body["note"] = opts.note;
@@ -2726,13 +2844,14 @@ Examples:
2726
2844
  }
2727
2845
  die(result);
2728
2846
  }
2729
- if (opts.json) {
2730
- console.log(JSON.stringify(result.data, null, 2));
2731
- } else {
2732
- console.log(`\u2713 Dismissed report ${id}`);
2733
- }
2847
+ printResult(result.data, {
2848
+ json: opts.json,
2849
+ render() {
2850
+ console.log(`\u2713 Dismissed report ${id}`);
2851
+ }
2852
+ });
2734
2853
  });
2735
- reports.command("reply <id> <message>").description("Send a visible reply to the reporter widget for a report").option("--author <name>", 'Display name for the sender (default: "Mushi Admin")').option("--json", "Machine-readable JSON output").addHelpText("after", `
2854
+ reports.command("reply <id> <message>").description("Send a visible reply to the reporter widget for a report").option("--author <name>", 'Display name for the sender (default: "Mushi Admin")').option("--json", "Output as JSON (alias for -o json)").addHelpText("after", `
2736
2855
  Examples:
2737
2856
  mushi reports reply abc123 "Thanks for reporting \u2014 fixing this in the next release."
2738
2857
  mushi reports reply abc123 "Can you share a screenshot?" --author "Alice"`).action(async (id, message, opts) => {
@@ -2751,13 +2870,14 @@ Examples:
2751
2870
  }
2752
2871
  die(result);
2753
2872
  }
2754
- if (opts.json) {
2755
- console.log(JSON.stringify(result.data, null, 2));
2756
- } else {
2757
- console.log(`\u2713 Reply sent to reporter for report ${id}`);
2758
- }
2873
+ printResult(result.data, {
2874
+ json: opts.json,
2875
+ render() {
2876
+ console.log(`\u2713 Reply sent to reporter for report ${id}`);
2877
+ }
2878
+ });
2759
2879
  });
2760
- reports.command("search <query>").description("Search reports by keyword in summary and description").option("--limit <n>", "Max results (1\u201350)", "10").option("--status <status>", "Filter by status").option("--json", "Machine-readable JSON output").addHelpText("after", `
2880
+ reports.command("search <query>").description("Search reports by keyword in summary and description").option("--limit <n>", "Max results (1\u201350)", "10").option("--status <status>", "Filter by status").option("--json", "Output as JSON (alias for -o json)").addHelpText("after", `
2761
2881
  Examples:
2762
2882
  mushi reports search "login button"
2763
2883
  mushi reports search "404 error" --status new --limit 20`).action(async (query, opts) => {
@@ -2767,24 +2887,25 @@ Examples:
2767
2887
  if (opts.status) params.set("status", opts.status);
2768
2888
  const result = await apiCall(`/v1/sync/reports?${params}`, config);
2769
2889
  if (!result.ok) die(result);
2770
- if (opts.json) {
2771
- console.log(JSON.stringify(result.data, null, 2));
2772
- return;
2773
- }
2774
- const rows = result.data.reports;
2775
- if (rows.length === 0) {
2776
- console.log(`No reports matching "${query}".`);
2777
- return;
2778
- }
2779
- console.log(`${rows.length} result${rows.length === 1 ? "" : "s"} for "${query}":`);
2780
- console.log("");
2781
- for (const r of rows) {
2782
- console.log(` ${r.id}`);
2783
- console.log(` ${r.severity ?? "unset"} \xB7 ${r.status ?? "new"} \xB7 ${fmtDate(r.created_at)}`);
2784
- const text2 = r.summary ?? r.description ?? "";
2785
- if (text2) console.log(` ${text2.slice(0, 80)}`);
2786
- console.log("");
2787
- }
2890
+ printResult(result.data, {
2891
+ json: opts.json,
2892
+ render(d) {
2893
+ const rows = d.reports;
2894
+ if (rows.length === 0) {
2895
+ console.log(`No reports matching "${query}".`);
2896
+ return;
2897
+ }
2898
+ console.log(`${rows.length} result${rows.length === 1 ? "" : "s"} for "${query}":`);
2899
+ console.log("");
2900
+ for (const r of rows) {
2901
+ console.log(` ${r.id}`);
2902
+ console.log(` ${r.severity ?? "unset"} \xB7 ${r.status ?? "new"} \xB7 ${fmtDate(r.created_at)}`);
2903
+ const text2 = r.summary ?? r.description ?? "";
2904
+ if (text2) console.log(` ${text2.slice(0, 80)}`);
2905
+ console.log("");
2906
+ }
2907
+ }
2908
+ });
2788
2909
  });
2789
2910
  }
2790
2911
 
@@ -2897,7 +3018,7 @@ AI code review context.
2897
3018
  Typical CI usage:
2898
3019
  MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
2899
3020
  npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
2900
- const { writeFile: writeFile2, mkdir } = await import("fs/promises");
3021
+ const { writeFile: writeFile3, mkdir } = await import("fs/promises");
2901
3022
  const nodePath2 = await import("path");
2902
3023
  const config = requireConfig();
2903
3024
  const cwd = opts.cwd ?? process.cwd();
@@ -2925,7 +3046,7 @@ Typical CI usage:
2925
3046
  return;
2926
3047
  }
2927
3048
  await mkdir(nodePath2.dirname(target), { recursive: true });
2928
- await writeFile2(target, JSON.stringify(output, null, 2) + "\n", "utf8");
3049
+ await writeFile3(target, JSON.stringify(output, null, 2) + "\n", "utf8");
2929
3050
  if (opts.json) {
2930
3051
  console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
2931
3052
  } else {
@@ -2936,8 +3057,8 @@ Typical CI usage:
2936
3057
 
2937
3058
  // src/sourcemaps.ts
2938
3059
  import { createReadStream } from "fs";
2939
- import { readFile, readdir } from "fs/promises";
2940
- import { createHash } from "crypto";
3060
+ import { readFile, readdir, writeFile } from "fs/promises";
3061
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
2941
3062
  import { join as join3, relative, basename } from "path";
2942
3063
  import * as p2 from "@clack/prompts";
2943
3064
  async function findMapFiles(dir) {
@@ -2965,7 +3086,31 @@ function fileHash(path) {
2965
3086
  stream.on("error", reject);
2966
3087
  });
2967
3088
  }
2968
- async function uploadFile(filePath, release, endpoint, apiKey) {
3089
+ async function injectDebugId(mapPath) {
3090
+ const jsPath = mapPath.endsWith(".js.map") ? mapPath.slice(0, -4) : null;
3091
+ if (!jsPath) return null;
3092
+ const debugId = randomUUID2();
3093
+ try {
3094
+ const jsContents = await readFile(jsPath, "utf-8");
3095
+ if (!jsContents.includes("//# debugId=")) {
3096
+ await writeFile(jsPath, `${jsContents}
3097
+ //# debugId=${debugId}`, "utf-8");
3098
+ }
3099
+ } catch {
3100
+ }
3101
+ try {
3102
+ const mapContents = await readFile(mapPath, "utf-8");
3103
+ const parsed = JSON.parse(mapContents);
3104
+ if (!parsed["debugId"]) {
3105
+ parsed["debugId"] = debugId;
3106
+ await writeFile(mapPath, JSON.stringify(parsed), "utf-8");
3107
+ }
3108
+ } catch {
3109
+ return null;
3110
+ }
3111
+ return debugId;
3112
+ }
3113
+ async function uploadFile(filePath, release, endpoint, apiKey, debugId) {
2969
3114
  const sha256 = await fileHash(filePath);
2970
3115
  try {
2971
3116
  const checkRes = await fetch(
@@ -2984,6 +3129,7 @@ async function uploadFile(filePath, release, endpoint, apiKey) {
2984
3129
  form.append("filename", relative(process.cwd(), filePath).replaceAll("\\", "/"));
2985
3130
  form.append("release", release);
2986
3131
  form.append("sha256", sha256);
3132
+ if (debugId) form.append("debug_id", debugId);
2987
3133
  const res = await fetch(`${endpoint}/v1/sourcemaps`, {
2988
3134
  method: "POST",
2989
3135
  headers: { Authorization: `Bearer ${apiKey}`, "X-Mushi-Api-Key": apiKey },
@@ -3040,6 +3186,20 @@ async function runSourcemapsUpload(opts) {
3040
3186
  p2.outro(`${files.length} file${files.length === 1 ? "" : "s"} would be uploaded`);
3041
3187
  return;
3042
3188
  }
3189
+ const debugIds = /* @__PURE__ */ new Map();
3190
+ if (opts.inject) {
3191
+ const injectSpin = p2.spinner();
3192
+ injectSpin.start("Injecting Debug IDs\u2026");
3193
+ let injected = 0;
3194
+ for (const filePath of files) {
3195
+ const debugId = await injectDebugId(filePath);
3196
+ if (debugId) {
3197
+ debugIds.set(filePath, debugId);
3198
+ injected++;
3199
+ }
3200
+ }
3201
+ injectSpin.stop(`Injected Debug IDs into ${injected} file${injected === 1 ? "" : "s"}`);
3202
+ }
3043
3203
  let uploaded = 0;
3044
3204
  let skipped = 0;
3045
3205
  let failed = 0;
@@ -3047,7 +3207,7 @@ async function runSourcemapsUpload(opts) {
3047
3207
  const rel = relative(process.cwd(), filePath).replaceAll("\\", "/");
3048
3208
  const fs = p2.spinner();
3049
3209
  fs.start(rel);
3050
- const result = await uploadFile(filePath, opts.release, endpoint, apiKey);
3210
+ const result = await uploadFile(filePath, opts.release, endpoint, apiKey, debugIds.get(filePath));
3051
3211
  if (result.skipped) {
3052
3212
  fs.stop(`\u21A9 ${rel} (already uploaded)`);
3053
3213
  skipped++;
@@ -3078,7 +3238,7 @@ async function runSourcemapsUpload(opts) {
3078
3238
 
3079
3239
  // src/commands/diagnostics.ts
3080
3240
  function registerDiagnosticsCommands(program2) {
3081
- program2.command("test").description("Submit a synthetic test report to verify the ingestion pipeline end-to-end").option("--json", "Machine-readable JSON output").action(async (opts) => {
3241
+ program2.command("test").description("Submit a synthetic test report to verify the ingestion pipeline end-to-end").option("--json", "Machine-readable JSON output (alias for -o json)").action(async (opts) => {
3082
3242
  const config = requireConfig();
3083
3243
  const result = await apiCall("/v1/reports", config, {
3084
3244
  method: "POST",
@@ -3101,17 +3261,17 @@ function registerDiagnosticsCommands(program2) {
3101
3261
  })
3102
3262
  });
3103
3263
  if (!result.ok) die(result);
3104
- if (opts.json) {
3105
- console.log(JSON.stringify(result.data, null, 2));
3106
- } else {
3107
- const d = result.data;
3108
- console.log(`\u2713 Test report submitted`);
3109
- console.log(` ID: ${d.reportId}`);
3110
- console.log(` Status: ${d.status}`);
3111
- console.log(` View: ${reportsUrl(resolveConsoleUrlSync(), d.reportId)}`);
3112
- }
3264
+ printResult(result.data, {
3265
+ json: opts.json,
3266
+ render(d) {
3267
+ console.log(`\u2713 Test report submitted`);
3268
+ console.log(` ID: ${d.reportId}`);
3269
+ console.log(` Status: ${d.status}`);
3270
+ console.log(` View: ${reportsUrl(resolveConsoleUrlSync(), d.reportId)}`);
3271
+ }
3272
+ });
3113
3273
  });
3114
- program2.command("index <path>").description("Walk a local repo and upload code chunks to the Mushi RAG indexer").option("--language <lang>", "Limit to one language: ts, tsx, js, py, go, rs").option("--dry-run", "Show what would be uploaded without sending").option("--json", "Machine-readable summary: { files, bytes }").addHelpText("after", `
3274
+ program2.command("index <path>").description("Walk a local repo and upload code chunks to the Mushi RAG indexer").option("--language <lang>", "Limit to one language: ts, tsx, js, py, go, rs").option("--dry-run", "Show what would be uploaded without sending").option("--json", "Machine-readable summary: { files, bytes } (alias for -o json)").addHelpText("after", `
3115
3275
  Uploads source code into the Mushi vector index so the fix-worker can
3116
3276
  retrieve relevant context when generating patches. Only needed for private
3117
3277
  repos that cannot be auto-indexed via GitHub App.
@@ -3169,8 +3329,9 @@ Examples:
3169
3329
  `);
3170
3330
  }
3171
3331
  }
3172
- if (opts.json) {
3173
- console.log(JSON.stringify({ ok: errors === 0, files: count, bytes, errors }));
3332
+ const summary = { ok: errors === 0, files: count, bytes, errors };
3333
+ if (outputIsJson(opts.json)) {
3334
+ console.log(JSON.stringify(summary, null, 2));
3174
3335
  } else {
3175
3336
  const kb = (bytes / 1024).toFixed(1);
3176
3337
  console.log(`
@@ -3179,13 +3340,15 @@ Indexed ${count} files (${kb} KB) into project ${config.projectId}${errors ? ` \
3179
3340
  if (errors > 0) process.exit(1);
3180
3341
  });
3181
3342
  const sourcemaps = program2.command("sourcemaps").description("Source map management");
3182
- sourcemaps.command("upload").description("Upload source maps to Mushi (idempotent, SHA256-keyed) for stack trace symbolication").requiredOption("--release <version>", "Release identifier, e.g. 1.0.0 or a git SHA").option("--dir <path>", "Directory containing .map files", "./dist").option("--dry-run", "List files that would be uploaded without uploading").option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--silent", "Suppress progress output").addHelpText("after", `
3343
+ sourcemaps.command("upload").description("Upload source maps to Mushi (idempotent, SHA256-keyed) for stack trace symbolication").requiredOption("--release <version>", "Release identifier, e.g. 1.0.0 or a git SHA").option("--dir <path>", "Directory containing .map files", "./dist").option("--inject", "Inject a Debug ID (UUID) into each .js and .map before uploading for exact stack-trace resolution").option("--dry-run", "List files that would be uploaded without uploading").option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--silent", "Suppress progress output").addHelpText("after", `
3183
3344
  Examples:
3184
3345
  mushi sourcemaps upload --release 1.0.0
3185
- mushi sourcemaps upload --release $(git rev-parse --short HEAD) --dir ./dist`).action(async (opts) => {
3346
+ mushi sourcemaps upload --release $(git rev-parse --short HEAD) --dir ./dist
3347
+ mushi sourcemaps upload --release 1.0.0 --inject # recommended: adds Debug IDs`).action(async (opts) => {
3186
3348
  await runSourcemapsUpload({
3187
3349
  release: opts.release,
3188
3350
  dir: opts.dir,
3351
+ inject: opts.inject,
3189
3352
  dryRun: opts.dryRun,
3190
3353
  endpoint: opts.endpoint,
3191
3354
  apiKey: opts.apiKey,
@@ -3195,7 +3358,7 @@ Examples:
3195
3358
  }
3196
3359
 
3197
3360
  // src/project-bootstrap.ts
3198
- import { writeFile, readFile as readFile2 } from "fs/promises";
3361
+ import { writeFile as writeFile2, readFile as readFile2 } from "fs/promises";
3199
3362
  import nodePath from "path";
3200
3363
  async function writeProjectBootstrapFiles(opts) {
3201
3364
  const cwd = opts.cwd ?? process.cwd();
@@ -3219,7 +3382,7 @@ async function writeProjectBootstrapFiles(opts) {
3219
3382
  const prefix = stripped.length > 0 ? `${stripped}
3220
3383
 
3221
3384
  ` : "";
3222
- await writeFile(envPath, `${prefix}${mushiBlock}
3385
+ await writeFile2(envPath, `${prefix}${mushiBlock}
3223
3386
  `, "utf8");
3224
3387
  const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
3225
3388
  const serverName = buildMcpServerName({ legacy: true });
@@ -3366,7 +3529,7 @@ function resolveLoginEndpoint(optsEndpoint, existingConfigEndpoint, envEndpoint)
3366
3529
  return optsEndpoint ?? existingConfigEndpoint ?? envEndpoint?.trim();
3367
3530
  }
3368
3531
  function registerSetupCommands(program2) {
3369
- program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").option("--verify", "Probe the MCP key after writing to confirm it has mcp:read scope (default: on)").option("--no-verify", "Skip the post-write key probe").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted) \u2014 used for first-run login if not yet configured").option("--stdio", "Write a local stdio entry with an API key instead of the hosted OAuth URL (cursor/claude default is hosted OAuth \u2014 no key on disk)").option("--ci", "Alias for --stdio: headless environments cannot drive the browser OAuth flow").addHelpText("after", `
3532
+ program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").option("--verify", "Probe the MCP key after writing to confirm it has mcp:read scope (default: on)").option("--no-verify", "Skip the post-write key probe").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted) \u2014 used for first-run login if not yet configured").option("--stdio", "Write a local stdio entry with an API key instead of the hosted OAuth URL (cursor/claude default is hosted OAuth \u2014 no key on disk)").option("--ci", "Alias for --stdio: headless environments cannot drive the browser OAuth flow").option("--inline-key", "Write the literal API key into the mcp.json env block (default: write ${MUSHI_API_KEY} placeholder to prevent accidental git leaks)").addHelpText("after", `
3370
3533
  Examples:
3371
3534
  mushi setup # wire Cursor (default: hosted OAuth \u2014 sign in from the IDE, no key on disk)
3372
3535
  mushi setup --stdio # local stdio entry with an API key (previous behavior)
@@ -3387,7 +3550,7 @@ key for you \u2014 nothing sensitive is written to the repo. Pass --stdio/--ci t
3387
3550
  get the local subprocess entry with an API key instead.
3388
3551
 
3389
3552
  The command reads credentials from ~/.config/mushi/config.json. If you are not logged in yet, it runs browser sign-in first, then writes MCP config.`).action(async (opts) => {
3390
- const { writeFile: writeFile2, mkdir, readFile: readFile3 } = await import("fs/promises");
3553
+ const { writeFile: writeFile3, mkdir, readFile: readFile3 } = await import("fs/promises");
3391
3554
  const { existsSync: existsSync3 } = await import("fs");
3392
3555
  const nodePath2 = await import("path");
3393
3556
  const os = await import("os");
@@ -3473,15 +3636,13 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
3473
3636
  }
3474
3637
  const hostedMcpUrl = (config.endpoint ?? "").replace(/\/api\/?$/, "/mcp");
3475
3638
  const hostedServerBlock = { url: hostedMcpUrl };
3476
- const mcpServerBlock = {
3477
- command: "npx",
3478
- args: ["-y", MUSHI_MCP_PIN_SPEC],
3479
- env: {
3480
- MUSHI_API_ENDPOINT: config.endpoint,
3481
- MUSHI_PROJECT_ID: config.projectId ?? "",
3482
- MUSHI_API_KEY: config.apiKey
3483
- }
3484
- };
3639
+ const inlineKey = !!opts.inlineKey;
3640
+ const mcpServerBlock = buildMcpServerBlock({
3641
+ endpoint: config.endpoint,
3642
+ projectId: config.projectId ?? "",
3643
+ apiKey: config.apiKey,
3644
+ inlineKey
3645
+ });
3485
3646
  const configDir = nodePath2.isAbsolute(ideEntry.dir) ? ideEntry.dir : nodePath2.join(cwd, ideEntry.dir);
3486
3647
  const configPath = nodePath2.join(configDir, ideEntry.file);
3487
3648
  if (ideEntry.format === "mcp-json") {
@@ -3491,7 +3652,8 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
3491
3652
  const pBlock = buildMcpServerBlock({
3492
3653
  endpoint: config.endpoint,
3493
3654
  projectId: p3.id,
3494
- apiKey: config.apiKey
3655
+ apiKey: config.apiKey,
3656
+ inlineKey
3495
3657
  });
3496
3658
  if (!opts.dryRun) {
3497
3659
  await writeMcpServerEntry({ configPath, serverName: pServerName, serverBlock: pBlock });
@@ -3515,6 +3677,8 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
3515
3677
  console.log(" Hosted MCP with OAuth login \u2014 no API key was written to this file.");
3516
3678
  console.log(` Restart ${opts.ide === "cursor" ? "Cursor" : "Claude Code"}, open the MCP panel (/mcp in Claude Code), pick "${serverName}" and sign in via the browser.`);
3517
3679
  console.log(" Prefer a local key-based entry (headless/CI)? Re-run with --stdio.");
3680
+ } else if (!inlineKey && config.apiKey) {
3681
+ printKeyExportHint(config.apiKey);
3518
3682
  }
3519
3683
  }
3520
3684
  }
@@ -3535,7 +3699,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
3535
3699
  env: {
3536
3700
  MUSHI_API_ENDPOINT: config.endpoint,
3537
3701
  MUSHI_PROJECT_ID: config.projectId ?? "",
3538
- MUSHI_API_KEY: config.apiKey
3702
+ MUSHI_API_KEY: inlineKey ? config.apiKey : "${MUSHI_API_KEY}"
3539
3703
  }
3540
3704
  },
3541
3705
  settings: {}
@@ -3547,7 +3711,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
3547
3711
  console.log(redactKeyForDisplay(output));
3548
3712
  } else {
3549
3713
  await mkdir(configDir, { recursive: true });
3550
- await writeFile2(configPath, output, "utf8");
3714
+ await writeFile3(configPath, output, "utf8");
3551
3715
  console.log(`\u2713 Written ${configPath}`);
3552
3716
  }
3553
3717
  }
@@ -3581,7 +3745,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
3581
3745
  if (opts.dryRun) {
3582
3746
  console.log(`[dry-run] Would write ${rulesPath}`);
3583
3747
  } else {
3584
- await writeFile2(rulesPath, rulesContent, "utf8");
3748
+ await writeFile3(rulesPath, rulesContent, "utf8");
3585
3749
  console.log(`\u2713 Written .cursorrules`);
3586
3750
  }
3587
3751
  } else if (opts.ide === "claude") {
@@ -3591,7 +3755,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
3591
3755
  console.log(`[dry-run] Would write ${rulesPath}`);
3592
3756
  } else {
3593
3757
  await mkdir(rulesDir, { recursive: true });
3594
- await writeFile2(rulesPath, rulesContent, "utf8");
3758
+ await writeFile3(rulesPath, rulesContent, "utf8");
3595
3759
  console.log(`\u2713 Written .claude/rules/mushi.md`);
3596
3760
  }
3597
3761
  }
@@ -3654,7 +3818,8 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
3654
3818
  serverBlock: buildMcpServerBlock({
3655
3819
  endpoint: fresh.endpoint ?? config.endpoint,
3656
3820
  projectId: fresh.projectId ?? config.projectId ?? "",
3657
- apiKey: fresh.apiKey
3821
+ apiKey: fresh.apiKey,
3822
+ inlineKey
3658
3823
  })
3659
3824
  });
3660
3825
  console.log(`\u2713 Rewrote ${configPath} with the upgraded key`);
@@ -3707,12 +3872,12 @@ Note: ${configRelPath} holds no secrets (OAuth login) \u2014 safe to commit and
3707
3872
  const gitignorePath = nodePath2.join(cwd, ".gitignore");
3708
3873
  const ignoreLine = configRelPath.replaceAll("\\", "/");
3709
3874
  try {
3710
- const { readFile: readFile4, writeFile: writeFile3 } = await import("fs/promises");
3875
+ const { readFile: readFile4, writeFile: writeFile4 } = await import("fs/promises");
3711
3876
  const existing = await readFile4(gitignorePath, "utf8").catch(() => "");
3712
3877
  const lines = existing.split(/\r?\n/);
3713
3878
  if (!lines.some((l) => l.trim() === ignoreLine)) {
3714
3879
  const sep = existing.endsWith("\n") || existing === "" ? "" : "\n";
3715
- await writeFile3(
3880
+ await writeFile4(
3716
3881
  gitignorePath,
3717
3882
  `${existing}${sep}# Mushi MCP config holds a plaintext API key
3718
3883
  ${ignoreLine}
@@ -4209,13 +4374,134 @@ async function runUpgrade(opts = {}) {
4209
4374
  };
4210
4375
  }
4211
4376
 
4377
+ // src/self-upgrade.ts
4378
+ import { execSync as execSync2 } from "child_process";
4379
+ var CLI_PACKAGE = "@mushi-mushi/cli";
4380
+ var SAFE_NPM_VERSION2 = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
4381
+ function detectInstallMethod(argv1 = process.argv[1] ?? "", userAgent = process.env["npm_config_user_agent"] ?? "") {
4382
+ const path = argv1.replace(/\\/g, "/").toLowerCase();
4383
+ if (path.includes("/_npx/") || path.includes("/.npm/_npx/")) return "npx";
4384
+ if (path.includes("/dlx-") || path.includes("/pnpm/dlx")) return "npx";
4385
+ if (userAgent.startsWith("bun")) return "bun";
4386
+ if (userAgent.startsWith("pnpm")) return "pnpm";
4387
+ if (userAgent.startsWith("yarn")) return "yarn";
4388
+ if (userAgent.startsWith("npm")) return "npm";
4389
+ if (path.includes("/.bun/")) return "bun";
4390
+ if (path.includes("/pnpm/")) return "pnpm";
4391
+ if (path.includes("/yarn/") || path.includes("/.yarn/")) return "yarn";
4392
+ if (path.includes("/npm/") || path.includes("node_modules")) return "npm";
4393
+ return "unknown";
4394
+ }
4395
+ function selfUpgradeCommand(method, version) {
4396
+ if (!SAFE_NPM_VERSION2.test(version)) return null;
4397
+ const spec = `${CLI_PACKAGE}@${version}`;
4398
+ switch (method) {
4399
+ case "npm":
4400
+ return `npm install -g ${spec}`;
4401
+ case "pnpm":
4402
+ return `pnpm add -g ${spec}`;
4403
+ case "yarn":
4404
+ return `yarn global add ${spec}`;
4405
+ case "bun":
4406
+ return `bun add -g ${spec}`;
4407
+ case "npx":
4408
+ case "unknown":
4409
+ default:
4410
+ return null;
4411
+ }
4412
+ }
4413
+ async function runSelfUpgrade(opts = {}) {
4414
+ const current = MUSHI_CLI_VERSION;
4415
+ const method = detectInstallMethod();
4416
+ const freshness = await checkFreshness(CLI_PACKAGE, current, {
4417
+ timeoutMs: 4e3,
4418
+ ignoreOptOut: true
4419
+ // an explicit `upgrade --self` must always hit the registry
4420
+ });
4421
+ const latest = freshness?.latest ?? null;
4422
+ const safeLatest = latest && SAFE_NPM_VERSION2.test(latest) ? latest : null;
4423
+ if (!freshness) {
4424
+ return {
4425
+ current,
4426
+ latest: null,
4427
+ method,
4428
+ command: null,
4429
+ upgraded: false,
4430
+ message: "Could not reach the npm registry to check for a newer CLI \u2014 try again in a moment."
4431
+ };
4432
+ }
4433
+ if (!freshness.isOutdated || !safeLatest) {
4434
+ return {
4435
+ current,
4436
+ latest,
4437
+ method,
4438
+ command: null,
4439
+ upgraded: false,
4440
+ message: `mushi CLI is already at the latest stable version (v${current}).`
4441
+ };
4442
+ }
4443
+ const command = selfUpgradeCommand(method, safeLatest);
4444
+ if (!command) {
4445
+ const via = method === "npx" ? "You are running via npx \u2014 the next `npx mushi` will fetch the latest automatically." : `Could not detect how the CLI was installed. Upgrade manually:
4446
+ npm install -g ${CLI_PACKAGE}@${safeLatest}`;
4447
+ return { current, latest, method, command: null, upgraded: false, message: `A newer CLI is available (v${current} \u2192 v${safeLatest}).
4448
+ ${via}` };
4449
+ }
4450
+ if (opts.dryRun) {
4451
+ return {
4452
+ current,
4453
+ latest,
4454
+ method,
4455
+ command,
4456
+ upgraded: false,
4457
+ message: `[dry-run] Would run: ${command}`
4458
+ };
4459
+ }
4460
+ try {
4461
+ const exec = opts.exec ?? ((cmd) => execSync2(cmd, { stdio: "inherit", env: process.env }));
4462
+ exec(command);
4463
+ } catch {
4464
+ return {
4465
+ current,
4466
+ latest,
4467
+ method,
4468
+ command,
4469
+ upgraded: false,
4470
+ message: `Self-upgrade command failed \u2014 run it manually:
4471
+ ${command}`
4472
+ };
4473
+ }
4474
+ return {
4475
+ current,
4476
+ latest,
4477
+ method,
4478
+ command,
4479
+ upgraded: true,
4480
+ message: `Upgraded mushi CLI: v${current} \u2192 v${safeLatest}.`
4481
+ };
4482
+ }
4483
+
4212
4484
  // src/commands/upgrade-cli.ts
4213
4485
  function registerUpgradeCommand(program2) {
4214
- 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", `
4486
+ program2.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--self", "Upgrade the globally-installed `mushi` CLI itself (not the project SDK deps)").option("--dry-run", "Print the install command without running it").option("--json", "Machine-readable plan + result").addHelpText("after", `
4215
4487
  Examples:
4216
4488
  mushi upgrade
4217
4489
  mushi upgrade --dry-run
4490
+ mushi upgrade --self # upgrade the CLI binary itself
4491
+ mushi upgrade --self --dry-run
4218
4492
  mushi upgrade --cwd ../glot.it`).action(async (opts) => {
4493
+ if (opts.self) {
4494
+ const selfResult = await runSelfUpgrade({ dryRun: opts.dryRun });
4495
+ if (opts.json) {
4496
+ console.log(JSON.stringify(selfResult, null, 2));
4497
+ } else {
4498
+ console.log(selfResult.message);
4499
+ }
4500
+ if (!selfResult.upgraded && selfResult.command && !opts.dryRun) {
4501
+ process.exit(1);
4502
+ }
4503
+ return;
4504
+ }
4219
4505
  const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
4220
4506
  if (opts.json) {
4221
4507
  console.log(JSON.stringify(result, null, 2));
@@ -4552,50 +4838,197 @@ Approve: mushi tdd approve <id>`);
4552
4838
  // src/commands/keys.ts
4553
4839
  function registerKeysCommands(program2) {
4554
4840
  const keys = program2.command("keys").description("Manage API key pool (BYOK)");
4555
- keys.command("list").description("List all API keys in the pool with their status").action(async () => {
4841
+ keys.command("list").description("List all API keys in the pool with their status").option("--json", "Output as JSON (alias for -o json)").action(async (opts) => {
4556
4842
  const config = loadConfig();
4557
4843
  if (!config.apiKey || !config.projectId) {
4558
4844
  console.error("Run `mushi login` first");
4559
4845
  process.exit(1);
4560
4846
  }
4561
- const res = await apiCall(
4562
- `/v1/admin/byok/keys?project_id=${encodeURIComponent(config.projectId)}`,
4563
- config
4564
- );
4847
+ const res = await apiCall(`/v1/admin/byok/keys?project_id=${encodeURIComponent(config.projectId)}`, config);
4565
4848
  if (!res.ok) {
4566
4849
  console.error(`Error: ${res.error.message}`);
4567
4850
  process.exit(1);
4568
4851
  }
4569
- if (res.data.keys.length === 0) {
4570
- console.log("No keys configured.");
4571
- return;
4852
+ printResult(res.data, {
4853
+ json: opts.json,
4854
+ render(d) {
4855
+ const legacyKeys = d.legacyKeys ?? [];
4856
+ if (d.keys.length === 0 && legacyKeys.length === 0) {
4857
+ console.log("No keys configured.");
4858
+ return;
4859
+ }
4860
+ for (const k of d.keys) {
4861
+ const cooldown = k.cooldown_until && new Date(k.cooldown_until) > /* @__PURE__ */ new Date() ? ` [cooldown until ${new Date(k.cooldown_until).toLocaleTimeString()}]` : "";
4862
+ const validation = k.test_status ? ` test=${k.test_status}` : " test=required";
4863
+ const lastTested = k.last_tested_at ? ` tested=${new Date(k.last_tested_at).toISOString()}` : "";
4864
+ const lastUsed = k.last_used_at ? ` used=${new Date(k.last_used_at).toISOString()}` : "";
4865
+ const baseUrl = k.base_url ? ` base=${k.base_url}` : "";
4866
+ const hint = k.key_hint ? ` hint=${k.key_hint}` : "";
4867
+ console.log(
4868
+ `${k.provider_slug.padEnd(14)} [${k.status}]${validation} p=${k.priority} ${k.label ?? "(no label)"}${hint}${baseUrl}${cooldown}${lastTested}${lastUsed} created=${new Date(k.created_at).toISOString()} \u2014 ${k.id}`
4869
+ );
4870
+ }
4871
+ for (const k of legacyKeys) {
4872
+ const validation = k.test_status ? ` test=${k.test_status}` : " test=required";
4873
+ const lastTested = k.last_tested_at ? ` tested=${new Date(k.last_tested_at).toISOString()}` : "";
4874
+ const lastUsed = k.last_used_at ? ` used=${new Date(k.last_used_at).toISOString()}` : "";
4875
+ const created = k.created_at ? ` created=${new Date(k.created_at).toISOString()}` : "";
4876
+ const hint = k.key_hint ? ` hint=${k.key_hint}` : "";
4877
+ console.log(
4878
+ `${k.provider_slug.padEnd(14)} [${k.status}]${validation} legacy${hint}${lastTested}${lastUsed}${created} \u2014 manage in the console`
4879
+ );
4880
+ }
4881
+ }
4882
+ });
4883
+ });
4884
+ keys.command("rotate").description("Rotate the project API key stored in local config (mints a fresh key)").option("--json", "Output as JSON (alias for -o json)").action(async (opts) => {
4885
+ const config = loadConfig();
4886
+ if (!config.projectId) {
4887
+ console.error("Run `mushi login` first");
4888
+ process.exit(1);
4572
4889
  }
4573
- for (const k of res.data.keys) {
4574
- const cooldown = k.cooldown_until && new Date(k.cooldown_until) > /* @__PURE__ */ new Date() ? ` [cooldown until ${new Date(k.cooldown_until).toLocaleTimeString()}]` : "";
4575
- console.log(`${k.provider_slug.padEnd(14)} [${k.status}] p=${k.priority} ${k.label ?? "(no label)"}${cooldown} \u2014 ${k.id}`);
4890
+ if (!config.endpoint) {
4891
+ console.error("No endpoint configured \u2014 run `mushi login` first");
4892
+ process.exit(1);
4576
4893
  }
4894
+ console.log("Authenticating to rotate key\u2026");
4895
+ const session = await startDeviceAuth(config.endpoint).catch((err) => {
4896
+ console.error(`Error starting auth: ${err.message}`);
4897
+ process.exit(1);
4898
+ });
4899
+ console.log(`
4900
+ Open this URL to confirm rotation:
4901
+ ${session.verification_uri}`);
4902
+ console.log(` Enter code: ${session.user_code}
4903
+ `);
4904
+ const cliToken = await waitForCliToken(config.endpoint, session).catch((err) => {
4905
+ console.error(`Auth failed: ${err.message}`);
4906
+ process.exit(1);
4907
+ });
4908
+ const existingScopes = Array.isArray(config.scopes) ? config.scopes : ["report:write", "mcp:read"];
4909
+ const newKey = await mintProjectKey(config.endpoint, cliToken, config.projectId, {
4910
+ label: "rotated",
4911
+ scopes: existingScopes
4912
+ }).catch((err) => {
4913
+ console.error(`Could not mint new key: ${err.message}`);
4914
+ process.exit(1);
4915
+ });
4916
+ const oldKey = config.apiKey;
4917
+ const oldPrefix = oldKey?.slice(0, 12) ?? "(none)";
4918
+ config.apiKey = newKey;
4919
+ saveConfig(config);
4920
+ let serverRevoked = false;
4921
+ if (oldKey && config.projectId) {
4922
+ const revokeResult = await revokeProjectKey(
4923
+ config.endpoint,
4924
+ cliToken,
4925
+ config.projectId,
4926
+ oldPrefix
4927
+ ).catch(() => null);
4928
+ serverRevoked = revokeResult?.revoked === 1;
4929
+ }
4930
+ printResult(
4931
+ { newPrefix: newKey.slice(0, 12), oldPrefix, serverRevoked },
4932
+ {
4933
+ json: opts.json,
4934
+ render(d) {
4935
+ console.log(`\u2713 Key rotated.`);
4936
+ console.log(` Old prefix: ${d.oldPrefix}\u2026 \u2192 New prefix: ${d.newPrefix}\u2026`);
4937
+ console.log(` New key saved to local config.`);
4938
+ if (d.serverRevoked) {
4939
+ console.log(` \u2713 Old key revoked on the server.`);
4940
+ } else {
4941
+ console.log(
4942
+ ` \u2139 Old key may still be active \u2014 revoke it at: ${config.consoleUrl ?? "your Mushi console"} \u2192 Settings \u2192 API Keys`
4943
+ );
4944
+ }
4945
+ }
4946
+ }
4947
+ );
4577
4948
  });
4578
- keys.command("add").description("Add a new API key to the pool").requiredOption("--provider <p>", "Provider: anthropic, openai, firecrawl, browserbase, cursor").option("--key <k>", "The API key value (prefer the MUSHI_BYOK_KEY env var to keep it out of shell history)").option("--label <l>", "Human-readable label").option("--priority <n>", "Priority (lower = higher priority)", "100").action(async (opts) => {
4949
+ keys.command("add").description("Add a new API key to the pool").requiredOption("--provider <p>", "Provider: anthropic, openai, firecrawl, browserbase, cursor").option(
4950
+ "--key <k>",
4951
+ "The API key value (prefer the MUSHI_BYOK_KEY env var to keep it out of shell history)"
4952
+ ).option("--label <l>", "Human-readable label").option("--priority <n>", "Priority (lower = higher priority)", "100").option("--base-url <url>", "Allow-listed OpenAI-compatible HTTPS base URL").action(
4953
+ async (opts) => {
4954
+ const config = loadConfig();
4955
+ if (!config.apiKey || !config.projectId) {
4956
+ console.error("Run `mushi login` first");
4957
+ process.exit(1);
4958
+ }
4959
+ const key = process.env.MUSHI_BYOK_KEY ?? opts.key;
4960
+ if (!key) {
4961
+ console.error(
4962
+ "Provide the key via the MUSHI_BYOK_KEY env var (recommended) or --key <value>."
4963
+ );
4964
+ process.exit(1);
4965
+ }
4966
+ const priority = Number(opts.priority);
4967
+ if (!Number.isInteger(priority) || priority < 0 || priority > 1e4) {
4968
+ console.error("Priority must be an integer between 0 and 10000.");
4969
+ process.exit(1);
4970
+ }
4971
+ const res = await apiCall("/v1/admin/byok/keys", config, {
4972
+ method: "POST",
4973
+ body: JSON.stringify({
4974
+ project_id: config.projectId,
4975
+ provider_slug: opts.provider,
4976
+ key,
4977
+ label: opts.label,
4978
+ priority,
4979
+ base_url: opts.baseUrl
4980
+ })
4981
+ });
4982
+ if (!res.ok) {
4983
+ console.error(`Error: ${res.error.message}`);
4984
+ process.exit(1);
4985
+ }
4986
+ if (res.data.validation.status === "ok") {
4987
+ console.log(`\u2713 Key validated and active \u2014 id: ${res.data.id}`);
4988
+ return;
4989
+ }
4990
+ console.log(
4991
+ `! Key saved but quarantined (${res.data.validation.status}) \u2014 id: ${res.data.id}`
4992
+ );
4993
+ console.log(` ${res.data.validation.detail}`);
4994
+ }
4995
+ );
4996
+ keys.command("test <key-id>").description("Re-test one pooled key and activate it only if validation passes").action(async (keyId) => {
4579
4997
  const config = loadConfig();
4580
4998
  if (!config.apiKey || !config.projectId) {
4581
4999
  console.error("Run `mushi login` first");
4582
5000
  process.exit(1);
4583
5001
  }
4584
- const key = process.env.MUSHI_BYOK_KEY ?? opts.key;
4585
- if (!key) {
4586
- console.error("Provide the key via the MUSHI_BYOK_KEY env var (recommended) or --key <value>.");
5002
+ const res = await apiCall(`/v1/admin/byok/keys/${encodeURIComponent(keyId)}/test`, config, { method: "POST" });
5003
+ if (!res.ok) {
5004
+ console.error(`Error: ${res.error.message}`);
5005
+ process.exit(1);
5006
+ }
5007
+ if (res.data.validation.status === "ok") {
5008
+ console.log(`\u2713 Key validated and active \u2014 id: ${res.data.key.id}`);
5009
+ return;
5010
+ }
5011
+ console.log(
5012
+ `! Key remains quarantined (${res.data.validation.status}) \u2014 id: ${res.data.key.id}`
5013
+ );
5014
+ console.log(` ${res.data.validation.detail}`);
5015
+ });
5016
+ keys.command("remove <key-id>").description("Permanently remove one pooled key and its Vault secret").action(async (keyId) => {
5017
+ const config = loadConfig();
5018
+ if (!config.apiKey || !config.projectId) {
5019
+ console.error("Run `mushi login` first");
4587
5020
  process.exit(1);
4588
5021
  }
4589
5022
  const res = await apiCall(
4590
- "/v1/admin/byok/keys",
5023
+ `/v1/admin/byok/keys/${encodeURIComponent(keyId)}`,
4591
5024
  config,
4592
- { method: "POST", body: JSON.stringify({ project_id: config.projectId, provider_slug: opts.provider, key, label: opts.label, priority: parseInt(opts.priority, 10) }) }
5025
+ { method: "DELETE" }
4593
5026
  );
4594
5027
  if (!res.ok) {
4595
5028
  console.error(`Error: ${res.error.message}`);
4596
5029
  process.exit(1);
4597
5030
  }
4598
- console.log(`\u2713 Key added \u2014 id: ${res.data.id}`);
5031
+ console.log(`\u2713 Key removed \u2014 id: ${res.data.keyId}`);
4599
5032
  });
4600
5033
  }
4601
5034
 
@@ -5492,7 +5925,7 @@ Examples:
5492
5925
  }
5493
5926
 
5494
5927
  // src/commands/selfhost.ts
5495
- import { execSync as execSync2, spawnSync } from "child_process";
5928
+ import { execSync as execSync3, spawnSync } from "child_process";
5496
5929
  import { randomBytes } from "crypto";
5497
5930
  function hasSupabaseCli() {
5498
5931
  try {
@@ -5510,7 +5943,7 @@ function run(cmd, opts) {
5510
5943
  const label = opts.label ?? cmd.slice(0, 60);
5511
5944
  process.stdout.write(` \u203A ${label} \u2026 `);
5512
5945
  try {
5513
- const out = execSync2(cmd, {
5946
+ const out = execSync3(cmd, {
5514
5947
  cwd: opts.cwd,
5515
5948
  encoding: "utf8",
5516
5949
  timeout: 3e5,
@@ -5823,14 +6256,54 @@ Examples:
5823
6256
  });
5824
6257
  }
5825
6258
 
6259
+ // src/commands/profile.ts
6260
+ function registerProfileCommands(program2) {
6261
+ const profile = program2.command("profile").description("Manage credential profiles (default, staging, per-client)");
6262
+ profile.command("list").alias("ls").description("List all profiles; the active one is marked with *").option("--json", "Machine-readable JSON output").action((opts) => {
6263
+ const { active, profiles } = listProfiles();
6264
+ if (outputIsJson(opts.json)) {
6265
+ console.log(JSON.stringify({ active, profiles }, null, 2));
6266
+ return;
6267
+ }
6268
+ for (const name of profiles.sort()) {
6269
+ const marker = name === active ? "*" : " ";
6270
+ const cfg = loadConfig(void 0, { profile: name });
6271
+ const state = cfg.apiKey ? "configured" : "empty";
6272
+ console.log(`${marker} ${name.padEnd(20)} [${state}]`);
6273
+ }
6274
+ });
6275
+ profile.command("current").description("Print the active profile name").action(() => {
6276
+ console.log(listProfiles().active);
6277
+ });
6278
+ profile.command("use <name>").description("Switch the active profile (creates an empty one if it does not exist)").action((name) => {
6279
+ const safe = name.trim();
6280
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(safe)) {
6281
+ process.stderr.write("error: profile name must be 1\u201364 chars of [A-Za-z0-9._-]\n");
6282
+ process.exit(2);
6283
+ }
6284
+ const before = listProfiles();
6285
+ setActiveProfile(safe);
6286
+ const isNew = !before.profiles.includes(safe);
6287
+ console.log(`\u2713 Active profile: ${safe}${isNew ? " (new \u2014 run `mushi login` to add credentials)" : ""}`);
6288
+ });
6289
+ }
6290
+
5826
6291
  // src/index.ts
5827
6292
  installSignalHandlers();
5828
- 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", `
6293
+ 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).option("--profile <name>", "Use a named credential profile for this invocation (or set MUSHI_PROFILE)").option("-o, --output <format>", "Output format: text | json", "text").hook("preAction", (_thisCommand, actionCommand) => {
6294
+ const opts = actionCommand.optsWithGlobals();
6295
+ const profile = opts["profile"];
6296
+ if (profile && profile.trim()) {
6297
+ process.env["MUSHI_PROFILE"] = profile.trim();
6298
+ }
6299
+ setGlobalOutputFormat(opts["output"]);
6300
+ }).addHelpText("after", `
5829
6301
  Environment variables:
5830
6302
  MUSHI_API_KEY SDK ingest key (report:write scope \u2014 from Onboarding \u2192 Verify in the console)
5831
6303
  MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
5832
6304
  MUSHI_API_ENDPOINT Supabase edge function URL
5833
6305
  e.g. https://<ref>.supabase.co/functions/v1/api
6306
+ MUSHI_PROFILE Credential profile to use (see \`mushi profile\`)
5834
6307
 
5835
6308
  Exit codes:
5836
6309
  0 success
@@ -5862,6 +6335,7 @@ registerAuditCommands(program);
5862
6335
  registerSkillsCommands(program);
5863
6336
  registerBillingCommands(program);
5864
6337
  registerSelfhostCommands(program);
6338
+ registerProfileCommands(program);
5865
6339
  registerCompletionCommand(program);
5866
6340
  program.parseAsync().catch((err) => {
5867
6341
  printAndExit(err);