@mushi-mushi/cli 0.21.0 → 0.22.1
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 +11 -10
- package/dist/chunk-YK4LF7AM.js +6 -0
- package/dist/index.js +1115 -1041
- package/dist/init.js +3 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-L7KBJ2XY.js +0 -6
package/dist/index.js
CHANGED
|
@@ -78,6 +78,23 @@ var MushiCliError = class extends Error {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
|
+
function printAndExit(err, asJson = false) {
|
|
82
|
+
const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
|
|
83
|
+
"E_INTERNAL",
|
|
84
|
+
err instanceof Error ? err.message : String(err),
|
|
85
|
+
"this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
|
|
86
|
+
err
|
|
87
|
+
);
|
|
88
|
+
if (asJson) {
|
|
89
|
+
process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
|
|
90
|
+
} else {
|
|
91
|
+
process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
|
|
92
|
+
`);
|
|
93
|
+
if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
process.exit(cliErr.exitCode);
|
|
97
|
+
}
|
|
81
98
|
|
|
82
99
|
// src/signals.ts
|
|
83
100
|
var installed = false;
|
|
@@ -110,7 +127,7 @@ function getAbortSignal(external) {
|
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
// src/version.ts
|
|
113
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
130
|
+
var MUSHI_CLI_VERSION = true ? "0.22.1" : "0.0.0-dev";
|
|
114
131
|
|
|
115
132
|
// src/init.ts
|
|
116
133
|
import * as p from "@clack/prompts";
|
|
@@ -237,6 +254,20 @@ async function openInBrowser(url) {
|
|
|
237
254
|
|
|
238
255
|
// src/cli-shared.ts
|
|
239
256
|
var API_TIMEOUT_MS = 15e3;
|
|
257
|
+
async function probeEndpointHealth(endpoint) {
|
|
258
|
+
const t0 = Date.now();
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
261
|
+
try {
|
|
262
|
+
const res = await fetch(`${endpoint}/health`, { signal: controller.signal });
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
|
|
265
|
+
return { ok: res.ok, status: res.status, latencyMs: Date.now() - t0, body };
|
|
266
|
+
} catch (err) {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
240
271
|
async function apiCall(path, config, options = {}) {
|
|
241
272
|
const endpoint = config.endpoint;
|
|
242
273
|
if (!endpoint) {
|
|
@@ -337,19 +368,21 @@ function die(result, exitCode = 1) {
|
|
|
337
368
|
function requireConfig(opts = {}) {
|
|
338
369
|
const config = loadConfig();
|
|
339
370
|
if (!config.apiKey) {
|
|
340
|
-
|
|
341
|
-
"
|
|
342
|
-
|
|
343
|
-
|
|
371
|
+
printAndExit(new MushiCliError(
|
|
372
|
+
"E_AUTH_MISSING",
|
|
373
|
+
"API key not configured.",
|
|
374
|
+
"Run: mushi login --api-key <key> Or: export MUSHI_API_KEY=<key>"
|
|
375
|
+
));
|
|
344
376
|
}
|
|
345
377
|
if (!config.endpoint) {
|
|
346
378
|
config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
|
|
347
379
|
}
|
|
348
380
|
if (opts.needsProject && !config.projectId) {
|
|
349
|
-
|
|
350
|
-
"
|
|
351
|
-
|
|
352
|
-
|
|
381
|
+
printAndExit(new MushiCliError(
|
|
382
|
+
"E_PROJECT_MISSING",
|
|
383
|
+
"Project ID not configured.",
|
|
384
|
+
`Run: mushi login --project-id <uuid> Or find your project ID at ${consoleUrl(resolveConsoleUrlSync(), "/projects")}`
|
|
385
|
+
));
|
|
353
386
|
}
|
|
354
387
|
return config;
|
|
355
388
|
}
|
|
@@ -716,7 +749,7 @@ var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
|
|
|
716
749
|
async function runInit(options = {}) {
|
|
717
750
|
const cwd = options.cwd ?? process.cwd();
|
|
718
751
|
ensureInteractiveOrBailOut(options);
|
|
719
|
-
p.intro("
|
|
752
|
+
p.intro("Mushi setup");
|
|
720
753
|
await printFreshnessHint();
|
|
721
754
|
warnIfWorkspaceRoot(cwd);
|
|
722
755
|
const pkg = readPackageJson(cwd);
|
|
@@ -750,7 +783,7 @@ async function runInit(options = {}) {
|
|
|
750
783
|
printNextSteps(framework, consoleBase, enableRewards);
|
|
751
784
|
await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
|
|
752
785
|
await maybeOfferConnect(credentials, options, consoleBase);
|
|
753
|
-
p.outro("Setup complete.
|
|
786
|
+
p.outro("Setup complete.");
|
|
754
787
|
}
|
|
755
788
|
function ensureInteractiveOrBailOut(options) {
|
|
756
789
|
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -1657,24 +1690,19 @@ Useful after 'mushi login' to confirm credentials are correct.`).action(async (o
|
|
|
1657
1690
|
const config = requireConfig();
|
|
1658
1691
|
const t0 = Date.now();
|
|
1659
1692
|
try {
|
|
1660
|
-
const
|
|
1661
|
-
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1662
|
-
const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
|
|
1663
|
-
clearTimeout(timer);
|
|
1664
|
-
const latency = Date.now() - t0;
|
|
1693
|
+
const probe = await probeEndpointHealth(config.endpoint);
|
|
1665
1694
|
if (opts.json) {
|
|
1666
|
-
console.log(JSON.stringify({ ok:
|
|
1695
|
+
console.log(JSON.stringify({ ok: probe.ok, status: probe.status, latency_ms: probe.latencyMs }));
|
|
1667
1696
|
} else {
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
if (!res.ok) process.exit(1);
|
|
1697
|
+
console.log(`${probe.ok ? "OK" : "FAIL"} \u2014 ${probe.status} (${probe.latencyMs}ms)`);
|
|
1698
|
+
if (!probe.ok) process.exit(1);
|
|
1671
1699
|
}
|
|
1672
1700
|
} catch (err) {
|
|
1673
1701
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1674
1702
|
if (opts.json) {
|
|
1675
1703
|
console.log(JSON.stringify({ ok: false, error: msg, latency_ms: Date.now() - t0 }));
|
|
1676
1704
|
} else {
|
|
1677
|
-
process.stderr.write(
|
|
1705
|
+
process.stderr.write(`FAIL Unreachable: ${msg}
|
|
1678
1706
|
`);
|
|
1679
1707
|
}
|
|
1680
1708
|
process.exit(1);
|
|
@@ -1739,22 +1767,16 @@ function registerDeployCommands(program2) {
|
|
|
1739
1767
|
const deploy = program2.command("deploy").description("Deployment management");
|
|
1740
1768
|
deploy.command("check").description("Check edge function health and measure latency").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
1741
1769
|
const config = requireConfig();
|
|
1742
|
-
const t0 = Date.now();
|
|
1743
1770
|
try {
|
|
1744
|
-
const
|
|
1745
|
-
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1746
|
-
const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
|
|
1747
|
-
clearTimeout(timer);
|
|
1748
|
-
const latency = Date.now() - t0;
|
|
1749
|
-
const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
|
|
1771
|
+
const probe = await probeEndpointHealth(config.endpoint);
|
|
1750
1772
|
if (opts.json) {
|
|
1751
|
-
console.log(JSON.stringify({ ok:
|
|
1773
|
+
console.log(JSON.stringify({ ok: probe.ok, status: probe.status, latency_ms: probe.latencyMs, ...probe.body }));
|
|
1752
1774
|
} else {
|
|
1753
|
-
console.log(`Health: ${
|
|
1754
|
-
if (body["version"]) console.log(` Version: ${body["version"]}`);
|
|
1755
|
-
if (body["region"]) console.log(` Region: ${body["region"]}`);
|
|
1775
|
+
console.log(`Health: ${probe.status === 200 ? "OK" : "FAIL"} (${probe.status}) \u2014 ${probe.latencyMs}ms`);
|
|
1776
|
+
if (probe.body["version"]) console.log(` Version: ${probe.body["version"]}`);
|
|
1777
|
+
if (probe.body["region"]) console.log(` Region: ${probe.body["region"]}`);
|
|
1756
1778
|
}
|
|
1757
|
-
if (!
|
|
1779
|
+
if (!probe.ok) process.exit(1);
|
|
1758
1780
|
} catch (err) {
|
|
1759
1781
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1760
1782
|
if (opts.json) {
|
|
@@ -2158,11 +2180,11 @@ AI code review context.
|
|
|
2158
2180
|
Typical CI usage:
|
|
2159
2181
|
MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
|
|
2160
2182
|
npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
|
|
2161
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
2162
|
-
const
|
|
2183
|
+
const { writeFile: writeFile2, mkdir } = await import("fs/promises");
|
|
2184
|
+
const nodePath2 = await import("path");
|
|
2163
2185
|
const config = requireConfig();
|
|
2164
2186
|
const cwd = opts.cwd ?? process.cwd();
|
|
2165
|
-
const target =
|
|
2187
|
+
const target = nodePath2.join(cwd, ".mushi", "lessons.json");
|
|
2166
2188
|
const result = await apiCall("/v1/sync/lessons?limit=500", config);
|
|
2167
2189
|
if (!result.ok) die(result);
|
|
2168
2190
|
const rows = Array.isArray(result.data) ? result.data : [];
|
|
@@ -2185,8 +2207,8 @@ Typical CI usage:
|
|
|
2185
2207
|
console.log(JSON.stringify(output, null, 2));
|
|
2186
2208
|
return;
|
|
2187
2209
|
}
|
|
2188
|
-
await mkdir(
|
|
2189
|
-
await
|
|
2210
|
+
await mkdir(nodePath2.dirname(target), { recursive: true });
|
|
2211
|
+
await writeFile2(target, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
2190
2212
|
if (opts.json) {
|
|
2191
2213
|
console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
|
|
2192
2214
|
} else {
|
|
@@ -2381,35 +2403,35 @@ Examples:
|
|
|
2381
2403
|
mushi index ./src
|
|
2382
2404
|
mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
|
|
2383
2405
|
const config = requireConfig({ needsProject: true });
|
|
2384
|
-
const { readdir: readdir2, readFile:
|
|
2385
|
-
const
|
|
2406
|
+
const { readdir: readdir2, readFile: readFile3, stat } = await import("fs/promises");
|
|
2407
|
+
const nodePath2 = await import("path");
|
|
2386
2408
|
const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
|
|
2387
2409
|
const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
2388
2410
|
const MAX_FILE_BYTES = 5e5;
|
|
2389
2411
|
async function* walk(dir) {
|
|
2390
2412
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
2391
2413
|
for (const e of entries) {
|
|
2392
|
-
const full =
|
|
2414
|
+
const full = nodePath2.join(dir, e.name);
|
|
2393
2415
|
if (SKIP.test(full)) continue;
|
|
2394
2416
|
if (e.isDirectory()) yield* walk(full);
|
|
2395
|
-
else if (EXTS.has(
|
|
2417
|
+
else if (EXTS.has(nodePath2.extname(e.name))) yield full;
|
|
2396
2418
|
}
|
|
2397
2419
|
}
|
|
2398
2420
|
let count = 0;
|
|
2399
2421
|
let bytes = 0;
|
|
2400
2422
|
let errors = 0;
|
|
2401
|
-
const root =
|
|
2423
|
+
const root = nodePath2.resolve(path);
|
|
2402
2424
|
for await (const file of walk(root)) {
|
|
2403
|
-
const lang =
|
|
2425
|
+
const lang = nodePath2.extname(file).slice(1);
|
|
2404
2426
|
if (opts.language && opts.language !== lang) continue;
|
|
2405
2427
|
const stats = await stat(file);
|
|
2406
2428
|
if (stats.size > MAX_FILE_BYTES) {
|
|
2407
|
-
if (!opts.json) process.stdout.write(` skip ${
|
|
2429
|
+
if (!opts.json) process.stdout.write(` skip ${nodePath2.relative(root, file)} (>${MAX_FILE_BYTES / 1e3}KB)
|
|
2408
2430
|
`);
|
|
2409
2431
|
continue;
|
|
2410
2432
|
}
|
|
2411
|
-
const source = await
|
|
2412
|
-
const relative2 =
|
|
2433
|
+
const source = await readFile3(file, "utf8");
|
|
2434
|
+
const relative2 = nodePath2.relative(root, file).replaceAll("\\", "/");
|
|
2413
2435
|
count++;
|
|
2414
2436
|
bytes += source.length;
|
|
2415
2437
|
if (opts.dryRun) {
|
|
@@ -2455,6 +2477,48 @@ Examples:
|
|
|
2455
2477
|
});
|
|
2456
2478
|
}
|
|
2457
2479
|
|
|
2480
|
+
// src/project-bootstrap.ts
|
|
2481
|
+
import { writeFile, readFile as readFile2 } from "fs/promises";
|
|
2482
|
+
import nodePath from "path";
|
|
2483
|
+
async function writeProjectBootstrapFiles(opts) {
|
|
2484
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2485
|
+
const envPath = nodePath.join(cwd, ".env.local");
|
|
2486
|
+
const mushiBlock = [
|
|
2487
|
+
"# Mushi MCP \u2014 drop into .env.local (gitignored). The MCP binary picks these up on spawn.",
|
|
2488
|
+
`MUSHI_API_ENDPOINT=${opts.endpoint}`,
|
|
2489
|
+
`MUSHI_PROJECT_ID=${opts.projectId}`,
|
|
2490
|
+
`MUSHI_API_KEY=${opts.apiKey}`
|
|
2491
|
+
].join("\n");
|
|
2492
|
+
let existing = "";
|
|
2493
|
+
let envUpdated = false;
|
|
2494
|
+
try {
|
|
2495
|
+
existing = await readFile2(envPath, "utf8");
|
|
2496
|
+
envUpdated = true;
|
|
2497
|
+
} catch {
|
|
2498
|
+
}
|
|
2499
|
+
const MUSHI_LINE_RE = /^(NEXT_PUBLIC_|NUXT_PUBLIC_|VITE_|EXPO_PUBLIC_)?MUSHI_[A-Z_]+=.*/gm;
|
|
2500
|
+
const MUSHI_COMMENT_RE = /^# Mushi MCP\b.*/gm;
|
|
2501
|
+
const stripped = existing.replace(MUSHI_LINE_RE, "").replace(MUSHI_COMMENT_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
2502
|
+
const prefix = stripped.length > 0 ? `${stripped}
|
|
2503
|
+
|
|
2504
|
+
` : "";
|
|
2505
|
+
await writeFile(envPath, `${prefix}${mushiBlock}
|
|
2506
|
+
`, "utf8");
|
|
2507
|
+
const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
|
|
2508
|
+
const serverName = buildMcpServerName({ legacy: true });
|
|
2509
|
+
const serverBlock = buildMcpServerBlock({
|
|
2510
|
+
endpoint: opts.endpoint,
|
|
2511
|
+
projectId: opts.projectId,
|
|
2512
|
+
apiKey: opts.apiKey
|
|
2513
|
+
});
|
|
2514
|
+
const { created: mcpCreated } = await writeMcpServerEntry({
|
|
2515
|
+
configPath: mcpPath,
|
|
2516
|
+
serverName,
|
|
2517
|
+
serverBlock
|
|
2518
|
+
});
|
|
2519
|
+
return { envUpdated, mcpUpdated: !mcpCreated };
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2458
2522
|
// src/commands/project.ts
|
|
2459
2523
|
function registerProjectCommands(program2) {
|
|
2460
2524
|
const project = program2.command("project").description("Project management");
|
|
@@ -2469,9 +2533,6 @@ Typical first-time flow:
|
|
|
2469
2533
|
# Browser opens \u2192 click Approve \u2192 pick or create a project
|
|
2470
2534
|
# CLI writes .env.local and .cursor/mcp.json
|
|
2471
2535
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
2472
|
-
const { writeFile } = await import("fs/promises");
|
|
2473
|
-
const { existsSync: existsSync4 } = await import("fs");
|
|
2474
|
-
const nodePath = await import("path");
|
|
2475
2536
|
const savedConfig = loadConfig();
|
|
2476
2537
|
const endpoint = resolveCloudEndpoint(
|
|
2477
2538
|
opts.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? savedConfig.endpoint
|
|
@@ -2568,24 +2629,14 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
2568
2629
|
config.projectId = projectId;
|
|
2569
2630
|
config.consoleUrl = consoleBase;
|
|
2570
2631
|
saveConfig(config);
|
|
2571
|
-
const
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
`MUSHI_PROJECT_ID=${projectId}`,
|
|
2577
|
-
`MUSHI_API_KEY=${apiKey}`,
|
|
2578
|
-
""
|
|
2579
|
-
];
|
|
2580
|
-
const envExisting = existsSync4(envPath);
|
|
2581
|
-
await writeFile(envPath, envLines.join("\n"), "utf8");
|
|
2632
|
+
const { envUpdated, mcpUpdated } = await writeProjectBootstrapFiles({
|
|
2633
|
+
endpoint,
|
|
2634
|
+
projectId,
|
|
2635
|
+
apiKey
|
|
2636
|
+
});
|
|
2582
2637
|
console.log(`
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
const serverName = buildMcpServerName({ legacy: true });
|
|
2586
|
-
const serverBlock = buildMcpServerBlock({ endpoint, projectId, apiKey });
|
|
2587
|
-
const { created: mcpCreated } = await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
|
|
2588
|
-
console.log(` \u2713 ${mcpCreated ? "Created" : "Updated"} .cursor/mcp.json`);
|
|
2638
|
+
OK ${envUpdated ? "Updated" : "Created"} .env.local`);
|
|
2639
|
+
console.log(` OK ${mcpUpdated ? "Updated" : "Created"} .cursor/mcp.json`);
|
|
2589
2640
|
console.log("");
|
|
2590
2641
|
console.log(' Done! Restart Cursor and ask: "list mushi tools"');
|
|
2591
2642
|
console.log(" Run `mushi whoami` to verify the connection.");
|
|
@@ -2609,9 +2660,9 @@ Supported IDEs:
|
|
|
2609
2660
|
zed \u2014 writes ~/.config/zed/settings.json mcpServers block
|
|
2610
2661
|
|
|
2611
2662
|
The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
|
|
2612
|
-
const { writeFile, mkdir, readFile:
|
|
2663
|
+
const { writeFile: writeFile2, mkdir, readFile: readFile3 } = await import("fs/promises");
|
|
2613
2664
|
const { existsSync: existsSync4 } = await import("fs");
|
|
2614
|
-
const
|
|
2665
|
+
const nodePath2 = await import("path");
|
|
2615
2666
|
const os = await import("os");
|
|
2616
2667
|
const config = requireConfig({ needsProject: true });
|
|
2617
2668
|
const redactKeyForDisplay = (text2) => {
|
|
@@ -2654,7 +2705,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2654
2705
|
cursor: { dir: ".cursor", file: "mcp.json", format: "mcp-json" },
|
|
2655
2706
|
claude: { dir: ".claude", file: "mcp.json", format: "mcp-json" },
|
|
2656
2707
|
continue: { dir: ".continue", file: "mcp.json", format: "mcp-json" },
|
|
2657
|
-
zed: { dir:
|
|
2708
|
+
zed: { dir: nodePath2.join(os.homedir(), ".config", "zed"), file: "settings.json", format: "zed" }
|
|
2658
2709
|
};
|
|
2659
2710
|
const ideEntry = IDE_CONFIG[opts.ide];
|
|
2660
2711
|
if (!ideEntry) {
|
|
@@ -2690,8 +2741,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2690
2741
|
MUSHI_API_KEY: config.apiKey
|
|
2691
2742
|
}
|
|
2692
2743
|
};
|
|
2693
|
-
const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir :
|
|
2694
|
-
const configPath =
|
|
2744
|
+
const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir : nodePath2.join(cwd, ideEntry.dir);
|
|
2745
|
+
const configPath = nodePath2.join(configDir, ideEntry.file);
|
|
2695
2746
|
if (ideEntry.format === "mcp-json") {
|
|
2696
2747
|
if (allProjectsList && allProjectsList.length > 0) {
|
|
2697
2748
|
for (const p3 of allProjectsList) {
|
|
@@ -2724,7 +2775,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2724
2775
|
let settings = {};
|
|
2725
2776
|
if (existsSync4(configPath)) {
|
|
2726
2777
|
try {
|
|
2727
|
-
const raw = await
|
|
2778
|
+
const raw = await readFile3(configPath, "utf8");
|
|
2728
2779
|
settings = JSON.parse(raw);
|
|
2729
2780
|
} catch {
|
|
2730
2781
|
}
|
|
@@ -2749,7 +2800,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2749
2800
|
console.log(redactKeyForDisplay(output));
|
|
2750
2801
|
} else {
|
|
2751
2802
|
await mkdir(configDir, { recursive: true });
|
|
2752
|
-
await
|
|
2803
|
+
await writeFile2(configPath, output, "utf8");
|
|
2753
2804
|
console.log(`\u2713 Written ${configPath}`);
|
|
2754
2805
|
}
|
|
2755
2806
|
}
|
|
@@ -2764,7 +2815,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2764
2815
|
"## Before writing a fix",
|
|
2765
2816
|
"",
|
|
2766
2817
|
"1. Call `get_fix_context` (MCP) for the report \u2014 get root cause + blast radius first.",
|
|
2767
|
-
"2. Call `
|
|
2818
|
+
"2. Call `list_lessons` (MCP) or read .mushi/lessons.json \u2014 apply every matching rule.",
|
|
2768
2819
|
"3. Prefer the smallest change that makes the test pass. Don't refactor unrelated code.",
|
|
2769
2820
|
"",
|
|
2770
2821
|
"## After writing a fix",
|
|
@@ -2779,21 +2830,21 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2779
2830
|
""
|
|
2780
2831
|
].join("\n");
|
|
2781
2832
|
if (opts.ide === "cursor") {
|
|
2782
|
-
const rulesPath =
|
|
2833
|
+
const rulesPath = nodePath2.join(cwd, ".cursorrules");
|
|
2783
2834
|
if (opts.dryRun) {
|
|
2784
2835
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2785
2836
|
} else {
|
|
2786
|
-
await
|
|
2837
|
+
await writeFile2(rulesPath, rulesContent, "utf8");
|
|
2787
2838
|
console.log(`\u2713 Written .cursorrules`);
|
|
2788
2839
|
}
|
|
2789
2840
|
} else if (opts.ide === "claude") {
|
|
2790
|
-
const rulesDir =
|
|
2791
|
-
const rulesPath =
|
|
2841
|
+
const rulesDir = nodePath2.join(cwd, ".claude", "rules");
|
|
2842
|
+
const rulesPath = nodePath2.join(rulesDir, "mushi.md");
|
|
2792
2843
|
if (opts.dryRun) {
|
|
2793
2844
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2794
2845
|
} else {
|
|
2795
2846
|
await mkdir(rulesDir, { recursive: true });
|
|
2796
|
-
await
|
|
2847
|
+
await writeFile2(rulesPath, rulesContent, "utf8");
|
|
2797
2848
|
console.log(`\u2713 Written .claude/rules/mushi.md`);
|
|
2798
2849
|
}
|
|
2799
2850
|
}
|
|
@@ -2852,393 +2903,809 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2852
2903
|
if (!opts.withRules) {
|
|
2853
2904
|
console.log(`Tip: run with --with-rules to also write the lesson-library coding hook.`);
|
|
2854
2905
|
}
|
|
2855
|
-
const configRelPath = ideEntry.dir.startsWith("/") ? configPath :
|
|
2906
|
+
const configRelPath = ideEntry.dir.startsWith("/") ? configPath : nodePath2.relative(cwd, configPath);
|
|
2856
2907
|
console.log(`
|
|
2857
2908
|
Note: ${configRelPath} contains your Mushi API key \u2014 add it to .gitignore if this is a shared repo.`);
|
|
2858
2909
|
}
|
|
2859
2910
|
});
|
|
2860
2911
|
}
|
|
2861
2912
|
|
|
2862
|
-
// src/
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
dismissCooldownHours: 24,
|
|
2884
|
-
suppressAfterDismissals: 3,
|
|
2885
|
-
pageDwellMinutes: 5,
|
|
2886
|
-
firstSessionSeconds: 45,
|
|
2887
|
-
rageClick: true,
|
|
2888
|
-
longTask: true,
|
|
2889
|
-
apiCascade: true,
|
|
2890
|
-
errorBoundary: true,
|
|
2891
|
-
betaMode: true,
|
|
2892
|
-
featureRequestCard: true
|
|
2893
|
-
},
|
|
2894
|
-
// GA / production: only the technical signals (error boundary, rage
|
|
2895
|
-
// click). No page-dwell, no welcome — those are explicitly beta-only
|
|
2896
|
-
// patterns. Feature-request card still on because it's user-initiated.
|
|
2897
|
-
ga: {
|
|
2898
|
-
maxProactivePerSession: 1,
|
|
2899
|
-
dismissCooldownHours: 168,
|
|
2900
|
-
suppressAfterDismissals: 2,
|
|
2901
|
-
pageDwellMinutes: 0,
|
|
2902
|
-
firstSessionSeconds: 0,
|
|
2903
|
-
rageClick: true,
|
|
2904
|
-
longTask: false,
|
|
2905
|
-
apiCascade: true,
|
|
2906
|
-
errorBoundary: true,
|
|
2907
|
-
betaMode: false,
|
|
2908
|
-
featureRequestCard: true
|
|
2909
|
-
}
|
|
2910
|
-
};
|
|
2911
|
-
function renderNudgeSnippet(opts) {
|
|
2912
|
-
const p3 = { ...PRESETS[opts.phase], ...opts.overrides };
|
|
2913
|
-
const dwellMs = p3.pageDwellMinutes > 0 ? `${p3.pageDwellMinutes} * 60 * 1000` : null;
|
|
2914
|
-
const firstMs = p3.firstSessionSeconds > 0 ? `${p3.firstSessionSeconds} * 1000` : null;
|
|
2915
|
-
const proactiveLines = [];
|
|
2916
|
-
if (p3.rageClick) proactiveLines.push(` rageClick: true,`);
|
|
2917
|
-
if (p3.longTask) proactiveLines.push(` longTask: true,`);
|
|
2918
|
-
if (p3.apiCascade) proactiveLines.push(` apiCascade: true,`);
|
|
2919
|
-
if (p3.errorBoundary) proactiveLines.push(` errorBoundary: true,`);
|
|
2920
|
-
if (dwellMs) proactiveLines.push(` pageDwell: { thresholdMs: ${dwellMs} }, // ${p3.pageDwellMinutes}min on the same route`);
|
|
2921
|
-
if (firstMs) proactiveLines.push(` firstSession: { delayMs: ${firstMs} }, // welcome new users after ${p3.firstSessionSeconds}s`);
|
|
2922
|
-
proactiveLines.push(` cooldown: {`);
|
|
2923
|
-
proactiveLines.push(` maxProactivePerSession: ${p3.maxProactivePerSession},`);
|
|
2924
|
-
proactiveLines.push(` dismissCooldownHours: ${p3.dismissCooldownHours},`);
|
|
2925
|
-
proactiveLines.push(` suppressAfterDismissals: ${p3.suppressAfterDismissals},`);
|
|
2926
|
-
proactiveLines.push(` },`);
|
|
2927
|
-
const widgetLines = [];
|
|
2928
|
-
if (p3.featureRequestCard) widgetLines.push(` featureRequestCard: true,`);
|
|
2929
|
-
if (p3.betaMode) {
|
|
2930
|
-
widgetLines.push(` betaMode: {`);
|
|
2931
|
-
widgetLines.push(` enabled: true,`);
|
|
2932
|
-
widgetLines.push(` label: 'BETA',`);
|
|
2933
|
-
widgetLines.push(` // changelogItems: [{ version: 'v0.42', date: '${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}', items: ['\u2026'] }],`);
|
|
2934
|
-
widgetLines.push(` },`);
|
|
2935
|
-
}
|
|
2936
|
-
return [
|
|
2937
|
-
`// Generated by \`mushi nudge --phase ${opts.phase}\``,
|
|
2938
|
-
`// Phase: ${opts.phase} \u2014 tune via --max, --cooldown, --dwell, --welcome.`,
|
|
2939
|
-
`Mushi.init({`,
|
|
2940
|
-
` projectId: process.env.MUSHI_PROJECT_ID!,`,
|
|
2941
|
-
` apiKey: process.env.MUSHI_API_KEY!,`,
|
|
2942
|
-
` proactive: {`,
|
|
2943
|
-
...proactiveLines,
|
|
2944
|
-
` },`,
|
|
2945
|
-
...widgetLines.length ? [` widget: {`, ...widgetLines, ` },`] : [],
|
|
2946
|
-
`})`,
|
|
2947
|
-
``
|
|
2948
|
-
].join("\n");
|
|
2949
|
-
}
|
|
2950
|
-
function renderNudgeExplainer(phase) {
|
|
2951
|
-
const p3 = PRESETS[phase];
|
|
2952
|
-
const lines = [];
|
|
2953
|
-
lines.push(``);
|
|
2954
|
-
lines.push(`Nudge preset for "${phase}" phase:`);
|
|
2955
|
-
lines.push(` - max ${p3.maxProactivePerSession} proactive prompts per session`);
|
|
2956
|
-
lines.push(` - ${p3.dismissCooldownHours}h cooldown after a dismissal`);
|
|
2957
|
-
lines.push(` - suppress permanently after ${p3.suppressAfterDismissals} consecutive dismissals`);
|
|
2958
|
-
if (p3.pageDwellMinutes > 0) lines.push(` - fire page-dwell trigger after ${p3.pageDwellMinutes} continuous minutes on a route`);
|
|
2959
|
-
if (p3.firstSessionSeconds > 0) lines.push(` - welcome new users with a button pulse ${p3.firstSessionSeconds}s after init`);
|
|
2960
|
-
lines.push(` - signals enabled: ${[
|
|
2961
|
-
p3.rageClick && "rage-click",
|
|
2962
|
-
p3.longTask && "long-task",
|
|
2963
|
-
p3.apiCascade && "api-cascade",
|
|
2964
|
-
p3.errorBoundary && "error-boundary"
|
|
2965
|
-
].filter(Boolean).join(", ")}`);
|
|
2966
|
-
lines.push(` - feature-request card: ${p3.featureRequestCard ? "shown" : "hidden"}`);
|
|
2967
|
-
lines.push(` - beta-mode UI: ${p3.betaMode ? "on" : "off"}`);
|
|
2968
|
-
lines.push(``);
|
|
2969
|
-
return lines.join("\n");
|
|
2970
|
-
}
|
|
2913
|
+
// src/commands/fix.ts
|
|
2914
|
+
function registerFixCommands(program2) {
|
|
2915
|
+
const fixCmd = program2.command("fix").description("Dispatch an agentic fix for a report");
|
|
2916
|
+
fixCmd.argument("<reportId>", "Report UUID to fix").option(
|
|
2917
|
+
"--agent <name>",
|
|
2918
|
+
"Agent adapter: claude_code (default), cursor_cloud, codex, mcp",
|
|
2919
|
+
"claude_code"
|
|
2920
|
+
).option(
|
|
2921
|
+
"--model <slug>",
|
|
2922
|
+
"Model override for cursor_cloud (e.g. composer-latest)"
|
|
2923
|
+
).option(
|
|
2924
|
+
"--no-auto-pr",
|
|
2925
|
+
"For cursor_cloud: skip automatic PR creation (branch only)"
|
|
2926
|
+
).option(
|
|
2927
|
+
"--wait",
|
|
2928
|
+
"Poll until terminal state and exit non-zero on error/cancelled (CI-friendly)"
|
|
2929
|
+
).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", `
|
|
2930
|
+
Examples:
|
|
2931
|
+
mushi fix abc123 --agent cursor_cloud --wait
|
|
2932
|
+
mushi fix abc123 --agent cursor_cloud --model composer-latest --no-auto-pr
|
|
2933
|
+
mushi fix abc123 --agent claude_code
|
|
2971
2934
|
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
name: "Project ID configured",
|
|
2987
|
-
ok: Boolean(config.projectId),
|
|
2988
|
-
detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
|
|
2989
|
-
}
|
|
2990
|
-
];
|
|
2991
|
-
}
|
|
2992
|
-
async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
2993
|
-
try {
|
|
2994
|
-
const safeEndpoint = sanitizeEndpoint(endpoint);
|
|
2995
|
-
const res = await doFetch(`${safeEndpoint}/health`, {
|
|
2996
|
-
signal: AbortSignal.timeout(5e3)
|
|
2997
|
-
});
|
|
2998
|
-
return {
|
|
2999
|
-
name: "Endpoint reachable",
|
|
3000
|
-
ok: res.status === 200,
|
|
3001
|
-
detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
|
|
2935
|
+
# CI: fail the pipeline if the fix errors
|
|
2936
|
+
mushi fix $REPORT_ID --agent cursor_cloud --wait && echo "Fix PR opened"`).action(async (reportId, opts) => {
|
|
2937
|
+
const cfg = loadConfig();
|
|
2938
|
+
if (opts.endpoint) cfg.endpoint = opts.endpoint;
|
|
2939
|
+
if (opts.apiKey) cfg.apiKey = opts.apiKey;
|
|
2940
|
+
if (opts.projectId) cfg.projectId = opts.projectId;
|
|
2941
|
+
const isTTY = process.stdout.isTTY;
|
|
2942
|
+
const emitEvent = (type, data) => {
|
|
2943
|
+
if (isTTY) {
|
|
2944
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2945
|
+
console.log(`[${ts}] ${type}`, JSON.stringify(data));
|
|
2946
|
+
} else {
|
|
2947
|
+
process.stdout.write(JSON.stringify({ type, ...data }) + "\n");
|
|
2948
|
+
}
|
|
3002
2949
|
};
|
|
3003
|
-
|
|
3004
|
-
const
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
async function checkSdkInstall(cwd) {
|
|
3009
|
-
try {
|
|
3010
|
-
const { readFile: readFile2 } = await import("fs/promises");
|
|
3011
|
-
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3012
|
-
const root = resolve3(cwd);
|
|
3013
|
-
const pkgPath = join5(root, "package.json");
|
|
3014
|
-
const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
|
|
3015
|
-
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3016
|
-
const sdks = [
|
|
3017
|
-
"@mushi-mushi/react",
|
|
3018
|
-
"@mushi-mushi/web",
|
|
3019
|
-
"@mushi-mushi/core",
|
|
3020
|
-
"@mushi-mushi/react-native"
|
|
3021
|
-
];
|
|
3022
|
-
const installed2 = sdks.filter((s) => deps[s]);
|
|
3023
|
-
return {
|
|
3024
|
-
name: "SDK installed in this repo",
|
|
3025
|
-
ok: installed2.length > 0,
|
|
3026
|
-
detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
|
|
2950
|
+
emitEvent("dispatch.start", { reportId, agent: opts.agent, model: opts.model ?? null });
|
|
2951
|
+
const body = {
|
|
2952
|
+
reportId,
|
|
2953
|
+
projectId: cfg.projectId,
|
|
2954
|
+
agent: opts.agent
|
|
3027
2955
|
};
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
}
|
|
3032
|
-
async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
3033
|
-
if (!config.projectId || !config.apiKey || !config.endpoint) {
|
|
3034
|
-
return [
|
|
3035
|
-
{
|
|
3036
|
-
name: "Server preflight",
|
|
3037
|
-
ok: false,
|
|
3038
|
-
detail: "Need projectId, apiKey, and endpoint. Run `mushi login` and `mushi config projectId <uuid>`."
|
|
3039
|
-
}
|
|
3040
|
-
];
|
|
3041
|
-
}
|
|
3042
|
-
try {
|
|
3043
|
-
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
3044
|
-
const res = await doFetch(
|
|
3045
|
-
`${endpoint}/v1/admin/projects/${projectId}/preflight`,
|
|
3046
|
-
{
|
|
3047
|
-
headers: apiKeyHeaders(apiKey, projectId),
|
|
3048
|
-
signal: AbortSignal.timeout(8e3)
|
|
3049
|
-
}
|
|
3050
|
-
);
|
|
3051
|
-
if (res.ok) {
|
|
3052
|
-
const body = await res.json();
|
|
3053
|
-
const serverChecks = body.data?.checks ?? [];
|
|
3054
|
-
return serverChecks.map((sc) => ({
|
|
3055
|
-
name: `[server] ${sc.label}`,
|
|
3056
|
-
ok: sc.ready,
|
|
3057
|
-
detail: sc.ready ? "" : sc.hint
|
|
3058
|
-
}));
|
|
2956
|
+
if (opts.agent === "cursor_cloud") {
|
|
2957
|
+
if (opts.model) body.cursorModel = opts.model;
|
|
2958
|
+
if (!opts.autoPr) body.cursorAutoCreatePR = false;
|
|
3059
2959
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
return [
|
|
3069
|
-
{
|
|
3070
|
-
name: "Server preflight",
|
|
3071
|
-
ok: true,
|
|
3072
|
-
detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and unlock admin checks."
|
|
3073
|
-
}
|
|
3074
|
-
];
|
|
3075
|
-
}
|
|
2960
|
+
const result = await apiCall("/v1/admin/fixes/dispatch", cfg, {
|
|
2961
|
+
method: "POST",
|
|
2962
|
+
headers: { "Content-Type": "application/json" },
|
|
2963
|
+
body: JSON.stringify(body)
|
|
2964
|
+
});
|
|
2965
|
+
if (!result.ok) {
|
|
2966
|
+
console.error("Error dispatching fix:", result.error.message);
|
|
2967
|
+
process.exit(1);
|
|
3076
2968
|
}
|
|
3077
|
-
const
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
ok: false,
|
|
3082
|
-
detail: `HTTP ${res.status}: ${text2.slice(0, 120)}`
|
|
3083
|
-
}
|
|
3084
|
-
];
|
|
3085
|
-
} catch (err) {
|
|
3086
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3087
|
-
return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
|
-
async function checkIngestSetup(config, doFetch = globalThis.fetch) {
|
|
3091
|
-
if (!config.apiKey || !config.endpoint) {
|
|
3092
|
-
return [
|
|
3093
|
-
{
|
|
3094
|
-
name: "Ingest setup",
|
|
3095
|
-
ok: false,
|
|
3096
|
-
detail: "Need apiKey and endpoint. Run `mushi connect`."
|
|
3097
|
-
}
|
|
3098
|
-
];
|
|
3099
|
-
}
|
|
3100
|
-
try {
|
|
3101
|
-
const data = await fetchIngestSetup(
|
|
3102
|
-
{ endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
|
|
3103
|
-
doFetch
|
|
3104
|
-
);
|
|
3105
|
-
if (!data) {
|
|
3106
|
-
return [{ name: "Ingest setup", ok: false, detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload" }];
|
|
2969
|
+
const { fixId, status, agentId, runId, prUrl } = result.data;
|
|
2970
|
+
emitEvent("dispatch.ok", { fixId, status, agentId, runId, prUrl });
|
|
2971
|
+
if (!opts.wait) {
|
|
2972
|
+
process.exit(0);
|
|
3107
2973
|
}
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
ok: s.complete,
|
|
3112
|
-
detail: s.complete ? "" : s.hint ?? ""
|
|
3113
|
-
}));
|
|
3114
|
-
const diag = data.diagnostic;
|
|
3115
|
-
if (diag?.last_sdk_seen_at) {
|
|
3116
|
-
checks.push({
|
|
3117
|
-
name: "[ingest] Last SDK heartbeat",
|
|
3118
|
-
ok: true,
|
|
3119
|
-
detail: `${diag.last_sdk_seen_at}${diag.last_sdk_endpoint_host ? ` @ ${diag.last_sdk_endpoint_host}` : ""}`
|
|
3120
|
-
});
|
|
2974
|
+
if (!fixId) {
|
|
2975
|
+
console.error("No fixId returned \u2014 cannot poll.");
|
|
2976
|
+
process.exit(1);
|
|
3121
2977
|
}
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
const
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
{
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
detail: "Need projectId, apiKey, and endpoint for QA story checks."
|
|
2978
|
+
const POLL_MS = 5e3;
|
|
2979
|
+
const MAX_POLLS = 120;
|
|
2980
|
+
const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "error", "cancelled", "skipped", "skipped_unsupported_agent", "skipped_no_sandbox"]);
|
|
2981
|
+
for (let i = 0; i < MAX_POLLS; i++) {
|
|
2982
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
2983
|
+
const pollResult = await apiCall(
|
|
2984
|
+
`/v1/admin/fixes/${fixId}`,
|
|
2985
|
+
cfg
|
|
2986
|
+
);
|
|
2987
|
+
if (!pollResult.ok) {
|
|
2988
|
+
emitEvent("poll.error", { error: pollResult.error.message });
|
|
2989
|
+
continue;
|
|
3135
2990
|
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
headers,
|
|
3146
|
-
signal: AbortSignal.timeout(8e3)
|
|
2991
|
+
const s = pollResult.data.status;
|
|
2992
|
+
emitEvent("fix.status", { status: s, pr_url: pollResult.data.pr_url, cursor_agent_id: pollResult.data.cursor_agent_id });
|
|
2993
|
+
if (s && TERMINAL.has(s)) {
|
|
2994
|
+
const success = s === "completed";
|
|
2995
|
+
if (!success) {
|
|
2996
|
+
console.error(`Fix ended with status: ${s}${pollResult.data.error ? ` \u2014 ${pollResult.data.error}` : ""}`);
|
|
2997
|
+
process.exit(1);
|
|
2998
|
+
}
|
|
2999
|
+
process.exit(0);
|
|
3147
3000
|
}
|
|
3148
|
-
);
|
|
3149
|
-
if (!storiesRes.ok) {
|
|
3150
|
-
checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
|
|
3151
|
-
return checks;
|
|
3152
3001
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3002
|
+
console.error("Polling timed out after 10 minutes. The fix may still be running.");
|
|
3003
|
+
process.exit(1);
|
|
3004
|
+
});
|
|
3005
|
+
const fixes = program2.command("fixes").description("Fix dispatch management");
|
|
3006
|
+
fixes.command("tail").description(
|
|
3007
|
+
"Stream SSE dispatch events for a report in real time. Useful for headless debugging without opening the browser."
|
|
3008
|
+
).requiredOption("--report-id <id>", "Report ID to follow").action(async (opts) => {
|
|
3009
|
+
const config = loadConfig();
|
|
3010
|
+
if (!config.apiKey) {
|
|
3011
|
+
console.error("Run `mushi login` first");
|
|
3012
|
+
process.exit(1);
|
|
3159
3013
|
}
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
detail: `${enabled.length} enabled story/stories configured`
|
|
3164
|
-
});
|
|
3165
|
-
const slackRes = await doFetch(
|
|
3166
|
-
`${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
|
|
3167
|
-
{
|
|
3168
|
-
method: "POST",
|
|
3169
|
-
headers,
|
|
3170
|
-
signal: AbortSignal.timeout(6e3)
|
|
3171
|
-
}
|
|
3172
|
-
);
|
|
3173
|
-
const slackBody = slackRes.ok ? await slackRes.json() : null;
|
|
3174
|
-
const slackOk = slackBody?.status === "ok";
|
|
3175
|
-
checks.push({
|
|
3176
|
-
name: "[qa] Slack notifications configured",
|
|
3177
|
-
ok: slackOk,
|
|
3178
|
-
detail: slackOk ? "Slack connected \u2014 failures will notify your channel" : "Slack not connected \u2014 you won't be notified when stories fail. Visit /integrations \u2192 Add to Slack."
|
|
3179
|
-
});
|
|
3180
|
-
const fcRes = await doFetch(
|
|
3181
|
-
`${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
|
|
3182
|
-
{
|
|
3183
|
-
method: "POST",
|
|
3184
|
-
headers,
|
|
3185
|
-
signal: AbortSignal.timeout(6e3)
|
|
3186
|
-
}
|
|
3187
|
-
);
|
|
3188
|
-
const fcBody = fcRes.ok ? await fcRes.json() : null;
|
|
3189
|
-
const hasFirecrawlStories = enabled.some(
|
|
3190
|
-
(s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
|
|
3191
|
-
);
|
|
3192
|
-
if (hasFirecrawlStories) {
|
|
3193
|
-
const fcOk = fcBody?.status === "ok";
|
|
3194
|
-
checks.push({
|
|
3195
|
-
name: "[qa] Firecrawl API key configured",
|
|
3196
|
-
ok: fcOk,
|
|
3197
|
-
detail: fcOk ? "Firecrawl key is resolvable \u2014 stories will run without Unauthorized errors" : "No Firecrawl key found \u2014 enabled stories using firecrawl_actions will 401. Add a key at /integrations \u2192 BYOK keys."
|
|
3198
|
-
});
|
|
3014
|
+
if (!config.endpoint) {
|
|
3015
|
+
console.error("No endpoint configured. Run `mushi init`");
|
|
3016
|
+
process.exit(1);
|
|
3199
3017
|
}
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3211
|
-
const root = resolve3(cwd);
|
|
3212
|
-
const pkgPath = join5(root, "package.json");
|
|
3213
|
-
const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
|
|
3214
|
-
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3215
|
-
const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
|
|
3216
|
-
const envCandidates = [".env.local", ".env"];
|
|
3217
|
-
let envContent = "";
|
|
3218
|
-
for (const f of envCandidates) {
|
|
3219
|
-
try {
|
|
3220
|
-
envContent = await readFile2(join5(root, f), "utf8");
|
|
3221
|
-
break;
|
|
3222
|
-
} catch {
|
|
3018
|
+
const url = `${config.endpoint}/v1/admin/reports/${opts.reportId}/dispatch/stream`;
|
|
3019
|
+
console.log(`Tailing dispatch stream for report ${opts.reportId}\u2026`);
|
|
3020
|
+
console.log(`(Ctrl-C to stop)
|
|
3021
|
+
`);
|
|
3022
|
+
const res = await fetch(url, {
|
|
3023
|
+
headers: {
|
|
3024
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
3025
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
3026
|
+
"X-Mushi-Project": config.projectId ?? "",
|
|
3027
|
+
"Accept": "text/event-stream"
|
|
3223
3028
|
}
|
|
3224
|
-
}
|
|
3225
|
-
const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
|
|
3226
|
-
const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(envContent);
|
|
3227
|
-
checks.push({
|
|
3228
|
-
name: "[host] Mushi env vars in .env.local",
|
|
3229
|
-
ok: hasProjectId && hasApiKey,
|
|
3230
|
-
detail: hasProjectId && hasApiKey ? "VITE_/MUSHI_ project id + API key found" : "Run `mushi connect --write-env` or add VITE_MUSHI_PROJECT_ID + VITE_MUSHI_API_KEY"
|
|
3231
3029
|
});
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
await
|
|
3235
|
-
|
|
3236
|
-
|
|
3030
|
+
if (!res.ok || !res.body) {
|
|
3031
|
+
console.error(`Failed to connect: HTTP ${res.status}`);
|
|
3032
|
+
const text2 = await res.text().catch(() => "");
|
|
3033
|
+
if (text2) console.error(text2.slice(0, 300));
|
|
3034
|
+
process.exit(1);
|
|
3237
3035
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3036
|
+
const decoder = new TextDecoder();
|
|
3037
|
+
const reader = res.body.getReader();
|
|
3038
|
+
let done = false;
|
|
3039
|
+
process.on("SIGINT", () => {
|
|
3040
|
+
done = true;
|
|
3041
|
+
void reader.cancel();
|
|
3042
|
+
console.log("\nDisconnected.");
|
|
3043
|
+
process.exit(0);
|
|
3044
|
+
});
|
|
3045
|
+
while (!done) {
|
|
3046
|
+
const { value, done: streamDone } = await reader.read();
|
|
3047
|
+
if (streamDone) break;
|
|
3048
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
3049
|
+
for (const line of chunk.split("\n")) {
|
|
3050
|
+
if (line.startsWith("data: ")) {
|
|
3051
|
+
const raw = line.slice(6).trim();
|
|
3052
|
+
if (raw === "[DONE]") {
|
|
3053
|
+
console.log("\n[stream ended]");
|
|
3054
|
+
process.exit(0);
|
|
3055
|
+
}
|
|
3056
|
+
try {
|
|
3057
|
+
const event = JSON.parse(raw);
|
|
3058
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3059
|
+
const type = event.type ?? event.event ?? "event";
|
|
3060
|
+
const status = event.status ?? event.data ?? "";
|
|
3061
|
+
console.log(`${ts} ${type.padEnd(24)} ${status}`);
|
|
3062
|
+
} catch {
|
|
3063
|
+
console.log(line);
|
|
3064
|
+
}
|
|
3065
|
+
} else if (line.startsWith("event: ")) {
|
|
3066
|
+
} else if (line && !line.startsWith(":")) {
|
|
3067
|
+
console.log(line);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
});
|
|
3072
|
+
fixes.command("merge <fixId>").description(
|
|
3073
|
+
"Squash-merge the fix PR on GitHub and mark the report Fixed (same as console merge)"
|
|
3074
|
+
).option(
|
|
3075
|
+
"--method <method>",
|
|
3076
|
+
"GitHub merge method: squash (default), merge, or rebase",
|
|
3077
|
+
"squash"
|
|
3078
|
+
).option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
|
|
3079
|
+
const config = loadConfig();
|
|
3080
|
+
if (!config.apiKey) {
|
|
3081
|
+
console.error("Run `mushi login` first");
|
|
3082
|
+
process.exit(1);
|
|
3083
|
+
}
|
|
3084
|
+
if (!config.endpoint) {
|
|
3085
|
+
console.error("No endpoint configured. Run `mushi init`");
|
|
3086
|
+
process.exit(1);
|
|
3087
|
+
}
|
|
3088
|
+
const method = opts.method;
|
|
3089
|
+
if (!["squash", "merge", "rebase"].includes(method)) {
|
|
3090
|
+
console.error("--method must be squash, merge, or rebase");
|
|
3091
|
+
process.exit(1);
|
|
3092
|
+
}
|
|
3093
|
+
const result = await apiCall(`/v1/admin/fixes/${fixId}/merge`, config, {
|
|
3094
|
+
method: "POST",
|
|
3095
|
+
body: JSON.stringify({ mergeMethod: method })
|
|
3096
|
+
});
|
|
3097
|
+
if (!result.ok) {
|
|
3098
|
+
if (opts.json) {
|
|
3099
|
+
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
3100
|
+
} else {
|
|
3101
|
+
console.error("Merge failed:", result.error.message);
|
|
3102
|
+
}
|
|
3103
|
+
process.exit(1);
|
|
3104
|
+
}
|
|
3105
|
+
const data = result.data ?? {};
|
|
3106
|
+
if (opts.json) {
|
|
3107
|
+
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
if (data.alreadyMerged) {
|
|
3111
|
+
console.log(`PR was already merged. Report status: ${data.reportStatus ?? "unknown"}`);
|
|
3112
|
+
} else {
|
|
3113
|
+
console.log(`Merged successfully. Report status: ${data.reportStatus ?? "fixed"}`);
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
fixes.command("refresh-ci <fixId>").description("Pull latest GitHub Actions check-run status for a fix attempt (on-demand ci-sync)").option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
|
|
3117
|
+
const config = loadConfig();
|
|
3118
|
+
if (!config.apiKey) {
|
|
3119
|
+
console.error("Run `mushi login` first");
|
|
3120
|
+
process.exit(1);
|
|
3121
|
+
}
|
|
3122
|
+
if (!config.endpoint) {
|
|
3123
|
+
console.error("No endpoint configured. Run `mushi init`");
|
|
3124
|
+
process.exit(1);
|
|
3125
|
+
}
|
|
3126
|
+
const result = await apiCall(`/v1/admin/fixes/${fixId}/refresh-ci`, config, { method: "POST" });
|
|
3127
|
+
if (!result.ok) {
|
|
3128
|
+
if (opts.json) {
|
|
3129
|
+
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
3130
|
+
} else {
|
|
3131
|
+
console.error("CI refresh failed:", result.error.message);
|
|
3132
|
+
}
|
|
3133
|
+
process.exit(1);
|
|
3134
|
+
}
|
|
3135
|
+
const data = result.data ?? {};
|
|
3136
|
+
if (opts.json) {
|
|
3137
|
+
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
const status = data.check_run_status ?? "unknown";
|
|
3141
|
+
const conclusion = data.check_run_conclusion ?? "\u2014";
|
|
3142
|
+
const updated = data.check_run_updated_at ? new Date(data.check_run_updated_at).toISOString() : "\u2014";
|
|
3143
|
+
console.log(`CI status: ${status} \xB7 conclusion: ${conclusion} \xB7 updated: ${updated}`);
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
// src/nudge.ts
|
|
3148
|
+
var PRESETS = {
|
|
3149
|
+
// Alpha: noisy is OK because there are very few users and you need
|
|
3150
|
+
// signal yesterday. Show all triggers, short cooldowns.
|
|
3151
|
+
alpha: {
|
|
3152
|
+
maxProactivePerSession: 3,
|
|
3153
|
+
dismissCooldownHours: 6,
|
|
3154
|
+
suppressAfterDismissals: 5,
|
|
3155
|
+
pageDwellMinutes: 3,
|
|
3156
|
+
firstSessionSeconds: 30,
|
|
3157
|
+
rageClick: true,
|
|
3158
|
+
longTask: true,
|
|
3159
|
+
apiCascade: true,
|
|
3160
|
+
errorBoundary: true,
|
|
3161
|
+
betaMode: true,
|
|
3162
|
+
featureRequestCard: true
|
|
3163
|
+
},
|
|
3164
|
+
// Beta: most apps spend the longest here. Conservative cadence so
|
|
3165
|
+
// we don't poison the well, but every trigger still on.
|
|
3166
|
+
beta: {
|
|
3167
|
+
maxProactivePerSession: 2,
|
|
3168
|
+
dismissCooldownHours: 24,
|
|
3169
|
+
suppressAfterDismissals: 3,
|
|
3170
|
+
pageDwellMinutes: 5,
|
|
3171
|
+
firstSessionSeconds: 45,
|
|
3172
|
+
rageClick: true,
|
|
3173
|
+
longTask: true,
|
|
3174
|
+
apiCascade: true,
|
|
3175
|
+
errorBoundary: true,
|
|
3176
|
+
betaMode: true,
|
|
3177
|
+
featureRequestCard: true
|
|
3178
|
+
},
|
|
3179
|
+
// GA / production: only the technical signals (error boundary, rage
|
|
3180
|
+
// click). No page-dwell, no welcome — those are explicitly beta-only
|
|
3181
|
+
// patterns. Feature-request card still on because it's user-initiated.
|
|
3182
|
+
ga: {
|
|
3183
|
+
maxProactivePerSession: 1,
|
|
3184
|
+
dismissCooldownHours: 168,
|
|
3185
|
+
suppressAfterDismissals: 2,
|
|
3186
|
+
pageDwellMinutes: 0,
|
|
3187
|
+
firstSessionSeconds: 0,
|
|
3188
|
+
rageClick: true,
|
|
3189
|
+
longTask: false,
|
|
3190
|
+
apiCascade: true,
|
|
3191
|
+
errorBoundary: true,
|
|
3192
|
+
betaMode: false,
|
|
3193
|
+
featureRequestCard: true
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
3196
|
+
function renderNudgeSnippet(opts) {
|
|
3197
|
+
const p3 = { ...PRESETS[opts.phase], ...opts.overrides };
|
|
3198
|
+
const dwellMs = p3.pageDwellMinutes > 0 ? `${p3.pageDwellMinutes} * 60 * 1000` : null;
|
|
3199
|
+
const firstMs = p3.firstSessionSeconds > 0 ? `${p3.firstSessionSeconds} * 1000` : null;
|
|
3200
|
+
const proactiveLines = [];
|
|
3201
|
+
if (p3.rageClick) proactiveLines.push(` rageClick: true,`);
|
|
3202
|
+
if (p3.longTask) proactiveLines.push(` longTask: true,`);
|
|
3203
|
+
if (p3.apiCascade) proactiveLines.push(` apiCascade: true,`);
|
|
3204
|
+
if (p3.errorBoundary) proactiveLines.push(` errorBoundary: true,`);
|
|
3205
|
+
if (dwellMs) proactiveLines.push(` pageDwell: { thresholdMs: ${dwellMs} }, // ${p3.pageDwellMinutes}min on the same route`);
|
|
3206
|
+
if (firstMs) proactiveLines.push(` firstSession: { delayMs: ${firstMs} }, // welcome new users after ${p3.firstSessionSeconds}s`);
|
|
3207
|
+
proactiveLines.push(` cooldown: {`);
|
|
3208
|
+
proactiveLines.push(` maxProactivePerSession: ${p3.maxProactivePerSession},`);
|
|
3209
|
+
proactiveLines.push(` dismissCooldownHours: ${p3.dismissCooldownHours},`);
|
|
3210
|
+
proactiveLines.push(` suppressAfterDismissals: ${p3.suppressAfterDismissals},`);
|
|
3211
|
+
proactiveLines.push(` },`);
|
|
3212
|
+
const widgetLines = [];
|
|
3213
|
+
if (p3.featureRequestCard) widgetLines.push(` featureRequestCard: true,`);
|
|
3214
|
+
if (p3.betaMode) {
|
|
3215
|
+
widgetLines.push(` betaMode: {`);
|
|
3216
|
+
widgetLines.push(` enabled: true,`);
|
|
3217
|
+
widgetLines.push(` label: 'BETA',`);
|
|
3218
|
+
widgetLines.push(` // changelogItems: [{ version: '1.0.0', date: '${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}', items: ['Fixed login timeout on slow networks'] }],`);
|
|
3219
|
+
widgetLines.push(` },`);
|
|
3220
|
+
}
|
|
3221
|
+
return [
|
|
3222
|
+
`// Generated by \`mushi nudge --phase ${opts.phase}\``,
|
|
3223
|
+
`// Phase: ${opts.phase} \u2014 tune via --max, --cooldown, --dwell, --welcome.`,
|
|
3224
|
+
`Mushi.init({`,
|
|
3225
|
+
` projectId: process.env.MUSHI_PROJECT_ID!,`,
|
|
3226
|
+
` apiKey: process.env.MUSHI_API_KEY!,`,
|
|
3227
|
+
` proactive: {`,
|
|
3228
|
+
...proactiveLines,
|
|
3229
|
+
` },`,
|
|
3230
|
+
...widgetLines.length ? [` widget: {`, ...widgetLines, ` },`] : [],
|
|
3231
|
+
`})`,
|
|
3232
|
+
``
|
|
3233
|
+
].join("\n");
|
|
3234
|
+
}
|
|
3235
|
+
function renderNudgeExplainer(phase) {
|
|
3236
|
+
const p3 = PRESETS[phase];
|
|
3237
|
+
const lines = [];
|
|
3238
|
+
lines.push(``);
|
|
3239
|
+
lines.push(`Nudge preset for "${phase}" phase:`);
|
|
3240
|
+
lines.push(` - max ${p3.maxProactivePerSession} proactive prompts per session`);
|
|
3241
|
+
lines.push(` - ${p3.dismissCooldownHours}h cooldown after a dismissal`);
|
|
3242
|
+
lines.push(` - suppress permanently after ${p3.suppressAfterDismissals} consecutive dismissals`);
|
|
3243
|
+
if (p3.pageDwellMinutes > 0) lines.push(` - fire page-dwell trigger after ${p3.pageDwellMinutes} continuous minutes on a route`);
|
|
3244
|
+
if (p3.firstSessionSeconds > 0) lines.push(` - welcome new users with a button pulse ${p3.firstSessionSeconds}s after init`);
|
|
3245
|
+
lines.push(` - signals enabled: ${[
|
|
3246
|
+
p3.rageClick && "rage-click",
|
|
3247
|
+
p3.longTask && "long-task",
|
|
3248
|
+
p3.apiCascade && "api-cascade",
|
|
3249
|
+
p3.errorBoundary && "error-boundary"
|
|
3250
|
+
].filter(Boolean).join(", ")}`);
|
|
3251
|
+
lines.push(` - feature-request card: ${p3.featureRequestCard ? "shown" : "hidden"}`);
|
|
3252
|
+
lines.push(` - beta-mode UI: ${p3.betaMode ? "on" : "off"}`);
|
|
3253
|
+
lines.push(``);
|
|
3254
|
+
return lines.join("\n");
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// src/commands/nudge.ts
|
|
3258
|
+
function registerNudgeCommand(program2) {
|
|
3259
|
+
program2.command("nudge").description(
|
|
3260
|
+
"Generate a Mushi.init() snippet tuned for your release phase (alpha, beta, ga). Customises proactive triggers, cooldowns, feature-request card, and beta-mode UI."
|
|
3261
|
+
).option("--phase <phase>", "Release phase: alpha | beta | ga", "beta").option("--explain", "Print a human-readable summary of what the preset does").option("--max <n>", "Override maxProactivePerSession").option("--cooldown <hours>", "Override dismissCooldownHours").option("--dwell <minutes>", "Override page-dwell threshold (0 disables)").option("--welcome <seconds>", "Override first-session welcome delay (0 disables)").action((opts) => {
|
|
3262
|
+
const validPhases = ["alpha", "beta", "ga"];
|
|
3263
|
+
if (!validPhases.includes(opts.phase)) {
|
|
3264
|
+
console.error(`Unknown phase "${opts.phase}". Use one of: ${validPhases.join(", ")}`);
|
|
3265
|
+
process.exit(1);
|
|
3266
|
+
}
|
|
3267
|
+
const phase = opts.phase;
|
|
3268
|
+
const overrides = {};
|
|
3269
|
+
const parseNumericFlag = (flag, raw, min) => {
|
|
3270
|
+
const n = Number(raw);
|
|
3271
|
+
if (!Number.isFinite(n) || n < min) {
|
|
3272
|
+
console.error(
|
|
3273
|
+
`error: --${flag} must be a finite number >= ${min} (got "${raw}")`
|
|
3274
|
+
);
|
|
3275
|
+
process.exit(1);
|
|
3276
|
+
}
|
|
3277
|
+
return n;
|
|
3278
|
+
};
|
|
3279
|
+
if (opts.max !== void 0) overrides.maxProactivePerSession = parseNumericFlag("max", opts.max, 1);
|
|
3280
|
+
if (opts.cooldown !== void 0) overrides.dismissCooldownHours = parseNumericFlag("cooldown", opts.cooldown, 0);
|
|
3281
|
+
if (opts.dwell !== void 0) overrides.pageDwellMinutes = parseNumericFlag("dwell", opts.dwell, 0);
|
|
3282
|
+
if (opts.welcome !== void 0) overrides.firstSessionSeconds = parseNumericFlag("welcome", opts.welcome, 0);
|
|
3283
|
+
if (opts.explain) {
|
|
3284
|
+
console.log(renderNudgeExplainer(phase));
|
|
3285
|
+
}
|
|
3286
|
+
console.log(renderNudgeSnippet({ phase, overrides }));
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
// src/upgrade.ts
|
|
3291
|
+
import { resolve as resolve2 } from "path";
|
|
3292
|
+
import { execSync } from "child_process";
|
|
3293
|
+
var SAFE_NPM_VERSION = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
3294
|
+
var MUSHI_PACKAGES = [
|
|
3295
|
+
"@mushi-mushi/web",
|
|
3296
|
+
"@mushi-mushi/core",
|
|
3297
|
+
"@mushi-mushi/react",
|
|
3298
|
+
"@mushi-mushi/react-native",
|
|
3299
|
+
"@mushi-mushi/capacitor",
|
|
3300
|
+
"@mushi-mushi/node",
|
|
3301
|
+
"@mushi-mushi/cli"
|
|
3302
|
+
];
|
|
3303
|
+
async function planUpgrade(cwd) {
|
|
3304
|
+
const root = resolve2(cwd);
|
|
3305
|
+
const pkg = readPackageJson(root);
|
|
3306
|
+
const pm = detectPackageManager(root);
|
|
3307
|
+
const deps = {
|
|
3308
|
+
...pkg?.dependencies ?? {},
|
|
3309
|
+
...pkg?.devDependencies ?? {}
|
|
3310
|
+
};
|
|
3311
|
+
const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
|
|
3312
|
+
const entries = await Promise.all(
|
|
3313
|
+
installed2.map(async (name) => {
|
|
3314
|
+
const current = deps[name] ?? "";
|
|
3315
|
+
const currentCore = current.replace(/^[\^~>=<]*/, "");
|
|
3316
|
+
if (!/^\d/.test(currentCore)) {
|
|
3317
|
+
return { name, current, latest: null, willUpgrade: false };
|
|
3318
|
+
}
|
|
3319
|
+
const freshness = await checkFreshness(name, currentCore, {
|
|
3320
|
+
timeoutMs: 4e3,
|
|
3321
|
+
ignoreOptOut: true
|
|
3322
|
+
});
|
|
3323
|
+
const latest = freshness?.latest ?? null;
|
|
3324
|
+
const safeLatest = latest && SAFE_NPM_VERSION.test(latest) ? latest : null;
|
|
3325
|
+
const willUpgrade = Boolean(freshness?.isOutdated && safeLatest);
|
|
3326
|
+
return {
|
|
3327
|
+
name,
|
|
3328
|
+
current,
|
|
3329
|
+
latest: safeLatest,
|
|
3330
|
+
willUpgrade,
|
|
3331
|
+
migrateToWeb: name === "@mushi-mushi/react" && willUpgrade
|
|
3332
|
+
};
|
|
3333
|
+
})
|
|
3334
|
+
);
|
|
3335
|
+
const toBump = entries.filter((e) => e.willUpgrade && e.latest).map((e) => `${e.name}@${e.latest}`);
|
|
3336
|
+
return {
|
|
3337
|
+
cwd: root,
|
|
3338
|
+
packageManager: pm,
|
|
3339
|
+
entries,
|
|
3340
|
+
installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
async function runUpgrade(opts = {}) {
|
|
3344
|
+
const plan = await planUpgrade(opts.cwd ?? process.cwd());
|
|
3345
|
+
if (plan.entries.length === 0) {
|
|
3346
|
+
return {
|
|
3347
|
+
plan,
|
|
3348
|
+
upgraded: false,
|
|
3349
|
+
message: "No @mushi-mushi/* packages in package.json \u2014 run `mushi init` first."
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
if (!plan.installCmd) {
|
|
3353
|
+
const semverEntries = plan.entries.filter((e) => /\d/.test(e.current));
|
|
3354
|
+
const allChecksFailed = semverEntries.length > 0 && semverEntries.every((e) => e.latest === null);
|
|
3355
|
+
return {
|
|
3356
|
+
plan,
|
|
3357
|
+
upgraded: false,
|
|
3358
|
+
message: allChecksFailed ? "Could not reach the npm registry to check for updates \u2014 try again in a moment." : "All installed Mushi packages are already at the latest stable version."
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
if (opts.dryRun) {
|
|
3362
|
+
return { plan, upgraded: false, message: `[dry-run] Would run: ${plan.installCmd}` };
|
|
3363
|
+
}
|
|
3364
|
+
try {
|
|
3365
|
+
execSync(plan.installCmd, { cwd: plan.cwd, stdio: "inherit", env: process.env });
|
|
3366
|
+
} catch {
|
|
3367
|
+
return {
|
|
3368
|
+
plan,
|
|
3369
|
+
upgraded: false,
|
|
3370
|
+
message: `Install command failed \u2014 fix the error above or run it manually:
|
|
3371
|
+
${plan.installCmd}`
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
const reactEntry = plan.entries.find((e) => e.migrateToWeb);
|
|
3375
|
+
const migrateNote = reactEntry ? "\nNote: @mushi-mushi/react is legacy \u2014 prefer @mushi-mushi/web for Vite/Capacitor/SPA apps." : "";
|
|
3376
|
+
return {
|
|
3377
|
+
plan,
|
|
3378
|
+
upgraded: true,
|
|
3379
|
+
message: `Upgraded Mushi SDK packages.
|
|
3380
|
+
${plan.installCmd}${migrateNote}`
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// src/commands/upgrade-cli.ts
|
|
3385
|
+
function registerUpgradeCommand(program2) {
|
|
3386
|
+
program2.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--dry-run", "Print the install command without running it").option("--json", "Machine-readable plan + result").addHelpText("after", `
|
|
3387
|
+
Examples:
|
|
3388
|
+
mushi upgrade
|
|
3389
|
+
mushi upgrade --dry-run
|
|
3390
|
+
mushi upgrade --cwd ../glot.it`).action(async (opts) => {
|
|
3391
|
+
const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
|
|
3392
|
+
if (opts.json) {
|
|
3393
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3394
|
+
} else {
|
|
3395
|
+
console.log(result.message);
|
|
3396
|
+
for (const e of result.plan.entries) {
|
|
3397
|
+
const tag = e.willUpgrade && e.latest ? `\u2192 v${e.latest}` : "(current)";
|
|
3398
|
+
console.log(` ${e.name}@${e.current} ${tag}`);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
if (!result.upgraded && result.plan.entries.some((e) => e.willUpgrade) && !opts.dryRun) {
|
|
3402
|
+
process.exit(1);
|
|
3403
|
+
}
|
|
3404
|
+
if (result.plan.entries.length === 0) process.exit(1);
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// src/commands/connect-cli.ts
|
|
3409
|
+
function registerConnectCommand(program2) {
|
|
3410
|
+
program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "Force-write SDK env vars to .env.local (default; overrides --no-env)").option("--wire-ide", "Force-wire Cursor MCP into .cursor/mcp.json (default; overrides --no-ide)").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
|
|
3411
|
+
Examples:
|
|
3412
|
+
MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
|
|
3413
|
+
mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
|
|
3414
|
+
const apiKey = process.env.MUSHI_API_KEY ?? opts.apiKey;
|
|
3415
|
+
if (!apiKey) {
|
|
3416
|
+
console.error("Provide the API key via the MUSHI_API_KEY env var (recommended) or --api-key <key>.");
|
|
3417
|
+
process.exit(1);
|
|
3418
|
+
}
|
|
3419
|
+
const result = await runConnect({
|
|
3420
|
+
apiKey,
|
|
3421
|
+
projectId: opts.projectId,
|
|
3422
|
+
endpoint: opts.endpoint,
|
|
3423
|
+
cwd: opts.cwd,
|
|
3424
|
+
writeEnv: opts.writeEnv === true ? true : opts.env !== false,
|
|
3425
|
+
wireIde: opts.wireIde === true ? true : opts.ide !== false,
|
|
3426
|
+
wait: opts.wait,
|
|
3427
|
+
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
3428
|
+
json: opts.json
|
|
3429
|
+
});
|
|
3430
|
+
if (opts.json) {
|
|
3431
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3432
|
+
} else {
|
|
3433
|
+
for (const line of result.messages) console.log(line);
|
|
3434
|
+
}
|
|
3435
|
+
if (!result.ok) process.exit(1);
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// src/doctor.ts
|
|
3440
|
+
function checkCliConfig(config) {
|
|
3441
|
+
return [
|
|
3442
|
+
{
|
|
3443
|
+
name: "CLI config file",
|
|
3444
|
+
ok: Boolean(config.endpoint),
|
|
3445
|
+
detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint \u2014 set MUSHI_API_ENDPOINT, run `mushi connect`, or `mushi config endpoint <url>`"
|
|
3446
|
+
},
|
|
3447
|
+
{
|
|
3448
|
+
name: "API key configured",
|
|
3449
|
+
ok: Boolean(config.apiKey),
|
|
3450
|
+
detail: config.apiKey ? `apiKey=${config.apiKey.slice(0, 8)}\u2026${config.apiKey.slice(-4)}` : "No API key set \u2014 run `mushi login --api-key <key>`"
|
|
3451
|
+
},
|
|
3452
|
+
{
|
|
3453
|
+
name: "Project ID configured",
|
|
3454
|
+
ok: Boolean(config.projectId),
|
|
3455
|
+
detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
|
|
3456
|
+
}
|
|
3457
|
+
];
|
|
3458
|
+
}
|
|
3459
|
+
async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
3460
|
+
try {
|
|
3461
|
+
const safeEndpoint = sanitizeEndpoint(endpoint);
|
|
3462
|
+
const res = await doFetch(`${safeEndpoint}/health`, {
|
|
3463
|
+
signal: AbortSignal.timeout(5e3)
|
|
3464
|
+
});
|
|
3465
|
+
return {
|
|
3466
|
+
name: "Endpoint reachable",
|
|
3467
|
+
ok: res.status === 200,
|
|
3468
|
+
detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
|
|
3469
|
+
};
|
|
3470
|
+
} catch (err) {
|
|
3471
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3472
|
+
return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
async function checkSdkInstall(cwd) {
|
|
3476
|
+
try {
|
|
3477
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
3478
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3479
|
+
const root = resolve3(cwd);
|
|
3480
|
+
const pkgPath = join5(root, "package.json");
|
|
3481
|
+
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
3482
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3483
|
+
const sdks = [
|
|
3484
|
+
"@mushi-mushi/react",
|
|
3485
|
+
"@mushi-mushi/web",
|
|
3486
|
+
"@mushi-mushi/core",
|
|
3487
|
+
"@mushi-mushi/react-native"
|
|
3488
|
+
];
|
|
3489
|
+
const installed2 = sdks.filter((s) => deps[s]);
|
|
3490
|
+
return {
|
|
3491
|
+
name: "SDK installed in this repo",
|
|
3492
|
+
ok: installed2.length > 0,
|
|
3493
|
+
detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
|
|
3494
|
+
};
|
|
3495
|
+
} catch {
|
|
3496
|
+
return null;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
3500
|
+
if (!config.projectId || !config.apiKey || !config.endpoint) {
|
|
3501
|
+
return [
|
|
3502
|
+
{
|
|
3503
|
+
name: "Server preflight",
|
|
3504
|
+
ok: false,
|
|
3505
|
+
detail: "Need projectId, apiKey, and endpoint. Run `mushi login` and `mushi config projectId <uuid>`."
|
|
3506
|
+
}
|
|
3507
|
+
];
|
|
3508
|
+
}
|
|
3509
|
+
try {
|
|
3510
|
+
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
3511
|
+
const res = await doFetch(
|
|
3512
|
+
`${endpoint}/v1/admin/projects/${projectId}/preflight`,
|
|
3513
|
+
{
|
|
3514
|
+
headers: apiKeyHeaders(apiKey, projectId),
|
|
3515
|
+
signal: AbortSignal.timeout(8e3)
|
|
3516
|
+
}
|
|
3517
|
+
);
|
|
3518
|
+
if (res.ok) {
|
|
3519
|
+
const body = await res.json();
|
|
3520
|
+
const serverChecks = body.data?.checks ?? [];
|
|
3521
|
+
return serverChecks.map((sc) => ({
|
|
3522
|
+
name: `[server] ${sc.label}`,
|
|
3523
|
+
ok: sc.ready,
|
|
3524
|
+
detail: sc.ready ? "" : sc.hint
|
|
3525
|
+
}));
|
|
3526
|
+
}
|
|
3527
|
+
if (res.status === 403) {
|
|
3528
|
+
let errCode;
|
|
3529
|
+
try {
|
|
3530
|
+
const body = await res.json();
|
|
3531
|
+
errCode = body?.error?.code;
|
|
3532
|
+
} catch {
|
|
3533
|
+
}
|
|
3534
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
3535
|
+
return [
|
|
3536
|
+
{
|
|
3537
|
+
name: "Server preflight",
|
|
3538
|
+
ok: true,
|
|
3539
|
+
detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and run admin checks."
|
|
3540
|
+
}
|
|
3541
|
+
];
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
const text2 = await res.text().catch(() => "");
|
|
3545
|
+
return [
|
|
3546
|
+
{
|
|
3547
|
+
name: "Server preflight",
|
|
3548
|
+
ok: false,
|
|
3549
|
+
detail: `HTTP ${res.status}: ${text2.slice(0, 120)}`
|
|
3550
|
+
}
|
|
3551
|
+
];
|
|
3552
|
+
} catch (err) {
|
|
3553
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3554
|
+
return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
async function checkIngestSetup(config, doFetch = globalThis.fetch) {
|
|
3558
|
+
if (!config.apiKey || !config.endpoint) {
|
|
3559
|
+
return [
|
|
3560
|
+
{
|
|
3561
|
+
name: "Ingest setup",
|
|
3562
|
+
ok: false,
|
|
3563
|
+
detail: "Need apiKey and endpoint. Run `mushi connect`."
|
|
3564
|
+
}
|
|
3565
|
+
];
|
|
3566
|
+
}
|
|
3567
|
+
try {
|
|
3568
|
+
const data = await fetchIngestSetup(
|
|
3569
|
+
{ endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
|
|
3570
|
+
doFetch
|
|
3571
|
+
);
|
|
3572
|
+
if (!data) {
|
|
3573
|
+
return [{ name: "Ingest setup", ok: false, detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload" }];
|
|
3574
|
+
}
|
|
3575
|
+
const steps = data.steps ?? [];
|
|
3576
|
+
const checks = steps.filter((s) => s.required).map((s) => ({
|
|
3577
|
+
name: `[ingest] ${s.label}`,
|
|
3578
|
+
ok: s.complete,
|
|
3579
|
+
detail: s.complete ? "" : s.hint ?? ""
|
|
3580
|
+
}));
|
|
3581
|
+
const diag = data.diagnostic;
|
|
3582
|
+
if (diag?.last_sdk_seen_at) {
|
|
3583
|
+
checks.push({
|
|
3584
|
+
name: "[ingest] Last SDK heartbeat",
|
|
3585
|
+
ok: true,
|
|
3586
|
+
detail: `${diag.last_sdk_seen_at}${diag.last_sdk_endpoint_host ? ` @ ${diag.last_sdk_endpoint_host}` : ""}`
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
return checks.length > 0 ? checks : [{ name: "Ingest setup", ok: false, detail: "Empty response from /v1/sync/ingest-setup" }];
|
|
3590
|
+
} catch (err) {
|
|
3591
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3592
|
+
return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
3596
|
+
if (!config.projectId || !config.apiKey || !config.endpoint) {
|
|
3597
|
+
return [
|
|
3598
|
+
{
|
|
3599
|
+
name: "QA stories health",
|
|
3600
|
+
ok: false,
|
|
3601
|
+
detail: "Need projectId, apiKey, and endpoint for QA story checks."
|
|
3602
|
+
}
|
|
3603
|
+
];
|
|
3604
|
+
}
|
|
3605
|
+
const checks = [];
|
|
3606
|
+
try {
|
|
3607
|
+
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
3608
|
+
const headers = apiKeyHeaders(apiKey, projectId);
|
|
3609
|
+
const storiesRes = await doFetch(
|
|
3610
|
+
`${endpoint}/v1/admin/projects/${projectId}/qa-coverage`,
|
|
3611
|
+
{
|
|
3612
|
+
headers,
|
|
3613
|
+
signal: AbortSignal.timeout(8e3)
|
|
3614
|
+
}
|
|
3615
|
+
);
|
|
3616
|
+
if (!storiesRes.ok) {
|
|
3617
|
+
checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
|
|
3618
|
+
return checks;
|
|
3619
|
+
}
|
|
3620
|
+
const storiesBody = await storiesRes.json();
|
|
3621
|
+
const stories = storiesBody.data?.coverage ?? [];
|
|
3622
|
+
const enabled = stories.filter((s) => s.enabled);
|
|
3623
|
+
if (enabled.length === 0) {
|
|
3624
|
+
checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
|
|
3625
|
+
return checks;
|
|
3626
|
+
}
|
|
3627
|
+
checks.push({
|
|
3628
|
+
name: "[qa] Enabled QA stories",
|
|
3629
|
+
ok: true,
|
|
3630
|
+
detail: `${enabled.length} enabled story/stories configured`
|
|
3631
|
+
});
|
|
3632
|
+
const slackRes = await doFetch(
|
|
3633
|
+
`${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
|
|
3634
|
+
{
|
|
3635
|
+
method: "POST",
|
|
3636
|
+
headers,
|
|
3637
|
+
signal: AbortSignal.timeout(6e3)
|
|
3638
|
+
}
|
|
3639
|
+
);
|
|
3640
|
+
const slackBody = slackRes.ok ? await slackRes.json() : null;
|
|
3641
|
+
const slackOk = slackBody?.status === "ok";
|
|
3642
|
+
checks.push({
|
|
3643
|
+
name: "[qa] Slack notifications configured",
|
|
3644
|
+
ok: slackOk,
|
|
3645
|
+
detail: slackOk ? "Slack connected \u2014 failures will notify your channel" : "Slack not connected \u2014 you won't be notified when stories fail. Visit /integrations \u2192 Add to Slack."
|
|
3646
|
+
});
|
|
3647
|
+
const fcRes = await doFetch(
|
|
3648
|
+
`${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
|
|
3649
|
+
{
|
|
3650
|
+
method: "POST",
|
|
3651
|
+
headers,
|
|
3652
|
+
signal: AbortSignal.timeout(6e3)
|
|
3653
|
+
}
|
|
3654
|
+
);
|
|
3655
|
+
const fcBody = fcRes.ok ? await fcRes.json() : null;
|
|
3656
|
+
const hasFirecrawlStories = enabled.some(
|
|
3657
|
+
(s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
|
|
3658
|
+
);
|
|
3659
|
+
if (hasFirecrawlStories) {
|
|
3660
|
+
const fcOk = fcBody?.status === "ok";
|
|
3661
|
+
checks.push({
|
|
3662
|
+
name: "[qa] Firecrawl API key configured",
|
|
3663
|
+
ok: fcOk,
|
|
3664
|
+
detail: fcOk ? "Firecrawl key is resolvable \u2014 stories will run without Unauthorized errors" : "No Firecrawl key found \u2014 enabled stories using firecrawl_actions will 401. Add a key at /integrations \u2192 BYOK keys."
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
} catch (err) {
|
|
3668
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3669
|
+
checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
|
|
3670
|
+
}
|
|
3671
|
+
return checks;
|
|
3672
|
+
}
|
|
3673
|
+
async function checkHostAppWiring(cwd) {
|
|
3674
|
+
const checks = [];
|
|
3675
|
+
try {
|
|
3676
|
+
const { readFile: readFile3, access } = await import("fs/promises");
|
|
3677
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3678
|
+
const root = resolve3(cwd);
|
|
3679
|
+
const pkgPath = join5(root, "package.json");
|
|
3680
|
+
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
3681
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3682
|
+
const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
|
|
3683
|
+
const envCandidates = [".env.local", ".env"];
|
|
3684
|
+
let envContent = "";
|
|
3685
|
+
for (const f of envCandidates) {
|
|
3686
|
+
try {
|
|
3687
|
+
envContent = await readFile3(join5(root, f), "utf8");
|
|
3688
|
+
break;
|
|
3689
|
+
} catch {
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
|
|
3693
|
+
const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(envContent);
|
|
3694
|
+
checks.push({
|
|
3695
|
+
name: "[host] Mushi env vars in .env.local",
|
|
3696
|
+
ok: hasProjectId && hasApiKey,
|
|
3697
|
+
detail: hasProjectId && hasApiKey ? "VITE_/MUSHI_ project id + API key found" : "Run `mushi connect --write-env` or add VITE_MUSHI_PROJECT_ID + VITE_MUSHI_API_KEY"
|
|
3698
|
+
});
|
|
3699
|
+
let mcpPresent = false;
|
|
3700
|
+
try {
|
|
3701
|
+
await access(join5(root, ".cursor", "mcp.json"));
|
|
3702
|
+
mcpPresent = true;
|
|
3703
|
+
} catch {
|
|
3704
|
+
}
|
|
3705
|
+
checks.push({
|
|
3706
|
+
name: "[host] Cursor MCP config",
|
|
3707
|
+
ok: mcpPresent,
|
|
3708
|
+
detail: mcpPresent ? ".cursor/mcp.json present" : "Run `mushi connect` to wire MCP for two-way reporter replies"
|
|
3242
3709
|
});
|
|
3243
3710
|
if (isCapHybrid) {
|
|
3244
3711
|
const hasWebSdk = Boolean(deps["@mushi-mushi/web"] || deps["@mushi-mushi/react"]);
|
|
@@ -3264,7 +3731,7 @@ async function checkHostAppWiring(cwd) {
|
|
|
3264
3731
|
}
|
|
3265
3732
|
async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
3266
3733
|
const checks = [];
|
|
3267
|
-
const { readFile:
|
|
3734
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
3268
3735
|
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3269
3736
|
const { homedir } = await import("os");
|
|
3270
3737
|
const root = resolve3(cwd);
|
|
@@ -3276,7 +3743,7 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
3276
3743
|
let mcpRaw = null;
|
|
3277
3744
|
for (const candidate of candidates) {
|
|
3278
3745
|
try {
|
|
3279
|
-
const raw = await
|
|
3746
|
+
const raw = await readFile3(candidate, "utf8");
|
|
3280
3747
|
let parsed = {};
|
|
3281
3748
|
try {
|
|
3282
3749
|
parsed = JSON.parse(raw);
|
|
@@ -3386,451 +3853,189 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
3386
3853
|
ok: false,
|
|
3387
3854
|
detail: `Probe failed: ${msg}. Check MUSHI_API_ENDPOINT and network connectivity.`
|
|
3388
3855
|
});
|
|
3389
|
-
}
|
|
3390
|
-
}
|
|
3391
|
-
return checks;
|
|
3392
|
-
}
|
|
3393
|
-
var FIX_HINTS = {
|
|
3394
|
-
"CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
|
|
3395
|
-
"API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
|
|
3396
|
-
"Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
|
|
3397
|
-
"Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
|
|
3398
|
-
"[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
|
|
3399
|
-
"[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
|
|
3400
|
-
"[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
|
|
3401
|
-
};
|
|
3402
|
-
function fixHintForCheck(name) {
|
|
3403
|
-
if (FIX_HINTS[name]) return FIX_HINTS[name];
|
|
3404
|
-
if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
|
|
3405
|
-
if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
|
|
3406
|
-
if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
|
|
3407
|
-
return void 0;
|
|
3408
|
-
}
|
|
3409
|
-
async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = globalThis.fetch) {
|
|
3410
|
-
if (!config.apiKey || !config.projectId || !config.endpoint) {
|
|
3411
|
-
if (config.apiKey && config.endpoint) {
|
|
3412
|
-
return {
|
|
3413
|
-
nextAction: "Select or create a project: run `mushi project create` or pick one with `mushi project list`",
|
|
3414
|
-
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3415
|
-
done: false
|
|
3416
|
-
};
|
|
3417
|
-
}
|
|
3418
|
-
return {
|
|
3419
|
-
nextAction: "Sign in to Mushi: run `npx mushi-mushi` (browser opens automatically \u2014 do NOT type the code in the terminal)",
|
|
3420
|
-
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3421
|
-
done: false
|
|
3422
|
-
};
|
|
3423
|
-
}
|
|
3424
|
-
try {
|
|
3425
|
-
const { readFile: readFile2 } = await import("fs/promises");
|
|
3426
|
-
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3427
|
-
const root = resolve3(cwd);
|
|
3428
|
-
let envContent = "";
|
|
3429
|
-
for (const f of [".env.local", ".env"]) {
|
|
3430
|
-
try {
|
|
3431
|
-
envContent = await readFile2(join5(root, f), "utf8");
|
|
3432
|
-
break;
|
|
3433
|
-
} catch {
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
|
|
3437
|
-
if (!hasEnvVars) {
|
|
3438
|
-
return {
|
|
3439
|
-
nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
|
|
3440
|
-
ctaPath: "/onboarding?tab=sdk",
|
|
3441
|
-
done: false
|
|
3442
|
-
};
|
|
3443
|
-
}
|
|
3444
|
-
} catch {
|
|
3445
|
-
}
|
|
3446
|
-
try {
|
|
3447
|
-
const sdkCheck = await checkSdkInstall(cwd);
|
|
3448
|
-
if (sdkCheck && !sdkCheck.ok) {
|
|
3449
|
-
return {
|
|
3450
|
-
nextAction: `Install the Mushi SDK: ${sdkCheck.detail}`,
|
|
3451
|
-
ctaPath: "/onboarding?tab=sdk",
|
|
3452
|
-
done: false
|
|
3453
|
-
};
|
|
3454
|
-
}
|
|
3455
|
-
} catch {
|
|
3456
|
-
}
|
|
3457
|
-
try {
|
|
3458
|
-
const data = await fetchIngestSetup(
|
|
3459
|
-
{ endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
|
|
3460
|
-
doFetch
|
|
3461
|
-
);
|
|
3462
|
-
if (data) {
|
|
3463
|
-
const steps = data.steps ?? [];
|
|
3464
|
-
const incomplete = steps.find((s) => s.required && !s.complete);
|
|
3465
|
-
if (incomplete) {
|
|
3466
|
-
const tabMap = {
|
|
3467
|
-
project_created: "/onboarding?tab=steps&setup=cli",
|
|
3468
|
-
api_key_generated: "/onboarding?tab=verify",
|
|
3469
|
-
sdk_installed: "/onboarding?tab=sdk",
|
|
3470
|
-
first_report_received: "/onboarding?tab=verify"
|
|
3471
|
-
};
|
|
3472
|
-
const tab = tabMap[incomplete.id] ?? "/onboarding?tab=verify";
|
|
3473
|
-
return {
|
|
3474
|
-
nextAction: incomplete.hint ?? `Complete setup step: ${incomplete.label}`,
|
|
3475
|
-
ctaPath: tab,
|
|
3476
|
-
done: false
|
|
3477
|
-
};
|
|
3478
|
-
}
|
|
3479
|
-
return {
|
|
3480
|
-
nextAction: "All 4 required setup steps are complete! Open the console to explore.",
|
|
3481
|
-
ctaPath: "/reports",
|
|
3482
|
-
done: true
|
|
3483
|
-
};
|
|
3484
|
-
}
|
|
3485
|
-
} catch {
|
|
3486
|
-
}
|
|
3487
|
-
return {
|
|
3488
|
-
nextAction: "Open the onboarding wizard in the console to check your setup status.",
|
|
3489
|
-
ctaPath: "/onboarding",
|
|
3490
|
-
done: false
|
|
3491
|
-
};
|
|
3492
|
-
}
|
|
3493
|
-
async function runDoctor(config, options = {}) {
|
|
3494
|
-
const doFetch = options.fetch ?? globalThis.fetch;
|
|
3495
|
-
const checks = [];
|
|
3496
|
-
const isFull = options.full === true;
|
|
3497
|
-
const runServer = isFull || options.server !== false;
|
|
3498
|
-
const runIngest = isFull || options.ingest !== false;
|
|
3499
|
-
checks.push(...checkCliConfig(config));
|
|
3500
|
-
if (config.endpoint) {
|
|
3501
|
-
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
3502
|
-
}
|
|
3503
|
-
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
3504
|
-
if (sdkCheck) checks.push(sdkCheck);
|
|
3505
|
-
if (runServer) {
|
|
3506
|
-
const serverChecks = await checkServerPreflight(config, doFetch);
|
|
3507
|
-
checks.push(...serverChecks);
|
|
3508
|
-
}
|
|
3509
|
-
if (runIngest) {
|
|
3510
|
-
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
3511
|
-
checks.push(...ingestChecks);
|
|
3512
|
-
}
|
|
3513
|
-
if (isFull || options.qaStories) {
|
|
3514
|
-
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
3515
|
-
checks.push(...qaChecks);
|
|
3516
|
-
}
|
|
3517
|
-
if (isFull || options.hostApp) {
|
|
3518
|
-
const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
|
|
3519
|
-
checks.push(...hostChecks);
|
|
3520
|
-
}
|
|
3521
|
-
if (isFull || options.mcp) {
|
|
3522
|
-
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
3523
|
-
checks.push(...mcpChecks);
|
|
3524
|
-
}
|
|
3525
|
-
const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
|
|
3526
|
-
if (heartbeatPassed) {
|
|
3527
|
-
const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
|
|
3528
|
-
if (idx >= 0) {
|
|
3529
|
-
checks[idx] = {
|
|
3530
|
-
...checks[idx],
|
|
3531
|
-
ok: true,
|
|
3532
|
-
warn: true,
|
|
3533
|
-
detail: "No @mushi-mushi/* found in cwd package.json \u2014 but live heartbeats confirm the SDK is active. Run from your app repo or install with `mushi init`."
|
|
3534
|
-
};
|
|
3535
|
-
}
|
|
3536
|
-
}
|
|
3537
|
-
return { checks, ready: checks.every((c) => c.ok) };
|
|
3538
|
-
}
|
|
3539
|
-
function formatDoctorResult(result) {
|
|
3540
|
-
const PASS = "\u2713";
|
|
3541
|
-
const WARN = "\u26A0";
|
|
3542
|
-
const FAIL = "\u2717";
|
|
3543
|
-
const lines = [];
|
|
3544
|
-
for (const c of result.checks) {
|
|
3545
|
-
const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
|
|
3546
|
-
lines.push(`${icon} ${c.name}`);
|
|
3547
|
-
if (c.detail) lines.push(` ${c.detail}`);
|
|
3548
|
-
if (!c.ok) {
|
|
3549
|
-
const hint = fixHintForCheck(c.name);
|
|
3550
|
-
if (hint) lines.push(` \u2192 Fix: ${hint}`);
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
const failed = result.checks.filter((c) => !c.ok);
|
|
3554
|
-
const warned = result.checks.filter((c) => c.ok && c.warn);
|
|
3555
|
-
if (failed.length === 0) {
|
|
3556
|
-
if (warned.length > 0) {
|
|
3557
|
-
lines.push(`
|
|
3558
|
-
All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
|
|
3559
|
-
} else {
|
|
3560
|
-
lines.push("\nAll checks passed. The CLI is ready.");
|
|
3561
|
-
}
|
|
3562
|
-
} else {
|
|
3563
|
-
lines.push(`
|
|
3564
|
-
${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
3565
|
-
lines.push("Fix the items above and re-run `mushi doctor`.");
|
|
3566
|
-
}
|
|
3567
|
-
return lines.join("\n");
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
// src/upgrade.ts
|
|
3571
|
-
import { resolve as resolve2 } from "path";
|
|
3572
|
-
import { execSync } from "child_process";
|
|
3573
|
-
var SAFE_NPM_VERSION = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
3574
|
-
var MUSHI_PACKAGES = [
|
|
3575
|
-
"@mushi-mushi/web",
|
|
3576
|
-
"@mushi-mushi/core",
|
|
3577
|
-
"@mushi-mushi/react",
|
|
3578
|
-
"@mushi-mushi/react-native",
|
|
3579
|
-
"@mushi-mushi/capacitor",
|
|
3580
|
-
"@mushi-mushi/node",
|
|
3581
|
-
"@mushi-mushi/cli"
|
|
3582
|
-
];
|
|
3583
|
-
async function planUpgrade(cwd) {
|
|
3584
|
-
const root = resolve2(cwd);
|
|
3585
|
-
const pkg = readPackageJson(root);
|
|
3586
|
-
const pm = detectPackageManager(root);
|
|
3587
|
-
const deps = {
|
|
3588
|
-
...pkg?.dependencies ?? {},
|
|
3589
|
-
...pkg?.devDependencies ?? {}
|
|
3590
|
-
};
|
|
3591
|
-
const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
|
|
3592
|
-
const entries = await Promise.all(
|
|
3593
|
-
installed2.map(async (name) => {
|
|
3594
|
-
const current = deps[name] ?? "";
|
|
3595
|
-
const currentCore = current.replace(/^[\^~>=<]*/, "");
|
|
3596
|
-
if (!/^\d/.test(currentCore)) {
|
|
3597
|
-
return { name, current, latest: null, willUpgrade: false };
|
|
3598
|
-
}
|
|
3599
|
-
const freshness = await checkFreshness(name, currentCore, {
|
|
3600
|
-
timeoutMs: 4e3,
|
|
3601
|
-
ignoreOptOut: true
|
|
3602
|
-
});
|
|
3603
|
-
const latest = freshness?.latest ?? null;
|
|
3604
|
-
const safeLatest = latest && SAFE_NPM_VERSION.test(latest) ? latest : null;
|
|
3605
|
-
const willUpgrade = Boolean(freshness?.isOutdated && safeLatest);
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
return checks;
|
|
3859
|
+
}
|
|
3860
|
+
var FIX_HINTS = {
|
|
3861
|
+
"CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
|
|
3862
|
+
"API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
|
|
3863
|
+
"Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
|
|
3864
|
+
"Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
|
|
3865
|
+
"[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
|
|
3866
|
+
"[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
|
|
3867
|
+
"[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
|
|
3868
|
+
};
|
|
3869
|
+
function fixHintForCheck(name) {
|
|
3870
|
+
if (FIX_HINTS[name]) return FIX_HINTS[name];
|
|
3871
|
+
if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
|
|
3872
|
+
if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
|
|
3873
|
+
if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
|
|
3874
|
+
return void 0;
|
|
3875
|
+
}
|
|
3876
|
+
async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = globalThis.fetch) {
|
|
3877
|
+
if (!config.apiKey || !config.projectId || !config.endpoint) {
|
|
3878
|
+
if (config.apiKey && config.endpoint) {
|
|
3606
3879
|
return {
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
willUpgrade,
|
|
3611
|
-
migrateToWeb: name === "@mushi-mushi/react" && willUpgrade
|
|
3880
|
+
nextAction: "Select or create a project: run `mushi project create` or pick one with `mushi project list`",
|
|
3881
|
+
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3882
|
+
done: false
|
|
3612
3883
|
};
|
|
3613
|
-
}
|
|
3614
|
-
);
|
|
3615
|
-
const toBump = entries.filter((e) => e.willUpgrade && e.latest).map((e) => `${e.name}@${e.latest}`);
|
|
3616
|
-
return {
|
|
3617
|
-
cwd: root,
|
|
3618
|
-
packageManager: pm,
|
|
3619
|
-
entries,
|
|
3620
|
-
installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
|
|
3621
|
-
};
|
|
3622
|
-
}
|
|
3623
|
-
async function runUpgrade(opts = {}) {
|
|
3624
|
-
const plan = await planUpgrade(opts.cwd ?? process.cwd());
|
|
3625
|
-
if (plan.entries.length === 0) {
|
|
3626
|
-
return {
|
|
3627
|
-
plan,
|
|
3628
|
-
upgraded: false,
|
|
3629
|
-
message: "No @mushi-mushi/* packages in package.json \u2014 run `mushi init` first."
|
|
3630
|
-
};
|
|
3631
|
-
}
|
|
3632
|
-
if (!plan.installCmd) {
|
|
3633
|
-
const semverEntries = plan.entries.filter((e) => /\d/.test(e.current));
|
|
3634
|
-
const allChecksFailed = semverEntries.length > 0 && semverEntries.every((e) => e.latest === null);
|
|
3884
|
+
}
|
|
3635
3885
|
return {
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3886
|
+
nextAction: "Sign in to Mushi: run `npx mushi-mushi` (browser opens automatically \u2014 do NOT type the code in the terminal)",
|
|
3887
|
+
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3888
|
+
done: false
|
|
3639
3889
|
};
|
|
3640
3890
|
}
|
|
3641
|
-
if (opts.dryRun) {
|
|
3642
|
-
return { plan, upgraded: false, message: `[dry-run] Would run: ${plan.installCmd}` };
|
|
3643
|
-
}
|
|
3644
3891
|
try {
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
const reactEntry = plan.entries.find((e) => e.migrateToWeb);
|
|
3655
|
-
const migrateNote = reactEntry ? "\nNote: @mushi-mushi/react is legacy \u2014 prefer @mushi-mushi/web for Vite/Capacitor/SPA apps." : "";
|
|
3656
|
-
return {
|
|
3657
|
-
plan,
|
|
3658
|
-
upgraded: true,
|
|
3659
|
-
message: `Upgraded Mushi SDK packages.
|
|
3660
|
-
${plan.installCmd}${migrateNote}`
|
|
3661
|
-
};
|
|
3662
|
-
}
|
|
3663
|
-
|
|
3664
|
-
// src/commands/fix.ts
|
|
3665
|
-
function registerFixCommands(program2) {
|
|
3666
|
-
const fixCmd = program2.command("fix").description("Dispatch an agentic fix for a report");
|
|
3667
|
-
fixCmd.argument("<reportId>", "Report UUID to fix").option(
|
|
3668
|
-
"--agent <name>",
|
|
3669
|
-
"Agent adapter: claude_code (default), cursor_cloud, codex, mcp",
|
|
3670
|
-
"claude_code"
|
|
3671
|
-
).option(
|
|
3672
|
-
"--model <slug>",
|
|
3673
|
-
"Model override for cursor_cloud (e.g. composer-latest)"
|
|
3674
|
-
).option(
|
|
3675
|
-
"--no-auto-pr",
|
|
3676
|
-
"For cursor_cloud: skip automatic PR creation (branch only)"
|
|
3677
|
-
).option(
|
|
3678
|
-
"--wait",
|
|
3679
|
-
"Poll until terminal state and exit non-zero on error/cancelled (CI-friendly)"
|
|
3680
|
-
).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", `
|
|
3681
|
-
Examples:
|
|
3682
|
-
mushi fix abc123 --agent cursor_cloud --wait
|
|
3683
|
-
mushi fix abc123 --agent cursor_cloud --model composer-latest --no-auto-pr
|
|
3684
|
-
mushi fix abc123 --agent claude_code
|
|
3685
|
-
|
|
3686
|
-
# CI: fail the pipeline if the fix errors
|
|
3687
|
-
mushi fix $REPORT_ID --agent cursor_cloud --wait && echo "Fix PR opened"`).action(async (reportId, opts) => {
|
|
3688
|
-
const cfg = loadConfig();
|
|
3689
|
-
if (opts.endpoint) cfg.endpoint = opts.endpoint;
|
|
3690
|
-
if (opts.apiKey) cfg.apiKey = opts.apiKey;
|
|
3691
|
-
if (opts.projectId) cfg.projectId = opts.projectId;
|
|
3692
|
-
const isTTY = process.stdout.isTTY;
|
|
3693
|
-
const emitEvent = (type, data) => {
|
|
3694
|
-
if (isTTY) {
|
|
3695
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3696
|
-
console.log(`[${ts}] ${type}`, JSON.stringify(data));
|
|
3697
|
-
} else {
|
|
3698
|
-
process.stdout.write(JSON.stringify({ type, ...data }) + "\n");
|
|
3699
|
-
}
|
|
3700
|
-
};
|
|
3701
|
-
emitEvent("dispatch.start", { reportId, agent: opts.agent, model: opts.model ?? null });
|
|
3702
|
-
const body = {
|
|
3703
|
-
reportId,
|
|
3704
|
-
projectId: cfg.projectId,
|
|
3705
|
-
agent: opts.agent
|
|
3706
|
-
};
|
|
3707
|
-
if (opts.agent === "cursor_cloud") {
|
|
3708
|
-
if (opts.model) body.cursorModel = opts.model;
|
|
3709
|
-
if (!opts.autoPr) body.cursorAutoCreatePR = false;
|
|
3710
|
-
}
|
|
3711
|
-
const result = await apiCall("/v1/admin/fixes/dispatch", cfg, {
|
|
3712
|
-
method: "POST",
|
|
3713
|
-
headers: { "Content-Type": "application/json" },
|
|
3714
|
-
body: JSON.stringify(body)
|
|
3715
|
-
});
|
|
3716
|
-
if (!result.ok) {
|
|
3717
|
-
console.error("Error dispatching fix:", result.error.message);
|
|
3718
|
-
process.exit(1);
|
|
3719
|
-
}
|
|
3720
|
-
const { fixId, status, agentId, runId, prUrl } = result.data;
|
|
3721
|
-
emitEvent("dispatch.ok", { fixId, status, agentId, runId, prUrl });
|
|
3722
|
-
if (!opts.wait) {
|
|
3723
|
-
process.exit(0);
|
|
3724
|
-
}
|
|
3725
|
-
if (!fixId) {
|
|
3726
|
-
console.error("No fixId returned \u2014 cannot poll.");
|
|
3727
|
-
process.exit(1);
|
|
3728
|
-
}
|
|
3729
|
-
const POLL_MS = 5e3;
|
|
3730
|
-
const MAX_POLLS = 120;
|
|
3731
|
-
const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "error", "cancelled", "skipped", "skipped_unsupported_agent", "skipped_no_sandbox"]);
|
|
3732
|
-
for (let i = 0; i < MAX_POLLS; i++) {
|
|
3733
|
-
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
3734
|
-
const pollResult = await apiCall(
|
|
3735
|
-
`/v1/admin/fixes/${fixId}`,
|
|
3736
|
-
cfg
|
|
3737
|
-
);
|
|
3738
|
-
if (!pollResult.ok) {
|
|
3739
|
-
emitEvent("poll.error", { error: pollResult.error.message });
|
|
3740
|
-
continue;
|
|
3741
|
-
}
|
|
3742
|
-
const s = pollResult.data.status;
|
|
3743
|
-
emitEvent("fix.status", { status: s, pr_url: pollResult.data.pr_url, cursor_agent_id: pollResult.data.cursor_agent_id });
|
|
3744
|
-
if (s && TERMINAL.has(s)) {
|
|
3745
|
-
const success = s === "completed";
|
|
3746
|
-
if (!success) {
|
|
3747
|
-
console.error(`Fix ended with status: ${s}${pollResult.data.error ? ` \u2014 ${pollResult.data.error}` : ""}`);
|
|
3748
|
-
process.exit(1);
|
|
3749
|
-
}
|
|
3750
|
-
process.exit(0);
|
|
3892
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
3893
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3894
|
+
const root = resolve3(cwd);
|
|
3895
|
+
let envContent = "";
|
|
3896
|
+
for (const f of [".env.local", ".env"]) {
|
|
3897
|
+
try {
|
|
3898
|
+
envContent = await readFile3(join5(root, f), "utf8");
|
|
3899
|
+
break;
|
|
3900
|
+
} catch {
|
|
3751
3901
|
}
|
|
3752
3902
|
}
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
if (!validPhases.includes(opts.phase)) {
|
|
3761
|
-
console.error(`Unknown phase "${opts.phase}". Use one of: ${validPhases.join(", ")}`);
|
|
3762
|
-
process.exit(1);
|
|
3903
|
+
const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
|
|
3904
|
+
if (!hasEnvVars) {
|
|
3905
|
+
return {
|
|
3906
|
+
nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
|
|
3907
|
+
ctaPath: "/onboarding?tab=sdk",
|
|
3908
|
+
done: false
|
|
3909
|
+
};
|
|
3763
3910
|
}
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
}
|
|
3774
|
-
return n;
|
|
3775
|
-
};
|
|
3776
|
-
if (opts.max !== void 0) overrides.maxProactivePerSession = parseNumericFlag("max", opts.max, 1);
|
|
3777
|
-
if (opts.cooldown !== void 0) overrides.dismissCooldownHours = parseNumericFlag("cooldown", opts.cooldown, 0);
|
|
3778
|
-
if (opts.dwell !== void 0) overrides.pageDwellMinutes = parseNumericFlag("dwell", opts.dwell, 0);
|
|
3779
|
-
if (opts.welcome !== void 0) overrides.firstSessionSeconds = parseNumericFlag("welcome", opts.welcome, 0);
|
|
3780
|
-
if (opts.explain) {
|
|
3781
|
-
console.log(renderNudgeExplainer(phase));
|
|
3911
|
+
} catch {
|
|
3912
|
+
}
|
|
3913
|
+
try {
|
|
3914
|
+
const sdkCheck = await checkSdkInstall(cwd);
|
|
3915
|
+
if (sdkCheck && !sdkCheck.ok) {
|
|
3916
|
+
return {
|
|
3917
|
+
nextAction: `Install the Mushi SDK: ${sdkCheck.detail}`,
|
|
3918
|
+
ctaPath: "/onboarding?tab=sdk",
|
|
3919
|
+
done: false
|
|
3920
|
+
};
|
|
3782
3921
|
}
|
|
3783
|
-
|
|
3784
|
-
}
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3922
|
+
} catch {
|
|
3923
|
+
}
|
|
3924
|
+
try {
|
|
3925
|
+
const data = await fetchIngestSetup(
|
|
3926
|
+
{ endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
|
|
3927
|
+
doFetch
|
|
3928
|
+
);
|
|
3929
|
+
if (data) {
|
|
3930
|
+
const steps = data.steps ?? [];
|
|
3931
|
+
const incomplete = steps.find((s) => s.required && !s.complete);
|
|
3932
|
+
if (incomplete) {
|
|
3933
|
+
const tabMap = {
|
|
3934
|
+
project_created: "/onboarding?tab=steps&setup=cli",
|
|
3935
|
+
api_key_generated: "/onboarding?tab=verify",
|
|
3936
|
+
sdk_installed: "/onboarding?tab=sdk",
|
|
3937
|
+
first_report_received: "/onboarding?tab=verify"
|
|
3938
|
+
};
|
|
3939
|
+
const tab = tabMap[incomplete.id] ?? "/onboarding?tab=verify";
|
|
3940
|
+
return {
|
|
3941
|
+
nextAction: incomplete.hint ?? `Complete setup step: ${incomplete.label}`,
|
|
3942
|
+
ctaPath: tab,
|
|
3943
|
+
done: false
|
|
3944
|
+
};
|
|
3798
3945
|
}
|
|
3946
|
+
return {
|
|
3947
|
+
nextAction: "All 4 required setup steps are complete! Open the console to explore.",
|
|
3948
|
+
ctaPath: "/reports",
|
|
3949
|
+
done: true
|
|
3950
|
+
};
|
|
3799
3951
|
}
|
|
3800
|
-
|
|
3801
|
-
|
|
3952
|
+
} catch {
|
|
3953
|
+
}
|
|
3954
|
+
return {
|
|
3955
|
+
nextAction: "Open the onboarding wizard in the console to check your setup status.",
|
|
3956
|
+
ctaPath: "/onboarding",
|
|
3957
|
+
done: false
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
async function runDoctor(config, options = {}) {
|
|
3961
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
3962
|
+
const checks = [];
|
|
3963
|
+
const isFull = options.full === true;
|
|
3964
|
+
const runServer = isFull || options.server !== false;
|
|
3965
|
+
const runIngest = isFull || options.ingest !== false;
|
|
3966
|
+
checks.push(...checkCliConfig(config));
|
|
3967
|
+
if (config.endpoint) {
|
|
3968
|
+
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
3969
|
+
}
|
|
3970
|
+
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
3971
|
+
if (sdkCheck) checks.push(sdkCheck);
|
|
3972
|
+
if (runServer) {
|
|
3973
|
+
const serverChecks = await checkServerPreflight(config, doFetch);
|
|
3974
|
+
checks.push(...serverChecks);
|
|
3975
|
+
}
|
|
3976
|
+
if (runIngest) {
|
|
3977
|
+
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
3978
|
+
checks.push(...ingestChecks);
|
|
3979
|
+
}
|
|
3980
|
+
if (isFull || options.qaStories) {
|
|
3981
|
+
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
3982
|
+
checks.push(...qaChecks);
|
|
3983
|
+
}
|
|
3984
|
+
if (isFull || options.hostApp) {
|
|
3985
|
+
const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
|
|
3986
|
+
checks.push(...hostChecks);
|
|
3987
|
+
}
|
|
3988
|
+
if (isFull || options.mcp) {
|
|
3989
|
+
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
3990
|
+
checks.push(...mcpChecks);
|
|
3991
|
+
}
|
|
3992
|
+
const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
|
|
3993
|
+
if (heartbeatPassed) {
|
|
3994
|
+
const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
|
|
3995
|
+
if (idx >= 0) {
|
|
3996
|
+
checks[idx] = {
|
|
3997
|
+
...checks[idx],
|
|
3998
|
+
ok: true,
|
|
3999
|
+
warn: true,
|
|
4000
|
+
detail: "No @mushi-mushi/* found in cwd package.json \u2014 but live heartbeats confirm the SDK is active. Run from your app repo or install with `mushi init`."
|
|
4001
|
+
};
|
|
3802
4002
|
}
|
|
3803
|
-
|
|
3804
|
-
}
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4003
|
+
}
|
|
4004
|
+
return { checks, ready: checks.every((c) => c.ok) };
|
|
4005
|
+
}
|
|
4006
|
+
function formatDoctorResult(result) {
|
|
4007
|
+
const PASS = "OK";
|
|
4008
|
+
const WARN = "WARN";
|
|
4009
|
+
const FAIL = "FAIL";
|
|
4010
|
+
const lines = [];
|
|
4011
|
+
for (const c of result.checks) {
|
|
4012
|
+
const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
|
|
4013
|
+
lines.push(`${icon} ${c.name}`);
|
|
4014
|
+
if (c.detail) lines.push(` ${c.detail}`);
|
|
4015
|
+
if (!c.ok) {
|
|
4016
|
+
const hint = fixHintForCheck(c.name);
|
|
4017
|
+
if (hint) lines.push(` \u2192 Fix: ${hint}`);
|
|
3813
4018
|
}
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
writeEnv: opts.writeEnv === true ? true : opts.env !== false,
|
|
3822
|
-
wireIde: opts.wireIde === true ? true : opts.ide !== false,
|
|
3823
|
-
wait: opts.wait,
|
|
3824
|
-
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
3825
|
-
json: opts.json
|
|
3826
|
-
});
|
|
3827
|
-
if (opts.json) {
|
|
3828
|
-
console.log(JSON.stringify(result, null, 2));
|
|
4019
|
+
}
|
|
4020
|
+
const failed = result.checks.filter((c) => !c.ok);
|
|
4021
|
+
const warned = result.checks.filter((c) => c.ok && c.warn);
|
|
4022
|
+
if (failed.length === 0) {
|
|
4023
|
+
if (warned.length > 0) {
|
|
4024
|
+
lines.push(`
|
|
4025
|
+
All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
|
|
3829
4026
|
} else {
|
|
3830
|
-
|
|
4027
|
+
lines.push("\nAll checks passed. The CLI is ready.");
|
|
3831
4028
|
}
|
|
3832
|
-
|
|
3833
|
-
|
|
4029
|
+
} else {
|
|
4030
|
+
lines.push(`
|
|
4031
|
+
${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
4032
|
+
lines.push("Fix the items above and re-run `mushi doctor`.");
|
|
4033
|
+
}
|
|
4034
|
+
return lines.join("\n");
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
// src/commands/doctor-cli.ts
|
|
4038
|
+
function registerDoctorCommand(program2) {
|
|
3834
4039
|
program2.command("doctor").description(
|
|
3835
4040
|
"Run pre-flight checks: CLI config, endpoint reachability, SDK install, ingest readiness (API key \u2192 heartbeat \u2192 first report), and dispatch readiness (GitHub, index, BYOK, autofix). Ingest + server checks run by default; pass --no-server or --no-ingest to skip."
|
|
3836
4041
|
).option("--cwd <path>", "Run package detection from a different directory").option("--json", "Machine-readable output").option("--no-server", "Skip dispatch-readiness /preflight checks").option("--no-ingest", "Skip ingest-setup checks (SDK heartbeat, first report)").option(
|
|
@@ -3859,11 +4064,11 @@ Examples:
|
|
|
3859
4064
|
const status = await checkOnboardingStatus(config, consoleBase, cwd);
|
|
3860
4065
|
const base = consoleBase.replace(/\/$/, "");
|
|
3861
4066
|
if (status.done) {
|
|
3862
|
-
console.log(
|
|
3863
|
-
console.log(`
|
|
4067
|
+
console.log(`OK Setup complete. ${status.nextAction}`);
|
|
4068
|
+
console.log(` ${base}${status.ctaPath}`);
|
|
3864
4069
|
} else {
|
|
3865
4070
|
console.log(`\u2192 Next: ${status.nextAction}`);
|
|
3866
|
-
console.log(`
|
|
4071
|
+
console.log(` Open: ${base}${status.ctaPath}`);
|
|
3867
4072
|
}
|
|
3868
4073
|
if (!status.done) process.exit(1);
|
|
3869
4074
|
return;
|
|
@@ -3899,6 +4104,10 @@ Examples:
|
|
|
3899
4104
|
console.log(formatDoctorResult(result));
|
|
3900
4105
|
if (!result.ready) process.exit(1);
|
|
3901
4106
|
});
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
// src/commands/reset-cli.ts
|
|
4110
|
+
function registerResetCommand(program2) {
|
|
3902
4111
|
program2.command("reset [projectId]").description(
|
|
3903
4112
|
"Archive a project and wipe its test data (codebase_files, fix_attempts, reports). Speeds up re-running the full onboarding flow from scratch. Requires `--confirm` to prevent accidents."
|
|
3904
4113
|
).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (projectId, opts) => {
|
|
@@ -3933,146 +4142,6 @@ Re-run with --confirm to proceed.`
|
|
|
3933
4142
|
process.exit(1);
|
|
3934
4143
|
}
|
|
3935
4144
|
});
|
|
3936
|
-
const fixes = program2.command("fixes").description("Fix dispatch management");
|
|
3937
|
-
fixes.command("tail").description(
|
|
3938
|
-
"Stream SSE dispatch events for a report in real time. Useful for headless debugging without opening the browser."
|
|
3939
|
-
).requiredOption("--report-id <id>", "Report ID to follow").action(async (opts) => {
|
|
3940
|
-
const config = loadConfig();
|
|
3941
|
-
if (!config.apiKey) {
|
|
3942
|
-
console.error("Run `mushi login` first");
|
|
3943
|
-
process.exit(1);
|
|
3944
|
-
}
|
|
3945
|
-
if (!config.endpoint) {
|
|
3946
|
-
console.error("No endpoint configured. Run `mushi init`");
|
|
3947
|
-
process.exit(1);
|
|
3948
|
-
}
|
|
3949
|
-
const url = `${config.endpoint}/v1/admin/reports/${opts.reportId}/dispatch/stream`;
|
|
3950
|
-
console.log(`Tailing dispatch stream for report ${opts.reportId}\u2026`);
|
|
3951
|
-
console.log(`(Ctrl-C to stop)
|
|
3952
|
-
`);
|
|
3953
|
-
const res = await fetch(url, {
|
|
3954
|
-
headers: {
|
|
3955
|
-
"Authorization": `Bearer ${config.apiKey}`,
|
|
3956
|
-
"X-Mushi-Api-Key": config.apiKey,
|
|
3957
|
-
"X-Mushi-Project": config.projectId ?? "",
|
|
3958
|
-
"Accept": "text/event-stream"
|
|
3959
|
-
}
|
|
3960
|
-
});
|
|
3961
|
-
if (!res.ok || !res.body) {
|
|
3962
|
-
console.error(`Failed to connect: HTTP ${res.status}`);
|
|
3963
|
-
const text2 = await res.text().catch(() => "");
|
|
3964
|
-
if (text2) console.error(text2.slice(0, 300));
|
|
3965
|
-
process.exit(1);
|
|
3966
|
-
}
|
|
3967
|
-
const decoder = new TextDecoder();
|
|
3968
|
-
const reader = res.body.getReader();
|
|
3969
|
-
let done = false;
|
|
3970
|
-
process.on("SIGINT", () => {
|
|
3971
|
-
done = true;
|
|
3972
|
-
void reader.cancel();
|
|
3973
|
-
console.log("\nDisconnected.");
|
|
3974
|
-
process.exit(0);
|
|
3975
|
-
});
|
|
3976
|
-
while (!done) {
|
|
3977
|
-
const { value, done: streamDone } = await reader.read();
|
|
3978
|
-
if (streamDone) break;
|
|
3979
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
3980
|
-
for (const line of chunk.split("\n")) {
|
|
3981
|
-
if (line.startsWith("data: ")) {
|
|
3982
|
-
const raw = line.slice(6).trim();
|
|
3983
|
-
if (raw === "[DONE]") {
|
|
3984
|
-
console.log("\n[stream ended]");
|
|
3985
|
-
process.exit(0);
|
|
3986
|
-
}
|
|
3987
|
-
try {
|
|
3988
|
-
const event = JSON.parse(raw);
|
|
3989
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3990
|
-
const type = event.type ?? event.event ?? "event";
|
|
3991
|
-
const status = event.status ?? event.data ?? "";
|
|
3992
|
-
console.log(`${ts} ${type.padEnd(24)} ${status}`);
|
|
3993
|
-
} catch {
|
|
3994
|
-
console.log(line);
|
|
3995
|
-
}
|
|
3996
|
-
} else if (line.startsWith("event: ")) {
|
|
3997
|
-
} else if (line && !line.startsWith(":")) {
|
|
3998
|
-
console.log(line);
|
|
3999
|
-
}
|
|
4000
|
-
}
|
|
4001
|
-
}
|
|
4002
|
-
});
|
|
4003
|
-
fixes.command("merge <fixId>").description(
|
|
4004
|
-
"Squash-merge the fix PR on GitHub and mark the report Fixed (same as console merge)"
|
|
4005
|
-
).option(
|
|
4006
|
-
"--method <method>",
|
|
4007
|
-
"GitHub merge method: squash (default), merge, or rebase",
|
|
4008
|
-
"squash"
|
|
4009
|
-
).option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
|
|
4010
|
-
const config = loadConfig();
|
|
4011
|
-
if (!config.apiKey) {
|
|
4012
|
-
console.error("Run `mushi login` first");
|
|
4013
|
-
process.exit(1);
|
|
4014
|
-
}
|
|
4015
|
-
if (!config.endpoint) {
|
|
4016
|
-
console.error("No endpoint configured. Run `mushi init`");
|
|
4017
|
-
process.exit(1);
|
|
4018
|
-
}
|
|
4019
|
-
const method = opts.method;
|
|
4020
|
-
if (!["squash", "merge", "rebase"].includes(method)) {
|
|
4021
|
-
console.error("--method must be squash, merge, or rebase");
|
|
4022
|
-
process.exit(1);
|
|
4023
|
-
}
|
|
4024
|
-
const result = await apiCall(`/v1/admin/fixes/${fixId}/merge`, config, {
|
|
4025
|
-
method: "POST",
|
|
4026
|
-
body: JSON.stringify({ mergeMethod: method })
|
|
4027
|
-
});
|
|
4028
|
-
if (!result.ok) {
|
|
4029
|
-
if (opts.json) {
|
|
4030
|
-
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
4031
|
-
} else {
|
|
4032
|
-
console.error("Merge failed:", result.error.message);
|
|
4033
|
-
}
|
|
4034
|
-
process.exit(1);
|
|
4035
|
-
}
|
|
4036
|
-
const data = result.data ?? {};
|
|
4037
|
-
if (opts.json) {
|
|
4038
|
-
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
4039
|
-
return;
|
|
4040
|
-
}
|
|
4041
|
-
if (data.alreadyMerged) {
|
|
4042
|
-
console.log(`PR was already merged. Report status: ${data.reportStatus ?? "unknown"}`);
|
|
4043
|
-
} else {
|
|
4044
|
-
console.log(`Merged successfully. Report status: ${data.reportStatus ?? "fixed"}`);
|
|
4045
|
-
}
|
|
4046
|
-
});
|
|
4047
|
-
fixes.command("refresh-ci <fixId>").description("Pull latest GitHub Actions check-run status for a fix attempt (on-demand ci-sync)").option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
|
|
4048
|
-
const config = loadConfig();
|
|
4049
|
-
if (!config.apiKey) {
|
|
4050
|
-
console.error("Run `mushi login` first");
|
|
4051
|
-
process.exit(1);
|
|
4052
|
-
}
|
|
4053
|
-
if (!config.endpoint) {
|
|
4054
|
-
console.error("No endpoint configured. Run `mushi init`");
|
|
4055
|
-
process.exit(1);
|
|
4056
|
-
}
|
|
4057
|
-
const result = await apiCall(`/v1/admin/fixes/${fixId}/refresh-ci`, config, { method: "POST" });
|
|
4058
|
-
if (!result.ok) {
|
|
4059
|
-
if (opts.json) {
|
|
4060
|
-
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
4061
|
-
} else {
|
|
4062
|
-
console.error("CI refresh failed:", result.error.message);
|
|
4063
|
-
}
|
|
4064
|
-
process.exit(1);
|
|
4065
|
-
}
|
|
4066
|
-
const data = result.data ?? {};
|
|
4067
|
-
if (opts.json) {
|
|
4068
|
-
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
4069
|
-
return;
|
|
4070
|
-
}
|
|
4071
|
-
const status = data.check_run_status ?? "unknown";
|
|
4072
|
-
const conclusion = data.check_run_conclusion ?? "\u2014";
|
|
4073
|
-
const updated = data.check_run_updated_at ? new Date(data.check_run_updated_at).toISOString() : "\u2014";
|
|
4074
|
-
console.log(`CI status: ${status} \xB7 conclusion: ${conclusion} \xB7 updated: ${updated}`);
|
|
4075
|
-
});
|
|
4076
4145
|
}
|
|
4077
4146
|
|
|
4078
4147
|
// src/commands/tdd.ts
|
|
@@ -4329,7 +4398,7 @@ function registerIntegrationsCommands(program2) {
|
|
|
4329
4398
|
console.log("\nIntegrations:\n");
|
|
4330
4399
|
for (const row of rows) {
|
|
4331
4400
|
const icon = icons[row.kind] ?? "\u{1F50C}";
|
|
4332
|
-
const statusIcon = row.status === "ok" ? "
|
|
4401
|
+
const statusIcon = row.status === "ok" ? "OK" : row.status === "error" ? "FAIL" : "SKIP";
|
|
4333
4402
|
console.log(` ${icon} ${row.kind.padEnd(20)} ${statusIcon} ${row.detail ?? ""}`);
|
|
4334
4403
|
}
|
|
4335
4404
|
console.log();
|
|
@@ -4361,7 +4430,7 @@ function registerIntegrationsCommands(program2) {
|
|
|
4361
4430
|
}
|
|
4362
4431
|
const probeOk = result.data?.status === "ok";
|
|
4363
4432
|
console.log(
|
|
4364
|
-
probeOk ?
|
|
4433
|
+
probeOk ? `OK ${kind} integration is healthy${result.data.detail ? ": " + result.data.detail : ""}` : `FAIL ${kind} integration check failed${result.data.detail ? ": " + result.data.detail : ""}`
|
|
4365
4434
|
);
|
|
4366
4435
|
if (!probeOk) process.exit(1);
|
|
4367
4436
|
});
|
|
@@ -4390,11 +4459,11 @@ function registerIntegrationsCommands(program2) {
|
|
|
4390
4459
|
return;
|
|
4391
4460
|
}
|
|
4392
4461
|
if (result.data?.status === "ok") {
|
|
4393
|
-
console.log("
|
|
4462
|
+
console.log("OK Slack connected");
|
|
4394
4463
|
if (result.data.detail) console.log(` ${result.data.detail}`);
|
|
4395
4464
|
console.log("\n To change the channel or notification prefs, visit /integrations in the Mushi console.");
|
|
4396
4465
|
} else {
|
|
4397
|
-
console.log("
|
|
4466
|
+
console.log("SKIP Slack not connected");
|
|
4398
4467
|
console.log(' Visit /integrations in the Mushi console and click "Add to Slack".');
|
|
4399
4468
|
}
|
|
4400
4469
|
});
|
|
@@ -4422,9 +4491,9 @@ function registerIntegrationsCommands(program2) {
|
|
|
4422
4491
|
return;
|
|
4423
4492
|
}
|
|
4424
4493
|
if (result.data?.ok) {
|
|
4425
|
-
console.log("
|
|
4494
|
+
console.log("OK Test message sent. Check your Slack channel.");
|
|
4426
4495
|
} else {
|
|
4427
|
-
console.error("
|
|
4496
|
+
console.error("FAIL Test failed:", result.data?.error ?? "unknown error");
|
|
4428
4497
|
process.exit(1);
|
|
4429
4498
|
}
|
|
4430
4499
|
});
|
|
@@ -4464,7 +4533,7 @@ function registerQaCommands(program2) {
|
|
|
4464
4533
|
QA Stories (${stories.length}):
|
|
4465
4534
|
`);
|
|
4466
4535
|
for (const s of stories) {
|
|
4467
|
-
const statusIcon = s.last_run_status === "passed" ? "
|
|
4536
|
+
const statusIcon = s.last_run_status === "passed" ? "PASS" : s.last_run_status === "failed" ? "FAIL" : s.last_run_status === "error" ? "ERROR" : "SKIP";
|
|
4468
4537
|
const enabled = s.enabled ? "" : " [disabled]";
|
|
4469
4538
|
const sid = s.story_id ?? s.id ?? "\u2014";
|
|
4470
4539
|
console.log(` ${statusIcon} ${s.name.slice(0, 50).padEnd(52)} ${sid}${enabled}`);
|
|
@@ -4505,7 +4574,7 @@ QA Stories (${stories.length}):
|
|
|
4505
4574
|
Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
4506
4575
|
`);
|
|
4507
4576
|
for (const r of runs) {
|
|
4508
|
-
const statusIcon = r.status === "passed" ? "
|
|
4577
|
+
const statusIcon = r.status === "passed" ? "PASS" : r.status === "failed" ? "FAIL" : r.status === "error" ? "ERROR" : "PEND";
|
|
4509
4578
|
const ts = r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace("T", " ") : "\u2014";
|
|
4510
4579
|
const latency = r.latency_ms ? ` (${(r.latency_ms / 1e3).toFixed(1)}s)` : "";
|
|
4511
4580
|
console.log(` ${statusIcon} ${ts}${latency} ${r.id.slice(0, 8)}`);
|
|
@@ -4518,7 +4587,7 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
|
4518
4587
|
}
|
|
4519
4588
|
}
|
|
4520
4589
|
}
|
|
4521
|
-
const consoleUrl2 = config.consoleUrl ??
|
|
4590
|
+
const consoleUrl2 = config.consoleUrl ?? resolveConsoleUrlSync();
|
|
4522
4591
|
console.log(`
|
|
4523
4592
|
Open in console: ${consoleUrl2}/qa-coverage?story=${storyId}`);
|
|
4524
4593
|
console.log(` Tip: run 'mushi config consoleUrl <url>' to override (e.g. http://localhost:6464 for local dev)`);
|
|
@@ -4549,10 +4618,10 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
|
4549
4618
|
}
|
|
4550
4619
|
const runId = result.data?.run_id;
|
|
4551
4620
|
if (runId) {
|
|
4552
|
-
console.log(
|
|
4553
|
-
console.log(`
|
|
4621
|
+
console.log(`RUN Triggered: ${runId.slice(0, 8)}\u2026`);
|
|
4622
|
+
console.log(` Check results: mushi qa runs ${storyId}`);
|
|
4554
4623
|
} else {
|
|
4555
|
-
console.error("
|
|
4624
|
+
console.error("FAIL Trigger failed: no run_id in response", JSON.stringify(result.data));
|
|
4556
4625
|
process.exit(1);
|
|
4557
4626
|
}
|
|
4558
4627
|
});
|
|
@@ -4569,10 +4638,10 @@ Description:
|
|
|
4569
4638
|
\u2022 Gate results: API contract (G3), spec drift (G6), orphan endpoints (G7),
|
|
4570
4639
|
unknown frontend calls (G8), schema drift, status claim (G5)
|
|
4571
4640
|
|
|
4572
|
-
Returns a
|
|
4641
|
+
Returns a human-readable summary with severity-ranked findings.
|
|
4573
4642
|
|
|
4574
4643
|
Prerequisites:
|
|
4575
|
-
1.
|
|
4644
|
+
1. Add your Supabase PAT in Admin \u2192 Settings \u2192 API Keys
|
|
4576
4645
|
2. Set supabase_project_ref in Admin \u2192 Settings \u2192 Project.
|
|
4577
4646
|
|
|
4578
4647
|
Examples:
|
|
@@ -4632,8 +4701,8 @@ Examples:
|
|
|
4632
4701
|
return;
|
|
4633
4702
|
}
|
|
4634
4703
|
const data = body.data;
|
|
4635
|
-
const
|
|
4636
|
-
process.stdout.write(`${
|
|
4704
|
+
const overallLabel = data.summary.overall === "fail" ? "FAIL" : data.summary.overall === "warn" ? "WARN" : "OK";
|
|
4705
|
+
process.stdout.write(`${overallLabel}
|
|
4637
4706
|
|
|
4638
4707
|
`);
|
|
4639
4708
|
console.log(`Full-Stack Audit \u2014 ${new Date(data.audit_at).toLocaleString()}`);
|
|
@@ -4641,18 +4710,18 @@ Examples:
|
|
|
4641
4710
|
console.log(`Summary: ${data.summary.error_count} error(s) \xB7 ${data.summary.warn_count} warning(s)
|
|
4642
4711
|
`);
|
|
4643
4712
|
if (data.findings.length === 0) {
|
|
4644
|
-
console.log("
|
|
4713
|
+
console.log(" OK No findings. Your project looks healthy.");
|
|
4645
4714
|
} else {
|
|
4646
4715
|
for (const f of data.findings) {
|
|
4647
|
-
const icon = f.severity === "error" ? "
|
|
4648
|
-
console.log(` ${icon}
|
|
4716
|
+
const icon = f.severity === "error" ? "FAIL" : f.severity === "warn" ? "WARN" : "INFO";
|
|
4717
|
+
console.log(` ${icon} ${f.title}`);
|
|
4649
4718
|
console.log(` ${f.detail.slice(0, 120)}${f.detail.length > 120 ? "\u2026" : ""}`);
|
|
4650
4719
|
}
|
|
4651
4720
|
}
|
|
4652
4721
|
if (data.gate_runs.length > 0) {
|
|
4653
4722
|
console.log("\nGate Results:");
|
|
4654
4723
|
for (const run of data.gate_runs) {
|
|
4655
|
-
const g = run.status === "pass" ? "
|
|
4724
|
+
const g = run.status === "pass" ? "OK" : run.status === "fail" ? "FAIL" : "SKIP";
|
|
4656
4725
|
console.log(` ${g} ${run.gate.padEnd(22)} ${run.status} (${run.findings_count} finding${run.findings_count !== 1 ? "s" : ""})`);
|
|
4657
4726
|
}
|
|
4658
4727
|
}
|
|
@@ -5053,7 +5122,7 @@ Examples:
|
|
|
5053
5122
|
|
|
5054
5123
|
// src/index.ts
|
|
5055
5124
|
installSignalHandlers();
|
|
5056
|
-
var program = new Command().name("mushi").description("Mushi
|
|
5125
|
+
var program = new Command().name("mushi").description("Mushi CLI \u2014 set up the SDK, triage a report, fix from your editor").version(MUSHI_CLI_VERSION).addHelpText("after", `
|
|
5057
5126
|
Environment variables:
|
|
5058
5127
|
MUSHI_API_KEY SDK ingest key (report:write scope \u2014 from Onboarding \u2192 Verify in the console)
|
|
5059
5128
|
MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
|
|
@@ -5076,6 +5145,11 @@ registerLessonsCommands(program);
|
|
|
5076
5145
|
registerDiagnosticsCommands(program);
|
|
5077
5146
|
registerProjectCommands(program);
|
|
5078
5147
|
registerSetupCommands(program);
|
|
5148
|
+
registerNudgeCommand(program);
|
|
5149
|
+
registerUpgradeCommand(program);
|
|
5150
|
+
registerConnectCommand(program);
|
|
5151
|
+
registerDoctorCommand(program);
|
|
5152
|
+
registerResetCommand(program);
|
|
5079
5153
|
registerFixCommands(program);
|
|
5080
5154
|
registerTddCommands(program);
|
|
5081
5155
|
registerKeysCommands(program);
|