@mushi-mushi/cli 0.20.0 → 0.22.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/CONTRIBUTING.md +14 -2
- package/README.md +11 -10
- package/SECURITY.md +1 -1
- package/dist/chunk-5KN6S63C.js +6 -0
- package/dist/{chunk-7Q44KWGN.js → chunk-JAUKBLPK.js} +15 -9
- package/dist/{chunk-D5IDNBJO.js → chunk-R2HPR2FV.js} +15 -9
- package/dist/{connect-IC7I76Z2.js → connect-2RM43XHO.js} +1 -1
- package/dist/{connect-W2Q6N7K3.js → connect-JIEIWTF3.js} +1 -1
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.js +1266 -792
- package/dist/init.js +107 -30
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-6LM2KW4S.js +0 -6
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
sanitizeProjectId,
|
|
19
19
|
saveConfig,
|
|
20
20
|
writeMcpServerEntry
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-R2HPR2FV.js";
|
|
22
22
|
import {
|
|
23
23
|
CLOUD_API_ENDPOINT,
|
|
24
24
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
@@ -78,6 +78,23 @@ var MushiCliError = class extends Error {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
|
+
function printAndExit(err, asJson = false) {
|
|
82
|
+
const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
|
|
83
|
+
"E_INTERNAL",
|
|
84
|
+
err instanceof Error ? err.message : String(err),
|
|
85
|
+
"this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
|
|
86
|
+
err
|
|
87
|
+
);
|
|
88
|
+
if (asJson) {
|
|
89
|
+
process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
|
|
90
|
+
} else {
|
|
91
|
+
process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
|
|
92
|
+
`);
|
|
93
|
+
if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
process.exit(cliErr.exitCode);
|
|
97
|
+
}
|
|
81
98
|
|
|
82
99
|
// src/signals.ts
|
|
83
100
|
var installed = false;
|
|
@@ -110,13 +127,13 @@ function getAbortSignal(external) {
|
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
// src/version.ts
|
|
113
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
130
|
+
var MUSHI_CLI_VERSION = true ? "0.22.0" : "0.0.0-dev";
|
|
114
131
|
|
|
115
132
|
// src/init.ts
|
|
116
133
|
import * as p from "@clack/prompts";
|
|
117
134
|
import { spawn } from "child_process";
|
|
118
135
|
import { randomUUID } from "crypto";
|
|
119
|
-
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
136
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
120
137
|
import { join as join3 } from "path";
|
|
121
138
|
|
|
122
139
|
// src/console-url.ts
|
|
@@ -237,6 +254,20 @@ async function openInBrowser(url) {
|
|
|
237
254
|
|
|
238
255
|
// src/cli-shared.ts
|
|
239
256
|
var API_TIMEOUT_MS = 15e3;
|
|
257
|
+
async function probeEndpointHealth(endpoint) {
|
|
258
|
+
const t0 = Date.now();
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
261
|
+
try {
|
|
262
|
+
const res = await fetch(`${endpoint}/health`, { signal: controller.signal });
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
|
|
265
|
+
return { ok: res.ok, status: res.status, latencyMs: Date.now() - t0, body };
|
|
266
|
+
} catch (err) {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
240
271
|
async function apiCall(path, config, options = {}) {
|
|
241
272
|
const endpoint = config.endpoint;
|
|
242
273
|
if (!endpoint) {
|
|
@@ -292,13 +323,13 @@ async function apiCall(path, config, options = {}) {
|
|
|
292
323
|
}
|
|
293
324
|
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
294
325
|
const b = body;
|
|
326
|
+
const nested = b["error"];
|
|
327
|
+
const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
|
|
328
|
+
const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
|
|
295
329
|
return {
|
|
296
330
|
ok: false,
|
|
297
331
|
httpStatus: res.status,
|
|
298
|
-
error: {
|
|
299
|
-
code: b["code"] ?? `HTTP_${res.status}`,
|
|
300
|
-
message: b["message"] ?? `Request failed (${res.status})`
|
|
301
|
-
}
|
|
332
|
+
error: { code: errCode, message: errMsg }
|
|
302
333
|
};
|
|
303
334
|
}
|
|
304
335
|
return body;
|
|
@@ -327,24 +358,31 @@ function die(result, exitCode = 1) {
|
|
|
327
358
|
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
328
359
|
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
329
360
|
`);
|
|
361
|
+
if (code === "INSUFFICIENT_SCOPE") {
|
|
362
|
+
process.stderr.write(
|
|
363
|
+
"\n Your current API key does not have the required scope for this command.\n New keys minted by the wizard already include both scopes. Existing keys\n can be upgraded with:\n\n mushi login --upgrade-scope\n\n Or visit your console \u2192 Projects \u2192 API Keys to mint a new key.\n"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
330
366
|
process.exit(exitCode);
|
|
331
367
|
}
|
|
332
368
|
function requireConfig(opts = {}) {
|
|
333
369
|
const config = loadConfig();
|
|
334
370
|
if (!config.apiKey) {
|
|
335
|
-
|
|
336
|
-
"
|
|
337
|
-
|
|
338
|
-
|
|
371
|
+
printAndExit(new MushiCliError(
|
|
372
|
+
"E_AUTH_MISSING",
|
|
373
|
+
"API key not configured.",
|
|
374
|
+
"Run: mushi login --api-key <key> Or: export MUSHI_API_KEY=<key>"
|
|
375
|
+
));
|
|
339
376
|
}
|
|
340
377
|
if (!config.endpoint) {
|
|
341
378
|
config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
|
|
342
379
|
}
|
|
343
380
|
if (opts.needsProject && !config.projectId) {
|
|
344
|
-
|
|
345
|
-
"
|
|
346
|
-
|
|
347
|
-
|
|
381
|
+
printAndExit(new MushiCliError(
|
|
382
|
+
"E_PROJECT_MISSING",
|
|
383
|
+
"Project ID not configured.",
|
|
384
|
+
`Run: mushi login --project-id <uuid> Or find your project ID at ${consoleUrl(resolveConsoleUrlSync(), "/projects")}`
|
|
385
|
+
));
|
|
348
386
|
}
|
|
349
387
|
return config;
|
|
350
388
|
}
|
|
@@ -424,8 +462,12 @@ async function waitForCliToken(endpoint, session, opts = {}) {
|
|
|
424
462
|
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
425
463
|
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
426
464
|
let consecutiveErrors = 0;
|
|
465
|
+
let firstPoll = true;
|
|
427
466
|
while (now() < deadline) {
|
|
428
|
-
|
|
467
|
+
if (!firstPoll) {
|
|
468
|
+
await sleep(intervalMs);
|
|
469
|
+
}
|
|
470
|
+
firstPoll = false;
|
|
429
471
|
const outcome = await pollDeviceToken(endpoint, session.device_code);
|
|
430
472
|
switch (outcome.status) {
|
|
431
473
|
case "approved":
|
|
@@ -653,6 +695,53 @@ function getFrameworkFromPkg(pkg) {
|
|
|
653
695
|
return void 0;
|
|
654
696
|
}
|
|
655
697
|
|
|
698
|
+
// src/auth-ui.ts
|
|
699
|
+
var LINE = "\u2500".repeat(58);
|
|
700
|
+
function printAuthBanner(userCode, verificationUri) {
|
|
701
|
+
console.log("");
|
|
702
|
+
console.log(` \u250C${LINE}\u2510`);
|
|
703
|
+
console.log(` \u2502 Browser sign-in \u2014 follow these 3 steps \u2502`);
|
|
704
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
705
|
+
console.log(` \u2502 \u2502`);
|
|
706
|
+
console.log(` \u2502 1. A browser tab will open (or visit the URL below) \u2502`);
|
|
707
|
+
console.log(` \u2502 2. Sign in if prompted, then click "Approve" \u2502`);
|
|
708
|
+
console.log(` \u2502 3. Come back here \u2014 setup continues automatically \u2502`);
|
|
709
|
+
console.log(` \u2502 \u2502`);
|
|
710
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
711
|
+
console.log(` \u2502 Verification code (show in browser \u2014 NOT terminal): \u2502`);
|
|
712
|
+
console.log(` \u2502 \u2502`);
|
|
713
|
+
console.log(` \u2502 ${userCode.padEnd(52)}\u2502`);
|
|
714
|
+
console.log(` \u2502 \u2502`);
|
|
715
|
+
console.log(` \u2502 \u26A0 Do NOT paste or type this code in your terminal \u2502`);
|
|
716
|
+
console.log(` \u2514${LINE}\u2518`);
|
|
717
|
+
console.log("");
|
|
718
|
+
console.log(` If the browser didn't open, visit this URL and approve:`);
|
|
719
|
+
console.log(` ${verificationUri}`);
|
|
720
|
+
console.log("");
|
|
721
|
+
console.log(" \u23F3 Waiting for browser approval\u2026 (Ctrl+C to cancel)");
|
|
722
|
+
console.log("");
|
|
723
|
+
}
|
|
724
|
+
function printAuthApproved() {
|
|
725
|
+
console.log("");
|
|
726
|
+
console.log(" \u2713 Approved! Continuing setup in this terminal\u2026");
|
|
727
|
+
console.log("");
|
|
728
|
+
}
|
|
729
|
+
function printAuthFailed(reason, detail) {
|
|
730
|
+
console.log("");
|
|
731
|
+
if (reason === "denied") {
|
|
732
|
+
console.error(" \u2717 Browser sign-in was denied.");
|
|
733
|
+
console.error(" If that was a mistake, run mushi login again.");
|
|
734
|
+
} else if (reason === "timeout") {
|
|
735
|
+
console.error(" \u2717 Browser sign-in timed out (10 minutes elapsed).");
|
|
736
|
+
console.error(" Run mushi login again \u2014 a new code will be generated.");
|
|
737
|
+
} else {
|
|
738
|
+
console.error(` \u2717 Browser sign-in failed: ${detail ?? "unknown error"}`);
|
|
739
|
+
console.error(" Run mushi login again, or use:");
|
|
740
|
+
console.error(" mushi login --api-key <key> --project-id <uuid>");
|
|
741
|
+
}
|
|
742
|
+
console.log("");
|
|
743
|
+
}
|
|
744
|
+
|
|
656
745
|
// src/init.ts
|
|
657
746
|
var ENV_FILES = [".env.local", ".env"];
|
|
658
747
|
var PROJECT_ID_PATTERN = /^(?:proj_[A-Za-z0-9_-]{10,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
@@ -660,7 +749,7 @@ var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
|
|
|
660
749
|
async function runInit(options = {}) {
|
|
661
750
|
const cwd = options.cwd ?? process.cwd();
|
|
662
751
|
ensureInteractiveOrBailOut(options);
|
|
663
|
-
p.intro("
|
|
752
|
+
p.intro("Mushi setup");
|
|
664
753
|
await printFreshnessHint();
|
|
665
754
|
warnIfWorkspaceRoot(cwd);
|
|
666
755
|
const pkg = readPackageJson(cwd);
|
|
@@ -688,13 +777,13 @@ async function runInit(options = {}) {
|
|
|
688
777
|
} else {
|
|
689
778
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
690
779
|
}
|
|
691
|
-
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
780
|
+
await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
|
|
692
781
|
persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
|
|
693
782
|
const enableRewards = await maybeEnableRewards(options);
|
|
694
783
|
printNextSteps(framework, consoleBase, enableRewards);
|
|
695
784
|
await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
|
|
696
785
|
await maybeOfferConnect(credentials, options, consoleBase);
|
|
697
|
-
p.outro("Setup complete.
|
|
786
|
+
p.outro("Setup complete.");
|
|
698
787
|
}
|
|
699
788
|
function ensureInteractiveOrBailOut(options) {
|
|
700
789
|
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -759,7 +848,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
759
848
|
}
|
|
760
849
|
}
|
|
761
850
|
if (options.yes) {
|
|
762
|
-
const creds = await runBrowserSignIn(options, endpoint);
|
|
851
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
763
852
|
if (creds) return creds;
|
|
764
853
|
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
765
854
|
} else {
|
|
@@ -775,7 +864,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
775
864
|
{
|
|
776
865
|
value: "manual",
|
|
777
866
|
label: "Paste a Project ID + API key",
|
|
778
|
-
hint: "Self-hosted or expert setup"
|
|
867
|
+
hint: "Self-hosted or expert setup \u2014 run npx mushi-mushi (not mushi setup)"
|
|
779
868
|
}
|
|
780
869
|
]
|
|
781
870
|
});
|
|
@@ -784,14 +873,14 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
784
873
|
process.exit(0);
|
|
785
874
|
}
|
|
786
875
|
if (method === "browser") {
|
|
787
|
-
const creds = await runBrowserSignIn(options, endpoint);
|
|
876
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
788
877
|
if (creds) return creds;
|
|
789
878
|
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
790
879
|
}
|
|
791
880
|
}
|
|
792
881
|
return collectCredentialsManually(options, consoleBase);
|
|
793
882
|
}
|
|
794
|
-
async function runBrowserSignIn(options, endpoint) {
|
|
883
|
+
async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
795
884
|
const startSpin = p.spinner();
|
|
796
885
|
startSpin.start("Starting secure browser sign-in\u2026");
|
|
797
886
|
let session;
|
|
@@ -803,13 +892,11 @@ async function runBrowserSignIn(options, endpoint) {
|
|
|
803
892
|
return null;
|
|
804
893
|
}
|
|
805
894
|
startSpin.stop("Browser sign-in ready.");
|
|
806
|
-
p.log.step(`Confirmation code: ${session.user_code}`);
|
|
807
|
-
p.log.info(`Opening ${session.verification_uri}`);
|
|
808
895
|
try {
|
|
809
896
|
await openInBrowser(session.verification_uri);
|
|
810
897
|
} catch {
|
|
811
898
|
}
|
|
812
|
-
|
|
899
|
+
printAuthBanner(session.user_code, session.verification_uri);
|
|
813
900
|
const waitSpin = p.spinner();
|
|
814
901
|
waitSpin.start("Waiting for you to approve in the browser\u2026");
|
|
815
902
|
let cliToken;
|
|
@@ -825,9 +912,14 @@ async function runBrowserSignIn(options, endpoint) {
|
|
|
825
912
|
let apiKey;
|
|
826
913
|
if (!projectId) {
|
|
827
914
|
let projects = [];
|
|
915
|
+
const fetchSpin = p.spinner();
|
|
916
|
+
fetchSpin.start("Loading your projects\u2026");
|
|
828
917
|
try {
|
|
829
918
|
projects = await listProjects(endpoint, cliToken);
|
|
830
|
-
|
|
919
|
+
fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
|
|
920
|
+
} catch (err) {
|
|
921
|
+
fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
|
|
922
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
831
923
|
}
|
|
832
924
|
const NEW = "__new__";
|
|
833
925
|
const choice = await p.select({
|
|
@@ -869,7 +961,24 @@ async function runBrowserSignIn(options, endpoint) {
|
|
|
869
961
|
}
|
|
870
962
|
}
|
|
871
963
|
if (projectId && !apiKey) {
|
|
872
|
-
|
|
964
|
+
const keySpin = p.spinner();
|
|
965
|
+
keySpin.start("Minting SDK key\u2026");
|
|
966
|
+
try {
|
|
967
|
+
const minted = await mintProjectKey(endpoint, cliToken, projectId);
|
|
968
|
+
apiKey = minted ?? void 0;
|
|
969
|
+
if (!apiKey) {
|
|
970
|
+
keySpin.stop("Could not mint an API key.");
|
|
971
|
+
p.log.warn(
|
|
972
|
+
`Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
973
|
+
);
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
keySpin.stop("SDK key ready.");
|
|
977
|
+
} catch (err) {
|
|
978
|
+
keySpin.stop("Could not mint an API key.");
|
|
979
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
873
982
|
}
|
|
874
983
|
if (!projectId || !apiKey) return null;
|
|
875
984
|
return { apiKey, projectId };
|
|
@@ -975,13 +1084,37 @@ function runCommand(pm, packages, cwd) {
|
|
|
975
1084
|
});
|
|
976
1085
|
});
|
|
977
1086
|
}
|
|
978
|
-
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
1087
|
+
async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
|
|
979
1088
|
const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
|
|
980
1089
|
const targetPath = join3(cwd, target);
|
|
981
|
-
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
1090
|
+
const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
|
|
982
1091
|
const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
|
|
983
1092
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
984
|
-
|
|
1093
|
+
let shouldOverwrite = overwrite;
|
|
1094
|
+
if (!shouldOverwrite) {
|
|
1095
|
+
const answer = await p.confirm({
|
|
1096
|
+
message: `Existing MUSHI_* vars found in ${target}. Update with new credentials?`,
|
|
1097
|
+
initialValue: true
|
|
1098
|
+
});
|
|
1099
|
+
if (p.isCancel(answer)) {
|
|
1100
|
+
p.log.info(`Kept existing env vars in ${target}.`);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
shouldOverwrite = Boolean(answer);
|
|
1104
|
+
}
|
|
1105
|
+
if (!shouldOverwrite) {
|
|
1106
|
+
p.log.info(`Kept existing env vars in ${target}. Re-run and confirm to overwrite.`);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const MUSHI_LINE_RE = /^(NEXT_PUBLIC_|NUXT_PUBLIC_|VITE_|EXPO_PUBLIC_)?MUSHI_[A-Z_]+=.*/gm;
|
|
1110
|
+
const stripped = existing.replace(MUSHI_LINE_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1111
|
+
const prefix2 = stripped.length > 0 ? "\n" : "";
|
|
1112
|
+
writeFileSync(targetPath, `${stripped}${prefix2}
|
|
1113
|
+
# Mushi Mushi
|
|
1114
|
+
${newVars}
|
|
1115
|
+
`);
|
|
1116
|
+
p.log.success(`Updated MUSHI_* env vars in ${target}`);
|
|
1117
|
+
warnIfMissingFromGitignore(cwd, target);
|
|
985
1118
|
return;
|
|
986
1119
|
}
|
|
987
1120
|
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
@@ -1042,12 +1175,14 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
|
1042
1175
|
p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
|
|
1043
1176
|
p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
|
|
1044
1177
|
}
|
|
1045
|
-
p.
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1178
|
+
p.note(
|
|
1179
|
+
[
|
|
1180
|
+
" [ ] 1. Paste the init snippet above into your app entry file",
|
|
1181
|
+
" [ ] 2. Start your dev server",
|
|
1182
|
+
" [ ] 3. Run: mushi connect --write-env --wire-ide --wait",
|
|
1183
|
+
` [ ] 4. Open the Verify tab to send a test report: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
1184
|
+
].join("\n"),
|
|
1185
|
+
"Next steps:"
|
|
1051
1186
|
);
|
|
1052
1187
|
}
|
|
1053
1188
|
async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
@@ -1059,7 +1194,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
1059
1194
|
if (p.isCancel(answer) || !answer) return;
|
|
1060
1195
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1061
1196
|
try {
|
|
1062
|
-
const { runConnect: runConnect2 } = await import("./connect-
|
|
1197
|
+
const { runConnect: runConnect2 } = await import("./connect-JIEIWTF3.js");
|
|
1063
1198
|
await runConnect2({
|
|
1064
1199
|
apiKey: credentials.apiKey,
|
|
1065
1200
|
projectId: credentials.projectId,
|
|
@@ -1344,11 +1479,15 @@ function registerAccountCommands(program2) {
|
|
|
1344
1479
|
const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
|
|
1345
1480
|
if (matches.length === 0) process.exit(1);
|
|
1346
1481
|
});
|
|
1347
|
-
program2.command("login").description("Authenticate the CLI via browser (zero copy-paste), or pass --api-key to skip").option("--api-key <key>", "API key (mushi_...) \u2014 non-interactive / CI path").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--project-id <id>", "Project UUID \u2014 skip the project picker").option("--no-browser", "Print the verification URL instead of opening the browser").
|
|
1482
|
+
program2.command("login").description("Authenticate the CLI via browser (zero copy-paste), or pass --api-key to skip").option("--api-key <key>", "API key (mushi_...) \u2014 non-interactive / CI path").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--project-id <id>", "Project UUID \u2014 skip the project picker").option("--no-browser", "Print the verification URL instead of opening the browser").option(
|
|
1483
|
+
"--upgrade-scope",
|
|
1484
|
+
"Re-authenticate and mint a new key with report:write + mcp:read + mcp:write scopes. Use this to upgrade an existing ingest-only key so MCP and admin commands (including writes like `mushi billing cap`) work."
|
|
1485
|
+
).addHelpText("after", `
|
|
1348
1486
|
Examples:
|
|
1349
1487
|
mushi login # browser-guided (recommended)
|
|
1350
1488
|
mushi login --api-key mushi_xxx # non-interactive / CI
|
|
1351
|
-
mushi login --api-key mushi_xxx --project-id <uuid
|
|
1489
|
+
mushi login --api-key mushi_xxx --project-id <uuid>
|
|
1490
|
+
mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
|
|
1352
1491
|
const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
|
|
1353
1492
|
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
|
|
1354
1493
|
const consoleBase = await resolveConsoleUrl();
|
|
@@ -1386,8 +1525,16 @@ Examples:
|
|
|
1386
1525
|
return;
|
|
1387
1526
|
}
|
|
1388
1527
|
console.log("");
|
|
1389
|
-
|
|
1390
|
-
|
|
1528
|
+
if (opts.upgradeScope) {
|
|
1529
|
+
console.log(" Mushi login \u2014 scope upgrade");
|
|
1530
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1531
|
+
console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
|
|
1532
|
+
console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
|
|
1533
|
+
console.log("");
|
|
1534
|
+
} else {
|
|
1535
|
+
console.log(" Mushi login");
|
|
1536
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1537
|
+
}
|
|
1391
1538
|
let deviceData;
|
|
1392
1539
|
try {
|
|
1393
1540
|
deviceData = await startDeviceAuth(endpoint);
|
|
@@ -1400,26 +1547,24 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
|
|
|
1400
1547
|
process.exit(1);
|
|
1401
1548
|
}
|
|
1402
1549
|
const verifyUrl = deviceData.verification_uri;
|
|
1403
|
-
console.log("");
|
|
1404
|
-
console.log(` Your confirmation code: ${deviceData.user_code}`);
|
|
1405
|
-
console.log("");
|
|
1406
1550
|
if (opts.browser !== false) {
|
|
1407
|
-
console.log(" Opening the Mushi console in your browser\u2026");
|
|
1408
1551
|
try {
|
|
1409
1552
|
await openInBrowser(verifyUrl);
|
|
1410
1553
|
} catch {
|
|
1411
1554
|
}
|
|
1412
1555
|
}
|
|
1413
|
-
|
|
1414
|
-
console.log("");
|
|
1415
|
-
console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
|
|
1556
|
+
printAuthBanner(deviceData.user_code, verifyUrl);
|
|
1416
1557
|
let cliToken = null;
|
|
1417
1558
|
const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
|
|
1418
1559
|
const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
|
|
1419
1560
|
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
1420
1561
|
let consecutiveErrors = 0;
|
|
1562
|
+
let firstPoll = true;
|
|
1421
1563
|
while (Date.now() < deadline) {
|
|
1422
|
-
|
|
1564
|
+
if (!firstPoll) {
|
|
1565
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1566
|
+
}
|
|
1567
|
+
firstPoll = false;
|
|
1423
1568
|
const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
|
|
1424
1569
|
if (outcome.status === "approved") {
|
|
1425
1570
|
cliToken = outcome.cliToken;
|
|
@@ -1427,38 +1572,30 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
|
|
|
1427
1572
|
}
|
|
1428
1573
|
if (outcome.status === "pending") {
|
|
1429
1574
|
consecutiveErrors = 0;
|
|
1430
|
-
process.stdout.write(".");
|
|
1431
1575
|
continue;
|
|
1432
1576
|
}
|
|
1433
1577
|
if (outcome.status === "error") {
|
|
1434
1578
|
if (outcome.retryable) {
|
|
1435
1579
|
consecutiveErrors += 1;
|
|
1436
|
-
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS)
|
|
1437
|
-
process.stdout.write("\xB7");
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1580
|
+
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
|
|
1440
1581
|
}
|
|
1441
|
-
|
|
1442
|
-
process.stderr.write(`
|
|
1443
|
-
error: ${outcome.message}
|
|
1444
|
-
`);
|
|
1582
|
+
printAuthFailed("error", outcome.message);
|
|
1445
1583
|
process.exit(1);
|
|
1446
1584
|
}
|
|
1447
|
-
console.log("");
|
|
1448
1585
|
if (outcome.status === "denied") {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1586
|
+
printAuthFailed("denied");
|
|
1587
|
+
process.exit(1);
|
|
1588
|
+
}
|
|
1589
|
+
if (outcome.status === "expired") {
|
|
1590
|
+
printAuthFailed("timeout");
|
|
1591
|
+
process.exit(1);
|
|
1452
1592
|
}
|
|
1453
|
-
process.exit(1);
|
|
1454
1593
|
}
|
|
1455
1594
|
if (!cliToken) {
|
|
1456
|
-
|
|
1457
|
-
process.stderr.write("\nerror: Login timed out. Run mushi login again.\n");
|
|
1595
|
+
printAuthFailed("timeout");
|
|
1458
1596
|
process.exit(1);
|
|
1459
1597
|
}
|
|
1460
|
-
|
|
1461
|
-
console.log(" \u2713 Approved!");
|
|
1598
|
+
printAuthApproved();
|
|
1462
1599
|
const projectsList = await listProjects(endpoint, cliToken);
|
|
1463
1600
|
let chosenProjectId = projectId;
|
|
1464
1601
|
let chosenProjectName;
|
|
@@ -1521,12 +1658,17 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
1521
1658
|
console.log("");
|
|
1522
1659
|
if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
|
|
1523
1660
|
if (apiKey) {
|
|
1524
|
-
console.log(` \u2713
|
|
1661
|
+
console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
|
|
1662
|
+
console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
|
|
1525
1663
|
} else {
|
|
1526
1664
|
console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
|
|
1527
1665
|
}
|
|
1528
1666
|
console.log("");
|
|
1529
|
-
|
|
1667
|
+
if (opts.upgradeScope) {
|
|
1668
|
+
console.log(" Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.");
|
|
1669
|
+
} else {
|
|
1670
|
+
console.log(" Run 'mushi init' to set up the SDK in this project.");
|
|
1671
|
+
}
|
|
1530
1672
|
});
|
|
1531
1673
|
program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1532
1674
|
Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
|
|
@@ -1548,24 +1690,19 @@ Useful after 'mushi login' to confirm credentials are correct.`).action(async (o
|
|
|
1548
1690
|
const config = requireConfig();
|
|
1549
1691
|
const t0 = Date.now();
|
|
1550
1692
|
try {
|
|
1551
|
-
const
|
|
1552
|
-
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1553
|
-
const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
|
|
1554
|
-
clearTimeout(timer);
|
|
1555
|
-
const latency = Date.now() - t0;
|
|
1693
|
+
const probe = await probeEndpointHealth(config.endpoint);
|
|
1556
1694
|
if (opts.json) {
|
|
1557
|
-
console.log(JSON.stringify({ ok:
|
|
1695
|
+
console.log(JSON.stringify({ ok: probe.ok, status: probe.status, latency_ms: probe.latencyMs }));
|
|
1558
1696
|
} else {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
if (!res.ok) process.exit(1);
|
|
1697
|
+
console.log(`${probe.ok ? "OK" : "FAIL"} \u2014 ${probe.status} (${probe.latencyMs}ms)`);
|
|
1698
|
+
if (!probe.ok) process.exit(1);
|
|
1562
1699
|
}
|
|
1563
1700
|
} catch (err) {
|
|
1564
1701
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1565
1702
|
if (opts.json) {
|
|
1566
1703
|
console.log(JSON.stringify({ ok: false, error: msg, latency_ms: Date.now() - t0 }));
|
|
1567
1704
|
} else {
|
|
1568
|
-
process.stderr.write(
|
|
1705
|
+
process.stderr.write(`FAIL Unreachable: ${msg}
|
|
1569
1706
|
`);
|
|
1570
1707
|
}
|
|
1571
1708
|
process.exit(1);
|
|
@@ -1630,22 +1767,16 @@ function registerDeployCommands(program2) {
|
|
|
1630
1767
|
const deploy = program2.command("deploy").description("Deployment management");
|
|
1631
1768
|
deploy.command("check").description("Check edge function health and measure latency").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
1632
1769
|
const config = requireConfig();
|
|
1633
|
-
const t0 = Date.now();
|
|
1634
1770
|
try {
|
|
1635
|
-
const
|
|
1636
|
-
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1637
|
-
const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
|
|
1638
|
-
clearTimeout(timer);
|
|
1639
|
-
const latency = Date.now() - t0;
|
|
1640
|
-
const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
|
|
1771
|
+
const probe = await probeEndpointHealth(config.endpoint);
|
|
1641
1772
|
if (opts.json) {
|
|
1642
|
-
console.log(JSON.stringify({ ok:
|
|
1773
|
+
console.log(JSON.stringify({ ok: probe.ok, status: probe.status, latency_ms: probe.latencyMs, ...probe.body }));
|
|
1643
1774
|
} else {
|
|
1644
|
-
console.log(`Health: ${
|
|
1645
|
-
if (body["version"]) console.log(` Version: ${body["version"]}`);
|
|
1646
|
-
if (body["region"]) console.log(` Region: ${body["region"]}`);
|
|
1775
|
+
console.log(`Health: ${probe.status === 200 ? "OK" : "FAIL"} (${probe.status}) \u2014 ${probe.latencyMs}ms`);
|
|
1776
|
+
if (probe.body["version"]) console.log(` Version: ${probe.body["version"]}`);
|
|
1777
|
+
if (probe.body["region"]) console.log(` Region: ${probe.body["region"]}`);
|
|
1647
1778
|
}
|
|
1648
|
-
if (!
|
|
1779
|
+
if (!probe.ok) process.exit(1);
|
|
1649
1780
|
} catch (err) {
|
|
1650
1781
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1651
1782
|
if (opts.json) {
|
|
@@ -2049,11 +2180,11 @@ AI code review context.
|
|
|
2049
2180
|
Typical CI usage:
|
|
2050
2181
|
MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
|
|
2051
2182
|
npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
|
|
2052
|
-
const { writeFile, mkdir } = await import("fs/promises");
|
|
2053
|
-
const
|
|
2183
|
+
const { writeFile: writeFile2, mkdir } = await import("fs/promises");
|
|
2184
|
+
const nodePath2 = await import("path");
|
|
2054
2185
|
const config = requireConfig();
|
|
2055
2186
|
const cwd = opts.cwd ?? process.cwd();
|
|
2056
|
-
const target =
|
|
2187
|
+
const target = nodePath2.join(cwd, ".mushi", "lessons.json");
|
|
2057
2188
|
const result = await apiCall("/v1/sync/lessons?limit=500", config);
|
|
2058
2189
|
if (!result.ok) die(result);
|
|
2059
2190
|
const rows = Array.isArray(result.data) ? result.data : [];
|
|
@@ -2076,8 +2207,8 @@ Typical CI usage:
|
|
|
2076
2207
|
console.log(JSON.stringify(output, null, 2));
|
|
2077
2208
|
return;
|
|
2078
2209
|
}
|
|
2079
|
-
await mkdir(
|
|
2080
|
-
await
|
|
2210
|
+
await mkdir(nodePath2.dirname(target), { recursive: true });
|
|
2211
|
+
await writeFile2(target, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
2081
2212
|
if (opts.json) {
|
|
2082
2213
|
console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
|
|
2083
2214
|
} else {
|
|
@@ -2273,34 +2404,34 @@ Examples:
|
|
|
2273
2404
|
mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
|
|
2274
2405
|
const config = requireConfig({ needsProject: true });
|
|
2275
2406
|
const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
|
|
2276
|
-
const
|
|
2407
|
+
const nodePath2 = await import("path");
|
|
2277
2408
|
const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
|
|
2278
2409
|
const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
2279
2410
|
const MAX_FILE_BYTES = 5e5;
|
|
2280
2411
|
async function* walk(dir) {
|
|
2281
2412
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
2282
2413
|
for (const e of entries) {
|
|
2283
|
-
const full =
|
|
2414
|
+
const full = nodePath2.join(dir, e.name);
|
|
2284
2415
|
if (SKIP.test(full)) continue;
|
|
2285
2416
|
if (e.isDirectory()) yield* walk(full);
|
|
2286
|
-
else if (EXTS.has(
|
|
2417
|
+
else if (EXTS.has(nodePath2.extname(e.name))) yield full;
|
|
2287
2418
|
}
|
|
2288
2419
|
}
|
|
2289
2420
|
let count = 0;
|
|
2290
2421
|
let bytes = 0;
|
|
2291
2422
|
let errors = 0;
|
|
2292
|
-
const root =
|
|
2423
|
+
const root = nodePath2.resolve(path);
|
|
2293
2424
|
for await (const file of walk(root)) {
|
|
2294
|
-
const lang =
|
|
2425
|
+
const lang = nodePath2.extname(file).slice(1);
|
|
2295
2426
|
if (opts.language && opts.language !== lang) continue;
|
|
2296
2427
|
const stats = await stat(file);
|
|
2297
2428
|
if (stats.size > MAX_FILE_BYTES) {
|
|
2298
|
-
if (!opts.json) process.stdout.write(` skip ${
|
|
2429
|
+
if (!opts.json) process.stdout.write(` skip ${nodePath2.relative(root, file)} (>${MAX_FILE_BYTES / 1e3}KB)
|
|
2299
2430
|
`);
|
|
2300
2431
|
continue;
|
|
2301
2432
|
}
|
|
2302
2433
|
const source = await readFile2(file, "utf8");
|
|
2303
|
-
const relative2 =
|
|
2434
|
+
const relative2 = nodePath2.relative(root, file).replaceAll("\\", "/");
|
|
2304
2435
|
count++;
|
|
2305
2436
|
bytes += source.length;
|
|
2306
2437
|
if (opts.dryRun) {
|
|
@@ -2346,6 +2477,37 @@ Examples:
|
|
|
2346
2477
|
});
|
|
2347
2478
|
}
|
|
2348
2479
|
|
|
2480
|
+
// src/project-bootstrap.ts
|
|
2481
|
+
import { writeFile } from "fs/promises";
|
|
2482
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2483
|
+
import nodePath from "path";
|
|
2484
|
+
async function writeProjectBootstrapFiles(opts) {
|
|
2485
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2486
|
+
const envPath = nodePath.join(cwd, ".env.local");
|
|
2487
|
+
const envLines = [
|
|
2488
|
+
"# Mushi MCP \u2014 drop into .env.local (gitignored). The MCP binary picks these up on spawn.",
|
|
2489
|
+
`MUSHI_API_ENDPOINT=${opts.endpoint}`,
|
|
2490
|
+
`MUSHI_PROJECT_ID=${opts.projectId}`,
|
|
2491
|
+
`MUSHI_API_KEY=${opts.apiKey}`,
|
|
2492
|
+
""
|
|
2493
|
+
];
|
|
2494
|
+
const envUpdated = existsSync4(envPath);
|
|
2495
|
+
await writeFile(envPath, envLines.join("\n"), "utf8");
|
|
2496
|
+
const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
|
|
2497
|
+
const serverName = buildMcpServerName({ legacy: true });
|
|
2498
|
+
const serverBlock = buildMcpServerBlock({
|
|
2499
|
+
endpoint: opts.endpoint,
|
|
2500
|
+
projectId: opts.projectId,
|
|
2501
|
+
apiKey: opts.apiKey
|
|
2502
|
+
});
|
|
2503
|
+
const { created: mcpCreated } = await writeMcpServerEntry({
|
|
2504
|
+
configPath: mcpPath,
|
|
2505
|
+
serverName,
|
|
2506
|
+
serverBlock
|
|
2507
|
+
});
|
|
2508
|
+
return { envUpdated, mcpUpdated: !mcpCreated };
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2349
2511
|
// src/commands/project.ts
|
|
2350
2512
|
function registerProjectCommands(program2) {
|
|
2351
2513
|
const project = program2.command("project").description("Project management");
|
|
@@ -2360,9 +2522,6 @@ Typical first-time flow:
|
|
|
2360
2522
|
# Browser opens \u2192 click Approve \u2192 pick or create a project
|
|
2361
2523
|
# CLI writes .env.local and .cursor/mcp.json
|
|
2362
2524
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
2363
|
-
const { writeFile } = await import("fs/promises");
|
|
2364
|
-
const { existsSync: existsSync4 } = await import("fs");
|
|
2365
|
-
const nodePath = await import("path");
|
|
2366
2525
|
const savedConfig = loadConfig();
|
|
2367
2526
|
const endpoint = resolveCloudEndpoint(
|
|
2368
2527
|
opts.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? savedConfig.endpoint
|
|
@@ -2382,33 +2541,24 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
|
|
|
2382
2541
|
process.stderr.write(" Fallback: mushi login --api-key <key> --project-id <uuid>\n");
|
|
2383
2542
|
process.exit(1);
|
|
2384
2543
|
}
|
|
2385
|
-
console.log(` Confirmation code: ${session.user_code}`);
|
|
2386
|
-
console.log("");
|
|
2387
2544
|
if (opts.browser !== false) {
|
|
2388
|
-
console.log(" Opening the Mushi console in your browser\u2026");
|
|
2389
2545
|
try {
|
|
2390
2546
|
await openInBrowser(session.verification_uri);
|
|
2391
2547
|
} catch {
|
|
2392
2548
|
}
|
|
2393
2549
|
}
|
|
2394
|
-
|
|
2395
|
-
console.log("");
|
|
2396
|
-
console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
|
|
2550
|
+
printAuthBanner(session.user_code, session.verification_uri);
|
|
2397
2551
|
let cliToken;
|
|
2398
2552
|
try {
|
|
2399
|
-
cliToken = await waitForCliToken(endpoint, session
|
|
2400
|
-
onPending: () => process.stdout.write("."),
|
|
2401
|
-
onTransientError: () => process.stdout.write("\xB7")
|
|
2402
|
-
});
|
|
2553
|
+
cliToken = await waitForCliToken(endpoint, session);
|
|
2403
2554
|
} catch (err) {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2555
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2556
|
+
if (msg.includes("denied")) printAuthFailed("denied");
|
|
2557
|
+
else if (msg.includes("timed out") || msg.includes("expired")) printAuthFailed("timeout");
|
|
2558
|
+
else printAuthFailed("error", msg);
|
|
2408
2559
|
process.exit(1);
|
|
2409
2560
|
}
|
|
2410
|
-
|
|
2411
|
-
console.log(" \u2713 Approved!");
|
|
2561
|
+
printAuthApproved();
|
|
2412
2562
|
const projectsList = await listProjects(endpoint, cliToken);
|
|
2413
2563
|
const { createInterface } = await import("readline");
|
|
2414
2564
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -2468,24 +2618,14 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
2468
2618
|
config.projectId = projectId;
|
|
2469
2619
|
config.consoleUrl = consoleBase;
|
|
2470
2620
|
saveConfig(config);
|
|
2471
|
-
const
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
`MUSHI_PROJECT_ID=${projectId}`,
|
|
2477
|
-
`MUSHI_API_KEY=${apiKey}`,
|
|
2478
|
-
""
|
|
2479
|
-
];
|
|
2480
|
-
const envExisting = existsSync4(envPath);
|
|
2481
|
-
await writeFile(envPath, envLines.join("\n"), "utf8");
|
|
2621
|
+
const { envUpdated, mcpUpdated } = await writeProjectBootstrapFiles({
|
|
2622
|
+
endpoint,
|
|
2623
|
+
projectId,
|
|
2624
|
+
apiKey
|
|
2625
|
+
});
|
|
2482
2626
|
console.log(`
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
const serverName = buildMcpServerName({ legacy: true });
|
|
2486
|
-
const serverBlock = buildMcpServerBlock({ endpoint, projectId, apiKey });
|
|
2487
|
-
const { created: mcpCreated } = await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
|
|
2488
|
-
console.log(` \u2713 ${mcpCreated ? "Created" : "Updated"} .cursor/mcp.json`);
|
|
2627
|
+
OK ${envUpdated ? "Updated" : "Created"} .env.local`);
|
|
2628
|
+
console.log(` OK ${mcpUpdated ? "Updated" : "Created"} .cursor/mcp.json`);
|
|
2489
2629
|
console.log("");
|
|
2490
2630
|
console.log(' Done! Restart Cursor and ask: "list mushi tools"');
|
|
2491
2631
|
console.log(" Run `mushi whoami` to verify the connection.");
|
|
@@ -2495,7 +2635,7 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
2495
2635
|
|
|
2496
2636
|
// src/commands/setup.ts
|
|
2497
2637
|
function registerSetupCommands(program2) {
|
|
2498
|
-
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").addHelpText("after", `
|
|
2638
|
+
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").addHelpText("after", `
|
|
2499
2639
|
Examples:
|
|
2500
2640
|
mushi setup # wire Cursor (default)
|
|
2501
2641
|
mushi setup --all-projects # one server entry per accessible project
|
|
@@ -2509,9 +2649,9 @@ Supported IDEs:
|
|
|
2509
2649
|
zed \u2014 writes ~/.config/zed/settings.json mcpServers block
|
|
2510
2650
|
|
|
2511
2651
|
The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
|
|
2512
|
-
const { writeFile, mkdir, readFile: readFile2 } = await import("fs/promises");
|
|
2513
|
-
const { existsSync:
|
|
2514
|
-
const
|
|
2652
|
+
const { writeFile: writeFile2, mkdir, readFile: readFile2 } = await import("fs/promises");
|
|
2653
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
2654
|
+
const nodePath2 = await import("path");
|
|
2515
2655
|
const os = await import("os");
|
|
2516
2656
|
const config = requireConfig({ needsProject: true });
|
|
2517
2657
|
const redactKeyForDisplay = (text2) => {
|
|
@@ -2554,7 +2694,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2554
2694
|
cursor: { dir: ".cursor", file: "mcp.json", format: "mcp-json" },
|
|
2555
2695
|
claude: { dir: ".claude", file: "mcp.json", format: "mcp-json" },
|
|
2556
2696
|
continue: { dir: ".continue", file: "mcp.json", format: "mcp-json" },
|
|
2557
|
-
zed: { dir:
|
|
2697
|
+
zed: { dir: nodePath2.join(os.homedir(), ".config", "zed"), file: "settings.json", format: "zed" }
|
|
2558
2698
|
};
|
|
2559
2699
|
const ideEntry = IDE_CONFIG[opts.ide];
|
|
2560
2700
|
if (!ideEntry) {
|
|
@@ -2590,8 +2730,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2590
2730
|
MUSHI_API_KEY: config.apiKey
|
|
2591
2731
|
}
|
|
2592
2732
|
};
|
|
2593
|
-
const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir :
|
|
2594
|
-
const configPath =
|
|
2733
|
+
const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir : nodePath2.join(cwd, ideEntry.dir);
|
|
2734
|
+
const configPath = nodePath2.join(configDir, ideEntry.file);
|
|
2595
2735
|
if (ideEntry.format === "mcp-json") {
|
|
2596
2736
|
if (allProjectsList && allProjectsList.length > 0) {
|
|
2597
2737
|
for (const p3 of allProjectsList) {
|
|
@@ -2622,7 +2762,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2622
2762
|
}
|
|
2623
2763
|
} else if (ideEntry.format === "zed") {
|
|
2624
2764
|
let settings = {};
|
|
2625
|
-
if (
|
|
2765
|
+
if (existsSync5(configPath)) {
|
|
2626
2766
|
try {
|
|
2627
2767
|
const raw = await readFile2(configPath, "utf8");
|
|
2628
2768
|
settings = JSON.parse(raw);
|
|
@@ -2649,7 +2789,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2649
2789
|
console.log(redactKeyForDisplay(output));
|
|
2650
2790
|
} else {
|
|
2651
2791
|
await mkdir(configDir, { recursive: true });
|
|
2652
|
-
await
|
|
2792
|
+
await writeFile2(configPath, output, "utf8");
|
|
2653
2793
|
console.log(`\u2713 Written ${configPath}`);
|
|
2654
2794
|
}
|
|
2655
2795
|
}
|
|
@@ -2664,7 +2804,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2664
2804
|
"## Before writing a fix",
|
|
2665
2805
|
"",
|
|
2666
2806
|
"1. Call `get_fix_context` (MCP) for the report \u2014 get root cause + blast radius first.",
|
|
2667
|
-
"2. Call `
|
|
2807
|
+
"2. Call `list_lessons` (MCP) or read .mushi/lessons.json \u2014 apply every matching rule.",
|
|
2668
2808
|
"3. Prefer the smallest change that makes the test pass. Don't refactor unrelated code.",
|
|
2669
2809
|
"",
|
|
2670
2810
|
"## After writing a fix",
|
|
@@ -2679,178 +2819,642 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2679
2819
|
""
|
|
2680
2820
|
].join("\n");
|
|
2681
2821
|
if (opts.ide === "cursor") {
|
|
2682
|
-
const rulesPath =
|
|
2822
|
+
const rulesPath = nodePath2.join(cwd, ".cursorrules");
|
|
2683
2823
|
if (opts.dryRun) {
|
|
2684
2824
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2685
2825
|
} else {
|
|
2686
|
-
await
|
|
2826
|
+
await writeFile2(rulesPath, rulesContent, "utf8");
|
|
2687
2827
|
console.log(`\u2713 Written .cursorrules`);
|
|
2688
2828
|
}
|
|
2689
2829
|
} else if (opts.ide === "claude") {
|
|
2690
|
-
const rulesDir =
|
|
2691
|
-
const rulesPath =
|
|
2830
|
+
const rulesDir = nodePath2.join(cwd, ".claude", "rules");
|
|
2831
|
+
const rulesPath = nodePath2.join(rulesDir, "mushi.md");
|
|
2692
2832
|
if (opts.dryRun) {
|
|
2693
2833
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2694
2834
|
} else {
|
|
2695
2835
|
await mkdir(rulesDir, { recursive: true });
|
|
2696
|
-
await
|
|
2836
|
+
await writeFile2(rulesPath, rulesContent, "utf8");
|
|
2697
2837
|
console.log(`\u2713 Written .claude/rules/mushi.md`);
|
|
2698
2838
|
}
|
|
2699
2839
|
}
|
|
2700
2840
|
}
|
|
2701
2841
|
if (!opts.dryRun) {
|
|
2842
|
+
const shouldVerify = opts.verify !== false;
|
|
2843
|
+
if (shouldVerify && config.endpoint && config.apiKey && config.projectId) {
|
|
2844
|
+
try {
|
|
2845
|
+
const probeRes = await fetch(
|
|
2846
|
+
`${config.endpoint.replace(/\/$/, "")}/v1/admin/mcp/account-overview`,
|
|
2847
|
+
{
|
|
2848
|
+
headers: {
|
|
2849
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
2850
|
+
"X-Mushi-Project": config.projectId
|
|
2851
|
+
},
|
|
2852
|
+
signal: AbortSignal.timeout(6e3)
|
|
2853
|
+
}
|
|
2854
|
+
);
|
|
2855
|
+
if (probeRes.ok) {
|
|
2856
|
+
console.log("\u2713 MCP key valid \u2014 restart Cursor (or your IDE) to activate");
|
|
2857
|
+
void fetch(
|
|
2858
|
+
`${config.endpoint.replace(/\/$/, "")}/v1/cli/funnel`,
|
|
2859
|
+
{
|
|
2860
|
+
method: "POST",
|
|
2861
|
+
headers: {
|
|
2862
|
+
"Content-Type": "application/json",
|
|
2863
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
2864
|
+
"X-Mushi-Project": config.projectId
|
|
2865
|
+
},
|
|
2866
|
+
body: JSON.stringify({ event: "mcp_setup_done", source: "cli" }),
|
|
2867
|
+
signal: AbortSignal.timeout(4e3)
|
|
2868
|
+
}
|
|
2869
|
+
).catch(() => {
|
|
2870
|
+
});
|
|
2871
|
+
} else if (probeRes.status === 403) {
|
|
2872
|
+
let errCode;
|
|
2873
|
+
try {
|
|
2874
|
+
const b = await probeRes.json();
|
|
2875
|
+
errCode = b?.error?.code;
|
|
2876
|
+
} catch {
|
|
2877
|
+
}
|
|
2878
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
2879
|
+
console.log("\n\u26A0 Your key has report:write scope only \u2014 MCP admin tools will not work.");
|
|
2880
|
+
console.log(" New keys minted by the wizard include both scopes automatically.");
|
|
2881
|
+
console.log(" To upgrade an existing key, run:\n");
|
|
2882
|
+
console.log(" mushi login --upgrade-scope\n");
|
|
2883
|
+
} else {
|
|
2884
|
+
console.log(`\u26A0 Key probe returned HTTP ${probeRes.status} \u2014 check your credentials.`);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
} catch {
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2702
2890
|
console.log("");
|
|
2703
2891
|
console.log(`Done! Restart ${opts.ide === "cursor" ? "Cursor" : opts.ide === "claude" ? "Claude Code" : opts.ide} and ask: "list mushi tools"`);
|
|
2704
2892
|
if (!opts.withRules) {
|
|
2705
2893
|
console.log(`Tip: run with --with-rules to also write the lesson-library coding hook.`);
|
|
2706
2894
|
}
|
|
2707
|
-
const configRelPath = ideEntry.dir.startsWith("/") ? configPath :
|
|
2895
|
+
const configRelPath = ideEntry.dir.startsWith("/") ? configPath : nodePath2.relative(cwd, configPath);
|
|
2708
2896
|
console.log(`
|
|
2709
2897
|
Note: ${configRelPath} contains your Mushi API key \u2014 add it to .gitignore if this is a shared repo.`);
|
|
2710
2898
|
}
|
|
2711
2899
|
});
|
|
2712
2900
|
}
|
|
2713
2901
|
|
|
2714
|
-
// src/
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
dismissCooldownHours: 24,
|
|
2736
|
-
suppressAfterDismissals: 3,
|
|
2737
|
-
pageDwellMinutes: 5,
|
|
2738
|
-
firstSessionSeconds: 45,
|
|
2739
|
-
rageClick: true,
|
|
2740
|
-
longTask: true,
|
|
2741
|
-
apiCascade: true,
|
|
2742
|
-
errorBoundary: true,
|
|
2743
|
-
betaMode: true,
|
|
2744
|
-
featureRequestCard: true
|
|
2745
|
-
},
|
|
2746
|
-
// GA / production: only the technical signals (error boundary, rage
|
|
2747
|
-
// click). No page-dwell, no welcome — those are explicitly beta-only
|
|
2748
|
-
// patterns. Feature-request card still on because it's user-initiated.
|
|
2749
|
-
ga: {
|
|
2750
|
-
maxProactivePerSession: 1,
|
|
2751
|
-
dismissCooldownHours: 168,
|
|
2752
|
-
suppressAfterDismissals: 2,
|
|
2753
|
-
pageDwellMinutes: 0,
|
|
2754
|
-
firstSessionSeconds: 0,
|
|
2755
|
-
rageClick: true,
|
|
2756
|
-
longTask: false,
|
|
2757
|
-
apiCascade: true,
|
|
2758
|
-
errorBoundary: true,
|
|
2759
|
-
betaMode: false,
|
|
2760
|
-
featureRequestCard: true
|
|
2761
|
-
}
|
|
2762
|
-
};
|
|
2763
|
-
function renderNudgeSnippet(opts) {
|
|
2764
|
-
const p3 = { ...PRESETS[opts.phase], ...opts.overrides };
|
|
2765
|
-
const dwellMs = p3.pageDwellMinutes > 0 ? `${p3.pageDwellMinutes} * 60 * 1000` : null;
|
|
2766
|
-
const firstMs = p3.firstSessionSeconds > 0 ? `${p3.firstSessionSeconds} * 1000` : null;
|
|
2767
|
-
const proactiveLines = [];
|
|
2768
|
-
if (p3.rageClick) proactiveLines.push(` rageClick: true,`);
|
|
2769
|
-
if (p3.longTask) proactiveLines.push(` longTask: true,`);
|
|
2770
|
-
if (p3.apiCascade) proactiveLines.push(` apiCascade: true,`);
|
|
2771
|
-
if (p3.errorBoundary) proactiveLines.push(` errorBoundary: true,`);
|
|
2772
|
-
if (dwellMs) proactiveLines.push(` pageDwell: { thresholdMs: ${dwellMs} }, // ${p3.pageDwellMinutes}min on the same route`);
|
|
2773
|
-
if (firstMs) proactiveLines.push(` firstSession: { delayMs: ${firstMs} }, // welcome new users after ${p3.firstSessionSeconds}s`);
|
|
2774
|
-
proactiveLines.push(` cooldown: {`);
|
|
2775
|
-
proactiveLines.push(` maxProactivePerSession: ${p3.maxProactivePerSession},`);
|
|
2776
|
-
proactiveLines.push(` dismissCooldownHours: ${p3.dismissCooldownHours},`);
|
|
2777
|
-
proactiveLines.push(` suppressAfterDismissals: ${p3.suppressAfterDismissals},`);
|
|
2778
|
-
proactiveLines.push(` },`);
|
|
2779
|
-
const widgetLines = [];
|
|
2780
|
-
if (p3.featureRequestCard) widgetLines.push(` featureRequestCard: true,`);
|
|
2781
|
-
if (p3.betaMode) {
|
|
2782
|
-
widgetLines.push(` betaMode: {`);
|
|
2783
|
-
widgetLines.push(` enabled: true,`);
|
|
2784
|
-
widgetLines.push(` label: 'BETA',`);
|
|
2785
|
-
widgetLines.push(` // changelogItems: [{ version: 'v0.42', date: '${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}', items: ['\u2026'] }],`);
|
|
2786
|
-
widgetLines.push(` },`);
|
|
2787
|
-
}
|
|
2788
|
-
return [
|
|
2789
|
-
`// Generated by \`mushi nudge --phase ${opts.phase}\``,
|
|
2790
|
-
`// Phase: ${opts.phase} \u2014 tune via --max, --cooldown, --dwell, --welcome.`,
|
|
2791
|
-
`Mushi.init({`,
|
|
2792
|
-
` projectId: process.env.MUSHI_PROJECT_ID!,`,
|
|
2793
|
-
` apiKey: process.env.MUSHI_API_KEY!,`,
|
|
2794
|
-
` proactive: {`,
|
|
2795
|
-
...proactiveLines,
|
|
2796
|
-
` },`,
|
|
2797
|
-
...widgetLines.length ? [` widget: {`, ...widgetLines, ` },`] : [],
|
|
2798
|
-
`})`,
|
|
2799
|
-
``
|
|
2800
|
-
].join("\n");
|
|
2801
|
-
}
|
|
2802
|
-
function renderNudgeExplainer(phase) {
|
|
2803
|
-
const p3 = PRESETS[phase];
|
|
2804
|
-
const lines = [];
|
|
2805
|
-
lines.push(``);
|
|
2806
|
-
lines.push(`Nudge preset for "${phase}" phase:`);
|
|
2807
|
-
lines.push(` - max ${p3.maxProactivePerSession} proactive prompts per session`);
|
|
2808
|
-
lines.push(` - ${p3.dismissCooldownHours}h cooldown after a dismissal`);
|
|
2809
|
-
lines.push(` - suppress permanently after ${p3.suppressAfterDismissals} consecutive dismissals`);
|
|
2810
|
-
if (p3.pageDwellMinutes > 0) lines.push(` - fire page-dwell trigger after ${p3.pageDwellMinutes} continuous minutes on a route`);
|
|
2811
|
-
if (p3.firstSessionSeconds > 0) lines.push(` - welcome new users with a button pulse ${p3.firstSessionSeconds}s after init`);
|
|
2812
|
-
lines.push(` - signals enabled: ${[
|
|
2813
|
-
p3.rageClick && "rage-click",
|
|
2814
|
-
p3.longTask && "long-task",
|
|
2815
|
-
p3.apiCascade && "api-cascade",
|
|
2816
|
-
p3.errorBoundary && "error-boundary"
|
|
2817
|
-
].filter(Boolean).join(", ")}`);
|
|
2818
|
-
lines.push(` - feature-request card: ${p3.featureRequestCard ? "shown" : "hidden"}`);
|
|
2819
|
-
lines.push(` - beta-mode UI: ${p3.betaMode ? "on" : "off"}`);
|
|
2820
|
-
lines.push(``);
|
|
2821
|
-
return lines.join("\n");
|
|
2822
|
-
}
|
|
2902
|
+
// src/commands/fix.ts
|
|
2903
|
+
function registerFixCommands(program2) {
|
|
2904
|
+
const fixCmd = program2.command("fix").description("Dispatch an agentic fix for a report");
|
|
2905
|
+
fixCmd.argument("<reportId>", "Report UUID to fix").option(
|
|
2906
|
+
"--agent <name>",
|
|
2907
|
+
"Agent adapter: claude_code (default), cursor_cloud, codex, mcp",
|
|
2908
|
+
"claude_code"
|
|
2909
|
+
).option(
|
|
2910
|
+
"--model <slug>",
|
|
2911
|
+
"Model override for cursor_cloud (e.g. composer-latest)"
|
|
2912
|
+
).option(
|
|
2913
|
+
"--no-auto-pr",
|
|
2914
|
+
"For cursor_cloud: skip automatic PR creation (branch only)"
|
|
2915
|
+
).option(
|
|
2916
|
+
"--wait",
|
|
2917
|
+
"Poll until terminal state and exit non-zero on error/cancelled (CI-friendly)"
|
|
2918
|
+
).option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--project-id <id>", "Project ID (overrides MUSHI_PROJECT_ID)").addHelpText("after", `
|
|
2919
|
+
Examples:
|
|
2920
|
+
mushi fix abc123 --agent cursor_cloud --wait
|
|
2921
|
+
mushi fix abc123 --agent cursor_cloud --model composer-latest --no-auto-pr
|
|
2922
|
+
mushi fix abc123 --agent claude_code
|
|
2823
2923
|
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2924
|
+
# CI: fail the pipeline if the fix errors
|
|
2925
|
+
mushi fix $REPORT_ID --agent cursor_cloud --wait && echo "Fix PR opened"`).action(async (reportId, opts) => {
|
|
2926
|
+
const cfg = loadConfig();
|
|
2927
|
+
if (opts.endpoint) cfg.endpoint = opts.endpoint;
|
|
2928
|
+
if (opts.apiKey) cfg.apiKey = opts.apiKey;
|
|
2929
|
+
if (opts.projectId) cfg.projectId = opts.projectId;
|
|
2930
|
+
const isTTY = process.stdout.isTTY;
|
|
2931
|
+
const emitEvent = (type, data) => {
|
|
2932
|
+
if (isTTY) {
|
|
2933
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2934
|
+
console.log(`[${ts}] ${type}`, JSON.stringify(data));
|
|
2935
|
+
} else {
|
|
2936
|
+
process.stdout.write(JSON.stringify({ type, ...data }) + "\n");
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2939
|
+
emitEvent("dispatch.start", { reportId, agent: opts.agent, model: opts.model ?? null });
|
|
2940
|
+
const body = {
|
|
2941
|
+
reportId,
|
|
2942
|
+
projectId: cfg.projectId,
|
|
2943
|
+
agent: opts.agent
|
|
2944
|
+
};
|
|
2945
|
+
if (opts.agent === "cursor_cloud") {
|
|
2946
|
+
if (opts.model) body.cursorModel = opts.model;
|
|
2947
|
+
if (!opts.autoPr) body.cursorAutoCreatePR = false;
|
|
2948
|
+
}
|
|
2949
|
+
const result = await apiCall("/v1/admin/fixes/dispatch", cfg, {
|
|
2950
|
+
method: "POST",
|
|
2951
|
+
headers: { "Content-Type": "application/json" },
|
|
2952
|
+
body: JSON.stringify(body)
|
|
2953
|
+
});
|
|
2954
|
+
if (!result.ok) {
|
|
2955
|
+
console.error("Error dispatching fix:", result.error.message);
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
const { fixId, status, agentId, runId, prUrl } = result.data;
|
|
2959
|
+
emitEvent("dispatch.ok", { fixId, status, agentId, runId, prUrl });
|
|
2960
|
+
if (!opts.wait) {
|
|
2961
|
+
process.exit(0);
|
|
2962
|
+
}
|
|
2963
|
+
if (!fixId) {
|
|
2964
|
+
console.error("No fixId returned \u2014 cannot poll.");
|
|
2965
|
+
process.exit(1);
|
|
2966
|
+
}
|
|
2967
|
+
const POLL_MS = 5e3;
|
|
2968
|
+
const MAX_POLLS = 120;
|
|
2969
|
+
const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "error", "cancelled", "skipped", "skipped_unsupported_agent", "skipped_no_sandbox"]);
|
|
2970
|
+
for (let i = 0; i < MAX_POLLS; i++) {
|
|
2971
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
2972
|
+
const pollResult = await apiCall(
|
|
2973
|
+
`/v1/admin/fixes/${fixId}`,
|
|
2974
|
+
cfg
|
|
2975
|
+
);
|
|
2976
|
+
if (!pollResult.ok) {
|
|
2977
|
+
emitEvent("poll.error", { error: pollResult.error.message });
|
|
2978
|
+
continue;
|
|
2979
|
+
}
|
|
2980
|
+
const s = pollResult.data.status;
|
|
2981
|
+
emitEvent("fix.status", { status: s, pr_url: pollResult.data.pr_url, cursor_agent_id: pollResult.data.cursor_agent_id });
|
|
2982
|
+
if (s && TERMINAL.has(s)) {
|
|
2983
|
+
const success = s === "completed";
|
|
2984
|
+
if (!success) {
|
|
2985
|
+
console.error(`Fix ended with status: ${s}${pollResult.data.error ? ` \u2014 ${pollResult.data.error}` : ""}`);
|
|
2986
|
+
process.exit(1);
|
|
2987
|
+
}
|
|
2988
|
+
process.exit(0);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
console.error("Polling timed out after 10 minutes. The fix may still be running.");
|
|
2992
|
+
process.exit(1);
|
|
2993
|
+
});
|
|
2994
|
+
const fixes = program2.command("fixes").description("Fix dispatch management");
|
|
2995
|
+
fixes.command("tail").description(
|
|
2996
|
+
"Stream SSE dispatch events for a report in real time. Useful for headless debugging without opening the browser."
|
|
2997
|
+
).requiredOption("--report-id <id>", "Report ID to follow").action(async (opts) => {
|
|
2998
|
+
const config = loadConfig();
|
|
2999
|
+
if (!config.apiKey) {
|
|
3000
|
+
console.error("Run `mushi login` first");
|
|
3001
|
+
process.exit(1);
|
|
3002
|
+
}
|
|
3003
|
+
if (!config.endpoint) {
|
|
3004
|
+
console.error("No endpoint configured. Run `mushi init`");
|
|
3005
|
+
process.exit(1);
|
|
3006
|
+
}
|
|
3007
|
+
const url = `${config.endpoint}/v1/admin/reports/${opts.reportId}/dispatch/stream`;
|
|
3008
|
+
console.log(`Tailing dispatch stream for report ${opts.reportId}\u2026`);
|
|
3009
|
+
console.log(`(Ctrl-C to stop)
|
|
3010
|
+
`);
|
|
3011
|
+
const res = await fetch(url, {
|
|
3012
|
+
headers: {
|
|
3013
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
3014
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
3015
|
+
"X-Mushi-Project": config.projectId ?? "",
|
|
3016
|
+
"Accept": "text/event-stream"
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
if (!res.ok || !res.body) {
|
|
3020
|
+
console.error(`Failed to connect: HTTP ${res.status}`);
|
|
3021
|
+
const text2 = await res.text().catch(() => "");
|
|
3022
|
+
if (text2) console.error(text2.slice(0, 300));
|
|
3023
|
+
process.exit(1);
|
|
3024
|
+
}
|
|
3025
|
+
const decoder = new TextDecoder();
|
|
3026
|
+
const reader = res.body.getReader();
|
|
3027
|
+
let done = false;
|
|
3028
|
+
process.on("SIGINT", () => {
|
|
3029
|
+
done = true;
|
|
3030
|
+
void reader.cancel();
|
|
3031
|
+
console.log("\nDisconnected.");
|
|
3032
|
+
process.exit(0);
|
|
3033
|
+
});
|
|
3034
|
+
while (!done) {
|
|
3035
|
+
const { value, done: streamDone } = await reader.read();
|
|
3036
|
+
if (streamDone) break;
|
|
3037
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
3038
|
+
for (const line of chunk.split("\n")) {
|
|
3039
|
+
if (line.startsWith("data: ")) {
|
|
3040
|
+
const raw = line.slice(6).trim();
|
|
3041
|
+
if (raw === "[DONE]") {
|
|
3042
|
+
console.log("\n[stream ended]");
|
|
3043
|
+
process.exit(0);
|
|
3044
|
+
}
|
|
3045
|
+
try {
|
|
3046
|
+
const event = JSON.parse(raw);
|
|
3047
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3048
|
+
const type = event.type ?? event.event ?? "event";
|
|
3049
|
+
const status = event.status ?? event.data ?? "";
|
|
3050
|
+
console.log(`${ts} ${type.padEnd(24)} ${status}`);
|
|
3051
|
+
} catch {
|
|
3052
|
+
console.log(line);
|
|
3053
|
+
}
|
|
3054
|
+
} else if (line.startsWith("event: ")) {
|
|
3055
|
+
} else if (line && !line.startsWith(":")) {
|
|
3056
|
+
console.log(line);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
});
|
|
3061
|
+
fixes.command("merge <fixId>").description(
|
|
3062
|
+
"Squash-merge the fix PR on GitHub and mark the report Fixed (same as console merge)"
|
|
3063
|
+
).option(
|
|
3064
|
+
"--method <method>",
|
|
3065
|
+
"GitHub merge method: squash (default), merge, or rebase",
|
|
3066
|
+
"squash"
|
|
3067
|
+
).option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
|
|
3068
|
+
const config = loadConfig();
|
|
3069
|
+
if (!config.apiKey) {
|
|
3070
|
+
console.error("Run `mushi login` first");
|
|
3071
|
+
process.exit(1);
|
|
3072
|
+
}
|
|
3073
|
+
if (!config.endpoint) {
|
|
3074
|
+
console.error("No endpoint configured. Run `mushi init`");
|
|
3075
|
+
process.exit(1);
|
|
3076
|
+
}
|
|
3077
|
+
const method = opts.method;
|
|
3078
|
+
if (!["squash", "merge", "rebase"].includes(method)) {
|
|
3079
|
+
console.error("--method must be squash, merge, or rebase");
|
|
3080
|
+
process.exit(1);
|
|
3081
|
+
}
|
|
3082
|
+
const result = await apiCall(`/v1/admin/fixes/${fixId}/merge`, config, {
|
|
3083
|
+
method: "POST",
|
|
3084
|
+
body: JSON.stringify({ mergeMethod: method })
|
|
3085
|
+
});
|
|
3086
|
+
if (!result.ok) {
|
|
3087
|
+
if (opts.json) {
|
|
3088
|
+
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
3089
|
+
} else {
|
|
3090
|
+
console.error("Merge failed:", result.error.message);
|
|
3091
|
+
}
|
|
3092
|
+
process.exit(1);
|
|
3093
|
+
}
|
|
3094
|
+
const data = result.data ?? {};
|
|
3095
|
+
if (opts.json) {
|
|
3096
|
+
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (data.alreadyMerged) {
|
|
3100
|
+
console.log(`PR was already merged. Report status: ${data.reportStatus ?? "unknown"}`);
|
|
3101
|
+
} else {
|
|
3102
|
+
console.log(`Merged successfully. Report status: ${data.reportStatus ?? "fixed"}`);
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
fixes.command("refresh-ci <fixId>").description("Pull latest GitHub Actions check-run status for a fix attempt (on-demand ci-sync)").option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
|
|
3106
|
+
const config = loadConfig();
|
|
3107
|
+
if (!config.apiKey) {
|
|
3108
|
+
console.error("Run `mushi login` first");
|
|
3109
|
+
process.exit(1);
|
|
3110
|
+
}
|
|
3111
|
+
if (!config.endpoint) {
|
|
3112
|
+
console.error("No endpoint configured. Run `mushi init`");
|
|
3113
|
+
process.exit(1);
|
|
3114
|
+
}
|
|
3115
|
+
const result = await apiCall(`/v1/admin/fixes/${fixId}/refresh-ci`, config, { method: "POST" });
|
|
3116
|
+
if (!result.ok) {
|
|
3117
|
+
if (opts.json) {
|
|
3118
|
+
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
3119
|
+
} else {
|
|
3120
|
+
console.error("CI refresh failed:", result.error.message);
|
|
3121
|
+
}
|
|
3122
|
+
process.exit(1);
|
|
3123
|
+
}
|
|
3124
|
+
const data = result.data ?? {};
|
|
3125
|
+
if (opts.json) {
|
|
3126
|
+
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
const status = data.check_run_status ?? "unknown";
|
|
3130
|
+
const conclusion = data.check_run_conclusion ?? "\u2014";
|
|
3131
|
+
const updated = data.check_run_updated_at ? new Date(data.check_run_updated_at).toISOString() : "\u2014";
|
|
3132
|
+
console.log(`CI status: ${status} \xB7 conclusion: ${conclusion} \xB7 updated: ${updated}`);
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
// src/nudge.ts
|
|
3137
|
+
var PRESETS = {
|
|
3138
|
+
// Alpha: noisy is OK because there are very few users and you need
|
|
3139
|
+
// signal yesterday. Show all triggers, short cooldowns.
|
|
3140
|
+
alpha: {
|
|
3141
|
+
maxProactivePerSession: 3,
|
|
3142
|
+
dismissCooldownHours: 6,
|
|
3143
|
+
suppressAfterDismissals: 5,
|
|
3144
|
+
pageDwellMinutes: 3,
|
|
3145
|
+
firstSessionSeconds: 30,
|
|
3146
|
+
rageClick: true,
|
|
3147
|
+
longTask: true,
|
|
3148
|
+
apiCascade: true,
|
|
3149
|
+
errorBoundary: true,
|
|
3150
|
+
betaMode: true,
|
|
3151
|
+
featureRequestCard: true
|
|
3152
|
+
},
|
|
3153
|
+
// Beta: most apps spend the longest here. Conservative cadence so
|
|
3154
|
+
// we don't poison the well, but every trigger still on.
|
|
3155
|
+
beta: {
|
|
3156
|
+
maxProactivePerSession: 2,
|
|
3157
|
+
dismissCooldownHours: 24,
|
|
3158
|
+
suppressAfterDismissals: 3,
|
|
3159
|
+
pageDwellMinutes: 5,
|
|
3160
|
+
firstSessionSeconds: 45,
|
|
3161
|
+
rageClick: true,
|
|
3162
|
+
longTask: true,
|
|
3163
|
+
apiCascade: true,
|
|
3164
|
+
errorBoundary: true,
|
|
3165
|
+
betaMode: true,
|
|
3166
|
+
featureRequestCard: true
|
|
3167
|
+
},
|
|
3168
|
+
// GA / production: only the technical signals (error boundary, rage
|
|
3169
|
+
// click). No page-dwell, no welcome — those are explicitly beta-only
|
|
3170
|
+
// patterns. Feature-request card still on because it's user-initiated.
|
|
3171
|
+
ga: {
|
|
3172
|
+
maxProactivePerSession: 1,
|
|
3173
|
+
dismissCooldownHours: 168,
|
|
3174
|
+
suppressAfterDismissals: 2,
|
|
3175
|
+
pageDwellMinutes: 0,
|
|
3176
|
+
firstSessionSeconds: 0,
|
|
3177
|
+
rageClick: true,
|
|
3178
|
+
longTask: false,
|
|
3179
|
+
apiCascade: true,
|
|
3180
|
+
errorBoundary: true,
|
|
3181
|
+
betaMode: false,
|
|
3182
|
+
featureRequestCard: true
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
3185
|
+
function renderNudgeSnippet(opts) {
|
|
3186
|
+
const p3 = { ...PRESETS[opts.phase], ...opts.overrides };
|
|
3187
|
+
const dwellMs = p3.pageDwellMinutes > 0 ? `${p3.pageDwellMinutes} * 60 * 1000` : null;
|
|
3188
|
+
const firstMs = p3.firstSessionSeconds > 0 ? `${p3.firstSessionSeconds} * 1000` : null;
|
|
3189
|
+
const proactiveLines = [];
|
|
3190
|
+
if (p3.rageClick) proactiveLines.push(` rageClick: true,`);
|
|
3191
|
+
if (p3.longTask) proactiveLines.push(` longTask: true,`);
|
|
3192
|
+
if (p3.apiCascade) proactiveLines.push(` apiCascade: true,`);
|
|
3193
|
+
if (p3.errorBoundary) proactiveLines.push(` errorBoundary: true,`);
|
|
3194
|
+
if (dwellMs) proactiveLines.push(` pageDwell: { thresholdMs: ${dwellMs} }, // ${p3.pageDwellMinutes}min on the same route`);
|
|
3195
|
+
if (firstMs) proactiveLines.push(` firstSession: { delayMs: ${firstMs} }, // welcome new users after ${p3.firstSessionSeconds}s`);
|
|
3196
|
+
proactiveLines.push(` cooldown: {`);
|
|
3197
|
+
proactiveLines.push(` maxProactivePerSession: ${p3.maxProactivePerSession},`);
|
|
3198
|
+
proactiveLines.push(` dismissCooldownHours: ${p3.dismissCooldownHours},`);
|
|
3199
|
+
proactiveLines.push(` suppressAfterDismissals: ${p3.suppressAfterDismissals},`);
|
|
3200
|
+
proactiveLines.push(` },`);
|
|
3201
|
+
const widgetLines = [];
|
|
3202
|
+
if (p3.featureRequestCard) widgetLines.push(` featureRequestCard: true,`);
|
|
3203
|
+
if (p3.betaMode) {
|
|
3204
|
+
widgetLines.push(` betaMode: {`);
|
|
3205
|
+
widgetLines.push(` enabled: true,`);
|
|
3206
|
+
widgetLines.push(` label: 'BETA',`);
|
|
3207
|
+
widgetLines.push(` // changelogItems: [{ version: '1.0.0', date: '${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}', items: ['Fixed login timeout on slow networks'] }],`);
|
|
3208
|
+
widgetLines.push(` },`);
|
|
3209
|
+
}
|
|
3210
|
+
return [
|
|
3211
|
+
`// Generated by \`mushi nudge --phase ${opts.phase}\``,
|
|
3212
|
+
`// Phase: ${opts.phase} \u2014 tune via --max, --cooldown, --dwell, --welcome.`,
|
|
3213
|
+
`Mushi.init({`,
|
|
3214
|
+
` projectId: process.env.MUSHI_PROJECT_ID!,`,
|
|
3215
|
+
` apiKey: process.env.MUSHI_API_KEY!,`,
|
|
3216
|
+
` proactive: {`,
|
|
3217
|
+
...proactiveLines,
|
|
3218
|
+
` },`,
|
|
3219
|
+
...widgetLines.length ? [` widget: {`, ...widgetLines, ` },`] : [],
|
|
3220
|
+
`})`,
|
|
3221
|
+
``
|
|
3222
|
+
].join("\n");
|
|
3223
|
+
}
|
|
3224
|
+
function renderNudgeExplainer(phase) {
|
|
3225
|
+
const p3 = PRESETS[phase];
|
|
3226
|
+
const lines = [];
|
|
3227
|
+
lines.push(``);
|
|
3228
|
+
lines.push(`Nudge preset for "${phase}" phase:`);
|
|
3229
|
+
lines.push(` - max ${p3.maxProactivePerSession} proactive prompts per session`);
|
|
3230
|
+
lines.push(` - ${p3.dismissCooldownHours}h cooldown after a dismissal`);
|
|
3231
|
+
lines.push(` - suppress permanently after ${p3.suppressAfterDismissals} consecutive dismissals`);
|
|
3232
|
+
if (p3.pageDwellMinutes > 0) lines.push(` - fire page-dwell trigger after ${p3.pageDwellMinutes} continuous minutes on a route`);
|
|
3233
|
+
if (p3.firstSessionSeconds > 0) lines.push(` - welcome new users with a button pulse ${p3.firstSessionSeconds}s after init`);
|
|
3234
|
+
lines.push(` - signals enabled: ${[
|
|
3235
|
+
p3.rageClick && "rage-click",
|
|
3236
|
+
p3.longTask && "long-task",
|
|
3237
|
+
p3.apiCascade && "api-cascade",
|
|
3238
|
+
p3.errorBoundary && "error-boundary"
|
|
3239
|
+
].filter(Boolean).join(", ")}`);
|
|
3240
|
+
lines.push(` - feature-request card: ${p3.featureRequestCard ? "shown" : "hidden"}`);
|
|
3241
|
+
lines.push(` - beta-mode UI: ${p3.betaMode ? "on" : "off"}`);
|
|
3242
|
+
lines.push(``);
|
|
3243
|
+
return lines.join("\n");
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// src/commands/nudge.ts
|
|
3247
|
+
function registerNudgeCommand(program2) {
|
|
3248
|
+
program2.command("nudge").description(
|
|
3249
|
+
"Generate a Mushi.init() snippet tuned for your release phase (alpha, beta, ga). Customises proactive triggers, cooldowns, feature-request card, and beta-mode UI."
|
|
3250
|
+
).option("--phase <phase>", "Release phase: alpha | beta | ga", "beta").option("--explain", "Print a human-readable summary of what the preset does").option("--max <n>", "Override maxProactivePerSession").option("--cooldown <hours>", "Override dismissCooldownHours").option("--dwell <minutes>", "Override page-dwell threshold (0 disables)").option("--welcome <seconds>", "Override first-session welcome delay (0 disables)").action((opts) => {
|
|
3251
|
+
const validPhases = ["alpha", "beta", "ga"];
|
|
3252
|
+
if (!validPhases.includes(opts.phase)) {
|
|
3253
|
+
console.error(`Unknown phase "${opts.phase}". Use one of: ${validPhases.join(", ")}`);
|
|
3254
|
+
process.exit(1);
|
|
3255
|
+
}
|
|
3256
|
+
const phase = opts.phase;
|
|
3257
|
+
const overrides = {};
|
|
3258
|
+
const parseNumericFlag = (flag, raw, min) => {
|
|
3259
|
+
const n = Number(raw);
|
|
3260
|
+
if (!Number.isFinite(n) || n < min) {
|
|
3261
|
+
console.error(
|
|
3262
|
+
`error: --${flag} must be a finite number >= ${min} (got "${raw}")`
|
|
3263
|
+
);
|
|
3264
|
+
process.exit(1);
|
|
3265
|
+
}
|
|
3266
|
+
return n;
|
|
3267
|
+
};
|
|
3268
|
+
if (opts.max !== void 0) overrides.maxProactivePerSession = parseNumericFlag("max", opts.max, 1);
|
|
3269
|
+
if (opts.cooldown !== void 0) overrides.dismissCooldownHours = parseNumericFlag("cooldown", opts.cooldown, 0);
|
|
3270
|
+
if (opts.dwell !== void 0) overrides.pageDwellMinutes = parseNumericFlag("dwell", opts.dwell, 0);
|
|
3271
|
+
if (opts.welcome !== void 0) overrides.firstSessionSeconds = parseNumericFlag("welcome", opts.welcome, 0);
|
|
3272
|
+
if (opts.explain) {
|
|
3273
|
+
console.log(renderNudgeExplainer(phase));
|
|
3274
|
+
}
|
|
3275
|
+
console.log(renderNudgeSnippet({ phase, overrides }));
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// src/upgrade.ts
|
|
3280
|
+
import { resolve as resolve2 } from "path";
|
|
3281
|
+
import { execSync } from "child_process";
|
|
3282
|
+
var SAFE_NPM_VERSION = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
3283
|
+
var MUSHI_PACKAGES = [
|
|
3284
|
+
"@mushi-mushi/web",
|
|
3285
|
+
"@mushi-mushi/core",
|
|
3286
|
+
"@mushi-mushi/react",
|
|
3287
|
+
"@mushi-mushi/react-native",
|
|
3288
|
+
"@mushi-mushi/capacitor",
|
|
3289
|
+
"@mushi-mushi/node",
|
|
3290
|
+
"@mushi-mushi/cli"
|
|
3291
|
+
];
|
|
3292
|
+
async function planUpgrade(cwd) {
|
|
3293
|
+
const root = resolve2(cwd);
|
|
3294
|
+
const pkg = readPackageJson(root);
|
|
3295
|
+
const pm = detectPackageManager(root);
|
|
3296
|
+
const deps = {
|
|
3297
|
+
...pkg?.dependencies ?? {},
|
|
3298
|
+
...pkg?.devDependencies ?? {}
|
|
3299
|
+
};
|
|
3300
|
+
const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
|
|
3301
|
+
const entries = await Promise.all(
|
|
3302
|
+
installed2.map(async (name) => {
|
|
3303
|
+
const current = deps[name] ?? "";
|
|
3304
|
+
const currentCore = current.replace(/^[\^~>=<]*/, "");
|
|
3305
|
+
if (!/^\d/.test(currentCore)) {
|
|
3306
|
+
return { name, current, latest: null, willUpgrade: false };
|
|
3307
|
+
}
|
|
3308
|
+
const freshness = await checkFreshness(name, currentCore, {
|
|
3309
|
+
timeoutMs: 4e3,
|
|
3310
|
+
ignoreOptOut: true
|
|
3311
|
+
});
|
|
3312
|
+
const latest = freshness?.latest ?? null;
|
|
3313
|
+
const safeLatest = latest && SAFE_NPM_VERSION.test(latest) ? latest : null;
|
|
3314
|
+
const willUpgrade = Boolean(freshness?.isOutdated && safeLatest);
|
|
3315
|
+
return {
|
|
3316
|
+
name,
|
|
3317
|
+
current,
|
|
3318
|
+
latest: safeLatest,
|
|
3319
|
+
willUpgrade,
|
|
3320
|
+
migrateToWeb: name === "@mushi-mushi/react" && willUpgrade
|
|
3321
|
+
};
|
|
3322
|
+
})
|
|
3323
|
+
);
|
|
3324
|
+
const toBump = entries.filter((e) => e.willUpgrade && e.latest).map((e) => `${e.name}@${e.latest}`);
|
|
3325
|
+
return {
|
|
3326
|
+
cwd: root,
|
|
3327
|
+
packageManager: pm,
|
|
3328
|
+
entries,
|
|
3329
|
+
installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
async function runUpgrade(opts = {}) {
|
|
3333
|
+
const plan = await planUpgrade(opts.cwd ?? process.cwd());
|
|
3334
|
+
if (plan.entries.length === 0) {
|
|
3335
|
+
return {
|
|
3336
|
+
plan,
|
|
3337
|
+
upgraded: false,
|
|
3338
|
+
message: "No @mushi-mushi/* packages in package.json \u2014 run `mushi init` first."
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
if (!plan.installCmd) {
|
|
3342
|
+
const semverEntries = plan.entries.filter((e) => /\d/.test(e.current));
|
|
3343
|
+
const allChecksFailed = semverEntries.length > 0 && semverEntries.every((e) => e.latest === null);
|
|
3344
|
+
return {
|
|
3345
|
+
plan,
|
|
3346
|
+
upgraded: false,
|
|
3347
|
+
message: allChecksFailed ? "Could not reach the npm registry to check for updates \u2014 try again in a moment." : "All installed Mushi packages are already at the latest stable version."
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
if (opts.dryRun) {
|
|
3351
|
+
return { plan, upgraded: false, message: `[dry-run] Would run: ${plan.installCmd}` };
|
|
3352
|
+
}
|
|
3353
|
+
try {
|
|
3354
|
+
execSync(plan.installCmd, { cwd: plan.cwd, stdio: "inherit", env: process.env });
|
|
3355
|
+
} catch {
|
|
3356
|
+
return {
|
|
3357
|
+
plan,
|
|
3358
|
+
upgraded: false,
|
|
3359
|
+
message: `Install command failed \u2014 fix the error above or run it manually:
|
|
3360
|
+
${plan.installCmd}`
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
const reactEntry = plan.entries.find((e) => e.migrateToWeb);
|
|
3364
|
+
const migrateNote = reactEntry ? "\nNote: @mushi-mushi/react is legacy \u2014 prefer @mushi-mushi/web for Vite/Capacitor/SPA apps." : "";
|
|
3365
|
+
return {
|
|
3366
|
+
plan,
|
|
3367
|
+
upgraded: true,
|
|
3368
|
+
message: `Upgraded Mushi SDK packages.
|
|
3369
|
+
${plan.installCmd}${migrateNote}`
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/commands/upgrade-cli.ts
|
|
3374
|
+
function registerUpgradeCommand(program2) {
|
|
3375
|
+
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", `
|
|
3376
|
+
Examples:
|
|
3377
|
+
mushi upgrade
|
|
3378
|
+
mushi upgrade --dry-run
|
|
3379
|
+
mushi upgrade --cwd ../glot.it`).action(async (opts) => {
|
|
3380
|
+
const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
|
|
3381
|
+
if (opts.json) {
|
|
3382
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3383
|
+
} else {
|
|
3384
|
+
console.log(result.message);
|
|
3385
|
+
for (const e of result.plan.entries) {
|
|
3386
|
+
const tag = e.willUpgrade && e.latest ? `\u2192 v${e.latest}` : "(current)";
|
|
3387
|
+
console.log(` ${e.name}@${e.current} ${tag}`);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
if (!result.upgraded && result.plan.entries.some((e) => e.willUpgrade) && !opts.dryRun) {
|
|
3391
|
+
process.exit(1);
|
|
3392
|
+
}
|
|
3393
|
+
if (result.plan.entries.length === 0) process.exit(1);
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
// src/commands/connect-cli.ts
|
|
3398
|
+
function registerConnectCommand(program2) {
|
|
3399
|
+
program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "Force-write SDK env vars to .env.local (default; overrides --no-env)").option("--wire-ide", "Force-wire Cursor MCP into .cursor/mcp.json (default; overrides --no-ide)").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
|
|
3400
|
+
Examples:
|
|
3401
|
+
MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
|
|
3402
|
+
mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
|
|
3403
|
+
const apiKey = process.env.MUSHI_API_KEY ?? opts.apiKey;
|
|
3404
|
+
if (!apiKey) {
|
|
3405
|
+
console.error("Provide the API key via the MUSHI_API_KEY env var (recommended) or --api-key <key>.");
|
|
3406
|
+
process.exit(1);
|
|
3407
|
+
}
|
|
3408
|
+
const result = await runConnect({
|
|
3409
|
+
apiKey,
|
|
3410
|
+
projectId: opts.projectId,
|
|
3411
|
+
endpoint: opts.endpoint,
|
|
3412
|
+
cwd: opts.cwd,
|
|
3413
|
+
writeEnv: opts.writeEnv === true ? true : opts.env !== false,
|
|
3414
|
+
wireIde: opts.wireIde === true ? true : opts.ide !== false,
|
|
3415
|
+
wait: opts.wait,
|
|
3416
|
+
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
3417
|
+
json: opts.json
|
|
3418
|
+
});
|
|
3419
|
+
if (opts.json) {
|
|
3420
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3421
|
+
} else {
|
|
3422
|
+
for (const line of result.messages) console.log(line);
|
|
3423
|
+
}
|
|
3424
|
+
if (!result.ok) process.exit(1);
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// src/doctor.ts
|
|
3429
|
+
function checkCliConfig(config) {
|
|
3430
|
+
return [
|
|
3431
|
+
{
|
|
3432
|
+
name: "CLI config file",
|
|
3433
|
+
ok: Boolean(config.endpoint),
|
|
3434
|
+
detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint \u2014 set MUSHI_API_ENDPOINT, run `mushi connect`, or `mushi config endpoint <url>`"
|
|
3435
|
+
},
|
|
3436
|
+
{
|
|
3437
|
+
name: "API key configured",
|
|
3438
|
+
ok: Boolean(config.apiKey),
|
|
3439
|
+
detail: config.apiKey ? `apiKey=${config.apiKey.slice(0, 8)}\u2026${config.apiKey.slice(-4)}` : "No API key set \u2014 run `mushi login --api-key <key>`"
|
|
3440
|
+
},
|
|
3441
|
+
{
|
|
3442
|
+
name: "Project ID configured",
|
|
3443
|
+
ok: Boolean(config.projectId),
|
|
3444
|
+
detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
|
|
3445
|
+
}
|
|
3446
|
+
];
|
|
3447
|
+
}
|
|
3448
|
+
async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
3449
|
+
try {
|
|
3450
|
+
const safeEndpoint = sanitizeEndpoint(endpoint);
|
|
3451
|
+
const res = await doFetch(`${safeEndpoint}/health`, {
|
|
3452
|
+
signal: AbortSignal.timeout(5e3)
|
|
3453
|
+
});
|
|
3454
|
+
return {
|
|
3455
|
+
name: "Endpoint reachable",
|
|
3456
|
+
ok: res.status === 200,
|
|
3457
|
+
detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
|
|
2854
3458
|
};
|
|
2855
3459
|
} catch (err) {
|
|
2856
3460
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2909,6 +3513,23 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
2909
3513
|
detail: sc.ready ? "" : sc.hint
|
|
2910
3514
|
}));
|
|
2911
3515
|
}
|
|
3516
|
+
if (res.status === 403) {
|
|
3517
|
+
let errCode;
|
|
3518
|
+
try {
|
|
3519
|
+
const body = await res.json();
|
|
3520
|
+
errCode = body?.error?.code;
|
|
3521
|
+
} catch {
|
|
3522
|
+
}
|
|
3523
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
3524
|
+
return [
|
|
3525
|
+
{
|
|
3526
|
+
name: "Server preflight",
|
|
3527
|
+
ok: true,
|
|
3528
|
+
detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and run admin checks."
|
|
3529
|
+
}
|
|
3530
|
+
];
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
2912
3533
|
const text2 = await res.text().catch(() => "");
|
|
2913
3534
|
return [
|
|
2914
3535
|
{
|
|
@@ -3111,7 +3732,17 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
3111
3732
|
let mcpRaw = null;
|
|
3112
3733
|
for (const candidate of candidates) {
|
|
3113
3734
|
try {
|
|
3114
|
-
|
|
3735
|
+
const raw = await readFile2(candidate, "utf8");
|
|
3736
|
+
let parsed = {};
|
|
3737
|
+
try {
|
|
3738
|
+
parsed = JSON.parse(raw);
|
|
3739
|
+
} catch {
|
|
3740
|
+
}
|
|
3741
|
+
const hasEntries = Object.keys(parsed.mcpServers ?? {}).length > 0;
|
|
3742
|
+
if (!hasEntries && candidates.indexOf(candidate) < candidates.length - 1) {
|
|
3743
|
+
continue;
|
|
3744
|
+
}
|
|
3745
|
+
mcpRaw = raw;
|
|
3115
3746
|
mcpPath = candidate;
|
|
3116
3747
|
break;
|
|
3117
3748
|
} catch {
|
|
@@ -3188,359 +3819,212 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
3188
3819
|
detail: `Key is valid; ${projectCount} accessible project${projectCount === 1 ? "" : "s"}`
|
|
3189
3820
|
});
|
|
3190
3821
|
} else {
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
return checks;
|
|
3207
|
-
}
|
|
3208
|
-
var FIX_HINTS = {
|
|
3209
|
-
"CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
|
|
3210
|
-
"API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
|
|
3211
|
-
"Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
|
|
3212
|
-
"Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
|
|
3213
|
-
"[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
|
|
3214
|
-
"[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
|
|
3215
|
-
"[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
|
|
3216
|
-
};
|
|
3217
|
-
function fixHintForCheck(name) {
|
|
3218
|
-
if (FIX_HINTS[name]) return FIX_HINTS[name];
|
|
3219
|
-
if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
|
|
3220
|
-
if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
|
|
3221
|
-
if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
|
|
3222
|
-
return void 0;
|
|
3223
|
-
}
|
|
3224
|
-
async function runDoctor(config, options = {}) {
|
|
3225
|
-
const doFetch = options.fetch ?? globalThis.fetch;
|
|
3226
|
-
const checks = [];
|
|
3227
|
-
const runServer = options.server !== false;
|
|
3228
|
-
const runIngest = options.ingest !== false;
|
|
3229
|
-
checks.push(...checkCliConfig(config));
|
|
3230
|
-
if (config.endpoint) {
|
|
3231
|
-
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
3232
|
-
}
|
|
3233
|
-
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
3234
|
-
if (sdkCheck) checks.push(sdkCheck);
|
|
3235
|
-
if (runServer) {
|
|
3236
|
-
const serverChecks = await checkServerPreflight(config, doFetch);
|
|
3237
|
-
checks.push(...serverChecks);
|
|
3238
|
-
}
|
|
3239
|
-
if (runIngest) {
|
|
3240
|
-
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
3241
|
-
checks.push(...ingestChecks);
|
|
3242
|
-
}
|
|
3243
|
-
if (options.qaStories) {
|
|
3244
|
-
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
3245
|
-
checks.push(...qaChecks);
|
|
3246
|
-
}
|
|
3247
|
-
if (options.hostApp) {
|
|
3248
|
-
const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
|
|
3249
|
-
checks.push(...hostChecks);
|
|
3250
|
-
}
|
|
3251
|
-
if (options.mcp) {
|
|
3252
|
-
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
3253
|
-
checks.push(...mcpChecks);
|
|
3254
|
-
}
|
|
3255
|
-
return { checks, ready: checks.every((c) => c.ok) };
|
|
3256
|
-
}
|
|
3257
|
-
function formatDoctorResult(result) {
|
|
3258
|
-
const PASS = "\u2713";
|
|
3259
|
-
const FAIL = "\u2717";
|
|
3260
|
-
const lines = [];
|
|
3261
|
-
for (const c of result.checks) {
|
|
3262
|
-
lines.push(`${c.ok ? PASS : FAIL} ${c.name}`);
|
|
3263
|
-
if (c.detail) lines.push(` ${c.detail}`);
|
|
3264
|
-
if (!c.ok) {
|
|
3265
|
-
const hint = fixHintForCheck(c.name);
|
|
3266
|
-
if (hint) lines.push(` \u2192 Fix: ${hint}`);
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
const failed = result.checks.filter((c) => !c.ok);
|
|
3270
|
-
if (failed.length === 0) {
|
|
3271
|
-
lines.push("\nAll checks passed. The CLI is ready.");
|
|
3272
|
-
} else {
|
|
3273
|
-
lines.push(`
|
|
3274
|
-
${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
3275
|
-
lines.push("Fix the items above and re-run `mushi doctor`.");
|
|
3276
|
-
}
|
|
3277
|
-
return lines.join("\n");
|
|
3278
|
-
}
|
|
3279
|
-
|
|
3280
|
-
// src/upgrade.ts
|
|
3281
|
-
import { resolve as resolve2 } from "path";
|
|
3282
|
-
import { execSync } from "child_process";
|
|
3283
|
-
var SAFE_NPM_VERSION = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
3284
|
-
var MUSHI_PACKAGES = [
|
|
3285
|
-
"@mushi-mushi/web",
|
|
3286
|
-
"@mushi-mushi/core",
|
|
3287
|
-
"@mushi-mushi/react",
|
|
3288
|
-
"@mushi-mushi/react-native",
|
|
3289
|
-
"@mushi-mushi/capacitor",
|
|
3290
|
-
"@mushi-mushi/node",
|
|
3291
|
-
"@mushi-mushi/cli"
|
|
3292
|
-
];
|
|
3293
|
-
async function planUpgrade(cwd) {
|
|
3294
|
-
const root = resolve2(cwd);
|
|
3295
|
-
const pkg = readPackageJson(root);
|
|
3296
|
-
const pm = detectPackageManager(root);
|
|
3297
|
-
const deps = {
|
|
3298
|
-
...pkg?.dependencies ?? {},
|
|
3299
|
-
...pkg?.devDependencies ?? {}
|
|
3300
|
-
};
|
|
3301
|
-
const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
|
|
3302
|
-
const entries = await Promise.all(
|
|
3303
|
-
installed2.map(async (name) => {
|
|
3304
|
-
const current = deps[name] ?? "";
|
|
3305
|
-
const currentCore = current.replace(/^[\^~>=<]*/, "");
|
|
3306
|
-
if (!/^\d/.test(currentCore)) {
|
|
3307
|
-
return { name, current, latest: null, willUpgrade: false };
|
|
3822
|
+
let detail = `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`;
|
|
3823
|
+
try {
|
|
3824
|
+
const errBody = await res.json();
|
|
3825
|
+
const nested = errBody["error"];
|
|
3826
|
+
const errCode = nested?.["code"] ?? errBody["code"] ?? "";
|
|
3827
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
3828
|
+
detail = `Key has report:write scope only \u2014 run \`mushi login --upgrade-scope\` then \`mushi setup\` to get mcp:read.`;
|
|
3829
|
+
}
|
|
3830
|
+
} catch {
|
|
3831
|
+
}
|
|
3832
|
+
checks.push({
|
|
3833
|
+
name: "[mcp] account-overview reachable",
|
|
3834
|
+
ok: false,
|
|
3835
|
+
detail
|
|
3836
|
+
});
|
|
3308
3837
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3838
|
+
} catch (err) {
|
|
3839
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3840
|
+
checks.push({
|
|
3841
|
+
name: "[mcp] account-overview reachable",
|
|
3842
|
+
ok: false,
|
|
3843
|
+
detail: `Probe failed: ${msg}. Check MUSHI_API_ENDPOINT and network connectivity.`
|
|
3312
3844
|
});
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return checks;
|
|
3848
|
+
}
|
|
3849
|
+
var FIX_HINTS = {
|
|
3850
|
+
"CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
|
|
3851
|
+
"API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
|
|
3852
|
+
"Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
|
|
3853
|
+
"Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
|
|
3854
|
+
"[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
|
|
3855
|
+
"[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
|
|
3856
|
+
"[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
|
|
3857
|
+
};
|
|
3858
|
+
function fixHintForCheck(name) {
|
|
3859
|
+
if (FIX_HINTS[name]) return FIX_HINTS[name];
|
|
3860
|
+
if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
|
|
3861
|
+
if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
|
|
3862
|
+
if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
|
|
3863
|
+
return void 0;
|
|
3864
|
+
}
|
|
3865
|
+
async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = globalThis.fetch) {
|
|
3866
|
+
if (!config.apiKey || !config.projectId || !config.endpoint) {
|
|
3867
|
+
if (config.apiKey && config.endpoint) {
|
|
3316
3868
|
return {
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
willUpgrade,
|
|
3321
|
-
migrateToWeb: name === "@mushi-mushi/react" && willUpgrade
|
|
3869
|
+
nextAction: "Select or create a project: run `mushi project create` or pick one with `mushi project list`",
|
|
3870
|
+
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3871
|
+
done: false
|
|
3322
3872
|
};
|
|
3323
|
-
}
|
|
3324
|
-
);
|
|
3325
|
-
const toBump = entries.filter((e) => e.willUpgrade && e.latest).map((e) => `${e.name}@${e.latest}`);
|
|
3326
|
-
return {
|
|
3327
|
-
cwd: root,
|
|
3328
|
-
packageManager: pm,
|
|
3329
|
-
entries,
|
|
3330
|
-
installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
|
|
3331
|
-
};
|
|
3332
|
-
}
|
|
3333
|
-
async function runUpgrade(opts = {}) {
|
|
3334
|
-
const plan = await planUpgrade(opts.cwd ?? process.cwd());
|
|
3335
|
-
if (plan.entries.length === 0) {
|
|
3873
|
+
}
|
|
3336
3874
|
return {
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3875
|
+
nextAction: "Sign in to Mushi: run `npx mushi-mushi` (browser opens automatically \u2014 do NOT type the code in the terminal)",
|
|
3876
|
+
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3877
|
+
done: false
|
|
3340
3878
|
};
|
|
3341
3879
|
}
|
|
3342
|
-
|
|
3343
|
-
const
|
|
3344
|
-
const
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3880
|
+
try {
|
|
3881
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
3882
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3883
|
+
const root = resolve3(cwd);
|
|
3884
|
+
let envContent = "";
|
|
3885
|
+
for (const f of [".env.local", ".env"]) {
|
|
3886
|
+
try {
|
|
3887
|
+
envContent = await readFile2(join5(root, f), "utf8");
|
|
3888
|
+
break;
|
|
3889
|
+
} catch {
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
|
|
3893
|
+
if (!hasEnvVars) {
|
|
3894
|
+
return {
|
|
3895
|
+
nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
|
|
3896
|
+
ctaPath: "/onboarding?tab=sdk",
|
|
3897
|
+
done: false
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
} catch {
|
|
3350
3901
|
}
|
|
3351
|
-
|
|
3352
|
-
|
|
3902
|
+
try {
|
|
3903
|
+
const sdkCheck = await checkSdkInstall(cwd);
|
|
3904
|
+
if (sdkCheck && !sdkCheck.ok) {
|
|
3905
|
+
return {
|
|
3906
|
+
nextAction: `Install the Mushi SDK: ${sdkCheck.detail}`,
|
|
3907
|
+
ctaPath: "/onboarding?tab=sdk",
|
|
3908
|
+
done: false
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
} catch {
|
|
3353
3912
|
}
|
|
3354
3913
|
try {
|
|
3355
|
-
|
|
3914
|
+
const data = await fetchIngestSetup(
|
|
3915
|
+
{ endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
|
|
3916
|
+
doFetch
|
|
3917
|
+
);
|
|
3918
|
+
if (data) {
|
|
3919
|
+
const steps = data.steps ?? [];
|
|
3920
|
+
const incomplete = steps.find((s) => s.required && !s.complete);
|
|
3921
|
+
if (incomplete) {
|
|
3922
|
+
const tabMap = {
|
|
3923
|
+
project_created: "/onboarding?tab=steps&setup=cli",
|
|
3924
|
+
api_key_generated: "/onboarding?tab=verify",
|
|
3925
|
+
sdk_installed: "/onboarding?tab=sdk",
|
|
3926
|
+
first_report_received: "/onboarding?tab=verify"
|
|
3927
|
+
};
|
|
3928
|
+
const tab = tabMap[incomplete.id] ?? "/onboarding?tab=verify";
|
|
3929
|
+
return {
|
|
3930
|
+
nextAction: incomplete.hint ?? `Complete setup step: ${incomplete.label}`,
|
|
3931
|
+
ctaPath: tab,
|
|
3932
|
+
done: false
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
return {
|
|
3936
|
+
nextAction: "All 4 required setup steps are complete! Open the console to explore.",
|
|
3937
|
+
ctaPath: "/reports",
|
|
3938
|
+
done: true
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3356
3941
|
} catch {
|
|
3357
|
-
return {
|
|
3358
|
-
plan,
|
|
3359
|
-
upgraded: false,
|
|
3360
|
-
message: `Install command failed \u2014 fix the error above or run it manually:
|
|
3361
|
-
${plan.installCmd}`
|
|
3362
|
-
};
|
|
3363
3942
|
}
|
|
3364
|
-
const reactEntry = plan.entries.find((e) => e.migrateToWeb);
|
|
3365
|
-
const migrateNote = reactEntry ? "\nNote: @mushi-mushi/react is legacy \u2014 prefer @mushi-mushi/web for Vite/Capacitor/SPA apps." : "";
|
|
3366
3943
|
return {
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
${plan.installCmd}${migrateNote}`
|
|
3944
|
+
nextAction: "Open the onboarding wizard in the console to check your setup status.",
|
|
3945
|
+
ctaPath: "/onboarding",
|
|
3946
|
+
done: false
|
|
3371
3947
|
};
|
|
3372
3948
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
const
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
).
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
const
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
const
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
agent: opts.agent
|
|
3416
|
-
};
|
|
3417
|
-
if (opts.agent === "cursor_cloud") {
|
|
3418
|
-
if (opts.model) body.cursorModel = opts.model;
|
|
3419
|
-
if (!opts.autoPr) body.cursorAutoCreatePR = false;
|
|
3420
|
-
}
|
|
3421
|
-
const result = await apiCall("/v1/admin/fixes/dispatch", cfg, {
|
|
3422
|
-
method: "POST",
|
|
3423
|
-
headers: { "Content-Type": "application/json" },
|
|
3424
|
-
body: JSON.stringify(body)
|
|
3425
|
-
});
|
|
3426
|
-
if (!result.ok) {
|
|
3427
|
-
console.error("Error dispatching fix:", result.error.message);
|
|
3428
|
-
process.exit(1);
|
|
3429
|
-
}
|
|
3430
|
-
const { fixId, status, agentId, runId, prUrl } = result.data;
|
|
3431
|
-
emitEvent("dispatch.ok", { fixId, status, agentId, runId, prUrl });
|
|
3432
|
-
if (!opts.wait) {
|
|
3433
|
-
process.exit(0);
|
|
3434
|
-
}
|
|
3435
|
-
if (!fixId) {
|
|
3436
|
-
console.error("No fixId returned \u2014 cannot poll.");
|
|
3437
|
-
process.exit(1);
|
|
3438
|
-
}
|
|
3439
|
-
const POLL_MS = 5e3;
|
|
3440
|
-
const MAX_POLLS = 120;
|
|
3441
|
-
const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "error", "cancelled", "skipped", "skipped_unsupported_agent", "skipped_no_sandbox"]);
|
|
3442
|
-
for (let i = 0; i < MAX_POLLS; i++) {
|
|
3443
|
-
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
3444
|
-
const pollResult = await apiCall(
|
|
3445
|
-
`/v1/admin/fixes/${fixId}`,
|
|
3446
|
-
cfg
|
|
3447
|
-
);
|
|
3448
|
-
if (!pollResult.ok) {
|
|
3449
|
-
emitEvent("poll.error", { error: pollResult.error.message });
|
|
3450
|
-
continue;
|
|
3451
|
-
}
|
|
3452
|
-
const s = pollResult.data.status;
|
|
3453
|
-
emitEvent("fix.status", { status: s, pr_url: pollResult.data.pr_url, cursor_agent_id: pollResult.data.cursor_agent_id });
|
|
3454
|
-
if (s && TERMINAL.has(s)) {
|
|
3455
|
-
const success = s === "completed";
|
|
3456
|
-
if (!success) {
|
|
3457
|
-
console.error(`Fix ended with status: ${s}${pollResult.data.error ? ` \u2014 ${pollResult.data.error}` : ""}`);
|
|
3458
|
-
process.exit(1);
|
|
3459
|
-
}
|
|
3460
|
-
process.exit(0);
|
|
3461
|
-
}
|
|
3462
|
-
}
|
|
3463
|
-
console.error("Polling timed out after 10 minutes. The fix may still be running.");
|
|
3464
|
-
process.exit(1);
|
|
3465
|
-
});
|
|
3466
|
-
program2.command("nudge").description(
|
|
3467
|
-
"Generate a Mushi.init() snippet tuned for your release phase (alpha, beta, ga). Customises proactive triggers, cooldowns, feature-request card, and beta-mode UI."
|
|
3468
|
-
).option("--phase <phase>", "Release phase: alpha | beta | ga", "beta").option("--explain", "Print a human-readable summary of what the preset does").option("--max <n>", "Override maxProactivePerSession").option("--cooldown <hours>", "Override dismissCooldownHours").option("--dwell <minutes>", "Override page-dwell threshold (0 disables)").option("--welcome <seconds>", "Override first-session welcome delay (0 disables)").action((opts) => {
|
|
3469
|
-
const validPhases = ["alpha", "beta", "ga"];
|
|
3470
|
-
if (!validPhases.includes(opts.phase)) {
|
|
3471
|
-
console.error(`Unknown phase "${opts.phase}". Use one of: ${validPhases.join(", ")}`);
|
|
3472
|
-
process.exit(1);
|
|
3473
|
-
}
|
|
3474
|
-
const phase = opts.phase;
|
|
3475
|
-
const overrides = {};
|
|
3476
|
-
const parseNumericFlag = (flag, raw, min) => {
|
|
3477
|
-
const n = Number(raw);
|
|
3478
|
-
if (!Number.isFinite(n) || n < min) {
|
|
3479
|
-
console.error(
|
|
3480
|
-
`error: --${flag} must be a finite number >= ${min} (got "${raw}")`
|
|
3481
|
-
);
|
|
3482
|
-
process.exit(1);
|
|
3483
|
-
}
|
|
3484
|
-
return n;
|
|
3485
|
-
};
|
|
3486
|
-
if (opts.max !== void 0) overrides.maxProactivePerSession = parseNumericFlag("max", opts.max, 1);
|
|
3487
|
-
if (opts.cooldown !== void 0) overrides.dismissCooldownHours = parseNumericFlag("cooldown", opts.cooldown, 0);
|
|
3488
|
-
if (opts.dwell !== void 0) overrides.pageDwellMinutes = parseNumericFlag("dwell", opts.dwell, 0);
|
|
3489
|
-
if (opts.welcome !== void 0) overrides.firstSessionSeconds = parseNumericFlag("welcome", opts.welcome, 0);
|
|
3490
|
-
if (opts.explain) {
|
|
3491
|
-
console.log(renderNudgeExplainer(phase));
|
|
3492
|
-
}
|
|
3493
|
-
console.log(renderNudgeSnippet({ phase, overrides }));
|
|
3494
|
-
});
|
|
3495
|
-
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", `
|
|
3496
|
-
Examples:
|
|
3497
|
-
mushi upgrade
|
|
3498
|
-
mushi upgrade --dry-run
|
|
3499
|
-
mushi upgrade --cwd ../glot.it`).action(async (opts) => {
|
|
3500
|
-
const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
|
|
3501
|
-
if (opts.json) {
|
|
3502
|
-
console.log(JSON.stringify(result, null, 2));
|
|
3503
|
-
} else {
|
|
3504
|
-
console.log(result.message);
|
|
3505
|
-
for (const e of result.plan.entries) {
|
|
3506
|
-
const tag = e.willUpgrade && e.latest ? `\u2192 v${e.latest}` : "(current)";
|
|
3507
|
-
console.log(` ${e.name}@${e.current} ${tag}`);
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
if (!result.upgraded && result.plan.entries.some((e) => e.willUpgrade) && !opts.dryRun) {
|
|
3511
|
-
process.exit(1);
|
|
3949
|
+
async function runDoctor(config, options = {}) {
|
|
3950
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
3951
|
+
const checks = [];
|
|
3952
|
+
const isFull = options.full === true;
|
|
3953
|
+
const runServer = isFull || options.server !== false;
|
|
3954
|
+
const runIngest = isFull || options.ingest !== false;
|
|
3955
|
+
checks.push(...checkCliConfig(config));
|
|
3956
|
+
if (config.endpoint) {
|
|
3957
|
+
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
3958
|
+
}
|
|
3959
|
+
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
3960
|
+
if (sdkCheck) checks.push(sdkCheck);
|
|
3961
|
+
if (runServer) {
|
|
3962
|
+
const serverChecks = await checkServerPreflight(config, doFetch);
|
|
3963
|
+
checks.push(...serverChecks);
|
|
3964
|
+
}
|
|
3965
|
+
if (runIngest) {
|
|
3966
|
+
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
3967
|
+
checks.push(...ingestChecks);
|
|
3968
|
+
}
|
|
3969
|
+
if (isFull || options.qaStories) {
|
|
3970
|
+
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
3971
|
+
checks.push(...qaChecks);
|
|
3972
|
+
}
|
|
3973
|
+
if (isFull || options.hostApp) {
|
|
3974
|
+
const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
|
|
3975
|
+
checks.push(...hostChecks);
|
|
3976
|
+
}
|
|
3977
|
+
if (isFull || options.mcp) {
|
|
3978
|
+
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
3979
|
+
checks.push(...mcpChecks);
|
|
3980
|
+
}
|
|
3981
|
+
const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
|
|
3982
|
+
if (heartbeatPassed) {
|
|
3983
|
+
const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
|
|
3984
|
+
if (idx >= 0) {
|
|
3985
|
+
checks[idx] = {
|
|
3986
|
+
...checks[idx],
|
|
3987
|
+
ok: true,
|
|
3988
|
+
warn: true,
|
|
3989
|
+
detail: "No @mushi-mushi/* found in cwd package.json \u2014 but live heartbeats confirm the SDK is active. Run from your app repo or install with `mushi init`."
|
|
3990
|
+
};
|
|
3512
3991
|
}
|
|
3513
|
-
|
|
3514
|
-
}
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3992
|
+
}
|
|
3993
|
+
return { checks, ready: checks.every((c) => c.ok) };
|
|
3994
|
+
}
|
|
3995
|
+
function formatDoctorResult(result) {
|
|
3996
|
+
const PASS = "OK";
|
|
3997
|
+
const WARN = "WARN";
|
|
3998
|
+
const FAIL = "FAIL";
|
|
3999
|
+
const lines = [];
|
|
4000
|
+
for (const c of result.checks) {
|
|
4001
|
+
const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
|
|
4002
|
+
lines.push(`${icon} ${c.name}`);
|
|
4003
|
+
if (c.detail) lines.push(` ${c.detail}`);
|
|
4004
|
+
if (!c.ok) {
|
|
4005
|
+
const hint = fixHintForCheck(c.name);
|
|
4006
|
+
if (hint) lines.push(` \u2192 Fix: ${hint}`);
|
|
3523
4007
|
}
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
writeEnv: opts.writeEnv === true ? true : opts.env !== false,
|
|
3532
|
-
wireIde: opts.wireIde === true ? true : opts.ide !== false,
|
|
3533
|
-
wait: opts.wait,
|
|
3534
|
-
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
3535
|
-
json: opts.json
|
|
3536
|
-
});
|
|
3537
|
-
if (opts.json) {
|
|
3538
|
-
console.log(JSON.stringify(result, null, 2));
|
|
4008
|
+
}
|
|
4009
|
+
const failed = result.checks.filter((c) => !c.ok);
|
|
4010
|
+
const warned = result.checks.filter((c) => c.ok && c.warn);
|
|
4011
|
+
if (failed.length === 0) {
|
|
4012
|
+
if (warned.length > 0) {
|
|
4013
|
+
lines.push(`
|
|
4014
|
+
All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
|
|
3539
4015
|
} else {
|
|
3540
|
-
|
|
4016
|
+
lines.push("\nAll checks passed. The CLI is ready.");
|
|
3541
4017
|
}
|
|
3542
|
-
|
|
3543
|
-
|
|
4018
|
+
} else {
|
|
4019
|
+
lines.push(`
|
|
4020
|
+
${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
4021
|
+
lines.push("Fix the items above and re-run `mushi doctor`.");
|
|
4022
|
+
}
|
|
4023
|
+
return lines.join("\n");
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
// src/commands/doctor-cli.ts
|
|
4027
|
+
function registerDoctorCommand(program2) {
|
|
3544
4028
|
program2.command("doctor").description(
|
|
3545
4029
|
"Run pre-flight checks: CLI config, endpoint reachability, SDK install, ingest readiness (API key \u2192 heartbeat \u2192 first report), and dispatch readiness (GitHub, index, BYOK, autofix). Ingest + server checks run by default; pass --no-server or --no-ingest to skip."
|
|
3546
4030
|
).option("--cwd <path>", "Run package detection from a different directory").option("--json", "Machine-readable output").option("--no-server", "Skip dispatch-readiness /preflight checks").option("--no-ingest", "Skip ingest-setup checks (SDK heartbeat, first report)").option(
|
|
@@ -3555,9 +4039,38 @@ Examples:
|
|
|
3555
4039
|
).option(
|
|
3556
4040
|
"--fix",
|
|
3557
4041
|
"Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
|
|
4042
|
+
).option(
|
|
4043
|
+
"--onboarding",
|
|
4044
|
+
"Focused onboarding check: prints the single next action you need to take to finish SDK setup, with a console deep link."
|
|
4045
|
+
).option(
|
|
4046
|
+
"--full",
|
|
4047
|
+
"Run ALL check categories (server, ingest, host-app, mcp, qa-stories) in one shot. Good for first-run diagnostics. Exit 0 only when all checks pass."
|
|
3558
4048
|
).action(async (opts) => {
|
|
3559
4049
|
const config = loadConfig();
|
|
3560
|
-
|
|
4050
|
+
if (opts.onboarding) {
|
|
4051
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
4052
|
+
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
4053
|
+
const status = await checkOnboardingStatus(config, consoleBase, cwd);
|
|
4054
|
+
const base = consoleBase.replace(/\/$/, "");
|
|
4055
|
+
if (status.done) {
|
|
4056
|
+
console.log(`OK Setup complete. ${status.nextAction}`);
|
|
4057
|
+
console.log(` ${base}${status.ctaPath}`);
|
|
4058
|
+
} else {
|
|
4059
|
+
console.log(`\u2192 Next: ${status.nextAction}`);
|
|
4060
|
+
console.log(` Open: ${base}${status.ctaPath}`);
|
|
4061
|
+
}
|
|
4062
|
+
if (!status.done) process.exit(1);
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
const doctorOpts = {
|
|
4066
|
+
cwd: opts.cwd,
|
|
4067
|
+
server: opts.server,
|
|
4068
|
+
ingest: opts.ingest,
|
|
4069
|
+
qaStories: opts.qaStories,
|
|
4070
|
+
hostApp: opts.hostApp,
|
|
4071
|
+
mcp: opts.mcp,
|
|
4072
|
+
full: opts.full
|
|
4073
|
+
};
|
|
3561
4074
|
let result = await runDoctor(config, doctorOpts);
|
|
3562
4075
|
if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
|
|
3563
4076
|
const connectResult = await runConnect({
|
|
@@ -3580,6 +4093,10 @@ Examples:
|
|
|
3580
4093
|
console.log(formatDoctorResult(result));
|
|
3581
4094
|
if (!result.ready) process.exit(1);
|
|
3582
4095
|
});
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
// src/commands/reset-cli.ts
|
|
4099
|
+
function registerResetCommand(program2) {
|
|
3583
4100
|
program2.command("reset [projectId]").description(
|
|
3584
4101
|
"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."
|
|
3585
4102
|
).option("--confirm", "Required safety flag \u2014 must pass to proceed").option("--json", "Machine-readable output").action(async (projectId, opts) => {
|
|
@@ -3614,146 +4131,6 @@ Re-run with --confirm to proceed.`
|
|
|
3614
4131
|
process.exit(1);
|
|
3615
4132
|
}
|
|
3616
4133
|
});
|
|
3617
|
-
const fixes = program2.command("fixes").description("Fix dispatch management");
|
|
3618
|
-
fixes.command("tail").description(
|
|
3619
|
-
"Stream SSE dispatch events for a report in real time. Useful for headless debugging without opening the browser."
|
|
3620
|
-
).requiredOption("--report-id <id>", "Report ID to follow").action(async (opts) => {
|
|
3621
|
-
const config = loadConfig();
|
|
3622
|
-
if (!config.apiKey) {
|
|
3623
|
-
console.error("Run `mushi login` first");
|
|
3624
|
-
process.exit(1);
|
|
3625
|
-
}
|
|
3626
|
-
if (!config.endpoint) {
|
|
3627
|
-
console.error("No endpoint configured. Run `mushi init`");
|
|
3628
|
-
process.exit(1);
|
|
3629
|
-
}
|
|
3630
|
-
const url = `${config.endpoint}/v1/admin/reports/${opts.reportId}/dispatch/stream`;
|
|
3631
|
-
console.log(`Tailing dispatch stream for report ${opts.reportId}\u2026`);
|
|
3632
|
-
console.log(`(Ctrl-C to stop)
|
|
3633
|
-
`);
|
|
3634
|
-
const res = await fetch(url, {
|
|
3635
|
-
headers: {
|
|
3636
|
-
"Authorization": `Bearer ${config.apiKey}`,
|
|
3637
|
-
"X-Mushi-Api-Key": config.apiKey,
|
|
3638
|
-
"X-Mushi-Project": config.projectId ?? "",
|
|
3639
|
-
"Accept": "text/event-stream"
|
|
3640
|
-
}
|
|
3641
|
-
});
|
|
3642
|
-
if (!res.ok || !res.body) {
|
|
3643
|
-
console.error(`Failed to connect: HTTP ${res.status}`);
|
|
3644
|
-
const text2 = await res.text().catch(() => "");
|
|
3645
|
-
if (text2) console.error(text2.slice(0, 300));
|
|
3646
|
-
process.exit(1);
|
|
3647
|
-
}
|
|
3648
|
-
const decoder = new TextDecoder();
|
|
3649
|
-
const reader = res.body.getReader();
|
|
3650
|
-
let done = false;
|
|
3651
|
-
process.on("SIGINT", () => {
|
|
3652
|
-
done = true;
|
|
3653
|
-
void reader.cancel();
|
|
3654
|
-
console.log("\nDisconnected.");
|
|
3655
|
-
process.exit(0);
|
|
3656
|
-
});
|
|
3657
|
-
while (!done) {
|
|
3658
|
-
const { value, done: streamDone } = await reader.read();
|
|
3659
|
-
if (streamDone) break;
|
|
3660
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
3661
|
-
for (const line of chunk.split("\n")) {
|
|
3662
|
-
if (line.startsWith("data: ")) {
|
|
3663
|
-
const raw = line.slice(6).trim();
|
|
3664
|
-
if (raw === "[DONE]") {
|
|
3665
|
-
console.log("\n[stream ended]");
|
|
3666
|
-
process.exit(0);
|
|
3667
|
-
}
|
|
3668
|
-
try {
|
|
3669
|
-
const event = JSON.parse(raw);
|
|
3670
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3671
|
-
const type = event.type ?? event.event ?? "event";
|
|
3672
|
-
const status = event.status ?? event.data ?? "";
|
|
3673
|
-
console.log(`${ts} ${type.padEnd(24)} ${status}`);
|
|
3674
|
-
} catch {
|
|
3675
|
-
console.log(line);
|
|
3676
|
-
}
|
|
3677
|
-
} else if (line.startsWith("event: ")) {
|
|
3678
|
-
} else if (line && !line.startsWith(":")) {
|
|
3679
|
-
console.log(line);
|
|
3680
|
-
}
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
});
|
|
3684
|
-
fixes.command("merge <fixId>").description(
|
|
3685
|
-
"Squash-merge the fix PR on GitHub and mark the report Fixed (same as console merge)"
|
|
3686
|
-
).option(
|
|
3687
|
-
"--method <method>",
|
|
3688
|
-
"GitHub merge method: squash (default), merge, or rebase",
|
|
3689
|
-
"squash"
|
|
3690
|
-
).option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
|
|
3691
|
-
const config = loadConfig();
|
|
3692
|
-
if (!config.apiKey) {
|
|
3693
|
-
console.error("Run `mushi login` first");
|
|
3694
|
-
process.exit(1);
|
|
3695
|
-
}
|
|
3696
|
-
if (!config.endpoint) {
|
|
3697
|
-
console.error("No endpoint configured. Run `mushi init`");
|
|
3698
|
-
process.exit(1);
|
|
3699
|
-
}
|
|
3700
|
-
const method = opts.method;
|
|
3701
|
-
if (!["squash", "merge", "rebase"].includes(method)) {
|
|
3702
|
-
console.error("--method must be squash, merge, or rebase");
|
|
3703
|
-
process.exit(1);
|
|
3704
|
-
}
|
|
3705
|
-
const result = await apiCall(`/v1/admin/fixes/${fixId}/merge`, config, {
|
|
3706
|
-
method: "POST",
|
|
3707
|
-
body: JSON.stringify({ mergeMethod: method })
|
|
3708
|
-
});
|
|
3709
|
-
if (!result.ok) {
|
|
3710
|
-
if (opts.json) {
|
|
3711
|
-
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
3712
|
-
} else {
|
|
3713
|
-
console.error("Merge failed:", result.error.message);
|
|
3714
|
-
}
|
|
3715
|
-
process.exit(1);
|
|
3716
|
-
}
|
|
3717
|
-
const data = result.data ?? {};
|
|
3718
|
-
if (opts.json) {
|
|
3719
|
-
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
3720
|
-
return;
|
|
3721
|
-
}
|
|
3722
|
-
if (data.alreadyMerged) {
|
|
3723
|
-
console.log(`PR was already merged. Report status: ${data.reportStatus ?? "unknown"}`);
|
|
3724
|
-
} else {
|
|
3725
|
-
console.log(`Merged successfully. Report status: ${data.reportStatus ?? "fixed"}`);
|
|
3726
|
-
}
|
|
3727
|
-
});
|
|
3728
|
-
fixes.command("refresh-ci <fixId>").description("Pull latest GitHub Actions check-run status for a fix attempt (on-demand ci-sync)").option("--json", "Machine-readable JSON output").action(async (fixId, opts) => {
|
|
3729
|
-
const config = loadConfig();
|
|
3730
|
-
if (!config.apiKey) {
|
|
3731
|
-
console.error("Run `mushi login` first");
|
|
3732
|
-
process.exit(1);
|
|
3733
|
-
}
|
|
3734
|
-
if (!config.endpoint) {
|
|
3735
|
-
console.error("No endpoint configured. Run `mushi init`");
|
|
3736
|
-
process.exit(1);
|
|
3737
|
-
}
|
|
3738
|
-
const result = await apiCall(`/v1/admin/fixes/${fixId}/refresh-ci`, config, { method: "POST" });
|
|
3739
|
-
if (!result.ok) {
|
|
3740
|
-
if (opts.json) {
|
|
3741
|
-
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
3742
|
-
} else {
|
|
3743
|
-
console.error("CI refresh failed:", result.error.message);
|
|
3744
|
-
}
|
|
3745
|
-
process.exit(1);
|
|
3746
|
-
}
|
|
3747
|
-
const data = result.data ?? {};
|
|
3748
|
-
if (opts.json) {
|
|
3749
|
-
console.log(JSON.stringify({ ok: true, ...data }, null, 2));
|
|
3750
|
-
return;
|
|
3751
|
-
}
|
|
3752
|
-
const status = data.check_run_status ?? "unknown";
|
|
3753
|
-
const conclusion = data.check_run_conclusion ?? "\u2014";
|
|
3754
|
-
const updated = data.check_run_updated_at ? new Date(data.check_run_updated_at).toISOString() : "\u2014";
|
|
3755
|
-
console.log(`CI status: ${status} \xB7 conclusion: ${conclusion} \xB7 updated: ${updated}`);
|
|
3756
|
-
});
|
|
3757
4134
|
}
|
|
3758
4135
|
|
|
3759
4136
|
// src/commands/tdd.ts
|
|
@@ -4010,7 +4387,7 @@ function registerIntegrationsCommands(program2) {
|
|
|
4010
4387
|
console.log("\nIntegrations:\n");
|
|
4011
4388
|
for (const row of rows) {
|
|
4012
4389
|
const icon = icons[row.kind] ?? "\u{1F50C}";
|
|
4013
|
-
const statusIcon = row.status === "ok" ? "
|
|
4390
|
+
const statusIcon = row.status === "ok" ? "OK" : row.status === "error" ? "FAIL" : "SKIP";
|
|
4014
4391
|
console.log(` ${icon} ${row.kind.padEnd(20)} ${statusIcon} ${row.detail ?? ""}`);
|
|
4015
4392
|
}
|
|
4016
4393
|
console.log();
|
|
@@ -4042,7 +4419,7 @@ function registerIntegrationsCommands(program2) {
|
|
|
4042
4419
|
}
|
|
4043
4420
|
const probeOk = result.data?.status === "ok";
|
|
4044
4421
|
console.log(
|
|
4045
|
-
probeOk ?
|
|
4422
|
+
probeOk ? `OK ${kind} integration is healthy${result.data.detail ? ": " + result.data.detail : ""}` : `FAIL ${kind} integration check failed${result.data.detail ? ": " + result.data.detail : ""}`
|
|
4046
4423
|
);
|
|
4047
4424
|
if (!probeOk) process.exit(1);
|
|
4048
4425
|
});
|
|
@@ -4071,11 +4448,11 @@ function registerIntegrationsCommands(program2) {
|
|
|
4071
4448
|
return;
|
|
4072
4449
|
}
|
|
4073
4450
|
if (result.data?.status === "ok") {
|
|
4074
|
-
console.log("
|
|
4451
|
+
console.log("OK Slack connected");
|
|
4075
4452
|
if (result.data.detail) console.log(` ${result.data.detail}`);
|
|
4076
4453
|
console.log("\n To change the channel or notification prefs, visit /integrations in the Mushi console.");
|
|
4077
4454
|
} else {
|
|
4078
|
-
console.log("
|
|
4455
|
+
console.log("SKIP Slack not connected");
|
|
4079
4456
|
console.log(' Visit /integrations in the Mushi console and click "Add to Slack".');
|
|
4080
4457
|
}
|
|
4081
4458
|
});
|
|
@@ -4103,9 +4480,9 @@ function registerIntegrationsCommands(program2) {
|
|
|
4103
4480
|
return;
|
|
4104
4481
|
}
|
|
4105
4482
|
if (result.data?.ok) {
|
|
4106
|
-
console.log("
|
|
4483
|
+
console.log("OK Test message sent. Check your Slack channel.");
|
|
4107
4484
|
} else {
|
|
4108
|
-
console.error("
|
|
4485
|
+
console.error("FAIL Test failed:", result.data?.error ?? "unknown error");
|
|
4109
4486
|
process.exit(1);
|
|
4110
4487
|
}
|
|
4111
4488
|
});
|
|
@@ -4145,7 +4522,7 @@ function registerQaCommands(program2) {
|
|
|
4145
4522
|
QA Stories (${stories.length}):
|
|
4146
4523
|
`);
|
|
4147
4524
|
for (const s of stories) {
|
|
4148
|
-
const statusIcon = s.last_run_status === "passed" ? "
|
|
4525
|
+
const statusIcon = s.last_run_status === "passed" ? "PASS" : s.last_run_status === "failed" ? "FAIL" : s.last_run_status === "error" ? "ERROR" : "SKIP";
|
|
4149
4526
|
const enabled = s.enabled ? "" : " [disabled]";
|
|
4150
4527
|
const sid = s.story_id ?? s.id ?? "\u2014";
|
|
4151
4528
|
console.log(` ${statusIcon} ${s.name.slice(0, 50).padEnd(52)} ${sid}${enabled}`);
|
|
@@ -4186,7 +4563,7 @@ QA Stories (${stories.length}):
|
|
|
4186
4563
|
Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
4187
4564
|
`);
|
|
4188
4565
|
for (const r of runs) {
|
|
4189
|
-
const statusIcon = r.status === "passed" ? "
|
|
4566
|
+
const statusIcon = r.status === "passed" ? "PASS" : r.status === "failed" ? "FAIL" : r.status === "error" ? "ERROR" : "PEND";
|
|
4190
4567
|
const ts = r.created_at ? new Date(r.created_at).toISOString().slice(0, 16).replace("T", " ") : "\u2014";
|
|
4191
4568
|
const latency = r.latency_ms ? ` (${(r.latency_ms / 1e3).toFixed(1)}s)` : "";
|
|
4192
4569
|
console.log(` ${statusIcon} ${ts}${latency} ${r.id.slice(0, 8)}`);
|
|
@@ -4199,7 +4576,7 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
|
4199
4576
|
}
|
|
4200
4577
|
}
|
|
4201
4578
|
}
|
|
4202
|
-
const consoleUrl2 = config.consoleUrl ??
|
|
4579
|
+
const consoleUrl2 = config.consoleUrl ?? resolveConsoleUrlSync();
|
|
4203
4580
|
console.log(`
|
|
4204
4581
|
Open in console: ${consoleUrl2}/qa-coverage?story=${storyId}`);
|
|
4205
4582
|
console.log(` Tip: run 'mushi config consoleUrl <url>' to override (e.g. http://localhost:6464 for local dev)`);
|
|
@@ -4230,10 +4607,10 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
|
4230
4607
|
}
|
|
4231
4608
|
const runId = result.data?.run_id;
|
|
4232
4609
|
if (runId) {
|
|
4233
|
-
console.log(
|
|
4234
|
-
console.log(`
|
|
4610
|
+
console.log(`RUN Triggered: ${runId.slice(0, 8)}\u2026`);
|
|
4611
|
+
console.log(` Check results: mushi qa runs ${storyId}`);
|
|
4235
4612
|
} else {
|
|
4236
|
-
console.error("
|
|
4613
|
+
console.error("FAIL Trigger failed: no run_id in response", JSON.stringify(result.data));
|
|
4237
4614
|
process.exit(1);
|
|
4238
4615
|
}
|
|
4239
4616
|
});
|
|
@@ -4250,10 +4627,10 @@ Description:
|
|
|
4250
4627
|
\u2022 Gate results: API contract (G3), spec drift (G6), orphan endpoints (G7),
|
|
4251
4628
|
unknown frontend calls (G8), schema drift, status claim (G5)
|
|
4252
4629
|
|
|
4253
|
-
Returns a
|
|
4630
|
+
Returns a human-readable summary with severity-ranked findings.
|
|
4254
4631
|
|
|
4255
4632
|
Prerequisites:
|
|
4256
|
-
1.
|
|
4633
|
+
1. Add your Supabase PAT in Admin \u2192 Settings \u2192 API Keys
|
|
4257
4634
|
2. Set supabase_project_ref in Admin \u2192 Settings \u2192 Project.
|
|
4258
4635
|
|
|
4259
4636
|
Examples:
|
|
@@ -4313,8 +4690,8 @@ Examples:
|
|
|
4313
4690
|
return;
|
|
4314
4691
|
}
|
|
4315
4692
|
const data = body.data;
|
|
4316
|
-
const
|
|
4317
|
-
process.stdout.write(`${
|
|
4693
|
+
const overallLabel = data.summary.overall === "fail" ? "FAIL" : data.summary.overall === "warn" ? "WARN" : "OK";
|
|
4694
|
+
process.stdout.write(`${overallLabel}
|
|
4318
4695
|
|
|
4319
4696
|
`);
|
|
4320
4697
|
console.log(`Full-Stack Audit \u2014 ${new Date(data.audit_at).toLocaleString()}`);
|
|
@@ -4322,18 +4699,18 @@ Examples:
|
|
|
4322
4699
|
console.log(`Summary: ${data.summary.error_count} error(s) \xB7 ${data.summary.warn_count} warning(s)
|
|
4323
4700
|
`);
|
|
4324
4701
|
if (data.findings.length === 0) {
|
|
4325
|
-
console.log("
|
|
4702
|
+
console.log(" OK No findings. Your project looks healthy.");
|
|
4326
4703
|
} else {
|
|
4327
4704
|
for (const f of data.findings) {
|
|
4328
|
-
const icon = f.severity === "error" ? "
|
|
4329
|
-
console.log(` ${icon}
|
|
4705
|
+
const icon = f.severity === "error" ? "FAIL" : f.severity === "warn" ? "WARN" : "INFO";
|
|
4706
|
+
console.log(` ${icon} ${f.title}`);
|
|
4330
4707
|
console.log(` ${f.detail.slice(0, 120)}${f.detail.length > 120 ? "\u2026" : ""}`);
|
|
4331
4708
|
}
|
|
4332
4709
|
}
|
|
4333
4710
|
if (data.gate_runs.length > 0) {
|
|
4334
4711
|
console.log("\nGate Results:");
|
|
4335
4712
|
for (const run of data.gate_runs) {
|
|
4336
|
-
const g = run.status === "pass" ? "
|
|
4713
|
+
const g = run.status === "pass" ? "OK" : run.status === "fail" ? "FAIL" : "SKIP";
|
|
4337
4714
|
console.log(` ${g} ${run.gate.padEnd(22)} ${run.status} (${run.findings_count} finding${run.findings_count !== 1 ? "s" : ""})`);
|
|
4338
4715
|
}
|
|
4339
4716
|
}
|
|
@@ -4641,9 +5018,100 @@ ${"\u2500".repeat(60)}`);
|
|
|
4641
5018
|
});
|
|
4642
5019
|
}
|
|
4643
5020
|
|
|
5021
|
+
// src/commands/billing.ts
|
|
5022
|
+
function registerBillingCommands(program2) {
|
|
5023
|
+
program2.command("usage").description("Show diagnoses used / limit / cap for the current billing period").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
5024
|
+
Examples:
|
|
5025
|
+
mushi usage
|
|
5026
|
+
mushi usage --json`).action(async (opts) => {
|
|
5027
|
+
const config = requireConfig();
|
|
5028
|
+
const result = await apiCall("/v1/admin/billing/stats", config);
|
|
5029
|
+
if (!result.ok) die(result);
|
|
5030
|
+
const s = result.data;
|
|
5031
|
+
if (opts.json) {
|
|
5032
|
+
console.log(JSON.stringify(s, null, 2));
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
const limitStr = s.diagnosesLimit != null ? String(s.diagnosesLimit) : "unlimited";
|
|
5036
|
+
const pctStr = s.diagnosesUsagePct != null ? `${Math.round(s.diagnosesUsagePct)}%` : "\u2014";
|
|
5037
|
+
const capStr = s.monthlySpendCapUsd != null ? `$${s.monthlySpendCapUsd}/mo` : "none";
|
|
5038
|
+
const badge = s.overDiagnosisQuota ? " \u26A0 OVER LIMIT" : s.approachingDiagnosisQuota ? " \u26A0 approaching limit" : "";
|
|
5039
|
+
console.log(`
|
|
5040
|
+
Diagnoses this period: ${s.diagnosesUsed} / ${limitStr} (${pctStr})${badge}`);
|
|
5041
|
+
console.log(`Plan: ${s.planId ?? "\u2014"}`);
|
|
5042
|
+
console.log(`Spend cap: ${capStr}`);
|
|
5043
|
+
console.log(`Period ends: ${s.periodEnd ? s.periodEnd.slice(0, 10) : "?"}`);
|
|
5044
|
+
if (s.overDiagnosisQuota) {
|
|
5045
|
+
console.log("\n Diagnoses paused \u2014 upgrade at https://kensaur.us/mushi-mushi/admin/billing");
|
|
5046
|
+
} else if (s.approachingDiagnosisQuota) {
|
|
5047
|
+
console.log("\n You're at 80%+ of your included diagnoses. Consider upgrading.");
|
|
5048
|
+
}
|
|
5049
|
+
});
|
|
5050
|
+
const billing = program2.command("billing").description("Show billing summary and manage spend cap");
|
|
5051
|
+
billing.command("status").description("Show full billing summary (plan, usage, cap)").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
5052
|
+
Examples:
|
|
5053
|
+
mushi billing status
|
|
5054
|
+
mushi billing status --json`).action(async (opts) => {
|
|
5055
|
+
const config = requireConfig();
|
|
5056
|
+
const result = await apiCall("/v1/admin/billing", config);
|
|
5057
|
+
if (!result.ok) die(result);
|
|
5058
|
+
const s = result.data;
|
|
5059
|
+
if (opts.json) {
|
|
5060
|
+
console.log(JSON.stringify(s, null, 2));
|
|
5061
|
+
return;
|
|
5062
|
+
}
|
|
5063
|
+
const limitStr = s.diagnosesLimit != null ? String(s.diagnosesLimit) : "unlimited";
|
|
5064
|
+
const pctStr = s.diagnosesUsagePct != null ? `${Math.round(s.diagnosesUsagePct)}%` : "\u2014";
|
|
5065
|
+
const capStr = s.monthlySpendCapUsd != null ? `$${s.monthlySpendCapUsd}/mo` : "none";
|
|
5066
|
+
const overageStr = s.overageRateDiagnoses != null ? `$${s.overageRateDiagnoses}/diagnosis` : "N/A";
|
|
5067
|
+
console.log(`
|
|
5068
|
+
Plan: ${s.planId ?? "\u2014"}`);
|
|
5069
|
+
console.log(`Status: ${s.subscriptionStatus ?? "free"}${s.cancelAtPeriodEnd ? " (cancels at period end)" : ""}`);
|
|
5070
|
+
console.log(`Diagnoses: ${s.diagnosesUsed} / ${limitStr} (${pctStr})`);
|
|
5071
|
+
console.log(`Overage: ${overageStr}`);
|
|
5072
|
+
console.log(`Spend cap: ${capStr}`);
|
|
5073
|
+
console.log(`Renews: ${s.periodEnd ? s.periodEnd.slice(0, 10) : "?"}`);
|
|
5074
|
+
});
|
|
5075
|
+
billing.command("cap [usd]").description("Set or clear the monthly spend cap (pass 0 to clear, omit to show current cap)").addHelpText("after", `
|
|
5076
|
+
Examples:
|
|
5077
|
+
mushi billing cap # show current cap
|
|
5078
|
+
mushi billing cap 100 # set $100/mo hard cap
|
|
5079
|
+
mushi billing cap 0 # clear cap`).action(async (usdArg) => {
|
|
5080
|
+
const config = requireConfig();
|
|
5081
|
+
if (usdArg === void 0) {
|
|
5082
|
+
const result2 = await apiCall("/v1/admin/billing/stats", config);
|
|
5083
|
+
if (!result2.ok) die(result2);
|
|
5084
|
+
const cap2 = result2.data.monthlySpendCapUsd;
|
|
5085
|
+
if (cap2 == null) {
|
|
5086
|
+
console.log("No spend cap set. Pass a dollar amount to set one, e.g.: mushi billing cap 100");
|
|
5087
|
+
} else {
|
|
5088
|
+
console.log(`Current spend cap: $${cap2}/mo`);
|
|
5089
|
+
}
|
|
5090
|
+
return;
|
|
5091
|
+
}
|
|
5092
|
+
const capUsd = parseFloat(usdArg);
|
|
5093
|
+
if (isNaN(capUsd) || capUsd < 0) {
|
|
5094
|
+
console.error("Error: spend cap must be a non-negative number (or 0 to clear).");
|
|
5095
|
+
process.exit(2);
|
|
5096
|
+
}
|
|
5097
|
+
const result = await apiCall("/v1/admin/billing/spend-cap", config, {
|
|
5098
|
+
method: "PUT",
|
|
5099
|
+
headers: { "Content-Type": "application/json" },
|
|
5100
|
+
body: JSON.stringify({ project_id: config.projectId, spend_cap_usd: capUsd === 0 ? null : capUsd })
|
|
5101
|
+
});
|
|
5102
|
+
if (!result.ok) die(result);
|
|
5103
|
+
const cap = result.data.spend_cap_usd;
|
|
5104
|
+
if (cap == null) {
|
|
5105
|
+
console.log("\u2713 Spend cap cleared \u2014 no hard monthly limit.");
|
|
5106
|
+
} else {
|
|
5107
|
+
console.log(`\u2713 Spend cap set to $${cap}/mo. Diagnoses will pause gracefully once reached.`);
|
|
5108
|
+
}
|
|
5109
|
+
});
|
|
5110
|
+
}
|
|
5111
|
+
|
|
4644
5112
|
// src/index.ts
|
|
4645
5113
|
installSignalHandlers();
|
|
4646
|
-
var program = new Command().name("mushi").description("Mushi
|
|
5114
|
+
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).addHelpText("after", `
|
|
4647
5115
|
Environment variables:
|
|
4648
5116
|
MUSHI_API_KEY SDK ingest key (report:write scope \u2014 from Onboarding \u2192 Verify in the console)
|
|
4649
5117
|
MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
|
|
@@ -4666,6 +5134,11 @@ registerLessonsCommands(program);
|
|
|
4666
5134
|
registerDiagnosticsCommands(program);
|
|
4667
5135
|
registerProjectCommands(program);
|
|
4668
5136
|
registerSetupCommands(program);
|
|
5137
|
+
registerNudgeCommand(program);
|
|
5138
|
+
registerUpgradeCommand(program);
|
|
5139
|
+
registerConnectCommand(program);
|
|
5140
|
+
registerDoctorCommand(program);
|
|
5141
|
+
registerResetCommand(program);
|
|
4669
5142
|
registerFixCommands(program);
|
|
4670
5143
|
registerTddCommands(program);
|
|
4671
5144
|
registerKeysCommands(program);
|
|
@@ -4673,6 +5146,7 @@ registerIntegrationsCommands(program);
|
|
|
4673
5146
|
registerQaCommands(program);
|
|
4674
5147
|
registerAuditCommands(program);
|
|
4675
5148
|
registerSkillsCommands(program);
|
|
5149
|
+
registerBillingCommands(program);
|
|
4676
5150
|
program.parseAsync().catch((err) => {
|
|
4677
5151
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
4678
5152
|
process.exit(1);
|