@mushi-mushi/cli 0.22.1 → 0.23.1

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