@mushi-mushi/cli 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/chunk-4W6VOIQT.js +6 -0
- package/dist/index.js +934 -47
- package/dist/init.js +8 -5
- package/dist/version.js +1 -1
- package/package.json +10 -10
- package/dist/chunk-F43X732E.js +0 -6
package/dist/index.js
CHANGED
|
@@ -35,10 +35,11 @@ function loadConfig(path = CONFIG_PATH) {
|
|
|
35
35
|
} else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
|
|
36
36
|
file = migrateLegacyConfig() ?? {};
|
|
37
37
|
}
|
|
38
|
+
const endpointFromEnv = process.env["MUSHI_API_ENDPOINT"] ?? process.env["MUSHI_ENDPOINT"] ?? void 0;
|
|
38
39
|
const fromEnv = {
|
|
39
40
|
...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
|
|
40
41
|
...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
|
|
41
|
-
...
|
|
42
|
+
...endpointFromEnv ? { endpoint: endpointFromEnv } : {}
|
|
42
43
|
};
|
|
43
44
|
return { ...file, ...fromEnv };
|
|
44
45
|
}
|
|
@@ -444,7 +445,7 @@ function normalizeEndpoint(url) {
|
|
|
444
445
|
var REGISTRY = "https://registry.npmjs.org";
|
|
445
446
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
446
447
|
async function checkFreshness(packageName, currentVersion, opts = {}) {
|
|
447
|
-
if (process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
|
|
448
|
+
if (!opts.ignoreOptOut && process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
|
|
448
449
|
const registry = opts.registry ?? REGISTRY;
|
|
449
450
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
450
451
|
const controller = new AbortController();
|
|
@@ -482,11 +483,13 @@ function isNewerStableVersion(latest, current) {
|
|
|
482
483
|
return lc > cc;
|
|
483
484
|
}
|
|
484
485
|
function stripPreRelease(version) {
|
|
485
|
-
const idx = version.
|
|
486
|
+
const idx = version.search(/[-+]/);
|
|
486
487
|
return idx === -1 ? version : version.slice(0, idx);
|
|
487
488
|
}
|
|
488
489
|
function hasPreReleaseTag(version) {
|
|
489
|
-
|
|
490
|
+
const plus = version.indexOf("+");
|
|
491
|
+
const hyphen = version.indexOf("-");
|
|
492
|
+
return hyphen !== -1 && (plus === -1 || hyphen < plus);
|
|
490
493
|
}
|
|
491
494
|
function parse(version) {
|
|
492
495
|
const parts = version.split(".").map((part) => Number(part));
|
|
@@ -595,7 +598,7 @@ function getFrameworkFromPkg(pkg) {
|
|
|
595
598
|
}
|
|
596
599
|
|
|
597
600
|
// src/version.ts
|
|
598
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
601
|
+
var MUSHI_CLI_VERSION = true ? "0.14.0" : "0.0.0-dev";
|
|
599
602
|
|
|
600
603
|
// src/init.ts
|
|
601
604
|
var ENV_FILES = [".env.local", ".env"];
|
|
@@ -744,7 +747,7 @@ async function installPackages(pm, packages, cwd) {
|
|
|
744
747
|
function runCommand(pm, packages, cwd) {
|
|
745
748
|
const verb = pm === "npm" ? "install" : "add";
|
|
746
749
|
const command = process.platform === "win32" ? `${pm}.cmd` : pm;
|
|
747
|
-
return new Promise((
|
|
750
|
+
return new Promise((resolve4, reject) => {
|
|
748
751
|
const child = spawn(command, [verb, ...packages], {
|
|
749
752
|
stdio: "inherit",
|
|
750
753
|
shell: false,
|
|
@@ -753,7 +756,7 @@ function runCommand(pm, packages, cwd) {
|
|
|
753
756
|
});
|
|
754
757
|
child.on("error", reject);
|
|
755
758
|
child.on("exit", (code) => {
|
|
756
|
-
if (code === 0)
|
|
759
|
+
if (code === 0) resolve4();
|
|
757
760
|
else reject(new Error(`${pm} exited with code ${code ?? "null"}`));
|
|
758
761
|
});
|
|
759
762
|
});
|
|
@@ -1108,11 +1111,11 @@ async function findMapFiles(dir) {
|
|
|
1108
1111
|
return results;
|
|
1109
1112
|
}
|
|
1110
1113
|
function fileHash(path) {
|
|
1111
|
-
return new Promise((
|
|
1114
|
+
return new Promise((resolve4, reject) => {
|
|
1112
1115
|
const hash = createHash("sha256");
|
|
1113
1116
|
const stream = createReadStream(path);
|
|
1114
1117
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
1115
|
-
stream.on("end", () =>
|
|
1118
|
+
stream.on("end", () => resolve4(hash.digest("hex")));
|
|
1116
1119
|
stream.on("error", reject);
|
|
1117
1120
|
});
|
|
1118
1121
|
}
|
|
@@ -1416,13 +1419,83 @@ function renderNudgeExplainer(phase) {
|
|
|
1416
1419
|
return lines.join("\n");
|
|
1417
1420
|
}
|
|
1418
1421
|
|
|
1422
|
+
// src/heartbeat-wait.ts
|
|
1423
|
+
var IngestSetupHttpError = class extends Error {
|
|
1424
|
+
status;
|
|
1425
|
+
constructor(status) {
|
|
1426
|
+
super(`ingest-setup HTTP ${status}`);
|
|
1427
|
+
this.name = "IngestSetupHttpError";
|
|
1428
|
+
this.status = status;
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
1432
|
+
async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
|
|
1433
|
+
const safeKey = config.apiKey.replace(/[\r\n]/g, "");
|
|
1434
|
+
const res = await doFetch(`${config.endpoint}/v1/sync/ingest-setup`, {
|
|
1435
|
+
headers: {
|
|
1436
|
+
Authorization: `Bearer ${safeKey}`,
|
|
1437
|
+
"X-Mushi-Api-Key": safeKey,
|
|
1438
|
+
...config.projectId ? { "X-Mushi-Project": config.projectId } : {}
|
|
1439
|
+
},
|
|
1440
|
+
signal: AbortSignal.timeout(8e3)
|
|
1441
|
+
});
|
|
1442
|
+
if (!res.ok) {
|
|
1443
|
+
if (NON_RETRYABLE_STATUSES.has(res.status)) throw new IngestSetupHttpError(res.status);
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
const body = await res.json();
|
|
1447
|
+
return body.ok && body.data ? body.data : null;
|
|
1448
|
+
}
|
|
1449
|
+
async function waitForIngestReady(options) {
|
|
1450
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
1451
|
+
const maxAttempts = options.maxAttempts ?? 40;
|
|
1452
|
+
const intervalMs = options.intervalMs ?? 3e3;
|
|
1453
|
+
let lastPayload = null;
|
|
1454
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1455
|
+
if (options.signal?.aborted) {
|
|
1456
|
+
return { ok: false, payload: lastPayload, attempts: attempt - 1, reason: "aborted" };
|
|
1457
|
+
}
|
|
1458
|
+
try {
|
|
1459
|
+
lastPayload = await fetchIngestSetup(
|
|
1460
|
+
{ endpoint: options.endpoint, apiKey: options.apiKey, projectId: options.projectId },
|
|
1461
|
+
doFetch
|
|
1462
|
+
);
|
|
1463
|
+
if (lastPayload) {
|
|
1464
|
+
options.onPoll?.(lastPayload, attempt);
|
|
1465
|
+
if (lastPayload.ready) {
|
|
1466
|
+
return { ok: true, payload: lastPayload, attempts: attempt, reason: "ready" };
|
|
1467
|
+
}
|
|
1468
|
+
const sdkStep = lastPayload.steps.find((s) => s.id === "sdk_installed");
|
|
1469
|
+
if (sdkStep?.complete) {
|
|
1470
|
+
return { ok: true, payload: lastPayload, attempts: attempt, reason: "heartbeat" };
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1475
|
+
if (err instanceof IngestSetupHttpError) {
|
|
1476
|
+
return { ok: false, payload: lastPayload, attempts: attempt, reason: "unauthorized", error: msg };
|
|
1477
|
+
}
|
|
1478
|
+
if (options.signal?.aborted) {
|
|
1479
|
+
return { ok: false, payload: lastPayload, attempts: attempt, reason: "aborted", error: msg };
|
|
1480
|
+
}
|
|
1481
|
+
if (attempt === maxAttempts) {
|
|
1482
|
+
return { ok: false, payload: lastPayload, attempts: attempt, reason: "fetch-error", error: msg };
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (attempt < maxAttempts) {
|
|
1486
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1419
1492
|
// src/doctor.ts
|
|
1420
1493
|
function checkCliConfig(config) {
|
|
1421
1494
|
return [
|
|
1422
1495
|
{
|
|
1423
1496
|
name: "CLI config file",
|
|
1424
1497
|
ok: Boolean(config.endpoint),
|
|
1425
|
-
detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint
|
|
1498
|
+
detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint \u2014 set MUSHI_API_ENDPOINT, run `mushi connect`, or `mushi config endpoint <url>`"
|
|
1426
1499
|
},
|
|
1427
1500
|
{
|
|
1428
1501
|
name: "API key configured",
|
|
@@ -1453,11 +1526,11 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
|
1453
1526
|
}
|
|
1454
1527
|
async function checkSdkInstall(cwd) {
|
|
1455
1528
|
try {
|
|
1456
|
-
const { readFile:
|
|
1457
|
-
const { join:
|
|
1458
|
-
const root =
|
|
1459
|
-
const pkgPath =
|
|
1460
|
-
const pkg = JSON.parse(await
|
|
1529
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
1530
|
+
const { join: join7, resolve: resolve4 } = await import("path");
|
|
1531
|
+
const root = resolve4(cwd);
|
|
1532
|
+
const pkgPath = join7(root, "package.json");
|
|
1533
|
+
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
1461
1534
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1462
1535
|
const sdks = [
|
|
1463
1536
|
"@mushi-mushi/react",
|
|
@@ -1503,7 +1576,7 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
1503
1576
|
return serverChecks.map((sc) => ({
|
|
1504
1577
|
name: `[server] ${sc.label}`,
|
|
1505
1578
|
ok: sc.ready,
|
|
1506
|
-
detail: sc.hint
|
|
1579
|
+
detail: sc.ready ? "" : sc.hint
|
|
1507
1580
|
}));
|
|
1508
1581
|
}
|
|
1509
1582
|
const text2 = await res.text().catch(() => "");
|
|
@@ -1519,6 +1592,132 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
1519
1592
|
return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
|
|
1520
1593
|
}
|
|
1521
1594
|
}
|
|
1595
|
+
async function checkIngestSetup(config, doFetch = globalThis.fetch) {
|
|
1596
|
+
if (!config.apiKey || !config.endpoint) {
|
|
1597
|
+
return [
|
|
1598
|
+
{
|
|
1599
|
+
name: "Ingest setup",
|
|
1600
|
+
ok: false,
|
|
1601
|
+
detail: "Need apiKey and endpoint. Run `mushi connect`."
|
|
1602
|
+
}
|
|
1603
|
+
];
|
|
1604
|
+
}
|
|
1605
|
+
try {
|
|
1606
|
+
const data = await fetchIngestSetup(
|
|
1607
|
+
{ endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
|
|
1608
|
+
doFetch
|
|
1609
|
+
);
|
|
1610
|
+
if (!data) {
|
|
1611
|
+
return [{ name: "Ingest setup", ok: false, detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload" }];
|
|
1612
|
+
}
|
|
1613
|
+
const steps = data.steps ?? [];
|
|
1614
|
+
const checks = steps.filter((s) => s.required).map((s) => ({
|
|
1615
|
+
name: `[ingest] ${s.label}`,
|
|
1616
|
+
ok: s.complete,
|
|
1617
|
+
detail: s.complete ? "" : s.hint ?? ""
|
|
1618
|
+
}));
|
|
1619
|
+
const diag = data.diagnostic;
|
|
1620
|
+
if (diag?.last_sdk_seen_at) {
|
|
1621
|
+
checks.push({
|
|
1622
|
+
name: "[ingest] Last SDK heartbeat",
|
|
1623
|
+
ok: true,
|
|
1624
|
+
detail: `${diag.last_sdk_seen_at}${diag.last_sdk_endpoint_host ? ` @ ${diag.last_sdk_endpoint_host}` : ""}`
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
return checks.length > 0 ? checks : [{ name: "Ingest setup", ok: false, detail: "Empty response from /v1/sync/ingest-setup" }];
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1630
|
+
return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
1634
|
+
if (!config.projectId || !config.apiKey || !config.endpoint) {
|
|
1635
|
+
return [
|
|
1636
|
+
{
|
|
1637
|
+
name: "QA stories health",
|
|
1638
|
+
ok: false,
|
|
1639
|
+
detail: "Need projectId, apiKey, and endpoint for QA story checks."
|
|
1640
|
+
}
|
|
1641
|
+
];
|
|
1642
|
+
}
|
|
1643
|
+
const checks = [];
|
|
1644
|
+
try {
|
|
1645
|
+
const storiesRes = await doFetch(
|
|
1646
|
+
`${config.endpoint}/v1/admin/projects/${config.projectId}/qa-coverage`,
|
|
1647
|
+
{
|
|
1648
|
+
headers: {
|
|
1649
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
1650
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
1651
|
+
"X-Mushi-Project": config.projectId
|
|
1652
|
+
},
|
|
1653
|
+
signal: AbortSignal.timeout(8e3)
|
|
1654
|
+
}
|
|
1655
|
+
);
|
|
1656
|
+
if (!storiesRes.ok) {
|
|
1657
|
+
checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
|
|
1658
|
+
return checks;
|
|
1659
|
+
}
|
|
1660
|
+
const storiesBody = await storiesRes.json();
|
|
1661
|
+
const stories2 = storiesBody.data?.coverage ?? [];
|
|
1662
|
+
const enabled = stories2.filter((s) => s.enabled);
|
|
1663
|
+
if (enabled.length === 0) {
|
|
1664
|
+
checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
|
|
1665
|
+
return checks;
|
|
1666
|
+
}
|
|
1667
|
+
checks.push({
|
|
1668
|
+
name: "[qa] Enabled QA stories",
|
|
1669
|
+
ok: true,
|
|
1670
|
+
detail: `${enabled.length} enabled story/stories configured`
|
|
1671
|
+
});
|
|
1672
|
+
const slackRes = await doFetch(
|
|
1673
|
+
`${config.endpoint}/v1/admin/projects/${config.projectId}/integrations/probe/slack`,
|
|
1674
|
+
{
|
|
1675
|
+
method: "POST",
|
|
1676
|
+
headers: {
|
|
1677
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
1678
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
1679
|
+
"X-Mushi-Project": config.projectId
|
|
1680
|
+
},
|
|
1681
|
+
signal: AbortSignal.timeout(6e3)
|
|
1682
|
+
}
|
|
1683
|
+
);
|
|
1684
|
+
const slackBody = slackRes.ok ? await slackRes.json() : null;
|
|
1685
|
+
const slackOk = slackBody?.status === "ok";
|
|
1686
|
+
checks.push({
|
|
1687
|
+
name: "[qa] Slack notifications configured",
|
|
1688
|
+
ok: slackOk,
|
|
1689
|
+
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."
|
|
1690
|
+
});
|
|
1691
|
+
const fcRes = await doFetch(
|
|
1692
|
+
`${config.endpoint}/v1/admin/projects/${config.projectId}/integrations/probe/firecrawl`,
|
|
1693
|
+
{
|
|
1694
|
+
method: "POST",
|
|
1695
|
+
headers: {
|
|
1696
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
1697
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
1698
|
+
"X-Mushi-Project": config.projectId
|
|
1699
|
+
},
|
|
1700
|
+
signal: AbortSignal.timeout(6e3)
|
|
1701
|
+
}
|
|
1702
|
+
);
|
|
1703
|
+
const fcBody = fcRes.ok ? await fcRes.json() : null;
|
|
1704
|
+
const hasFirecrawlStories = enabled.some(
|
|
1705
|
+
(s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
|
|
1706
|
+
);
|
|
1707
|
+
if (hasFirecrawlStories) {
|
|
1708
|
+
const fcOk = fcBody?.status === "ok";
|
|
1709
|
+
checks.push({
|
|
1710
|
+
name: "[qa] Firecrawl API key configured",
|
|
1711
|
+
ok: fcOk,
|
|
1712
|
+
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."
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1717
|
+
checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
|
|
1718
|
+
}
|
|
1719
|
+
return checks;
|
|
1720
|
+
}
|
|
1522
1721
|
async function runDoctor(config, options = {}) {
|
|
1523
1722
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
1524
1723
|
const checks = [];
|
|
@@ -1532,6 +1731,14 @@ async function runDoctor(config, options = {}) {
|
|
|
1532
1731
|
const serverChecks = await checkServerPreflight(config, doFetch);
|
|
1533
1732
|
checks.push(...serverChecks);
|
|
1534
1733
|
}
|
|
1734
|
+
if (options.ingest) {
|
|
1735
|
+
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
1736
|
+
checks.push(...ingestChecks);
|
|
1737
|
+
}
|
|
1738
|
+
if (options.qaStories) {
|
|
1739
|
+
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
1740
|
+
checks.push(...qaChecks);
|
|
1741
|
+
}
|
|
1535
1742
|
return { checks, ready: checks.every((c) => c.ok) };
|
|
1536
1743
|
}
|
|
1537
1744
|
function formatDoctorResult(result) {
|
|
@@ -1540,7 +1747,7 @@ function formatDoctorResult(result) {
|
|
|
1540
1747
|
const lines = [];
|
|
1541
1748
|
for (const c of result.checks) {
|
|
1542
1749
|
lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
|
|
1543
|
-
lines.push(` ${c.detail}`);
|
|
1750
|
+
if (c.detail) lines.push(` ${c.detail}`);
|
|
1544
1751
|
}
|
|
1545
1752
|
const failed = result.checks.filter((c) => !c.ok);
|
|
1546
1753
|
if (failed.length === 0) {
|
|
@@ -1553,6 +1760,237 @@ ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
|
1553
1760
|
return lines.join("\n");
|
|
1554
1761
|
}
|
|
1555
1762
|
|
|
1763
|
+
// src/upgrade.ts
|
|
1764
|
+
import { resolve as resolve2 } from "path";
|
|
1765
|
+
import { execSync } from "child_process";
|
|
1766
|
+
var SAFE_NPM_VERSION = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
1767
|
+
var MUSHI_PACKAGES = [
|
|
1768
|
+
"@mushi-mushi/web",
|
|
1769
|
+
"@mushi-mushi/core",
|
|
1770
|
+
"@mushi-mushi/react",
|
|
1771
|
+
"@mushi-mushi/react-native",
|
|
1772
|
+
"@mushi-mushi/capacitor",
|
|
1773
|
+
"@mushi-mushi/node",
|
|
1774
|
+
"@mushi-mushi/cli"
|
|
1775
|
+
];
|
|
1776
|
+
async function planUpgrade(cwd) {
|
|
1777
|
+
const root = resolve2(cwd);
|
|
1778
|
+
const pkg = readPackageJson(root);
|
|
1779
|
+
const pm = detectPackageManager(root);
|
|
1780
|
+
const deps = {
|
|
1781
|
+
...pkg?.dependencies ?? {},
|
|
1782
|
+
...pkg?.devDependencies ?? {}
|
|
1783
|
+
};
|
|
1784
|
+
const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
|
|
1785
|
+
const entries = await Promise.all(
|
|
1786
|
+
installed2.map(async (name) => {
|
|
1787
|
+
const current = deps[name] ?? "";
|
|
1788
|
+
const currentCore = current.replace(/^[\^~>=<]*/, "");
|
|
1789
|
+
if (!/^\d/.test(currentCore)) {
|
|
1790
|
+
return { name, current, latest: null, willUpgrade: false };
|
|
1791
|
+
}
|
|
1792
|
+
const freshness = await checkFreshness(name, currentCore, {
|
|
1793
|
+
timeoutMs: 4e3,
|
|
1794
|
+
ignoreOptOut: true
|
|
1795
|
+
});
|
|
1796
|
+
const latest = freshness?.latest ?? null;
|
|
1797
|
+
const safeLatest = latest && SAFE_NPM_VERSION.test(latest) ? latest : null;
|
|
1798
|
+
const willUpgrade = Boolean(freshness?.isOutdated && safeLatest);
|
|
1799
|
+
return {
|
|
1800
|
+
name,
|
|
1801
|
+
current,
|
|
1802
|
+
latest: safeLatest,
|
|
1803
|
+
willUpgrade,
|
|
1804
|
+
migrateToWeb: name === "@mushi-mushi/react" && willUpgrade
|
|
1805
|
+
};
|
|
1806
|
+
})
|
|
1807
|
+
);
|
|
1808
|
+
const toBump = entries.filter((e) => e.willUpgrade && e.latest).map((e) => `${e.name}@${e.latest}`);
|
|
1809
|
+
return {
|
|
1810
|
+
cwd: root,
|
|
1811
|
+
packageManager: pm,
|
|
1812
|
+
entries,
|
|
1813
|
+
installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
async function runUpgrade(opts = {}) {
|
|
1817
|
+
const plan = await planUpgrade(opts.cwd ?? process.cwd());
|
|
1818
|
+
if (plan.entries.length === 0) {
|
|
1819
|
+
return {
|
|
1820
|
+
plan,
|
|
1821
|
+
upgraded: false,
|
|
1822
|
+
message: "No @mushi-mushi/* packages in package.json \u2014 run `mushi init` first."
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
if (!plan.installCmd) {
|
|
1826
|
+
const semverEntries = plan.entries.filter((e) => /\d/.test(e.current));
|
|
1827
|
+
const allChecksFailed = semverEntries.length > 0 && semverEntries.every((e) => e.latest === null);
|
|
1828
|
+
return {
|
|
1829
|
+
plan,
|
|
1830
|
+
upgraded: false,
|
|
1831
|
+
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."
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
if (opts.dryRun) {
|
|
1835
|
+
return { plan, upgraded: false, message: `[dry-run] Would run: ${plan.installCmd}` };
|
|
1836
|
+
}
|
|
1837
|
+
try {
|
|
1838
|
+
execSync(plan.installCmd, { cwd: plan.cwd, stdio: "inherit", env: process.env });
|
|
1839
|
+
} catch {
|
|
1840
|
+
return {
|
|
1841
|
+
plan,
|
|
1842
|
+
upgraded: false,
|
|
1843
|
+
message: `Install command failed \u2014 fix the error above or run it manually:
|
|
1844
|
+
${plan.installCmd}`
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const reactEntry = plan.entries.find((e) => e.migrateToWeb);
|
|
1848
|
+
const migrateNote = reactEntry ? "\nNote: @mushi-mushi/react is legacy \u2014 prefer @mushi-mushi/web for Vite/Capacitor/SPA apps." : "";
|
|
1849
|
+
return {
|
|
1850
|
+
plan,
|
|
1851
|
+
upgraded: true,
|
|
1852
|
+
message: `Upgraded Mushi SDK packages.
|
|
1853
|
+
${plan.installCmd}${migrateNote}`
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/connect.ts
|
|
1858
|
+
import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
1859
|
+
import { join as join6, resolve as resolve3 } from "path";
|
|
1860
|
+
function envKeyPresent(content, key) {
|
|
1861
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1862
|
+
return new RegExp(`^${escaped}=`, "m").test(content);
|
|
1863
|
+
}
|
|
1864
|
+
async function mergeEnvFile(path, lines) {
|
|
1865
|
+
const block = `
|
|
1866
|
+
# Mushi \u2014 added by mushi connect
|
|
1867
|
+
${lines.join("\n")}
|
|
1868
|
+
`;
|
|
1869
|
+
let existing = null;
|
|
1870
|
+
try {
|
|
1871
|
+
existing = await readFile2(path, "utf8");
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
if (existing !== null) {
|
|
1875
|
+
const needs = lines.filter((line) => {
|
|
1876
|
+
const key = line.split("=")[0];
|
|
1877
|
+
return !envKeyPresent(existing, key);
|
|
1878
|
+
});
|
|
1879
|
+
if (needs.length === 0) return false;
|
|
1880
|
+
await appendFile(path, `
|
|
1881
|
+
# Mushi \u2014 added by mushi connect
|
|
1882
|
+
${needs.join("\n")}
|
|
1883
|
+
`, "utf8");
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
await writeFile(path, block, "utf8");
|
|
1887
|
+
return true;
|
|
1888
|
+
}
|
|
1889
|
+
async function ensureMcpJsonGitignored(cwd, messages) {
|
|
1890
|
+
const gitignorePath = join6(cwd, ".gitignore");
|
|
1891
|
+
const patterns = [".cursor/mcp.json", ".cursor/"];
|
|
1892
|
+
let content = null;
|
|
1893
|
+
try {
|
|
1894
|
+
content = await readFile2(gitignorePath, "utf8");
|
|
1895
|
+
} catch {
|
|
1896
|
+
}
|
|
1897
|
+
if (content === null) {
|
|
1898
|
+
messages.push(
|
|
1899
|
+
"\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
|
|
1900
|
+
);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
const covered = patterns.some((p3) => content.split("\n").some((line) => line.trim() === p3 || line.trim() === `${p3}/`));
|
|
1904
|
+
if (covered) return;
|
|
1905
|
+
await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
|
|
1906
|
+
messages.push("\u2713 Added .cursor/mcp.json to .gitignore (contains API key)");
|
|
1907
|
+
}
|
|
1908
|
+
async function runConnect(opts, baseConfig = {}) {
|
|
1909
|
+
const cwd = resolve3(opts.cwd ?? process.cwd());
|
|
1910
|
+
const endpoint = assertEndpoint(opts.endpoint);
|
|
1911
|
+
const messages = [];
|
|
1912
|
+
const config = {
|
|
1913
|
+
...baseConfig,
|
|
1914
|
+
apiKey: opts.apiKey,
|
|
1915
|
+
projectId: opts.projectId,
|
|
1916
|
+
endpoint
|
|
1917
|
+
};
|
|
1918
|
+
saveConfig(config);
|
|
1919
|
+
messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
|
|
1920
|
+
let envPath = null;
|
|
1921
|
+
if (opts.writeEnv !== false) {
|
|
1922
|
+
const pkg = readPackageJson(cwd);
|
|
1923
|
+
const framework = detectFramework(cwd, pkg);
|
|
1924
|
+
const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
|
|
1925
|
+
envPath = join6(cwd, ".env.local");
|
|
1926
|
+
const wrote = await mergeEnvFile(envPath, lines);
|
|
1927
|
+
messages.push(
|
|
1928
|
+
wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
let mcpPath = null;
|
|
1932
|
+
if (opts.wireIde !== false) {
|
|
1933
|
+
const mcpDir = join6(cwd, ".cursor");
|
|
1934
|
+
mcpPath = join6(mcpDir, "mcp.json");
|
|
1935
|
+
const serverName = `mushi-${opts.projectId.slice(0, 8)}`;
|
|
1936
|
+
const mcpServerBlock = {
|
|
1937
|
+
command: "npx",
|
|
1938
|
+
args: ["-y", "@mushi-mushi/mcp@latest"],
|
|
1939
|
+
env: {
|
|
1940
|
+
MUSHI_API_ENDPOINT: endpoint,
|
|
1941
|
+
MUSHI_PROJECT_ID: opts.projectId,
|
|
1942
|
+
MUSHI_API_KEY: opts.apiKey
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
let merged = { mcpServers: {} };
|
|
1946
|
+
try {
|
|
1947
|
+
merged = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
1948
|
+
} catch {
|
|
1949
|
+
}
|
|
1950
|
+
const servers = merged.mcpServers ?? {};
|
|
1951
|
+
servers[serverName] = mcpServerBlock;
|
|
1952
|
+
merged.mcpServers = servers;
|
|
1953
|
+
await mkdir(mcpDir, { recursive: true });
|
|
1954
|
+
await writeFile(mcpPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
1955
|
+
await ensureMcpJsonGitignored(cwd, messages);
|
|
1956
|
+
messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
|
|
1957
|
+
}
|
|
1958
|
+
let heartbeat = null;
|
|
1959
|
+
if (opts.wait) {
|
|
1960
|
+
const timeoutSec = opts.waitTimeoutSec ?? 120;
|
|
1961
|
+
const maxAttempts = Math.max(1, Math.ceil(timeoutSec * 1e3 / 3e3));
|
|
1962
|
+
messages.push(`\u2026 Waiting for SDK heartbeat (up to ${timeoutSec}s) \u2014 start your dev server with the snippet installed`);
|
|
1963
|
+
heartbeat = await waitForIngestReady({
|
|
1964
|
+
endpoint,
|
|
1965
|
+
apiKey: opts.apiKey,
|
|
1966
|
+
projectId: opts.projectId,
|
|
1967
|
+
maxAttempts,
|
|
1968
|
+
onPoll: (payload, attempt) => {
|
|
1969
|
+
if (!opts.json && attempt % 3 === 0) {
|
|
1970
|
+
const sdk = payload.steps.find((s) => s.id === "sdk_installed");
|
|
1971
|
+
const seen = payload.diagnostic?.last_sdk_seen_at ?? "never";
|
|
1972
|
+
process.stdout.write(` poll ${attempt}: sdk_installed=${sdk?.complete ? "yes" : "no"} last_seen=${seen}
|
|
1973
|
+
`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
if (heartbeat.ok) {
|
|
1978
|
+
const label = heartbeat.reason === "heartbeat" ? "SDK heartbeat detected" : "Ingest setup complete";
|
|
1979
|
+
messages.push(`\u2713 ${label} \u2014 ingest pipeline is live`);
|
|
1980
|
+
} else if (heartbeat.reason === "unauthorized") {
|
|
1981
|
+
messages.push(
|
|
1982
|
+
`\u2717 The backend rejected these credentials (${heartbeat.error ?? "auth error"}). Double-check --api-key, --project-id, and --endpoint, then re-run \`mushi connect --wait\`.`
|
|
1983
|
+
);
|
|
1984
|
+
} else {
|
|
1985
|
+
messages.push(
|
|
1986
|
+
`\u2717 No heartbeat before timeout (${heartbeat.reason}). Confirm env vars are in your build, restart the dev server, then re-run \`mushi connect --wait\`.`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const ok = !opts.wait || Boolean(heartbeat?.ok);
|
|
1991
|
+
return { ok, envPath, mcpPath, heartbeat, messages };
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1556
1994
|
// src/index.ts
|
|
1557
1995
|
installSignalHandlers();
|
|
1558
1996
|
var API_TIMEOUT_MS = 15e3;
|
|
@@ -1799,7 +2237,7 @@ Examples:
|
|
|
1799
2237
|
mushi config endpoint https://... # set endpoint
|
|
1800
2238
|
mushi config projectId <uuid> # set project`).action((key, value) => {
|
|
1801
2239
|
const config = loadConfig();
|
|
1802
|
-
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
|
|
2240
|
+
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId", "consoleUrl"]);
|
|
1803
2241
|
if (key && value) {
|
|
1804
2242
|
if (!ALLOWED_KEYS.has(key)) {
|
|
1805
2243
|
process.stderr.write(`error: unknown config key "${key}". Allowed: ${[...ALLOWED_KEYS].join(", ")}
|
|
@@ -2016,6 +2454,31 @@ reports.command("dismiss <id>").description("Dismiss a report (not a real bug /
|
|
|
2016
2454
|
console.log(`\u2713 Dismissed report ${id}`);
|
|
2017
2455
|
}
|
|
2018
2456
|
});
|
|
2457
|
+
reports.command("reply <id> <message>").description("Send a visible reply to the reporter widget for a report").option("--author <name>", 'Display name for the sender (default: "Mushi Admin")').option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
2458
|
+
Examples:
|
|
2459
|
+
mushi reports reply abc123 "Thanks for reporting \u2014 fixing this in the next release."
|
|
2460
|
+
mushi reports reply abc123 "Can you share a screenshot?" --author "Alice"`).action(async (id, message, opts) => {
|
|
2461
|
+
const config = requireConfig();
|
|
2462
|
+
const body = { message };
|
|
2463
|
+
if (opts.author) body["author_name"] = opts.author;
|
|
2464
|
+
const result = await apiCall(`/v1/sync/reports/${id}/reply`, config, {
|
|
2465
|
+
method: "POST",
|
|
2466
|
+
body: JSON.stringify(body)
|
|
2467
|
+
});
|
|
2468
|
+
if (!result.ok) {
|
|
2469
|
+
if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
|
|
2470
|
+
process.stderr.write(`error: report "${id}" not found
|
|
2471
|
+
`);
|
|
2472
|
+
process.exit(3);
|
|
2473
|
+
}
|
|
2474
|
+
die(result);
|
|
2475
|
+
}
|
|
2476
|
+
if (opts.json) {
|
|
2477
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
2478
|
+
} else {
|
|
2479
|
+
console.log(`\u2713 Reply sent to reporter for report ${id}`);
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2019
2482
|
reports.command("search <query>").description("Search reports by keyword in summary and description").option("--limit <n>", "Max results (1\u201350)", "10").option("--status <status>", "Filter by status").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
2020
2483
|
Examples:
|
|
2021
2484
|
mushi reports search "login button"
|
|
@@ -2117,7 +2580,7 @@ AI code review context.
|
|
|
2117
2580
|
Typical CI usage:
|
|
2118
2581
|
MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
|
|
2119
2582
|
npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
|
|
2120
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
2583
|
+
const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
|
|
2121
2584
|
const nodePath = await import("path");
|
|
2122
2585
|
const config = requireConfig();
|
|
2123
2586
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -2144,8 +2607,8 @@ Typical CI usage:
|
|
|
2144
2607
|
console.log(JSON.stringify(output, null, 2));
|
|
2145
2608
|
return;
|
|
2146
2609
|
}
|
|
2147
|
-
await
|
|
2148
|
-
await
|
|
2610
|
+
await mkdir2(nodePath.dirname(target), { recursive: true });
|
|
2611
|
+
await writeFile2(target, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
2149
2612
|
if (opts.json) {
|
|
2150
2613
|
console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
|
|
2151
2614
|
} else {
|
|
@@ -2194,7 +2657,7 @@ Examples:
|
|
|
2194
2657
|
mushi index ./src
|
|
2195
2658
|
mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
|
|
2196
2659
|
const config = requireConfig({ needsProject: true });
|
|
2197
|
-
const { readdir: readdir2, readFile:
|
|
2660
|
+
const { readdir: readdir2, readFile: readFile3, stat } = await import("fs/promises");
|
|
2198
2661
|
const nodePath = await import("path");
|
|
2199
2662
|
const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
|
|
2200
2663
|
const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
@@ -2221,7 +2684,7 @@ Examples:
|
|
|
2221
2684
|
`);
|
|
2222
2685
|
continue;
|
|
2223
2686
|
}
|
|
2224
|
-
const source = await
|
|
2687
|
+
const source = await readFile3(file, "utf8");
|
|
2225
2688
|
const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
|
|
2226
2689
|
count++;
|
|
2227
2690
|
bytes += source.length;
|
|
@@ -2278,7 +2741,7 @@ Typical first-time flow:
|
|
|
2278
2741
|
# Browser opens \u2192 sign up / magic-link \u2192 come back to terminal
|
|
2279
2742
|
# CLI writes .env.local and .cursor/mcp.json
|
|
2280
2743
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
2281
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
2744
|
+
const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
|
|
2282
2745
|
const { existsSync: existsSync5 } = await import("fs");
|
|
2283
2746
|
const nodePath = await import("path");
|
|
2284
2747
|
const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
|
|
@@ -2304,7 +2767,7 @@ Typical first-time flow:
|
|
|
2304
2767
|
console.log("");
|
|
2305
2768
|
const { createInterface } = await import("readline");
|
|
2306
2769
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2307
|
-
const ask = (q) => new Promise((
|
|
2770
|
+
const ask = (q) => new Promise((resolve4) => rl.question(q, (a) => resolve4(a.trim())));
|
|
2308
2771
|
const projectId = await ask(" Project ID (uuid): ");
|
|
2309
2772
|
const apiKey = await ask(" API key (mushi_...): ");
|
|
2310
2773
|
rl.close();
|
|
@@ -2327,17 +2790,17 @@ Typical first-time flow:
|
|
|
2327
2790
|
""
|
|
2328
2791
|
];
|
|
2329
2792
|
const envExisting = existsSync5(envPath);
|
|
2330
|
-
await
|
|
2793
|
+
await writeFile2(envPath, envLines.join("\n"), "utf8");
|
|
2331
2794
|
console.log(`
|
|
2332
2795
|
\u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
|
|
2333
2796
|
const mcpDir = nodePath.join(cwd, ".cursor");
|
|
2334
|
-
await
|
|
2797
|
+
await mkdir2(mcpDir, { recursive: true });
|
|
2335
2798
|
const mcpPath = nodePath.join(mcpDir, "mcp.json");
|
|
2336
2799
|
const mcpJson = {
|
|
2337
2800
|
mcpServers: {
|
|
2338
2801
|
mushi: {
|
|
2339
2802
|
command: "npx",
|
|
2340
|
-
args: ["-y", "mushi-mcp@latest"],
|
|
2803
|
+
args: ["-y", "@mushi-mushi/mcp@latest"],
|
|
2341
2804
|
env: {
|
|
2342
2805
|
MUSHI_API_ENDPOINT: endpoint,
|
|
2343
2806
|
MUSHI_PROJECT_ID: projectId,
|
|
@@ -2349,16 +2812,16 @@ Typical first-time flow:
|
|
|
2349
2812
|
const mcpExisting = existsSync5(mcpPath);
|
|
2350
2813
|
if (mcpExisting) {
|
|
2351
2814
|
try {
|
|
2352
|
-
const { readFile:
|
|
2353
|
-
const raw = JSON.parse(await
|
|
2815
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
2816
|
+
const raw = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
2354
2817
|
const existing = raw;
|
|
2355
2818
|
existing.mcpServers = { ...existing.mcpServers ?? {}, mushi: mcpJson.mcpServers.mushi };
|
|
2356
|
-
await
|
|
2819
|
+
await writeFile2(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
2357
2820
|
} catch {
|
|
2358
|
-
await
|
|
2821
|
+
await writeFile2(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
|
|
2359
2822
|
}
|
|
2360
2823
|
} else {
|
|
2361
|
-
await
|
|
2824
|
+
await writeFile2(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
|
|
2362
2825
|
}
|
|
2363
2826
|
console.log(` \u2713 ${mcpExisting ? "Updated" : "Created"} .cursor/mcp.json`);
|
|
2364
2827
|
console.log("");
|
|
@@ -2379,7 +2842,7 @@ Supported IDEs:
|
|
|
2379
2842
|
zed \u2014 writes ~/.config/zed/settings.json mcpServers block
|
|
2380
2843
|
|
|
2381
2844
|
The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
|
|
2382
|
-
const { writeFile, mkdir, readFile:
|
|
2845
|
+
const { writeFile: writeFile2, mkdir: mkdir2, readFile: readFile3 } = await import("fs/promises");
|
|
2383
2846
|
const { existsSync: existsSync5 } = await import("fs");
|
|
2384
2847
|
const nodePath = await import("path");
|
|
2385
2848
|
const os = await import("os");
|
|
@@ -2401,7 +2864,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2401
2864
|
const serverName = `mushi-${slug}`;
|
|
2402
2865
|
const mcpServerBlock = {
|
|
2403
2866
|
command: "npx",
|
|
2404
|
-
args: ["-y", "mushi-mcp@latest"],
|
|
2867
|
+
args: ["-y", "@mushi-mushi/mcp@latest"],
|
|
2405
2868
|
env: {
|
|
2406
2869
|
MUSHI_API_ENDPOINT: config.endpoint,
|
|
2407
2870
|
MUSHI_PROJECT_ID: config.projectId ?? "",
|
|
@@ -2414,7 +2877,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2414
2877
|
let merged = { mcpServers: {} };
|
|
2415
2878
|
if (existsSync5(configPath)) {
|
|
2416
2879
|
try {
|
|
2417
|
-
const raw = await
|
|
2880
|
+
const raw = await readFile3(configPath, "utf8");
|
|
2418
2881
|
merged = JSON.parse(raw);
|
|
2419
2882
|
} catch {
|
|
2420
2883
|
}
|
|
@@ -2427,15 +2890,15 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2427
2890
|
console.log(`[dry-run] Would write ${configPath}:`);
|
|
2428
2891
|
console.log(output);
|
|
2429
2892
|
} else {
|
|
2430
|
-
await
|
|
2431
|
-
await
|
|
2893
|
+
await mkdir2(configDir, { recursive: true });
|
|
2894
|
+
await writeFile2(configPath, output, "utf8");
|
|
2432
2895
|
console.log(`\u2713 Written ${configPath}`);
|
|
2433
2896
|
}
|
|
2434
2897
|
} else if (ideEntry.format === "zed") {
|
|
2435
2898
|
let settings = {};
|
|
2436
2899
|
if (existsSync5(configPath)) {
|
|
2437
2900
|
try {
|
|
2438
|
-
const raw = await
|
|
2901
|
+
const raw = await readFile3(configPath, "utf8");
|
|
2439
2902
|
settings = JSON.parse(raw);
|
|
2440
2903
|
} catch {
|
|
2441
2904
|
}
|
|
@@ -2444,7 +2907,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2444
2907
|
servers[serverName] = {
|
|
2445
2908
|
command: {
|
|
2446
2909
|
path: "npx",
|
|
2447
|
-
args: ["-y", "mushi-mcp@latest"],
|
|
2910
|
+
args: ["-y", "@mushi-mushi/mcp@latest"],
|
|
2448
2911
|
env: {
|
|
2449
2912
|
MUSHI_API_ENDPOINT: config.endpoint,
|
|
2450
2913
|
MUSHI_PROJECT_ID: config.projectId ?? "",
|
|
@@ -2459,8 +2922,8 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2459
2922
|
console.log(`[dry-run] Would write ${configPath}:`);
|
|
2460
2923
|
console.log(output);
|
|
2461
2924
|
} else {
|
|
2462
|
-
await
|
|
2463
|
-
await
|
|
2925
|
+
await mkdir2(configDir, { recursive: true });
|
|
2926
|
+
await writeFile2(configPath, output, "utf8");
|
|
2464
2927
|
console.log(`\u2713 Written ${configPath}`);
|
|
2465
2928
|
}
|
|
2466
2929
|
}
|
|
@@ -2494,7 +2957,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2494
2957
|
if (opts.dryRun) {
|
|
2495
2958
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2496
2959
|
} else {
|
|
2497
|
-
await
|
|
2960
|
+
await writeFile2(rulesPath, rulesContent, "utf8");
|
|
2498
2961
|
console.log(`\u2713 Written .cursorrules`);
|
|
2499
2962
|
}
|
|
2500
2963
|
} else if (opts.ide === "claude") {
|
|
@@ -2503,8 +2966,8 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2503
2966
|
if (opts.dryRun) {
|
|
2504
2967
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2505
2968
|
} else {
|
|
2506
|
-
await
|
|
2507
|
-
await
|
|
2969
|
+
await mkdir2(rulesDir, { recursive: true });
|
|
2970
|
+
await writeFile2(rulesPath, rulesContent, "utf8");
|
|
2508
2971
|
console.log(`\u2713 Written .claude/rules/mushi.md`);
|
|
2509
2972
|
}
|
|
2510
2973
|
}
|
|
@@ -2558,6 +3021,7 @@ Examples:
|
|
|
2558
3021
|
emitEvent("dispatch.start", { reportId, agent: opts.agent, model: opts.model ?? null });
|
|
2559
3022
|
const body = {
|
|
2560
3023
|
reportId,
|
|
3024
|
+
projectId: cfg.projectId,
|
|
2561
3025
|
agent: opts.agent
|
|
2562
3026
|
};
|
|
2563
3027
|
if (opts.agent === "cursor_cloud") {
|
|
@@ -2638,14 +3102,67 @@ program.command("nudge").description(
|
|
|
2638
3102
|
}
|
|
2639
3103
|
console.log(renderNudgeSnippet({ phase, overrides }));
|
|
2640
3104
|
});
|
|
3105
|
+
program.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", `
|
|
3106
|
+
Examples:
|
|
3107
|
+
mushi upgrade
|
|
3108
|
+
mushi upgrade --dry-run
|
|
3109
|
+
mushi upgrade --cwd ../glot.it`).action(async (opts) => {
|
|
3110
|
+
const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
|
|
3111
|
+
if (opts.json) {
|
|
3112
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3113
|
+
} else {
|
|
3114
|
+
console.log(result.message);
|
|
3115
|
+
for (const e of result.plan.entries) {
|
|
3116
|
+
const tag = e.willUpgrade && e.latest ? `\u2192 v${e.latest}` : "(current)";
|
|
3117
|
+
console.log(` ${e.name}@${e.current} ${tag}`);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
if (!result.upgraded && result.plan.entries.some((e) => e.willUpgrade) && !opts.dryRun) {
|
|
3121
|
+
process.exit(1);
|
|
3122
|
+
}
|
|
3123
|
+
if (result.plan.entries.length === 0) process.exit(1);
|
|
3124
|
+
});
|
|
3125
|
+
program.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("--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", `
|
|
3126
|
+
Examples:
|
|
3127
|
+
MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
|
|
3128
|
+
mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
|
|
3129
|
+
const apiKey = process.env.MUSHI_API_KEY ?? opts.apiKey;
|
|
3130
|
+
if (!apiKey) {
|
|
3131
|
+
console.error("Provide the API key via the MUSHI_API_KEY env var (recommended) or --api-key <key>.");
|
|
3132
|
+
process.exit(1);
|
|
3133
|
+
}
|
|
3134
|
+
const result = await runConnect({
|
|
3135
|
+
apiKey,
|
|
3136
|
+
projectId: opts.projectId,
|
|
3137
|
+
endpoint: opts.endpoint,
|
|
3138
|
+
cwd: opts.cwd,
|
|
3139
|
+
writeEnv: opts.env !== false,
|
|
3140
|
+
wireIde: opts.ide !== false,
|
|
3141
|
+
wait: opts.wait,
|
|
3142
|
+
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
3143
|
+
json: opts.json
|
|
3144
|
+
});
|
|
3145
|
+
if (opts.json) {
|
|
3146
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3147
|
+
} else {
|
|
3148
|
+
for (const line of result.messages) console.log(line);
|
|
3149
|
+
}
|
|
3150
|
+
if (!result.ok) process.exit(1);
|
|
3151
|
+
});
|
|
2641
3152
|
program.command("doctor").description(
|
|
2642
3153
|
"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."
|
|
2643
3154
|
).option("--cwd <path>", "Run package detection from a different directory").option("--json", "Machine-readable output").option(
|
|
2644
3155
|
"--server",
|
|
2645
3156
|
"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."
|
|
3157
|
+
).option(
|
|
3158
|
+
"--ingest",
|
|
3159
|
+
"Also call GET /v1/sync/ingest-setup for the 4 required ingest steps (API key, SDK heartbeat, first report). Composable with --server."
|
|
3160
|
+
).option(
|
|
3161
|
+
"--qa-stories",
|
|
3162
|
+
"Check enabled QA stories for common setup issues: missing Firecrawl key, missing target URL, Slack not connected. Requires --server credentials."
|
|
2646
3163
|
).action(async (opts) => {
|
|
2647
3164
|
const config = loadConfig();
|
|
2648
|
-
const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server });
|
|
3165
|
+
const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories });
|
|
2649
3166
|
const { checks } = result;
|
|
2650
3167
|
if (opts.json) {
|
|
2651
3168
|
console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
|
|
@@ -2957,4 +3474,374 @@ keys.command("add").description("Add a new API key to the pool").requiredOption(
|
|
|
2957
3474
|
}
|
|
2958
3475
|
console.log(`\u2713 Key added \u2014 id: ${res.data.id}`);
|
|
2959
3476
|
});
|
|
2960
|
-
program.
|
|
3477
|
+
var integrations = program.command("integrations").description("Manage service integrations");
|
|
3478
|
+
integrations.command("list").description("List all configured integrations and their current health status").option("--json", "Machine-readable output").action(async (opts) => {
|
|
3479
|
+
const config = loadConfig();
|
|
3480
|
+
if (!config.apiKey) {
|
|
3481
|
+
console.error("Run `mushi login` first");
|
|
3482
|
+
process.exit(2);
|
|
3483
|
+
}
|
|
3484
|
+
if (!config.projectId) {
|
|
3485
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3486
|
+
process.exit(2);
|
|
3487
|
+
}
|
|
3488
|
+
const result = await apiCall(
|
|
3489
|
+
`/v1/admin/projects/${config.projectId}/integrations`,
|
|
3490
|
+
config
|
|
3491
|
+
);
|
|
3492
|
+
const rawResult = result;
|
|
3493
|
+
if (!rawResult.integrations && !result.ok) {
|
|
3494
|
+
console.error("Failed:", result.error);
|
|
3495
|
+
process.exit(1);
|
|
3496
|
+
}
|
|
3497
|
+
if (opts.json) {
|
|
3498
|
+
console.log(JSON.stringify(rawResult, null, 2));
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
const rows = rawResult.integrations ?? [];
|
|
3502
|
+
if (rows.length === 0) {
|
|
3503
|
+
console.log("No integrations configured. Visit the Integrations page to connect services.");
|
|
3504
|
+
return;
|
|
3505
|
+
}
|
|
3506
|
+
const icons = {
|
|
3507
|
+
slack: "\u{1F514}",
|
|
3508
|
+
github: "\u{1F419}",
|
|
3509
|
+
sentry: "\u{1FAB2}",
|
|
3510
|
+
langfuse: "\u{1F52D}",
|
|
3511
|
+
discord: "\u{1F4AC}",
|
|
3512
|
+
linear: "\u{1F4D0}",
|
|
3513
|
+
jira: "\u{1F5C2}\uFE0F",
|
|
3514
|
+
cursor_cloud: "\u{1F5B1}\uFE0F",
|
|
3515
|
+
claude_code_agent: "\u{1F916}"
|
|
3516
|
+
};
|
|
3517
|
+
console.log("\nIntegrations:\n");
|
|
3518
|
+
for (const row of rows) {
|
|
3519
|
+
const icon = icons[row.kind] ?? "\u{1F50C}";
|
|
3520
|
+
const statusIcon = row.status === "ok" ? "\u2705" : row.status === "error" ? "\u274C" : "\u26AA";
|
|
3521
|
+
console.log(` ${icon} ${row.kind.padEnd(20)} ${statusIcon} ${row.detail ?? ""}`);
|
|
3522
|
+
}
|
|
3523
|
+
console.log();
|
|
3524
|
+
});
|
|
3525
|
+
integrations.command("test <kind>").description(
|
|
3526
|
+
"Run a health probe for a specific integration (e.g. slack, sentry, github, langfuse, discord, cursor_cloud, claude_code_agent)"
|
|
3527
|
+
).option("--json", "Machine-readable output").action(async (kind, opts) => {
|
|
3528
|
+
const config = loadConfig();
|
|
3529
|
+
if (!config.apiKey) {
|
|
3530
|
+
console.error("Run `mushi login` first");
|
|
3531
|
+
process.exit(2);
|
|
3532
|
+
}
|
|
3533
|
+
if (!config.projectId) {
|
|
3534
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3535
|
+
process.exit(2);
|
|
3536
|
+
}
|
|
3537
|
+
const result = await apiCall(
|
|
3538
|
+
`/v1/admin/projects/${config.projectId}/integrations/probe/${kind}`,
|
|
3539
|
+
config,
|
|
3540
|
+
{ method: "POST" }
|
|
3541
|
+
);
|
|
3542
|
+
if (!result.ok) {
|
|
3543
|
+
console.error("Request failed:", result.error);
|
|
3544
|
+
process.exit(1);
|
|
3545
|
+
}
|
|
3546
|
+
if (opts.json) {
|
|
3547
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
const probeOk = result.data?.status === "ok";
|
|
3551
|
+
console.log(
|
|
3552
|
+
probeOk ? `\u2705 ${kind} integration is healthy${result.data.detail ? ": " + result.data.detail : ""}` : `\u274C ${kind} integration check failed${result.data.detail ? ": " + result.data.detail : ""}`
|
|
3553
|
+
);
|
|
3554
|
+
if (!probeOk) process.exit(1);
|
|
3555
|
+
});
|
|
3556
|
+
var slack = program.command("slack").description("Slack integration commands");
|
|
3557
|
+
slack.command("status").description("Show whether Slack is connected and which channel receives notifications").option("--json", "Machine-readable output").action(async (opts) => {
|
|
3558
|
+
const config = loadConfig();
|
|
3559
|
+
if (!config.apiKey) {
|
|
3560
|
+
console.error("Run `mushi login` first");
|
|
3561
|
+
process.exit(2);
|
|
3562
|
+
}
|
|
3563
|
+
if (!config.projectId) {
|
|
3564
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3565
|
+
process.exit(2);
|
|
3566
|
+
}
|
|
3567
|
+
const result = await apiCall(
|
|
3568
|
+
`/v1/admin/projects/${config.projectId}/integrations/probe/slack`,
|
|
3569
|
+
config,
|
|
3570
|
+
{ method: "POST" }
|
|
3571
|
+
);
|
|
3572
|
+
if (!result.ok) {
|
|
3573
|
+
console.error("Request failed:", result.error);
|
|
3574
|
+
process.exit(1);
|
|
3575
|
+
}
|
|
3576
|
+
if (opts.json) {
|
|
3577
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
if (result.data?.status === "ok") {
|
|
3581
|
+
console.log("\u2705 Slack connected");
|
|
3582
|
+
if (result.data.detail) console.log(` ${result.data.detail}`);
|
|
3583
|
+
console.log("\n To change the channel or notification prefs, visit /integrations in the Mushi console.");
|
|
3584
|
+
} else {
|
|
3585
|
+
console.log("\u26AA Slack not connected");
|
|
3586
|
+
console.log(' Visit /integrations in the Mushi console and click "Add to Slack".');
|
|
3587
|
+
}
|
|
3588
|
+
});
|
|
3589
|
+
slack.command("test").description("Send a test Slack notification to confirm the current channel is working").option("--json", "Machine-readable output").action(async (opts) => {
|
|
3590
|
+
const config = loadConfig();
|
|
3591
|
+
if (!config.apiKey) {
|
|
3592
|
+
console.error("Run `mushi login` first");
|
|
3593
|
+
process.exit(2);
|
|
3594
|
+
}
|
|
3595
|
+
if (!config.projectId) {
|
|
3596
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3597
|
+
process.exit(2);
|
|
3598
|
+
}
|
|
3599
|
+
const result = await apiCall(
|
|
3600
|
+
`/v1/admin/projects/${config.projectId}/integrations/slack/test`,
|
|
3601
|
+
config,
|
|
3602
|
+
{ method: "POST" }
|
|
3603
|
+
);
|
|
3604
|
+
if (!result.ok) {
|
|
3605
|
+
console.error("Request failed:", result.error);
|
|
3606
|
+
process.exit(1);
|
|
3607
|
+
}
|
|
3608
|
+
if (opts.json) {
|
|
3609
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
if (result.data?.ok) {
|
|
3613
|
+
console.log("\u2705 Test message sent! Check your Slack channel.");
|
|
3614
|
+
} else {
|
|
3615
|
+
console.error("\u274C Test failed:", result.data?.error ?? "unknown error");
|
|
3616
|
+
process.exit(1);
|
|
3617
|
+
}
|
|
3618
|
+
});
|
|
3619
|
+
var qa = program.command("qa").description("QA story management");
|
|
3620
|
+
qa.command("stories").description("List QA stories for the current project").option("--json", "Machine-readable output").option("-n, --limit <n>", "Max stories to return (not applied server-side; all stories returned)", "20").action(async (opts) => {
|
|
3621
|
+
const config = loadConfig();
|
|
3622
|
+
if (!config.apiKey) {
|
|
3623
|
+
console.error("Run `mushi login` first");
|
|
3624
|
+
process.exit(2);
|
|
3625
|
+
}
|
|
3626
|
+
if (!config.projectId) {
|
|
3627
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3628
|
+
process.exit(2);
|
|
3629
|
+
}
|
|
3630
|
+
const result = await apiCall(
|
|
3631
|
+
`/v1/admin/projects/${config.projectId}/qa-coverage`,
|
|
3632
|
+
config
|
|
3633
|
+
);
|
|
3634
|
+
if (!result.ok) {
|
|
3635
|
+
console.error("Failed:", result.error);
|
|
3636
|
+
process.exit(1);
|
|
3637
|
+
}
|
|
3638
|
+
if (opts.json) {
|
|
3639
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
const stories2 = result.data?.coverage ?? [];
|
|
3643
|
+
if (stories2.length === 0) {
|
|
3644
|
+
console.log("No QA stories yet. Create one at /qa-coverage in the Mushi console.");
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
console.log(`
|
|
3648
|
+
QA Stories (${stories2.length}):
|
|
3649
|
+
`);
|
|
3650
|
+
for (const s of stories2) {
|
|
3651
|
+
const statusIcon = s.last_run_status === "passed" ? "\u2705" : s.last_run_status === "failed" ? "\u274C" : s.last_run_status === "error" ? "\u{1F6A8}" : "\u26AA";
|
|
3652
|
+
const enabled = s.enabled ? "" : " [disabled]";
|
|
3653
|
+
const sid = s.story_id ?? s.id ?? "\u2014";
|
|
3654
|
+
console.log(` ${statusIcon} ${s.name.slice(0, 50).padEnd(52)} ${sid}${enabled}`);
|
|
3655
|
+
}
|
|
3656
|
+
console.log(`
|
|
3657
|
+
Use 'mushi qa runs <storyId>' to see recent runs for a story.`);
|
|
3658
|
+
console.log();
|
|
3659
|
+
});
|
|
3660
|
+
qa.command("runs <storyId>").description("Show recent runs for a QA story, including error heads").option("--json", "Machine-readable output").option("-n, --limit <n>", "Max runs to return", "10").action(async (storyId, opts) => {
|
|
3661
|
+
const config = loadConfig();
|
|
3662
|
+
if (!config.apiKey) {
|
|
3663
|
+
console.error("Run `mushi login` first");
|
|
3664
|
+
process.exit(2);
|
|
3665
|
+
}
|
|
3666
|
+
if (!config.projectId) {
|
|
3667
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3668
|
+
process.exit(2);
|
|
3669
|
+
}
|
|
3670
|
+
const limit = parseInt(opts.limit ?? "10", 10);
|
|
3671
|
+
const result = await apiCall(
|
|
3672
|
+
`/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/runs?limit=${limit}`,
|
|
3673
|
+
config
|
|
3674
|
+
);
|
|
3675
|
+
if (!result.ok) {
|
|
3676
|
+
console.error("Failed:", result.error);
|
|
3677
|
+
process.exit(1);
|
|
3678
|
+
}
|
|
3679
|
+
if (opts.json) {
|
|
3680
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
const runs = result.data?.runs ?? [];
|
|
3684
|
+
if (runs.length === 0) {
|
|
3685
|
+
console.log("No runs yet for this story. Trigger one with `mushi qa run <storyId>`.");
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
console.log(`
|
|
3689
|
+
Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
3690
|
+
`);
|
|
3691
|
+
for (const r of runs) {
|
|
3692
|
+
const statusIcon = r.status === "passed" ? "\u2705" : r.status === "failed" ? "\u274C" : r.status === "error" ? "\u{1F6A8}" : "\u23F3";
|
|
3693
|
+
const ts = r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace("T", " ") : "\u2014";
|
|
3694
|
+
const latency = r.latency_ms ? ` (${(r.latency_ms / 1e3).toFixed(1)}s)` : "";
|
|
3695
|
+
console.log(` ${statusIcon} ${ts}${latency} ${r.id.slice(0, 8)}`);
|
|
3696
|
+
if (r.error_message) {
|
|
3697
|
+
console.log(` Error: ${r.error_message.slice(0, 120)}`);
|
|
3698
|
+
}
|
|
3699
|
+
if (r.assertion_failures?.length) {
|
|
3700
|
+
for (const af of r.assertion_failures.slice(0, 3)) {
|
|
3701
|
+
console.log(` \xB7 ${String(af).slice(0, 100)}`);
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
const consoleUrl = config.consoleUrl ?? "https://app.mushi.ai";
|
|
3706
|
+
console.log(`
|
|
3707
|
+
Open in console: ${consoleUrl}/qa-coverage?story=${storyId}`);
|
|
3708
|
+
console.log(` Tip: run 'mushi config consoleUrl http://localhost:6464' to set your local console URL`);
|
|
3709
|
+
console.log();
|
|
3710
|
+
});
|
|
3711
|
+
qa.command("run <storyId>").description("Manually trigger a QA story run (fire-and-forget; check results with `mushi qa runs <id>`)").option("--json", "Machine-readable output").action(async (storyId, opts) => {
|
|
3712
|
+
const config = loadConfig();
|
|
3713
|
+
if (!config.apiKey) {
|
|
3714
|
+
console.error("Run `mushi login` first");
|
|
3715
|
+
process.exit(2);
|
|
3716
|
+
}
|
|
3717
|
+
if (!config.projectId) {
|
|
3718
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3719
|
+
process.exit(2);
|
|
3720
|
+
}
|
|
3721
|
+
const result = await apiCall(
|
|
3722
|
+
`/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/run`,
|
|
3723
|
+
config,
|
|
3724
|
+
{ method: "POST" }
|
|
3725
|
+
);
|
|
3726
|
+
if (!result.ok) {
|
|
3727
|
+
console.error("Failed:", result.error);
|
|
3728
|
+
process.exit(1);
|
|
3729
|
+
}
|
|
3730
|
+
if (opts.json) {
|
|
3731
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
const runId = result.data?.run_id;
|
|
3735
|
+
if (runId) {
|
|
3736
|
+
console.log(`\u25B6 Run triggered: ${runId.slice(0, 8)}\u2026`);
|
|
3737
|
+
console.log(` Check results: mushi qa runs ${storyId}`);
|
|
3738
|
+
} else {
|
|
3739
|
+
console.error("\u274C Trigger failed: no run_id in response", JSON.stringify(result.data));
|
|
3740
|
+
process.exit(1);
|
|
3741
|
+
}
|
|
3742
|
+
});
|
|
3743
|
+
program.command("audit").description("Run a full-stack health audit for the current project").option("--json", "Machine-readable JSON output").option("--project-id <id>", "Project ID to audit (defaults to MUSHI_PROJECT_ID from config)").addHelpText("after", `
|
|
3744
|
+
Description:
|
|
3745
|
+
Fans out to the Mushi backend to run a full-stack health audit:
|
|
3746
|
+
\u2022 DB schema + Supabase advisors (requires Supabase PAT in API Keys)
|
|
3747
|
+
\u2022 Recent backend error logs
|
|
3748
|
+
\u2022 Tables without RLS enabled
|
|
3749
|
+
\u2022 Gate results: API contract (G3), spec drift (G6), orphan endpoints (G7),
|
|
3750
|
+
unknown frontend calls (G8), schema drift, status claim (G5)
|
|
3751
|
+
|
|
3752
|
+
Returns a PM-readable scorecard with severity-ranked findings.
|
|
3753
|
+
|
|
3754
|
+
Prerequisites:
|
|
3755
|
+
1. Configure your Supabase PAT: mushi settings set supabase-pat <token>
|
|
3756
|
+
2. Set supabase_project_ref in Admin \u2192 Settings \u2192 Project.
|
|
3757
|
+
|
|
3758
|
+
Examples:
|
|
3759
|
+
mushi audit
|
|
3760
|
+
mushi audit --json
|
|
3761
|
+
mushi audit --project-id abc123`).action(async (opts) => {
|
|
3762
|
+
const config = requireConfig();
|
|
3763
|
+
const projectId = opts.projectId ?? config.projectId;
|
|
3764
|
+
if (!projectId) {
|
|
3765
|
+
process.stderr.write("error: project ID required. Run `mushi login` or pass --project-id\n");
|
|
3766
|
+
process.exit(1);
|
|
3767
|
+
}
|
|
3768
|
+
const headers = {
|
|
3769
|
+
"Content-Type": "application/json",
|
|
3770
|
+
"X-Mushi-Project-Id": projectId
|
|
3771
|
+
};
|
|
3772
|
+
const jwt = config.jwt ?? null;
|
|
3773
|
+
const apiKey = config.apiKey ?? null;
|
|
3774
|
+
if (jwt) {
|
|
3775
|
+
headers["Authorization"] = `Bearer ${jwt}`;
|
|
3776
|
+
} else if (apiKey) {
|
|
3777
|
+
headers["X-Mushi-Api-Key"] = apiKey;
|
|
3778
|
+
} else {
|
|
3779
|
+
process.stderr.write("error: no credentials found. Run `mushi login` first.\n");
|
|
3780
|
+
process.exit(1);
|
|
3781
|
+
}
|
|
3782
|
+
if (!opts.json) process.stdout.write("Running full-stack audit\u2026 ");
|
|
3783
|
+
try {
|
|
3784
|
+
const controller = new AbortController();
|
|
3785
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
3786
|
+
const res = await fetch(
|
|
3787
|
+
`${config.endpoint}/v1/admin/projects/${projectId}/audit`,
|
|
3788
|
+
{ method: "POST", headers, body: "{}", signal: controller.signal }
|
|
3789
|
+
);
|
|
3790
|
+
clearTimeout(timer);
|
|
3791
|
+
const body = await res.json();
|
|
3792
|
+
if (!res.ok || !body.ok) {
|
|
3793
|
+
if (opts.json) {
|
|
3794
|
+
console.log(JSON.stringify(body));
|
|
3795
|
+
process.exit(1);
|
|
3796
|
+
}
|
|
3797
|
+
process.stdout.write("FAIL\n");
|
|
3798
|
+
process.stderr.write(`error: ${body.error?.message ?? `HTTP ${res.status}`}
|
|
3799
|
+
`);
|
|
3800
|
+
process.exit(1);
|
|
3801
|
+
}
|
|
3802
|
+
if (opts.json) {
|
|
3803
|
+
console.log(JSON.stringify(body.data, null, 2));
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3806
|
+
const data = body.data;
|
|
3807
|
+
const overallGlyph = data.summary.overall === "fail" ? "\u274C" : data.summary.overall === "warn" ? "\u26A0\uFE0F " : "\u2705";
|
|
3808
|
+
process.stdout.write(`${overallGlyph}
|
|
3809
|
+
|
|
3810
|
+
`);
|
|
3811
|
+
console.log(`Full-Stack Audit \u2014 ${new Date(data.audit_at).toLocaleString()}`);
|
|
3812
|
+
console.log(`Backend linked: ${data.backend_linked ? "yes" : "no (configure Supabase PAT + project ref)"}`);
|
|
3813
|
+
console.log(`Summary: ${data.summary.error_count} error(s) \xB7 ${data.summary.warn_count} warning(s)
|
|
3814
|
+
`);
|
|
3815
|
+
if (data.findings.length === 0) {
|
|
3816
|
+
console.log(" \u2713 No findings. Your project looks healthy.");
|
|
3817
|
+
} else {
|
|
3818
|
+
for (const f of data.findings) {
|
|
3819
|
+
const icon = f.severity === "error" ? "\u{1F534}" : f.severity === "warn" ? "\u{1F7E1}" : "\u2139\uFE0F ";
|
|
3820
|
+
console.log(` ${icon} ${f.title}`);
|
|
3821
|
+
console.log(` ${f.detail.slice(0, 120)}${f.detail.length > 120 ? "\u2026" : ""}`);
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
if (data.gate_runs.length > 0) {
|
|
3825
|
+
console.log("\nGate Results:");
|
|
3826
|
+
for (const run of data.gate_runs) {
|
|
3827
|
+
const g = run.status === "pass" ? "\u2713" : run.status === "fail" ? "\u2717" : "~";
|
|
3828
|
+
console.log(` ${g} ${run.gate.padEnd(22)} ${run.status} (${run.findings_count} finding${run.findings_count !== 1 ? "s" : ""})`);
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
if (data.summary.overall === "fail") process.exit(1);
|
|
3832
|
+
} catch (err) {
|
|
3833
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3834
|
+
if (opts.json) {
|
|
3835
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
3836
|
+
} else {
|
|
3837
|
+
process.stdout.write("ERROR\n");
|
|
3838
|
+
process.stderr.write(`error: ${msg}
|
|
3839
|
+
`);
|
|
3840
|
+
}
|
|
3841
|
+
process.exit(1);
|
|
3842
|
+
}
|
|
3843
|
+
});
|
|
3844
|
+
program.parseAsync().catch((err) => {
|
|
3845
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3846
|
+
process.exit(1);
|
|
3847
|
+
});
|