@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/README.md +402 -69
- package/dist/chunk-KUQZQFOL.js +6 -0
- package/dist/index.js +638 -190
- package/dist/init.js +23 -15
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LBQX6RYS.js +0 -6
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
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
|
|
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: "
|
|
544
|
-
hint: "
|
|
545
|
-
validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected
|
|
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
|
|
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.
|
|
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
|
-
|
|
1097
|
-
|
|
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
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1113
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1169
|
-
const config =
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
1178
|
-
const config =
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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]", "
|
|
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(
|
|
1353
|
+
console.log(`\u2713 Set ${key}`);
|
|
1196
1354
|
} else {
|
|
1197
|
-
|
|
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 =
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1218
|
-
|
|
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
|
-
|
|
1222
|
-
const config =
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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 (
|
|
1228
|
-
console.
|
|
1229
|
-
|
|
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 >
|
|
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
|
-
|
|
1770
|
+
if (!opts.json) process.stdout.write(` ${relative2} (${source.length} bytes)
|
|
1771
|
+
`);
|
|
1258
1772
|
continue;
|
|
1259
1773
|
}
|
|
1260
|
-
const
|
|
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 (!
|
|
1269
|
-
|
|
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
|
-
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
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
|
|
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();
|