@mushi-mushi/cli 0.25.0 → 0.26.1

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,10 +1,12 @@
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,
9
11
  normalizeEndpoint,
10
12
  openInBrowser,
@@ -13,8 +15,13 @@ import {
13
15
  resolveCloudEndpoint,
14
16
  resolveConsoleUrl,
15
17
  resolveConsoleUrlSync,
18
+ sanitizeCliCredentials,
19
+ sanitizeEndpoint,
16
20
  saveConfig
17
- } from "./chunk-2WHQONHK.js";
21
+ } from "./chunk-QOY3DULB.js";
22
+ import {
23
+ MUSHI_CLI_VERSION
24
+ } from "./chunk-PCTMFRKN.js";
18
25
  import {
19
26
  FRAMEWORKS,
20
27
  detectFramework,
@@ -23,9 +30,6 @@ import {
23
30
  installCommand,
24
31
  readPackageJson
25
32
  } from "./chunk-AIZRI665.js";
26
- import {
27
- MUSHI_CLI_VERSION
28
- } from "./chunk-SS42XQ57.js";
29
33
 
30
34
  // src/init.ts
31
35
  import * as p from "@clack/prompts";
@@ -208,12 +212,12 @@ async function listProjects(endpoint, cliToken) {
208
212
  );
209
213
  });
210
214
  }
211
- async function createProject(endpoint, cliToken, name) {
215
+ async function createProject(endpoint, cliToken, name, opts = {}) {
212
216
  const base = trimTrailingSlash(endpoint);
213
217
  const res = await deviceFetch(`${base}/v1/cli/projects`, {
214
218
  method: "POST",
215
219
  headers: cliAuthHeaders(cliToken),
216
- body: JSON.stringify({ name })
220
+ body: JSON.stringify({ name, ...opts.scopes ? { scopes: opts.scopes } : {} })
217
221
  });
218
222
  const json = await res.json().catch(() => null);
219
223
  if (!res.ok || !json?.ok || !json.data) {
@@ -222,14 +226,17 @@ async function createProject(endpoint, cliToken, name) {
222
226
  const { id, name: created, slug, apiKey } = json.data;
223
227
  return { id, name: created, slug, apiKey: apiKey ?? null };
224
228
  }
225
- async function mintProjectKey(endpoint, cliToken, projectId) {
229
+ async function mintProjectKey(endpoint, cliToken, projectId, opts = {}) {
226
230
  const base = trimTrailingSlash(endpoint);
227
231
  let res;
228
232
  try {
229
233
  res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
230
234
  method: "POST",
231
235
  headers: cliAuthHeaders(cliToken),
232
- body: JSON.stringify({})
236
+ body: JSON.stringify({
237
+ ...opts.scopes ? { scopes: opts.scopes } : {},
238
+ ...opts.label ? { label: opts.label } : {}
239
+ })
233
240
  });
234
241
  } catch (err) {
235
242
  throw new DeviceAuthRequestError(
@@ -429,17 +436,744 @@ function printAuthBanner(userCode, verificationUri) {
429
436
  console.log("");
430
437
  }
431
438
 
439
+ // src/doctor.ts
440
+ function checkCliConfig(config) {
441
+ return [
442
+ {
443
+ name: "CLI config file",
444
+ ok: Boolean(config.endpoint),
445
+ detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint \u2014 set MUSHI_API_ENDPOINT, run `mushi connect`, or `mushi config endpoint <url>`"
446
+ },
447
+ {
448
+ name: "API key configured",
449
+ ok: Boolean(config.apiKey),
450
+ 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>`"
451
+ },
452
+ {
453
+ name: "Project ID configured",
454
+ ok: Boolean(config.projectId),
455
+ detail: config.projectId ? `projectId=${config.projectId}` : "No default project \u2014 set via `mushi config projectId <uuid>`"
456
+ }
457
+ ];
458
+ }
459
+ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
460
+ try {
461
+ const safeEndpoint = sanitizeEndpoint(endpoint);
462
+ const res = await doFetch(`${safeEndpoint}/health`, {
463
+ signal: AbortSignal.timeout(5e3)
464
+ });
465
+ return {
466
+ name: "Endpoint reachable",
467
+ ok: res.status === 200,
468
+ detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
469
+ };
470
+ } catch (err) {
471
+ const msg = err instanceof Error ? err.message : String(err);
472
+ return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
473
+ }
474
+ }
475
+ async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
476
+ const checks = [];
477
+ if (!config.endpoint) {
478
+ return [
479
+ {
480
+ name: "Sign-in route reachable",
481
+ ok: false,
482
+ detail: "No endpoint configured \u2014 run `npx mushi-mushi` or pass MUSHI_API_ENDPOINT."
483
+ }
484
+ ];
485
+ }
486
+ const base = sanitizeEndpoint(config.endpoint);
487
+ let dateHeader = null;
488
+ try {
489
+ const res = await doFetch(`${base}/v1/cli/auth/device/token`, {
490
+ method: "POST",
491
+ headers: { "Content-Type": "application/json" },
492
+ body: JSON.stringify({ device_code: "doctor-probe" }),
493
+ signal: AbortSignal.timeout(8e3)
494
+ });
495
+ dateHeader = res.headers?.get?.("date") ?? null;
496
+ const signInRouteReachable = res.status === 400 || res.status === 429;
497
+ checks.push({
498
+ name: "Sign-in route reachable",
499
+ ok: signInRouteReachable,
500
+ 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.`
501
+ });
502
+ } catch (err) {
503
+ checks.push({
504
+ name: "Sign-in route reachable",
505
+ ok: false,
506
+ detail: `Fetch failed: ${err instanceof Error ? err.message : String(err)} \u2014 check network/proxy/firewall to ${base}.`
507
+ });
508
+ }
509
+ if (dateHeader) {
510
+ const serverMs = Date.parse(dateHeader);
511
+ if (!Number.isNaN(serverMs)) {
512
+ const skewSec = Math.round(Math.abs(Date.now() - serverMs) / 1e3);
513
+ const ok = skewSec <= 120;
514
+ checks.push({
515
+ name: "System clock in sync",
516
+ ok,
517
+ warn: !ok && skewSec <= 300,
518
+ 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.`
519
+ });
520
+ }
521
+ }
522
+ if (config.apiKey && config.projectId) {
523
+ try {
524
+ const res = await doFetch(`${base}/v1/sync/whoami`, {
525
+ method: "POST",
526
+ headers: {
527
+ "Content-Type": "application/json",
528
+ ...apiKeyHeaders(config.apiKey, config.projectId)
529
+ },
530
+ body: JSON.stringify({}),
531
+ signal: AbortSignal.timeout(8e3)
532
+ });
533
+ const json = await res.json().catch(() => null);
534
+ const ok = res.ok && json?.ok === true;
535
+ checks.push({
536
+ name: "Saved credentials valid",
537
+ ok,
538
+ 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.`
539
+ });
540
+ } catch (err) {
541
+ checks.push({
542
+ name: "Saved credentials valid",
543
+ ok: false,
544
+ detail: `whoami fetch failed: ${err instanceof Error ? err.message : String(err)}`
545
+ });
546
+ }
547
+ } else {
548
+ checks.push({
549
+ name: "Saved credentials valid",
550
+ ok: true,
551
+ warn: true,
552
+ detail: "No saved CLI credentials yet \u2014 normal before the first successful sign-in."
553
+ });
554
+ }
555
+ return checks;
556
+ }
557
+ async function checkSdkInstall(cwd) {
558
+ try {
559
+ const { readFile } = await import("fs/promises");
560
+ const { join: join3, resolve: resolve2 } = await import("path");
561
+ const root = resolve2(cwd);
562
+ const pkgPath = join3(root, "package.json");
563
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
564
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
565
+ const sdks = [
566
+ "@mushi-mushi/react",
567
+ "@mushi-mushi/web",
568
+ "@mushi-mushi/core",
569
+ "@mushi-mushi/react-native"
570
+ ];
571
+ const installed = sdks.filter((s) => deps[s]);
572
+ return {
573
+ name: "SDK installed in this repo",
574
+ ok: installed.length > 0,
575
+ detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
576
+ };
577
+ } catch {
578
+ return null;
579
+ }
580
+ }
581
+ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
582
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
583
+ return [
584
+ {
585
+ name: "Server preflight",
586
+ ok: false,
587
+ detail: "Need projectId, apiKey, and endpoint. Run `mushi login` and `mushi config projectId <uuid>`."
588
+ }
589
+ ];
590
+ }
591
+ try {
592
+ const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
593
+ const res = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/preflight`, {
594
+ headers: apiKeyHeaders(apiKey, projectId),
595
+ signal: AbortSignal.timeout(8e3)
596
+ });
597
+ if (res.ok) {
598
+ const body = await res.json();
599
+ const serverChecks = body.data?.checks ?? [];
600
+ return serverChecks.map((sc) => ({
601
+ name: `[server] ${sc.label}`,
602
+ ok: sc.ready,
603
+ detail: sc.ready ? "" : sc.hint
604
+ }));
605
+ }
606
+ if (res.status === 403) {
607
+ let errCode;
608
+ try {
609
+ const body = await res.json();
610
+ errCode = body?.error?.code;
611
+ } catch {
612
+ }
613
+ if (errCode === "INSUFFICIENT_SCOPE") {
614
+ return [
615
+ {
616
+ name: "Server preflight",
617
+ ok: true,
618
+ 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."
619
+ }
620
+ ];
621
+ }
622
+ }
623
+ const text2 = await res.text().catch(() => "");
624
+ return [
625
+ {
626
+ name: "Server preflight",
627
+ ok: false,
628
+ detail: `HTTP ${res.status}: ${text2.slice(0, 120)}`
629
+ }
630
+ ];
631
+ } catch (err) {
632
+ const msg = err instanceof Error ? err.message : String(err);
633
+ return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
634
+ }
635
+ }
636
+ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
637
+ if (!config.apiKey || !config.endpoint) {
638
+ return [
639
+ {
640
+ name: "Ingest setup",
641
+ ok: false,
642
+ detail: "Need apiKey and endpoint. Run `mushi connect`."
643
+ }
644
+ ];
645
+ }
646
+ try {
647
+ const data = await fetchIngestSetup(
648
+ { endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
649
+ doFetch
650
+ );
651
+ if (!data) {
652
+ return [
653
+ {
654
+ name: "Ingest setup",
655
+ ok: false,
656
+ detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload"
657
+ }
658
+ ];
659
+ }
660
+ const steps = data.steps ?? [];
661
+ const checks = steps.filter((s) => s.required).map((s) => ({
662
+ name: `[ingest] ${s.label}`,
663
+ ok: s.complete,
664
+ detail: s.complete ? "" : s.hint ?? ""
665
+ }));
666
+ const diag = data.diagnostic;
667
+ if (diag?.last_sdk_seen_at) {
668
+ checks.push({
669
+ name: "[ingest] Last SDK heartbeat",
670
+ ok: true,
671
+ detail: `${diag.last_sdk_seen_at}${diag.last_sdk_endpoint_host ? ` @ ${diag.last_sdk_endpoint_host}` : ""}`
672
+ });
673
+ }
674
+ return checks.length > 0 ? checks : [{ name: "Ingest setup", ok: false, detail: "Empty response from /v1/sync/ingest-setup" }];
675
+ } catch (err) {
676
+ const msg = err instanceof Error ? err.message : String(err);
677
+ return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
678
+ }
679
+ }
680
+ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
681
+ if (!config.projectId || !config.apiKey || !config.endpoint) {
682
+ return [
683
+ {
684
+ name: "QA stories health",
685
+ ok: false,
686
+ detail: "Need projectId, apiKey, and endpoint for QA story checks."
687
+ }
688
+ ];
689
+ }
690
+ const checks = [];
691
+ try {
692
+ const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
693
+ const headers = apiKeyHeaders(apiKey, projectId);
694
+ const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/qa-coverage`, {
695
+ headers,
696
+ signal: AbortSignal.timeout(8e3)
697
+ });
698
+ if (!storiesRes.ok) {
699
+ checks.push({
700
+ name: "[qa] Fetch QA stories",
701
+ ok: false,
702
+ detail: `HTTP ${storiesRes.status}`
703
+ });
704
+ return checks;
705
+ }
706
+ const storiesBody = await storiesRes.json();
707
+ const stories = storiesBody.data?.coverage ?? [];
708
+ const enabled = stories.filter((s) => s.enabled);
709
+ if (enabled.length === 0) {
710
+ checks.push({
711
+ name: "[qa] Enabled QA stories",
712
+ ok: true,
713
+ detail: "No enabled stories \u2014 create one at /qa-coverage"
714
+ });
715
+ return checks;
716
+ }
717
+ checks.push({
718
+ name: "[qa] Enabled QA stories",
719
+ ok: true,
720
+ detail: `${enabled.length} enabled story/stories configured`
721
+ });
722
+ const slackRes = await doFetch(
723
+ `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/slack`,
724
+ {
725
+ method: "POST",
726
+ headers,
727
+ signal: AbortSignal.timeout(6e3)
728
+ }
729
+ );
730
+ const slackBody = slackRes.ok ? await slackRes.json() : null;
731
+ const slackOk = slackBody?.status === "ok";
732
+ checks.push({
733
+ name: "[qa] Slack notifications configured",
734
+ ok: slackOk,
735
+ 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."
736
+ });
737
+ const fcRes = await doFetch(
738
+ `${endpoint}/v1/admin/projects/${projectId}/integrations/probe/firecrawl`,
739
+ {
740
+ method: "POST",
741
+ headers,
742
+ signal: AbortSignal.timeout(6e3)
743
+ }
744
+ );
745
+ const fcBody = fcRes.ok ? await fcRes.json() : null;
746
+ const hasFirecrawlStories = enabled.some(
747
+ (s) => !s.browser_provider || s.browser_provider === "firecrawl_actions"
748
+ );
749
+ if (hasFirecrawlStories) {
750
+ const fcOk = fcBody?.status === "ok";
751
+ checks.push({
752
+ name: "[qa] Firecrawl API key configured",
753
+ ok: fcOk,
754
+ 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."
755
+ });
756
+ }
757
+ } catch (err) {
758
+ const msg = err instanceof Error ? err.message : String(err);
759
+ checks.push({ name: "[qa] QA stories health", ok: false, detail: `Fetch failed: ${msg}` });
760
+ }
761
+ return checks;
762
+ }
763
+ async function checkHostAppWiring(cwd) {
764
+ const checks = [];
765
+ try {
766
+ const { readFile, access } = await import("fs/promises");
767
+ const { join: join3, resolve: resolve2 } = await import("path");
768
+ const root = resolve2(cwd);
769
+ const pkgPath = join3(root, "package.json");
770
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
771
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
772
+ const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
773
+ const envCandidates = [".env.local", ".env"];
774
+ let envContent = "";
775
+ for (const f of envCandidates) {
776
+ try {
777
+ envContent = await readFile(join3(root, f), "utf8");
778
+ break;
779
+ } catch {
780
+ }
781
+ }
782
+ const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
783
+ const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(
784
+ envContent
785
+ );
786
+ checks.push({
787
+ name: "[host] Mushi env vars in .env.local",
788
+ ok: hasProjectId && hasApiKey,
789
+ 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"
790
+ });
791
+ let mcpPresent = false;
792
+ try {
793
+ await access(join3(root, ".cursor", "mcp.json"));
794
+ mcpPresent = true;
795
+ } catch {
796
+ }
797
+ checks.push({
798
+ name: "[host] Cursor MCP config",
799
+ ok: mcpPresent,
800
+ detail: mcpPresent ? ".cursor/mcp.json present" : "Run `mushi connect` to wire MCP for two-way reporter replies"
801
+ });
802
+ if (isCapHybrid) {
803
+ const hasWebSdk = Boolean(deps["@mushi-mushi/web"] || deps["@mushi-mushi/react"]);
804
+ checks.push({
805
+ name: "[host] Capacitor hybrid \u2014 WebView SDK",
806
+ ok: hasWebSdk,
807
+ 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"
808
+ });
809
+ checks.push({
810
+ name: "[host] Capacitor native plugin (optional)",
811
+ ok: true,
812
+ 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"
813
+ });
814
+ }
815
+ } catch {
816
+ checks.push({
817
+ name: "[host] Host app detection",
818
+ ok: false,
819
+ detail: "No package.json in cwd \u2014 run from your app repo root"
820
+ });
821
+ }
822
+ return checks;
823
+ }
824
+ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
825
+ const checks = [];
826
+ const { readFile } = await import("fs/promises");
827
+ const { join: join3, resolve: resolve2 } = await import("path");
828
+ const { homedir } = await import("os");
829
+ const root = resolve2(cwd);
830
+ const candidates = [join3(root, ".cursor", "mcp.json"), join3(homedir(), ".cursor", "mcp.json")];
831
+ let mcpPath = null;
832
+ let mcpRaw = null;
833
+ for (const candidate of candidates) {
834
+ try {
835
+ const raw = await readFile(candidate, "utf8");
836
+ let parsed = {};
837
+ try {
838
+ parsed = JSON.parse(raw);
839
+ } catch {
840
+ }
841
+ const hasEntries = Object.keys(parsed.mcpServers ?? {}).length > 0;
842
+ if (!hasEntries && candidates.indexOf(candidate) < candidates.length - 1) {
843
+ continue;
844
+ }
845
+ mcpRaw = raw;
846
+ mcpPath = candidate;
847
+ break;
848
+ } catch {
849
+ }
850
+ }
851
+ if (!mcpPath || !mcpRaw) {
852
+ checks.push({
853
+ name: "[mcp] mcp.json present",
854
+ ok: false,
855
+ detail: "No .cursor/mcp.json found in cwd or ~/.cursor/. Run `mushi setup` to create it."
856
+ });
857
+ return checks;
858
+ }
859
+ checks.push({
860
+ name: "[mcp] mcp.json present",
861
+ ok: true,
862
+ detail: `Found at ${mcpPath}`
863
+ });
864
+ let mcpConfig = {};
865
+ try {
866
+ mcpConfig = JSON.parse(mcpRaw);
867
+ } catch {
868
+ checks.push({
869
+ name: "[mcp] mcp.json valid JSON",
870
+ ok: false,
871
+ detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`."
872
+ });
873
+ return checks;
874
+ }
875
+ const servers = mcpConfig.mcpServers ?? {};
876
+ const mushiEntries = Object.entries(servers).filter(
877
+ ([k]) => k === "mushi" || k.startsWith("mushi-")
878
+ );
879
+ if (mushiEntries.length === 0) {
880
+ checks.push({
881
+ name: "[mcp] mushi server entry",
882
+ ok: false,
883
+ detail: "No mushi or mushi-* server found in mcpServers. Run `mushi setup` to add one."
884
+ });
885
+ return checks;
886
+ }
887
+ checks.push({
888
+ name: "[mcp] mushi server entry",
889
+ ok: true,
890
+ detail: `Found: ${mushiEntries.map(([k]) => k).join(", ")}`
891
+ });
892
+ let anyKeyValid = false;
893
+ let anyEndpointSet = false;
894
+ for (const [, srv] of mushiEntries) {
895
+ const s = srv;
896
+ const env = s.env ?? {};
897
+ const key = env["MUSHI_API_KEY"] ?? "";
898
+ const endpoint = env["MUSHI_API_ENDPOINT"] ?? "";
899
+ if (key.startsWith("mushi_")) anyKeyValid = true;
900
+ if (endpoint.includes("supabase.co") || endpoint.includes("localhost")) anyEndpointSet = true;
901
+ }
902
+ checks.push({
903
+ name: "[mcp] MUSHI_API_KEY set",
904
+ ok: anyKeyValid,
905
+ 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."
906
+ });
907
+ checks.push({
908
+ name: "[mcp] MUSHI_API_ENDPOINT set",
909
+ ok: anyEndpointSet,
910
+ detail: anyEndpointSet ? "MUSHI_API_ENDPOINT is present and looks valid" : "MUSHI_API_ENDPOINT missing or not a Supabase URL. Re-run `mushi setup`."
911
+ });
912
+ if (anyKeyValid && anyEndpointSet && config.apiKey && config.endpoint) {
913
+ try {
914
+ const { endpoint, apiKey } = sanitizeCliCredentials(config);
915
+ const res = await doFetch(`${endpoint}/v1/admin/mcp/account-overview`, {
916
+ headers: apiKeyHeaders(apiKey, config.projectId),
917
+ signal: AbortSignal.timeout(6e3)
918
+ });
919
+ if (res.ok) {
920
+ const body = await res.json();
921
+ const projectCount = body?.data?.total ?? 0;
922
+ checks.push({
923
+ name: "[mcp] account-overview reachable",
924
+ ok: true,
925
+ detail: `Key is valid; ${projectCount} accessible project${projectCount === 1 ? "" : "s"}`
926
+ });
927
+ } else {
928
+ let detail = `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`;
929
+ try {
930
+ const errBody = await res.json();
931
+ const nested = errBody["error"];
932
+ const errCode = nested?.["code"] ?? errBody["code"] ?? "";
933
+ if (errCode === "INSUFFICIENT_SCOPE") {
934
+ detail = `Key has report:write scope only \u2014 run \`mushi login --upgrade-scope\` then \`mushi setup\` to get mcp:read.`;
935
+ }
936
+ } catch {
937
+ }
938
+ checks.push({
939
+ name: "[mcp] account-overview reachable",
940
+ ok: false,
941
+ detail
942
+ });
943
+ }
944
+ } catch (err) {
945
+ const msg = err instanceof Error ? err.message : String(err);
946
+ checks.push({
947
+ name: "[mcp] account-overview reachable",
948
+ ok: false,
949
+ detail: `Probe failed: ${msg}. Check MUSHI_API_ENDPOINT and network connectivity.`
950
+ });
951
+ }
952
+ }
953
+ return checks;
954
+ }
955
+ var FIX_HINTS = {
956
+ "CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
957
+ "API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
958
+ "Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
959
+ "Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
960
+ "[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
961
+ "[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
962
+ "[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
963
+ };
964
+ function fixHintForCheck(name) {
965
+ if (FIX_HINTS[name]) return FIX_HINTS[name];
966
+ if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
967
+ if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
968
+ if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
969
+ return void 0;
970
+ }
971
+ async function checkVersionDrift(cwd, doFetch) {
972
+ const checks = [];
973
+ const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version.js");
974
+ if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
975
+ const parse2 = (v) => {
976
+ const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
977
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : [0, 0, 0];
978
+ };
979
+ const isBehind = (installed, latest) => {
980
+ const [a1, a2, a3] = parse2(installed);
981
+ const [b1, b2, b3] = parse2(latest);
982
+ return a1 !== b1 ? a1 < b1 : a2 !== b2 ? a2 < b2 : a3 < b3;
983
+ };
984
+ try {
985
+ const res = await doFetch("https://registry.npmjs.org/@mushi-mushi/cli/latest", {
986
+ signal: AbortSignal.timeout(3e3)
987
+ });
988
+ if (res.ok) {
989
+ const { version: latest } = await res.json();
990
+ if (latest && isBehind(MUSHI_CLI_VERSION2, latest)) {
991
+ checks.push({
992
+ name: "CLI version",
993
+ ok: true,
994
+ warn: true,
995
+ detail: `Installed ${MUSHI_CLI_VERSION2}, latest is ${latest}. Run \`mushi upgrade\` (or npm i -g @mushi-mushi/cli@latest).`
996
+ });
997
+ }
998
+ }
999
+ } catch {
1000
+ }
1001
+ try {
1002
+ const { readFile } = await import("fs/promises");
1003
+ const { join: join3 } = await import("path");
1004
+ const pkg = JSON.parse(await readFile(join3(cwd, "package.json"), "utf8"));
1005
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1006
+ const candidates = Object.entries(deps).filter(([name]) => name.startsWith("@mushi-mushi/")).filter(([, v]) => /^[~^]?\d/.test(v));
1007
+ const drifted = (await Promise.all(
1008
+ candidates.map(async ([name, v]) => {
1009
+ try {
1010
+ const res = await doFetch(`https://registry.npmjs.org/${name}/latest`, {
1011
+ signal: AbortSignal.timeout(3e3)
1012
+ });
1013
+ if (!res.ok) return null;
1014
+ const { version: latest } = await res.json();
1015
+ return latest && isBehind(v, latest) ? [name, v, latest] : null;
1016
+ } catch {
1017
+ return null;
1018
+ }
1019
+ })
1020
+ )).filter((d) => d !== null);
1021
+ if (drifted.length > 0) {
1022
+ checks.push({
1023
+ name: "SDK version drift",
1024
+ ok: true,
1025
+ warn: true,
1026
+ detail: `${drifted.map(([n, v, l]) => `${n}@${v} (latest ${l})`).join(", ")} \u2014 behind npm latest. Run \`mushi upgrade\` in this repo.`
1027
+ });
1028
+ }
1029
+ } catch {
1030
+ }
1031
+ return checks;
1032
+ }
1033
+ async function checkPipelineDoctor(config, doFetch) {
1034
+ if (!config.endpoint || !config.apiKey) return [];
1035
+ try {
1036
+ const res = await doFetch(`${config.endpoint.replace(/\/$/, "")}/v1/admin/doctor`, {
1037
+ headers: apiKeyHeaders(config.apiKey),
1038
+ signal: AbortSignal.timeout(1e4)
1039
+ });
1040
+ if (res.status === 401 || res.status === 403) return [];
1041
+ if (!res.ok) {
1042
+ return [{
1043
+ name: "[pipeline] Server doctor",
1044
+ ok: true,
1045
+ warn: true,
1046
+ detail: `GET /v1/admin/doctor returned HTTP ${res.status} \u2014 pipeline health unknown.`
1047
+ }];
1048
+ }
1049
+ const body = await res.json();
1050
+ return (body.data?.checks ?? []).map((ch) => ({
1051
+ name: `[pipeline] ${ch.name}`,
1052
+ ok: ch.status !== "fail",
1053
+ warn: ch.status === "warn",
1054
+ detail: ch.hint ? `${ch.summary} \u2192 Fix: ${ch.hint}` : ch.summary
1055
+ }));
1056
+ } catch {
1057
+ return [{
1058
+ name: "[pipeline] Server doctor",
1059
+ ok: true,
1060
+ warn: true,
1061
+ detail: "Could not reach /v1/admin/doctor (timeout) \u2014 pipeline health unknown."
1062
+ }];
1063
+ }
1064
+ }
1065
+ async function runDoctor(config, options = {}) {
1066
+ const doFetch = options.fetch ?? globalThis.fetch;
1067
+ const checks = [];
1068
+ const isFull = options.full === true;
1069
+ const runServer = isFull || options.server !== false;
1070
+ const runIngest = isFull || options.ingest !== false;
1071
+ checks.push(...checkCliConfig(config));
1072
+ if (config.endpoint) {
1073
+ checks.push(await checkEndpointReachability(config.endpoint, doFetch));
1074
+ }
1075
+ const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
1076
+ if (sdkCheck) checks.push(sdkCheck);
1077
+ if (sdkCheck?.ok && !sdkCheck.warn) {
1078
+ const { findSdkImport } = await import("./snippet-inject-NDAPEL2T.js");
1079
+ const imported = await findSdkImport(options.cwd ?? process.cwd());
1080
+ checks.push(
1081
+ imported ? { name: "SDK init snippet wired", ok: true, detail: `Found SDK usage in ${imported.file}` } : {
1082
+ name: "SDK init snippet wired",
1083
+ ok: true,
1084
+ warn: true,
1085
+ detail: "Package installed but no @mushi-mushi import found in src/app \u2014 paste the init snippet from `mushi init` into your entry file."
1086
+ }
1087
+ );
1088
+ }
1089
+ if (runServer) {
1090
+ const serverChecks = await checkServerPreflight(config, doFetch);
1091
+ checks.push(...serverChecks);
1092
+ checks.push(...await checkPipelineDoctor(config, doFetch));
1093
+ }
1094
+ checks.push(...await checkVersionDrift(options.cwd ?? process.cwd(), doFetch));
1095
+ if (runIngest) {
1096
+ const ingestChecks = await checkIngestSetup(config, doFetch);
1097
+ checks.push(...ingestChecks);
1098
+ }
1099
+ if (isFull || options.qaStories) {
1100
+ const qaChecks = await checkQaStoriesHealth(config, doFetch);
1101
+ checks.push(...qaChecks);
1102
+ }
1103
+ if (isFull || options.hostApp) {
1104
+ const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
1105
+ checks.push(...hostChecks);
1106
+ }
1107
+ if (isFull || options.mcp) {
1108
+ const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
1109
+ checks.push(...mcpChecks);
1110
+ }
1111
+ if (isFull || options.auth) {
1112
+ checks.push(...await checkCliAuthPath(config, doFetch));
1113
+ }
1114
+ const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
1115
+ if (heartbeatPassed) {
1116
+ const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
1117
+ if (idx >= 0) {
1118
+ checks[idx] = {
1119
+ ...checks[idx],
1120
+ ok: true,
1121
+ warn: true,
1122
+ 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`."
1123
+ };
1124
+ }
1125
+ }
1126
+ return { checks, ready: checks.every((c) => c.ok) };
1127
+ }
1128
+ function formatDoctorResult(result) {
1129
+ const PASS = "OK";
1130
+ const WARN = "WARN";
1131
+ const FAIL = "FAIL";
1132
+ const lines = [];
1133
+ for (const c of result.checks) {
1134
+ const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
1135
+ lines.push(`${icon} ${c.name}`);
1136
+ if (c.detail) lines.push(` ${c.detail}`);
1137
+ if (!c.ok) {
1138
+ const hint = fixHintForCheck(c.name);
1139
+ if (hint) lines.push(` \u2192 Fix: ${hint}`);
1140
+ }
1141
+ }
1142
+ const failed = result.checks.filter((c) => !c.ok);
1143
+ const warned = result.checks.filter((c) => c.ok && c.warn);
1144
+ if (failed.length === 0) {
1145
+ if (warned.length > 0) {
1146
+ lines.push(
1147
+ `
1148
+ All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`
1149
+ );
1150
+ } else {
1151
+ lines.push("\nAll checks passed. The CLI is ready.");
1152
+ }
1153
+ } else {
1154
+ lines.push(`
1155
+ ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
1156
+ lines.push("Fix the items above and re-run `mushi doctor`.");
1157
+ }
1158
+ return lines.join("\n");
1159
+ }
1160
+
432
1161
  // src/init.ts
433
1162
  var ENV_FILES = [".env.local", ".env"];
434
1163
  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
1164
  var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
436
1165
  async function runInit(options = {}) {
437
1166
  const cwd = options.cwd ?? process.cwd();
1167
+ if (options.audit) {
1168
+ await runInitAudit(cwd);
1169
+ return;
1170
+ }
438
1171
  ensureInteractiveOrBailOut(options);
439
1172
  p.intro("Mushi setup");
440
1173
  await printFreshnessHint();
441
1174
  warnIfWorkspaceRoot(cwd);
442
1175
  const pkg = readPackageJson(cwd);
1176
+ maybeSuggestAudit(pkg);
443
1177
  if (!pkg) {
444
1178
  p.log.warn("No package.json found in this directory.");
445
1179
  const cont = await p.confirm({
@@ -478,11 +1212,35 @@ async function runInit(options = {}) {
478
1212
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
479
1213
  emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
480
1214
  const enableRewards = await maybeEnableRewards(options);
1215
+ await maybeInjectSnippet(cwd, framework, options);
481
1216
  printNextSteps(framework, consoleBase, enableRewards);
482
1217
  await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
483
1218
  await maybeOfferConnect(credentials, options, consoleBase);
484
1219
  p.outro("Setup complete.");
485
1220
  }
1221
+ async function runInitAudit(cwd) {
1222
+ p.intro("Mushi setup audit");
1223
+ const config = loadConfig();
1224
+ const result = await runDoctor(config, { cwd, hostApp: true, mcp: true });
1225
+ p.log.message(formatDoctorResult(result));
1226
+ if (result.ready) {
1227
+ p.outro("Audit complete \u2014 setup looks healthy.");
1228
+ } else {
1229
+ p.outro("Audit found issues \u2014 each FAIL line above has a \u2192 Fix hint. `mushi doctor --fix` applies the safe ones.");
1230
+ process.exitCode = 1;
1231
+ }
1232
+ }
1233
+ function maybeSuggestAudit(pkg) {
1234
+ const deps = {
1235
+ ...pkg?.dependencies,
1236
+ ...pkg?.devDependencies
1237
+ };
1238
+ if (Object.keys(deps).some((d) => d.startsWith("@mushi-mushi/"))) {
1239
+ p.log.info(
1240
+ "A Mushi SDK is already installed here. To health-check the existing setup instead, run `npx mushi-mushi --audit`."
1241
+ );
1242
+ }
1243
+ }
486
1244
  function ensureInteractiveOrBailOut(options) {
487
1245
  const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
488
1246
  if (isTTY) return;
@@ -651,7 +1409,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
651
1409
  const createSpin = p.spinner();
652
1410
  createSpin.start(`Creating "${name.trim()}"\u2026`);
653
1411
  try {
654
- const created = await createProject(endpoint, cliToken, name.trim());
1412
+ const created = await createProject(endpoint, cliToken, name.trim(), {
1413
+ scopes: ["report:write", "mcp:read"]
1414
+ });
655
1415
  projectId = created.id;
656
1416
  apiKey = created.apiKey ?? void 0;
657
1417
  createSpin.stop(`Created project "${created.name}".`);
@@ -668,7 +1428,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
668
1428
  const keySpin = p.spinner();
669
1429
  keySpin.start("Minting SDK key\u2026");
670
1430
  try {
671
- apiKey = await mintProjectKey(endpoint, cliToken, projectId);
1431
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId, {
1432
+ scopes: ["report:write", "mcp:read"]
1433
+ });
672
1434
  keySpin.stop("SDK key ready.");
673
1435
  } catch (err) {
674
1436
  keySpin.stop("Could not mint an API key.");
@@ -821,7 +1583,12 @@ async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwri
821
1583
  const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
822
1584
  const targetPath = join2(cwd, target);
823
1585
  const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
824
- const existing = existsSync2(targetPath) ? readFileSync2(targetPath, "utf-8") : "";
1586
+ let existing = "";
1587
+ try {
1588
+ existing = readFileSync2(targetPath, "utf-8");
1589
+ } catch {
1590
+ existing = "";
1591
+ }
825
1592
  if (existing.includes("MUSHI_PROJECT_ID")) {
826
1593
  let shouldOverwrite = overwrite;
827
1594
  if (!shouldOverwrite) {
@@ -908,6 +1675,31 @@ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
908
1675
  }).catch(() => {
909
1676
  });
910
1677
  }
1678
+ async function maybeInjectSnippet(cwd, framework, options) {
1679
+ if (options.yes || !process.stdin.isTTY) return;
1680
+ const { ENTRY_CANDIDATES, injectSnippet, MUSHI_MARKER_START } = await import("./snippet-inject-NDAPEL2T.js");
1681
+ const candidates = ENTRY_CANDIDATES[framework.id];
1682
+ if (!candidates) return;
1683
+ const { readFile, writeFile } = await import("fs/promises");
1684
+ const nodePath = await import("path");
1685
+ for (const rel of candidates) {
1686
+ const abs = nodePath.join(cwd, rel);
1687
+ let source;
1688
+ try {
1689
+ source = await readFile(abs, "utf8");
1690
+ } catch {
1691
+ continue;
1692
+ }
1693
+ const alreadyInjected = source.includes(MUSHI_MARKER_START);
1694
+ const confirmed = await p.confirm({
1695
+ message: alreadyInjected ? `Refresh the Mushi init block in ${rel}?` : `Add the Mushi init snippet to ${rel}? (wrapped in markers; re-runs update in place)`
1696
+ });
1697
+ if (p.isCancel(confirmed) || !confirmed) return;
1698
+ await writeFile(abs, injectSnippet(source, framework.snippet()), "utf8");
1699
+ p.log.success(`${alreadyInjected ? "Updated" : "Added"} Mushi init block in ${rel}`);
1700
+ return;
1701
+ }
1702
+ }
911
1703
  function printNextSteps(framework, consoleBase, enableRewards = false) {
912
1704
  p.note(framework.snippet(), "Add this to your app:");
913
1705
  if (enableRewards) {
@@ -941,7 +1733,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
941
1733
  if (p.isCancel(answer) || !answer) return;
942
1734
  const endpoint = resolveCloudEndpoint(options.endpoint);
943
1735
  try {
944
- const { runConnect } = await import("./connect-XFWRSEMW.js");
1736
+ const { runConnect } = await import("./connect-PXLW4D64.js");
945
1737
  await runConnect({
946
1738
  apiKey: credentials.apiKey,
947
1739
  projectId: credentials.projectId,