@mushi-mushi/cli 0.7.0 → 0.10.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
@@ -4,24 +4,79 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/config.ts
7
- import { chmodSync, readFileSync, statSync, writeFileSync, existsSync } from "fs";
8
- import { join } from "path";
7
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
9
8
  import { homedir } from "os";
10
- var CONFIG_PATH = join(homedir(), ".mushirc");
9
+ import { dirname, join } from "path";
11
10
  var SECURE_FILE_MODE = 384;
11
+ var SECURE_DIR_MODE = 448;
12
+ function resolveXdgConfigPath() {
13
+ const xdg = process.env["XDG_CONFIG_HOME"];
14
+ if (xdg && xdg.length > 0) {
15
+ return join(xdg, "mushi", "config.json");
16
+ }
17
+ if (process.platform === "win32") {
18
+ const appData = process.env["APPDATA"];
19
+ if (appData && appData.length > 0) {
20
+ return join(appData, "mushi", "config.json");
21
+ }
22
+ }
23
+ return join(homedir(), ".config", "mushi", "config.json");
24
+ }
25
+ var LEGACY_CONFIG_PATH = join(homedir(), ".mushirc");
26
+ var CONFIG_PATH = resolveXdgConfigPath();
12
27
  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 {};
28
+ let file = {};
29
+ if (existsSync(path)) {
30
+ tightenPermissions(path);
31
+ try {
32
+ file = JSON.parse(readFileSync(path, "utf-8"));
33
+ } catch {
34
+ }
35
+ } else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
36
+ file = migrateLegacyConfig() ?? {};
19
37
  }
38
+ const fromEnv = {
39
+ ...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
40
+ ...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
41
+ ...process.env["MUSHI_API_ENDPOINT"] ? { endpoint: process.env["MUSHI_API_ENDPOINT"] } : {}
42
+ };
43
+ return { ...file, ...fromEnv };
20
44
  }
21
45
  function saveConfig(config, path = CONFIG_PATH) {
46
+ const dir = dirname(path);
47
+ if (!existsSync(dir)) {
48
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
49
+ } else {
50
+ tightenDirPermissions(dir);
51
+ }
22
52
  writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
23
53
  tightenPermissions(path);
24
54
  }
55
+ function migrateLegacyConfig(legacyPath = LEGACY_CONFIG_PATH, destPath = CONFIG_PATH) {
56
+ if (!existsSync(legacyPath)) return null;
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(readFileSync(legacyPath, "utf-8"));
60
+ } catch {
61
+ return null;
62
+ }
63
+ const dir = dirname(destPath);
64
+ if (!existsSync(dir)) {
65
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
66
+ }
67
+ try {
68
+ renameSync(legacyPath, destPath);
69
+ } catch {
70
+ writeFileSync(destPath, JSON.stringify(parsed, null, 2), { mode: SECURE_FILE_MODE });
71
+ try {
72
+ unlinkSync(legacyPath);
73
+ } catch {
74
+ }
75
+ }
76
+ tightenPermissions(destPath);
77
+ tightenDirPermissions(dir);
78
+ return parsed;
79
+ }
25
80
  function tightenPermissions(path) {
26
81
  if (process.platform === "win32") return;
27
82
  try {
@@ -30,6 +85,14 @@ function tightenPermissions(path) {
30
85
  } catch {
31
86
  }
32
87
  }
88
+ function tightenDirPermissions(path) {
89
+ if (process.platform === "win32") return;
90
+ try {
91
+ const current = statSync(path).mode & 511;
92
+ if (current !== SECURE_DIR_MODE) chmodSync(path, SECURE_DIR_MODE);
93
+ } catch {
94
+ }
95
+ }
33
96
 
34
97
  // src/init.ts
35
98
  import * as p from "@clack/prompts";
@@ -361,7 +424,7 @@ function parse(version) {
361
424
 
362
425
  // src/monorepo.ts
363
426
  import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "fs";
364
- import { dirname, join as join3, resolve } from "path";
427
+ import { dirname as dirname2, join as join3, resolve } from "path";
365
428
  var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
366
429
  var FRAMEWORK_DEPS = {
367
430
  next: "Next.js",
@@ -389,7 +452,7 @@ function findWorkspaceRoot(start) {
389
452
  let dir = resolve(start);
390
453
  for (let i = 0; i < 8; i++) {
391
454
  if (isWorkspaceRoot(dir)) return dir;
392
- const parent = dirname(dir);
455
+ const parent = dirname2(dir);
393
456
  if (parent === dir) break;
394
457
  dir = parent;
395
458
  }
@@ -457,11 +520,11 @@ function getFrameworkFromPkg(pkg) {
457
520
  }
458
521
 
459
522
  // src/version.ts
460
- var MUSHI_CLI_VERSION = true ? "0.7.0" : "0.0.0-dev";
523
+ var MUSHI_CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
461
524
 
462
525
  // src/init.ts
463
526
  var ENV_FILES = [".env.local", ".env"];
464
- var PROJECT_ID_PATTERN = /^proj_[A-Za-z0-9_-]{10,}$/;
527
+ 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
528
  var API_KEY_PATTERN = /^mushi_[A-Za-z0-9_-]{10,}$/;
466
529
  async function runInit(options = {}) {
467
530
  const cwd = options.cwd ?? process.cwd();
@@ -506,7 +569,7 @@ function ensureInteractiveOrBailOut(options) {
506
569
  );
507
570
  if (hasAllFlags) return;
508
571
  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"
572
+ "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
573
  );
511
574
  process.exit(1);
512
575
  }
@@ -540,21 +603,22 @@ async function collectCredentials(options) {
540
603
  const existing = loadConfig();
541
604
  const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
542
605
  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"
606
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
607
+ hint: "Where to find it: https://kensaur.us/mushi-mushi/projects \u2192 click your project \u2192 copy the UUID below the project name.",
608
+ 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
609
  });
547
610
  const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
548
611
  message: "API key",
549
612
  placeholder: "mushi_xxxxxxxxxxxx",
550
- hint: "Treat this like a password \u2014 it goes in your env file, not in source.",
613
+ 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
614
  validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
552
615
  });
553
616
  const projectId = sanitizeSecret(rawProjectId);
554
617
  const apiKey = sanitizeSecret(rawApiKey);
555
618
  if (!PROJECT_ID_PATTERN.test(projectId)) {
556
619
  throw new Error(
557
- `Invalid project ID. Expected format: proj_[A-Za-z0-9_-]{10,}. Got: ${redact(projectId)}`
620
+ `Invalid project ID. Got: ${redact(projectId)}
621
+ 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
622
  );
559
623
  }
560
624
  if (!API_KEY_PATTERN.test(apiKey)) {
@@ -572,6 +636,7 @@ function redact(value) {
572
636
  return `${value.slice(0, 4)}\u2026${value.slice(-2)}`;
573
637
  }
574
638
  async function promptText(opts) {
639
+ if (opts.hint) p.log.info(opts.hint);
575
640
  const value = await p.text({
576
641
  message: opts.message,
577
642
  placeholder: opts.placeholder,
@@ -588,7 +653,6 @@ async function promptText(opts) {
588
653
  p.cancel("Aborted.");
589
654
  process.exit(0);
590
655
  }
591
- if (opts.hint) p.log.info(opts.hint);
592
656
  return value;
593
657
  }
594
658
  async function installPackages(pm, packages, cwd) {
@@ -1089,28 +1153,225 @@ async function runSourcemapsUpload(opts) {
1089
1153
  if (failed > 0) process.exit(1);
1090
1154
  }
1091
1155
 
1156
+ // src/errors.ts
1157
+ var EXIT_CODE_MAP = {
1158
+ E_AUTH_MISSING: 2,
1159
+ E_AUTH_INVALID: 2,
1160
+ E_PROJECT_MISSING: 2,
1161
+ E_ENDPOINT_INVALID: 2,
1162
+ E_INVALID_INPUT: 2,
1163
+ E_NOT_INTERACTIVE: 2,
1164
+ E_NETWORK: 3,
1165
+ E_TIMEOUT: 3,
1166
+ E_API_ERROR: 3,
1167
+ E_RATE_LIMITED: 3,
1168
+ E_FILE_NOT_FOUND: 1,
1169
+ E_FILE_PERMISSION: 1,
1170
+ E_FRESHNESS_STALE: 1,
1171
+ E_INTERRUPTED: 130,
1172
+ // 128 + SIGINT (2). POSIX convention.
1173
+ E_INTERNAL: 1
1174
+ };
1175
+ var MushiCliError = class extends Error {
1176
+ code;
1177
+ hint;
1178
+ cause;
1179
+ constructor(code, message, hint, cause) {
1180
+ super(message);
1181
+ this.name = "MushiCliError";
1182
+ this.code = code;
1183
+ if (hint) this.hint = hint;
1184
+ if (cause !== void 0) this.cause = cause;
1185
+ }
1186
+ /** Exit code POSIX-aware shell scripts should branch on. */
1187
+ get exitCode() {
1188
+ return EXIT_CODE_MAP[this.code];
1189
+ }
1190
+ /**
1191
+ * Format for `--json` output. Used by the JSON writer when a CLI
1192
+ * subcommand was invoked with `--json` and an error escapes.
1193
+ */
1194
+ toJSON() {
1195
+ return {
1196
+ error: {
1197
+ code: this.code,
1198
+ message: this.message,
1199
+ ...this.hint ? { hint: this.hint } : {}
1200
+ }
1201
+ };
1202
+ }
1203
+ };
1204
+
1205
+ // src/signals.ts
1206
+ var installed = false;
1207
+ var activeController = null;
1208
+ function installSignalHandlers() {
1209
+ if (installed) return;
1210
+ installed = true;
1211
+ const abortAndExit = (signal) => {
1212
+ activeController?.abort(
1213
+ new MushiCliError(
1214
+ "E_INTERRUPTED",
1215
+ `aborted by ${signal}`,
1216
+ "partial state on the server is safe to retry \u2014 re-run the same command"
1217
+ )
1218
+ );
1219
+ const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
1220
+ process.nextTick(() => {
1221
+ process.exit(code);
1222
+ });
1223
+ };
1224
+ process.on("SIGINT", () => abortAndExit("SIGINT"));
1225
+ process.on("SIGTERM", () => abortAndExit("SIGTERM"));
1226
+ }
1227
+ function getAbortSignal(external) {
1228
+ if (external) return external.signal;
1229
+ if (!activeController || activeController.signal.aborted) {
1230
+ activeController = new AbortController();
1231
+ }
1232
+ return activeController.signal;
1233
+ }
1234
+
1092
1235
  // src/index.ts
1236
+ installSignalHandlers();
1237
+ var API_TIMEOUT_MS = 15e3;
1093
1238
  async function apiCall(path, config, options = {}) {
1094
1239
  const endpoint = config.endpoint;
1095
1240
  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
- );
1241
+ return {
1242
+ ok: false,
1243
+ error: {
1244
+ code: "NO_ENDPOINT",
1245
+ 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"
1246
+ }
1247
+ };
1099
1248
  }
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
1249
+ const controller = new AbortController();
1250
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1251
+ const signals = [controller.signal, getAbortSignal()];
1252
+ const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
1253
+ try {
1254
+ const res = await fetch(`${endpoint}${path}`, {
1255
+ ...options,
1256
+ signal: compositeSignal,
1257
+ headers: {
1258
+ "Content-Type": "application/json",
1259
+ // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
1260
+ // fallback accepted by some older middleware. Both are sent.
1261
+ "Authorization": `Bearer ${config.apiKey ?? ""}`,
1262
+ "X-Mushi-Api-Key": config.apiKey ?? "",
1263
+ "X-Mushi-Project": config.projectId ?? "",
1264
+ "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
1265
+ ...options.headers
1266
+ }
1267
+ });
1268
+ clearTimeout(timer);
1269
+ let body;
1270
+ const contentType = res.headers.get("content-type") ?? "";
1271
+ if (contentType.includes("application/json")) {
1272
+ try {
1273
+ body = await res.json();
1274
+ } catch {
1275
+ body = null;
1276
+ }
1277
+ } else {
1278
+ const text2 = await res.text();
1279
+ try {
1280
+ body = JSON.parse(text2);
1281
+ } catch {
1282
+ body = {
1283
+ ok: false,
1284
+ error: {
1285
+ code: `HTTP_${res.status}`,
1286
+ message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
1287
+ }
1288
+ };
1289
+ }
1108
1290
  }
1109
- });
1110
- return res.json();
1291
+ if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
1292
+ const b = body;
1293
+ return {
1294
+ ok: false,
1295
+ httpStatus: res.status,
1296
+ error: {
1297
+ code: b["code"] ?? `HTTP_${res.status}`,
1298
+ message: b["message"] ?? `Request failed (${res.status})`
1299
+ }
1300
+ };
1301
+ }
1302
+ return body;
1303
+ } catch (err) {
1304
+ clearTimeout(timer);
1305
+ if (err instanceof Error && err.name === "AbortError") {
1306
+ return {
1307
+ ok: false,
1308
+ error: {
1309
+ code: "TIMEOUT",
1310
+ message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
1311
+ }
1312
+ };
1313
+ }
1314
+ return {
1315
+ ok: false,
1316
+ error: {
1317
+ code: "NETWORK_ERROR",
1318
+ message: err instanceof Error ? err.message : String(err)
1319
+ }
1320
+ };
1321
+ }
1111
1322
  }
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) => {
1323
+ function die(result, exitCode = 1) {
1324
+ const { code, message } = result.error;
1325
+ const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
1326
+ process.stderr.write(`error${status}: ${code} \u2014 ${message}
1327
+ `);
1328
+ process.exit(exitCode);
1329
+ }
1330
+ function requireConfig(opts = {}) {
1331
+ const config = loadConfig();
1332
+ if (!config.apiKey) {
1333
+ process.stderr.write(
1334
+ "error: API key not configured.\n Run: mushi login --api-key <key> --endpoint <url>\n Or: export MUSHI_API_KEY=<key>\n"
1335
+ );
1336
+ process.exit(2);
1337
+ }
1338
+ if (!config.endpoint) {
1339
+ process.stderr.write(
1340
+ "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"
1341
+ );
1342
+ process.exit(2);
1343
+ }
1344
+ if (opts.needsProject && !config.projectId) {
1345
+ process.stderr.write(
1346
+ "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"
1347
+ );
1348
+ process.exit(2);
1349
+ }
1350
+ return config;
1351
+ }
1352
+ function fmtDate(iso) {
1353
+ if (!iso) return "\u2014";
1354
+ return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
1355
+ }
1356
+ function pad(s, width) {
1357
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
1358
+ }
1359
+ 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", `
1360
+ Environment variables:
1361
+ MUSHI_API_KEY Project API key (from Settings \u2192 API Keys in the console)
1362
+ MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
1363
+ MUSHI_API_ENDPOINT Supabase edge function URL
1364
+ e.g. https://<ref>.supabase.co/functions/v1/api
1365
+
1366
+ Exit codes:
1367
+ 0 success
1368
+ 1 API / runtime error
1369
+ 2 configuration error (missing credentials or endpoint)
1370
+ 3 not found (resource does not exist)
1371
+
1372
+ Console: https://kensaur.us/mushi-mushi/
1373
+ Docs: https://github.com/kensaurus/mushi-mushi`);
1374
+ 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
1375
  await runInit({
1115
1376
  projectId: opts.projectId,
1116
1377
  apiKey: opts.apiKey,
@@ -1122,116 +1383,501 @@ program.command("init").description("Set up the Mushi Mushi SDK in this project
1122
1383
  sendTestReport: opts.skipTestReport ? false : void 0
1123
1384
  });
1124
1385
  });
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) => {
1386
+ 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
1387
  const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
1129
1388
  if (matches.length === 0) process.exit(1);
1130
1389
  });
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) => {
1390
+ 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", `
1391
+ Examples:
1392
+ mushi login --api-key mushi_xxx --endpoint https://xyz.supabase.co/functions/v1/api
1393
+ mushi login --api-key mushi_xxx --project-id 542b34e0-019e-41fe-b900-7b637717bb86`).action((opts) => {
1132
1394
  const config = loadConfig();
1133
1395
  config.apiKey = opts.apiKey;
1134
1396
  if (opts.endpoint) config.endpoint = assertEndpoint(opts.endpoint);
1135
1397
  if (opts.projectId) config.projectId = opts.projectId;
1136
1398
  saveConfig(config);
1137
- console.log("Saved credentials to ~/.mushirc (mode 0o600)");
1399
+ console.log("\u2713 Credentials saved to ~/.mushirc (mode 0o600)");
1400
+ console.log(" Run 'mushi whoami' to verify the connection.");
1138
1401
  });
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);
1402
+ program.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1403
+ Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
1404
+ Useful after 'mushi login' to confirm credentials are correct.`).action(async (opts) => {
1405
+ const config = requireConfig();
1406
+ const result = await apiCall("/v1/sync/whoami", config);
1407
+ if (!result.ok) die(result);
1159
1408
  if (opts.json) {
1160
- console.log(JSON.stringify(data, null, 2));
1409
+ console.log(JSON.stringify(result.data, null, 2));
1161
1410
  } 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
- }
1411
+ const d = result.data;
1412
+ console.log(`\u2713 Authenticated`);
1413
+ console.log(` Project: ${d.project_name} (${d.project_id})`);
1414
+ console.log(` Endpoint: ${config.endpoint}`);
1415
+ console.log(` Reports: ${d.stats.total_reports} total \xB7 ${d.stats.open_reports} open`);
1166
1416
  }
1167
1417
  });
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");
1418
+ program.command("ping").description("Check connectivity to the Mushi backend").option("--json", "Machine-readable JSON output").action(async (opts) => {
1419
+ const config = requireConfig();
1420
+ const t0 = Date.now();
1421
+ try {
1422
+ const controller = new AbortController();
1423
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1424
+ const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
1425
+ clearTimeout(timer);
1426
+ const latency = Date.now() - t0;
1427
+ if (opts.json) {
1428
+ console.log(JSON.stringify({ ok: res.ok, status: res.status, latency_ms: latency }));
1429
+ } else {
1430
+ const symbol = res.ok ? "\u2713" : "\u2717";
1431
+ console.log(`${symbol} ${res.ok ? "OK" : "FAIL"} \u2014 ${res.status} (${latency}ms)`);
1432
+ if (!res.ok) process.exit(1);
1433
+ }
1434
+ } catch (err) {
1435
+ const msg = err instanceof Error ? err.message : String(err);
1436
+ if (opts.json) {
1437
+ console.log(JSON.stringify({ ok: false, error: msg, latency_ms: Date.now() - t0 }));
1438
+ } else {
1439
+ process.stderr.write(`\u2717 Unreachable: ${msg}
1440
+ `);
1441
+ }
1172
1442
  process.exit(1);
1173
1443
  }
1174
- const data = await apiCall(`/v1/admin/reports/${id}`, config);
1175
- console.log(JSON.stringify(data, null, 2));
1176
1444
  });
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);
1445
+ program.command("status").description("Show project stats: report counts by severity and status").option("--json", "Machine-readable JSON output").action(async (opts) => {
1446
+ const config = requireConfig();
1447
+ const result = await apiCall("/v1/sync/stats", config);
1448
+ if (!result.ok) die(result);
1449
+ if (opts.json) {
1450
+ console.log(JSON.stringify(result.data, null, 2));
1451
+ return;
1182
1452
  }
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));
1453
+ const d = result.data;
1454
+ console.log(`Project: ${d.project_name}`);
1455
+ console.log("");
1456
+ console.log("Reports by status:");
1457
+ for (const [status, count] of Object.entries(d.by_status)) {
1458
+ console.log(` ${pad(status, 14)} ${count}`);
1459
+ }
1460
+ console.log("");
1461
+ console.log("Reports by severity:");
1462
+ for (const [severity, count] of Object.entries(d.by_severity)) {
1463
+ console.log(` ${pad(severity, 14)} ${count}`);
1464
+ }
1465
+ console.log("");
1466
+ console.log(`Fixes: ${d.fixes_count} total \xB7 ${d.fixes_merged} merged`);
1467
+ console.log(`Lessons: ${d.lessons_count} active rules`);
1188
1468
  });
1189
- program.command("config").description("View or update CLI config").argument("[key]", "Config key to set").argument("[value]", "Value").action((key, value) => {
1469
+ 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", `
1470
+ Keys:
1471
+ apiKey \u2014 Mushi API key (mushi_...)
1472
+ endpoint \u2014 Supabase edge function URL
1473
+ projectId \u2014 Project UUID
1474
+
1475
+ Examples:
1476
+ mushi config # show all config
1477
+ mushi config apiKey mushi_xxx # set API key
1478
+ mushi config endpoint https://... # set endpoint
1479
+ mushi config projectId <uuid> # set project`).action((key, value) => {
1190
1480
  const config = loadConfig();
1481
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
1191
1482
  if (key && value) {
1483
+ if (!ALLOWED_KEYS.has(key)) {
1484
+ process.stderr.write(`error: unknown config key "${key}". Allowed: ${[...ALLOWED_KEYS].join(", ")}
1485
+ `);
1486
+ process.exit(2);
1487
+ }
1192
1488
  const safeValue = key === "endpoint" ? assertEndpoint(value) : value;
1193
1489
  config[key] = safeValue;
1194
1490
  saveConfig(config);
1195
- console.log(`Set ${key} = ${safeValue}`);
1491
+ console.log(`\u2713 Set ${key}`);
1196
1492
  } else {
1197
- console.log(JSON.stringify(config, null, 2));
1493
+ const safe = { ...config, apiKey: config.apiKey ? `${config.apiKey.slice(0, 10)}\u2026` : void 0 };
1494
+ console.log(JSON.stringify(safe, null, 2));
1198
1495
  }
1199
1496
  });
1200
1497
  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");
1498
+ deploy.command("check").description("Check edge function health and measure latency").option("--json", "Machine-readable JSON output").action(async (opts) => {
1499
+ const config = requireConfig();
1500
+ const t0 = Date.now();
1501
+ try {
1502
+ const controller = new AbortController();
1503
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1504
+ const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
1505
+ clearTimeout(timer);
1506
+ const latency = Date.now() - t0;
1507
+ const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
1508
+ if (opts.json) {
1509
+ console.log(JSON.stringify({ ok: res.ok, status: res.status, latency_ms: latency, ...body }));
1510
+ } else {
1511
+ console.log(`Health: ${res.status === 200 ? "OK" : "FAIL"} (${res.status}) \u2014 ${latency}ms`);
1512
+ if (body["version"]) console.log(` Version: ${body["version"]}`);
1513
+ if (body["region"]) console.log(` Region: ${body["region"]}`);
1514
+ }
1515
+ if (!res.ok) process.exit(1);
1516
+ } catch (err) {
1517
+ const msg = err instanceof Error ? err.message : String(err);
1518
+ if (opts.json) {
1519
+ console.log(JSON.stringify({ ok: false, error: msg }));
1520
+ } else {
1521
+ process.stderr.write(`error: ${msg}
1522
+ `);
1523
+ }
1205
1524
  process.exit(1);
1206
1525
  }
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);
1526
+ });
1527
+ var reports = program.command("reports").description("Manage bug reports");
1528
+ 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", `
1529
+ Examples:
1530
+ mushi reports list
1531
+ mushi reports list --status new --severity critical
1532
+ mushi reports list --search "button not working" --limit 5 --json`).action(async (opts) => {
1533
+ const config = requireConfig();
1534
+ const limit = Math.min(Math.max(1, parseInt(opts.limit) || 20), 100);
1535
+ const params = new URLSearchParams({ limit: String(limit) });
1536
+ if (opts.status) params.set("status", opts.status);
1537
+ if (opts.severity) params.set("severity", opts.severity);
1538
+ if (opts.search) params.set("search", opts.search);
1539
+ const result = await apiCall(`/v1/sync/reports?${params}`, config);
1540
+ if (!result.ok) die(result);
1541
+ if (opts.json) {
1542
+ console.log(JSON.stringify(result.data, null, 2));
1543
+ return;
1212
1544
  }
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);
1545
+ const rows = result.data.reports;
1546
+ if (rows.length === 0) {
1547
+ console.log("No reports found.");
1548
+ return;
1549
+ }
1550
+ console.log(`${pad("ID", 38)} ${pad("SEV", 9)} ${pad("STATUS", 12)} ${pad("CREATED", 17)} SUMMARY`);
1551
+ console.log("\u2500".repeat(110));
1552
+ for (const r of rows) {
1553
+ const sev = r.severity ?? "unset";
1554
+ const status = r.status ?? "new";
1555
+ const summary = (r.summary ?? r.description ?? "").slice(0, 50);
1556
+ console.log(`${pad(r.id, 38)} ${pad(sev, 9)} ${pad(status, 12)} ${pad(fmtDate(r.created_at), 17)} ${summary}`);
1557
+ }
1558
+ if (result.data.total > rows.length) {
1559
+ console.log(`
1560
+ \u2026 ${result.data.total - rows.length} more. Use --limit to see more.`);
1219
1561
  }
1220
1562
  });
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);
1563
+ reports.command("show <id>").description("Show full details for a single report").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
1564
+ const config = requireConfig();
1565
+ const result = await apiCall(`/v1/sync/reports/${id}`, config);
1566
+ if (!result.ok) {
1567
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1568
+ process.stderr.write(`error: report "${id}" not found
1569
+ `);
1570
+ process.exit(3);
1571
+ }
1572
+ die(result);
1226
1573
  }
1227
- if (!config.projectId) {
1228
- console.error("Set projectId via `mushi config projectId <id>`");
1229
- process.exit(1);
1574
+ if (opts.json) {
1575
+ console.log(JSON.stringify(result.data, null, 2));
1576
+ return;
1577
+ }
1578
+ const r = result.data;
1579
+ console.log(`Report: ${r.id}`);
1580
+ console.log(` Status: ${r.status ?? "new"}`);
1581
+ console.log(` Severity: ${r.severity ?? "unset"}`);
1582
+ console.log(` Category: ${r.category ?? "\u2014"}`);
1583
+ console.log(` Created: ${fmtDate(r.created_at)}`);
1584
+ if (r.summary) console.log(` Summary: ${r.summary}`);
1585
+ if (r.description) {
1586
+ console.log(` Description:`);
1587
+ console.log(` ${r.description.replace(/\n/g, "\n ")}`);
1588
+ }
1589
+ if (r.environment?.url) console.log(` URL: ${r.environment.url}`);
1590
+ if (r.component) console.log(` Component: ${r.component}`);
1591
+ if (r.sentry_event_id) console.log(` Sentry: ${r.sentry_event_id}`);
1592
+ if (r.fix_id) console.log(` Fix: ${r.fix_id}`);
1593
+ if (r.tags && Object.keys(r.tags).length > 0) {
1594
+ console.log(` Tags: ${JSON.stringify(r.tags)}`);
1595
+ }
1596
+ });
1597
+ 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", `
1598
+ Examples:
1599
+ mushi reports triage <id> --status triaged --severity high
1600
+ mushi reports triage <id> --status in_progress --note "assigned to @alice"`).action(async (id, opts) => {
1601
+ const config = requireConfig();
1602
+ const body = {};
1603
+ if (opts.status) body["status"] = opts.status;
1604
+ if (opts.severity) body["severity"] = opts.severity;
1605
+ if (opts.note) body["note"] = opts.note;
1606
+ if (Object.keys(body).length === 0) {
1607
+ process.stderr.write("error: provide at least one of --status, --severity, or --note\n");
1608
+ process.exit(2);
1609
+ }
1610
+ const result = await apiCall(`/v1/sync/reports/${id}`, config, {
1611
+ method: "PATCH",
1612
+ body: JSON.stringify(body)
1613
+ });
1614
+ if (!result.ok) {
1615
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1616
+ process.stderr.write(`error: report "${id}" not found
1617
+ `);
1618
+ process.exit(3);
1619
+ }
1620
+ die(result);
1621
+ }
1622
+ if (opts.json) {
1623
+ console.log(JSON.stringify(result.data, null, 2));
1624
+ } else {
1625
+ console.log(`\u2713 Updated report ${id}`);
1626
+ if (opts.status) console.log(` Status: ${opts.status}`);
1627
+ if (opts.severity) console.log(` Severity: ${opts.severity}`);
1628
+ if (opts.note) console.log(` Note: ${opts.note}`);
1629
+ }
1630
+ });
1631
+ 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) => {
1632
+ const config = requireConfig();
1633
+ const body = { status: "resolved" };
1634
+ if (opts.note) body["note"] = opts.note;
1635
+ const result = await apiCall(`/v1/sync/reports/${id}`, config, {
1636
+ method: "PATCH",
1637
+ body: JSON.stringify(body)
1638
+ });
1639
+ if (!result.ok) {
1640
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1641
+ process.stderr.write(`error: report "${id}" not found
1642
+ `);
1643
+ process.exit(3);
1644
+ }
1645
+ die(result);
1646
+ }
1647
+ if (opts.json) {
1648
+ console.log(JSON.stringify(result.data, null, 2));
1649
+ } else {
1650
+ console.log(`\u2713 Resolved report ${id}`);
1651
+ if (opts.note) console.log(` Note: ${opts.note}`);
1652
+ }
1653
+ });
1654
+ 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) => {
1655
+ const config = requireConfig();
1656
+ const body = { status: "new" };
1657
+ if (opts.note) body["note"] = opts.note;
1658
+ const result = await apiCall(`/v1/sync/reports/${id}`, config, {
1659
+ method: "PATCH",
1660
+ body: JSON.stringify(body)
1661
+ });
1662
+ if (!result.ok) {
1663
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1664
+ process.stderr.write(`error: report "${id}" not found
1665
+ `);
1666
+ process.exit(3);
1667
+ }
1668
+ die(result);
1669
+ }
1670
+ if (opts.json) {
1671
+ console.log(JSON.stringify(result.data, null, 2));
1672
+ } else {
1673
+ console.log(`\u2713 Reopened report ${id}`);
1674
+ }
1675
+ });
1676
+ 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) => {
1677
+ const config = requireConfig();
1678
+ const body = { status: "dismissed" };
1679
+ if (opts.note) body["note"] = opts.note;
1680
+ const result = await apiCall(`/v1/sync/reports/${id}`, config, {
1681
+ method: "PATCH",
1682
+ body: JSON.stringify(body)
1683
+ });
1684
+ if (!result.ok) {
1685
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1686
+ process.stderr.write(`error: report "${id}" not found
1687
+ `);
1688
+ process.exit(3);
1689
+ }
1690
+ die(result);
1691
+ }
1692
+ if (opts.json) {
1693
+ console.log(JSON.stringify(result.data, null, 2));
1694
+ } else {
1695
+ console.log(`\u2713 Dismissed report ${id}`);
1696
+ }
1697
+ });
1698
+ 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", `
1699
+ Examples:
1700
+ mushi reports search "login button"
1701
+ mushi reports search "404 error" --status new --limit 20`).action(async (query, opts) => {
1702
+ const config = requireConfig();
1703
+ const limit = Math.min(Math.max(1, parseInt(opts.limit) || 10), 50);
1704
+ const params = new URLSearchParams({ search: query, limit: String(limit) });
1705
+ if (opts.status) params.set("status", opts.status);
1706
+ const result = await apiCall(`/v1/sync/reports?${params}`, config);
1707
+ if (!result.ok) die(result);
1708
+ if (opts.json) {
1709
+ console.log(JSON.stringify(result.data, null, 2));
1710
+ return;
1711
+ }
1712
+ const rows = result.data.reports;
1713
+ if (rows.length === 0) {
1714
+ console.log(`No reports matching "${query}".`);
1715
+ return;
1716
+ }
1717
+ console.log(`${rows.length} result${rows.length === 1 ? "" : "s"} for "${query}":`);
1718
+ console.log("");
1719
+ for (const r of rows) {
1720
+ console.log(` ${r.id}`);
1721
+ console.log(` ${r.severity ?? "unset"} \xB7 ${r.status ?? "new"} \xB7 ${fmtDate(r.created_at)}`);
1722
+ const text2 = r.summary ?? r.description ?? "";
1723
+ if (text2) console.log(` ${text2.slice(0, 80)}`);
1724
+ console.log("");
1725
+ }
1726
+ });
1727
+ var lessons = program.command("lessons").description("Manage learned mistake rules");
1728
+ 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", `
1729
+ Lessons are mistake rules extracted from past bug reports by the clustering
1730
+ pipeline. They are injected into AI code-review context via the MCP server.`).action(async (opts) => {
1731
+ const config = requireConfig();
1732
+ const limit = Math.min(Math.max(1, parseInt(opts.limit) || 50), 200);
1733
+ const params = new URLSearchParams({ limit: String(limit) });
1734
+ if (opts.severity) params.set("severity", opts.severity);
1735
+ const result = await apiCall(`/v1/sync/lessons?${params}`, config);
1736
+ if (!result.ok) die(result);
1737
+ if (opts.json) {
1738
+ console.log(JSON.stringify(result.data, null, 2));
1739
+ return;
1740
+ }
1741
+ const rows = result.data;
1742
+ if (!Array.isArray(rows) || rows.length === 0) {
1743
+ console.log("No active lessons yet. Reports are clustered nightly.");
1744
+ return;
1745
+ }
1746
+ console.log(`${pad("SEV", 9)} ${pad("FREQ", 6)} RULE`);
1747
+ console.log("\u2500".repeat(90));
1748
+ for (const l of rows) {
1749
+ const sev = l.severity ?? "info";
1750
+ const freq = String(l.frequency ?? 0);
1751
+ const rule = (l.rule_text ?? "").slice(0, 70);
1752
+ console.log(`${pad(sev, 9)} ${pad(freq, 6)} ${rule}`);
1753
+ }
1754
+ console.log(`
1755
+ ${rows.length} active lesson${rows.length === 1 ? "" : "s"}`);
1756
+ });
1757
+ 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) => {
1758
+ const config = requireConfig();
1759
+ const result = await apiCall(`/v1/sync/lessons/${id}`, config);
1760
+ if (!result.ok) {
1761
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
1762
+ process.stderr.write(`error: lesson "${id}" not found
1763
+ `);
1764
+ process.exit(3);
1765
+ }
1766
+ die(result);
1767
+ }
1768
+ if (opts.json) {
1769
+ console.log(JSON.stringify(result.data, null, 2));
1770
+ return;
1230
1771
  }
1772
+ const l = result.data;
1773
+ console.log(`Lesson: ${l.id}`);
1774
+ console.log(` Severity: ${l.severity}`);
1775
+ console.log(` Frequency: ${l.frequency} reports`);
1776
+ if (l.last_reinforced_at) console.log(` Updated: ${fmtDate(l.last_reinforced_at)}`);
1777
+ console.log("");
1778
+ console.log(`Rule:`);
1779
+ console.log(` ${l.rule_text}`);
1780
+ if (l.anti_pattern) {
1781
+ console.log("");
1782
+ console.log(`Anti-pattern:`);
1783
+ console.log(` ${l.anti_pattern}`);
1784
+ }
1785
+ if (l.summary_paragraph) {
1786
+ console.log("");
1787
+ console.log(`Summary:`);
1788
+ console.log(` ${l.summary_paragraph}`);
1789
+ }
1790
+ });
1791
+ 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", `
1792
+ Used in CI to keep .mushi/lessons.json up to date so the Mushi MCP server
1793
+ and Cursor rules can inject the latest project-specific mistake rules into
1794
+ AI code review context.
1795
+
1796
+ Typical CI usage:
1797
+ MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
1798
+ npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
1799
+ const { writeFile, mkdir } = await import("fs/promises");
1800
+ const nodePath = await import("path");
1801
+ const config = requireConfig();
1802
+ const cwd = opts.cwd ?? process.cwd();
1803
+ const target = nodePath.join(cwd, ".mushi", "lessons.json");
1804
+ const result = await apiCall("/v1/sync/lessons?limit=500", config);
1805
+ if (!result.ok) die(result);
1806
+ const rows = Array.isArray(result.data) ? result.data : [];
1807
+ const lessons2 = rows.map((l) => ({
1808
+ id: l.id,
1809
+ rule: l.rule_text,
1810
+ anti_pattern: l.anti_pattern ?? void 0,
1811
+ severity: l.severity,
1812
+ frequency: l.frequency,
1813
+ last_reinforced: l.last_reinforced_at?.slice(0, 10) ?? "",
1814
+ cluster_id: l.cluster_id ?? void 0
1815
+ }));
1816
+ const output = {
1817
+ schema_version: "1",
1818
+ project_id: config.projectId ?? "",
1819
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1820
+ lessons: lessons2
1821
+ };
1822
+ if (opts.dryRun) {
1823
+ console.log(JSON.stringify(output, null, 2));
1824
+ return;
1825
+ }
1826
+ await mkdir(nodePath.dirname(target), { recursive: true });
1827
+ await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1828
+ if (opts.json) {
1829
+ console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
1830
+ } else {
1831
+ console.log(`\u2713 Wrote ${lessons2.length} lesson${lessons2.length === 1 ? "" : "s"} to ${target}`);
1832
+ }
1833
+ });
1834
+ 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) => {
1835
+ const config = requireConfig();
1836
+ const result = await apiCall("/v1/reports", config, {
1837
+ method: "POST",
1838
+ body: JSON.stringify({
1839
+ projectId: config.projectId,
1840
+ description: "CLI test report \u2014 verifying ingestion pipeline",
1841
+ category: "other",
1842
+ reporterToken: `cli-test-${Date.now()}`,
1843
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1844
+ environment: {
1845
+ url: "cli://test",
1846
+ userAgent: `mushi-cli/${MUSHI_CLI_VERSION}`,
1847
+ platform: process.platform,
1848
+ language: "en",
1849
+ viewport: { width: 0, height: 0 },
1850
+ referrer: "",
1851
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1852
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
1853
+ }
1854
+ })
1855
+ });
1856
+ if (!result.ok) die(result);
1857
+ if (opts.json) {
1858
+ console.log(JSON.stringify(result.data, null, 2));
1859
+ } else {
1860
+ const d = result.data;
1861
+ console.log(`\u2713 Test report submitted`);
1862
+ console.log(` ID: ${d.reportId}`);
1863
+ console.log(` Status: ${d.status}`);
1864
+ console.log(` View: https://kensaur.us/mushi-mushi/reports/${d.reportId}`);
1865
+ }
1866
+ });
1867
+ 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", `
1868
+ Uploads source code into the Mushi vector index so the fix-worker can
1869
+ retrieve relevant context when generating patches. Only needed for private
1870
+ repos that cannot be auto-indexed via GitHub App.
1871
+
1872
+ Examples:
1873
+ mushi index ./src
1874
+ mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
1875
+ const config = requireConfig({ needsProject: true });
1231
1876
  const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
1232
1877
  const nodePath = await import("path");
1233
1878
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
1234
1879
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
1880
+ const MAX_FILE_BYTES = 5e5;
1235
1881
  async function* walk(dir) {
1236
1882
  const entries = await readdir2(dir, { withFileTypes: true });
1237
1883
  for (const e of entries) {
@@ -1243,64 +1889,53 @@ program.command("index <path>").description("Walk a local repo and upload code c
1243
1889
  }
1244
1890
  let count = 0;
1245
1891
  let bytes = 0;
1892
+ let errors = 0;
1246
1893
  const root = nodePath.resolve(path);
1247
1894
  for await (const file of walk(root)) {
1248
1895
  const lang = nodePath.extname(file).slice(1);
1249
1896
  if (opts.language && opts.language !== lang) continue;
1250
1897
  const stats = await stat(file);
1251
- if (stats.size > 5e5) continue;
1898
+ if (stats.size > MAX_FILE_BYTES) {
1899
+ if (!opts.json) process.stdout.write(` skip ${nodePath.relative(root, file)} (>${MAX_FILE_BYTES / 1e3}KB)
1900
+ `);
1901
+ continue;
1902
+ }
1252
1903
  const source = await readFile2(file, "utf8");
1253
1904
  const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
1254
1905
  count++;
1255
1906
  bytes += source.length;
1256
1907
  if (opts.dryRun) {
1257
- console.log(` ${relative2} (${source.length} bytes)`);
1908
+ if (!opts.json) process.stdout.write(` ${relative2} (${source.length} bytes)
1909
+ `);
1258
1910
  continue;
1259
1911
  }
1260
- const res = await apiCall("/v1/admin/codebase/upload", config, {
1912
+ const result = await apiCall("/v1/sync/codebase/upload", config, {
1261
1913
  method: "POST",
1262
- body: JSON.stringify({
1263
- projectId: config.projectId,
1264
- filePath: relative2,
1265
- source
1266
- })
1914
+ body: JSON.stringify({ projectId: config.projectId, filePath: relative2, source })
1267
1915
  });
1268
- if (!res.ok) console.error(` FAIL ${relative2}: ${res.error ?? "unknown"}`);
1269
- else process.stdout.write(` ${relative2} \u2192 ${res.chunks ?? 0} chunks
1916
+ if (!result.ok) {
1917
+ errors++;
1918
+ process.stderr.write(` FAIL ${relative2}: ${result.error.message}
1919
+ `);
1920
+ } else if (!opts.json) {
1921
+ process.stdout.write(` ok ${relative2} \u2192 ${result.data.chunks} chunks
1270
1922
  `);
1923
+ }
1271
1924
  }
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);
1925
+ if (opts.json) {
1926
+ console.log(JSON.stringify({ ok: errors === 0, files: count, bytes, errors }));
1927
+ } else {
1928
+ const kb = (bytes / 1024).toFixed(1);
1929
+ console.log(`
1930
+ Indexed ${count} files (${kb} KB) into project ${config.projectId}${errors ? ` \u2014 ${errors} failed` : ""}`);
1279
1931
  }
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));
1932
+ if (errors > 0) process.exit(1);
1301
1933
  });
1302
1934
  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) => {
1935
+ 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", `
1936
+ Examples:
1937
+ mushi sourcemaps upload --release 1.0.0
1938
+ mushi sourcemaps upload --release $(git rev-parse --short HEAD) --dir ./dist`).action(async (opts) => {
1304
1939
  await runSourcemapsUpload({
1305
1940
  release: opts.release,
1306
1941
  dir: opts.dir,
@@ -1310,53 +1945,93 @@ sourcemaps.command("upload").description("Upload source map files to the Mushi p
1310
1945
  silent: opts.silent
1311
1946
  });
1312
1947
  });
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);
1948
+ var fixCmd = program.command("fix").description("Dispatch an agentic fix for a report");
1949
+ fixCmd.argument("<reportId>", "Report UUID to fix").option(
1950
+ "--agent <name>",
1951
+ "Agent adapter: claude_code (default), cursor_cloud, codex, mcp",
1952
+ "claude_code"
1953
+ ).option(
1954
+ "--model <slug>",
1955
+ "Model override for cursor_cloud (e.g. composer-latest)"
1956
+ ).option(
1957
+ "--no-auto-pr",
1958
+ "For cursor_cloud: skip automatic PR creation (branch only)"
1959
+ ).option(
1960
+ "--wait",
1961
+ "Poll until terminal state and exit non-zero on error/cancelled (CI-friendly)"
1962
+ ).option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--project-id <id>", "Project ID (overrides MUSHI_PROJECT_ID)").addHelpText("after", `
1963
+ Examples:
1964
+ mushi fix abc123 --agent cursor_cloud --wait
1965
+ mushi fix abc123 --agent cursor_cloud --model composer-latest --no-auto-pr
1966
+ mushi fix abc123 --agent claude_code
1967
+
1968
+ # CI: fail the pipeline if the fix errors
1969
+ mushi fix $REPORT_ID --agent cursor_cloud --wait && echo "Fix PR opened"`).action(async (reportId, opts) => {
1970
+ const cfg = loadConfig();
1971
+ if (opts.endpoint) cfg.endpoint = opts.endpoint;
1972
+ if (opts.apiKey) cfg.apiKey = opts.apiKey;
1973
+ if (opts.projectId) cfg.projectId = opts.projectId;
1974
+ const isTTY = process.stdout.isTTY;
1975
+ const emitEvent = (type, data) => {
1976
+ if (isTTY) {
1977
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1978
+ console.log(`[${ts}] ${type}`, JSON.stringify(data));
1979
+ } else {
1980
+ process.stdout.write(JSON.stringify({ type, ...data }) + "\n");
1981
+ }
1982
+ };
1983
+ emitEvent("dispatch.start", { reportId, agent: opts.agent, model: opts.model ?? null });
1984
+ const body = {
1985
+ reportId,
1986
+ agent: opts.agent
1987
+ };
1988
+ if (opts.agent === "cursor_cloud") {
1989
+ if (opts.model) body.cursorModel = opts.model;
1990
+ if (!opts.autoPr) body.cursorAutoCreatePR = false;
1320
1991
  }
1321
- if (!config.projectId) {
1322
- console.error("Set projectId via `mushi config projectId <id>`");
1992
+ const result = await apiCall("/v1/admin/fixes/dispatch", cfg, {
1993
+ method: "POST",
1994
+ headers: { "Content-Type": "application/json" },
1995
+ body: JSON.stringify(body)
1996
+ });
1997
+ if (!result.ok) {
1998
+ console.error("Error dispatching fix:", result.error.message);
1323
1999
  process.exit(1);
1324
2000
  }
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);
2001
+ const { fixId, status, agentId, runId, prUrl } = result.data;
2002
+ emitEvent("dispatch.ok", { fixId, status, agentId, runId, prUrl });
2003
+ if (!opts.wait) {
2004
+ process.exit(0);
1334
2005
  }
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;
2006
+ if (!fixId) {
2007
+ console.error("No fixId returned \u2014 cannot poll.");
2008
+ process.exit(1);
1353
2009
  }
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}`);
2010
+ const POLL_MS = 5e3;
2011
+ const MAX_POLLS = 120;
2012
+ const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "error", "cancelled", "skipped", "skipped_unsupported_agent", "skipped_no_sandbox"]);
2013
+ for (let i = 0; i < MAX_POLLS; i++) {
2014
+ await new Promise((r) => setTimeout(r, POLL_MS));
2015
+ const pollResult = await apiCall(
2016
+ `/v1/admin/fixes/${fixId}`,
2017
+ cfg
2018
+ );
2019
+ if (!pollResult.ok) {
2020
+ emitEvent("poll.error", { error: pollResult.error.message });
2021
+ continue;
2022
+ }
2023
+ const s = pollResult.data.status;
2024
+ emitEvent("fix.status", { status: s, pr_url: pollResult.data.pr_url, cursor_agent_id: pollResult.data.cursor_agent_id });
2025
+ if (s && TERMINAL.has(s)) {
2026
+ const success = s === "completed";
2027
+ if (!success) {
2028
+ console.error(`Fix ended with status: ${s}${pollResult.data.error ? ` \u2014 ${pollResult.data.error}` : ""}`);
2029
+ process.exit(1);
2030
+ }
2031
+ process.exit(0);
2032
+ }
1360
2033
  }
2034
+ console.error("Polling timed out after 10 minutes. The fix may still be running.");
2035
+ process.exit(1);
1361
2036
  });
1362
2037
  program.parse();