@mushi-mushi/cli 0.7.0 → 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 });
@@ -457,11 +464,11 @@ function getFrameworkFromPkg(pkg) {
457
464
  }
458
465
 
459
466
  // src/version.ts
460
- var MUSHI_CLI_VERSION = true ? "0.7.0" : "0.0.0-dev";
467
+ var MUSHI_CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
461
468
 
462
469
  // src/init.ts
463
470
  var ENV_FILES = [".env.local", ".env"];
464
- 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;
465
472
  var API_KEY_PATTERN = /^mushi_[A-Za-z0-9_-]{10,}$/;
466
473
  async function runInit(options = {}) {
467
474
  const cwd = options.cwd ?? process.cwd();
@@ -506,7 +513,7 @@ function ensureInteractiveOrBailOut(options) {
506
513
  );
507
514
  if (hasAllFlags) return;
508
515
  process.stderr.write(
509
- "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"
510
517
  );
511
518
  process.exit(1);
512
519
  }
@@ -540,21 +547,22 @@ async function collectCredentials(options) {
540
547
  const existing = loadConfig();
541
548
  const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
542
549
  message: "Project ID",
543
- placeholder: "proj_xxxxxxxxxxxx",
544
- hint: "Find this at https://kensaur.us/mushi-mushi/projects",
545
- 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."
546
553
  });
547
554
  const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
548
555
  message: "API key",
549
556
  placeholder: "mushi_xxxxxxxxxxxx",
550
- 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.",
551
558
  validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
552
559
  });
553
560
  const projectId = sanitizeSecret(rawProjectId);
554
561
  const apiKey = sanitizeSecret(rawApiKey);
555
562
  if (!PROJECT_ID_PATTERN.test(projectId)) {
556
563
  throw new Error(
557
- `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`
558
566
  );
559
567
  }
560
568
  if (!API_KEY_PATTERN.test(apiKey)) {
@@ -572,6 +580,7 @@ function redact(value) {
572
580
  return `${value.slice(0, 4)}\u2026${value.slice(-2)}`;
573
581
  }
574
582
  async function promptText(opts) {
583
+ if (opts.hint) p.log.info(opts.hint);
575
584
  const value = await p.text({
576
585
  message: opts.message,
577
586
  placeholder: opts.placeholder,
@@ -588,7 +597,6 @@ async function promptText(opts) {
588
597
  p.cancel("Aborted.");
589
598
  process.exit(0);
590
599
  }
591
- if (opts.hint) p.log.info(opts.hint);
592
600
  return value;
593
601
  }
594
602
  async function installPackages(pm, packages, cwd) {
@@ -1090,27 +1098,142 @@ async function runSourcemapsUpload(opts) {
1090
1098
  }
1091
1099
 
1092
1100
  // src/index.ts
1101
+ var API_TIMEOUT_MS = 15e3;
1093
1102
  async function apiCall(path, config, options = {}) {
1094
1103
  const endpoint = config.endpoint;
1095
1104
  if (!endpoint) {
1096
- throw new Error(
1097
- "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"
1098
- );
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
+ };
1099
1112
  }
1100
- const res = await fetch(`${endpoint}${path}`, {
1101
- ...options,
1102
- headers: {
1103
- "Content-Type": "application/json",
1104
- "Authorization": `Bearer ${config.apiKey}`,
1105
- "X-Mushi-Api-Key": config.apiKey ?? "",
1106
- "X-Mushi-Project": config.projectId ?? "",
1107
- ...options.headers
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
+ }
1108
1152
  }
1109
- });
1110
- 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;
1111
1213
  }
1112
- var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION);
1113
- 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) => {
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);
1220
+ }
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) => {
1114
1237
  await runInit({
1115
1238
  projectId: opts.projectId,
1116
1239
  apiKey: opts.apiKey,
@@ -1122,116 +1245,501 @@ program.command("init").description("Set up the Mushi Mushi SDK in this project
1122
1245
  sendTestReport: opts.skipTestReport ? false : void 0
1123
1246
  });
1124
1247
  });
1125
- program.command("migrate").description(
1126
- "Suggest the most relevant Mushi Mushi migration guide based on your package.json"
1127
- ).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) => {
1128
1249
  const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
1129
1250
  if (matches.length === 0) process.exit(1);
1130
1251
  });
1131
- 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) => {
1132
1256
  const config = loadConfig();
1133
1257
  config.apiKey = opts.apiKey;
1134
1258
  if (opts.endpoint) config.endpoint = assertEndpoint(opts.endpoint);
1135
1259
  if (opts.projectId) config.projectId = opts.projectId;
1136
1260
  saveConfig(config);
1137
- console.log("Saved credentials to ~/.mushirc (mode 0o600)");
1261
+ console.log("\u2713 Credentials saved to ~/.mushirc (mode 0o600)");
1262
+ console.log(" Run 'mushi whoami' to verify the connection.");
1138
1263
  });
1139
- program.command("status").description("Show project stats").action(async () => {
1140
- const config = loadConfig();
1141
- if (!config.apiKey) {
1142
- console.error("Run `mushi login` first");
1143
- process.exit(1);
1144
- }
1145
- const data = await apiCall("/v1/admin/stats", config);
1146
- console.log(JSON.stringify(data, null, 2));
1147
- });
1148
- var reports = program.command("reports").description("Manage bug reports");
1149
- 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) => {
1150
- const config = loadConfig();
1151
- if (!config.apiKey) {
1152
- console.error("Run `mushi login` first");
1153
- process.exit(1);
1154
- }
1155
- const params = new URLSearchParams();
1156
- params.set("limit", opts.limit);
1157
- if (opts.status) params.set("status", opts.status);
1158
- 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);
1159
1270
  if (opts.json) {
1160
- console.log(JSON.stringify(data, null, 2));
1271
+ console.log(JSON.stringify(result.data, null, 2));
1161
1272
  } else {
1162
- const reports2 = data.data?.reports ?? [];
1163
- for (const r of reports2) {
1164
- console.log(`${r.id} ${r.severity ?? "unset"} ${r.status ?? "new"} ${(r.summary ?? "").slice(0, 60)}`);
1165
- }
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`);
1166
1278
  }
1167
1279
  });
1168
- reports.command("show <id>").description("Show report details").action(async (id) => {
1169
- const config = loadConfig();
1170
- if (!config.apiKey) {
1171
- 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
+ }
1172
1304
  process.exit(1);
1173
1305
  }
1174
- const data = await apiCall(`/v1/admin/reports/${id}`, config);
1175
- console.log(JSON.stringify(data, null, 2));
1176
1306
  });
1177
- reports.command("triage <id>").description("Update report status/severity").option("--status <status>", "New status").option("--severity <severity>", "New severity").action(async (id, opts) => {
1178
- const config = loadConfig();
1179
- if (!config.apiKey) {
1180
- console.error("Run `mushi login` first");
1181
- 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;
1182
1314
  }
1183
- const body = {};
1184
- if (opts.status) body.status = opts.status;
1185
- if (opts.severity) body.severity = opts.severity;
1186
- const data = await apiCall(`/v1/admin/reports/${id}`, config, { method: "PATCH", body: JSON.stringify(body) });
1187
- 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`);
1188
1330
  });
1189
- 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) => {
1190
1342
  const config = loadConfig();
1343
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
1191
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
+ }
1192
1350
  const safeValue = key === "endpoint" ? assertEndpoint(value) : value;
1193
1351
  config[key] = safeValue;
1194
1352
  saveConfig(config);
1195
- console.log(`Set ${key} = ${safeValue}`);
1353
+ console.log(`\u2713 Set ${key}`);
1196
1354
  } else {
1197
- 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));
1198
1357
  }
1199
1358
  });
1200
1359
  var deploy = program.command("deploy").description("Deployment management");
1201
- deploy.command("check").description("Check edge function health").action(async () => {
1202
- const config = loadConfig();
1203
- if (!config.apiKey) {
1204
- console.error("Run `mushi login` first");
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();
1363
+ try {
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);
1378
+ } catch (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
+ }
1205
1386
  process.exit(1);
1206
1387
  }
1207
- if (!config.endpoint) {
1208
- console.error(
1209
- "No API endpoint configured. Run `mushi init` or set MUSHI_API_ENDPOINT.\nSet endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api"
1210
- );
1211
- process.exit(1);
1388
+ });
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;
1212
1406
  }
1213
- const endpoint = config.endpoint;
1214
- try {
1215
- const res = await fetch(`${endpoint}/health`);
1216
- console.log(`Health: ${res.status === 200 ? "OK" : "FAIL"} (${res.status})`);
1217
- } catch (err) {
1218
- console.error("Failed:", err);
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.`);
1219
1423
  }
1220
1424
  });
1221
- 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) => {
1222
- const config = loadConfig();
1223
- if (!config.apiKey) {
1224
- console.error("Run `mushi login` first");
1225
- process.exit(1);
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);
1226
1435
  }
1227
- if (!config.projectId) {
1228
- console.error("Set projectId via `mushi config projectId <id>`");
1229
- process.exit(1);
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}`);
1230
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}`);
1727
+ }
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 });
1231
1738
  const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
1232
1739
  const nodePath = await import("path");
1233
1740
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
1234
1741
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
1742
+ const MAX_FILE_BYTES = 5e5;
1235
1743
  async function* walk(dir) {
1236
1744
  const entries = await readdir2(dir, { withFileTypes: true });
1237
1745
  for (const e of entries) {
@@ -1243,64 +1751,53 @@ program.command("index <path>").description("Walk a local repo and upload code c
1243
1751
  }
1244
1752
  let count = 0;
1245
1753
  let bytes = 0;
1754
+ let errors = 0;
1246
1755
  const root = nodePath.resolve(path);
1247
1756
  for await (const file of walk(root)) {
1248
1757
  const lang = nodePath.extname(file).slice(1);
1249
1758
  if (opts.language && opts.language !== lang) continue;
1250
1759
  const stats = await stat(file);
1251
- if (stats.size > 5e5) continue;
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
+ }
1252
1765
  const source = await readFile2(file, "utf8");
1253
1766
  const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
1254
1767
  count++;
1255
1768
  bytes += source.length;
1256
1769
  if (opts.dryRun) {
1257
- console.log(` ${relative2} (${source.length} bytes)`);
1770
+ if (!opts.json) process.stdout.write(` ${relative2} (${source.length} bytes)
1771
+ `);
1258
1772
  continue;
1259
1773
  }
1260
- const res = await apiCall("/v1/admin/codebase/upload", config, {
1774
+ const result = await apiCall("/v1/sync/codebase/upload", config, {
1261
1775
  method: "POST",
1262
- body: JSON.stringify({
1263
- projectId: config.projectId,
1264
- filePath: relative2,
1265
- source
1266
- })
1776
+ body: JSON.stringify({ projectId: config.projectId, filePath: relative2, source })
1267
1777
  });
1268
- if (!res.ok) console.error(` FAIL ${relative2}: ${res.error ?? "unknown"}`);
1269
- else process.stdout.write(` ${relative2} \u2192 ${res.chunks ?? 0} chunks
1778
+ if (!result.ok) {
1779
+ errors++;
1780
+ process.stderr.write(` FAIL ${relative2}: ${result.error.message}
1270
1781
  `);
1782
+ } else if (!opts.json) {
1783
+ process.stdout.write(` ok ${relative2} \u2192 ${result.data.chunks} chunks
1784
+ `);
1785
+ }
1271
1786
  }
1272
- console.log(`Indexed ${count} files (${(bytes / 1024).toFixed(1)} KB) into project ${config.projectId}`);
1273
- });
1274
- program.command("test").description("Submit a test report to verify pipeline").action(async () => {
1275
- const config = loadConfig();
1276
- if (!config.apiKey) {
1277
- console.error("Run `mushi login` first");
1278
- 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` : ""}`);
1279
1793
  }
1280
- const data = await apiCall("/v1/reports", config, {
1281
- method: "POST",
1282
- body: JSON.stringify({
1283
- projectId: config.projectId,
1284
- description: "CLI test report \u2014 verifying pipeline",
1285
- category: "other",
1286
- reporterToken: `cli-test-${Date.now()}`,
1287
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1288
- environment: {
1289
- url: "cli://test",
1290
- userAgent: "mushi-cli",
1291
- platform: process.platform,
1292
- language: "en",
1293
- viewport: { width: 0, height: 0 },
1294
- referrer: "",
1295
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1296
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
1297
- }
1298
- })
1299
- });
1300
- console.log("Test report submitted:", JSON.stringify(data, null, 2));
1794
+ if (errors > 0) process.exit(1);
1301
1795
  });
1302
1796
  var sourcemaps = program.command("sourcemaps").description("Source map management");
1303
- sourcemaps.command("upload").description("Upload source map files to the Mushi platform (idempotent, sha256-keyed)").requiredOption("--release <version>", "Release version (e.g. 1.0.0 or git SHA)").option("--dir <path>", "Directory containing source maps", "./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 (exit code still reflects failure)").action(async (opts) => {
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) => {
1304
1801
  await runSourcemapsUpload({
1305
1802
  release: opts.release,
1306
1803
  dir: opts.dir,
@@ -1310,53 +1807,4 @@ sourcemaps.command("upload").description("Upload source map files to the Mushi p
1310
1807
  silent: opts.silent
1311
1808
  });
1312
1809
  });
1313
- program.command("sync-lessons").description("Sync promoted lessons from Mushi into .mushi/lessons.json in this repo").option("--cwd <path>", "Target directory (default: current working dir)").option("--dry-run", "Print what would be written without writing").option("--json", "Machine-readable JSON output").action(async (opts) => {
1314
- const { writeFile, mkdir } = await import("fs/promises");
1315
- const nodePath = await import("path");
1316
- const config = loadConfig();
1317
- if (!config.apiKey) {
1318
- console.error("Run `mushi login` first");
1319
- process.exit(1);
1320
- }
1321
- if (!config.projectId) {
1322
- console.error("Set projectId via `mushi config projectId <id>`");
1323
- process.exit(1);
1324
- }
1325
- const cwd = opts.cwd ?? process.cwd();
1326
- const target = nodePath.join(cwd, ".mushi", "lessons.json");
1327
- const res = await apiCall(
1328
- `/v1/admin/lessons?projectId=${config.projectId}&limit=500`,
1329
- config
1330
- );
1331
- if (!res.ok || !res.data) {
1332
- console.error("Failed to fetch lessons:", res.error ?? JSON.stringify(res));
1333
- process.exit(1);
1334
- }
1335
- const lessons = res.data.map((l) => ({
1336
- id: l.id,
1337
- rule: l.rule_text,
1338
- anti_pattern: l.anti_pattern ?? void 0,
1339
- severity: l.severity,
1340
- frequency: l.frequency,
1341
- last_reinforced: l.last_reinforced_at?.slice(0, 10) ?? "",
1342
- cluster_id: l.cluster_id ?? void 0
1343
- }));
1344
- const output = {
1345
- schema_version: "1",
1346
- project_id: config.projectId,
1347
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1348
- lessons
1349
- };
1350
- if (opts.dryRun) {
1351
- console.log(JSON.stringify(output, null, 2));
1352
- return;
1353
- }
1354
- await mkdir(nodePath.dirname(target), { recursive: true });
1355
- await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1356
- if (opts.json) {
1357
- console.log(JSON.stringify({ ok: true, path: target, count: lessons.length }));
1358
- } else {
1359
- console.log(`\u2713 Wrote ${lessons.length} lessons to ${target}`);
1360
- }
1361
- });
1362
1810
  program.parse();