@mushi-mushi/cli 0.13.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/dist/chunk-4W6VOIQT.js +6 -0
- package/dist/index.js +493 -25
- package/dist/init.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IMDLL4EO.js +0 -6
package/dist/index.js
CHANGED
|
@@ -598,7 +598,7 @@ function getFrameworkFromPkg(pkg) {
|
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
// src/version.ts
|
|
601
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
601
|
+
var MUSHI_CLI_VERSION = true ? "0.14.0" : "0.0.0-dev";
|
|
602
602
|
|
|
603
603
|
// src/init.ts
|
|
604
604
|
var ENV_FILES = [".env.local", ".env"];
|
|
@@ -1430,10 +1430,11 @@ var IngestSetupHttpError = class extends Error {
|
|
|
1430
1430
|
};
|
|
1431
1431
|
var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
1432
1432
|
async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
|
|
1433
|
+
const safeKey = config.apiKey.replace(/[\r\n]/g, "");
|
|
1433
1434
|
const res = await doFetch(`${config.endpoint}/v1/sync/ingest-setup`, {
|
|
1434
1435
|
headers: {
|
|
1435
|
-
Authorization: `Bearer ${
|
|
1436
|
-
"X-Mushi-Api-Key":
|
|
1436
|
+
Authorization: `Bearer ${safeKey}`,
|
|
1437
|
+
"X-Mushi-Api-Key": safeKey,
|
|
1437
1438
|
...config.projectId ? { "X-Mushi-Project": config.projectId } : {}
|
|
1438
1439
|
},
|
|
1439
1440
|
signal: AbortSignal.timeout(8e3)
|
|
@@ -1575,7 +1576,7 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
1575
1576
|
return serverChecks.map((sc) => ({
|
|
1576
1577
|
name: `[server] ${sc.label}`,
|
|
1577
1578
|
ok: sc.ready,
|
|
1578
|
-
detail: sc.hint
|
|
1579
|
+
detail: sc.ready ? "" : sc.hint
|
|
1579
1580
|
}));
|
|
1580
1581
|
}
|
|
1581
1582
|
const text2 = await res.text().catch(() => "");
|
|
@@ -1613,7 +1614,7 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
|
|
|
1613
1614
|
const checks = steps.filter((s) => s.required).map((s) => ({
|
|
1614
1615
|
name: `[ingest] ${s.label}`,
|
|
1615
1616
|
ok: s.complete,
|
|
1616
|
-
detail: s.hint ?? ""
|
|
1617
|
+
detail: s.complete ? "" : s.hint ?? ""
|
|
1617
1618
|
}));
|
|
1618
1619
|
const diag = data.diagnostic;
|
|
1619
1620
|
if (diag?.last_sdk_seen_at) {
|
|
@@ -1629,6 +1630,94 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
|
|
|
1629
1630
|
return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
|
|
1630
1631
|
}
|
|
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
|
+
}
|
|
1632
1721
|
async function runDoctor(config, options = {}) {
|
|
1633
1722
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
1634
1723
|
const checks = [];
|
|
@@ -1646,6 +1735,10 @@ async function runDoctor(config, options = {}) {
|
|
|
1646
1735
|
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
1647
1736
|
checks.push(...ingestChecks);
|
|
1648
1737
|
}
|
|
1738
|
+
if (options.qaStories) {
|
|
1739
|
+
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
1740
|
+
checks.push(...qaChecks);
|
|
1741
|
+
}
|
|
1649
1742
|
return { checks, ready: checks.every((c) => c.ok) };
|
|
1650
1743
|
}
|
|
1651
1744
|
function formatDoctorResult(result) {
|
|
@@ -1654,7 +1747,7 @@ function formatDoctorResult(result) {
|
|
|
1654
1747
|
const lines = [];
|
|
1655
1748
|
for (const c of result.checks) {
|
|
1656
1749
|
lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
|
|
1657
|
-
lines.push(` ${c.detail}`);
|
|
1750
|
+
if (c.detail) lines.push(` ${c.detail}`);
|
|
1658
1751
|
}
|
|
1659
1752
|
const failed = result.checks.filter((c) => !c.ok);
|
|
1660
1753
|
if (failed.length === 0) {
|
|
@@ -1763,7 +1856,6 @@ async function runUpgrade(opts = {}) {
|
|
|
1763
1856
|
|
|
1764
1857
|
// src/connect.ts
|
|
1765
1858
|
import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
1766
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1767
1859
|
import { join as join6, resolve as resolve3 } from "path";
|
|
1768
1860
|
function envKeyPresent(content, key) {
|
|
1769
1861
|
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1774,11 +1866,15 @@ async function mergeEnvFile(path, lines) {
|
|
|
1774
1866
|
# Mushi \u2014 added by mushi connect
|
|
1775
1867
|
${lines.join("\n")}
|
|
1776
1868
|
`;
|
|
1777
|
-
|
|
1778
|
-
|
|
1869
|
+
let existing = null;
|
|
1870
|
+
try {
|
|
1871
|
+
existing = await readFile2(path, "utf8");
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
if (existing !== null) {
|
|
1779
1875
|
const needs = lines.filter((line) => {
|
|
1780
1876
|
const key = line.split("=")[0];
|
|
1781
|
-
return !envKeyPresent(
|
|
1877
|
+
return !envKeyPresent(existing, key);
|
|
1782
1878
|
});
|
|
1783
1879
|
if (needs.length === 0) return false;
|
|
1784
1880
|
await appendFile(path, `
|
|
@@ -1793,13 +1889,17 @@ ${needs.join("\n")}
|
|
|
1793
1889
|
async function ensureMcpJsonGitignored(cwd, messages) {
|
|
1794
1890
|
const gitignorePath = join6(cwd, ".gitignore");
|
|
1795
1891
|
const patterns = [".cursor/mcp.json", ".cursor/"];
|
|
1796
|
-
|
|
1892
|
+
let content = null;
|
|
1893
|
+
try {
|
|
1894
|
+
content = await readFile2(gitignorePath, "utf8");
|
|
1895
|
+
} catch {
|
|
1896
|
+
}
|
|
1897
|
+
if (content === null) {
|
|
1797
1898
|
messages.push(
|
|
1798
1899
|
"\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
|
|
1799
1900
|
);
|
|
1800
1901
|
return;
|
|
1801
1902
|
}
|
|
1802
|
-
const content = await readFile2(gitignorePath, "utf8");
|
|
1803
1903
|
const covered = patterns.some((p3) => content.split("\n").some((line) => line.trim() === p3 || line.trim() === `${p3}/`));
|
|
1804
1904
|
if (covered) return;
|
|
1805
1905
|
await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
|
|
@@ -1843,11 +1943,9 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
1843
1943
|
}
|
|
1844
1944
|
};
|
|
1845
1945
|
let merged = { mcpServers: {} };
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
} catch {
|
|
1850
|
-
}
|
|
1946
|
+
try {
|
|
1947
|
+
merged = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
1948
|
+
} catch {
|
|
1851
1949
|
}
|
|
1852
1950
|
const servers = merged.mcpServers ?? {};
|
|
1853
1951
|
servers[serverName] = mcpServerBlock;
|
|
@@ -2139,7 +2237,7 @@ Examples:
|
|
|
2139
2237
|
mushi config endpoint https://... # set endpoint
|
|
2140
2238
|
mushi config projectId <uuid> # set project`).action((key, value) => {
|
|
2141
2239
|
const config = loadConfig();
|
|
2142
|
-
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
|
|
2240
|
+
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId", "consoleUrl"]);
|
|
2143
2241
|
if (key && value) {
|
|
2144
2242
|
if (!ALLOWED_KEYS.has(key)) {
|
|
2145
2243
|
process.stderr.write(`error: unknown config key "${key}". Allowed: ${[...ALLOWED_KEYS].join(", ")}
|
|
@@ -2644,7 +2742,7 @@ Typical first-time flow:
|
|
|
2644
2742
|
# CLI writes .env.local and .cursor/mcp.json
|
|
2645
2743
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
2646
2744
|
const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
|
|
2647
|
-
const { existsSync:
|
|
2745
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
2648
2746
|
const nodePath = await import("path");
|
|
2649
2747
|
const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
|
|
2650
2748
|
const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
|
|
@@ -2691,7 +2789,7 @@ Typical first-time flow:
|
|
|
2691
2789
|
`MUSHI_API_KEY=${apiKey}`,
|
|
2692
2790
|
""
|
|
2693
2791
|
];
|
|
2694
|
-
const envExisting =
|
|
2792
|
+
const envExisting = existsSync5(envPath);
|
|
2695
2793
|
await writeFile2(envPath, envLines.join("\n"), "utf8");
|
|
2696
2794
|
console.log(`
|
|
2697
2795
|
\u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
|
|
@@ -2711,7 +2809,7 @@ Typical first-time flow:
|
|
|
2711
2809
|
}
|
|
2712
2810
|
}
|
|
2713
2811
|
};
|
|
2714
|
-
const mcpExisting =
|
|
2812
|
+
const mcpExisting = existsSync5(mcpPath);
|
|
2715
2813
|
if (mcpExisting) {
|
|
2716
2814
|
try {
|
|
2717
2815
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
@@ -2745,7 +2843,7 @@ Supported IDEs:
|
|
|
2745
2843
|
|
|
2746
2844
|
The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
|
|
2747
2845
|
const { writeFile: writeFile2, mkdir: mkdir2, readFile: readFile3 } = await import("fs/promises");
|
|
2748
|
-
const { existsSync:
|
|
2846
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
2749
2847
|
const nodePath = await import("path");
|
|
2750
2848
|
const os = await import("os");
|
|
2751
2849
|
const config = requireConfig({ needsProject: true });
|
|
@@ -2777,7 +2875,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2777
2875
|
const configPath = nodePath.join(configDir, ideEntry.file);
|
|
2778
2876
|
if (ideEntry.format === "mcp-json") {
|
|
2779
2877
|
let merged = { mcpServers: {} };
|
|
2780
|
-
if (
|
|
2878
|
+
if (existsSync5(configPath)) {
|
|
2781
2879
|
try {
|
|
2782
2880
|
const raw = await readFile3(configPath, "utf8");
|
|
2783
2881
|
merged = JSON.parse(raw);
|
|
@@ -2798,7 +2896,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2798
2896
|
}
|
|
2799
2897
|
} else if (ideEntry.format === "zed") {
|
|
2800
2898
|
let settings = {};
|
|
2801
|
-
if (
|
|
2899
|
+
if (existsSync5(configPath)) {
|
|
2802
2900
|
try {
|
|
2803
2901
|
const raw = await readFile3(configPath, "utf8");
|
|
2804
2902
|
settings = JSON.parse(raw);
|
|
@@ -3059,9 +3157,12 @@ program.command("doctor").description(
|
|
|
3059
3157
|
).option(
|
|
3060
3158
|
"--ingest",
|
|
3061
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."
|
|
3062
3163
|
).action(async (opts) => {
|
|
3063
3164
|
const config = loadConfig();
|
|
3064
|
-
const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest });
|
|
3165
|
+
const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories });
|
|
3065
3166
|
const { checks } = result;
|
|
3066
3167
|
if (opts.json) {
|
|
3067
3168
|
console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
|
|
@@ -3373,6 +3474,373 @@ keys.command("add").description("Add a new API key to the pool").requiredOption(
|
|
|
3373
3474
|
}
|
|
3374
3475
|
console.log(`\u2713 Key added \u2014 id: ${res.data.id}`);
|
|
3375
3476
|
});
|
|
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
|
+
});
|
|
3376
3844
|
program.parseAsync().catch((err) => {
|
|
3377
3845
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3378
3846
|
process.exit(1);
|
package/dist/init.js
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED