@mushi-mushi/cli 0.20.0 → 0.21.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/SECURITY.md +1 -1
- package/dist/{chunk-7Q44KWGN.js → chunk-JAUKBLPK.js} +15 -9
- package/dist/chunk-L7KBJ2XY.js +6 -0
- 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 +498 -87
- package/dist/init.js +105 -28
- 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,
|
|
@@ -110,13 +110,13 @@ function getAbortSignal(external) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// src/version.ts
|
|
113
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
113
|
+
var MUSHI_CLI_VERSION = true ? "0.21.0" : "0.0.0-dev";
|
|
114
114
|
|
|
115
115
|
// src/init.ts
|
|
116
116
|
import * as p from "@clack/prompts";
|
|
117
117
|
import { spawn } from "child_process";
|
|
118
118
|
import { randomUUID } from "crypto";
|
|
119
|
-
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
119
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
120
120
|
import { join as join3 } from "path";
|
|
121
121
|
|
|
122
122
|
// src/console-url.ts
|
|
@@ -292,13 +292,13 @@ async function apiCall(path, config, options = {}) {
|
|
|
292
292
|
}
|
|
293
293
|
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
294
294
|
const b = body;
|
|
295
|
+
const nested = b["error"];
|
|
296
|
+
const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
|
|
297
|
+
const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
|
|
295
298
|
return {
|
|
296
299
|
ok: false,
|
|
297
300
|
httpStatus: res.status,
|
|
298
|
-
error: {
|
|
299
|
-
code: b["code"] ?? `HTTP_${res.status}`,
|
|
300
|
-
message: b["message"] ?? `Request failed (${res.status})`
|
|
301
|
-
}
|
|
301
|
+
error: { code: errCode, message: errMsg }
|
|
302
302
|
};
|
|
303
303
|
}
|
|
304
304
|
return body;
|
|
@@ -327,6 +327,11 @@ function die(result, exitCode = 1) {
|
|
|
327
327
|
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
328
328
|
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
329
329
|
`);
|
|
330
|
+
if (code === "INSUFFICIENT_SCOPE") {
|
|
331
|
+
process.stderr.write(
|
|
332
|
+
"\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"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
330
335
|
process.exit(exitCode);
|
|
331
336
|
}
|
|
332
337
|
function requireConfig(opts = {}) {
|
|
@@ -424,8 +429,12 @@ async function waitForCliToken(endpoint, session, opts = {}) {
|
|
|
424
429
|
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
425
430
|
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
426
431
|
let consecutiveErrors = 0;
|
|
432
|
+
let firstPoll = true;
|
|
427
433
|
while (now() < deadline) {
|
|
428
|
-
|
|
434
|
+
if (!firstPoll) {
|
|
435
|
+
await sleep(intervalMs);
|
|
436
|
+
}
|
|
437
|
+
firstPoll = false;
|
|
429
438
|
const outcome = await pollDeviceToken(endpoint, session.device_code);
|
|
430
439
|
switch (outcome.status) {
|
|
431
440
|
case "approved":
|
|
@@ -653,6 +662,53 @@ function getFrameworkFromPkg(pkg) {
|
|
|
653
662
|
return void 0;
|
|
654
663
|
}
|
|
655
664
|
|
|
665
|
+
// src/auth-ui.ts
|
|
666
|
+
var LINE = "\u2500".repeat(58);
|
|
667
|
+
function printAuthBanner(userCode, verificationUri) {
|
|
668
|
+
console.log("");
|
|
669
|
+
console.log(` \u250C${LINE}\u2510`);
|
|
670
|
+
console.log(` \u2502 Browser sign-in \u2014 follow these 3 steps \u2502`);
|
|
671
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
672
|
+
console.log(` \u2502 \u2502`);
|
|
673
|
+
console.log(` \u2502 1. A browser tab will open (or visit the URL below) \u2502`);
|
|
674
|
+
console.log(` \u2502 2. Sign in if prompted, then click "Approve" \u2502`);
|
|
675
|
+
console.log(` \u2502 3. Come back here \u2014 setup continues automatically \u2502`);
|
|
676
|
+
console.log(` \u2502 \u2502`);
|
|
677
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
678
|
+
console.log(` \u2502 Verification code (show in browser \u2014 NOT terminal): \u2502`);
|
|
679
|
+
console.log(` \u2502 \u2502`);
|
|
680
|
+
console.log(` \u2502 ${userCode.padEnd(52)}\u2502`);
|
|
681
|
+
console.log(` \u2502 \u2502`);
|
|
682
|
+
console.log(` \u2502 \u26A0 Do NOT paste or type this code in your terminal \u2502`);
|
|
683
|
+
console.log(` \u2514${LINE}\u2518`);
|
|
684
|
+
console.log("");
|
|
685
|
+
console.log(` If the browser didn't open, visit this URL and approve:`);
|
|
686
|
+
console.log(` ${verificationUri}`);
|
|
687
|
+
console.log("");
|
|
688
|
+
console.log(" \u23F3 Waiting for browser approval\u2026 (Ctrl+C to cancel)");
|
|
689
|
+
console.log("");
|
|
690
|
+
}
|
|
691
|
+
function printAuthApproved() {
|
|
692
|
+
console.log("");
|
|
693
|
+
console.log(" \u2713 Approved! Continuing setup in this terminal\u2026");
|
|
694
|
+
console.log("");
|
|
695
|
+
}
|
|
696
|
+
function printAuthFailed(reason, detail) {
|
|
697
|
+
console.log("");
|
|
698
|
+
if (reason === "denied") {
|
|
699
|
+
console.error(" \u2717 Browser sign-in was denied.");
|
|
700
|
+
console.error(" If that was a mistake, run mushi login again.");
|
|
701
|
+
} else if (reason === "timeout") {
|
|
702
|
+
console.error(" \u2717 Browser sign-in timed out (10 minutes elapsed).");
|
|
703
|
+
console.error(" Run mushi login again \u2014 a new code will be generated.");
|
|
704
|
+
} else {
|
|
705
|
+
console.error(` \u2717 Browser sign-in failed: ${detail ?? "unknown error"}`);
|
|
706
|
+
console.error(" Run mushi login again, or use:");
|
|
707
|
+
console.error(" mushi login --api-key <key> --project-id <uuid>");
|
|
708
|
+
}
|
|
709
|
+
console.log("");
|
|
710
|
+
}
|
|
711
|
+
|
|
656
712
|
// src/init.ts
|
|
657
713
|
var ENV_FILES = [".env.local", ".env"];
|
|
658
714
|
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;
|
|
@@ -688,7 +744,7 @@ async function runInit(options = {}) {
|
|
|
688
744
|
} else {
|
|
689
745
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
690
746
|
}
|
|
691
|
-
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
747
|
+
await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
|
|
692
748
|
persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
|
|
693
749
|
const enableRewards = await maybeEnableRewards(options);
|
|
694
750
|
printNextSteps(framework, consoleBase, enableRewards);
|
|
@@ -759,7 +815,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
759
815
|
}
|
|
760
816
|
}
|
|
761
817
|
if (options.yes) {
|
|
762
|
-
const creds = await runBrowserSignIn(options, endpoint);
|
|
818
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
763
819
|
if (creds) return creds;
|
|
764
820
|
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
765
821
|
} else {
|
|
@@ -775,7 +831,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
775
831
|
{
|
|
776
832
|
value: "manual",
|
|
777
833
|
label: "Paste a Project ID + API key",
|
|
778
|
-
hint: "Self-hosted or expert setup"
|
|
834
|
+
hint: "Self-hosted or expert setup \u2014 run npx mushi-mushi (not mushi setup)"
|
|
779
835
|
}
|
|
780
836
|
]
|
|
781
837
|
});
|
|
@@ -784,14 +840,14 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
784
840
|
process.exit(0);
|
|
785
841
|
}
|
|
786
842
|
if (method === "browser") {
|
|
787
|
-
const creds = await runBrowserSignIn(options, endpoint);
|
|
843
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
788
844
|
if (creds) return creds;
|
|
789
845
|
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
790
846
|
}
|
|
791
847
|
}
|
|
792
848
|
return collectCredentialsManually(options, consoleBase);
|
|
793
849
|
}
|
|
794
|
-
async function runBrowserSignIn(options, endpoint) {
|
|
850
|
+
async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
795
851
|
const startSpin = p.spinner();
|
|
796
852
|
startSpin.start("Starting secure browser sign-in\u2026");
|
|
797
853
|
let session;
|
|
@@ -803,13 +859,11 @@ async function runBrowserSignIn(options, endpoint) {
|
|
|
803
859
|
return null;
|
|
804
860
|
}
|
|
805
861
|
startSpin.stop("Browser sign-in ready.");
|
|
806
|
-
p.log.step(`Confirmation code: ${session.user_code}`);
|
|
807
|
-
p.log.info(`Opening ${session.verification_uri}`);
|
|
808
862
|
try {
|
|
809
863
|
await openInBrowser(session.verification_uri);
|
|
810
864
|
} catch {
|
|
811
865
|
}
|
|
812
|
-
|
|
866
|
+
printAuthBanner(session.user_code, session.verification_uri);
|
|
813
867
|
const waitSpin = p.spinner();
|
|
814
868
|
waitSpin.start("Waiting for you to approve in the browser\u2026");
|
|
815
869
|
let cliToken;
|
|
@@ -825,9 +879,14 @@ async function runBrowserSignIn(options, endpoint) {
|
|
|
825
879
|
let apiKey;
|
|
826
880
|
if (!projectId) {
|
|
827
881
|
let projects = [];
|
|
882
|
+
const fetchSpin = p.spinner();
|
|
883
|
+
fetchSpin.start("Loading your projects\u2026");
|
|
828
884
|
try {
|
|
829
885
|
projects = await listProjects(endpoint, cliToken);
|
|
830
|
-
|
|
886
|
+
fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
|
|
887
|
+
} catch (err) {
|
|
888
|
+
fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
|
|
889
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
831
890
|
}
|
|
832
891
|
const NEW = "__new__";
|
|
833
892
|
const choice = await p.select({
|
|
@@ -869,7 +928,24 @@ async function runBrowserSignIn(options, endpoint) {
|
|
|
869
928
|
}
|
|
870
929
|
}
|
|
871
930
|
if (projectId && !apiKey) {
|
|
872
|
-
|
|
931
|
+
const keySpin = p.spinner();
|
|
932
|
+
keySpin.start("Minting SDK key\u2026");
|
|
933
|
+
try {
|
|
934
|
+
const minted = await mintProjectKey(endpoint, cliToken, projectId);
|
|
935
|
+
apiKey = minted ?? void 0;
|
|
936
|
+
if (!apiKey) {
|
|
937
|
+
keySpin.stop("Could not mint an API key.");
|
|
938
|
+
p.log.warn(
|
|
939
|
+
`Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
940
|
+
);
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
keySpin.stop("SDK key ready.");
|
|
944
|
+
} catch (err) {
|
|
945
|
+
keySpin.stop("Could not mint an API key.");
|
|
946
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
873
949
|
}
|
|
874
950
|
if (!projectId || !apiKey) return null;
|
|
875
951
|
return { apiKey, projectId };
|
|
@@ -975,13 +1051,37 @@ function runCommand(pm, packages, cwd) {
|
|
|
975
1051
|
});
|
|
976
1052
|
});
|
|
977
1053
|
}
|
|
978
|
-
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
1054
|
+
async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
|
|
979
1055
|
const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
|
|
980
1056
|
const targetPath = join3(cwd, target);
|
|
981
|
-
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
1057
|
+
const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
|
|
982
1058
|
const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
|
|
983
1059
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
984
|
-
|
|
1060
|
+
let shouldOverwrite = overwrite;
|
|
1061
|
+
if (!shouldOverwrite) {
|
|
1062
|
+
const answer = await p.confirm({
|
|
1063
|
+
message: `Existing MUSHI_* vars found in ${target}. Update with new credentials?`,
|
|
1064
|
+
initialValue: true
|
|
1065
|
+
});
|
|
1066
|
+
if (p.isCancel(answer)) {
|
|
1067
|
+
p.log.info(`Kept existing env vars in ${target}.`);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
shouldOverwrite = Boolean(answer);
|
|
1071
|
+
}
|
|
1072
|
+
if (!shouldOverwrite) {
|
|
1073
|
+
p.log.info(`Kept existing env vars in ${target}. Re-run and confirm to overwrite.`);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const MUSHI_LINE_RE = /^(NEXT_PUBLIC_|NUXT_PUBLIC_|VITE_|EXPO_PUBLIC_)?MUSHI_[A-Z_]+=.*/gm;
|
|
1077
|
+
const stripped = existing.replace(MUSHI_LINE_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1078
|
+
const prefix2 = stripped.length > 0 ? "\n" : "";
|
|
1079
|
+
writeFileSync(targetPath, `${stripped}${prefix2}
|
|
1080
|
+
# Mushi Mushi
|
|
1081
|
+
${newVars}
|
|
1082
|
+
`);
|
|
1083
|
+
p.log.success(`Updated MUSHI_* env vars in ${target}`);
|
|
1084
|
+
warnIfMissingFromGitignore(cwd, target);
|
|
985
1085
|
return;
|
|
986
1086
|
}
|
|
987
1087
|
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
@@ -1042,12 +1142,14 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
|
1042
1142
|
p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
|
|
1043
1143
|
p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
|
|
1044
1144
|
}
|
|
1045
|
-
p.
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1145
|
+
p.note(
|
|
1146
|
+
[
|
|
1147
|
+
" [ ] 1. Paste the init snippet above into your app entry file",
|
|
1148
|
+
" [ ] 2. Start your dev server",
|
|
1149
|
+
" [ ] 3. Run: mushi connect --write-env --wire-ide --wait",
|
|
1150
|
+
` [ ] 4. Open the Verify tab to send a test report: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
1151
|
+
].join("\n"),
|
|
1152
|
+
"Next steps:"
|
|
1051
1153
|
);
|
|
1052
1154
|
}
|
|
1053
1155
|
async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
@@ -1059,7 +1161,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
1059
1161
|
if (p.isCancel(answer) || !answer) return;
|
|
1060
1162
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1061
1163
|
try {
|
|
1062
|
-
const { runConnect: runConnect2 } = await import("./connect-
|
|
1164
|
+
const { runConnect: runConnect2 } = await import("./connect-JIEIWTF3.js");
|
|
1063
1165
|
await runConnect2({
|
|
1064
1166
|
apiKey: credentials.apiKey,
|
|
1065
1167
|
projectId: credentials.projectId,
|
|
@@ -1344,11 +1446,15 @@ function registerAccountCommands(program2) {
|
|
|
1344
1446
|
const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
|
|
1345
1447
|
if (matches.length === 0) process.exit(1);
|
|
1346
1448
|
});
|
|
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").
|
|
1449
|
+
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(
|
|
1450
|
+
"--upgrade-scope",
|
|
1451
|
+
"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."
|
|
1452
|
+
).addHelpText("after", `
|
|
1348
1453
|
Examples:
|
|
1349
1454
|
mushi login # browser-guided (recommended)
|
|
1350
1455
|
mushi login --api-key mushi_xxx # non-interactive / CI
|
|
1351
|
-
mushi login --api-key mushi_xxx --project-id <uuid
|
|
1456
|
+
mushi login --api-key mushi_xxx --project-id <uuid>
|
|
1457
|
+
mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
|
|
1352
1458
|
const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
|
|
1353
1459
|
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
|
|
1354
1460
|
const consoleBase = await resolveConsoleUrl();
|
|
@@ -1386,8 +1492,16 @@ Examples:
|
|
|
1386
1492
|
return;
|
|
1387
1493
|
}
|
|
1388
1494
|
console.log("");
|
|
1389
|
-
|
|
1390
|
-
|
|
1495
|
+
if (opts.upgradeScope) {
|
|
1496
|
+
console.log(" Mushi login \u2014 scope upgrade");
|
|
1497
|
+
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");
|
|
1498
|
+
console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
|
|
1499
|
+
console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
|
|
1500
|
+
console.log("");
|
|
1501
|
+
} else {
|
|
1502
|
+
console.log(" Mushi login");
|
|
1503
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1504
|
+
}
|
|
1391
1505
|
let deviceData;
|
|
1392
1506
|
try {
|
|
1393
1507
|
deviceData = await startDeviceAuth(endpoint);
|
|
@@ -1400,26 +1514,24 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
|
|
|
1400
1514
|
process.exit(1);
|
|
1401
1515
|
}
|
|
1402
1516
|
const verifyUrl = deviceData.verification_uri;
|
|
1403
|
-
console.log("");
|
|
1404
|
-
console.log(` Your confirmation code: ${deviceData.user_code}`);
|
|
1405
|
-
console.log("");
|
|
1406
1517
|
if (opts.browser !== false) {
|
|
1407
|
-
console.log(" Opening the Mushi console in your browser\u2026");
|
|
1408
1518
|
try {
|
|
1409
1519
|
await openInBrowser(verifyUrl);
|
|
1410
1520
|
} catch {
|
|
1411
1521
|
}
|
|
1412
1522
|
}
|
|
1413
|
-
|
|
1414
|
-
console.log("");
|
|
1415
|
-
console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
|
|
1523
|
+
printAuthBanner(deviceData.user_code, verifyUrl);
|
|
1416
1524
|
let cliToken = null;
|
|
1417
1525
|
const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
|
|
1418
1526
|
const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
|
|
1419
1527
|
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
1420
1528
|
let consecutiveErrors = 0;
|
|
1529
|
+
let firstPoll = true;
|
|
1421
1530
|
while (Date.now() < deadline) {
|
|
1422
|
-
|
|
1531
|
+
if (!firstPoll) {
|
|
1532
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1533
|
+
}
|
|
1534
|
+
firstPoll = false;
|
|
1423
1535
|
const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
|
|
1424
1536
|
if (outcome.status === "approved") {
|
|
1425
1537
|
cliToken = outcome.cliToken;
|
|
@@ -1427,38 +1539,30 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
|
|
|
1427
1539
|
}
|
|
1428
1540
|
if (outcome.status === "pending") {
|
|
1429
1541
|
consecutiveErrors = 0;
|
|
1430
|
-
process.stdout.write(".");
|
|
1431
1542
|
continue;
|
|
1432
1543
|
}
|
|
1433
1544
|
if (outcome.status === "error") {
|
|
1434
1545
|
if (outcome.retryable) {
|
|
1435
1546
|
consecutiveErrors += 1;
|
|
1436
|
-
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS)
|
|
1437
|
-
process.stdout.write("\xB7");
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1547
|
+
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
|
|
1440
1548
|
}
|
|
1441
|
-
|
|
1442
|
-
process.stderr.write(`
|
|
1443
|
-
error: ${outcome.message}
|
|
1444
|
-
`);
|
|
1549
|
+
printAuthFailed("error", outcome.message);
|
|
1445
1550
|
process.exit(1);
|
|
1446
1551
|
}
|
|
1447
|
-
console.log("");
|
|
1448
1552
|
if (outcome.status === "denied") {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1553
|
+
printAuthFailed("denied");
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
if (outcome.status === "expired") {
|
|
1557
|
+
printAuthFailed("timeout");
|
|
1558
|
+
process.exit(1);
|
|
1452
1559
|
}
|
|
1453
|
-
process.exit(1);
|
|
1454
1560
|
}
|
|
1455
1561
|
if (!cliToken) {
|
|
1456
|
-
|
|
1457
|
-
process.stderr.write("\nerror: Login timed out. Run mushi login again.\n");
|
|
1562
|
+
printAuthFailed("timeout");
|
|
1458
1563
|
process.exit(1);
|
|
1459
1564
|
}
|
|
1460
|
-
|
|
1461
|
-
console.log(" \u2713 Approved!");
|
|
1565
|
+
printAuthApproved();
|
|
1462
1566
|
const projectsList = await listProjects(endpoint, cliToken);
|
|
1463
1567
|
let chosenProjectId = projectId;
|
|
1464
1568
|
let chosenProjectName;
|
|
@@ -1521,12 +1625,17 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
1521
1625
|
console.log("");
|
|
1522
1626
|
if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
|
|
1523
1627
|
if (apiKey) {
|
|
1524
|
-
console.log(` \u2713
|
|
1628
|
+
console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
|
|
1629
|
+
console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
|
|
1525
1630
|
} else {
|
|
1526
1631
|
console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
|
|
1527
1632
|
}
|
|
1528
1633
|
console.log("");
|
|
1529
|
-
|
|
1634
|
+
if (opts.upgradeScope) {
|
|
1635
|
+
console.log(" Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.");
|
|
1636
|
+
} else {
|
|
1637
|
+
console.log(" Run 'mushi init' to set up the SDK in this project.");
|
|
1638
|
+
}
|
|
1530
1639
|
});
|
|
1531
1640
|
program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1532
1641
|
Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
|
|
@@ -2382,33 +2491,24 @@ error: Could not start browser sign-in: ${err instanceof Error ? err.message : S
|
|
|
2382
2491
|
process.stderr.write(" Fallback: mushi login --api-key <key> --project-id <uuid>\n");
|
|
2383
2492
|
process.exit(1);
|
|
2384
2493
|
}
|
|
2385
|
-
console.log(` Confirmation code: ${session.user_code}`);
|
|
2386
|
-
console.log("");
|
|
2387
2494
|
if (opts.browser !== false) {
|
|
2388
|
-
console.log(" Opening the Mushi console in your browser\u2026");
|
|
2389
2495
|
try {
|
|
2390
2496
|
await openInBrowser(session.verification_uri);
|
|
2391
2497
|
} catch {
|
|
2392
2498
|
}
|
|
2393
2499
|
}
|
|
2394
|
-
|
|
2395
|
-
console.log("");
|
|
2396
|
-
console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
|
|
2500
|
+
printAuthBanner(session.user_code, session.verification_uri);
|
|
2397
2501
|
let cliToken;
|
|
2398
2502
|
try {
|
|
2399
|
-
cliToken = await waitForCliToken(endpoint, session
|
|
2400
|
-
onPending: () => process.stdout.write("."),
|
|
2401
|
-
onTransientError: () => process.stdout.write("\xB7")
|
|
2402
|
-
});
|
|
2503
|
+
cliToken = await waitForCliToken(endpoint, session);
|
|
2403
2504
|
} catch (err) {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2505
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2506
|
+
if (msg.includes("denied")) printAuthFailed("denied");
|
|
2507
|
+
else if (msg.includes("timed out") || msg.includes("expired")) printAuthFailed("timeout");
|
|
2508
|
+
else printAuthFailed("error", msg);
|
|
2408
2509
|
process.exit(1);
|
|
2409
2510
|
}
|
|
2410
|
-
|
|
2411
|
-
console.log(" \u2713 Approved!");
|
|
2511
|
+
printAuthApproved();
|
|
2412
2512
|
const projectsList = await listProjects(endpoint, cliToken);
|
|
2413
2513
|
const { createInterface } = await import("readline");
|
|
2414
2514
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -2495,7 +2595,7 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
2495
2595
|
|
|
2496
2596
|
// src/commands/setup.ts
|
|
2497
2597
|
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", `
|
|
2598
|
+
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
2599
|
Examples:
|
|
2500
2600
|
mushi setup # wire Cursor (default)
|
|
2501
2601
|
mushi setup --all-projects # one server entry per accessible project
|
|
@@ -2699,6 +2799,54 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2699
2799
|
}
|
|
2700
2800
|
}
|
|
2701
2801
|
if (!opts.dryRun) {
|
|
2802
|
+
const shouldVerify = opts.verify !== false;
|
|
2803
|
+
if (shouldVerify && config.endpoint && config.apiKey && config.projectId) {
|
|
2804
|
+
try {
|
|
2805
|
+
const probeRes = await fetch(
|
|
2806
|
+
`${config.endpoint.replace(/\/$/, "")}/v1/admin/mcp/account-overview`,
|
|
2807
|
+
{
|
|
2808
|
+
headers: {
|
|
2809
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
2810
|
+
"X-Mushi-Project": config.projectId
|
|
2811
|
+
},
|
|
2812
|
+
signal: AbortSignal.timeout(6e3)
|
|
2813
|
+
}
|
|
2814
|
+
);
|
|
2815
|
+
if (probeRes.ok) {
|
|
2816
|
+
console.log("\u2713 MCP key valid \u2014 restart Cursor (or your IDE) to activate");
|
|
2817
|
+
void fetch(
|
|
2818
|
+
`${config.endpoint.replace(/\/$/, "")}/v1/cli/funnel`,
|
|
2819
|
+
{
|
|
2820
|
+
method: "POST",
|
|
2821
|
+
headers: {
|
|
2822
|
+
"Content-Type": "application/json",
|
|
2823
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
2824
|
+
"X-Mushi-Project": config.projectId
|
|
2825
|
+
},
|
|
2826
|
+
body: JSON.stringify({ event: "mcp_setup_done", source: "cli" }),
|
|
2827
|
+
signal: AbortSignal.timeout(4e3)
|
|
2828
|
+
}
|
|
2829
|
+
).catch(() => {
|
|
2830
|
+
});
|
|
2831
|
+
} else if (probeRes.status === 403) {
|
|
2832
|
+
let errCode;
|
|
2833
|
+
try {
|
|
2834
|
+
const b = await probeRes.json();
|
|
2835
|
+
errCode = b?.error?.code;
|
|
2836
|
+
} catch {
|
|
2837
|
+
}
|
|
2838
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
2839
|
+
console.log("\n\u26A0 Your key has report:write scope only \u2014 MCP admin tools will not work.");
|
|
2840
|
+
console.log(" New keys minted by the wizard include both scopes automatically.");
|
|
2841
|
+
console.log(" To upgrade an existing key, run:\n");
|
|
2842
|
+
console.log(" mushi login --upgrade-scope\n");
|
|
2843
|
+
} else {
|
|
2844
|
+
console.log(`\u26A0 Key probe returned HTTP ${probeRes.status} \u2014 check your credentials.`);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
} catch {
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2702
2850
|
console.log("");
|
|
2703
2851
|
console.log(`Done! Restart ${opts.ide === "cursor" ? "Cursor" : opts.ide === "claude" ? "Claude Code" : opts.ide} and ask: "list mushi tools"`);
|
|
2704
2852
|
if (!opts.withRules) {
|
|
@@ -2909,6 +3057,23 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
2909
3057
|
detail: sc.ready ? "" : sc.hint
|
|
2910
3058
|
}));
|
|
2911
3059
|
}
|
|
3060
|
+
if (res.status === 403) {
|
|
3061
|
+
let errCode;
|
|
3062
|
+
try {
|
|
3063
|
+
const body = await res.json();
|
|
3064
|
+
errCode = body?.error?.code;
|
|
3065
|
+
} catch {
|
|
3066
|
+
}
|
|
3067
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
3068
|
+
return [
|
|
3069
|
+
{
|
|
3070
|
+
name: "Server preflight",
|
|
3071
|
+
ok: true,
|
|
3072
|
+
detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and unlock admin checks."
|
|
3073
|
+
}
|
|
3074
|
+
];
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
2912
3077
|
const text2 = await res.text().catch(() => "");
|
|
2913
3078
|
return [
|
|
2914
3079
|
{
|
|
@@ -3111,7 +3276,17 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
3111
3276
|
let mcpRaw = null;
|
|
3112
3277
|
for (const candidate of candidates) {
|
|
3113
3278
|
try {
|
|
3114
|
-
|
|
3279
|
+
const raw = await readFile2(candidate, "utf8");
|
|
3280
|
+
let parsed = {};
|
|
3281
|
+
try {
|
|
3282
|
+
parsed = JSON.parse(raw);
|
|
3283
|
+
} catch {
|
|
3284
|
+
}
|
|
3285
|
+
const hasEntries = Object.keys(parsed.mcpServers ?? {}).length > 0;
|
|
3286
|
+
if (!hasEntries && candidates.indexOf(candidate) < candidates.length - 1) {
|
|
3287
|
+
continue;
|
|
3288
|
+
}
|
|
3289
|
+
mcpRaw = raw;
|
|
3115
3290
|
mcpPath = candidate;
|
|
3116
3291
|
break;
|
|
3117
3292
|
} catch {
|
|
@@ -3188,10 +3363,20 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
3188
3363
|
detail: `Key is valid; ${projectCount} accessible project${projectCount === 1 ? "" : "s"}`
|
|
3189
3364
|
});
|
|
3190
3365
|
} else {
|
|
3366
|
+
let detail = `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`;
|
|
3367
|
+
try {
|
|
3368
|
+
const errBody = await res.json();
|
|
3369
|
+
const nested = errBody["error"];
|
|
3370
|
+
const errCode = nested?.["code"] ?? errBody["code"] ?? "";
|
|
3371
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
3372
|
+
detail = `Key has report:write scope only \u2014 run \`mushi login --upgrade-scope\` then \`mushi setup\` to get mcp:read.`;
|
|
3373
|
+
}
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
3191
3376
|
checks.push({
|
|
3192
3377
|
name: "[mcp] account-overview reachable",
|
|
3193
3378
|
ok: false,
|
|
3194
|
-
detail
|
|
3379
|
+
detail
|
|
3195
3380
|
});
|
|
3196
3381
|
}
|
|
3197
3382
|
} catch (err) {
|
|
@@ -3221,11 +3406,96 @@ function fixHintForCheck(name) {
|
|
|
3221
3406
|
if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
|
|
3222
3407
|
return void 0;
|
|
3223
3408
|
}
|
|
3409
|
+
async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = globalThis.fetch) {
|
|
3410
|
+
if (!config.apiKey || !config.projectId || !config.endpoint) {
|
|
3411
|
+
if (config.apiKey && config.endpoint) {
|
|
3412
|
+
return {
|
|
3413
|
+
nextAction: "Select or create a project: run `mushi project create` or pick one with `mushi project list`",
|
|
3414
|
+
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3415
|
+
done: false
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
return {
|
|
3419
|
+
nextAction: "Sign in to Mushi: run `npx mushi-mushi` (browser opens automatically \u2014 do NOT type the code in the terminal)",
|
|
3420
|
+
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3421
|
+
done: false
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
try {
|
|
3425
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
3426
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3427
|
+
const root = resolve3(cwd);
|
|
3428
|
+
let envContent = "";
|
|
3429
|
+
for (const f of [".env.local", ".env"]) {
|
|
3430
|
+
try {
|
|
3431
|
+
envContent = await readFile2(join5(root, f), "utf8");
|
|
3432
|
+
break;
|
|
3433
|
+
} catch {
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
|
|
3437
|
+
if (!hasEnvVars) {
|
|
3438
|
+
return {
|
|
3439
|
+
nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
|
|
3440
|
+
ctaPath: "/onboarding?tab=sdk",
|
|
3441
|
+
done: false
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
} catch {
|
|
3445
|
+
}
|
|
3446
|
+
try {
|
|
3447
|
+
const sdkCheck = await checkSdkInstall(cwd);
|
|
3448
|
+
if (sdkCheck && !sdkCheck.ok) {
|
|
3449
|
+
return {
|
|
3450
|
+
nextAction: `Install the Mushi SDK: ${sdkCheck.detail}`,
|
|
3451
|
+
ctaPath: "/onboarding?tab=sdk",
|
|
3452
|
+
done: false
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
} catch {
|
|
3456
|
+
}
|
|
3457
|
+
try {
|
|
3458
|
+
const data = await fetchIngestSetup(
|
|
3459
|
+
{ endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
|
|
3460
|
+
doFetch
|
|
3461
|
+
);
|
|
3462
|
+
if (data) {
|
|
3463
|
+
const steps = data.steps ?? [];
|
|
3464
|
+
const incomplete = steps.find((s) => s.required && !s.complete);
|
|
3465
|
+
if (incomplete) {
|
|
3466
|
+
const tabMap = {
|
|
3467
|
+
project_created: "/onboarding?tab=steps&setup=cli",
|
|
3468
|
+
api_key_generated: "/onboarding?tab=verify",
|
|
3469
|
+
sdk_installed: "/onboarding?tab=sdk",
|
|
3470
|
+
first_report_received: "/onboarding?tab=verify"
|
|
3471
|
+
};
|
|
3472
|
+
const tab = tabMap[incomplete.id] ?? "/onboarding?tab=verify";
|
|
3473
|
+
return {
|
|
3474
|
+
nextAction: incomplete.hint ?? `Complete setup step: ${incomplete.label}`,
|
|
3475
|
+
ctaPath: tab,
|
|
3476
|
+
done: false
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
return {
|
|
3480
|
+
nextAction: "All 4 required setup steps are complete! Open the console to explore.",
|
|
3481
|
+
ctaPath: "/reports",
|
|
3482
|
+
done: true
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
} catch {
|
|
3486
|
+
}
|
|
3487
|
+
return {
|
|
3488
|
+
nextAction: "Open the onboarding wizard in the console to check your setup status.",
|
|
3489
|
+
ctaPath: "/onboarding",
|
|
3490
|
+
done: false
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3224
3493
|
async function runDoctor(config, options = {}) {
|
|
3225
3494
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
3226
3495
|
const checks = [];
|
|
3227
|
-
const
|
|
3228
|
-
const
|
|
3496
|
+
const isFull = options.full === true;
|
|
3497
|
+
const runServer = isFull || options.server !== false;
|
|
3498
|
+
const runIngest = isFull || options.ingest !== false;
|
|
3229
3499
|
checks.push(...checkCliConfig(config));
|
|
3230
3500
|
if (config.endpoint) {
|
|
3231
3501
|
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
@@ -3240,26 +3510,40 @@ async function runDoctor(config, options = {}) {
|
|
|
3240
3510
|
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
3241
3511
|
checks.push(...ingestChecks);
|
|
3242
3512
|
}
|
|
3243
|
-
if (options.qaStories) {
|
|
3513
|
+
if (isFull || options.qaStories) {
|
|
3244
3514
|
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
3245
3515
|
checks.push(...qaChecks);
|
|
3246
3516
|
}
|
|
3247
|
-
if (options.hostApp) {
|
|
3517
|
+
if (isFull || options.hostApp) {
|
|
3248
3518
|
const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
|
|
3249
3519
|
checks.push(...hostChecks);
|
|
3250
3520
|
}
|
|
3251
|
-
if (options.mcp) {
|
|
3521
|
+
if (isFull || options.mcp) {
|
|
3252
3522
|
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
3253
3523
|
checks.push(...mcpChecks);
|
|
3254
3524
|
}
|
|
3525
|
+
const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
|
|
3526
|
+
if (heartbeatPassed) {
|
|
3527
|
+
const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
|
|
3528
|
+
if (idx >= 0) {
|
|
3529
|
+
checks[idx] = {
|
|
3530
|
+
...checks[idx],
|
|
3531
|
+
ok: true,
|
|
3532
|
+
warn: true,
|
|
3533
|
+
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`."
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3255
3537
|
return { checks, ready: checks.every((c) => c.ok) };
|
|
3256
3538
|
}
|
|
3257
3539
|
function formatDoctorResult(result) {
|
|
3258
3540
|
const PASS = "\u2713";
|
|
3541
|
+
const WARN = "\u26A0";
|
|
3259
3542
|
const FAIL = "\u2717";
|
|
3260
3543
|
const lines = [];
|
|
3261
3544
|
for (const c of result.checks) {
|
|
3262
|
-
|
|
3545
|
+
const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
|
|
3546
|
+
lines.push(`${icon} ${c.name}`);
|
|
3263
3547
|
if (c.detail) lines.push(` ${c.detail}`);
|
|
3264
3548
|
if (!c.ok) {
|
|
3265
3549
|
const hint = fixHintForCheck(c.name);
|
|
@@ -3267,8 +3551,14 @@ function formatDoctorResult(result) {
|
|
|
3267
3551
|
}
|
|
3268
3552
|
}
|
|
3269
3553
|
const failed = result.checks.filter((c) => !c.ok);
|
|
3554
|
+
const warned = result.checks.filter((c) => c.ok && c.warn);
|
|
3270
3555
|
if (failed.length === 0) {
|
|
3271
|
-
|
|
3556
|
+
if (warned.length > 0) {
|
|
3557
|
+
lines.push(`
|
|
3558
|
+
All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
|
|
3559
|
+
} else {
|
|
3560
|
+
lines.push("\nAll checks passed. The CLI is ready.");
|
|
3561
|
+
}
|
|
3272
3562
|
} else {
|
|
3273
3563
|
lines.push(`
|
|
3274
3564
|
${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
@@ -3555,9 +3845,38 @@ Examples:
|
|
|
3555
3845
|
).option(
|
|
3556
3846
|
"--fix",
|
|
3557
3847
|
"Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
|
|
3848
|
+
).option(
|
|
3849
|
+
"--onboarding",
|
|
3850
|
+
"Focused onboarding check: prints the single next action you need to take to finish SDK setup, with a console deep link."
|
|
3851
|
+
).option(
|
|
3852
|
+
"--full",
|
|
3853
|
+
"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
3854
|
).action(async (opts) => {
|
|
3559
3855
|
const config = loadConfig();
|
|
3560
|
-
|
|
3856
|
+
if (opts.onboarding) {
|
|
3857
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
3858
|
+
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
3859
|
+
const status = await checkOnboardingStatus(config, consoleBase, cwd);
|
|
3860
|
+
const base = consoleBase.replace(/\/$/, "");
|
|
3861
|
+
if (status.done) {
|
|
3862
|
+
console.log(`\u2713 Setup complete. ${status.nextAction}`);
|
|
3863
|
+
console.log(` ${base}${status.ctaPath}`);
|
|
3864
|
+
} else {
|
|
3865
|
+
console.log(`\u2192 Next: ${status.nextAction}`);
|
|
3866
|
+
console.log(` Open: ${base}${status.ctaPath}`);
|
|
3867
|
+
}
|
|
3868
|
+
if (!status.done) process.exit(1);
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
const doctorOpts = {
|
|
3872
|
+
cwd: opts.cwd,
|
|
3873
|
+
server: opts.server,
|
|
3874
|
+
ingest: opts.ingest,
|
|
3875
|
+
qaStories: opts.qaStories,
|
|
3876
|
+
hostApp: opts.hostApp,
|
|
3877
|
+
mcp: opts.mcp,
|
|
3878
|
+
full: opts.full
|
|
3879
|
+
};
|
|
3561
3880
|
let result = await runDoctor(config, doctorOpts);
|
|
3562
3881
|
if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
|
|
3563
3882
|
const connectResult = await runConnect({
|
|
@@ -4641,6 +4960,97 @@ ${"\u2500".repeat(60)}`);
|
|
|
4641
4960
|
});
|
|
4642
4961
|
}
|
|
4643
4962
|
|
|
4963
|
+
// src/commands/billing.ts
|
|
4964
|
+
function registerBillingCommands(program2) {
|
|
4965
|
+
program2.command("usage").description("Show diagnoses used / limit / cap for the current billing period").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
4966
|
+
Examples:
|
|
4967
|
+
mushi usage
|
|
4968
|
+
mushi usage --json`).action(async (opts) => {
|
|
4969
|
+
const config = requireConfig();
|
|
4970
|
+
const result = await apiCall("/v1/admin/billing/stats", config);
|
|
4971
|
+
if (!result.ok) die(result);
|
|
4972
|
+
const s = result.data;
|
|
4973
|
+
if (opts.json) {
|
|
4974
|
+
console.log(JSON.stringify(s, null, 2));
|
|
4975
|
+
return;
|
|
4976
|
+
}
|
|
4977
|
+
const limitStr = s.diagnosesLimit != null ? String(s.diagnosesLimit) : "unlimited";
|
|
4978
|
+
const pctStr = s.diagnosesUsagePct != null ? `${Math.round(s.diagnosesUsagePct)}%` : "\u2014";
|
|
4979
|
+
const capStr = s.monthlySpendCapUsd != null ? `$${s.monthlySpendCapUsd}/mo` : "none";
|
|
4980
|
+
const badge = s.overDiagnosisQuota ? " \u26A0 OVER LIMIT" : s.approachingDiagnosisQuota ? " \u26A0 approaching limit" : "";
|
|
4981
|
+
console.log(`
|
|
4982
|
+
Diagnoses this period: ${s.diagnosesUsed} / ${limitStr} (${pctStr})${badge}`);
|
|
4983
|
+
console.log(`Plan: ${s.planId ?? "\u2014"}`);
|
|
4984
|
+
console.log(`Spend cap: ${capStr}`);
|
|
4985
|
+
console.log(`Period ends: ${s.periodEnd ? s.periodEnd.slice(0, 10) : "?"}`);
|
|
4986
|
+
if (s.overDiagnosisQuota) {
|
|
4987
|
+
console.log("\n Diagnoses paused \u2014 upgrade at https://kensaur.us/mushi-mushi/admin/billing");
|
|
4988
|
+
} else if (s.approachingDiagnosisQuota) {
|
|
4989
|
+
console.log("\n You're at 80%+ of your included diagnoses. Consider upgrading.");
|
|
4990
|
+
}
|
|
4991
|
+
});
|
|
4992
|
+
const billing = program2.command("billing").description("Show billing summary and manage spend cap");
|
|
4993
|
+
billing.command("status").description("Show full billing summary (plan, usage, cap)").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
4994
|
+
Examples:
|
|
4995
|
+
mushi billing status
|
|
4996
|
+
mushi billing status --json`).action(async (opts) => {
|
|
4997
|
+
const config = requireConfig();
|
|
4998
|
+
const result = await apiCall("/v1/admin/billing", config);
|
|
4999
|
+
if (!result.ok) die(result);
|
|
5000
|
+
const s = result.data;
|
|
5001
|
+
if (opts.json) {
|
|
5002
|
+
console.log(JSON.stringify(s, null, 2));
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
5005
|
+
const limitStr = s.diagnosesLimit != null ? String(s.diagnosesLimit) : "unlimited";
|
|
5006
|
+
const pctStr = s.diagnosesUsagePct != null ? `${Math.round(s.diagnosesUsagePct)}%` : "\u2014";
|
|
5007
|
+
const capStr = s.monthlySpendCapUsd != null ? `$${s.monthlySpendCapUsd}/mo` : "none";
|
|
5008
|
+
const overageStr = s.overageRateDiagnoses != null ? `$${s.overageRateDiagnoses}/diagnosis` : "N/A";
|
|
5009
|
+
console.log(`
|
|
5010
|
+
Plan: ${s.planId ?? "\u2014"}`);
|
|
5011
|
+
console.log(`Status: ${s.subscriptionStatus ?? "free"}${s.cancelAtPeriodEnd ? " (cancels at period end)" : ""}`);
|
|
5012
|
+
console.log(`Diagnoses: ${s.diagnosesUsed} / ${limitStr} (${pctStr})`);
|
|
5013
|
+
console.log(`Overage: ${overageStr}`);
|
|
5014
|
+
console.log(`Spend cap: ${capStr}`);
|
|
5015
|
+
console.log(`Renews: ${s.periodEnd ? s.periodEnd.slice(0, 10) : "?"}`);
|
|
5016
|
+
});
|
|
5017
|
+
billing.command("cap [usd]").description("Set or clear the monthly spend cap (pass 0 to clear, omit to show current cap)").addHelpText("after", `
|
|
5018
|
+
Examples:
|
|
5019
|
+
mushi billing cap # show current cap
|
|
5020
|
+
mushi billing cap 100 # set $100/mo hard cap
|
|
5021
|
+
mushi billing cap 0 # clear cap`).action(async (usdArg) => {
|
|
5022
|
+
const config = requireConfig();
|
|
5023
|
+
if (usdArg === void 0) {
|
|
5024
|
+
const result2 = await apiCall("/v1/admin/billing/stats", config);
|
|
5025
|
+
if (!result2.ok) die(result2);
|
|
5026
|
+
const cap2 = result2.data.monthlySpendCapUsd;
|
|
5027
|
+
if (cap2 == null) {
|
|
5028
|
+
console.log("No spend cap set. Pass a dollar amount to set one, e.g.: mushi billing cap 100");
|
|
5029
|
+
} else {
|
|
5030
|
+
console.log(`Current spend cap: $${cap2}/mo`);
|
|
5031
|
+
}
|
|
5032
|
+
return;
|
|
5033
|
+
}
|
|
5034
|
+
const capUsd = parseFloat(usdArg);
|
|
5035
|
+
if (isNaN(capUsd) || capUsd < 0) {
|
|
5036
|
+
console.error("Error: spend cap must be a non-negative number (or 0 to clear).");
|
|
5037
|
+
process.exit(2);
|
|
5038
|
+
}
|
|
5039
|
+
const result = await apiCall("/v1/admin/billing/spend-cap", config, {
|
|
5040
|
+
method: "PUT",
|
|
5041
|
+
headers: { "Content-Type": "application/json" },
|
|
5042
|
+
body: JSON.stringify({ project_id: config.projectId, spend_cap_usd: capUsd === 0 ? null : capUsd })
|
|
5043
|
+
});
|
|
5044
|
+
if (!result.ok) die(result);
|
|
5045
|
+
const cap = result.data.spend_cap_usd;
|
|
5046
|
+
if (cap == null) {
|
|
5047
|
+
console.log("\u2713 Spend cap cleared \u2014 no hard monthly limit.");
|
|
5048
|
+
} else {
|
|
5049
|
+
console.log(`\u2713 Spend cap set to $${cap}/mo. Diagnoses will pause gracefully once reached.`);
|
|
5050
|
+
}
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
|
|
4644
5054
|
// src/index.ts
|
|
4645
5055
|
installSignalHandlers();
|
|
4646
5056
|
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION).addHelpText("after", `
|
|
@@ -4673,6 +5083,7 @@ registerIntegrationsCommands(program);
|
|
|
4673
5083
|
registerQaCommands(program);
|
|
4674
5084
|
registerAuditCommands(program);
|
|
4675
5085
|
registerSkillsCommands(program);
|
|
5086
|
+
registerBillingCommands(program);
|
|
4676
5087
|
program.parseAsync().catch((err) => {
|
|
4677
5088
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
4678
5089
|
process.exit(1);
|