@mushi-mushi/cli 0.22.1 → 0.23.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.
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CLOUD_API_ENDPOINT,
3
4
  assertEndpoint,
4
5
  normalizeEndpoint
5
6
  } from "./chunk-LDPFTQLE.js";
6
7
 
7
8
  // src/connect.ts
8
9
  import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
9
- import { join as join3, resolve } from "path";
10
+ import { join as join4, resolve } from "path";
10
11
 
11
12
  // src/config.ts
12
13
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
@@ -101,9 +102,386 @@ function tightenDirPermissions(path) {
101
102
  }
102
103
  }
103
104
 
105
+ // src/version.ts
106
+ var MUSHI_CLI_VERSION = true ? "0.23.0" : "0.0.0-dev";
107
+
108
+ // src/errors.ts
109
+ var EXIT_CODE_MAP = {
110
+ E_AUTH_MISSING: 2,
111
+ E_AUTH_INVALID: 2,
112
+ E_PROJECT_MISSING: 2,
113
+ E_ENDPOINT_INVALID: 2,
114
+ E_INVALID_INPUT: 2,
115
+ E_NOT_INTERACTIVE: 2,
116
+ E_NETWORK: 3,
117
+ E_TIMEOUT: 3,
118
+ E_API_ERROR: 3,
119
+ E_RATE_LIMITED: 3,
120
+ E_FILE_NOT_FOUND: 1,
121
+ E_FILE_PERMISSION: 1,
122
+ E_FRESHNESS_STALE: 1,
123
+ E_INTERRUPTED: 130,
124
+ // 128 + SIGINT (2). POSIX convention.
125
+ E_INTERNAL: 1
126
+ };
127
+ var MushiCliError = class extends Error {
128
+ code;
129
+ hint;
130
+ cause;
131
+ constructor(code, message, hint, cause) {
132
+ super(message);
133
+ this.name = "MushiCliError";
134
+ this.code = code;
135
+ if (hint) this.hint = hint;
136
+ if (cause !== void 0) this.cause = cause;
137
+ }
138
+ /** Exit code POSIX-aware shell scripts should branch on. */
139
+ get exitCode() {
140
+ return EXIT_CODE_MAP[this.code];
141
+ }
142
+ /**
143
+ * Format for `--json` output. Used by the JSON writer when a CLI
144
+ * subcommand was invoked with `--json` and an error escapes.
145
+ */
146
+ toJSON() {
147
+ return {
148
+ error: {
149
+ code: this.code,
150
+ message: this.message,
151
+ ...this.hint ? { hint: this.hint } : {}
152
+ }
153
+ };
154
+ }
155
+ };
156
+ function printAndExit(err, asJson = false) {
157
+ const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
158
+ "E_INTERNAL",
159
+ err instanceof Error ? err.message : String(err),
160
+ "this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
161
+ err
162
+ );
163
+ if (asJson) {
164
+ process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
165
+ } else {
166
+ process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
167
+ `);
168
+ if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
169
+ `);
170
+ }
171
+ if (process.env.MUSHI_CLI_JSON_LOG === "1" || process.env.CI === "true") {
172
+ process.stderr.write(
173
+ JSON.stringify({
174
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
175
+ level: "error",
176
+ scope: "mushi:cli",
177
+ ...cliErr.toJSON(),
178
+ exitCode: cliErr.exitCode
179
+ }) + "\n"
180
+ );
181
+ }
182
+ process.exit(cliErr.exitCode);
183
+ }
184
+
185
+ // src/signals.ts
186
+ var installed = false;
187
+ var activeController = null;
188
+ function installSignalHandlers() {
189
+ if (installed) return;
190
+ installed = true;
191
+ const abortAndExit = (signal) => {
192
+ activeController?.abort(
193
+ new MushiCliError(
194
+ "E_INTERRUPTED",
195
+ `aborted by ${signal}`,
196
+ "partial state on the server is safe to retry \u2014 re-run the same command"
197
+ )
198
+ );
199
+ const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
200
+ process.nextTick(() => {
201
+ process.exit(code);
202
+ });
203
+ };
204
+ process.on("SIGINT", () => abortAndExit("SIGINT"));
205
+ process.on("SIGTERM", () => abortAndExit("SIGTERM"));
206
+ }
207
+ function getAbortSignal(external) {
208
+ if (external) return external.signal;
209
+ if (!activeController || activeController.signal.aborted) {
210
+ activeController = new AbortController();
211
+ }
212
+ return activeController.signal;
213
+ }
214
+
215
+ // src/console-url.ts
216
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
217
+ import { dirname as dirname2, join as join2 } from "path";
218
+ var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
219
+ var LOCAL_ADMIN_URL = "http://localhost:6464";
220
+ var LOCAL_PROBE_MS = 800;
221
+ function normalizeConsoleBase(url) {
222
+ const trimmed = url.trim();
223
+ let end = trimmed.length;
224
+ while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
225
+ return trimmed.slice(0, end);
226
+ }
227
+ function consoleUrl(base, route) {
228
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
229
+ return `${normalizeConsoleBase(base)}${normalizedRoute}`;
230
+ }
231
+ function resolveConsoleUrlFromEnv() {
232
+ const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
233
+ return raw ? normalizeConsoleBase(raw) : void 0;
234
+ }
235
+ function isInsideMushiMonorepo(cwd) {
236
+ let dir = cwd;
237
+ for (let depth = 0; depth < 14; depth += 1) {
238
+ const pkgPath = join2(dir, "package.json");
239
+ if (existsSync2(pkgPath)) {
240
+ try {
241
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
242
+ if (pkg.name === "mushi-mushi") return true;
243
+ } catch {
244
+ }
245
+ }
246
+ const parent = dirname2(dir);
247
+ if (parent === dir) break;
248
+ dir = parent;
249
+ }
250
+ return false;
251
+ }
252
+ async function probeLocalAdminConsole() {
253
+ try {
254
+ const controller = new AbortController();
255
+ const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
256
+ const res = await fetch(LOCAL_ADMIN_URL, {
257
+ signal: controller.signal,
258
+ method: "HEAD"
259
+ });
260
+ clearTimeout(timer);
261
+ if (res.ok || res.status === 404 || res.status === 401) {
262
+ return LOCAL_ADMIN_URL;
263
+ }
264
+ } catch {
265
+ }
266
+ return null;
267
+ }
268
+ function resolveConsoleUrlSync(cwd = process.cwd()) {
269
+ const fromEnv = resolveConsoleUrlFromEnv();
270
+ if (fromEnv) return fromEnv;
271
+ const config = loadConfig();
272
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
273
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
274
+ return HOSTED_CONSOLE_BASE;
275
+ }
276
+ async function resolveConsoleUrl(options) {
277
+ const fromEnv = resolveConsoleUrlFromEnv();
278
+ if (fromEnv) return fromEnv;
279
+ const config = loadConfig();
280
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
281
+ const local = await probeLocalAdminConsole();
282
+ if (local) return local;
283
+ const cwd = options?.cwd ?? process.cwd();
284
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
285
+ return HOSTED_CONSOLE_BASE;
286
+ }
287
+ function projectIdHint(base) {
288
+ 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).`;
289
+ }
290
+ function apiKeyHint(base) {
291
+ 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.`;
292
+ }
293
+ function cliSetupDeepLink(base) {
294
+ return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
295
+ }
296
+ function reportsUrl(base, reportId) {
297
+ return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
298
+ }
299
+ async function openInBrowser(url) {
300
+ let parsed;
301
+ try {
302
+ parsed = new URL(url);
303
+ } catch {
304
+ return;
305
+ }
306
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
307
+ return;
308
+ }
309
+ const safeUrl = parsed.toString();
310
+ const { spawn } = await import("child_process");
311
+ const [command, args] = process.platform === "win32" ? (
312
+ // Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
313
+ // `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
314
+ // characters like `&` / `|` / `^` could still break out — exactly the
315
+ // injection surface CodeQL flags. rundll32 never invokes a shell: the URL
316
+ // is handed to the default browser as one opaque argument.
317
+ ["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
318
+ ) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
319
+ await new Promise((resolve2) => {
320
+ try {
321
+ const child = spawn(command, [...args], { stdio: "ignore", shell: false });
322
+ child.on("error", () => resolve2());
323
+ child.on("exit", () => resolve2());
324
+ child.unref();
325
+ } catch {
326
+ resolve2();
327
+ }
328
+ });
329
+ }
330
+
331
+ // src/cli-shared.ts
332
+ var UUID_RX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
333
+ function requireUuid(value, label = "id") {
334
+ if (UUID_RX.test(value)) return value;
335
+ throw new MushiCliError(
336
+ "E_INVALID_INPUT",
337
+ `${label} must be a valid UUID`,
338
+ "copy the full id from `mushi reports list` or the console URL"
339
+ );
340
+ }
341
+ var API_TIMEOUT_MS = 15e3;
342
+ async function probeEndpointHealth(endpoint) {
343
+ const t0 = Date.now();
344
+ const controller = new AbortController();
345
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
346
+ try {
347
+ const res = await fetch(`${endpoint}/health`, { signal: controller.signal });
348
+ clearTimeout(timer);
349
+ const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
350
+ return { ok: res.ok, status: res.status, latencyMs: Date.now() - t0, body };
351
+ } catch (err) {
352
+ clearTimeout(timer);
353
+ throw err;
354
+ }
355
+ }
356
+ async function apiCall(path, config, options = {}) {
357
+ const endpoint = config.endpoint;
358
+ if (!endpoint) {
359
+ return {
360
+ ok: false,
361
+ error: {
362
+ code: "NO_ENDPOINT",
363
+ 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"
364
+ }
365
+ };
366
+ }
367
+ const controller = new AbortController();
368
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
369
+ const signals = [controller.signal, getAbortSignal()];
370
+ const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
371
+ try {
372
+ const res = await fetch(`${endpoint}${path}`, {
373
+ ...options,
374
+ signal: compositeSignal,
375
+ headers: {
376
+ "Content-Type": "application/json",
377
+ // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
378
+ // fallback accepted by some older middleware. Both are sent.
379
+ "Authorization": `Bearer ${config.apiKey ?? ""}`,
380
+ "X-Mushi-Api-Key": config.apiKey ?? "",
381
+ "X-Mushi-Project": config.projectId ?? "",
382
+ "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
383
+ ...options.headers
384
+ }
385
+ });
386
+ clearTimeout(timer);
387
+ let body;
388
+ const contentType = res.headers.get("content-type") ?? "";
389
+ if (contentType.includes("application/json")) {
390
+ try {
391
+ body = await res.json();
392
+ } catch {
393
+ body = null;
394
+ }
395
+ } else {
396
+ const text = await res.text();
397
+ try {
398
+ body = JSON.parse(text);
399
+ } catch {
400
+ body = {
401
+ ok: false,
402
+ error: {
403
+ code: `HTTP_${res.status}`,
404
+ message: text.trim().slice(0, 300) || `HTTP ${res.status}`
405
+ }
406
+ };
407
+ }
408
+ }
409
+ if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
410
+ const b = body;
411
+ const nested = b["error"];
412
+ const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
413
+ const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
414
+ return {
415
+ ok: false,
416
+ httpStatus: res.status,
417
+ error: { code: errCode, message: errMsg }
418
+ };
419
+ }
420
+ return body;
421
+ } catch (err) {
422
+ clearTimeout(timer);
423
+ if (err instanceof Error && err.name === "AbortError") {
424
+ return {
425
+ ok: false,
426
+ error: {
427
+ code: "TIMEOUT",
428
+ message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
429
+ }
430
+ };
431
+ }
432
+ return {
433
+ ok: false,
434
+ error: {
435
+ code: "NETWORK_ERROR",
436
+ message: err instanceof Error ? err.message : String(err)
437
+ }
438
+ };
439
+ }
440
+ }
441
+ function die(result, exitCode = 1) {
442
+ const { code, message } = result.error;
443
+ const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
444
+ process.stderr.write(`error${status}: ${code} \u2014 ${message}
445
+ `);
446
+ if (code === "INSUFFICIENT_SCOPE") {
447
+ process.stderr.write(
448
+ "\n Your current API key does not have the required scope for this command.\n New keys minted by the wizard already include both scopes. Existing keys\n can be upgraded with:\n\n mushi login --upgrade-scope\n\n Or visit your console \u2192 Projects \u2192 API Keys to mint a new key.\n"
449
+ );
450
+ }
451
+ process.exit(exitCode);
452
+ }
453
+ function requireConfig(opts = {}) {
454
+ const config = loadConfig();
455
+ if (!config.apiKey) {
456
+ printAndExit(new MushiCliError(
457
+ "E_AUTH_MISSING",
458
+ "API key not configured.",
459
+ "Run: mushi login --api-key <key> Or: export MUSHI_API_KEY=<key>"
460
+ ));
461
+ }
462
+ if (!config.endpoint) {
463
+ config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
464
+ }
465
+ if (opts.needsProject && !config.projectId) {
466
+ printAndExit(new MushiCliError(
467
+ "E_PROJECT_MISSING",
468
+ "Project ID not configured.",
469
+ `Run: mushi login --project-id <uuid> Or find your project ID at ${consoleUrl(resolveConsoleUrlSync(), "/projects")}`
470
+ ));
471
+ }
472
+ return config;
473
+ }
474
+ function fmtDate(iso) {
475
+ if (!iso) return "\u2014";
476
+ return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
477
+ }
478
+ function pad(s, width) {
479
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
480
+ }
481
+
104
482
  // src/detect.ts
105
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
106
- import { join as join2 } from "path";
483
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
484
+ import { join as join3 } from "path";
107
485
  var FRAMEWORKS = {
108
486
  next: {
109
487
  id: "next",
@@ -141,6 +519,90 @@ createRoot(document.getElementById('root')!).render(
141
519
  <App />
142
520
  </MushiProvider>
143
521
  )`
522
+ },
523
+ cra: {
524
+ id: "cra",
525
+ label: "Create React App",
526
+ packageName: "@mushi-mushi/react",
527
+ needsWebPackage: false,
528
+ // CRA inlines only REACT_APP_*-prefixed vars into process.env at build time.
529
+ snippet: () => `// src/index.tsx
530
+ import { MushiProvider } from '@mushi-mushi/react'
531
+
532
+ root.render(
533
+ <MushiProvider config={{
534
+ projectId: process.env.REACT_APP_MUSHI_PROJECT_ID,
535
+ apiKey: process.env.REACT_APP_MUSHI_API_KEY,
536
+ }}>
537
+ <App />
538
+ </MushiProvider>
539
+ )`
540
+ },
541
+ remix: {
542
+ id: "remix",
543
+ label: "Remix",
544
+ packageName: "@mushi-mushi/react",
545
+ needsWebPackage: false,
546
+ // Remix doesn't inline client env. Expose public values at runtime via the
547
+ // root loader + window.ENV (works on both the classic compiler and Vite).
548
+ snippet: () => `// app/root.tsx
549
+ import { MushiProvider } from '@mushi-mushi/react'
550
+ import { useLoaderData } from '@remix-run/react'
551
+
552
+ export async function loader() {
553
+ return { ENV: {
554
+ MUSHI_PROJECT_ID: process.env.MUSHI_PROJECT_ID,
555
+ MUSHI_API_KEY: process.env.MUSHI_API_KEY,
556
+ } }
557
+ }
558
+
559
+ export default function App() {
560
+ const { ENV } = useLoaderData<typeof loader>()
561
+ return (
562
+ <html>
563
+ <body>
564
+ <MushiProvider config={{ projectId: ENV.MUSHI_PROJECT_ID, apiKey: ENV.MUSHI_API_KEY }}>
565
+ <Outlet />
566
+ </MushiProvider>
567
+ <Scripts />
568
+ </body>
569
+ </html>
570
+ )
571
+ }`
572
+ },
573
+ astro: {
574
+ id: "astro",
575
+ label: "Astro",
576
+ packageName: "@mushi-mushi/web",
577
+ needsWebPackage: false,
578
+ // Astro exposes only PUBLIC_*-prefixed vars to client code (Vite-based).
579
+ snippet: () => `---
580
+ // src/layouts/Layout.astro (add this <script> once, e.g. in your base layout)
581
+ ---
582
+ <script>
583
+ import { Mushi } from '@mushi-mushi/web'
584
+ Mushi.init({
585
+ projectId: import.meta.env.PUBLIC_MUSHI_PROJECT_ID,
586
+ apiKey: import.meta.env.PUBLIC_MUSHI_API_KEY,
587
+ })
588
+ </script>`
589
+ },
590
+ solid: {
591
+ id: "solid",
592
+ label: "Solid",
593
+ packageName: "@mushi-mushi/web",
594
+ needsWebPackage: false,
595
+ // Solid / SolidStart are Vite-based → VITE_*-prefixed client env.
596
+ snippet: () => `// src/index.tsx (or src/app.tsx for SolidStart)
597
+ import { onMount } from 'solid-js'
598
+ import { Mushi } from '@mushi-mushi/web'
599
+
600
+ onMount(() => {
601
+ Mushi.init({
602
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
603
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
604
+ })
605
+ })`
144
606
  },
145
607
  vue: {
146
608
  id: "vue",
@@ -355,10 +817,10 @@ Mushi.init({
355
817
  }
356
818
  };
357
819
  function readPackageJson(cwd) {
358
- const path = join2(cwd, "package.json");
359
- if (!existsSync2(path)) return null;
820
+ const path = join3(cwd, "package.json");
821
+ if (!existsSync3(path)) return null;
360
822
  try {
361
- return JSON.parse(readFileSync2(path, "utf-8"));
823
+ return JSON.parse(readFileSync3(path, "utf-8"));
362
824
  } catch {
363
825
  return null;
364
826
  }
@@ -371,29 +833,41 @@ function detectFramework(cwd, pkg) {
371
833
  if (deps.has("@angular/core")) return FRAMEWORKS.angular;
372
834
  if (deps.has("expo")) return FRAMEWORKS.expo;
373
835
  if (deps.has("react-native")) return FRAMEWORKS["react-native"];
836
+ if (deps.has("@remix-run/react") || deps.has("@remix-run/node") || deps.has("@remix-run/serve")) {
837
+ return FRAMEWORKS.remix;
838
+ }
839
+ if (deps.has("astro")) return FRAMEWORKS.astro;
840
+ if (deps.has("@solidjs/start") || deps.has("solid-js")) return FRAMEWORKS.solid;
374
841
  if (deps.has("@capacitor/core") && deps.has("react")) return FRAMEWORKS.react;
375
842
  if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
376
843
  if (deps.has("svelte")) return FRAMEWORKS.svelte;
377
844
  if (deps.has("vue")) return FRAMEWORKS.vue;
845
+ if (deps.has("react-scripts")) return FRAMEWORKS.cra;
378
846
  if (deps.has("react")) return FRAMEWORKS.react;
379
847
  if (deps.has("express")) return FRAMEWORKS.express;
380
848
  if (deps.has("fastify")) return FRAMEWORKS.fastify;
381
849
  if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
382
- if (existsSync2(join2(cwd, "next.config.js")) || existsSync2(join2(cwd, "next.config.ts"))) {
850
+ if (existsSync3(join3(cwd, "next.config.js")) || existsSync3(join3(cwd, "next.config.ts"))) {
383
851
  return FRAMEWORKS.next;
384
852
  }
385
- if (existsSync2(join2(cwd, "nuxt.config.ts")) || existsSync2(join2(cwd, "nuxt.config.js"))) {
853
+ if (existsSync3(join3(cwd, "nuxt.config.ts")) || existsSync3(join3(cwd, "nuxt.config.js"))) {
386
854
  return FRAMEWORKS.nuxt;
387
855
  }
388
- if (existsSync2(join2(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
389
- if (existsSync2(join2(cwd, "angular.json"))) return FRAMEWORKS.angular;
856
+ if (existsSync3(join3(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
857
+ if (existsSync3(join3(cwd, "angular.json"))) return FRAMEWORKS.angular;
858
+ if (existsSync3(join3(cwd, "remix.config.js")) || existsSync3(join3(cwd, "remix.config.ts"))) {
859
+ return FRAMEWORKS.remix;
860
+ }
861
+ if (existsSync3(join3(cwd, "astro.config.mjs")) || existsSync3(join3(cwd, "astro.config.ts")) || existsSync3(join3(cwd, "astro.config.js"))) {
862
+ return FRAMEWORKS.astro;
863
+ }
390
864
  return FRAMEWORKS.vanilla;
391
865
  }
392
866
  function detectPackageManager(cwd) {
393
- if (existsSync2(join2(cwd, "bun.lockb")) || existsSync2(join2(cwd, "bun.lock"))) return "bun";
394
- if (existsSync2(join2(cwd, "pnpm-lock.yaml"))) return "pnpm";
395
- if (existsSync2(join2(cwd, "yarn.lock"))) return "yarn";
396
- if (existsSync2(join2(cwd, "package-lock.json"))) return "npm";
867
+ if (existsSync3(join3(cwd, "bun.lockb")) || existsSync3(join3(cwd, "bun.lock"))) return "bun";
868
+ if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) return "pnpm";
869
+ if (existsSync3(join3(cwd, "yarn.lock"))) return "yarn";
870
+ if (existsSync3(join3(cwd, "package-lock.json"))) return "npm";
397
871
  const userAgent = process.env.npm_config_user_agent ?? "";
398
872
  if (userAgent.startsWith("bun")) return "bun";
399
873
  if (userAgent.startsWith("pnpm")) return "pnpm";
@@ -415,7 +889,7 @@ function envVarsToWrite(apiKey, projectId, framework, endpoint) {
415
889
  endpointLine
416
890
  ].filter(Boolean).join("\n");
417
891
  }
418
- if (framework.id === "react-native") {
892
+ if (framework.id === "react-native" || framework.id === "remix") {
419
893
  return [
420
894
  `MUSHI_PROJECT_ID=${projectId}`,
421
895
  `MUSHI_API_KEY=${apiKey}`,
@@ -429,7 +903,7 @@ function envVarsToWrite(apiKey, projectId, framework, endpoint) {
429
903
  endpointLine ? `EXPO_PUBLIC_${endpointLine}` : null
430
904
  ].filter(Boolean).join("\n");
431
905
  }
432
- const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
906
+ const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : framework.id === "cra" ? "REACT_APP_" : framework.id === "astro" ? "PUBLIC_" : "VITE_";
433
907
  return [
434
908
  `${prefix}MUSHI_PROJECT_ID=${projectId}`,
435
909
  `${prefix}MUSHI_API_KEY=${apiKey}`,
@@ -558,7 +1032,7 @@ async function waitForIngestReady(options) {
558
1032
 
559
1033
  // src/mcp-config.ts
560
1034
  import { mkdir, readFile, writeFile } from "fs/promises";
561
- import { dirname as dirname2 } from "path";
1035
+ import { dirname as dirname3 } from "path";
562
1036
  var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
563
1037
  async function writeMcpServerEntry(opts) {
564
1038
  const { configPath, serverName, serverBlock, dryRun = false } = opts;
@@ -580,7 +1054,7 @@ async function writeMcpServerEntry(opts) {
580
1054
  merged.mcpServers = servers;
581
1055
  const output = JSON.stringify(merged, null, 2) + "\n";
582
1056
  if (!dryRun) {
583
- await mkdir(dirname2(configPath), { recursive: true });
1057
+ await mkdir(dirname3(configPath), { recursive: true });
584
1058
  await writeFile(configPath, output, "utf8");
585
1059
  }
586
1060
  return { created, path: configPath };
@@ -637,7 +1111,7 @@ ${needs.join("\n")}
637
1111
  return true;
638
1112
  }
639
1113
  async function ensureMcpJsonGitignored(cwd, messages) {
640
- const gitignorePath = join3(cwd, ".gitignore");
1114
+ const gitignorePath = join4(cwd, ".gitignore");
641
1115
  const patterns = [".cursor/mcp.json", ".cursor/"];
642
1116
  let content = null;
643
1117
  try {
@@ -658,11 +1132,12 @@ async function ensureMcpJsonGitignored(cwd, messages) {
658
1132
  async function runConnect(opts, baseConfig = {}) {
659
1133
  const cwd = resolve(opts.cwd ?? process.cwd());
660
1134
  const endpoint = assertEndpoint(opts.endpoint);
1135
+ const projectId = requireUuid(opts.projectId, "projectId");
661
1136
  const messages = [];
662
1137
  const config = {
663
1138
  ...baseConfig,
664
1139
  apiKey: opts.apiKey,
665
- projectId: opts.projectId,
1140
+ projectId,
666
1141
  endpoint
667
1142
  };
668
1143
  saveConfig(config);
@@ -671,8 +1146,8 @@ async function runConnect(opts, baseConfig = {}) {
671
1146
  if (opts.writeEnv !== false) {
672
1147
  const pkg = readPackageJson(cwd);
673
1148
  const framework = detectFramework(cwd, pkg);
674
- const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
675
- envPath = join3(cwd, ".env.local");
1149
+ const lines = envVarsToWrite(opts.apiKey, projectId, framework).split("\n");
1150
+ envPath = join4(cwd, ".env.local");
676
1151
  const wrote = await mergeEnvFile(envPath, lines);
677
1152
  messages.push(
678
1153
  wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
@@ -686,12 +1161,12 @@ async function runConnect(opts, baseConfig = {}) {
686
1161
  }
687
1162
  let mcpPath = null;
688
1163
  if (opts.wireIde !== false) {
689
- const mcpDir = join3(cwd, ".cursor");
690
- mcpPath = join3(mcpDir, "mcp.json");
691
- const serverName = buildMcpServerName({ projectId: opts.projectId });
1164
+ const mcpDir = join4(cwd, ".cursor");
1165
+ mcpPath = join4(mcpDir, "mcp.json");
1166
+ const serverName = buildMcpServerName({ projectId });
692
1167
  const serverBlock = buildMcpServerBlock({
693
1168
  endpoint,
694
- projectId: opts.projectId,
1169
+ projectId,
695
1170
  apiKey: opts.apiKey
696
1171
  });
697
1172
  await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
@@ -706,7 +1181,7 @@ async function runConnect(opts, baseConfig = {}) {
706
1181
  heartbeat = await waitForIngestReady({
707
1182
  endpoint,
708
1183
  apiKey: opts.apiKey,
709
- projectId: opts.projectId,
1184
+ projectId,
710
1185
  maxAttempts,
711
1186
  onPoll: (payload, attempt) => {
712
1187
  if (!opts.json && attempt % 3 === 0) {
@@ -735,6 +1210,9 @@ async function runConnect(opts, baseConfig = {}) {
735
1210
  }
736
1211
 
737
1212
  export {
1213
+ printAndExit,
1214
+ installSignalHandlers,
1215
+ MUSHI_CLI_VERSION,
738
1216
  loadConfig,
739
1217
  saveConfig,
740
1218
  FRAMEWORKS,
@@ -743,6 +1221,21 @@ export {
743
1221
  detectPackageManager,
744
1222
  installCommand,
745
1223
  envVarsToWrite,
1224
+ consoleUrl,
1225
+ resolveConsoleUrlSync,
1226
+ resolveConsoleUrl,
1227
+ projectIdHint,
1228
+ apiKeyHint,
1229
+ cliSetupDeepLink,
1230
+ reportsUrl,
1231
+ openInBrowser,
1232
+ requireUuid,
1233
+ probeEndpointHealth,
1234
+ apiCall,
1235
+ die,
1236
+ requireConfig,
1237
+ fmtDate,
1238
+ pad,
746
1239
  sanitizeApiKey,
747
1240
  sanitizeProjectId,
748
1241
  sanitizeEndpoint,