@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/CONTRIBUTING.md +11 -0
- package/README.md +402 -69
- package/dist/chunk-GHDS4VGP.js +6 -0
- package/dist/index.js +863 -188
- package/dist/init.js +84 -20
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LBQX6RYS.js +0 -6
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,
|
|
8
|
-
import { join } from "path";
|
|
7
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
9
8
|
import { homedir } from "os";
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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: "
|
|
544
|
-
hint: "
|
|
545
|
-
validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected
|
|
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
|
|
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.
|
|
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
|
-
|
|
1097
|
-
|
|
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
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1113
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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);
|
|
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
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1169
|
-
const config =
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
1178
|
-
const config =
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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]", "
|
|
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(
|
|
1491
|
+
console.log(`\u2713 Set ${key}`);
|
|
1196
1492
|
} else {
|
|
1197
|
-
|
|
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 =
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1218
|
-
|
|
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
|
-
|
|
1222
|
-
const config =
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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 (
|
|
1228
|
-
console.
|
|
1229
|
-
|
|
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 >
|
|
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
|
-
|
|
1908
|
+
if (!opts.json) process.stdout.write(` ${relative2} (${source.length} bytes)
|
|
1909
|
+
`);
|
|
1258
1910
|
continue;
|
|
1259
1911
|
}
|
|
1260
|
-
const
|
|
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 (!
|
|
1269
|
-
|
|
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
|
-
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1322
|
-
|
|
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
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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();
|