@mushi-mushi/cli 0.13.0 → 0.15.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-UTST6AEP.js +6 -0
- package/dist/index.js +796 -37
- 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.15.0" : "0.0.0-dev";
|
|
602
602
|
|
|
603
603
|
// src/init.ts
|
|
604
604
|
var ENV_FILES = [".env.local", ".env"];
|
|
@@ -1088,6 +1088,49 @@ function runMigrate(opts = {}) {
|
|
|
1088
1088
|
return { matches };
|
|
1089
1089
|
}
|
|
1090
1090
|
|
|
1091
|
+
// src/sanitize-config.ts
|
|
1092
|
+
var PROJECT_ID_RE = /^(?:proj_[A-Za-z0-9_-]{10,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
1093
|
+
var API_KEY_RE = /^mushi_[A-Za-z0-9_-]{10,}$/;
|
|
1094
|
+
function sanitizeApiKey(raw) {
|
|
1095
|
+
const key = raw.replace(/[\r\n\0]/g, "");
|
|
1096
|
+
if (!API_KEY_RE.test(key)) {
|
|
1097
|
+
throw new Error(
|
|
1098
|
+
"Invalid API key in config \u2014 run `mushi login --api-key <key>` to refresh credentials."
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
return key;
|
|
1102
|
+
}
|
|
1103
|
+
function sanitizeProjectId(raw) {
|
|
1104
|
+
const id = raw.trim();
|
|
1105
|
+
if (!PROJECT_ID_RE.test(id)) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
"Invalid project ID in config \u2014 expected a UUID or proj_* slug from the admin console."
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
return id;
|
|
1111
|
+
}
|
|
1112
|
+
function sanitizeEndpoint(raw) {
|
|
1113
|
+
return assertEndpoint(normalizeEndpoint(raw));
|
|
1114
|
+
}
|
|
1115
|
+
function sanitizeCliCredentials(config) {
|
|
1116
|
+
if (!config.endpoint || !config.apiKey || !config.projectId) {
|
|
1117
|
+
throw new Error("Missing endpoint, apiKey, or projectId");
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
endpoint: sanitizeEndpoint(config.endpoint),
|
|
1121
|
+
apiKey: sanitizeApiKey(config.apiKey),
|
|
1122
|
+
projectId: sanitizeProjectId(config.projectId)
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function apiKeyHeaders(apiKey, projectId) {
|
|
1126
|
+
const headers = {
|
|
1127
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1128
|
+
"X-Mushi-Api-Key": apiKey
|
|
1129
|
+
};
|
|
1130
|
+
if (projectId) headers["X-Mushi-Project"] = projectId;
|
|
1131
|
+
return headers;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1091
1134
|
// src/sourcemaps.ts
|
|
1092
1135
|
import { createReadStream } from "fs";
|
|
1093
1136
|
import { readFile, readdir } from "fs/promises";
|
|
@@ -1430,12 +1473,11 @@ var IngestSetupHttpError = class extends Error {
|
|
|
1430
1473
|
};
|
|
1431
1474
|
var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
1432
1475
|
async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
},
|
|
1476
|
+
const endpoint = sanitizeEndpoint(config.endpoint);
|
|
1477
|
+
const apiKey = sanitizeApiKey(config.apiKey);
|
|
1478
|
+
const projectId = config.projectId ? sanitizeProjectId(config.projectId) : void 0;
|
|
1479
|
+
const res = await doFetch(`${endpoint}/v1/sync/ingest-setup`, {
|
|
1480
|
+
headers: apiKeyHeaders(apiKey, projectId),
|
|
1439
1481
|
signal: AbortSignal.timeout(8e3)
|
|
1440
1482
|
});
|
|
1441
1483
|
if (!res.ok) {
|
|
@@ -1510,13 +1552,14 @@ function checkCliConfig(config) {
|
|
|
1510
1552
|
}
|
|
1511
1553
|
async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
1512
1554
|
try {
|
|
1513
|
-
const
|
|
1555
|
+
const safeEndpoint = sanitizeEndpoint(endpoint);
|
|
1556
|
+
const res = await doFetch(`${safeEndpoint}/health`, {
|
|
1514
1557
|
signal: AbortSignal.timeout(5e3)
|
|
1515
1558
|
});
|
|
1516
1559
|
return {
|
|
1517
1560
|
name: "Endpoint reachable",
|
|
1518
1561
|
ok: res.status === 200,
|
|
1519
|
-
detail: `GET ${
|
|
1562
|
+
detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
|
|
1520
1563
|
};
|
|
1521
1564
|
} catch (err) {
|
|
1522
1565
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1558,14 +1601,11 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
1558
1601
|
];
|
|
1559
1602
|
}
|
|
1560
1603
|
try {
|
|
1604
|
+
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
1561
1605
|
const res = await doFetch(
|
|
1562
|
-
`${
|
|
1606
|
+
`${endpoint}/v1/admin/projects/${projectId}/preflight`,
|
|
1563
1607
|
{
|
|
1564
|
-
headers:
|
|
1565
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
1566
|
-
"X-Mushi-Api-Key": config.apiKey,
|
|
1567
|
-
"X-Mushi-Project": config.projectId
|
|
1568
|
-
},
|
|
1608
|
+
headers: apiKeyHeaders(apiKey, projectId),
|
|
1569
1609
|
signal: AbortSignal.timeout(8e3)
|
|
1570
1610
|
}
|
|
1571
1611
|
);
|
|
@@ -1575,7 +1615,7 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
1575
1615
|
return serverChecks.map((sc) => ({
|
|
1576
1616
|
name: `[server] ${sc.label}`,
|
|
1577
1617
|
ok: sc.ready,
|
|
1578
|
-
detail: sc.hint
|
|
1618
|
+
detail: sc.ready ? "" : sc.hint
|
|
1579
1619
|
}));
|
|
1580
1620
|
}
|
|
1581
1621
|
const text2 = await res.text().catch(() => "");
|
|
@@ -1613,7 +1653,7 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
|
|
|
1613
1653
|
const checks = steps.filter((s) => s.required).map((s) => ({
|
|
1614
1654
|
name: `[ingest] ${s.label}`,
|
|
1615
1655
|
ok: s.complete,
|
|
1616
|
-
detail: s.hint ?? ""
|
|
1656
|
+
detail: s.complete ? "" : s.hint ?? ""
|
|
1617
1657
|
}));
|
|
1618
1658
|
const diag = data.diagnostic;
|
|
1619
1659
|
if (diag?.last_sdk_seen_at) {
|
|
@@ -1629,6 +1669,84 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
|
|
|
1629
1669
|
return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
|
|
1630
1670
|
}
|
|
1631
1671
|
}
|
|
1672
|
+
async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
1673
|
+
if (!config.projectId || !config.apiKey || !config.endpoint) {
|
|
1674
|
+
return [
|
|
1675
|
+
{
|
|
1676
|
+
name: "QA stories health",
|
|
1677
|
+
ok: false,
|
|
1678
|
+
detail: "Need projectId, apiKey, and endpoint for QA story checks."
|
|
1679
|
+
}
|
|
1680
|
+
];
|
|
1681
|
+
}
|
|
1682
|
+
const checks = [];
|
|
1683
|
+
try {
|
|
1684
|
+
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
1685
|
+
const headers = apiKeyHeaders(apiKey, projectId);
|
|
1686
|
+
const storiesRes = await doFetch(
|
|
1687
|
+
`${endpoint}/v1/admin/projects/${projectId}/qa-coverage`,
|
|
1688
|
+
{
|
|
1689
|
+
headers,
|
|
1690
|
+
signal: AbortSignal.timeout(8e3)
|
|
1691
|
+
}
|
|
1692
|
+
);
|
|
1693
|
+
if (!storiesRes.ok) {
|
|
1694
|
+
checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
|
|
1695
|
+
return checks;
|
|
1696
|
+
}
|
|
1697
|
+
const storiesBody = await storiesRes.json();
|
|
1698
|
+
const stories2 = storiesBody.data?.coverage ?? [];
|
|
1699
|
+
const enabled = stories2.filter((s) => s.enabled);
|
|
1700
|
+
if (enabled.length === 0) {
|
|
1701
|
+
checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
|
|
1702
|
+
return checks;
|
|
1703
|
+
}
|
|
1704
|
+
checks.push({
|
|
1705
|
+
name: "[qa] Enabled QA stories",
|
|
1706
|
+
ok: true,
|
|
1707
|
+
detail: `${enabled.length} enabled story/stories configured`
|
|
1708
|
+
});
|
|
1709
|
+
const slackRes = await doFetch(
|
|
1710
|
+
`${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
|
|
1711
|
+
{
|
|
1712
|
+
method: "POST",
|
|
1713
|
+
headers,
|
|
1714
|
+
signal: AbortSignal.timeout(6e3)
|
|
1715
|
+
}
|
|
1716
|
+
);
|
|
1717
|
+
const slackBody = slackRes.ok ? await slackRes.json() : null;
|
|
1718
|
+
const slackOk = slackBody?.status === "ok";
|
|
1719
|
+
checks.push({
|
|
1720
|
+
name: "[qa] Slack notifications configured",
|
|
1721
|
+
ok: slackOk,
|
|
1722
|
+
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."
|
|
1723
|
+
});
|
|
1724
|
+
const fcRes = await doFetch(
|
|
1725
|
+
`${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
|
|
1726
|
+
{
|
|
1727
|
+
method: "POST",
|
|
1728
|
+
headers,
|
|
1729
|
+
signal: AbortSignal.timeout(6e3)
|
|
1730
|
+
}
|
|
1731
|
+
);
|
|
1732
|
+
const fcBody = fcRes.ok ? await fcRes.json() : null;
|
|
1733
|
+
const hasFirecrawlStories = enabled.some(
|
|
1734
|
+
(s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
|
|
1735
|
+
);
|
|
1736
|
+
if (hasFirecrawlStories) {
|
|
1737
|
+
const fcOk = fcBody?.status === "ok";
|
|
1738
|
+
checks.push({
|
|
1739
|
+
name: "[qa] Firecrawl API key configured",
|
|
1740
|
+
ok: fcOk,
|
|
1741
|
+
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."
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1746
|
+
checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
|
|
1747
|
+
}
|
|
1748
|
+
return checks;
|
|
1749
|
+
}
|
|
1632
1750
|
async function runDoctor(config, options = {}) {
|
|
1633
1751
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
1634
1752
|
const checks = [];
|
|
@@ -1646,6 +1764,10 @@ async function runDoctor(config, options = {}) {
|
|
|
1646
1764
|
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
1647
1765
|
checks.push(...ingestChecks);
|
|
1648
1766
|
}
|
|
1767
|
+
if (options.qaStories) {
|
|
1768
|
+
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
1769
|
+
checks.push(...qaChecks);
|
|
1770
|
+
}
|
|
1649
1771
|
return { checks, ready: checks.every((c) => c.ok) };
|
|
1650
1772
|
}
|
|
1651
1773
|
function formatDoctorResult(result) {
|
|
@@ -1654,7 +1776,7 @@ function formatDoctorResult(result) {
|
|
|
1654
1776
|
const lines = [];
|
|
1655
1777
|
for (const c of result.checks) {
|
|
1656
1778
|
lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
|
|
1657
|
-
lines.push(` ${c.detail}`);
|
|
1779
|
+
if (c.detail) lines.push(` ${c.detail}`);
|
|
1658
1780
|
}
|
|
1659
1781
|
const failed = result.checks.filter((c) => !c.ok);
|
|
1660
1782
|
if (failed.length === 0) {
|
|
@@ -1763,7 +1885,6 @@ async function runUpgrade(opts = {}) {
|
|
|
1763
1885
|
|
|
1764
1886
|
// src/connect.ts
|
|
1765
1887
|
import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
1766
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1767
1888
|
import { join as join6, resolve as resolve3 } from "path";
|
|
1768
1889
|
function envKeyPresent(content, key) {
|
|
1769
1890
|
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1774,11 +1895,15 @@ async function mergeEnvFile(path, lines) {
|
|
|
1774
1895
|
# Mushi \u2014 added by mushi connect
|
|
1775
1896
|
${lines.join("\n")}
|
|
1776
1897
|
`;
|
|
1777
|
-
|
|
1778
|
-
|
|
1898
|
+
let existing = null;
|
|
1899
|
+
try {
|
|
1900
|
+
existing = await readFile2(path, "utf8");
|
|
1901
|
+
} catch {
|
|
1902
|
+
}
|
|
1903
|
+
if (existing !== null) {
|
|
1779
1904
|
const needs = lines.filter((line) => {
|
|
1780
1905
|
const key = line.split("=")[0];
|
|
1781
|
-
return !envKeyPresent(
|
|
1906
|
+
return !envKeyPresent(existing, key);
|
|
1782
1907
|
});
|
|
1783
1908
|
if (needs.length === 0) return false;
|
|
1784
1909
|
await appendFile(path, `
|
|
@@ -1793,13 +1918,17 @@ ${needs.join("\n")}
|
|
|
1793
1918
|
async function ensureMcpJsonGitignored(cwd, messages) {
|
|
1794
1919
|
const gitignorePath = join6(cwd, ".gitignore");
|
|
1795
1920
|
const patterns = [".cursor/mcp.json", ".cursor/"];
|
|
1796
|
-
|
|
1921
|
+
let content = null;
|
|
1922
|
+
try {
|
|
1923
|
+
content = await readFile2(gitignorePath, "utf8");
|
|
1924
|
+
} catch {
|
|
1925
|
+
}
|
|
1926
|
+
if (content === null) {
|
|
1797
1927
|
messages.push(
|
|
1798
1928
|
"\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
|
|
1799
1929
|
);
|
|
1800
1930
|
return;
|
|
1801
1931
|
}
|
|
1802
|
-
const content = await readFile2(gitignorePath, "utf8");
|
|
1803
1932
|
const covered = patterns.some((p3) => content.split("\n").some((line) => line.trim() === p3 || line.trim() === `${p3}/`));
|
|
1804
1933
|
if (covered) return;
|
|
1805
1934
|
await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
|
|
@@ -1843,11 +1972,9 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
1843
1972
|
}
|
|
1844
1973
|
};
|
|
1845
1974
|
let merged = { mcpServers: {} };
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
} catch {
|
|
1850
|
-
}
|
|
1975
|
+
try {
|
|
1976
|
+
merged = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
1977
|
+
} catch {
|
|
1851
1978
|
}
|
|
1852
1979
|
const servers = merged.mcpServers ?? {};
|
|
1853
1980
|
servers[serverName] = mcpServerBlock;
|
|
@@ -2139,7 +2266,7 @@ Examples:
|
|
|
2139
2266
|
mushi config endpoint https://... # set endpoint
|
|
2140
2267
|
mushi config projectId <uuid> # set project`).action((key, value) => {
|
|
2141
2268
|
const config = loadConfig();
|
|
2142
|
-
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
|
|
2269
|
+
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId", "consoleUrl"]);
|
|
2143
2270
|
if (key && value) {
|
|
2144
2271
|
if (!ALLOWED_KEYS.has(key)) {
|
|
2145
2272
|
process.stderr.write(`error: unknown config key "${key}". Allowed: ${[...ALLOWED_KEYS].join(", ")}
|
|
@@ -2644,7 +2771,7 @@ Typical first-time flow:
|
|
|
2644
2771
|
# CLI writes .env.local and .cursor/mcp.json
|
|
2645
2772
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
2646
2773
|
const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
|
|
2647
|
-
const { existsSync:
|
|
2774
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
2648
2775
|
const nodePath = await import("path");
|
|
2649
2776
|
const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
|
|
2650
2777
|
const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
|
|
@@ -2691,7 +2818,7 @@ Typical first-time flow:
|
|
|
2691
2818
|
`MUSHI_API_KEY=${apiKey}`,
|
|
2692
2819
|
""
|
|
2693
2820
|
];
|
|
2694
|
-
const envExisting =
|
|
2821
|
+
const envExisting = existsSync5(envPath);
|
|
2695
2822
|
await writeFile2(envPath, envLines.join("\n"), "utf8");
|
|
2696
2823
|
console.log(`
|
|
2697
2824
|
\u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
|
|
@@ -2711,7 +2838,7 @@ Typical first-time flow:
|
|
|
2711
2838
|
}
|
|
2712
2839
|
}
|
|
2713
2840
|
};
|
|
2714
|
-
const mcpExisting =
|
|
2841
|
+
const mcpExisting = existsSync5(mcpPath);
|
|
2715
2842
|
if (mcpExisting) {
|
|
2716
2843
|
try {
|
|
2717
2844
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
@@ -2745,7 +2872,7 @@ Supported IDEs:
|
|
|
2745
2872
|
|
|
2746
2873
|
The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
|
|
2747
2874
|
const { writeFile: writeFile2, mkdir: mkdir2, readFile: readFile3 } = await import("fs/promises");
|
|
2748
|
-
const { existsSync:
|
|
2875
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
2749
2876
|
const nodePath = await import("path");
|
|
2750
2877
|
const os = await import("os");
|
|
2751
2878
|
const config = requireConfig({ needsProject: true });
|
|
@@ -2777,7 +2904,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2777
2904
|
const configPath = nodePath.join(configDir, ideEntry.file);
|
|
2778
2905
|
if (ideEntry.format === "mcp-json") {
|
|
2779
2906
|
let merged = { mcpServers: {} };
|
|
2780
|
-
if (
|
|
2907
|
+
if (existsSync5(configPath)) {
|
|
2781
2908
|
try {
|
|
2782
2909
|
const raw = await readFile3(configPath, "utf8");
|
|
2783
2910
|
merged = JSON.parse(raw);
|
|
@@ -2798,7 +2925,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2798
2925
|
}
|
|
2799
2926
|
} else if (ideEntry.format === "zed") {
|
|
2800
2927
|
let settings = {};
|
|
2801
|
-
if (
|
|
2928
|
+
if (existsSync5(configPath)) {
|
|
2802
2929
|
try {
|
|
2803
2930
|
const raw = await readFile3(configPath, "utf8");
|
|
2804
2931
|
settings = JSON.parse(raw);
|
|
@@ -3059,9 +3186,12 @@ program.command("doctor").description(
|
|
|
3059
3186
|
).option(
|
|
3060
3187
|
"--ingest",
|
|
3061
3188
|
"Also call GET /v1/sync/ingest-setup for the 4 required ingest steps (API key, SDK heartbeat, first report). Composable with --server."
|
|
3189
|
+
).option(
|
|
3190
|
+
"--qa-stories",
|
|
3191
|
+
"Check enabled QA stories for common setup issues: missing Firecrawl key, missing target URL, Slack not connected. Requires --server credentials."
|
|
3062
3192
|
).action(async (opts) => {
|
|
3063
3193
|
const config = loadConfig();
|
|
3064
|
-
const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest });
|
|
3194
|
+
const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories });
|
|
3065
3195
|
const { checks } = result;
|
|
3066
3196
|
if (opts.json) {
|
|
3067
3197
|
console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
|
|
@@ -3373,7 +3503,636 @@ keys.command("add").description("Add a new API key to the pool").requiredOption(
|
|
|
3373
3503
|
}
|
|
3374
3504
|
console.log(`\u2713 Key added \u2014 id: ${res.data.id}`);
|
|
3375
3505
|
});
|
|
3506
|
+
var integrations = program.command("integrations").description("Manage service integrations");
|
|
3507
|
+
integrations.command("list").description("List all configured integrations and their current health status").option("--json", "Machine-readable output").action(async (opts) => {
|
|
3508
|
+
const config = loadConfig();
|
|
3509
|
+
if (!config.apiKey) {
|
|
3510
|
+
console.error("Run `mushi login` first");
|
|
3511
|
+
process.exit(2);
|
|
3512
|
+
}
|
|
3513
|
+
if (!config.projectId) {
|
|
3514
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3515
|
+
process.exit(2);
|
|
3516
|
+
}
|
|
3517
|
+
const result = await apiCall(
|
|
3518
|
+
`/v1/admin/projects/${config.projectId}/integrations`,
|
|
3519
|
+
config
|
|
3520
|
+
);
|
|
3521
|
+
const rawResult = result;
|
|
3522
|
+
if (!rawResult.integrations && !result.ok) {
|
|
3523
|
+
console.error("Failed:", result.error);
|
|
3524
|
+
process.exit(1);
|
|
3525
|
+
}
|
|
3526
|
+
if (opts.json) {
|
|
3527
|
+
console.log(JSON.stringify(rawResult, null, 2));
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
const rows = rawResult.integrations ?? [];
|
|
3531
|
+
if (rows.length === 0) {
|
|
3532
|
+
console.log("No integrations configured. Visit the Integrations page to connect services.");
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
const icons = {
|
|
3536
|
+
slack: "\u{1F514}",
|
|
3537
|
+
github: "\u{1F419}",
|
|
3538
|
+
sentry: "\u{1FAB2}",
|
|
3539
|
+
langfuse: "\u{1F52D}",
|
|
3540
|
+
discord: "\u{1F4AC}",
|
|
3541
|
+
linear: "\u{1F4D0}",
|
|
3542
|
+
jira: "\u{1F5C2}\uFE0F",
|
|
3543
|
+
cursor_cloud: "\u{1F5B1}\uFE0F",
|
|
3544
|
+
claude_code_agent: "\u{1F916}"
|
|
3545
|
+
};
|
|
3546
|
+
console.log("\nIntegrations:\n");
|
|
3547
|
+
for (const row of rows) {
|
|
3548
|
+
const icon = icons[row.kind] ?? "\u{1F50C}";
|
|
3549
|
+
const statusIcon = row.status === "ok" ? "\u2705" : row.status === "error" ? "\u274C" : "\u26AA";
|
|
3550
|
+
console.log(` ${icon} ${row.kind.padEnd(20)} ${statusIcon} ${row.detail ?? ""}`);
|
|
3551
|
+
}
|
|
3552
|
+
console.log();
|
|
3553
|
+
});
|
|
3554
|
+
integrations.command("test <kind>").description(
|
|
3555
|
+
"Run a health probe for a specific integration (e.g. slack, sentry, github, langfuse, discord, cursor_cloud, claude_code_agent)"
|
|
3556
|
+
).option("--json", "Machine-readable output").action(async (kind, opts) => {
|
|
3557
|
+
const config = loadConfig();
|
|
3558
|
+
if (!config.apiKey) {
|
|
3559
|
+
console.error("Run `mushi login` first");
|
|
3560
|
+
process.exit(2);
|
|
3561
|
+
}
|
|
3562
|
+
if (!config.projectId) {
|
|
3563
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3564
|
+
process.exit(2);
|
|
3565
|
+
}
|
|
3566
|
+
const result = await apiCall(
|
|
3567
|
+
`/v1/admin/projects/${config.projectId}/integrations/probe/${kind}`,
|
|
3568
|
+
config,
|
|
3569
|
+
{ method: "POST" }
|
|
3570
|
+
);
|
|
3571
|
+
if (!result.ok) {
|
|
3572
|
+
console.error("Request failed:", result.error);
|
|
3573
|
+
process.exit(1);
|
|
3574
|
+
}
|
|
3575
|
+
if (opts.json) {
|
|
3576
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
const probeOk = result.data?.status === "ok";
|
|
3580
|
+
console.log(
|
|
3581
|
+
probeOk ? `\u2705 ${kind} integration is healthy${result.data.detail ? ": " + result.data.detail : ""}` : `\u274C ${kind} integration check failed${result.data.detail ? ": " + result.data.detail : ""}`
|
|
3582
|
+
);
|
|
3583
|
+
if (!probeOk) process.exit(1);
|
|
3584
|
+
});
|
|
3585
|
+
var slack = program.command("slack").description("Slack integration commands");
|
|
3586
|
+
slack.command("status").description("Show whether Slack is connected and which channel receives notifications").option("--json", "Machine-readable output").action(async (opts) => {
|
|
3587
|
+
const config = loadConfig();
|
|
3588
|
+
if (!config.apiKey) {
|
|
3589
|
+
console.error("Run `mushi login` first");
|
|
3590
|
+
process.exit(2);
|
|
3591
|
+
}
|
|
3592
|
+
if (!config.projectId) {
|
|
3593
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3594
|
+
process.exit(2);
|
|
3595
|
+
}
|
|
3596
|
+
const result = await apiCall(
|
|
3597
|
+
`/v1/admin/projects/${config.projectId}/integrations/probe/slack`,
|
|
3598
|
+
config,
|
|
3599
|
+
{ method: "POST" }
|
|
3600
|
+
);
|
|
3601
|
+
if (!result.ok) {
|
|
3602
|
+
console.error("Request failed:", result.error);
|
|
3603
|
+
process.exit(1);
|
|
3604
|
+
}
|
|
3605
|
+
if (opts.json) {
|
|
3606
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
if (result.data?.status === "ok") {
|
|
3610
|
+
console.log("\u2705 Slack connected");
|
|
3611
|
+
if (result.data.detail) console.log(` ${result.data.detail}`);
|
|
3612
|
+
console.log("\n To change the channel or notification prefs, visit /integrations in the Mushi console.");
|
|
3613
|
+
} else {
|
|
3614
|
+
console.log("\u26AA Slack not connected");
|
|
3615
|
+
console.log(' Visit /integrations in the Mushi console and click "Add to Slack".');
|
|
3616
|
+
}
|
|
3617
|
+
});
|
|
3618
|
+
slack.command("test").description("Send a test Slack notification to confirm the current channel is working").option("--json", "Machine-readable output").action(async (opts) => {
|
|
3619
|
+
const config = loadConfig();
|
|
3620
|
+
if (!config.apiKey) {
|
|
3621
|
+
console.error("Run `mushi login` first");
|
|
3622
|
+
process.exit(2);
|
|
3623
|
+
}
|
|
3624
|
+
if (!config.projectId) {
|
|
3625
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3626
|
+
process.exit(2);
|
|
3627
|
+
}
|
|
3628
|
+
const result = await apiCall(
|
|
3629
|
+
`/v1/admin/projects/${config.projectId}/integrations/slack/test`,
|
|
3630
|
+
config,
|
|
3631
|
+
{ method: "POST" }
|
|
3632
|
+
);
|
|
3633
|
+
if (!result.ok) {
|
|
3634
|
+
console.error("Request failed:", result.error);
|
|
3635
|
+
process.exit(1);
|
|
3636
|
+
}
|
|
3637
|
+
if (opts.json) {
|
|
3638
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
if (result.data?.ok) {
|
|
3642
|
+
console.log("\u2705 Test message sent! Check your Slack channel.");
|
|
3643
|
+
} else {
|
|
3644
|
+
console.error("\u274C Test failed:", result.data?.error ?? "unknown error");
|
|
3645
|
+
process.exit(1);
|
|
3646
|
+
}
|
|
3647
|
+
});
|
|
3648
|
+
var qa = program.command("qa").description("QA story management");
|
|
3649
|
+
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) => {
|
|
3650
|
+
const config = loadConfig();
|
|
3651
|
+
if (!config.apiKey) {
|
|
3652
|
+
console.error("Run `mushi login` first");
|
|
3653
|
+
process.exit(2);
|
|
3654
|
+
}
|
|
3655
|
+
if (!config.projectId) {
|
|
3656
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3657
|
+
process.exit(2);
|
|
3658
|
+
}
|
|
3659
|
+
const result = await apiCall(
|
|
3660
|
+
`/v1/admin/projects/${config.projectId}/qa-coverage`,
|
|
3661
|
+
config
|
|
3662
|
+
);
|
|
3663
|
+
if (!result.ok) {
|
|
3664
|
+
console.error("Failed:", result.error);
|
|
3665
|
+
process.exit(1);
|
|
3666
|
+
}
|
|
3667
|
+
if (opts.json) {
|
|
3668
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3669
|
+
return;
|
|
3670
|
+
}
|
|
3671
|
+
const stories2 = result.data?.coverage ?? [];
|
|
3672
|
+
if (stories2.length === 0) {
|
|
3673
|
+
console.log("No QA stories yet. Create one at /qa-coverage in the Mushi console.");
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
console.log(`
|
|
3677
|
+
QA Stories (${stories2.length}):
|
|
3678
|
+
`);
|
|
3679
|
+
for (const s of stories2) {
|
|
3680
|
+
const statusIcon = s.last_run_status === "passed" ? "\u2705" : s.last_run_status === "failed" ? "\u274C" : s.last_run_status === "error" ? "\u{1F6A8}" : "\u26AA";
|
|
3681
|
+
const enabled = s.enabled ? "" : " [disabled]";
|
|
3682
|
+
const sid = s.story_id ?? s.id ?? "\u2014";
|
|
3683
|
+
console.log(` ${statusIcon} ${s.name.slice(0, 50).padEnd(52)} ${sid}${enabled}`);
|
|
3684
|
+
}
|
|
3685
|
+
console.log(`
|
|
3686
|
+
Use 'mushi qa runs <storyId>' to see recent runs for a story.`);
|
|
3687
|
+
console.log();
|
|
3688
|
+
});
|
|
3689
|
+
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) => {
|
|
3690
|
+
const config = loadConfig();
|
|
3691
|
+
if (!config.apiKey) {
|
|
3692
|
+
console.error("Run `mushi login` first");
|
|
3693
|
+
process.exit(2);
|
|
3694
|
+
}
|
|
3695
|
+
if (!config.projectId) {
|
|
3696
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3697
|
+
process.exit(2);
|
|
3698
|
+
}
|
|
3699
|
+
const limit = parseInt(opts.limit ?? "10", 10);
|
|
3700
|
+
const result = await apiCall(
|
|
3701
|
+
`/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/runs?limit=${limit}`,
|
|
3702
|
+
config
|
|
3703
|
+
);
|
|
3704
|
+
if (!result.ok) {
|
|
3705
|
+
console.error("Failed:", result.error);
|
|
3706
|
+
process.exit(1);
|
|
3707
|
+
}
|
|
3708
|
+
if (opts.json) {
|
|
3709
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
const runs = result.data?.runs ?? [];
|
|
3713
|
+
if (runs.length === 0) {
|
|
3714
|
+
console.log("No runs yet for this story. Trigger one with `mushi qa run <storyId>`.");
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
console.log(`
|
|
3718
|
+
Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
3719
|
+
`);
|
|
3720
|
+
for (const r of runs) {
|
|
3721
|
+
const statusIcon = r.status === "passed" ? "\u2705" : r.status === "failed" ? "\u274C" : r.status === "error" ? "\u{1F6A8}" : "\u23F3";
|
|
3722
|
+
const ts = r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace("T", " ") : "\u2014";
|
|
3723
|
+
const latency = r.latency_ms ? ` (${(r.latency_ms / 1e3).toFixed(1)}s)` : "";
|
|
3724
|
+
console.log(` ${statusIcon} ${ts}${latency} ${r.id.slice(0, 8)}`);
|
|
3725
|
+
if (r.error_message) {
|
|
3726
|
+
console.log(` Error: ${r.error_message.slice(0, 120)}`);
|
|
3727
|
+
}
|
|
3728
|
+
if (r.assertion_failures?.length) {
|
|
3729
|
+
for (const af of r.assertion_failures.slice(0, 3)) {
|
|
3730
|
+
console.log(` \xB7 ${String(af).slice(0, 100)}`);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
const consoleUrl = config.consoleUrl ?? "https://app.mushi.ai";
|
|
3735
|
+
console.log(`
|
|
3736
|
+
Open in console: ${consoleUrl}/qa-coverage?story=${storyId}`);
|
|
3737
|
+
console.log(` Tip: run 'mushi config consoleUrl http://localhost:6464' to set your local console URL`);
|
|
3738
|
+
console.log();
|
|
3739
|
+
});
|
|
3740
|
+
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) => {
|
|
3741
|
+
const config = loadConfig();
|
|
3742
|
+
if (!config.apiKey) {
|
|
3743
|
+
console.error("Run `mushi login` first");
|
|
3744
|
+
process.exit(2);
|
|
3745
|
+
}
|
|
3746
|
+
if (!config.projectId) {
|
|
3747
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3748
|
+
process.exit(2);
|
|
3749
|
+
}
|
|
3750
|
+
const result = await apiCall(
|
|
3751
|
+
`/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/run`,
|
|
3752
|
+
config,
|
|
3753
|
+
{ method: "POST" }
|
|
3754
|
+
);
|
|
3755
|
+
if (!result.ok) {
|
|
3756
|
+
console.error("Failed:", result.error);
|
|
3757
|
+
process.exit(1);
|
|
3758
|
+
}
|
|
3759
|
+
if (opts.json) {
|
|
3760
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
3761
|
+
return;
|
|
3762
|
+
}
|
|
3763
|
+
const runId = result.data?.run_id;
|
|
3764
|
+
if (runId) {
|
|
3765
|
+
console.log(`\u25B6 Run triggered: ${runId.slice(0, 8)}\u2026`);
|
|
3766
|
+
console.log(` Check results: mushi qa runs ${storyId}`);
|
|
3767
|
+
} else {
|
|
3768
|
+
console.error("\u274C Trigger failed: no run_id in response", JSON.stringify(result.data));
|
|
3769
|
+
process.exit(1);
|
|
3770
|
+
}
|
|
3771
|
+
});
|
|
3772
|
+
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", `
|
|
3773
|
+
Description:
|
|
3774
|
+
Fans out to the Mushi backend to run a full-stack health audit:
|
|
3775
|
+
\u2022 DB schema + Supabase advisors (requires Supabase PAT in API Keys)
|
|
3776
|
+
\u2022 Recent backend error logs
|
|
3777
|
+
\u2022 Tables without RLS enabled
|
|
3778
|
+
\u2022 Gate results: API contract (G3), spec drift (G6), orphan endpoints (G7),
|
|
3779
|
+
unknown frontend calls (G8), schema drift, status claim (G5)
|
|
3780
|
+
|
|
3781
|
+
Returns a PM-readable scorecard with severity-ranked findings.
|
|
3782
|
+
|
|
3783
|
+
Prerequisites:
|
|
3784
|
+
1. Configure your Supabase PAT: mushi settings set supabase-pat <token>
|
|
3785
|
+
2. Set supabase_project_ref in Admin \u2192 Settings \u2192 Project.
|
|
3786
|
+
|
|
3787
|
+
Examples:
|
|
3788
|
+
mushi audit
|
|
3789
|
+
mushi audit --json
|
|
3790
|
+
mushi audit --project-id abc123`).action(async (opts) => {
|
|
3791
|
+
const config = requireConfig();
|
|
3792
|
+
const rawProjectId = opts.projectId ?? config.projectId;
|
|
3793
|
+
if (!rawProjectId) {
|
|
3794
|
+
process.stderr.write("error: project ID required. Run `mushi login` or pass --project-id\n");
|
|
3795
|
+
process.exit(1);
|
|
3796
|
+
}
|
|
3797
|
+
let endpoint;
|
|
3798
|
+
let projectId;
|
|
3799
|
+
try {
|
|
3800
|
+
endpoint = sanitizeEndpoint(config.endpoint);
|
|
3801
|
+
projectId = sanitizeProjectId(rawProjectId);
|
|
3802
|
+
} catch (err) {
|
|
3803
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3804
|
+
process.stderr.write(`error: ${msg}
|
|
3805
|
+
`);
|
|
3806
|
+
process.exit(2);
|
|
3807
|
+
}
|
|
3808
|
+
const headers = {
|
|
3809
|
+
"Content-Type": "application/json",
|
|
3810
|
+
"X-Mushi-Project-Id": projectId
|
|
3811
|
+
};
|
|
3812
|
+
const jwt = config.jwt ?? null;
|
|
3813
|
+
const apiKey = config.apiKey ?? null;
|
|
3814
|
+
if (jwt) {
|
|
3815
|
+
headers["Authorization"] = `Bearer ${jwt.replace(/[\r\n\0]/g, "")}`;
|
|
3816
|
+
} else if (apiKey) {
|
|
3817
|
+
headers["X-Mushi-Api-Key"] = sanitizeApiKey(apiKey);
|
|
3818
|
+
} else {
|
|
3819
|
+
process.stderr.write("error: no credentials found. Run `mushi login` first.\n");
|
|
3820
|
+
process.exit(1);
|
|
3821
|
+
}
|
|
3822
|
+
if (!opts.json) process.stdout.write("Running full-stack audit\u2026 ");
|
|
3823
|
+
try {
|
|
3824
|
+
const controller = new AbortController();
|
|
3825
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
3826
|
+
const res = await fetch(
|
|
3827
|
+
`${endpoint}/v1/admin/projects/${projectId}/audit`,
|
|
3828
|
+
{ method: "POST", headers, body: "{}", signal: controller.signal }
|
|
3829
|
+
);
|
|
3830
|
+
clearTimeout(timer);
|
|
3831
|
+
const body = await res.json();
|
|
3832
|
+
if (!res.ok || !body.ok) {
|
|
3833
|
+
if (opts.json) {
|
|
3834
|
+
console.log(JSON.stringify(body));
|
|
3835
|
+
process.exit(1);
|
|
3836
|
+
}
|
|
3837
|
+
process.stdout.write("FAIL\n");
|
|
3838
|
+
process.stderr.write(`error: ${body.error?.message ?? `HTTP ${res.status}`}
|
|
3839
|
+
`);
|
|
3840
|
+
process.exit(1);
|
|
3841
|
+
}
|
|
3842
|
+
if (opts.json) {
|
|
3843
|
+
console.log(JSON.stringify(body.data, null, 2));
|
|
3844
|
+
return;
|
|
3845
|
+
}
|
|
3846
|
+
const data = body.data;
|
|
3847
|
+
const overallGlyph = data.summary.overall === "fail" ? "\u274C" : data.summary.overall === "warn" ? "\u26A0\uFE0F " : "\u2705";
|
|
3848
|
+
process.stdout.write(`${overallGlyph}
|
|
3849
|
+
|
|
3850
|
+
`);
|
|
3851
|
+
console.log(`Full-Stack Audit \u2014 ${new Date(data.audit_at).toLocaleString()}`);
|
|
3852
|
+
console.log(`Backend linked: ${data.backend_linked ? "yes" : "no (configure Supabase PAT + project ref)"}`);
|
|
3853
|
+
console.log(`Summary: ${data.summary.error_count} error(s) \xB7 ${data.summary.warn_count} warning(s)
|
|
3854
|
+
`);
|
|
3855
|
+
if (data.findings.length === 0) {
|
|
3856
|
+
console.log(" \u2713 No findings. Your project looks healthy.");
|
|
3857
|
+
} else {
|
|
3858
|
+
for (const f of data.findings) {
|
|
3859
|
+
const icon = f.severity === "error" ? "\u{1F534}" : f.severity === "warn" ? "\u{1F7E1}" : "\u2139\uFE0F ";
|
|
3860
|
+
console.log(` ${icon} ${f.title}`);
|
|
3861
|
+
console.log(` ${f.detail.slice(0, 120)}${f.detail.length > 120 ? "\u2026" : ""}`);
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
if (data.gate_runs.length > 0) {
|
|
3865
|
+
console.log("\nGate Results:");
|
|
3866
|
+
for (const run of data.gate_runs) {
|
|
3867
|
+
const g = run.status === "pass" ? "\u2713" : run.status === "fail" ? "\u2717" : "~";
|
|
3868
|
+
console.log(` ${g} ${run.gate.padEnd(22)} ${run.status} (${run.findings_count} finding${run.findings_count !== 1 ? "s" : ""})`);
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
if (data.summary.overall === "fail") process.exit(1);
|
|
3872
|
+
} catch (err) {
|
|
3873
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3874
|
+
if (opts.json) {
|
|
3875
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
3876
|
+
} else {
|
|
3877
|
+
process.stdout.write("ERROR\n");
|
|
3878
|
+
process.stderr.write(`error: ${msg}
|
|
3879
|
+
`);
|
|
3880
|
+
}
|
|
3881
|
+
process.exit(1);
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3376
3884
|
program.parseAsync().catch((err) => {
|
|
3377
3885
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3378
3886
|
process.exit(1);
|
|
3379
3887
|
});
|
|
3888
|
+
var skills = program.command("skills").description("Manage agent skill catalog");
|
|
3889
|
+
skills.command("list").description("List all skills in the catalog").option("--category <cat>", "Filter by category (workflow, debug, test, audit, \u2026)").option("--search <q>", "Search slug, title, or description").option("--page <n>", "Page number (default 1)", "1").option("--limit <n>", "Max results per page (1\u2013200, default 200)", "200").option("--json", "Machine-readable output").action(async (opts) => {
|
|
3890
|
+
const config = loadConfig();
|
|
3891
|
+
if (!config.apiKey) {
|
|
3892
|
+
console.error("Run `mushi login` first");
|
|
3893
|
+
process.exit(2);
|
|
3894
|
+
}
|
|
3895
|
+
const qs = new URLSearchParams();
|
|
3896
|
+
if (opts.category) qs.set("category", opts.category);
|
|
3897
|
+
if (opts.search) qs.set("q", opts.search);
|
|
3898
|
+
qs.set("page", String(Math.max(1, parseInt(opts.page) || 1)));
|
|
3899
|
+
qs.set("limit", String(Math.min(Math.max(1, parseInt(opts.limit) || 200), 200)));
|
|
3900
|
+
const result = await apiCall(
|
|
3901
|
+
`/v1/admin/skills?${qs}`,
|
|
3902
|
+
config
|
|
3903
|
+
);
|
|
3904
|
+
if (!result.ok) {
|
|
3905
|
+
console.error("Failed:", result.error);
|
|
3906
|
+
process.exit(1);
|
|
3907
|
+
}
|
|
3908
|
+
const rows = result.data ?? [];
|
|
3909
|
+
if (opts.json) {
|
|
3910
|
+
console.log(JSON.stringify({ skills: rows, count: rows.length }, null, 2));
|
|
3911
|
+
return;
|
|
3912
|
+
}
|
|
3913
|
+
if (rows.length === 0) {
|
|
3914
|
+
console.log("No skills in catalog. Add a source with `mushi skills sync`.");
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
console.log(`
|
|
3918
|
+
Skill catalog (${rows.length} skills):
|
|
3919
|
+
`);
|
|
3920
|
+
let lastCat = "";
|
|
3921
|
+
for (const s of rows) {
|
|
3922
|
+
if (s.category !== lastCat) {
|
|
3923
|
+
lastCat = s.category;
|
|
3924
|
+
console.log(`
|
|
3925
|
+
[${s.category}]`);
|
|
3926
|
+
}
|
|
3927
|
+
const chain = s.chain_slugs?.length ? ` \u2192 ${s.chain_slugs.length} steps` : "";
|
|
3928
|
+
console.log(` ${s.slug.padEnd(40)} ${s.title}${chain}`);
|
|
3929
|
+
}
|
|
3930
|
+
console.log();
|
|
3931
|
+
});
|
|
3932
|
+
skills.command("show <slug>").description("Show full details and chain for a skill").action(async (slug) => {
|
|
3933
|
+
const config = loadConfig();
|
|
3934
|
+
if (!config.apiKey) {
|
|
3935
|
+
console.error("Run `mushi login` first");
|
|
3936
|
+
process.exit(2);
|
|
3937
|
+
}
|
|
3938
|
+
const result = await apiCall(`/v1/admin/skills/${slug}`, config);
|
|
3939
|
+
if (!result.ok) {
|
|
3940
|
+
console.error("Skill not found:", slug);
|
|
3941
|
+
process.exit(1);
|
|
3942
|
+
}
|
|
3943
|
+
const s = result.data;
|
|
3944
|
+
console.log(`
|
|
3945
|
+
${s.title} (${s.slug})
|
|
3946
|
+
${"\u2500".repeat(50)}`);
|
|
3947
|
+
console.log(`Category: ${s.category}`);
|
|
3948
|
+
console.log(`Chain: ${s.chain_slugs?.length ? s.chain_slugs.join(" \u2192 ") : "none"}`);
|
|
3949
|
+
console.log(`
|
|
3950
|
+
Description:
|
|
3951
|
+
${s.description}
|
|
3952
|
+
`);
|
|
3953
|
+
});
|
|
3954
|
+
skills.command("sync").description("Trigger skill sync for all configured skill sources").option("--source-id <id>", "Sync only a specific source ID").action(async (opts) => {
|
|
3955
|
+
const config = loadConfig();
|
|
3956
|
+
if (!config.apiKey) {
|
|
3957
|
+
console.error("Run `mushi login` first");
|
|
3958
|
+
process.exit(2);
|
|
3959
|
+
}
|
|
3960
|
+
if (!config.projectId) {
|
|
3961
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
3962
|
+
process.exit(2);
|
|
3963
|
+
}
|
|
3964
|
+
let ids;
|
|
3965
|
+
if (opts.sourceId) {
|
|
3966
|
+
ids = [opts.sourceId];
|
|
3967
|
+
} else {
|
|
3968
|
+
const sourcesResult = await apiCall(
|
|
3969
|
+
`/v1/admin/skills/sources?project_id=${config.projectId}`,
|
|
3970
|
+
config
|
|
3971
|
+
);
|
|
3972
|
+
if (!sourcesResult.ok) {
|
|
3973
|
+
console.error("Failed to list sources:", sourcesResult.error);
|
|
3974
|
+
process.exit(1);
|
|
3975
|
+
}
|
|
3976
|
+
ids = (sourcesResult.data ?? []).map((s) => s.id);
|
|
3977
|
+
}
|
|
3978
|
+
if (ids.length === 0) {
|
|
3979
|
+
console.log("No skill sources configured. Add one in the Skill Pipelines console page.");
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
for (const id of ids) {
|
|
3983
|
+
console.log(`Syncing source ${id.slice(0, 8)}\u2026`);
|
|
3984
|
+
const result = await apiCall(
|
|
3985
|
+
`/v1/admin/skills/sources/${id}/sync`,
|
|
3986
|
+
config,
|
|
3987
|
+
{ method: "POST" }
|
|
3988
|
+
);
|
|
3989
|
+
if (!result.ok) {
|
|
3990
|
+
console.error(" Sync failed:", result.error);
|
|
3991
|
+
} else console.log(` Done: ${result.data?.synced ?? 0} synced, ${result.data?.skipped ?? 0} skipped, ${result.data?.errors ?? 0} errors`);
|
|
3992
|
+
}
|
|
3993
|
+
console.log();
|
|
3994
|
+
});
|
|
3995
|
+
var pipeline = program.command("pipeline").description("Manage skill pipeline runs");
|
|
3996
|
+
pipeline.command("start <reportId>").description("Start a skill pipeline for a report").requiredOption("--skill <slug>", "Root skill slug (e.g. workflow-fix-and-ship)").option("--mode <mode>", "Execution mode: handoff (default) or cloud", "handoff").option("--json", "Machine-readable output").action(async (reportId, opts) => {
|
|
3997
|
+
const config = loadConfig();
|
|
3998
|
+
if (!config.apiKey) {
|
|
3999
|
+
console.error("Run `mushi login` first");
|
|
4000
|
+
process.exit(2);
|
|
4001
|
+
}
|
|
4002
|
+
if (!config.projectId) {
|
|
4003
|
+
console.error("No projectId. Run `mushi config projectId <uuid>`");
|
|
4004
|
+
process.exit(2);
|
|
4005
|
+
}
|
|
4006
|
+
const result = await apiCall(
|
|
4007
|
+
`/v1/admin/skills/pipelines`,
|
|
4008
|
+
config,
|
|
4009
|
+
{
|
|
4010
|
+
method: "POST",
|
|
4011
|
+
body: JSON.stringify({
|
|
4012
|
+
project_id: config.projectId,
|
|
4013
|
+
root_skill_slug: opts.skill,
|
|
4014
|
+
report_id: reportId,
|
|
4015
|
+
mode: opts.mode
|
|
4016
|
+
})
|
|
4017
|
+
}
|
|
4018
|
+
);
|
|
4019
|
+
if (!result.ok) {
|
|
4020
|
+
console.error("Failed:", result.error);
|
|
4021
|
+
process.exit(1);
|
|
4022
|
+
}
|
|
4023
|
+
if (opts.json) {
|
|
4024
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
const runId = result.data?.id ?? "";
|
|
4028
|
+
const chain = result.data?.chain_slugs ?? [];
|
|
4029
|
+
console.log(`
|
|
4030
|
+
Pipeline started!
|
|
4031
|
+
`);
|
|
4032
|
+
console.log(` Run ID: ${runId.slice(0, 8)}\u2026 (full: ${runId})`);
|
|
4033
|
+
console.log(` Skill: ${opts.skill}`);
|
|
4034
|
+
console.log(` Chain: ${chain.length > 0 ? chain.join(" \u2192 ") : "(root only)"}`);
|
|
4035
|
+
console.log(` Mode: ${opts.mode}`);
|
|
4036
|
+
if (opts.mode === "handoff") {
|
|
4037
|
+
console.log(`
|
|
4038
|
+
Get context packet: mushi pipeline watch ${runId.slice(0, 8)}`);
|
|
4039
|
+
console.log(` Check in step 0: mushi pipeline checkin ${runId.slice(0, 8)} --step 0 --status passed`);
|
|
4040
|
+
}
|
|
4041
|
+
console.log();
|
|
4042
|
+
});
|
|
4043
|
+
pipeline.command("watch <runIdOrPrefix>").description("Watch a pipeline run and print the context packet").option("--json", "Machine-readable output").action(async (runIdOrPrefix, opts) => {
|
|
4044
|
+
const config = loadConfig();
|
|
4045
|
+
if (!config.apiKey) {
|
|
4046
|
+
console.error("Run `mushi login` first");
|
|
4047
|
+
process.exit(2);
|
|
4048
|
+
}
|
|
4049
|
+
let runId = runIdOrPrefix;
|
|
4050
|
+
if (runIdOrPrefix.length < 36) {
|
|
4051
|
+
const list = await apiCall(
|
|
4052
|
+
`/v1/admin/skills/pipelines?project_id=${config.projectId}&limit=50`,
|
|
4053
|
+
config
|
|
4054
|
+
);
|
|
4055
|
+
if (!list.ok) {
|
|
4056
|
+
console.error("Failed:", list.error);
|
|
4057
|
+
process.exit(1);
|
|
4058
|
+
}
|
|
4059
|
+
const match = list.data.find((r) => r.id.startsWith(runIdOrPrefix));
|
|
4060
|
+
if (!match) {
|
|
4061
|
+
console.error("Run not found:", runIdOrPrefix);
|
|
4062
|
+
process.exit(1);
|
|
4063
|
+
}
|
|
4064
|
+
runId = match.id;
|
|
4065
|
+
}
|
|
4066
|
+
const result = await apiCall(
|
|
4067
|
+
`/v1/admin/skills/pipelines/${runId}`,
|
|
4068
|
+
config
|
|
4069
|
+
);
|
|
4070
|
+
if (!result.ok) {
|
|
4071
|
+
console.error("Failed:", result.error);
|
|
4072
|
+
process.exit(1);
|
|
4073
|
+
}
|
|
4074
|
+
if (opts.json) {
|
|
4075
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
const run = result.data;
|
|
4079
|
+
const statusIcon = run.status === "completed" ? "\u2705" : run.status === "failed" ? "\u274C" : run.status === "running" ? "\u23F3" : "\u26AA";
|
|
4080
|
+
console.log(`
|
|
4081
|
+
${statusIcon} Pipeline ${runId.slice(0, 8)} \xB7 ${run.root_skill_slug} \xB7 ${run.mode}
|
|
4082
|
+
`);
|
|
4083
|
+
const steps = run.steps ?? [];
|
|
4084
|
+
for (const step of steps) {
|
|
4085
|
+
const icon = step.status === "passed" ? "\u2705" : step.status === "failed" ? "\u274C" : step.status === "running" ? "\u23F3" : "\u26AA";
|
|
4086
|
+
const pr = step.pr_url ? ` \u2192 ${step.pr_url}` : "";
|
|
4087
|
+
console.log(` ${icon} Step ${step.step_index + 1}: ${step.skill_slug}${pr}`);
|
|
4088
|
+
if (step.notes) console.log(` ${step.notes}`);
|
|
4089
|
+
}
|
|
4090
|
+
if (run.context_packet) {
|
|
4091
|
+
console.log(`
|
|
4092
|
+
${"\u2500".repeat(60)}`);
|
|
4093
|
+
console.log(`Context Packet (paste into your Cursor agent):
|
|
4094
|
+
`);
|
|
4095
|
+
console.log(run.context_packet.slice(0, 6e3));
|
|
4096
|
+
if (run.context_packet.length > 6e3) console.log("\n\u2026 [truncated \u2014 full packet via --json]");
|
|
4097
|
+
}
|
|
4098
|
+
console.log();
|
|
4099
|
+
});
|
|
4100
|
+
pipeline.command("checkin <runIdOrPrefix>").description("Check in a pipeline step (CLI agent reports status after completing a step)").requiredOption("--step <n>", "Step index (0-based)", parseInt).requiredOption("--status <status>", "Step status: passed | failed | running | skipped").option("--notes <text>", "Optional notes / output summary").option("--pr-url <url>", "PR URL opened during this step").action(async (runIdOrPrefix, opts) => {
|
|
4101
|
+
const config = loadConfig();
|
|
4102
|
+
if (!config.apiKey) {
|
|
4103
|
+
console.error("Run `mushi login` first");
|
|
4104
|
+
process.exit(2);
|
|
4105
|
+
}
|
|
4106
|
+
let runId = runIdOrPrefix;
|
|
4107
|
+
if (runIdOrPrefix.length < 36) {
|
|
4108
|
+
const list = await apiCall(
|
|
4109
|
+
`/v1/admin/skills/pipelines?project_id=${config.projectId}&limit=50`,
|
|
4110
|
+
config
|
|
4111
|
+
);
|
|
4112
|
+
if (!list.ok) {
|
|
4113
|
+
console.error("Failed:", list.error);
|
|
4114
|
+
process.exit(1);
|
|
4115
|
+
}
|
|
4116
|
+
const match = list.data.find((r) => r.id.startsWith(runIdOrPrefix));
|
|
4117
|
+
if (!match) {
|
|
4118
|
+
console.error("Run not found:", runIdOrPrefix);
|
|
4119
|
+
process.exit(1);
|
|
4120
|
+
}
|
|
4121
|
+
runId = match.id;
|
|
4122
|
+
}
|
|
4123
|
+
const result = await apiCall(
|
|
4124
|
+
`/v1/admin/skills/pipelines/${runId}/steps/${opts.step}/checkin`,
|
|
4125
|
+
config,
|
|
4126
|
+
{
|
|
4127
|
+
method: "POST",
|
|
4128
|
+
body: JSON.stringify({ status: opts.status, notes: opts.notes, pr_url: opts.prUrl })
|
|
4129
|
+
}
|
|
4130
|
+
);
|
|
4131
|
+
if (!result.ok) {
|
|
4132
|
+
console.error("Failed:", result.error);
|
|
4133
|
+
process.exit(1);
|
|
4134
|
+
}
|
|
4135
|
+
console.log(` Step ${opts.step} \u2192 ${opts.status}. Console live flow updated.`);
|
|
4136
|
+
console.log(` Next: mushi pipeline watch ${runId.slice(0, 8)}`);
|
|
4137
|
+
console.log();
|
|
4138
|
+
});
|
package/dist/init.js
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED