@mushi-mushi/cli 0.27.1 → 0.29.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/README.md CHANGED
@@ -181,10 +181,15 @@ local bumps; use the PR flow when you want review + CI on the dependency change.
181
181
  ```bash
182
182
  mushi upgrade # plan + install
183
183
  mushi upgrade --dry-run # print the install command without running it
184
+ mushi upgrade --check # plan only: exit 0 current, 1 outdated, 2 registry down
184
185
  mushi upgrade --json # machine-readable plan + result
185
186
  mushi upgrade --cwd ../app # target another repo
186
187
  ```
187
188
 
189
+ `--check` never installs. Use it in a **non-required** CI job so a one-patch
190
+ drift cannot freeze production deploys. Exit `2` (registry unreachable) should
191
+ not fail a required gate — retry or treat as a warning.
192
+
188
193
  Pre-release versions are never auto-selected, registry versions are validated
189
194
  against strict semver before being interpolated into the install command, and
190
195
  non-registry specifiers (`workspace:`, `file:`, git URLs, dist-tags) are left
@@ -440,4 +445,4 @@ The CLI is also importable for tooling (used by `create-mushi-mushi`):
440
445
  MIT
441
446
 
442
447
  <!-- mushi-readme-stats-footer -->
443
- <sub>Monorepo scale (July 2026): 55 edge functions · 331 SQL migrations · 13 outbound plugins · 11 inbound adapters · 19 pipeline agents. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
448
+ <sub>Monorepo scale (July 2026): 64 edge functions · 410 SQL migrations · 13 outbound plugins · 11 inbound adapters · 19 pipeline agents. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/version.ts
4
+ var MUSHI_CLI_VERSION = true ? "0.29.0" : "0.0.0-dev";
5
+ var MUSHI_MCP_PIN_SPEC = "@mushi-mushi/mcp@0.24.0";
6
+
7
+ export {
8
+ MUSHI_CLI_VERSION,
9
+ MUSHI_MCP_PIN_SPEC
10
+ };
@@ -1,17 +1,14 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-XLGHUOKY.js";
3
+ } from "./chunk-UZZUGLI2.js";
4
+ import {
5
+ assertEndpoint,
6
+ normalizeEndpoint
7
+ } from "./chunk-SXSI2W6Z.js";
4
8
  import {
5
9
  __require
6
10
  } from "./chunk-3RG5ZIWI.js";
7
11
 
8
- // src/config.ts
9
- import { randomUUID } from "crypto";
10
- import { spawnSync } from "child_process";
11
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
12
- import { homedir } from "os";
13
- import { dirname, join } from "path";
14
-
15
12
  // src/keychain.ts
16
13
  var KEYCHAIN_SERVICE = "mushi-cli";
17
14
  function accountName(profile = "default") {
@@ -29,6 +26,18 @@ function getEntryClass() {
29
26
  }
30
27
  return _EntryClass ?? null;
31
28
  }
29
+ function trySaveKeyToKeychain(apiKey, profile = "default") {
30
+ if (!apiKey) return false;
31
+ const EntryClass = getEntryClass();
32
+ if (!EntryClass) return false;
33
+ try {
34
+ const entry = new EntryClass(KEYCHAIN_SERVICE, accountName(profile));
35
+ entry.setPassword(apiKey);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
32
41
  function tryLoadKeyFromKeychain(profile = "default") {
33
42
  const EntryClass = getEntryClass();
34
43
  if (!EntryClass) return null;
@@ -42,10 +51,20 @@ function tryLoadKeyFromKeychain(profile = "default") {
42
51
  }
43
52
 
44
53
  // src/config.ts
54
+ import { randomUUID } from "crypto";
55
+ import { spawnSync } from "child_process";
56
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
57
+ import { homedir } from "os";
58
+ import { dirname, join } from "path";
45
59
  var DEFAULT_PROFILE = "default";
46
60
  function isMultiProfileFile(value) {
47
61
  return typeof value === "object" && value !== null && "profiles" in value && typeof value.profiles === "object" && value.profiles !== null;
48
62
  }
63
+ function savedSdkKeyFor(config, projectId) {
64
+ const saved = config.sdkKey;
65
+ if (!projectId || !saved || typeof saved !== "object") return void 0;
66
+ return saved.projectId === projectId && typeof saved.key === "string" && saved.key ? saved.key : void 0;
67
+ }
49
68
  function resolveProfileName(explicit, fileActive) {
50
69
  const fromEnv = process.env["MUSHI_PROFILE"]?.trim();
51
70
  return explicit?.trim() || fromEnv || fileActive || DEFAULT_PROFILE;
@@ -225,6 +244,47 @@ function tightenWindowsAcl(path, type) {
225
244
  }
226
245
  }
227
246
 
247
+ // src/key-scopes.ts
248
+ var SDK_KEY_SCOPES = ["report:write"];
249
+ var CLI_KEY_SCOPES = ["report:write", "mcp:read"];
250
+ var SDK_KEY_LABEL = "sdk-ingest";
251
+ var CLI_KEY_LABEL = "cli-login";
252
+ async function probeKeyScope(endpoint, apiKey, projectId, doFetch = globalThis.fetch) {
253
+ let base = endpoint;
254
+ while (base.endsWith("/")) base = base.slice(0, -1);
255
+ let res;
256
+ try {
257
+ res = await doFetch(`${base}/v1/admin/mcp/account-overview`, {
258
+ headers: { "X-Mushi-Api-Key": apiKey, "X-Mushi-Project": projectId },
259
+ signal: AbortSignal.timeout(8e3)
260
+ });
261
+ } catch (err) {
262
+ return { result: "unreachable", message: err instanceof Error ? err.message : String(err) };
263
+ }
264
+ if (res.ok) return { result: "mcp", status: res.status };
265
+ if (res.status === 401) return { result: "invalid", status: res.status };
266
+ if (res.status === 403) {
267
+ const body = await res.json().catch(() => null);
268
+ const code = body?.error?.code ?? body?.code;
269
+ if (code === "INSUFFICIENT_SCOPE") return { result: "ingest-only", status: res.status };
270
+ }
271
+ return { result: "unknown", status: res.status };
272
+ }
273
+ function describeUnsafeSdkKey(probe, endpoint) {
274
+ switch (probe.result) {
275
+ case "mcp":
276
+ return "this key can read your bug reports (mcp scope), and SDK env vars ship inside the app bundle";
277
+ case "invalid":
278
+ return "the backend rejected this key";
279
+ case "unreachable":
280
+ return `could not reach ${endpoint} to confirm the key is ingest-only (${probe.message ?? "network error"})`;
281
+ case "unknown":
282
+ return `could not confirm the key is ingest-only (HTTP ${probe.status ?? "?"})`;
283
+ case "ingest-only":
284
+ return "the key is ingest-only";
285
+ }
286
+ }
287
+
228
288
  // src/console-url.ts
229
289
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
230
290
  import { dirname as dirname2, join as join2 } from "path";
@@ -341,41 +401,6 @@ async function openInBrowser(url) {
341
401
  });
342
402
  }
343
403
 
344
- // src/endpoint.ts
345
- var TEST_REPORT_TIMEOUT_MS = 1e4;
346
- var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
347
- var CLOUD_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/functions/v1/api";
348
- function resolveCloudEndpoint(explicit) {
349
- if (explicit?.trim()) return explicit.trim();
350
- const fromEnv = process.env.MUSHI_API_ENDPOINT?.trim();
351
- if (fromEnv) return fromEnv;
352
- return CLOUD_API_ENDPOINT;
353
- }
354
- function assertEndpoint(url) {
355
- let parsed;
356
- try {
357
- parsed = new URL(url);
358
- } catch {
359
- throw new Error(`Invalid endpoint URL: ${url}`);
360
- }
361
- const host = parsed.hostname;
362
- const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local");
363
- if (parsed.protocol !== "https:" && !isLocal) {
364
- throw new Error(`Endpoint must use https:// (got ${parsed.protocol}//${host}).`);
365
- }
366
- return normalizeEndpoint(parsed.origin + parsed.pathname);
367
- }
368
- function normalizeEndpoint(url) {
369
- if (!url) {
370
- throw new Error(
371
- "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"
372
- );
373
- }
374
- let end = url.length;
375
- while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
376
- return url.slice(0, end);
377
- }
378
-
379
404
  // src/errors.ts
380
405
  var EXIT_CODE_MAP = {
381
406
  E_AUTH_MISSING: 2,
@@ -446,7 +471,30 @@ function requireUuid(value, label = "id") {
446
471
  );
447
472
  }
448
473
  var API_TIMEOUT_MS = 15e3;
449
- async function apiCall(path, config, options = {}) {
474
+ function normalizeApiError(error, httpStatus) {
475
+ const fallbackCode = httpStatus ? `HTTP_${httpStatus}` : "API_ERROR";
476
+ if (typeof error === "string") return { code: fallbackCode, message: error };
477
+ if (error && typeof error === "object") {
478
+ const e = error;
479
+ if (typeof e["code"] === "string" && typeof e["message"] === "string") {
480
+ return e;
481
+ }
482
+ const formErrors = Array.isArray(e["formErrors"]) ? e["formErrors"].map(String) : [];
483
+ const fieldErrors = e["fieldErrors"] && typeof e["fieldErrors"] === "object" ? Object.entries(e["fieldErrors"]).map(
484
+ ([field, msgs]) => `${field}: ${Array.isArray(msgs) ? msgs.join(", ") : String(msgs)}`
485
+ ) : [];
486
+ const parts = [...formErrors, ...fieldErrors];
487
+ if (parts.length > 0) return { code: "VALIDATION_ERROR", message: parts.join("; ") };
488
+ return {
489
+ ...e,
490
+ code: typeof e["code"] === "string" ? e["code"] : fallbackCode,
491
+ message: typeof e["message"] === "string" ? e["message"] : `Request failed${httpStatus ? ` (${httpStatus})` : ""}`
492
+ };
493
+ }
494
+ return { code: fallbackCode, message: `Request failed${httpStatus ? ` (${httpStatus})` : ""}` };
495
+ }
496
+ async function apiCall(path, config, options = {}, callOptions = {}) {
497
+ const timeoutMs = callOptions.timeoutMs ?? API_TIMEOUT_MS;
450
498
  const endpoint = config.endpoint;
451
499
  if (!endpoint) {
452
500
  return {
@@ -458,7 +506,7 @@ async function apiCall(path, config, options = {}) {
458
506
  };
459
507
  }
460
508
  const controller = new AbortController();
461
- const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
509
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
462
510
  const signals = [controller.signal, getAbortSignal()];
463
511
  const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
464
512
  try {
@@ -487,6 +535,7 @@ async function apiCall(path, config, options = {}) {
487
535
  }
488
536
  } else {
489
537
  const text = await res.text();
538
+ if (callOptions.text && res.ok) return { ok: true, data: text };
490
539
  try {
491
540
  body = JSON.parse(text);
492
541
  } catch {
@@ -510,6 +559,13 @@ async function apiCall(path, config, options = {}) {
510
559
  error: { code: errCode, message: errMsg }
511
560
  };
512
561
  }
562
+ if (typeof body === "object" && body !== null && body.ok === false) {
563
+ return {
564
+ ok: false,
565
+ httpStatus: res.status,
566
+ error: normalizeApiError(body.error, res.status)
567
+ };
568
+ }
513
569
  return body;
514
570
  } catch (err) {
515
571
  clearTimeout(timer);
@@ -518,7 +574,7 @@ async function apiCall(path, config, options = {}) {
518
574
  ok: false,
519
575
  error: {
520
576
  code: "TIMEOUT",
521
- message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
577
+ message: `Request timed out after ${timeoutMs / 1e3}s. Check your network or endpoint.`
522
578
  }
523
579
  };
524
580
  }
@@ -644,11 +700,20 @@ async function waitForIngestReady(options) {
644
700
  }
645
701
 
646
702
  export {
703
+ trySaveKeyToKeychain,
704
+ savedSdkKeyFor,
705
+ resolveProfileName,
647
706
  CONFIG_PATH,
648
707
  loadConfig,
649
708
  ensureClientId,
650
709
  maybeShowTelemetryNotice,
651
710
  saveConfig,
711
+ SDK_KEY_SCOPES,
712
+ CLI_KEY_SCOPES,
713
+ SDK_KEY_LABEL,
714
+ CLI_KEY_LABEL,
715
+ probeKeyScope,
716
+ describeUnsafeSdkKey,
652
717
  consoleUrl,
653
718
  resolveConsoleUrlSync,
654
719
  resolveConsoleUrl,
@@ -657,10 +722,6 @@ export {
657
722
  cliSetupDeepLink,
658
723
  reportsUrl,
659
724
  openInBrowser,
660
- TEST_REPORT_FETCH_TIMEOUT_MS,
661
- resolveCloudEndpoint,
662
- assertEndpoint,
663
- normalizeEndpoint,
664
725
  requireUuid,
665
726
  apiCall,
666
727
  sanitizeEndpoint,