@mushi-mushi/cli 0.27.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/{chunk-M7CDSRZM.js → chunk-AV3RKAEE.js} +77 -47
- package/dist/{chunk-NNB2NONG.js → chunk-FJETUM25.js} +156 -497
- package/dist/chunk-KOBEOGNF.js +475 -0
- package/dist/chunk-SXSI2W6Z.js +42 -0
- package/dist/chunk-U2VGB63C.js +8 -0
- package/dist/chunk-UVO4A2G6.js +10 -0
- package/dist/connect-63GYZLFM.js +13 -0
- package/dist/{connect-5EQA7G3U.js → connect-C673EKS3.js} +74 -42
- package/dist/detect-4PYRZXVK.js +22 -0
- package/dist/endpoint-NHECZL47.js +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1202 -814
- package/dist/init.d.ts +179 -1
- package/dist/init.js +468 -225
- package/dist/{version-AYMUW3UQ.js → version-VR37VEE4.js} +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wizard-args.d.ts +2 -2
- package/dist/wizard-args.js +5 -3
- package/package.json +10 -15
- package/CODE_OF_CONDUCT.md +0 -51
- package/CONTRIBUTING.md +0 -226
- package/SECURITY.md +0 -359
- package/dist/chunk-DBAEB56N.js +0 -10
- package/dist/chunk-XLGHUOKY.js +0 -8
- package/dist/connect-TWO2ZNFB.js +0 -10
package/dist/init.js
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
CLI_KEY_LABEL,
|
|
3
|
+
CLI_KEY_SCOPES,
|
|
4
|
+
SDK_KEY_LABEL,
|
|
5
|
+
SDK_KEY_SCOPES,
|
|
3
6
|
apiCall,
|
|
4
7
|
apiKeyHeaders,
|
|
5
8
|
apiKeyHint,
|
|
6
9
|
cliSetupDeepLink,
|
|
7
10
|
consoleUrl,
|
|
11
|
+
describeUnsafeSdkKey,
|
|
8
12
|
ensureClientId,
|
|
9
13
|
fetchIngestSetup,
|
|
10
14
|
loadConfig,
|
|
11
15
|
maybeShowTelemetryNotice,
|
|
12
|
-
normalizeEndpoint,
|
|
13
16
|
openInBrowser,
|
|
17
|
+
probeKeyScope,
|
|
14
18
|
projectIdHint,
|
|
15
19
|
reportsUrl,
|
|
16
|
-
resolveCloudEndpoint,
|
|
17
20
|
resolveConsoleUrl,
|
|
18
21
|
resolveConsoleUrlSync,
|
|
22
|
+
resolveProfileName,
|
|
19
23
|
sanitizeCliCredentials,
|
|
20
24
|
sanitizeEndpoint,
|
|
21
|
-
saveConfig
|
|
22
|
-
|
|
25
|
+
saveConfig,
|
|
26
|
+
savedSdkKeyFor,
|
|
27
|
+
trySaveKeyToKeychain
|
|
28
|
+
} from "./chunk-AV3RKAEE.js";
|
|
23
29
|
import {
|
|
24
30
|
MUSHI_CLI_VERSION
|
|
25
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-U2VGB63C.js";
|
|
26
32
|
import {
|
|
27
33
|
FRAMEWORKS,
|
|
28
34
|
detectFramework,
|
|
@@ -31,6 +37,12 @@ import {
|
|
|
31
37
|
installCommand,
|
|
32
38
|
readPackageJson
|
|
33
39
|
} from "./chunk-AIZRI665.js";
|
|
40
|
+
import {
|
|
41
|
+
CLOUD_API_ENDPOINT,
|
|
42
|
+
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
43
|
+
normalizeEndpoint,
|
|
44
|
+
resolveCloudEndpoint
|
|
45
|
+
} from "./chunk-SXSI2W6Z.js";
|
|
34
46
|
import "./chunk-3RG5ZIWI.js";
|
|
35
47
|
|
|
36
48
|
// src/init.ts
|
|
@@ -255,6 +267,49 @@ async function mintProjectKey(endpoint, cliToken, projectId, opts = {}) {
|
|
|
255
267
|
);
|
|
256
268
|
}
|
|
257
269
|
|
|
270
|
+
// src/auth-ui.ts
|
|
271
|
+
var LINE = "\u2500".repeat(58);
|
|
272
|
+
function printAuthBanner(userCode, verificationUri) {
|
|
273
|
+
console.log("");
|
|
274
|
+
console.log(` \u250C${LINE}\u2510`);
|
|
275
|
+
console.log(` \u2502 Browser sign-in \u2014 follow these 3 steps \u2502`);
|
|
276
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
277
|
+
console.log(` \u2502 \u2502`);
|
|
278
|
+
console.log(` \u2502 1. A browser tab will open (or visit the URL below) \u2502`);
|
|
279
|
+
console.log(` \u2502 2. Sign in if prompted, then click "Approve" \u2502`);
|
|
280
|
+
console.log(` \u2502 3. Come back here \u2014 setup continues automatically \u2502`);
|
|
281
|
+
console.log(` \u2502 \u2502`);
|
|
282
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
283
|
+
console.log(` \u2502 Verification code (show in browser \u2014 NOT terminal): \u2502`);
|
|
284
|
+
console.log(` \u2502 \u2502`);
|
|
285
|
+
console.log(` \u2502 ${userCode.padEnd(52)}\u2502`);
|
|
286
|
+
console.log(` \u2502 \u2502`);
|
|
287
|
+
console.log(` \u2502 \u26A0 Do NOT paste or type this code in your terminal \u2502`);
|
|
288
|
+
console.log(` \u2514${LINE}\u2518`);
|
|
289
|
+
console.log("");
|
|
290
|
+
console.log(` If the browser didn't open, visit this URL and approve:`);
|
|
291
|
+
console.log(` ${verificationUri}`);
|
|
292
|
+
console.log("");
|
|
293
|
+
console.log(" \u23F3 Waiting for browser approval\u2026 (Ctrl+C to cancel)");
|
|
294
|
+
console.log("");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/login.ts
|
|
298
|
+
function slugify(value) {
|
|
299
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
300
|
+
}
|
|
301
|
+
function defaultProjectName(packageName, cwd) {
|
|
302
|
+
const fromPkg = packageName?.replace(/^@[^/]+\//, "").trim();
|
|
303
|
+
if (fromPkg) return fromPkg;
|
|
304
|
+
const parts = cwd.split(/[\\/]/).filter(Boolean);
|
|
305
|
+
return parts[parts.length - 1] || "my-app";
|
|
306
|
+
}
|
|
307
|
+
function chooseProjectNonInteractive(projects, preferredName) {
|
|
308
|
+
const wanted = slugify(preferredName);
|
|
309
|
+
const match = projects.find((p2) => slugify(p2.name) === wanted || p2.slug !== void 0 && slugify(p2.slug) === wanted);
|
|
310
|
+
return match ? { kind: "existing", id: match.id, name: match.name } : { kind: "new", name: preferredName };
|
|
311
|
+
}
|
|
312
|
+
|
|
258
313
|
// src/freshness.ts
|
|
259
314
|
var REGISTRY = "https://registry.npmjs.org";
|
|
260
315
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
@@ -411,50 +466,27 @@ function getFrameworkFromPkg(pkg) {
|
|
|
411
466
|
return void 0;
|
|
412
467
|
}
|
|
413
468
|
|
|
414
|
-
// src/auth-ui.ts
|
|
415
|
-
var LINE = "\u2500".repeat(58);
|
|
416
|
-
function printAuthBanner(userCode, verificationUri) {
|
|
417
|
-
console.log("");
|
|
418
|
-
console.log(` \u250C${LINE}\u2510`);
|
|
419
|
-
console.log(` \u2502 Browser sign-in \u2014 follow these 3 steps \u2502`);
|
|
420
|
-
console.log(` \u251C${LINE}\u2524`);
|
|
421
|
-
console.log(` \u2502 \u2502`);
|
|
422
|
-
console.log(` \u2502 1. A browser tab will open (or visit the URL below) \u2502`);
|
|
423
|
-
console.log(` \u2502 2. Sign in if prompted, then click "Approve" \u2502`);
|
|
424
|
-
console.log(` \u2502 3. Come back here \u2014 setup continues automatically \u2502`);
|
|
425
|
-
console.log(` \u2502 \u2502`);
|
|
426
|
-
console.log(` \u251C${LINE}\u2524`);
|
|
427
|
-
console.log(` \u2502 Verification code (show in browser \u2014 NOT terminal): \u2502`);
|
|
428
|
-
console.log(` \u2502 \u2502`);
|
|
429
|
-
console.log(` \u2502 ${userCode.padEnd(52)}\u2502`);
|
|
430
|
-
console.log(` \u2502 \u2502`);
|
|
431
|
-
console.log(` \u2502 \u26A0 Do NOT paste or type this code in your terminal \u2502`);
|
|
432
|
-
console.log(` \u2514${LINE}\u2518`);
|
|
433
|
-
console.log("");
|
|
434
|
-
console.log(` If the browser didn't open, visit this URL and approve:`);
|
|
435
|
-
console.log(` ${verificationUri}`);
|
|
436
|
-
console.log("");
|
|
437
|
-
console.log(" \u23F3 Waiting for browser approval\u2026 (Ctrl+C to cancel)");
|
|
438
|
-
console.log("");
|
|
439
|
-
}
|
|
440
|
-
|
|
441
469
|
// src/doctor.ts
|
|
470
|
+
var SIGN_IN_HINT = "Run `npx mushi-mushi` in your app folder \u2014 browser sign-in saves the API key, project and endpoint.";
|
|
442
471
|
function checkCliConfig(config) {
|
|
443
472
|
return [
|
|
444
473
|
{
|
|
474
|
+
// No saved endpoint is not a failure: every command falls back to Mushi
|
|
475
|
+
// Cloud (endpoint.ts), so reporting "No endpoint" first sent people
|
|
476
|
+
// hunting for a URL they never needed.
|
|
445
477
|
name: "CLI config file",
|
|
446
|
-
ok:
|
|
447
|
-
detail: config.endpoint ? `endpoint=${config.endpoint}` :
|
|
478
|
+
ok: true,
|
|
479
|
+
detail: config.endpoint ? `endpoint=${config.endpoint}` : `endpoint=${CLOUD_API_ENDPOINT} (Mushi Cloud default \u2014 set MUSHI_API_ENDPOINT for a self-hosted backend)`
|
|
448
480
|
},
|
|
449
481
|
{
|
|
450
482
|
name: "API key configured",
|
|
451
483
|
ok: Boolean(config.apiKey),
|
|
452
|
-
detail: config.apiKey ? `apiKey=${config.apiKey.slice(0, 8)}\u2026${config.apiKey.slice(-4)}` : "No API key
|
|
484
|
+
detail: config.apiKey ? `apiKey=${config.apiKey.slice(0, 8)}\u2026${config.apiKey.slice(-4)}` : "No API key saved."
|
|
453
485
|
},
|
|
454
486
|
{
|
|
455
487
|
name: "Project ID configured",
|
|
456
488
|
ok: Boolean(config.projectId),
|
|
457
|
-
detail: config.projectId ? `projectId=${config.projectId}` : "No default project
|
|
489
|
+
detail: config.projectId ? `projectId=${config.projectId}` : "No default project saved."
|
|
458
490
|
}
|
|
459
491
|
];
|
|
460
492
|
}
|
|
@@ -476,16 +508,7 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
|
476
508
|
}
|
|
477
509
|
async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
|
|
478
510
|
const checks = [];
|
|
479
|
-
|
|
480
|
-
return [
|
|
481
|
-
{
|
|
482
|
-
name: "Sign-in route reachable",
|
|
483
|
-
ok: false,
|
|
484
|
-
detail: "No endpoint configured \u2014 run `npx mushi-mushi` or pass MUSHI_API_ENDPOINT."
|
|
485
|
-
}
|
|
486
|
-
];
|
|
487
|
-
}
|
|
488
|
-
const base = sanitizeEndpoint(config.endpoint);
|
|
511
|
+
const base = sanitizeEndpoint(config.endpoint ?? CLOUD_API_ENDPOINT);
|
|
489
512
|
let dateHeader = null;
|
|
490
513
|
try {
|
|
491
514
|
const res = await doFetch(`${base}/v1/cli/auth/device/token`, {
|
|
@@ -568,13 +591,18 @@ async function checkSdkInstall(cwd) {
|
|
|
568
591
|
"@mushi-mushi/react",
|
|
569
592
|
"@mushi-mushi/web",
|
|
570
593
|
"@mushi-mushi/core",
|
|
571
|
-
"@mushi-mushi/react-native"
|
|
594
|
+
"@mushi-mushi/react-native",
|
|
595
|
+
"@mushi-mushi/vue",
|
|
596
|
+
"@mushi-mushi/svelte",
|
|
597
|
+
"@mushi-mushi/angular",
|
|
598
|
+
"@mushi-mushi/capacitor",
|
|
599
|
+
"@mushi-mushi/node"
|
|
572
600
|
];
|
|
573
601
|
const installed = sdks.filter((s) => deps[s]);
|
|
574
602
|
return {
|
|
575
603
|
name: "SDK installed in this repo",
|
|
576
604
|
ok: installed.length > 0,
|
|
577
|
-
detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi
|
|
605
|
+
detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `npx mushi-mushi` to install"
|
|
578
606
|
};
|
|
579
607
|
} catch {
|
|
580
608
|
return null;
|
|
@@ -790,16 +818,19 @@ async function checkHostAppWiring(cwd) {
|
|
|
790
818
|
ok: hasProjectId && hasApiKey,
|
|
791
819
|
detail: hasProjectId && hasApiKey ? "VITE_/MUSHI_ project id + API key found" : "Run `mushi connect --write-env` or add VITE_MUSHI_PROJECT_ID + VITE_MUSHI_API_KEY"
|
|
792
820
|
});
|
|
793
|
-
let
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
821
|
+
let mcpFound = null;
|
|
822
|
+
for (const rel of [".mcp.json", join3(".cursor", "mcp.json")]) {
|
|
823
|
+
try {
|
|
824
|
+
await access(join3(root, rel));
|
|
825
|
+
mcpFound = rel;
|
|
826
|
+
break;
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
798
829
|
}
|
|
799
830
|
checks.push({
|
|
800
|
-
name: "[host]
|
|
801
|
-
ok:
|
|
802
|
-
detail:
|
|
831
|
+
name: "[host] MCP config",
|
|
832
|
+
ok: mcpFound !== null,
|
|
833
|
+
detail: mcpFound ? `${mcpFound} present` : "No .mcp.json (Claude Code) or .cursor/mcp.json (Cursor) \u2014 run `npx mushi-mushi setup --ide claude` or `--ide cursor` for two-way reporter replies"
|
|
803
834
|
});
|
|
804
835
|
if (isCapHybrid) {
|
|
805
836
|
const hasWebSdk = Boolean(deps["@mushi-mushi/web"] || deps["@mushi-mushi/react"]);
|
|
@@ -888,95 +919,140 @@ async function checkHashRouterCapture(cwd) {
|
|
|
888
919
|
}
|
|
889
920
|
];
|
|
890
921
|
}
|
|
891
|
-
|
|
892
|
-
|
|
922
|
+
function isJsonObject(value) {
|
|
923
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
924
|
+
}
|
|
925
|
+
function normalizeProjectPath(path) {
|
|
926
|
+
const slashed = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
927
|
+
return process.platform === "win32" ? slashed.toLowerCase() : slashed;
|
|
928
|
+
}
|
|
929
|
+
async function collectMcpSources(root, home) {
|
|
893
930
|
const { readFile } = await import("fs/promises");
|
|
894
|
-
const { join: join3
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
931
|
+
const { join: join3 } = await import("path");
|
|
932
|
+
const sources = [];
|
|
933
|
+
const invalid = [];
|
|
934
|
+
const plainFiles = [
|
|
935
|
+
{ path: join3(root, ".mcp.json"), claudeCode: true },
|
|
936
|
+
{ path: join3(root, ".cursor", "mcp.json"), claudeCode: false },
|
|
937
|
+
{ path: join3(home, ".cursor", "mcp.json"), claudeCode: false }
|
|
938
|
+
];
|
|
939
|
+
for (const file of plainFiles) {
|
|
940
|
+
let raw;
|
|
901
941
|
try {
|
|
902
|
-
|
|
903
|
-
let parsed = {};
|
|
904
|
-
try {
|
|
905
|
-
parsed = JSON.parse(raw);
|
|
906
|
-
} catch {
|
|
907
|
-
}
|
|
908
|
-
const hasEntries = Object.keys(parsed.mcpServers ?? {}).length > 0;
|
|
909
|
-
if (!hasEntries && candidates.indexOf(candidate) < candidates.length - 1) {
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
mcpRaw = raw;
|
|
913
|
-
mcpPath = candidate;
|
|
914
|
-
break;
|
|
942
|
+
raw = await readFile(file.path, "utf8");
|
|
915
943
|
} catch {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const parsed = JSON.parse(raw);
|
|
948
|
+
const servers = isJsonObject(parsed) && isJsonObject(parsed.mcpServers) ? parsed.mcpServers : {};
|
|
949
|
+
sources.push({ path: file.path, servers, claudeCode: file.claudeCode });
|
|
950
|
+
} catch {
|
|
951
|
+
invalid.push(file.path);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const claudeJsonPath = join3(home, ".claude.json");
|
|
955
|
+
try {
|
|
956
|
+
const parsed = JSON.parse(await readFile(claudeJsonPath, "utf8"));
|
|
957
|
+
if (isJsonObject(parsed)) {
|
|
958
|
+
const servers = isJsonObject(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
|
|
959
|
+
const projects = isJsonObject(parsed.projects) ? parsed.projects : {};
|
|
960
|
+
const wanted = normalizeProjectPath(root);
|
|
961
|
+
for (const [projectPath, entry] of Object.entries(projects)) {
|
|
962
|
+
if (normalizeProjectPath(projectPath) === wanted && isJsonObject(entry) && isJsonObject(entry.mcpServers)) {
|
|
963
|
+
Object.assign(servers, entry.mcpServers);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
sources.push({ path: claudeJsonPath, servers, claudeCode: true });
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
return { sources, invalid };
|
|
971
|
+
}
|
|
972
|
+
function assessMushiEntry(source, name, entry, cliKeySaved) {
|
|
973
|
+
const label = `${name} (${source.path})`;
|
|
974
|
+
if (!isJsonObject(entry)) return { usable: false, stdio: false, detail: `${label}: not an object` };
|
|
975
|
+
if (typeof entry.url === "string") {
|
|
976
|
+
if (source.claudeCode && entry.type !== "http" && entry.type !== "sse") {
|
|
977
|
+
return {
|
|
978
|
+
usable: false,
|
|
979
|
+
stdio: false,
|
|
980
|
+
detail: `${label}: has a url but no "type": "http" \u2014 Claude Code skips it`
|
|
981
|
+
};
|
|
916
982
|
}
|
|
983
|
+
return { usable: true, stdio: false, detail: `${label}: hosted OAuth \u2014 sign in from the IDE's MCP panel` };
|
|
984
|
+
}
|
|
985
|
+
const env = isJsonObject(entry.env) ? entry.env : {};
|
|
986
|
+
const key = env.MUSHI_API_KEY;
|
|
987
|
+
const endpoint = env.MUSHI_API_ENDPOINT;
|
|
988
|
+
if (typeof endpoint === "string" && !/^https?:\/\//.test(endpoint) && !/^\$\{.+\}$/.test(endpoint)) {
|
|
989
|
+
return { usable: false, stdio: true, detail: `${label}: MUSHI_API_ENDPOINT is not a URL` };
|
|
990
|
+
}
|
|
991
|
+
if (typeof key === "string" && key.startsWith("mushi_")) {
|
|
992
|
+
return { usable: true, stdio: true, detail: `${label}: key written inline` };
|
|
993
|
+
}
|
|
994
|
+
if (key === void 0 || typeof key === "string" && /^\$\{.*\}$/.test(key)) {
|
|
995
|
+
return cliKeySaved ? { usable: true, stdio: true, detail: `${label}: key from MUSHI_API_KEY or the saved CLI config` } : {
|
|
996
|
+
usable: false,
|
|
997
|
+
stdio: true,
|
|
998
|
+
detail: `${label}: reads the key from MUSHI_API_KEY or the CLI config, and neither is set`
|
|
999
|
+
};
|
|
917
1000
|
}
|
|
918
|
-
|
|
1001
|
+
return { usable: false, stdio: true, detail: `${label}: MUSHI_API_KEY is neither a mushi_ key nor a placeholder` };
|
|
1002
|
+
}
|
|
1003
|
+
async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch, home) {
|
|
1004
|
+
const checks = [];
|
|
1005
|
+
const { resolve: resolve2 } = await import("path");
|
|
1006
|
+
const { homedir } = await import("os");
|
|
1007
|
+
const root = resolve2(cwd);
|
|
1008
|
+
const { sources, invalid } = await collectMcpSources(root, home ?? homedir());
|
|
1009
|
+
if (invalid.length > 0) {
|
|
919
1010
|
checks.push({
|
|
920
|
-
name: "[mcp]
|
|
1011
|
+
name: "[mcp] MCP config valid JSON",
|
|
921
1012
|
ok: false,
|
|
922
|
-
detail:
|
|
1013
|
+
detail: `Not valid JSON: ${invalid.join(", ")}`
|
|
923
1014
|
});
|
|
1015
|
+
}
|
|
1016
|
+
if (sources.length === 0) {
|
|
1017
|
+
if (invalid.length === 0) {
|
|
1018
|
+
checks.push({
|
|
1019
|
+
name: "[mcp] MCP config present",
|
|
1020
|
+
ok: false,
|
|
1021
|
+
detail: "No .mcp.json, .cursor/mcp.json, ~/.cursor/mcp.json or ~/.claude.json found."
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
924
1024
|
return checks;
|
|
925
1025
|
}
|
|
926
1026
|
checks.push({
|
|
927
|
-
name: "[mcp]
|
|
1027
|
+
name: "[mcp] MCP config present",
|
|
928
1028
|
ok: true,
|
|
929
|
-
detail: `Found
|
|
1029
|
+
detail: `Found: ${sources.map((s) => s.path).join(", ")}`
|
|
930
1030
|
});
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
mcpConfig = JSON.parse(mcpRaw);
|
|
934
|
-
} catch {
|
|
935
|
-
checks.push({
|
|
936
|
-
name: "[mcp] mcp.json valid JSON",
|
|
937
|
-
ok: false,
|
|
938
|
-
detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`."
|
|
939
|
-
});
|
|
940
|
-
return checks;
|
|
941
|
-
}
|
|
942
|
-
const servers = mcpConfig.mcpServers ?? {};
|
|
943
|
-
const mushiEntries = Object.entries(servers).filter(
|
|
944
|
-
([k]) => k === "mushi" || k.startsWith("mushi-")
|
|
1031
|
+
const mushiEntries = sources.flatMap(
|
|
1032
|
+
(source) => Object.entries(source.servers).filter(([name]) => name === "mushi" || name.startsWith("mushi-")).map(([name, entry]) => ({ source, name, entry }))
|
|
945
1033
|
);
|
|
946
1034
|
if (mushiEntries.length === 0) {
|
|
947
1035
|
checks.push({
|
|
948
1036
|
name: "[mcp] mushi server entry",
|
|
949
1037
|
ok: false,
|
|
950
|
-
detail: "No mushi or mushi-* server
|
|
1038
|
+
detail: "No mushi or mushi-* server in any MCP config."
|
|
951
1039
|
});
|
|
952
1040
|
return checks;
|
|
953
1041
|
}
|
|
954
1042
|
checks.push({
|
|
955
1043
|
name: "[mcp] mushi server entry",
|
|
956
1044
|
ok: true,
|
|
957
|
-
detail: `Found: ${mushiEntries.map((
|
|
958
|
-
});
|
|
959
|
-
let anyKeyValid = false;
|
|
960
|
-
let anyEndpointSet = false;
|
|
961
|
-
for (const [, srv] of mushiEntries) {
|
|
962
|
-
const s = srv;
|
|
963
|
-
const env = s.env ?? {};
|
|
964
|
-
const key = env["MUSHI_API_KEY"] ?? "";
|
|
965
|
-
const endpoint = env["MUSHI_API_ENDPOINT"] ?? "";
|
|
966
|
-
if (key.startsWith("mushi_")) anyKeyValid = true;
|
|
967
|
-
if (endpoint.includes("supabase.co") || endpoint.includes("localhost")) anyEndpointSet = true;
|
|
968
|
-
}
|
|
969
|
-
checks.push({
|
|
970
|
-
name: "[mcp] MUSHI_API_KEY set",
|
|
971
|
-
ok: anyKeyValid,
|
|
972
|
-
detail: anyKeyValid ? "At least one mushi server has a valid mushi_* API key" : "No mushi_* API key found in any mushi server env. Re-run `mushi setup` to regenerate."
|
|
1045
|
+
detail: `Found: ${mushiEntries.map(({ name }) => name).join(", ")}`
|
|
973
1046
|
});
|
|
1047
|
+
const assessed = mushiEntries.map(
|
|
1048
|
+
({ source, name, entry }) => assessMushiEntry(source, name, entry, Boolean(config.apiKey))
|
|
1049
|
+
);
|
|
974
1050
|
checks.push({
|
|
975
|
-
name: "[mcp]
|
|
976
|
-
ok:
|
|
977
|
-
detail:
|
|
1051
|
+
name: "[mcp] mushi entries usable",
|
|
1052
|
+
ok: assessed.every((a) => a.usable),
|
|
1053
|
+
detail: assessed.map((a) => a.detail).join("; ")
|
|
978
1054
|
});
|
|
979
|
-
if (
|
|
1055
|
+
if (assessed.some((a) => a.stdio && a.usable) && config.apiKey && config.endpoint) {
|
|
980
1056
|
try {
|
|
981
1057
|
const { endpoint, apiKey } = sanitizeCliCredentials(config);
|
|
982
1058
|
const res = await doFetch(`${endpoint}/v1/admin/mcp/account-overview`, {
|
|
@@ -1020,13 +1096,13 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
1020
1096
|
return checks;
|
|
1021
1097
|
}
|
|
1022
1098
|
var FIX_HINTS = {
|
|
1023
|
-
"
|
|
1024
|
-
"API key configured":
|
|
1025
|
-
"Project ID configured":
|
|
1099
|
+
"Mushi set up here": "Run `npx mushi-mushi` in your app folder \u2014 it signs you in, installs the SDK and writes the env vars.",
|
|
1100
|
+
"API key configured": SIGN_IN_HINT,
|
|
1101
|
+
"Project ID configured": SIGN_IN_HINT,
|
|
1026
1102
|
"Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
|
|
1027
1103
|
"[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
|
|
1028
1104
|
"[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
|
|
1029
|
-
"[mcp]": "Run `mushi setup`
|
|
1105
|
+
"[mcp]": "Run `npx mushi-mushi setup --ide cursor` (or `--ide claude`) to rewrite the Mushi MCP entry."
|
|
1030
1106
|
};
|
|
1031
1107
|
function fixHintForCheck(name) {
|
|
1032
1108
|
if (FIX_HINTS[name]) return FIX_HINTS[name];
|
|
@@ -1135,11 +1211,23 @@ async function runDoctor(config, options = {}) {
|
|
|
1135
1211
|
const isFull = options.full === true;
|
|
1136
1212
|
const runServer = isFull || options.server !== false;
|
|
1137
1213
|
const runIngest = isFull || options.ingest !== false;
|
|
1214
|
+
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
1215
|
+
if (!config.apiKey && !config.projectId && !sdkCheck?.ok && !options.auth && !isFull) {
|
|
1216
|
+
return {
|
|
1217
|
+
ready: false,
|
|
1218
|
+
checks: [
|
|
1219
|
+
{
|
|
1220
|
+
name: "Mushi set up here",
|
|
1221
|
+
ok: false,
|
|
1222
|
+
detail: "Not set up yet \u2014 no saved Mushi credentials and no Mushi SDK in this folder."
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1138
1227
|
checks.push(...checkCliConfig(config));
|
|
1139
1228
|
if (config.endpoint) {
|
|
1140
1229
|
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
1141
1230
|
}
|
|
1142
|
-
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
1143
1231
|
if (sdkCheck) checks.push(sdkCheck);
|
|
1144
1232
|
if (sdkCheck?.ok && !sdkCheck.warn) {
|
|
1145
1233
|
const { findSdkImport } = await import("./snippet-inject-7ABYE57O.js");
|
|
@@ -1199,13 +1287,17 @@ function formatDoctorResult(result) {
|
|
|
1199
1287
|
const WARN = "WARN";
|
|
1200
1288
|
const FAIL = "FAIL";
|
|
1201
1289
|
const lines = [];
|
|
1290
|
+
const printedHints = /* @__PURE__ */ new Set();
|
|
1202
1291
|
for (const c of result.checks) {
|
|
1203
1292
|
const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
|
|
1204
1293
|
lines.push(`${icon} ${c.name}`);
|
|
1205
1294
|
if (c.detail) lines.push(` ${c.detail}`);
|
|
1206
1295
|
if (!c.ok) {
|
|
1207
1296
|
const hint = fixHintForCheck(c.name);
|
|
1208
|
-
if (hint
|
|
1297
|
+
if (hint && !printedHints.has(hint)) {
|
|
1298
|
+
printedHints.add(hint);
|
|
1299
|
+
lines.push(` \u2192 Fix: ${hint}`);
|
|
1300
|
+
}
|
|
1209
1301
|
}
|
|
1210
1302
|
}
|
|
1211
1303
|
const failed = result.checks.filter((c) => !c.ok);
|
|
@@ -1229,6 +1321,9 @@ ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
|
1229
1321
|
|
|
1230
1322
|
// src/init.ts
|
|
1231
1323
|
var ENV_FILES = [".env.local", ".env"];
|
|
1324
|
+
function isInteractive() {
|
|
1325
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1326
|
+
}
|
|
1232
1327
|
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;
|
|
1233
1328
|
var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
|
|
1234
1329
|
async function runInit(options = {}) {
|
|
@@ -1238,6 +1333,7 @@ async function runInit(options = {}) {
|
|
|
1238
1333
|
return;
|
|
1239
1334
|
}
|
|
1240
1335
|
ensureInteractiveOrBailOut(options);
|
|
1336
|
+
if (!isInteractive()) options = { ...options, yes: true };
|
|
1241
1337
|
p.intro("Mushi setup");
|
|
1242
1338
|
await printFreshnessHint();
|
|
1243
1339
|
warnIfWorkspaceRoot(cwd);
|
|
@@ -1245,6 +1341,10 @@ async function runInit(options = {}) {
|
|
|
1245
1341
|
maybeSuggestAudit(pkg);
|
|
1246
1342
|
if (!pkg) {
|
|
1247
1343
|
p.log.warn("No package.json found in this directory.");
|
|
1344
|
+
if (!isInteractive()) {
|
|
1345
|
+
p.cancel("Aborted. Run from your project root, or pass --cwd <path>.");
|
|
1346
|
+
process.exit(1);
|
|
1347
|
+
}
|
|
1248
1348
|
const cont = await p.confirm({
|
|
1249
1349
|
message: "Continue anyway? (Mushi will install into the current folder)",
|
|
1250
1350
|
initialValue: false
|
|
@@ -1272,13 +1372,13 @@ async function runInit(options = {}) {
|
|
|
1272
1372
|
}
|
|
1273
1373
|
await writeEnvFile(
|
|
1274
1374
|
cwd,
|
|
1275
|
-
credentials.
|
|
1375
|
+
credentials.sdkKey,
|
|
1276
1376
|
credentials.projectId,
|
|
1277
1377
|
framework,
|
|
1278
1378
|
endpoint,
|
|
1279
1379
|
Boolean(options.yes)
|
|
1280
1380
|
);
|
|
1281
|
-
persistCliConfig(credentials
|
|
1381
|
+
persistCliConfig(credentials, endpoint);
|
|
1282
1382
|
emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
|
|
1283
1383
|
const enableRewards = await maybeEnableRewards(options);
|
|
1284
1384
|
await maybeInjectSnippet(cwd, framework, options);
|
|
@@ -1311,14 +1411,11 @@ function maybeSuggestAudit(pkg) {
|
|
|
1311
1411
|
}
|
|
1312
1412
|
}
|
|
1313
1413
|
function ensureInteractiveOrBailOut(options) {
|
|
1314
|
-
|
|
1315
|
-
if (
|
|
1316
|
-
|
|
1317
|
-
(options.framework || options.yes) && options.projectId && options.apiKey
|
|
1318
|
-
);
|
|
1319
|
-
if (hasAllFlags) return;
|
|
1414
|
+
if (isInteractive()) return;
|
|
1415
|
+
if (options.yes) return;
|
|
1416
|
+
if (options.framework && options.projectId && options.apiKey) return;
|
|
1320
1417
|
process.stderr.write(
|
|
1321
|
-
"mushi-mushi: non-interactive terminal detected.\
|
|
1418
|
+
"mushi-mushi: non-interactive terminal detected.\nRe-run with --yes to set up without prompts: it reuses the credentials saved by\n`npx mushi-mushi login`, or prints a sign-in URL and waits while you approve it in a browser.\nExample: npx mushi-mushi --yes\nCI with no browser: npx mushi-mushi --yes --project-id <uuid> --api-key <ingest-only key>\n"
|
|
1322
1419
|
);
|
|
1323
1420
|
process.exit(1);
|
|
1324
1421
|
}
|
|
@@ -1349,14 +1446,16 @@ async function chooseFramework(detected, options) {
|
|
|
1349
1446
|
return FRAMEWORKS[confirmed];
|
|
1350
1447
|
}
|
|
1351
1448
|
async function acquireCredentials(options, consoleBase, endpoint) {
|
|
1449
|
+
const probe = (e, k, pid) => probeKeyScope(e, k, pid);
|
|
1352
1450
|
if (options.projectId && options.apiKey) {
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
};
|
|
1451
|
+
const projectId = sanitizeSecret(options.projectId);
|
|
1452
|
+
const sdkKey = sanitizeSecret(options.apiKey);
|
|
1453
|
+
await assertUserKeyIsIngestOnly(sdkKey, projectId, endpoint, probe);
|
|
1454
|
+
return { projectId, sdkKey };
|
|
1357
1455
|
}
|
|
1358
1456
|
const existing = loadConfig();
|
|
1359
1457
|
maybeShowTelemetryNotice(existing);
|
|
1458
|
+
let browserTried = false;
|
|
1360
1459
|
if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
|
|
1361
1460
|
const reuse = options.yes ? true : await p.confirm({
|
|
1362
1461
|
message: "Use the Mushi credentials saved from your last sign-in?",
|
|
@@ -1367,13 +1466,32 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
1367
1466
|
process.exit(0);
|
|
1368
1467
|
}
|
|
1369
1468
|
if (reuse) {
|
|
1370
|
-
|
|
1371
|
-
projectId:
|
|
1372
|
-
|
|
1373
|
-
|
|
1469
|
+
const saved = await resolveSavedCredentials(
|
|
1470
|
+
{ projectId: existing.projectId, apiKey: existing.apiKey, sdkKey: existing.sdkKey },
|
|
1471
|
+
endpoint,
|
|
1472
|
+
probe
|
|
1473
|
+
);
|
|
1474
|
+
if (saved.kind === "ready") return saved.credentials;
|
|
1475
|
+
if (saved.kind === "needs-sdk-key") {
|
|
1476
|
+
p.log.info(
|
|
1477
|
+
"Your saved sign-in key can read bug reports, so it stays in the CLI config. Approve one browser sign-in to mint a separate ingest-only key for the app's env vars."
|
|
1478
|
+
);
|
|
1479
|
+
browserTried = true;
|
|
1480
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase, {
|
|
1481
|
+
projectId: saved.projectId,
|
|
1482
|
+
cliKey: saved.cliKey
|
|
1483
|
+
});
|
|
1484
|
+
if (creds) return creds;
|
|
1485
|
+
} else {
|
|
1486
|
+
p.log.warn(`Saved credentials can't be reused (${saved.reason}) \u2014 signing in again.`);
|
|
1487
|
+
}
|
|
1374
1488
|
}
|
|
1375
1489
|
}
|
|
1376
|
-
if (
|
|
1490
|
+
if (browserTried) {
|
|
1491
|
+
p.log.warn(
|
|
1492
|
+
"Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
|
|
1493
|
+
);
|
|
1494
|
+
} else if (options.yes) {
|
|
1377
1495
|
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
1378
1496
|
if (creds) return creds;
|
|
1379
1497
|
p.log.warn(
|
|
@@ -1408,9 +1526,70 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
1408
1526
|
);
|
|
1409
1527
|
}
|
|
1410
1528
|
}
|
|
1411
|
-
return collectCredentialsManually(options, consoleBase, endpoint);
|
|
1529
|
+
return collectCredentialsManually(options, consoleBase, endpoint, probe);
|
|
1530
|
+
}
|
|
1531
|
+
async function assertUserKeyIsIngestOnly(key, projectId, endpoint, probe) {
|
|
1532
|
+
const result = await probe(endpoint, key, projectId);
|
|
1533
|
+
if (result.result === "mcp" || result.result === "unknown") {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
`Not writing this API key into your app's env vars: ${describeUnsafeSdkKey(result, endpoint)}. Use an ingest-only (report:write) key from the console, or leave out --api-key to sign in with the browser, which mints one.`
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
async function resolveSavedCredentials(saved, endpoint, probe) {
|
|
1540
|
+
const projectId = sanitizeSecret(saved.projectId);
|
|
1541
|
+
const apiKey = sanitizeSecret(saved.apiKey);
|
|
1542
|
+
const savedSdkKey = savedSdkKeyFor(saved, projectId);
|
|
1543
|
+
if (savedSdkKey) {
|
|
1544
|
+
const sdkProbe = await probe(endpoint, savedSdkKey, projectId);
|
|
1545
|
+
if (sdkProbe.result === "ingest-only") {
|
|
1546
|
+
return { kind: "ready", credentials: { projectId, sdkKey: savedSdkKey, cliKey: apiKey } };
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const cliProbe = await probe(endpoint, apiKey, projectId);
|
|
1550
|
+
if (cliProbe.result === "ingest-only") {
|
|
1551
|
+
return { kind: "ready", credentials: { projectId, sdkKey: apiKey } };
|
|
1552
|
+
}
|
|
1553
|
+
if (cliProbe.result === "mcp") {
|
|
1554
|
+
return { kind: "needs-sdk-key", projectId, cliKey: apiKey };
|
|
1555
|
+
}
|
|
1556
|
+
return { kind: "unusable", reason: describeUnsafeSdkKey(cliProbe, endpoint) };
|
|
1557
|
+
}
|
|
1558
|
+
async function mintWizardKeys(deps, args) {
|
|
1559
|
+
let projectId;
|
|
1560
|
+
let createdName;
|
|
1561
|
+
let sdkKey = null;
|
|
1562
|
+
if (args.target.kind === "new") {
|
|
1563
|
+
const created = await deps.createProject(args.endpoint, args.cliToken, args.target.name, {
|
|
1564
|
+
scopes: SDK_KEY_SCOPES
|
|
1565
|
+
});
|
|
1566
|
+
projectId = created.id;
|
|
1567
|
+
createdName = created.name;
|
|
1568
|
+
sdkKey = created.apiKey;
|
|
1569
|
+
} else {
|
|
1570
|
+
projectId = args.target.projectId;
|
|
1571
|
+
}
|
|
1572
|
+
if (!sdkKey) {
|
|
1573
|
+
sdkKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId, {
|
|
1574
|
+
scopes: SDK_KEY_SCOPES,
|
|
1575
|
+
label: SDK_KEY_LABEL
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
let cliKey;
|
|
1579
|
+
let cliKeyError;
|
|
1580
|
+
if (args.withCliKey) {
|
|
1581
|
+
try {
|
|
1582
|
+
cliKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId, {
|
|
1583
|
+
scopes: CLI_KEY_SCOPES,
|
|
1584
|
+
label: CLI_KEY_LABEL
|
|
1585
|
+
});
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
cliKeyError = err instanceof Error ? err.message : String(err);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return { projectId, createdName, sdkKey, cliKey, cliKeyError };
|
|
1412
1591
|
}
|
|
1413
|
-
async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
1592
|
+
async function runBrowserSignIn(options, endpoint, consoleBase, preset) {
|
|
1414
1593
|
const startSpin = p.spinner();
|
|
1415
1594
|
startSpin.start("Starting secure browser sign-in\u2026");
|
|
1416
1595
|
let session;
|
|
@@ -1438,9 +1617,11 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
|
1438
1617
|
return null;
|
|
1439
1618
|
}
|
|
1440
1619
|
waitSpin.stop("Approved.");
|
|
1441
|
-
|
|
1442
|
-
let
|
|
1443
|
-
if (
|
|
1620
|
+
const presetProjectId = preset?.projectId ?? (options.projectId ? sanitizeSecret(options.projectId) : void 0);
|
|
1621
|
+
let target;
|
|
1622
|
+
if (presetProjectId) {
|
|
1623
|
+
target = { kind: "existing", projectId: presetProjectId };
|
|
1624
|
+
} else {
|
|
1444
1625
|
let projects = [];
|
|
1445
1626
|
const fetchSpin = p.spinner();
|
|
1446
1627
|
fetchSpin.start("Loading your projects\u2026");
|
|
@@ -1450,91 +1631,117 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
|
1450
1631
|
projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet."
|
|
1451
1632
|
);
|
|
1452
1633
|
} catch (err) {
|
|
1634
|
+
if (!isInteractive()) {
|
|
1635
|
+
fetchSpin.stop("Could not load projects.");
|
|
1636
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1453
1639
|
fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
|
|
1454
1640
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
1455
1641
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1642
|
+
if (!isInteractive()) {
|
|
1643
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1644
|
+
const choice = chooseProjectNonInteractive(
|
|
1645
|
+
projects,
|
|
1646
|
+
defaultProjectName(readPackageJson(cwd)?.name, cwd)
|
|
1647
|
+
);
|
|
1648
|
+
if (choice.kind === "existing") {
|
|
1649
|
+
p.log.info(`Using project "${choice.name}" (matches this app \u2014 pass --project-id to pick another).`);
|
|
1650
|
+
target = { kind: "existing", projectId: choice.id };
|
|
1651
|
+
} else {
|
|
1652
|
+
p.log.info(`Creating project "${choice.name}" (no project matches this app \u2014 pass --project-id to use an existing one).`);
|
|
1653
|
+
target = { kind: "new", name: choice.name };
|
|
1654
|
+
}
|
|
1655
|
+
} else {
|
|
1656
|
+
const NEW = "__new__";
|
|
1657
|
+
const choice = await p.select({
|
|
1658
|
+
message: "Choose a project",
|
|
1659
|
+
initialValue: projects[0]?.id ?? NEW,
|
|
1660
|
+
options: [
|
|
1661
|
+
...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
|
|
1662
|
+
{ value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
|
|
1663
|
+
]
|
|
1474
1664
|
});
|
|
1475
|
-
if (p.isCancel(
|
|
1665
|
+
if (p.isCancel(choice)) {
|
|
1476
1666
|
p.cancel("Aborted.");
|
|
1477
1667
|
process.exit(0);
|
|
1478
1668
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1669
|
+
if (choice === NEW) {
|
|
1670
|
+
const name = await p.text({
|
|
1671
|
+
message: "Project name",
|
|
1672
|
+
placeholder: "My app",
|
|
1673
|
+
validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
|
|
1484
1674
|
});
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1675
|
+
if (p.isCancel(name)) {
|
|
1676
|
+
p.cancel("Aborted.");
|
|
1677
|
+
process.exit(0);
|
|
1678
|
+
}
|
|
1679
|
+
target = { kind: "new", name: name.trim() };
|
|
1680
|
+
} else {
|
|
1681
|
+
target = { kind: "existing", projectId: choice };
|
|
1492
1682
|
}
|
|
1493
|
-
} else {
|
|
1494
|
-
projectId = choice;
|
|
1495
1683
|
}
|
|
1496
1684
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1685
|
+
const keySpin = p.spinner();
|
|
1686
|
+
keySpin.start(target.kind === "new" ? `Creating "${target.name}"\u2026` : "Minting keys\u2026");
|
|
1687
|
+
let minted;
|
|
1688
|
+
try {
|
|
1689
|
+
minted = await mintWizardKeys(
|
|
1690
|
+
{ createProject, mintProjectKey },
|
|
1691
|
+
{ endpoint, cliToken, target, withCliKey: !preset }
|
|
1692
|
+
);
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
keySpin.stop(target.kind === "new" ? "Could not create the project." : "Could not mint an API key.");
|
|
1695
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
1696
|
+
if (target.kind === "existing") {
|
|
1508
1697
|
p.log.warn(
|
|
1509
1698
|
`You're signed in, but key minting failed. Generate one manually in the console Verify tab: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
1510
1699
|
);
|
|
1511
|
-
return null;
|
|
1512
1700
|
}
|
|
1701
|
+
return null;
|
|
1513
1702
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1703
|
+
keySpin.stop(
|
|
1704
|
+
minted.createdName ? `Created project "${minted.createdName}" with an ingest-only SDK key.` : "SDK key ready (ingest-only)."
|
|
1705
|
+
);
|
|
1706
|
+
if (minted.cliKeyError) {
|
|
1707
|
+
p.log.warn(
|
|
1708
|
+
`The CLI/MCP key could not be minted (${minted.cliKeyError}). The SDK install continues; run \`npx mushi-mushi login\` later for MCP and admin commands.`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
projectId: minted.projectId,
|
|
1713
|
+
sdkKey: minted.sdkKey,
|
|
1714
|
+
cliKey: preset?.cliKey ?? minted.cliKey
|
|
1715
|
+
};
|
|
1516
1716
|
}
|
|
1517
|
-
async function collectCredentialsManually(options, consoleBase, endpoint) {
|
|
1717
|
+
async function collectCredentialsManually(options, consoleBase, endpoint, probe) {
|
|
1518
1718
|
const existing = loadConfig();
|
|
1519
1719
|
let savedProjectId = existing.projectId;
|
|
1520
|
-
let savedApiKey
|
|
1521
|
-
if (!options.projectId &&
|
|
1720
|
+
let savedApiKey;
|
|
1721
|
+
if (!isInteractive() && !(options.projectId && options.apiKey)) {
|
|
1722
|
+
throw new Error(
|
|
1723
|
+
"Browser sign-in didn't complete and this terminal cannot prompt for a key. Re-run `npx mushi-mushi --yes` and approve the sign-in in the browser, or pass --project-id and an ingest-only --api-key."
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
if (!options.projectId && !options.apiKey && savedProjectId && existing.apiKey) {
|
|
1727
|
+
const projectIdForCheck = sanitizeSecret(savedProjectId);
|
|
1728
|
+
const candidate = savedSdkKeyFor(existing, projectIdForCheck) ?? sanitizeSecret(existing.apiKey);
|
|
1522
1729
|
p.log.info(
|
|
1523
|
-
`Found saved credentials from a previous sign-in (project ${
|
|
1730
|
+
`Found saved credentials from a previous sign-in (project ${projectIdForCheck.slice(0, 8)}\u2026).`
|
|
1524
1731
|
);
|
|
1525
1732
|
const checkSpin = p.spinner();
|
|
1526
1733
|
checkSpin.start("Checking saved credentials\u2026");
|
|
1527
|
-
const check = await
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
})
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
} else {
|
|
1734
|
+
const check = await probe(endpoint, candidate, projectIdForCheck);
|
|
1735
|
+
if (check.result === "ingest-only") {
|
|
1736
|
+
checkSpin.stop("Saved ingest key still works.");
|
|
1737
|
+
savedApiKey = candidate;
|
|
1738
|
+
} else if (check.result === "mcp") {
|
|
1739
|
+
checkSpin.stop("Saved key can read reports, so it can't go into your app's env \u2014 paste an ingest-only key below.");
|
|
1740
|
+
} else if (check.result === "invalid") {
|
|
1535
1741
|
checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
|
|
1536
1742
|
savedProjectId = void 0;
|
|
1537
|
-
|
|
1743
|
+
} else {
|
|
1744
|
+
checkSpin.stop(`Could not check the saved key (${describeUnsafeSdkKey(check, endpoint)}) \u2014 enter one below.`);
|
|
1538
1745
|
}
|
|
1539
1746
|
}
|
|
1540
1747
|
const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
|
|
@@ -1561,17 +1768,29 @@ async function collectCredentialsManually(options, consoleBase, endpoint) {
|
|
|
1561
1768
|
`Invalid API key. Expected format: mushi_[A-Za-z0-9_-]{10,}. Got: ${redact(apiKey)}`
|
|
1562
1769
|
);
|
|
1563
1770
|
}
|
|
1564
|
-
|
|
1771
|
+
if (apiKey !== savedApiKey) {
|
|
1772
|
+
await assertUserKeyIsIngestOnly(apiKey, projectId, endpoint, probe);
|
|
1773
|
+
}
|
|
1774
|
+
return { projectId, sdkKey: apiKey };
|
|
1565
1775
|
}
|
|
1566
1776
|
async function verifyCredentials(credentials, options, consoleBase) {
|
|
1567
1777
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1568
1778
|
const spinner2 = p.spinner();
|
|
1569
1779
|
spinner2.start("Verifying credentials\u2026");
|
|
1570
1780
|
const result = await apiCall("/v1/sync/whoami", {
|
|
1571
|
-
apiKey: credentials.
|
|
1781
|
+
apiKey: credentials.sdkKey,
|
|
1572
1782
|
projectId: credentials.projectId,
|
|
1573
1783
|
endpoint
|
|
1574
1784
|
});
|
|
1785
|
+
if (!result.ok && (result.error.code === "NETWORK_ERROR" || result.error.code === "TIMEOUT")) {
|
|
1786
|
+
spinner2.stop(`Could not reach ${endpoint}.`);
|
|
1787
|
+
p.log.error(result.error.message);
|
|
1788
|
+
p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
|
|
1789
|
+
p.log.info(
|
|
1790
|
+
"Check your network, proxy or VPN, or pass --endpoint <url> if you run a self-hosted backend, then re-run `npx mushi-mushi`."
|
|
1791
|
+
);
|
|
1792
|
+
throw new Error(`Could not reach the Mushi API at ${endpoint} \u2014 nothing was changed.`);
|
|
1793
|
+
}
|
|
1575
1794
|
if (!result.ok) {
|
|
1576
1795
|
spinner2.stop("Credentials could not be verified.");
|
|
1577
1796
|
p.log.error(result.error?.message ?? "Authentication failed.");
|
|
@@ -1727,9 +1946,24 @@ function warnIfMissingFromGitignore(cwd, envFile) {
|
|
|
1727
1946
|
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
1728
1947
|
}
|
|
1729
1948
|
}
|
|
1730
|
-
function
|
|
1949
|
+
function planCliConfigUpdate(existing, credentials, endpoint) {
|
|
1950
|
+
const keepSavedKey = existing.projectId === credentials.projectId && Boolean(existing.apiKey);
|
|
1951
|
+
const apiKey = credentials.cliKey ?? (keepSavedKey ? existing.apiKey : credentials.sdkKey);
|
|
1952
|
+
return {
|
|
1953
|
+
...existing,
|
|
1954
|
+
apiKey,
|
|
1955
|
+
projectId: credentials.projectId,
|
|
1956
|
+
endpoint,
|
|
1957
|
+
sdkKey: { projectId: credentials.projectId, key: credentials.sdkKey }
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
function persistCliConfig(credentials, endpoint) {
|
|
1731
1961
|
const existing = loadConfig();
|
|
1732
|
-
|
|
1962
|
+
const next = planCliConfigUpdate(existing, credentials, endpoint);
|
|
1963
|
+
saveConfig(next);
|
|
1964
|
+
if (next.apiKey && next.apiKey !== existing.apiKey) {
|
|
1965
|
+
trySaveKeyToKeychain(next.apiKey, resolveProfileName());
|
|
1966
|
+
}
|
|
1733
1967
|
}
|
|
1734
1968
|
function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
|
|
1735
1969
|
if (process.env.MUSHI_NO_TELEMETRY) return;
|
|
@@ -1737,7 +1971,7 @@ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
|
|
|
1737
1971
|
method: "POST",
|
|
1738
1972
|
headers: {
|
|
1739
1973
|
"Content-Type": "application/json",
|
|
1740
|
-
"X-Mushi-Api-Key": credentials.
|
|
1974
|
+
"X-Mushi-Api-Key": credentials.sdkKey,
|
|
1741
1975
|
"X-Mushi-Project": credentials.projectId
|
|
1742
1976
|
},
|
|
1743
1977
|
body: JSON.stringify({ event, source: "cli", metadata }),
|
|
@@ -1788,7 +2022,7 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
|
1788
2022
|
[
|
|
1789
2023
|
" [ ] 1. Paste the init snippet above into your app entry file",
|
|
1790
2024
|
" [ ] 2. Start your dev server",
|
|
1791
|
-
" [ ] 3.
|
|
2025
|
+
" [ ] 3. Wire your editor: npx mushi-mushi setup --ide cursor (or --ide claude)",
|
|
1792
2026
|
` [ ] 4. Open the Verify tab to send a test report: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
1793
2027
|
].join("\n"),
|
|
1794
2028
|
"Next steps:"
|
|
@@ -1803,16 +2037,22 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
1803
2037
|
if (p.isCancel(answer) || !answer) return;
|
|
1804
2038
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1805
2039
|
try {
|
|
1806
|
-
const { runConnect } = await import("./connect-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2040
|
+
const { runConnect } = await import("./connect-C673EKS3.js");
|
|
2041
|
+
const saved = loadConfig();
|
|
2042
|
+
const result = await runConnect(
|
|
2043
|
+
{
|
|
2044
|
+
apiKey: saved.apiKey ?? credentials.sdkKey,
|
|
2045
|
+
sdkKey: credentials.sdkKey,
|
|
2046
|
+
projectId: credentials.projectId,
|
|
2047
|
+
endpoint,
|
|
2048
|
+
cwd: options.cwd,
|
|
2049
|
+
writeEnv: true,
|
|
2050
|
+
wireIde: true,
|
|
2051
|
+
wait: true
|
|
2052
|
+
},
|
|
2053
|
+
saved
|
|
2054
|
+
);
|
|
2055
|
+
for (const line of result.messages) p.log.message(line);
|
|
1816
2056
|
} catch (err) {
|
|
1817
2057
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
1818
2058
|
p.log.info("You can run manually: mushi connect --write-env --wire-ide --wait");
|
|
@@ -1852,7 +2092,7 @@ async function maybeSendTestReport(credentials, options) {
|
|
|
1852
2092
|
signal: controller.signal,
|
|
1853
2093
|
headers: {
|
|
1854
2094
|
"Content-Type": "application/json",
|
|
1855
|
-
"X-Mushi-Api-Key": credentials.
|
|
2095
|
+
"X-Mushi-Api-Key": credentials.sdkKey,
|
|
1856
2096
|
"X-Mushi-Project": credentials.projectId
|
|
1857
2097
|
},
|
|
1858
2098
|
body: JSON.stringify({
|
|
@@ -1931,6 +2171,9 @@ function isSameDirectory(a, b) {
|
|
|
1931
2171
|
}
|
|
1932
2172
|
export {
|
|
1933
2173
|
isEnvFileCoveredByGitignore,
|
|
2174
|
+
mintWizardKeys,
|
|
2175
|
+
planCliConfigUpdate,
|
|
2176
|
+
resolveSavedCredentials,
|
|
1934
2177
|
runInit,
|
|
1935
2178
|
sanitizeSecret
|
|
1936
2179
|
};
|