@mushi-mushi/cli 0.22.1 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FRAMEWORKS,
4
+ MUSHI_CLI_VERSION,
5
+ apiCall,
4
6
  apiKeyHeaders,
7
+ apiKeyHint,
5
8
  buildMcpServerBlock,
6
9
  buildMcpServerName,
10
+ cliSetupDeepLink,
11
+ consoleUrl,
7
12
  detectFramework,
8
13
  detectPackageManager,
14
+ die,
15
+ ensureClientId,
9
16
  envVarsToWrite,
10
17
  fetchIngestSetup,
18
+ fmtDate,
11
19
  installCommand,
20
+ installSignalHandlers,
12
21
  loadConfig,
22
+ openInBrowser,
23
+ pad,
24
+ printAndExit,
25
+ probeEndpointHealth,
26
+ projectIdHint,
13
27
  readPackageJson,
28
+ reportsUrl,
29
+ requireConfig,
30
+ requireUuid,
31
+ resolveConsoleUrl,
32
+ resolveConsoleUrlSync,
14
33
  runConnect,
15
34
  sanitizeApiKey,
16
35
  sanitizeCliCredentials,
@@ -18,9 +37,8 @@ import {
18
37
  sanitizeProjectId,
19
38
  saveConfig,
20
39
  writeMcpServerEntry
21
- } from "./chunk-R2HPR2FV.js";
40
+ } from "./chunk-M6L22NJU.js";
22
41
  import {
23
- CLOUD_API_ENDPOINT,
24
42
  TEST_REPORT_FETCH_TIMEOUT_MS,
25
43
  assertEndpoint,
26
44
  normalizeEndpoint,
@@ -30,372 +48,16 @@ import {
30
48
  // src/index.ts
31
49
  import { Command } from "commander";
32
50
 
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
- function printAndExit(err, asJson = false) {
82
- const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
83
- "E_INTERNAL",
84
- err instanceof Error ? err.message : String(err),
85
- "this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
86
- err
87
- );
88
- if (asJson) {
89
- process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
90
- } else {
91
- process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
92
- `);
93
- if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
94
- `);
95
- }
96
- process.exit(cliErr.exitCode);
97
- }
98
-
99
- // src/signals.ts
100
- var installed = false;
101
- var activeController = null;
102
- function installSignalHandlers() {
103
- if (installed) return;
104
- installed = true;
105
- const abortAndExit = (signal) => {
106
- activeController?.abort(
107
- new MushiCliError(
108
- "E_INTERRUPTED",
109
- `aborted by ${signal}`,
110
- "partial state on the server is safe to retry \u2014 re-run the same command"
111
- )
112
- );
113
- const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
114
- process.nextTick(() => {
115
- process.exit(code);
116
- });
117
- };
118
- process.on("SIGINT", () => abortAndExit("SIGINT"));
119
- process.on("SIGTERM", () => abortAndExit("SIGTERM"));
120
- }
121
- function getAbortSignal(external) {
122
- if (external) return external.signal;
123
- if (!activeController || activeController.signal.aborted) {
124
- activeController = new AbortController();
125
- }
126
- return activeController.signal;
127
- }
128
-
129
- // src/version.ts
130
- var MUSHI_CLI_VERSION = true ? "0.22.1" : "0.0.0-dev";
131
-
132
51
  // src/init.ts
133
52
  import * as p from "@clack/prompts";
134
53
  import { spawn } from "child_process";
135
54
  import { randomUUID } from "crypto";
136
- import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
137
- import { join as join3 } from "path";
138
-
139
- // src/console-url.ts
140
- import { existsSync, readFileSync } from "fs";
141
- import { dirname, join } from "path";
142
- var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
143
- var LOCAL_ADMIN_URL = "http://localhost:6464";
144
- var LOCAL_PROBE_MS = 800;
145
- function normalizeConsoleBase(url) {
146
- const trimmed = url.trim();
147
- let end = trimmed.length;
148
- while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
149
- return trimmed.slice(0, end);
150
- }
151
- function consoleUrl(base, route) {
152
- const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
153
- return `${normalizeConsoleBase(base)}${normalizedRoute}`;
154
- }
155
- function resolveConsoleUrlFromEnv() {
156
- const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
157
- return raw ? normalizeConsoleBase(raw) : void 0;
158
- }
159
- function isInsideMushiMonorepo(cwd) {
160
- let dir = cwd;
161
- for (let depth = 0; depth < 14; depth += 1) {
162
- const pkgPath = join(dir, "package.json");
163
- if (existsSync(pkgPath)) {
164
- try {
165
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
166
- if (pkg.name === "mushi-mushi") return true;
167
- } catch {
168
- }
169
- }
170
- const parent = dirname(dir);
171
- if (parent === dir) break;
172
- dir = parent;
173
- }
174
- return false;
175
- }
176
- async function probeLocalAdminConsole() {
177
- try {
178
- const controller = new AbortController();
179
- const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
180
- const res = await fetch(LOCAL_ADMIN_URL, {
181
- signal: controller.signal,
182
- method: "HEAD"
183
- });
184
- clearTimeout(timer);
185
- if (res.ok || res.status === 404 || res.status === 401) {
186
- return LOCAL_ADMIN_URL;
187
- }
188
- } catch {
189
- }
190
- return null;
191
- }
192
- function resolveConsoleUrlSync(cwd = process.cwd()) {
193
- const fromEnv = resolveConsoleUrlFromEnv();
194
- if (fromEnv) return fromEnv;
195
- const config = loadConfig();
196
- if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
197
- if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
198
- return HOSTED_CONSOLE_BASE;
199
- }
200
- async function resolveConsoleUrl(options) {
201
- const fromEnv = resolveConsoleUrlFromEnv();
202
- if (fromEnv) return fromEnv;
203
- const config = loadConfig();
204
- if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
205
- const local = await probeLocalAdminConsole();
206
- if (local) return local;
207
- const cwd = options?.cwd ?? process.cwd();
208
- if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
209
- return HOSTED_CONSOLE_BASE;
210
- }
211
- function projectIdHint(base) {
212
- 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).`;
213
- }
214
- function apiKeyHint(base) {
215
- 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.`;
216
- }
217
- function cliSetupDeepLink(base) {
218
- return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
219
- }
220
- function reportsUrl(base, reportId) {
221
- return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
222
- }
223
- async function openInBrowser(url) {
224
- let parsed;
225
- try {
226
- parsed = new URL(url);
227
- } catch {
228
- return;
229
- }
230
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
231
- return;
232
- }
233
- const safeUrl = parsed.toString();
234
- const { spawn: spawn2 } = await import("child_process");
235
- const [command, args] = process.platform === "win32" ? (
236
- // Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
237
- // `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
238
- // characters like `&` / `|` / `^` could still break out — exactly the
239
- // injection surface CodeQL flags. rundll32 never invokes a shell: the URL
240
- // is handed to the default browser as one opaque argument.
241
- ["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
242
- ) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
243
- await new Promise((resolve3) => {
244
- try {
245
- const child = spawn2(command, [...args], { stdio: "ignore", shell: false });
246
- child.on("error", () => resolve3());
247
- child.on("exit", () => resolve3());
248
- child.unref();
249
- } catch {
250
- resolve3();
251
- }
252
- });
253
- }
254
-
255
- // src/cli-shared.ts
256
- var API_TIMEOUT_MS = 15e3;
257
- async function probeEndpointHealth(endpoint) {
258
- const t0 = Date.now();
259
- const controller = new AbortController();
260
- const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
261
- try {
262
- const res = await fetch(`${endpoint}/health`, { signal: controller.signal });
263
- clearTimeout(timer);
264
- const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
265
- return { ok: res.ok, status: res.status, latencyMs: Date.now() - t0, body };
266
- } catch (err) {
267
- clearTimeout(timer);
268
- throw err;
269
- }
270
- }
271
- async function apiCall(path, config, options = {}) {
272
- const endpoint = config.endpoint;
273
- if (!endpoint) {
274
- return {
275
- ok: false,
276
- error: {
277
- code: "NO_ENDPOINT",
278
- 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"
279
- }
280
- };
281
- }
282
- const controller = new AbortController();
283
- const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
284
- const signals = [controller.signal, getAbortSignal()];
285
- const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
286
- try {
287
- const res = await fetch(`${endpoint}${path}`, {
288
- ...options,
289
- signal: compositeSignal,
290
- headers: {
291
- "Content-Type": "application/json",
292
- // `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
293
- // fallback accepted by some older middleware. Both are sent.
294
- "Authorization": `Bearer ${config.apiKey ?? ""}`,
295
- "X-Mushi-Api-Key": config.apiKey ?? "",
296
- "X-Mushi-Project": config.projectId ?? "",
297
- "X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
298
- ...options.headers
299
- }
300
- });
301
- clearTimeout(timer);
302
- let body;
303
- const contentType = res.headers.get("content-type") ?? "";
304
- if (contentType.includes("application/json")) {
305
- try {
306
- body = await res.json();
307
- } catch {
308
- body = null;
309
- }
310
- } else {
311
- const text2 = await res.text();
312
- try {
313
- body = JSON.parse(text2);
314
- } catch {
315
- body = {
316
- ok: false,
317
- error: {
318
- code: `HTTP_${res.status}`,
319
- message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
320
- }
321
- };
322
- }
323
- }
324
- if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
325
- const b = body;
326
- const nested = b["error"];
327
- const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
328
- const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
329
- return {
330
- ok: false,
331
- httpStatus: res.status,
332
- error: { code: errCode, message: errMsg }
333
- };
334
- }
335
- return body;
336
- } catch (err) {
337
- clearTimeout(timer);
338
- if (err instanceof Error && err.name === "AbortError") {
339
- return {
340
- ok: false,
341
- error: {
342
- code: "TIMEOUT",
343
- message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
344
- }
345
- };
346
- }
347
- return {
348
- ok: false,
349
- error: {
350
- code: "NETWORK_ERROR",
351
- message: err instanceof Error ? err.message : String(err)
352
- }
353
- };
354
- }
355
- }
356
- function die(result, exitCode = 1) {
357
- const { code, message } = result.error;
358
- const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
359
- process.stderr.write(`error${status}: ${code} \u2014 ${message}
360
- `);
361
- if (code === "INSUFFICIENT_SCOPE") {
362
- process.stderr.write(
363
- "\n Your current API key does not have the required scope for this command.\n New keys minted by the wizard already include both scopes. Existing keys\n can be upgraded with:\n\n mushi login --upgrade-scope\n\n Or visit your console \u2192 Projects \u2192 API Keys to mint a new key.\n"
364
- );
365
- }
366
- process.exit(exitCode);
367
- }
368
- function requireConfig(opts = {}) {
369
- const config = loadConfig();
370
- if (!config.apiKey) {
371
- printAndExit(new MushiCliError(
372
- "E_AUTH_MISSING",
373
- "API key not configured.",
374
- "Run: mushi login --api-key <key> Or: export MUSHI_API_KEY=<key>"
375
- ));
376
- }
377
- if (!config.endpoint) {
378
- config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
379
- }
380
- if (opts.needsProject && !config.projectId) {
381
- printAndExit(new MushiCliError(
382
- "E_PROJECT_MISSING",
383
- "Project ID not configured.",
384
- `Run: mushi login --project-id <uuid> Or find your project ID at ${consoleUrl(resolveConsoleUrlSync(), "/projects")}`
385
- ));
386
- }
387
- return config;
388
- }
389
- function fmtDate(iso) {
390
- if (!iso) return "\u2014";
391
- return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
392
- }
393
- function pad(s, width) {
394
- return s.length >= width ? s : s + " ".repeat(width - s.length);
395
- }
55
+ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
56
+ import { join as join2 } from "path";
396
57
 
397
58
  // src/device-auth.ts
398
59
  var DEVICE_FETCH_TIMEOUT_MS = 15e3;
60
+ var DEFAULT_SLOW_DOWN_MS = 5e3;
399
61
  function trimTrailingSlash(endpoint) {
400
62
  let end = endpoint.length;
401
63
  while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
@@ -410,11 +72,12 @@ async function deviceFetch(url, init = {}) {
410
72
  clearTimeout(timer);
411
73
  }
412
74
  }
413
- async function startDeviceAuth(endpoint) {
75
+ async function startDeviceAuth(endpoint, clientId) {
414
76
  const base = trimTrailingSlash(endpoint);
415
77
  const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
416
78
  method: "POST",
417
- headers: { "Content-Type": "application/json" }
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify(clientId ? { client_id: clientId } : {})
418
81
  });
419
82
  const json = await res.json().catch(() => null);
420
83
  if (!res.ok || !json?.ok || !json.data) {
@@ -437,6 +100,12 @@ async function pollDeviceToken(endpoint, deviceCode) {
437
100
  switch (json.error) {
438
101
  case "authorization_pending":
439
102
  return { status: "pending" };
103
+ case "slow_down": {
104
+ const retryAfterHeader = res.headers?.get?.("Retry-After");
105
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : NaN;
106
+ const retryAfterMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : DEFAULT_SLOW_DOWN_MS;
107
+ return { status: "slow_down", retryAfterMs };
108
+ }
440
109
  case "access_denied":
441
110
  return { status: "denied" };
442
111
  case "expired_token":
@@ -445,9 +114,11 @@ async function pollDeviceToken(endpoint, deviceCode) {
445
114
  return {
446
115
  status: "error",
447
116
  message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
448
- // 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
449
- // rejection (already claimed / invalid_grant) that won't fix itself.
450
- retryable: res.status >= 500
117
+ // 5xx is a server hiccup the next poll may clear; 408 (gateway
118
+ // timeout) is equally transient. Any other 4xx is a definitive
119
+ // rejection (already claimed / invalid_grant) that won't fix
120
+ // itself. (429/slow_down is handled above as its own outcome.)
121
+ retryable: res.status >= 500 || res.status === 408
451
122
  };
452
123
  }
453
124
  } catch (err) {
@@ -458,7 +129,7 @@ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
458
129
  async function waitForCliToken(endpoint, session, opts = {}) {
459
130
  const sleep = opts.sleep ?? defaultSleep;
460
131
  const now = opts.now ?? Date.now;
461
- const intervalMs = (session.interval || 5) * 1e3;
132
+ let intervalMs = (session.interval || 5) * 1e3;
462
133
  const deadline = now() + (session.expires_in || 600) * 1e3;
463
134
  const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
464
135
  let consecutiveErrors = 0;
@@ -476,6 +147,11 @@ async function waitForCliToken(endpoint, session, opts = {}) {
476
147
  consecutiveErrors = 0;
477
148
  opts.onPending?.();
478
149
  continue;
150
+ case "slow_down":
151
+ consecutiveErrors = 0;
152
+ intervalMs = Math.max(intervalMs, outcome.retryAfterMs);
153
+ opts.onSlowDown?.(outcome.retryAfterMs);
154
+ continue;
479
155
  case "denied":
480
156
  throw new Error("Login was denied in the browser.");
481
157
  case "expired":
@@ -506,7 +182,11 @@ async function listProjects(endpoint, cliToken) {
506
182
  const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
507
183
  const json = await res.json().catch(() => null);
508
184
  if (res.ok && json?.ok) return json.data?.projects ?? [];
509
- } catch {
185
+ } catch (err) {
186
+ console.error(
187
+ "[mushi] Failed to list projects:",
188
+ err instanceof Error ? err.message : err
189
+ );
510
190
  }
511
191
  return [];
512
192
  }
@@ -599,8 +279,8 @@ function parse(version) {
599
279
  }
600
280
 
601
281
  // src/monorepo.ts
602
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
603
- import { dirname as dirname2, join as join2, resolve } from "path";
282
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
283
+ import { dirname, join, resolve } from "path";
604
284
  var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
605
285
  var FRAMEWORK_DEPS = {
606
286
  next: "Next.js",
@@ -617,9 +297,9 @@ var FRAMEWORK_DEPS = {
617
297
  function detectWorkspaceHint(cwd) {
618
298
  const root = findWorkspaceRoot(cwd);
619
299
  if (!root) return null;
620
- const rootPkg = readPackageJsonSafely(join2(root, "package.json"));
300
+ const rootPkg = readPackageJsonSafely(join(root, "package.json"));
621
301
  if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
622
- const source = existsSync2(join2(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
302
+ const source = existsSync(join(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
623
303
  const apps = collectAppsFromGlobs(root);
624
304
  if (apps.length === 0) return null;
625
305
  return { root, apps, source };
@@ -628,15 +308,15 @@ function findWorkspaceRoot(start) {
628
308
  let dir = resolve(start);
629
309
  for (let i = 0; i < 8; i++) {
630
310
  if (isWorkspaceRoot(dir)) return dir;
631
- const parent = dirname2(dir);
311
+ const parent = dirname(dir);
632
312
  if (parent === dir) break;
633
313
  dir = parent;
634
314
  }
635
315
  return null;
636
316
  }
637
317
  function isWorkspaceRoot(dir) {
638
- if (existsSync2(join2(dir, "pnpm-workspace.yaml"))) return true;
639
- const pkg = readPackageJsonSafely(join2(dir, "package.json"));
318
+ if (existsSync(join(dir, "pnpm-workspace.yaml"))) return true;
319
+ const pkg = readPackageJsonSafely(join(dir, "package.json"));
640
320
  if (!pkg) return false;
641
321
  return Boolean(pkg.workspaces);
642
322
  }
@@ -644,8 +324,8 @@ function collectAppsFromGlobs(root) {
644
324
  const results = [];
645
325
  for (const glob of WORKSPACE_GLOB_CANDIDATES) {
646
326
  const prefix = glob.replace("/*", "");
647
- const parentDir = join2(root, prefix);
648
- if (!existsSync2(parentDir)) continue;
327
+ const parentDir = join(root, prefix);
328
+ if (!existsSync(parentDir)) continue;
649
329
  let entries;
650
330
  try {
651
331
  entries = readdirSync(parentDir);
@@ -653,7 +333,7 @@ function collectAppsFromGlobs(root) {
653
333
  continue;
654
334
  }
655
335
  for (const entry of entries) {
656
- const pkgPath = join2(parentDir, entry, "package.json");
336
+ const pkgPath = join(parentDir, entry, "package.json");
657
337
  if (!isFileSafe(pkgPath)) continue;
658
338
  const pkg = readPackageJsonSafely(pkgPath);
659
339
  if (!pkg) continue;
@@ -671,14 +351,14 @@ function collectAppsFromGlobs(root) {
671
351
  function readPackageJsonSafely(path) {
672
352
  if (!isFileSafe(path)) return null;
673
353
  try {
674
- return JSON.parse(readFileSync2(path, "utf-8"));
354
+ return JSON.parse(readFileSync(path, "utf-8"));
675
355
  } catch {
676
356
  return null;
677
357
  }
678
358
  }
679
359
  function isFileSafe(path) {
680
360
  try {
681
- return existsSync2(path) && statSync(path).isFile();
361
+ return existsSync(path) && statSync(path).isFile();
682
362
  } catch {
683
363
  return false;
684
364
  }
@@ -767,7 +447,10 @@ async function runInit(options = {}) {
767
447
  const detected = detectFramework(cwd, pkg);
768
448
  const framework = await chooseFramework(detected, options);
769
449
  const consoleBase = await resolveConsoleUrl({ cwd });
770
- const endpoint = resolveCloudEndpoint(options.endpoint);
450
+ const endpoint = resolveCloudEndpoint(
451
+ options.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? loadConfig().endpoint
452
+ );
453
+ options = { ...options, endpoint };
771
454
  const credentials = await acquireCredentials(options, consoleBase, endpoint);
772
455
  await verifyCredentials(credentials, options, consoleBase);
773
456
  const pm = detectPackageManager(cwd);
@@ -878,14 +561,14 @@ async function acquireCredentials(options, consoleBase, endpoint) {
878
561
  p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
879
562
  }
880
563
  }
881
- return collectCredentialsManually(options, consoleBase);
564
+ return collectCredentialsManually(options, consoleBase, endpoint);
882
565
  }
883
566
  async function runBrowserSignIn(options, endpoint, consoleBase) {
884
567
  const startSpin = p.spinner();
885
568
  startSpin.start("Starting secure browser sign-in\u2026");
886
569
  let session;
887
570
  try {
888
- session = await startDeviceAuth(endpoint);
571
+ session = await startDeviceAuth(endpoint, ensureClientId());
889
572
  } catch (err) {
890
573
  startSpin.stop("Could not start browser sign-in.");
891
574
  p.log.warn(err instanceof Error ? err.message : String(err));
@@ -983,15 +666,36 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
983
666
  if (!projectId || !apiKey) return null;
984
667
  return { apiKey, projectId };
985
668
  }
986
- async function collectCredentialsManually(options, consoleBase) {
669
+ async function collectCredentialsManually(options, consoleBase, endpoint) {
987
670
  const existing = loadConfig();
988
- const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
671
+ let savedProjectId = existing.projectId;
672
+ let savedApiKey = existing.apiKey;
673
+ if (!options.projectId && !options.apiKey && savedProjectId && savedApiKey) {
674
+ p.log.info(
675
+ `Found saved credentials from a previous sign-in (project ${sanitizeSecret(savedProjectId).slice(0, 8)}\u2026).`
676
+ );
677
+ const checkSpin = p.spinner();
678
+ checkSpin.start("Checking saved credentials\u2026");
679
+ const check = await apiCall("/v1/sync/whoami", {
680
+ apiKey: sanitizeSecret(savedApiKey),
681
+ projectId: sanitizeSecret(savedProjectId),
682
+ endpoint
683
+ });
684
+ if (check.ok) {
685
+ checkSpin.stop(`Saved credentials still work (${check.data.project_name}).`);
686
+ } else {
687
+ checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
688
+ savedProjectId = void 0;
689
+ savedApiKey = void 0;
690
+ }
691
+ }
692
+ const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
989
693
  message: "Project ID",
990
694
  placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
991
695
  hint: projectIdHint(consoleBase),
992
696
  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."
993
697
  });
994
- const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
698
+ const rawApiKey = options.apiKey ?? savedApiKey ?? await promptText({
995
699
  message: "API key",
996
700
  placeholder: "mushi_xxxxxxxxxxxx",
997
701
  hint: apiKeyHint(consoleBase),
@@ -1022,10 +726,22 @@ async function verifyCredentials(credentials, options, consoleBase) {
1022
726
  if (!result.ok) {
1023
727
  spinner3.stop("Credentials could not be verified.");
1024
728
  p.log.error(result.error?.message ?? "Authentication failed.");
1025
- p.log.info(`Double-check values in the console:
1026
- ${cliSetupDeepLink(consoleBase)}`);
729
+ p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
730
+ p.log.info(
731
+ [
732
+ "To recover:",
733
+ ' \u2022 Re-run `npx mushi-mushi` and choose "Sign in with your browser", or',
734
+ ` \u2022 Double-check the Project ID / API key in the console: ${cliSetupDeepLink(consoleBase)}`
735
+ ].join("\n")
736
+ );
1027
737
  throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
1028
738
  }
739
+ if (result.data.project_id && result.data.project_id !== credentials.projectId) {
740
+ p.log.warn(
741
+ `Project ID ${redact(credentials.projectId)} doesn't match this API key's project \u2014 using the key's actual project (${result.data.project_name}) instead.`
742
+ );
743
+ credentials.projectId = result.data.project_id;
744
+ }
1029
745
  spinner3.stop(`Connected to ${result.data.project_name}`);
1030
746
  }
1031
747
  function sanitizeSecret(raw) {
@@ -1085,10 +801,10 @@ function runCommand(pm, packages, cwd) {
1085
801
  });
1086
802
  }
1087
803
  async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
1088
- const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
1089
- const targetPath = join3(cwd, target);
804
+ const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
805
+ const targetPath = join2(cwd, target);
1090
806
  const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
1091
- const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
807
+ const existing = existsSync2(targetPath) ? readFileSync2(targetPath, "utf-8") : "";
1092
808
  if (existing.includes("MUSHI_PROJECT_ID")) {
1093
809
  let shouldOverwrite = overwrite;
1094
810
  if (!shouldOverwrite) {
@@ -1147,12 +863,12 @@ function escapeRegexChar(ch) {
1147
863
  return /[-/\\^$+?.()|[\]{}]/.test(ch) ? `\\${ch}` : ch;
1148
864
  }
1149
865
  function warnIfMissingFromGitignore(cwd, envFile) {
1150
- const gitignorePath = join3(cwd, ".gitignore");
1151
- if (!existsSync3(gitignorePath)) {
866
+ const gitignorePath = join2(cwd, ".gitignore");
867
+ if (!existsSync2(gitignorePath)) {
1152
868
  p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
1153
869
  return;
1154
870
  }
1155
- const content = readFileSync3(gitignorePath, "utf-8");
871
+ const content = readFileSync2(gitignorePath, "utf-8");
1156
872
  if (!isEnvFileCoveredByGitignore(content, envFile)) {
1157
873
  p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
1158
874
  }
@@ -1194,7 +910,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
1194
910
  if (p.isCancel(answer) || !answer) return;
1195
911
  const endpoint = resolveCloudEndpoint(options.endpoint);
1196
912
  try {
1197
- const { runConnect: runConnect2 } = await import("./connect-JIEIWTF3.js");
913
+ const { runConnect: runConnect2 } = await import("./connect-5VPUYE6H.js");
1198
914
  await runConnect2({
1199
915
  apiKey: credentials.apiKey,
1200
916
  projectId: credentials.projectId,
@@ -1326,7 +1042,7 @@ function isSameDirectory(a, b) {
1326
1042
  }
1327
1043
 
1328
1044
  // src/migrate.ts
1329
- var DOCS_BASE = "https://docs.mushimushi.dev";
1045
+ var DOCS_BASE = "https://kensaur.us/mushi-mushi/docs";
1330
1046
  var COMPETITOR_PACKAGES = {
1331
1047
  "instabug-to-mushi": [
1332
1048
  "instabug-reactnative",
@@ -1488,8 +1204,8 @@ Examples:
1488
1204
  mushi login --api-key mushi_xxx # non-interactive / CI
1489
1205
  mushi login --api-key mushi_xxx --project-id <uuid>
1490
1206
  mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
1491
- const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1492
- const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
1207
+ const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1208
+ const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
1493
1209
  const consoleBase = await resolveConsoleUrl();
1494
1210
  let apiKey = opts.apiKey;
1495
1211
  let projectId = opts.projectId;
@@ -1537,7 +1253,7 @@ Examples:
1537
1253
  }
1538
1254
  let deviceData;
1539
1255
  try {
1540
- deviceData = await startDeviceAuth(endpoint);
1256
+ deviceData = await startDeviceAuth(endpoint, ensureClientId());
1541
1257
  } catch (err) {
1542
1258
  process.stderr.write(`
1543
1259
  error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
@@ -2221,14 +1937,14 @@ Typical CI usage:
2221
1937
  import { createReadStream } from "fs";
2222
1938
  import { readFile, readdir } from "fs/promises";
2223
1939
  import { createHash } from "crypto";
2224
- import { join as join4, relative, basename } from "path";
1940
+ import { join as join3, relative, basename } from "path";
2225
1941
  import * as p2 from "@clack/prompts";
2226
1942
  async function findMapFiles(dir) {
2227
1943
  const results = [];
2228
1944
  async function walk(current) {
2229
1945
  const entries = await readdir(current, { withFileTypes: true });
2230
1946
  for (const e of entries) {
2231
- const full = join4(current, e.name);
1947
+ const full = join3(current, e.name);
2232
1948
  if (e.isDirectory()) {
2233
1949
  await walk(full);
2234
1950
  } else if (e.isFile() && (e.name.endsWith(".js.map") || e.name.endsWith(".css.map"))) {
@@ -2544,7 +2260,7 @@ Typical first-time flow:
2544
2260
  console.log("");
2545
2261
  let session;
2546
2262
  try {
2547
- session = await startDeviceAuth(endpoint);
2263
+ session = await startDeviceAuth(endpoint, ensureClientId());
2548
2264
  } catch (err) {
2549
2265
  process.stderr.write(`
2550
2266
  error: Could not start browser sign-in: ${err instanceof Error ? err.message : String(err)}
@@ -2661,7 +2377,7 @@ Supported IDEs:
2661
2377
 
2662
2378
  The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
2663
2379
  const { writeFile: writeFile2, mkdir, readFile: readFile3 } = await import("fs/promises");
2664
- const { existsSync: existsSync4 } = await import("fs");
2380
+ const { existsSync: existsSync3 } = await import("fs");
2665
2381
  const nodePath2 = await import("path");
2666
2382
  const os = await import("os");
2667
2383
  const config = requireConfig({ needsProject: true });
@@ -2773,7 +2489,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2773
2489
  }
2774
2490
  } else if (ideEntry.format === "zed") {
2775
2491
  let settings = {};
2776
- if (existsSync4(configPath)) {
2492
+ if (existsSync3(configPath)) {
2777
2493
  try {
2778
2494
  const raw = await readFile3(configPath, "utf8");
2779
2495
  settings = JSON.parse(raw);
@@ -2895,7 +2611,11 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2895
2611
  console.log(`\u26A0 Key probe returned HTTP ${probeRes.status} \u2014 check your credentials.`);
2896
2612
  }
2897
2613
  }
2898
- } catch {
2614
+ } catch (probeErr) {
2615
+ console.warn(
2616
+ "[mushi setup] API key probe failed \u2014 verify credentials manually.",
2617
+ probeErr instanceof Error ? probeErr.message : probeErr
2618
+ );
2899
2619
  }
2900
2620
  }
2901
2621
  console.log("");
@@ -2934,6 +2654,7 @@ Examples:
2934
2654
 
2935
2655
  # CI: fail the pipeline if the fix errors
2936
2656
  mushi fix $REPORT_ID --agent cursor_cloud --wait && echo "Fix PR opened"`).action(async (reportId, opts) => {
2657
+ requireUuid(reportId, "reportId");
2937
2658
  const cfg = loadConfig();
2938
2659
  if (opts.endpoint) cfg.endpoint = opts.endpoint;
2939
2660
  if (opts.apiKey) cfg.apiKey = opts.apiKey;
@@ -3308,9 +3029,9 @@ async function planUpgrade(cwd) {
3308
3029
  ...pkg?.dependencies ?? {},
3309
3030
  ...pkg?.devDependencies ?? {}
3310
3031
  };
3311
- const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
3032
+ const installed = MUSHI_PACKAGES.filter((name) => deps[name]);
3312
3033
  const entries = await Promise.all(
3313
- installed2.map(async (name) => {
3034
+ installed.map(async (name) => {
3314
3035
  const current = deps[name] ?? "";
3315
3036
  const currentCore = current.replace(/^[\^~>=<]*/, "");
3316
3037
  if (!/^\d/.test(currentCore)) {
@@ -3475,9 +3196,9 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
3475
3196
  async function checkSdkInstall(cwd) {
3476
3197
  try {
3477
3198
  const { readFile: readFile3 } = await import("fs/promises");
3478
- const { join: join5, resolve: resolve3 } = await import("path");
3199
+ const { join: join4, resolve: resolve3 } = await import("path");
3479
3200
  const root = resolve3(cwd);
3480
- const pkgPath = join5(root, "package.json");
3201
+ const pkgPath = join4(root, "package.json");
3481
3202
  const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
3482
3203
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
3483
3204
  const sdks = [
@@ -3486,11 +3207,11 @@ async function checkSdkInstall(cwd) {
3486
3207
  "@mushi-mushi/core",
3487
3208
  "@mushi-mushi/react-native"
3488
3209
  ];
3489
- const installed2 = sdks.filter((s) => deps[s]);
3210
+ const installed = sdks.filter((s) => deps[s]);
3490
3211
  return {
3491
3212
  name: "SDK installed in this repo",
3492
- ok: installed2.length > 0,
3493
- detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
3213
+ ok: installed.length > 0,
3214
+ detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
3494
3215
  };
3495
3216
  } catch {
3496
3217
  return null;
@@ -3674,9 +3395,9 @@ async function checkHostAppWiring(cwd) {
3674
3395
  const checks = [];
3675
3396
  try {
3676
3397
  const { readFile: readFile3, access } = await import("fs/promises");
3677
- const { join: join5, resolve: resolve3 } = await import("path");
3398
+ const { join: join4, resolve: resolve3 } = await import("path");
3678
3399
  const root = resolve3(cwd);
3679
- const pkgPath = join5(root, "package.json");
3400
+ const pkgPath = join4(root, "package.json");
3680
3401
  const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
3681
3402
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
3682
3403
  const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
@@ -3684,7 +3405,7 @@ async function checkHostAppWiring(cwd) {
3684
3405
  let envContent = "";
3685
3406
  for (const f of envCandidates) {
3686
3407
  try {
3687
- envContent = await readFile3(join5(root, f), "utf8");
3408
+ envContent = await readFile3(join4(root, f), "utf8");
3688
3409
  break;
3689
3410
  } catch {
3690
3411
  }
@@ -3698,7 +3419,7 @@ async function checkHostAppWiring(cwd) {
3698
3419
  });
3699
3420
  let mcpPresent = false;
3700
3421
  try {
3701
- await access(join5(root, ".cursor", "mcp.json"));
3422
+ await access(join4(root, ".cursor", "mcp.json"));
3702
3423
  mcpPresent = true;
3703
3424
  } catch {
3704
3425
  }
@@ -3732,12 +3453,12 @@ async function checkHostAppWiring(cwd) {
3732
3453
  async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
3733
3454
  const checks = [];
3734
3455
  const { readFile: readFile3 } = await import("fs/promises");
3735
- const { join: join5, resolve: resolve3 } = await import("path");
3456
+ const { join: join4, resolve: resolve3 } = await import("path");
3736
3457
  const { homedir } = await import("os");
3737
3458
  const root = resolve3(cwd);
3738
3459
  const candidates = [
3739
- join5(root, ".cursor", "mcp.json"),
3740
- join5(homedir(), ".cursor", "mcp.json")
3460
+ join4(root, ".cursor", "mcp.json"),
3461
+ join4(homedir(), ".cursor", "mcp.json")
3741
3462
  ];
3742
3463
  let mcpPath = null;
3743
3464
  let mcpRaw = null;
@@ -3890,12 +3611,12 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
3890
3611
  }
3891
3612
  try {
3892
3613
  const { readFile: readFile3 } = await import("fs/promises");
3893
- const { join: join5, resolve: resolve3 } = await import("path");
3614
+ const { join: join4, resolve: resolve3 } = await import("path");
3894
3615
  const root = resolve3(cwd);
3895
3616
  let envContent = "";
3896
3617
  for (const f of [".env.local", ".env"]) {
3897
3618
  try {
3898
- envContent = await readFile3(join5(root, f), "utf8");
3619
+ envContent = await readFile3(join4(root, f), "utf8");
3899
3620
  break;
3900
3621
  } catch {
3901
3622
  }
@@ -5120,6 +4841,131 @@ Examples:
5120
4841
  });
5121
4842
  }
5122
4843
 
4844
+ // src/commands/completion-cli.ts
4845
+ import { Argument } from "commander";
4846
+
4847
+ // src/completion.ts
4848
+ var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
4849
+ function isSupportedShell(value) {
4850
+ return SUPPORTED_SHELLS.includes(value);
4851
+ }
4852
+ function buildCommandTree(program2) {
4853
+ return program2.commands.filter((cmd) => cmd.name() !== "help").map((cmd) => ({
4854
+ name: cmd.name(),
4855
+ subcommands: cmd.commands.filter((sub) => sub.name() !== "help").map((sub) => sub.name()).sort()
4856
+ })).sort((a, b) => a.name.localeCompare(b.name));
4857
+ }
4858
+ var COMPLETION_ARG_VALUES = [...SUPPORTED_SHELLS];
4859
+ function subcommandsFor(tree, name) {
4860
+ if (name === "completion") return COMPLETION_ARG_VALUES;
4861
+ return tree.find((n) => n.name === name)?.subcommands ?? [];
4862
+ }
4863
+ function generateBash(tree) {
4864
+ const topLevel = tree.map((n) => n.name).join(" ");
4865
+ const cases = tree.map((n) => {
4866
+ const subs = subcommandsFor(tree, n.name);
4867
+ if (subs.length === 0) return null;
4868
+ return ` ${n.name})
4869
+ COMPREPLY=( $(compgen -W "${subs.join(" ")}" -- "$cur") )
4870
+ ;;`;
4871
+ }).filter((line) => line !== null).join("\n");
4872
+ return `###-begin-mushi-completion-###
4873
+ # mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi
4874
+ # Install: mushi completion bash >> ~/.bashrc (then restart your shell)
4875
+ _mushi_completion() {
4876
+ local cur
4877
+ cur="\${COMP_WORDS[COMP_CWORD]}"
4878
+ COMPREPLY=()
4879
+
4880
+ if [ "$COMP_CWORD" -eq 1 ]; then
4881
+ COMPREPLY=( $(compgen -W "${topLevel}" -- "$cur") )
4882
+ return 0
4883
+ fi
4884
+
4885
+ case "\${COMP_WORDS[1]}" in
4886
+ ${cases}
4887
+ *)
4888
+ ;;
4889
+ esac
4890
+ }
4891
+ complete -F _mushi_completion mushi
4892
+ ###-end-mushi-completion-###
4893
+ `;
4894
+ }
4895
+ function generateZsh(tree) {
4896
+ const topLevel = tree.map((n) => n.name).join(" ");
4897
+ const cases = tree.map((n) => {
4898
+ const subs = subcommandsFor(tree, n.name);
4899
+ if (subs.length === 0) return null;
4900
+ return ` ${n.name})
4901
+ local -a subcmds; subcmds=(${subs.join(" ")})
4902
+ _describe '${n.name} subcommand' subcmds
4903
+ ;;`;
4904
+ }).filter((line) => line !== null).join("\n");
4905
+ return `#compdef mushi
4906
+ # mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi
4907
+ # Install: mushi completion zsh > "\${fpath[1]}/_mushi" (then restart your shell)
4908
+ _mushi() {
4909
+ local -a top_level
4910
+ top_level=(${topLevel})
4911
+
4912
+ if (( CURRENT == 2 )); then
4913
+ _describe 'command' top_level
4914
+ return
4915
+ fi
4916
+
4917
+ case "\${words[2]}" in
4918
+ ${cases}
4919
+ *)
4920
+ ;;
4921
+ esac
4922
+ }
4923
+
4924
+ _mushi
4925
+ `;
4926
+ }
4927
+ function generateFish(tree) {
4928
+ const topLevel = tree.map((n) => n.name).join(" ");
4929
+ const lines = [
4930
+ "# mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi",
4931
+ "# Install: mushi completion fish > ~/.config/fish/completions/mushi.fish",
4932
+ `complete -c mushi -f -n "__fish_use_subcommand" -a "${topLevel}"`
4933
+ ];
4934
+ for (const n of tree) {
4935
+ const subs = subcommandsFor(tree, n.name);
4936
+ if (subs.length === 0) continue;
4937
+ lines.push(`complete -c mushi -f -n "__fish_seen_subcommand_from ${n.name}" -a "${subs.join(" ")}"`);
4938
+ }
4939
+ return lines.join("\n") + "\n";
4940
+ }
4941
+ function generateCompletionScript(shell, tree) {
4942
+ switch (shell) {
4943
+ case "bash":
4944
+ return generateBash(tree);
4945
+ case "zsh":
4946
+ return generateZsh(tree);
4947
+ case "fish":
4948
+ return generateFish(tree);
4949
+ }
4950
+ }
4951
+
4952
+ // src/commands/completion-cli.ts
4953
+ function registerCompletionCommand(program2) {
4954
+ program2.command("completion").description("Print a shell completion script for bash, zsh, or fish").addArgument(new Argument("<shell>", "Target shell").choices([...SUPPORTED_SHELLS])).addHelpText("after", `
4955
+ Examples:
4956
+ eval "$(mushi completion bash)" # try it for the current session
4957
+ mushi completion bash >> ~/.bashrc # bash: persist across sessions
4958
+ mushi completion zsh > "\${fpath[1]}/_mushi" # zsh: needs a dir already on $fpath
4959
+ mushi completion fish > ~/.config/fish/completions/mushi.fish`).action((shell) => {
4960
+ if (!isSupportedShell(shell)) {
4961
+ console.error(`Unsupported shell "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
4962
+ process.exit(2);
4963
+ }
4964
+ const tree = buildCommandTree(program2);
4965
+ console.log(generateCompletionScript(shell, tree));
4966
+ });
4967
+ }
4968
+
5123
4969
  // src/index.ts
5124
4970
  installSignalHandlers();
5125
4971
  var program = new Command().name("mushi").description("Mushi CLI \u2014 set up the SDK, triage a report, fix from your editor").version(MUSHI_CLI_VERSION).addHelpText("after", `
@@ -5158,7 +5004,7 @@ registerQaCommands(program);
5158
5004
  registerAuditCommands(program);
5159
5005
  registerSkillsCommands(program);
5160
5006
  registerBillingCommands(program);
5007
+ registerCompletionCommand(program);
5161
5008
  program.parseAsync().catch((err) => {
5162
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
5163
- process.exit(1);
5009
+ printAndExit(err);
5164
5010
  });