@mushi-mushi/cli 0.26.1 → 0.27.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 +25 -0
- package/SECURITY.md +49 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/{chunk-2ZLYNNA3.js → chunk-7JZUKNN5.js} +207 -9
- package/dist/chunk-LFZ5ZIBI.js +8 -0
- package/dist/chunk-MXD5UNMP.js +10 -0
- package/dist/chunk-PDX44BCA.js +11 -0
- package/dist/{chunk-QOY3DULB.js → chunk-TZL3F3GT.js} +125 -7
- package/dist/{connect-PXLW4D64.js → connect-FAXWVFG2.js} +8 -4
- package/dist/{connect-2LCLLN7R.js → connect-QHVFH77E.js} +3 -2
- package/dist/detect.js +1 -0
- package/dist/{endpoint-DGS73AVM.js → endpoint-TEBF4IH6.js} +1 -0
- package/dist/index.js +557 -165
- package/dist/init.js +75 -5
- package/dist/{snippet-inject-NDAPEL2T.js → snippet-inject-7ABYE57O.js} +2 -0
- package/dist/{snippet-inject-LPRWTQDE.js → snippet-inject-CCXILWYZ.js} +1 -0
- package/dist/{version-5DCYWDRK.js → version-F3ZOPM5F.js} +2 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -1
- package/dist/wizard-args.js +1 -0
- package/package.json +4 -1
- package/dist/chunk-6XYPFV23.js +0 -10
- package/dist/chunk-PCTMFRKN.js +0 -8
package/dist/index.js
CHANGED
|
@@ -17,10 +17,15 @@ import {
|
|
|
17
17
|
fmtDate,
|
|
18
18
|
installCommand,
|
|
19
19
|
installSignalHandlers,
|
|
20
|
+
listProfiles,
|
|
20
21
|
loadConfig,
|
|
22
|
+
maybeShowTelemetryNotice,
|
|
21
23
|
openInBrowser,
|
|
24
|
+
outputIsJson,
|
|
22
25
|
pad,
|
|
23
26
|
printAndExit,
|
|
27
|
+
printKeyExportHint,
|
|
28
|
+
printResult,
|
|
24
29
|
probeEndpointHealth,
|
|
25
30
|
projectIdHint,
|
|
26
31
|
readPackageJson,
|
|
@@ -29,24 +34,29 @@ import {
|
|
|
29
34
|
requireUuid,
|
|
30
35
|
resolveConsoleUrl,
|
|
31
36
|
resolveConsoleUrlSync,
|
|
37
|
+
resolveProfileName,
|
|
32
38
|
runConnect,
|
|
33
39
|
sanitizeApiKey,
|
|
34
40
|
sanitizeCliCredentials,
|
|
35
41
|
sanitizeEndpoint,
|
|
36
42
|
sanitizeProjectId,
|
|
37
43
|
saveConfig,
|
|
44
|
+
setActiveProfile,
|
|
45
|
+
setGlobalOutputFormat,
|
|
46
|
+
trySaveKeyToKeychain,
|
|
38
47
|
writeMcpServerEntry
|
|
39
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-7JZUKNN5.js";
|
|
40
49
|
import {
|
|
41
50
|
MUSHI_CLI_VERSION,
|
|
42
51
|
MUSHI_MCP_PIN_SPEC
|
|
43
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-MXD5UNMP.js";
|
|
44
53
|
import {
|
|
45
54
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
46
55
|
assertEndpoint,
|
|
47
56
|
normalizeEndpoint,
|
|
48
57
|
resolveCloudEndpoint
|
|
49
58
|
} from "./chunk-LDPFTQLE.js";
|
|
59
|
+
import "./chunk-PDX44BCA.js";
|
|
50
60
|
|
|
51
61
|
// src/index.ts
|
|
52
62
|
import { Command } from "commander";
|
|
@@ -272,6 +282,38 @@ async function mintProjectKey(endpoint, cliToken, projectId, opts = {}) {
|
|
|
272
282
|
res.status
|
|
273
283
|
);
|
|
274
284
|
}
|
|
285
|
+
async function revokeProjectKey(endpoint, cliToken, projectId, keyPrefix, newKeyId) {
|
|
286
|
+
const base = trimTrailingSlash(endpoint);
|
|
287
|
+
const url = `${base}/v1/cli/projects/${encodeURIComponent(projectId)}/keys/${encodeURIComponent(keyPrefix)}/revoke`;
|
|
288
|
+
let res;
|
|
289
|
+
try {
|
|
290
|
+
res = await deviceFetch(url, {
|
|
291
|
+
method: "DELETE",
|
|
292
|
+
headers: {
|
|
293
|
+
...cliAuthHeaders(cliToken),
|
|
294
|
+
"Content-Type": "application/json"
|
|
295
|
+
},
|
|
296
|
+
body: newKeyId ? JSON.stringify({ newKeyId }) : void 0
|
|
297
|
+
});
|
|
298
|
+
} catch (err) {
|
|
299
|
+
throw new DeviceAuthRequestError(
|
|
300
|
+
"revoke_key",
|
|
301
|
+
`Could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const json = await res.json().catch(() => null);
|
|
305
|
+
if (res.ok && json?.ok) {
|
|
306
|
+
return {
|
|
307
|
+
revoked: json.revoked ?? 0,
|
|
308
|
+
revokedAt: json.revokedAt
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
throw new DeviceAuthRequestError(
|
|
312
|
+
"revoke_key",
|
|
313
|
+
json?.error?.message ?? `Could not revoke key (HTTP ${res.status}).`,
|
|
314
|
+
res.status
|
|
315
|
+
);
|
|
316
|
+
}
|
|
275
317
|
|
|
276
318
|
// src/freshness.ts
|
|
277
319
|
var REGISTRY = "https://registry.npmjs.org";
|
|
@@ -861,6 +903,71 @@ async function checkHostAppWiring(cwd) {
|
|
|
861
903
|
}
|
|
862
904
|
return checks;
|
|
863
905
|
}
|
|
906
|
+
var HASH_ROUTER_PATTERNS = [
|
|
907
|
+
/\bHashRouter\b/,
|
|
908
|
+
/\bcreateHashRouter\b/,
|
|
909
|
+
/location\.hash\b/,
|
|
910
|
+
/addEventListener\(['"]hashchange['"]/
|
|
911
|
+
];
|
|
912
|
+
var HASH_TEMPLATE_PATTERN = /['"`]#\//;
|
|
913
|
+
async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
|
|
914
|
+
const { readdir: readdir2, readFile: readFile3 } = await import("fs/promises");
|
|
915
|
+
const { join: join4 } = await import("path");
|
|
916
|
+
let count = 0;
|
|
917
|
+
async function walk(d, depth) {
|
|
918
|
+
if (depth > maxDepth || count > maxFiles) return null;
|
|
919
|
+
let entries = [];
|
|
920
|
+
try {
|
|
921
|
+
entries = await readdir2(d, { withFileTypes: true });
|
|
922
|
+
} catch {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
for (const e of entries) {
|
|
926
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue;
|
|
927
|
+
const full = join4(d, e.name);
|
|
928
|
+
if (e.isDirectory()) {
|
|
929
|
+
const found = await walk(full, depth + 1);
|
|
930
|
+
if (found) return found;
|
|
931
|
+
} else if (/\.[cm]?[jt]sx?$/.test(e.name)) {
|
|
932
|
+
count++;
|
|
933
|
+
try {
|
|
934
|
+
const src = await readFile3(full, "utf8");
|
|
935
|
+
if (patterns.some((p3) => p3.test(src))) return full;
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
const { resolve: resolve3 } = await import("path");
|
|
943
|
+
for (const sub of ["src", "app", "pages", "lib", "components"]) {
|
|
944
|
+
const found = await walk(join4(resolve3(dir), sub), 0);
|
|
945
|
+
if (found) return found;
|
|
946
|
+
}
|
|
947
|
+
return walk(resolve3(dir), 0);
|
|
948
|
+
}
|
|
949
|
+
async function checkHashRouterCapture(cwd) {
|
|
950
|
+
const hashFile = await scanDirForPattern(cwd, HASH_ROUTER_PATTERNS);
|
|
951
|
+
if (!hashFile) return [];
|
|
952
|
+
const templateFile = await scanDirForPattern(cwd, [HASH_TEMPLATE_PATTERN]);
|
|
953
|
+
if (templateFile) {
|
|
954
|
+
return [
|
|
955
|
+
{
|
|
956
|
+
name: "[host] Hash-router route capture",
|
|
957
|
+
ok: true,
|
|
958
|
+
detail: `Hash-router detected (${hashFile}) and /#/ route templates found in Mushi config \u2014 routes will be captured correctly.`
|
|
959
|
+
}
|
|
960
|
+
];
|
|
961
|
+
}
|
|
962
|
+
return [
|
|
963
|
+
{
|
|
964
|
+
name: "[host] Hash-router route capture",
|
|
965
|
+
ok: true,
|
|
966
|
+
warn: true,
|
|
967
|
+
detail: `Hash-router detected (${hashFile}) but no /#/ route templates found in your Mushi init call. Add discoverInventory.routeTemplates with /#/-prefixed patterns (e.g. ['/#/article/[slug]']) so Mushi captures parametric hash routes correctly \u2014 without these, all hash navigations appear as a single "/#/" route in the inventory.`
|
|
968
|
+
}
|
|
969
|
+
];
|
|
970
|
+
}
|
|
864
971
|
async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
865
972
|
const checks = [];
|
|
866
973
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
@@ -1096,7 +1203,7 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
|
|
|
1096
1203
|
}
|
|
1097
1204
|
async function checkVersionDrift(cwd, doFetch) {
|
|
1098
1205
|
const checks = [];
|
|
1099
|
-
const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-
|
|
1206
|
+
const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-F3ZOPM5F.js");
|
|
1100
1207
|
if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
|
|
1101
1208
|
const parse2 = (v) => {
|
|
1102
1209
|
const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
@@ -1201,7 +1308,7 @@ async function runDoctor(config, options = {}) {
|
|
|
1201
1308
|
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
1202
1309
|
if (sdkCheck) checks.push(sdkCheck);
|
|
1203
1310
|
if (sdkCheck?.ok && !sdkCheck.warn) {
|
|
1204
|
-
const { findSdkImport } = await import("./snippet-inject-
|
|
1311
|
+
const { findSdkImport } = await import("./snippet-inject-CCXILWYZ.js");
|
|
1205
1312
|
const imported = await findSdkImport(options.cwd ?? process.cwd());
|
|
1206
1313
|
checks.push(
|
|
1207
1314
|
imported ? { name: "SDK init snippet wired", ok: true, detail: `Found SDK usage in ${imported.file}` } : {
|
|
@@ -1229,6 +1336,8 @@ async function runDoctor(config, options = {}) {
|
|
|
1229
1336
|
if (isFull || options.hostApp) {
|
|
1230
1337
|
const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
|
|
1231
1338
|
checks.push(...hostChecks);
|
|
1339
|
+
const hashChecks = await checkHashRouterCapture(options.cwd ?? process.cwd());
|
|
1340
|
+
checks.push(...hashChecks);
|
|
1232
1341
|
}
|
|
1233
1342
|
if (isFull || options.mcp) {
|
|
1234
1343
|
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
@@ -1413,6 +1522,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
1413
1522
|
};
|
|
1414
1523
|
}
|
|
1415
1524
|
const existing = loadConfig();
|
|
1525
|
+
maybeShowTelemetryNotice(existing);
|
|
1416
1526
|
if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
|
|
1417
1527
|
const reuse = options.yes ? true : await p.confirm({
|
|
1418
1528
|
message: "Use the Mushi credentials saved from your last sign-in?",
|
|
@@ -1803,10 +1913,10 @@ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
|
|
|
1803
1913
|
}
|
|
1804
1914
|
async function maybeInjectSnippet(cwd, framework, options) {
|
|
1805
1915
|
if (options.yes || !process.stdin.isTTY) return;
|
|
1806
|
-
const { ENTRY_CANDIDATES, injectSnippet, MUSHI_MARKER_START } = await import("./snippet-inject-
|
|
1916
|
+
const { ENTRY_CANDIDATES, injectSnippet, MUSHI_MARKER_START } = await import("./snippet-inject-CCXILWYZ.js");
|
|
1807
1917
|
const candidates = ENTRY_CANDIDATES[framework.id];
|
|
1808
1918
|
if (!candidates) return;
|
|
1809
|
-
const { readFile: readFile3, writeFile:
|
|
1919
|
+
const { readFile: readFile3, writeFile: writeFile3 } = await import("fs/promises");
|
|
1810
1920
|
const nodePath2 = await import("path");
|
|
1811
1921
|
for (const rel of candidates) {
|
|
1812
1922
|
const abs = nodePath2.join(cwd, rel);
|
|
@@ -1821,7 +1931,7 @@ async function maybeInjectSnippet(cwd, framework, options) {
|
|
|
1821
1931
|
message: alreadyInjected ? `Refresh the Mushi init block in ${rel}?` : `Add the Mushi init snippet to ${rel}? (wrapped in markers; re-runs update in place)`
|
|
1822
1932
|
});
|
|
1823
1933
|
if (p.isCancel(confirmed) || !confirmed) return;
|
|
1824
|
-
await
|
|
1934
|
+
await writeFile3(abs, injectSnippet(source, framework.snippet()), "utf8");
|
|
1825
1935
|
p.log.success(`${alreadyInjected ? "Updated" : "Added"} Mushi init block in ${rel}`);
|
|
1826
1936
|
return;
|
|
1827
1937
|
}
|
|
@@ -1859,7 +1969,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
1859
1969
|
if (p.isCancel(answer) || !answer) return;
|
|
1860
1970
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1861
1971
|
try {
|
|
1862
|
-
const { runConnect: runConnect2 } = await import("./connect-
|
|
1972
|
+
const { runConnect: runConnect2 } = await import("./connect-QHVFH77E.js");
|
|
1863
1973
|
await runConnect2({
|
|
1864
1974
|
apiKey: credentials.apiKey,
|
|
1865
1975
|
projectId: credentials.projectId,
|
|
@@ -2058,7 +2168,7 @@ async function resolveProjectChoice(projectsList, ask) {
|
|
|
2058
2168
|
return { kind: "create", name: newName };
|
|
2059
2169
|
}
|
|
2060
2170
|
async function runLogin(opts = {}) {
|
|
2061
|
-
const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-
|
|
2171
|
+
const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-TEBF4IH6.js");
|
|
2062
2172
|
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
|
|
2063
2173
|
const consoleBase = await resolveConsoleUrl();
|
|
2064
2174
|
let apiKey = opts.apiKey;
|
|
@@ -2180,6 +2290,10 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
2180
2290
|
if (chosenProjectId) config.projectId = chosenProjectId;
|
|
2181
2291
|
if (apiKey) config.apiKey = apiKey;
|
|
2182
2292
|
saveConfig(config);
|
|
2293
|
+
if (apiKey) {
|
|
2294
|
+
const profile = resolveProfileName(void 0, config.activeProfile);
|
|
2295
|
+
trySaveKeyToKeychain(apiKey, profile);
|
|
2296
|
+
}
|
|
2183
2297
|
console.log("");
|
|
2184
2298
|
if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
|
|
2185
2299
|
if (apiKey) {
|
|
@@ -2510,7 +2624,7 @@ function registerDeployCommands(program2) {
|
|
|
2510
2624
|
// src/commands/reports.ts
|
|
2511
2625
|
function registerReportsCommands(program2) {
|
|
2512
2626
|
const reports = program2.command("reports").description("Manage bug reports");
|
|
2513
|
-
reports.command("list").description("List recent reports for the current project").option("--limit <n>", "Max results (1\u2013100)", "20").option("--status <status>", "Filter by status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "Filter by severity: critical|high|medium|low").option("--search <query>", "Full-text search in summary and description").option("--json", "
|
|
2627
|
+
reports.command("list").description("List recent reports for the current project").option("--limit <n>", "Max results (1\u2013100)", "20").option("--status <status>", "Filter by status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "Filter by severity: critical|high|medium|low").option("--search <query>", "Full-text search in summary and description").option("--json", "Output as JSON (alias for -o json)").addHelpText("after", `
|
|
2514
2628
|
Examples:
|
|
2515
2629
|
mushi reports list
|
|
2516
2630
|
mushi reports list --status new --severity critical
|
|
@@ -2523,29 +2637,28 @@ Examples:
|
|
|
2523
2637
|
if (opts.search) params.set("search", opts.search);
|
|
2524
2638
|
const result = await apiCall(`/v1/sync/reports?${params}`, config);
|
|
2525
2639
|
if (!result.ok) die(result);
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
}
|
|
2640
|
+
printResult(result.data, {
|
|
2641
|
+
json: opts.json,
|
|
2642
|
+
render(d) {
|
|
2643
|
+
const rows = d.reports;
|
|
2644
|
+
if (rows.length === 0) {
|
|
2645
|
+
console.log("No reports found.");
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
console.log(`${pad("ID", 38)} ${pad("SEV", 9)} ${pad("STATUS", 12)} ${pad("CREATED", 17)} SUMMARY`);
|
|
2649
|
+
console.log("\u2500".repeat(110));
|
|
2650
|
+
for (const r of rows) {
|
|
2651
|
+
const sev = r.severity ?? "unset";
|
|
2652
|
+
const status = r.status ?? "new";
|
|
2653
|
+
const summary = (r.summary ?? r.description ?? "").slice(0, 50);
|
|
2654
|
+
console.log(`${pad(r.id, 38)} ${pad(sev, 9)} ${pad(status, 12)} ${pad(fmtDate(r.created_at), 17)} ${summary}`);
|
|
2655
|
+
}
|
|
2656
|
+
if (d.total > rows.length) console.log(`
|
|
2657
|
+
\u2026 ${d.total - rows.length} more. Use --limit to see more.`);
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2547
2660
|
});
|
|
2548
|
-
reports.command("thread <id>").description("Show unified timeline for a report (comments, fixes, QA, pipelines)").option("--json", "
|
|
2661
|
+
reports.command("thread <id>").description("Show unified timeline for a report (comments, fixes, QA, pipelines)").option("--json", "Output as JSON (alias for -o json)").option("--watch", "Poll for new timeline entries (TTY only)").action(async (id, opts) => {
|
|
2549
2662
|
const config = requireConfig();
|
|
2550
2663
|
const fetchTimeline = async () => {
|
|
2551
2664
|
const result = await apiCall(`/v1/sync/reports/${id}/timeline`, config);
|
|
@@ -2560,7 +2673,7 @@ Examples:
|
|
|
2560
2673
|
return result.data;
|
|
2561
2674
|
};
|
|
2562
2675
|
const render = (data) => {
|
|
2563
|
-
if (opts.json) {
|
|
2676
|
+
if (outputIsJson(opts.json)) {
|
|
2564
2677
|
console.log(JSON.stringify(data, null, 2));
|
|
2565
2678
|
return;
|
|
2566
2679
|
}
|
|
@@ -2585,7 +2698,7 @@ Examples:
|
|
|
2585
2698
|
}
|
|
2586
2699
|
render(await fetchTimeline());
|
|
2587
2700
|
});
|
|
2588
|
-
reports.command("show <id>").description("Show full details for a single report").option("--json", "
|
|
2701
|
+
reports.command("show <id>").description("Show full details for a single report").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
|
|
2589
2702
|
const config = requireConfig();
|
|
2590
2703
|
const result = await apiCall(`/v1/sync/reports/${id}`, config);
|
|
2591
2704
|
if (!result.ok) {
|
|
@@ -2596,30 +2709,28 @@ Examples:
|
|
|
2596
2709
|
}
|
|
2597
2710
|
die(result);
|
|
2598
2711
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
console.log(` Tags: ${JSON.stringify(r.tags)}`);
|
|
2620
|
-
}
|
|
2712
|
+
printResult(result.data, {
|
|
2713
|
+
json: opts.json,
|
|
2714
|
+
render(r) {
|
|
2715
|
+
console.log(`Report: ${r.id}`);
|
|
2716
|
+
console.log(` Status: ${r.status ?? "new"}`);
|
|
2717
|
+
console.log(` Severity: ${r.severity ?? "unset"}`);
|
|
2718
|
+
console.log(` Category: ${r.category ?? "\u2014"}`);
|
|
2719
|
+
console.log(` Created: ${fmtDate(r.created_at)}`);
|
|
2720
|
+
if (r.summary) console.log(` Summary: ${r.summary}`);
|
|
2721
|
+
if (r.description) {
|
|
2722
|
+
console.log(` Description:`);
|
|
2723
|
+
console.log(` ${r.description.replace(/\n/g, "\n ")}`);
|
|
2724
|
+
}
|
|
2725
|
+
if (r.environment?.url) console.log(` URL: ${r.environment.url}`);
|
|
2726
|
+
if (r.component) console.log(` Component: ${r.component}`);
|
|
2727
|
+
if (r.sentry_event_id) console.log(` Sentry: ${r.sentry_event_id}`);
|
|
2728
|
+
if (r.fix_id) console.log(` Fix: ${r.fix_id}`);
|
|
2729
|
+
if (r.tags && Object.keys(r.tags).length > 0) console.log(` Tags: ${JSON.stringify(r.tags)}`);
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2621
2732
|
});
|
|
2622
|
-
reports.command("triage <id>").description("Update the status and/or severity of a report").option("--status <status>", "New status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "New severity: critical|high|medium|low").option("--note <text>", "Internal triage note").option("--json", "
|
|
2733
|
+
reports.command("triage <id>").description("Update the status and/or severity of a report").option("--status <status>", "New status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "New severity: critical|high|medium|low").option("--note <text>", "Internal triage note").option("--json", "Output as JSON (alias for -o json)").addHelpText("after", `
|
|
2623
2734
|
Examples:
|
|
2624
2735
|
mushi reports triage <id> --status triaged --severity high
|
|
2625
2736
|
mushi reports triage <id> --status in_progress --note "assigned to @alice"`).action(async (id, opts) => {
|
|
@@ -2644,16 +2755,17 @@ Examples:
|
|
|
2644
2755
|
}
|
|
2645
2756
|
die(result);
|
|
2646
2757
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2758
|
+
printResult(result.data, {
|
|
2759
|
+
json: opts.json,
|
|
2760
|
+
render() {
|
|
2761
|
+
console.log(`\u2713 Updated report ${id}`);
|
|
2762
|
+
if (opts.status) console.log(` Status: ${opts.status}`);
|
|
2763
|
+
if (opts.severity) console.log(` Severity: ${opts.severity}`);
|
|
2764
|
+
if (opts.note) console.log(` Note: ${opts.note}`);
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2655
2767
|
});
|
|
2656
|
-
reports.command("resolve <id>").description("Mark a report as resolved (shorthand for triage --status resolved)").option("--note <text>", "Resolution note").option("--json", "
|
|
2768
|
+
reports.command("resolve <id>").description("Mark a report as resolved (shorthand for triage --status resolved)").option("--note <text>", "Resolution note").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
|
|
2657
2769
|
const config = requireConfig();
|
|
2658
2770
|
const body = { status: "resolved" };
|
|
2659
2771
|
if (opts.note) body["note"] = opts.note;
|
|
@@ -2669,14 +2781,15 @@ Examples:
|
|
|
2669
2781
|
}
|
|
2670
2782
|
die(result);
|
|
2671
2783
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2784
|
+
printResult(result.data, {
|
|
2785
|
+
json: opts.json,
|
|
2786
|
+
render() {
|
|
2787
|
+
console.log(`\u2713 Resolved report ${id}`);
|
|
2788
|
+
if (opts.note) console.log(` Note: ${opts.note}`);
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2678
2791
|
});
|
|
2679
|
-
reports.command("reopen <id>").description("Reopen a resolved or dismissed report").option("--note <text>", "Note explaining the reopen").option("--json", "
|
|
2792
|
+
reports.command("reopen <id>").description("Reopen a resolved or dismissed report").option("--note <text>", "Note explaining the reopen").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
|
|
2680
2793
|
const config = requireConfig();
|
|
2681
2794
|
const body = { status: "reopened" };
|
|
2682
2795
|
if (opts.note) body["note"] = opts.note;
|
|
@@ -2692,13 +2805,14 @@ Examples:
|
|
|
2692
2805
|
}
|
|
2693
2806
|
die(result);
|
|
2694
2807
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2808
|
+
printResult(result.data, {
|
|
2809
|
+
json: opts.json,
|
|
2810
|
+
render() {
|
|
2811
|
+
console.log(`\u2713 Reopened report ${id}`);
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2700
2814
|
});
|
|
2701
|
-
reports.command("verify <id>").description("Mark a fixed report as verified by the reporter (operator shortcut)").option("--note <text>", "Optional audit note").option("--json", "
|
|
2815
|
+
reports.command("verify <id>").description("Mark a fixed report as verified by the reporter (operator shortcut)").option("--note <text>", "Optional audit note").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
|
|
2702
2816
|
const config = requireConfig();
|
|
2703
2817
|
const body = { status: "verified" };
|
|
2704
2818
|
if (opts.note) body["note"] = opts.note;
|
|
@@ -2707,10 +2821,14 @@ Examples:
|
|
|
2707
2821
|
body: JSON.stringify(body)
|
|
2708
2822
|
});
|
|
2709
2823
|
if (!result.ok) die(result);
|
|
2710
|
-
|
|
2711
|
-
|
|
2824
|
+
printResult(result.data, {
|
|
2825
|
+
json: opts.json,
|
|
2826
|
+
render() {
|
|
2827
|
+
console.log(`\u2713 Verified report ${id}`);
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2712
2830
|
});
|
|
2713
|
-
reports.command("dismiss <id>").description("Dismiss a report (not a real bug / out of scope)").option("--note <text>", "Reason for dismissal").option("--json", "
|
|
2831
|
+
reports.command("dismiss <id>").description("Dismiss a report (not a real bug / out of scope)").option("--note <text>", "Reason for dismissal").option("--json", "Output as JSON (alias for -o json)").action(async (id, opts) => {
|
|
2714
2832
|
const config = requireConfig();
|
|
2715
2833
|
const body = { status: "dismissed" };
|
|
2716
2834
|
if (opts.note) body["note"] = opts.note;
|
|
@@ -2726,13 +2844,14 @@ Examples:
|
|
|
2726
2844
|
}
|
|
2727
2845
|
die(result);
|
|
2728
2846
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2847
|
+
printResult(result.data, {
|
|
2848
|
+
json: opts.json,
|
|
2849
|
+
render() {
|
|
2850
|
+
console.log(`\u2713 Dismissed report ${id}`);
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2734
2853
|
});
|
|
2735
|
-
reports.command("reply <id> <message>").description("Send a visible reply to the reporter widget for a report").option("--author <name>", 'Display name for the sender (default: "Mushi Admin")').option("--json", "
|
|
2854
|
+
reports.command("reply <id> <message>").description("Send a visible reply to the reporter widget for a report").option("--author <name>", 'Display name for the sender (default: "Mushi Admin")').option("--json", "Output as JSON (alias for -o json)").addHelpText("after", `
|
|
2736
2855
|
Examples:
|
|
2737
2856
|
mushi reports reply abc123 "Thanks for reporting \u2014 fixing this in the next release."
|
|
2738
2857
|
mushi reports reply abc123 "Can you share a screenshot?" --author "Alice"`).action(async (id, message, opts) => {
|
|
@@ -2751,13 +2870,14 @@ Examples:
|
|
|
2751
2870
|
}
|
|
2752
2871
|
die(result);
|
|
2753
2872
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2873
|
+
printResult(result.data, {
|
|
2874
|
+
json: opts.json,
|
|
2875
|
+
render() {
|
|
2876
|
+
console.log(`\u2713 Reply sent to reporter for report ${id}`);
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2759
2879
|
});
|
|
2760
|
-
reports.command("search <query>").description("Search reports by keyword in summary and description").option("--limit <n>", "Max results (1\u201350)", "10").option("--status <status>", "Filter by status").option("--json", "
|
|
2880
|
+
reports.command("search <query>").description("Search reports by keyword in summary and description").option("--limit <n>", "Max results (1\u201350)", "10").option("--status <status>", "Filter by status").option("--json", "Output as JSON (alias for -o json)").addHelpText("after", `
|
|
2761
2881
|
Examples:
|
|
2762
2882
|
mushi reports search "login button"
|
|
2763
2883
|
mushi reports search "404 error" --status new --limit 20`).action(async (query, opts) => {
|
|
@@ -2767,24 +2887,25 @@ Examples:
|
|
|
2767
2887
|
if (opts.status) params.set("status", opts.status);
|
|
2768
2888
|
const result = await apiCall(`/v1/sync/reports?${params}`, config);
|
|
2769
2889
|
if (!result.ok) die(result);
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2890
|
+
printResult(result.data, {
|
|
2891
|
+
json: opts.json,
|
|
2892
|
+
render(d) {
|
|
2893
|
+
const rows = d.reports;
|
|
2894
|
+
if (rows.length === 0) {
|
|
2895
|
+
console.log(`No reports matching "${query}".`);
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
console.log(`${rows.length} result${rows.length === 1 ? "" : "s"} for "${query}":`);
|
|
2899
|
+
console.log("");
|
|
2900
|
+
for (const r of rows) {
|
|
2901
|
+
console.log(` ${r.id}`);
|
|
2902
|
+
console.log(` ${r.severity ?? "unset"} \xB7 ${r.status ?? "new"} \xB7 ${fmtDate(r.created_at)}`);
|
|
2903
|
+
const text2 = r.summary ?? r.description ?? "";
|
|
2904
|
+
if (text2) console.log(` ${text2.slice(0, 80)}`);
|
|
2905
|
+
console.log("");
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
});
|
|
2788
2909
|
});
|
|
2789
2910
|
}
|
|
2790
2911
|
|
|
@@ -2897,7 +3018,7 @@ AI code review context.
|
|
|
2897
3018
|
Typical CI usage:
|
|
2898
3019
|
MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
|
|
2899
3020
|
npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
|
|
2900
|
-
const { writeFile:
|
|
3021
|
+
const { writeFile: writeFile3, mkdir } = await import("fs/promises");
|
|
2901
3022
|
const nodePath2 = await import("path");
|
|
2902
3023
|
const config = requireConfig();
|
|
2903
3024
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -2925,7 +3046,7 @@ Typical CI usage:
|
|
|
2925
3046
|
return;
|
|
2926
3047
|
}
|
|
2927
3048
|
await mkdir(nodePath2.dirname(target), { recursive: true });
|
|
2928
|
-
await
|
|
3049
|
+
await writeFile3(target, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
2929
3050
|
if (opts.json) {
|
|
2930
3051
|
console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
|
|
2931
3052
|
} else {
|
|
@@ -2936,8 +3057,8 @@ Typical CI usage:
|
|
|
2936
3057
|
|
|
2937
3058
|
// src/sourcemaps.ts
|
|
2938
3059
|
import { createReadStream } from "fs";
|
|
2939
|
-
import { readFile, readdir } from "fs/promises";
|
|
2940
|
-
import { createHash } from "crypto";
|
|
3060
|
+
import { readFile, readdir, writeFile } from "fs/promises";
|
|
3061
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2941
3062
|
import { join as join3, relative, basename } from "path";
|
|
2942
3063
|
import * as p2 from "@clack/prompts";
|
|
2943
3064
|
async function findMapFiles(dir) {
|
|
@@ -2965,7 +3086,31 @@ function fileHash(path) {
|
|
|
2965
3086
|
stream.on("error", reject);
|
|
2966
3087
|
});
|
|
2967
3088
|
}
|
|
2968
|
-
async function
|
|
3089
|
+
async function injectDebugId(mapPath) {
|
|
3090
|
+
const jsPath = mapPath.endsWith(".js.map") ? mapPath.slice(0, -4) : null;
|
|
3091
|
+
if (!jsPath) return null;
|
|
3092
|
+
const debugId = randomUUID2();
|
|
3093
|
+
try {
|
|
3094
|
+
const jsContents = await readFile(jsPath, "utf-8");
|
|
3095
|
+
if (!jsContents.includes("//# debugId=")) {
|
|
3096
|
+
await writeFile(jsPath, `${jsContents}
|
|
3097
|
+
//# debugId=${debugId}`, "utf-8");
|
|
3098
|
+
}
|
|
3099
|
+
} catch {
|
|
3100
|
+
}
|
|
3101
|
+
try {
|
|
3102
|
+
const mapContents = await readFile(mapPath, "utf-8");
|
|
3103
|
+
const parsed = JSON.parse(mapContents);
|
|
3104
|
+
if (!parsed["debugId"]) {
|
|
3105
|
+
parsed["debugId"] = debugId;
|
|
3106
|
+
await writeFile(mapPath, JSON.stringify(parsed), "utf-8");
|
|
3107
|
+
}
|
|
3108
|
+
} catch {
|
|
3109
|
+
return null;
|
|
3110
|
+
}
|
|
3111
|
+
return debugId;
|
|
3112
|
+
}
|
|
3113
|
+
async function uploadFile(filePath, release, endpoint, apiKey, debugId) {
|
|
2969
3114
|
const sha256 = await fileHash(filePath);
|
|
2970
3115
|
try {
|
|
2971
3116
|
const checkRes = await fetch(
|
|
@@ -2984,6 +3129,7 @@ async function uploadFile(filePath, release, endpoint, apiKey) {
|
|
|
2984
3129
|
form.append("filename", relative(process.cwd(), filePath).replaceAll("\\", "/"));
|
|
2985
3130
|
form.append("release", release);
|
|
2986
3131
|
form.append("sha256", sha256);
|
|
3132
|
+
if (debugId) form.append("debug_id", debugId);
|
|
2987
3133
|
const res = await fetch(`${endpoint}/v1/sourcemaps`, {
|
|
2988
3134
|
method: "POST",
|
|
2989
3135
|
headers: { Authorization: `Bearer ${apiKey}`, "X-Mushi-Api-Key": apiKey },
|
|
@@ -3040,6 +3186,20 @@ async function runSourcemapsUpload(opts) {
|
|
|
3040
3186
|
p2.outro(`${files.length} file${files.length === 1 ? "" : "s"} would be uploaded`);
|
|
3041
3187
|
return;
|
|
3042
3188
|
}
|
|
3189
|
+
const debugIds = /* @__PURE__ */ new Map();
|
|
3190
|
+
if (opts.inject) {
|
|
3191
|
+
const injectSpin = p2.spinner();
|
|
3192
|
+
injectSpin.start("Injecting Debug IDs\u2026");
|
|
3193
|
+
let injected = 0;
|
|
3194
|
+
for (const filePath of files) {
|
|
3195
|
+
const debugId = await injectDebugId(filePath);
|
|
3196
|
+
if (debugId) {
|
|
3197
|
+
debugIds.set(filePath, debugId);
|
|
3198
|
+
injected++;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
injectSpin.stop(`Injected Debug IDs into ${injected} file${injected === 1 ? "" : "s"}`);
|
|
3202
|
+
}
|
|
3043
3203
|
let uploaded = 0;
|
|
3044
3204
|
let skipped = 0;
|
|
3045
3205
|
let failed = 0;
|
|
@@ -3047,7 +3207,7 @@ async function runSourcemapsUpload(opts) {
|
|
|
3047
3207
|
const rel = relative(process.cwd(), filePath).replaceAll("\\", "/");
|
|
3048
3208
|
const fs = p2.spinner();
|
|
3049
3209
|
fs.start(rel);
|
|
3050
|
-
const result = await uploadFile(filePath, opts.release, endpoint, apiKey);
|
|
3210
|
+
const result = await uploadFile(filePath, opts.release, endpoint, apiKey, debugIds.get(filePath));
|
|
3051
3211
|
if (result.skipped) {
|
|
3052
3212
|
fs.stop(`\u21A9 ${rel} (already uploaded)`);
|
|
3053
3213
|
skipped++;
|
|
@@ -3078,7 +3238,7 @@ async function runSourcemapsUpload(opts) {
|
|
|
3078
3238
|
|
|
3079
3239
|
// src/commands/diagnostics.ts
|
|
3080
3240
|
function registerDiagnosticsCommands(program2) {
|
|
3081
|
-
program2.command("test").description("Submit a synthetic test report to verify the ingestion pipeline end-to-end").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
3241
|
+
program2.command("test").description("Submit a synthetic test report to verify the ingestion pipeline end-to-end").option("--json", "Machine-readable JSON output (alias for -o json)").action(async (opts) => {
|
|
3082
3242
|
const config = requireConfig();
|
|
3083
3243
|
const result = await apiCall("/v1/reports", config, {
|
|
3084
3244
|
method: "POST",
|
|
@@ -3101,17 +3261,17 @@ function registerDiagnosticsCommands(program2) {
|
|
|
3101
3261
|
})
|
|
3102
3262
|
});
|
|
3103
3263
|
if (!result.ok) die(result);
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
}
|
|
3264
|
+
printResult(result.data, {
|
|
3265
|
+
json: opts.json,
|
|
3266
|
+
render(d) {
|
|
3267
|
+
console.log(`\u2713 Test report submitted`);
|
|
3268
|
+
console.log(` ID: ${d.reportId}`);
|
|
3269
|
+
console.log(` Status: ${d.status}`);
|
|
3270
|
+
console.log(` View: ${reportsUrl(resolveConsoleUrlSync(), d.reportId)}`);
|
|
3271
|
+
}
|
|
3272
|
+
});
|
|
3113
3273
|
});
|
|
3114
|
-
program2.command("index <path>").description("Walk a local repo and upload code chunks to the Mushi RAG indexer").option("--language <lang>", "Limit to one language: ts, tsx, js, py, go, rs").option("--dry-run", "Show what would be uploaded without sending").option("--json", "Machine-readable summary: { files, bytes }").addHelpText("after", `
|
|
3274
|
+
program2.command("index <path>").description("Walk a local repo and upload code chunks to the Mushi RAG indexer").option("--language <lang>", "Limit to one language: ts, tsx, js, py, go, rs").option("--dry-run", "Show what would be uploaded without sending").option("--json", "Machine-readable summary: { files, bytes } (alias for -o json)").addHelpText("after", `
|
|
3115
3275
|
Uploads source code into the Mushi vector index so the fix-worker can
|
|
3116
3276
|
retrieve relevant context when generating patches. Only needed for private
|
|
3117
3277
|
repos that cannot be auto-indexed via GitHub App.
|
|
@@ -3169,8 +3329,9 @@ Examples:
|
|
|
3169
3329
|
`);
|
|
3170
3330
|
}
|
|
3171
3331
|
}
|
|
3172
|
-
|
|
3173
|
-
|
|
3332
|
+
const summary = { ok: errors === 0, files: count, bytes, errors };
|
|
3333
|
+
if (outputIsJson(opts.json)) {
|
|
3334
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3174
3335
|
} else {
|
|
3175
3336
|
const kb = (bytes / 1024).toFixed(1);
|
|
3176
3337
|
console.log(`
|
|
@@ -3179,13 +3340,15 @@ Indexed ${count} files (${kb} KB) into project ${config.projectId}${errors ? ` \
|
|
|
3179
3340
|
if (errors > 0) process.exit(1);
|
|
3180
3341
|
});
|
|
3181
3342
|
const sourcemaps = program2.command("sourcemaps").description("Source map management");
|
|
3182
|
-
sourcemaps.command("upload").description("Upload source maps to Mushi (idempotent, SHA256-keyed) for stack trace symbolication").requiredOption("--release <version>", "Release identifier, e.g. 1.0.0 or a git SHA").option("--dir <path>", "Directory containing .map files", "./dist").option("--dry-run", "List files that would be uploaded without uploading").option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--silent", "Suppress progress output").addHelpText("after", `
|
|
3343
|
+
sourcemaps.command("upload").description("Upload source maps to Mushi (idempotent, SHA256-keyed) for stack trace symbolication").requiredOption("--release <version>", "Release identifier, e.g. 1.0.0 or a git SHA").option("--dir <path>", "Directory containing .map files", "./dist").option("--inject", "Inject a Debug ID (UUID) into each .js and .map before uploading for exact stack-trace resolution").option("--dry-run", "List files that would be uploaded without uploading").option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--silent", "Suppress progress output").addHelpText("after", `
|
|
3183
3344
|
Examples:
|
|
3184
3345
|
mushi sourcemaps upload --release 1.0.0
|
|
3185
|
-
mushi sourcemaps upload --release $(git rev-parse --short HEAD) --dir ./dist
|
|
3346
|
+
mushi sourcemaps upload --release $(git rev-parse --short HEAD) --dir ./dist
|
|
3347
|
+
mushi sourcemaps upload --release 1.0.0 --inject # recommended: adds Debug IDs`).action(async (opts) => {
|
|
3186
3348
|
await runSourcemapsUpload({
|
|
3187
3349
|
release: opts.release,
|
|
3188
3350
|
dir: opts.dir,
|
|
3351
|
+
inject: opts.inject,
|
|
3189
3352
|
dryRun: opts.dryRun,
|
|
3190
3353
|
endpoint: opts.endpoint,
|
|
3191
3354
|
apiKey: opts.apiKey,
|
|
@@ -3195,7 +3358,7 @@ Examples:
|
|
|
3195
3358
|
}
|
|
3196
3359
|
|
|
3197
3360
|
// src/project-bootstrap.ts
|
|
3198
|
-
import { writeFile, readFile as readFile2 } from "fs/promises";
|
|
3361
|
+
import { writeFile as writeFile2, readFile as readFile2 } from "fs/promises";
|
|
3199
3362
|
import nodePath from "path";
|
|
3200
3363
|
async function writeProjectBootstrapFiles(opts) {
|
|
3201
3364
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -3219,7 +3382,7 @@ async function writeProjectBootstrapFiles(opts) {
|
|
|
3219
3382
|
const prefix = stripped.length > 0 ? `${stripped}
|
|
3220
3383
|
|
|
3221
3384
|
` : "";
|
|
3222
|
-
await
|
|
3385
|
+
await writeFile2(envPath, `${prefix}${mushiBlock}
|
|
3223
3386
|
`, "utf8");
|
|
3224
3387
|
const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
|
|
3225
3388
|
const serverName = buildMcpServerName({ legacy: true });
|
|
@@ -3366,7 +3529,7 @@ function resolveLoginEndpoint(optsEndpoint, existingConfigEndpoint, envEndpoint)
|
|
|
3366
3529
|
return optsEndpoint ?? existingConfigEndpoint ?? envEndpoint?.trim();
|
|
3367
3530
|
}
|
|
3368
3531
|
function registerSetupCommands(program2) {
|
|
3369
|
-
program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").option("--verify", "Probe the MCP key after writing to confirm it has mcp:read scope (default: on)").option("--no-verify", "Skip the post-write key probe").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted) \u2014 used for first-run login if not yet configured").option("--stdio", "Write a local stdio entry with an API key instead of the hosted OAuth URL (cursor/claude default is hosted OAuth \u2014 no key on disk)").option("--ci", "Alias for --stdio: headless environments cannot drive the browser OAuth flow").addHelpText("after", `
|
|
3532
|
+
program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").option("--verify", "Probe the MCP key after writing to confirm it has mcp:read scope (default: on)").option("--no-verify", "Skip the post-write key probe").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted) \u2014 used for first-run login if not yet configured").option("--stdio", "Write a local stdio entry with an API key instead of the hosted OAuth URL (cursor/claude default is hosted OAuth \u2014 no key on disk)").option("--ci", "Alias for --stdio: headless environments cannot drive the browser OAuth flow").option("--inline-key", "Write the literal API key into the mcp.json env block (default: write ${MUSHI_API_KEY} placeholder to prevent accidental git leaks)").addHelpText("after", `
|
|
3370
3533
|
Examples:
|
|
3371
3534
|
mushi setup # wire Cursor (default: hosted OAuth \u2014 sign in from the IDE, no key on disk)
|
|
3372
3535
|
mushi setup --stdio # local stdio entry with an API key (previous behavior)
|
|
@@ -3387,7 +3550,7 @@ key for you \u2014 nothing sensitive is written to the repo. Pass --stdio/--ci t
|
|
|
3387
3550
|
get the local subprocess entry with an API key instead.
|
|
3388
3551
|
|
|
3389
3552
|
The command reads credentials from ~/.config/mushi/config.json. If you are not logged in yet, it runs browser sign-in first, then writes MCP config.`).action(async (opts) => {
|
|
3390
|
-
const { writeFile:
|
|
3553
|
+
const { writeFile: writeFile3, mkdir, readFile: readFile3 } = await import("fs/promises");
|
|
3391
3554
|
const { existsSync: existsSync3 } = await import("fs");
|
|
3392
3555
|
const nodePath2 = await import("path");
|
|
3393
3556
|
const os = await import("os");
|
|
@@ -3473,15 +3636,13 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3473
3636
|
}
|
|
3474
3637
|
const hostedMcpUrl = (config.endpoint ?? "").replace(/\/api\/?$/, "/mcp");
|
|
3475
3638
|
const hostedServerBlock = { url: hostedMcpUrl };
|
|
3476
|
-
const
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
}
|
|
3484
|
-
};
|
|
3639
|
+
const inlineKey = !!opts.inlineKey;
|
|
3640
|
+
const mcpServerBlock = buildMcpServerBlock({
|
|
3641
|
+
endpoint: config.endpoint,
|
|
3642
|
+
projectId: config.projectId ?? "",
|
|
3643
|
+
apiKey: config.apiKey,
|
|
3644
|
+
inlineKey
|
|
3645
|
+
});
|
|
3485
3646
|
const configDir = nodePath2.isAbsolute(ideEntry.dir) ? ideEntry.dir : nodePath2.join(cwd, ideEntry.dir);
|
|
3486
3647
|
const configPath = nodePath2.join(configDir, ideEntry.file);
|
|
3487
3648
|
if (ideEntry.format === "mcp-json") {
|
|
@@ -3491,7 +3652,8 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3491
3652
|
const pBlock = buildMcpServerBlock({
|
|
3492
3653
|
endpoint: config.endpoint,
|
|
3493
3654
|
projectId: p3.id,
|
|
3494
|
-
apiKey: config.apiKey
|
|
3655
|
+
apiKey: config.apiKey,
|
|
3656
|
+
inlineKey
|
|
3495
3657
|
});
|
|
3496
3658
|
if (!opts.dryRun) {
|
|
3497
3659
|
await writeMcpServerEntry({ configPath, serverName: pServerName, serverBlock: pBlock });
|
|
@@ -3515,6 +3677,8 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3515
3677
|
console.log(" Hosted MCP with OAuth login \u2014 no API key was written to this file.");
|
|
3516
3678
|
console.log(` Restart ${opts.ide === "cursor" ? "Cursor" : "Claude Code"}, open the MCP panel (/mcp in Claude Code), pick "${serverName}" and sign in via the browser.`);
|
|
3517
3679
|
console.log(" Prefer a local key-based entry (headless/CI)? Re-run with --stdio.");
|
|
3680
|
+
} else if (!inlineKey && config.apiKey) {
|
|
3681
|
+
printKeyExportHint(config.apiKey);
|
|
3518
3682
|
}
|
|
3519
3683
|
}
|
|
3520
3684
|
}
|
|
@@ -3535,7 +3699,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3535
3699
|
env: {
|
|
3536
3700
|
MUSHI_API_ENDPOINT: config.endpoint,
|
|
3537
3701
|
MUSHI_PROJECT_ID: config.projectId ?? "",
|
|
3538
|
-
MUSHI_API_KEY: config.apiKey
|
|
3702
|
+
MUSHI_API_KEY: inlineKey ? config.apiKey : "${MUSHI_API_KEY}"
|
|
3539
3703
|
}
|
|
3540
3704
|
},
|
|
3541
3705
|
settings: {}
|
|
@@ -3547,7 +3711,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3547
3711
|
console.log(redactKeyForDisplay(output));
|
|
3548
3712
|
} else {
|
|
3549
3713
|
await mkdir(configDir, { recursive: true });
|
|
3550
|
-
await
|
|
3714
|
+
await writeFile3(configPath, output, "utf8");
|
|
3551
3715
|
console.log(`\u2713 Written ${configPath}`);
|
|
3552
3716
|
}
|
|
3553
3717
|
}
|
|
@@ -3581,7 +3745,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3581
3745
|
if (opts.dryRun) {
|
|
3582
3746
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
3583
3747
|
} else {
|
|
3584
|
-
await
|
|
3748
|
+
await writeFile3(rulesPath, rulesContent, "utf8");
|
|
3585
3749
|
console.log(`\u2713 Written .cursorrules`);
|
|
3586
3750
|
}
|
|
3587
3751
|
} else if (opts.ide === "claude") {
|
|
@@ -3591,7 +3755,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3591
3755
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
3592
3756
|
} else {
|
|
3593
3757
|
await mkdir(rulesDir, { recursive: true });
|
|
3594
|
-
await
|
|
3758
|
+
await writeFile3(rulesPath, rulesContent, "utf8");
|
|
3595
3759
|
console.log(`\u2713 Written .claude/rules/mushi.md`);
|
|
3596
3760
|
}
|
|
3597
3761
|
}
|
|
@@ -3654,7 +3818,8 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3654
3818
|
serverBlock: buildMcpServerBlock({
|
|
3655
3819
|
endpoint: fresh.endpoint ?? config.endpoint,
|
|
3656
3820
|
projectId: fresh.projectId ?? config.projectId ?? "",
|
|
3657
|
-
apiKey: fresh.apiKey
|
|
3821
|
+
apiKey: fresh.apiKey,
|
|
3822
|
+
inlineKey
|
|
3658
3823
|
})
|
|
3659
3824
|
});
|
|
3660
3825
|
console.log(`\u2713 Rewrote ${configPath} with the upgraded key`);
|
|
@@ -3707,12 +3872,12 @@ Note: ${configRelPath} holds no secrets (OAuth login) \u2014 safe to commit and
|
|
|
3707
3872
|
const gitignorePath = nodePath2.join(cwd, ".gitignore");
|
|
3708
3873
|
const ignoreLine = configRelPath.replaceAll("\\", "/");
|
|
3709
3874
|
try {
|
|
3710
|
-
const { readFile: readFile4, writeFile:
|
|
3875
|
+
const { readFile: readFile4, writeFile: writeFile4 } = await import("fs/promises");
|
|
3711
3876
|
const existing = await readFile4(gitignorePath, "utf8").catch(() => "");
|
|
3712
3877
|
const lines = existing.split(/\r?\n/);
|
|
3713
3878
|
if (!lines.some((l) => l.trim() === ignoreLine)) {
|
|
3714
3879
|
const sep = existing.endsWith("\n") || existing === "" ? "" : "\n";
|
|
3715
|
-
await
|
|
3880
|
+
await writeFile4(
|
|
3716
3881
|
gitignorePath,
|
|
3717
3882
|
`${existing}${sep}# Mushi MCP config holds a plaintext API key
|
|
3718
3883
|
${ignoreLine}
|
|
@@ -4209,13 +4374,134 @@ async function runUpgrade(opts = {}) {
|
|
|
4209
4374
|
};
|
|
4210
4375
|
}
|
|
4211
4376
|
|
|
4377
|
+
// src/self-upgrade.ts
|
|
4378
|
+
import { execSync as execSync2 } from "child_process";
|
|
4379
|
+
var CLI_PACKAGE = "@mushi-mushi/cli";
|
|
4380
|
+
var SAFE_NPM_VERSION2 = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
4381
|
+
function detectInstallMethod(argv1 = process.argv[1] ?? "", userAgent = process.env["npm_config_user_agent"] ?? "") {
|
|
4382
|
+
const path = argv1.replace(/\\/g, "/").toLowerCase();
|
|
4383
|
+
if (path.includes("/_npx/") || path.includes("/.npm/_npx/")) return "npx";
|
|
4384
|
+
if (path.includes("/dlx-") || path.includes("/pnpm/dlx")) return "npx";
|
|
4385
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
4386
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
4387
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
4388
|
+
if (userAgent.startsWith("npm")) return "npm";
|
|
4389
|
+
if (path.includes("/.bun/")) return "bun";
|
|
4390
|
+
if (path.includes("/pnpm/")) return "pnpm";
|
|
4391
|
+
if (path.includes("/yarn/") || path.includes("/.yarn/")) return "yarn";
|
|
4392
|
+
if (path.includes("/npm/") || path.includes("node_modules")) return "npm";
|
|
4393
|
+
return "unknown";
|
|
4394
|
+
}
|
|
4395
|
+
function selfUpgradeCommand(method, version) {
|
|
4396
|
+
if (!SAFE_NPM_VERSION2.test(version)) return null;
|
|
4397
|
+
const spec = `${CLI_PACKAGE}@${version}`;
|
|
4398
|
+
switch (method) {
|
|
4399
|
+
case "npm":
|
|
4400
|
+
return `npm install -g ${spec}`;
|
|
4401
|
+
case "pnpm":
|
|
4402
|
+
return `pnpm add -g ${spec}`;
|
|
4403
|
+
case "yarn":
|
|
4404
|
+
return `yarn global add ${spec}`;
|
|
4405
|
+
case "bun":
|
|
4406
|
+
return `bun add -g ${spec}`;
|
|
4407
|
+
case "npx":
|
|
4408
|
+
case "unknown":
|
|
4409
|
+
default:
|
|
4410
|
+
return null;
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
async function runSelfUpgrade(opts = {}) {
|
|
4414
|
+
const current = MUSHI_CLI_VERSION;
|
|
4415
|
+
const method = detectInstallMethod();
|
|
4416
|
+
const freshness = await checkFreshness(CLI_PACKAGE, current, {
|
|
4417
|
+
timeoutMs: 4e3,
|
|
4418
|
+
ignoreOptOut: true
|
|
4419
|
+
// an explicit `upgrade --self` must always hit the registry
|
|
4420
|
+
});
|
|
4421
|
+
const latest = freshness?.latest ?? null;
|
|
4422
|
+
const safeLatest = latest && SAFE_NPM_VERSION2.test(latest) ? latest : null;
|
|
4423
|
+
if (!freshness) {
|
|
4424
|
+
return {
|
|
4425
|
+
current,
|
|
4426
|
+
latest: null,
|
|
4427
|
+
method,
|
|
4428
|
+
command: null,
|
|
4429
|
+
upgraded: false,
|
|
4430
|
+
message: "Could not reach the npm registry to check for a newer CLI \u2014 try again in a moment."
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
if (!freshness.isOutdated || !safeLatest) {
|
|
4434
|
+
return {
|
|
4435
|
+
current,
|
|
4436
|
+
latest,
|
|
4437
|
+
method,
|
|
4438
|
+
command: null,
|
|
4439
|
+
upgraded: false,
|
|
4440
|
+
message: `mushi CLI is already at the latest stable version (v${current}).`
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
4443
|
+
const command = selfUpgradeCommand(method, safeLatest);
|
|
4444
|
+
if (!command) {
|
|
4445
|
+
const via = method === "npx" ? "You are running via npx \u2014 the next `npx mushi` will fetch the latest automatically." : `Could not detect how the CLI was installed. Upgrade manually:
|
|
4446
|
+
npm install -g ${CLI_PACKAGE}@${safeLatest}`;
|
|
4447
|
+
return { current, latest, method, command: null, upgraded: false, message: `A newer CLI is available (v${current} \u2192 v${safeLatest}).
|
|
4448
|
+
${via}` };
|
|
4449
|
+
}
|
|
4450
|
+
if (opts.dryRun) {
|
|
4451
|
+
return {
|
|
4452
|
+
current,
|
|
4453
|
+
latest,
|
|
4454
|
+
method,
|
|
4455
|
+
command,
|
|
4456
|
+
upgraded: false,
|
|
4457
|
+
message: `[dry-run] Would run: ${command}`
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
try {
|
|
4461
|
+
const exec = opts.exec ?? ((cmd) => execSync2(cmd, { stdio: "inherit", env: process.env }));
|
|
4462
|
+
exec(command);
|
|
4463
|
+
} catch {
|
|
4464
|
+
return {
|
|
4465
|
+
current,
|
|
4466
|
+
latest,
|
|
4467
|
+
method,
|
|
4468
|
+
command,
|
|
4469
|
+
upgraded: false,
|
|
4470
|
+
message: `Self-upgrade command failed \u2014 run it manually:
|
|
4471
|
+
${command}`
|
|
4472
|
+
};
|
|
4473
|
+
}
|
|
4474
|
+
return {
|
|
4475
|
+
current,
|
|
4476
|
+
latest,
|
|
4477
|
+
method,
|
|
4478
|
+
command,
|
|
4479
|
+
upgraded: true,
|
|
4480
|
+
message: `Upgraded mushi CLI: v${current} \u2192 v${safeLatest}.`
|
|
4481
|
+
};
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4212
4484
|
// src/commands/upgrade-cli.ts
|
|
4213
4485
|
function registerUpgradeCommand(program2) {
|
|
4214
|
-
program2.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--dry-run", "Print the install command without running it").option("--json", "Machine-readable plan + result").addHelpText("after", `
|
|
4486
|
+
program2.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--self", "Upgrade the globally-installed `mushi` CLI itself (not the project SDK deps)").option("--dry-run", "Print the install command without running it").option("--json", "Machine-readable plan + result").addHelpText("after", `
|
|
4215
4487
|
Examples:
|
|
4216
4488
|
mushi upgrade
|
|
4217
4489
|
mushi upgrade --dry-run
|
|
4490
|
+
mushi upgrade --self # upgrade the CLI binary itself
|
|
4491
|
+
mushi upgrade --self --dry-run
|
|
4218
4492
|
mushi upgrade --cwd ../glot.it`).action(async (opts) => {
|
|
4493
|
+
if (opts.self) {
|
|
4494
|
+
const selfResult = await runSelfUpgrade({ dryRun: opts.dryRun });
|
|
4495
|
+
if (opts.json) {
|
|
4496
|
+
console.log(JSON.stringify(selfResult, null, 2));
|
|
4497
|
+
} else {
|
|
4498
|
+
console.log(selfResult.message);
|
|
4499
|
+
}
|
|
4500
|
+
if (!selfResult.upgraded && selfResult.command && !opts.dryRun) {
|
|
4501
|
+
process.exit(1);
|
|
4502
|
+
}
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4219
4505
|
const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
|
|
4220
4506
|
if (opts.json) {
|
|
4221
4507
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -4552,7 +4838,7 @@ Approve: mushi tdd approve <id>`);
|
|
|
4552
4838
|
// src/commands/keys.ts
|
|
4553
4839
|
function registerKeysCommands(program2) {
|
|
4554
4840
|
const keys = program2.command("keys").description("Manage API key pool (BYOK)");
|
|
4555
|
-
keys.command("list").description("List all API keys in the pool with their status").action(async () => {
|
|
4841
|
+
keys.command("list").description("List all API keys in the pool with their status").option("--json", "Output as JSON (alias for -o json)").action(async (opts) => {
|
|
4556
4842
|
const config = loadConfig();
|
|
4557
4843
|
if (!config.apiKey || !config.projectId) {
|
|
4558
4844
|
console.error("Run `mushi login` first");
|
|
@@ -4566,14 +4852,79 @@ function registerKeysCommands(program2) {
|
|
|
4566
4852
|
console.error(`Error: ${res.error.message}`);
|
|
4567
4853
|
process.exit(1);
|
|
4568
4854
|
}
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4855
|
+
printResult(res.data, {
|
|
4856
|
+
json: opts.json,
|
|
4857
|
+
render(d) {
|
|
4858
|
+
if (d.keys.length === 0) {
|
|
4859
|
+
console.log("No keys configured.");
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
4862
|
+
for (const k of d.keys) {
|
|
4863
|
+
const cooldown = k.cooldown_until && new Date(k.cooldown_until) > /* @__PURE__ */ new Date() ? ` [cooldown until ${new Date(k.cooldown_until).toLocaleTimeString()}]` : "";
|
|
4864
|
+
console.log(`${k.provider_slug.padEnd(14)} [${k.status}] p=${k.priority} ${k.label ?? "(no label)"}${cooldown} \u2014 ${k.id}`);
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
});
|
|
4868
|
+
});
|
|
4869
|
+
keys.command("rotate").description("Rotate the project API key stored in local config (mints a fresh key)").option("--json", "Output as JSON (alias for -o json)").action(async (opts) => {
|
|
4870
|
+
const config = loadConfig();
|
|
4871
|
+
if (!config.projectId) {
|
|
4872
|
+
console.error("Run `mushi login` first");
|
|
4873
|
+
process.exit(1);
|
|
4572
4874
|
}
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4875
|
+
if (!config.endpoint) {
|
|
4876
|
+
console.error("No endpoint configured \u2014 run `mushi login` first");
|
|
4877
|
+
process.exit(1);
|
|
4576
4878
|
}
|
|
4879
|
+
console.log("Authenticating to rotate key\u2026");
|
|
4880
|
+
const session = await startDeviceAuth(config.endpoint).catch((err) => {
|
|
4881
|
+
console.error(`Error starting auth: ${err.message}`);
|
|
4882
|
+
process.exit(1);
|
|
4883
|
+
});
|
|
4884
|
+
console.log(`
|
|
4885
|
+
Open this URL to confirm rotation:
|
|
4886
|
+
${session.verification_uri}`);
|
|
4887
|
+
console.log(` Enter code: ${session.user_code}
|
|
4888
|
+
`);
|
|
4889
|
+
const cliToken = await waitForCliToken(config.endpoint, session).catch((err) => {
|
|
4890
|
+
console.error(`Auth failed: ${err.message}`);
|
|
4891
|
+
process.exit(1);
|
|
4892
|
+
});
|
|
4893
|
+
const existingScopes = Array.isArray(config.scopes) ? config.scopes : ["report:write", "mcp:read"];
|
|
4894
|
+
const newKey = await mintProjectKey(config.endpoint, cliToken, config.projectId, {
|
|
4895
|
+
label: "rotated",
|
|
4896
|
+
scopes: existingScopes
|
|
4897
|
+
}).catch((err) => {
|
|
4898
|
+
console.error(`Could not mint new key: ${err.message}`);
|
|
4899
|
+
process.exit(1);
|
|
4900
|
+
});
|
|
4901
|
+
const oldKey = config.apiKey;
|
|
4902
|
+
const oldPrefix = oldKey?.slice(0, 12) ?? "(none)";
|
|
4903
|
+
config.apiKey = newKey;
|
|
4904
|
+
saveConfig(config);
|
|
4905
|
+
let serverRevoked = false;
|
|
4906
|
+
if (oldKey && config.projectId) {
|
|
4907
|
+
const revokeResult = await revokeProjectKey(
|
|
4908
|
+
config.endpoint,
|
|
4909
|
+
cliToken,
|
|
4910
|
+
config.projectId,
|
|
4911
|
+
oldPrefix
|
|
4912
|
+
).catch(() => null);
|
|
4913
|
+
serverRevoked = revokeResult?.revoked === 1;
|
|
4914
|
+
}
|
|
4915
|
+
printResult({ newPrefix: newKey.slice(0, 12), oldPrefix, serverRevoked }, {
|
|
4916
|
+
json: opts.json,
|
|
4917
|
+
render(d) {
|
|
4918
|
+
console.log(`\u2713 Key rotated.`);
|
|
4919
|
+
console.log(` Old prefix: ${d.oldPrefix}\u2026 \u2192 New prefix: ${d.newPrefix}\u2026`);
|
|
4920
|
+
console.log(` New key saved to local config.`);
|
|
4921
|
+
if (d.serverRevoked) {
|
|
4922
|
+
console.log(` \u2713 Old key revoked on the server.`);
|
|
4923
|
+
} else {
|
|
4924
|
+
console.log(` \u2139 Old key may still be active \u2014 revoke it at: ${config.consoleUrl ?? "your Mushi console"} \u2192 Settings \u2192 API Keys`);
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
});
|
|
4577
4928
|
});
|
|
4578
4929
|
keys.command("add").description("Add a new API key to the pool").requiredOption("--provider <p>", "Provider: anthropic, openai, firecrawl, browserbase, cursor").option("--key <k>", "The API key value (prefer the MUSHI_BYOK_KEY env var to keep it out of shell history)").option("--label <l>", "Human-readable label").option("--priority <n>", "Priority (lower = higher priority)", "100").action(async (opts) => {
|
|
4579
4930
|
const config = loadConfig();
|
|
@@ -5492,7 +5843,7 @@ Examples:
|
|
|
5492
5843
|
}
|
|
5493
5844
|
|
|
5494
5845
|
// src/commands/selfhost.ts
|
|
5495
|
-
import { execSync as
|
|
5846
|
+
import { execSync as execSync3, spawnSync } from "child_process";
|
|
5496
5847
|
import { randomBytes } from "crypto";
|
|
5497
5848
|
function hasSupabaseCli() {
|
|
5498
5849
|
try {
|
|
@@ -5510,7 +5861,7 @@ function run(cmd, opts) {
|
|
|
5510
5861
|
const label = opts.label ?? cmd.slice(0, 60);
|
|
5511
5862
|
process.stdout.write(` \u203A ${label} \u2026 `);
|
|
5512
5863
|
try {
|
|
5513
|
-
const out =
|
|
5864
|
+
const out = execSync3(cmd, {
|
|
5514
5865
|
cwd: opts.cwd,
|
|
5515
5866
|
encoding: "utf8",
|
|
5516
5867
|
timeout: 3e5,
|
|
@@ -5823,14 +6174,54 @@ Examples:
|
|
|
5823
6174
|
});
|
|
5824
6175
|
}
|
|
5825
6176
|
|
|
6177
|
+
// src/commands/profile.ts
|
|
6178
|
+
function registerProfileCommands(program2) {
|
|
6179
|
+
const profile = program2.command("profile").description("Manage credential profiles (default, staging, per-client)");
|
|
6180
|
+
profile.command("list").alias("ls").description("List all profiles; the active one is marked with *").option("--json", "Machine-readable JSON output").action((opts) => {
|
|
6181
|
+
const { active, profiles } = listProfiles();
|
|
6182
|
+
if (outputIsJson(opts.json)) {
|
|
6183
|
+
console.log(JSON.stringify({ active, profiles }, null, 2));
|
|
6184
|
+
return;
|
|
6185
|
+
}
|
|
6186
|
+
for (const name of profiles.sort()) {
|
|
6187
|
+
const marker = name === active ? "*" : " ";
|
|
6188
|
+
const cfg = loadConfig(void 0, { profile: name });
|
|
6189
|
+
const state = cfg.apiKey ? "configured" : "empty";
|
|
6190
|
+
console.log(`${marker} ${name.padEnd(20)} [${state}]`);
|
|
6191
|
+
}
|
|
6192
|
+
});
|
|
6193
|
+
profile.command("current").description("Print the active profile name").action(() => {
|
|
6194
|
+
console.log(listProfiles().active);
|
|
6195
|
+
});
|
|
6196
|
+
profile.command("use <name>").description("Switch the active profile (creates an empty one if it does not exist)").action((name) => {
|
|
6197
|
+
const safe = name.trim();
|
|
6198
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(safe)) {
|
|
6199
|
+
process.stderr.write("error: profile name must be 1\u201364 chars of [A-Za-z0-9._-]\n");
|
|
6200
|
+
process.exit(2);
|
|
6201
|
+
}
|
|
6202
|
+
const before = listProfiles();
|
|
6203
|
+
setActiveProfile(safe);
|
|
6204
|
+
const isNew = !before.profiles.includes(safe);
|
|
6205
|
+
console.log(`\u2713 Active profile: ${safe}${isNew ? " (new \u2014 run `mushi login` to add credentials)" : ""}`);
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
|
|
5826
6209
|
// src/index.ts
|
|
5827
6210
|
installSignalHandlers();
|
|
5828
|
-
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).
|
|
6211
|
+
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) => {
|
|
6212
|
+
const opts = actionCommand.optsWithGlobals();
|
|
6213
|
+
const profile = opts["profile"];
|
|
6214
|
+
if (profile && profile.trim()) {
|
|
6215
|
+
process.env["MUSHI_PROFILE"] = profile.trim();
|
|
6216
|
+
}
|
|
6217
|
+
setGlobalOutputFormat(opts["output"]);
|
|
6218
|
+
}).addHelpText("after", `
|
|
5829
6219
|
Environment variables:
|
|
5830
6220
|
MUSHI_API_KEY SDK ingest key (report:write scope \u2014 from Onboarding \u2192 Verify in the console)
|
|
5831
6221
|
MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
|
|
5832
6222
|
MUSHI_API_ENDPOINT Supabase edge function URL
|
|
5833
6223
|
e.g. https://<ref>.supabase.co/functions/v1/api
|
|
6224
|
+
MUSHI_PROFILE Credential profile to use (see \`mushi profile\`)
|
|
5834
6225
|
|
|
5835
6226
|
Exit codes:
|
|
5836
6227
|
0 success
|
|
@@ -5862,6 +6253,7 @@ registerAuditCommands(program);
|
|
|
5862
6253
|
registerSkillsCommands(program);
|
|
5863
6254
|
registerBillingCommands(program);
|
|
5864
6255
|
registerSelfhostCommands(program);
|
|
6256
|
+
registerProfileCommands(program);
|
|
5865
6257
|
registerCompletionCommand(program);
|
|
5866
6258
|
program.parseAsync().catch((err) => {
|
|
5867
6259
|
printAndExit(err);
|