@mushi-mushi/cli 0.22.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,423 @@
1
+ import {
2
+ MUSHI_CLI_VERSION
3
+ } from "./chunk-BY5SZYRO.js";
4
+
5
+ // src/config.ts
6
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { dirname, join } from "path";
9
+ var SECURE_FILE_MODE = 384;
10
+ var SECURE_DIR_MODE = 448;
11
+ function resolveXdgConfigPath() {
12
+ const xdg = process.env["XDG_CONFIG_HOME"];
13
+ if (xdg && xdg.length > 0) {
14
+ return join(xdg, "mushi", "config.json");
15
+ }
16
+ if (process.platform === "win32") {
17
+ const appData = process.env["APPDATA"];
18
+ if (appData && appData.length > 0) {
19
+ return join(appData, "mushi", "config.json");
20
+ }
21
+ }
22
+ return join(homedir(), ".config", "mushi", "config.json");
23
+ }
24
+ var LEGACY_CONFIG_PATH = join(homedir(), ".mushirc");
25
+ var CONFIG_PATH = resolveXdgConfigPath();
26
+ function loadConfig(path = CONFIG_PATH) {
27
+ let file = {};
28
+ if (existsSync(path)) {
29
+ tightenPermissions(path);
30
+ try {
31
+ file = JSON.parse(readFileSync(path, "utf-8"));
32
+ } catch {
33
+ }
34
+ } else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
35
+ file = migrateLegacyConfig() ?? {};
36
+ }
37
+ const endpointFromEnv = process.env["MUSHI_API_ENDPOINT"] ?? process.env["MUSHI_ENDPOINT"] ?? void 0;
38
+ const fromEnv = {
39
+ ...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
40
+ ...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
41
+ ...endpointFromEnv ? { endpoint: endpointFromEnv } : {},
42
+ ...process.env["MUSHI_CONSOLE_URL"] ? { consoleUrl: process.env["MUSHI_CONSOLE_URL"].trim() } : {}
43
+ };
44
+ return { ...file, ...fromEnv };
45
+ }
46
+ function saveConfig(config, path = CONFIG_PATH) {
47
+ const dir = dirname(path);
48
+ if (!existsSync(dir)) {
49
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
50
+ } else {
51
+ tightenDirPermissions(dir);
52
+ }
53
+ writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
54
+ tightenPermissions(path);
55
+ }
56
+ function migrateLegacyConfig(legacyPath = LEGACY_CONFIG_PATH, destPath = CONFIG_PATH) {
57
+ if (!existsSync(legacyPath)) return null;
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(readFileSync(legacyPath, "utf-8"));
61
+ } catch {
62
+ return null;
63
+ }
64
+ const dir = dirname(destPath);
65
+ if (!existsSync(dir)) {
66
+ mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
67
+ }
68
+ try {
69
+ renameSync(legacyPath, destPath);
70
+ } catch {
71
+ writeFileSync(destPath, JSON.stringify(parsed, null, 2), { mode: SECURE_FILE_MODE });
72
+ try {
73
+ unlinkSync(legacyPath);
74
+ } catch {
75
+ }
76
+ }
77
+ tightenPermissions(destPath);
78
+ tightenDirPermissions(dir);
79
+ return parsed;
80
+ }
81
+ function tightenPermissions(path) {
82
+ if (process.platform === "win32") return;
83
+ try {
84
+ const current = statSync(path).mode & 511;
85
+ if (current !== SECURE_FILE_MODE) chmodSync(path, SECURE_FILE_MODE);
86
+ } catch {
87
+ }
88
+ }
89
+ function tightenDirPermissions(path) {
90
+ if (process.platform === "win32") return;
91
+ try {
92
+ const current = statSync(path).mode & 511;
93
+ if (current !== SECURE_DIR_MODE) chmodSync(path, SECURE_DIR_MODE);
94
+ } catch {
95
+ }
96
+ }
97
+
98
+ // src/console-url.ts
99
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
100
+ import { dirname as dirname2, join as join2 } from "path";
101
+ var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
102
+ var LOCAL_ADMIN_URL = "http://localhost:6464";
103
+ var LOCAL_PROBE_MS = 800;
104
+ function normalizeConsoleBase(url) {
105
+ const trimmed = url.trim();
106
+ let end = trimmed.length;
107
+ while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
108
+ return trimmed.slice(0, end);
109
+ }
110
+ function consoleUrl(base, route) {
111
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
112
+ return `${normalizeConsoleBase(base)}${normalizedRoute}`;
113
+ }
114
+ function resolveConsoleUrlFromEnv() {
115
+ const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
116
+ return raw ? normalizeConsoleBase(raw) : void 0;
117
+ }
118
+ function isInsideMushiMonorepo(cwd) {
119
+ let dir = cwd;
120
+ for (let depth = 0; depth < 14; depth += 1) {
121
+ const pkgPath = join2(dir, "package.json");
122
+ if (existsSync2(pkgPath)) {
123
+ try {
124
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
125
+ if (pkg.name === "mushi-mushi") return true;
126
+ } catch {
127
+ }
128
+ }
129
+ const parent = dirname2(dir);
130
+ if (parent === dir) break;
131
+ dir = parent;
132
+ }
133
+ return false;
134
+ }
135
+ async function probeLocalAdminConsole() {
136
+ try {
137
+ const controller = new AbortController();
138
+ const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
139
+ const res = await fetch(LOCAL_ADMIN_URL, {
140
+ signal: controller.signal,
141
+ method: "HEAD"
142
+ });
143
+ clearTimeout(timer);
144
+ if (res.ok || res.status === 404 || res.status === 401) {
145
+ return LOCAL_ADMIN_URL;
146
+ }
147
+ } catch {
148
+ }
149
+ return null;
150
+ }
151
+ function resolveConsoleUrlSync(cwd = process.cwd()) {
152
+ const fromEnv = resolveConsoleUrlFromEnv();
153
+ if (fromEnv) return fromEnv;
154
+ const config = loadConfig();
155
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
156
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
157
+ return HOSTED_CONSOLE_BASE;
158
+ }
159
+ async function resolveConsoleUrl(options) {
160
+ const fromEnv = resolveConsoleUrlFromEnv();
161
+ if (fromEnv) return fromEnv;
162
+ const config = loadConfig();
163
+ if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
164
+ const local = await probeLocalAdminConsole();
165
+ if (local) return local;
166
+ const cwd = options?.cwd ?? process.cwd();
167
+ if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
168
+ return HOSTED_CONSOLE_BASE;
169
+ }
170
+ function projectIdHint(base) {
171
+ 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).`;
172
+ }
173
+ function apiKeyHint(base) {
174
+ 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.`;
175
+ }
176
+ function cliSetupDeepLink(base) {
177
+ return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
178
+ }
179
+ function reportsUrl(base, reportId) {
180
+ return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
181
+ }
182
+ async function openInBrowser(url) {
183
+ let parsed;
184
+ try {
185
+ parsed = new URL(url);
186
+ } catch {
187
+ return;
188
+ }
189
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
190
+ return;
191
+ }
192
+ const safeUrl = parsed.toString();
193
+ const { spawn } = await import("child_process");
194
+ const [command, args] = process.platform === "win32" ? (
195
+ // Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
196
+ // `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
197
+ // characters like `&` / `|` / `^` could still break out — exactly the
198
+ // injection surface CodeQL flags. rundll32 never invokes a shell: the URL
199
+ // is handed to the default browser as one opaque argument.
200
+ ["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
201
+ ) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
202
+ await new Promise((resolve) => {
203
+ try {
204
+ const child = spawn(command, [...args], { stdio: "ignore", shell: false });
205
+ child.on("error", () => resolve());
206
+ child.on("exit", () => resolve());
207
+ child.unref();
208
+ } catch {
209
+ resolve();
210
+ }
211
+ });
212
+ }
213
+
214
+ // src/endpoint.ts
215
+ var TEST_REPORT_TIMEOUT_MS = 1e4;
216
+ var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
217
+ var CLOUD_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/functions/v1/api";
218
+ function resolveCloudEndpoint(explicit) {
219
+ if (explicit?.trim()) return explicit.trim();
220
+ const fromEnv = process.env.MUSHI_API_ENDPOINT?.trim();
221
+ if (fromEnv) return fromEnv;
222
+ return CLOUD_API_ENDPOINT;
223
+ }
224
+ function assertEndpoint(url) {
225
+ let parsed;
226
+ try {
227
+ parsed = new URL(url);
228
+ } catch {
229
+ throw new Error(`Invalid endpoint URL: ${url}`);
230
+ }
231
+ const host = parsed.hostname;
232
+ const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local");
233
+ if (parsed.protocol !== "https:" && !isLocal) {
234
+ throw new Error(`Endpoint must use https:// (got ${parsed.protocol}//${host}).`);
235
+ }
236
+ return normalizeEndpoint(parsed.origin + parsed.pathname);
237
+ }
238
+ function normalizeEndpoint(url) {
239
+ if (!url) {
240
+ throw new Error(
241
+ "No API endpoint configured. Run `mushi init` or set MUSHI_API_ENDPOINT. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api"
242
+ );
243
+ }
244
+ let end = url.length;
245
+ while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
246
+ return url.slice(0, end);
247
+ }
248
+
249
+ // src/errors.ts
250
+ var EXIT_CODE_MAP = {
251
+ E_AUTH_MISSING: 2,
252
+ E_AUTH_INVALID: 2,
253
+ E_PROJECT_MISSING: 2,
254
+ E_ENDPOINT_INVALID: 2,
255
+ E_INVALID_INPUT: 2,
256
+ E_NOT_INTERACTIVE: 2,
257
+ E_NETWORK: 3,
258
+ E_TIMEOUT: 3,
259
+ E_API_ERROR: 3,
260
+ E_RATE_LIMITED: 3,
261
+ E_FILE_NOT_FOUND: 1,
262
+ E_FILE_PERMISSION: 1,
263
+ E_FRESHNESS_STALE: 1,
264
+ E_INTERRUPTED: 130,
265
+ // 128 + SIGINT (2). POSIX convention.
266
+ E_INTERNAL: 1
267
+ };
268
+ var MushiCliError = class extends Error {
269
+ code;
270
+ hint;
271
+ cause;
272
+ constructor(code, message, hint, cause) {
273
+ super(message);
274
+ this.name = "MushiCliError";
275
+ this.code = code;
276
+ if (hint) this.hint = hint;
277
+ if (cause !== void 0) this.cause = cause;
278
+ }
279
+ /** Exit code POSIX-aware shell scripts should branch on. */
280
+ get exitCode() {
281
+ return EXIT_CODE_MAP[this.code];
282
+ }
283
+ /**
284
+ * Format for `--json` output. Used by the JSON writer when a CLI
285
+ * subcommand was invoked with `--json` and an error escapes.
286
+ */
287
+ toJSON() {
288
+ return {
289
+ error: {
290
+ code: this.code,
291
+ message: this.message,
292
+ ...this.hint ? { hint: this.hint } : {}
293
+ }
294
+ };
295
+ }
296
+ };
297
+
298
+ // src/signals.ts
299
+ var activeController = null;
300
+ function getAbortSignal(external) {
301
+ if (external) return external.signal;
302
+ if (!activeController || activeController.signal.aborted) {
303
+ activeController = new AbortController();
304
+ }
305
+ return activeController.signal;
306
+ }
307
+
308
+ // src/cli-shared.ts
309
+ var UUID_RX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
310
+ function requireUuid(value, label = "id") {
311
+ if (UUID_RX.test(value)) return value;
312
+ throw new MushiCliError(
313
+ "E_INVALID_INPUT",
314
+ `${label} must be a valid UUID`,
315
+ "copy the full id from `mushi reports list` or the console URL"
316
+ );
317
+ }
318
+ var API_TIMEOUT_MS = 15e3;
319
+ async function apiCall(path, config, options = {}) {
320
+ const endpoint = config.endpoint;
321
+ if (!endpoint) {
322
+ return {
323
+ ok: false,
324
+ error: {
325
+ code: "NO_ENDPOINT",
326
+ 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"
327
+ }
328
+ };
329
+ }
330
+ const controller = new AbortController();
331
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
332
+ const signals = [controller.signal, getAbortSignal()];
333
+ const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
334
+ try {
335
+ const res = await fetch(`${endpoint}${path}`, {
336
+ ...options,
337
+ signal: compositeSignal,
338
+ headers: {
339
+ "Content-Type": "application/json",
340
+ // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
341
+ // fallback accepted by some older middleware. Both are sent.
342
+ "Authorization": `Bearer ${config.apiKey ?? ""}`,
343
+ "X-Mushi-Api-Key": config.apiKey ?? "",
344
+ "X-Mushi-Project": config.projectId ?? "",
345
+ "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
346
+ ...options.headers
347
+ }
348
+ });
349
+ clearTimeout(timer);
350
+ let body;
351
+ const contentType = res.headers.get("content-type") ?? "";
352
+ if (contentType.includes("application/json")) {
353
+ try {
354
+ body = await res.json();
355
+ } catch {
356
+ body = null;
357
+ }
358
+ } else {
359
+ const text = await res.text();
360
+ try {
361
+ body = JSON.parse(text);
362
+ } catch {
363
+ body = {
364
+ ok: false,
365
+ error: {
366
+ code: `HTTP_${res.status}`,
367
+ message: text.trim().slice(0, 300) || `HTTP ${res.status}`
368
+ }
369
+ };
370
+ }
371
+ }
372
+ if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
373
+ const b = body;
374
+ const nested = b["error"];
375
+ const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
376
+ const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
377
+ return {
378
+ ok: false,
379
+ httpStatus: res.status,
380
+ error: { code: errCode, message: errMsg }
381
+ };
382
+ }
383
+ return body;
384
+ } catch (err) {
385
+ clearTimeout(timer);
386
+ if (err instanceof Error && err.name === "AbortError") {
387
+ return {
388
+ ok: false,
389
+ error: {
390
+ code: "TIMEOUT",
391
+ message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
392
+ }
393
+ };
394
+ }
395
+ return {
396
+ ok: false,
397
+ error: {
398
+ code: "NETWORK_ERROR",
399
+ message: err instanceof Error ? err.message : String(err)
400
+ }
401
+ };
402
+ }
403
+ }
404
+
405
+ export {
406
+ CONFIG_PATH,
407
+ loadConfig,
408
+ saveConfig,
409
+ consoleUrl,
410
+ resolveConsoleUrlSync,
411
+ resolveConsoleUrl,
412
+ projectIdHint,
413
+ apiKeyHint,
414
+ cliSetupDeepLink,
415
+ reportsUrl,
416
+ openInBrowser,
417
+ TEST_REPORT_FETCH_TIMEOUT_MS,
418
+ resolveCloudEndpoint,
419
+ assertEndpoint,
420
+ normalizeEndpoint,
421
+ requireUuid,
422
+ apiCall
423
+ };
@@ -2,13 +2,15 @@ import {
2
2
  CONFIG_PATH,
3
3
  assertEndpoint,
4
4
  normalizeEndpoint,
5
+ requireUuid,
5
6
  saveConfig
6
- } from "./chunk-XEXYGDQV.js";
7
+ } from "./chunk-WLLFV26V.js";
7
8
  import {
8
9
  detectFramework,
9
10
  envVarsToWrite,
10
11
  readPackageJson
11
- } from "./chunk-JAUKBLPK.js";
12
+ } from "./chunk-AIZRI665.js";
13
+ import "./chunk-BY5SZYRO.js";
12
14
 
13
15
  // src/connect.ts
14
16
  import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -217,11 +219,12 @@ async function ensureMcpJsonGitignored(cwd, messages) {
217
219
  async function runConnect(opts, baseConfig = {}) {
218
220
  const cwd = resolve(opts.cwd ?? process.cwd());
219
221
  const endpoint = assertEndpoint(opts.endpoint);
222
+ const projectId = requireUuid(opts.projectId, "projectId");
220
223
  const messages = [];
221
224
  const config = {
222
225
  ...baseConfig,
223
226
  apiKey: opts.apiKey,
224
- projectId: opts.projectId,
227
+ projectId,
225
228
  endpoint
226
229
  };
227
230
  saveConfig(config);
@@ -230,7 +233,7 @@ async function runConnect(opts, baseConfig = {}) {
230
233
  if (opts.writeEnv !== false) {
231
234
  const pkg = readPackageJson(cwd);
232
235
  const framework = detectFramework(cwd, pkg);
233
- const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
236
+ const lines = envVarsToWrite(opts.apiKey, projectId, framework).split("\n");
234
237
  envPath = join(cwd, ".env.local");
235
238
  const wrote = await mergeEnvFile(envPath, lines);
236
239
  messages.push(
@@ -247,10 +250,10 @@ async function runConnect(opts, baseConfig = {}) {
247
250
  if (opts.wireIde !== false) {
248
251
  const mcpDir = join(cwd, ".cursor");
249
252
  mcpPath = join(mcpDir, "mcp.json");
250
- const serverName = buildMcpServerName({ projectId: opts.projectId });
253
+ const serverName = buildMcpServerName({ projectId });
251
254
  const serverBlock = buildMcpServerBlock({
252
255
  endpoint,
253
- projectId: opts.projectId,
256
+ projectId,
254
257
  apiKey: opts.apiKey
255
258
  });
256
259
  await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
@@ -265,7 +268,7 @@ async function runConnect(opts, baseConfig = {}) {
265
268
  heartbeat = await waitForIngestReady({
266
269
  endpoint,
267
270
  apiKey: opts.apiKey,
268
- projectId: opts.projectId,
271
+ projectId,
269
272
  maxAttempts,
270
273
  onPoll: (payload, attempt) => {
271
274
  if (!opts.json && attempt % 3 === 0) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runConnect
4
- } from "./chunk-R2HPR2FV.js";
4
+ } from "./chunk-GSQO35YD.js";
5
5
  import "./chunk-LDPFTQLE.js";
6
6
  export {
7
7
  runConnect
package/dist/detect.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * PURPOSE: Pure detection helpers for framework, package manager, and project state.
4
4
  * Kept side-effect-free so the wizard remains unit-testable.
5
5
  */
6
- type FrameworkId = 'next' | 'react' | 'vue' | 'nuxt' | 'svelte' | 'sveltekit' | 'angular' | 'expo' | 'react-native' | 'capacitor' | 'express' | 'fastify' | 'hono' | 'vanilla';
6
+ type FrameworkId = 'next' | 'react' | 'cra' | 'remix' | 'astro' | 'solid' | 'vue' | 'nuxt' | 'svelte' | 'sveltekit' | 'angular' | 'expo' | 'react-native' | 'capacitor' | 'express' | 'fastify' | 'hono' | 'vanilla';
7
7
  interface Framework {
8
8
  id: FrameworkId;
9
9
  label: string;
package/dist/detect.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  installCommand,
8
8
  isFrameworkId,
9
9
  readPackageJson
10
- } from "./chunk-JAUKBLPK.js";
10
+ } from "./chunk-AIZRI665.js";
11
11
  export {
12
12
  FRAMEWORKS,
13
13
  FRAMEWORK_IDS,