@mushi-mushi/cli 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,18 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- API_TIMEOUT_MS,
4
- CONFIG_PATH,
5
- MUSHI_CLI_VERSION,
6
- apiCall,
7
- die,
8
- fmtDate,
9
- installSignalHandlers,
3
+ FRAMEWORKS,
4
+ apiKeyHeaders,
5
+ buildMcpServerBlock,
6
+ buildMcpServerName,
7
+ detectFramework,
8
+ detectPackageManager,
9
+ envVarsToWrite,
10
+ fetchIngestSetup,
11
+ installCommand,
10
12
  loadConfig,
11
- pad,
12
- requireConfig,
13
- saveConfig
14
- } from "./chunk-LKJ7KLBC.js";
13
+ readPackageJson,
14
+ runConnect,
15
+ sanitizeApiKey,
16
+ sanitizeCliCredentials,
17
+ sanitizeEndpoint,
18
+ sanitizeProjectId,
19
+ saveConfig,
20
+ writeMcpServerEntry
21
+ } from "./chunk-D5IDNBJO.js";
15
22
  import {
23
+ CLOUD_API_ENDPOINT,
16
24
  TEST_REPORT_FETCH_TIMEOUT_MS,
17
25
  assertEndpoint,
18
26
  normalizeEndpoint,
@@ -22,6 +30,88 @@ import {
22
30
  // src/index.ts
23
31
  import { Command } from "commander";
24
32
 
33
+ // src/errors.ts
34
+ var EXIT_CODE_MAP = {
35
+ E_AUTH_MISSING: 2,
36
+ E_AUTH_INVALID: 2,
37
+ E_PROJECT_MISSING: 2,
38
+ E_ENDPOINT_INVALID: 2,
39
+ E_INVALID_INPUT: 2,
40
+ E_NOT_INTERACTIVE: 2,
41
+ E_NETWORK: 3,
42
+ E_TIMEOUT: 3,
43
+ E_API_ERROR: 3,
44
+ E_RATE_LIMITED: 3,
45
+ E_FILE_NOT_FOUND: 1,
46
+ E_FILE_PERMISSION: 1,
47
+ E_FRESHNESS_STALE: 1,
48
+ E_INTERRUPTED: 130,
49
+ // 128 + SIGINT (2). POSIX convention.
50
+ E_INTERNAL: 1
51
+ };
52
+ var MushiCliError = class extends Error {
53
+ code;
54
+ hint;
55
+ cause;
56
+ constructor(code, message, hint, cause) {
57
+ super(message);
58
+ this.name = "MushiCliError";
59
+ this.code = code;
60
+ if (hint) this.hint = hint;
61
+ if (cause !== void 0) this.cause = cause;
62
+ }
63
+ /** Exit code POSIX-aware shell scripts should branch on. */
64
+ get exitCode() {
65
+ return EXIT_CODE_MAP[this.code];
66
+ }
67
+ /**
68
+ * Format for `--json` output. Used by the JSON writer when a CLI
69
+ * subcommand was invoked with `--json` and an error escapes.
70
+ */
71
+ toJSON() {
72
+ return {
73
+ error: {
74
+ code: this.code,
75
+ message: this.message,
76
+ ...this.hint ? { hint: this.hint } : {}
77
+ }
78
+ };
79
+ }
80
+ };
81
+
82
+ // src/signals.ts
83
+ var installed = false;
84
+ var activeController = null;
85
+ function installSignalHandlers() {
86
+ if (installed) return;
87
+ installed = true;
88
+ const abortAndExit = (signal) => {
89
+ activeController?.abort(
90
+ new MushiCliError(
91
+ "E_INTERRUPTED",
92
+ `aborted by ${signal}`,
93
+ "partial state on the server is safe to retry \u2014 re-run the same command"
94
+ )
95
+ );
96
+ const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
97
+ process.nextTick(() => {
98
+ process.exit(code);
99
+ });
100
+ };
101
+ process.on("SIGINT", () => abortAndExit("SIGINT"));
102
+ process.on("SIGTERM", () => abortAndExit("SIGTERM"));
103
+ }
104
+ function getAbortSignal(external) {
105
+ if (external) return external.signal;
106
+ if (!activeController || activeController.signal.aborted) {
107
+ activeController = new AbortController();
108
+ }
109
+ return activeController.signal;
110
+ }
111
+
112
+ // src/version.ts
113
+ var MUSHI_CLI_VERSION = true ? "0.19.0" : "0.0.0-dev";
114
+
25
115
  // src/init.ts
26
116
  import * as p from "@clack/prompts";
27
117
  import { spawn } from "child_process";
@@ -29,342 +119,214 @@ import { randomUUID } from "crypto";
29
119
  import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
30
120
  import { join as join3 } from "path";
31
121
 
32
- // src/detect.ts
33
- import { readFileSync, existsSync } from "fs";
34
- import { join } from "path";
35
- var FRAMEWORKS = {
36
- next: {
37
- id: "next",
38
- label: "Next.js",
39
- packageName: "@mushi-mushi/react",
40
- needsWebPackage: false,
41
- snippet: () => `// app/providers.tsx (or pages/_app.tsx for /pages router)
42
- 'use client'
43
- import { MushiProvider } from '@mushi-mushi/react'
44
-
45
- export function Providers({ children }: { children: React.ReactNode }) {
46
- return (
47
- <MushiProvider config={{
48
- projectId: process.env.NEXT_PUBLIC_MUSHI_PROJECT_ID!,
49
- apiKey: process.env.NEXT_PUBLIC_MUSHI_API_KEY!,
50
- }}>
51
- {children}
52
- </MushiProvider>
53
- )
54
- }`
55
- },
56
- react: {
57
- id: "react",
58
- label: "React",
59
- packageName: "@mushi-mushi/react",
60
- needsWebPackage: false,
61
- snippet: () => `// src/main.tsx
62
- import { MushiProvider } from '@mushi-mushi/react'
63
-
64
- createRoot(document.getElementById('root')!).render(
65
- <MushiProvider config={{
66
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
67
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
68
- }}>
69
- <App />
70
- </MushiProvider>
71
- )`
72
- },
73
- vue: {
74
- id: "vue",
75
- label: "Vue 3",
76
- packageName: "@mushi-mushi/vue",
77
- needsWebPackage: false,
78
- snippet: () => `// src/main.ts
79
- import { MushiPlugin } from '@mushi-mushi/vue'
80
-
81
- app.use(MushiPlugin, {
82
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
83
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
84
- })`
85
- },
86
- nuxt: {
87
- id: "nuxt",
88
- label: "Nuxt",
89
- packageName: "@mushi-mushi/vue",
90
- needsWebPackage: false,
91
- snippet: () => `// plugins/mushi.client.ts
92
- import { MushiPlugin } from '@mushi-mushi/vue'
93
-
94
- export default defineNuxtPlugin((nuxtApp) => {
95
- nuxtApp.vueApp.use(MushiPlugin, {
96
- projectId: import.meta.env.NUXT_PUBLIC_MUSHI_PROJECT_ID,
97
- apiKey: import.meta.env.NUXT_PUBLIC_MUSHI_API_KEY,
98
- })
99
- })`
100
- },
101
- svelte: {
102
- id: "svelte",
103
- label: "Svelte",
104
- packageName: "@mushi-mushi/svelte",
105
- needsWebPackage: false,
106
- snippet: () => `// src/main.ts
107
- import { initMushi } from '@mushi-mushi/svelte'
108
-
109
- initMushi({
110
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
111
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
112
- })`
113
- },
114
- sveltekit: {
115
- id: "sveltekit",
116
- label: "SvelteKit",
117
- packageName: "@mushi-mushi/svelte",
118
- needsWebPackage: false,
119
- snippet: () => `// src/routes/+layout.svelte
120
- <script>
121
- import { onMount } from 'svelte'
122
-
123
- onMount(async () => {
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
- })
129
- })
130
- </script>`
131
- },
132
- angular: {
133
- id: "angular",
134
- label: "Angular",
135
- packageName: "@mushi-mushi/angular",
136
- needsWebPackage: false,
137
- snippet: () => `// src/main.ts
138
- import { provideMushi } from '@mushi-mushi/angular'
139
-
140
- bootstrapApplication(AppComponent, {
141
- providers: [
142
- provideMushi({
143
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
144
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
145
- }),
146
- ],
147
- })`
148
- },
149
- expo: {
150
- id: "expo",
151
- label: "Expo",
152
- packageName: "@mushi-mushi/react-native",
153
- needsWebPackage: false,
154
- snippet: () => `// App.tsx
155
- import { MushiProvider } from '@mushi-mushi/react-native'
156
-
157
- export default function App() {
158
- return (
159
- <MushiProvider
160
- projectId={process.env.EXPO_PUBLIC_MUSHI_PROJECT_ID!}
161
- apiKey={process.env.EXPO_PUBLIC_MUSHI_API_KEY!}
162
- >
163
- <YourApp />
164
- </MushiProvider>
165
- )
166
- }`
167
- },
168
- "react-native": {
169
- id: "react-native",
170
- label: "React Native",
171
- packageName: "@mushi-mushi/react-native",
172
- needsWebPackage: false,
173
- snippet: () => `// App.tsx
174
- import { MushiProvider } from '@mushi-mushi/react-native'
175
-
176
- export default function App() {
177
- return (
178
- <MushiProvider
179
- projectId={process.env.MUSHI_PROJECT_ID!}
180
- apiKey={process.env.MUSHI_API_KEY!}
181
- >
182
- <YourApp />
183
- </MushiProvider>
184
- )
185
- }`
186
- },
187
- capacitor: {
188
- id: "capacitor",
189
- label: "Capacitor (Ionic)",
190
- packageName: "@mushi-mushi/capacitor",
191
- needsWebPackage: false,
192
- /* The Capacitor plugin's public API is `Mushi.configure(...)`, not
193
- * `Mushi.init(...)` — see packages/capacitor/src/definitions.ts. We
194
- * shipped the wrong call here for two releases and users got a runtime
195
- * `TypeError: Mushi.init is not a function`. The accompanying admin
196
- * console snippet (apps/admin/src/lib/sdkSnippets.ts) emits the same
197
- * `Mushi.configure(...)` shape; both are pinned by tests. */
198
- snippet: () => `// src/main.ts \u2014 after install, run \`npx cap sync\`
199
- import { Mushi } from '@mushi-mushi/capacitor'
200
-
201
- await Mushi.configure({
202
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
203
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
204
- })`
205
- },
206
- express: {
207
- id: "express",
208
- label: "Express",
209
- packageName: "@mushi-mushi/node",
210
- needsWebPackage: false,
211
- snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
212
- import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
213
- import { mushiExpressErrorHandler } from '@mushi-mushi/node/express'
214
- import type { Express } from 'express'
215
-
216
- export const mushi = new MushiNodeClient({
217
- projectId: process.env.MUSHI_PROJECT_ID!,
218
- apiKey: process.env.MUSHI_API_KEY!,
219
- environment: process.env.NODE_ENV ?? 'production',
220
- })
221
- attachUnhandledHook({ client: mushi })
222
-
223
- export function attachMushi(app: Express) {
224
- app.use(mushiExpressErrorHandler({ client: mushi }))
225
- }`
226
- },
227
- fastify: {
228
- id: "fastify",
229
- label: "Fastify",
230
- packageName: "@mushi-mushi/node",
231
- needsWebPackage: false,
232
- snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
233
- import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
234
- import { mushiFastifyPlugin } from '@mushi-mushi/node/fastify'
235
- import Fastify from 'fastify'
236
-
237
- export const mushi = new MushiNodeClient({
238
- projectId: process.env.MUSHI_PROJECT_ID!,
239
- apiKey: process.env.MUSHI_API_KEY!,
240
- environment: process.env.NODE_ENV ?? 'production',
241
- })
242
- attachUnhandledHook({ client: mushi })
243
-
244
- const app = Fastify()
245
- mushiFastifyPlugin(app, { client: mushi })`
246
- },
247
- hono: {
248
- id: "hono",
249
- label: "Hono",
250
- packageName: "@mushi-mushi/node",
251
- needsWebPackage: false,
252
- snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
253
- import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
254
- import { mushiHonoErrorHandler } from '@mushi-mushi/node/hono'
255
- import { Hono } from 'hono'
256
-
257
- export const mushi = new MushiNodeClient({
258
- projectId: process.env.MUSHI_PROJECT_ID!,
259
- apiKey: process.env.MUSHI_API_KEY!,
260
- environment: process.env.NODE_ENV ?? 'production',
261
- })
262
- attachUnhandledHook({ client: mushi })
263
-
264
- const app = new Hono()
265
- app.onError(
266
- mushiHonoErrorHandler({ client: mushi }, (err, c) =>
267
- c.text('Internal Server Error', 500),
268
- ),
269
- )`
270
- },
271
- vanilla: {
272
- id: "vanilla",
273
- label: "Vanilla JS / unknown",
274
- packageName: "@mushi-mushi/web",
275
- needsWebPackage: false,
276
- snippet: () => `// Anywhere in your client bundle
277
- import { Mushi } from '@mushi-mushi/web'
278
-
279
- Mushi.init({
280
- projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
281
- apiKey: import.meta.env.VITE_MUSHI_API_KEY,
282
- })`
122
+ // src/console-url.ts
123
+ import { existsSync, readFileSync } from "fs";
124
+ import { dirname, join } from "path";
125
+ var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
126
+ var LOCAL_ADMIN_URL = "http://localhost:6464";
127
+ var LOCAL_PROBE_MS = 800;
128
+ function normalizeConsoleBase(url) {
129
+ return url.trim().replace(/\/+$/, "");
130
+ }
131
+ function consoleUrl(base, route) {
132
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
133
+ return `${normalizeConsoleBase(base)}${normalizedRoute}`;
134
+ }
135
+ function resolveConsoleUrlFromEnv() {
136
+ const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
137
+ return raw ? normalizeConsoleBase(raw) : void 0;
138
+ }
139
+ function isInsideMushiMonorepo(cwd) {
140
+ let dir = cwd;
141
+ for (let depth = 0; depth < 14; depth += 1) {
142
+ const pkgPath = join(dir, "package.json");
143
+ if (existsSync(pkgPath)) {
144
+ try {
145
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
146
+ if (pkg.name === "mushi-mushi") return true;
147
+ } catch {
148
+ }
149
+ }
150
+ const parent = dirname(dir);
151
+ if (parent === dir) break;
152
+ dir = parent;
283
153
  }
284
- };
285
- function readPackageJson(cwd) {
286
- const path = join(cwd, "package.json");
287
- if (!existsSync(path)) return null;
154
+ return false;
155
+ }
156
+ async function probeLocalAdminConsole() {
288
157
  try {
289
- return JSON.parse(readFileSync(path, "utf-8"));
158
+ const controller = new AbortController();
159
+ const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
160
+ const res = await fetch(LOCAL_ADMIN_URL, {
161
+ signal: controller.signal,
162
+ method: "HEAD"
163
+ });
164
+ clearTimeout(timer);
165
+ if (res.ok || res.status === 404 || res.status === 401) {
166
+ return LOCAL_ADMIN_URL;
167
+ }
290
168
  } catch {
291
- return null;
292
169
  }
170
+ return null;
171
+ }
172
+ function resolveConsoleUrlSync(cwd = process.cwd()) {
173
+ const fromEnv = resolveConsoleUrlFromEnv();
174
+ if (fromEnv) return fromEnv;
175
+ const config = loadConfig();
176
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
177
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
178
+ return HOSTED_CONSOLE_BASE;
179
+ }
180
+ async function resolveConsoleUrl(options) {
181
+ const fromEnv = resolveConsoleUrlFromEnv();
182
+ if (fromEnv) return fromEnv;
183
+ const config = loadConfig();
184
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
185
+ const local = await probeLocalAdminConsole();
186
+ if (local) return local;
187
+ const cwd = options?.cwd ?? process.cwd();
188
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
189
+ return HOSTED_CONSOLE_BASE;
190
+ }
191
+ function projectIdHint(base) {
192
+ return `Where to find it: ${consoleUrl(base, "/projects")} \u2192 select your project \u2192 click the UUID chip to copy (or copy it from the panel right after you create the project).`;
193
+ }
194
+ function apiKeyHint(base) {
195
+ return `Where to find it: ${consoleUrl(base, "/onboarding?tab=verify")} \u2192 Generate API key (SDK ingest needs report:write \u2014 not Settings \u2192 BYOK keys). Treat it like a password \u2014 env file only, never commit.`;
293
196
  }
294
- function detectFramework(cwd, pkg) {
295
- const deps = collectDeps(pkg);
296
- if (deps.has("next")) return FRAMEWORKS.next;
297
- if (deps.has("nuxt")) return FRAMEWORKS.nuxt;
298
- if (deps.has("@sveltejs/kit")) return FRAMEWORKS.sveltekit;
299
- if (deps.has("@angular/core")) return FRAMEWORKS.angular;
300
- if (deps.has("expo")) return FRAMEWORKS.expo;
301
- if (deps.has("react-native")) return FRAMEWORKS["react-native"];
302
- if (deps.has("@capacitor/core") && deps.has("react")) return FRAMEWORKS.react;
303
- if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
304
- if (deps.has("svelte")) return FRAMEWORKS.svelte;
305
- if (deps.has("vue")) return FRAMEWORKS.vue;
306
- if (deps.has("react")) return FRAMEWORKS.react;
307
- if (deps.has("express")) return FRAMEWORKS.express;
308
- if (deps.has("fastify")) return FRAMEWORKS.fastify;
309
- if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
310
- if (existsSync(join(cwd, "next.config.js")) || existsSync(join(cwd, "next.config.ts"))) {
311
- return FRAMEWORKS.next;
197
+ function cliSetupDeepLink(base) {
198
+ return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
199
+ }
200
+ function reportsUrl(base, reportId) {
201
+ return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
202
+ }
203
+ async function openInBrowser(url) {
204
+ const { exec } = await import("child_process");
205
+ const openCmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
206
+ await new Promise((resolve3) => {
207
+ exec(openCmd, () => resolve3());
208
+ });
209
+ }
210
+
211
+ // src/cli-shared.ts
212
+ var API_TIMEOUT_MS = 15e3;
213
+ async function apiCall(path, config, options = {}) {
214
+ const endpoint = config.endpoint;
215
+ if (!endpoint) {
216
+ return {
217
+ ok: false,
218
+ error: {
219
+ code: "NO_ENDPOINT",
220
+ 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"
221
+ }
222
+ };
312
223
  }
313
- if (existsSync(join(cwd, "nuxt.config.ts")) || existsSync(join(cwd, "nuxt.config.js"))) {
314
- return FRAMEWORKS.nuxt;
224
+ const controller = new AbortController();
225
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
226
+ const signals = [controller.signal, getAbortSignal()];
227
+ const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
228
+ try {
229
+ const res = await fetch(`${endpoint}${path}`, {
230
+ ...options,
231
+ signal: compositeSignal,
232
+ headers: {
233
+ "Content-Type": "application/json",
234
+ // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
235
+ // fallback accepted by some older middleware. Both are sent.
236
+ "Authorization": `Bearer ${config.apiKey ?? ""}`,
237
+ "X-Mushi-Api-Key": config.apiKey ?? "",
238
+ "X-Mushi-Project": config.projectId ?? "",
239
+ "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
240
+ ...options.headers
241
+ }
242
+ });
243
+ clearTimeout(timer);
244
+ let body;
245
+ const contentType = res.headers.get("content-type") ?? "";
246
+ if (contentType.includes("application/json")) {
247
+ try {
248
+ body = await res.json();
249
+ } catch {
250
+ body = null;
251
+ }
252
+ } else {
253
+ const text2 = await res.text();
254
+ try {
255
+ body = JSON.parse(text2);
256
+ } catch {
257
+ body = {
258
+ ok: false,
259
+ error: {
260
+ code: `HTTP_${res.status}`,
261
+ message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
262
+ }
263
+ };
264
+ }
265
+ }
266
+ if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
267
+ const b = body;
268
+ return {
269
+ ok: false,
270
+ httpStatus: res.status,
271
+ error: {
272
+ code: b["code"] ?? `HTTP_${res.status}`,
273
+ message: b["message"] ?? `Request failed (${res.status})`
274
+ }
275
+ };
276
+ }
277
+ return body;
278
+ } catch (err) {
279
+ clearTimeout(timer);
280
+ if (err instanceof Error && err.name === "AbortError") {
281
+ return {
282
+ ok: false,
283
+ error: {
284
+ code: "TIMEOUT",
285
+ message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
286
+ }
287
+ };
288
+ }
289
+ return {
290
+ ok: false,
291
+ error: {
292
+ code: "NETWORK_ERROR",
293
+ message: err instanceof Error ? err.message : String(err)
294
+ }
295
+ };
315
296
  }
316
- if (existsSync(join(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
317
- if (existsSync(join(cwd, "angular.json"))) return FRAMEWORKS.angular;
318
- return FRAMEWORKS.vanilla;
319
297
  }
320
- function detectPackageManager(cwd) {
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";
325
- const userAgent = process.env.npm_config_user_agent ?? "";
326
- if (userAgent.startsWith("bun")) return "bun";
327
- if (userAgent.startsWith("pnpm")) return "pnpm";
328
- if (userAgent.startsWith("yarn")) return "yarn";
329
- return "npm";
330
- }
331
- function installCommand(pm, packages) {
332
- const verb = pm === "npm" ? "install" : "add";
333
- return `${pm} ${verb} ${packages.join(" ")}`;
298
+ function die(result, exitCode = 1) {
299
+ const { code, message } = result.error;
300
+ const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
301
+ process.stderr.write(`error${status}: ${code} \u2014 ${message}
302
+ `);
303
+ process.exit(exitCode);
334
304
  }
335
- var SERVER_FRAMEWORK_IDS = /* @__PURE__ */ new Set(["express", "fastify", "hono"]);
336
- function envVarsToWrite(apiKey, projectId, framework) {
337
- if (SERVER_FRAMEWORK_IDS.has(framework.id)) {
338
- return [
339
- `MUSHI_PROJECT_ID=${projectId}`,
340
- `MUSHI_API_KEY=${apiKey}`
341
- ].join("\n");
305
+ function requireConfig(opts = {}) {
306
+ const config = loadConfig();
307
+ if (!config.apiKey) {
308
+ process.stderr.write(
309
+ "error: API key not configured.\n Run: mushi login --api-key <key>\n Or: export MUSHI_API_KEY=<key>\n"
310
+ );
311
+ process.exit(2);
342
312
  }
343
- if (framework.id === "react-native") {
344
- return [
345
- `MUSHI_PROJECT_ID=${projectId}`,
346
- `MUSHI_API_KEY=${apiKey}`
347
- ].join("\n");
313
+ if (!config.endpoint) {
314
+ config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
348
315
  }
349
- if (framework.id === "expo") {
350
- return [
351
- `EXPO_PUBLIC_MUSHI_PROJECT_ID=${projectId}`,
352
- `EXPO_PUBLIC_MUSHI_API_KEY=${apiKey}`
353
- ].join("\n");
316
+ if (opts.needsProject && !config.projectId) {
317
+ process.stderr.write(
318
+ "error: Project ID not configured.\n Run: mushi login --project-id <uuid>\n Or: export MUSHI_PROJECT_ID=<uuid>\n Find your project ID: " + consoleUrl(resolveConsoleUrlSync(), "/projects") + "\n"
319
+ );
320
+ process.exit(2);
354
321
  }
355
- const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
356
- return [
357
- `${prefix}MUSHI_PROJECT_ID=${projectId}`,
358
- `${prefix}MUSHI_API_KEY=${apiKey}`
359
- ].join("\n");
322
+ return config;
360
323
  }
361
- function collectDeps(pkg) {
362
- if (!pkg) return /* @__PURE__ */ new Set();
363
- return /* @__PURE__ */ new Set([
364
- ...Object.keys(pkg.dependencies ?? {}),
365
- ...Object.keys(pkg.devDependencies ?? {}),
366
- ...Object.keys(pkg.peerDependencies ?? {})
367
- ]);
324
+ function fmtDate(iso) {
325
+ if (!iso) return "\u2014";
326
+ return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
327
+ }
328
+ function pad(s, width) {
329
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
368
330
  }
369
331
 
370
332
  // src/freshness.ts
@@ -428,7 +390,7 @@ function parse(version) {
428
390
 
429
391
  // src/monorepo.ts
430
392
  import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
431
- import { dirname, join as join2, resolve } from "path";
393
+ import { dirname as dirname2, join as join2, resolve } from "path";
432
394
  var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
433
395
  var FRAMEWORK_DEPS = {
434
396
  next: "Next.js",
@@ -456,7 +418,7 @@ function findWorkspaceRoot(start) {
456
418
  let dir = resolve(start);
457
419
  for (let i = 0; i < 8; i++) {
458
420
  if (isWorkspaceRoot(dir)) return dir;
459
- const parent = dirname(dir);
421
+ const parent = dirname2(dir);
460
422
  if (parent === dir) break;
461
423
  dir = parent;
462
424
  }
@@ -547,7 +509,10 @@ async function runInit(options = {}) {
547
509
  }
548
510
  const detected = detectFramework(cwd, pkg);
549
511
  const framework = await chooseFramework(detected, options);
550
- const credentials = await collectCredentials(options);
512
+ const consoleBase = await resolveConsoleUrl({ cwd });
513
+ await maybeGuideConsoleSetup(options, consoleBase);
514
+ const credentials = await collectCredentials(options, consoleBase);
515
+ await verifyCredentials(credentials, options, consoleBase);
551
516
  const pm = detectPackageManager(cwd);
552
517
  const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
553
518
  if (!options.skipInstall) {
@@ -559,8 +524,9 @@ async function runInit(options = {}) {
559
524
  writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
560
525
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
561
526
  const enableRewards = await maybeEnableRewards(options);
562
- printNextSteps(framework, credentials.apiKey, credentials.projectId, enableRewards);
563
- await maybeSendTestReport(credentials, { ...options, endpoint });
527
+ printNextSteps(framework, consoleBase, enableRewards);
528
+ await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
529
+ await maybeOfferConnect(credentials, options, consoleBase);
564
530
  p.outro("Setup complete. Happy bug squashing \u{1F41B}");
565
531
  }
566
532
  function ensureInteractiveOrBailOut(options) {
@@ -601,25 +567,93 @@ async function chooseFramework(detected, options) {
601
567
  }
602
568
  return FRAMEWORKS[confirmed];
603
569
  }
604
- async function collectCredentials(options) {
570
+ async function maybeGuideConsoleSetup(options, consoleBase) {
571
+ if (options.yes) return;
572
+ if (options.projectId && options.apiKey) return;
605
573
  const existing = loadConfig();
574
+ if (existing.projectId && existing.apiKey) return;
575
+ const choice = await p.select({
576
+ message: "Do you already have a Mushi project?",
577
+ options: [
578
+ {
579
+ value: "create",
580
+ label: "No \u2014 open the console to create one",
581
+ hint: "Recommended for first-time setup"
582
+ },
583
+ {
584
+ value: "have",
585
+ label: "Yes \u2014 I have Project ID + API key",
586
+ hint: "Paste values from the admin console"
587
+ },
588
+ {
589
+ value: "login",
590
+ label: "Use mushi login first",
591
+ hint: "Run `mushi login` in another terminal, then re-run this wizard"
592
+ }
593
+ ]
594
+ });
595
+ if (p.isCancel(choice)) {
596
+ p.cancel("Aborted.");
597
+ process.exit(0);
598
+ }
599
+ if (choice === "login") {
600
+ p.log.info("Run `mushi login` to save credentials, then re-run `mushi init`.");
601
+ p.cancel("Aborted.");
602
+ process.exit(0);
603
+ }
604
+ if (choice !== "create") return;
605
+ const setupUrl = cliSetupDeepLink(consoleBase);
606
+ p.log.info(`Opening the Mushi console:
607
+ ${setupUrl}`);
608
+ await openInBrowser(setupUrl);
609
+ p.log.message("In the console:");
610
+ p.log.message(" 1. Enter a project name and click Create");
611
+ p.log.message(" 2. Copy the Project ID from the success panel");
612
+ p.log.message(" 3. Generate an API key (Verify tab \u2014 report:write scope)");
613
+ p.log.message(" 4. Return here and paste both values");
614
+ const ready = await p.confirm({
615
+ message: "Created your project and ready to paste credentials?",
616
+ initialValue: false
617
+ });
618
+ if (p.isCancel(ready) || !ready) {
619
+ p.log.warn(`Finish in the console first: ${setupUrl}`);
620
+ }
621
+ }
622
+ async function collectCredentials(options, consoleBase) {
623
+ const existing = loadConfig();
624
+ if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
625
+ const reuse = options.yes ? true : await p.confirm({
626
+ message: "Use Project ID + API key from ~/.config/mushi/config.json?",
627
+ initialValue: true
628
+ });
629
+ if (p.isCancel(reuse)) {
630
+ p.cancel("Aborted.");
631
+ process.exit(0);
632
+ }
633
+ if (reuse) {
634
+ return {
635
+ projectId: sanitizeSecret(existing.projectId),
636
+ apiKey: sanitizeSecret(existing.apiKey)
637
+ };
638
+ }
639
+ }
606
640
  const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
607
641
  message: "Project ID",
608
642
  placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
609
- hint: "Where to find it: https://kensaur.us/mushi-mushi/projects \u2192 click your project \u2192 copy the UUID below the project name.",
610
- validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) \u2014 copy it from the Mushi admin console Projects page."
643
+ hint: projectIdHint(consoleBase),
644
+ validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID \u2014 copy it from the Projects page or the panel right after you create a project."
611
645
  });
612
646
  const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
613
647
  message: "API key",
614
648
  placeholder: "mushi_xxxxxxxxxxxx",
615
- hint: "Where to find it: https://kensaur.us/mushi-mushi/settings \u2192 API Keys tab. Treat it like a password \u2014 env file only, never commit it.",
649
+ hint: apiKeyHint(consoleBase),
616
650
  validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
617
651
  });
618
652
  const projectId = sanitizeSecret(rawProjectId);
619
653
  const apiKey = sanitizeSecret(rawApiKey);
620
654
  if (!PROJECT_ID_PATTERN.test(projectId)) {
621
655
  throw new Error(
622
- `Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId)} \u2014 copy it from the Projects page in the Mushi console at https://kensaur.us/mushi-mushi/projects`
656
+ `Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId)} \u2014 copy it from ${projectIdHint(consoleBase)}`
623
657
  );
624
658
  }
625
659
  if (!API_KEY_PATTERN.test(apiKey)) {
@@ -629,6 +663,23 @@ async function collectCredentials(options) {
629
663
  }
630
664
  return { projectId, apiKey };
631
665
  }
666
+ async function verifyCredentials(credentials, options, consoleBase) {
667
+ const endpoint = resolveCloudEndpoint(options.endpoint);
668
+ const spinner3 = p.spinner();
669
+ spinner3.start("Verifying credentials\u2026");
670
+ const result = await apiCall(
671
+ "/v1/sync/whoami",
672
+ { apiKey: credentials.apiKey, projectId: credentials.projectId, endpoint }
673
+ );
674
+ if (!result.ok) {
675
+ spinner3.stop("Credentials could not be verified.");
676
+ p.log.error(result.error?.message ?? "Authentication failed.");
677
+ p.log.info(`Double-check values in the console:
678
+ ${cliSetupDeepLink(consoleBase)}`);
679
+ throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
680
+ }
681
+ spinner3.stop(`Connected to ${result.data.project_name}`);
682
+ }
632
683
  function sanitizeSecret(raw) {
633
684
  return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[\r\n\0]/g, "");
634
685
  }
@@ -671,7 +722,7 @@ async function installPackages(pm, packages, cwd) {
671
722
  function runCommand(pm, packages, cwd) {
672
723
  const verb = pm === "npm" ? "install" : "add";
673
724
  const command = process.platform === "win32" ? `${pm}.cmd` : pm;
674
- return new Promise((resolve4, reject) => {
725
+ return new Promise((resolve3, reject) => {
675
726
  const child = spawn(command, [verb, ...packages], {
676
727
  stdio: "inherit",
677
728
  shell: false,
@@ -680,7 +731,7 @@ function runCommand(pm, packages, cwd) {
680
731
  });
681
732
  child.on("error", reject);
682
733
  child.on("exit", (code) => {
683
- if (code === 0) resolve4();
734
+ if (code === 0) resolve3();
684
735
  else reject(new Error(`${pm} exited with code ${code ?? "null"}`));
685
736
  });
686
737
  });
@@ -738,7 +789,7 @@ function persistCliConfig(apiKey, projectId, endpoint) {
738
789
  const existing = loadConfig();
739
790
  saveConfig({ ...existing, apiKey, projectId, endpoint });
740
791
  }
741
- function printNextSteps(framework, _apiKey, _projectId, enableRewards = false) {
792
+ function printNextSteps(framework, consoleBase, enableRewards = false) {
742
793
  p.note(framework.snippet(), "Add this to your app:");
743
794
  if (enableRewards) {
744
795
  const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
@@ -749,13 +800,44 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
749
800
  // import { MushiRewardsBadge } from '@mushi-mushi/react';
750
801
  // <MushiRewardsBadge showPoints />`;
751
802
  p.note(badgeSnippet, "Rewards badge snippet:");
752
- p.log.info("Enable rewards in your project settings at https://kensaur.us/mushi-mushi/rewards");
803
+ p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
753
804
  p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
754
805
  }
755
806
  p.log.message("Verify the install:");
756
807
  p.log.message(" \u2022 Start your dev server");
757
808
  p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
758
- p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
809
+ p.log.message(` \u2022 Submit a test report \u2014 it should appear at ${reportsUrl(consoleBase)}`);
810
+ p.log.info(
811
+ "Tip: `mushi connect --write-env --wire-ide --wait` also wires Cursor MCP and waits for SDK heartbeat."
812
+ );
813
+ }
814
+ async function maybeOfferConnect(credentials, options, _consoleBase) {
815
+ if (options.yes) return;
816
+ const answer = await p.confirm({
817
+ message: "Run `mushi connect --write-env --wire-ide --wait` now? (SDK env + Cursor MCP + heartbeat check)",
818
+ initialValue: false
819
+ });
820
+ if (p.isCancel(answer) || !answer) return;
821
+ const endpoint = resolveCloudEndpoint(options.endpoint);
822
+ try {
823
+ const { runConnect: runConnect2 } = await import("./connect-W2Q6N7K3.js");
824
+ await runConnect2({
825
+ apiKey: credentials.apiKey,
826
+ projectId: credentials.projectId,
827
+ endpoint,
828
+ cwd: options.cwd,
829
+ writeEnv: true,
830
+ wireIde: true,
831
+ wait: true
832
+ });
833
+ } catch (err) {
834
+ p.log.warn(
835
+ err instanceof Error ? err.message : String(err)
836
+ );
837
+ p.log.info(
838
+ "You can run manually: mushi connect --write-env --wire-ide --wait"
839
+ );
840
+ }
759
841
  }
760
842
  async function maybeEnableRewards(options) {
761
843
  if (options.yes) return false;
@@ -826,8 +908,8 @@ async function maybeSendTestReport(credentials, options) {
826
908
  reportId = body.data?.reportId;
827
909
  } catch {
828
910
  }
829
- const reportPath = reportId ? `/reports/${reportId}` : "/reports";
830
- p.log.success(`View it at https://kensaur.us/mushi-mushi/admin${reportPath}`);
911
+ const consoleBase = options.consoleBase ?? resolveConsoleUrlSync(options.cwd);
912
+ p.log.success(`View it at ${reportsUrl(consoleBase, reportId)}`);
831
913
  } catch (err) {
832
914
  const aborted = err instanceof Error && err.name === "AbortError";
833
915
  spinner3.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
@@ -1023,78 +1105,224 @@ function registerAccountCommands(program2) {
1023
1105
  const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
1024
1106
  if (matches.length === 0) process.exit(1);
1025
1107
  });
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", `
1108
+ program2.command("login").description("Authenticate the CLI via browser (zero copy-paste), or pass --api-key to skip").option("--api-key <key>", "API key (mushi_...) \u2014 non-interactive / CI path").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--project-id <id>", "Project UUID \u2014 skip the project picker").option("--no-browser", "Print the verification URL instead of opening the browser").addHelpText("after", `
1027
1109
  Examples:
1028
- mushi login # browser-guided: open console \u2192 paste key
1029
- mushi login --api-key mushi_xxx # non-interactive: save key directly
1110
+ mushi login # browser-guided (recommended)
1111
+ mushi login --api-key mushi_xxx # non-interactive / CI
1030
1112
  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";
1113
+ const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1114
+ const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
1115
+ const consoleBase = await resolveConsoleUrl();
1035
1116
  let apiKey = opts.apiKey;
1036
1117
  let projectId = opts.projectId;
1037
- if (!apiKey) {
1038
- const signInUrl = "https://kensaur.us/mushi-mushi/sign-in";
1118
+ if (apiKey) {
1119
+ const config2 = loadConfig();
1120
+ config2.apiKey = apiKey;
1121
+ config2.endpoint = endpoint;
1122
+ if (projectId) config2.projectId = projectId;
1123
+ config2.consoleUrl = consoleBase;
1124
+ saveConfig(config2);
1039
1125
  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 {
1126
+ console.log(" \u2713 Credentials saved.");
1127
+ const verifyResult = await apiCall(
1128
+ "/v1/sync/whoami",
1129
+ { apiKey, endpoint, projectId }
1130
+ );
1131
+ if (!verifyResult.ok) {
1132
+ console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
1133
+ console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
1134
+ return;
1135
+ }
1136
+ const d = verifyResult.data;
1137
+ if (d.project_name) {
1138
+ if (!projectId && d.project_id) {
1139
+ config2.projectId = d.project_id;
1140
+ saveConfig(config2);
1049
1141
  }
1050
- } else {
1051
- console.log(` Sign in at: ${signInUrl}`);
1142
+ console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
1052
1143
  }
1144
+ console.log(` \u2713 Endpoint: ${endpoint}`);
1053
1145
  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.");
1146
+ console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1147
+ return;
1148
+ }
1149
+ console.log("");
1150
+ console.log(" Mushi login");
1151
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1152
+ let deviceData;
1153
+ try {
1154
+ const res = await fetch(`${endpoint}/v1/cli/auth/device/start`, {
1155
+ method: "POST",
1156
+ headers: { "Content-Type": "application/json" }
1157
+ });
1158
+ const json = await res.json();
1159
+ if (!res.ok || !json.ok || !json.data) {
1160
+ throw new Error(json.error?.message ?? `HTTP ${res.status}`);
1161
+ }
1162
+ deviceData = json.data;
1163
+ } catch (err) {
1164
+ process.stderr.write(`
1165
+ error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
1166
+ `);
1167
+ process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
1168
+ `);
1169
+ process.exit(1);
1170
+ }
1171
+ const verifyUrl = deviceData.verification_uri;
1172
+ console.log("");
1173
+ console.log(` Your confirmation code: ${deviceData.user_code}`);
1174
+ console.log("");
1175
+ if (opts.browser !== false) {
1176
+ console.log(" Opening the Mushi console in your browser\u2026");
1177
+ try {
1178
+ await openInBrowser(verifyUrl);
1179
+ } catch {
1180
+ }
1181
+ }
1182
+ console.log(` If the browser didn't open: ${verifyUrl}`);
1183
+ console.log("");
1184
+ console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
1185
+ let cliToken = null;
1186
+ const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
1187
+ const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
1188
+ while (Date.now() < deadline) {
1189
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1190
+ try {
1191
+ const pollRes = await fetch(`${endpoint}/v1/cli/auth/device/token`, {
1192
+ method: "POST",
1193
+ headers: { "Content-Type": "application/json" },
1194
+ body: JSON.stringify({ device_code: deviceData.device_code })
1195
+ });
1196
+ const pollJson = await pollRes.json();
1197
+ if (pollRes.ok && pollJson.ok && pollJson.data?.cli_token) {
1198
+ cliToken = pollJson.data.cli_token;
1199
+ break;
1200
+ }
1201
+ const errCode = pollJson.error ?? "";
1202
+ if (errCode === "authorization_pending") {
1203
+ process.stdout.write(".");
1204
+ continue;
1205
+ }
1206
+ if (errCode === "access_denied") {
1207
+ console.log("");
1208
+ process.stderr.write("\nerror: Login denied in the browser.\n");
1209
+ process.exit(1);
1210
+ }
1211
+ if (errCode === "expired_token") {
1212
+ console.log("");
1213
+ process.stderr.write("\nerror: Login code expired. Run mushi login again.\n");
1214
+ process.exit(1);
1215
+ }
1216
+ throw new Error(pollJson.error_description ?? errCode ?? `HTTP ${pollRes.status}`);
1217
+ } catch (err) {
1218
+ console.log("");
1219
+ process.stderr.write(`
1220
+ error: Poll failed: ${err instanceof Error ? err.message : String(err)}
1221
+ `);
1222
+ process.exit(1);
1223
+ }
1224
+ }
1225
+ if (!cliToken) {
1056
1226
  console.log("");
1227
+ process.stderr.write("\nerror: Login timed out. Run mushi login again.\n");
1228
+ process.exit(1);
1229
+ }
1230
+ console.log("");
1231
+ console.log(" \u2713 Approved!");
1232
+ const cliHeaders = {
1233
+ "Content-Type": "application/json",
1234
+ "Authorization": `Bearer ${cliToken}`
1235
+ };
1236
+ let projectsList = [];
1237
+ try {
1238
+ const listRes = await fetch(`${endpoint}/v1/cli/projects`, { headers: cliHeaders });
1239
+ const listJson = await listRes.json();
1240
+ if (listRes.ok && listJson.ok) projectsList = listJson.data?.projects ?? [];
1241
+ } catch {
1242
+ }
1243
+ let chosenProjectId = projectId;
1244
+ let chosenProjectName;
1245
+ if (!chosenProjectId) {
1057
1246
  const { createInterface } = await import("readline");
1058
1247
  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;
1248
+ const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
1249
+ if (projectsList.length > 0) {
1250
+ console.log("");
1251
+ console.log(" Your projects:");
1252
+ projectsList.forEach((p3, i) => {
1253
+ console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
1254
+ });
1255
+ console.log(` ${projectsList.length + 1}. Create a new project`);
1256
+ console.log("");
1257
+ const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
1258
+ const num = parseInt(choice, 10);
1259
+ if (num >= 1 && num <= projectsList.length) {
1260
+ const picked = projectsList[num - 1];
1261
+ chosenProjectId = picked.id;
1262
+ chosenProjectName = picked.name;
1263
+ }
1064
1264
  }
1065
- rl.close();
1066
- if (!apiKey) {
1067
- process.stderr.write("\nerror: API key is required.\n");
1068
- process.exit(2);
1265
+ if (!chosenProjectId) {
1266
+ console.log("");
1267
+ const newName = await ask(" Project name: ");
1268
+ rl.close();
1269
+ if (!newName.trim()) {
1270
+ process.stderr.write("\nerror: Project name is required.\n");
1271
+ process.exit(2);
1272
+ }
1273
+ try {
1274
+ const createRes = await fetch(`${endpoint}/v1/cli/projects`, {
1275
+ method: "POST",
1276
+ headers: cliHeaders,
1277
+ body: JSON.stringify({ name: newName.trim() })
1278
+ });
1279
+ const createJson = await createRes.json();
1280
+ if (!createRes.ok || !createJson.ok || !createJson.data) {
1281
+ throw new Error(createJson.error?.message ?? `HTTP ${createRes.status}`);
1282
+ }
1283
+ chosenProjectId = createJson.data.id;
1284
+ chosenProjectName = createJson.data.name;
1285
+ apiKey = createJson.data.apiKey ?? void 0;
1286
+ console.log(` \u2713 Created project "${chosenProjectName}"`);
1287
+ } catch (err) {
1288
+ process.stderr.write(`
1289
+ error: Could not create project: ${err instanceof Error ? err.message : String(err)}
1290
+ `);
1291
+ process.exit(1);
1292
+ }
1293
+ } else {
1294
+ rl.close();
1295
+ }
1296
+ }
1297
+ if (!apiKey && chosenProjectId) {
1298
+ try {
1299
+ const keyRes = await fetch(`${endpoint}/v1/cli/projects/${chosenProjectId}/keys`, {
1300
+ method: "POST",
1301
+ headers: cliHeaders,
1302
+ body: JSON.stringify({})
1303
+ });
1304
+ const keyJson = await keyRes.json();
1305
+ if (keyRes.ok && keyJson.ok) {
1306
+ apiKey = keyJson.data?.key ?? void 0;
1307
+ }
1308
+ } catch {
1069
1309
  }
1070
1310
  }
1071
1311
  const config = loadConfig();
1072
- config.apiKey = apiKey;
1073
1312
  config.endpoint = endpoint;
1074
- if (projectId) config.projectId = projectId;
1313
+ config.consoleUrl = consoleBase;
1314
+ if (chosenProjectId) config.projectId = chosenProjectId;
1315
+ if (apiKey) config.apiKey = apiKey;
1075
1316
  saveConfig(config);
1076
1317
  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})`);
1318
+ if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
1319
+ if (apiKey) {
1320
+ console.log(` \u2713 SDK key saved (${apiKey.slice(0, 12)}\u2026)`);
1321
+ } else {
1322
+ console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
1094
1323
  }
1095
- console.log(` \u2713 Endpoint: ${endpoint}`);
1096
1324
  console.log("");
1097
- console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1325
+ console.log(" Run 'mushi init' to set up the SDK in this project.");
1098
1326
  });
1099
1327
  program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1100
1328
  Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
@@ -1617,7 +1845,7 @@ AI code review context.
1617
1845
  Typical CI usage:
1618
1846
  MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
1619
1847
  npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
1620
- const { writeFile: writeFile3, mkdir: mkdir2 } = await import("fs/promises");
1848
+ const { writeFile, mkdir } = await import("fs/promises");
1621
1849
  const nodePath = await import("path");
1622
1850
  const config = requireConfig();
1623
1851
  const cwd = opts.cwd ?? process.cwd();
@@ -1644,8 +1872,8 @@ Typical CI usage:
1644
1872
  console.log(JSON.stringify(output, null, 2));
1645
1873
  return;
1646
1874
  }
1647
- await mkdir2(nodePath.dirname(target), { recursive: true });
1648
- await writeFile3(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1875
+ await mkdir(nodePath.dirname(target), { recursive: true });
1876
+ await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1649
1877
  if (opts.json) {
1650
1878
  console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
1651
1879
  } else {
@@ -1677,11 +1905,11 @@ async function findMapFiles(dir) {
1677
1905
  return results;
1678
1906
  }
1679
1907
  function fileHash(path) {
1680
- return new Promise((resolve4, reject) => {
1908
+ return new Promise((resolve3, reject) => {
1681
1909
  const hash = createHash("sha256");
1682
1910
  const stream = createReadStream(path);
1683
1911
  stream.on("data", (chunk) => hash.update(chunk));
1684
- stream.on("end", () => resolve4(hash.digest("hex")));
1912
+ stream.on("end", () => resolve3(hash.digest("hex")));
1685
1913
  stream.on("error", reject);
1686
1914
  });
1687
1915
  }
@@ -1828,7 +2056,7 @@ function registerDiagnosticsCommands(program2) {
1828
2056
  console.log(`\u2713 Test report submitted`);
1829
2057
  console.log(` ID: ${d.reportId}`);
1830
2058
  console.log(` Status: ${d.status}`);
1831
- console.log(` View: https://kensaur.us/mushi-mushi/reports/${d.reportId}`);
2059
+ console.log(` View: ${reportsUrl(resolveConsoleUrlSync(), d.reportId)}`);
1832
2060
  }
1833
2061
  });
1834
2062
  program2.command("index <path>").description("Walk a local repo and upload code chunks to the Mushi RAG indexer").option("--language <lang>", "Limit to one language: ts, tsx, js, py, go, rs").option("--dry-run", "Show what would be uploaded without sending").option("--json", "Machine-readable summary: { files, bytes }").addHelpText("after", `
@@ -1840,7 +2068,7 @@ Examples:
1840
2068
  mushi index ./src
1841
2069
  mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
1842
2070
  const config = requireConfig({ needsProject: true });
1843
- const { readdir: readdir2, readFile: readFile4, stat } = await import("fs/promises");
2071
+ const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
1844
2072
  const nodePath = await import("path");
1845
2073
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
1846
2074
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
@@ -1867,7 +2095,7 @@ Examples:
1867
2095
  `);
1868
2096
  continue;
1869
2097
  }
1870
- const source = await readFile4(file, "utf8");
2098
+ const source = await readFile2(file, "utf8");
1871
2099
  const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
1872
2100
  count++;
1873
2101
  bytes += source.length;
@@ -1914,61 +2142,11 @@ Examples:
1914
2142
  });
1915
2143
  }
1916
2144
 
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
-
1967
2145
  // src/commands/project.ts
1968
2146
  function registerProjectCommands(program2) {
1969
2147
  const project = program2.command("project").description("Project management");
1970
2148
  project.command("create").description("Create a new Mushi project, mint an API key, and write config files").option("--name <name>", "Project name (skip the prompt)").option("--no-browser", "Skip opening the browser for the sign-up / magic-link step").option("--endpoint <url>", "Override API endpoint (self-hosted)").addHelpText("after", `
1971
- Creates a project on app.mushimushi.dev, mints an API key with mcp:read+write scope,
2149
+ Creates a project on the Mushi console, mints an API key with mcp:read+write scope,
1972
2150
  and writes the following to the current directory:
1973
2151
  .env.local \u2014 MUSHI_API_KEY, MUSHI_PROJECT_ID, MUSHI_API_ENDPOINT
1974
2152
  .cursor/mcp.json \u2014 pre-filled mcpServers.mushi block for Cursor
@@ -1978,11 +2156,12 @@ Typical first-time flow:
1978
2156
  # Browser opens \u2192 sign up / magic-link \u2192 come back to terminal
1979
2157
  # CLI writes .env.local and .cursor/mcp.json
1980
2158
  # mushi whoami to confirm`).action(async (opts) => {
1981
- const { writeFile: writeFile3 } = await import("fs/promises");
2159
+ const { writeFile } = await import("fs/promises");
1982
2160
  const { existsSync: existsSync4 } = await import("fs");
1983
2161
  const nodePath = await import("path");
1984
2162
  const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
1985
- const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
2163
+ const consoleBase = resolveConsoleUrlSync();
2164
+ const signUpUrl = consoleUrl(consoleBase, "/sign-up");
1986
2165
  console.log("");
1987
2166
  console.log(" Mushi project create");
1988
2167
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -2004,7 +2183,7 @@ Typical first-time flow:
2004
2183
  console.log("");
2005
2184
  const { createInterface } = await import("readline");
2006
2185
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2007
- const ask = (q) => new Promise((resolve4) => rl.question(q, (a) => resolve4(a.trim())));
2186
+ const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
2008
2187
  const projectId = await ask(" Project ID (uuid): ");
2009
2188
  const apiKey = await ask(" API key (mushi_...): ");
2010
2189
  rl.close();
@@ -2027,7 +2206,7 @@ Typical first-time flow:
2027
2206
  ""
2028
2207
  ];
2029
2208
  const envExisting = existsSync4(envPath);
2030
- await writeFile3(envPath, envLines.join("\n"), "utf8");
2209
+ await writeFile(envPath, envLines.join("\n"), "utf8");
2031
2210
  console.log(`
2032
2211
  \u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
2033
2212
  const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
@@ -2044,7 +2223,7 @@ Typical first-time flow:
2044
2223
 
2045
2224
  // src/commands/setup.ts
2046
2225
  function registerSetupCommands(program2) {
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", `
2226
+ program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").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", `
2048
2227
  Examples:
2049
2228
  mushi setup # wire Cursor (default)
2050
2229
  mushi setup --all-projects # one server entry per accessible project
@@ -2058,7 +2237,7 @@ Supported IDEs:
2058
2237
  zed \u2014 writes ~/.config/zed/settings.json mcpServers block
2059
2238
 
2060
2239
  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");
2240
+ const { writeFile, mkdir, readFile: readFile2 } = await import("fs/promises");
2062
2241
  const { existsSync: existsSync4 } = await import("fs");
2063
2242
  const nodePath = await import("path");
2064
2243
  const os = await import("os");
@@ -2173,7 +2352,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2173
2352
  let settings = {};
2174
2353
  if (existsSync4(configPath)) {
2175
2354
  try {
2176
- const raw = await readFile4(configPath, "utf8");
2355
+ const raw = await readFile2(configPath, "utf8");
2177
2356
  settings = JSON.parse(raw);
2178
2357
  } catch {
2179
2358
  }
@@ -2197,8 +2376,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2197
2376
  console.log(`[dry-run] Would write ${configPath}:`);
2198
2377
  console.log(redactKeyForDisplay(output));
2199
2378
  } else {
2200
- await mkdir2(configDir, { recursive: true });
2201
- await writeFile3(configPath, output, "utf8");
2379
+ await mkdir(configDir, { recursive: true });
2380
+ await writeFile(configPath, output, "utf8");
2202
2381
  console.log(`\u2713 Written ${configPath}`);
2203
2382
  }
2204
2383
  }
@@ -2232,7 +2411,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2232
2411
  if (opts.dryRun) {
2233
2412
  console.log(`[dry-run] Would write ${rulesPath}`);
2234
2413
  } else {
2235
- await writeFile3(rulesPath, rulesContent, "utf8");
2414
+ await writeFile(rulesPath, rulesContent, "utf8");
2236
2415
  console.log(`\u2713 Written .cursorrules`);
2237
2416
  }
2238
2417
  } else if (opts.ide === "claude") {
@@ -2241,8 +2420,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2241
2420
  if (opts.dryRun) {
2242
2421
  console.log(`[dry-run] Would write ${rulesPath}`);
2243
2422
  } else {
2244
- await mkdir2(rulesDir, { recursive: true });
2245
- await writeFile3(rulesPath, rulesContent, "utf8");
2423
+ await mkdir(rulesDir, { recursive: true });
2424
+ await writeFile(rulesPath, rulesContent, "utf8");
2246
2425
  console.log(`\u2713 Written .claude/rules/mushi.md`);
2247
2426
  }
2248
2427
  }
@@ -2370,117 +2549,6 @@ function renderNudgeExplainer(phase) {
2370
2549
  return lines.join("\n");
2371
2550
  }
2372
2551
 
2373
- // src/sanitize-config.ts
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;
2375
- var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
2376
- function sanitizeApiKey(raw) {
2377
- const key = raw.replace(/[\r\n\0]/g, "");
2378
- if (!API_KEY_RE.test(key)) {
2379
- throw new Error(
2380
- "Invalid API key in config \u2014 run `mushi login --api-key <key>` to refresh credentials."
2381
- );
2382
- }
2383
- return key;
2384
- }
2385
- function sanitizeProjectId(raw) {
2386
- const id = raw.trim();
2387
- if (!PROJECT_ID_RE.test(id)) {
2388
- throw new Error(
2389
- "Invalid project ID in config \u2014 expected a UUID or proj_* slug from the admin console."
2390
- );
2391
- }
2392
- return id;
2393
- }
2394
- function sanitizeEndpoint(raw) {
2395
- return assertEndpoint(normalizeEndpoint(raw));
2396
- }
2397
- function sanitizeCliCredentials(config) {
2398
- if (!config.endpoint || !config.apiKey || !config.projectId) {
2399
- throw new Error("Missing endpoint, apiKey, or projectId");
2400
- }
2401
- return {
2402
- endpoint: sanitizeEndpoint(config.endpoint),
2403
- apiKey: sanitizeApiKey(config.apiKey),
2404
- projectId: sanitizeProjectId(config.projectId)
2405
- };
2406
- }
2407
- function apiKeyHeaders(apiKey, projectId) {
2408
- const headers = {
2409
- Authorization: `Bearer ${apiKey}`,
2410
- "X-Mushi-Api-Key": apiKey
2411
- };
2412
- if (projectId) headers["X-Mushi-Project"] = projectId;
2413
- return headers;
2414
- }
2415
-
2416
- // src/heartbeat-wait.ts
2417
- var IngestSetupHttpError = class extends Error {
2418
- status;
2419
- constructor(status) {
2420
- super(`ingest-setup HTTP ${status}`);
2421
- this.name = "IngestSetupHttpError";
2422
- this.status = status;
2423
- }
2424
- };
2425
- var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
2426
- async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
2427
- const endpoint = sanitizeEndpoint(config.endpoint);
2428
- const apiKey = sanitizeApiKey(config.apiKey);
2429
- const projectId = config.projectId ? sanitizeProjectId(config.projectId) : void 0;
2430
- const res = await doFetch(`${endpoint}/v1/sync/ingest-setup`, {
2431
- headers: apiKeyHeaders(apiKey, projectId),
2432
- signal: AbortSignal.timeout(8e3)
2433
- });
2434
- if (!res.ok) {
2435
- if (NON_RETRYABLE_STATUSES.has(res.status)) throw new IngestSetupHttpError(res.status);
2436
- return null;
2437
- }
2438
- const body = await res.json();
2439
- return body.ok && body.data ? body.data : null;
2440
- }
2441
- async function waitForIngestReady(options) {
2442
- const doFetch = options.fetch ?? globalThis.fetch;
2443
- const maxAttempts = options.maxAttempts ?? 40;
2444
- const intervalMs = options.intervalMs ?? 3e3;
2445
- let lastPayload = null;
2446
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2447
- if (options.signal?.aborted) {
2448
- return { ok: false, payload: lastPayload, attempts: attempt - 1, reason: "aborted" };
2449
- }
2450
- try {
2451
- lastPayload = await fetchIngestSetup(
2452
- { endpoint: options.endpoint, apiKey: options.apiKey, projectId: options.projectId },
2453
- doFetch
2454
- );
2455
- if (lastPayload) {
2456
- options.onPoll?.(lastPayload, attempt);
2457
- if (lastPayload.ready) {
2458
- return { ok: true, payload: lastPayload, attempts: attempt, reason: "ready" };
2459
- }
2460
- const sdkStep = lastPayload.steps.find((s) => s.id === "sdk_installed");
2461
- if (sdkStep?.complete) {
2462
- return { ok: true, payload: lastPayload, attempts: attempt, reason: "heartbeat" };
2463
- }
2464
- }
2465
- } catch (err) {
2466
- const msg = err instanceof Error ? err.message : String(err);
2467
- if (err instanceof IngestSetupHttpError) {
2468
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "unauthorized", error: msg };
2469
- }
2470
- if (options.signal?.aborted) {
2471
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "aborted", error: msg };
2472
- }
2473
- if (attempt === maxAttempts) {
2474
- return { ok: false, payload: lastPayload, attempts: attempt, reason: "fetch-error", error: msg };
2475
- }
2476
- }
2477
- if (attempt < maxAttempts) {
2478
- await new Promise((r) => setTimeout(r, intervalMs));
2479
- }
2480
- }
2481
- return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
2482
- }
2483
-
2484
2552
  // src/doctor.ts
2485
2553
  function checkCliConfig(config) {
2486
2554
  return [
@@ -2519,11 +2587,11 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
2519
2587
  }
2520
2588
  async function checkSdkInstall(cwd) {
2521
2589
  try {
2522
- const { readFile: readFile4 } = await import("fs/promises");
2523
- const { join: join6, resolve: resolve4 } = await import("path");
2524
- const root = resolve4(cwd);
2525
- const pkgPath = join6(root, "package.json");
2526
- const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
2590
+ const { readFile: readFile2 } = await import("fs/promises");
2591
+ const { join: join5, resolve: resolve3 } = await import("path");
2592
+ const root = resolve3(cwd);
2593
+ const pkgPath = join5(root, "package.json");
2594
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
2527
2595
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2528
2596
  const sdks = [
2529
2597
  "@mushi-mushi/react",
@@ -2531,11 +2599,11 @@ async function checkSdkInstall(cwd) {
2531
2599
  "@mushi-mushi/core",
2532
2600
  "@mushi-mushi/react-native"
2533
2601
  ];
2534
- const installed = sdks.filter((s) => deps[s]);
2602
+ const installed2 = sdks.filter((s) => deps[s]);
2535
2603
  return {
2536
2604
  name: "SDK installed in this repo",
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"
2605
+ ok: installed2.length > 0,
2606
+ detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
2539
2607
  };
2540
2608
  } catch {
2541
2609
  return null;
@@ -2701,18 +2769,18 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
2701
2769
  async function checkHostAppWiring(cwd) {
2702
2770
  const checks = [];
2703
2771
  try {
2704
- const { readFile: readFile4, access } = await import("fs/promises");
2705
- const { join: join6, resolve: resolve4 } = await import("path");
2706
- const root = resolve4(cwd);
2707
- const pkgPath = join6(root, "package.json");
2708
- const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
2772
+ const { readFile: readFile2, access } = await import("fs/promises");
2773
+ const { join: join5, resolve: resolve3 } = await import("path");
2774
+ const root = resolve3(cwd);
2775
+ const pkgPath = join5(root, "package.json");
2776
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
2709
2777
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2710
2778
  const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
2711
2779
  const envCandidates = [".env.local", ".env"];
2712
2780
  let envContent = "";
2713
2781
  for (const f of envCandidates) {
2714
2782
  try {
2715
- envContent = await readFile4(join6(root, f), "utf8");
2783
+ envContent = await readFile2(join5(root, f), "utf8");
2716
2784
  break;
2717
2785
  } catch {
2718
2786
  }
@@ -2726,7 +2794,7 @@ async function checkHostAppWiring(cwd) {
2726
2794
  });
2727
2795
  let mcpPresent = false;
2728
2796
  try {
2729
- await access(join6(root, ".cursor", "mcp.json"));
2797
+ await access(join5(root, ".cursor", "mcp.json"));
2730
2798
  mcpPresent = true;
2731
2799
  } catch {
2732
2800
  }
@@ -2759,19 +2827,19 @@ async function checkHostAppWiring(cwd) {
2759
2827
  }
2760
2828
  async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
2761
2829
  const checks = [];
2762
- const { readFile: readFile4 } = await import("fs/promises");
2763
- const { join: join6, resolve: resolve4 } = await import("path");
2830
+ const { readFile: readFile2 } = await import("fs/promises");
2831
+ const { join: join5, resolve: resolve3 } = await import("path");
2764
2832
  const { homedir } = await import("os");
2765
- const root = resolve4(cwd);
2833
+ const root = resolve3(cwd);
2766
2834
  const candidates = [
2767
- join6(root, ".cursor", "mcp.json"),
2768
- join6(homedir(), ".cursor", "mcp.json")
2835
+ join5(root, ".cursor", "mcp.json"),
2836
+ join5(homedir(), ".cursor", "mcp.json")
2769
2837
  ];
2770
2838
  let mcpPath = null;
2771
2839
  let mcpRaw = null;
2772
2840
  for (const candidate of candidates) {
2773
2841
  try {
2774
- mcpRaw = await readFile4(candidate, "utf8");
2842
+ mcpRaw = await readFile2(candidate, "utf8");
2775
2843
  mcpPath = candidate;
2776
2844
  break;
2777
2845
  } catch {
@@ -2958,9 +3026,9 @@ async function planUpgrade(cwd) {
2958
3026
  ...pkg?.dependencies ?? {},
2959
3027
  ...pkg?.devDependencies ?? {}
2960
3028
  };
2961
- const installed = MUSHI_PACKAGES.filter((name) => deps[name]);
3029
+ const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
2962
3030
  const entries = await Promise.all(
2963
- installed.map(async (name) => {
3031
+ installed2.map(async (name) => {
2964
3032
  const current = deps[name] ?? "";
2965
3033
  const currentCore = current.replace(/^[\^~>=<]*/, "");
2966
3034
  if (!/^\d/.test(currentCore)) {
@@ -3031,136 +3099,6 @@ async function runUpgrade(opts = {}) {
3031
3099
  };
3032
3100
  }
3033
3101
 
3034
- // src/connect.ts
3035
- import { appendFile, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3036
- import { join as join5, resolve as resolve3 } from "path";
3037
- function envKeyPresent(content, key) {
3038
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3039
- return new RegExp(`^${escaped}=`, "m").test(content);
3040
- }
3041
- async function mergeEnvFile(path, lines) {
3042
- const block = `
3043
- # Mushi \u2014 added by mushi connect
3044
- ${lines.join("\n")}
3045
- `;
3046
- let existing = null;
3047
- try {
3048
- existing = await readFile3(path, "utf8");
3049
- } catch {
3050
- }
3051
- if (existing !== null) {
3052
- const needs = lines.filter((line) => {
3053
- const key = line.split("=")[0];
3054
- return !envKeyPresent(existing, key);
3055
- });
3056
- if (needs.length === 0) return false;
3057
- await appendFile(path, `
3058
- # Mushi \u2014 added by mushi connect
3059
- ${needs.join("\n")}
3060
- `, "utf8");
3061
- return true;
3062
- }
3063
- await writeFile2(path, block, "utf8");
3064
- return true;
3065
- }
3066
- async function ensureMcpJsonGitignored(cwd, messages) {
3067
- const gitignorePath = join5(cwd, ".gitignore");
3068
- const patterns = [".cursor/mcp.json", ".cursor/"];
3069
- let content = null;
3070
- try {
3071
- content = await readFile3(gitignorePath, "utf8");
3072
- } catch {
3073
- }
3074
- if (content === null) {
3075
- messages.push(
3076
- "\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
3077
- );
3078
- return;
3079
- }
3080
- const covered = patterns.some((p3) => content.split("\n").some((line) => line.trim() === p3 || line.trim() === `${p3}/`));
3081
- if (covered) return;
3082
- await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
3083
- messages.push("\u2713 Added .cursor/mcp.json to .gitignore (contains API key)");
3084
- }
3085
- async function runConnect(opts, baseConfig = {}) {
3086
- const cwd = resolve3(opts.cwd ?? process.cwd());
3087
- const endpoint = assertEndpoint(opts.endpoint);
3088
- const messages = [];
3089
- const config = {
3090
- ...baseConfig,
3091
- apiKey: opts.apiKey,
3092
- projectId: opts.projectId,
3093
- endpoint
3094
- };
3095
- saveConfig(config);
3096
- messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
3097
- let envPath = null;
3098
- if (opts.writeEnv !== false) {
3099
- const pkg = readPackageJson(cwd);
3100
- const framework = detectFramework(cwd, pkg);
3101
- const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
3102
- envPath = join5(cwd, ".env.local");
3103
- const wrote = await mergeEnvFile(envPath, lines);
3104
- messages.push(
3105
- wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
3106
- );
3107
- const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
3108
- if (deps["@capacitor/core"] && deps["react"]) {
3109
- messages.push(
3110
- "\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."
3111
- );
3112
- }
3113
- }
3114
- let mcpPath = null;
3115
- if (opts.wireIde !== false) {
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 });
3125
- await ensureMcpJsonGitignored(cwd, messages);
3126
- messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
3127
- }
3128
- let heartbeat = null;
3129
- if (opts.wait) {
3130
- const timeoutSec = opts.waitTimeoutSec ?? 120;
3131
- const maxAttempts = Math.max(1, Math.ceil(timeoutSec * 1e3 / 3e3));
3132
- messages.push(`\u2026 Waiting for SDK heartbeat (up to ${timeoutSec}s) \u2014 start your dev server with the snippet installed`);
3133
- heartbeat = await waitForIngestReady({
3134
- endpoint,
3135
- apiKey: opts.apiKey,
3136
- projectId: opts.projectId,
3137
- maxAttempts,
3138
- onPoll: (payload, attempt) => {
3139
- if (!opts.json && attempt % 3 === 0) {
3140
- const sdk = payload.steps.find((s) => s.id === "sdk_installed");
3141
- const seen = payload.diagnostic?.last_sdk_seen_at ?? "never";
3142
- process.stdout.write(` poll ${attempt}: sdk_installed=${sdk?.complete ? "yes" : "no"} last_seen=${seen}
3143
- `);
3144
- }
3145
- }
3146
- });
3147
- if (heartbeat.ok) {
3148
- const label = heartbeat.reason === "heartbeat" ? "SDK heartbeat detected" : "Ingest setup complete";
3149
- messages.push(`\u2713 ${label} \u2014 ingest pipeline is live`);
3150
- } else if (heartbeat.reason === "unauthorized") {
3151
- messages.push(
3152
- `\u2717 The backend rejected these credentials (${heartbeat.error ?? "auth error"}). Double-check --api-key, --project-id, and --endpoint, then re-run \`mushi connect --wait\`.`
3153
- );
3154
- } else {
3155
- messages.push(
3156
- `\u2717 No heartbeat before timeout (${heartbeat.reason}). Confirm env vars are in your build, restart the dev server, then re-run \`mushi connect --wait\`.`
3157
- );
3158
- }
3159
- }
3160
- const ok = !opts.wait || Boolean(heartbeat?.ok);
3161
- return { ok, envPath, mcpPath, heartbeat, messages };
3162
- }
3163
-
3164
3102
  // src/commands/fix.ts
3165
3103
  function registerFixCommands(program2) {
3166
3104
  const fixCmd = program2.command("fix").description("Dispatch an agentic fix for a report");
@@ -3302,7 +3240,7 @@ Examples:
3302
3240
  }
3303
3241
  if (result.plan.entries.length === 0) process.exit(1);
3304
3242
  });
3305
- program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
3243
+ program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "Write SDK env vars to .env.local (default: enabled)").option("--wire-ide", "Wire Cursor MCP into .cursor/mcp.json (default: enabled)").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
3306
3244
  Examples:
3307
3245
  MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
3308
3246
  mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
@@ -3987,9 +3925,9 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
3987
3925
  }
3988
3926
  }
3989
3927
  }
3990
- const consoleUrl = config.consoleUrl ?? "https://app.mushi.ai";
3928
+ const consoleUrl2 = config.consoleUrl ?? "https://app.mushi.ai";
3991
3929
  console.log(`
3992
- Open in console: ${consoleUrl}/qa-coverage?story=${storyId}`);
3930
+ Open in console: ${consoleUrl2}/qa-coverage?story=${storyId}`);
3993
3931
  console.log(` Tip: run 'mushi config consoleUrl <url>' to override (e.g. http://localhost:6464 for local dev)`);
3994
3932
  console.log();
3995
3933
  });
@@ -4433,8 +4371,8 @@ ${"\u2500".repeat(60)}`);
4433
4371
  installSignalHandlers();
4434
4372
  var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION).addHelpText("after", `
4435
4373
  Environment variables:
4436
- MUSHI_API_KEY Project API key (from Settings \u2192 API Keys in the console)
4437
- MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
4374
+ MUSHI_API_KEY SDK ingest key (report:write scope \u2014 from Onboarding \u2192 Verify in the console)
4375
+ MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
4438
4376
  MUSHI_API_ENDPOINT Supabase edge function URL
4439
4377
  e.g. https://<ref>.supabase.co/functions/v1/api
4440
4378
 
@@ -4444,7 +4382,7 @@ Exit codes:
4444
4382
  2 configuration error (missing credentials or endpoint)
4445
4383
  3 not found (resource does not exist)
4446
4384
 
4447
- Console: https://kensaur.us/mushi-mushi/
4385
+ Console: https://kensaur.us/mushi-mushi/admin
4448
4386
  Docs: https://github.com/kensaurus/mushi-mushi`);
4449
4387
  registerAccountCommands(program);
4450
4388
  registerDeployCommands(program);