@mushi-mushi/cli 0.29.2 → 0.30.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 +47 -406
- package/dist/chunk-27FTLLR2.js +10 -0
- package/dist/{chunk-HNLTFQ4N.js → chunk-HIIZZEAH.js} +5 -5
- package/dist/{chunk-KZW4X6WB.js → chunk-LCCMRMPN.js} +5 -5
- package/dist/chunk-WNKM6APL.js +8 -0
- package/dist/{connect-NCBKOV6O.js → connect-KWNFOEUX.js} +2 -2
- package/dist/{connect-RIKV53NI.js → connect-M2YV6EKD.js} +2 -2
- package/dist/index.js +334 -123
- package/dist/init.js +3 -3
- package/dist/{version-QRFWGGZY.js → version-A2FOZS5T.js} +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-4H5TRTM3.js +0 -8
- package/dist/chunk-MKUU3GHS.js +0 -10
package/dist/index.js
CHANGED
|
@@ -53,11 +53,11 @@ import {
|
|
|
53
53
|
setGlobalOutputFormat,
|
|
54
54
|
trySaveKeyToKeychain,
|
|
55
55
|
writeMcpServerEntry
|
|
56
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-LCCMRMPN.js";
|
|
57
57
|
import {
|
|
58
58
|
MUSHI_CLI_VERSION,
|
|
59
59
|
MUSHI_MCP_PIN_SPEC
|
|
60
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-27FTLLR2.js";
|
|
61
61
|
import {
|
|
62
62
|
FRAMEWORKS,
|
|
63
63
|
detectFramework,
|
|
@@ -273,11 +273,11 @@ async function createProject(endpoint, cliToken, name, opts = {}) {
|
|
|
273
273
|
const { id, name: created, slug, apiKey } = json2.data;
|
|
274
274
|
return { id, name: created, slug, apiKey: apiKey ?? null };
|
|
275
275
|
}
|
|
276
|
-
async function mintProjectKey(endpoint, cliToken,
|
|
276
|
+
async function mintProjectKey(endpoint, cliToken, projectId2, opts = {}) {
|
|
277
277
|
const base = trimTrailingSlash(endpoint);
|
|
278
278
|
let res;
|
|
279
279
|
try {
|
|
280
|
-
res = await deviceFetch(`${base}/v1/cli/projects/${
|
|
280
|
+
res = await deviceFetch(`${base}/v1/cli/projects/${projectId2}/keys`, {
|
|
281
281
|
method: "POST",
|
|
282
282
|
headers: cliAuthHeaders(cliToken),
|
|
283
283
|
body: JSON.stringify({
|
|
@@ -299,9 +299,9 @@ async function mintProjectKey(endpoint, cliToken, projectId, opts = {}) {
|
|
|
299
299
|
res.status
|
|
300
300
|
);
|
|
301
301
|
}
|
|
302
|
-
async function revokeProjectKey(endpoint, cliToken,
|
|
302
|
+
async function revokeProjectKey(endpoint, cliToken, projectId2, keyPrefix, newKeyId) {
|
|
303
303
|
const base = trimTrailingSlash(endpoint);
|
|
304
|
-
const url = `${base}/v1/cli/projects/${encodeURIComponent(
|
|
304
|
+
const url = `${base}/v1/cli/projects/${encodeURIComponent(projectId2)}/keys/${encodeURIComponent(keyPrefix)}/revoke`;
|
|
305
305
|
let res;
|
|
306
306
|
try {
|
|
307
307
|
res = await deviceFetch(url, {
|
|
@@ -469,17 +469,17 @@ async function runLogin(opts = {}) {
|
|
|
469
469
|
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
|
|
470
470
|
const consoleBase = await resolveConsoleUrl();
|
|
471
471
|
let apiKey = opts.apiKey;
|
|
472
|
-
let
|
|
472
|
+
let projectId2 = opts.projectId;
|
|
473
473
|
if (apiKey) {
|
|
474
474
|
const config2 = loadConfig();
|
|
475
475
|
config2.apiKey = apiKey;
|
|
476
476
|
config2.endpoint = endpoint;
|
|
477
|
-
if (
|
|
477
|
+
if (projectId2) config2.projectId = projectId2;
|
|
478
478
|
config2.consoleUrl = consoleBase;
|
|
479
479
|
saveConfig(config2);
|
|
480
480
|
console.log("");
|
|
481
481
|
console.log(" \u2713 Credentials saved.");
|
|
482
|
-
const verifyResult = await apiCall("/v1/sync/whoami", { apiKey, endpoint, projectId });
|
|
482
|
+
const verifyResult = await apiCall("/v1/sync/whoami", { apiKey, endpoint, projectId: projectId2 });
|
|
483
483
|
if (!verifyResult.ok) {
|
|
484
484
|
console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
|
|
485
485
|
console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
|
|
@@ -487,7 +487,7 @@ async function runLogin(opts = {}) {
|
|
|
487
487
|
}
|
|
488
488
|
const d = verifyResult.data;
|
|
489
489
|
if (d.project_name) {
|
|
490
|
-
if (!
|
|
490
|
+
if (!projectId2 && d.project_id) {
|
|
491
491
|
config2.projectId = d.project_id;
|
|
492
492
|
saveConfig(config2);
|
|
493
493
|
}
|
|
@@ -544,7 +544,7 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
|
|
|
544
544
|
const cliToken = pollResult.cliToken;
|
|
545
545
|
printAuthApproved();
|
|
546
546
|
const projectsList = await listProjects(endpoint, cliToken);
|
|
547
|
-
let chosenProjectId =
|
|
547
|
+
let chosenProjectId = projectId2;
|
|
548
548
|
let chosenProjectName;
|
|
549
549
|
const mintScopes = opts.upgradeScope ? FULL_CLI_KEY_SCOPES : CLI_KEY_SCOPES;
|
|
550
550
|
if (!chosenProjectId) {
|
|
@@ -589,12 +589,24 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
589
589
|
}
|
|
590
590
|
}
|
|
591
591
|
}
|
|
592
|
+
let mintError = null;
|
|
592
593
|
if (!apiKey && chosenProjectId) {
|
|
593
594
|
try {
|
|
594
595
|
apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId, { scopes: mintScopes }) ?? void 0;
|
|
595
|
-
} catch {
|
|
596
|
+
} catch (err) {
|
|
597
|
+
mintError = err instanceof Error ? err.message : String(err);
|
|
596
598
|
}
|
|
597
599
|
}
|
|
600
|
+
if (mintError) {
|
|
601
|
+
process.stderr.write(`
|
|
602
|
+
\u2717 No key was created for ${chosenProjectName ?? chosenProjectId}: ${mintError}
|
|
603
|
+
`);
|
|
604
|
+
if (/owner or admin|forbidden|403/i.test(mintError)) {
|
|
605
|
+
process.stderr.write(" Only the project's owners and admins can create keys. Ask one of them to make you an admin, then run this again.\n");
|
|
606
|
+
}
|
|
607
|
+
process.stderr.write(" Your saved login was not changed.\n\n");
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
598
610
|
const config = loadConfig();
|
|
599
611
|
config.endpoint = endpoint;
|
|
600
612
|
config.consoleUrl = consoleBase;
|
|
@@ -895,9 +907,9 @@ async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
|
|
|
895
907
|
async function checkSdkInstall(cwd) {
|
|
896
908
|
try {
|
|
897
909
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
898
|
-
const { join:
|
|
910
|
+
const { join: join11, resolve: resolve7 } = await import("path");
|
|
899
911
|
const root = resolve7(cwd);
|
|
900
|
-
const pkgPath =
|
|
912
|
+
const pkgPath = join11(root, "package.json");
|
|
901
913
|
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
902
914
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
903
915
|
const sdks = [
|
|
@@ -932,9 +944,9 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
932
944
|
];
|
|
933
945
|
}
|
|
934
946
|
try {
|
|
935
|
-
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
936
|
-
const res = await doFetch(`${endpoint}/v1/admin/projects/${
|
|
937
|
-
headers: apiKeyHeaders(apiKey,
|
|
947
|
+
const { endpoint, apiKey, projectId: projectId2 } = sanitizeCliCredentials(config);
|
|
948
|
+
const res = await doFetch(`${endpoint}/v1/admin/projects/${projectId2}/preflight`, {
|
|
949
|
+
headers: apiKeyHeaders(apiKey, projectId2),
|
|
938
950
|
signal: AbortSignal.timeout(8e3)
|
|
939
951
|
});
|
|
940
952
|
if (res.ok) {
|
|
@@ -1032,9 +1044,9 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
|
1032
1044
|
}
|
|
1033
1045
|
const checks = [];
|
|
1034
1046
|
try {
|
|
1035
|
-
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
1036
|
-
const headers = apiKeyHeaders(apiKey,
|
|
1037
|
-
const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${
|
|
1047
|
+
const { endpoint, apiKey, projectId: projectId2 } = sanitizeCliCredentials(config);
|
|
1048
|
+
const headers = apiKeyHeaders(apiKey, projectId2);
|
|
1049
|
+
const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${projectId2}/qa-coverage`, {
|
|
1038
1050
|
headers,
|
|
1039
1051
|
signal: AbortSignal.timeout(8e3)
|
|
1040
1052
|
});
|
|
@@ -1063,7 +1075,7 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
|
1063
1075
|
detail: `${enabled.length} enabled story/stories configured`
|
|
1064
1076
|
});
|
|
1065
1077
|
const slackRes = await doFetch(
|
|
1066
|
-
`${endpoint}/v1/admin/projects/${
|
|
1078
|
+
`${endpoint}/v1/admin/projects/${projectId2}/integrations/probe/slack`,
|
|
1067
1079
|
{
|
|
1068
1080
|
method: "POST",
|
|
1069
1081
|
headers,
|
|
@@ -1078,7 +1090,7 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
|
1078
1090
|
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."
|
|
1079
1091
|
});
|
|
1080
1092
|
const fcRes = await doFetch(
|
|
1081
|
-
`${endpoint}/v1/admin/projects/${
|
|
1093
|
+
`${endpoint}/v1/admin/projects/${projectId2}/integrations/probe/firecrawl`,
|
|
1082
1094
|
{
|
|
1083
1095
|
method: "POST",
|
|
1084
1096
|
headers,
|
|
@@ -1107,9 +1119,9 @@ async function checkHostAppWiring(cwd) {
|
|
|
1107
1119
|
const checks = [];
|
|
1108
1120
|
try {
|
|
1109
1121
|
const { readFile: readFile3, access } = await import("fs/promises");
|
|
1110
|
-
const { join:
|
|
1122
|
+
const { join: join11, resolve: resolve7 } = await import("path");
|
|
1111
1123
|
const root = resolve7(cwd);
|
|
1112
|
-
const pkgPath =
|
|
1124
|
+
const pkgPath = join11(root, "package.json");
|
|
1113
1125
|
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
1114
1126
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1115
1127
|
const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
|
|
@@ -1117,7 +1129,7 @@ async function checkHostAppWiring(cwd) {
|
|
|
1117
1129
|
let envContent = "";
|
|
1118
1130
|
for (const f of envCandidates) {
|
|
1119
1131
|
try {
|
|
1120
|
-
envContent = await readFile3(
|
|
1132
|
+
envContent = await readFile3(join11(root, f), "utf8");
|
|
1121
1133
|
break;
|
|
1122
1134
|
} catch {
|
|
1123
1135
|
}
|
|
@@ -1132,9 +1144,9 @@ async function checkHostAppWiring(cwd) {
|
|
|
1132
1144
|
detail: hasProjectId && hasApiKey ? "VITE_/MUSHI_ project id + API key found" : "Run `mushi connect --write-env` or add VITE_MUSHI_PROJECT_ID + VITE_MUSHI_API_KEY"
|
|
1133
1145
|
});
|
|
1134
1146
|
let mcpFound = null;
|
|
1135
|
-
for (const rel of [".mcp.json",
|
|
1147
|
+
for (const rel of [".mcp.json", join11(".cursor", "mcp.json")]) {
|
|
1136
1148
|
try {
|
|
1137
|
-
await access(
|
|
1149
|
+
await access(join11(root, rel));
|
|
1138
1150
|
mcpFound = rel;
|
|
1139
1151
|
break;
|
|
1140
1152
|
} catch {
|
|
@@ -1176,7 +1188,7 @@ var HASH_ROUTER_PATTERNS = [
|
|
|
1176
1188
|
var HASH_TEMPLATE_PATTERN = /['"`]#\//;
|
|
1177
1189
|
async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
|
|
1178
1190
|
const { readdir: readdir2, readFile: readFile3 } = await import("fs/promises");
|
|
1179
|
-
const { join:
|
|
1191
|
+
const { join: join11 } = await import("path");
|
|
1180
1192
|
let count = 0;
|
|
1181
1193
|
async function walk2(d, depth) {
|
|
1182
1194
|
if (depth > maxDepth || count > maxFiles) return null;
|
|
@@ -1188,7 +1200,7 @@ async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
|
|
|
1188
1200
|
}
|
|
1189
1201
|
for (const e of entries) {
|
|
1190
1202
|
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue;
|
|
1191
|
-
const full =
|
|
1203
|
+
const full = join11(d, e.name);
|
|
1192
1204
|
if (e.isDirectory()) {
|
|
1193
1205
|
const found = await walk2(full, depth + 1);
|
|
1194
1206
|
if (found) return found;
|
|
@@ -1205,7 +1217,7 @@ async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
|
|
|
1205
1217
|
}
|
|
1206
1218
|
const { resolve: resolve7 } = await import("path");
|
|
1207
1219
|
for (const sub of ["src", "app", "pages", "lib", "components"]) {
|
|
1208
|
-
const found = await walk2(
|
|
1220
|
+
const found = await walk2(join11(resolve7(dir), sub), 0);
|
|
1209
1221
|
if (found) return found;
|
|
1210
1222
|
}
|
|
1211
1223
|
return walk2(resolve7(dir), 0);
|
|
@@ -1241,13 +1253,13 @@ function normalizeProjectPath(path) {
|
|
|
1241
1253
|
}
|
|
1242
1254
|
async function collectMcpSources(root, home) {
|
|
1243
1255
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
1244
|
-
const { join:
|
|
1256
|
+
const { join: join11 } = await import("path");
|
|
1245
1257
|
const sources = [];
|
|
1246
1258
|
const invalid = [];
|
|
1247
1259
|
const plainFiles = [
|
|
1248
|
-
{ path:
|
|
1249
|
-
{ path:
|
|
1250
|
-
{ path:
|
|
1260
|
+
{ path: join11(root, ".mcp.json"), claudeCode: true },
|
|
1261
|
+
{ path: join11(root, ".cursor", "mcp.json"), claudeCode: false },
|
|
1262
|
+
{ path: join11(home, ".cursor", "mcp.json"), claudeCode: false }
|
|
1251
1263
|
];
|
|
1252
1264
|
for (const file of plainFiles) {
|
|
1253
1265
|
let raw;
|
|
@@ -1264,7 +1276,7 @@ async function collectMcpSources(root, home) {
|
|
|
1264
1276
|
invalid.push(file.path);
|
|
1265
1277
|
}
|
|
1266
1278
|
}
|
|
1267
|
-
const claudeJsonPath =
|
|
1279
|
+
const claudeJsonPath = join11(home, ".claude.json");
|
|
1268
1280
|
try {
|
|
1269
1281
|
const parsed = JSON.parse(await readFile3(claudeJsonPath, "utf8"));
|
|
1270
1282
|
if (isJsonObject(parsed)) {
|
|
@@ -1441,12 +1453,12 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
|
|
|
1441
1453
|
}
|
|
1442
1454
|
try {
|
|
1443
1455
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
1444
|
-
const { join:
|
|
1456
|
+
const { join: join11, resolve: resolve7 } = await import("path");
|
|
1445
1457
|
const root = resolve7(cwd);
|
|
1446
1458
|
let envContent = "";
|
|
1447
1459
|
for (const f of [".env.local", ".env"]) {
|
|
1448
1460
|
try {
|
|
1449
|
-
envContent = await readFile3(
|
|
1461
|
+
envContent = await readFile3(join11(root, f), "utf8");
|
|
1450
1462
|
break;
|
|
1451
1463
|
} catch {
|
|
1452
1464
|
}
|
|
@@ -1512,7 +1524,7 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
|
|
|
1512
1524
|
}
|
|
1513
1525
|
async function checkVersionDrift(cwd, doFetch) {
|
|
1514
1526
|
const checks = [];
|
|
1515
|
-
const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-
|
|
1527
|
+
const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-A2FOZS5T.js");
|
|
1516
1528
|
if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
|
|
1517
1529
|
const parse2 = (v) => {
|
|
1518
1530
|
const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
@@ -1542,8 +1554,8 @@ async function checkVersionDrift(cwd, doFetch) {
|
|
|
1542
1554
|
}
|
|
1543
1555
|
try {
|
|
1544
1556
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
1545
|
-
const { join:
|
|
1546
|
-
const pkg = JSON.parse(await readFile3(
|
|
1557
|
+
const { join: join11 } = await import("path");
|
|
1558
|
+
const pkg = JSON.parse(await readFile3(join11(cwd, "package.json"), "utf8"));
|
|
1547
1559
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1548
1560
|
const candidates = Object.entries(deps).filter(([name]) => name.startsWith("@mushi-mushi/")).filter(([, v]) => /^[~^]?\d/.test(v));
|
|
1549
1561
|
const drifted = (await Promise.all(
|
|
@@ -1847,10 +1859,10 @@ async function chooseFramework(detected, options) {
|
|
|
1847
1859
|
async function acquireCredentials(options, consoleBase, endpoint) {
|
|
1848
1860
|
const probe = (e, k, pid) => probeKeyScope(e, k, pid);
|
|
1849
1861
|
if (options.projectId && options.apiKey) {
|
|
1850
|
-
const
|
|
1862
|
+
const projectId2 = sanitizeSecret(options.projectId);
|
|
1851
1863
|
const sdkKey = sanitizeSecret(options.apiKey);
|
|
1852
|
-
await assertUserKeyIsIngestOnly(sdkKey,
|
|
1853
|
-
return { projectId, sdkKey };
|
|
1864
|
+
await assertUserKeyIsIngestOnly(sdkKey, projectId2, endpoint, probe);
|
|
1865
|
+
return { projectId: projectId2, sdkKey };
|
|
1854
1866
|
}
|
|
1855
1867
|
const existing = loadConfig();
|
|
1856
1868
|
maybeShowTelemetryNotice(existing);
|
|
@@ -1927,8 +1939,8 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
1927
1939
|
}
|
|
1928
1940
|
return collectCredentialsManually(options, consoleBase, endpoint, probe);
|
|
1929
1941
|
}
|
|
1930
|
-
async function assertUserKeyIsIngestOnly(key,
|
|
1931
|
-
const result = await probe(endpoint, key,
|
|
1942
|
+
async function assertUserKeyIsIngestOnly(key, projectId2, endpoint, probe) {
|
|
1943
|
+
const result = await probe(endpoint, key, projectId2);
|
|
1932
1944
|
if (result.result === "mcp" || result.result === "unknown") {
|
|
1933
1945
|
throw new Error(
|
|
1934
1946
|
`Not writing this API key into your app's env vars: ${describeUnsafeSdkKey(result, endpoint)}. Use an ingest-only (report:write) key from the console, or leave out --api-key to sign in with the browser, which mints one.`
|
|
@@ -1936,40 +1948,40 @@ async function assertUserKeyIsIngestOnly(key, projectId, endpoint, probe) {
|
|
|
1936
1948
|
}
|
|
1937
1949
|
}
|
|
1938
1950
|
async function resolveSavedCredentials(saved, endpoint, probe) {
|
|
1939
|
-
const
|
|
1951
|
+
const projectId2 = sanitizeSecret(saved.projectId);
|
|
1940
1952
|
const apiKey = sanitizeSecret(saved.apiKey);
|
|
1941
|
-
const savedSdkKey = savedSdkKeyFor(saved,
|
|
1953
|
+
const savedSdkKey = savedSdkKeyFor(saved, projectId2);
|
|
1942
1954
|
if (savedSdkKey) {
|
|
1943
|
-
const sdkProbe = await probe(endpoint, savedSdkKey,
|
|
1955
|
+
const sdkProbe = await probe(endpoint, savedSdkKey, projectId2);
|
|
1944
1956
|
if (sdkProbe.result === "ingest-only") {
|
|
1945
|
-
return { kind: "ready", credentials: { projectId, sdkKey: savedSdkKey, cliKey: apiKey } };
|
|
1957
|
+
return { kind: "ready", credentials: { projectId: projectId2, sdkKey: savedSdkKey, cliKey: apiKey } };
|
|
1946
1958
|
}
|
|
1947
1959
|
}
|
|
1948
|
-
const cliProbe = await probe(endpoint, apiKey,
|
|
1960
|
+
const cliProbe = await probe(endpoint, apiKey, projectId2);
|
|
1949
1961
|
if (cliProbe.result === "ingest-only") {
|
|
1950
|
-
return { kind: "ready", credentials: { projectId, sdkKey: apiKey } };
|
|
1962
|
+
return { kind: "ready", credentials: { projectId: projectId2, sdkKey: apiKey } };
|
|
1951
1963
|
}
|
|
1952
1964
|
if (cliProbe.result === "mcp") {
|
|
1953
|
-
return { kind: "needs-sdk-key", projectId, cliKey: apiKey };
|
|
1965
|
+
return { kind: "needs-sdk-key", projectId: projectId2, cliKey: apiKey };
|
|
1954
1966
|
}
|
|
1955
1967
|
return { kind: "unusable", reason: describeUnsafeSdkKey(cliProbe, endpoint) };
|
|
1956
1968
|
}
|
|
1957
1969
|
async function mintWizardKeys(deps, args) {
|
|
1958
|
-
let
|
|
1970
|
+
let projectId2;
|
|
1959
1971
|
let createdName;
|
|
1960
1972
|
let sdkKey = null;
|
|
1961
1973
|
if (args.target.kind === "new") {
|
|
1962
1974
|
const created = await deps.createProject(args.endpoint, args.cliToken, args.target.name, {
|
|
1963
1975
|
scopes: SDK_KEY_SCOPES
|
|
1964
1976
|
});
|
|
1965
|
-
|
|
1977
|
+
projectId2 = created.id;
|
|
1966
1978
|
createdName = created.name;
|
|
1967
1979
|
sdkKey = created.apiKey;
|
|
1968
1980
|
} else {
|
|
1969
|
-
|
|
1981
|
+
projectId2 = args.target.projectId;
|
|
1970
1982
|
}
|
|
1971
1983
|
if (!sdkKey) {
|
|
1972
|
-
sdkKey = await deps.mintProjectKey(args.endpoint, args.cliToken,
|
|
1984
|
+
sdkKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId2, {
|
|
1973
1985
|
scopes: SDK_KEY_SCOPES,
|
|
1974
1986
|
label: SDK_KEY_LABEL
|
|
1975
1987
|
});
|
|
@@ -1978,7 +1990,7 @@ async function mintWizardKeys(deps, args) {
|
|
|
1978
1990
|
let cliKeyError;
|
|
1979
1991
|
if (args.withCliKey) {
|
|
1980
1992
|
try {
|
|
1981
|
-
cliKey = await deps.mintProjectKey(args.endpoint, args.cliToken,
|
|
1993
|
+
cliKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId2, {
|
|
1982
1994
|
scopes: CLI_KEY_SCOPES,
|
|
1983
1995
|
label: CLI_KEY_LABEL
|
|
1984
1996
|
});
|
|
@@ -1986,7 +1998,7 @@ async function mintWizardKeys(deps, args) {
|
|
|
1986
1998
|
cliKeyError = err instanceof Error ? err.message : String(err);
|
|
1987
1999
|
}
|
|
1988
2000
|
}
|
|
1989
|
-
return { projectId, createdName, sdkKey, cliKey, cliKeyError };
|
|
2001
|
+
return { projectId: projectId2, createdName, sdkKey, cliKey, cliKeyError };
|
|
1990
2002
|
}
|
|
1991
2003
|
async function runBrowserSignIn(options, endpoint, consoleBase, preset) {
|
|
1992
2004
|
const startSpin = p.spinner();
|
|
@@ -2155,11 +2167,11 @@ async function collectCredentialsManually(options, consoleBase, endpoint, probe)
|
|
|
2155
2167
|
hint: apiKeyHint(consoleBase),
|
|
2156
2168
|
validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
|
|
2157
2169
|
});
|
|
2158
|
-
const
|
|
2170
|
+
const projectId2 = sanitizeSecret(rawProjectId);
|
|
2159
2171
|
const apiKey = sanitizeSecret(rawApiKey);
|
|
2160
|
-
if (!PROJECT_ID_PATTERN.test(
|
|
2172
|
+
if (!PROJECT_ID_PATTERN.test(projectId2)) {
|
|
2161
2173
|
throw new Error(
|
|
2162
|
-
`Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(
|
|
2174
|
+
`Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId2)} \u2014 copy it from ${projectIdHint(consoleBase)}`
|
|
2163
2175
|
);
|
|
2164
2176
|
}
|
|
2165
2177
|
if (!API_KEY_PATTERN.test(apiKey)) {
|
|
@@ -2168,9 +2180,9 @@ async function collectCredentialsManually(options, consoleBase, endpoint, probe)
|
|
|
2168
2180
|
);
|
|
2169
2181
|
}
|
|
2170
2182
|
if (apiKey !== savedApiKey) {
|
|
2171
|
-
await assertUserKeyIsIngestOnly(apiKey,
|
|
2183
|
+
await assertUserKeyIsIngestOnly(apiKey, projectId2, endpoint, probe);
|
|
2172
2184
|
}
|
|
2173
|
-
return { projectId, sdkKey: apiKey };
|
|
2185
|
+
return { projectId: projectId2, sdkKey: apiKey };
|
|
2174
2186
|
}
|
|
2175
2187
|
async function verifyCredentials(credentials, options, consoleBase) {
|
|
2176
2188
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
@@ -2267,10 +2279,10 @@ function runCommand(pm, packages, cwd) {
|
|
|
2267
2279
|
});
|
|
2268
2280
|
});
|
|
2269
2281
|
}
|
|
2270
|
-
async function writeEnvFile(cwd, apiKey,
|
|
2282
|
+
async function writeEnvFile(cwd, apiKey, projectId2, framework, endpoint, overwrite) {
|
|
2271
2283
|
const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
|
|
2272
2284
|
const targetPath = join2(cwd, target);
|
|
2273
|
-
const newVars = envVarsToWrite(apiKey,
|
|
2285
|
+
const newVars = envVarsToWrite(apiKey, projectId2, framework, endpoint);
|
|
2274
2286
|
let existing = "";
|
|
2275
2287
|
try {
|
|
2276
2288
|
existing = readFileSync2(targetPath, "utf-8");
|
|
@@ -2436,7 +2448,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
2436
2448
|
if (p.isCancel(answer) || !answer) return;
|
|
2437
2449
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
2438
2450
|
try {
|
|
2439
|
-
const { runConnect: runConnect2 } = await import("./connect-
|
|
2451
|
+
const { runConnect: runConnect2 } = await import("./connect-KWNFOEUX.js");
|
|
2440
2452
|
const saved = loadConfig();
|
|
2441
2453
|
const result = await runConnect2(
|
|
2442
2454
|
{
|
|
@@ -3736,7 +3748,7 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
|
|
|
3736
3748
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3737
3749
|
const ask = (q) => new Promise((resolve7) => rl.question(q, (a) => resolve7(a.trim())));
|
|
3738
3750
|
let chosen;
|
|
3739
|
-
let
|
|
3751
|
+
let projectId2;
|
|
3740
3752
|
let projectName;
|
|
3741
3753
|
let apiKey;
|
|
3742
3754
|
if (!opts.name && projectsList.length > 0) {
|
|
@@ -3751,11 +3763,11 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
|
|
|
3751
3763
|
const num2 = parseInt(choice, 10);
|
|
3752
3764
|
if (num2 >= 1 && num2 <= projectsList.length) {
|
|
3753
3765
|
chosen = projectsList[num2 - 1];
|
|
3754
|
-
|
|
3766
|
+
projectId2 = chosen.id;
|
|
3755
3767
|
projectName = chosen.name;
|
|
3756
3768
|
}
|
|
3757
3769
|
}
|
|
3758
|
-
if (!
|
|
3770
|
+
if (!projectId2) {
|
|
3759
3771
|
const newName = opts.name?.trim() || await ask(" Project name: ");
|
|
3760
3772
|
if (!newName) {
|
|
3761
3773
|
rl.close();
|
|
@@ -3764,7 +3776,7 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
|
|
|
3764
3776
|
}
|
|
3765
3777
|
try {
|
|
3766
3778
|
const created = await createProject(endpoint, cliToken, newName);
|
|
3767
|
-
|
|
3779
|
+
projectId2 = created.id;
|
|
3768
3780
|
projectName = created.name;
|
|
3769
3781
|
apiKey = created.apiKey ?? void 0;
|
|
3770
3782
|
console.log(` \u2713 Created project "${projectName}"`);
|
|
@@ -3777,22 +3789,22 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
3777
3789
|
}
|
|
3778
3790
|
}
|
|
3779
3791
|
rl.close();
|
|
3780
|
-
if (
|
|
3781
|
-
apiKey = await mintProjectKey(endpoint, cliToken,
|
|
3792
|
+
if (projectId2 && !apiKey) {
|
|
3793
|
+
apiKey = await mintProjectKey(endpoint, cliToken, projectId2) ?? void 0;
|
|
3782
3794
|
}
|
|
3783
|
-
if (!
|
|
3795
|
+
if (!projectId2 || !apiKey) {
|
|
3784
3796
|
process.stderr.write("\nerror: Could not obtain an SDK key for the project. Run `mushi login` and try again.\n");
|
|
3785
3797
|
process.exit(1);
|
|
3786
3798
|
}
|
|
3787
3799
|
const config = loadConfig();
|
|
3788
3800
|
config.apiKey = apiKey;
|
|
3789
3801
|
config.endpoint = endpoint;
|
|
3790
|
-
config.projectId =
|
|
3802
|
+
config.projectId = projectId2;
|
|
3791
3803
|
config.consoleUrl = consoleBase;
|
|
3792
3804
|
saveConfig(config);
|
|
3793
3805
|
const { envUpdated, mcpUpdated } = await writeProjectBootstrapFiles({
|
|
3794
3806
|
endpoint,
|
|
3795
|
-
projectId,
|
|
3807
|
+
projectId: projectId2,
|
|
3796
3808
|
apiKey
|
|
3797
3809
|
});
|
|
3798
3810
|
console.log(`
|
|
@@ -5079,9 +5091,9 @@ function registerDoctorCommand(program2) {
|
|
|
5079
5091
|
function registerResetCommand(program2) {
|
|
5080
5092
|
program2.command("reset [projectId]").description(
|
|
5081
5093
|
"Archive a project and wipe its test data (codebase_files, fix_attempts, reports). Speeds up re-running the full onboarding flow from scratch. Requires `--confirm` to prevent accidents."
|
|
5082
|
-
).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (
|
|
5094
|
+
).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (projectId2, opts) => {
|
|
5083
5095
|
const config = loadConfig();
|
|
5084
|
-
const resolvedId =
|
|
5096
|
+
const resolvedId = projectId2 ?? config.projectId;
|
|
5085
5097
|
if (!config.apiKey) {
|
|
5086
5098
|
console.error("Run `mushi login` first");
|
|
5087
5099
|
process.exit(1);
|
|
@@ -5383,7 +5395,7 @@ function registerKeysCommands(program2) {
|
|
|
5383
5395
|
}
|
|
5384
5396
|
);
|
|
5385
5397
|
});
|
|
5386
|
-
keys.command("add").description("Add a new API key to the pool").requiredOption("--provider <p>", "Provider: anthropic, openai, firecrawl, browserbase, cursor, supabase").option(
|
|
5398
|
+
keys.command("add").description("Add a new API key to the pool").requiredOption("--provider <p>", "Provider: anthropic, openai, openrouter, firecrawl, browserbase, cursor, supabase").option(
|
|
5387
5399
|
"--key <k>",
|
|
5388
5400
|
"The API key value (prefer the MUSHI_BYOK_KEY env var to keep it out of shell history)"
|
|
5389
5401
|
).option("--label <l>", "Human-readable label").option("--priority <n>", "Priority (lower = higher priority)", "100").option("--base-url <url>", "Allow-listed OpenAI-compatible HTTPS base URL").action(
|
|
@@ -5872,11 +5884,11 @@ Examples:
|
|
|
5872
5884
|
throw new MushiCliError("E_PROJECT_MISSING", "Project ID not configured.", "pass --project-id <id> or run `mushi login --project-id <id>`");
|
|
5873
5885
|
}
|
|
5874
5886
|
let endpoint;
|
|
5875
|
-
let
|
|
5887
|
+
let projectId2;
|
|
5876
5888
|
let apiKey;
|
|
5877
5889
|
try {
|
|
5878
5890
|
endpoint = sanitizeEndpoint(config.endpoint);
|
|
5879
|
-
|
|
5891
|
+
projectId2 = sanitizeProjectId(rawProjectId);
|
|
5880
5892
|
apiKey = sanitizeApiKey(config.apiKey);
|
|
5881
5893
|
} catch (err) {
|
|
5882
5894
|
throw new MushiCliError("E_INVALID_INPUT", err instanceof Error ? err.message : String(err));
|
|
@@ -5884,9 +5896,9 @@ Examples:
|
|
|
5884
5896
|
const json2 = outputIsJson(opts.json);
|
|
5885
5897
|
if (!json2) process.stdout.write("Running full-stack audit\u2026 ");
|
|
5886
5898
|
const result = await apiCall(
|
|
5887
|
-
`/v1/admin/projects/${
|
|
5899
|
+
`/v1/admin/projects/${projectId2}/audit`,
|
|
5888
5900
|
{ ...config, endpoint, apiKey },
|
|
5889
|
-
{ method: "POST", body: "{}", headers: { "X-Mushi-Project-Id":
|
|
5901
|
+
{ method: "POST", body: "{}", headers: { "X-Mushi-Project-Id": projectId2 } },
|
|
5890
5902
|
{ timeoutMs: AUDIT_TIMEOUT_MS }
|
|
5891
5903
|
);
|
|
5892
5904
|
if (!result.ok) {
|
|
@@ -5926,6 +5938,14 @@ Examples:
|
|
|
5926
5938
|
console.log(` ${g} ${run2.gate.padEnd(22)} ${run2.status} (${run2.findings_count} finding${run2.findings_count !== 1 ? "s" : ""})`);
|
|
5927
5939
|
}
|
|
5928
5940
|
}
|
|
5941
|
+
const refresh = data.gate_refresh;
|
|
5942
|
+
if (refresh && (refresh.triggered.length > 0 || refresh.skipped.length > 0)) {
|
|
5943
|
+
console.log("\nGate re-runs:");
|
|
5944
|
+
if (refresh.triggered.length > 0) {
|
|
5945
|
+
console.log(` Started: ${refresh.triggered.join(", ")} (results show in the next audit)`);
|
|
5946
|
+
}
|
|
5947
|
+
for (const s of refresh.skipped) console.log(` Not re-run: ${s.gate.padEnd(16)} ${oneLine(s.reason, 110)}`);
|
|
5948
|
+
}
|
|
5929
5949
|
console.log("\nEach gate finding with its file and line: mushi audit findings");
|
|
5930
5950
|
if (data.summary.overall === "fail") process.exit(1);
|
|
5931
5951
|
});
|
|
@@ -5939,12 +5959,12 @@ Examples:
|
|
|
5939
5959
|
throw new MushiCliError("E_INVALID_INPUT", "--severity must be info, warn or error");
|
|
5940
5960
|
}
|
|
5941
5961
|
const config = requireConfig();
|
|
5942
|
-
const
|
|
5962
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
5943
5963
|
const qs = new URLSearchParams();
|
|
5944
5964
|
if (opts.gate) qs.set("gate", opts.gate);
|
|
5945
5965
|
if (opts.severity) qs.set("severity", opts.severity);
|
|
5946
5966
|
const suffix = qs.toString() ? `?${qs}` : "";
|
|
5947
|
-
const result = await apiCall(`/v1/admin/inventory/${
|
|
5967
|
+
const result = await apiCall(`/v1/admin/inventory/${projectId2}/findings${suffix}`, config);
|
|
5948
5968
|
if (!result.ok) {
|
|
5949
5969
|
if (result.error.code === "feature_not_in_plan") {
|
|
5950
5970
|
process.stderr.write("The per-finding list is not on your plan. `mushi audit` (no subcommand) still runs the summary audit.\n");
|
|
@@ -7316,8 +7336,8 @@ Add one step to your existing CI job, after the build step (no new job needed):
|
|
|
7316
7336
|
});
|
|
7317
7337
|
radar.command("show").description("Show every hole check for this project and what each one found").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
7318
7338
|
const config = requireConfig({ needsProject: !opts.projectId });
|
|
7319
|
-
const
|
|
7320
|
-
const result = await apiCall(`/v1/admin/projects/${
|
|
7339
|
+
const projectId2 = requireUuid(opts.projectId ?? config.projectId, "project id");
|
|
7340
|
+
const result = await apiCall(`/v1/admin/projects/${projectId2}/radar`, config);
|
|
7321
7341
|
if (!result.ok) die(result);
|
|
7322
7342
|
if (outputIsJson(opts.json)) {
|
|
7323
7343
|
console.log(JSON.stringify(result.data, null, 2));
|
|
@@ -9088,8 +9108,8 @@ function registerRecipeCommands(program2) {
|
|
|
9088
9108
|
});
|
|
9089
9109
|
recipe.command("show").description("Show this project's recipe: each part and its state").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
9090
9110
|
const config = requireConfig({ needsProject: !opts.projectId });
|
|
9091
|
-
const
|
|
9092
|
-
const res = await apiCall(`/v1/admin/projects/${
|
|
9111
|
+
const projectId2 = requireUuid(opts.projectId ?? config.projectId, "project id");
|
|
9112
|
+
const res = await apiCall(`/v1/admin/projects/${projectId2}/recipe`, config);
|
|
9093
9113
|
if (!res.ok) die(res);
|
|
9094
9114
|
if (outputIsJson(opts.json)) {
|
|
9095
9115
|
console.log(JSON.stringify(res.data, null, 2));
|
|
@@ -9103,8 +9123,8 @@ function registerRecipeCommands(program2) {
|
|
|
9103
9123
|
throw new MushiCliError("E_INVALID_INPUT", `--element must be one of ${SOURCE_ELEMENTS.join(", ")}`);
|
|
9104
9124
|
}
|
|
9105
9125
|
const config = requireConfig({ needsProject: !opts.projectId });
|
|
9106
|
-
const
|
|
9107
|
-
const res = await apiCall(`/v1/admin/projects/${
|
|
9126
|
+
const projectId2 = requireUuid(opts.projectId ?? config.projectId, "project id");
|
|
9127
|
+
const res = await apiCall(`/v1/admin/projects/${projectId2}/recipe/sources?element=${opts.element}`, config);
|
|
9108
9128
|
if (!res.ok) die(res);
|
|
9109
9129
|
if (outputIsJson(opts.json)) {
|
|
9110
9130
|
console.log(JSON.stringify(res.data, null, 2));
|
|
@@ -9123,8 +9143,8 @@ function registerRecipeCommands(program2) {
|
|
|
9123
9143
|
recipe.command("change <jobId>").description("One recipe change job: queued, running, pr_opened, rejected or failed").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (jobId, opts) => {
|
|
9124
9144
|
const id = requireUuid(jobId, "job id");
|
|
9125
9145
|
const config = requireConfig({ needsProject: !opts.projectId });
|
|
9126
|
-
const
|
|
9127
|
-
const res = await apiCall(`/v1/admin/projects/${
|
|
9146
|
+
const projectId2 = requireUuid(opts.projectId ?? config.projectId, "project id");
|
|
9147
|
+
const res = await apiCall(`/v1/admin/projects/${projectId2}/recipe/changes/${id}`, config);
|
|
9128
9148
|
if (!res.ok) die(res);
|
|
9129
9149
|
if (outputIsJson(opts.json)) {
|
|
9130
9150
|
console.log(JSON.stringify(res.data, null, 2));
|
|
@@ -9418,10 +9438,11 @@ function renderPortfolioFindings(data) {
|
|
|
9418
9438
|
}
|
|
9419
9439
|
function registerPortfolioCommands(program2) {
|
|
9420
9440
|
const portfolio = program2.command("portfolio").description("Every app in your team on one screen (needs an account-level key)");
|
|
9421
|
-
portfolio.command("show").description("Health, open reports, holes, SDK version and last release for each app").option(ORG_HELP, ORG_DESC).option("--page <n>", "Page of apps (25 per page)", "1").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
9441
|
+
portfolio.command("show").description("Health, open reports, holes, SDK version and last release for each app").option(ORG_HELP, ORG_DESC).option("--page <n>", "Page of apps (25 per page)", "1").option("--group <slug>", "Only the apps in this project group (see `mushi portfolio groups list`)").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
9422
9442
|
const config = requireConfig();
|
|
9423
9443
|
const page = Math.max(1, Number.parseInt(opts.page, 10) || 1);
|
|
9424
|
-
const
|
|
9444
|
+
const group = opts.group ? `&group=${encodeURIComponent(opts.group)}` : "";
|
|
9445
|
+
const result = await apiCall(`/v1/admin/orgs/${orgSegment(opts.org)}/portfolio?page=${page}${group}`, config);
|
|
9425
9446
|
if (!result.ok) dieOrgError(result);
|
|
9426
9447
|
if (outputIsJson(opts.json)) {
|
|
9427
9448
|
console.log(JSON.stringify(result.data, null, 2));
|
|
@@ -9449,6 +9470,60 @@ function registerPortfolioCommands(program2) {
|
|
|
9449
9470
|
}
|
|
9450
9471
|
for (const line of renderPortfolioResources(result.data)) console.log(line);
|
|
9451
9472
|
});
|
|
9473
|
+
const groups = portfolio.command("groups").description("Named groups of apps in your team, for the portfolio filter and project switcher");
|
|
9474
|
+
groups.command("list").description("Each group and how many of your apps are in it").option(ORG_HELP, ORG_DESC).option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
9475
|
+
const config = requireConfig();
|
|
9476
|
+
const result = await apiCall(`/v1/admin/orgs/${orgSegment(opts.org)}/project-groups`, config);
|
|
9477
|
+
if (!result.ok) dieOrgError(result);
|
|
9478
|
+
if (outputIsJson(opts.json)) {
|
|
9479
|
+
console.log(JSON.stringify(result.data.groups, null, 2));
|
|
9480
|
+
return;
|
|
9481
|
+
}
|
|
9482
|
+
if (result.data.groups.length === 0) console.log('No groups yet. Add one: mushi portfolio groups add "Client work"');
|
|
9483
|
+
for (const g of result.data.groups) console.log(`${g.slug.padEnd(24)} ${g.name} (${g.project_ids.length} app${g.project_ids.length === 1 ? "" : "s"}) ${g.id}`);
|
|
9484
|
+
});
|
|
9485
|
+
groups.command("add <name>").description("Create a group (team owners and admins)").option(ORG_HELP, ORG_DESC).option("--json", "Machine-readable JSON output").action(async (name, opts) => {
|
|
9486
|
+
const config = requireConfig();
|
|
9487
|
+
const result = await apiCall(`/v1/admin/orgs/${orgSegment(opts.org)}/project-groups`, config, {
|
|
9488
|
+
method: "POST",
|
|
9489
|
+
body: JSON.stringify({ name })
|
|
9490
|
+
});
|
|
9491
|
+
if (!result.ok) dieOrgError(result);
|
|
9492
|
+
if (outputIsJson(opts.json)) console.log(JSON.stringify(result.data.group, null, 2));
|
|
9493
|
+
else console.log(`Created "${result.data.group.name}" (${result.data.group.slug}) ${result.data.group.id}`);
|
|
9494
|
+
});
|
|
9495
|
+
groups.command("set <groupId> [projectIds...]").description("Set exactly which apps are in a group (team owners and admins); no ids empties it").option(ORG_HELP, ORG_DESC).action(async (groupId, projectIds, opts) => {
|
|
9496
|
+
const config = requireConfig();
|
|
9497
|
+
const gid = requireUuid(groupId, "group id");
|
|
9498
|
+
const ids = (projectIds ?? []).map((p3) => requireUuid(p3, "project id"));
|
|
9499
|
+
const result = await apiCall(
|
|
9500
|
+
`/v1/admin/orgs/${orgSegment(opts.org)}/project-groups/${gid}/projects`,
|
|
9501
|
+
config,
|
|
9502
|
+
{ method: "PUT", body: JSON.stringify({ project_ids: ids }) }
|
|
9503
|
+
);
|
|
9504
|
+
if (!result.ok) dieOrgError(result);
|
|
9505
|
+
console.log(`Added ${result.data.added}, removed ${result.data.removed}.`);
|
|
9506
|
+
});
|
|
9507
|
+
groups.command("rename <groupId> <name>").description("Rename a group (team owners and admins)").option(ORG_HELP, ORG_DESC).action(async (groupId, name, opts) => {
|
|
9508
|
+
const config = requireConfig();
|
|
9509
|
+
const result = await apiCall(
|
|
9510
|
+
`/v1/admin/orgs/${orgSegment(opts.org)}/project-groups/${requireUuid(groupId, "group id")}`,
|
|
9511
|
+
config,
|
|
9512
|
+
{ method: "PATCH", body: JSON.stringify({ name }) }
|
|
9513
|
+
);
|
|
9514
|
+
if (!result.ok) dieOrgError(result);
|
|
9515
|
+
console.log(`Renamed to "${result.data.group.name}" (${result.data.group.slug}).`);
|
|
9516
|
+
});
|
|
9517
|
+
groups.command("remove <groupId>").description("Delete a group; its apps stay (team owners and admins)").option(ORG_HELP, ORG_DESC).action(async (groupId, opts) => {
|
|
9518
|
+
const config = requireConfig();
|
|
9519
|
+
const result = await apiCall(
|
|
9520
|
+
`/v1/admin/orgs/${orgSegment(opts.org)}/project-groups/${requireUuid(groupId, "group id")}`,
|
|
9521
|
+
config,
|
|
9522
|
+
{ method: "DELETE" }
|
|
9523
|
+
);
|
|
9524
|
+
if (!result.ok) dieOrgError(result);
|
|
9525
|
+
console.log("Group deleted. Its apps are unchanged.");
|
|
9526
|
+
});
|
|
9452
9527
|
portfolio.command("import <file>").description("Record shared resources from a CSV: kind,external_id,project[,role] (team owners and admins)").option(ORG_HELP, ORG_DESC).option("--json", "Machine-readable JSON output").action(async (file, opts) => {
|
|
9453
9528
|
const read = readTextFileCapped(file, MAX_RESOURCE_CSV_BYTES);
|
|
9454
9529
|
if (read.kind === "absent") throw new MushiCliError("E_INVALID_INPUT", `Cannot read ${file}.`);
|
|
@@ -9683,8 +9758,8 @@ function registerCodeHealthCommands(program2) {
|
|
|
9683
9758
|
const codeHealth = program2.command("code-health").description("Oversized files and bundle size from your CI (Code Health page)");
|
|
9684
9759
|
codeHealth.command("show").description("Findings from the latest code-health run and the size summary").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--days <n>", "Trend window in days (1-365)", "30").option("--json", "Machine-readable JSON output (includes the trend series)").action(async (opts) => {
|
|
9685
9760
|
const config = requireConfig();
|
|
9686
|
-
const
|
|
9687
|
-
const qs = new URLSearchParams({ project_id:
|
|
9761
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
9762
|
+
const qs = new URLSearchParams({ project_id: projectId2, days: opts.days });
|
|
9688
9763
|
const result = await apiCall(`/v1/admin/code-health?${qs}`, config);
|
|
9689
9764
|
if (!result.ok) die(result);
|
|
9690
9765
|
if (outputIsJson(opts.json)) {
|
|
@@ -9695,9 +9770,9 @@ function registerCodeHealthCommands(program2) {
|
|
|
9695
9770
|
});
|
|
9696
9771
|
codeHealth.command("stats").description("Counts only: errors, warnings and oversized files").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
9697
9772
|
const config = requireConfig();
|
|
9698
|
-
const
|
|
9773
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
9699
9774
|
const result = await apiCall(
|
|
9700
|
-
`/v1/admin/code-health/stats?${new URLSearchParams({ project_id:
|
|
9775
|
+
`/v1/admin/code-health/stats?${new URLSearchParams({ project_id: projectId2 })}`,
|
|
9701
9776
|
config
|
|
9702
9777
|
);
|
|
9703
9778
|
if (!result.ok) die(result);
|
|
@@ -9710,7 +9785,7 @@ function registerCodeHealthCommands(program2) {
|
|
|
9710
9785
|
console.log("No code-health run yet.");
|
|
9711
9786
|
return;
|
|
9712
9787
|
}
|
|
9713
|
-
console.log(`${d.projectName ??
|
|
9788
|
+
console.log(`${d.projectName ?? projectId2}: ${d.errorCount} error(s), ${d.warnCount} warning(s), ${d.godFileCount} oversized file(s) \xB7 last run ${fmtDate(d.latestRunAt)}`);
|
|
9714
9789
|
if (d.topPriorityLabel) console.log(`Next: ${d.topPriorityLabel}`);
|
|
9715
9790
|
});
|
|
9716
9791
|
}
|
|
@@ -9805,8 +9880,8 @@ ${r.body_md}`);
|
|
|
9805
9880
|
});
|
|
9806
9881
|
releases.command("draft <version>").description("Draft notes from the fixes merged in a window (nothing is sent)").option("--title <title>", "Release title").option("--since <iso>", "Window start (ISO date); default: since the last release").option("--until <iso>", "Window end (ISO date); default: now").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (version, opts) => {
|
|
9807
9882
|
const config = requireConfig();
|
|
9808
|
-
const
|
|
9809
|
-
const body = { project_id:
|
|
9883
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
9884
|
+
const body = { project_id: projectId2, version };
|
|
9810
9885
|
if (opts.title) body["title"] = opts.title;
|
|
9811
9886
|
if (opts.since) body["window_start"] = opts.since;
|
|
9812
9887
|
if (opts.until) body["window_end"] = opts.until;
|
|
@@ -9950,9 +10025,9 @@ Examples:
|
|
|
9950
10025
|
mushi sentry import --query "is:unresolved level:error" --since-days 7`).action(async (issueIds, opts) => {
|
|
9951
10026
|
const body = buildSentryImportBody(issueIds, opts);
|
|
9952
10027
|
const config = requireConfig();
|
|
9953
|
-
const
|
|
10028
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
9954
10029
|
const result = await apiCall(
|
|
9955
|
-
`/v1/admin/projects/${
|
|
10030
|
+
`/v1/admin/projects/${projectId2}/sentry/import`,
|
|
9956
10031
|
config,
|
|
9957
10032
|
{ method: "POST", body: JSON.stringify(body) },
|
|
9958
10033
|
{ timeoutMs: IMPORT_TIMEOUT_MS }
|
|
@@ -10087,18 +10162,18 @@ function registerBudgetsCommands(program2) {
|
|
|
10087
10162
|
const budgets = program2.command("budgets").description("What Mushi may spend for a project: AI budget, auto-fix caps, plan spend cap");
|
|
10088
10163
|
budgets.command("show").description("Every spend limit and whether auto-fix is on").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
10089
10164
|
const config = requireConfig();
|
|
10090
|
-
const
|
|
10091
|
-
const qs = new URLSearchParams({ project_id:
|
|
10165
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
10166
|
+
const qs = new URLSearchParams({ project_id: projectId2 });
|
|
10092
10167
|
const [settings, billing, autofix] = await Promise.all([
|
|
10093
10168
|
apiCall(`/v1/admin/settings?${qs}`, config),
|
|
10094
10169
|
apiCall(`/v1/admin/billing/stats?${qs}`, config),
|
|
10095
|
-
apiCall(`/v1/admin/projects/${
|
|
10170
|
+
apiCall(`/v1/admin/projects/${projectId2}/autofix`, config)
|
|
10096
10171
|
]);
|
|
10097
10172
|
if (!settings.ok) die(settings);
|
|
10098
10173
|
if (!billing.ok) die(billing);
|
|
10099
10174
|
if (!autofix.ok) die(autofix);
|
|
10100
10175
|
const view = {
|
|
10101
|
-
projectId,
|
|
10176
|
+
projectId: projectId2,
|
|
10102
10177
|
limits: pickLimits(settings.data),
|
|
10103
10178
|
monthlySpendCapUsd: billing.data.monthlySpendCapUsd ?? null,
|
|
10104
10179
|
autofixEnabled: autofix.data.autofix_enabled
|
|
@@ -10114,14 +10189,14 @@ function registerBudgetsCommands(program2) {
|
|
|
10114
10189
|
throw new MushiCliError("E_INVALID_INPUT", "Pass on or off.", "e.g. mushi budgets autofix off");
|
|
10115
10190
|
}
|
|
10116
10191
|
const config = requireConfig();
|
|
10117
|
-
const
|
|
10192
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
10118
10193
|
const result = await apiCall(
|
|
10119
|
-
`/v1/admin/projects/${
|
|
10194
|
+
`/v1/admin/projects/${projectId2}/autofix/toggle`,
|
|
10120
10195
|
config,
|
|
10121
10196
|
{ method: "POST", body: JSON.stringify({ enabled: state === "on" }) }
|
|
10122
10197
|
);
|
|
10123
10198
|
if (!result.ok) die(result);
|
|
10124
|
-
console.log(`Auto-fix is ${result.data.autofix_enabled ? "on" : "off"} for project ${
|
|
10199
|
+
console.log(`Auto-fix is ${result.data.autofix_enabled ? "on" : "off"} for project ${projectId2}.`);
|
|
10125
10200
|
});
|
|
10126
10201
|
}
|
|
10127
10202
|
|
|
@@ -10169,9 +10244,9 @@ function registerRepoCommands(program2) {
|
|
|
10169
10244
|
repo.command("digest").description("One token-budgeted text of the repo (tree + the files that matter), secrets removed").option("--report <id>", "Start from the files linked to this report's stack trace and fixes").option("--path <folder>", "Only this folder").option("--ref <ref>", "Branch, tag or commit (default: the default branch)").option("--budget <tokens>", "Token budget").option("--include <globs>", "Comma-separated globs a file must match").option("--exclude <globs>", "Comma-separated globs to leave out").option("--out <file>", "Write the digest text to this file instead of stdout").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output (metadata and text)").action(async (opts) => {
|
|
10170
10245
|
const qs = digestQuery(opts);
|
|
10171
10246
|
const config = requireConfig();
|
|
10172
|
-
const
|
|
10247
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
10173
10248
|
const suffix = qs.toString() ? `?${qs}` : "";
|
|
10174
|
-
const result = await apiCall(`/v1/admin/projects/${
|
|
10249
|
+
const result = await apiCall(`/v1/admin/projects/${projectId2}/codebase/digest${suffix}`, config, {}, { timeoutMs: DIGEST_TIMEOUT_MS });
|
|
10175
10250
|
if (!result.ok) die(result);
|
|
10176
10251
|
if (outputIsJson(opts.json)) {
|
|
10177
10252
|
console.log(JSON.stringify(result.data, null, 2));
|
|
@@ -10191,8 +10266,8 @@ function registerRepoCommands(program2) {
|
|
|
10191
10266
|
const diagram = repo.command("diagram").description("The architecture diagram of your repo");
|
|
10192
10267
|
diagram.command("show").description("The latest diagram as an outline, and its public page if published").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
10193
10268
|
const config = requireConfig();
|
|
10194
|
-
const
|
|
10195
|
-
const result = await apiCall(`/v1/admin/projects/${
|
|
10269
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
10270
|
+
const result = await apiCall(`/v1/admin/projects/${projectId2}/codebase/diagram`, config);
|
|
10196
10271
|
if (!result.ok) die(result);
|
|
10197
10272
|
if (outputIsJson(opts.json)) {
|
|
10198
10273
|
console.log(JSON.stringify(result.data, null, 2));
|
|
@@ -10202,9 +10277,9 @@ function registerRepoCommands(program2) {
|
|
|
10202
10277
|
});
|
|
10203
10278
|
diagram.command("generate").description("Draw the diagram for the latest commit (reuses the one for that commit unless --force)").option("--force", "Draw it again even if this commit already has one").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
10204
10279
|
const config = requireConfig();
|
|
10205
|
-
const
|
|
10280
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
10206
10281
|
const result = await apiCall(
|
|
10207
|
-
`/v1/admin/projects/${
|
|
10282
|
+
`/v1/admin/projects/${projectId2}/codebase/diagram`,
|
|
10208
10283
|
config,
|
|
10209
10284
|
{ method: "POST", body: JSON.stringify({ force: opts.force === true }) },
|
|
10210
10285
|
{ timeoutMs: DIAGRAM_TIMEOUT_MS }
|
|
@@ -10219,8 +10294,8 @@ function registerRepoCommands(program2) {
|
|
|
10219
10294
|
});
|
|
10220
10295
|
diagram.command("publish").description("Publish the latest diagram as a public page (project owners and admins)").option("--yes", "Confirm: the diagram becomes public").option("--project-id <id>", "Project ID (defaults to the configured project)").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
10221
10296
|
const config = requireConfig();
|
|
10222
|
-
const
|
|
10223
|
-
const preview = await apiCall(`/v1/admin/projects/${
|
|
10297
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
10298
|
+
const preview = await apiCall(`/v1/admin/projects/${projectId2}/codebase/diagram/publish-preview`, config);
|
|
10224
10299
|
if (!preview.ok) die(preview);
|
|
10225
10300
|
const p3 = preview.data;
|
|
10226
10301
|
if (!p3.can_publish) {
|
|
@@ -10229,7 +10304,7 @@ function registerRepoCommands(program2) {
|
|
|
10229
10304
|
console.log(`Will publish to ${p3.url}${p3.repo_private ? " (the repo is private on GitHub)" : ""}.`);
|
|
10230
10305
|
requireYes(opts.yes, "Publishing makes the diagram public.");
|
|
10231
10306
|
const result = await apiCall(
|
|
10232
|
-
`/v1/admin/projects/${
|
|
10307
|
+
`/v1/admin/projects/${projectId2}/codebase/diagram/publish`,
|
|
10233
10308
|
config,
|
|
10234
10309
|
{ method: "POST", body: JSON.stringify({ diagram_id: p3.diagram_id, payload_hash: p3.payload_hash }) },
|
|
10235
10310
|
{ timeoutMs: PUBLISH_TIMEOUT_MS2 }
|
|
@@ -10245,8 +10320,8 @@ function registerRepoCommands(program2) {
|
|
|
10245
10320
|
diagram.command("unpublish").description("Take the public diagram page down").option("--yes", "Confirm the unpublish").option("--project-id <id>", "Project ID (defaults to the configured project)").action(async (opts) => {
|
|
10246
10321
|
requireYes(opts.yes, "This removes the public diagram page.");
|
|
10247
10322
|
const config = requireConfig();
|
|
10248
|
-
const
|
|
10249
|
-
const result = await apiCall(`/v1/admin/projects/${
|
|
10323
|
+
const projectId2 = resolveProjectId(opts.projectId, config.projectId);
|
|
10324
|
+
const result = await apiCall(`/v1/admin/projects/${projectId2}/codebase/diagram/publish`, config, { method: "DELETE" });
|
|
10250
10325
|
if (!result.ok) die(result);
|
|
10251
10326
|
console.log("The public diagram page is down.");
|
|
10252
10327
|
});
|
|
@@ -10407,7 +10482,7 @@ function registerSpendCommands(program2) {
|
|
|
10407
10482
|
});
|
|
10408
10483
|
spend.command("import <file>").description("Import a bill CSV (FOCUS, AWS CUR, or date,service,cost); a re-import of the same days replaces them").requiredOption("--vendor <vendor>", VENDORS.join(" | ")).option("--project-id <id>", "Put every row on this app (default: match an app column by name or slug)").option(ORG_FLAG2, ORG_DESC3).option("--json", "Machine-readable JSON output").action(async (file, opts) => {
|
|
10409
10484
|
if (!VENDORS.includes(opts.vendor)) throw new MushiCliError("E_INVALID_INPUT", `--vendor must be one of ${VENDORS.join(", ")}`);
|
|
10410
|
-
const
|
|
10485
|
+
const projectId2 = opts.projectId ? requireUuid(opts.projectId, "project id") : void 0;
|
|
10411
10486
|
const read = readTextFileCapped(file, MAX_BILL_CSV_BYTES);
|
|
10412
10487
|
if (read.kind === "absent") throw new MushiCliError("E_INVALID_INPUT", `Cannot read ${file}.`);
|
|
10413
10488
|
if (read.kind === "too_large") throw new MushiCliError("E_INVALID_INPUT", `${file} is over 5 MB.`, "export daily rather than hourly rows, or split it by month");
|
|
@@ -10416,7 +10491,7 @@ function registerSpendCommands(program2) {
|
|
|
10416
10491
|
const result = await apiCall(
|
|
10417
10492
|
`/v1/admin/orgs/${orgSegment(opts.org)}/spend/imports`,
|
|
10418
10493
|
config,
|
|
10419
|
-
{ method: "POST", body: JSON.stringify({ vendor: opts.vendor, csv, filename: basename2(file), ...
|
|
10494
|
+
{ method: "POST", body: JSON.stringify({ vendor: opts.vendor, csv, filename: basename2(file), ...projectId2 ? { projectId: projectId2 } : {} }) },
|
|
10420
10495
|
{ timeoutMs: IMPORT_TIMEOUT_MS2 }
|
|
10421
10496
|
);
|
|
10422
10497
|
if (!result.ok) dieOrgError(result);
|
|
@@ -10495,6 +10570,140 @@ function registerDesignCommands(program2) {
|
|
|
10495
10570
|
});
|
|
10496
10571
|
}
|
|
10497
10572
|
|
|
10573
|
+
// src/commands/ux.ts
|
|
10574
|
+
import { spawn as spawn2 } from "child_process";
|
|
10575
|
+
import { existsSync as existsSync5 } from "fs";
|
|
10576
|
+
import { dirname as dirname3, join as join10 } from "path";
|
|
10577
|
+
var UX_PACKAGE = "@mushi-mushi/ux@^0.1.0";
|
|
10578
|
+
function cmdQuote(arg) {
|
|
10579
|
+
return /^[\w./:=@-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '""')}"`;
|
|
10580
|
+
}
|
|
10581
|
+
var NPX_CLI_CANDIDATES = [
|
|
10582
|
+
join10(dirname3(process.execPath), "node_modules", "npm", "bin", "npx-cli.js"),
|
|
10583
|
+
join10(dirname3(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npx-cli.js")
|
|
10584
|
+
];
|
|
10585
|
+
function uxCommandLine(args, env = process.env, platform = process.platform, npxCli = NPX_CLI_CANDIDATES, hasFile = existsSync5) {
|
|
10586
|
+
const local = env.MUSHI_UX_BIN?.trim();
|
|
10587
|
+
if (local) return { cmd: process.execPath, argv: [local, ...args], shell: false };
|
|
10588
|
+
const npxArgs = ["--yes", "--package", UX_PACKAGE, "mushi-ux", ...args];
|
|
10589
|
+
const found = (typeof npxCli === "string" ? [npxCli] : npxCli).find((p3) => hasFile(p3));
|
|
10590
|
+
if (found) return { cmd: process.execPath, argv: [found, ...npxArgs], shell: false };
|
|
10591
|
+
if (platform === "win32") return { cmd: "npx", argv: npxArgs.map(cmdQuote), shell: true };
|
|
10592
|
+
return { cmd: "npx", argv: npxArgs, shell: false };
|
|
10593
|
+
}
|
|
10594
|
+
function registerUxCommands(program2) {
|
|
10595
|
+
program2.command("ux").description("Run your coding agent over every screen of your app and keep only measured UX improvements (local, uses @mushi-mushi/ux)").argument("[args...]", "Passed to mushi-ux: ui | run | discover | login | models | skills | open").allowUnknownOption().helpOption(false).addHelpText(
|
|
10596
|
+
"after",
|
|
10597
|
+
`
|
|
10598
|
+
Examples:
|
|
10599
|
+
mushi ux ui # studio: pick agent, model, skill; watch every attempt
|
|
10600
|
+
mushi ux models --agent cursor # models your Cursor account can use
|
|
10601
|
+
mushi ux discover --url http://localhost:5173
|
|
10602
|
+
mushi ux run --dev "pnpm dev --port {port}" --agent claude-code
|
|
10603
|
+
mushi ux run --dev "pnpm dev --port {port}" --agent cursor --model <id from \`agent --list-models\`>
|
|
10604
|
+
mushi ux --help`
|
|
10605
|
+
).action((args) => {
|
|
10606
|
+
const { cmd, argv, shell } = uxCommandLine(args);
|
|
10607
|
+
let env = process.env;
|
|
10608
|
+
if (args.includes("--sync")) {
|
|
10609
|
+
const config = requireConfig({ needsProject: true });
|
|
10610
|
+
env = { ...process.env, MUSHI_API_KEY: config.apiKey, MUSHI_PROJECT_ID: config.projectId, MUSHI_API_ENDPOINT: config.endpoint };
|
|
10611
|
+
} else if (args[0] === "ui") {
|
|
10612
|
+
const config = loadConfig();
|
|
10613
|
+
if (config.apiKey && config.projectId) {
|
|
10614
|
+
env = { ...process.env, MUSHI_API_KEY: config.apiKey, MUSHI_PROJECT_ID: config.projectId, ...config.endpoint ? { MUSHI_API_ENDPOINT: config.endpoint } : {} };
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
const child = spawn2(cmd, argv, { stdio: "inherit", shell, env });
|
|
10618
|
+
child.on("error", (err) => {
|
|
10619
|
+
process.stderr.write(`error: could not start mushi-ux (${err.message}). Install it with: npm i -g @mushi-mushi/ux
|
|
10620
|
+
`);
|
|
10621
|
+
process.exit(1);
|
|
10622
|
+
});
|
|
10623
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
10624
|
+
});
|
|
10625
|
+
}
|
|
10626
|
+
|
|
10627
|
+
// src/commands/ux-runs.ts
|
|
10628
|
+
var STATUS_LABEL = {
|
|
10629
|
+
accepted: "improved",
|
|
10630
|
+
reverted: "rolled back",
|
|
10631
|
+
regressed: "moved by another fix",
|
|
10632
|
+
blocked: "could not load",
|
|
10633
|
+
skipped: "no change needed",
|
|
10634
|
+
iterating: "agent working",
|
|
10635
|
+
baseline: "measuring",
|
|
10636
|
+
pending: "queued"
|
|
10637
|
+
};
|
|
10638
|
+
var RUN_ID_RE = /^[0-9]{8}-[0-9]{6}-[a-z0-9]{4}$/;
|
|
10639
|
+
var KEY_RE = /^[a-z0-9][a-z0-9-]{0,79}$/;
|
|
10640
|
+
function projectId() {
|
|
10641
|
+
const config = requireConfig({ needsProject: true });
|
|
10642
|
+
return { config, pid: encodeURIComponent(config.projectId) };
|
|
10643
|
+
}
|
|
10644
|
+
function requireRunId(runId) {
|
|
10645
|
+
if (!RUN_ID_RE.test(runId)) throw new MushiCliError("E_INVALID_INPUT", `"${runId}" is not a run id (like 20261006-011207-qafu).`);
|
|
10646
|
+
return runId;
|
|
10647
|
+
}
|
|
10648
|
+
function registerUxRunsCommands(program2) {
|
|
10649
|
+
const runs = program2.command("ux-runs").description("UX runs synced from `mushi ux run --sync`: which screens were improved, rolled back or moved");
|
|
10650
|
+
runs.command("list").description("Recent UX runs for this project").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
10651
|
+
const { config, pid } = projectId();
|
|
10652
|
+
const result = await apiCall(`/v1/admin/projects/${pid}/ux-runs`, config);
|
|
10653
|
+
if (!result.ok) die(result);
|
|
10654
|
+
if (outputIsJson(opts.json)) {
|
|
10655
|
+
console.log(JSON.stringify(result.data.runs, null, 2));
|
|
10656
|
+
return;
|
|
10657
|
+
}
|
|
10658
|
+
if (result.data.runs.length === 0) {
|
|
10659
|
+
console.log('No UX runs yet. Start one with: mushi ux run --dev "pnpm dev --port {port}" --sync');
|
|
10660
|
+
return;
|
|
10661
|
+
}
|
|
10662
|
+
for (const r of result.data.runs) {
|
|
10663
|
+
const c = r.counts;
|
|
10664
|
+
console.log(
|
|
10665
|
+
`${r.local_run_id} ${r.status.padEnd(7)} ${r.agent}${r.model ? ` \xB7 ${r.model}` : ""} improved ${c.accepted ?? 0} \xB7 rolled back ${c.reverted ?? 0} \xB7 moved ${c.regressed ?? 0} \xB7 blocked ${c.blocked ?? 0}`
|
|
10666
|
+
);
|
|
10667
|
+
}
|
|
10668
|
+
});
|
|
10669
|
+
runs.command("show <runId>").description("Every screen of one run, what still needs a look first").option("--json", "Machine-readable JSON output").action(async (runId, opts) => {
|
|
10670
|
+
const { config, pid } = projectId();
|
|
10671
|
+
const result = await apiCall(
|
|
10672
|
+
`/v1/admin/projects/${pid}/ux-runs/${requireRunId(runId)}`,
|
|
10673
|
+
config
|
|
10674
|
+
);
|
|
10675
|
+
if (!result.ok) die(result);
|
|
10676
|
+
if (outputIsJson(opts.json)) {
|
|
10677
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
10678
|
+
return;
|
|
10679
|
+
}
|
|
10680
|
+
const { run: run2, surfaces } = result.data;
|
|
10681
|
+
if (run2.branch) console.log(`Kept changes: ${run2.branch}
|
|
10682
|
+
`);
|
|
10683
|
+
const order = ["regressed", "reverted", "blocked", "iterating", "baseline", "accepted", "skipped", "pending"];
|
|
10684
|
+
for (const s of [...surfaces].sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status))) {
|
|
10685
|
+
const score = s.penalty_before !== null ? ` score ${s.penalty_before}${s.penalty_after !== null ? `\u2192${s.penalty_after}` : ""}` : "";
|
|
10686
|
+
console.log(`${(STATUS_LABEL[s.status] ?? s.status).padEnd(22)} ${s.label} ${s.path}${score}${s.report_id ? " (filed)" : ""}`);
|
|
10687
|
+
if (s.note) console.log(`${" ".repeat(23)}${s.note}`);
|
|
10688
|
+
}
|
|
10689
|
+
});
|
|
10690
|
+
runs.command("file <runId> <screen>").description("File one screen of a run as a bug (source ux_loop; no AI call)").option("--json", "Machine-readable JSON output").action(async (runId, screen, opts) => {
|
|
10691
|
+
if (!KEY_RE.test(screen)) throw new MushiCliError("E_INVALID_INPUT", `"${screen}" is not a screen key (see \`mushi ux-runs show ${runId}\` --json).`);
|
|
10692
|
+
const { config, pid } = projectId();
|
|
10693
|
+
const result = await apiCall(
|
|
10694
|
+
`/v1/admin/projects/${pid}/ux-runs/${requireRunId(runId)}/surfaces/${screen}/report`,
|
|
10695
|
+
config,
|
|
10696
|
+
{ method: "POST", body: "{}" }
|
|
10697
|
+
);
|
|
10698
|
+
if (!result.ok) die(result);
|
|
10699
|
+
if (outputIsJson(opts.json)) {
|
|
10700
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
10701
|
+
return;
|
|
10702
|
+
}
|
|
10703
|
+
console.log(result.data.reused ? `Already filed: report ${result.data.report_id}` : `Filed report ${result.data.report_id}`);
|
|
10704
|
+
});
|
|
10705
|
+
}
|
|
10706
|
+
|
|
10498
10707
|
// src/index.ts
|
|
10499
10708
|
installSignalHandlers();
|
|
10500
10709
|
var program = new Command().name("mushi").description("Mushi CLI \u2014 set up the SDK, triage a report, fix from your editor").version(MUSHI_CLI_VERSION).option("--profile <name>", "Use a named credential profile for this invocation (or set MUSHI_PROFILE)").option("-o, --output <format>", "Output format: text | json", "text").hook("preAction", (_thisCommand, actionCommand) => {
|
|
@@ -10558,6 +10767,8 @@ registerRepoCommands(program);
|
|
|
10558
10767
|
registerAccountsCommands(program);
|
|
10559
10768
|
registerSpendCommands(program);
|
|
10560
10769
|
registerDesignCommands(program);
|
|
10770
|
+
registerUxCommands(program);
|
|
10771
|
+
registerUxRunsCommands(program);
|
|
10561
10772
|
registerCompletionCommand(program);
|
|
10562
10773
|
program.parseAsync().catch((err) => {
|
|
10563
10774
|
printAndExit(err);
|