@mushi-mushi/cli 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,13 +10,20 @@ import { homedir } from "os";
10
10
  var CONFIG_PATH = join(homedir(), ".mushirc");
11
11
  var SECURE_FILE_MODE = 384;
12
12
  function loadConfig(path = CONFIG_PATH) {
13
- if (!existsSync(path)) return {};
14
- tightenPermissions(path);
15
- try {
16
- return JSON.parse(readFileSync(path, "utf-8"));
17
- } catch {
18
- return {};
13
+ let file = {};
14
+ if (existsSync(path)) {
15
+ tightenPermissions(path);
16
+ try {
17
+ file = JSON.parse(readFileSync(path, "utf-8"));
18
+ } catch {
19
+ }
19
20
  }
21
+ const fromEnv = {
22
+ ...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
23
+ ...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
24
+ ...process.env["MUSHI_API_ENDPOINT"] ? { endpoint: process.env["MUSHI_API_ENDPOINT"] } : {}
25
+ };
26
+ return { ...file, ...fromEnv };
20
27
  }
21
28
  function saveConfig(config, path = CONFIG_PATH) {
22
29
  writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
@@ -275,7 +282,6 @@ function collectDeps(pkg) {
275
282
  }
276
283
 
277
284
  // src/endpoint.ts
278
- var DEFAULT_ENDPOINT = "https://api.mushimushi.dev";
279
285
  var TEST_REPORT_TIMEOUT_MS = 1e4;
280
286
  var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
281
287
  function assertEndpoint(url) {
@@ -293,10 +299,14 @@ function assertEndpoint(url) {
293
299
  return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname);
294
300
  }
295
301
  function normalizeEndpoint(url) {
296
- const input = url ?? DEFAULT_ENDPOINT;
297
- let end = input.length;
298
- while (end > 0 && input.charCodeAt(end - 1) === 47) end--;
299
- return input.slice(0, end);
302
+ if (!url) {
303
+ throw new Error(
304
+ "No API endpoint configured. Run `mushi init` or set MUSHI_API_ENDPOINT. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api"
305
+ );
306
+ }
307
+ let end = url.length;
308
+ while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
309
+ return url.slice(0, end);
300
310
  }
301
311
 
302
312
  // src/freshness.ts
@@ -454,11 +464,11 @@ function getFrameworkFromPkg(pkg) {
454
464
  }
455
465
 
456
466
  // src/version.ts
457
- var MUSHI_CLI_VERSION = true ? "0.6.1" : "0.0.0-dev";
467
+ var MUSHI_CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
458
468
 
459
469
  // src/init.ts
460
470
  var ENV_FILES = [".env.local", ".env"];
461
- var PROJECT_ID_PATTERN = /^proj_[A-Za-z0-9_-]{10,}$/;
471
+ var PROJECT_ID_PATTERN = /^(?:proj_[A-Za-z0-9_-]{10,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
462
472
  var API_KEY_PATTERN = /^mushi_[A-Za-z0-9_-]{10,}$/;
463
473
  async function runInit(options = {}) {
464
474
  const cwd = options.cwd ?? process.cwd();
@@ -490,7 +500,8 @@ async function runInit(options = {}) {
490
500
  }
491
501
  writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
492
502
  persistCliConfig(credentials.apiKey, credentials.projectId);
493
- printNextSteps(framework, credentials.apiKey, credentials.projectId);
503
+ const enableRewards = await maybeEnableRewards(options);
504
+ printNextSteps(framework, credentials.apiKey, credentials.projectId, enableRewards);
494
505
  await maybeSendTestReport(credentials, options);
495
506
  p.outro("Setup complete. Happy bug squashing \u{1F41B}");
496
507
  }
@@ -502,7 +513,7 @@ function ensureInteractiveOrBailOut(options) {
502
513
  );
503
514
  if (hasAllFlags) return;
504
515
  process.stderr.write(
505
- "mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id proj_xxx --api-key mushi_xxx\n"
516
+ "mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --api-key mushi_xxx\nYour project ID is the UUID shown in the Projects page of the Mushi admin console.\n"
506
517
  );
507
518
  process.exit(1);
508
519
  }
@@ -536,21 +547,22 @@ async function collectCredentials(options) {
536
547
  const existing = loadConfig();
537
548
  const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
538
549
  message: "Project ID",
539
- placeholder: "proj_xxxxxxxxxxxx",
540
- hint: "Find this at https://kensaur.us/mushi-mushi/projects",
541
- validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected format: proj_ followed by 10+ alphanumeric characters"
550
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
551
+ hint: "Where to find it: https://kensaur.us/mushi-mushi/projects \u2192 click your project \u2192 copy the UUID below the project name.",
552
+ validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected a UUID (xxxxxxxx-xxxx-...) \u2014 copy it from the Mushi admin console Projects page."
542
553
  });
543
554
  const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
544
555
  message: "API key",
545
556
  placeholder: "mushi_xxxxxxxxxxxx",
546
- hint: "Treat this like a password \u2014 it goes in your env file, not in source.",
557
+ hint: "Where to find it: https://kensaur.us/mushi-mushi/settings \u2192 API Keys tab. Treat it like a password \u2014 env file only, never commit it.",
547
558
  validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
548
559
  });
549
560
  const projectId = sanitizeSecret(rawProjectId);
550
561
  const apiKey = sanitizeSecret(rawApiKey);
551
562
  if (!PROJECT_ID_PATTERN.test(projectId)) {
552
563
  throw new Error(
553
- `Invalid project ID. Expected format: proj_[A-Za-z0-9_-]{10,}. Got: ${redact(projectId)}`
564
+ `Invalid project ID. Got: ${redact(projectId)}
565
+ Expected a UUID (e.g. 542b34e0-019e-41fe-b900-7b637717bb86) \u2014 copy it from the Projects page in the Mushi console at https://kensaur.us/mushi-mushi/projects`
554
566
  );
555
567
  }
556
568
  if (!API_KEY_PATTERN.test(apiKey)) {
@@ -568,6 +580,7 @@ function redact(value) {
568
580
  return `${value.slice(0, 4)}\u2026${value.slice(-2)}`;
569
581
  }
570
582
  async function promptText(opts) {
583
+ if (opts.hint) p.log.info(opts.hint);
571
584
  const value = await p.text({
572
585
  message: opts.message,
573
586
  placeholder: opts.placeholder,
@@ -584,18 +597,17 @@ async function promptText(opts) {
584
597
  p.cancel("Aborted.");
585
598
  process.exit(0);
586
599
  }
587
- if (opts.hint) p.log.info(opts.hint);
588
600
  return value;
589
601
  }
590
602
  async function installPackages(pm, packages, cwd) {
591
603
  const command = installCommand(pm, packages);
592
- const spinner2 = p.spinner();
593
- spinner2.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
604
+ const spinner3 = p.spinner();
605
+ spinner3.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
594
606
  try {
595
607
  await runCommand(pm, packages, cwd);
596
- spinner2.stop(`Installed ${packages.join(", ")}`);
608
+ spinner3.stop(`Installed ${packages.join(", ")}`);
597
609
  } catch (err) {
598
- spinner2.stop(`Install failed \u2014 run \`${command}\` manually.`);
610
+ spinner3.stop(`Install failed \u2014 run \`${command}\` manually.`);
599
611
  p.log.error(err instanceof Error ? err.name + ": " + err.message : String(err));
600
612
  }
601
613
  }
@@ -669,13 +681,34 @@ function persistCliConfig(apiKey, projectId) {
669
681
  const existing = loadConfig();
670
682
  saveConfig({ ...existing, apiKey, projectId });
671
683
  }
672
- function printNextSteps(framework, apiKey, projectId) {
684
+ function printNextSteps(framework, apiKey, projectId, enableRewards = false) {
673
685
  p.note(framework.snippet(apiKey, projectId), "Add this to your app:");
686
+ if (enableRewards) {
687
+ const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
688
+ import { MushiRewardsBadge } from '@mushi-mushi/react';
689
+
690
+ // Inside your component:
691
+ <MushiRewardsBadge showPoints />` : `// Add to your user menu:
692
+ // import { MushiRewardsBadge } from '@mushi-mushi/react';
693
+ // <MushiRewardsBadge showPoints />`;
694
+ p.note(badgeSnippet, "Rewards badge snippet:");
695
+ p.log.info("Enable rewards in your project settings at https://kensaur.us/mushi-mushi/rewards");
696
+ p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
697
+ }
674
698
  p.log.message("Verify the install:");
675
699
  p.log.message(" \u2022 Start your dev server");
676
700
  p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
677
701
  p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
678
702
  }
703
+ async function maybeEnableRewards(options) {
704
+ if (options.yes) return false;
705
+ const answer = await p.confirm({
706
+ message: "Enable Mushi Rewards? (users earn points for bug reports + app activity)",
707
+ initialValue: false
708
+ });
709
+ if (p.isCancel(answer)) return false;
710
+ return Boolean(answer);
711
+ }
679
712
  async function maybeSendTestReport(credentials, options) {
680
713
  if (options.sendTestReport === false) return;
681
714
  let shouldSend;
@@ -690,8 +723,15 @@ async function maybeSendTestReport(credentials, options) {
690
723
  shouldSend = answer;
691
724
  }
692
725
  if (!shouldSend) return;
693
- const spinner2 = p.spinner();
694
- spinner2.start("Sending test report\u2026");
726
+ if (!options.endpoint) {
727
+ p.note(
728
+ "No endpoint configured \u2014 skipping test report.\nSet endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api",
729
+ "Skipped"
730
+ );
731
+ return;
732
+ }
733
+ const spinner3 = p.spinner();
734
+ spinner3.start("Sending test report\u2026");
695
735
  const endpoint = normalizeEndpoint(options.endpoint);
696
736
  const controller = new AbortController();
697
737
  const timer = setTimeout(() => controller.abort(), TEST_REPORT_FETCH_TIMEOUT_MS);
@@ -723,17 +763,24 @@ async function maybeSendTestReport(credentials, options) {
723
763
  })
724
764
  });
725
765
  if (!res.ok) {
726
- spinner2.stop(`Test report rejected (HTTP ${res.status}).`);
766
+ spinner3.stop(`Test report rejected (HTTP ${res.status}).`);
727
767
  p.log.warn(
728
768
  res.status === 401 || res.status === 403 ? "Credentials did not authenticate \u2014 double-check the project ID and API key." : "Skipping test report. You can retry with `mushi test`."
729
769
  );
730
770
  return;
731
771
  }
732
- spinner2.stop("Test report sent.");
733
- p.log.success("View it at https://kensaur.us/mushi-mushi/reports");
772
+ spinner3.stop("Test report sent.");
773
+ let reportId;
774
+ try {
775
+ const body = await res.json();
776
+ reportId = body.data?.reportId;
777
+ } catch {
778
+ }
779
+ const reportPath = reportId ? `/reports/${reportId}` : "/reports";
780
+ p.log.success(`View it at https://kensaur.us/mushi-mushi/admin${reportPath}`);
734
781
  } catch (err) {
735
782
  const aborted = err instanceof Error && err.name === "AbortError";
736
- spinner2.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
783
+ spinner3.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
737
784
  p.log.warn(err instanceof Error ? err.message : String(err));
738
785
  } finally {
739
786
  clearTimeout(timer);
@@ -828,7 +875,7 @@ var MIGRATE_CATALOG = [
828
875
  category: "competitor",
829
876
  status: "published",
830
877
  detectionLabel: pkgs[0],
831
- match: (d) => pkgs.some((p2) => d.has(p2))
878
+ match: (d) => pkgs.some((p3) => d.has(p3))
832
879
  }))
833
880
  ];
834
881
  function titleForCompetitor(slug) {
@@ -860,10 +907,10 @@ function depsFromPackageJson(pkg) {
860
907
  }
861
908
  function runMigrate(opts = {}) {
862
909
  const cwd = opts.cwd ?? process.cwd();
863
- const log2 = opts.log ?? ((s) => console.log(s));
910
+ const log3 = opts.log ?? ((s) => console.log(s));
864
911
  const pkg = readPackageJson(cwd);
865
912
  if (!pkg) {
866
- log2(
913
+ log3(
867
914
  opts.json ? JSON.stringify({ ok: false, error: "no-package-json", cwd, matches: [] }, null, 2) : `No package.json found in ${cwd}. Run \`mushi migrate\` from your project root.`
868
915
  );
869
916
  return { matches: [] };
@@ -871,7 +918,7 @@ function runMigrate(opts = {}) {
871
918
  const deps = depsFromPackageJson(pkg);
872
919
  const matches = detectMigrations(deps);
873
920
  if (opts.json) {
874
- log2(
921
+ log3(
875
922
  JSON.stringify(
876
923
  {
877
924
  ok: true,
@@ -891,40 +938,302 @@ function runMigrate(opts = {}) {
891
938
  return { matches };
892
939
  }
893
940
  if (matches.length === 0) {
894
- log2("No migrations suggested for this project.");
895
- log2(`Browse the full catalog: ${DOCS_BASE}/migrations`);
941
+ log3("No migrations suggested for this project.");
942
+ log3(`Browse the full catalog: ${DOCS_BASE}/migrations`);
896
943
  return { matches };
897
944
  }
898
- log2(`Suggested migration${matches.length > 1 ? "s" : ""} for this project:`);
899
- log2("");
945
+ log3(`Suggested migration${matches.length > 1 ? "s" : ""} for this project:`);
946
+ log3("");
900
947
  for (const { guide, url } of matches) {
901
- log2(` \u2022 ${guide.title}`);
902
- if (guide.detectionLabel) log2(` detected: ${guide.detectionLabel}`);
903
- log2(` ${guide.summary}`);
904
- log2(` ${url}`);
905
- log2("");
948
+ log3(` \u2022 ${guide.title}`);
949
+ if (guide.detectionLabel) log3(` detected: ${guide.detectionLabel}`);
950
+ log3(` ${guide.summary}`);
951
+ log3(` ${url}`);
952
+ log3("");
906
953
  }
907
- log2(`Browse the full catalog: ${DOCS_BASE}/migrations`);
954
+ log3(`Browse the full catalog: ${DOCS_BASE}/migrations`);
908
955
  return { matches };
909
956
  }
910
957
 
958
+ // src/sourcemaps.ts
959
+ import { createReadStream } from "fs";
960
+ import { readFile, readdir } from "fs/promises";
961
+ import { createHash } from "crypto";
962
+ import { join as join5, relative, basename } from "path";
963
+ import * as p2 from "@clack/prompts";
964
+ async function findMapFiles(dir) {
965
+ const results = [];
966
+ async function walk(current) {
967
+ const entries = await readdir(current, { withFileTypes: true });
968
+ for (const e of entries) {
969
+ const full = join5(current, e.name);
970
+ if (e.isDirectory()) {
971
+ await walk(full);
972
+ } else if (e.isFile() && (e.name.endsWith(".js.map") || e.name.endsWith(".css.map"))) {
973
+ results.push(full);
974
+ }
975
+ }
976
+ }
977
+ await walk(dir);
978
+ return results;
979
+ }
980
+ function fileHash(path) {
981
+ return new Promise((resolve2, reject) => {
982
+ const hash = createHash("sha256");
983
+ const stream = createReadStream(path);
984
+ stream.on("data", (chunk) => hash.update(chunk));
985
+ stream.on("end", () => resolve2(hash.digest("hex")));
986
+ stream.on("error", reject);
987
+ });
988
+ }
989
+ async function uploadFile(filePath, release, endpoint, apiKey) {
990
+ const sha256 = await fileHash(filePath);
991
+ try {
992
+ const checkRes = await fetch(
993
+ `${endpoint}/v1/sourcemaps?sha256=${encodeURIComponent(sha256)}&release=${encodeURIComponent(release)}`,
994
+ { headers: { Authorization: `Bearer ${apiKey}`, "X-Mushi-Api-Key": apiKey } }
995
+ );
996
+ if (checkRes.ok) {
997
+ const json = await checkRes.json();
998
+ if (json.exists) return { ok: true, skipped: true };
999
+ }
1000
+ } catch {
1001
+ }
1002
+ const contents = await readFile(filePath);
1003
+ const form = new FormData();
1004
+ form.append("file", new Blob([contents]), basename(filePath));
1005
+ form.append("filename", relative(process.cwd(), filePath).replaceAll("\\", "/"));
1006
+ form.append("release", release);
1007
+ form.append("sha256", sha256);
1008
+ const res = await fetch(`${endpoint}/v1/sourcemaps`, {
1009
+ method: "POST",
1010
+ headers: { Authorization: `Bearer ${apiKey}`, "X-Mushi-Api-Key": apiKey },
1011
+ body: form
1012
+ });
1013
+ if (!res.ok) {
1014
+ const text2 = await res.text().catch(() => "");
1015
+ return {
1016
+ ok: false,
1017
+ skipped: false,
1018
+ reason: `HTTP ${res.status}: ${text2.slice(0, 120)}`
1019
+ };
1020
+ }
1021
+ return { ok: true, skipped: false };
1022
+ }
1023
+ async function runSourcemapsUpload(opts) {
1024
+ const endpoint = opts.endpoint ?? process.env["MUSHI_API_ENDPOINT"] ?? "";
1025
+ const apiKey = opts.apiKey ?? process.env["MUSHI_API_KEY"] ?? "";
1026
+ if (!opts.dryRun && !endpoint) {
1027
+ p2.log.error(
1028
+ "No API endpoint configured. Pass --endpoint <url>, set MUSHI_API_ENDPOINT,\n or run `mushi config endpoint <url>` to persist it. For Supabase self-hosting,\n this is your edge-functions URL, e.g. https://xyz.supabase.co/functions/v1/api"
1029
+ );
1030
+ process.exit(1);
1031
+ }
1032
+ if (!opts.dryRun && !apiKey) {
1033
+ p2.log.error("No API key \u2014 set MUSHI_API_KEY or pass --api-key <key>");
1034
+ process.exit(1);
1035
+ }
1036
+ if (!opts.silent) p2.intro(`sourcemaps upload \xB7 release ${opts.release}`);
1037
+ const spin = p2.spinner();
1038
+ spin.start(`Scanning ${opts.dir} for .map files\u2026`);
1039
+ let files;
1040
+ try {
1041
+ files = await findMapFiles(opts.dir);
1042
+ } catch (err) {
1043
+ spin.stop("Scan failed");
1044
+ p2.log.error(
1045
+ `Cannot read ${opts.dir}: ${err instanceof Error ? err.message : String(err)}`
1046
+ );
1047
+ process.exit(1);
1048
+ }
1049
+ spin.stop(
1050
+ `Found ${files.length} map file${files.length === 1 ? "" : "s"}`
1051
+ );
1052
+ if (files.length === 0) {
1053
+ p2.log.warn("No .js.map or .css.map files found \u2014 nothing to upload.");
1054
+ return;
1055
+ }
1056
+ if (opts.dryRun) {
1057
+ p2.log.info("Dry run \u2014 files that would be uploaded:");
1058
+ for (const f of files) {
1059
+ p2.log.message(` ${relative(process.cwd(), f).replaceAll("\\", "/")}`);
1060
+ }
1061
+ p2.outro(`${files.length} file${files.length === 1 ? "" : "s"} would be uploaded`);
1062
+ return;
1063
+ }
1064
+ let uploaded = 0;
1065
+ let skipped = 0;
1066
+ let failed = 0;
1067
+ for (const filePath of files) {
1068
+ const rel = relative(process.cwd(), filePath).replaceAll("\\", "/");
1069
+ const fs = p2.spinner();
1070
+ fs.start(rel);
1071
+ const result = await uploadFile(filePath, opts.release, endpoint, apiKey);
1072
+ if (result.skipped) {
1073
+ fs.stop(`\u21A9 ${rel} (already uploaded)`);
1074
+ skipped++;
1075
+ } else if (result.ok) {
1076
+ fs.stop(`\u2713 ${rel}`);
1077
+ uploaded++;
1078
+ } else {
1079
+ fs.stop(`\u2717 ${rel} \u2014 ${result.reason ?? "unknown error"}`);
1080
+ failed++;
1081
+ }
1082
+ }
1083
+ const total = files.length;
1084
+ const parts = [
1085
+ `Uploaded ${uploaded} / ${total} file${total === 1 ? "" : "s"}`,
1086
+ skipped > 0 ? `(${skipped} already existed)` : "",
1087
+ failed > 0 ? `\u2014 ${failed} failed` : ""
1088
+ ].filter(Boolean);
1089
+ const summary = parts.join(" ");
1090
+ if (!opts.silent) {
1091
+ if (failed > 0) {
1092
+ p2.log.error(summary);
1093
+ } else {
1094
+ p2.outro(summary);
1095
+ }
1096
+ }
1097
+ if (failed > 0) process.exit(1);
1098
+ }
1099
+
911
1100
  // src/index.ts
1101
+ var API_TIMEOUT_MS = 15e3;
912
1102
  async function apiCall(path, config, options = {}) {
913
- const endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
914
- const res = await fetch(`${endpoint}${path}`, {
915
- ...options,
916
- headers: {
917
- "Content-Type": "application/json",
918
- "Authorization": `Bearer ${config.apiKey}`,
919
- "X-Mushi-Api-Key": config.apiKey ?? "",
920
- "X-Mushi-Project": config.projectId ?? "",
921
- ...options.headers
1103
+ const endpoint = config.endpoint;
1104
+ if (!endpoint) {
1105
+ return {
1106
+ ok: false,
1107
+ error: {
1108
+ code: "NO_ENDPOINT",
1109
+ message: "No API endpoint configured.\n Run: mushi login --api-key <key> --endpoint <url> --project-id <id>\n Or: export MUSHI_API_ENDPOINT=https://<project-ref>.supabase.co/functions/v1/api"
1110
+ }
1111
+ };
1112
+ }
1113
+ const controller = new AbortController();
1114
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1115
+ try {
1116
+ const res = await fetch(`${endpoint}${path}`, {
1117
+ ...options,
1118
+ signal: controller.signal,
1119
+ headers: {
1120
+ "Content-Type": "application/json",
1121
+ // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
1122
+ // fallback accepted by some older middleware. Both are sent.
1123
+ "Authorization": `Bearer ${config.apiKey ?? ""}`,
1124
+ "X-Mushi-Api-Key": config.apiKey ?? "",
1125
+ "X-Mushi-Project": config.projectId ?? "",
1126
+ "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
1127
+ ...options.headers
1128
+ }
1129
+ });
1130
+ clearTimeout(timer);
1131
+ let body;
1132
+ const contentType = res.headers.get("content-type") ?? "";
1133
+ if (contentType.includes("application/json")) {
1134
+ try {
1135
+ body = await res.json();
1136
+ } catch {
1137
+ body = null;
1138
+ }
1139
+ } else {
1140
+ const text2 = await res.text();
1141
+ try {
1142
+ body = JSON.parse(text2);
1143
+ } catch {
1144
+ body = {
1145
+ ok: false,
1146
+ error: {
1147
+ code: `HTTP_${res.status}`,
1148
+ message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
1149
+ }
1150
+ };
1151
+ }
922
1152
  }
923
- });
924
- return res.json();
1153
+ if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
1154
+ const b = body;
1155
+ return {
1156
+ ok: false,
1157
+ httpStatus: res.status,
1158
+ error: {
1159
+ code: b["code"] ?? `HTTP_${res.status}`,
1160
+ message: b["message"] ?? `Request failed (${res.status})`
1161
+ }
1162
+ };
1163
+ }
1164
+ return body;
1165
+ } catch (err) {
1166
+ clearTimeout(timer);
1167
+ if (err instanceof Error && err.name === "AbortError") {
1168
+ return {
1169
+ ok: false,
1170
+ error: {
1171
+ code: "TIMEOUT",
1172
+ message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
1173
+ }
1174
+ };
1175
+ }
1176
+ return {
1177
+ ok: false,
1178
+ error: {
1179
+ code: "NETWORK_ERROR",
1180
+ message: err instanceof Error ? err.message : String(err)
1181
+ }
1182
+ };
1183
+ }
1184
+ }
1185
+ function die(result, exitCode = 1) {
1186
+ const { code, message } = result.error;
1187
+ const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
1188
+ process.stderr.write(`error${status}: ${code} \u2014 ${message}
1189
+ `);
1190
+ process.exit(exitCode);
1191
+ }
1192
+ function requireConfig(opts = {}) {
1193
+ const config = loadConfig();
1194
+ if (!config.apiKey) {
1195
+ process.stderr.write(
1196
+ "error: API key not configured.\n Run: mushi login --api-key <key> --endpoint <url>\n Or: export MUSHI_API_KEY=<key>\n"
1197
+ );
1198
+ process.exit(2);
1199
+ }
1200
+ if (!config.endpoint) {
1201
+ process.stderr.write(
1202
+ "error: API endpoint not configured.\n Run: mushi login --endpoint https://<ref>.supabase.co/functions/v1/api\n Or: export MUSHI_API_ENDPOINT=<url>\n"
1203
+ );
1204
+ process.exit(2);
1205
+ }
1206
+ if (opts.needsProject && !config.projectId) {
1207
+ process.stderr.write(
1208
+ "error: Project ID not configured.\n Run: mushi login --project-id <uuid>\n Or: export MUSHI_PROJECT_ID=<uuid>\n Find your project ID: https://kensaur.us/mushi-mushi/projects\n"
1209
+ );
1210
+ process.exit(2);
1211
+ }
1212
+ return config;
1213
+ }
1214
+ function fmtDate(iso) {
1215
+ if (!iso) return "\u2014";
1216
+ return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
1217
+ }
1218
+ function pad(s, width) {
1219
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
925
1220
  }
926
- var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION);
927
- program.command("init").description("Set up the Mushi Mushi SDK in this project (auto-detects framework)").option("--project-id <id>", "Skip the prompt by passing the project ID").option("--api-key <key>", "Skip the prompt by passing the API key").option("--framework <id>", "Force a framework (next, react, vue, nuxt, svelte, sveltekit, angular, expo, react-native, capacitor, vanilla)").option("--skip-install", "Don't auto-install the SDK package \u2014 print the command instead").option("-y, --yes", "Accept detected framework without prompting").option("--cwd <path>", "Run the wizard in a different directory").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--skip-test-report", 'Skip the end-of-wizard "send a test report" prompt').action(async (opts) => {
1221
+ var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION).addHelpText("after", `
1222
+ Environment variables:
1223
+ MUSHI_API_KEY Project API key (from Settings \u2192 API Keys in the console)
1224
+ MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
1225
+ MUSHI_API_ENDPOINT Supabase edge function URL
1226
+ e.g. https://<ref>.supabase.co/functions/v1/api
1227
+
1228
+ Exit codes:
1229
+ 0 success
1230
+ 1 API / runtime error
1231
+ 2 configuration error (missing credentials or endpoint)
1232
+ 3 not found (resource does not exist)
1233
+
1234
+ Console: https://kensaur.us/mushi-mushi/
1235
+ Docs: https://github.com/kensaurus/mushi-mushi`);
1236
+ program.command("init").description("Set up the Mushi Mushi SDK in this project (auto-detects framework)").option("--project-id <id>", "Skip the prompt \u2014 pass UUID from the Projects page").option("--api-key <key>", "Skip the prompt \u2014 pass the API key (CI only)").option("--framework <id>", "Force a framework (next, react, vue, nuxt, svelte, sveltekit, angular, expo, react-native, capacitor, vanilla)").option("--skip-install", "Print the install command instead of running it").option("-y, --yes", "Accept detected framework without prompting").option("--cwd <path>", "Run the wizard in a different directory").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--skip-test-report", 'Skip the end-of-wizard "send a test report" prompt').action(async (opts) => {
928
1237
  await runInit({
929
1238
  projectId: opts.projectId,
930
1239
  apiKey: opts.apiKey,
@@ -936,112 +1245,503 @@ program.command("init").description("Set up the Mushi Mushi SDK in this project
936
1245
  sendTestReport: opts.skipTestReport ? false : void 0
937
1246
  });
938
1247
  });
939
- program.command("migrate").description(
940
- "Suggest the most relevant Mushi Mushi migration guide based on your package.json"
941
- ).option("--cwd <path>", "Run from a different directory").option("--json", "Machine-readable JSON output").action((opts) => {
1248
+ program.command("migrate").description("Suggest the most relevant Mushi Mushi migration guide based on your package.json").option("--cwd <path>", "Run from a different directory").option("--json", "Machine-readable JSON output").action((opts) => {
942
1249
  const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
943
1250
  if (matches.length === 0) process.exit(1);
944
1251
  });
945
- program.command("login").description("Store API key for authentication").requiredOption("--api-key <key>", "API key").option("--endpoint <url>", "API endpoint URL").option("--project-id <id>", "Default project ID").action((opts) => {
1252
+ program.command("login").description("Save API credentials to ~/.mushirc (mode 0o600)").requiredOption("--api-key <key>", "Mushi API key (mushi_...)").option("--endpoint <url>", "Supabase edge function URL").option("--project-id <id>", "Project UUID (from the Projects page)").addHelpText("after", `
1253
+ Examples:
1254
+ mushi login --api-key mushi_xxx --endpoint https://xyz.supabase.co/functions/v1/api
1255
+ mushi login --api-key mushi_xxx --project-id 542b34e0-019e-41fe-b900-7b637717bb86`).action((opts) => {
946
1256
  const config = loadConfig();
947
1257
  config.apiKey = opts.apiKey;
948
1258
  if (opts.endpoint) config.endpoint = assertEndpoint(opts.endpoint);
949
1259
  if (opts.projectId) config.projectId = opts.projectId;
950
1260
  saveConfig(config);
951
- console.log("Saved credentials to ~/.mushirc (mode 0o600)");
952
- });
953
- program.command("status").description("Show project stats").action(async () => {
954
- const config = loadConfig();
955
- if (!config.apiKey) {
956
- console.error("Run `mushi login` first");
957
- process.exit(1);
958
- }
959
- const data = await apiCall("/v1/admin/stats", config);
960
- console.log(JSON.stringify(data, null, 2));
1261
+ console.log("\u2713 Credentials saved to ~/.mushirc (mode 0o600)");
1262
+ console.log(" Run 'mushi whoami' to verify the connection.");
961
1263
  });
962
- var reports = program.command("reports").description("Manage bug reports");
963
- reports.command("list").description("List recent reports").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status").option("--json", "JSON output").action(async (opts) => {
964
- const config = loadConfig();
965
- if (!config.apiKey) {
966
- console.error("Run `mushi login` first");
967
- process.exit(1);
968
- }
969
- const params = new URLSearchParams();
970
- params.set("limit", opts.limit);
971
- if (opts.status) params.set("status", opts.status);
972
- const data = await apiCall(`/v1/admin/reports?${params}`, config);
1264
+ program.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1265
+ Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
1266
+ Useful after 'mushi login' to confirm credentials are correct.`).action(async (opts) => {
1267
+ const config = requireConfig();
1268
+ const result = await apiCall("/v1/sync/whoami", config);
1269
+ if (!result.ok) die(result);
973
1270
  if (opts.json) {
974
- console.log(JSON.stringify(data, null, 2));
1271
+ console.log(JSON.stringify(result.data, null, 2));
975
1272
  } else {
976
- const reports2 = data.data?.reports ?? [];
977
- for (const r of reports2) {
978
- console.log(`${r.id} ${r.severity ?? "unset"} ${r.status ?? "new"} ${(r.summary ?? "").slice(0, 60)}`);
979
- }
1273
+ const d = result.data;
1274
+ console.log(`\u2713 Authenticated`);
1275
+ console.log(` Project: ${d.project_name} (${d.project_id})`);
1276
+ console.log(` Endpoint: ${config.endpoint}`);
1277
+ console.log(` Reports: ${d.stats.total_reports} total \xB7 ${d.stats.open_reports} open`);
980
1278
  }
981
1279
  });
982
- reports.command("show <id>").description("Show report details").action(async (id) => {
983
- const config = loadConfig();
984
- if (!config.apiKey) {
985
- console.error("Run `mushi login` first");
1280
+ program.command("ping").description("Check connectivity to the Mushi backend").option("--json", "Machine-readable JSON output").action(async (opts) => {
1281
+ const config = requireConfig();
1282
+ const t0 = Date.now();
1283
+ try {
1284
+ const controller = new AbortController();
1285
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1286
+ const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
1287
+ clearTimeout(timer);
1288
+ const latency = Date.now() - t0;
1289
+ if (opts.json) {
1290
+ console.log(JSON.stringify({ ok: res.ok, status: res.status, latency_ms: latency }));
1291
+ } else {
1292
+ const symbol = res.ok ? "\u2713" : "\u2717";
1293
+ console.log(`${symbol} ${res.ok ? "OK" : "FAIL"} \u2014 ${res.status} (${latency}ms)`);
1294
+ if (!res.ok) process.exit(1);
1295
+ }
1296
+ } catch (err) {
1297
+ const msg = err instanceof Error ? err.message : String(err);
1298
+ if (opts.json) {
1299
+ console.log(JSON.stringify({ ok: false, error: msg, latency_ms: Date.now() - t0 }));
1300
+ } else {
1301
+ process.stderr.write(`\u2717 Unreachable: ${msg}
1302
+ `);
1303
+ }
986
1304
  process.exit(1);
987
1305
  }
988
- const data = await apiCall(`/v1/admin/reports/${id}`, config);
989
- console.log(JSON.stringify(data, null, 2));
990
1306
  });
991
- reports.command("triage <id>").description("Update report status/severity").option("--status <status>", "New status").option("--severity <severity>", "New severity").action(async (id, opts) => {
992
- const config = loadConfig();
993
- if (!config.apiKey) {
994
- console.error("Run `mushi login` first");
995
- process.exit(1);
1307
+ program.command("status").description("Show project stats: report counts by severity and status").option("--json", "Machine-readable JSON output").action(async (opts) => {
1308
+ const config = requireConfig();
1309
+ const result = await apiCall("/v1/sync/stats", config);
1310
+ if (!result.ok) die(result);
1311
+ if (opts.json) {
1312
+ console.log(JSON.stringify(result.data, null, 2));
1313
+ return;
996
1314
  }
997
- const body = {};
998
- if (opts.status) body.status = opts.status;
999
- if (opts.severity) body.severity = opts.severity;
1000
- const data = await apiCall(`/v1/admin/reports/${id}`, config, { method: "PATCH", body: JSON.stringify(body) });
1001
- console.log(JSON.stringify(data, null, 2));
1315
+ const d = result.data;
1316
+ console.log(`Project: ${d.project_name}`);
1317
+ console.log("");
1318
+ console.log("Reports by status:");
1319
+ for (const [status, count] of Object.entries(d.by_status)) {
1320
+ console.log(` ${pad(status, 14)} ${count}`);
1321
+ }
1322
+ console.log("");
1323
+ console.log("Reports by severity:");
1324
+ for (const [severity, count] of Object.entries(d.by_severity)) {
1325
+ console.log(` ${pad(severity, 14)} ${count}`);
1326
+ }
1327
+ console.log("");
1328
+ console.log(`Fixes: ${d.fixes_count} total \xB7 ${d.fixes_merged} merged`);
1329
+ console.log(`Lessons: ${d.lessons_count} active rules`);
1002
1330
  });
1003
- program.command("config").description("View or update CLI config").argument("[key]", "Config key to set").argument("[value]", "Value").action((key, value) => {
1331
+ program.command("config").description("View or update CLI config (stored in ~/.mushirc)").argument("[key]", "Config key to set: apiKey | endpoint | projectId").argument("[value]", "New value").addHelpText("after", `
1332
+ Keys:
1333
+ apiKey \u2014 Mushi API key (mushi_...)
1334
+ endpoint \u2014 Supabase edge function URL
1335
+ projectId \u2014 Project UUID
1336
+
1337
+ Examples:
1338
+ mushi config # show all config
1339
+ mushi config apiKey mushi_xxx # set API key
1340
+ mushi config endpoint https://... # set endpoint
1341
+ mushi config projectId <uuid> # set project`).action((key, value) => {
1004
1342
  const config = loadConfig();
1343
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
1005
1344
  if (key && value) {
1345
+ if (!ALLOWED_KEYS.has(key)) {
1346
+ process.stderr.write(`error: unknown config key "${key}". Allowed: ${[...ALLOWED_KEYS].join(", ")}
1347
+ `);
1348
+ process.exit(2);
1349
+ }
1006
1350
  const safeValue = key === "endpoint" ? assertEndpoint(value) : value;
1007
1351
  config[key] = safeValue;
1008
1352
  saveConfig(config);
1009
- console.log(`Set ${key} = ${safeValue}`);
1353
+ console.log(`\u2713 Set ${key}`);
1010
1354
  } else {
1011
- console.log(JSON.stringify(config, null, 2));
1355
+ const safe = { ...config, apiKey: config.apiKey ? `${config.apiKey.slice(0, 10)}\u2026` : void 0 };
1356
+ console.log(JSON.stringify(safe, null, 2));
1012
1357
  }
1013
1358
  });
1014
1359
  var deploy = program.command("deploy").description("Deployment management");
1015
- deploy.command("check").description("Check edge function health").action(async () => {
1016
- const config = loadConfig();
1017
- if (!config.apiKey) {
1018
- console.error("Run `mushi login` first");
1019
- process.exit(1);
1020
- }
1021
- const endpoint = config.endpoint ?? "https://api.mushimushi.dev";
1360
+ deploy.command("check").description("Check edge function health and measure latency").option("--json", "Machine-readable JSON output").action(async (opts) => {
1361
+ const config = requireConfig();
1362
+ const t0 = Date.now();
1022
1363
  try {
1023
- const res = await fetch(`${endpoint}/health`);
1024
- console.log(`Health: ${res.status === 200 ? "OK" : "FAIL"} (${res.status})`);
1364
+ const controller = new AbortController();
1365
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1366
+ const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
1367
+ clearTimeout(timer);
1368
+ const latency = Date.now() - t0;
1369
+ const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
1370
+ if (opts.json) {
1371
+ console.log(JSON.stringify({ ok: res.ok, status: res.status, latency_ms: latency, ...body }));
1372
+ } else {
1373
+ console.log(`Health: ${res.status === 200 ? "OK" : "FAIL"} (${res.status}) \u2014 ${latency}ms`);
1374
+ if (body["version"]) console.log(` Version: ${body["version"]}`);
1375
+ if (body["region"]) console.log(` Region: ${body["region"]}`);
1376
+ }
1377
+ if (!res.ok) process.exit(1);
1025
1378
  } catch (err) {
1026
- console.error("Failed:", err);
1379
+ const msg = err instanceof Error ? err.message : String(err);
1380
+ if (opts.json) {
1381
+ console.log(JSON.stringify({ ok: false, error: msg }));
1382
+ } else {
1383
+ process.stderr.write(`error: ${msg}
1384
+ `);
1385
+ }
1386
+ process.exit(1);
1027
1387
  }
1028
1388
  });
1029
- program.command("index <path>").description("Walk a local repo and upload code chunks to the RAG indexer (non-GitHub fallback for V5.3 \xA72.3.4)").option("--language <lang>", "Limit to one language (ts, tsx, js, py, go, rs)").option("--dry-run", "Show what would be uploaded without sending").action(async (path, opts) => {
1030
- const config = loadConfig();
1031
- if (!config.apiKey) {
1032
- console.error("Run `mushi login` first");
1033
- process.exit(1);
1389
+ var reports = program.command("reports").description("Manage bug reports");
1390
+ 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", `
1391
+ Examples:
1392
+ mushi reports list
1393
+ mushi reports list --status new --severity critical
1394
+ mushi reports list --search "button not working" --limit 5 --json`).action(async (opts) => {
1395
+ const config = requireConfig();
1396
+ const limit = Math.min(Math.max(1, parseInt(opts.limit) || 20), 100);
1397
+ const params = new URLSearchParams({ limit: String(limit) });
1398
+ if (opts.status) params.set("status", opts.status);
1399
+ if (opts.severity) params.set("severity", opts.severity);
1400
+ if (opts.search) params.set("search", opts.search);
1401
+ const result = await apiCall(`/v1/sync/reports?${params}`, config);
1402
+ if (!result.ok) die(result);
1403
+ if (opts.json) {
1404
+ console.log(JSON.stringify(result.data, null, 2));
1405
+ return;
1034
1406
  }
1035
- if (!config.projectId) {
1036
- console.error("Set projectId via `mushi config projectId <id>`");
1037
- process.exit(1);
1407
+ const rows = result.data.reports;
1408
+ if (rows.length === 0) {
1409
+ console.log("No reports found.");
1410
+ return;
1411
+ }
1412
+ console.log(`${pad("ID", 38)} ${pad("SEV", 9)} ${pad("STATUS", 12)} ${pad("CREATED", 17)} SUMMARY`);
1413
+ console.log("\u2500".repeat(110));
1414
+ for (const r of rows) {
1415
+ const sev = r.severity ?? "unset";
1416
+ const status = r.status ?? "new";
1417
+ const summary = (r.summary ?? r.description ?? "").slice(0, 50);
1418
+ console.log(`${pad(r.id, 38)} ${pad(sev, 9)} ${pad(status, 12)} ${pad(fmtDate(r.created_at), 17)} ${summary}`);
1419
+ }
1420
+ if (result.data.total > rows.length) {
1421
+ console.log(`
1422
+ \u2026 ${result.data.total - rows.length} more. Use --limit to see more.`);
1423
+ }
1424
+ });
1425
+ reports.command("show <id>").description("Show full details for a single report").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
1426
+ const config = requireConfig();
1427
+ const result = await apiCall(`/v1/sync/reports/${id}`, config);
1428
+ if (!result.ok) {
1429
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1430
+ process.stderr.write(`error: report "${id}" not found
1431
+ `);
1432
+ process.exit(3);
1433
+ }
1434
+ die(result);
1435
+ }
1436
+ if (opts.json) {
1437
+ console.log(JSON.stringify(result.data, null, 2));
1438
+ return;
1439
+ }
1440
+ const r = result.data;
1441
+ console.log(`Report: ${r.id}`);
1442
+ console.log(` Status: ${r.status ?? "new"}`);
1443
+ console.log(` Severity: ${r.severity ?? "unset"}`);
1444
+ console.log(` Category: ${r.category ?? "\u2014"}`);
1445
+ console.log(` Created: ${fmtDate(r.created_at)}`);
1446
+ if (r.summary) console.log(` Summary: ${r.summary}`);
1447
+ if (r.description) {
1448
+ console.log(` Description:`);
1449
+ console.log(` ${r.description.replace(/\n/g, "\n ")}`);
1450
+ }
1451
+ if (r.environment?.url) console.log(` URL: ${r.environment.url}`);
1452
+ if (r.component) console.log(` Component: ${r.component}`);
1453
+ if (r.sentry_event_id) console.log(` Sentry: ${r.sentry_event_id}`);
1454
+ if (r.fix_id) console.log(` Fix: ${r.fix_id}`);
1455
+ if (r.tags && Object.keys(r.tags).length > 0) {
1456
+ console.log(` Tags: ${JSON.stringify(r.tags)}`);
1457
+ }
1458
+ });
1459
+ 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", `
1460
+ Examples:
1461
+ mushi reports triage <id> --status triaged --severity high
1462
+ mushi reports triage <id> --status in_progress --note "assigned to @alice"`).action(async (id, opts) => {
1463
+ const config = requireConfig();
1464
+ const body = {};
1465
+ if (opts.status) body["status"] = opts.status;
1466
+ if (opts.severity) body["severity"] = opts.severity;
1467
+ if (opts.note) body["note"] = opts.note;
1468
+ if (Object.keys(body).length === 0) {
1469
+ process.stderr.write("error: provide at least one of --status, --severity, or --note\n");
1470
+ process.exit(2);
1471
+ }
1472
+ const result = await apiCall(`/v1/sync/reports/${id}`, config, {
1473
+ method: "PATCH",
1474
+ body: JSON.stringify(body)
1475
+ });
1476
+ if (!result.ok) {
1477
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1478
+ process.stderr.write(`error: report "${id}" not found
1479
+ `);
1480
+ process.exit(3);
1481
+ }
1482
+ die(result);
1483
+ }
1484
+ if (opts.json) {
1485
+ console.log(JSON.stringify(result.data, null, 2));
1486
+ } else {
1487
+ console.log(`\u2713 Updated report ${id}`);
1488
+ if (opts.status) console.log(` Status: ${opts.status}`);
1489
+ if (opts.severity) console.log(` Severity: ${opts.severity}`);
1490
+ if (opts.note) console.log(` Note: ${opts.note}`);
1491
+ }
1492
+ });
1493
+ 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) => {
1494
+ const config = requireConfig();
1495
+ const body = { status: "resolved" };
1496
+ if (opts.note) body["note"] = opts.note;
1497
+ const result = await apiCall(`/v1/sync/reports/${id}`, config, {
1498
+ method: "PATCH",
1499
+ body: JSON.stringify(body)
1500
+ });
1501
+ if (!result.ok) {
1502
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1503
+ process.stderr.write(`error: report "${id}" not found
1504
+ `);
1505
+ process.exit(3);
1506
+ }
1507
+ die(result);
1508
+ }
1509
+ if (opts.json) {
1510
+ console.log(JSON.stringify(result.data, null, 2));
1511
+ } else {
1512
+ console.log(`\u2713 Resolved report ${id}`);
1513
+ if (opts.note) console.log(` Note: ${opts.note}`);
1514
+ }
1515
+ });
1516
+ 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) => {
1517
+ const config = requireConfig();
1518
+ const body = { status: "new" };
1519
+ if (opts.note) body["note"] = opts.note;
1520
+ const result = await apiCall(`/v1/sync/reports/${id}`, config, {
1521
+ method: "PATCH",
1522
+ body: JSON.stringify(body)
1523
+ });
1524
+ if (!result.ok) {
1525
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1526
+ process.stderr.write(`error: report "${id}" not found
1527
+ `);
1528
+ process.exit(3);
1529
+ }
1530
+ die(result);
1531
+ }
1532
+ if (opts.json) {
1533
+ console.log(JSON.stringify(result.data, null, 2));
1534
+ } else {
1535
+ console.log(`\u2713 Reopened report ${id}`);
1536
+ }
1537
+ });
1538
+ 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) => {
1539
+ const config = requireConfig();
1540
+ const body = { status: "dismissed" };
1541
+ if (opts.note) body["note"] = opts.note;
1542
+ const result = await apiCall(`/v1/sync/reports/${id}`, config, {
1543
+ method: "PATCH",
1544
+ body: JSON.stringify(body)
1545
+ });
1546
+ if (!result.ok) {
1547
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1548
+ process.stderr.write(`error: report "${id}" not found
1549
+ `);
1550
+ process.exit(3);
1551
+ }
1552
+ die(result);
1553
+ }
1554
+ if (opts.json) {
1555
+ console.log(JSON.stringify(result.data, null, 2));
1556
+ } else {
1557
+ console.log(`\u2713 Dismissed report ${id}`);
1558
+ }
1559
+ });
1560
+ 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", `
1561
+ Examples:
1562
+ mushi reports search "login button"
1563
+ mushi reports search "404 error" --status new --limit 20`).action(async (query, opts) => {
1564
+ const config = requireConfig();
1565
+ const limit = Math.min(Math.max(1, parseInt(opts.limit) || 10), 50);
1566
+ const params = new URLSearchParams({ search: query, limit: String(limit) });
1567
+ if (opts.status) params.set("status", opts.status);
1568
+ const result = await apiCall(`/v1/sync/reports?${params}`, config);
1569
+ if (!result.ok) die(result);
1570
+ if (opts.json) {
1571
+ console.log(JSON.stringify(result.data, null, 2));
1572
+ return;
1573
+ }
1574
+ const rows = result.data.reports;
1575
+ if (rows.length === 0) {
1576
+ console.log(`No reports matching "${query}".`);
1577
+ return;
1578
+ }
1579
+ console.log(`${rows.length} result${rows.length === 1 ? "" : "s"} for "${query}":`);
1580
+ console.log("");
1581
+ for (const r of rows) {
1582
+ console.log(` ${r.id}`);
1583
+ console.log(` ${r.severity ?? "unset"} \xB7 ${r.status ?? "new"} \xB7 ${fmtDate(r.created_at)}`);
1584
+ const text2 = r.summary ?? r.description ?? "";
1585
+ if (text2) console.log(` ${text2.slice(0, 80)}`);
1586
+ console.log("");
1587
+ }
1588
+ });
1589
+ var lessons = program.command("lessons").description("Manage learned mistake rules");
1590
+ lessons.command("list").description("List active lessons (mistake rules) for the current project").option("--severity <sev>", "Filter: info|warn|critical").option("--limit <n>", "Max results (1\u2013200)", "50").option("--json", "Machine-readable JSON output").addHelpText("after", `
1591
+ Lessons are mistake rules extracted from past bug reports by the clustering
1592
+ pipeline. They are injected into AI code-review context via the MCP server.`).action(async (opts) => {
1593
+ const config = requireConfig();
1594
+ const limit = Math.min(Math.max(1, parseInt(opts.limit) || 50), 200);
1595
+ const params = new URLSearchParams({ limit: String(limit) });
1596
+ if (opts.severity) params.set("severity", opts.severity);
1597
+ const result = await apiCall(`/v1/sync/lessons?${params}`, config);
1598
+ if (!result.ok) die(result);
1599
+ if (opts.json) {
1600
+ console.log(JSON.stringify(result.data, null, 2));
1601
+ return;
1602
+ }
1603
+ const rows = result.data;
1604
+ if (!Array.isArray(rows) || rows.length === 0) {
1605
+ console.log("No active lessons yet. Reports are clustered nightly.");
1606
+ return;
1607
+ }
1608
+ console.log(`${pad("SEV", 9)} ${pad("FREQ", 6)} RULE`);
1609
+ console.log("\u2500".repeat(90));
1610
+ for (const l of rows) {
1611
+ const sev = l.severity ?? "info";
1612
+ const freq = String(l.frequency ?? 0);
1613
+ const rule = (l.rule_text ?? "").slice(0, 70);
1614
+ console.log(`${pad(sev, 9)} ${pad(freq, 6)} ${rule}`);
1615
+ }
1616
+ console.log(`
1617
+ ${rows.length} active lesson${rows.length === 1 ? "" : "s"}`);
1618
+ });
1619
+ lessons.command("show <id>").description("Show full detail for a single lesson (rule text, anti-pattern, source reports)").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
1620
+ const config = requireConfig();
1621
+ const result = await apiCall(`/v1/sync/lessons/${id}`, config);
1622
+ if (!result.ok) {
1623
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1624
+ process.stderr.write(`error: lesson "${id}" not found
1625
+ `);
1626
+ process.exit(3);
1627
+ }
1628
+ die(result);
1629
+ }
1630
+ if (opts.json) {
1631
+ console.log(JSON.stringify(result.data, null, 2));
1632
+ return;
1633
+ }
1634
+ const l = result.data;
1635
+ console.log(`Lesson: ${l.id}`);
1636
+ console.log(` Severity: ${l.severity}`);
1637
+ console.log(` Frequency: ${l.frequency} reports`);
1638
+ if (l.last_reinforced_at) console.log(` Updated: ${fmtDate(l.last_reinforced_at)}`);
1639
+ console.log("");
1640
+ console.log(`Rule:`);
1641
+ console.log(` ${l.rule_text}`);
1642
+ if (l.anti_pattern) {
1643
+ console.log("");
1644
+ console.log(`Anti-pattern:`);
1645
+ console.log(` ${l.anti_pattern}`);
1646
+ }
1647
+ if (l.summary_paragraph) {
1648
+ console.log("");
1649
+ console.log(`Summary:`);
1650
+ console.log(` ${l.summary_paragraph}`);
1651
+ }
1652
+ });
1653
+ program.command("sync-lessons").description("Pull promoted lessons from Mushi and write .mushi/lessons.json into this repo").option("--cwd <path>", "Target directory (default: current working dir)").option("--dry-run", "Print the JSON that would be written without writing anything").option("--json", "Machine-readable output: { ok, path, count }").addHelpText("after", `
1654
+ Used in CI to keep .mushi/lessons.json up to date so the Mushi MCP server
1655
+ and Cursor rules can inject the latest project-specific mistake rules into
1656
+ AI code review context.
1657
+
1658
+ Typical CI usage:
1659
+ MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
1660
+ npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
1661
+ const { writeFile, mkdir } = await import("fs/promises");
1662
+ const nodePath = await import("path");
1663
+ const config = requireConfig();
1664
+ const cwd = opts.cwd ?? process.cwd();
1665
+ const target = nodePath.join(cwd, ".mushi", "lessons.json");
1666
+ const result = await apiCall("/v1/sync/lessons?limit=500", config);
1667
+ if (!result.ok) die(result);
1668
+ const rows = Array.isArray(result.data) ? result.data : [];
1669
+ const lessons2 = rows.map((l) => ({
1670
+ id: l.id,
1671
+ rule: l.rule_text,
1672
+ anti_pattern: l.anti_pattern ?? void 0,
1673
+ severity: l.severity,
1674
+ frequency: l.frequency,
1675
+ last_reinforced: l.last_reinforced_at?.slice(0, 10) ?? "",
1676
+ cluster_id: l.cluster_id ?? void 0
1677
+ }));
1678
+ const output = {
1679
+ schema_version: "1",
1680
+ project_id: config.projectId ?? "",
1681
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1682
+ lessons: lessons2
1683
+ };
1684
+ if (opts.dryRun) {
1685
+ console.log(JSON.stringify(output, null, 2));
1686
+ return;
1687
+ }
1688
+ await mkdir(nodePath.dirname(target), { recursive: true });
1689
+ await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1690
+ if (opts.json) {
1691
+ console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
1692
+ } else {
1693
+ console.log(`\u2713 Wrote ${lessons2.length} lesson${lessons2.length === 1 ? "" : "s"} to ${target}`);
1694
+ }
1695
+ });
1696
+ program.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) => {
1697
+ const config = requireConfig();
1698
+ const result = await apiCall("/v1/reports", config, {
1699
+ method: "POST",
1700
+ body: JSON.stringify({
1701
+ projectId: config.projectId,
1702
+ description: "CLI test report \u2014 verifying ingestion pipeline",
1703
+ category: "other",
1704
+ reporterToken: `cli-test-${Date.now()}`,
1705
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1706
+ environment: {
1707
+ url: "cli://test",
1708
+ userAgent: `mushi-cli/${MUSHI_CLI_VERSION}`,
1709
+ platform: process.platform,
1710
+ language: "en",
1711
+ viewport: { width: 0, height: 0 },
1712
+ referrer: "",
1713
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1714
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
1715
+ }
1716
+ })
1717
+ });
1718
+ if (!result.ok) die(result);
1719
+ if (opts.json) {
1720
+ console.log(JSON.stringify(result.data, null, 2));
1721
+ } else {
1722
+ const d = result.data;
1723
+ console.log(`\u2713 Test report submitted`);
1724
+ console.log(` ID: ${d.reportId}`);
1725
+ console.log(` Status: ${d.status}`);
1726
+ console.log(` View: https://kensaur.us/mushi-mushi/reports/${d.reportId}`);
1038
1727
  }
1039
- const { readdir, readFile, stat } = await import("fs/promises");
1728
+ });
1729
+ program.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", `
1730
+ Uploads source code into the Mushi vector index so the fix-worker can
1731
+ retrieve relevant context when generating patches. Only needed for private
1732
+ repos that cannot be auto-indexed via GitHub App.
1733
+
1734
+ Examples:
1735
+ mushi index ./src
1736
+ mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
1737
+ const config = requireConfig({ needsProject: true });
1738
+ const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
1040
1739
  const nodePath = await import("path");
1041
1740
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
1042
1741
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
1742
+ const MAX_FILE_BYTES = 5e5;
1043
1743
  async function* walk(dir) {
1044
- const entries = await readdir(dir, { withFileTypes: true });
1744
+ const entries = await readdir2(dir, { withFileTypes: true });
1045
1745
  for (const e of entries) {
1046
1746
  const full = nodePath.join(dir, e.name);
1047
1747
  if (SKIP.test(full)) continue;
@@ -1051,60 +1751,60 @@ program.command("index <path>").description("Walk a local repo and upload code c
1051
1751
  }
1052
1752
  let count = 0;
1053
1753
  let bytes = 0;
1754
+ let errors = 0;
1054
1755
  const root = nodePath.resolve(path);
1055
1756
  for await (const file of walk(root)) {
1056
1757
  const lang = nodePath.extname(file).slice(1);
1057
1758
  if (opts.language && opts.language !== lang) continue;
1058
1759
  const stats = await stat(file);
1059
- if (stats.size > 5e5) continue;
1060
- const source = await readFile(file, "utf8");
1061
- const relative = nodePath.relative(root, file).replaceAll("\\", "/");
1760
+ if (stats.size > MAX_FILE_BYTES) {
1761
+ if (!opts.json) process.stdout.write(` skip ${nodePath.relative(root, file)} (>${MAX_FILE_BYTES / 1e3}KB)
1762
+ `);
1763
+ continue;
1764
+ }
1765
+ const source = await readFile2(file, "utf8");
1766
+ const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
1062
1767
  count++;
1063
1768
  bytes += source.length;
1064
1769
  if (opts.dryRun) {
1065
- console.log(` ${relative} (${source.length} bytes)`);
1770
+ if (!opts.json) process.stdout.write(` ${relative2} (${source.length} bytes)
1771
+ `);
1066
1772
  continue;
1067
1773
  }
1068
- const res = await apiCall("/v1/admin/codebase/upload", config, {
1774
+ const result = await apiCall("/v1/sync/codebase/upload", config, {
1069
1775
  method: "POST",
1070
- body: JSON.stringify({
1071
- projectId: config.projectId,
1072
- filePath: relative,
1073
- source
1074
- })
1776
+ body: JSON.stringify({ projectId: config.projectId, filePath: relative2, source })
1075
1777
  });
1076
- if (!res.ok) console.error(` FAIL ${relative}: ${res.error ?? "unknown"}`);
1077
- else process.stdout.write(` ${relative} \u2192 ${res.chunks ?? 0} chunks
1778
+ if (!result.ok) {
1779
+ errors++;
1780
+ process.stderr.write(` FAIL ${relative2}: ${result.error.message}
1078
1781
  `);
1782
+ } else if (!opts.json) {
1783
+ process.stdout.write(` ok ${relative2} \u2192 ${result.data.chunks} chunks
1784
+ `);
1785
+ }
1079
1786
  }
1080
- console.log(`Indexed ${count} files (${(bytes / 1024).toFixed(1)} KB) into project ${config.projectId}`);
1081
- });
1082
- program.command("test").description("Submit a test report to verify pipeline").action(async () => {
1083
- const config = loadConfig();
1084
- if (!config.apiKey) {
1085
- console.error("Run `mushi login` first");
1086
- process.exit(1);
1787
+ if (opts.json) {
1788
+ console.log(JSON.stringify({ ok: errors === 0, files: count, bytes, errors }));
1789
+ } else {
1790
+ const kb = (bytes / 1024).toFixed(1);
1791
+ console.log(`
1792
+ Indexed ${count} files (${kb} KB) into project ${config.projectId}${errors ? ` \u2014 ${errors} failed` : ""}`);
1087
1793
  }
1088
- const data = await apiCall("/v1/reports", config, {
1089
- method: "POST",
1090
- body: JSON.stringify({
1091
- projectId: config.projectId,
1092
- description: "CLI test report \u2014 verifying pipeline",
1093
- category: "other",
1094
- reporterToken: `cli-test-${Date.now()}`,
1095
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1096
- environment: {
1097
- url: "cli://test",
1098
- userAgent: "mushi-cli",
1099
- platform: process.platform,
1100
- language: "en",
1101
- viewport: { width: 0, height: 0 },
1102
- referrer: "",
1103
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1104
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
1105
- }
1106
- })
1794
+ if (errors > 0) process.exit(1);
1795
+ });
1796
+ var sourcemaps = program.command("sourcemaps").description("Source map management");
1797
+ 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", `
1798
+ Examples:
1799
+ mushi sourcemaps upload --release 1.0.0
1800
+ mushi sourcemaps upload --release $(git rev-parse --short HEAD) --dir ./dist`).action(async (opts) => {
1801
+ await runSourcemapsUpload({
1802
+ release: opts.release,
1803
+ dir: opts.dir,
1804
+ dryRun: opts.dryRun,
1805
+ endpoint: opts.endpoint,
1806
+ apiKey: opts.apiKey,
1807
+ silent: opts.silent
1107
1808
  });
1108
- console.log("Test report submitted:", JSON.stringify(data, null, 2));
1109
1809
  });
1110
1810
  program.parse();