@mushi-mushi/cli 0.18.2 → 0.20.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-A2NHESSJ.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.20.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,382 @@ 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
+ const trimmed = url.trim();
130
+ let end = trimmed.length;
131
+ while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
132
+ return trimmed.slice(0, end);
133
+ }
134
+ function consoleUrl(base, route) {
135
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
136
+ return `${normalizeConsoleBase(base)}${normalizedRoute}`;
137
+ }
138
+ function resolveConsoleUrlFromEnv() {
139
+ const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
140
+ return raw ? normalizeConsoleBase(raw) : void 0;
141
+ }
142
+ function isInsideMushiMonorepo(cwd) {
143
+ let dir = cwd;
144
+ for (let depth = 0; depth < 14; depth += 1) {
145
+ const pkgPath = join(dir, "package.json");
146
+ if (existsSync(pkgPath)) {
147
+ try {
148
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
149
+ if (pkg.name === "mushi-mushi") return true;
150
+ } catch {
151
+ }
152
+ }
153
+ const parent = dirname(dir);
154
+ if (parent === dir) break;
155
+ dir = parent;
283
156
  }
284
- };
285
- function readPackageJson(cwd) {
286
- const path = join(cwd, "package.json");
287
- if (!existsSync(path)) return null;
157
+ return false;
158
+ }
159
+ async function probeLocalAdminConsole() {
288
160
  try {
289
- return JSON.parse(readFileSync(path, "utf-8"));
161
+ const controller = new AbortController();
162
+ const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
163
+ const res = await fetch(LOCAL_ADMIN_URL, {
164
+ signal: controller.signal,
165
+ method: "HEAD"
166
+ });
167
+ clearTimeout(timer);
168
+ if (res.ok || res.status === 404 || res.status === 401) {
169
+ return LOCAL_ADMIN_URL;
170
+ }
290
171
  } catch {
291
- return null;
292
172
  }
173
+ return null;
174
+ }
175
+ function resolveConsoleUrlSync(cwd = process.cwd()) {
176
+ const fromEnv = resolveConsoleUrlFromEnv();
177
+ if (fromEnv) return fromEnv;
178
+ const config = loadConfig();
179
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
180
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
181
+ return HOSTED_CONSOLE_BASE;
182
+ }
183
+ async function resolveConsoleUrl(options) {
184
+ const fromEnv = resolveConsoleUrlFromEnv();
185
+ if (fromEnv) return fromEnv;
186
+ const config = loadConfig();
187
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
188
+ const local = await probeLocalAdminConsole();
189
+ if (local) return local;
190
+ const cwd = options?.cwd ?? process.cwd();
191
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
192
+ return HOSTED_CONSOLE_BASE;
193
+ }
194
+ function projectIdHint(base) {
195
+ 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).`;
196
+ }
197
+ function apiKeyHint(base) {
198
+ 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.`;
199
+ }
200
+ function cliSetupDeepLink(base) {
201
+ return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
202
+ }
203
+ function reportsUrl(base, reportId) {
204
+ return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
205
+ }
206
+ async function openInBrowser(url) {
207
+ let parsed;
208
+ try {
209
+ parsed = new URL(url);
210
+ } catch {
211
+ return;
212
+ }
213
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
214
+ return;
215
+ }
216
+ const safeUrl = parsed.toString();
217
+ const { spawn: spawn2 } = await import("child_process");
218
+ const [command, args] = process.platform === "win32" ? (
219
+ // Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
220
+ // `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
221
+ // characters like `&` / `|` / `^` could still break out — exactly the
222
+ // injection surface CodeQL flags. rundll32 never invokes a shell: the URL
223
+ // is handed to the default browser as one opaque argument.
224
+ ["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
225
+ ) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
226
+ await new Promise((resolve3) => {
227
+ try {
228
+ const child = spawn2(command, [...args], { stdio: "ignore", shell: false });
229
+ child.on("error", () => resolve3());
230
+ child.on("exit", () => resolve3());
231
+ child.unref();
232
+ } catch {
233
+ resolve3();
234
+ }
235
+ });
236
+ }
237
+
238
+ // src/cli-shared.ts
239
+ var API_TIMEOUT_MS = 15e3;
240
+ async function apiCall(path, config, options = {}) {
241
+ const endpoint = config.endpoint;
242
+ if (!endpoint) {
243
+ return {
244
+ ok: false,
245
+ error: {
246
+ code: "NO_ENDPOINT",
247
+ 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"
248
+ }
249
+ };
250
+ }
251
+ const controller = new AbortController();
252
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
253
+ const signals = [controller.signal, getAbortSignal()];
254
+ const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
255
+ try {
256
+ const res = await fetch(`${endpoint}${path}`, {
257
+ ...options,
258
+ signal: compositeSignal,
259
+ headers: {
260
+ "Content-Type": "application/json",
261
+ // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
262
+ // fallback accepted by some older middleware. Both are sent.
263
+ "Authorization": `Bearer ${config.apiKey ?? ""}`,
264
+ "X-Mushi-Api-Key": config.apiKey ?? "",
265
+ "X-Mushi-Project": config.projectId ?? "",
266
+ "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
267
+ ...options.headers
268
+ }
269
+ });
270
+ clearTimeout(timer);
271
+ let body;
272
+ const contentType = res.headers.get("content-type") ?? "";
273
+ if (contentType.includes("application/json")) {
274
+ try {
275
+ body = await res.json();
276
+ } catch {
277
+ body = null;
278
+ }
279
+ } else {
280
+ const text2 = await res.text();
281
+ try {
282
+ body = JSON.parse(text2);
283
+ } catch {
284
+ body = {
285
+ ok: false,
286
+ error: {
287
+ code: `HTTP_${res.status}`,
288
+ message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
289
+ }
290
+ };
291
+ }
292
+ }
293
+ if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
294
+ const b = body;
295
+ return {
296
+ ok: false,
297
+ httpStatus: res.status,
298
+ error: {
299
+ code: b["code"] ?? `HTTP_${res.status}`,
300
+ message: b["message"] ?? `Request failed (${res.status})`
301
+ }
302
+ };
303
+ }
304
+ return body;
305
+ } catch (err) {
306
+ clearTimeout(timer);
307
+ if (err instanceof Error && err.name === "AbortError") {
308
+ return {
309
+ ok: false,
310
+ error: {
311
+ code: "TIMEOUT",
312
+ message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
313
+ }
314
+ };
315
+ }
316
+ return {
317
+ ok: false,
318
+ error: {
319
+ code: "NETWORK_ERROR",
320
+ message: err instanceof Error ? err.message : String(err)
321
+ }
322
+ };
323
+ }
324
+ }
325
+ function die(result, exitCode = 1) {
326
+ const { code, message } = result.error;
327
+ const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
328
+ process.stderr.write(`error${status}: ${code} \u2014 ${message}
329
+ `);
330
+ process.exit(exitCode);
293
331
  }
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;
332
+ function requireConfig(opts = {}) {
333
+ const config = loadConfig();
334
+ if (!config.apiKey) {
335
+ process.stderr.write(
336
+ "error: API key not configured.\n Run: mushi login --api-key <key>\n Or: export MUSHI_API_KEY=<key>\n"
337
+ );
338
+ process.exit(2);
312
339
  }
313
- if (existsSync(join(cwd, "nuxt.config.ts")) || existsSync(join(cwd, "nuxt.config.js"))) {
314
- return FRAMEWORKS.nuxt;
340
+ if (!config.endpoint) {
341
+ config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
315
342
  }
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;
343
+ if (opts.needsProject && !config.projectId) {
344
+ process.stderr.write(
345
+ "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"
346
+ );
347
+ process.exit(2);
348
+ }
349
+ return config;
319
350
  }
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";
351
+ function fmtDate(iso) {
352
+ if (!iso) return "\u2014";
353
+ return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
330
354
  }
331
- function installCommand(pm, packages) {
332
- const verb = pm === "npm" ? "install" : "add";
333
- return `${pm} ${verb} ${packages.join(" ")}`;
355
+ function pad(s, width) {
356
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
334
357
  }
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");
358
+
359
+ // src/device-auth.ts
360
+ var DEVICE_FETCH_TIMEOUT_MS = 15e3;
361
+ function trimTrailingSlash(endpoint) {
362
+ let end = endpoint.length;
363
+ while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
364
+ return endpoint.slice(0, end);
365
+ }
366
+ async function deviceFetch(url, init = {}) {
367
+ const controller = new AbortController();
368
+ const timer = setTimeout(() => controller.abort(), DEVICE_FETCH_TIMEOUT_MS);
369
+ try {
370
+ return await fetch(url, { ...init, signal: controller.signal });
371
+ } finally {
372
+ clearTimeout(timer);
342
373
  }
343
- if (framework.id === "react-native") {
344
- return [
345
- `MUSHI_PROJECT_ID=${projectId}`,
346
- `MUSHI_API_KEY=${apiKey}`
347
- ].join("\n");
374
+ }
375
+ async function startDeviceAuth(endpoint) {
376
+ const base = trimTrailingSlash(endpoint);
377
+ const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
378
+ method: "POST",
379
+ headers: { "Content-Type": "application/json" }
380
+ });
381
+ const json = await res.json().catch(() => null);
382
+ if (!res.ok || !json?.ok || !json.data) {
383
+ throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
348
384
  }
349
- if (framework.id === "expo") {
350
- return [
351
- `EXPO_PUBLIC_MUSHI_PROJECT_ID=${projectId}`,
352
- `EXPO_PUBLIC_MUSHI_API_KEY=${apiKey}`
353
- ].join("\n");
385
+ return json.data;
386
+ }
387
+ async function pollDeviceToken(endpoint, deviceCode) {
388
+ const base = trimTrailingSlash(endpoint);
389
+ try {
390
+ const res = await deviceFetch(`${base}/v1/cli/auth/device/token`, {
391
+ method: "POST",
392
+ headers: { "Content-Type": "application/json" },
393
+ body: JSON.stringify({ device_code: deviceCode })
394
+ });
395
+ const json = await res.json().catch(() => ({}));
396
+ if (res.ok && json.ok && json.data?.cli_token) {
397
+ return { status: "approved", cliToken: json.data.cli_token, userId: json.data.user_id };
398
+ }
399
+ switch (json.error) {
400
+ case "authorization_pending":
401
+ return { status: "pending" };
402
+ case "access_denied":
403
+ return { status: "denied" };
404
+ case "expired_token":
405
+ return { status: "expired" };
406
+ default:
407
+ return {
408
+ status: "error",
409
+ message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
410
+ // 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
411
+ // rejection (already claimed / invalid_grant) that won't fix itself.
412
+ retryable: res.status >= 500
413
+ };
414
+ }
415
+ } catch (err) {
416
+ return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
354
417
  }
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");
360
418
  }
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
- ]);
419
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
420
+ async function waitForCliToken(endpoint, session, opts = {}) {
421
+ const sleep = opts.sleep ?? defaultSleep;
422
+ const now = opts.now ?? Date.now;
423
+ const intervalMs = (session.interval || 5) * 1e3;
424
+ const deadline = now() + (session.expires_in || 600) * 1e3;
425
+ const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
426
+ let consecutiveErrors = 0;
427
+ while (now() < deadline) {
428
+ await sleep(intervalMs);
429
+ const outcome = await pollDeviceToken(endpoint, session.device_code);
430
+ switch (outcome.status) {
431
+ case "approved":
432
+ return outcome.cliToken;
433
+ case "pending":
434
+ consecutiveErrors = 0;
435
+ opts.onPending?.();
436
+ continue;
437
+ case "denied":
438
+ throw new Error("Login was denied in the browser.");
439
+ case "expired":
440
+ throw new Error("The login code expired. Run sign-in again.");
441
+ case "error":
442
+ if (!outcome.retryable) {
443
+ throw new Error(outcome.message);
444
+ }
445
+ consecutiveErrors += 1;
446
+ if (consecutiveErrors >= maxConsecutiveErrors) {
447
+ throw new Error(outcome.message);
448
+ }
449
+ opts.onTransientError?.(outcome.message, consecutiveErrors);
450
+ continue;
451
+ }
452
+ }
453
+ throw new Error("Login timed out before approval. Run sign-in again.");
454
+ }
455
+ function cliAuthHeaders(cliToken) {
456
+ return {
457
+ "Content-Type": "application/json",
458
+ Authorization: `Bearer ${cliToken}`
459
+ };
460
+ }
461
+ async function listProjects(endpoint, cliToken) {
462
+ const base = trimTrailingSlash(endpoint);
463
+ try {
464
+ const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
465
+ const json = await res.json().catch(() => null);
466
+ if (res.ok && json?.ok) return json.data?.projects ?? [];
467
+ } catch {
468
+ }
469
+ return [];
470
+ }
471
+ async function createProject(endpoint, cliToken, name) {
472
+ const base = trimTrailingSlash(endpoint);
473
+ const res = await deviceFetch(`${base}/v1/cli/projects`, {
474
+ method: "POST",
475
+ headers: cliAuthHeaders(cliToken),
476
+ body: JSON.stringify({ name })
477
+ });
478
+ const json = await res.json().catch(() => null);
479
+ if (!res.ok || !json?.ok || !json.data) {
480
+ throw new Error(json?.error?.message ?? `Could not create project (HTTP ${res.status}).`);
481
+ }
482
+ const { id, name: created, slug, apiKey } = json.data;
483
+ return { id, name: created, slug, apiKey: apiKey ?? null };
484
+ }
485
+ async function mintProjectKey(endpoint, cliToken, projectId) {
486
+ const base = trimTrailingSlash(endpoint);
487
+ try {
488
+ const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
489
+ method: "POST",
490
+ headers: cliAuthHeaders(cliToken),
491
+ body: JSON.stringify({})
492
+ });
493
+ const json = await res.json().catch(() => null);
494
+ if (res.ok && json?.ok) return json.data?.key ?? null;
495
+ } catch {
496
+ }
497
+ return null;
368
498
  }
369
499
 
370
500
  // src/freshness.ts
@@ -428,7 +558,7 @@ function parse(version) {
428
558
 
429
559
  // src/monorepo.ts
430
560
  import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
431
- import { dirname, join as join2, resolve } from "path";
561
+ import { dirname as dirname2, join as join2, resolve } from "path";
432
562
  var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
433
563
  var FRAMEWORK_DEPS = {
434
564
  next: "Next.js",
@@ -456,7 +586,7 @@ function findWorkspaceRoot(start) {
456
586
  let dir = resolve(start);
457
587
  for (let i = 0; i < 8; i++) {
458
588
  if (isWorkspaceRoot(dir)) return dir;
459
- const parent = dirname(dir);
589
+ const parent = dirname2(dir);
460
590
  if (parent === dir) break;
461
591
  dir = parent;
462
592
  }
@@ -547,7 +677,10 @@ async function runInit(options = {}) {
547
677
  }
548
678
  const detected = detectFramework(cwd, pkg);
549
679
  const framework = await chooseFramework(detected, options);
550
- const credentials = await collectCredentials(options);
680
+ const consoleBase = await resolveConsoleUrl({ cwd });
681
+ const endpoint = resolveCloudEndpoint(options.endpoint);
682
+ const credentials = await acquireCredentials(options, consoleBase, endpoint);
683
+ await verifyCredentials(credentials, options, consoleBase);
551
684
  const pm = detectPackageManager(cwd);
552
685
  const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
553
686
  if (!options.skipInstall) {
@@ -555,12 +688,12 @@ async function runInit(options = {}) {
555
688
  } else {
556
689
  p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
557
690
  }
558
- const endpoint = resolveCloudEndpoint(options.endpoint);
559
691
  writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
560
692
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
561
693
  const enableRewards = await maybeEnableRewards(options);
562
- printNextSteps(framework, credentials.apiKey, credentials.projectId, enableRewards);
563
- await maybeSendTestReport(credentials, { ...options, endpoint });
694
+ printNextSteps(framework, consoleBase, enableRewards);
695
+ await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
696
+ await maybeOfferConnect(credentials, options, consoleBase);
564
697
  p.outro("Setup complete. Happy bug squashing \u{1F41B}");
565
698
  }
566
699
  function ensureInteractiveOrBailOut(options) {
@@ -601,25 +734,165 @@ async function chooseFramework(detected, options) {
601
734
  }
602
735
  return FRAMEWORKS[confirmed];
603
736
  }
604
- async function collectCredentials(options) {
737
+ async function acquireCredentials(options, consoleBase, endpoint) {
738
+ if (options.projectId && options.apiKey) {
739
+ return {
740
+ projectId: sanitizeSecret(options.projectId),
741
+ apiKey: sanitizeSecret(options.apiKey)
742
+ };
743
+ }
744
+ const existing = loadConfig();
745
+ if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
746
+ const reuse = options.yes ? true : await p.confirm({
747
+ message: "Use the Mushi credentials saved from your last sign-in?",
748
+ initialValue: true
749
+ });
750
+ if (p.isCancel(reuse)) {
751
+ p.cancel("Aborted.");
752
+ process.exit(0);
753
+ }
754
+ if (reuse) {
755
+ return {
756
+ projectId: sanitizeSecret(existing.projectId),
757
+ apiKey: sanitizeSecret(existing.apiKey)
758
+ };
759
+ }
760
+ }
761
+ if (options.yes) {
762
+ const creds = await runBrowserSignIn(options, endpoint);
763
+ if (creds) return creds;
764
+ p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
765
+ } else {
766
+ const method = await p.select({
767
+ message: "Connect this app to Mushi",
768
+ initialValue: "browser",
769
+ options: [
770
+ {
771
+ value: "browser",
772
+ label: "Sign in with your browser",
773
+ hint: "Recommended \u2014 no copy-paste, creates the project + key for you"
774
+ },
775
+ {
776
+ value: "manual",
777
+ label: "Paste a Project ID + API key",
778
+ hint: "Self-hosted or expert setup"
779
+ }
780
+ ]
781
+ });
782
+ if (p.isCancel(method)) {
783
+ p.cancel("Aborted.");
784
+ process.exit(0);
785
+ }
786
+ if (method === "browser") {
787
+ const creds = await runBrowserSignIn(options, endpoint);
788
+ if (creds) return creds;
789
+ p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
790
+ }
791
+ }
792
+ return collectCredentialsManually(options, consoleBase);
793
+ }
794
+ async function runBrowserSignIn(options, endpoint) {
795
+ const startSpin = p.spinner();
796
+ startSpin.start("Starting secure browser sign-in\u2026");
797
+ let session;
798
+ try {
799
+ session = await startDeviceAuth(endpoint);
800
+ } catch (err) {
801
+ startSpin.stop("Could not start browser sign-in.");
802
+ p.log.warn(err instanceof Error ? err.message : String(err));
803
+ return null;
804
+ }
805
+ startSpin.stop("Browser sign-in ready.");
806
+ p.log.step(`Confirmation code: ${session.user_code}`);
807
+ p.log.info(`Opening ${session.verification_uri}`);
808
+ try {
809
+ await openInBrowser(session.verification_uri);
810
+ } catch {
811
+ }
812
+ p.log.message("Approve the request in your browser to continue. (Ctrl+C to cancel)");
813
+ const waitSpin = p.spinner();
814
+ waitSpin.start("Waiting for you to approve in the browser\u2026");
815
+ let cliToken;
816
+ try {
817
+ cliToken = await waitForCliToken(endpoint, session);
818
+ } catch (err) {
819
+ waitSpin.stop("Browser sign-in didn't complete.");
820
+ p.log.warn(err instanceof Error ? err.message : String(err));
821
+ return null;
822
+ }
823
+ waitSpin.stop("Approved.");
824
+ let projectId = options.projectId ? sanitizeSecret(options.projectId) : void 0;
825
+ let apiKey;
826
+ if (!projectId) {
827
+ let projects = [];
828
+ try {
829
+ projects = await listProjects(endpoint, cliToken);
830
+ } catch {
831
+ }
832
+ const NEW = "__new__";
833
+ const choice = await p.select({
834
+ message: "Choose a project",
835
+ initialValue: projects[0]?.id ?? NEW,
836
+ options: [
837
+ ...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
838
+ { value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
839
+ ]
840
+ });
841
+ if (p.isCancel(choice)) {
842
+ p.cancel("Aborted.");
843
+ process.exit(0);
844
+ }
845
+ if (choice === NEW) {
846
+ const name = await p.text({
847
+ message: "Project name",
848
+ placeholder: "My app",
849
+ validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
850
+ });
851
+ if (p.isCancel(name)) {
852
+ p.cancel("Aborted.");
853
+ process.exit(0);
854
+ }
855
+ const createSpin = p.spinner();
856
+ createSpin.start(`Creating "${name.trim()}"\u2026`);
857
+ try {
858
+ const created = await createProject(endpoint, cliToken, name.trim());
859
+ projectId = created.id;
860
+ apiKey = created.apiKey ?? void 0;
861
+ createSpin.stop(`Created project "${created.name}".`);
862
+ } catch (err) {
863
+ createSpin.stop("Could not create the project.");
864
+ p.log.warn(err instanceof Error ? err.message : String(err));
865
+ return null;
866
+ }
867
+ } else {
868
+ projectId = choice;
869
+ }
870
+ }
871
+ if (projectId && !apiKey) {
872
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
873
+ }
874
+ if (!projectId || !apiKey) return null;
875
+ return { apiKey, projectId };
876
+ }
877
+ async function collectCredentialsManually(options, consoleBase) {
605
878
  const existing = loadConfig();
606
879
  const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
607
880
  message: "Project ID",
608
881
  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."
882
+ hint: projectIdHint(consoleBase),
883
+ 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
884
  });
612
885
  const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
613
886
  message: "API key",
614
887
  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.",
888
+ hint: apiKeyHint(consoleBase),
616
889
  validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
617
890
  });
618
891
  const projectId = sanitizeSecret(rawProjectId);
619
892
  const apiKey = sanitizeSecret(rawApiKey);
620
893
  if (!PROJECT_ID_PATTERN.test(projectId)) {
621
894
  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`
895
+ `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
896
  );
624
897
  }
625
898
  if (!API_KEY_PATTERN.test(apiKey)) {
@@ -629,6 +902,23 @@ async function collectCredentials(options) {
629
902
  }
630
903
  return { projectId, apiKey };
631
904
  }
905
+ async function verifyCredentials(credentials, options, consoleBase) {
906
+ const endpoint = resolveCloudEndpoint(options.endpoint);
907
+ const spinner3 = p.spinner();
908
+ spinner3.start("Verifying credentials\u2026");
909
+ const result = await apiCall(
910
+ "/v1/sync/whoami",
911
+ { apiKey: credentials.apiKey, projectId: credentials.projectId, endpoint }
912
+ );
913
+ if (!result.ok) {
914
+ spinner3.stop("Credentials could not be verified.");
915
+ p.log.error(result.error?.message ?? "Authentication failed.");
916
+ p.log.info(`Double-check values in the console:
917
+ ${cliSetupDeepLink(consoleBase)}`);
918
+ throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
919
+ }
920
+ spinner3.stop(`Connected to ${result.data.project_name}`);
921
+ }
632
922
  function sanitizeSecret(raw) {
633
923
  return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[\r\n\0]/g, "");
634
924
  }
@@ -671,7 +961,7 @@ async function installPackages(pm, packages, cwd) {
671
961
  function runCommand(pm, packages, cwd) {
672
962
  const verb = pm === "npm" ? "install" : "add";
673
963
  const command = process.platform === "win32" ? `${pm}.cmd` : pm;
674
- return new Promise((resolve4, reject) => {
964
+ return new Promise((resolve3, reject) => {
675
965
  const child = spawn(command, [verb, ...packages], {
676
966
  stdio: "inherit",
677
967
  shell: false,
@@ -680,7 +970,7 @@ function runCommand(pm, packages, cwd) {
680
970
  });
681
971
  child.on("error", reject);
682
972
  child.on("exit", (code) => {
683
- if (code === 0) resolve4();
973
+ if (code === 0) resolve3();
684
974
  else reject(new Error(`${pm} exited with code ${code ?? "null"}`));
685
975
  });
686
976
  });
@@ -738,7 +1028,7 @@ function persistCliConfig(apiKey, projectId, endpoint) {
738
1028
  const existing = loadConfig();
739
1029
  saveConfig({ ...existing, apiKey, projectId, endpoint });
740
1030
  }
741
- function printNextSteps(framework, _apiKey, _projectId, enableRewards = false) {
1031
+ function printNextSteps(framework, consoleBase, enableRewards = false) {
742
1032
  p.note(framework.snippet(), "Add this to your app:");
743
1033
  if (enableRewards) {
744
1034
  const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
@@ -749,13 +1039,44 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
749
1039
  // import { MushiRewardsBadge } from '@mushi-mushi/react';
750
1040
  // <MushiRewardsBadge showPoints />`;
751
1041
  p.note(badgeSnippet, "Rewards badge snippet:");
752
- p.log.info("Enable rewards in your project settings at https://kensaur.us/mushi-mushi/rewards");
1042
+ p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
753
1043
  p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
754
1044
  }
755
1045
  p.log.message("Verify the install:");
756
1046
  p.log.message(" \u2022 Start your dev server");
757
1047
  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");
1048
+ p.log.message(` \u2022 Submit a test report \u2014 it should appear at ${reportsUrl(consoleBase)}`);
1049
+ p.log.info(
1050
+ "Tip: `mushi connect --write-env --wire-ide --wait` also wires Cursor MCP and waits for SDK heartbeat."
1051
+ );
1052
+ }
1053
+ async function maybeOfferConnect(credentials, options, _consoleBase) {
1054
+ if (options.yes) return;
1055
+ const answer = await p.confirm({
1056
+ message: "Run `mushi connect --write-env --wire-ide --wait` now? (SDK env + Cursor MCP + heartbeat check)",
1057
+ initialValue: false
1058
+ });
1059
+ if (p.isCancel(answer) || !answer) return;
1060
+ const endpoint = resolveCloudEndpoint(options.endpoint);
1061
+ try {
1062
+ const { runConnect: runConnect2 } = await import("./connect-W2Q6N7K3.js");
1063
+ await runConnect2({
1064
+ apiKey: credentials.apiKey,
1065
+ projectId: credentials.projectId,
1066
+ endpoint,
1067
+ cwd: options.cwd,
1068
+ writeEnv: true,
1069
+ wireIde: true,
1070
+ wait: true
1071
+ });
1072
+ } catch (err) {
1073
+ p.log.warn(
1074
+ err instanceof Error ? err.message : String(err)
1075
+ );
1076
+ p.log.info(
1077
+ "You can run manually: mushi connect --write-env --wire-ide --wait"
1078
+ );
1079
+ }
759
1080
  }
760
1081
  async function maybeEnableRewards(options) {
761
1082
  if (options.yes) return false;
@@ -826,8 +1147,8 @@ async function maybeSendTestReport(credentials, options) {
826
1147
  reportId = body.data?.reportId;
827
1148
  } catch {
828
1149
  }
829
- const reportPath = reportId ? `/reports/${reportId}` : "/reports";
830
- p.log.success(`View it at https://kensaur.us/mushi-mushi/admin${reportPath}`);
1150
+ const consoleBase = options.consoleBase ?? resolveConsoleUrlSync(options.cwd);
1151
+ p.log.success(`View it at ${reportsUrl(consoleBase, reportId)}`);
831
1152
  } catch (err) {
832
1153
  const aborted = err instanceof Error && err.name === "AbortError";
833
1154
  spinner3.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
@@ -1023,78 +1344,189 @@ function registerAccountCommands(program2) {
1023
1344
  const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
1024
1345
  if (matches.length === 0) process.exit(1);
1025
1346
  });
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", `
1347
+ 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
1348
  Examples:
1028
- mushi login # browser-guided: open console \u2192 paste key
1029
- mushi login --api-key mushi_xxx # non-interactive: save key directly
1349
+ mushi login # browser-guided (recommended)
1350
+ mushi login --api-key mushi_xxx # non-interactive / CI
1030
1351
  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-XH3XYIS7.js");
1033
- const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
1034
- const consoleUrl = "https://kensaur.us/mushi-mushi/projects";
1352
+ const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1353
+ const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
1354
+ const consoleBase = await resolveConsoleUrl();
1035
1355
  let apiKey = opts.apiKey;
1036
1356
  let projectId = opts.projectId;
1037
- if (!apiKey) {
1038
- const signInUrl = "https://kensaur.us/mushi-mushi/sign-in";
1357
+ if (apiKey) {
1358
+ const config2 = loadConfig();
1359
+ config2.apiKey = apiKey;
1360
+ config2.endpoint = endpoint;
1361
+ if (projectId) config2.projectId = projectId;
1362
+ config2.consoleUrl = consoleBase;
1363
+ saveConfig(config2);
1039
1364
  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 {
1365
+ console.log(" \u2713 Credentials saved.");
1366
+ const verifyResult = await apiCall(
1367
+ "/v1/sync/whoami",
1368
+ { apiKey, endpoint, projectId }
1369
+ );
1370
+ if (!verifyResult.ok) {
1371
+ console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
1372
+ console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
1373
+ return;
1374
+ }
1375
+ const d = verifyResult.data;
1376
+ if (d.project_name) {
1377
+ if (!projectId && d.project_id) {
1378
+ config2.projectId = d.project_id;
1379
+ saveConfig(config2);
1049
1380
  }
1050
- } else {
1051
- console.log(` Sign in at: ${signInUrl}`);
1381
+ console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
1382
+ }
1383
+ console.log(` \u2713 Endpoint: ${endpoint}`);
1384
+ console.log("");
1385
+ console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1386
+ return;
1387
+ }
1388
+ console.log("");
1389
+ console.log(" Mushi login");
1390
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1391
+ let deviceData;
1392
+ try {
1393
+ deviceData = await startDeviceAuth(endpoint);
1394
+ } catch (err) {
1395
+ process.stderr.write(`
1396
+ error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
1397
+ `);
1398
+ process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
1399
+ `);
1400
+ process.exit(1);
1401
+ }
1402
+ const verifyUrl = deviceData.verification_uri;
1403
+ console.log("");
1404
+ console.log(` Your confirmation code: ${deviceData.user_code}`);
1405
+ console.log("");
1406
+ if (opts.browser !== false) {
1407
+ console.log(" Opening the Mushi console in your browser\u2026");
1408
+ try {
1409
+ await openInBrowser(verifyUrl);
1410
+ } catch {
1411
+ }
1412
+ }
1413
+ console.log(` If the browser didn't open: ${verifyUrl}`);
1414
+ console.log("");
1415
+ console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
1416
+ let cliToken = null;
1417
+ const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
1418
+ const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
1419
+ const MAX_CONSECUTIVE_ERRORS = 5;
1420
+ let consecutiveErrors = 0;
1421
+ while (Date.now() < deadline) {
1422
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1423
+ const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
1424
+ if (outcome.status === "approved") {
1425
+ cliToken = outcome.cliToken;
1426
+ break;
1427
+ }
1428
+ if (outcome.status === "pending") {
1429
+ consecutiveErrors = 0;
1430
+ process.stdout.write(".");
1431
+ continue;
1432
+ }
1433
+ if (outcome.status === "error") {
1434
+ if (outcome.retryable) {
1435
+ consecutiveErrors += 1;
1436
+ if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) {
1437
+ process.stdout.write("\xB7");
1438
+ continue;
1439
+ }
1440
+ }
1441
+ console.log("");
1442
+ process.stderr.write(`
1443
+ error: ${outcome.message}
1444
+ `);
1445
+ process.exit(1);
1052
1446
  }
1053
1447
  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.");
1448
+ if (outcome.status === "denied") {
1449
+ process.stderr.write("\nerror: Login denied in the browser.\n");
1450
+ } else if (outcome.status === "expired") {
1451
+ process.stderr.write("\nerror: Login code expired. Run mushi login again.\n");
1452
+ }
1453
+ process.exit(1);
1454
+ }
1455
+ if (!cliToken) {
1056
1456
  console.log("");
1457
+ process.stderr.write("\nerror: Login timed out. Run mushi login again.\n");
1458
+ process.exit(1);
1459
+ }
1460
+ console.log("");
1461
+ console.log(" \u2713 Approved!");
1462
+ const projectsList = await listProjects(endpoint, cliToken);
1463
+ let chosenProjectId = projectId;
1464
+ let chosenProjectName;
1465
+ if (!chosenProjectId) {
1057
1466
  const { createInterface } = await import("readline");
1058
1467
  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;
1468
+ const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
1469
+ if (projectsList.length > 0) {
1470
+ console.log("");
1471
+ console.log(" Your projects:");
1472
+ projectsList.forEach((p3, i) => {
1473
+ console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
1474
+ });
1475
+ console.log(` ${projectsList.length + 1}. Create a new project`);
1476
+ console.log("");
1477
+ const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
1478
+ const num = parseInt(choice, 10);
1479
+ if (num >= 1 && num <= projectsList.length) {
1480
+ const picked = projectsList[num - 1];
1481
+ chosenProjectId = picked.id;
1482
+ chosenProjectName = picked.name;
1483
+ }
1064
1484
  }
1065
- rl.close();
1066
- if (!apiKey) {
1067
- process.stderr.write("\nerror: API key is required.\n");
1068
- process.exit(2);
1485
+ if (!chosenProjectId) {
1486
+ console.log("");
1487
+ const newName = await ask(" Project name: ");
1488
+ rl.close();
1489
+ if (!newName.trim()) {
1490
+ process.stderr.write("\nerror: Project name is required.\n");
1491
+ process.exit(2);
1492
+ }
1493
+ try {
1494
+ const created = await createProject(endpoint, cliToken, newName.trim());
1495
+ chosenProjectId = created.id;
1496
+ chosenProjectName = created.name;
1497
+ apiKey = created.apiKey ?? void 0;
1498
+ console.log(` \u2713 Created project "${chosenProjectName}"`);
1499
+ } catch (err) {
1500
+ process.stderr.write(`
1501
+ error: Could not create project: ${err instanceof Error ? err.message : String(err)}
1502
+ `);
1503
+ process.exit(1);
1504
+ }
1505
+ } else {
1506
+ rl.close();
1507
+ }
1508
+ }
1509
+ if (!apiKey && chosenProjectId) {
1510
+ try {
1511
+ apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId) ?? void 0;
1512
+ } catch {
1069
1513
  }
1070
1514
  }
1071
1515
  const config = loadConfig();
1072
- config.apiKey = apiKey;
1073
1516
  config.endpoint = endpoint;
1074
- if (projectId) config.projectId = projectId;
1517
+ config.consoleUrl = consoleBase;
1518
+ if (chosenProjectId) config.projectId = chosenProjectId;
1519
+ if (apiKey) config.apiKey = apiKey;
1075
1520
  saveConfig(config);
1076
1521
  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})`);
1522
+ if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
1523
+ if (apiKey) {
1524
+ console.log(` \u2713 SDK key saved (${apiKey.slice(0, 12)}\u2026)`);
1525
+ } else {
1526
+ console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
1094
1527
  }
1095
- console.log(` \u2713 Endpoint: ${endpoint}`);
1096
1528
  console.log("");
1097
- console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1529
+ console.log(" Run 'mushi init' to set up the SDK in this project.");
1098
1530
  });
1099
1531
  program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1100
1532
  Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
@@ -1617,7 +2049,7 @@ AI code review context.
1617
2049
  Typical CI usage:
1618
2050
  MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
1619
2051
  npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
1620
- const { writeFile: writeFile3, mkdir: mkdir2 } = await import("fs/promises");
2052
+ const { writeFile, mkdir } = await import("fs/promises");
1621
2053
  const nodePath = await import("path");
1622
2054
  const config = requireConfig();
1623
2055
  const cwd = opts.cwd ?? process.cwd();
@@ -1644,8 +2076,8 @@ Typical CI usage:
1644
2076
  console.log(JSON.stringify(output, null, 2));
1645
2077
  return;
1646
2078
  }
1647
- await mkdir2(nodePath.dirname(target), { recursive: true });
1648
- await writeFile3(target, JSON.stringify(output, null, 2) + "\n", "utf8");
2079
+ await mkdir(nodePath.dirname(target), { recursive: true });
2080
+ await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1649
2081
  if (opts.json) {
1650
2082
  console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
1651
2083
  } else {
@@ -1677,11 +2109,11 @@ async function findMapFiles(dir) {
1677
2109
  return results;
1678
2110
  }
1679
2111
  function fileHash(path) {
1680
- return new Promise((resolve4, reject) => {
2112
+ return new Promise((resolve3, reject) => {
1681
2113
  const hash = createHash("sha256");
1682
2114
  const stream = createReadStream(path);
1683
2115
  stream.on("data", (chunk) => hash.update(chunk));
1684
- stream.on("end", () => resolve4(hash.digest("hex")));
2116
+ stream.on("end", () => resolve3(hash.digest("hex")));
1685
2117
  stream.on("error", reject);
1686
2118
  });
1687
2119
  }
@@ -1828,7 +2260,7 @@ function registerDiagnosticsCommands(program2) {
1828
2260
  console.log(`\u2713 Test report submitted`);
1829
2261
  console.log(` ID: ${d.reportId}`);
1830
2262
  console.log(` Status: ${d.status}`);
1831
- console.log(` View: https://kensaur.us/mushi-mushi/reports/${d.reportId}`);
2263
+ console.log(` View: ${reportsUrl(resolveConsoleUrlSync(), d.reportId)}`);
1832
2264
  }
1833
2265
  });
1834
2266
  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 +2272,7 @@ Examples:
1840
2272
  mushi index ./src
1841
2273
  mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
1842
2274
  const config = requireConfig({ needsProject: true });
1843
- const { readdir: readdir2, readFile: readFile4, stat } = await import("fs/promises");
2275
+ const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
1844
2276
  const nodePath = await import("path");
1845
2277
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
1846
2278
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
@@ -1867,7 +2299,7 @@ Examples:
1867
2299
  `);
1868
2300
  continue;
1869
2301
  }
1870
- const source = await readFile4(file, "utf8");
2302
+ const source = await readFile2(file, "utf8");
1871
2303
  const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
1872
2304
  count++;
1873
2305
  bytes += source.length;
@@ -1914,108 +2346,127 @@ Examples:
1914
2346
  });
1915
2347
  }
1916
2348
 
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
2349
  // src/commands/project.ts
1968
2350
  function registerProjectCommands(program2) {
1969
2351
  const project = program2.command("project").description("Project management");
1970
- 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,
1972
- and writes the following to the current directory:
2352
+ project.command("create").description("Create or select a Mushi project via browser sign-in, then write config files").option("--name <name>", "Project name for a new project (skips the prompt)").option("--no-browser", "Print the verification URL instead of opening the browser").option("--endpoint <url>", "Override API endpoint (self-hosted)").addHelpText("after", `
2353
+ Signs you in through the browser (no copy-paste), creates or selects a project,
2354
+ mints a report:write SDK key, and writes the following to the current directory:
1973
2355
  .env.local \u2014 MUSHI_API_KEY, MUSHI_PROJECT_ID, MUSHI_API_ENDPOINT
1974
2356
  .cursor/mcp.json \u2014 pre-filled mcpServers.mushi block for Cursor
1975
2357
 
1976
2358
  Typical first-time flow:
1977
2359
  npx mushi-mushi project create
1978
- # Browser opens \u2192 sign up / magic-link \u2192 come back to terminal
2360
+ # Browser opens \u2192 click Approve \u2192 pick or create a project
1979
2361
  # CLI writes .env.local and .cursor/mcp.json
1980
2362
  # mushi whoami to confirm`).action(async (opts) => {
1981
- const { writeFile: writeFile3 } = await import("fs/promises");
2363
+ const { writeFile } = await import("fs/promises");
1982
2364
  const { existsSync: existsSync4 } = await import("fs");
1983
2365
  const nodePath = await import("path");
1984
- const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
1985
- const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
2366
+ const savedConfig = loadConfig();
2367
+ const endpoint = resolveCloudEndpoint(
2368
+ opts.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? savedConfig.endpoint
2369
+ );
2370
+ const consoleBase = await resolveConsoleUrl();
1986
2371
  console.log("");
1987
2372
  console.log(" Mushi project create");
1988
2373
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1989
2374
  console.log("");
2375
+ let session;
2376
+ try {
2377
+ session = await startDeviceAuth(endpoint);
2378
+ } catch (err) {
2379
+ process.stderr.write(`
2380
+ error: Could not start browser sign-in: ${err instanceof Error ? err.message : String(err)}
2381
+ `);
2382
+ process.stderr.write(" Fallback: mushi login --api-key <key> --project-id <uuid>\n");
2383
+ process.exit(1);
2384
+ }
2385
+ console.log(` Confirmation code: ${session.user_code}`);
2386
+ console.log("");
1990
2387
  if (opts.browser !== false) {
1991
- console.log(" 1. Opening the Mushi sign-up page in your browser...");
2388
+ console.log(" Opening the Mushi console in your browser\u2026");
1992
2389
  try {
1993
- const { exec } = await import("child_process");
1994
- const openCmd = process.platform === "win32" ? `start "" "${signUpUrl}"` : process.platform === "darwin" ? `open "${signUpUrl}"` : `xdg-open "${signUpUrl}"`;
1995
- exec(openCmd);
2390
+ await openInBrowser(session.verification_uri);
1996
2391
  } catch {
1997
2392
  }
1998
- } else {
1999
- console.log(` 1. Sign up or log in at: ${signUpUrl}`);
2000
2393
  }
2394
+ console.log(` If the browser didn't open: ${session.verification_uri}`);
2001
2395
  console.log("");
2002
- console.log(" 2. Create a project in the console, then paste your credentials below.");
2003
- console.log(" (Settings \u2192 API Keys \u2192 New key \u2192 Copy as .env.local)");
2396
+ console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
2397
+ let cliToken;
2398
+ try {
2399
+ cliToken = await waitForCliToken(endpoint, session, {
2400
+ onPending: () => process.stdout.write("."),
2401
+ onTransientError: () => process.stdout.write("\xB7")
2402
+ });
2403
+ } catch (err) {
2404
+ console.log("");
2405
+ process.stderr.write(`
2406
+ error: ${err instanceof Error ? err.message : String(err)}
2407
+ `);
2408
+ process.exit(1);
2409
+ }
2004
2410
  console.log("");
2411
+ console.log(" \u2713 Approved!");
2412
+ const projectsList = await listProjects(endpoint, cliToken);
2005
2413
  const { createInterface } = await import("readline");
2006
2414
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2007
- const ask = (q) => new Promise((resolve4) => rl.question(q, (a) => resolve4(a.trim())));
2008
- const projectId = await ask(" Project ID (uuid): ");
2009
- const apiKey = await ask(" API key (mushi_...): ");
2415
+ const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
2416
+ let chosen;
2417
+ let projectId;
2418
+ let projectName;
2419
+ let apiKey;
2420
+ if (!opts.name && projectsList.length > 0) {
2421
+ console.log("");
2422
+ console.log(" Your projects:");
2423
+ projectsList.forEach((pr, i) => {
2424
+ console.log(` ${i + 1}. ${pr.name} (${pr.id})`);
2425
+ });
2426
+ console.log(` ${projectsList.length + 1}. Create a new project`);
2427
+ console.log("");
2428
+ const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
2429
+ const num = parseInt(choice, 10);
2430
+ if (num >= 1 && num <= projectsList.length) {
2431
+ chosen = projectsList[num - 1];
2432
+ projectId = chosen.id;
2433
+ projectName = chosen.name;
2434
+ }
2435
+ }
2436
+ if (!projectId) {
2437
+ const newName = opts.name?.trim() || await ask(" Project name: ");
2438
+ if (!newName) {
2439
+ rl.close();
2440
+ process.stderr.write("\nerror: Project name is required.\n");
2441
+ process.exit(2);
2442
+ }
2443
+ try {
2444
+ const created = await createProject(endpoint, cliToken, newName);
2445
+ projectId = created.id;
2446
+ projectName = created.name;
2447
+ apiKey = created.apiKey ?? void 0;
2448
+ console.log(` \u2713 Created project "${projectName}"`);
2449
+ } catch (err) {
2450
+ rl.close();
2451
+ process.stderr.write(`
2452
+ error: Could not create project: ${err instanceof Error ? err.message : String(err)}
2453
+ `);
2454
+ process.exit(1);
2455
+ }
2456
+ }
2010
2457
  rl.close();
2458
+ if (projectId && !apiKey) {
2459
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
2460
+ }
2011
2461
  if (!projectId || !apiKey) {
2012
- process.stderr.write("\nerror: Project ID and API key are required.\n");
2013
- process.exit(2);
2462
+ process.stderr.write("\nerror: Could not obtain an SDK key for the project. Run `mushi login` and try again.\n");
2463
+ process.exit(1);
2014
2464
  }
2015
2465
  const config = loadConfig();
2016
2466
  config.apiKey = apiKey;
2017
2467
  config.endpoint = endpoint;
2018
2468
  config.projectId = projectId;
2469
+ config.consoleUrl = consoleBase;
2019
2470
  saveConfig(config);
2020
2471
  const cwd = process.cwd();
2021
2472
  const envPath = nodePath.join(cwd, ".env.local");
@@ -2027,7 +2478,7 @@ Typical first-time flow:
2027
2478
  ""
2028
2479
  ];
2029
2480
  const envExisting = existsSync4(envPath);
2030
- await writeFile3(envPath, envLines.join("\n"), "utf8");
2481
+ await writeFile(envPath, envLines.join("\n"), "utf8");
2031
2482
  console.log(`
2032
2483
  \u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
2033
2484
  const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
@@ -2044,7 +2495,7 @@ Typical first-time flow:
2044
2495
 
2045
2496
  // src/commands/setup.ts
2046
2497
  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", `
2498
+ 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
2499
  Examples:
2049
2500
  mushi setup # wire Cursor (default)
2050
2501
  mushi setup --all-projects # one server entry per accessible project
@@ -2058,7 +2509,7 @@ Supported IDEs:
2058
2509
  zed \u2014 writes ~/.config/zed/settings.json mcpServers block
2059
2510
 
2060
2511
  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");
2512
+ const { writeFile, mkdir, readFile: readFile2 } = await import("fs/promises");
2062
2513
  const { existsSync: existsSync4 } = await import("fs");
2063
2514
  const nodePath = await import("path");
2064
2515
  const os = await import("os");
@@ -2173,7 +2624,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2173
2624
  let settings = {};
2174
2625
  if (existsSync4(configPath)) {
2175
2626
  try {
2176
- const raw = await readFile4(configPath, "utf8");
2627
+ const raw = await readFile2(configPath, "utf8");
2177
2628
  settings = JSON.parse(raw);
2178
2629
  } catch {
2179
2630
  }
@@ -2197,8 +2648,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2197
2648
  console.log(`[dry-run] Would write ${configPath}:`);
2198
2649
  console.log(redactKeyForDisplay(output));
2199
2650
  } else {
2200
- await mkdir2(configDir, { recursive: true });
2201
- await writeFile3(configPath, output, "utf8");
2651
+ await mkdir(configDir, { recursive: true });
2652
+ await writeFile(configPath, output, "utf8");
2202
2653
  console.log(`\u2713 Written ${configPath}`);
2203
2654
  }
2204
2655
  }
@@ -2232,7 +2683,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2232
2683
  if (opts.dryRun) {
2233
2684
  console.log(`[dry-run] Would write ${rulesPath}`);
2234
2685
  } else {
2235
- await writeFile3(rulesPath, rulesContent, "utf8");
2686
+ await writeFile(rulesPath, rulesContent, "utf8");
2236
2687
  console.log(`\u2713 Written .cursorrules`);
2237
2688
  }
2238
2689
  } else if (opts.ide === "claude") {
@@ -2241,8 +2692,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2241
2692
  if (opts.dryRun) {
2242
2693
  console.log(`[dry-run] Would write ${rulesPath}`);
2243
2694
  } else {
2244
- await mkdir2(rulesDir, { recursive: true });
2245
- await writeFile3(rulesPath, rulesContent, "utf8");
2695
+ await mkdir(rulesDir, { recursive: true });
2696
+ await writeFile(rulesPath, rulesContent, "utf8");
2246
2697
  console.log(`\u2713 Written .claude/rules/mushi.md`);
2247
2698
  }
2248
2699
  }
@@ -2370,117 +2821,6 @@ function renderNudgeExplainer(phase) {
2370
2821
  return lines.join("\n");
2371
2822
  }
2372
2823
 
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
2824
  // src/doctor.ts
2485
2825
  function checkCliConfig(config) {
2486
2826
  return [
@@ -2519,11 +2859,11 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
2519
2859
  }
2520
2860
  async function checkSdkInstall(cwd) {
2521
2861
  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"));
2862
+ const { readFile: readFile2 } = await import("fs/promises");
2863
+ const { join: join5, resolve: resolve3 } = await import("path");
2864
+ const root = resolve3(cwd);
2865
+ const pkgPath = join5(root, "package.json");
2866
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
2527
2867
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2528
2868
  const sdks = [
2529
2869
  "@mushi-mushi/react",
@@ -2531,11 +2871,11 @@ async function checkSdkInstall(cwd) {
2531
2871
  "@mushi-mushi/core",
2532
2872
  "@mushi-mushi/react-native"
2533
2873
  ];
2534
- const installed = sdks.filter((s) => deps[s]);
2874
+ const installed2 = sdks.filter((s) => deps[s]);
2535
2875
  return {
2536
2876
  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"
2877
+ ok: installed2.length > 0,
2878
+ 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
2879
  };
2540
2880
  } catch {
2541
2881
  return null;
@@ -2701,18 +3041,18 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
2701
3041
  async function checkHostAppWiring(cwd) {
2702
3042
  const checks = [];
2703
3043
  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"));
3044
+ const { readFile: readFile2, access } = await import("fs/promises");
3045
+ const { join: join5, resolve: resolve3 } = await import("path");
3046
+ const root = resolve3(cwd);
3047
+ const pkgPath = join5(root, "package.json");
3048
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
2709
3049
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2710
3050
  const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
2711
3051
  const envCandidates = [".env.local", ".env"];
2712
3052
  let envContent = "";
2713
3053
  for (const f of envCandidates) {
2714
3054
  try {
2715
- envContent = await readFile4(join6(root, f), "utf8");
3055
+ envContent = await readFile2(join5(root, f), "utf8");
2716
3056
  break;
2717
3057
  } catch {
2718
3058
  }
@@ -2726,7 +3066,7 @@ async function checkHostAppWiring(cwd) {
2726
3066
  });
2727
3067
  let mcpPresent = false;
2728
3068
  try {
2729
- await access(join6(root, ".cursor", "mcp.json"));
3069
+ await access(join5(root, ".cursor", "mcp.json"));
2730
3070
  mcpPresent = true;
2731
3071
  } catch {
2732
3072
  }
@@ -2759,19 +3099,19 @@ async function checkHostAppWiring(cwd) {
2759
3099
  }
2760
3100
  async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
2761
3101
  const checks = [];
2762
- const { readFile: readFile4 } = await import("fs/promises");
2763
- const { join: join6, resolve: resolve4 } = await import("path");
3102
+ const { readFile: readFile2 } = await import("fs/promises");
3103
+ const { join: join5, resolve: resolve3 } = await import("path");
2764
3104
  const { homedir } = await import("os");
2765
- const root = resolve4(cwd);
3105
+ const root = resolve3(cwd);
2766
3106
  const candidates = [
2767
- join6(root, ".cursor", "mcp.json"),
2768
- join6(homedir(), ".cursor", "mcp.json")
3107
+ join5(root, ".cursor", "mcp.json"),
3108
+ join5(homedir(), ".cursor", "mcp.json")
2769
3109
  ];
2770
3110
  let mcpPath = null;
2771
3111
  let mcpRaw = null;
2772
3112
  for (const candidate of candidates) {
2773
3113
  try {
2774
- mcpRaw = await readFile4(candidate, "utf8");
3114
+ mcpRaw = await readFile2(candidate, "utf8");
2775
3115
  mcpPath = candidate;
2776
3116
  break;
2777
3117
  } catch {
@@ -2958,9 +3298,9 @@ async function planUpgrade(cwd) {
2958
3298
  ...pkg?.dependencies ?? {},
2959
3299
  ...pkg?.devDependencies ?? {}
2960
3300
  };
2961
- const installed = MUSHI_PACKAGES.filter((name) => deps[name]);
3301
+ const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
2962
3302
  const entries = await Promise.all(
2963
- installed.map(async (name) => {
3303
+ installed2.map(async (name) => {
2964
3304
  const current = deps[name] ?? "";
2965
3305
  const currentCore = current.replace(/^[\^~>=<]*/, "");
2966
3306
  if (!/^\d/.test(currentCore)) {
@@ -3031,136 +3371,6 @@ async function runUpgrade(opts = {}) {
3031
3371
  };
3032
3372
  }
3033
3373
 
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
3374
  // src/commands/fix.ts
3165
3375
  function registerFixCommands(program2) {
3166
3376
  const fixCmd = program2.command("fix").description("Dispatch an agentic fix for a report");
@@ -3302,7 +3512,7 @@ Examples:
3302
3512
  }
3303
3513
  if (result.plan.entries.length === 0) process.exit(1);
3304
3514
  });
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", `
3515
+ 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", "Force-write SDK env vars to .env.local (default; overrides --no-env)").option("--wire-ide", "Force-wire Cursor MCP into .cursor/mcp.json (default; overrides --no-ide)").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
3516
  Examples:
3307
3517
  MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
3308
3518
  mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
@@ -3316,8 +3526,10 @@ Examples:
3316
3526
  projectId: opts.projectId,
3317
3527
  endpoint: opts.endpoint,
3318
3528
  cwd: opts.cwd,
3319
- writeEnv: opts.env !== false,
3320
- wireIde: opts.ide !== false,
3529
+ // Explicit --write-env / --wire-ide force the action on even if --no-env /
3530
+ // --no-ide was also passed; otherwise writing is the default.
3531
+ writeEnv: opts.writeEnv === true ? true : opts.env !== false,
3532
+ wireIde: opts.wireIde === true ? true : opts.ide !== false,
3321
3533
  wait: opts.wait,
3322
3534
  waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
3323
3535
  json: opts.json
@@ -3987,9 +4199,9 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
3987
4199
  }
3988
4200
  }
3989
4201
  }
3990
- const consoleUrl = config.consoleUrl ?? "https://app.mushi.ai";
4202
+ const consoleUrl2 = config.consoleUrl ?? "https://app.mushi.ai";
3991
4203
  console.log(`
3992
- Open in console: ${consoleUrl}/qa-coverage?story=${storyId}`);
4204
+ Open in console: ${consoleUrl2}/qa-coverage?story=${storyId}`);
3993
4205
  console.log(` Tip: run 'mushi config consoleUrl <url>' to override (e.g. http://localhost:6464 for local dev)`);
3994
4206
  console.log();
3995
4207
  });
@@ -4433,8 +4645,8 @@ ${"\u2500".repeat(60)}`);
4433
4645
  installSignalHandlers();
4434
4646
  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
4647
  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)
4648
+ MUSHI_API_KEY SDK ingest key (report:write scope \u2014 from Onboarding \u2192 Verify in the console)
4649
+ MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
4438
4650
  MUSHI_API_ENDPOINT Supabase edge function URL
4439
4651
  e.g. https://<ref>.supabase.co/functions/v1/api
4440
4652
 
@@ -4444,7 +4656,7 @@ Exit codes:
4444
4656
  2 configuration error (missing credentials or endpoint)
4445
4657
  3 not found (resource does not exist)
4446
4658
 
4447
- Console: https://kensaur.us/mushi-mushi/
4659
+ Console: https://kensaur.us/mushi-mushi/admin
4448
4660
  Docs: https://github.com/kensaurus/mushi-mushi`);
4449
4661
  registerAccountCommands(program);
4450
4662
  registerDeployCommands(program);