@mushi-mushi/cli 0.12.0 → 0.13.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
@@ -52,6 +52,9 @@ mushi --version
52
52
 
53
53
  ```bash
54
54
  mushi login --api-key mushi_xxx # store credentials in ~/.mushirc (mode 0o600)
55
+ mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --wait
56
+ # one-shot wiring: ~/.mushirc + .env.local + .cursor/mcp.json + heartbeat wait
57
+ mushi upgrade # bump installed @mushi-mushi/* packages to latest stable
55
58
  mushi status # project overview
56
59
  mushi reports list # recent reports
57
60
  mushi reports show <id> # one report
@@ -65,6 +68,51 @@ mushi config endpoint https://... # set API endpoint (https:// required outsi
65
68
  mushi sourcemaps upload --release <ver> --dir <dist> # upload .js.map / .css.map (sha256-idempotent)
66
69
  ```
67
70
 
71
+ ### `mushi connect`
72
+
73
+ Non-interactive equivalent of `mushi init` for agents and scripts — wires a
74
+ repo to an existing project in one shot:
75
+
76
+ 1. Saves credentials to `~/.mushirc` (mode `0o600`).
77
+ 2. Merges `MUSHI_*` / framework-prefixed env vars into `.env.local` —
78
+ existing keys are never overwritten (skip with `--no-env`).
79
+ 3. Writes the `@mushi-mushi/mcp` server block into `.cursor/mcp.json` and
80
+ ensures that file is gitignored, since it embeds the API key (skip with
81
+ `--no-ide`).
82
+ 4. With `--wait`, polls `GET /v1/sync/ingest-setup` every 3 s until the SDK
83
+ heartbeat (or first report) lands, up to `--wait-timeout <sec>`
84
+ (default 120). Rejected credentials (401/403/404) fail fast instead of
85
+ polling out the timeout.
86
+
87
+ ```bash
88
+ # Recommended: pass the key via env so it stays out of shell history
89
+ MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> \
90
+ --endpoint https://<ref>.supabase.co/functions/v1/api --wait
91
+ mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide --json
92
+ ```
93
+
94
+ Exits non-zero when `--wait` times out or credentials are rejected, so it
95
+ composes in setup scripts and CI.
96
+
97
+ ### `mushi upgrade`
98
+
99
+ Reads `package.json`, checks the npm registry for each installed
100
+ `@mushi-mushi/*` package, and runs the right install command for your package
101
+ manager (npm / pnpm / yarn / bun) to bump them to the latest stable release.
102
+
103
+ ```bash
104
+ mushi upgrade # plan + install
105
+ mushi upgrade --dry-run # print the install command without running it
106
+ mushi upgrade --json # machine-readable plan + result
107
+ mushi upgrade --cwd ../app # target another repo
108
+ ```
109
+
110
+ Pre-release versions are never auto-selected, registry versions are validated
111
+ against strict semver before being interpolated into the install command, and
112
+ non-registry specifiers (`workspace:`, `file:`, git URLs, dist-tags) are left
113
+ untouched. Legacy `@mushi-mushi/react` installs get a migration note pointing
114
+ at `@mushi-mushi/web`.
115
+
68
116
  ### `mushi sourcemaps upload`
69
117
 
70
118
  Recursively scans `--dir` for `.js.map` and `.css.map` files and uploads them
@@ -110,6 +158,7 @@ reachability only.
110
158
  ```bash
111
159
  mushi doctor # local checks only
112
160
  mushi doctor --server # + calls /preflight on the backend (all 4 dispatch checks)
161
+ mushi doctor --ingest # + calls /v1/sync/ingest-setup (API key → heartbeat → first report)
113
162
  mushi doctor --json # machine-readable JSON output (exits 1 if any check fails)
114
163
  ```
115
164
 
@@ -135,6 +184,13 @@ server checks (`key` values returned by the endpoint):
135
184
  `--server` requires `adminOrApiKey` credentials — set `MUSHI_API_KEY` to an
136
185
  admin key (not a public SDK key).
137
186
 
187
+ With `--ingest`, also calls `GET /v1/sync/ingest-setup` (authenticated with the
188
+ SDK API key) and reports each **required ingest step** — project exists, active
189
+ API key, SDK heartbeat, at least one ingested report — plus a
190
+ `Last SDK heartbeat` diagnostic with the timestamp and endpoint host. This is
191
+ the same payload `mushi connect --wait` polls, so a failing step here tells you
192
+ exactly why the banner isn't showing up.
193
+
138
194
  ### `mushi reset <projectId>`
139
195
 
140
196
  Archives a project and wipes its test data so the full onboarding flow can be
@@ -169,3 +225,9 @@ Exits automatically when the job reaches a terminal status (`completed`,
169
225
  ## License
170
226
 
171
227
  MIT
228
+
229
+
230
+ <!-- mushi-readme-stats-footer -->
231
+ ---
232
+
233
+ <sub>Monorepo scale (June 2026): 43 edge functions · 234 SQL migrations · 13 outbound plugins · 11 inbound adapters. 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,6 @@
1
+ // src/version.ts
2
+ var MUSHI_CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ MUSHI_CLI_VERSION
6
+ };
package/dist/index.js CHANGED
@@ -35,10 +35,11 @@ function loadConfig(path = CONFIG_PATH) {
35
35
  } else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
36
36
  file = migrateLegacyConfig() ?? {};
37
37
  }
38
+ const endpointFromEnv = process.env["MUSHI_API_ENDPOINT"] ?? process.env["MUSHI_ENDPOINT"] ?? void 0;
38
39
  const fromEnv = {
39
40
  ...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
40
41
  ...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
41
- ...process.env["MUSHI_API_ENDPOINT"] ? { endpoint: process.env["MUSHI_API_ENDPOINT"] } : {}
42
+ ...endpointFromEnv ? { endpoint: endpointFromEnv } : {}
42
43
  };
43
44
  return { ...file, ...fromEnv };
44
45
  }
@@ -444,7 +445,7 @@ function normalizeEndpoint(url) {
444
445
  var REGISTRY = "https://registry.npmjs.org";
445
446
  var DEFAULT_TIMEOUT_MS = 2e3;
446
447
  async function checkFreshness(packageName, currentVersion, opts = {}) {
447
- if (process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
448
+ if (!opts.ignoreOptOut && process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
448
449
  const registry = opts.registry ?? REGISTRY;
449
450
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
450
451
  const controller = new AbortController();
@@ -482,11 +483,13 @@ function isNewerStableVersion(latest, current) {
482
483
  return lc > cc;
483
484
  }
484
485
  function stripPreRelease(version) {
485
- const idx = version.indexOf("-");
486
+ const idx = version.search(/[-+]/);
486
487
  return idx === -1 ? version : version.slice(0, idx);
487
488
  }
488
489
  function hasPreReleaseTag(version) {
489
- return version.includes("-");
490
+ const plus = version.indexOf("+");
491
+ const hyphen = version.indexOf("-");
492
+ return hyphen !== -1 && (plus === -1 || hyphen < plus);
490
493
  }
491
494
  function parse(version) {
492
495
  const parts = version.split(".").map((part) => Number(part));
@@ -595,7 +598,7 @@ function getFrameworkFromPkg(pkg) {
595
598
  }
596
599
 
597
600
  // src/version.ts
598
- var MUSHI_CLI_VERSION = true ? "0.12.0" : "0.0.0-dev";
601
+ var MUSHI_CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
599
602
 
600
603
  // src/init.ts
601
604
  var ENV_FILES = [".env.local", ".env"];
@@ -744,7 +747,7 @@ async function installPackages(pm, packages, cwd) {
744
747
  function runCommand(pm, packages, cwd) {
745
748
  const verb = pm === "npm" ? "install" : "add";
746
749
  const command = process.platform === "win32" ? `${pm}.cmd` : pm;
747
- return new Promise((resolve2, reject) => {
750
+ return new Promise((resolve4, reject) => {
748
751
  const child = spawn(command, [verb, ...packages], {
749
752
  stdio: "inherit",
750
753
  shell: false,
@@ -753,7 +756,7 @@ function runCommand(pm, packages, cwd) {
753
756
  });
754
757
  child.on("error", reject);
755
758
  child.on("exit", (code) => {
756
- if (code === 0) resolve2();
759
+ if (code === 0) resolve4();
757
760
  else reject(new Error(`${pm} exited with code ${code ?? "null"}`));
758
761
  });
759
762
  });
@@ -1108,11 +1111,11 @@ async function findMapFiles(dir) {
1108
1111
  return results;
1109
1112
  }
1110
1113
  function fileHash(path) {
1111
- return new Promise((resolve2, reject) => {
1114
+ return new Promise((resolve4, reject) => {
1112
1115
  const hash = createHash("sha256");
1113
1116
  const stream = createReadStream(path);
1114
1117
  stream.on("data", (chunk) => hash.update(chunk));
1115
- stream.on("end", () => resolve2(hash.digest("hex")));
1118
+ stream.on("end", () => resolve4(hash.digest("hex")));
1116
1119
  stream.on("error", reject);
1117
1120
  });
1118
1121
  }
@@ -1416,13 +1419,82 @@ function renderNudgeExplainer(phase) {
1416
1419
  return lines.join("\n");
1417
1420
  }
1418
1421
 
1422
+ // src/heartbeat-wait.ts
1423
+ var IngestSetupHttpError = class extends Error {
1424
+ status;
1425
+ constructor(status) {
1426
+ super(`ingest-setup HTTP ${status}`);
1427
+ this.name = "IngestSetupHttpError";
1428
+ this.status = status;
1429
+ }
1430
+ };
1431
+ var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
1432
+ async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
1433
+ const res = await doFetch(`${config.endpoint}/v1/sync/ingest-setup`, {
1434
+ headers: {
1435
+ Authorization: `Bearer ${config.apiKey}`,
1436
+ "X-Mushi-Api-Key": config.apiKey,
1437
+ ...config.projectId ? { "X-Mushi-Project": config.projectId } : {}
1438
+ },
1439
+ signal: AbortSignal.timeout(8e3)
1440
+ });
1441
+ if (!res.ok) {
1442
+ if (NON_RETRYABLE_STATUSES.has(res.status)) throw new IngestSetupHttpError(res.status);
1443
+ return null;
1444
+ }
1445
+ const body = await res.json();
1446
+ return body.ok && body.data ? body.data : null;
1447
+ }
1448
+ async function waitForIngestReady(options) {
1449
+ const doFetch = options.fetch ?? globalThis.fetch;
1450
+ const maxAttempts = options.maxAttempts ?? 40;
1451
+ const intervalMs = options.intervalMs ?? 3e3;
1452
+ let lastPayload = null;
1453
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1454
+ if (options.signal?.aborted) {
1455
+ return { ok: false, payload: lastPayload, attempts: attempt - 1, reason: "aborted" };
1456
+ }
1457
+ try {
1458
+ lastPayload = await fetchIngestSetup(
1459
+ { endpoint: options.endpoint, apiKey: options.apiKey, projectId: options.projectId },
1460
+ doFetch
1461
+ );
1462
+ if (lastPayload) {
1463
+ options.onPoll?.(lastPayload, attempt);
1464
+ if (lastPayload.ready) {
1465
+ return { ok: true, payload: lastPayload, attempts: attempt, reason: "ready" };
1466
+ }
1467
+ const sdkStep = lastPayload.steps.find((s) => s.id === "sdk_installed");
1468
+ if (sdkStep?.complete) {
1469
+ return { ok: true, payload: lastPayload, attempts: attempt, reason: "heartbeat" };
1470
+ }
1471
+ }
1472
+ } catch (err) {
1473
+ const msg = err instanceof Error ? err.message : String(err);
1474
+ if (err instanceof IngestSetupHttpError) {
1475
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "unauthorized", error: msg };
1476
+ }
1477
+ if (options.signal?.aborted) {
1478
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "aborted", error: msg };
1479
+ }
1480
+ if (attempt === maxAttempts) {
1481
+ return { ok: false, payload: lastPayload, attempts: attempt, reason: "fetch-error", error: msg };
1482
+ }
1483
+ }
1484
+ if (attempt < maxAttempts) {
1485
+ await new Promise((r) => setTimeout(r, intervalMs));
1486
+ }
1487
+ }
1488
+ return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
1489
+ }
1490
+
1419
1491
  // src/doctor.ts
1420
1492
  function checkCliConfig(config) {
1421
1493
  return [
1422
1494
  {
1423
1495
  name: "CLI config file",
1424
1496
  ok: Boolean(config.endpoint),
1425
- detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint in ~/.mushirc \u2014 run `mushi init` or `mushi config endpoint <url>`"
1497
+ detail: config.endpoint ? `endpoint=${config.endpoint}` : "No endpoint \u2014 set MUSHI_API_ENDPOINT, run `mushi connect`, or `mushi config endpoint <url>`"
1426
1498
  },
1427
1499
  {
1428
1500
  name: "API key configured",
@@ -1453,11 +1525,11 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
1453
1525
  }
1454
1526
  async function checkSdkInstall(cwd) {
1455
1527
  try {
1456
- const { readFile: readFile2 } = await import("fs/promises");
1457
- const { join: join6, resolve: resolve2 } = await import("path");
1458
- const root = resolve2(cwd);
1459
- const pkgPath = join6(root, "package.json");
1460
- const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
1528
+ const { readFile: readFile3 } = await import("fs/promises");
1529
+ const { join: join7, resolve: resolve4 } = await import("path");
1530
+ const root = resolve4(cwd);
1531
+ const pkgPath = join7(root, "package.json");
1532
+ const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
1461
1533
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1462
1534
  const sdks = [
1463
1535
  "@mushi-mushi/react",
@@ -1519,6 +1591,44 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
1519
1591
  return [{ name: "Server preflight", ok: false, detail: `Fetch failed: ${msg}` }];
1520
1592
  }
1521
1593
  }
1594
+ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
1595
+ if (!config.apiKey || !config.endpoint) {
1596
+ return [
1597
+ {
1598
+ name: "Ingest setup",
1599
+ ok: false,
1600
+ detail: "Need apiKey and endpoint. Run `mushi connect`."
1601
+ }
1602
+ ];
1603
+ }
1604
+ try {
1605
+ const data = await fetchIngestSetup(
1606
+ { endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
1607
+ doFetch
1608
+ );
1609
+ if (!data) {
1610
+ return [{ name: "Ingest setup", ok: false, detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload" }];
1611
+ }
1612
+ const steps = data.steps ?? [];
1613
+ const checks = steps.filter((s) => s.required).map((s) => ({
1614
+ name: `[ingest] ${s.label}`,
1615
+ ok: s.complete,
1616
+ detail: s.hint ?? ""
1617
+ }));
1618
+ const diag = data.diagnostic;
1619
+ if (diag?.last_sdk_seen_at) {
1620
+ checks.push({
1621
+ name: "[ingest] Last SDK heartbeat",
1622
+ ok: true,
1623
+ detail: `${diag.last_sdk_seen_at}${diag.last_sdk_endpoint_host ? ` @ ${diag.last_sdk_endpoint_host}` : ""}`
1624
+ });
1625
+ }
1626
+ return checks.length > 0 ? checks : [{ name: "Ingest setup", ok: false, detail: "Empty response from /v1/sync/ingest-setup" }];
1627
+ } catch (err) {
1628
+ const msg = err instanceof Error ? err.message : String(err);
1629
+ return [{ name: "Ingest setup", ok: false, detail: `Fetch failed: ${msg}` }];
1630
+ }
1631
+ }
1522
1632
  async function runDoctor(config, options = {}) {
1523
1633
  const doFetch = options.fetch ?? globalThis.fetch;
1524
1634
  const checks = [];
@@ -1532,6 +1642,10 @@ async function runDoctor(config, options = {}) {
1532
1642
  const serverChecks = await checkServerPreflight(config, doFetch);
1533
1643
  checks.push(...serverChecks);
1534
1644
  }
1645
+ if (options.ingest) {
1646
+ const ingestChecks = await checkIngestSetup(config, doFetch);
1647
+ checks.push(...ingestChecks);
1648
+ }
1535
1649
  return { checks, ready: checks.every((c) => c.ok) };
1536
1650
  }
1537
1651
  function formatDoctorResult(result) {
@@ -1553,6 +1667,232 @@ ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
1553
1667
  return lines.join("\n");
1554
1668
  }
1555
1669
 
1670
+ // src/upgrade.ts
1671
+ import { resolve as resolve2 } from "path";
1672
+ import { execSync } from "child_process";
1673
+ var SAFE_NPM_VERSION = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
1674
+ var MUSHI_PACKAGES = [
1675
+ "@mushi-mushi/web",
1676
+ "@mushi-mushi/core",
1677
+ "@mushi-mushi/react",
1678
+ "@mushi-mushi/react-native",
1679
+ "@mushi-mushi/capacitor",
1680
+ "@mushi-mushi/node",
1681
+ "@mushi-mushi/cli"
1682
+ ];
1683
+ async function planUpgrade(cwd) {
1684
+ const root = resolve2(cwd);
1685
+ const pkg = readPackageJson(root);
1686
+ const pm = detectPackageManager(root);
1687
+ const deps = {
1688
+ ...pkg?.dependencies ?? {},
1689
+ ...pkg?.devDependencies ?? {}
1690
+ };
1691
+ const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
1692
+ const entries = await Promise.all(
1693
+ installed2.map(async (name) => {
1694
+ const current = deps[name] ?? "";
1695
+ const currentCore = current.replace(/^[\^~>=<]*/, "");
1696
+ if (!/^\d/.test(currentCore)) {
1697
+ return { name, current, latest: null, willUpgrade: false };
1698
+ }
1699
+ const freshness = await checkFreshness(name, currentCore, {
1700
+ timeoutMs: 4e3,
1701
+ ignoreOptOut: true
1702
+ });
1703
+ const latest = freshness?.latest ?? null;
1704
+ const safeLatest = latest && SAFE_NPM_VERSION.test(latest) ? latest : null;
1705
+ const willUpgrade = Boolean(freshness?.isOutdated && safeLatest);
1706
+ return {
1707
+ name,
1708
+ current,
1709
+ latest: safeLatest,
1710
+ willUpgrade,
1711
+ migrateToWeb: name === "@mushi-mushi/react" && willUpgrade
1712
+ };
1713
+ })
1714
+ );
1715
+ const toBump = entries.filter((e) => e.willUpgrade && e.latest).map((e) => `${e.name}@${e.latest}`);
1716
+ return {
1717
+ cwd: root,
1718
+ packageManager: pm,
1719
+ entries,
1720
+ installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
1721
+ };
1722
+ }
1723
+ async function runUpgrade(opts = {}) {
1724
+ const plan = await planUpgrade(opts.cwd ?? process.cwd());
1725
+ if (plan.entries.length === 0) {
1726
+ return {
1727
+ plan,
1728
+ upgraded: false,
1729
+ message: "No @mushi-mushi/* packages in package.json \u2014 run `mushi init` first."
1730
+ };
1731
+ }
1732
+ if (!plan.installCmd) {
1733
+ const semverEntries = plan.entries.filter((e) => /\d/.test(e.current));
1734
+ const allChecksFailed = semverEntries.length > 0 && semverEntries.every((e) => e.latest === null);
1735
+ return {
1736
+ plan,
1737
+ upgraded: false,
1738
+ message: allChecksFailed ? "Could not reach the npm registry to check for updates \u2014 try again in a moment." : "All installed Mushi packages are already at the latest stable version."
1739
+ };
1740
+ }
1741
+ if (opts.dryRun) {
1742
+ return { plan, upgraded: false, message: `[dry-run] Would run: ${plan.installCmd}` };
1743
+ }
1744
+ try {
1745
+ execSync(plan.installCmd, { cwd: plan.cwd, stdio: "inherit", env: process.env });
1746
+ } catch {
1747
+ return {
1748
+ plan,
1749
+ upgraded: false,
1750
+ message: `Install command failed \u2014 fix the error above or run it manually:
1751
+ ${plan.installCmd}`
1752
+ };
1753
+ }
1754
+ const reactEntry = plan.entries.find((e) => e.migrateToWeb);
1755
+ const migrateNote = reactEntry ? "\nNote: @mushi-mushi/react is legacy \u2014 prefer @mushi-mushi/web for Vite/Capacitor/SPA apps." : "";
1756
+ return {
1757
+ plan,
1758
+ upgraded: true,
1759
+ message: `Upgraded Mushi SDK packages.
1760
+ ${plan.installCmd}${migrateNote}`
1761
+ };
1762
+ }
1763
+
1764
+ // src/connect.ts
1765
+ import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
1766
+ import { existsSync as existsSync5 } from "fs";
1767
+ import { join as join6, resolve as resolve3 } from "path";
1768
+ function envKeyPresent(content, key) {
1769
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1770
+ return new RegExp(`^${escaped}=`, "m").test(content);
1771
+ }
1772
+ async function mergeEnvFile(path, lines) {
1773
+ const block = `
1774
+ # Mushi \u2014 added by mushi connect
1775
+ ${lines.join("\n")}
1776
+ `;
1777
+ if (existsSync5(path)) {
1778
+ const content = await readFile2(path, "utf8");
1779
+ const needs = lines.filter((line) => {
1780
+ const key = line.split("=")[0];
1781
+ return !envKeyPresent(content, key);
1782
+ });
1783
+ if (needs.length === 0) return false;
1784
+ await appendFile(path, `
1785
+ # Mushi \u2014 added by mushi connect
1786
+ ${needs.join("\n")}
1787
+ `, "utf8");
1788
+ return true;
1789
+ }
1790
+ await writeFile(path, block, "utf8");
1791
+ return true;
1792
+ }
1793
+ async function ensureMcpJsonGitignored(cwd, messages) {
1794
+ const gitignorePath = join6(cwd, ".gitignore");
1795
+ const patterns = [".cursor/mcp.json", ".cursor/"];
1796
+ if (!existsSync5(gitignorePath)) {
1797
+ messages.push(
1798
+ "\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
1799
+ );
1800
+ return;
1801
+ }
1802
+ const content = await readFile2(gitignorePath, "utf8");
1803
+ const covered = patterns.some((p3) => content.split("\n").some((line) => line.trim() === p3 || line.trim() === `${p3}/`));
1804
+ if (covered) return;
1805
+ await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
1806
+ messages.push("\u2713 Added .cursor/mcp.json to .gitignore (contains API key)");
1807
+ }
1808
+ async function runConnect(opts, baseConfig = {}) {
1809
+ const cwd = resolve3(opts.cwd ?? process.cwd());
1810
+ const endpoint = assertEndpoint(opts.endpoint);
1811
+ const messages = [];
1812
+ const config = {
1813
+ ...baseConfig,
1814
+ apiKey: opts.apiKey,
1815
+ projectId: opts.projectId,
1816
+ endpoint
1817
+ };
1818
+ saveConfig(config);
1819
+ messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
1820
+ let envPath = null;
1821
+ if (opts.writeEnv !== false) {
1822
+ const pkg = readPackageJson(cwd);
1823
+ const framework = detectFramework(cwd, pkg);
1824
+ const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
1825
+ envPath = join6(cwd, ".env.local");
1826
+ const wrote = await mergeEnvFile(envPath, lines);
1827
+ messages.push(
1828
+ wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
1829
+ );
1830
+ }
1831
+ let mcpPath = null;
1832
+ if (opts.wireIde !== false) {
1833
+ const mcpDir = join6(cwd, ".cursor");
1834
+ mcpPath = join6(mcpDir, "mcp.json");
1835
+ const serverName = `mushi-${opts.projectId.slice(0, 8)}`;
1836
+ const mcpServerBlock = {
1837
+ command: "npx",
1838
+ args: ["-y", "@mushi-mushi/mcp@latest"],
1839
+ env: {
1840
+ MUSHI_API_ENDPOINT: endpoint,
1841
+ MUSHI_PROJECT_ID: opts.projectId,
1842
+ MUSHI_API_KEY: opts.apiKey
1843
+ }
1844
+ };
1845
+ let merged = { mcpServers: {} };
1846
+ if (existsSync5(mcpPath)) {
1847
+ try {
1848
+ merged = JSON.parse(await readFile2(mcpPath, "utf8"));
1849
+ } catch {
1850
+ }
1851
+ }
1852
+ const servers = merged.mcpServers ?? {};
1853
+ servers[serverName] = mcpServerBlock;
1854
+ merged.mcpServers = servers;
1855
+ await mkdir(mcpDir, { recursive: true });
1856
+ await writeFile(mcpPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
1857
+ await ensureMcpJsonGitignored(cwd, messages);
1858
+ messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
1859
+ }
1860
+ let heartbeat = null;
1861
+ if (opts.wait) {
1862
+ const timeoutSec = opts.waitTimeoutSec ?? 120;
1863
+ const maxAttempts = Math.max(1, Math.ceil(timeoutSec * 1e3 / 3e3));
1864
+ messages.push(`\u2026 Waiting for SDK heartbeat (up to ${timeoutSec}s) \u2014 start your dev server with the snippet installed`);
1865
+ heartbeat = await waitForIngestReady({
1866
+ endpoint,
1867
+ apiKey: opts.apiKey,
1868
+ projectId: opts.projectId,
1869
+ maxAttempts,
1870
+ onPoll: (payload, attempt) => {
1871
+ if (!opts.json && attempt % 3 === 0) {
1872
+ const sdk = payload.steps.find((s) => s.id === "sdk_installed");
1873
+ const seen = payload.diagnostic?.last_sdk_seen_at ?? "never";
1874
+ process.stdout.write(` poll ${attempt}: sdk_installed=${sdk?.complete ? "yes" : "no"} last_seen=${seen}
1875
+ `);
1876
+ }
1877
+ }
1878
+ });
1879
+ if (heartbeat.ok) {
1880
+ const label = heartbeat.reason === "heartbeat" ? "SDK heartbeat detected" : "Ingest setup complete";
1881
+ messages.push(`\u2713 ${label} \u2014 ingest pipeline is live`);
1882
+ } else if (heartbeat.reason === "unauthorized") {
1883
+ messages.push(
1884
+ `\u2717 The backend rejected these credentials (${heartbeat.error ?? "auth error"}). Double-check --api-key, --project-id, and --endpoint, then re-run \`mushi connect --wait\`.`
1885
+ );
1886
+ } else {
1887
+ messages.push(
1888
+ `\u2717 No heartbeat before timeout (${heartbeat.reason}). Confirm env vars are in your build, restart the dev server, then re-run \`mushi connect --wait\`.`
1889
+ );
1890
+ }
1891
+ }
1892
+ const ok = !opts.wait || Boolean(heartbeat?.ok);
1893
+ return { ok, envPath, mcpPath, heartbeat, messages };
1894
+ }
1895
+
1556
1896
  // src/index.ts
1557
1897
  installSignalHandlers();
1558
1898
  var API_TIMEOUT_MS = 15e3;
@@ -2016,6 +2356,31 @@ reports.command("dismiss <id>").description("Dismiss a report (not a real bug /
2016
2356
  console.log(`\u2713 Dismissed report ${id}`);
2017
2357
  }
2018
2358
  });
2359
+ reports.command("reply <id> <message>").description("Send a visible reply to the reporter widget for a report").option("--author <name>", 'Display name for the sender (default: "Mushi Admin")').option("--json", "Machine-readable JSON output").addHelpText("after", `
2360
+ Examples:
2361
+ mushi reports reply abc123 "Thanks for reporting \u2014 fixing this in the next release."
2362
+ mushi reports reply abc123 "Can you share a screenshot?" --author "Alice"`).action(async (id, message, opts) => {
2363
+ const config = requireConfig();
2364
+ const body = { message };
2365
+ if (opts.author) body["author_name"] = opts.author;
2366
+ const result = await apiCall(`/v1/sync/reports/${id}/reply`, config, {
2367
+ method: "POST",
2368
+ body: JSON.stringify(body)
2369
+ });
2370
+ if (!result.ok) {
2371
+ if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
2372
+ process.stderr.write(`error: report "${id}" not found
2373
+ `);
2374
+ process.exit(3);
2375
+ }
2376
+ die(result);
2377
+ }
2378
+ if (opts.json) {
2379
+ console.log(JSON.stringify(result.data, null, 2));
2380
+ } else {
2381
+ console.log(`\u2713 Reply sent to reporter for report ${id}`);
2382
+ }
2383
+ });
2019
2384
  reports.command("search <query>").description("Search reports by keyword in summary and description").option("--limit <n>", "Max results (1\u201350)", "10").option("--status <status>", "Filter by status").option("--json", "Machine-readable JSON output").addHelpText("after", `
2020
2385
  Examples:
2021
2386
  mushi reports search "login button"
@@ -2117,7 +2482,7 @@ AI code review context.
2117
2482
  Typical CI usage:
2118
2483
  MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
2119
2484
  npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
2120
- const { writeFile, mkdir } = await import("fs/promises");
2485
+ const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
2121
2486
  const nodePath = await import("path");
2122
2487
  const config = requireConfig();
2123
2488
  const cwd = opts.cwd ?? process.cwd();
@@ -2144,8 +2509,8 @@ Typical CI usage:
2144
2509
  console.log(JSON.stringify(output, null, 2));
2145
2510
  return;
2146
2511
  }
2147
- await mkdir(nodePath.dirname(target), { recursive: true });
2148
- await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
2512
+ await mkdir2(nodePath.dirname(target), { recursive: true });
2513
+ await writeFile2(target, JSON.stringify(output, null, 2) + "\n", "utf8");
2149
2514
  if (opts.json) {
2150
2515
  console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
2151
2516
  } else {
@@ -2194,7 +2559,7 @@ Examples:
2194
2559
  mushi index ./src
2195
2560
  mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
2196
2561
  const config = requireConfig({ needsProject: true });
2197
- const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
2562
+ const { readdir: readdir2, readFile: readFile3, stat } = await import("fs/promises");
2198
2563
  const nodePath = await import("path");
2199
2564
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
2200
2565
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
@@ -2221,7 +2586,7 @@ Examples:
2221
2586
  `);
2222
2587
  continue;
2223
2588
  }
2224
- const source = await readFile2(file, "utf8");
2589
+ const source = await readFile3(file, "utf8");
2225
2590
  const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
2226
2591
  count++;
2227
2592
  bytes += source.length;
@@ -2278,8 +2643,8 @@ Typical first-time flow:
2278
2643
  # Browser opens \u2192 sign up / magic-link \u2192 come back to terminal
2279
2644
  # CLI writes .env.local and .cursor/mcp.json
2280
2645
  # mushi whoami to confirm`).action(async (opts) => {
2281
- const { writeFile, mkdir } = await import("fs/promises");
2282
- const { existsSync: existsSync5 } = await import("fs");
2646
+ const { writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
2647
+ const { existsSync: existsSync6 } = await import("fs");
2283
2648
  const nodePath = await import("path");
2284
2649
  const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
2285
2650
  const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
@@ -2304,7 +2669,7 @@ Typical first-time flow:
2304
2669
  console.log("");
2305
2670
  const { createInterface } = await import("readline");
2306
2671
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2307
- const ask = (q) => new Promise((resolve2) => rl.question(q, (a) => resolve2(a.trim())));
2672
+ const ask = (q) => new Promise((resolve4) => rl.question(q, (a) => resolve4(a.trim())));
2308
2673
  const projectId = await ask(" Project ID (uuid): ");
2309
2674
  const apiKey = await ask(" API key (mushi_...): ");
2310
2675
  rl.close();
@@ -2326,18 +2691,18 @@ Typical first-time flow:
2326
2691
  `MUSHI_API_KEY=${apiKey}`,
2327
2692
  ""
2328
2693
  ];
2329
- const envExisting = existsSync5(envPath);
2330
- await writeFile(envPath, envLines.join("\n"), "utf8");
2694
+ const envExisting = existsSync6(envPath);
2695
+ await writeFile2(envPath, envLines.join("\n"), "utf8");
2331
2696
  console.log(`
2332
2697
  \u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
2333
2698
  const mcpDir = nodePath.join(cwd, ".cursor");
2334
- await mkdir(mcpDir, { recursive: true });
2699
+ await mkdir2(mcpDir, { recursive: true });
2335
2700
  const mcpPath = nodePath.join(mcpDir, "mcp.json");
2336
2701
  const mcpJson = {
2337
2702
  mcpServers: {
2338
2703
  mushi: {
2339
2704
  command: "npx",
2340
- args: ["-y", "mushi-mcp@latest"],
2705
+ args: ["-y", "@mushi-mushi/mcp@latest"],
2341
2706
  env: {
2342
2707
  MUSHI_API_ENDPOINT: endpoint,
2343
2708
  MUSHI_PROJECT_ID: projectId,
@@ -2346,19 +2711,19 @@ Typical first-time flow:
2346
2711
  }
2347
2712
  }
2348
2713
  };
2349
- const mcpExisting = existsSync5(mcpPath);
2714
+ const mcpExisting = existsSync6(mcpPath);
2350
2715
  if (mcpExisting) {
2351
2716
  try {
2352
- const { readFile: readFile2 } = await import("fs/promises");
2353
- const raw = JSON.parse(await readFile2(mcpPath, "utf8"));
2717
+ const { readFile: readFile3 } = await import("fs/promises");
2718
+ const raw = JSON.parse(await readFile3(mcpPath, "utf8"));
2354
2719
  const existing = raw;
2355
2720
  existing.mcpServers = { ...existing.mcpServers ?? {}, mushi: mcpJson.mcpServers.mushi };
2356
- await writeFile(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
2721
+ await writeFile2(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
2357
2722
  } catch {
2358
- await writeFile(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
2723
+ await writeFile2(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
2359
2724
  }
2360
2725
  } else {
2361
- await writeFile(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
2726
+ await writeFile2(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
2362
2727
  }
2363
2728
  console.log(` \u2713 ${mcpExisting ? "Updated" : "Created"} .cursor/mcp.json`);
2364
2729
  console.log("");
@@ -2379,8 +2744,8 @@ Supported IDEs:
2379
2744
  zed \u2014 writes ~/.config/zed/settings.json mcpServers block
2380
2745
 
2381
2746
  The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).action(async (opts) => {
2382
- const { writeFile, mkdir, readFile: readFile2 } = await import("fs/promises");
2383
- const { existsSync: existsSync5 } = await import("fs");
2747
+ const { writeFile: writeFile2, mkdir: mkdir2, readFile: readFile3 } = await import("fs/promises");
2748
+ const { existsSync: existsSync6 } = await import("fs");
2384
2749
  const nodePath = await import("path");
2385
2750
  const os = await import("os");
2386
2751
  const config = requireConfig({ needsProject: true });
@@ -2401,7 +2766,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2401
2766
  const serverName = `mushi-${slug}`;
2402
2767
  const mcpServerBlock = {
2403
2768
  command: "npx",
2404
- args: ["-y", "mushi-mcp@latest"],
2769
+ args: ["-y", "@mushi-mushi/mcp@latest"],
2405
2770
  env: {
2406
2771
  MUSHI_API_ENDPOINT: config.endpoint,
2407
2772
  MUSHI_PROJECT_ID: config.projectId ?? "",
@@ -2412,9 +2777,9 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2412
2777
  const configPath = nodePath.join(configDir, ideEntry.file);
2413
2778
  if (ideEntry.format === "mcp-json") {
2414
2779
  let merged = { mcpServers: {} };
2415
- if (existsSync5(configPath)) {
2780
+ if (existsSync6(configPath)) {
2416
2781
  try {
2417
- const raw = await readFile2(configPath, "utf8");
2782
+ const raw = await readFile3(configPath, "utf8");
2418
2783
  merged = JSON.parse(raw);
2419
2784
  } catch {
2420
2785
  }
@@ -2427,15 +2792,15 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2427
2792
  console.log(`[dry-run] Would write ${configPath}:`);
2428
2793
  console.log(output);
2429
2794
  } else {
2430
- await mkdir(configDir, { recursive: true });
2431
- await writeFile(configPath, output, "utf8");
2795
+ await mkdir2(configDir, { recursive: true });
2796
+ await writeFile2(configPath, output, "utf8");
2432
2797
  console.log(`\u2713 Written ${configPath}`);
2433
2798
  }
2434
2799
  } else if (ideEntry.format === "zed") {
2435
2800
  let settings = {};
2436
- if (existsSync5(configPath)) {
2801
+ if (existsSync6(configPath)) {
2437
2802
  try {
2438
- const raw = await readFile2(configPath, "utf8");
2803
+ const raw = await readFile3(configPath, "utf8");
2439
2804
  settings = JSON.parse(raw);
2440
2805
  } catch {
2441
2806
  }
@@ -2444,7 +2809,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2444
2809
  servers[serverName] = {
2445
2810
  command: {
2446
2811
  path: "npx",
2447
- args: ["-y", "mushi-mcp@latest"],
2812
+ args: ["-y", "@mushi-mushi/mcp@latest"],
2448
2813
  env: {
2449
2814
  MUSHI_API_ENDPOINT: config.endpoint,
2450
2815
  MUSHI_PROJECT_ID: config.projectId ?? "",
@@ -2459,8 +2824,8 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2459
2824
  console.log(`[dry-run] Would write ${configPath}:`);
2460
2825
  console.log(output);
2461
2826
  } else {
2462
- await mkdir(configDir, { recursive: true });
2463
- await writeFile(configPath, output, "utf8");
2827
+ await mkdir2(configDir, { recursive: true });
2828
+ await writeFile2(configPath, output, "utf8");
2464
2829
  console.log(`\u2713 Written ${configPath}`);
2465
2830
  }
2466
2831
  }
@@ -2494,7 +2859,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2494
2859
  if (opts.dryRun) {
2495
2860
  console.log(`[dry-run] Would write ${rulesPath}`);
2496
2861
  } else {
2497
- await writeFile(rulesPath, rulesContent, "utf8");
2862
+ await writeFile2(rulesPath, rulesContent, "utf8");
2498
2863
  console.log(`\u2713 Written .cursorrules`);
2499
2864
  }
2500
2865
  } else if (opts.ide === "claude") {
@@ -2503,8 +2868,8 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
2503
2868
  if (opts.dryRun) {
2504
2869
  console.log(`[dry-run] Would write ${rulesPath}`);
2505
2870
  } else {
2506
- await mkdir(rulesDir, { recursive: true });
2507
- await writeFile(rulesPath, rulesContent, "utf8");
2871
+ await mkdir2(rulesDir, { recursive: true });
2872
+ await writeFile2(rulesPath, rulesContent, "utf8");
2508
2873
  console.log(`\u2713 Written .claude/rules/mushi.md`);
2509
2874
  }
2510
2875
  }
@@ -2558,6 +2923,7 @@ Examples:
2558
2923
  emitEvent("dispatch.start", { reportId, agent: opts.agent, model: opts.model ?? null });
2559
2924
  const body = {
2560
2925
  reportId,
2926
+ projectId: cfg.projectId,
2561
2927
  agent: opts.agent
2562
2928
  };
2563
2929
  if (opts.agent === "cursor_cloud") {
@@ -2638,14 +3004,64 @@ program.command("nudge").description(
2638
3004
  }
2639
3005
  console.log(renderNudgeSnippet({ phase, overrides }));
2640
3006
  });
3007
+ program.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--dry-run", "Print the install command without running it").option("--json", "Machine-readable plan + result").addHelpText("after", `
3008
+ Examples:
3009
+ mushi upgrade
3010
+ mushi upgrade --dry-run
3011
+ mushi upgrade --cwd ../glot.it`).action(async (opts) => {
3012
+ const result = await runUpgrade({ cwd: opts.cwd, dryRun: opts.dryRun, json: opts.json });
3013
+ if (opts.json) {
3014
+ console.log(JSON.stringify(result, null, 2));
3015
+ } else {
3016
+ console.log(result.message);
3017
+ for (const e of result.plan.entries) {
3018
+ const tag = e.willUpgrade && e.latest ? `\u2192 v${e.latest}` : "(current)";
3019
+ console.log(` ${e.name}@${e.current} ${tag}`);
3020
+ }
3021
+ }
3022
+ if (!result.upgraded && result.plan.entries.some((e) => e.willUpgrade) && !opts.dryRun) {
3023
+ process.exit(1);
3024
+ }
3025
+ if (result.plan.entries.length === 0) process.exit(1);
3026
+ });
3027
+ program.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", `
3028
+ Examples:
3029
+ MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
3030
+ mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
3031
+ const apiKey = process.env.MUSHI_API_KEY ?? opts.apiKey;
3032
+ if (!apiKey) {
3033
+ console.error("Provide the API key via the MUSHI_API_KEY env var (recommended) or --api-key <key>.");
3034
+ process.exit(1);
3035
+ }
3036
+ const result = await runConnect({
3037
+ apiKey,
3038
+ projectId: opts.projectId,
3039
+ endpoint: opts.endpoint,
3040
+ cwd: opts.cwd,
3041
+ writeEnv: opts.env !== false,
3042
+ wireIde: opts.ide !== false,
3043
+ wait: opts.wait,
3044
+ waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
3045
+ json: opts.json
3046
+ });
3047
+ if (opts.json) {
3048
+ console.log(JSON.stringify(result, null, 2));
3049
+ } else {
3050
+ for (const line of result.messages) console.log(line);
3051
+ }
3052
+ if (!result.ok) process.exit(1);
3053
+ });
2641
3054
  program.command("doctor").description(
2642
3055
  "Run pre-flight checks: CLI config, endpoint reachability, API key shape, SDK install status, and (with --server) the same 4 dispatch-readiness checks shown in the Mushi console. Mirrors the in-console dispatch preflight so you can spot setup gaps before opening the admin UI."
2643
3056
  ).option("--cwd <path>", "Run package detection from a different directory").option("--json", "Machine-readable output").option(
2644
3057
  "--server",
2645
3058
  "Also call GET /preflight on the backend and include the 4 dispatch checks (GitHub repo, codebase indexed, Anthropic key, autofix enabled). Requires a configured projectId and API key."
3059
+ ).option(
3060
+ "--ingest",
3061
+ "Also call GET /v1/sync/ingest-setup for the 4 required ingest steps (API key, SDK heartbeat, first report). Composable with --server."
2646
3062
  ).action(async (opts) => {
2647
3063
  const config = loadConfig();
2648
- const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server });
3064
+ const result = await runDoctor(config, { cwd: opts.cwd, server: opts.server, ingest: opts.ingest });
2649
3065
  const { checks } = result;
2650
3066
  if (opts.json) {
2651
3067
  console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
@@ -2957,4 +3373,7 @@ keys.command("add").description("Add a new API key to the pool").requiredOption(
2957
3373
  }
2958
3374
  console.log(`\u2713 Key added \u2014 id: ${res.data.id}`);
2959
3375
  });
2960
- program.parse();
3376
+ program.parseAsync().catch((err) => {
3377
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
3378
+ process.exit(1);
3379
+ });
package/dist/init.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-NYPX5KXR.js";
9
9
  import {
10
10
  MUSHI_CLI_VERSION
11
- } from "./chunk-F43X732E.js";
11
+ } from "./chunk-IMDLL4EO.js";
12
12
 
13
13
  // src/init.ts
14
14
  import * as p from "@clack/prompts";
@@ -49,10 +49,11 @@ function loadConfig(path = CONFIG_PATH) {
49
49
  } else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
50
50
  file = migrateLegacyConfig() ?? {};
51
51
  }
52
+ const endpointFromEnv = process.env["MUSHI_API_ENDPOINT"] ?? process.env["MUSHI_ENDPOINT"] ?? void 0;
52
53
  const fromEnv = {
53
54
  ...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
54
55
  ...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
55
- ...process.env["MUSHI_API_ENDPOINT"] ? { endpoint: process.env["MUSHI_API_ENDPOINT"] } : {}
56
+ ...endpointFromEnv ? { endpoint: endpointFromEnv } : {}
56
57
  };
57
58
  return { ...file, ...fromEnv };
58
59
  }
@@ -126,7 +127,7 @@ function normalizeEndpoint(url) {
126
127
  var REGISTRY = "https://registry.npmjs.org";
127
128
  var DEFAULT_TIMEOUT_MS = 2e3;
128
129
  async function checkFreshness(packageName, currentVersion, opts = {}) {
129
- if (process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
130
+ if (!opts.ignoreOptOut && process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
130
131
  const registry = opts.registry ?? REGISTRY;
131
132
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
132
133
  const controller = new AbortController();
@@ -164,11 +165,13 @@ function isNewerStableVersion(latest, current) {
164
165
  return lc > cc;
165
166
  }
166
167
  function stripPreRelease(version) {
167
- const idx = version.indexOf("-");
168
+ const idx = version.search(/[-+]/);
168
169
  return idx === -1 ? version : version.slice(0, idx);
169
170
  }
170
171
  function hasPreReleaseTag(version) {
171
- return version.includes("-");
172
+ const plus = version.indexOf("+");
173
+ const hyphen = version.indexOf("-");
174
+ return hyphen !== -1 && (plus === -1 || hyphen < plus);
172
175
  }
173
176
  function parse(version) {
174
177
  const parts = version.split(".").map((part) => Number(part));
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-F43X732E.js";
3
+ } from "./chunk-IMDLL4EO.js";
4
4
  export {
5
5
  MUSHI_CLI_VERSION
6
6
  };
package/package.json CHANGED
@@ -1,28 +1,22 @@
1
1
  {
2
2
  "name": "@mushi-mushi/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "description": "CLI for Mushi Mushi — `mushi init` wizard installs the right SDK for your framework, plus report triage and pipeline health commands",
6
6
  "bin": {
7
7
  "mushi": "./dist/index.js"
8
8
  },
9
- "scripts": {
10
- "build": "tsup",
11
- "lint": "eslint src/",
12
- "test": "vitest run",
13
- "typecheck": "tsc --noEmit"
14
- },
15
9
  "dependencies": {
16
10
  "@clack/prompts": "^1.3.0",
17
11
  "commander": "^14.0.3"
18
12
  },
19
13
  "devDependencies": {
20
- "@mushi-mushi/eslint-config": "workspace:*",
21
14
  "@types/node": "^22.19.17",
22
15
  "eslint": "^10.3.0",
23
16
  "tsup": "^8.5.1",
24
17
  "typescript": "^6.0.3",
25
- "vitest": "^4.1.5"
18
+ "vitest": "^4.1.5",
19
+ "@mushi-mushi/eslint-config": "0.0.0"
26
20
  },
27
21
  "author": "Kenji Sakuramoto",
28
22
  "repository": {
@@ -109,5 +103,11 @@
109
103
  "type": "module",
110
104
  "engines": {
111
105
  "node": ">=20"
106
+ },
107
+ "scripts": {
108
+ "build": "tsup",
109
+ "lint": "eslint src/",
110
+ "test": "vitest run",
111
+ "typecheck": "tsc --noEmit"
112
112
  }
113
- }
113
+ }
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var MUSHI_CLI_VERSION = true ? "0.12.0" : "0.0.0-dev";
3
-
4
- export {
5
- MUSHI_CLI_VERSION
6
- };