@mushi-mushi/cli 0.17.2 → 0.18.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.
package/dist/index.js CHANGED
@@ -1,207 +1,52 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ API_TIMEOUT_MS,
4
+ CONFIG_PATH,
5
+ MUSHI_CLI_VERSION,
6
+ apiCall,
7
+ die,
8
+ fmtDate,
9
+ installSignalHandlers,
10
+ loadConfig,
11
+ pad,
12
+ requireConfig,
13
+ saveConfig
14
+ } from "./chunk-LKJ7KLBC.js";
15
+ import {
16
+ TEST_REPORT_FETCH_TIMEOUT_MS,
17
+ assertEndpoint,
18
+ normalizeEndpoint,
19
+ resolveCloudEndpoint
20
+ } from "./chunk-LDPFTQLE.js";
2
21
 
3
22
  // src/index.ts
4
23
  import { Command } from "commander";
5
24
 
6
- // src/errors.ts
7
- var EXIT_CODE_MAP = {
8
- E_AUTH_MISSING: 2,
9
- E_AUTH_INVALID: 2,
10
- E_PROJECT_MISSING: 2,
11
- E_ENDPOINT_INVALID: 2,
12
- E_INVALID_INPUT: 2,
13
- E_NOT_INTERACTIVE: 2,
14
- E_NETWORK: 3,
15
- E_TIMEOUT: 3,
16
- E_API_ERROR: 3,
17
- E_RATE_LIMITED: 3,
18
- E_FILE_NOT_FOUND: 1,
19
- E_FILE_PERMISSION: 1,
20
- E_FRESHNESS_STALE: 1,
21
- E_INTERRUPTED: 130,
22
- // 128 + SIGINT (2). POSIX convention.
23
- E_INTERNAL: 1
24
- };
25
- var MushiCliError = class extends Error {
26
- code;
27
- hint;
28
- cause;
29
- constructor(code, message, hint, cause) {
30
- super(message);
31
- this.name = "MushiCliError";
32
- this.code = code;
33
- if (hint) this.hint = hint;
34
- if (cause !== void 0) this.cause = cause;
35
- }
36
- /** Exit code POSIX-aware shell scripts should branch on. */
37
- get exitCode() {
38
- return EXIT_CODE_MAP[this.code];
39
- }
40
- /**
41
- * Format for `--json` output. Used by the JSON writer when a CLI
42
- * subcommand was invoked with `--json` and an error escapes.
43
- */
44
- toJSON() {
45
- return {
46
- error: {
47
- code: this.code,
48
- message: this.message,
49
- ...this.hint ? { hint: this.hint } : {}
50
- }
51
- };
52
- }
53
- };
54
-
55
- // src/signals.ts
56
- var installed = false;
57
- var activeController = null;
58
- function installSignalHandlers() {
59
- if (installed) return;
60
- installed = true;
61
- const abortAndExit = (signal) => {
62
- activeController?.abort(
63
- new MushiCliError(
64
- "E_INTERRUPTED",
65
- `aborted by ${signal}`,
66
- "partial state on the server is safe to retry \u2014 re-run the same command"
67
- )
68
- );
69
- const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
70
- process.nextTick(() => {
71
- process.exit(code);
72
- });
73
- };
74
- process.on("SIGINT", () => abortAndExit("SIGINT"));
75
- process.on("SIGTERM", () => abortAndExit("SIGTERM"));
76
- }
77
- function getAbortSignal(external) {
78
- if (external) return external.signal;
79
- if (!activeController || activeController.signal.aborted) {
80
- activeController = new AbortController();
81
- }
82
- return activeController.signal;
83
- }
84
-
85
- // src/version.ts
86
- var MUSHI_CLI_VERSION = true ? "0.17.2" : "0.0.0-dev";
87
-
88
- // src/config.ts
89
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
90
- import { homedir } from "os";
91
- import { dirname, join } from "path";
92
- var SECURE_FILE_MODE = 384;
93
- var SECURE_DIR_MODE = 448;
94
- function resolveXdgConfigPath() {
95
- const xdg = process.env["XDG_CONFIG_HOME"];
96
- if (xdg && xdg.length > 0) {
97
- return join(xdg, "mushi", "config.json");
98
- }
99
- if (process.platform === "win32") {
100
- const appData = process.env["APPDATA"];
101
- if (appData && appData.length > 0) {
102
- return join(appData, "mushi", "config.json");
103
- }
104
- }
105
- return join(homedir(), ".config", "mushi", "config.json");
106
- }
107
- var LEGACY_CONFIG_PATH = join(homedir(), ".mushirc");
108
- var CONFIG_PATH = resolveXdgConfigPath();
109
- function loadConfig(path = CONFIG_PATH) {
110
- let file = {};
111
- if (existsSync(path)) {
112
- tightenPermissions(path);
113
- try {
114
- file = JSON.parse(readFileSync(path, "utf-8"));
115
- } catch {
116
- }
117
- } else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
118
- file = migrateLegacyConfig() ?? {};
119
- }
120
- const endpointFromEnv = process.env["MUSHI_API_ENDPOINT"] ?? process.env["MUSHI_ENDPOINT"] ?? void 0;
121
- const fromEnv = {
122
- ...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
123
- ...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
124
- ...endpointFromEnv ? { endpoint: endpointFromEnv } : {}
125
- };
126
- return { ...file, ...fromEnv };
127
- }
128
- function saveConfig(config, path = CONFIG_PATH) {
129
- const dir = dirname(path);
130
- if (!existsSync(dir)) {
131
- mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
132
- } else {
133
- tightenDirPermissions(dir);
134
- }
135
- writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
136
- tightenPermissions(path);
137
- }
138
- function migrateLegacyConfig(legacyPath = LEGACY_CONFIG_PATH, destPath = CONFIG_PATH) {
139
- if (!existsSync(legacyPath)) return null;
140
- let parsed;
141
- try {
142
- parsed = JSON.parse(readFileSync(legacyPath, "utf-8"));
143
- } catch {
144
- return null;
145
- }
146
- const dir = dirname(destPath);
147
- if (!existsSync(dir)) {
148
- mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
149
- }
150
- try {
151
- renameSync(legacyPath, destPath);
152
- } catch {
153
- writeFileSync(destPath, JSON.stringify(parsed, null, 2), { mode: SECURE_FILE_MODE });
154
- try {
155
- unlinkSync(legacyPath);
156
- } catch {
157
- }
158
- }
159
- tightenPermissions(destPath);
160
- tightenDirPermissions(dir);
161
- return parsed;
162
- }
163
- function tightenPermissions(path) {
164
- if (process.platform === "win32") return;
165
- try {
166
- const current = statSync(path).mode & 511;
167
- if (current !== SECURE_FILE_MODE) chmodSync(path, SECURE_FILE_MODE);
168
- } catch {
169
- }
170
- }
171
- function tightenDirPermissions(path) {
172
- if (process.platform === "win32") return;
173
- try {
174
- const current = statSync(path).mode & 511;
175
- if (current !== SECURE_DIR_MODE) chmodSync(path, SECURE_DIR_MODE);
176
- } catch {
177
- }
178
- }
179
-
180
25
  // src/init.ts
181
26
  import * as p from "@clack/prompts";
182
27
  import { spawn } from "child_process";
183
28
  import { randomUUID } from "crypto";
184
- import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
185
- import { join as join4 } from "path";
29
+ import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
30
+ import { join as join3 } from "path";
186
31
 
187
32
  // src/detect.ts
188
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
189
- import { join as join2 } from "path";
33
+ import { readFileSync, existsSync } from "fs";
34
+ import { join } from "path";
190
35
  var FRAMEWORKS = {
191
36
  next: {
192
37
  id: "next",
193
38
  label: "Next.js",
194
39
  packageName: "@mushi-mushi/react",
195
40
  needsWebPackage: false,
196
- snippet: (apiKey, projectId) => `// app/providers.tsx (or pages/_app.tsx for /pages router)
41
+ snippet: () => `// app/providers.tsx (or pages/_app.tsx for /pages router)
197
42
  'use client'
198
43
  import { MushiProvider } from '@mushi-mushi/react'
199
44
 
200
45
  export function Providers({ children }: { children: React.ReactNode }) {
201
46
  return (
202
47
  <MushiProvider config={{
203
- projectId: '${projectId}',
204
- apiKey: '${apiKey}',
48
+ projectId: process.env.NEXT_PUBLIC_MUSHI_PROJECT_ID!,
49
+ apiKey: process.env.NEXT_PUBLIC_MUSHI_API_KEY!,
205
50
  }}>
206
51
  {children}
207
52
  </MushiProvider>
@@ -213,13 +58,13 @@ export function Providers({ children }: { children: React.ReactNode }) {
213
58
  label: "React",
214
59
  packageName: "@mushi-mushi/react",
215
60
  needsWebPackage: false,
216
- snippet: (apiKey, projectId) => `// src/main.tsx
61
+ snippet: () => `// src/main.tsx
217
62
  import { MushiProvider } from '@mushi-mushi/react'
218
63
 
219
64
  createRoot(document.getElementById('root')!).render(
220
65
  <MushiProvider config={{
221
- projectId: '${projectId}',
222
- apiKey: '${apiKey}',
66
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
67
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
223
68
  }}>
224
69
  <App />
225
70
  </MushiProvider>
@@ -229,57 +74,58 @@ createRoot(document.getElementById('root')!).render(
229
74
  id: "vue",
230
75
  label: "Vue 3",
231
76
  packageName: "@mushi-mushi/vue",
232
- needsWebPackage: true,
233
- snippet: (apiKey, projectId) => `// src/main.ts
77
+ needsWebPackage: false,
78
+ snippet: () => `// src/main.ts
234
79
  import { MushiPlugin } from '@mushi-mushi/vue'
235
- import { Mushi } from '@mushi-mushi/web'
236
80
 
237
- app.use(MushiPlugin, { projectId: '${projectId}', apiKey: '${apiKey}' })
238
- Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
81
+ app.use(MushiPlugin, {
82
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
83
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
84
+ })`
239
85
  },
240
86
  nuxt: {
241
87
  id: "nuxt",
242
88
  label: "Nuxt",
243
89
  packageName: "@mushi-mushi/vue",
244
- needsWebPackage: true,
245
- snippet: (apiKey, projectId) => `// plugins/mushi.client.ts
90
+ needsWebPackage: false,
91
+ snippet: () => `// plugins/mushi.client.ts
246
92
  import { MushiPlugin } from '@mushi-mushi/vue'
247
- import { Mushi } from '@mushi-mushi/web'
248
93
 
249
94
  export default defineNuxtPlugin((nuxtApp) => {
250
95
  nuxtApp.vueApp.use(MushiPlugin, {
251
- projectId: '${projectId}',
252
- apiKey: '${apiKey}',
96
+ projectId: import.meta.env.NUXT_PUBLIC_MUSHI_PROJECT_ID,
97
+ apiKey: import.meta.env.NUXT_PUBLIC_MUSHI_API_KEY,
253
98
  })
254
- Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })
255
99
  })`
256
100
  },
257
101
  svelte: {
258
102
  id: "svelte",
259
103
  label: "Svelte",
260
104
  packageName: "@mushi-mushi/svelte",
261
- needsWebPackage: true,
262
- snippet: (apiKey, projectId) => `// src/main.ts (or +layout.svelte for SvelteKit)
105
+ needsWebPackage: false,
106
+ snippet: () => `// src/main.ts
263
107
  import { initMushi } from '@mushi-mushi/svelte'
264
- import { Mushi } from '@mushi-mushi/web'
265
108
 
266
- initMushi({ projectId: '${projectId}', apiKey: '${apiKey}' })
267
- Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
109
+ initMushi({
110
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
111
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
112
+ })`
268
113
  },
269
114
  sveltekit: {
270
115
  id: "sveltekit",
271
116
  label: "SvelteKit",
272
117
  packageName: "@mushi-mushi/svelte",
273
- needsWebPackage: true,
274
- snippet: (apiKey, projectId) => `// src/routes/+layout.svelte
118
+ needsWebPackage: false,
119
+ snippet: () => `// src/routes/+layout.svelte
275
120
  <script>
276
121
  import { onMount } from 'svelte'
277
- import { initMushi } from '@mushi-mushi/svelte'
278
122
 
279
123
  onMount(async () => {
280
- const { Mushi } = await import('@mushi-mushi/web')
281
- initMushi({ projectId: '${projectId}', apiKey: '${apiKey}' })
282
- Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })
124
+ const { initMushi } = await import('@mushi-mushi/svelte')
125
+ initMushi({
126
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
127
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
128
+ })
283
129
  })
284
130
  </script>`
285
131
  },
@@ -287,29 +133,33 @@ Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
287
133
  id: "angular",
288
134
  label: "Angular",
289
135
  packageName: "@mushi-mushi/angular",
290
- needsWebPackage: true,
291
- snippet: (apiKey, projectId) => `// src/main.ts
136
+ needsWebPackage: false,
137
+ snippet: () => `// src/main.ts
292
138
  import { provideMushi } from '@mushi-mushi/angular'
293
- import { Mushi } from '@mushi-mushi/web'
294
139
 
295
140
  bootstrapApplication(AppComponent, {
296
141
  providers: [
297
- provideMushi({ projectId: '${projectId}', apiKey: '${apiKey}' }),
142
+ provideMushi({
143
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
144
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
145
+ }),
298
146
  ],
299
- })
300
- Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
147
+ })`
301
148
  },
302
149
  expo: {
303
150
  id: "expo",
304
151
  label: "Expo",
305
152
  packageName: "@mushi-mushi/react-native",
306
153
  needsWebPackage: false,
307
- snippet: (apiKey, projectId) => `// App.tsx
154
+ snippet: () => `// App.tsx
308
155
  import { MushiProvider } from '@mushi-mushi/react-native'
309
156
 
310
157
  export default function App() {
311
158
  return (
312
- <MushiProvider projectId="${projectId}" apiKey="${apiKey}">
159
+ <MushiProvider
160
+ projectId={process.env.EXPO_PUBLIC_MUSHI_PROJECT_ID!}
161
+ apiKey={process.env.EXPO_PUBLIC_MUSHI_API_KEY!}
162
+ >
313
163
  <YourApp />
314
164
  </MushiProvider>
315
165
  )
@@ -320,12 +170,15 @@ export default function App() {
320
170
  label: "React Native",
321
171
  packageName: "@mushi-mushi/react-native",
322
172
  needsWebPackage: false,
323
- snippet: (apiKey, projectId) => `// App.tsx
173
+ snippet: () => `// App.tsx
324
174
  import { MushiProvider } from '@mushi-mushi/react-native'
325
175
 
326
176
  export default function App() {
327
177
  return (
328
- <MushiProvider projectId="${projectId}" apiKey="${apiKey}">
178
+ <MushiProvider
179
+ projectId={process.env.MUSHI_PROJECT_ID!}
180
+ apiKey={process.env.MUSHI_API_KEY!}
181
+ >
329
182
  <YourApp />
330
183
  </MushiProvider>
331
184
  )
@@ -342,24 +195,27 @@ export default function App() {
342
195
  * `TypeError: Mushi.init is not a function`. The accompanying admin
343
196
  * console snippet (apps/admin/src/lib/sdkSnippets.ts) emits the same
344
197
  * `Mushi.configure(...)` shape; both are pinned by tests. */
345
- snippet: (apiKey, projectId) => `// src/main.ts \u2014 after install, run \`npx cap sync\`
198
+ snippet: () => `// src/main.ts \u2014 after install, run \`npx cap sync\`
346
199
  import { Mushi } from '@mushi-mushi/capacitor'
347
200
 
348
- await Mushi.configure({ projectId: '${projectId}', apiKey: '${apiKey}' })`
201
+ await Mushi.configure({
202
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
203
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
204
+ })`
349
205
  },
350
206
  express: {
351
207
  id: "express",
352
208
  label: "Express",
353
209
  packageName: "@mushi-mushi/node",
354
210
  needsWebPackage: false,
355
- snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
211
+ snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
356
212
  import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
357
213
  import { mushiExpressErrorHandler } from '@mushi-mushi/node/express'
358
214
  import type { Express } from 'express'
359
215
 
360
216
  export const mushi = new MushiNodeClient({
361
- projectId: '${projectId}',
362
- apiKey: '${apiKey}',
217
+ projectId: process.env.MUSHI_PROJECT_ID!,
218
+ apiKey: process.env.MUSHI_API_KEY!,
363
219
  environment: process.env.NODE_ENV ?? 'production',
364
220
  })
365
221
  attachUnhandledHook({ client: mushi })
@@ -373,14 +229,14 @@ export function attachMushi(app: Express) {
373
229
  label: "Fastify",
374
230
  packageName: "@mushi-mushi/node",
375
231
  needsWebPackage: false,
376
- snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
232
+ snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
377
233
  import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
378
234
  import { mushiFastifyPlugin } from '@mushi-mushi/node/fastify'
379
235
  import Fastify from 'fastify'
380
236
 
381
237
  export const mushi = new MushiNodeClient({
382
- projectId: '${projectId}',
383
- apiKey: '${apiKey}',
238
+ projectId: process.env.MUSHI_PROJECT_ID!,
239
+ apiKey: process.env.MUSHI_API_KEY!,
384
240
  environment: process.env.NODE_ENV ?? 'production',
385
241
  })
386
242
  attachUnhandledHook({ client: mushi })
@@ -393,14 +249,14 @@ mushiFastifyPlugin(app, { client: mushi })`
393
249
  label: "Hono",
394
250
  packageName: "@mushi-mushi/node",
395
251
  needsWebPackage: false,
396
- snippet: (apiKey, projectId) => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
252
+ snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
397
253
  import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
398
254
  import { mushiHonoErrorHandler } from '@mushi-mushi/node/hono'
399
255
  import { Hono } from 'hono'
400
256
 
401
257
  export const mushi = new MushiNodeClient({
402
- projectId: '${projectId}',
403
- apiKey: '${apiKey}',
258
+ projectId: process.env.MUSHI_PROJECT_ID!,
259
+ apiKey: process.env.MUSHI_API_KEY!,
404
260
  environment: process.env.NODE_ENV ?? 'production',
405
261
  })
406
262
  attachUnhandledHook({ client: mushi })
@@ -417,17 +273,20 @@ app.onError(
417
273
  label: "Vanilla JS / unknown",
418
274
  packageName: "@mushi-mushi/web",
419
275
  needsWebPackage: false,
420
- snippet: (apiKey, projectId) => `// Anywhere in your client bundle
276
+ snippet: () => `// Anywhere in your client bundle
421
277
  import { Mushi } from '@mushi-mushi/web'
422
278
 
423
- Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
279
+ Mushi.init({
280
+ projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
281
+ apiKey: import.meta.env.VITE_MUSHI_API_KEY,
282
+ })`
424
283
  }
425
284
  };
426
285
  function readPackageJson(cwd) {
427
- const path = join2(cwd, "package.json");
428
- if (!existsSync2(path)) return null;
286
+ const path = join(cwd, "package.json");
287
+ if (!existsSync(path)) return null;
429
288
  try {
430
- return JSON.parse(readFileSync2(path, "utf-8"));
289
+ return JSON.parse(readFileSync(path, "utf-8"));
431
290
  } catch {
432
291
  return null;
433
292
  }
@@ -448,21 +307,21 @@ function detectFramework(cwd, pkg) {
448
307
  if (deps.has("express")) return FRAMEWORKS.express;
449
308
  if (deps.has("fastify")) return FRAMEWORKS.fastify;
450
309
  if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
451
- if (existsSync2(join2(cwd, "next.config.js")) || existsSync2(join2(cwd, "next.config.ts"))) {
310
+ if (existsSync(join(cwd, "next.config.js")) || existsSync(join(cwd, "next.config.ts"))) {
452
311
  return FRAMEWORKS.next;
453
312
  }
454
- if (existsSync2(join2(cwd, "nuxt.config.ts")) || existsSync2(join2(cwd, "nuxt.config.js"))) {
313
+ if (existsSync(join(cwd, "nuxt.config.ts")) || existsSync(join(cwd, "nuxt.config.js"))) {
455
314
  return FRAMEWORKS.nuxt;
456
315
  }
457
- if (existsSync2(join2(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
458
- if (existsSync2(join2(cwd, "angular.json"))) return FRAMEWORKS.angular;
316
+ if (existsSync(join(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
317
+ if (existsSync(join(cwd, "angular.json"))) return FRAMEWORKS.angular;
459
318
  return FRAMEWORKS.vanilla;
460
319
  }
461
320
  function detectPackageManager(cwd) {
462
- if (existsSync2(join2(cwd, "bun.lockb")) || existsSync2(join2(cwd, "bun.lock"))) return "bun";
463
- if (existsSync2(join2(cwd, "pnpm-lock.yaml"))) return "pnpm";
464
- if (existsSync2(join2(cwd, "yarn.lock"))) return "yarn";
465
- if (existsSync2(join2(cwd, "package-lock.json"))) return "npm";
321
+ if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
322
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
323
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
324
+ if (existsSync(join(cwd, "package-lock.json"))) return "npm";
466
325
  const userAgent = process.env.npm_config_user_agent ?? "";
467
326
  if (userAgent.startsWith("bun")) return "bun";
468
327
  if (userAgent.startsWith("pnpm")) return "pnpm";
@@ -481,6 +340,18 @@ function envVarsToWrite(apiKey, projectId, framework) {
481
340
  `MUSHI_API_KEY=${apiKey}`
482
341
  ].join("\n");
483
342
  }
343
+ if (framework.id === "react-native") {
344
+ return [
345
+ `MUSHI_PROJECT_ID=${projectId}`,
346
+ `MUSHI_API_KEY=${apiKey}`
347
+ ].join("\n");
348
+ }
349
+ if (framework.id === "expo") {
350
+ return [
351
+ `EXPO_PUBLIC_MUSHI_PROJECT_ID=${projectId}`,
352
+ `EXPO_PUBLIC_MUSHI_API_KEY=${apiKey}`
353
+ ].join("\n");
354
+ }
484
355
  const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
485
356
  return [
486
357
  `${prefix}MUSHI_PROJECT_ID=${projectId}`,
@@ -496,41 +367,6 @@ function collectDeps(pkg) {
496
367
  ]);
497
368
  }
498
369
 
499
- // src/endpoint.ts
500
- var TEST_REPORT_TIMEOUT_MS = 1e4;
501
- var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
502
- var CLOUD_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/functions/v1/api";
503
- function resolveCloudEndpoint(explicit) {
504
- if (explicit?.trim()) return explicit.trim();
505
- const fromEnv = process.env.MUSHI_API_ENDPOINT?.trim();
506
- if (fromEnv) return fromEnv;
507
- return CLOUD_API_ENDPOINT;
508
- }
509
- function assertEndpoint(url) {
510
- let parsed;
511
- try {
512
- parsed = new URL(url);
513
- } catch {
514
- throw new Error(`Invalid endpoint URL: ${url}`);
515
- }
516
- const host = parsed.hostname;
517
- const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local");
518
- if (parsed.protocol !== "https:" && !isLocal) {
519
- throw new Error(`Endpoint must use https:// (got ${parsed.protocol}//${host}).`);
520
- }
521
- return normalizeEndpoint(parsed.origin + parsed.pathname);
522
- }
523
- function normalizeEndpoint(url) {
524
- if (!url) {
525
- throw new Error(
526
- "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"
527
- );
528
- }
529
- let end = url.length;
530
- while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
531
- return url.slice(0, end);
532
- }
533
-
534
370
  // src/freshness.ts
535
371
  var REGISTRY = "https://registry.npmjs.org";
536
372
  var DEFAULT_TIMEOUT_MS = 2e3;
@@ -591,8 +427,8 @@ function parse(version) {
591
427
  }
592
428
 
593
429
  // src/monorepo.ts
594
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "fs";
595
- import { dirname as dirname2, join as join3, resolve } from "path";
430
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
431
+ import { dirname, join as join2, resolve } from "path";
596
432
  var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
597
433
  var FRAMEWORK_DEPS = {
598
434
  next: "Next.js",
@@ -609,9 +445,9 @@ var FRAMEWORK_DEPS = {
609
445
  function detectWorkspaceHint(cwd) {
610
446
  const root = findWorkspaceRoot(cwd);
611
447
  if (!root) return null;
612
- const rootPkg = readPackageJsonSafely(join3(root, "package.json"));
448
+ const rootPkg = readPackageJsonSafely(join2(root, "package.json"));
613
449
  if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
614
- const source = existsSync3(join3(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
450
+ const source = existsSync2(join2(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
615
451
  const apps = collectAppsFromGlobs(root);
616
452
  if (apps.length === 0) return null;
617
453
  return { root, apps, source };
@@ -620,15 +456,15 @@ function findWorkspaceRoot(start) {
620
456
  let dir = resolve(start);
621
457
  for (let i = 0; i < 8; i++) {
622
458
  if (isWorkspaceRoot(dir)) return dir;
623
- const parent = dirname2(dir);
459
+ const parent = dirname(dir);
624
460
  if (parent === dir) break;
625
461
  dir = parent;
626
462
  }
627
463
  return null;
628
464
  }
629
465
  function isWorkspaceRoot(dir) {
630
- if (existsSync3(join3(dir, "pnpm-workspace.yaml"))) return true;
631
- const pkg = readPackageJsonSafely(join3(dir, "package.json"));
466
+ if (existsSync2(join2(dir, "pnpm-workspace.yaml"))) return true;
467
+ const pkg = readPackageJsonSafely(join2(dir, "package.json"));
632
468
  if (!pkg) return false;
633
469
  return Boolean(pkg.workspaces);
634
470
  }
@@ -636,8 +472,8 @@ function collectAppsFromGlobs(root) {
636
472
  const results = [];
637
473
  for (const glob of WORKSPACE_GLOB_CANDIDATES) {
638
474
  const prefix = glob.replace("/*", "");
639
- const parentDir = join3(root, prefix);
640
- if (!existsSync3(parentDir)) continue;
475
+ const parentDir = join2(root, prefix);
476
+ if (!existsSync2(parentDir)) continue;
641
477
  let entries;
642
478
  try {
643
479
  entries = readdirSync(parentDir);
@@ -645,7 +481,7 @@ function collectAppsFromGlobs(root) {
645
481
  continue;
646
482
  }
647
483
  for (const entry of entries) {
648
- const pkgPath = join3(parentDir, entry, "package.json");
484
+ const pkgPath = join2(parentDir, entry, "package.json");
649
485
  if (!isFileSafe(pkgPath)) continue;
650
486
  const pkg = readPackageJsonSafely(pkgPath);
651
487
  if (!pkg) continue;
@@ -663,14 +499,14 @@ function collectAppsFromGlobs(root) {
663
499
  function readPackageJsonSafely(path) {
664
500
  if (!isFileSafe(path)) return null;
665
501
  try {
666
- return JSON.parse(readFileSync3(path, "utf-8"));
502
+ return JSON.parse(readFileSync2(path, "utf-8"));
667
503
  } catch {
668
504
  return null;
669
505
  }
670
506
  }
671
507
  function isFileSafe(path) {
672
508
  try {
673
- return existsSync3(path) && statSync2(path).isFile();
509
+ return existsSync2(path) && statSync(path).isFile();
674
510
  } catch {
675
511
  return false;
676
512
  }
@@ -690,7 +526,7 @@ function getFrameworkFromPkg(pkg) {
690
526
  // src/init.ts
691
527
  var ENV_FILES = [".env.local", ".env"];
692
528
  var PROJECT_ID_PATTERN = /^(?:proj_[A-Za-z0-9_-]{10,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
693
- var API_KEY_PATTERN = /^mushi_[A-Za-z0-9_-]{10,}$/;
529
+ var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
694
530
  async function runInit(options = {}) {
695
531
  const cwd = options.cwd ?? process.cwd();
696
532
  ensureInteractiveOrBailOut(options);
@@ -850,10 +686,10 @@ function runCommand(pm, packages, cwd) {
850
686
  });
851
687
  }
852
688
  function writeEnvFile(cwd, apiKey, projectId, framework) {
853
- const target = ENV_FILES.find((f) => existsSync4(join4(cwd, f))) ?? ENV_FILES[0];
854
- const targetPath = join4(cwd, target);
689
+ const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
690
+ const targetPath = join3(cwd, target);
855
691
  const newVars = envVarsToWrite(apiKey, projectId, framework);
856
- const existing = existsSync4(targetPath) ? readFileSync4(targetPath, "utf-8") : "";
692
+ const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
857
693
  if (existing.includes("MUSHI_PROJECT_ID")) {
858
694
  p.log.warn(`Existing MUSHI_* vars found in ${target} \u2014 leaving them untouched.`);
859
695
  return;
@@ -888,12 +724,12 @@ function escapeRegexChar(ch) {
888
724
  return /[-/\\^$+?.()|[\]{}]/.test(ch) ? `\\${ch}` : ch;
889
725
  }
890
726
  function warnIfMissingFromGitignore(cwd, envFile) {
891
- const gitignorePath = join4(cwd, ".gitignore");
892
- if (!existsSync4(gitignorePath)) {
727
+ const gitignorePath = join3(cwd, ".gitignore");
728
+ if (!existsSync3(gitignorePath)) {
893
729
  p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
894
730
  return;
895
731
  }
896
- const content = readFileSync4(gitignorePath, "utf-8");
732
+ const content = readFileSync3(gitignorePath, "utf-8");
897
733
  if (!isEnvFileCoveredByGitignore(content, envFile)) {
898
734
  p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
899
735
  }
@@ -902,8 +738,8 @@ function persistCliConfig(apiKey, projectId, endpoint) {
902
738
  const existing = loadConfig();
903
739
  saveConfig({ ...existing, apiKey, projectId, endpoint });
904
740
  }
905
- function printNextSteps(framework, apiKey, projectId, enableRewards = false) {
906
- p.note(framework.snippet(apiKey, projectId), "Add this to your app:");
741
+ function printNextSteps(framework, _apiKey, _projectId, enableRewards = false) {
742
+ p.note(framework.snippet(), "Add this to your app:");
907
743
  if (enableRewards) {
908
744
  const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
909
745
  import { MushiRewardsBadge } from '@mushi-mushi/react';
@@ -1169,130 +1005,6 @@ function runMigrate(opts = {}) {
1169
1005
  return { matches };
1170
1006
  }
1171
1007
 
1172
- // src/cli-shared.ts
1173
- var API_TIMEOUT_MS = 15e3;
1174
- async function apiCall(path, config, options = {}) {
1175
- const endpoint = config.endpoint;
1176
- if (!endpoint) {
1177
- return {
1178
- ok: false,
1179
- error: {
1180
- code: "NO_ENDPOINT",
1181
- 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"
1182
- }
1183
- };
1184
- }
1185
- const controller = new AbortController();
1186
- const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1187
- const signals = [controller.signal, getAbortSignal()];
1188
- const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
1189
- try {
1190
- const res = await fetch(`${endpoint}${path}`, {
1191
- ...options,
1192
- signal: compositeSignal,
1193
- headers: {
1194
- "Content-Type": "application/json",
1195
- // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
1196
- // fallback accepted by some older middleware. Both are sent.
1197
- "Authorization": `Bearer ${config.apiKey ?? ""}`,
1198
- "X-Mushi-Api-Key": config.apiKey ?? "",
1199
- "X-Mushi-Project": config.projectId ?? "",
1200
- "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
1201
- ...options.headers
1202
- }
1203
- });
1204
- clearTimeout(timer);
1205
- let body;
1206
- const contentType = res.headers.get("content-type") ?? "";
1207
- if (contentType.includes("application/json")) {
1208
- try {
1209
- body = await res.json();
1210
- } catch {
1211
- body = null;
1212
- }
1213
- } else {
1214
- const text2 = await res.text();
1215
- try {
1216
- body = JSON.parse(text2);
1217
- } catch {
1218
- body = {
1219
- ok: false,
1220
- error: {
1221
- code: `HTTP_${res.status}`,
1222
- message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
1223
- }
1224
- };
1225
- }
1226
- }
1227
- if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
1228
- const b = body;
1229
- return {
1230
- ok: false,
1231
- httpStatus: res.status,
1232
- error: {
1233
- code: b["code"] ?? `HTTP_${res.status}`,
1234
- message: b["message"] ?? `Request failed (${res.status})`
1235
- }
1236
- };
1237
- }
1238
- return body;
1239
- } catch (err) {
1240
- clearTimeout(timer);
1241
- if (err instanceof Error && err.name === "AbortError") {
1242
- return {
1243
- ok: false,
1244
- error: {
1245
- code: "TIMEOUT",
1246
- message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
1247
- }
1248
- };
1249
- }
1250
- return {
1251
- ok: false,
1252
- error: {
1253
- code: "NETWORK_ERROR",
1254
- message: err instanceof Error ? err.message : String(err)
1255
- }
1256
- };
1257
- }
1258
- }
1259
- function die(result, exitCode = 1) {
1260
- const { code, message } = result.error;
1261
- const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
1262
- process.stderr.write(`error${status}: ${code} \u2014 ${message}
1263
- `);
1264
- process.exit(exitCode);
1265
- }
1266
- function requireConfig(opts = {}) {
1267
- const config = loadConfig();
1268
- if (!config.apiKey) {
1269
- process.stderr.write(
1270
- "error: API key not configured.\n Run: mushi login --api-key <key> --endpoint <url>\n Or: export MUSHI_API_KEY=<key>\n"
1271
- );
1272
- process.exit(2);
1273
- }
1274
- if (!config.endpoint) {
1275
- process.stderr.write(
1276
- "error: API endpoint not configured.\n Run: mushi login --endpoint https://<ref>.supabase.co/functions/v1/api\n Or: export MUSHI_API_ENDPOINT=<url>\n"
1277
- );
1278
- process.exit(2);
1279
- }
1280
- if (opts.needsProject && !config.projectId) {
1281
- process.stderr.write(
1282
- "error: Project ID not configured.\n Run: mushi login --project-id <uuid>\n Or: export MUSHI_PROJECT_ID=<uuid>\n Find your project ID: https://kensaur.us/mushi-mushi/projects\n"
1283
- );
1284
- process.exit(2);
1285
- }
1286
- return config;
1287
- }
1288
- function fmtDate(iso) {
1289
- if (!iso) return "\u2014";
1290
- return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
1291
- }
1292
- function pad(s, width) {
1293
- return s.length >= width ? s : s + " ".repeat(width - s.length);
1294
- }
1295
-
1296
1008
  // src/commands/account.ts
1297
1009
  function registerAccountCommands(program2) {
1298
1010
  program2.command("init").description("Set up the Mushi Mushi SDK in this project (auto-detects framework)").option("--project-id <id>", "Skip the prompt \u2014 pass UUID from the Projects page").option("--api-key <key>", "Skip the prompt \u2014 pass the API key (CI only)").option("--framework <id>", "Force a framework (next, react, vue, nuxt, svelte, sveltekit, angular, expo, react-native, capacitor, vanilla)").option("--skip-install", "Print the install command instead of running it").option("-y, --yes", "Accept detected framework without prompting").option("--cwd <path>", "Run the wizard in a different directory").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--skip-test-report", 'Skip the end-of-wizard "send a test report" prompt').action(async (opts) => {
@@ -1311,17 +1023,78 @@ function registerAccountCommands(program2) {
1311
1023
  const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
1312
1024
  if (matches.length === 0) process.exit(1);
1313
1025
  });
1314
- program2.command("login").description("Save API credentials to ~/.mushirc (mode 0o600)").requiredOption("--api-key <key>", "Mushi API key (mushi_...)").option("--endpoint <url>", "Supabase edge function URL").option("--project-id <id>", "Project UUID (from the Projects page)").addHelpText("after", `
1026
+ program2.command("login").description("Authenticate the CLI \u2014 opens browser to get your API key, or pass --api-key to skip").option("--api-key <key>", "Mushi API key (mushi_...) \u2014 skip the browser flow").option("--endpoint <url>", "Supabase edge function URL (defaults to Mushi Cloud)").option("--project-id <id>", "Project UUID \u2014 skip the whoami project-list prompt").option("--no-browser", "Skip opening the browser (print sign-in URL instead)").addHelpText("after", `
1315
1027
  Examples:
1316
- mushi login --api-key mushi_xxx --endpoint https://xyz.supabase.co/functions/v1/api
1317
- mushi login --api-key mushi_xxx --project-id 542b34e0-019e-41fe-b900-7b637717bb86`).action((opts) => {
1028
+ mushi login # browser-guided: open console \u2192 paste key
1029
+ mushi login --api-key mushi_xxx # non-interactive: save key directly
1030
+ mushi login --api-key mushi_xxx --project-id <uuid>`).action(async (opts) => {
1031
+ const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1032
+ const { apiCall: apiCall2 } = await import("./cli-shared-FZWLZCCR.js");
1033
+ const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
1034
+ const consoleUrl = "https://kensaur.us/mushi-mushi/projects";
1035
+ let apiKey = opts.apiKey;
1036
+ let projectId = opts.projectId;
1037
+ if (!apiKey) {
1038
+ const signInUrl = "https://kensaur.us/mushi-mushi/sign-in";
1039
+ console.log("");
1040
+ console.log(" Mushi login \u2014 browser-guided");
1041
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1042
+ if (opts.browser !== false) {
1043
+ console.log(" Opening the Mushi console in your browser\u2026");
1044
+ try {
1045
+ const { exec } = await import("child_process");
1046
+ const openCmd = process.platform === "win32" ? `start "" "${signInUrl}"` : process.platform === "darwin" ? `open "${signInUrl}"` : `xdg-open "${signInUrl}"`;
1047
+ exec(openCmd);
1048
+ } catch {
1049
+ }
1050
+ } else {
1051
+ console.log(` Sign in at: ${signInUrl}`);
1052
+ }
1053
+ console.log("");
1054
+ console.log(" After signing in, go to Projects \u2192 API Keys \u2192 New key");
1055
+ console.log(" then paste the key below.");
1056
+ console.log("");
1057
+ const { createInterface } = await import("readline");
1058
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1059
+ const ask = (q) => new Promise((resolve4) => rl.question(q, (a) => resolve4(a.trim())));
1060
+ apiKey = await ask(" API key (mushi_...): ");
1061
+ if (!projectId) {
1062
+ projectId = await ask(` Project ID (uuid, or leave blank to list projects): `);
1063
+ if (!projectId) projectId = void 0;
1064
+ }
1065
+ rl.close();
1066
+ if (!apiKey) {
1067
+ process.stderr.write("\nerror: API key is required.\n");
1068
+ process.exit(2);
1069
+ }
1070
+ }
1318
1071
  const config = loadConfig();
1319
- config.apiKey = opts.apiKey;
1320
- if (opts.endpoint) config.endpoint = assertEndpoint(opts.endpoint);
1321
- if (opts.projectId) config.projectId = opts.projectId;
1072
+ config.apiKey = apiKey;
1073
+ config.endpoint = endpoint;
1074
+ if (projectId) config.projectId = projectId;
1322
1075
  saveConfig(config);
1323
- console.log("\u2713 Credentials saved to ~/.mushirc (mode 0o600)");
1324
- console.log(" Run 'mushi whoami' to verify the connection.");
1076
+ console.log("");
1077
+ console.log(" \u2713 Credentials saved to ~/.config/mushi/config.json");
1078
+ const verifyResult = await apiCall2(
1079
+ "/v1/sync/whoami",
1080
+ { apiKey, endpoint, projectId }
1081
+ );
1082
+ if (!verifyResult.ok) {
1083
+ console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
1084
+ console.warn(` Check your key at: ${consoleUrl}`);
1085
+ return;
1086
+ }
1087
+ const d = verifyResult.data;
1088
+ if (!projectId && d.project_id) {
1089
+ config.projectId = d.project_id;
1090
+ saveConfig(config);
1091
+ console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
1092
+ } else if (d.project_name) {
1093
+ console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
1094
+ }
1095
+ console.log(` \u2713 Endpoint: ${endpoint}`);
1096
+ console.log("");
1097
+ console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1325
1098
  });
1326
1099
  program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1327
1100
  Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
@@ -1390,7 +1163,7 @@ Useful after 'mushi login' to confirm credentials are correct.`).action(async (o
1390
1163
  console.log(`Fixes: ${d.fixes_count} total \xB7 ${d.fixes_merged} merged`);
1391
1164
  console.log(`Lessons: ${d.lessons_count} active rules`);
1392
1165
  });
1393
- program2.command("config").description("View or update CLI config (stored in ~/.mushirc)").argument("[key]", "Config key to set: apiKey | endpoint | projectId").argument("[value]", "New value").addHelpText("after", `
1166
+ program2.command("config").description("View or update CLI config (stored in ~/.config/mushi/config.json)").argument("[key]", "Config key to set: apiKey | endpoint | projectId").argument("[value]", "New value").addHelpText("after", `
1394
1167
  Keys:
1395
1168
  apiKey \u2014 Mushi API key (mushi_...)
1396
1169
  endpoint \u2014 Supabase edge function URL
@@ -1844,7 +1617,7 @@ AI code review context.
1844
1617
  Typical CI usage:
1845
1618
  MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
1846
1619
  npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
1847
- const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
1620
+ const { writeFile: writeFile3, mkdir: mkdir2 } = await import("fs/promises");
1848
1621
  const nodePath = await import("path");
1849
1622
  const config = requireConfig();
1850
1623
  const cwd = opts.cwd ?? process.cwd();
@@ -1872,7 +1645,7 @@ Typical CI usage:
1872
1645
  return;
1873
1646
  }
1874
1647
  await mkdir2(nodePath.dirname(target), { recursive: true });
1875
- await writeFile2(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1648
+ await writeFile3(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1876
1649
  if (opts.json) {
1877
1650
  console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
1878
1651
  } else {
@@ -1885,14 +1658,14 @@ Typical CI usage:
1885
1658
  import { createReadStream } from "fs";
1886
1659
  import { readFile, readdir } from "fs/promises";
1887
1660
  import { createHash } from "crypto";
1888
- import { join as join5, relative, basename } from "path";
1661
+ import { join as join4, relative, basename } from "path";
1889
1662
  import * as p2 from "@clack/prompts";
1890
1663
  async function findMapFiles(dir) {
1891
1664
  const results = [];
1892
1665
  async function walk(current) {
1893
1666
  const entries = await readdir(current, { withFileTypes: true });
1894
1667
  for (const e of entries) {
1895
- const full = join5(current, e.name);
1668
+ const full = join4(current, e.name);
1896
1669
  if (e.isDirectory()) {
1897
1670
  await walk(full);
1898
1671
  } else if (e.isFile() && (e.name.endsWith(".js.map") || e.name.endsWith(".css.map"))) {
@@ -2067,7 +1840,7 @@ Examples:
2067
1840
  mushi index ./src
2068
1841
  mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
2069
1842
  const config = requireConfig({ needsProject: true });
2070
- const { readdir: readdir2, readFile: readFile3, stat } = await import("fs/promises");
1843
+ const { readdir: readdir2, readFile: readFile4, stat } = await import("fs/promises");
2071
1844
  const nodePath = await import("path");
2072
1845
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
2073
1846
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
@@ -2094,7 +1867,7 @@ Examples:
2094
1867
  `);
2095
1868
  continue;
2096
1869
  }
2097
- const source = await readFile3(file, "utf8");
1870
+ const source = await readFile4(file, "utf8");
2098
1871
  const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
2099
1872
  count++;
2100
1873
  bytes += source.length;
@@ -2141,6 +1914,56 @@ Examples:
2141
1914
  });
2142
1915
  }
2143
1916
 
1917
+ // src/mcp-config.ts
1918
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
1919
+ import { dirname as dirname2 } from "path";
1920
+ var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
1921
+ async function writeMcpServerEntry(opts) {
1922
+ const { configPath, serverName, serverBlock, dryRun = false } = opts;
1923
+ let merged = { mcpServers: {} };
1924
+ let created = false;
1925
+ try {
1926
+ const existing = await readFile2(configPath, "utf8");
1927
+ try {
1928
+ merged = JSON.parse(existing);
1929
+ } catch {
1930
+ merged = { mcpServers: {} };
1931
+ }
1932
+ } catch {
1933
+ created = true;
1934
+ merged = { mcpServers: {} };
1935
+ }
1936
+ const servers = merged.mcpServers ?? {};
1937
+ servers[serverName] = serverBlock;
1938
+ merged.mcpServers = servers;
1939
+ const output = JSON.stringify(merged, null, 2) + "\n";
1940
+ if (!dryRun) {
1941
+ await mkdir(dirname2(configPath), { recursive: true });
1942
+ await writeFile(configPath, output, "utf8");
1943
+ }
1944
+ return { created, path: configPath };
1945
+ }
1946
+ function buildMcpServerBlock(opts) {
1947
+ return {
1948
+ command: "npx",
1949
+ args: ["-y", "@mushi-mushi/mcp@latest"],
1950
+ env: {
1951
+ MUSHI_API_ENDPOINT: opts.endpoint,
1952
+ MUSHI_PROJECT_ID: opts.projectId,
1953
+ MUSHI_API_KEY: opts.apiKey,
1954
+ MUSHI_FEATURES: DEFAULT_MUSHI_FEATURES
1955
+ }
1956
+ };
1957
+ }
1958
+ function buildMcpServerName(opts) {
1959
+ if (opts.legacy) return "mushi";
1960
+ if (opts.projectName) {
1961
+ const slug = opts.projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
1962
+ return `mushi-${slug}`;
1963
+ }
1964
+ return `mushi-${(opts.projectId ?? "unknown").slice(0, 8)}`;
1965
+ }
1966
+
2144
1967
  // src/commands/project.ts
2145
1968
  function registerProjectCommands(program2) {
2146
1969
  const project = program2.command("project").description("Project management");
@@ -2155,8 +1978,8 @@ Typical first-time flow:
2155
1978
  # Browser opens \u2192 sign up / magic-link \u2192 come back to terminal
2156
1979
  # CLI writes .env.local and .cursor/mcp.json
2157
1980
  # mushi whoami to confirm`).action(async (opts) => {
2158
- const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
2159
- const { existsSync: existsSync5 } = await import("fs");
1981
+ const { writeFile: writeFile3 } = await import("fs/promises");
1982
+ const { existsSync: existsSync4 } = await import("fs");
2160
1983
  const nodePath = await import("path");
2161
1984
  const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
2162
1985
  const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
@@ -2203,41 +2026,15 @@ Typical first-time flow:
2203
2026
  `MUSHI_API_KEY=${apiKey}`,
2204
2027
  ""
2205
2028
  ];
2206
- const envExisting = existsSync5(envPath);
2207
- await writeFile2(envPath, envLines.join("\n"), "utf8");
2029
+ const envExisting = existsSync4(envPath);
2030
+ await writeFile3(envPath, envLines.join("\n"), "utf8");
2208
2031
  console.log(`
2209
2032
  \u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
2210
- const mcpDir = nodePath.join(cwd, ".cursor");
2211
- await mkdir2(mcpDir, { recursive: true });
2212
- const mcpPath = nodePath.join(mcpDir, "mcp.json");
2213
- const mcpJson = {
2214
- mcpServers: {
2215
- mushi: {
2216
- command: "npx",
2217
- args: ["-y", "@mushi-mushi/mcp@latest"],
2218
- env: {
2219
- MUSHI_API_ENDPOINT: endpoint,
2220
- MUSHI_PROJECT_ID: projectId,
2221
- MUSHI_API_KEY: apiKey
2222
- }
2223
- }
2224
- }
2225
- };
2226
- const mcpExisting = existsSync5(mcpPath);
2227
- if (mcpExisting) {
2228
- try {
2229
- const { readFile: readFile3 } = await import("fs/promises");
2230
- const raw = JSON.parse(await readFile3(mcpPath, "utf8"));
2231
- const existing = raw;
2232
- existing.mcpServers = { ...existing.mcpServers ?? {}, mushi: mcpJson.mcpServers.mushi };
2233
- await writeFile2(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
2234
- } catch {
2235
- await writeFile2(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
2236
- }
2237
- } else {
2238
- await writeFile2(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
2239
- }
2240
- console.log(` \u2713 ${mcpExisting ? "Updated" : "Created"} .cursor/mcp.json`);
2033
+ const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
2034
+ const serverName = buildMcpServerName({ legacy: true });
2035
+ const serverBlock = buildMcpServerBlock({ endpoint, projectId, apiKey });
2036
+ const { created: mcpCreated } = await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
2037
+ console.log(` \u2713 ${mcpCreated ? "Created" : "Updated"} .cursor/mcp.json`);
2241
2038
  console.log("");
2242
2039
  console.log(' Done! Restart Cursor and ask: "list mushi tools"');
2243
2040
  console.log(" Run `mushi whoami` to verify the connection.");
@@ -2247,9 +2044,10 @@ Typical first-time flow:
2247
2044
 
2248
2045
  // src/commands/setup.ts
2249
2046
  function registerSetupCommands(program2) {
2250
- program2.command("setup").description("Wire Mushi into your IDE with one command").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: project ID prefix)").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").addHelpText("after", `
2047
+ program2.command("setup").description("Wire Mushi into your IDE with one command").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").addHelpText("after", `
2251
2048
  Examples:
2252
2049
  mushi setup # wire Cursor (default)
2050
+ mushi setup --all-projects # one server entry per accessible project
2253
2051
  mushi setup --ide claude # wire Claude Code
2254
2052
  mushi setup --ide cursor --with-rules # also write .cursorrules
2255
2053
 
@@ -2259,12 +2057,48 @@ Supported IDEs:
2259
2057
  continue \u2014 writes .continue/mcp.json
2260
2058
  zed \u2014 writes ~/.config/zed/settings.json mcpServers block
2261
2059
 
2262
- The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
2263
- const { writeFile: writeFile2, mkdir: mkdir2, readFile: readFile3 } = await import("fs/promises");
2264
- const { existsSync: existsSync5 } = await import("fs");
2060
+ The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
2061
+ const { writeFile: writeFile3, mkdir: mkdir2, readFile: readFile4 } = await import("fs/promises");
2062
+ const { existsSync: existsSync4 } = await import("fs");
2265
2063
  const nodePath = await import("path");
2266
2064
  const os = await import("os");
2267
2065
  const config = requireConfig({ needsProject: true });
2066
+ const redactKeyForDisplay = (text2) => {
2067
+ const key = config.apiKey;
2068
+ if (!key) return text2;
2069
+ const masked = key.length > 12 ? `${key.slice(0, 10)}\u2026${key.slice(-2)}` : "\u2022\u2022\u2022\u2022";
2070
+ return text2.split(key).join(masked);
2071
+ };
2072
+ let slug;
2073
+ if (opts.projectSlug) {
2074
+ slug = opts.projectSlug;
2075
+ } else {
2076
+ try {
2077
+ const res = await fetch(
2078
+ `${config.endpoint?.replace(/\/$/, "")}/v1/admin/mcp/projects`,
2079
+ {
2080
+ headers: {
2081
+ "X-Mushi-Api-Key": config.apiKey ?? "",
2082
+ "X-Mushi-Project": config.projectId ?? ""
2083
+ },
2084
+ signal: AbortSignal.timeout(5e3)
2085
+ }
2086
+ );
2087
+ if (res.ok) {
2088
+ const body = await res.json();
2089
+ const project = body?.data?.projects?.find((p3) => p3.id === config.projectId);
2090
+ if (project?.name) {
2091
+ slug = project.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
2092
+ } else {
2093
+ slug = config.projectId?.slice(0, 8) ?? "mushi";
2094
+ }
2095
+ } else {
2096
+ slug = config.projectId?.slice(0, 8) ?? "mushi";
2097
+ }
2098
+ } catch {
2099
+ slug = config.projectId?.slice(0, 8) ?? "mushi";
2100
+ }
2101
+ }
2268
2102
  const IDE_CONFIG = {
2269
2103
  cursor: { dir: ".cursor", file: "mcp.json", format: "mcp-json" },
2270
2104
  claude: { dir: ".claude", file: "mcp.json", format: "mcp-json" },
@@ -2278,7 +2112,23 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2278
2112
  process.exit(2);
2279
2113
  }
2280
2114
  const cwd = process.cwd();
2281
- const slug = opts.projectSlug ?? (config.projectId?.slice(0, 8) ?? "mushi");
2115
+ let allProjectsList = null;
2116
+ if (opts.allProjects && config.endpoint && config.apiKey) {
2117
+ try {
2118
+ const res = await fetch(
2119
+ `${config.endpoint.replace(/\/$/, "")}/v1/admin/mcp/projects`,
2120
+ {
2121
+ headers: { "X-Mushi-Api-Key": config.apiKey, "X-Mushi-Project": config.projectId ?? "" },
2122
+ signal: AbortSignal.timeout(8e3)
2123
+ }
2124
+ );
2125
+ if (res.ok) {
2126
+ const body = await res.json();
2127
+ allProjectsList = body?.data?.projects ?? [];
2128
+ }
2129
+ } catch {
2130
+ }
2131
+ }
2282
2132
  const serverName = `mushi-${slug}`;
2283
2133
  const mcpServerBlock = {
2284
2134
  command: "npx",
@@ -2292,31 +2142,38 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2292
2142
  const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir : nodePath.join(cwd, ideEntry.dir);
2293
2143
  const configPath = nodePath.join(configDir, ideEntry.file);
2294
2144
  if (ideEntry.format === "mcp-json") {
2295
- let merged = { mcpServers: {} };
2296
- if (existsSync5(configPath)) {
2297
- try {
2298
- const raw = await readFile3(configPath, "utf8");
2299
- merged = JSON.parse(raw);
2300
- } catch {
2145
+ if (allProjectsList && allProjectsList.length > 0) {
2146
+ for (const p3 of allProjectsList) {
2147
+ const pServerName = buildMcpServerName({ projectId: p3.id, projectName: p3.name ?? void 0 });
2148
+ const pBlock = buildMcpServerBlock({
2149
+ endpoint: config.endpoint,
2150
+ projectId: p3.id,
2151
+ apiKey: config.apiKey
2152
+ });
2153
+ if (!opts.dryRun) {
2154
+ await writeMcpServerEntry({ configPath, serverName: pServerName, serverBlock: pBlock });
2155
+ }
2156
+ }
2157
+ if (opts.dryRun) {
2158
+ console.log(`[dry-run] Would add ${allProjectsList.length} mushi-* entries to ${configPath}`);
2159
+ } else {
2160
+ console.log(`\u2713 Added ${allProjectsList.length} mushi-* server entries (${allProjectsList.map((p3) => p3.name ?? p3.id.slice(0, 8)).join(", ")})`);
2301
2161
  }
2302
- }
2303
- const servers = merged.mcpServers ?? {};
2304
- servers[serverName] = mcpServerBlock;
2305
- merged.mcpServers = servers;
2306
- const output = JSON.stringify(merged, null, 2) + "\n";
2307
- if (opts.dryRun) {
2308
- console.log(`[dry-run] Would write ${configPath}:`);
2309
- console.log(output);
2310
2162
  } else {
2311
- await mkdir2(configDir, { recursive: true });
2312
- await writeFile2(configPath, output, "utf8");
2313
- console.log(`\u2713 Written ${configPath}`);
2163
+ if (opts.dryRun) {
2164
+ const preview = JSON.stringify({ mcpServers: { [serverName]: mcpServerBlock } }, null, 2) + "\n";
2165
+ console.log(`[dry-run] Would write ${configPath}:`);
2166
+ console.log(redactKeyForDisplay(preview));
2167
+ } else {
2168
+ await writeMcpServerEntry({ configPath, serverName, serverBlock: mcpServerBlock });
2169
+ console.log(`\u2713 Written ${configPath}`);
2170
+ }
2314
2171
  }
2315
2172
  } else if (ideEntry.format === "zed") {
2316
2173
  let settings = {};
2317
- if (existsSync5(configPath)) {
2174
+ if (existsSync4(configPath)) {
2318
2175
  try {
2319
- const raw = await readFile3(configPath, "utf8");
2176
+ const raw = await readFile4(configPath, "utf8");
2320
2177
  settings = JSON.parse(raw);
2321
2178
  } catch {
2322
2179
  }
@@ -2338,10 +2195,10 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2338
2195
  const output = JSON.stringify(settings, null, 2) + "\n";
2339
2196
  if (opts.dryRun) {
2340
2197
  console.log(`[dry-run] Would write ${configPath}:`);
2341
- console.log(output);
2198
+ console.log(redactKeyForDisplay(output));
2342
2199
  } else {
2343
2200
  await mkdir2(configDir, { recursive: true });
2344
- await writeFile2(configPath, output, "utf8");
2201
+ await writeFile3(configPath, output, "utf8");
2345
2202
  console.log(`\u2713 Written ${configPath}`);
2346
2203
  }
2347
2204
  }
@@ -2375,7 +2232,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2375
2232
  if (opts.dryRun) {
2376
2233
  console.log(`[dry-run] Would write ${rulesPath}`);
2377
2234
  } else {
2378
- await writeFile2(rulesPath, rulesContent, "utf8");
2235
+ await writeFile3(rulesPath, rulesContent, "utf8");
2379
2236
  console.log(`\u2713 Written .cursorrules`);
2380
2237
  }
2381
2238
  } else if (opts.ide === "claude") {
@@ -2385,7 +2242,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2385
2242
  console.log(`[dry-run] Would write ${rulesPath}`);
2386
2243
  } else {
2387
2244
  await mkdir2(rulesDir, { recursive: true });
2388
- await writeFile2(rulesPath, rulesContent, "utf8");
2245
+ await writeFile3(rulesPath, rulesContent, "utf8");
2389
2246
  console.log(`\u2713 Written .claude/rules/mushi.md`);
2390
2247
  }
2391
2248
  }
@@ -2515,7 +2372,7 @@ function renderNudgeExplainer(phase) {
2515
2372
 
2516
2373
  // src/sanitize-config.ts
2517
2374
  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;
2518
- var API_KEY_RE = /^mushi_[A-Za-z0-9_-]{10,}$/;
2375
+ var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
2519
2376
  function sanitizeApiKey(raw) {
2520
2377
  const key = raw.replace(/[\r\n\0]/g, "");
2521
2378
  if (!API_KEY_RE.test(key)) {
@@ -2662,11 +2519,11 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
2662
2519
  }
2663
2520
  async function checkSdkInstall(cwd) {
2664
2521
  try {
2665
- const { readFile: readFile3 } = await import("fs/promises");
2666
- const { join: join7, resolve: resolve4 } = await import("path");
2522
+ const { readFile: readFile4 } = await import("fs/promises");
2523
+ const { join: join6, resolve: resolve4 } = await import("path");
2667
2524
  const root = resolve4(cwd);
2668
- const pkgPath = join7(root, "package.json");
2669
- const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
2525
+ const pkgPath = join6(root, "package.json");
2526
+ const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
2670
2527
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2671
2528
  const sdks = [
2672
2529
  "@mushi-mushi/react",
@@ -2674,11 +2531,11 @@ async function checkSdkInstall(cwd) {
2674
2531
  "@mushi-mushi/core",
2675
2532
  "@mushi-mushi/react-native"
2676
2533
  ];
2677
- const installed2 = sdks.filter((s) => deps[s]);
2534
+ const installed = sdks.filter((s) => deps[s]);
2678
2535
  return {
2679
2536
  name: "SDK installed in this repo",
2680
- ok: installed2.length > 0,
2681
- detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
2537
+ ok: installed.length > 0,
2538
+ detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
2682
2539
  };
2683
2540
  } catch {
2684
2541
  return null;
@@ -2844,18 +2701,18 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
2844
2701
  async function checkHostAppWiring(cwd) {
2845
2702
  const checks = [];
2846
2703
  try {
2847
- const { readFile: readFile3, access } = await import("fs/promises");
2848
- const { join: join7, resolve: resolve4 } = await import("path");
2704
+ const { readFile: readFile4, access } = await import("fs/promises");
2705
+ const { join: join6, resolve: resolve4 } = await import("path");
2849
2706
  const root = resolve4(cwd);
2850
- const pkgPath = join7(root, "package.json");
2851
- const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
2707
+ const pkgPath = join6(root, "package.json");
2708
+ const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
2852
2709
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2853
2710
  const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
2854
2711
  const envCandidates = [".env.local", ".env"];
2855
2712
  let envContent = "";
2856
2713
  for (const f of envCandidates) {
2857
2714
  try {
2858
- envContent = await readFile3(join7(root, f), "utf8");
2715
+ envContent = await readFile4(join6(root, f), "utf8");
2859
2716
  break;
2860
2717
  } catch {
2861
2718
  }
@@ -2869,7 +2726,7 @@ async function checkHostAppWiring(cwd) {
2869
2726
  });
2870
2727
  let mcpPresent = false;
2871
2728
  try {
2872
- await access(join7(root, ".cursor", "mcp.json"));
2729
+ await access(join6(root, ".cursor", "mcp.json"));
2873
2730
  mcpPresent = true;
2874
2731
  } catch {
2875
2732
  }
@@ -2900,18 +2757,128 @@ async function checkHostAppWiring(cwd) {
2900
2757
  }
2901
2758
  return checks;
2902
2759
  }
2760
+ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
2761
+ const checks = [];
2762
+ const { readFile: readFile4 } = await import("fs/promises");
2763
+ const { join: join6, resolve: resolve4 } = await import("path");
2764
+ const { homedir } = await import("os");
2765
+ const root = resolve4(cwd);
2766
+ const candidates = [
2767
+ join6(root, ".cursor", "mcp.json"),
2768
+ join6(homedir(), ".cursor", "mcp.json")
2769
+ ];
2770
+ let mcpPath = null;
2771
+ let mcpRaw = null;
2772
+ for (const candidate of candidates) {
2773
+ try {
2774
+ mcpRaw = await readFile4(candidate, "utf8");
2775
+ mcpPath = candidate;
2776
+ break;
2777
+ } catch {
2778
+ }
2779
+ }
2780
+ if (!mcpPath || !mcpRaw) {
2781
+ checks.push({
2782
+ name: "[mcp] mcp.json present",
2783
+ ok: false,
2784
+ detail: "No .cursor/mcp.json found in cwd or ~/.cursor/. Run `mushi setup` to create it."
2785
+ });
2786
+ return checks;
2787
+ }
2788
+ checks.push({
2789
+ name: "[mcp] mcp.json present",
2790
+ ok: true,
2791
+ detail: `Found at ${mcpPath}`
2792
+ });
2793
+ let mcpConfig = {};
2794
+ try {
2795
+ mcpConfig = JSON.parse(mcpRaw);
2796
+ } catch {
2797
+ checks.push({ name: "[mcp] mcp.json valid JSON", ok: false, detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`." });
2798
+ return checks;
2799
+ }
2800
+ const servers = mcpConfig.mcpServers ?? {};
2801
+ const mushiEntries = Object.entries(servers).filter(([k]) => k === "mushi" || k.startsWith("mushi-"));
2802
+ if (mushiEntries.length === 0) {
2803
+ checks.push({
2804
+ name: "[mcp] mushi server entry",
2805
+ ok: false,
2806
+ detail: "No mushi or mushi-* server found in mcpServers. Run `mushi setup` to add one."
2807
+ });
2808
+ return checks;
2809
+ }
2810
+ checks.push({
2811
+ name: "[mcp] mushi server entry",
2812
+ ok: true,
2813
+ detail: `Found: ${mushiEntries.map(([k]) => k).join(", ")}`
2814
+ });
2815
+ let anyKeyValid = false;
2816
+ let anyEndpointSet = false;
2817
+ for (const [, srv] of mushiEntries) {
2818
+ const s = srv;
2819
+ const env = s.env ?? {};
2820
+ const key = env["MUSHI_API_KEY"] ?? "";
2821
+ const endpoint = env["MUSHI_API_ENDPOINT"] ?? "";
2822
+ if (key.startsWith("mushi_")) anyKeyValid = true;
2823
+ if (endpoint.includes("supabase.co") || endpoint.includes("localhost")) anyEndpointSet = true;
2824
+ }
2825
+ checks.push({
2826
+ name: "[mcp] MUSHI_API_KEY set",
2827
+ ok: anyKeyValid,
2828
+ detail: anyKeyValid ? "At least one mushi server has a valid mushi_* API key" : "No mushi_* API key found in any mushi server env. Re-run `mushi setup` to regenerate."
2829
+ });
2830
+ checks.push({
2831
+ name: "[mcp] MUSHI_API_ENDPOINT set",
2832
+ ok: anyEndpointSet,
2833
+ detail: anyEndpointSet ? "MUSHI_API_ENDPOINT is present and looks valid" : "MUSHI_API_ENDPOINT missing or not a Supabase URL. Re-run `mushi setup`."
2834
+ });
2835
+ if (anyKeyValid && anyEndpointSet && config.apiKey && config.endpoint) {
2836
+ try {
2837
+ const { endpoint, apiKey } = sanitizeCliCredentials(config);
2838
+ const res = await doFetch(`${endpoint}/v1/admin/mcp/account-overview`, {
2839
+ headers: apiKeyHeaders(apiKey, config.projectId),
2840
+ signal: AbortSignal.timeout(6e3)
2841
+ });
2842
+ if (res.ok) {
2843
+ const body = await res.json();
2844
+ const projectCount = body?.data?.total ?? 0;
2845
+ checks.push({
2846
+ name: "[mcp] account-overview reachable",
2847
+ ok: true,
2848
+ detail: `Key is valid; ${projectCount} accessible project${projectCount === 1 ? "" : "s"}`
2849
+ });
2850
+ } else {
2851
+ checks.push({
2852
+ name: "[mcp] account-overview reachable",
2853
+ ok: false,
2854
+ detail: `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`
2855
+ });
2856
+ }
2857
+ } catch (err) {
2858
+ const msg = err instanceof Error ? err.message : String(err);
2859
+ checks.push({
2860
+ name: "[mcp] account-overview reachable",
2861
+ ok: false,
2862
+ detail: `Probe failed: ${msg}. Check MUSHI_API_ENDPOINT and network connectivity.`
2863
+ });
2864
+ }
2865
+ }
2866
+ return checks;
2867
+ }
2903
2868
  var FIX_HINTS = {
2904
2869
  "CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
2905
2870
  "API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
2906
2871
  "Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
2907
2872
  "Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
2908
2873
  "[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
2909
- "[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix."
2874
+ "[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
2875
+ "[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
2910
2876
  };
2911
2877
  function fixHintForCheck(name) {
2912
2878
  if (FIX_HINTS[name]) return FIX_HINTS[name];
2913
2879
  if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
2914
2880
  if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
2881
+ if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
2915
2882
  return void 0;
2916
2883
  }
2917
2884
  async function runDoctor(config, options = {}) {
@@ -2941,6 +2908,10 @@ async function runDoctor(config, options = {}) {
2941
2908
  const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
2942
2909
  checks.push(...hostChecks);
2943
2910
  }
2911
+ if (options.mcp) {
2912
+ const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
2913
+ checks.push(...mcpChecks);
2914
+ }
2944
2915
  return { checks, ready: checks.every((c) => c.ok) };
2945
2916
  }
2946
2917
  function formatDoctorResult(result) {
@@ -2987,9 +2958,9 @@ async function planUpgrade(cwd) {
2987
2958
  ...pkg?.dependencies ?? {},
2988
2959
  ...pkg?.devDependencies ?? {}
2989
2960
  };
2990
- const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
2961
+ const installed = MUSHI_PACKAGES.filter((name) => deps[name]);
2991
2962
  const entries = await Promise.all(
2992
- installed2.map(async (name) => {
2963
+ installed.map(async (name) => {
2993
2964
  const current = deps[name] ?? "";
2994
2965
  const currentCore = current.replace(/^[\^~>=<]*/, "");
2995
2966
  if (!/^\d/.test(currentCore)) {
@@ -3061,8 +3032,8 @@ async function runUpgrade(opts = {}) {
3061
3032
  }
3062
3033
 
3063
3034
  // src/connect.ts
3064
- import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
3065
- import { join as join6, resolve as resolve3 } from "path";
3035
+ import { appendFile, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3036
+ import { join as join5, resolve as resolve3 } from "path";
3066
3037
  function envKeyPresent(content, key) {
3067
3038
  const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3068
3039
  return new RegExp(`^${escaped}=`, "m").test(content);
@@ -3074,7 +3045,7 @@ ${lines.join("\n")}
3074
3045
  `;
3075
3046
  let existing = null;
3076
3047
  try {
3077
- existing = await readFile2(path, "utf8");
3048
+ existing = await readFile3(path, "utf8");
3078
3049
  } catch {
3079
3050
  }
3080
3051
  if (existing !== null) {
@@ -3089,15 +3060,15 @@ ${needs.join("\n")}
3089
3060
  `, "utf8");
3090
3061
  return true;
3091
3062
  }
3092
- await writeFile(path, block, "utf8");
3063
+ await writeFile2(path, block, "utf8");
3093
3064
  return true;
3094
3065
  }
3095
3066
  async function ensureMcpJsonGitignored(cwd, messages) {
3096
- const gitignorePath = join6(cwd, ".gitignore");
3067
+ const gitignorePath = join5(cwd, ".gitignore");
3097
3068
  const patterns = [".cursor/mcp.json", ".cursor/"];
3098
3069
  let content = null;
3099
3070
  try {
3100
- content = await readFile2(gitignorePath, "utf8");
3071
+ content = await readFile3(gitignorePath, "utf8");
3101
3072
  } catch {
3102
3073
  }
3103
3074
  if (content === null) {
@@ -3128,7 +3099,7 @@ async function runConnect(opts, baseConfig = {}) {
3128
3099
  const pkg = readPackageJson(cwd);
3129
3100
  const framework = detectFramework(cwd, pkg);
3130
3101
  const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
3131
- envPath = join6(cwd, ".env.local");
3102
+ envPath = join5(cwd, ".env.local");
3132
3103
  const wrote = await mergeEnvFile(envPath, lines);
3133
3104
  messages.push(
3134
3105
  wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
@@ -3142,28 +3113,15 @@ async function runConnect(opts, baseConfig = {}) {
3142
3113
  }
3143
3114
  let mcpPath = null;
3144
3115
  if (opts.wireIde !== false) {
3145
- const mcpDir = join6(cwd, ".cursor");
3146
- mcpPath = join6(mcpDir, "mcp.json");
3147
- const serverName = `mushi-${opts.projectId.slice(0, 8)}`;
3148
- const mcpServerBlock = {
3149
- command: "npx",
3150
- args: ["-y", "@mushi-mushi/mcp@latest"],
3151
- env: {
3152
- MUSHI_API_ENDPOINT: endpoint,
3153
- MUSHI_PROJECT_ID: opts.projectId,
3154
- MUSHI_API_KEY: opts.apiKey
3155
- }
3156
- };
3157
- let merged = { mcpServers: {} };
3158
- try {
3159
- merged = JSON.parse(await readFile2(mcpPath, "utf8"));
3160
- } catch {
3161
- }
3162
- const servers = merged.mcpServers ?? {};
3163
- servers[serverName] = mcpServerBlock;
3164
- merged.mcpServers = servers;
3165
- await mkdir(mcpDir, { recursive: true });
3166
- await writeFile(mcpPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
3116
+ const mcpDir = join5(cwd, ".cursor");
3117
+ mcpPath = join5(mcpDir, "mcp.json");
3118
+ const serverName = buildMcpServerName({ projectId: opts.projectId });
3119
+ const serverBlock = buildMcpServerBlock({
3120
+ endpoint,
3121
+ projectId: opts.projectId,
3122
+ apiKey: opts.apiKey
3123
+ });
3124
+ await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
3167
3125
  await ensureMcpJsonGitignored(cwd, messages);
3168
3126
  messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
3169
3127
  }
@@ -3379,12 +3337,15 @@ Examples:
3379
3337
  ).option(
3380
3338
  "--host-app",
3381
3339
  "Verify host-app wiring: Mushi env vars, Cursor MCP config, Capacitor hybrid SDK notes."
3340
+ ).option(
3341
+ "--mcp",
3342
+ "Check Cursor MCP config: verify mushi-* server entry, credentials, and probe account-overview connectivity."
3382
3343
  ).option(
3383
3344
  "--fix",
3384
3345
  "Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
3385
3346
  ).action(async (opts) => {
3386
3347
  const config = loadConfig();
3387
- const doctorOpts = { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories, hostApp: opts.hostApp };
3348
+ const doctorOpts = { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories, hostApp: opts.hostApp, mcp: opts.mcp };
3388
3349
  let result = await runDoctor(config, doctorOpts);
3389
3350
  if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
3390
3351
  const connectResult = await runConnect({
@@ -3396,7 +3357,7 @@ Examples:
3396
3357
  wireIde: true
3397
3358
  }, config);
3398
3359
  for (const msg of connectResult.messages) console.log(msg);
3399
- result = await runDoctor(config, doctorOpts);
3360
+ result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
3400
3361
  }
3401
3362
  const { checks } = result;
3402
3363
  if (opts.json) {