@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/CONTRIBUTING.md +10 -2
- package/SECURITY.md +24 -1
- package/dist/{chunk-3JRQN6S4.js → chunk-2ZLYNNA3.js} +5 -5
- package/dist/chunk-6XYPFV23.js +10 -0
- package/dist/chunk-PCTMFRKN.js +8 -0
- package/dist/{chunk-2WHQONHK.js → chunk-QOY3DULB.js} +118 -2
- package/dist/{connect-22IHGT2L.js → connect-2LCLLN7R.js} +2 -1
- package/dist/{connect-XFWRSEMW.js → connect-PXLW4D64.js} +7 -106
- package/dist/index.js +3855 -3185
- package/dist/init.d.ts +2 -0
- package/dist/init.js +804 -12
- package/dist/snippet-inject-LPRWTQDE.js +83 -0
- package/dist/snippet-inject-NDAPEL2T.js +81 -0
- package/dist/version-5DCYWDRK.js +9 -0
- package/dist/version.d.ts +9 -1
- package/dist/version.js +5 -3
- package/dist/wizard-args.d.ts +45 -0
- package/dist/wizard-args.js +100 -0
- package/package.json +7 -1
- package/dist/chunk-SS42XQ57.js +0 -6
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-
|
|
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
|
-
|
|
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-
|
|
1736
|
+
const { runConnect } = await import("./connect-PXLW4D64.js");
|
|
945
1737
|
await runConnect({
|
|
946
1738
|
apiKey: credentials.apiKey,
|
|
947
1739
|
projectId: credentials.projectId,
|