@mushi-mushi/cli 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/init.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-T4XX5FMH.js";
3
+ } from "./chunk-L7KBJ2XY.js";
4
4
  import {
5
5
  TEST_REPORT_FETCH_TIMEOUT_MS,
6
6
  loadConfig,
@@ -15,13 +15,13 @@ import {
15
15
  envVarsToWrite,
16
16
  installCommand,
17
17
  readPackageJson
18
- } from "./chunk-7Q44KWGN.js";
18
+ } from "./chunk-JAUKBLPK.js";
19
19
 
20
20
  // src/init.ts
21
21
  import * as p from "@clack/prompts";
22
22
  import { spawn } from "child_process";
23
23
  import { randomUUID } from "crypto";
24
- import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
24
+ import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
25
25
  import { join as join3 } from "path";
26
26
 
27
27
  // src/console-url.ts
@@ -31,7 +31,10 @@ var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
31
31
  var LOCAL_ADMIN_URL = "http://localhost:6464";
32
32
  var LOCAL_PROBE_MS = 800;
33
33
  function normalizeConsoleBase(url) {
34
- return url.trim().replace(/\/+$/, "");
34
+ const trimmed = url.trim();
35
+ let end = trimmed.length;
36
+ while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
37
+ return trimmed.slice(0, end);
35
38
  }
36
39
  function consoleUrl(base, route) {
37
40
  const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
@@ -106,10 +109,34 @@ function reportsUrl(base, reportId) {
106
109
  return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
107
110
  }
108
111
  async function openInBrowser(url) {
109
- const { exec } = await import("child_process");
110
- const openCmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
112
+ let parsed;
113
+ try {
114
+ parsed = new URL(url);
115
+ } catch {
116
+ return;
117
+ }
118
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
119
+ return;
120
+ }
121
+ const safeUrl = parsed.toString();
122
+ const { spawn: spawn2 } = await import("child_process");
123
+ const [command, args] = process.platform === "win32" ? (
124
+ // Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
125
+ // `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
126
+ // characters like `&` / `|` / `^` could still break out — exactly the
127
+ // injection surface CodeQL flags. rundll32 never invokes a shell: the URL
128
+ // is handed to the default browser as one opaque argument.
129
+ ["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
130
+ ) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
111
131
  await new Promise((resolve2) => {
112
- exec(openCmd, () => resolve2());
132
+ try {
133
+ const child = spawn2(command, [...args], { stdio: "ignore", shell: false });
134
+ child.on("error", () => resolve2());
135
+ child.on("exit", () => resolve2());
136
+ child.unref();
137
+ } catch {
138
+ resolve2();
139
+ }
113
140
  });
114
141
  }
115
142
 
@@ -180,13 +207,13 @@ async function apiCall(path, config, options = {}) {
180
207
  }
181
208
  if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
182
209
  const b = body;
210
+ const nested = b["error"];
211
+ const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
212
+ const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
183
213
  return {
184
214
  ok: false,
185
215
  httpStatus: res.status,
186
- error: {
187
- code: b["code"] ?? `HTTP_${res.status}`,
188
- message: b["message"] ?? `Request failed (${res.status})`
189
- }
216
+ error: { code: errCode, message: errMsg }
190
217
  };
191
218
  }
192
219
  return body;
@@ -211,6 +238,151 @@ async function apiCall(path, config, options = {}) {
211
238
  }
212
239
  }
213
240
 
241
+ // src/device-auth.ts
242
+ var DEVICE_FETCH_TIMEOUT_MS = 15e3;
243
+ function trimTrailingSlash(endpoint) {
244
+ let end = endpoint.length;
245
+ while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
246
+ return endpoint.slice(0, end);
247
+ }
248
+ async function deviceFetch(url, init = {}) {
249
+ const controller = new AbortController();
250
+ const timer = setTimeout(() => controller.abort(), DEVICE_FETCH_TIMEOUT_MS);
251
+ try {
252
+ return await fetch(url, { ...init, signal: controller.signal });
253
+ } finally {
254
+ clearTimeout(timer);
255
+ }
256
+ }
257
+ async function startDeviceAuth(endpoint) {
258
+ const base = trimTrailingSlash(endpoint);
259
+ const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
260
+ method: "POST",
261
+ headers: { "Content-Type": "application/json" }
262
+ });
263
+ const json = await res.json().catch(() => null);
264
+ if (!res.ok || !json?.ok || !json.data) {
265
+ throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
266
+ }
267
+ return json.data;
268
+ }
269
+ async function pollDeviceToken(endpoint, deviceCode) {
270
+ const base = trimTrailingSlash(endpoint);
271
+ try {
272
+ const res = await deviceFetch(`${base}/v1/cli/auth/device/token`, {
273
+ method: "POST",
274
+ headers: { "Content-Type": "application/json" },
275
+ body: JSON.stringify({ device_code: deviceCode })
276
+ });
277
+ const json = await res.json().catch(() => ({}));
278
+ if (res.ok && json.ok && json.data?.cli_token) {
279
+ return { status: "approved", cliToken: json.data.cli_token, userId: json.data.user_id };
280
+ }
281
+ switch (json.error) {
282
+ case "authorization_pending":
283
+ return { status: "pending" };
284
+ case "access_denied":
285
+ return { status: "denied" };
286
+ case "expired_token":
287
+ return { status: "expired" };
288
+ default:
289
+ return {
290
+ status: "error",
291
+ message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
292
+ // 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
293
+ // rejection (already claimed / invalid_grant) that won't fix itself.
294
+ retryable: res.status >= 500
295
+ };
296
+ }
297
+ } catch (err) {
298
+ return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
299
+ }
300
+ }
301
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
302
+ async function waitForCliToken(endpoint, session, opts = {}) {
303
+ const sleep = opts.sleep ?? defaultSleep;
304
+ const now = opts.now ?? Date.now;
305
+ const intervalMs = (session.interval || 5) * 1e3;
306
+ const deadline = now() + (session.expires_in || 600) * 1e3;
307
+ const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
308
+ let consecutiveErrors = 0;
309
+ let firstPoll = true;
310
+ while (now() < deadline) {
311
+ if (!firstPoll) {
312
+ await sleep(intervalMs);
313
+ }
314
+ firstPoll = false;
315
+ const outcome = await pollDeviceToken(endpoint, session.device_code);
316
+ switch (outcome.status) {
317
+ case "approved":
318
+ return outcome.cliToken;
319
+ case "pending":
320
+ consecutiveErrors = 0;
321
+ opts.onPending?.();
322
+ continue;
323
+ case "denied":
324
+ throw new Error("Login was denied in the browser.");
325
+ case "expired":
326
+ throw new Error("The login code expired. Run sign-in again.");
327
+ case "error":
328
+ if (!outcome.retryable) {
329
+ throw new Error(outcome.message);
330
+ }
331
+ consecutiveErrors += 1;
332
+ if (consecutiveErrors >= maxConsecutiveErrors) {
333
+ throw new Error(outcome.message);
334
+ }
335
+ opts.onTransientError?.(outcome.message, consecutiveErrors);
336
+ continue;
337
+ }
338
+ }
339
+ throw new Error("Login timed out before approval. Run sign-in again.");
340
+ }
341
+ function cliAuthHeaders(cliToken) {
342
+ return {
343
+ "Content-Type": "application/json",
344
+ Authorization: `Bearer ${cliToken}`
345
+ };
346
+ }
347
+ async function listProjects(endpoint, cliToken) {
348
+ const base = trimTrailingSlash(endpoint);
349
+ try {
350
+ const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
351
+ const json = await res.json().catch(() => null);
352
+ if (res.ok && json?.ok) return json.data?.projects ?? [];
353
+ } catch {
354
+ }
355
+ return [];
356
+ }
357
+ async function createProject(endpoint, cliToken, name) {
358
+ const base = trimTrailingSlash(endpoint);
359
+ const res = await deviceFetch(`${base}/v1/cli/projects`, {
360
+ method: "POST",
361
+ headers: cliAuthHeaders(cliToken),
362
+ body: JSON.stringify({ name })
363
+ });
364
+ const json = await res.json().catch(() => null);
365
+ if (!res.ok || !json?.ok || !json.data) {
366
+ throw new Error(json?.error?.message ?? `Could not create project (HTTP ${res.status}).`);
367
+ }
368
+ const { id, name: created, slug, apiKey } = json.data;
369
+ return { id, name: created, slug, apiKey: apiKey ?? null };
370
+ }
371
+ async function mintProjectKey(endpoint, cliToken, projectId) {
372
+ const base = trimTrailingSlash(endpoint);
373
+ try {
374
+ const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
375
+ method: "POST",
376
+ headers: cliAuthHeaders(cliToken),
377
+ body: JSON.stringify({})
378
+ });
379
+ const json = await res.json().catch(() => null);
380
+ if (res.ok && json?.ok) return json.data?.key ?? null;
381
+ } catch {
382
+ }
383
+ return null;
384
+ }
385
+
214
386
  // src/freshness.ts
215
387
  var REGISTRY = "https://registry.npmjs.org";
216
388
  var DEFAULT_TIMEOUT_MS = 2e3;
@@ -367,6 +539,33 @@ function getFrameworkFromPkg(pkg) {
367
539
  return void 0;
368
540
  }
369
541
 
542
+ // src/auth-ui.ts
543
+ var LINE = "\u2500".repeat(58);
544
+ function printAuthBanner(userCode, verificationUri) {
545
+ console.log("");
546
+ console.log(` \u250C${LINE}\u2510`);
547
+ console.log(` \u2502 Browser sign-in \u2014 follow these 3 steps \u2502`);
548
+ console.log(` \u251C${LINE}\u2524`);
549
+ console.log(` \u2502 \u2502`);
550
+ console.log(` \u2502 1. A browser tab will open (or visit the URL below) \u2502`);
551
+ console.log(` \u2502 2. Sign in if prompted, then click "Approve" \u2502`);
552
+ console.log(` \u2502 3. Come back here \u2014 setup continues automatically \u2502`);
553
+ console.log(` \u2502 \u2502`);
554
+ console.log(` \u251C${LINE}\u2524`);
555
+ console.log(` \u2502 Verification code (show in browser \u2014 NOT terminal): \u2502`);
556
+ console.log(` \u2502 \u2502`);
557
+ console.log(` \u2502 ${userCode.padEnd(52)}\u2502`);
558
+ console.log(` \u2502 \u2502`);
559
+ console.log(` \u2502 \u26A0 Do NOT paste or type this code in your terminal \u2502`);
560
+ console.log(` \u2514${LINE}\u2518`);
561
+ console.log("");
562
+ console.log(` If the browser didn't open, visit this URL and approve:`);
563
+ console.log(` ${verificationUri}`);
564
+ console.log("");
565
+ console.log(" \u23F3 Waiting for browser approval\u2026 (Ctrl+C to cancel)");
566
+ console.log("");
567
+ }
568
+
370
569
  // src/init.ts
371
570
  var ENV_FILES = [".env.local", ".env"];
372
571
  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;
@@ -392,8 +591,8 @@ async function runInit(options = {}) {
392
591
  const detected = detectFramework(cwd, pkg);
393
592
  const framework = await chooseFramework(detected, options);
394
593
  const consoleBase = await resolveConsoleUrl({ cwd });
395
- await maybeGuideConsoleSetup(options, consoleBase);
396
- const credentials = await collectCredentials(options, consoleBase);
594
+ const endpoint = resolveCloudEndpoint(options.endpoint);
595
+ const credentials = await acquireCredentials(options, consoleBase, endpoint);
397
596
  await verifyCredentials(credentials, options, consoleBase);
398
597
  const pm = detectPackageManager(cwd);
399
598
  const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
@@ -402,8 +601,7 @@ async function runInit(options = {}) {
402
601
  } else {
403
602
  p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
404
603
  }
405
- const endpoint = resolveCloudEndpoint(options.endpoint);
406
- writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
604
+ await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
407
605
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
408
606
  const enableRewards = await maybeEnableRewards(options);
409
607
  printNextSteps(framework, consoleBase, enableRewards);
@@ -449,63 +647,17 @@ async function chooseFramework(detected, options) {
449
647
  }
450
648
  return FRAMEWORKS[confirmed];
451
649
  }
452
- async function maybeGuideConsoleSetup(options, consoleBase) {
453
- if (options.yes) return;
454
- if (options.projectId && options.apiKey) return;
455
- const existing = loadConfig();
456
- if (existing.projectId && existing.apiKey) return;
457
- const choice = await p.select({
458
- message: "Do you already have a Mushi project?",
459
- options: [
460
- {
461
- value: "create",
462
- label: "No \u2014 open the console to create one",
463
- hint: "Recommended for first-time setup"
464
- },
465
- {
466
- value: "have",
467
- label: "Yes \u2014 I have Project ID + API key",
468
- hint: "Paste values from the admin console"
469
- },
470
- {
471
- value: "login",
472
- label: "Use mushi login first",
473
- hint: "Run `mushi login` in another terminal, then re-run this wizard"
474
- }
475
- ]
476
- });
477
- if (p.isCancel(choice)) {
478
- p.cancel("Aborted.");
479
- process.exit(0);
480
- }
481
- if (choice === "login") {
482
- p.log.info("Run `mushi login` to save credentials, then re-run `mushi init`.");
483
- p.cancel("Aborted.");
484
- process.exit(0);
485
- }
486
- if (choice !== "create") return;
487
- const setupUrl = cliSetupDeepLink(consoleBase);
488
- p.log.info(`Opening the Mushi console:
489
- ${setupUrl}`);
490
- await openInBrowser(setupUrl);
491
- p.log.message("In the console:");
492
- p.log.message(" 1. Enter a project name and click Create");
493
- p.log.message(" 2. Copy the Project ID from the success panel");
494
- p.log.message(" 3. Generate an API key (Verify tab \u2014 report:write scope)");
495
- p.log.message(" 4. Return here and paste both values");
496
- const ready = await p.confirm({
497
- message: "Created your project and ready to paste credentials?",
498
- initialValue: false
499
- });
500
- if (p.isCancel(ready) || !ready) {
501
- p.log.warn(`Finish in the console first: ${setupUrl}`);
650
+ async function acquireCredentials(options, consoleBase, endpoint) {
651
+ if (options.projectId && options.apiKey) {
652
+ return {
653
+ projectId: sanitizeSecret(options.projectId),
654
+ apiKey: sanitizeSecret(options.apiKey)
655
+ };
502
656
  }
503
- }
504
- async function collectCredentials(options, consoleBase) {
505
657
  const existing = loadConfig();
506
658
  if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
507
659
  const reuse = options.yes ? true : await p.confirm({
508
- message: "Use Project ID + API key from ~/.config/mushi/config.json?",
660
+ message: "Use the Mushi credentials saved from your last sign-in?",
509
661
  initialValue: true
510
662
  });
511
663
  if (p.isCancel(reuse)) {
@@ -519,6 +671,144 @@ async function collectCredentials(options, consoleBase) {
519
671
  };
520
672
  }
521
673
  }
674
+ if (options.yes) {
675
+ const creds = await runBrowserSignIn(options, endpoint, consoleBase);
676
+ if (creds) return creds;
677
+ p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
678
+ } else {
679
+ const method = await p.select({
680
+ message: "Connect this app to Mushi",
681
+ initialValue: "browser",
682
+ options: [
683
+ {
684
+ value: "browser",
685
+ label: "Sign in with your browser",
686
+ hint: "Recommended \u2014 no copy-paste, creates the project + key for you"
687
+ },
688
+ {
689
+ value: "manual",
690
+ label: "Paste a Project ID + API key",
691
+ hint: "Self-hosted or expert setup \u2014 run npx mushi-mushi (not mushi setup)"
692
+ }
693
+ ]
694
+ });
695
+ if (p.isCancel(method)) {
696
+ p.cancel("Aborted.");
697
+ process.exit(0);
698
+ }
699
+ if (method === "browser") {
700
+ const creds = await runBrowserSignIn(options, endpoint, consoleBase);
701
+ if (creds) return creds;
702
+ p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
703
+ }
704
+ }
705
+ return collectCredentialsManually(options, consoleBase);
706
+ }
707
+ async function runBrowserSignIn(options, endpoint, consoleBase) {
708
+ const startSpin = p.spinner();
709
+ startSpin.start("Starting secure browser sign-in\u2026");
710
+ let session;
711
+ try {
712
+ session = await startDeviceAuth(endpoint);
713
+ } catch (err) {
714
+ startSpin.stop("Could not start browser sign-in.");
715
+ p.log.warn(err instanceof Error ? err.message : String(err));
716
+ return null;
717
+ }
718
+ startSpin.stop("Browser sign-in ready.");
719
+ try {
720
+ await openInBrowser(session.verification_uri);
721
+ } catch {
722
+ }
723
+ printAuthBanner(session.user_code, session.verification_uri);
724
+ const waitSpin = p.spinner();
725
+ waitSpin.start("Waiting for you to approve in the browser\u2026");
726
+ let cliToken;
727
+ try {
728
+ cliToken = await waitForCliToken(endpoint, session);
729
+ } catch (err) {
730
+ waitSpin.stop("Browser sign-in didn't complete.");
731
+ p.log.warn(err instanceof Error ? err.message : String(err));
732
+ return null;
733
+ }
734
+ waitSpin.stop("Approved.");
735
+ let projectId = options.projectId ? sanitizeSecret(options.projectId) : void 0;
736
+ let apiKey;
737
+ if (!projectId) {
738
+ let projects = [];
739
+ const fetchSpin = p.spinner();
740
+ fetchSpin.start("Loading your projects\u2026");
741
+ try {
742
+ projects = await listProjects(endpoint, cliToken);
743
+ fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
744
+ } catch (err) {
745
+ fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
746
+ p.log.warn(err instanceof Error ? err.message : String(err));
747
+ }
748
+ const NEW = "__new__";
749
+ const choice = await p.select({
750
+ message: "Choose a project",
751
+ initialValue: projects[0]?.id ?? NEW,
752
+ options: [
753
+ ...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
754
+ { value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
755
+ ]
756
+ });
757
+ if (p.isCancel(choice)) {
758
+ p.cancel("Aborted.");
759
+ process.exit(0);
760
+ }
761
+ if (choice === NEW) {
762
+ const name = await p.text({
763
+ message: "Project name",
764
+ placeholder: "My app",
765
+ validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
766
+ });
767
+ if (p.isCancel(name)) {
768
+ p.cancel("Aborted.");
769
+ process.exit(0);
770
+ }
771
+ const createSpin = p.spinner();
772
+ createSpin.start(`Creating "${name.trim()}"\u2026`);
773
+ try {
774
+ const created = await createProject(endpoint, cliToken, name.trim());
775
+ projectId = created.id;
776
+ apiKey = created.apiKey ?? void 0;
777
+ createSpin.stop(`Created project "${created.name}".`);
778
+ } catch (err) {
779
+ createSpin.stop("Could not create the project.");
780
+ p.log.warn(err instanceof Error ? err.message : String(err));
781
+ return null;
782
+ }
783
+ } else {
784
+ projectId = choice;
785
+ }
786
+ }
787
+ if (projectId && !apiKey) {
788
+ const keySpin = p.spinner();
789
+ keySpin.start("Minting SDK key\u2026");
790
+ try {
791
+ const minted = await mintProjectKey(endpoint, cliToken, projectId);
792
+ apiKey = minted ?? void 0;
793
+ if (!apiKey) {
794
+ keySpin.stop("Could not mint an API key.");
795
+ p.log.warn(
796
+ `Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
797
+ );
798
+ return null;
799
+ }
800
+ keySpin.stop("SDK key ready.");
801
+ } catch (err) {
802
+ keySpin.stop("Could not mint an API key.");
803
+ p.log.warn(err instanceof Error ? err.message : String(err));
804
+ return null;
805
+ }
806
+ }
807
+ if (!projectId || !apiKey) return null;
808
+ return { apiKey, projectId };
809
+ }
810
+ async function collectCredentialsManually(options, consoleBase) {
811
+ const existing = loadConfig();
522
812
  const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
523
813
  message: "Project ID",
524
814
  placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
@@ -618,13 +908,37 @@ function runCommand(pm, packages, cwd) {
618
908
  });
619
909
  });
620
910
  }
621
- function writeEnvFile(cwd, apiKey, projectId, framework) {
911
+ async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
622
912
  const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
623
913
  const targetPath = join3(cwd, target);
624
- const newVars = envVarsToWrite(apiKey, projectId, framework);
914
+ const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
625
915
  const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
626
916
  if (existing.includes("MUSHI_PROJECT_ID")) {
627
- p.log.warn(`Existing MUSHI_* vars found in ${target} \u2014 leaving them untouched.`);
917
+ let shouldOverwrite = overwrite;
918
+ if (!shouldOverwrite) {
919
+ const answer = await p.confirm({
920
+ message: `Existing MUSHI_* vars found in ${target}. Update with new credentials?`,
921
+ initialValue: true
922
+ });
923
+ if (p.isCancel(answer)) {
924
+ p.log.info(`Kept existing env vars in ${target}.`);
925
+ return;
926
+ }
927
+ shouldOverwrite = Boolean(answer);
928
+ }
929
+ if (!shouldOverwrite) {
930
+ p.log.info(`Kept existing env vars in ${target}. Re-run and confirm to overwrite.`);
931
+ return;
932
+ }
933
+ const MUSHI_LINE_RE = /^(NEXT_PUBLIC_|NUXT_PUBLIC_|VITE_|EXPO_PUBLIC_)?MUSHI_[A-Z_]+=.*/gm;
934
+ const stripped = existing.replace(MUSHI_LINE_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
935
+ const prefix2 = stripped.length > 0 ? "\n" : "";
936
+ writeFileSync(targetPath, `${stripped}${prefix2}
937
+ # Mushi Mushi
938
+ ${newVars}
939
+ `);
940
+ p.log.success(`Updated MUSHI_* env vars in ${target}`);
941
+ warnIfMissingFromGitignore(cwd, target);
628
942
  return;
629
943
  }
630
944
  const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
@@ -685,12 +999,14 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
685
999
  p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
686
1000
  p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
687
1001
  }
688
- p.log.message("Verify the install:");
689
- p.log.message(" \u2022 Start your dev server");
690
- p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
691
- p.log.message(` \u2022 Submit a test report \u2014 it should appear at ${reportsUrl(consoleBase)}`);
692
- p.log.info(
693
- "Tip: `mushi connect --write-env --wire-ide --wait` also wires Cursor MCP and waits for SDK heartbeat."
1002
+ p.note(
1003
+ [
1004
+ " [ ] 1. Paste the init snippet above into your app entry file",
1005
+ " [ ] 2. Start your dev server",
1006
+ " [ ] 3. Run: mushi connect --write-env --wire-ide --wait",
1007
+ ` [ ] 4. Open the Verify tab to send a test report: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
1008
+ ].join("\n"),
1009
+ "Next steps:"
694
1010
  );
695
1011
  }
696
1012
  async function maybeOfferConnect(credentials, options, _consoleBase) {
@@ -702,7 +1018,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
702
1018
  if (p.isCancel(answer) || !answer) return;
703
1019
  const endpoint = resolveCloudEndpoint(options.endpoint);
704
1020
  try {
705
- const { runConnect } = await import("./connect-IC7I76Z2.js");
1021
+ const { runConnect } = await import("./connect-2RM43XHO.js");
706
1022
  await runConnect({
707
1023
  apiKey: credentials.apiKey,
708
1024
  projectId: credentials.projectId,
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-T4XX5FMH.js";
3
+ } from "./chunk-L7KBJ2XY.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.19.0",
3
+ "version": "0.21.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": {
@@ -18,13 +18,13 @@
18
18
  "vitest": "^4.1.8",
19
19
  "@mushi-mushi/eslint-config": "0.0.0"
20
20
  },
21
- "author": "Kenji Sakuramoto",
21
+ "author": "Kenji Sakuramoto (https://bsky.app/profile/mushimushi.dev)",
22
22
  "repository": {
23
23
  "type": "git",
24
24
  "url": "https://github.com/kensaurus/mushi-mushi.git",
25
25
  "directory": "packages/cli"
26
26
  },
27
- "homepage": "https://github.com/kensaurus/mushi-mushi/tree/main/packages/cli#readme",
27
+ "homepage": "https://kensaur.us/mushi-mushi",
28
28
  "bugs": {
29
29
  "url": "https://github.com/kensaurus/mushi-mushi/issues"
30
30
  },
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var MUSHI_CLI_VERSION = true ? "0.19.0" : "0.0.0-dev";
3
-
4
- export {
5
- MUSHI_CLI_VERSION
6
- };