@mushi-mushi/cli 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +0 -11
- package/README.md +106 -382
- package/dist/chunk-LXO45AYO.js +6 -0
- package/dist/{chunk-XHD3H54W.js → chunk-NYPX5KXR.js} +78 -0
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.js +719 -6
- package/dist/init.js +6 -7
- package/dist/version.js +1 -1
- package/package.json +10 -10
- package/dist/chunk-GHDS4VGP.js +0 -6
package/dist/index.js
CHANGED
|
@@ -263,6 +263,71 @@ export default function App() {
|
|
|
263
263
|
import { Mushi } from '@mushi-mushi/capacitor'
|
|
264
264
|
|
|
265
265
|
await Mushi.configure({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
266
|
+
},
|
|
267
|
+
express: {
|
|
268
|
+
id: "express",
|
|
269
|
+
label: "Express",
|
|
270
|
+
packageName: "@mushi-mushi/node",
|
|
271
|
+
needsWebPackage: false,
|
|
272
|
+
snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
273
|
+
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
274
|
+
import { mushiExpressErrorHandler } from '@mushi-mushi/node/express'
|
|
275
|
+
import type { Express } from 'express'
|
|
276
|
+
|
|
277
|
+
export const mushi = new MushiNodeClient({
|
|
278
|
+
projectId: '${projectId}',
|
|
279
|
+
apiKey: '${apiKey}',
|
|
280
|
+
environment: process.env.NODE_ENV ?? 'production',
|
|
281
|
+
})
|
|
282
|
+
attachUnhandledHook({ client: mushi })
|
|
283
|
+
|
|
284
|
+
export function attachMushi(app: Express) {
|
|
285
|
+
app.use(mushiExpressErrorHandler({ client: mushi }))
|
|
286
|
+
}`
|
|
287
|
+
},
|
|
288
|
+
fastify: {
|
|
289
|
+
id: "fastify",
|
|
290
|
+
label: "Fastify",
|
|
291
|
+
packageName: "@mushi-mushi/node",
|
|
292
|
+
needsWebPackage: false,
|
|
293
|
+
snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
294
|
+
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
295
|
+
import { mushiFastifyPlugin } from '@mushi-mushi/node/fastify'
|
|
296
|
+
import Fastify from 'fastify'
|
|
297
|
+
|
|
298
|
+
export const mushi = new MushiNodeClient({
|
|
299
|
+
projectId: '${projectId}',
|
|
300
|
+
apiKey: '${apiKey}',
|
|
301
|
+
environment: process.env.NODE_ENV ?? 'production',
|
|
302
|
+
})
|
|
303
|
+
attachUnhandledHook({ client: mushi })
|
|
304
|
+
|
|
305
|
+
const app = Fastify()
|
|
306
|
+
mushiFastifyPlugin(app, { client: mushi })`
|
|
307
|
+
},
|
|
308
|
+
hono: {
|
|
309
|
+
id: "hono",
|
|
310
|
+
label: "Hono",
|
|
311
|
+
packageName: "@mushi-mushi/node",
|
|
312
|
+
needsWebPackage: false,
|
|
313
|
+
snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
314
|
+
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
315
|
+
import { mushiHonoErrorHandler } from '@mushi-mushi/node/hono'
|
|
316
|
+
import { Hono } from 'hono'
|
|
317
|
+
|
|
318
|
+
export const mushi = new MushiNodeClient({
|
|
319
|
+
projectId: '${projectId}',
|
|
320
|
+
apiKey: '${apiKey}',
|
|
321
|
+
environment: process.env.NODE_ENV ?? 'production',
|
|
322
|
+
})
|
|
323
|
+
attachUnhandledHook({ client: mushi })
|
|
324
|
+
|
|
325
|
+
const app = new Hono()
|
|
326
|
+
app.onError(
|
|
327
|
+
mushiHonoErrorHandler({ client: mushi }, (err, c) =>
|
|
328
|
+
c.text('Internal Server Error', 500),
|
|
329
|
+
),
|
|
330
|
+
)`
|
|
266
331
|
},
|
|
267
332
|
vanilla: {
|
|
268
333
|
id: "vanilla",
|
|
@@ -296,6 +361,9 @@ function detectFramework(cwd, pkg) {
|
|
|
296
361
|
if (deps.has("svelte")) return FRAMEWORKS.svelte;
|
|
297
362
|
if (deps.has("vue")) return FRAMEWORKS.vue;
|
|
298
363
|
if (deps.has("react")) return FRAMEWORKS.react;
|
|
364
|
+
if (deps.has("express")) return FRAMEWORKS.express;
|
|
365
|
+
if (deps.has("fastify")) return FRAMEWORKS.fastify;
|
|
366
|
+
if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
|
|
299
367
|
if (existsSync2(join2(cwd, "next.config.js")) || existsSync2(join2(cwd, "next.config.ts"))) {
|
|
300
368
|
return FRAMEWORKS.next;
|
|
301
369
|
}
|
|
@@ -321,7 +389,14 @@ function installCommand(pm, packages) {
|
|
|
321
389
|
const verb = pm === "npm" ? "install" : "add";
|
|
322
390
|
return `${pm} ${verb} ${packages.join(" ")}`;
|
|
323
391
|
}
|
|
392
|
+
var SERVER_FRAMEWORK_IDS = /* @__PURE__ */ new Set(["express", "fastify", "hono"]);
|
|
324
393
|
function envVarsToWrite(apiKey, projectId, framework) {
|
|
394
|
+
if (SERVER_FRAMEWORK_IDS.has(framework.id)) {
|
|
395
|
+
return [
|
|
396
|
+
`MUSHI_PROJECT_ID=${projectId}`,
|
|
397
|
+
`MUSHI_API_KEY=${apiKey}`
|
|
398
|
+
].join("\n");
|
|
399
|
+
}
|
|
325
400
|
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
|
|
326
401
|
return [
|
|
327
402
|
`${prefix}MUSHI_PROJECT_ID=${projectId}`,
|
|
@@ -520,7 +595,7 @@ function getFrameworkFromPkg(pkg) {
|
|
|
520
595
|
}
|
|
521
596
|
|
|
522
597
|
// src/version.ts
|
|
523
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
598
|
+
var MUSHI_CLI_VERSION = true ? "0.11.0" : "0.0.0-dev";
|
|
524
599
|
|
|
525
600
|
// src/init.ts
|
|
526
601
|
var ENV_FILES = [".env.local", ".env"];
|
|
@@ -569,7 +644,7 @@ function ensureInteractiveOrBailOut(options) {
|
|
|
569
644
|
);
|
|
570
645
|
if (hasAllFlags) return;
|
|
571
646
|
process.stderr.write(
|
|
572
|
-
"mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id
|
|
647
|
+
"mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id <uuid-from-console> --api-key mushi_xxx\nYour project ID is the UUID shown in the Projects page of the Mushi admin console.\n"
|
|
573
648
|
);
|
|
574
649
|
process.exit(1);
|
|
575
650
|
}
|
|
@@ -603,9 +678,9 @@ async function collectCredentials(options) {
|
|
|
603
678
|
const existing = loadConfig();
|
|
604
679
|
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
605
680
|
message: "Project ID",
|
|
606
|
-
placeholder: "
|
|
681
|
+
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
|
607
682
|
hint: "Where to find it: https://kensaur.us/mushi-mushi/projects \u2192 click your project \u2192 copy the UUID below the project name.",
|
|
608
|
-
validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected a UUID (
|
|
683
|
+
validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) \u2014 copy it from the Mushi admin console Projects page."
|
|
609
684
|
});
|
|
610
685
|
const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
|
|
611
686
|
message: "API key",
|
|
@@ -617,8 +692,7 @@ async function collectCredentials(options) {
|
|
|
617
692
|
const apiKey = sanitizeSecret(rawApiKey);
|
|
618
693
|
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
619
694
|
throw new Error(
|
|
620
|
-
`Invalid project ID. Got: ${redact(projectId)}
|
|
621
|
-
Expected a UUID (e.g. 542b34e0-019e-41fe-b900-7b637717bb86) \u2014 copy it from the Projects page in the Mushi console at https://kensaur.us/mushi-mushi/projects`
|
|
695
|
+
`Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId)} \u2014 copy it from the Projects page in the Mushi console at https://kensaur.us/mushi-mushi/projects`
|
|
622
696
|
);
|
|
623
697
|
}
|
|
624
698
|
if (!API_KEY_PATTERN.test(apiKey)) {
|
|
@@ -1232,6 +1306,254 @@ function getAbortSignal(external) {
|
|
|
1232
1306
|
return activeController.signal;
|
|
1233
1307
|
}
|
|
1234
1308
|
|
|
1309
|
+
// src/nudge.ts
|
|
1310
|
+
var PRESETS = {
|
|
1311
|
+
// Alpha: noisy is OK because there are very few users and you need
|
|
1312
|
+
// signal yesterday. Show all triggers, short cooldowns.
|
|
1313
|
+
alpha: {
|
|
1314
|
+
maxProactivePerSession: 3,
|
|
1315
|
+
dismissCooldownHours: 6,
|
|
1316
|
+
suppressAfterDismissals: 5,
|
|
1317
|
+
pageDwellMinutes: 3,
|
|
1318
|
+
firstSessionSeconds: 30,
|
|
1319
|
+
rageClick: true,
|
|
1320
|
+
longTask: true,
|
|
1321
|
+
apiCascade: true,
|
|
1322
|
+
errorBoundary: true,
|
|
1323
|
+
betaMode: true,
|
|
1324
|
+
featureRequestCard: true
|
|
1325
|
+
},
|
|
1326
|
+
// Beta: most apps spend the longest here. Conservative cadence so
|
|
1327
|
+
// we don't poison the well, but every trigger still on.
|
|
1328
|
+
beta: {
|
|
1329
|
+
maxProactivePerSession: 2,
|
|
1330
|
+
dismissCooldownHours: 24,
|
|
1331
|
+
suppressAfterDismissals: 3,
|
|
1332
|
+
pageDwellMinutes: 5,
|
|
1333
|
+
firstSessionSeconds: 45,
|
|
1334
|
+
rageClick: true,
|
|
1335
|
+
longTask: true,
|
|
1336
|
+
apiCascade: true,
|
|
1337
|
+
errorBoundary: true,
|
|
1338
|
+
betaMode: true,
|
|
1339
|
+
featureRequestCard: true
|
|
1340
|
+
},
|
|
1341
|
+
// GA / production: only the technical signals (error boundary, rage
|
|
1342
|
+
// click). No page-dwell, no welcome — those are explicitly beta-only
|
|
1343
|
+
// patterns. Feature-request card still on because it's user-initiated.
|
|
1344
|
+
ga: {
|
|
1345
|
+
maxProactivePerSession: 1,
|
|
1346
|
+
dismissCooldownHours: 168,
|
|
1347
|
+
suppressAfterDismissals: 2,
|
|
1348
|
+
pageDwellMinutes: 0,
|
|
1349
|
+
firstSessionSeconds: 0,
|
|
1350
|
+
rageClick: true,
|
|
1351
|
+
longTask: false,
|
|
1352
|
+
apiCascade: true,
|
|
1353
|
+
errorBoundary: true,
|
|
1354
|
+
betaMode: false,
|
|
1355
|
+
featureRequestCard: true
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
function renderNudgeSnippet(opts) {
|
|
1359
|
+
const p3 = { ...PRESETS[opts.phase], ...opts.overrides };
|
|
1360
|
+
const dwellMs = p3.pageDwellMinutes > 0 ? `${p3.pageDwellMinutes} * 60 * 1000` : null;
|
|
1361
|
+
const firstMs = p3.firstSessionSeconds > 0 ? `${p3.firstSessionSeconds} * 1000` : null;
|
|
1362
|
+
const proactiveLines = [];
|
|
1363
|
+
if (p3.rageClick) proactiveLines.push(` rageClick: true,`);
|
|
1364
|
+
if (p3.longTask) proactiveLines.push(` longTask: true,`);
|
|
1365
|
+
if (p3.apiCascade) proactiveLines.push(` apiCascade: true,`);
|
|
1366
|
+
if (p3.errorBoundary) proactiveLines.push(` errorBoundary: true,`);
|
|
1367
|
+
if (dwellMs) proactiveLines.push(` pageDwell: { thresholdMs: ${dwellMs} }, // ${p3.pageDwellMinutes}min on the same route`);
|
|
1368
|
+
if (firstMs) proactiveLines.push(` firstSession: { delayMs: ${firstMs} }, // welcome new users after ${p3.firstSessionSeconds}s`);
|
|
1369
|
+
proactiveLines.push(` cooldown: {`);
|
|
1370
|
+
proactiveLines.push(` maxProactivePerSession: ${p3.maxProactivePerSession},`);
|
|
1371
|
+
proactiveLines.push(` dismissCooldownHours: ${p3.dismissCooldownHours},`);
|
|
1372
|
+
proactiveLines.push(` suppressAfterDismissals: ${p3.suppressAfterDismissals},`);
|
|
1373
|
+
proactiveLines.push(` },`);
|
|
1374
|
+
const widgetLines = [];
|
|
1375
|
+
if (p3.featureRequestCard) widgetLines.push(` featureRequestCard: true,`);
|
|
1376
|
+
if (p3.betaMode) {
|
|
1377
|
+
widgetLines.push(` betaMode: {`);
|
|
1378
|
+
widgetLines.push(` enabled: true,`);
|
|
1379
|
+
widgetLines.push(` label: 'BETA',`);
|
|
1380
|
+
widgetLines.push(` // changelogItems: [{ version: 'v0.42', date: '${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}', items: ['\u2026'] }],`);
|
|
1381
|
+
widgetLines.push(` },`);
|
|
1382
|
+
}
|
|
1383
|
+
return [
|
|
1384
|
+
`// Generated by \`mushi nudge --phase ${opts.phase}\``,
|
|
1385
|
+
`// Phase: ${opts.phase} \u2014 tune via --max, --cooldown, --dwell, --welcome.`,
|
|
1386
|
+
`Mushi.init({`,
|
|
1387
|
+
` projectId: process.env.MUSHI_PROJECT_ID!,`,
|
|
1388
|
+
` apiKey: process.env.MUSHI_API_KEY!,`,
|
|
1389
|
+
` proactive: {`,
|
|
1390
|
+
...proactiveLines,
|
|
1391
|
+
` },`,
|
|
1392
|
+
...widgetLines.length ? [` widget: {`, ...widgetLines, ` },`] : [],
|
|
1393
|
+
`})`,
|
|
1394
|
+
``
|
|
1395
|
+
].join("\n");
|
|
1396
|
+
}
|
|
1397
|
+
function renderNudgeExplainer(phase) {
|
|
1398
|
+
const p3 = PRESETS[phase];
|
|
1399
|
+
const lines = [];
|
|
1400
|
+
lines.push(``);
|
|
1401
|
+
lines.push(`Nudge preset for "${phase}" phase:`);
|
|
1402
|
+
lines.push(` - max ${p3.maxProactivePerSession} proactive prompts per session`);
|
|
1403
|
+
lines.push(` - ${p3.dismissCooldownHours}h cooldown after a dismissal`);
|
|
1404
|
+
lines.push(` - suppress permanently after ${p3.suppressAfterDismissals} consecutive dismissals`);
|
|
1405
|
+
if (p3.pageDwellMinutes > 0) lines.push(` - fire page-dwell trigger after ${p3.pageDwellMinutes} continuous minutes on a route`);
|
|
1406
|
+
if (p3.firstSessionSeconds > 0) lines.push(` - welcome new users with a button pulse ${p3.firstSessionSeconds}s after init`);
|
|
1407
|
+
lines.push(` - signals enabled: ${[
|
|
1408
|
+
p3.rageClick && "rage-click",
|
|
1409
|
+
p3.longTask && "long-task",
|
|
1410
|
+
p3.apiCascade && "api-cascade",
|
|
1411
|
+
p3.errorBoundary && "error-boundary"
|
|
1412
|
+
].filter(Boolean).join(", ")}`);
|
|
1413
|
+
lines.push(` - feature-request card: ${p3.featureRequestCard ? "shown" : "hidden"}`);
|
|
1414
|
+
lines.push(` - beta-mode UI: ${p3.betaMode ? "on" : "off"}`);
|
|
1415
|
+
lines.push(``);
|
|
1416
|
+
return lines.join("\n");
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/doctor.ts
|
|
1420
|
+
function checkCliConfig(config) {
|
|
1421
|
+
return [
|
|
1422
|
+
{
|
|
1423
|
+
name: "CLI config file",
|
|
1424
|
+
ok: Boolean(config.endpoint),
|
|
1425
|
+
detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint in ~/.mushirc \u2014 run `mushi init` or `mushi config endpoint <url>`"
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
name: "API key configured",
|
|
1429
|
+
ok: Boolean(config.apiKey),
|
|
1430
|
+
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>`"
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
name: "Project ID configured",
|
|
1434
|
+
ok: Boolean(config.projectId),
|
|
1435
|
+
detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
|
|
1436
|
+
}
|
|
1437
|
+
];
|
|
1438
|
+
}
|
|
1439
|
+
async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
1440
|
+
try {
|
|
1441
|
+
const res = await doFetch(`${endpoint}/health`, {
|
|
1442
|
+
signal: AbortSignal.timeout(5e3)
|
|
1443
|
+
});
|
|
1444
|
+
return {
|
|
1445
|
+
name: "Endpoint reachable",
|
|
1446
|
+
ok: res.status === 200,
|
|
1447
|
+
detail: `GET ${endpoint}/health \u2192 ${res.status}`
|
|
1448
|
+
};
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1451
|
+
return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
async function checkSdkInstall(cwd) {
|
|
1455
|
+
try {
|
|
1456
|
+
const { readFile: readFile2, access } = await import("fs/promises");
|
|
1457
|
+
const { join: join6, resolve: resolve2 } = await import("path");
|
|
1458
|
+
const root = resolve2(cwd);
|
|
1459
|
+
const pkgPath = join6(root, "package.json");
|
|
1460
|
+
await access(pkgPath);
|
|
1461
|
+
const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
|
|
1462
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1463
|
+
const sdks = [
|
|
1464
|
+
"@mushi-mushi/react",
|
|
1465
|
+
"@mushi-mushi/web",
|
|
1466
|
+
"@mushi-mushi/core",
|
|
1467
|
+
"@mushi-mushi/react-native"
|
|
1468
|
+
];
|
|
1469
|
+
const installed2 = sdks.filter((s) => deps[s]);
|
|
1470
|
+
return {
|
|
1471
|
+
name: "SDK installed in this repo",
|
|
1472
|
+
ok: installed2.length > 0,
|
|
1473
|
+
detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
|
|
1474
|
+
};
|
|
1475
|
+
} catch {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
1480
|
+
if (!config.projectId || !config.apiKey || !config.endpoint) {
|
|
1481
|
+
return [
|
|
1482
|
+
{
|
|
1483
|
+
name: "Server preflight",
|
|
1484
|
+
ok: false,
|
|
1485
|
+
detail: "Need projectId, apiKey, and endpoint. Run `mushi login` and `mushi config projectId <uuid>`."
|
|
1486
|
+
}
|
|
1487
|
+
];
|
|
1488
|
+
}
|
|
1489
|
+
try {
|
|
1490
|
+
const res = await doFetch(
|
|
1491
|
+
`${config.endpoint}/v1/admin/projects/${config.projectId}/preflight`,
|
|
1492
|
+
{
|
|
1493
|
+
headers: {
|
|
1494
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
1495
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
1496
|
+
"X-Mushi-Project": config.projectId
|
|
1497
|
+
},
|
|
1498
|
+
signal: AbortSignal.timeout(8e3)
|
|
1499
|
+
}
|
|
1500
|
+
);
|
|
1501
|
+
if (res.ok) {
|
|
1502
|
+
const body = await res.json();
|
|
1503
|
+
const serverChecks = body.data?.checks ?? [];
|
|
1504
|
+
return serverChecks.map((sc) => ({
|
|
1505
|
+
name: `[server] ${sc.label}`,
|
|
1506
|
+
ok: sc.ready,
|
|
1507
|
+
detail: sc.hint
|
|
1508
|
+
}));
|
|
1509
|
+
}
|
|
1510
|
+
const text2 = await res.text().catch(() => "");
|
|
1511
|
+
return [
|
|
1512
|
+
{
|
|
1513
|
+
name: "Server preflight",
|
|
1514
|
+
ok: false,
|
|
1515
|
+
detail: `HTTP ${res.status}: ${text2.slice(0, 120)}`
|
|
1516
|
+
}
|
|
1517
|
+
];
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1520
|
+
return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async function runDoctor(config, options = {}) {
|
|
1524
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
1525
|
+
const checks = [];
|
|
1526
|
+
checks.push(...checkCliConfig(config));
|
|
1527
|
+
if (config.endpoint) {
|
|
1528
|
+
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
1529
|
+
}
|
|
1530
|
+
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
1531
|
+
if (sdkCheck) checks.push(sdkCheck);
|
|
1532
|
+
if (options.server) {
|
|
1533
|
+
const serverChecks = await checkServerPreflight(config, doFetch);
|
|
1534
|
+
checks.push(...serverChecks);
|
|
1535
|
+
}
|
|
1536
|
+
return { checks, ready: checks.every((c) => c.ok) };
|
|
1537
|
+
}
|
|
1538
|
+
function formatDoctorResult(result) {
|
|
1539
|
+
const PASS = "\u2713";
|
|
1540
|
+
const FAIL = "\u2717";
|
|
1541
|
+
const lines = [];
|
|
1542
|
+
for (const c of result.checks) {
|
|
1543
|
+
lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
|
|
1544
|
+
lines.push(` ${c.detail}`);
|
|
1545
|
+
}
|
|
1546
|
+
const failed = result.checks.filter((c) => !c.ok);
|
|
1547
|
+
if (failed.length === 0) {
|
|
1548
|
+
lines.push("\nAll checks passed. The CLI is ready.");
|
|
1549
|
+
} else {
|
|
1550
|
+
lines.push(`
|
|
1551
|
+
${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
1552
|
+
lines.push("Fix the items above and re-run `mushi doctor`.");
|
|
1553
|
+
}
|
|
1554
|
+
return lines.join("\n");
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1235
1557
|
// src/index.ts
|
|
1236
1558
|
installSignalHandlers();
|
|
1237
1559
|
var API_TIMEOUT_MS = 15e3;
|
|
@@ -1945,6 +2267,260 @@ Examples:
|
|
|
1945
2267
|
silent: opts.silent
|
|
1946
2268
|
});
|
|
1947
2269
|
});
|
|
2270
|
+
var project = program.command("project").description("Project management");
|
|
2271
|
+
project.command("create").description("Create a new Mushi project, mint an API key, and write config files").option("--name <name>", "Project name (skip the prompt)").option("--no-browser", "Skip opening the browser for the sign-up / magic-link step").option("--endpoint <url>", "Override API endpoint (self-hosted)").addHelpText("after", `
|
|
2272
|
+
Creates a project on app.mushimushi.dev, mints an API key with mcp:read+write scope,
|
|
2273
|
+
and writes the following to the current directory:
|
|
2274
|
+
.env.local \u2014 MUSHI_API_KEY, MUSHI_PROJECT_ID, MUSHI_API_ENDPOINT
|
|
2275
|
+
.cursor/mcp.json \u2014 pre-filled mcpServers.mushi block for Cursor
|
|
2276
|
+
|
|
2277
|
+
Typical first-time flow:
|
|
2278
|
+
npx mushi-mushi project create
|
|
2279
|
+
# Browser opens \u2192 sign up / magic-link \u2192 come back to terminal
|
|
2280
|
+
# CLI writes .env.local and .cursor/mcp.json
|
|
2281
|
+
# mushi whoami to confirm`).action(async (opts) => {
|
|
2282
|
+
const { writeFile, mkdir } = await import("fs/promises");
|
|
2283
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
2284
|
+
const nodePath = await import("path");
|
|
2285
|
+
const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
|
|
2286
|
+
const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
|
|
2287
|
+
console.log("");
|
|
2288
|
+
console.log(" Mushi project create");
|
|
2289
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2290
|
+
console.log("");
|
|
2291
|
+
if (opts.browser !== false) {
|
|
2292
|
+
console.log(" 1. Opening the Mushi sign-up page in your browser...");
|
|
2293
|
+
try {
|
|
2294
|
+
const { exec } = await import("child_process");
|
|
2295
|
+
const openCmd = process.platform === "win32" ? `start "" "${signUpUrl}"` : process.platform === "darwin" ? `open "${signUpUrl}"` : `xdg-open "${signUpUrl}"`;
|
|
2296
|
+
exec(openCmd);
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
2299
|
+
} else {
|
|
2300
|
+
console.log(` 1. Sign up or log in at: ${signUpUrl}`);
|
|
2301
|
+
}
|
|
2302
|
+
console.log("");
|
|
2303
|
+
console.log(" 2. Create a project in the console, then paste your credentials below.");
|
|
2304
|
+
console.log(" (Settings \u2192 API Keys \u2192 New key \u2192 Copy as .env.local)");
|
|
2305
|
+
console.log("");
|
|
2306
|
+
const { createInterface } = await import("readline");
|
|
2307
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2308
|
+
const ask = (q) => new Promise((resolve2) => rl.question(q, (a) => resolve2(a.trim())));
|
|
2309
|
+
const projectId = await ask(" Project ID (uuid): ");
|
|
2310
|
+
const apiKey = await ask(" API key (mushi_...): ");
|
|
2311
|
+
rl.close();
|
|
2312
|
+
if (!projectId || !apiKey) {
|
|
2313
|
+
process.stderr.write("\nerror: Project ID and API key are required.\n");
|
|
2314
|
+
process.exit(2);
|
|
2315
|
+
}
|
|
2316
|
+
const config = loadConfig();
|
|
2317
|
+
config.apiKey = apiKey;
|
|
2318
|
+
config.endpoint = endpoint;
|
|
2319
|
+
config.projectId = projectId;
|
|
2320
|
+
saveConfig(config);
|
|
2321
|
+
const cwd = process.cwd();
|
|
2322
|
+
const envPath = nodePath.join(cwd, ".env.local");
|
|
2323
|
+
const envLines = [
|
|
2324
|
+
"# Mushi MCP \u2014 drop into .env.local (gitignored). The MCP binary picks these up on spawn.",
|
|
2325
|
+
`MUSHI_API_ENDPOINT=${endpoint}`,
|
|
2326
|
+
`MUSHI_PROJECT_ID=${projectId}`,
|
|
2327
|
+
`MUSHI_API_KEY=${apiKey}`,
|
|
2328
|
+
""
|
|
2329
|
+
];
|
|
2330
|
+
const envExisting = existsSync5(envPath);
|
|
2331
|
+
await writeFile(envPath, envLines.join("\n"), "utf8");
|
|
2332
|
+
console.log(`
|
|
2333
|
+
\u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
|
|
2334
|
+
const mcpDir = nodePath.join(cwd, ".cursor");
|
|
2335
|
+
await mkdir(mcpDir, { recursive: true });
|
|
2336
|
+
const mcpPath = nodePath.join(mcpDir, "mcp.json");
|
|
2337
|
+
const mcpJson = {
|
|
2338
|
+
mcpServers: {
|
|
2339
|
+
mushi: {
|
|
2340
|
+
command: "npx",
|
|
2341
|
+
args: ["-y", "mushi-mcp@latest"],
|
|
2342
|
+
env: {
|
|
2343
|
+
MUSHI_API_ENDPOINT: endpoint,
|
|
2344
|
+
MUSHI_PROJECT_ID: projectId,
|
|
2345
|
+
MUSHI_API_KEY: apiKey
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
const mcpExisting = existsSync5(mcpPath);
|
|
2351
|
+
if (mcpExisting) {
|
|
2352
|
+
try {
|
|
2353
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
2354
|
+
const raw = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
2355
|
+
const existing = raw;
|
|
2356
|
+
existing.mcpServers = { ...existing.mcpServers ?? {}, mushi: mcpJson.mcpServers.mushi };
|
|
2357
|
+
await writeFile(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
2358
|
+
} catch {
|
|
2359
|
+
await writeFile(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
|
|
2360
|
+
}
|
|
2361
|
+
} else {
|
|
2362
|
+
await writeFile(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
|
|
2363
|
+
}
|
|
2364
|
+
console.log(` \u2713 ${mcpExisting ? "Updated" : "Created"} .cursor/mcp.json`);
|
|
2365
|
+
console.log("");
|
|
2366
|
+
console.log(' Done! Restart Cursor and ask: "list mushi tools"');
|
|
2367
|
+
console.log(" Run `mushi whoami` to verify the connection.");
|
|
2368
|
+
console.log("");
|
|
2369
|
+
});
|
|
2370
|
+
program.command("setup").description("Wire Mushi into your IDE with one command").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: project ID prefix)").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").addHelpText("after", `
|
|
2371
|
+
Examples:
|
|
2372
|
+
mushi setup # wire Cursor (default)
|
|
2373
|
+
mushi setup --ide claude # wire Claude Code
|
|
2374
|
+
mushi setup --ide cursor --with-rules # also write .cursorrules
|
|
2375
|
+
|
|
2376
|
+
Supported IDEs:
|
|
2377
|
+
cursor \u2014 writes .cursor/mcp.json
|
|
2378
|
+
claude \u2014 writes .claude/mcp.json (Claude Code / Claude Desktop)
|
|
2379
|
+
continue \u2014 writes .continue/mcp.json
|
|
2380
|
+
zed \u2014 writes ~/.config/zed/settings.json mcpServers block
|
|
2381
|
+
|
|
2382
|
+
The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
|
|
2383
|
+
const { writeFile, mkdir, readFile: readFile2 } = await import("fs/promises");
|
|
2384
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
2385
|
+
const nodePath = await import("path");
|
|
2386
|
+
const os = await import("os");
|
|
2387
|
+
const config = requireConfig({ needsProject: true });
|
|
2388
|
+
const IDE_CONFIG = {
|
|
2389
|
+
cursor: { dir: ".cursor", file: "mcp.json", format: "mcp-json" },
|
|
2390
|
+
claude: { dir: ".claude", file: "mcp.json", format: "mcp-json" },
|
|
2391
|
+
continue: { dir: ".continue", file: "mcp.json", format: "mcp-json" },
|
|
2392
|
+
zed: { dir: nodePath.join(os.homedir(), ".config", "zed"), file: "settings.json", format: "zed" }
|
|
2393
|
+
};
|
|
2394
|
+
const ideEntry = IDE_CONFIG[opts.ide];
|
|
2395
|
+
if (!ideEntry) {
|
|
2396
|
+
process.stderr.write(`error: unsupported IDE "${opts.ide}". Supported: ${Object.keys(IDE_CONFIG).join(", ")}
|
|
2397
|
+
`);
|
|
2398
|
+
process.exit(2);
|
|
2399
|
+
}
|
|
2400
|
+
const cwd = process.cwd();
|
|
2401
|
+
const slug = opts.projectSlug ?? (config.projectId?.slice(0, 8) ?? "mushi");
|
|
2402
|
+
const serverName = `mushi-${slug}`;
|
|
2403
|
+
const mcpServerBlock = {
|
|
2404
|
+
command: "npx",
|
|
2405
|
+
args: ["-y", "mushi-mcp@latest"],
|
|
2406
|
+
env: {
|
|
2407
|
+
MUSHI_API_ENDPOINT: config.endpoint,
|
|
2408
|
+
MUSHI_PROJECT_ID: config.projectId ?? "",
|
|
2409
|
+
MUSHI_API_KEY: config.apiKey
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir : nodePath.join(cwd, ideEntry.dir);
|
|
2413
|
+
const configPath = nodePath.join(configDir, ideEntry.file);
|
|
2414
|
+
if (ideEntry.format === "mcp-json") {
|
|
2415
|
+
let merged = { mcpServers: {} };
|
|
2416
|
+
if (existsSync5(configPath)) {
|
|
2417
|
+
try {
|
|
2418
|
+
const raw = await readFile2(configPath, "utf8");
|
|
2419
|
+
merged = JSON.parse(raw);
|
|
2420
|
+
} catch {
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
const servers = merged.mcpServers ?? {};
|
|
2424
|
+
servers[serverName] = mcpServerBlock;
|
|
2425
|
+
merged.mcpServers = servers;
|
|
2426
|
+
const output = JSON.stringify(merged, null, 2) + "\n";
|
|
2427
|
+
if (opts.dryRun) {
|
|
2428
|
+
console.log(`[dry-run] Would write ${configPath}:`);
|
|
2429
|
+
console.log(output);
|
|
2430
|
+
} else {
|
|
2431
|
+
await mkdir(configDir, { recursive: true });
|
|
2432
|
+
await writeFile(configPath, output, "utf8");
|
|
2433
|
+
console.log(`\u2713 Written ${configPath}`);
|
|
2434
|
+
}
|
|
2435
|
+
} else if (ideEntry.format === "zed") {
|
|
2436
|
+
let settings = {};
|
|
2437
|
+
if (existsSync5(configPath)) {
|
|
2438
|
+
try {
|
|
2439
|
+
const raw = await readFile2(configPath, "utf8");
|
|
2440
|
+
settings = JSON.parse(raw);
|
|
2441
|
+
} catch {
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
const servers = settings.context_servers ?? {};
|
|
2445
|
+
servers[serverName] = {
|
|
2446
|
+
command: {
|
|
2447
|
+
path: "npx",
|
|
2448
|
+
args: ["-y", "mushi-mcp@latest"],
|
|
2449
|
+
env: {
|
|
2450
|
+
MUSHI_API_ENDPOINT: config.endpoint,
|
|
2451
|
+
MUSHI_PROJECT_ID: config.projectId ?? "",
|
|
2452
|
+
MUSHI_API_KEY: config.apiKey
|
|
2453
|
+
}
|
|
2454
|
+
},
|
|
2455
|
+
settings: {}
|
|
2456
|
+
};
|
|
2457
|
+
settings.context_servers = servers;
|
|
2458
|
+
const output = JSON.stringify(settings, null, 2) + "\n";
|
|
2459
|
+
if (opts.dryRun) {
|
|
2460
|
+
console.log(`[dry-run] Would write ${configPath}:`);
|
|
2461
|
+
console.log(output);
|
|
2462
|
+
} else {
|
|
2463
|
+
await mkdir(configDir, { recursive: true });
|
|
2464
|
+
await writeFile(configPath, output, "utf8");
|
|
2465
|
+
console.log(`\u2713 Written ${configPath}`);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (opts.withRules) {
|
|
2469
|
+
const rulesContent = [
|
|
2470
|
+
"# Mushi Mushi \u2014 evolution-loop coding rules",
|
|
2471
|
+
"#",
|
|
2472
|
+
"# These rules are generated from your project's live lesson library.",
|
|
2473
|
+
"# Run `mushi sync-lessons` to refresh .mushi/lessons.json",
|
|
2474
|
+
"# The MCP server (mushi tools) also injects lessons dynamically at fix time.",
|
|
2475
|
+
"",
|
|
2476
|
+
"## Before writing a fix",
|
|
2477
|
+
"",
|
|
2478
|
+
"1. Call `get_fix_context` (MCP) for the report \u2014 get root cause + blast radius first.",
|
|
2479
|
+
"2. Call `lessons.query` (MCP) or read .mushi/lessons.json \u2014 apply every matching rule.",
|
|
2480
|
+
"3. Prefer the smallest change that makes the test pass. Don't refactor unrelated code.",
|
|
2481
|
+
"",
|
|
2482
|
+
"## After writing a fix",
|
|
2483
|
+
"",
|
|
2484
|
+
"1. Call `submit_fix_result` (MCP) with the branch, PR URL, and files changed.",
|
|
2485
|
+
"2. The judge batch will score the fix overnight \u2014 high-frequency lessons surface in /admin/lessons.",
|
|
2486
|
+
"",
|
|
2487
|
+
"## Mushi lesson library (auto-updated by `mushi sync-lessons`)",
|
|
2488
|
+
"",
|
|
2489
|
+
"<!-- lessons synced from .mushi/lessons.json -->",
|
|
2490
|
+
"<!-- run `mushi sync-lessons` to refresh -->",
|
|
2491
|
+
""
|
|
2492
|
+
].join("\n");
|
|
2493
|
+
if (opts.ide === "cursor") {
|
|
2494
|
+
const rulesPath = nodePath.join(cwd, ".cursorrules");
|
|
2495
|
+
if (opts.dryRun) {
|
|
2496
|
+
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2497
|
+
} else {
|
|
2498
|
+
await writeFile(rulesPath, rulesContent, "utf8");
|
|
2499
|
+
console.log(`\u2713 Written .cursorrules`);
|
|
2500
|
+
}
|
|
2501
|
+
} else if (opts.ide === "claude") {
|
|
2502
|
+
const rulesDir = nodePath.join(cwd, ".claude", "rules");
|
|
2503
|
+
const rulesPath = nodePath.join(rulesDir, "mushi.md");
|
|
2504
|
+
if (opts.dryRun) {
|
|
2505
|
+
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2506
|
+
} else {
|
|
2507
|
+
await mkdir(rulesDir, { recursive: true });
|
|
2508
|
+
await writeFile(rulesPath, rulesContent, "utf8");
|
|
2509
|
+
console.log(`\u2713 Written .claude/rules/mushi.md`);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
if (!opts.dryRun) {
|
|
2514
|
+
console.log("");
|
|
2515
|
+
console.log(`Done! Restart ${opts.ide === "cursor" ? "Cursor" : opts.ide === "claude" ? "Claude Code" : opts.ide} and ask: "list mushi tools"`);
|
|
2516
|
+
if (!opts.withRules) {
|
|
2517
|
+
console.log(`Tip: run with --with-rules to also write the lesson-library coding hook.`);
|
|
2518
|
+
}
|
|
2519
|
+
const configRelPath = ideEntry.dir.startsWith("/") ? configPath : nodePath.relative(cwd, configPath);
|
|
2520
|
+
console.log(`
|
|
2521
|
+
Note: ${configRelPath} contains your Mushi API key \u2014 add it to .gitignore if this is a shared repo.`);
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
1948
2524
|
var fixCmd = program.command("fix").description("Dispatch an agentic fix for a report");
|
|
1949
2525
|
fixCmd.argument("<reportId>", "Report UUID to fix").option(
|
|
1950
2526
|
"--agent <name>",
|
|
@@ -2034,4 +2610,141 @@ Examples:
|
|
|
2034
2610
|
console.error("Polling timed out after 10 minutes. The fix may still be running.");
|
|
2035
2611
|
process.exit(1);
|
|
2036
2612
|
});
|
|
2613
|
+
program.command("nudge").description(
|
|
2614
|
+
"Generate a Mushi.init() snippet tuned for your release phase (alpha, beta, ga). Customises proactive triggers, cooldowns, feature-request card, and beta-mode UI."
|
|
2615
|
+
).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) => {
|
|
2616
|
+
const validPhases = ["alpha", "beta", "ga"];
|
|
2617
|
+
if (!validPhases.includes(opts.phase)) {
|
|
2618
|
+
console.error(`Unknown phase "${opts.phase}". Use one of: ${validPhases.join(", ")}`);
|
|
2619
|
+
process.exit(1);
|
|
2620
|
+
}
|
|
2621
|
+
const phase = opts.phase;
|
|
2622
|
+
const overrides = {};
|
|
2623
|
+
if (opts.max) overrides.maxProactivePerSession = Number(opts.max);
|
|
2624
|
+
if (opts.cooldown) overrides.dismissCooldownHours = Number(opts.cooldown);
|
|
2625
|
+
if (opts.dwell !== void 0) overrides.pageDwellMinutes = Number(opts.dwell);
|
|
2626
|
+
if (opts.welcome !== void 0) overrides.firstSessionSeconds = Number(opts.welcome);
|
|
2627
|
+
if (opts.explain) {
|
|
2628
|
+
console.log(renderNudgeExplainer(phase));
|
|
2629
|
+
}
|
|
2630
|
+
console.log(renderNudgeSnippet({ phase, overrides }));
|
|
2631
|
+
});
|
|
2632
|
+
program.command("doctor").description(
|
|
2633
|
+
"Run pre-flight checks: CLI config, endpoint reachability, API key shape, SDK install status, and (with --server) the same 4 dispatch-readiness checks shown in the Mushi console. Mirrors the in-console dispatch preflight so you can spot setup gaps before opening the admin UI."
|
|
2634
|
+
).option("--cwd <path>", "Run package detection from a different directory").option("--json", "Machine-readable output").option(
|
|
2635
|
+
"--server",
|
|
2636
|
+
"Also call GET /preflight on the backend and include the 4 dispatch checks (GitHub repo, codebase indexed, Anthropic key, autofix enabled). Requires a configured projectId and API key."
|
|
2637
|
+
).action(async (opts) => {
|
|
2638
|
+
const config = loadConfig();
|
|
2639
|
+
const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server });
|
|
2640
|
+
const { checks } = result;
|
|
2641
|
+
if (opts.json) {
|
|
2642
|
+
console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
|
|
2643
|
+
if (!result.ready) process.exit(1);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
console.log(formatDoctorResult(result));
|
|
2647
|
+
if (!result.ready) process.exit(1);
|
|
2648
|
+
});
|
|
2649
|
+
program.command("reset [projectId]").description(
|
|
2650
|
+
"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."
|
|
2651
|
+
).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (projectId, opts) => {
|
|
2652
|
+
const config = loadConfig();
|
|
2653
|
+
const resolvedId = projectId ?? config.projectId;
|
|
2654
|
+
if (!config.apiKey) {
|
|
2655
|
+
console.error("Run `mushi login` first");
|
|
2656
|
+
process.exit(1);
|
|
2657
|
+
}
|
|
2658
|
+
if (!resolvedId) {
|
|
2659
|
+
console.error("Provide a projectId or set one via `mushi config projectId <uuid>`");
|
|
2660
|
+
process.exit(1);
|
|
2661
|
+
}
|
|
2662
|
+
if (!opts.confirm) {
|
|
2663
|
+
console.error(
|
|
2664
|
+
`This will archive project ${resolvedId} and delete all its reports, fix_attempts, and codebase_files.
|
|
2665
|
+
Re-run with --confirm to proceed.`
|
|
2666
|
+
);
|
|
2667
|
+
process.exit(1);
|
|
2668
|
+
}
|
|
2669
|
+
const data = await apiCall(
|
|
2670
|
+
`/v1/admin/projects/${resolvedId}/reset`,
|
|
2671
|
+
config,
|
|
2672
|
+
{ method: "POST" }
|
|
2673
|
+
);
|
|
2674
|
+
if (opts.json) {
|
|
2675
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2676
|
+
} else if (data.ok) {
|
|
2677
|
+
console.log(`Project ${resolvedId} archived and test data wiped.`);
|
|
2678
|
+
} else {
|
|
2679
|
+
console.error("Reset failed:", JSON.stringify(data, null, 2));
|
|
2680
|
+
process.exit(1);
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
var fixes = program.command("fixes").description("Fix dispatch management");
|
|
2684
|
+
fixes.command("tail").description(
|
|
2685
|
+
"Stream SSE dispatch events for a report in real time. Useful for headless debugging without opening the browser."
|
|
2686
|
+
).requiredOption("--report-id <id>", "Report ID to follow").action(async (opts) => {
|
|
2687
|
+
const config = loadConfig();
|
|
2688
|
+
if (!config.apiKey) {
|
|
2689
|
+
console.error("Run `mushi login` first");
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
if (!config.endpoint) {
|
|
2693
|
+
console.error("No endpoint configured. Run `mushi init`");
|
|
2694
|
+
process.exit(1);
|
|
2695
|
+
}
|
|
2696
|
+
const url = `${config.endpoint}/v1/admin/reports/${opts.reportId}/dispatch/stream`;
|
|
2697
|
+
console.log(`Tailing dispatch stream for report ${opts.reportId}\u2026`);
|
|
2698
|
+
console.log(`(Ctrl-C to stop)
|
|
2699
|
+
`);
|
|
2700
|
+
const res = await fetch(url, {
|
|
2701
|
+
headers: {
|
|
2702
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
2703
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
2704
|
+
"X-Mushi-Project": config.projectId ?? "",
|
|
2705
|
+
"Accept": "text/event-stream"
|
|
2706
|
+
}
|
|
2707
|
+
});
|
|
2708
|
+
if (!res.ok || !res.body) {
|
|
2709
|
+
console.error(`Failed to connect: HTTP ${res.status}`);
|
|
2710
|
+
const text2 = await res.text().catch(() => "");
|
|
2711
|
+
if (text2) console.error(text2.slice(0, 300));
|
|
2712
|
+
process.exit(1);
|
|
2713
|
+
}
|
|
2714
|
+
const decoder = new TextDecoder();
|
|
2715
|
+
const reader = res.body.getReader();
|
|
2716
|
+
let done = false;
|
|
2717
|
+
process.on("SIGINT", () => {
|
|
2718
|
+
done = true;
|
|
2719
|
+
void reader.cancel();
|
|
2720
|
+
console.log("\nDisconnected.");
|
|
2721
|
+
process.exit(0);
|
|
2722
|
+
});
|
|
2723
|
+
while (!done) {
|
|
2724
|
+
const { value, done: streamDone } = await reader.read();
|
|
2725
|
+
if (streamDone) break;
|
|
2726
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
2727
|
+
for (const line of chunk.split("\n")) {
|
|
2728
|
+
if (line.startsWith("data: ")) {
|
|
2729
|
+
const raw = line.slice(6).trim();
|
|
2730
|
+
if (raw === "[DONE]") {
|
|
2731
|
+
console.log("\n[stream ended]");
|
|
2732
|
+
process.exit(0);
|
|
2733
|
+
}
|
|
2734
|
+
try {
|
|
2735
|
+
const event = JSON.parse(raw);
|
|
2736
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2737
|
+
const type = event.type ?? event.event ?? "event";
|
|
2738
|
+
const status = event.status ?? event.data ?? "";
|
|
2739
|
+
console.log(`${ts} ${type.padEnd(24)} ${status}`);
|
|
2740
|
+
} catch {
|
|
2741
|
+
console.log(line);
|
|
2742
|
+
}
|
|
2743
|
+
} else if (line.startsWith("event: ")) {
|
|
2744
|
+
} else if (line && !line.startsWith(":")) {
|
|
2745
|
+
console.log(line);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2037
2750
|
program.parse();
|