@mushi-mushi/cli 0.27.0 → 0.28.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 · 328 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): 59 edge functions · 374 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>
@@ -1,17 +1,14 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-LFZ5ZIBI.js";
3
+ } from "./chunk-U2VGB63C.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,
@@ -644,11 +669,20 @@ async function waitForIngestReady(options) {
644
669
  }
645
670
 
646
671
  export {
672
+ trySaveKeyToKeychain,
673
+ savedSdkKeyFor,
674
+ resolveProfileName,
647
675
  CONFIG_PATH,
648
676
  loadConfig,
649
677
  ensureClientId,
650
678
  maybeShowTelemetryNotice,
651
679
  saveConfig,
680
+ SDK_KEY_SCOPES,
681
+ CLI_KEY_SCOPES,
682
+ SDK_KEY_LABEL,
683
+ CLI_KEY_LABEL,
684
+ probeKeyScope,
685
+ describeUnsafeSdkKey,
652
686
  consoleUrl,
653
687
  resolveConsoleUrlSync,
654
688
  resolveConsoleUrl,
@@ -657,10 +691,6 @@ export {
657
691
  cliSetupDeepLink,
658
692
  reportsUrl,
659
693
  openInBrowser,
660
- TEST_REPORT_FETCH_TIMEOUT_MS,
661
- resolveCloudEndpoint,
662
- assertEndpoint,
663
- normalizeEndpoint,
664
694
  requireUuid,
665
695
  apiCall,
666
696
  sanitizeEndpoint,