@mushi-mushi/cli 0.27.0 → 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/dist/init.js CHANGED
@@ -1,28 +1,34 @@
1
1
  import {
2
- TEST_REPORT_FETCH_TIMEOUT_MS,
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
- } from "./chunk-TZL3F3GT.js";
25
+ saveConfig,
26
+ savedSdkKeyFor,
27
+ trySaveKeyToKeychain
28
+ } from "./chunk-AV3RKAEE.js";
23
29
  import {
24
30
  MUSHI_CLI_VERSION
25
- } from "./chunk-LFZ5ZIBI.js";
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: Boolean(config.endpoint),
447
- detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint \u2014 set MUSHI_API_ENDPOINT, run `mushi connect`, or `mushi config endpoint <url>`"
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 set \u2014 run `mushi login --api-key <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 \u2014 set via `mushi config projectId <uuid>`"
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
- if (!config.endpoint) {
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 init` to install"
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 mcpPresent = false;
794
- try {
795
- await access(join3(root, ".cursor", "mcp.json"));
796
- mcpPresent = true;
797
- } catch {
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] Cursor MCP config",
801
- ok: mcpPresent,
802
- detail: mcpPresent ? ".cursor/mcp.json present" : "Run `mushi connect` to wire MCP for two-way reporter replies"
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
- async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
892
- const checks = [];
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, resolve: resolve2 } = await import("path");
895
- const { homedir } = await import("os");
896
- const root = resolve2(cwd);
897
- const candidates = [join3(root, ".cursor", "mcp.json"), join3(homedir(), ".cursor", "mcp.json")];
898
- let mcpPath = null;
899
- let mcpRaw = null;
900
- for (const candidate of candidates) {
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
- const raw = await readFile(candidate, "utf8");
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
- if (!mcpPath || !mcpRaw) {
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] mcp.json present",
1011
+ name: "[mcp] MCP config valid JSON",
921
1012
  ok: false,
922
- detail: "No .cursor/mcp.json found in cwd or ~/.cursor/. Run `mushi setup` to create it."
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] mcp.json present",
1027
+ name: "[mcp] MCP config present",
928
1028
  ok: true,
929
- detail: `Found at ${mcpPath}`
1029
+ detail: `Found: ${sources.map((s) => s.path).join(", ")}`
930
1030
  });
931
- let mcpConfig = {};
932
- try {
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 found in mcpServers. Run `mushi setup` to add one."
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(([k]) => k).join(", ")}`
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] MUSHI_API_ENDPOINT set",
976
- ok: anyEndpointSet,
977
- detail: anyEndpointSet ? "MUSHI_API_ENDPOINT is present and looks valid" : "MUSHI_API_ENDPOINT missing or not a Supabase URL. Re-run `mushi setup`."
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 (anyKeyValid && anyEndpointSet && config.apiKey && config.endpoint) {
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
- "CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
1024
- "API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
1025
- "Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
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` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
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) lines.push(` \u2192 Fix: ${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.apiKey,
1375
+ credentials.sdkKey,
1276
1376
  credentials.projectId,
1277
1377
  framework,
1278
1378
  endpoint,
1279
1379
  Boolean(options.yes)
1280
1380
  );
1281
- persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
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
- const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1315
- if (isTTY) return;
1316
- const hasAllFlags = Boolean(
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.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id <uuid-from-console> --api-key mushi_xxx\nYour project ID is the UUID shown in the Projects page of the Mushi admin console.\n"
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
- return {
1354
- projectId: sanitizeSecret(options.projectId),
1355
- apiKey: sanitizeSecret(options.apiKey)
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
- return {
1371
- projectId: sanitizeSecret(existing.projectId),
1372
- apiKey: sanitizeSecret(existing.apiKey)
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 (options.yes) {
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
- let projectId = options.projectId ? sanitizeSecret(options.projectId) : void 0;
1442
- let apiKey;
1443
- if (!projectId) {
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
- const NEW = "__new__";
1457
- const choice = await p.select({
1458
- message: "Choose a project",
1459
- initialValue: projects[0]?.id ?? NEW,
1460
- options: [
1461
- ...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
1462
- { value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
1463
- ]
1464
- });
1465
- if (p.isCancel(choice)) {
1466
- p.cancel("Aborted.");
1467
- process.exit(0);
1468
- }
1469
- if (choice === NEW) {
1470
- const name = await p.text({
1471
- message: "Project name",
1472
- placeholder: "My app",
1473
- validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
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(name)) {
1665
+ if (p.isCancel(choice)) {
1476
1666
  p.cancel("Aborted.");
1477
1667
  process.exit(0);
1478
1668
  }
1479
- const createSpin = p.spinner();
1480
- createSpin.start(`Creating "${name.trim()}"\u2026`);
1481
- try {
1482
- const created = await createProject(endpoint, cliToken, name.trim(), {
1483
- scopes: ["report:write", "mcp:read"]
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
- projectId = created.id;
1486
- apiKey = created.apiKey ?? void 0;
1487
- createSpin.stop(`Created project "${created.name}".`);
1488
- } catch (err) {
1489
- createSpin.stop("Could not create the project.");
1490
- p.log.warn(err instanceof Error ? err.message : String(err));
1491
- return null;
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
- if (projectId && !apiKey) {
1498
- const keySpin = p.spinner();
1499
- keySpin.start("Minting SDK key\u2026");
1500
- try {
1501
- apiKey = await mintProjectKey(endpoint, cliToken, projectId, {
1502
- scopes: ["report:write", "mcp:read"]
1503
- });
1504
- keySpin.stop("SDK key ready.");
1505
- } catch (err) {
1506
- keySpin.stop("Could not mint an API key.");
1507
- p.log.warn(err instanceof Error ? err.message : String(err));
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
- if (!projectId || !apiKey) return null;
1515
- return { apiKey, projectId };
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 = existing.apiKey;
1521
- if (!options.projectId && !options.apiKey && savedProjectId && savedApiKey) {
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 ${sanitizeSecret(savedProjectId).slice(0, 8)}\u2026).`
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 apiCall("/v1/sync/whoami", {
1528
- apiKey: sanitizeSecret(savedApiKey),
1529
- projectId: sanitizeSecret(savedProjectId),
1530
- endpoint
1531
- });
1532
- if (check.ok) {
1533
- checkSpin.stop(`Saved credentials still work (${check.data.project_name}).`);
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
- savedApiKey = void 0;
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
- return { projectId, apiKey };
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.apiKey,
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 persistCliConfig(apiKey, projectId, endpoint) {
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
- saveConfig({ ...existing, apiKey, projectId, endpoint });
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.apiKey,
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. Run: mushi connect --write-env --wire-ide --wait",
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-FAXWVFG2.js");
1807
- await runConnect({
1808
- apiKey: credentials.apiKey,
1809
- projectId: credentials.projectId,
1810
- endpoint,
1811
- cwd: options.cwd,
1812
- writeEnv: true,
1813
- wireIde: true,
1814
- wait: true
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.apiKey,
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
  };