@mushi-mushi/cli 0.18.2 → 0.19.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,3 +1,13 @@
1
+ import {
2
+ MUSHI_CLI_VERSION
3
+ } from "./chunk-T4XX5FMH.js";
4
+ import {
5
+ TEST_REPORT_FETCH_TIMEOUT_MS,
6
+ loadConfig,
7
+ normalizeEndpoint,
8
+ resolveCloudEndpoint,
9
+ saveConfig
10
+ } from "./chunk-XEXYGDQV.js";
1
11
  import {
2
12
  FRAMEWORKS,
3
13
  detectFramework,
@@ -6,9 +16,6 @@ import {
6
16
  installCommand,
7
17
  readPackageJson
8
18
  } from "./chunk-7Q44KWGN.js";
9
- import {
10
- MUSHI_CLI_VERSION
11
- } from "./chunk-E4IZPV6D.js";
12
19
 
13
20
  // src/init.ts
14
21
  import * as p from "@clack/prompts";
@@ -17,117 +24,191 @@ import { randomUUID } from "crypto";
17
24
  import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
18
25
  import { join as join3 } from "path";
19
26
 
20
- // src/config.ts
21
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
22
- import { homedir } from "os";
27
+ // src/console-url.ts
28
+ import { existsSync, readFileSync } from "fs";
23
29
  import { dirname, join } from "path";
24
- var SECURE_FILE_MODE = 384;
25
- var SECURE_DIR_MODE = 448;
26
- function resolveXdgConfigPath() {
27
- const xdg = process.env["XDG_CONFIG_HOME"];
28
- if (xdg && xdg.length > 0) {
29
- return join(xdg, "mushi", "config.json");
30
- }
31
- if (process.platform === "win32") {
32
- const appData = process.env["APPDATA"];
33
- if (appData && appData.length > 0) {
34
- return join(appData, "mushi", "config.json");
35
- }
36
- }
37
- return join(homedir(), ".config", "mushi", "config.json");
38
- }
39
- var LEGACY_CONFIG_PATH = join(homedir(), ".mushirc");
40
- var CONFIG_PATH = resolveXdgConfigPath();
41
- function loadConfig(path = CONFIG_PATH) {
42
- let file = {};
43
- if (existsSync(path)) {
44
- tightenPermissions(path);
45
- try {
46
- file = JSON.parse(readFileSync(path, "utf-8"));
47
- } catch {
30
+ var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
31
+ var LOCAL_ADMIN_URL = "http://localhost:6464";
32
+ var LOCAL_PROBE_MS = 800;
33
+ function normalizeConsoleBase(url) {
34
+ return url.trim().replace(/\/+$/, "");
35
+ }
36
+ function consoleUrl(base, route) {
37
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
38
+ return `${normalizeConsoleBase(base)}${normalizedRoute}`;
39
+ }
40
+ function resolveConsoleUrlFromEnv() {
41
+ const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
42
+ return raw ? normalizeConsoleBase(raw) : void 0;
43
+ }
44
+ function isInsideMushiMonorepo(cwd) {
45
+ let dir = cwd;
46
+ for (let depth = 0; depth < 14; depth += 1) {
47
+ const pkgPath = join(dir, "package.json");
48
+ if (existsSync(pkgPath)) {
49
+ try {
50
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
51
+ if (pkg.name === "mushi-mushi") return true;
52
+ } catch {
53
+ }
48
54
  }
49
- } else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
50
- file = migrateLegacyConfig() ?? {};
51
- }
52
- const endpointFromEnv = process.env["MUSHI_API_ENDPOINT"] ?? process.env["MUSHI_ENDPOINT"] ?? void 0;
53
- const fromEnv = {
54
- ...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
55
- ...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
56
- ...endpointFromEnv ? { endpoint: endpointFromEnv } : {}
57
- };
58
- return { ...file, ...fromEnv };
59
- }
60
- function saveConfig(config, path = CONFIG_PATH) {
61
- const dir = dirname(path);
62
- if (!existsSync(dir)) {
63
- mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
64
- } else {
65
- tightenDirPermissions(dir);
55
+ const parent = dirname(dir);
56
+ if (parent === dir) break;
57
+ dir = parent;
66
58
  }
67
- writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
68
- tightenPermissions(path);
59
+ return false;
69
60
  }
70
- function migrateLegacyConfig(legacyPath = LEGACY_CONFIG_PATH, destPath = CONFIG_PATH) {
71
- if (!existsSync(legacyPath)) return null;
72
- let parsed;
73
- try {
74
- parsed = JSON.parse(readFileSync(legacyPath, "utf-8"));
75
- } catch {
76
- return null;
77
- }
78
- const dir = dirname(destPath);
79
- if (!existsSync(dir)) {
80
- mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
81
- }
61
+ async function probeLocalAdminConsole() {
82
62
  try {
83
- renameSync(legacyPath, destPath);
84
- } catch {
85
- writeFileSync(destPath, JSON.stringify(parsed, null, 2), { mode: SECURE_FILE_MODE });
86
- try {
87
- unlinkSync(legacyPath);
88
- } catch {
63
+ const controller = new AbortController();
64
+ const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
65
+ const res = await fetch(LOCAL_ADMIN_URL, {
66
+ signal: controller.signal,
67
+ method: "HEAD"
68
+ });
69
+ clearTimeout(timer);
70
+ if (res.ok || res.status === 404 || res.status === 401) {
71
+ return LOCAL_ADMIN_URL;
89
72
  }
90
- }
91
- tightenPermissions(destPath);
92
- tightenDirPermissions(dir);
93
- return parsed;
94
- }
95
- function tightenPermissions(path) {
96
- if (process.platform === "win32") return;
97
- try {
98
- const current = statSync(path).mode & 511;
99
- if (current !== SECURE_FILE_MODE) chmodSync(path, SECURE_FILE_MODE);
100
73
  } catch {
101
74
  }
75
+ return null;
102
76
  }
103
- function tightenDirPermissions(path) {
104
- if (process.platform === "win32") return;
105
- try {
106
- const current = statSync(path).mode & 511;
107
- if (current !== SECURE_DIR_MODE) chmodSync(path, SECURE_DIR_MODE);
108
- } catch {
109
- }
77
+ function resolveConsoleUrlSync(cwd = process.cwd()) {
78
+ const fromEnv = resolveConsoleUrlFromEnv();
79
+ if (fromEnv) return fromEnv;
80
+ const config = loadConfig();
81
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
82
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
83
+ return HOSTED_CONSOLE_BASE;
110
84
  }
111
-
112
- // src/endpoint.ts
113
- var TEST_REPORT_TIMEOUT_MS = 1e4;
114
- var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
115
- var CLOUD_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/functions/v1/api";
116
- function resolveCloudEndpoint(explicit) {
117
- if (explicit?.trim()) return explicit.trim();
118
- const fromEnv = process.env.MUSHI_API_ENDPOINT?.trim();
85
+ async function resolveConsoleUrl(options) {
86
+ const fromEnv = resolveConsoleUrlFromEnv();
119
87
  if (fromEnv) return fromEnv;
120
- return CLOUD_API_ENDPOINT;
88
+ const config = loadConfig();
89
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
90
+ const local = await probeLocalAdminConsole();
91
+ if (local) return local;
92
+ const cwd = options?.cwd ?? process.cwd();
93
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
94
+ return HOSTED_CONSOLE_BASE;
95
+ }
96
+ function projectIdHint(base) {
97
+ return `Where to find it: ${consoleUrl(base, "/projects")} \u2192 select your project \u2192 click the UUID chip to copy (or copy it from the panel right after you create the project).`;
98
+ }
99
+ function apiKeyHint(base) {
100
+ return `Where to find it: ${consoleUrl(base, "/onboarding?tab=verify")} \u2192 Generate API key (SDK ingest needs report:write \u2014 not Settings \u2192 BYOK keys). Treat it like a password \u2014 env file only, never commit.`;
101
+ }
102
+ function cliSetupDeepLink(base) {
103
+ return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
104
+ }
105
+ function reportsUrl(base, reportId) {
106
+ return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
107
+ }
108
+ 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}"`;
111
+ await new Promise((resolve2) => {
112
+ exec(openCmd, () => resolve2());
113
+ });
121
114
  }
122
- function normalizeEndpoint(url) {
123
- if (!url) {
124
- throw new Error(
125
- "No API endpoint configured. Run `mushi init` or set MUSHI_API_ENDPOINT. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api"
126
- );
115
+
116
+ // src/signals.ts
117
+ var activeController = null;
118
+ function getAbortSignal(external) {
119
+ if (external) return external.signal;
120
+ if (!activeController || activeController.signal.aborted) {
121
+ activeController = new AbortController();
122
+ }
123
+ return activeController.signal;
124
+ }
125
+
126
+ // src/cli-shared.ts
127
+ var API_TIMEOUT_MS = 15e3;
128
+ async function apiCall(path, config, options = {}) {
129
+ const endpoint = config.endpoint;
130
+ if (!endpoint) {
131
+ return {
132
+ ok: false,
133
+ error: {
134
+ code: "NO_ENDPOINT",
135
+ message: "No API endpoint configured.\n Run: mushi login --api-key <key> --endpoint <url> --project-id <id>\n Or: export MUSHI_API_ENDPOINT=https://<project-ref>.supabase.co/functions/v1/api"
136
+ }
137
+ };
138
+ }
139
+ const controller = new AbortController();
140
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
141
+ const signals = [controller.signal, getAbortSignal()];
142
+ const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
143
+ try {
144
+ const res = await fetch(`${endpoint}${path}`, {
145
+ ...options,
146
+ signal: compositeSignal,
147
+ headers: {
148
+ "Content-Type": "application/json",
149
+ // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
150
+ // fallback accepted by some older middleware. Both are sent.
151
+ "Authorization": `Bearer ${config.apiKey ?? ""}`,
152
+ "X-Mushi-Api-Key": config.apiKey ?? "",
153
+ "X-Mushi-Project": config.projectId ?? "",
154
+ "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
155
+ ...options.headers
156
+ }
157
+ });
158
+ clearTimeout(timer);
159
+ let body;
160
+ const contentType = res.headers.get("content-type") ?? "";
161
+ if (contentType.includes("application/json")) {
162
+ try {
163
+ body = await res.json();
164
+ } catch {
165
+ body = null;
166
+ }
167
+ } else {
168
+ const text2 = await res.text();
169
+ try {
170
+ body = JSON.parse(text2);
171
+ } catch {
172
+ body = {
173
+ ok: false,
174
+ error: {
175
+ code: `HTTP_${res.status}`,
176
+ message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
177
+ }
178
+ };
179
+ }
180
+ }
181
+ if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
182
+ const b = body;
183
+ return {
184
+ ok: false,
185
+ httpStatus: res.status,
186
+ error: {
187
+ code: b["code"] ?? `HTTP_${res.status}`,
188
+ message: b["message"] ?? `Request failed (${res.status})`
189
+ }
190
+ };
191
+ }
192
+ return body;
193
+ } catch (err) {
194
+ clearTimeout(timer);
195
+ if (err instanceof Error && err.name === "AbortError") {
196
+ return {
197
+ ok: false,
198
+ error: {
199
+ code: "TIMEOUT",
200
+ message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
201
+ }
202
+ };
203
+ }
204
+ return {
205
+ ok: false,
206
+ error: {
207
+ code: "NETWORK_ERROR",
208
+ message: err instanceof Error ? err.message : String(err)
209
+ }
210
+ };
127
211
  }
128
- let end = url.length;
129
- while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
130
- return url.slice(0, end);
131
212
  }
132
213
 
133
214
  // src/freshness.ts
@@ -190,7 +271,7 @@ function parse(version) {
190
271
  }
191
272
 
192
273
  // src/monorepo.ts
193
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
274
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
194
275
  import { dirname as dirname2, join as join2, resolve } from "path";
195
276
  var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
196
277
  var FRAMEWORK_DEPS = {
@@ -269,7 +350,7 @@ function readPackageJsonSafely(path) {
269
350
  }
270
351
  function isFileSafe(path) {
271
352
  try {
272
- return existsSync2(path) && statSync2(path).isFile();
353
+ return existsSync2(path) && statSync(path).isFile();
273
354
  } catch {
274
355
  return false;
275
356
  }
@@ -310,7 +391,10 @@ async function runInit(options = {}) {
310
391
  }
311
392
  const detected = detectFramework(cwd, pkg);
312
393
  const framework = await chooseFramework(detected, options);
313
- const credentials = await collectCredentials(options);
394
+ const consoleBase = await resolveConsoleUrl({ cwd });
395
+ await maybeGuideConsoleSetup(options, consoleBase);
396
+ const credentials = await collectCredentials(options, consoleBase);
397
+ await verifyCredentials(credentials, options, consoleBase);
314
398
  const pm = detectPackageManager(cwd);
315
399
  const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
316
400
  if (!options.skipInstall) {
@@ -322,8 +406,9 @@ async function runInit(options = {}) {
322
406
  writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
323
407
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
324
408
  const enableRewards = await maybeEnableRewards(options);
325
- printNextSteps(framework, credentials.apiKey, credentials.projectId, enableRewards);
326
- await maybeSendTestReport(credentials, { ...options, endpoint });
409
+ printNextSteps(framework, consoleBase, enableRewards);
410
+ await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
411
+ await maybeOfferConnect(credentials, options, consoleBase);
327
412
  p.outro("Setup complete. Happy bug squashing \u{1F41B}");
328
413
  }
329
414
  function ensureInteractiveOrBailOut(options) {
@@ -364,25 +449,93 @@ async function chooseFramework(detected, options) {
364
449
  }
365
450
  return FRAMEWORKS[confirmed];
366
451
  }
367
- async function collectCredentials(options) {
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}`);
502
+ }
503
+ }
504
+ async function collectCredentials(options, consoleBase) {
368
505
  const existing = loadConfig();
506
+ if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
507
+ const reuse = options.yes ? true : await p.confirm({
508
+ message: "Use Project ID + API key from ~/.config/mushi/config.json?",
509
+ initialValue: true
510
+ });
511
+ if (p.isCancel(reuse)) {
512
+ p.cancel("Aborted.");
513
+ process.exit(0);
514
+ }
515
+ if (reuse) {
516
+ return {
517
+ projectId: sanitizeSecret(existing.projectId),
518
+ apiKey: sanitizeSecret(existing.apiKey)
519
+ };
520
+ }
521
+ }
369
522
  const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
370
523
  message: "Project ID",
371
524
  placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
372
- hint: "Where to find it: https://kensaur.us/mushi-mushi/projects \u2192 click your project \u2192 copy the UUID below the project name.",
373
- validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) \u2014 copy it from the Mushi admin console Projects page."
525
+ hint: projectIdHint(consoleBase),
526
+ 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."
374
527
  });
375
528
  const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
376
529
  message: "API key",
377
530
  placeholder: "mushi_xxxxxxxxxxxx",
378
- hint: "Where to find it: https://kensaur.us/mushi-mushi/settings \u2192 API Keys tab. Treat it like a password \u2014 env file only, never commit it.",
531
+ hint: apiKeyHint(consoleBase),
379
532
  validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
380
533
  });
381
534
  const projectId = sanitizeSecret(rawProjectId);
382
535
  const apiKey = sanitizeSecret(rawApiKey);
383
536
  if (!PROJECT_ID_PATTERN.test(projectId)) {
384
537
  throw new Error(
385
- `Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId)} \u2014 copy it from the Projects page in the Mushi console at https://kensaur.us/mushi-mushi/projects`
538
+ `Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId)} \u2014 copy it from ${projectIdHint(consoleBase)}`
386
539
  );
387
540
  }
388
541
  if (!API_KEY_PATTERN.test(apiKey)) {
@@ -392,6 +545,23 @@ async function collectCredentials(options) {
392
545
  }
393
546
  return { projectId, apiKey };
394
547
  }
548
+ async function verifyCredentials(credentials, options, consoleBase) {
549
+ const endpoint = resolveCloudEndpoint(options.endpoint);
550
+ const spinner2 = p.spinner();
551
+ spinner2.start("Verifying credentials\u2026");
552
+ const result = await apiCall(
553
+ "/v1/sync/whoami",
554
+ { apiKey: credentials.apiKey, projectId: credentials.projectId, endpoint }
555
+ );
556
+ if (!result.ok) {
557
+ spinner2.stop("Credentials could not be verified.");
558
+ p.log.error(result.error?.message ?? "Authentication failed.");
559
+ p.log.info(`Double-check values in the console:
560
+ ${cliSetupDeepLink(consoleBase)}`);
561
+ throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
562
+ }
563
+ spinner2.stop(`Connected to ${result.data.project_name}`);
564
+ }
395
565
  function sanitizeSecret(raw) {
396
566
  return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[\r\n\0]/g, "");
397
567
  }
@@ -501,7 +671,7 @@ function persistCliConfig(apiKey, projectId, endpoint) {
501
671
  const existing = loadConfig();
502
672
  saveConfig({ ...existing, apiKey, projectId, endpoint });
503
673
  }
504
- function printNextSteps(framework, _apiKey, _projectId, enableRewards = false) {
674
+ function printNextSteps(framework, consoleBase, enableRewards = false) {
505
675
  p.note(framework.snippet(), "Add this to your app:");
506
676
  if (enableRewards) {
507
677
  const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
@@ -512,13 +682,44 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
512
682
  // import { MushiRewardsBadge } from '@mushi-mushi/react';
513
683
  // <MushiRewardsBadge showPoints />`;
514
684
  p.note(badgeSnippet, "Rewards badge snippet:");
515
- p.log.info("Enable rewards in your project settings at https://kensaur.us/mushi-mushi/rewards");
685
+ p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
516
686
  p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
517
687
  }
518
688
  p.log.message("Verify the install:");
519
689
  p.log.message(" \u2022 Start your dev server");
520
690
  p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
521
- p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
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."
694
+ );
695
+ }
696
+ async function maybeOfferConnect(credentials, options, _consoleBase) {
697
+ if (options.yes) return;
698
+ const answer = await p.confirm({
699
+ message: "Run `mushi connect --write-env --wire-ide --wait` now? (SDK env + Cursor MCP + heartbeat check)",
700
+ initialValue: false
701
+ });
702
+ if (p.isCancel(answer) || !answer) return;
703
+ const endpoint = resolveCloudEndpoint(options.endpoint);
704
+ try {
705
+ const { runConnect } = await import("./connect-IC7I76Z2.js");
706
+ await runConnect({
707
+ apiKey: credentials.apiKey,
708
+ projectId: credentials.projectId,
709
+ endpoint,
710
+ cwd: options.cwd,
711
+ writeEnv: true,
712
+ wireIde: true,
713
+ wait: true
714
+ });
715
+ } catch (err) {
716
+ p.log.warn(
717
+ err instanceof Error ? err.message : String(err)
718
+ );
719
+ p.log.info(
720
+ "You can run manually: mushi connect --write-env --wire-ide --wait"
721
+ );
722
+ }
522
723
  }
523
724
  async function maybeEnableRewards(options) {
524
725
  if (options.yes) return false;
@@ -589,8 +790,8 @@ async function maybeSendTestReport(credentials, options) {
589
790
  reportId = body.data?.reportId;
590
791
  } catch {
591
792
  }
592
- const reportPath = reportId ? `/reports/${reportId}` : "/reports";
593
- p.log.success(`View it at https://kensaur.us/mushi-mushi/admin${reportPath}`);
793
+ const consoleBase = options.consoleBase ?? resolveConsoleUrlSync(options.cwd);
794
+ p.log.success(`View it at ${reportsUrl(consoleBase, reportId)}`);
594
795
  } catch (err) {
595
796
  const aborted = err instanceof Error && err.name === "AbortError";
596
797
  spinner2.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-E4IZPV6D.js";
3
+ } from "./chunk-T4XX5FMH.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.18.2",
3
+ "version": "0.19.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": {