@mushi-mushi/cli 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/init.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import {
2
2
  TEST_REPORT_FETCH_TIMEOUT_MS,
3
3
  apiCall,
4
+ apiKeyHeaders,
4
5
  apiKeyHint,
5
6
  cliSetupDeepLink,
6
7
  consoleUrl,
7
8
  ensureClientId,
9
+ fetchIngestSetup,
8
10
  loadConfig,
11
+ maybeShowTelemetryNotice,
9
12
  normalizeEndpoint,
10
13
  openInBrowser,
11
14
  projectIdHint,
@@ -13,11 +16,13 @@ import {
13
16
  resolveCloudEndpoint,
14
17
  resolveConsoleUrl,
15
18
  resolveConsoleUrlSync,
19
+ sanitizeCliCredentials,
20
+ sanitizeEndpoint,
16
21
  saveConfig
17
- } from "./chunk-L3LUPBFG.js";
22
+ } from "./chunk-TZL3F3GT.js";
18
23
  import {
19
24
  MUSHI_CLI_VERSION
20
- } from "./chunk-MCVRTQAI.js";
25
+ } from "./chunk-LFZ5ZIBI.js";
21
26
  import {
22
27
  FRAMEWORKS,
23
28
  detectFramework,
@@ -26,6 +31,7 @@ import {
26
31
  installCommand,
27
32
  readPackageJson
28
33
  } from "./chunk-AIZRI665.js";
34
+ import "./chunk-3RG5ZIWI.js";
29
35
 
30
36
  // src/init.ts
31
37
  import * as p from "@clack/prompts";
@@ -208,12 +214,12 @@ async function listProjects(endpoint, cliToken) {
208
214
  );
209
215
  });
210
216
  }
211
- async function createProject(endpoint, cliToken, name) {
217
+ async function createProject(endpoint, cliToken, name, opts = {}) {
212
218
  const base = trimTrailingSlash(endpoint);
213
219
  const res = await deviceFetch(`${base}/v1/cli/projects`, {
214
220
  method: "POST",
215
221
  headers: cliAuthHeaders(cliToken),
216
- body: JSON.stringify({ name })
222
+ body: JSON.stringify({ name, ...opts.scopes ? { scopes: opts.scopes } : {} })
217
223
  });
218
224
  const json = await res.json().catch(() => null);
219
225
  if (!res.ok || !json?.ok || !json.data) {
@@ -222,14 +228,17 @@ async function createProject(endpoint, cliToken, name) {
222
228
  const { id, name: created, slug, apiKey } = json.data;
223
229
  return { id, name: created, slug, apiKey: apiKey ?? null };
224
230
  }
225
- async function mintProjectKey(endpoint, cliToken, projectId) {
231
+ async function mintProjectKey(endpoint, cliToken, projectId, opts = {}) {
226
232
  const base = trimTrailingSlash(endpoint);
227
233
  let res;
228
234
  try {
229
235
  res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
230
236
  method: "POST",
231
237
  headers: cliAuthHeaders(cliToken),
232
- body: JSON.stringify({})
238
+ body: JSON.stringify({
239
+ ...opts.scopes ? { scopes: opts.scopes } : {},
240
+ ...opts.label ? { label: opts.label } : {}
241
+ })
233
242
  });
234
243
  } catch (err) {
235
244
  throw new DeviceAuthRequestError(
@@ -429,17 +438,811 @@ function printAuthBanner(userCode, verificationUri) {
429
438
  console.log("");
430
439
  }
431
440
 
441
+ // src/doctor.ts
442
+ function checkCliConfig(config) {
443
+ return [
444
+ {
445
+ 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>`"
448
+ },
449
+ {
450
+ name: "API key configured",
451
+ 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>`"
453
+ },
454
+ {
455
+ name: "Project ID configured",
456
+ ok: Boolean(config.projectId),
457
+ detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
458
+ }
459
+ ];
460
+ }
461
+ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
462
+ try {
463
+ const safeEndpoint = sanitizeEndpoint(endpoint);
464
+ const res = await doFetch(`${safeEndpoint}/health`, {
465
+ signal: AbortSignal.timeout(5e3)
466
+ });
467
+ return {
468
+ name: "Endpoint reachable",
469
+ ok: res.status === 200,
470
+ detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
471
+ };
472
+ } catch (err) {
473
+ const msg = err instanceof Error ? err.message : String(err);
474
+ return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
475
+ }
476
+ }
477
+ async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
478
+ 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);
489
+ let dateHeader = null;
490
+ try {
491
+ const res = await doFetch(`${base}/v1/cli/auth/device/token`, {
492
+ method: "POST",
493
+ headers: { "Content-Type": "application/json" },
494
+ body: JSON.stringify({ device_code: "doctor-probe" }),
495
+ signal: AbortSignal.timeout(8e3)
496
+ });
497
+ dateHeader = res.headers?.get?.("date") ?? null;
498
+ const signInRouteReachable = res.status === 400 || res.status === 429;
499
+ checks.push({
500
+ name: "Sign-in route reachable",
501
+ ok: signInRouteReachable,
502
+ detail: signInRouteReachable ? `POST ${base}/v1/cli/auth/device/token \u2192 ${res.status} (route deployed and answering)` : `POST ${base}/v1/cli/auth/device/token \u2192 ${res.status} \u2014 expected HTTP 400 (invalid device_code) or 429 (slow_down); the API may be down or mis-deployed.`
503
+ });
504
+ } catch (err) {
505
+ checks.push({
506
+ name: "Sign-in route reachable",
507
+ ok: false,
508
+ detail: `Fetch failed: ${err instanceof Error ? err.message : String(err)} \u2014 check network/proxy/firewall to ${base}.`
509
+ });
510
+ }
511
+ if (dateHeader) {
512
+ const serverMs = Date.parse(dateHeader);
513
+ if (!Number.isNaN(serverMs)) {
514
+ const skewSec = Math.round(Math.abs(Date.now() - serverMs) / 1e3);
515
+ const ok = skewSec <= 120;
516
+ checks.push({
517
+ name: "System clock in sync",
518
+ ok,
519
+ warn: !ok && skewSec <= 300,
520
+ detail: ok ? `Local clock within ${skewSec}s of the server.` : `Local clock is ${skewSec}s off the server \u2014 sign-in codes expire fast; fix your system time.`
521
+ });
522
+ }
523
+ }
524
+ if (config.apiKey && config.projectId) {
525
+ try {
526
+ const res = await doFetch(`${base}/v1/sync/whoami`, {
527
+ method: "POST",
528
+ headers: {
529
+ "Content-Type": "application/json",
530
+ ...apiKeyHeaders(config.apiKey, config.projectId)
531
+ },
532
+ body: JSON.stringify({}),
533
+ signal: AbortSignal.timeout(8e3)
534
+ });
535
+ const json = await res.json().catch(() => null);
536
+ const ok = res.ok && json?.ok === true;
537
+ checks.push({
538
+ name: "Saved credentials valid",
539
+ ok,
540
+ detail: ok ? `API key resolves to project "${json?.data?.project_name ?? config.projectId}".` : `${json?.error?.message ?? `whoami \u2192 HTTP ${res.status}`} \u2014 re-run \`npx mushi-mushi\` to sign in again.`
541
+ });
542
+ } catch (err) {
543
+ checks.push({
544
+ name: "Saved credentials valid",
545
+ ok: false,
546
+ detail: `whoami fetch failed: ${err instanceof Error ? err.message : String(err)}`
547
+ });
548
+ }
549
+ } else {
550
+ checks.push({
551
+ name: "Saved credentials valid",
552
+ ok: true,
553
+ warn: true,
554
+ detail: "No saved CLI credentials yet \u2014 normal before the first successful sign-in."
555
+ });
556
+ }
557
+ return checks;
558
+ }
559
+ async function checkSdkInstall(cwd) {
560
+ try {
561
+ const { readFile } = await import("fs/promises");
562
+ const { join: join3, resolve: resolve2 } = await import("path");
563
+ const root = resolve2(cwd);
564
+ const pkgPath = join3(root, "package.json");
565
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
566
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
567
+ const sdks = [
568
+ "@mushi-mushi/react",
569
+ "@mushi-mushi/web",
570
+ "@mushi-mushi/core",
571
+ "@mushi-mushi/react-native"
572
+ ];
573
+ const installed = sdks.filter((s) => deps[s]);
574
+ return {
575
+ name: "SDK installed in this repo",
576
+ 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"
578
+ };
579
+ } catch {
580
+ return null;
581
+ }
582
+ }
583
+ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
584
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
585
+ return [
586
+ {
587
+ name: "Server preflight",
588
+ ok: false,
589
+ detail: "Need projectId, apiKey, and endpoint. Run `mushi login` and `mushi config projectId <uuid>`."
590
+ }
591
+ ];
592
+ }
593
+ try {
594
+ const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
595
+ const res = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/preflight`, {
596
+ headers: apiKeyHeaders(apiKey, projectId),
597
+ signal: AbortSignal.timeout(8e3)
598
+ });
599
+ if (res.ok) {
600
+ const body = await res.json();
601
+ const serverChecks = body.data?.checks ?? [];
602
+ return serverChecks.map((sc) => ({
603
+ name: `[server] ${sc.label}`,
604
+ ok: sc.ready,
605
+ detail: sc.ready ? "" : sc.hint
606
+ }));
607
+ }
608
+ if (res.status === 403) {
609
+ let errCode;
610
+ try {
611
+ const body = await res.json();
612
+ errCode = body?.error?.code;
613
+ } catch {
614
+ }
615
+ if (errCode === "INSUFFICIENT_SCOPE") {
616
+ return [
617
+ {
618
+ name: "Server preflight",
619
+ ok: true,
620
+ detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and run admin checks."
621
+ }
622
+ ];
623
+ }
624
+ }
625
+ const text2 = await res.text().catch(() => "");
626
+ return [
627
+ {
628
+ name: "Server preflight",
629
+ ok: false,
630
+ detail: `HTTP ${res.status}: ${text2.slice(0, 120)}`
631
+ }
632
+ ];
633
+ } catch (err) {
634
+ const msg = err instanceof Error ? err.message : String(err);
635
+ return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
636
+ }
637
+ }
638
+ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
639
+ if (!config.apiKey || !config.endpoint) {
640
+ return [
641
+ {
642
+ name: "Ingest setup",
643
+ ok: false,
644
+ detail: "Need apiKey and endpoint. Run `mushi connect`."
645
+ }
646
+ ];
647
+ }
648
+ try {
649
+ const data = await fetchIngestSetup(
650
+ { endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
651
+ doFetch
652
+ );
653
+ if (!data) {
654
+ return [
655
+ {
656
+ name: "Ingest setup",
657
+ ok: false,
658
+ detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload"
659
+ }
660
+ ];
661
+ }
662
+ const steps = data.steps ?? [];
663
+ const checks = steps.filter((s) => s.required).map((s) => ({
664
+ name: `[ingest] ${s.label}`,
665
+ ok: s.complete,
666
+ detail: s.complete ? "" : s.hint ?? ""
667
+ }));
668
+ const diag = data.diagnostic;
669
+ if (diag?.last_sdk_seen_at) {
670
+ checks.push({
671
+ name: "[ingest] Last SDK heartbeat",
672
+ ok: true,
673
+ detail: `${diag.last_sdk_seen_at}${diag.last_sdk_endpoint_host ? ` @ ${diag.last_sdk_endpoint_host}` : ""}`
674
+ });
675
+ }
676
+ return checks.length > 0 ? checks : [{ name: "Ingest setup", ok: false, detail: "Empty response from /v1/sync/ingest-setup" }];
677
+ } catch (err) {
678
+ const msg = err instanceof Error ? err.message : String(err);
679
+ return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
680
+ }
681
+ }
682
+ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
683
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
684
+ return [
685
+ {
686
+ name: "QA stories health",
687
+ ok: false,
688
+ detail: "Need projectId, apiKey, and endpoint for QA story checks."
689
+ }
690
+ ];
691
+ }
692
+ const checks = [];
693
+ try {
694
+ const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
695
+ const headers = apiKeyHeaders(apiKey, projectId);
696
+ const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/qa-coverage`, {
697
+ headers,
698
+ signal: AbortSignal.timeout(8e3)
699
+ });
700
+ if (!storiesRes.ok) {
701
+ checks.push({
702
+ name: "[qa] Fetch QA stories",
703
+ ok: false,
704
+ detail: `HTTP ${storiesRes.status}`
705
+ });
706
+ return checks;
707
+ }
708
+ const storiesBody = await storiesRes.json();
709
+ const stories = storiesBody.data?.coverage ?? [];
710
+ const enabled = stories.filter((s) => s.enabled);
711
+ if (enabled.length === 0) {
712
+ checks.push({
713
+ name: "[qa] Enabled QA stories",
714
+ ok: true,
715
+ detail: "No enabled stories \u2014 create one at /qa-coverage"
716
+ });
717
+ return checks;
718
+ }
719
+ checks.push({
720
+ name: "[qa] Enabled QA stories",
721
+ ok: true,
722
+ detail: `${enabled.length} enabled story/stories configured`
723
+ });
724
+ const slackRes = await doFetch(
725
+ `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
726
+ {
727
+ method: "POST",
728
+ headers,
729
+ signal: AbortSignal.timeout(6e3)
730
+ }
731
+ );
732
+ const slackBody = slackRes.ok ? await slackRes.json() : null;
733
+ const slackOk = slackBody?.status === "ok";
734
+ checks.push({
735
+ name: "[qa] Slack notifications configured",
736
+ ok: slackOk,
737
+ detail: slackOk ? "Slack connected \u2014 failures will notify your channel" : "Slack not connected \u2014 you won't be notified when stories fail. Visit /integrations \u2192 Add to Slack."
738
+ });
739
+ const fcRes = await doFetch(
740
+ `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
741
+ {
742
+ method: "POST",
743
+ headers,
744
+ signal: AbortSignal.timeout(6e3)
745
+ }
746
+ );
747
+ const fcBody = fcRes.ok ? await fcRes.json() : null;
748
+ const hasFirecrawlStories = enabled.some(
749
+ (s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
750
+ );
751
+ if (hasFirecrawlStories) {
752
+ const fcOk = fcBody?.status === "ok";
753
+ checks.push({
754
+ name: "[qa] Firecrawl API key configured",
755
+ ok: fcOk,
756
+ detail: fcOk ? "Firecrawl key is resolvable \u2014 stories will run without Unauthorized errors" : "No Firecrawl key found \u2014 enabled stories using firecrawl_actions will 401. Add a key at /integrations \u2192 BYOK keys."
757
+ });
758
+ }
759
+ } catch (err) {
760
+ const msg = err instanceof Error ? err.message : String(err);
761
+ checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
762
+ }
763
+ return checks;
764
+ }
765
+ async function checkHostAppWiring(cwd) {
766
+ const checks = [];
767
+ try {
768
+ const { readFile, access } = await import("fs/promises");
769
+ const { join: join3, resolve: resolve2 } = await import("path");
770
+ const root = resolve2(cwd);
771
+ const pkgPath = join3(root, "package.json");
772
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
773
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
774
+ const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
775
+ const envCandidates = [".env.local", ".env"];
776
+ let envContent = "";
777
+ for (const f of envCandidates) {
778
+ try {
779
+ envContent = await readFile(join3(root, f), "utf8");
780
+ break;
781
+ } catch {
782
+ }
783
+ }
784
+ const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
785
+ const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(
786
+ envContent
787
+ );
788
+ checks.push({
789
+ name: "[host] Mushi env vars in .env.local",
790
+ ok: hasProjectId && hasApiKey,
791
+ 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
+ });
793
+ let mcpPresent = false;
794
+ try {
795
+ await access(join3(root, ".cursor", "mcp.json"));
796
+ mcpPresent = true;
797
+ } catch {
798
+ }
799
+ 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"
803
+ });
804
+ if (isCapHybrid) {
805
+ const hasWebSdk = Boolean(deps["@mushi-mushi/web"] || deps["@mushi-mushi/react"]);
806
+ checks.push({
807
+ name: "[host] Capacitor hybrid \u2014 WebView SDK",
808
+ ok: hasWebSdk,
809
+ detail: hasWebSdk ? "Use @mushi-mushi/web or @mushi-mushi/react in the WebView (initMushi in main.tsx)" : "Install @mushi-mushi/web for Capacitor WebView reporting"
810
+ });
811
+ checks.push({
812
+ name: "[host] Capacitor native plugin (optional)",
813
+ ok: true,
814
+ detail: deps["@mushi-mushi/capacitor"] ? `@mushi-mushi/capacitor@${deps["@mushi-mushi/capacitor"]} installed` : "Optional: @mushi-mushi/capacitor for native shell parity \u2014 WebView SDK covers most flows"
815
+ });
816
+ }
817
+ } catch {
818
+ checks.push({
819
+ name: "[host] Host app detection",
820
+ ok: false,
821
+ detail: "No package.json in cwd \u2014 run from your app repo root"
822
+ });
823
+ }
824
+ return checks;
825
+ }
826
+ var HASH_ROUTER_PATTERNS = [
827
+ /\bHashRouter\b/,
828
+ /\bcreateHashRouter\b/,
829
+ /location\.hash\b/,
830
+ /addEventListener\(['"]hashchange['"]/
831
+ ];
832
+ var HASH_TEMPLATE_PATTERN = /['"`]#\//;
833
+ async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
834
+ const { readdir, readFile } = await import("fs/promises");
835
+ const { join: join3 } = await import("path");
836
+ let count = 0;
837
+ async function walk(d, depth) {
838
+ if (depth > maxDepth || count > maxFiles) return null;
839
+ let entries = [];
840
+ try {
841
+ entries = await readdir(d, { withFileTypes: true });
842
+ } catch {
843
+ return null;
844
+ }
845
+ for (const e of entries) {
846
+ if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue;
847
+ const full = join3(d, e.name);
848
+ if (e.isDirectory()) {
849
+ const found = await walk(full, depth + 1);
850
+ if (found) return found;
851
+ } else if (/\.[cm]?[jt]sx?$/.test(e.name)) {
852
+ count++;
853
+ try {
854
+ const src = await readFile(full, "utf8");
855
+ if (patterns.some((p2) => p2.test(src))) return full;
856
+ } catch {
857
+ }
858
+ }
859
+ }
860
+ return null;
861
+ }
862
+ const { resolve: resolve2 } = await import("path");
863
+ for (const sub of ["src", "app", "pages", "lib", "components"]) {
864
+ const found = await walk(join3(resolve2(dir), sub), 0);
865
+ if (found) return found;
866
+ }
867
+ return walk(resolve2(dir), 0);
868
+ }
869
+ async function checkHashRouterCapture(cwd) {
870
+ const hashFile = await scanDirForPattern(cwd, HASH_ROUTER_PATTERNS);
871
+ if (!hashFile) return [];
872
+ const templateFile = await scanDirForPattern(cwd, [HASH_TEMPLATE_PATTERN]);
873
+ if (templateFile) {
874
+ return [
875
+ {
876
+ name: "[host] Hash-router route capture",
877
+ ok: true,
878
+ detail: `Hash-router detected (${hashFile}) and /#/ route templates found in Mushi config \u2014 routes will be captured correctly.`
879
+ }
880
+ ];
881
+ }
882
+ return [
883
+ {
884
+ name: "[host] Hash-router route capture",
885
+ ok: true,
886
+ warn: true,
887
+ detail: `Hash-router detected (${hashFile}) but no /#/ route templates found in your Mushi init call. Add discoverInventory.routeTemplates with /#/-prefixed patterns (e.g. ['/#/article/[slug]']) so Mushi captures parametric hash routes correctly \u2014 without these, all hash navigations appear as a single "/#/" route in the inventory.`
888
+ }
889
+ ];
890
+ }
891
+ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
892
+ const checks = [];
893
+ 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) {
901
+ 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;
915
+ } catch {
916
+ }
917
+ }
918
+ if (!mcpPath || !mcpRaw) {
919
+ checks.push({
920
+ name: "[mcp] mcp.json present",
921
+ ok: false,
922
+ detail: "No .cursor/mcp.json found in cwd or ~/.cursor/. Run `mushi setup` to create it."
923
+ });
924
+ return checks;
925
+ }
926
+ checks.push({
927
+ name: "[mcp] mcp.json present",
928
+ ok: true,
929
+ detail: `Found at ${mcpPath}`
930
+ });
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-")
945
+ );
946
+ if (mushiEntries.length === 0) {
947
+ checks.push({
948
+ name: "[mcp] mushi server entry",
949
+ ok: false,
950
+ detail: "No mushi or mushi-* server found in mcpServers. Run `mushi setup` to add one."
951
+ });
952
+ return checks;
953
+ }
954
+ checks.push({
955
+ name: "[mcp] mushi server entry",
956
+ 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."
973
+ });
974
+ 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`."
978
+ });
979
+ if (anyKeyValid && anyEndpointSet && config.apiKey && config.endpoint) {
980
+ try {
981
+ const { endpoint, apiKey } = sanitizeCliCredentials(config);
982
+ const res = await doFetch(`${endpoint}/v1/admin/mcp/account-overview`, {
983
+ headers: apiKeyHeaders(apiKey, config.projectId),
984
+ signal: AbortSignal.timeout(6e3)
985
+ });
986
+ if (res.ok) {
987
+ const body = await res.json();
988
+ const projectCount = body?.data?.total ?? 0;
989
+ checks.push({
990
+ name: "[mcp] account-overview reachable",
991
+ ok: true,
992
+ detail: `Key is valid; ${projectCount} accessible project${projectCount === 1 ? "" : "s"}`
993
+ });
994
+ } else {
995
+ let detail = `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`;
996
+ try {
997
+ const errBody = await res.json();
998
+ const nested = errBody["error"];
999
+ const errCode = nested?.["code"] ?? errBody["code"] ?? "";
1000
+ if (errCode === "INSUFFICIENT_SCOPE") {
1001
+ detail = `Key has report:write scope only \u2014 run \`mushi login --upgrade-scope\` then \`mushi setup\` to get mcp:read.`;
1002
+ }
1003
+ } catch {
1004
+ }
1005
+ checks.push({
1006
+ name: "[mcp] account-overview reachable",
1007
+ ok: false,
1008
+ detail
1009
+ });
1010
+ }
1011
+ } catch (err) {
1012
+ const msg = err instanceof Error ? err.message : String(err);
1013
+ checks.push({
1014
+ name: "[mcp] account-overview reachable",
1015
+ ok: false,
1016
+ detail: `Probe failed: ${msg}. Check MUSHI_API_ENDPOINT and network connectivity.`
1017
+ });
1018
+ }
1019
+ }
1020
+ return checks;
1021
+ }
1022
+ 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>`.",
1026
+ "Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
1027
+ "[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
1028
+ "[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."
1030
+ };
1031
+ function fixHintForCheck(name) {
1032
+ if (FIX_HINTS[name]) return FIX_HINTS[name];
1033
+ if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
1034
+ if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
1035
+ if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
1036
+ return void 0;
1037
+ }
1038
+ async function checkVersionDrift(cwd, doFetch) {
1039
+ const checks = [];
1040
+ const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version.js");
1041
+ if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
1042
+ const parse2 = (v) => {
1043
+ const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
1044
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : [0, 0, 0];
1045
+ };
1046
+ const isBehind = (installed, latest) => {
1047
+ const [a1, a2, a3] = parse2(installed);
1048
+ const [b1, b2, b3] = parse2(latest);
1049
+ return a1 !== b1 ? a1 < b1 : a2 !== b2 ? a2 < b2 : a3 < b3;
1050
+ };
1051
+ try {
1052
+ const res = await doFetch("https://registry.npmjs.org/@mushi-mushi/cli/latest", {
1053
+ signal: AbortSignal.timeout(3e3)
1054
+ });
1055
+ if (res.ok) {
1056
+ const { version: latest } = await res.json();
1057
+ if (latest && isBehind(MUSHI_CLI_VERSION2, latest)) {
1058
+ checks.push({
1059
+ name: "CLI version",
1060
+ ok: true,
1061
+ warn: true,
1062
+ detail: `Installed ${MUSHI_CLI_VERSION2}, latest is ${latest}. Run \`mushi upgrade\` (or npm i -g @mushi-mushi/cli@latest).`
1063
+ });
1064
+ }
1065
+ }
1066
+ } catch {
1067
+ }
1068
+ try {
1069
+ const { readFile } = await import("fs/promises");
1070
+ const { join: join3 } = await import("path");
1071
+ const pkg = JSON.parse(await readFile(join3(cwd, "package.json"), "utf8"));
1072
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1073
+ const candidates = Object.entries(deps).filter(([name]) => name.startsWith("@mushi-mushi/")).filter(([, v]) => /^[~^]?\d/.test(v));
1074
+ const drifted = (await Promise.all(
1075
+ candidates.map(async ([name, v]) => {
1076
+ try {
1077
+ const res = await doFetch(`https://registry.npmjs.org/${name}/latest`, {
1078
+ signal: AbortSignal.timeout(3e3)
1079
+ });
1080
+ if (!res.ok) return null;
1081
+ const { version: latest } = await res.json();
1082
+ return latest && isBehind(v, latest) ? [name, v, latest] : null;
1083
+ } catch {
1084
+ return null;
1085
+ }
1086
+ })
1087
+ )).filter((d) => d !== null);
1088
+ if (drifted.length > 0) {
1089
+ checks.push({
1090
+ name: "SDK version drift",
1091
+ ok: true,
1092
+ warn: true,
1093
+ detail: `${drifted.map(([n, v, l]) => `${n}@${v} (latest ${l})`).join(", ")} \u2014 behind npm latest. Run \`mushi upgrade\` in this repo.`
1094
+ });
1095
+ }
1096
+ } catch {
1097
+ }
1098
+ return checks;
1099
+ }
1100
+ async function checkPipelineDoctor(config, doFetch) {
1101
+ if (!config.endpoint || !config.apiKey) return [];
1102
+ try {
1103
+ const res = await doFetch(`${config.endpoint.replace(/\/$/, "")}/v1/admin/doctor`, {
1104
+ headers: apiKeyHeaders(config.apiKey),
1105
+ signal: AbortSignal.timeout(1e4)
1106
+ });
1107
+ if (res.status === 401 || res.status === 403) return [];
1108
+ if (!res.ok) {
1109
+ return [{
1110
+ name: "[pipeline] Server doctor",
1111
+ ok: true,
1112
+ warn: true,
1113
+ detail: `GET /v1/admin/doctor returned HTTP ${res.status} \u2014 pipeline health unknown.`
1114
+ }];
1115
+ }
1116
+ const body = await res.json();
1117
+ return (body.data?.checks ?? []).map((ch) => ({
1118
+ name: `[pipeline] ${ch.name}`,
1119
+ ok: ch.status !== "fail",
1120
+ warn: ch.status === "warn",
1121
+ detail: ch.hint ? `${ch.summary} \u2192 Fix: ${ch.hint}` : ch.summary
1122
+ }));
1123
+ } catch {
1124
+ return [{
1125
+ name: "[pipeline] Server doctor",
1126
+ ok: true,
1127
+ warn: true,
1128
+ detail: "Could not reach /v1/admin/doctor (timeout) \u2014 pipeline health unknown."
1129
+ }];
1130
+ }
1131
+ }
1132
+ async function runDoctor(config, options = {}) {
1133
+ const doFetch = options.fetch ?? globalThis.fetch;
1134
+ const checks = [];
1135
+ const isFull = options.full === true;
1136
+ const runServer = isFull || options.server !== false;
1137
+ const runIngest = isFull || options.ingest !== false;
1138
+ checks.push(...checkCliConfig(config));
1139
+ if (config.endpoint) {
1140
+ checks.push(await checkEndpointReachability(config.endpoint, doFetch));
1141
+ }
1142
+ const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
1143
+ if (sdkCheck) checks.push(sdkCheck);
1144
+ if (sdkCheck?.ok && !sdkCheck.warn) {
1145
+ const { findSdkImport } = await import("./snippet-inject-7ABYE57O.js");
1146
+ const imported = await findSdkImport(options.cwd ?? process.cwd());
1147
+ checks.push(
1148
+ imported ? { name: "SDK init snippet wired", ok: true, detail: `Found SDK usage in ${imported.file}` } : {
1149
+ name: "SDK init snippet wired",
1150
+ ok: true,
1151
+ warn: true,
1152
+ detail: "Package installed but no @mushi-mushi import found in src/app \u2014 paste the init snippet from `mushi init` into your entry file."
1153
+ }
1154
+ );
1155
+ }
1156
+ if (runServer) {
1157
+ const serverChecks = await checkServerPreflight(config, doFetch);
1158
+ checks.push(...serverChecks);
1159
+ checks.push(...await checkPipelineDoctor(config, doFetch));
1160
+ }
1161
+ checks.push(...await checkVersionDrift(options.cwd ?? process.cwd(), doFetch));
1162
+ if (runIngest) {
1163
+ const ingestChecks = await checkIngestSetup(config, doFetch);
1164
+ checks.push(...ingestChecks);
1165
+ }
1166
+ if (isFull || options.qaStories) {
1167
+ const qaChecks = await checkQaStoriesHealth(config, doFetch);
1168
+ checks.push(...qaChecks);
1169
+ }
1170
+ if (isFull || options.hostApp) {
1171
+ const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
1172
+ checks.push(...hostChecks);
1173
+ const hashChecks = await checkHashRouterCapture(options.cwd ?? process.cwd());
1174
+ checks.push(...hashChecks);
1175
+ }
1176
+ if (isFull || options.mcp) {
1177
+ const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
1178
+ checks.push(...mcpChecks);
1179
+ }
1180
+ if (isFull || options.auth) {
1181
+ checks.push(...await checkCliAuthPath(config, doFetch));
1182
+ }
1183
+ const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
1184
+ if (heartbeatPassed) {
1185
+ const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
1186
+ if (idx >= 0) {
1187
+ checks[idx] = {
1188
+ ...checks[idx],
1189
+ ok: true,
1190
+ warn: true,
1191
+ detail: "No @mushi-mushi/* found in cwd package.json \u2014 but live heartbeats confirm the SDK is active. Run from your app repo or install with `mushi init`."
1192
+ };
1193
+ }
1194
+ }
1195
+ return { checks, ready: checks.every((c) => c.ok) };
1196
+ }
1197
+ function formatDoctorResult(result) {
1198
+ const PASS = "OK";
1199
+ const WARN = "WARN";
1200
+ const FAIL = "FAIL";
1201
+ const lines = [];
1202
+ for (const c of result.checks) {
1203
+ const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
1204
+ lines.push(`${icon} ${c.name}`);
1205
+ if (c.detail) lines.push(` ${c.detail}`);
1206
+ if (!c.ok) {
1207
+ const hint = fixHintForCheck(c.name);
1208
+ if (hint) lines.push(` \u2192 Fix: ${hint}`);
1209
+ }
1210
+ }
1211
+ const failed = result.checks.filter((c) => !c.ok);
1212
+ const warned = result.checks.filter((c) => c.ok && c.warn);
1213
+ if (failed.length === 0) {
1214
+ if (warned.length > 0) {
1215
+ lines.push(
1216
+ `
1217
+ All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`
1218
+ );
1219
+ } else {
1220
+ lines.push("\nAll checks passed. The CLI is ready.");
1221
+ }
1222
+ } else {
1223
+ lines.push(`
1224
+ ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
1225
+ lines.push("Fix the items above and re-run `mushi doctor`.");
1226
+ }
1227
+ return lines.join("\n");
1228
+ }
1229
+
432
1230
  // src/init.ts
433
1231
  var ENV_FILES = [".env.local", ".env"];
434
1232
  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;
435
1233
  var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
436
1234
  async function runInit(options = {}) {
437
1235
  const cwd = options.cwd ?? process.cwd();
1236
+ if (options.audit) {
1237
+ await runInitAudit(cwd);
1238
+ return;
1239
+ }
438
1240
  ensureInteractiveOrBailOut(options);
439
1241
  p.intro("Mushi setup");
440
1242
  await printFreshnessHint();
441
1243
  warnIfWorkspaceRoot(cwd);
442
1244
  const pkg = readPackageJson(cwd);
1245
+ maybeSuggestAudit(pkg);
443
1246
  if (!pkg) {
444
1247
  p.log.warn("No package.json found in this directory.");
445
1248
  const cont = await p.confirm({
@@ -484,6 +1287,29 @@ async function runInit(options = {}) {
484
1287
  await maybeOfferConnect(credentials, options, consoleBase);
485
1288
  p.outro("Setup complete.");
486
1289
  }
1290
+ async function runInitAudit(cwd) {
1291
+ p.intro("Mushi setup audit");
1292
+ const config = loadConfig();
1293
+ const result = await runDoctor(config, { cwd, hostApp: true, mcp: true });
1294
+ p.log.message(formatDoctorResult(result));
1295
+ if (result.ready) {
1296
+ p.outro("Audit complete \u2014 setup looks healthy.");
1297
+ } else {
1298
+ p.outro("Audit found issues \u2014 each FAIL line above has a \u2192 Fix hint. `mushi doctor --fix` applies the safe ones.");
1299
+ process.exitCode = 1;
1300
+ }
1301
+ }
1302
+ function maybeSuggestAudit(pkg) {
1303
+ const deps = {
1304
+ ...pkg?.dependencies,
1305
+ ...pkg?.devDependencies
1306
+ };
1307
+ if (Object.keys(deps).some((d) => d.startsWith("@mushi-mushi/"))) {
1308
+ p.log.info(
1309
+ "A Mushi SDK is already installed here. To health-check the existing setup instead, run `npx mushi-mushi --audit`."
1310
+ );
1311
+ }
1312
+ }
487
1313
  function ensureInteractiveOrBailOut(options) {
488
1314
  const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
489
1315
  if (isTTY) return;
@@ -530,6 +1356,7 @@ async function acquireCredentials(options, consoleBase, endpoint) {
530
1356
  };
531
1357
  }
532
1358
  const existing = loadConfig();
1359
+ maybeShowTelemetryNotice(existing);
533
1360
  if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
534
1361
  const reuse = options.yes ? true : await p.confirm({
535
1362
  message: "Use the Mushi credentials saved from your last sign-in?",
@@ -652,7 +1479,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
652
1479
  const createSpin = p.spinner();
653
1480
  createSpin.start(`Creating "${name.trim()}"\u2026`);
654
1481
  try {
655
- const created = await createProject(endpoint, cliToken, name.trim());
1482
+ const created = await createProject(endpoint, cliToken, name.trim(), {
1483
+ scopes: ["report:write", "mcp:read"]
1484
+ });
656
1485
  projectId = created.id;
657
1486
  apiKey = created.apiKey ?? void 0;
658
1487
  createSpin.stop(`Created project "${created.name}".`);
@@ -669,7 +1498,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
669
1498
  const keySpin = p.spinner();
670
1499
  keySpin.start("Minting SDK key\u2026");
671
1500
  try {
672
- apiKey = await mintProjectKey(endpoint, cliToken, projectId);
1501
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId, {
1502
+ scopes: ["report:write", "mcp:read"]
1503
+ });
673
1504
  keySpin.stop("SDK key ready.");
674
1505
  } catch (err) {
675
1506
  keySpin.stop("Could not mint an API key.");
@@ -916,7 +1747,7 @@ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
916
1747
  }
917
1748
  async function maybeInjectSnippet(cwd, framework, options) {
918
1749
  if (options.yes || !process.stdin.isTTY) return;
919
- const { ENTRY_CANDIDATES, injectSnippet, MUSHI_MARKER_START } = await import("./snippet-inject-NDAPEL2T.js");
1750
+ const { ENTRY_CANDIDATES, injectSnippet, MUSHI_MARKER_START } = await import("./snippet-inject-7ABYE57O.js");
920
1751
  const candidates = ENTRY_CANDIDATES[framework.id];
921
1752
  if (!candidates) return;
922
1753
  const { readFile, writeFile } = await import("fs/promises");
@@ -972,7 +1803,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
972
1803
  if (p.isCancel(answer) || !answer) return;
973
1804
  const endpoint = resolveCloudEndpoint(options.endpoint);
974
1805
  try {
975
- const { runConnect } = await import("./connect-HCWJ6NXP.js");
1806
+ const { runConnect } = await import("./connect-FAXWVFG2.js");
976
1807
  await runConnect({
977
1808
  apiKey: credentials.apiKey,
978
1809
  projectId: credentials.projectId,