@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.
@@ -0,0 +1,750 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ assertEndpoint,
4
+ normalizeEndpoint
5
+ } from "./chunk-LDPFTQLE.js";
6
+
7
+ // src/connect.ts
8
+ import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
9
+ import { join as join3, resolve } from "path";
10
+
11
+ // src/config.ts
12
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { dirname, join } from "path";
15
+ var SECURE_FILE_MODE = 384;
16
+ var SECURE_DIR_MODE = 448;
17
+ function resolveXdgConfigPath() {
18
+ const xdg = process.env["XDG_CONFIG_HOME"];
19
+ if (xdg && xdg.length > 0) {
20
+ return join(xdg, "mushi", "config.json");
21
+ }
22
+ if (process.platform === "win32") {
23
+ const appData = process.env["APPDATA"];
24
+ if (appData && appData.length > 0) {
25
+ return join(appData, "mushi", "config.json");
26
+ }
27
+ }
28
+ return join(homedir(), ".config", "mushi", "config.json");
29
+ }
30
+ var LEGACY_CONFIG_PATH = join(homedir(), ".mushirc");
31
+ var CONFIG_PATH = resolveXdgConfigPath();
32
+ function loadConfig(path = CONFIG_PATH) {
33
+ let file = {};
34
+ if (existsSync(path)) {
35
+ tightenPermissions(path);
36
+ try {
37
+ file = JSON.parse(readFileSync(path, "utf-8"));
38
+ } catch {
39
+ }
40
+ } else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
41
+ file = migrateLegacyConfig() ?? {};
42
+ }
43
+ const endpointFromEnv = process.env["MUSHI_API_ENDPOINT"] ?? process.env["MUSHI_ENDPOINT"] ?? void 0;
44
+ const fromEnv = {
45
+ ...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
46
+ ...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
47
+ ...endpointFromEnv ? { endpoint: endpointFromEnv } : {},
48
+ ...process.env["MUSHI_CONSOLE_URL"] ? { consoleUrl: process.env["MUSHI_CONSOLE_URL"].trim() } : {}
49
+ };
50
+ return { ...file, ...fromEnv };
51
+ }
52
+ function saveConfig(config, path = CONFIG_PATH) {
53
+ const dir = dirname(path);
54
+ if (!existsSync(dir)) {
55
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
56
+ } else {
57
+ tightenDirPermissions(dir);
58
+ }
59
+ writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
60
+ tightenPermissions(path);
61
+ }
62
+ function migrateLegacyConfig(legacyPath = LEGACY_CONFIG_PATH, destPath = CONFIG_PATH) {
63
+ if (!existsSync(legacyPath)) return null;
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(readFileSync(legacyPath, "utf-8"));
67
+ } catch {
68
+ return null;
69
+ }
70
+ const dir = dirname(destPath);
71
+ if (!existsSync(dir)) {
72
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
73
+ }
74
+ try {
75
+ renameSync(legacyPath, destPath);
76
+ } catch {
77
+ writeFileSync(destPath, JSON.stringify(parsed, null, 2), { mode: SECURE_FILE_MODE });
78
+ try {
79
+ unlinkSync(legacyPath);
80
+ } catch {
81
+ }
82
+ }
83
+ tightenPermissions(destPath);
84
+ tightenDirPermissions(dir);
85
+ return parsed;
86
+ }
87
+ function tightenPermissions(path) {
88
+ if (process.platform === "win32") return;
89
+ try {
90
+ const current = statSync(path).mode & 511;
91
+ if (current !== SECURE_FILE_MODE) chmodSync(path, SECURE_FILE_MODE);
92
+ } catch {
93
+ }
94
+ }
95
+ function tightenDirPermissions(path) {
96
+ if (process.platform === "win32") return;
97
+ try {
98
+ const current = statSync(path).mode & 511;
99
+ if (current !== SECURE_DIR_MODE) chmodSync(path, SECURE_DIR_MODE);
100
+ } catch {
101
+ }
102
+ }
103
+
104
+ // src/detect.ts
105
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
106
+ import { join as join2 } from "path";
107
+ var FRAMEWORKS = {
108
+ next: {
109
+ id: "next",
110
+ label: "Next.js",
111
+ packageName: "@mushi-mushi/react",
112
+ needsWebPackage: false,
113
+ snippet: () => `// app/providers.tsx (or pages/_app.tsx for /pages router)
114
+ 'use client'
115
+ import { MushiProvider } from '@mushi-mushi/react'
116
+
117
+ export function Providers({ children }: { children: React.ReactNode }) {
118
+ return (
119
+ <MushiProvider config={{
120
+ projectId: process.env.NEXT_PUBLIC_MUSHI_PROJECT_ID!,
121
+ apiKey: process.env.NEXT_PUBLIC_MUSHI_API_KEY!,
122
+ }}>
123
+ {children}
124
+ </MushiProvider>
125
+ )
126
+ }`
127
+ },
128
+ react: {
129
+ id: "react",
130
+ label: "React",
131
+ packageName: "@mushi-mushi/react",
132
+ needsWebPackage: false,
133
+ snippet: () => `// src/main.tsx
134
+ import { MushiProvider } from '@mushi-mushi/react'
135
+
136
+ createRoot(document.getElementById('root')!).render(
137
+ <MushiProvider config={{
138
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
139
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
140
+ }}>
141
+ <App />
142
+ </MushiProvider>
143
+ )`
144
+ },
145
+ vue: {
146
+ id: "vue",
147
+ label: "Vue 3",
148
+ packageName: "@mushi-mushi/vue",
149
+ needsWebPackage: false,
150
+ snippet: () => `// src/main.ts
151
+ import { MushiPlugin } from '@mushi-mushi/vue'
152
+
153
+ app.use(MushiPlugin, {
154
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
155
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
156
+ })`
157
+ },
158
+ nuxt: {
159
+ id: "nuxt",
160
+ label: "Nuxt",
161
+ packageName: "@mushi-mushi/vue",
162
+ needsWebPackage: false,
163
+ snippet: () => `// plugins/mushi.client.ts
164
+ import { MushiPlugin } from '@mushi-mushi/vue'
165
+
166
+ export default defineNuxtPlugin((nuxtApp) => {
167
+ nuxtApp.vueApp.use(MushiPlugin, {
168
+ projectId: import.meta.env.NUXT_PUBLIC_MUSHI_PROJECT_ID,
169
+ apiKey: import.meta.env.NUXT_PUBLIC_MUSHI_API_KEY,
170
+ })
171
+ })`
172
+ },
173
+ svelte: {
174
+ id: "svelte",
175
+ label: "Svelte",
176
+ packageName: "@mushi-mushi/svelte",
177
+ needsWebPackage: false,
178
+ snippet: () => `// src/main.ts
179
+ import { initMushi } from '@mushi-mushi/svelte'
180
+
181
+ initMushi({
182
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
183
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
184
+ })`
185
+ },
186
+ sveltekit: {
187
+ id: "sveltekit",
188
+ label: "SvelteKit",
189
+ packageName: "@mushi-mushi/svelte",
190
+ needsWebPackage: false,
191
+ snippet: () => `// src/routes/+layout.svelte
192
+ <script>
193
+ import { onMount } from 'svelte'
194
+
195
+ onMount(async () => {
196
+ const { initMushi } = await import('@mushi-mushi/svelte')
197
+ initMushi({
198
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
199
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
200
+ })
201
+ })
202
+ </script>`
203
+ },
204
+ angular: {
205
+ id: "angular",
206
+ label: "Angular",
207
+ packageName: "@mushi-mushi/angular",
208
+ needsWebPackage: false,
209
+ snippet: () => `// src/main.ts
210
+ import { provideMushi } from '@mushi-mushi/angular'
211
+
212
+ bootstrapApplication(AppComponent, {
213
+ providers: [
214
+ provideMushi({
215
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
216
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
217
+ }),
218
+ ],
219
+ })`
220
+ },
221
+ expo: {
222
+ id: "expo",
223
+ label: "Expo",
224
+ packageName: "@mushi-mushi/react-native",
225
+ needsWebPackage: false,
226
+ snippet: () => `// App.tsx
227
+ import { MushiProvider } from '@mushi-mushi/react-native'
228
+
229
+ export default function App() {
230
+ return (
231
+ <MushiProvider
232
+ projectId={process.env.EXPO_PUBLIC_MUSHI_PROJECT_ID!}
233
+ apiKey={process.env.EXPO_PUBLIC_MUSHI_API_KEY!}
234
+ >
235
+ <YourApp />
236
+ </MushiProvider>
237
+ )
238
+ }`
239
+ },
240
+ "react-native": {
241
+ id: "react-native",
242
+ label: "React Native",
243
+ packageName: "@mushi-mushi/react-native",
244
+ needsWebPackage: false,
245
+ snippet: () => `// App.tsx
246
+ import { MushiProvider } from '@mushi-mushi/react-native'
247
+
248
+ export default function App() {
249
+ return (
250
+ <MushiProvider
251
+ projectId={process.env.MUSHI_PROJECT_ID!}
252
+ apiKey={process.env.MUSHI_API_KEY!}
253
+ >
254
+ <YourApp />
255
+ </MushiProvider>
256
+ )
257
+ }`
258
+ },
259
+ capacitor: {
260
+ id: "capacitor",
261
+ label: "Capacitor (Ionic)",
262
+ packageName: "@mushi-mushi/capacitor",
263
+ needsWebPackage: false,
264
+ /* The Capacitor plugin's public API is `Mushi.configure(...)`, not
265
+ * `Mushi.init(...)` — see packages/capacitor/src/definitions.ts. We
266
+ * shipped the wrong call here for two releases and users got a runtime
267
+ * `TypeError: Mushi.init is not a function`. The accompanying admin
268
+ * console snippet (apps/admin/src/lib/sdkSnippets.ts) emits the same
269
+ * `Mushi.configure(...)` shape; both are pinned by tests. */
270
+ snippet: () => `// src/main.ts \u2014 after install, run \`npx cap sync\`
271
+ import { Mushi } from '@mushi-mushi/capacitor'
272
+
273
+ await Mushi.configure({
274
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
275
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
276
+ })`
277
+ },
278
+ express: {
279
+ id: "express",
280
+ label: "Express",
281
+ packageName: "@mushi-mushi/node",
282
+ needsWebPackage: false,
283
+ snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
284
+ import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
285
+ import { mushiExpressErrorHandler } from '@mushi-mushi/node/express'
286
+ import type { Express } from 'express'
287
+
288
+ export const mushi = new MushiNodeClient({
289
+ projectId: process.env.MUSHI_PROJECT_ID!,
290
+ apiKey: process.env.MUSHI_API_KEY!,
291
+ environment: process.env.NODE_ENV ?? 'production',
292
+ })
293
+ attachUnhandledHook({ client: mushi })
294
+
295
+ export function attachMushi(app: Express) {
296
+ app.use(mushiExpressErrorHandler({ client: mushi }))
297
+ }`
298
+ },
299
+ fastify: {
300
+ id: "fastify",
301
+ label: "Fastify",
302
+ packageName: "@mushi-mushi/node",
303
+ needsWebPackage: false,
304
+ snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
305
+ import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
306
+ import { mushiFastifyPlugin } from '@mushi-mushi/node/fastify'
307
+ import Fastify from 'fastify'
308
+
309
+ export const mushi = new MushiNodeClient({
310
+ projectId: process.env.MUSHI_PROJECT_ID!,
311
+ apiKey: process.env.MUSHI_API_KEY!,
312
+ environment: process.env.NODE_ENV ?? 'production',
313
+ })
314
+ attachUnhandledHook({ client: mushi })
315
+
316
+ const app = Fastify()
317
+ mushiFastifyPlugin(app, { client: mushi })`
318
+ },
319
+ hono: {
320
+ id: "hono",
321
+ label: "Hono",
322
+ packageName: "@mushi-mushi/node",
323
+ needsWebPackage: false,
324
+ snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
325
+ import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
326
+ import { mushiHonoErrorHandler } from '@mushi-mushi/node/hono'
327
+ import { Hono } from 'hono'
328
+
329
+ export const mushi = new MushiNodeClient({
330
+ projectId: process.env.MUSHI_PROJECT_ID!,
331
+ apiKey: process.env.MUSHI_API_KEY!,
332
+ environment: process.env.NODE_ENV ?? 'production',
333
+ })
334
+ attachUnhandledHook({ client: mushi })
335
+
336
+ const app = new Hono()
337
+ app.onError(
338
+ mushiHonoErrorHandler({ client: mushi }, (err, c) =>
339
+ c.text('Internal Server Error', 500),
340
+ ),
341
+ )`
342
+ },
343
+ vanilla: {
344
+ id: "vanilla",
345
+ label: "Vanilla JS / unknown",
346
+ packageName: "@mushi-mushi/web",
347
+ needsWebPackage: false,
348
+ snippet: () => `// Anywhere in your client bundle
349
+ import { Mushi } from '@mushi-mushi/web'
350
+
351
+ Mushi.init({
352
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
353
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
354
+ })`
355
+ }
356
+ };
357
+ function readPackageJson(cwd) {
358
+ const path = join2(cwd, "package.json");
359
+ if (!existsSync2(path)) return null;
360
+ try {
361
+ return JSON.parse(readFileSync2(path, "utf-8"));
362
+ } catch {
363
+ return null;
364
+ }
365
+ }
366
+ function detectFramework(cwd, pkg) {
367
+ const deps = collectDeps(pkg);
368
+ if (deps.has("next")) return FRAMEWORKS.next;
369
+ if (deps.has("nuxt")) return FRAMEWORKS.nuxt;
370
+ if (deps.has("@sveltejs/kit")) return FRAMEWORKS.sveltekit;
371
+ if (deps.has("@angular/core")) return FRAMEWORKS.angular;
372
+ if (deps.has("expo")) return FRAMEWORKS.expo;
373
+ if (deps.has("react-native")) return FRAMEWORKS["react-native"];
374
+ if (deps.has("@capacitor/core") && deps.has("react")) return FRAMEWORKS.react;
375
+ if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
376
+ if (deps.has("svelte")) return FRAMEWORKS.svelte;
377
+ if (deps.has("vue")) return FRAMEWORKS.vue;
378
+ if (deps.has("react")) return FRAMEWORKS.react;
379
+ if (deps.has("express")) return FRAMEWORKS.express;
380
+ if (deps.has("fastify")) return FRAMEWORKS.fastify;
381
+ 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"))) {
383
+ return FRAMEWORKS.next;
384
+ }
385
+ if (existsSync2(join2(cwd, "nuxt.config.ts")) || existsSync2(join2(cwd, "nuxt.config.js"))) {
386
+ return FRAMEWORKS.nuxt;
387
+ }
388
+ if (existsSync2(join2(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
389
+ if (existsSync2(join2(cwd, "angular.json"))) return FRAMEWORKS.angular;
390
+ return FRAMEWORKS.vanilla;
391
+ }
392
+ 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";
397
+ const userAgent = process.env.npm_config_user_agent ?? "";
398
+ if (userAgent.startsWith("bun")) return "bun";
399
+ if (userAgent.startsWith("pnpm")) return "pnpm";
400
+ if (userAgent.startsWith("yarn")) return "yarn";
401
+ return "npm";
402
+ }
403
+ function installCommand(pm, packages) {
404
+ const verb = pm === "npm" ? "install" : "add";
405
+ return `${pm} ${verb} ${packages.join(" ")}`;
406
+ }
407
+ var SERVER_FRAMEWORK_IDS = /* @__PURE__ */ new Set(["express", "fastify", "hono"]);
408
+ function envVarsToWrite(apiKey, projectId, framework) {
409
+ if (SERVER_FRAMEWORK_IDS.has(framework.id)) {
410
+ return [
411
+ `MUSHI_PROJECT_ID=${projectId}`,
412
+ `MUSHI_API_KEY=${apiKey}`
413
+ ].join("\n");
414
+ }
415
+ if (framework.id === "react-native") {
416
+ return [
417
+ `MUSHI_PROJECT_ID=${projectId}`,
418
+ `MUSHI_API_KEY=${apiKey}`
419
+ ].join("\n");
420
+ }
421
+ if (framework.id === "expo") {
422
+ return [
423
+ `EXPO_PUBLIC_MUSHI_PROJECT_ID=${projectId}`,
424
+ `EXPO_PUBLIC_MUSHI_API_KEY=${apiKey}`
425
+ ].join("\n");
426
+ }
427
+ const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
428
+ return [
429
+ `${prefix}MUSHI_PROJECT_ID=${projectId}`,
430
+ `${prefix}MUSHI_API_KEY=${apiKey}`
431
+ ].join("\n");
432
+ }
433
+ function collectDeps(pkg) {
434
+ if (!pkg) return /* @__PURE__ */ new Set();
435
+ return /* @__PURE__ */ new Set([
436
+ ...Object.keys(pkg.dependencies ?? {}),
437
+ ...Object.keys(pkg.devDependencies ?? {}),
438
+ ...Object.keys(pkg.peerDependencies ?? {})
439
+ ]);
440
+ }
441
+
442
+ // src/sanitize-config.ts
443
+ var PROJECT_ID_RE = /^(?: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;
444
+ var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
445
+ function sanitizeApiKey(raw) {
446
+ const key = raw.replace(/[\r\n\0]/g, "");
447
+ if (!API_KEY_RE.test(key)) {
448
+ throw new Error(
449
+ "Invalid API key in config \u2014 run `mushi login --api-key <key>` to refresh credentials."
450
+ );
451
+ }
452
+ return key;
453
+ }
454
+ function sanitizeProjectId(raw) {
455
+ const id = raw.trim();
456
+ if (!PROJECT_ID_RE.test(id)) {
457
+ throw new Error(
458
+ "Invalid project ID in config \u2014 expected a UUID or proj_* slug from the admin console."
459
+ );
460
+ }
461
+ return id;
462
+ }
463
+ function sanitizeEndpoint(raw) {
464
+ return assertEndpoint(normalizeEndpoint(raw));
465
+ }
466
+ function sanitizeCliCredentials(config) {
467
+ if (!config.endpoint || !config.apiKey || !config.projectId) {
468
+ throw new Error("Missing endpoint, apiKey, or projectId");
469
+ }
470
+ return {
471
+ endpoint: sanitizeEndpoint(config.endpoint),
472
+ apiKey: sanitizeApiKey(config.apiKey),
473
+ projectId: sanitizeProjectId(config.projectId)
474
+ };
475
+ }
476
+ function apiKeyHeaders(apiKey, projectId) {
477
+ const headers = {
478
+ Authorization: `Bearer ${apiKey}`,
479
+ "X-Mushi-Api-Key": apiKey
480
+ };
481
+ if (projectId) headers["X-Mushi-Project"] = projectId;
482
+ return headers;
483
+ }
484
+
485
+ // src/heartbeat-wait.ts
486
+ var IngestSetupHttpError = class extends Error {
487
+ status;
488
+ constructor(status) {
489
+ super(`ingest-setup HTTP ${status}`);
490
+ this.name = "IngestSetupHttpError";
491
+ this.status = status;
492
+ }
493
+ };
494
+ var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
495
+ async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
496
+ const endpoint = sanitizeEndpoint(config.endpoint);
497
+ const apiKey = sanitizeApiKey(config.apiKey);
498
+ const projectId = config.projectId ? sanitizeProjectId(config.projectId) : void 0;
499
+ const res = await doFetch(`${endpoint}/v1/sync/ingest-setup`, {
500
+ headers: apiKeyHeaders(apiKey, projectId),
501
+ signal: AbortSignal.timeout(8e3)
502
+ });
503
+ if (!res.ok) {
504
+ if (NON_RETRYABLE_STATUSES.has(res.status)) throw new IngestSetupHttpError(res.status);
505
+ return null;
506
+ }
507
+ const body = await res.json();
508
+ return body.ok && body.data ? body.data : null;
509
+ }
510
+ async function waitForIngestReady(options) {
511
+ const doFetch = options.fetch ?? globalThis.fetch;
512
+ const maxAttempts = options.maxAttempts ?? 40;
513
+ const intervalMs = options.intervalMs ?? 3e3;
514
+ let lastPayload = null;
515
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
516
+ if (options.signal?.aborted) {
517
+ return { ok: false, payload: lastPayload, attempts: attempt - 1, reason: "aborted" };
518
+ }
519
+ try {
520
+ lastPayload = await fetchIngestSetup(
521
+ { endpoint: options.endpoint, apiKey: options.apiKey, projectId: options.projectId },
522
+ doFetch
523
+ );
524
+ if (lastPayload) {
525
+ options.onPoll?.(lastPayload, attempt);
526
+ if (lastPayload.ready) {
527
+ return { ok: true, payload: lastPayload, attempts: attempt, reason: "ready" };
528
+ }
529
+ const sdkStep = lastPayload.steps.find((s) => s.id === "sdk_installed");
530
+ if (sdkStep?.complete) {
531
+ return { ok: true, payload: lastPayload, attempts: attempt, reason: "heartbeat" };
532
+ }
533
+ }
534
+ } catch (err) {
535
+ const msg = err instanceof Error ? err.message : String(err);
536
+ if (err instanceof IngestSetupHttpError) {
537
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "unauthorized", error: msg };
538
+ }
539
+ if (options.signal?.aborted) {
540
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "aborted", error: msg };
541
+ }
542
+ if (attempt === maxAttempts) {
543
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "fetch-error", error: msg };
544
+ }
545
+ }
546
+ if (attempt < maxAttempts) {
547
+ await new Promise((r) => setTimeout(r, intervalMs));
548
+ }
549
+ }
550
+ return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
551
+ }
552
+
553
+ // src/mcp-config.ts
554
+ import { mkdir, readFile, writeFile } from "fs/promises";
555
+ import { dirname as dirname2 } from "path";
556
+ var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
557
+ async function writeMcpServerEntry(opts) {
558
+ const { configPath, serverName, serverBlock, dryRun = false } = opts;
559
+ let merged = { mcpServers: {} };
560
+ let created = false;
561
+ try {
562
+ const existing = await readFile(configPath, "utf8");
563
+ try {
564
+ merged = JSON.parse(existing);
565
+ } catch {
566
+ merged = { mcpServers: {} };
567
+ }
568
+ } catch {
569
+ created = true;
570
+ merged = { mcpServers: {} };
571
+ }
572
+ const servers = merged.mcpServers ?? {};
573
+ servers[serverName] = serverBlock;
574
+ merged.mcpServers = servers;
575
+ const output = JSON.stringify(merged, null, 2) + "\n";
576
+ if (!dryRun) {
577
+ await mkdir(dirname2(configPath), { recursive: true });
578
+ await writeFile(configPath, output, "utf8");
579
+ }
580
+ return { created, path: configPath };
581
+ }
582
+ function buildMcpServerBlock(opts) {
583
+ return {
584
+ command: "npx",
585
+ args: ["-y", "@mushi-mushi/mcp@latest"],
586
+ env: {
587
+ MUSHI_API_ENDPOINT: opts.endpoint,
588
+ MUSHI_PROJECT_ID: opts.projectId,
589
+ MUSHI_API_KEY: opts.apiKey,
590
+ MUSHI_FEATURES: DEFAULT_MUSHI_FEATURES
591
+ }
592
+ };
593
+ }
594
+ function buildMcpServerName(opts) {
595
+ if (opts.legacy) return "mushi";
596
+ if (opts.projectName) {
597
+ const slug = opts.projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
598
+ return `mushi-${slug}`;
599
+ }
600
+ return `mushi-${(opts.projectId ?? "unknown").slice(0, 8)}`;
601
+ }
602
+
603
+ // src/connect.ts
604
+ function envKeyPresent(content, key) {
605
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
606
+ return new RegExp(`^${escaped}=`, "m").test(content);
607
+ }
608
+ async function mergeEnvFile(path, lines) {
609
+ const block = `
610
+ # Mushi \u2014 added by mushi connect
611
+ ${lines.join("\n")}
612
+ `;
613
+ let existing = null;
614
+ try {
615
+ existing = await readFile2(path, "utf8");
616
+ } catch {
617
+ }
618
+ if (existing !== null) {
619
+ const needs = lines.filter((line) => {
620
+ const key = line.split("=")[0];
621
+ return !envKeyPresent(existing, key);
622
+ });
623
+ if (needs.length === 0) return false;
624
+ await appendFile(path, `
625
+ # Mushi \u2014 added by mushi connect
626
+ ${needs.join("\n")}
627
+ `, "utf8");
628
+ return true;
629
+ }
630
+ await writeFile2(path, block, "utf8");
631
+ return true;
632
+ }
633
+ async function ensureMcpJsonGitignored(cwd, messages) {
634
+ const gitignorePath = join3(cwd, ".gitignore");
635
+ const patterns = [".cursor/mcp.json", ".cursor/"];
636
+ let content = null;
637
+ try {
638
+ content = await readFile2(gitignorePath, "utf8");
639
+ } catch {
640
+ }
641
+ if (content === null) {
642
+ messages.push(
643
+ "\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
644
+ );
645
+ return;
646
+ }
647
+ const covered = patterns.some((p) => content.split("\n").some((line) => line.trim() === p || line.trim() === `${p}/`));
648
+ if (covered) return;
649
+ await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
650
+ messages.push("\u2713 Added .cursor/mcp.json to .gitignore (contains API key)");
651
+ }
652
+ async function runConnect(opts, baseConfig = {}) {
653
+ const cwd = resolve(opts.cwd ?? process.cwd());
654
+ const endpoint = assertEndpoint(opts.endpoint);
655
+ const messages = [];
656
+ const config = {
657
+ ...baseConfig,
658
+ apiKey: opts.apiKey,
659
+ projectId: opts.projectId,
660
+ endpoint
661
+ };
662
+ saveConfig(config);
663
+ messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
664
+ let envPath = null;
665
+ if (opts.writeEnv !== false) {
666
+ const pkg = readPackageJson(cwd);
667
+ const framework = detectFramework(cwd, pkg);
668
+ const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
669
+ envPath = join3(cwd, ".env.local");
670
+ const wrote = await mergeEnvFile(envPath, lines);
671
+ messages.push(
672
+ wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
673
+ );
674
+ const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
675
+ if (deps["@capacitor/core"] && deps["react"]) {
676
+ messages.push(
677
+ "\u2139 Capacitor + React detected \u2014 install @mushi-mushi/web (or @mushi-mushi/react) and call initMushi() in main.tsx. Optional @mushi-mushi/capacitor for native shell parity."
678
+ );
679
+ }
680
+ }
681
+ let mcpPath = null;
682
+ if (opts.wireIde !== false) {
683
+ const mcpDir = join3(cwd, ".cursor");
684
+ mcpPath = join3(mcpDir, "mcp.json");
685
+ const serverName = buildMcpServerName({ projectId: opts.projectId });
686
+ const serverBlock = buildMcpServerBlock({
687
+ endpoint,
688
+ projectId: opts.projectId,
689
+ apiKey: opts.apiKey
690
+ });
691
+ await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
692
+ await ensureMcpJsonGitignored(cwd, messages);
693
+ messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
694
+ }
695
+ let heartbeat = null;
696
+ if (opts.wait) {
697
+ const timeoutSec = opts.waitTimeoutSec ?? 120;
698
+ const maxAttempts = Math.max(1, Math.ceil(timeoutSec * 1e3 / 3e3));
699
+ messages.push(`\u2026 Waiting for SDK heartbeat (up to ${timeoutSec}s) \u2014 start your dev server with the snippet installed`);
700
+ heartbeat = await waitForIngestReady({
701
+ endpoint,
702
+ apiKey: opts.apiKey,
703
+ projectId: opts.projectId,
704
+ maxAttempts,
705
+ onPoll: (payload, attempt) => {
706
+ if (!opts.json && attempt % 3 === 0) {
707
+ const sdk = payload.steps.find((s) => s.id === "sdk_installed");
708
+ const seen = payload.diagnostic?.last_sdk_seen_at ?? "never";
709
+ process.stdout.write(` poll ${attempt}: sdk_installed=${sdk?.complete ? "yes" : "no"} last_seen=${seen}
710
+ `);
711
+ }
712
+ }
713
+ });
714
+ if (heartbeat.ok) {
715
+ const label = heartbeat.reason === "heartbeat" ? "SDK heartbeat detected" : "Ingest setup complete";
716
+ messages.push(`\u2713 ${label} \u2014 ingest pipeline is live`);
717
+ } else if (heartbeat.reason === "unauthorized") {
718
+ messages.push(
719
+ `\u2717 The backend rejected these credentials (${heartbeat.error ?? "auth error"}). Double-check --api-key, --project-id, and --endpoint, then re-run \`mushi connect --wait\`.`
720
+ );
721
+ } else {
722
+ messages.push(
723
+ `\u2717 No heartbeat before timeout (${heartbeat.reason}). Confirm env vars are in your build, restart the dev server, then re-run \`mushi connect --wait\`.`
724
+ );
725
+ }
726
+ }
727
+ const ok = !opts.wait || Boolean(heartbeat?.ok);
728
+ return { ok, envPath, mcpPath, heartbeat, messages };
729
+ }
730
+
731
+ export {
732
+ loadConfig,
733
+ saveConfig,
734
+ FRAMEWORKS,
735
+ readPackageJson,
736
+ detectFramework,
737
+ detectPackageManager,
738
+ installCommand,
739
+ envVarsToWrite,
740
+ sanitizeApiKey,
741
+ sanitizeProjectId,
742
+ sanitizeEndpoint,
743
+ sanitizeCliCredentials,
744
+ apiKeyHeaders,
745
+ fetchIngestSetup,
746
+ writeMcpServerEntry,
747
+ buildMcpServerBlock,
748
+ buildMcpServerName,
749
+ runConnect
750
+ };