@mushi-mushi/cli 0.23.1 → 0.24.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.
@@ -116,7 +116,7 @@ function tightenDirPermissions(path) {
116
116
  }
117
117
 
118
118
  // src/version.ts
119
- var MUSHI_CLI_VERSION = true ? "0.23.1" : "0.0.0-dev";
119
+ var MUSHI_CLI_VERSION = true ? "0.24.0" : "0.0.0-dev";
120
120
 
121
121
  // src/errors.ts
122
122
  var EXIT_CODE_MAP = {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-I4UK3WTN.js";
3
+ } from "./chunk-X74T7JKC.js";
4
4
 
5
5
  // src/config.ts
6
6
  import { randomUUID } from "crypto";
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var MUSHI_CLI_VERSION = true ? "0.24.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ MUSHI_CLI_VERSION
6
+ };
@@ -4,13 +4,13 @@ import {
4
4
  normalizeEndpoint,
5
5
  requireUuid,
6
6
  saveConfig
7
- } from "./chunk-NZ64LBLU.js";
7
+ } from "./chunk-GUVGTFVK.js";
8
8
  import {
9
9
  detectFramework,
10
10
  envVarsToWrite,
11
11
  readPackageJson
12
12
  } from "./chunk-AIZRI665.js";
13
- import "./chunk-I4UK3WTN.js";
13
+ import "./chunk-X74T7JKC.js";
14
14
 
15
15
  // src/connect.ts
16
16
  import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runConnect
4
- } from "./chunk-M6L22NJU.js";
4
+ } from "./chunk-BWASDQI6.js";
5
5
  import "./chunk-LDPFTQLE.js";
6
6
  export {
7
7
  runConnect
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  sanitizeProjectId,
38
38
  saveConfig,
39
39
  writeMcpServerEntry
40
- } from "./chunk-M6L22NJU.js";
40
+ } from "./chunk-BWASDQI6.js";
41
41
  import {
42
42
  TEST_REPORT_FETCH_TIMEOUT_MS,
43
43
  assertEndpoint,
@@ -81,7 +81,9 @@ async function startDeviceAuth(endpoint, clientId) {
81
81
  });
82
82
  const json = await res.json().catch(() => null);
83
83
  if (!res.ok || !json?.ok || !json.data) {
84
- throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
84
+ throw new Error(
85
+ json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`
86
+ );
85
87
  }
86
88
  return json.data;
87
89
  }
@@ -122,10 +124,15 @@ async function pollDeviceToken(endpoint, deviceCode) {
122
124
  };
123
125
  }
124
126
  } catch (err) {
125
- return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
127
+ return {
128
+ status: "error",
129
+ message: err instanceof Error ? err.message : String(err),
130
+ retryable: true
131
+ };
126
132
  }
127
133
  }
128
134
  var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
135
+ var deviceAuthRetryDelayMs = 1e3;
129
136
  async function waitForCliToken(endpoint, session, opts = {}) {
130
137
  const sleep = opts.sleep ?? defaultSleep;
131
138
  const now = opts.now ?? Date.now;
@@ -176,19 +183,51 @@ function cliAuthHeaders(cliToken) {
176
183
  Authorization: `Bearer ${cliToken}`
177
184
  };
178
185
  }
186
+ var DeviceAuthRequestError = class extends Error {
187
+ step;
188
+ status;
189
+ retryable;
190
+ constructor(step, message, status) {
191
+ super(message);
192
+ this.name = "DeviceAuthRequestError";
193
+ this.step = step;
194
+ this.status = status;
195
+ this.retryable = status === void 0 || status >= 500 || status === 408;
196
+ }
197
+ };
198
+ async function withOneRetry(run) {
199
+ try {
200
+ return await run();
201
+ } catch (err) {
202
+ if (err instanceof DeviceAuthRequestError && err.retryable) {
203
+ if (deviceAuthRetryDelayMs > 0) {
204
+ await defaultSleep(deviceAuthRetryDelayMs);
205
+ }
206
+ return run();
207
+ }
208
+ throw err;
209
+ }
210
+ }
179
211
  async function listProjects(endpoint, cliToken) {
180
212
  const base = trimTrailingSlash(endpoint);
181
- try {
182
- const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
213
+ return withOneRetry(async () => {
214
+ let res;
215
+ try {
216
+ res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
217
+ } catch (err) {
218
+ throw new DeviceAuthRequestError(
219
+ "list_projects",
220
+ `Could not reach ${base}/v1/cli/projects: ${err instanceof Error ? err.message : String(err)}`
221
+ );
222
+ }
183
223
  const json = await res.json().catch(() => null);
184
224
  if (res.ok && json?.ok) return json.data?.projects ?? [];
185
- } catch (err) {
186
- console.error(
187
- "[mushi] Failed to list projects:",
188
- err instanceof Error ? err.message : err
225
+ throw new DeviceAuthRequestError(
226
+ "list_projects",
227
+ json?.error?.message ?? `Could not list projects (HTTP ${res.status}).`,
228
+ res.status
189
229
  );
190
- }
191
- return [];
230
+ });
192
231
  }
193
232
  async function createProject(endpoint, cliToken, name) {
194
233
  const base = trimTrailingSlash(endpoint);
@@ -206,17 +245,28 @@ async function createProject(endpoint, cliToken, name) {
206
245
  }
207
246
  async function mintProjectKey(endpoint, cliToken, projectId) {
208
247
  const base = trimTrailingSlash(endpoint);
209
- try {
210
- const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
211
- method: "POST",
212
- headers: cliAuthHeaders(cliToken),
213
- body: JSON.stringify({})
214
- });
248
+ return withOneRetry(async () => {
249
+ let res;
250
+ try {
251
+ res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
252
+ method: "POST",
253
+ headers: cliAuthHeaders(cliToken),
254
+ body: JSON.stringify({})
255
+ });
256
+ } catch (err) {
257
+ throw new DeviceAuthRequestError(
258
+ "mint_key",
259
+ `Could not reach ${base}/v1/cli/projects/\u2026/keys: ${err instanceof Error ? err.message : String(err)}`
260
+ );
261
+ }
215
262
  const json = await res.json().catch(() => null);
216
- if (res.ok && json?.ok) return json.data?.key ?? null;
217
- } catch {
218
- }
219
- return null;
263
+ if (res.ok && json?.ok && json.data?.key) return json.data.key;
264
+ throw new DeviceAuthRequestError(
265
+ "mint_key",
266
+ json?.error?.message ?? `Could not mint an SDK key (HTTP ${res.status}).`,
267
+ res.ok ? void 0 : res.status
268
+ );
269
+ });
220
270
  }
221
271
 
222
272
  // src/freshness.ts
@@ -460,8 +510,16 @@ async function runInit(options = {}) {
460
510
  } else {
461
511
  p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
462
512
  }
463
- await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
513
+ await writeEnvFile(
514
+ cwd,
515
+ credentials.apiKey,
516
+ credentials.projectId,
517
+ framework,
518
+ endpoint,
519
+ Boolean(options.yes)
520
+ );
464
521
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
522
+ emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
465
523
  const enableRewards = await maybeEnableRewards(options);
466
524
  printNextSteps(framework, consoleBase, enableRewards);
467
525
  await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
@@ -533,7 +591,9 @@ async function acquireCredentials(options, consoleBase, endpoint) {
533
591
  if (options.yes) {
534
592
  const creds = await runBrowserSignIn(options, endpoint, consoleBase);
535
593
  if (creds) return creds;
536
- p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
594
+ p.log.warn(
595
+ "Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
596
+ );
537
597
  } else {
538
598
  const method = await p.select({
539
599
  message: "Connect this app to Mushi",
@@ -558,7 +618,9 @@ async function acquireCredentials(options, consoleBase, endpoint) {
558
618
  if (method === "browser") {
559
619
  const creds = await runBrowserSignIn(options, endpoint, consoleBase);
560
620
  if (creds) return creds;
561
- p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
621
+ p.log.warn(
622
+ "Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
623
+ );
562
624
  }
563
625
  }
564
626
  return collectCredentialsManually(options, consoleBase, endpoint);
@@ -599,7 +661,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
599
661
  fetchSpin.start("Loading your projects\u2026");
600
662
  try {
601
663
  projects = await listProjects(endpoint, cliToken);
602
- fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
664
+ fetchSpin.stop(
665
+ projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet."
666
+ );
603
667
  } catch (err) {
604
668
  fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
605
669
  p.log.warn(err instanceof Error ? err.message : String(err));
@@ -647,19 +711,14 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
647
711
  const keySpin = p.spinner();
648
712
  keySpin.start("Minting SDK key\u2026");
649
713
  try {
650
- const minted = await mintProjectKey(endpoint, cliToken, projectId);
651
- apiKey = minted ?? void 0;
652
- if (!apiKey) {
653
- keySpin.stop("Could not mint an API key.");
654
- p.log.warn(
655
- `Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
656
- );
657
- return null;
658
- }
714
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId);
659
715
  keySpin.stop("SDK key ready.");
660
716
  } catch (err) {
661
717
  keySpin.stop("Could not mint an API key.");
662
718
  p.log.warn(err instanceof Error ? err.message : String(err));
719
+ p.log.warn(
720
+ `You're signed in, but key minting failed. Generate one manually in the console Verify tab: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
721
+ );
663
722
  return null;
664
723
  }
665
724
  }
@@ -719,10 +778,11 @@ async function verifyCredentials(credentials, options, consoleBase) {
719
778
  const endpoint = resolveCloudEndpoint(options.endpoint);
720
779
  const spinner3 = p.spinner();
721
780
  spinner3.start("Verifying credentials\u2026");
722
- const result = await apiCall(
723
- "/v1/sync/whoami",
724
- { apiKey: credentials.apiKey, projectId: credentials.projectId, endpoint }
725
- );
781
+ const result = await apiCall("/v1/sync/whoami", {
782
+ apiKey: credentials.apiKey,
783
+ projectId: credentials.projectId,
784
+ endpoint
785
+ });
726
786
  if (!result.ok) {
727
787
  spinner3.stop("Credentials could not be verified.");
728
788
  p.log.error(result.error?.message ?? "Authentication failed.");
@@ -877,6 +937,20 @@ function persistCliConfig(apiKey, projectId, endpoint) {
877
937
  const existing = loadConfig();
878
938
  saveConfig({ ...existing, apiKey, projectId, endpoint });
879
939
  }
940
+ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
941
+ if (process.env.MUSHI_NO_TELEMETRY) return;
942
+ void fetch(`${endpoint.replace(/\/$/, "")}/v1/cli/funnel`, {
943
+ method: "POST",
944
+ headers: {
945
+ "Content-Type": "application/json",
946
+ "X-Mushi-Api-Key": credentials.apiKey,
947
+ "X-Mushi-Project": credentials.projectId
948
+ },
949
+ body: JSON.stringify({ event, source: "cli", metadata }),
950
+ signal: AbortSignal.timeout(4e3)
951
+ }).catch(() => {
952
+ });
953
+ }
880
954
  function printNextSteps(framework, consoleBase, enableRewards = false) {
881
955
  p.note(framework.snippet(), "Add this to your app:");
882
956
  if (enableRewards) {
@@ -910,7 +984,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
910
984
  if (p.isCancel(answer) || !answer) return;
911
985
  const endpoint = resolveCloudEndpoint(options.endpoint);
912
986
  try {
913
- const { runConnect: runConnect2 } = await import("./connect-5VPUYE6H.js");
987
+ const { runConnect: runConnect2 } = await import("./connect-KKN62YK2.js");
914
988
  await runConnect2({
915
989
  apiKey: credentials.apiKey,
916
990
  projectId: credentials.projectId,
@@ -921,12 +995,8 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
921
995
  wait: true
922
996
  });
923
997
  } catch (err) {
924
- p.log.warn(
925
- err instanceof Error ? err.message : String(err)
926
- );
927
- p.log.info(
928
- "You can run manually: mushi connect --write-env --wire-ide --wait"
929
- );
998
+ p.log.warn(err instanceof Error ? err.message : String(err));
999
+ p.log.info("You can run manually: mushi connect --write-env --wire-ide --wait");
930
1000
  }
931
1001
  }
932
1002
  async function maybeEnableRewards(options) {
@@ -3193,6 +3263,88 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
3193
3263
  return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
3194
3264
  }
3195
3265
  }
3266
+ async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
3267
+ const checks = [];
3268
+ if (!config.endpoint) {
3269
+ return [
3270
+ {
3271
+ name: "Sign-in route reachable",
3272
+ ok: false,
3273
+ detail: "No endpoint configured \u2014 run `npx mushi-mushi` or pass MUSHI_API_ENDPOINT."
3274
+ }
3275
+ ];
3276
+ }
3277
+ const base = sanitizeEndpoint(config.endpoint);
3278
+ let dateHeader = null;
3279
+ try {
3280
+ const res = await doFetch(`${base}/v1/cli/auth/device/token`, {
3281
+ method: "POST",
3282
+ headers: { "Content-Type": "application/json" },
3283
+ body: JSON.stringify({ device_code: "doctor-probe" }),
3284
+ signal: AbortSignal.timeout(8e3)
3285
+ });
3286
+ dateHeader = res.headers?.get?.("date") ?? null;
3287
+ const signInRouteReachable = res.status === 400 || res.status === 429;
3288
+ checks.push({
3289
+ name: "Sign-in route reachable",
3290
+ ok: signInRouteReachable,
3291
+ 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.`
3292
+ });
3293
+ } catch (err) {
3294
+ checks.push({
3295
+ name: "Sign-in route reachable",
3296
+ ok: false,
3297
+ detail: `Fetch failed: ${err instanceof Error ? err.message : String(err)} \u2014 check network/proxy/firewall to ${base}.`
3298
+ });
3299
+ }
3300
+ if (dateHeader) {
3301
+ const serverMs = Date.parse(dateHeader);
3302
+ if (!Number.isNaN(serverMs)) {
3303
+ const skewSec = Math.round(Math.abs(Date.now() - serverMs) / 1e3);
3304
+ const ok = skewSec <= 120;
3305
+ checks.push({
3306
+ name: "System clock in sync",
3307
+ ok,
3308
+ warn: !ok && skewSec <= 300,
3309
+ 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.`
3310
+ });
3311
+ }
3312
+ }
3313
+ if (config.apiKey && config.projectId) {
3314
+ try {
3315
+ const res = await doFetch(`${base}/v1/sync/whoami`, {
3316
+ method: "POST",
3317
+ headers: {
3318
+ "Content-Type": "application/json",
3319
+ ...apiKeyHeaders(config.apiKey, config.projectId)
3320
+ },
3321
+ body: JSON.stringify({}),
3322
+ signal: AbortSignal.timeout(8e3)
3323
+ });
3324
+ const json = await res.json().catch(() => null);
3325
+ const ok = res.ok && json?.ok === true;
3326
+ checks.push({
3327
+ name: "Saved credentials valid",
3328
+ ok,
3329
+ 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.`
3330
+ });
3331
+ } catch (err) {
3332
+ checks.push({
3333
+ name: "Saved credentials valid",
3334
+ ok: false,
3335
+ detail: `whoami fetch failed: ${err instanceof Error ? err.message : String(err)}`
3336
+ });
3337
+ }
3338
+ } else {
3339
+ checks.push({
3340
+ name: "Saved credentials valid",
3341
+ ok: true,
3342
+ warn: true,
3343
+ detail: "No saved CLI credentials yet \u2014 normal before the first successful sign-in."
3344
+ });
3345
+ }
3346
+ return checks;
3347
+ }
3196
3348
  async function checkSdkInstall(cwd) {
3197
3349
  try {
3198
3350
  const { readFile: readFile3 } = await import("fs/promises");
@@ -3229,13 +3381,10 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
3229
3381
  }
3230
3382
  try {
3231
3383
  const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
3232
- const res = await doFetch(
3233
- `${endpoint}/v1/admin/projects/${projectId}/preflight`,
3234
- {
3235
- headers: apiKeyHeaders(apiKey, projectId),
3236
- signal: AbortSignal.timeout(8e3)
3237
- }
3238
- );
3384
+ const res = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/preflight`, {
3385
+ headers: apiKeyHeaders(apiKey, projectId),
3386
+ signal: AbortSignal.timeout(8e3)
3387
+ });
3239
3388
  if (res.ok) {
3240
3389
  const body = await res.json();
3241
3390
  const serverChecks = body.data?.checks ?? [];
@@ -3291,7 +3440,13 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
3291
3440
  doFetch
3292
3441
  );
3293
3442
  if (!data) {
3294
- return [{ name: "Ingest setup", ok: false, detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload" }];
3443
+ return [
3444
+ {
3445
+ name: "Ingest setup",
3446
+ ok: false,
3447
+ detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload"
3448
+ }
3449
+ ];
3295
3450
  }
3296
3451
  const steps = data.steps ?? [];
3297
3452
  const checks = steps.filter((s) => s.required).map((s) => ({
@@ -3327,22 +3482,27 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
3327
3482
  try {
3328
3483
  const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
3329
3484
  const headers = apiKeyHeaders(apiKey, projectId);
3330
- const storiesRes = await doFetch(
3331
- `${endpoint}/v1/admin/projects/${projectId}/qa-coverage`,
3332
- {
3333
- headers,
3334
- signal: AbortSignal.timeout(8e3)
3335
- }
3336
- );
3485
+ const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/qa-coverage`, {
3486
+ headers,
3487
+ signal: AbortSignal.timeout(8e3)
3488
+ });
3337
3489
  if (!storiesRes.ok) {
3338
- checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
3490
+ checks.push({
3491
+ name: "[qa] Fetch QA stories",
3492
+ ok: false,
3493
+ detail: `HTTP ${storiesRes.status}`
3494
+ });
3339
3495
  return checks;
3340
3496
  }
3341
3497
  const storiesBody = await storiesRes.json();
3342
3498
  const stories = storiesBody.data?.coverage ?? [];
3343
3499
  const enabled = stories.filter((s) => s.enabled);
3344
3500
  if (enabled.length === 0) {
3345
- checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
3501
+ checks.push({
3502
+ name: "[qa] Enabled QA stories",
3503
+ ok: true,
3504
+ detail: "No enabled stories \u2014 create one at /qa-coverage"
3505
+ });
3346
3506
  return checks;
3347
3507
  }
3348
3508
  checks.push({
@@ -3411,7 +3571,9 @@ async function checkHostAppWiring(cwd) {
3411
3571
  }
3412
3572
  }
3413
3573
  const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
3414
- const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(envContent);
3574
+ const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(
3575
+ envContent
3576
+ );
3415
3577
  checks.push({
3416
3578
  name: "[host] Mushi env vars in .env.local",
3417
3579
  ok: hasProjectId && hasApiKey,
@@ -3456,10 +3618,7 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
3456
3618
  const { join: join4, resolve: resolve3 } = await import("path");
3457
3619
  const { homedir } = await import("os");
3458
3620
  const root = resolve3(cwd);
3459
- const candidates = [
3460
- join4(root, ".cursor", "mcp.json"),
3461
- join4(homedir(), ".cursor", "mcp.json")
3462
- ];
3621
+ const candidates = [join4(root, ".cursor", "mcp.json"), join4(homedir(), ".cursor", "mcp.json")];
3463
3622
  let mcpPath = null;
3464
3623
  let mcpRaw = null;
3465
3624
  for (const candidate of candidates) {
@@ -3497,11 +3656,17 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
3497
3656
  try {
3498
3657
  mcpConfig = JSON.parse(mcpRaw);
3499
3658
  } catch {
3500
- checks.push({ name: "[mcp] mcp.json valid JSON", ok: false, detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`." });
3659
+ checks.push({
3660
+ name: "[mcp] mcp.json valid JSON",
3661
+ ok: false,
3662
+ detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`."
3663
+ });
3501
3664
  return checks;
3502
3665
  }
3503
3666
  const servers = mcpConfig.mcpServers ?? {};
3504
- const mushiEntries = Object.entries(servers).filter(([k]) => k === "mushi" || k.startsWith("mushi-"));
3667
+ const mushiEntries = Object.entries(servers).filter(
3668
+ ([k]) => k === "mushi" || k.startsWith("mushi-")
3669
+ );
3505
3670
  if (mushiEntries.length === 0) {
3506
3671
  checks.push({
3507
3672
  name: "[mcp] mushi server entry",
@@ -3621,7 +3786,9 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
3621
3786
  } catch {
3622
3787
  }
3623
3788
  }
3624
- const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
3789
+ const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(
3790
+ envContent
3791
+ );
3625
3792
  if (!hasEnvVars) {
3626
3793
  return {
3627
3794
  nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
@@ -3710,6 +3877,9 @@ async function runDoctor(config, options = {}) {
3710
3877
  const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
3711
3878
  checks.push(...mcpChecks);
3712
3879
  }
3880
+ if (isFull || options.auth) {
3881
+ checks.push(...await checkCliAuthPath(config, doFetch));
3882
+ }
3713
3883
  const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
3714
3884
  if (heartbeatPassed) {
3715
3885
  const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
@@ -3742,8 +3912,10 @@ function formatDoctorResult(result) {
3742
3912
  const warned = result.checks.filter((c) => c.ok && c.warn);
3743
3913
  if (failed.length === 0) {
3744
3914
  if (warned.length > 0) {
3745
- lines.push(`
3746
- All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
3915
+ lines.push(
3916
+ `
3917
+ All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`
3918
+ );
3747
3919
  } else {
3748
3920
  lines.push("\nAll checks passed. The CLI is ready.");
3749
3921
  }
@@ -3768,6 +3940,9 @@ function registerDoctorCommand(program2) {
3768
3940
  ).option(
3769
3941
  "--mcp",
3770
3942
  "Check Cursor MCP config: verify mushi-* server entry, credentials, and probe account-overview connectivity."
3943
+ ).option(
3944
+ "--auth",
3945
+ "Diagnose the browser sign-in handshake: device-auth route reachability, system clock skew, and saved-credential validity. Run this when `npx mushi-mushi` browser sign-in does not complete."
3771
3946
  ).option(
3772
3947
  "--fix",
3773
3948
  "Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
@@ -3777,54 +3952,60 @@ function registerDoctorCommand(program2) {
3777
3952
  ).option(
3778
3953
  "--full",
3779
3954
  "Run ALL check categories (server, ingest, host-app, mcp, qa-stories) in one shot. Good for first-run diagnostics. Exit 0 only when all checks pass."
3780
- ).action(async (opts) => {
3781
- const config = loadConfig();
3782
- if (opts.onboarding) {
3783
- const cwd = opts.cwd ?? process.cwd();
3784
- const consoleBase = await resolveConsoleUrl({ cwd });
3785
- const status = await checkOnboardingStatus(config, consoleBase, cwd);
3786
- const base = consoleBase.replace(/\/$/, "");
3787
- if (status.done) {
3788
- console.log(`OK Setup complete. ${status.nextAction}`);
3789
- console.log(` ${base}${status.ctaPath}`);
3790
- } else {
3791
- console.log(`\u2192 Next: ${status.nextAction}`);
3792
- console.log(` Open: ${base}${status.ctaPath}`);
3955
+ ).action(
3956
+ async (opts) => {
3957
+ const config = loadConfig();
3958
+ if (opts.onboarding) {
3959
+ const cwd = opts.cwd ?? process.cwd();
3960
+ const consoleBase = await resolveConsoleUrl({ cwd });
3961
+ const status = await checkOnboardingStatus(config, consoleBase, cwd);
3962
+ const base = consoleBase.replace(/\/$/, "");
3963
+ if (status.done) {
3964
+ console.log(`OK Setup complete. ${status.nextAction}`);
3965
+ console.log(` ${base}${status.ctaPath}`);
3966
+ } else {
3967
+ console.log(`\u2192 Next: ${status.nextAction}`);
3968
+ console.log(` Open: ${base}${status.ctaPath}`);
3969
+ }
3970
+ if (!status.done) process.exit(1);
3971
+ return;
3793
3972
  }
3794
- if (!status.done) process.exit(1);
3795
- return;
3796
- }
3797
- const doctorOpts = {
3798
- cwd: opts.cwd,
3799
- server: opts.server,
3800
- ingest: opts.ingest,
3801
- qaStories: opts.qaStories,
3802
- hostApp: opts.hostApp,
3803
- mcp: opts.mcp,
3804
- full: opts.full
3805
- };
3806
- let result = await runDoctor(config, doctorOpts);
3807
- if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
3808
- const connectResult = await runConnect({
3809
- apiKey: config.apiKey,
3810
- projectId: config.projectId,
3811
- endpoint: config.endpoint,
3812
- cwd: opts.cwd ?? process.cwd(),
3813
- writeEnv: true,
3814
- wireIde: true
3815
- }, config);
3816
- for (const msg of connectResult.messages) console.log(msg);
3817
- result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
3818
- }
3819
- const { checks } = result;
3820
- if (opts.json) {
3821
- console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
3973
+ const doctorOpts = {
3974
+ cwd: opts.cwd,
3975
+ server: opts.server,
3976
+ ingest: opts.ingest,
3977
+ qaStories: opts.qaStories,
3978
+ hostApp: opts.hostApp,
3979
+ mcp: opts.mcp,
3980
+ auth: opts.auth,
3981
+ full: opts.full
3982
+ };
3983
+ let result = await runDoctor(config, doctorOpts);
3984
+ if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
3985
+ const connectResult = await runConnect(
3986
+ {
3987
+ apiKey: config.apiKey,
3988
+ projectId: config.projectId,
3989
+ endpoint: config.endpoint,
3990
+ cwd: opts.cwd ?? process.cwd(),
3991
+ writeEnv: true,
3992
+ wireIde: true
3993
+ },
3994
+ config
3995
+ );
3996
+ for (const msg of connectResult.messages) console.log(msg);
3997
+ result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
3998
+ }
3999
+ const { checks } = result;
4000
+ if (opts.json) {
4001
+ console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
4002
+ if (!result.ready) process.exit(1);
4003
+ return;
4004
+ }
4005
+ console.log(formatDoctorResult(result));
3822
4006
  if (!result.ready) process.exit(1);
3823
- return;
3824
4007
  }
3825
- console.log(formatDoctorResult(result));
3826
- if (!result.ready) process.exit(1);
3827
- });
4008
+ );
3828
4009
  }
3829
4010
 
3830
4011
  // src/commands/reset-cli.ts
package/dist/init.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  resolveConsoleUrl,
15
15
  resolveConsoleUrlSync,
16
16
  saveConfig
17
- } from "./chunk-NZ64LBLU.js";
17
+ } from "./chunk-GUVGTFVK.js";
18
18
  import {
19
19
  FRAMEWORKS,
20
20
  detectFramework,
@@ -25,7 +25,7 @@ import {
25
25
  } from "./chunk-AIZRI665.js";
26
26
  import {
27
27
  MUSHI_CLI_VERSION
28
- } from "./chunk-I4UK3WTN.js";
28
+ } from "./chunk-X74T7JKC.js";
29
29
 
30
30
  // src/init.ts
31
31
  import * as p from "@clack/prompts";
@@ -60,7 +60,9 @@ async function startDeviceAuth(endpoint, clientId) {
60
60
  });
61
61
  const json = await res.json().catch(() => null);
62
62
  if (!res.ok || !json?.ok || !json.data) {
63
- throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
63
+ throw new Error(
64
+ json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`
65
+ );
64
66
  }
65
67
  return json.data;
66
68
  }
@@ -101,10 +103,15 @@ async function pollDeviceToken(endpoint, deviceCode) {
101
103
  };
102
104
  }
103
105
  } catch (err) {
104
- return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
106
+ return {
107
+ status: "error",
108
+ message: err instanceof Error ? err.message : String(err),
109
+ retryable: true
110
+ };
105
111
  }
106
112
  }
107
113
  var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
114
+ var deviceAuthRetryDelayMs = 1e3;
108
115
  async function waitForCliToken(endpoint, session, opts = {}) {
109
116
  const sleep = opts.sleep ?? defaultSleep;
110
117
  const now = opts.now ?? Date.now;
@@ -155,19 +162,51 @@ function cliAuthHeaders(cliToken) {
155
162
  Authorization: `Bearer ${cliToken}`
156
163
  };
157
164
  }
165
+ var DeviceAuthRequestError = class extends Error {
166
+ step;
167
+ status;
168
+ retryable;
169
+ constructor(step, message, status) {
170
+ super(message);
171
+ this.name = "DeviceAuthRequestError";
172
+ this.step = step;
173
+ this.status = status;
174
+ this.retryable = status === void 0 || status >= 500 || status === 408;
175
+ }
176
+ };
177
+ async function withOneRetry(run) {
178
+ try {
179
+ return await run();
180
+ } catch (err) {
181
+ if (err instanceof DeviceAuthRequestError && err.retryable) {
182
+ if (deviceAuthRetryDelayMs > 0) {
183
+ await defaultSleep(deviceAuthRetryDelayMs);
184
+ }
185
+ return run();
186
+ }
187
+ throw err;
188
+ }
189
+ }
158
190
  async function listProjects(endpoint, cliToken) {
159
191
  const base = trimTrailingSlash(endpoint);
160
- try {
161
- const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
192
+ return withOneRetry(async () => {
193
+ let res;
194
+ try {
195
+ res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
196
+ } catch (err) {
197
+ throw new DeviceAuthRequestError(
198
+ "list_projects",
199
+ `Could not reach ${base}/v1/cli/projects: ${err instanceof Error ? err.message : String(err)}`
200
+ );
201
+ }
162
202
  const json = await res.json().catch(() => null);
163
203
  if (res.ok && json?.ok) return json.data?.projects ?? [];
164
- } catch (err) {
165
- console.error(
166
- "[mushi] Failed to list projects:",
167
- err instanceof Error ? err.message : err
204
+ throw new DeviceAuthRequestError(
205
+ "list_projects",
206
+ json?.error?.message ?? `Could not list projects (HTTP ${res.status}).`,
207
+ res.status
168
208
  );
169
- }
170
- return [];
209
+ });
171
210
  }
172
211
  async function createProject(endpoint, cliToken, name) {
173
212
  const base = trimTrailingSlash(endpoint);
@@ -185,17 +224,28 @@ async function createProject(endpoint, cliToken, name) {
185
224
  }
186
225
  async function mintProjectKey(endpoint, cliToken, projectId) {
187
226
  const base = trimTrailingSlash(endpoint);
188
- try {
189
- const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
190
- method: "POST",
191
- headers: cliAuthHeaders(cliToken),
192
- body: JSON.stringify({})
193
- });
227
+ return withOneRetry(async () => {
228
+ let res;
229
+ try {
230
+ res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
231
+ method: "POST",
232
+ headers: cliAuthHeaders(cliToken),
233
+ body: JSON.stringify({})
234
+ });
235
+ } catch (err) {
236
+ throw new DeviceAuthRequestError(
237
+ "mint_key",
238
+ `Could not reach ${base}/v1/cli/projects/\u2026/keys: ${err instanceof Error ? err.message : String(err)}`
239
+ );
240
+ }
194
241
  const json = await res.json().catch(() => null);
195
- if (res.ok && json?.ok) return json.data?.key ?? null;
196
- } catch {
197
- }
198
- return null;
242
+ if (res.ok && json?.ok && json.data?.key) return json.data.key;
243
+ throw new DeviceAuthRequestError(
244
+ "mint_key",
245
+ json?.error?.message ?? `Could not mint an SDK key (HTTP ${res.status}).`,
246
+ res.ok ? void 0 : res.status
247
+ );
248
+ });
199
249
  }
200
250
 
201
251
  // src/freshness.ts
@@ -419,8 +469,16 @@ async function runInit(options = {}) {
419
469
  } else {
420
470
  p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
421
471
  }
422
- await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
472
+ await writeEnvFile(
473
+ cwd,
474
+ credentials.apiKey,
475
+ credentials.projectId,
476
+ framework,
477
+ endpoint,
478
+ Boolean(options.yes)
479
+ );
423
480
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
481
+ emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
424
482
  const enableRewards = await maybeEnableRewards(options);
425
483
  printNextSteps(framework, consoleBase, enableRewards);
426
484
  await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
@@ -492,7 +550,9 @@ async function acquireCredentials(options, consoleBase, endpoint) {
492
550
  if (options.yes) {
493
551
  const creds = await runBrowserSignIn(options, endpoint, consoleBase);
494
552
  if (creds) return creds;
495
- p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
553
+ p.log.warn(
554
+ "Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
555
+ );
496
556
  } else {
497
557
  const method = await p.select({
498
558
  message: "Connect this app to Mushi",
@@ -517,7 +577,9 @@ async function acquireCredentials(options, consoleBase, endpoint) {
517
577
  if (method === "browser") {
518
578
  const creds = await runBrowserSignIn(options, endpoint, consoleBase);
519
579
  if (creds) return creds;
520
- p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
580
+ p.log.warn(
581
+ "Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
582
+ );
521
583
  }
522
584
  }
523
585
  return collectCredentialsManually(options, consoleBase, endpoint);
@@ -558,7 +620,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
558
620
  fetchSpin.start("Loading your projects\u2026");
559
621
  try {
560
622
  projects = await listProjects(endpoint, cliToken);
561
- fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
623
+ fetchSpin.stop(
624
+ projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet."
625
+ );
562
626
  } catch (err) {
563
627
  fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
564
628
  p.log.warn(err instanceof Error ? err.message : String(err));
@@ -606,19 +670,14 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
606
670
  const keySpin = p.spinner();
607
671
  keySpin.start("Minting SDK key\u2026");
608
672
  try {
609
- const minted = await mintProjectKey(endpoint, cliToken, projectId);
610
- apiKey = minted ?? void 0;
611
- if (!apiKey) {
612
- keySpin.stop("Could not mint an API key.");
613
- p.log.warn(
614
- `Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
615
- );
616
- return null;
617
- }
673
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId);
618
674
  keySpin.stop("SDK key ready.");
619
675
  } catch (err) {
620
676
  keySpin.stop("Could not mint an API key.");
621
677
  p.log.warn(err instanceof Error ? err.message : String(err));
678
+ p.log.warn(
679
+ `You're signed in, but key minting failed. Generate one manually in the console Verify tab: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
680
+ );
622
681
  return null;
623
682
  }
624
683
  }
@@ -678,10 +737,11 @@ async function verifyCredentials(credentials, options, consoleBase) {
678
737
  const endpoint = resolveCloudEndpoint(options.endpoint);
679
738
  const spinner2 = p.spinner();
680
739
  spinner2.start("Verifying credentials\u2026");
681
- const result = await apiCall(
682
- "/v1/sync/whoami",
683
- { apiKey: credentials.apiKey, projectId: credentials.projectId, endpoint }
684
- );
740
+ const result = await apiCall("/v1/sync/whoami", {
741
+ apiKey: credentials.apiKey,
742
+ projectId: credentials.projectId,
743
+ endpoint
744
+ });
685
745
  if (!result.ok) {
686
746
  spinner2.stop("Credentials could not be verified.");
687
747
  p.log.error(result.error?.message ?? "Authentication failed.");
@@ -836,6 +896,20 @@ function persistCliConfig(apiKey, projectId, endpoint) {
836
896
  const existing = loadConfig();
837
897
  saveConfig({ ...existing, apiKey, projectId, endpoint });
838
898
  }
899
+ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
900
+ if (process.env.MUSHI_NO_TELEMETRY) return;
901
+ void fetch(`${endpoint.replace(/\/$/, "")}/v1/cli/funnel`, {
902
+ method: "POST",
903
+ headers: {
904
+ "Content-Type": "application/json",
905
+ "X-Mushi-Api-Key": credentials.apiKey,
906
+ "X-Mushi-Project": credentials.projectId
907
+ },
908
+ body: JSON.stringify({ event, source: "cli", metadata }),
909
+ signal: AbortSignal.timeout(4e3)
910
+ }).catch(() => {
911
+ });
912
+ }
839
913
  function printNextSteps(framework, consoleBase, enableRewards = false) {
840
914
  p.note(framework.snippet(), "Add this to your app:");
841
915
  if (enableRewards) {
@@ -869,7 +943,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
869
943
  if (p.isCancel(answer) || !answer) return;
870
944
  const endpoint = resolveCloudEndpoint(options.endpoint);
871
945
  try {
872
- const { runConnect } = await import("./connect-IWFKKXGZ.js");
946
+ const { runConnect } = await import("./connect-7LTQRBEW.js");
873
947
  await runConnect({
874
948
  apiKey: credentials.apiKey,
875
949
  projectId: credentials.projectId,
@@ -880,12 +954,8 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
880
954
  wait: true
881
955
  });
882
956
  } catch (err) {
883
- p.log.warn(
884
- err instanceof Error ? err.message : String(err)
885
- );
886
- p.log.info(
887
- "You can run manually: mushi connect --write-env --wire-ide --wait"
888
- );
957
+ p.log.warn(err instanceof Error ? err.message : String(err));
958
+ p.log.info("You can run manually: mushi connect --write-env --wire-ide --wait");
889
959
  }
890
960
  }
891
961
  async function maybeEnableRewards(options) {
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-I4UK3WTN.js";
3
+ } from "./chunk-X74T7JKC.js";
4
4
  export {
5
5
  MUSHI_CLI_VERSION
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mushi-mushi/cli",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
4
4
  "license": "MIT",
5
5
  "description": "CLI for Mushi Mushi — `mushi init` wizard installs the right SDK for your framework, plus report triage and pipeline health commands",
6
6
  "bin": {
@@ -11,11 +11,11 @@
11
11
  "commander": "^15.0.0"
12
12
  },
13
13
  "devDependencies": {
14
- "@types/node": "^22.19.21",
15
- "eslint": "^10.4.1",
14
+ "@types/node": "^22.20.0",
15
+ "eslint": "^10.6.0",
16
16
  "tsup": "^8.5.1",
17
17
  "typescript": "^6.0.3",
18
- "vitest": "^4.1.8",
18
+ "vitest": "^4.1.9",
19
19
  "@mushi-mushi/eslint-config": "0.0.0"
20
20
  },
21
21
  "author": "Kenji Sakuramoto (https://bsky.app/profile/mushimushi.dev)",
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var MUSHI_CLI_VERSION = true ? "0.23.1" : "0.0.0-dev";
3
-
4
- export {
5
- MUSHI_CLI_VERSION
6
- };