@mushi-mushi/cli 0.23.0 → 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.
package/dist/init.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  apiKeyHint,
5
5
  cliSetupDeepLink,
6
6
  consoleUrl,
7
+ ensureClientId,
7
8
  loadConfig,
8
9
  normalizeEndpoint,
9
10
  openInBrowser,
@@ -13,7 +14,7 @@ import {
13
14
  resolveConsoleUrl,
14
15
  resolveConsoleUrlSync,
15
16
  saveConfig
16
- } from "./chunk-WLLFV26V.js";
17
+ } from "./chunk-GUVGTFVK.js";
17
18
  import {
18
19
  FRAMEWORKS,
19
20
  detectFramework,
@@ -24,7 +25,7 @@ import {
24
25
  } from "./chunk-AIZRI665.js";
25
26
  import {
26
27
  MUSHI_CLI_VERSION
27
- } from "./chunk-BY5SZYRO.js";
28
+ } from "./chunk-X74T7JKC.js";
28
29
 
29
30
  // src/init.ts
30
31
  import * as p from "@clack/prompts";
@@ -35,6 +36,7 @@ import { join as join2 } from "path";
35
36
 
36
37
  // src/device-auth.ts
37
38
  var DEVICE_FETCH_TIMEOUT_MS = 15e3;
39
+ var DEFAULT_SLOW_DOWN_MS = 5e3;
38
40
  function trimTrailingSlash(endpoint) {
39
41
  let end = endpoint.length;
40
42
  while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
@@ -49,15 +51,18 @@ async function deviceFetch(url, init = {}) {
49
51
  clearTimeout(timer);
50
52
  }
51
53
  }
52
- async function startDeviceAuth(endpoint) {
54
+ async function startDeviceAuth(endpoint, clientId) {
53
55
  const base = trimTrailingSlash(endpoint);
54
56
  const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
55
57
  method: "POST",
56
- headers: { "Content-Type": "application/json" }
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify(clientId ? { client_id: clientId } : {})
57
60
  });
58
61
  const json = await res.json().catch(() => null);
59
62
  if (!res.ok || !json?.ok || !json.data) {
60
- 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
+ );
61
66
  }
62
67
  return json.data;
63
68
  }
@@ -76,6 +81,12 @@ async function pollDeviceToken(endpoint, deviceCode) {
76
81
  switch (json.error) {
77
82
  case "authorization_pending":
78
83
  return { status: "pending" };
84
+ case "slow_down": {
85
+ const retryAfterHeader = res.headers?.get?.("Retry-After");
86
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : NaN;
87
+ const retryAfterMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : DEFAULT_SLOW_DOWN_MS;
88
+ return { status: "slow_down", retryAfterMs };
89
+ }
79
90
  case "access_denied":
80
91
  return { status: "denied" };
81
92
  case "expired_token":
@@ -84,20 +95,27 @@ async function pollDeviceToken(endpoint, deviceCode) {
84
95
  return {
85
96
  status: "error",
86
97
  message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
87
- // 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
88
- // rejection (already claimed / invalid_grant) that won't fix itself.
89
- retryable: res.status >= 500
98
+ // 5xx is a server hiccup the next poll may clear; 408 (gateway
99
+ // timeout) is equally transient. Any other 4xx is a definitive
100
+ // rejection (already claimed / invalid_grant) that won't fix
101
+ // itself. (429/slow_down is handled above as its own outcome.)
102
+ retryable: res.status >= 500 || res.status === 408
90
103
  };
91
104
  }
92
105
  } catch (err) {
93
- 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
+ };
94
111
  }
95
112
  }
96
113
  var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
114
+ var deviceAuthRetryDelayMs = 1e3;
97
115
  async function waitForCliToken(endpoint, session, opts = {}) {
98
116
  const sleep = opts.sleep ?? defaultSleep;
99
117
  const now = opts.now ?? Date.now;
100
- const intervalMs = (session.interval || 5) * 1e3;
118
+ let intervalMs = (session.interval || 5) * 1e3;
101
119
  const deadline = now() + (session.expires_in || 600) * 1e3;
102
120
  const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
103
121
  let consecutiveErrors = 0;
@@ -115,6 +133,11 @@ async function waitForCliToken(endpoint, session, opts = {}) {
115
133
  consecutiveErrors = 0;
116
134
  opts.onPending?.();
117
135
  continue;
136
+ case "slow_down":
137
+ consecutiveErrors = 0;
138
+ intervalMs = Math.max(intervalMs, outcome.retryAfterMs);
139
+ opts.onSlowDown?.(outcome.retryAfterMs);
140
+ continue;
118
141
  case "denied":
119
142
  throw new Error("Login was denied in the browser.");
120
143
  case "expired":
@@ -139,19 +162,51 @@ function cliAuthHeaders(cliToken) {
139
162
  Authorization: `Bearer ${cliToken}`
140
163
  };
141
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
+ }
142
190
  async function listProjects(endpoint, cliToken) {
143
191
  const base = trimTrailingSlash(endpoint);
144
- try {
145
- 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
+ }
146
202
  const json = await res.json().catch(() => null);
147
203
  if (res.ok && json?.ok) return json.data?.projects ?? [];
148
- } catch (err) {
149
- console.error(
150
- "[mushi] Failed to list projects:",
151
- 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
152
208
  );
153
- }
154
- return [];
209
+ });
155
210
  }
156
211
  async function createProject(endpoint, cliToken, name) {
157
212
  const base = trimTrailingSlash(endpoint);
@@ -169,17 +224,28 @@ async function createProject(endpoint, cliToken, name) {
169
224
  }
170
225
  async function mintProjectKey(endpoint, cliToken, projectId) {
171
226
  const base = trimTrailingSlash(endpoint);
172
- try {
173
- const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
174
- method: "POST",
175
- headers: cliAuthHeaders(cliToken),
176
- body: JSON.stringify({})
177
- });
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
+ }
178
241
  const json = await res.json().catch(() => null);
179
- if (res.ok && json?.ok) return json.data?.key ?? null;
180
- } catch {
181
- }
182
- 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
+ });
183
249
  }
184
250
 
185
251
  // src/freshness.ts
@@ -390,7 +456,10 @@ async function runInit(options = {}) {
390
456
  const detected = detectFramework(cwd, pkg);
391
457
  const framework = await chooseFramework(detected, options);
392
458
  const consoleBase = await resolveConsoleUrl({ cwd });
393
- const endpoint = resolveCloudEndpoint(options.endpoint);
459
+ const endpoint = resolveCloudEndpoint(
460
+ options.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? loadConfig().endpoint
461
+ );
462
+ options = { ...options, endpoint };
394
463
  const credentials = await acquireCredentials(options, consoleBase, endpoint);
395
464
  await verifyCredentials(credentials, options, consoleBase);
396
465
  const pm = detectPackageManager(cwd);
@@ -400,8 +469,16 @@ async function runInit(options = {}) {
400
469
  } else {
401
470
  p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
402
471
  }
403
- 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
+ );
404
480
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
481
+ emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
405
482
  const enableRewards = await maybeEnableRewards(options);
406
483
  printNextSteps(framework, consoleBase, enableRewards);
407
484
  await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
@@ -473,7 +550,9 @@ async function acquireCredentials(options, consoleBase, endpoint) {
473
550
  if (options.yes) {
474
551
  const creds = await runBrowserSignIn(options, endpoint, consoleBase);
475
552
  if (creds) return creds;
476
- 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
+ );
477
556
  } else {
478
557
  const method = await p.select({
479
558
  message: "Connect this app to Mushi",
@@ -498,17 +577,19 @@ async function acquireCredentials(options, consoleBase, endpoint) {
498
577
  if (method === "browser") {
499
578
  const creds = await runBrowserSignIn(options, endpoint, consoleBase);
500
579
  if (creds) return creds;
501
- 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
+ );
502
583
  }
503
584
  }
504
- return collectCredentialsManually(options, consoleBase);
585
+ return collectCredentialsManually(options, consoleBase, endpoint);
505
586
  }
506
587
  async function runBrowserSignIn(options, endpoint, consoleBase) {
507
588
  const startSpin = p.spinner();
508
589
  startSpin.start("Starting secure browser sign-in\u2026");
509
590
  let session;
510
591
  try {
511
- session = await startDeviceAuth(endpoint);
592
+ session = await startDeviceAuth(endpoint, ensureClientId());
512
593
  } catch (err) {
513
594
  startSpin.stop("Could not start browser sign-in.");
514
595
  p.log.warn(err instanceof Error ? err.message : String(err));
@@ -539,7 +620,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
539
620
  fetchSpin.start("Loading your projects\u2026");
540
621
  try {
541
622
  projects = await listProjects(endpoint, cliToken);
542
- 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
+ );
543
626
  } catch (err) {
544
627
  fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
545
628
  p.log.warn(err instanceof Error ? err.message : String(err));
@@ -587,34 +670,50 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
587
670
  const keySpin = p.spinner();
588
671
  keySpin.start("Minting SDK key\u2026");
589
672
  try {
590
- const minted = await mintProjectKey(endpoint, cliToken, projectId);
591
- apiKey = minted ?? void 0;
592
- if (!apiKey) {
593
- keySpin.stop("Could not mint an API key.");
594
- p.log.warn(
595
- `Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
596
- );
597
- return null;
598
- }
673
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId);
599
674
  keySpin.stop("SDK key ready.");
600
675
  } catch (err) {
601
676
  keySpin.stop("Could not mint an API key.");
602
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
+ );
603
681
  return null;
604
682
  }
605
683
  }
606
684
  if (!projectId || !apiKey) return null;
607
685
  return { apiKey, projectId };
608
686
  }
609
- async function collectCredentialsManually(options, consoleBase) {
687
+ async function collectCredentialsManually(options, consoleBase, endpoint) {
610
688
  const existing = loadConfig();
611
- const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
689
+ let savedProjectId = existing.projectId;
690
+ let savedApiKey = existing.apiKey;
691
+ if (!options.projectId && !options.apiKey && savedProjectId && savedApiKey) {
692
+ p.log.info(
693
+ `Found saved credentials from a previous sign-in (project ${sanitizeSecret(savedProjectId).slice(0, 8)}\u2026).`
694
+ );
695
+ const checkSpin = p.spinner();
696
+ checkSpin.start("Checking saved credentials\u2026");
697
+ const check = await apiCall("/v1/sync/whoami", {
698
+ apiKey: sanitizeSecret(savedApiKey),
699
+ projectId: sanitizeSecret(savedProjectId),
700
+ endpoint
701
+ });
702
+ if (check.ok) {
703
+ checkSpin.stop(`Saved credentials still work (${check.data.project_name}).`);
704
+ } else {
705
+ checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
706
+ savedProjectId = void 0;
707
+ savedApiKey = void 0;
708
+ }
709
+ }
710
+ const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
612
711
  message: "Project ID",
613
712
  placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
614
713
  hint: projectIdHint(consoleBase),
615
714
  validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID \u2014 copy it from the Projects page or the panel right after you create a project."
616
715
  });
617
- const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
716
+ const rawApiKey = options.apiKey ?? savedApiKey ?? await promptText({
618
717
  message: "API key",
619
718
  placeholder: "mushi_xxxxxxxxxxxx",
620
719
  hint: apiKeyHint(consoleBase),
@@ -638,17 +737,30 @@ async function verifyCredentials(credentials, options, consoleBase) {
638
737
  const endpoint = resolveCloudEndpoint(options.endpoint);
639
738
  const spinner2 = p.spinner();
640
739
  spinner2.start("Verifying credentials\u2026");
641
- const result = await apiCall(
642
- "/v1/sync/whoami",
643
- { apiKey: credentials.apiKey, projectId: credentials.projectId, endpoint }
644
- );
740
+ const result = await apiCall("/v1/sync/whoami", {
741
+ apiKey: credentials.apiKey,
742
+ projectId: credentials.projectId,
743
+ endpoint
744
+ });
645
745
  if (!result.ok) {
646
746
  spinner2.stop("Credentials could not be verified.");
647
747
  p.log.error(result.error?.message ?? "Authentication failed.");
648
- p.log.info(`Double-check values in the console:
649
- ${cliSetupDeepLink(consoleBase)}`);
748
+ p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
749
+ p.log.info(
750
+ [
751
+ "To recover:",
752
+ ' \u2022 Re-run `npx mushi-mushi` and choose "Sign in with your browser", or',
753
+ ` \u2022 Double-check the Project ID / API key in the console: ${cliSetupDeepLink(consoleBase)}`
754
+ ].join("\n")
755
+ );
650
756
  throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
651
757
  }
758
+ if (result.data.project_id && result.data.project_id !== credentials.projectId) {
759
+ p.log.warn(
760
+ `Project ID ${redact(credentials.projectId)} doesn't match this API key's project \u2014 using the key's actual project (${result.data.project_name}) instead.`
761
+ );
762
+ credentials.projectId = result.data.project_id;
763
+ }
652
764
  spinner2.stop(`Connected to ${result.data.project_name}`);
653
765
  }
654
766
  function sanitizeSecret(raw) {
@@ -784,6 +896,20 @@ function persistCliConfig(apiKey, projectId, endpoint) {
784
896
  const existing = loadConfig();
785
897
  saveConfig({ ...existing, apiKey, projectId, endpoint });
786
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
+ }
787
913
  function printNextSteps(framework, consoleBase, enableRewards = false) {
788
914
  p.note(framework.snippet(), "Add this to your app:");
789
915
  if (enableRewards) {
@@ -817,7 +943,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
817
943
  if (p.isCancel(answer) || !answer) return;
818
944
  const endpoint = resolveCloudEndpoint(options.endpoint);
819
945
  try {
820
- const { runConnect } = await import("./connect-KX4UGY6Z.js");
946
+ const { runConnect } = await import("./connect-7LTQRBEW.js");
821
947
  await runConnect({
822
948
  apiKey: credentials.apiKey,
823
949
  projectId: credentials.projectId,
@@ -828,12 +954,8 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
828
954
  wait: true
829
955
  });
830
956
  } catch (err) {
831
- p.log.warn(
832
- err instanceof Error ? err.message : String(err)
833
- );
834
- p.log.info(
835
- "You can run manually: mushi connect --write-env --wire-ide --wait"
836
- );
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");
837
959
  }
838
960
  }
839
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-BY5SZYRO.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.0",
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.0" : "0.0.0-dev";
3
-
4
- export {
5
- MUSHI_CLI_VERSION
6
- };