@mushi-mushi/cli 0.6.1 → 0.7.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/CONTRIBUTING.md CHANGED
@@ -91,6 +91,33 @@ pnpm changeset
91
91
 
92
92
  Select the affected packages, the semver bump type, and write a summary. The changeset file gets committed with your PR.
93
93
 
94
+ ## Release flow
95
+
96
+ Releases are fully automated. Maintainers don't run `npm publish` by hand.
97
+
98
+ 1. PRs land on `master` with one or more changeset files in `.changeset/`.
99
+ 2. `release.yml` runs on every push to `master`. It opens (or updates) a `chore: version packages` PR that bumps every affected `package.json`, rolls up the changelogs, and deletes the consumed changesets.
100
+ 3. Merging that "Version Packages" PR re-fires `release.yml`. The publish step authenticates to npm via **OpenID Connect (OIDC) Trusted Publishers** — no long-lived `NPM_TOKEN` is exchanged — and every tarball ships with a **Sigstore provenance attestation** uploaded to the public transparency log.
101
+
102
+ If GitHub's anti-loop protection suppresses the auto re-fire (the squash merge can be attributed to `github-actions[bot]`), trigger the workflow manually: **Actions → release → Run workflow → master**.
103
+
104
+ ### Adding a brand-new publishable package
105
+
106
+ Trusted Publisher bindings are configured **per package** on `npmjs.com` and require the package to already exist on the registry. New packages therefore need a one-time bootstrap before OIDC can take over.
107
+
108
+ 1. Add the package under `packages/<name>/` with a real `version`, `files`, `publishConfig.access: "public"`, `LICENSE`, and the standard fields enforced by `pnpm check:publish-readiness`.
109
+ 2. Build it locally: `pnpm install && pnpm -r build`.
110
+ 3. Mint a short-lived granular access token at `https://www.npmjs.com/settings/<your-user>/tokens/granular-access-tokens/new` — **Bypass 2FA: ON**, **Read and write: All packages**, **Expiration: 7 days**.
111
+ 4. Bootstrap-publish:
112
+ ```bash
113
+ NPM_TOKEN=npm_xxx pnpm bootstrap:new-package
114
+ ```
115
+ The script auto-detects which workspace packages are missing on npm and publishes them via `pnpm publish --no-provenance` (so `workspace:^` specifiers get rewritten to real semver in the tarball).
116
+ 5. The script prints one URL per freshly-published package. Open each, click **GitHub Actions** under "Trusted Publisher", confirm the auto-filled fields (`<owner>` / `<repo>` / `release.yml`), and tap your security key.
117
+ 6. Revoke the bootstrap token at `https://www.npmjs.com/settings/<your-user>/tokens`.
118
+
119
+ From the next changeset bump onward, that package publishes through the normal `release.yml` flow with full OIDC provenance — same as the rest.
120
+
94
121
  ## Code Style
95
122
 
96
123
  - **TypeScript strict mode** — no `any` unless absolutely necessary
package/README.md CHANGED
@@ -62,8 +62,25 @@ mushi test # submit a test report end-to-end
62
62
  mushi migrate # suggest the most relevant migration guide
63
63
  mushi migrate --json # machine-readable JSON for CI
64
64
  mushi config endpoint https://... # set API endpoint (https:// required outside localhost)
65
+ mushi sourcemaps upload --release <ver> --dir <dist> # upload .js.map / .css.map (sha256-idempotent)
65
66
  ```
66
67
 
68
+ ### `mushi sourcemaps upload`
69
+
70
+ Recursively scans `--dir` for `.js.map` and `.css.map` files and uploads them
71
+ under the given `--release`. Each file is hashed (sha256) and skipped if the
72
+ server already has it for that release, so the command is safe to run from
73
+ every CI build without churning storage.
74
+
75
+ ```bash
76
+ mushi sourcemaps upload --release 1.4.2 --dir ./dist
77
+ mushi sourcemaps upload --release "$GITHUB_SHA" --dir ./build --dry-run
78
+ mushi sourcemaps upload --release "$GITHUB_SHA" --dir ./build --silent
79
+ ```
80
+
81
+ Requires `MUSHI_API_ENDPOINT` and `MUSHI_API_KEY` (or pass `--endpoint` /
82
+ `--api-key`). Exits non-zero on any upload failure so CI gates fail fast.
83
+
67
84
  ### `mushi migrate`
68
85
 
69
86
  Reads `package.json` (deps + devDeps + peerDeps) and prints links to the
package/SECURITY.md CHANGED
@@ -19,15 +19,59 @@ If you discover a security vulnerability, please report it responsibly.
19
19
 
20
20
  **Do NOT open a public GitHub issue.**
21
21
 
22
- Instead, email: **kensaurus@gmail.com**
22
+ Use either channel below:
23
+
24
+ 1. **GitHub Private Vulnerability Reporting** — strongly preferred.
25
+ <https://github.com/kensaurus/mushi-mushi/security/advisories/new>
26
+ Routes the report into a private advisory with built-in CVE issuance,
27
+ patch coordination, and contributor-credit workflow.
28
+ 2. **Email** — `kensaurus@gmail.com`, subject prefix `[mushi-security]`.
29
+ PGP welcome but not required.
23
30
 
24
31
  Include:
25
32
  - Description of the vulnerability
26
- - Steps to reproduce
27
- - Impact assessment
33
+ - Steps to reproduce (smallest reproducer wins)
34
+ - Impact assessment (what an attacker gains)
28
35
  - Suggested fix (if any)
36
+ - Whether you want public credit (and how to spell your name)
37
+
38
+ ### Coordinated-disclosure timeline
39
+
40
+ | Day | Action |
41
+ |-----|--------|
42
+ | 0 | Report received |
43
+ | ≤ 2 | Acknowledgment + assigned a tracking ID |
44
+ | ≤ 7 | Triage complete: severity assigned (CVSS 3.1) and target patch date communicated |
45
+ | ≤ 30 | Patch released for critical / high (CVSS ≥ 7.0); ≤ 60 days for medium; best-effort for low |
46
+ | Patch + 7 | Public advisory + CVE published; reporter credited unless they declined |
47
+ | Patch + 90 | Embargo expires regardless; if upstream is unresponsive, the reporter is free to publish |
48
+
49
+ ### Safe harbor
50
+
51
+ Good-faith security research on Mushi Mushi is welcome. If you stay
52
+ within the rules below, we will not pursue legal action, will not ask
53
+ your hosting provider to take you offline, and will publicly credit your
54
+ work:
55
+
56
+ - Test only against your own self-hosted instance, the public demo at
57
+ <https://kensaur.us/mushi-mushi/admin/>, or accounts you own.
58
+ - Do not access, exfiltrate, or modify data belonging to other users.
59
+ - Do not run automated scanning that affects availability for others
60
+ (rate-limit your tooling, exclude `/health`).
61
+ - Disclose privately first (channels above); do not publish before the
62
+ embargo above expires.
63
+ - Do not intentionally exploit a finding to escalate beyond proving it
64
+ exists.
65
+
66
+ If a finding requires touching production data to confirm, **stop and
67
+ ask first** — describe what you'd need to do and we'll spin up a sandbox.
68
+
69
+ ### Hall of fame
29
70
 
30
- We will acknowledge receipt within 48 hours and aim to release a patch within 7 days for critical issues.
71
+ Researchers who report a confirmed vulnerability are credited in the
72
+ release notes for the patched version and added to
73
+ [`docs/SECURITY_HALL_OF_FAME.md`](./docs/SECURITY_HALL_OF_FAME.md) (with
74
+ permission).
31
75
 
32
76
  ## Scope
33
77
 
@@ -48,6 +92,125 @@ We will acknowledge receipt within 48 hours and aim to release a patch within 7
48
92
  - **Rotate API keys** regularly via the admin console
49
93
  - **Enable SSO** for team projects (Enterprise tier)
50
94
  - **Review audit logs** periodically for suspicious activity
95
+ - **Verify SDK integrity** with `npm audit signatures` after install
96
+ - **Set `Content-Security-Policy`** on any page embedding the Mushi widget;
97
+ the widget itself ships with `script-src 'self'` and does not load
98
+ remote scripts.
99
+
100
+ ## Threat model
101
+
102
+ What we treat as in-scope attacker capabilities, and what we don't.
103
+
104
+ | Capability | In scope | Notes |
105
+ |-----------|----------|-------|
106
+ | Unauthenticated network attacker hitting public endpoints | ✅ | Rate-limit + HMAC + replay protection on every webhook endpoint (`packages/server/supabase/functions/_shared/webhook-middleware.ts`). |
107
+ | Authenticated user trying to read another tenant's data | ✅ | Postgres RLS on every `public.*` table; advisor lints reviewed monthly. |
108
+ | Authenticated user trying to escalate to super-admin | ✅ | Role lives in `auth.users.raw_app_meta_data.role`; cannot be self-edited via PostgREST. |
109
+ | Compromised dependency (npm supply-chain attack) | ✅ | 7-day cooldown + provenance + Harden-Runner + pinned SHAs (see "Supply-chain hardening" below). |
110
+ | Stolen API key | ✅ | Per-key scopes (`api_key_has_scope`), revocation via admin console, audit log of every use. |
111
+ | User pasting a Stripe / OpenAI / GitHub PAT into a bug report | ✅ | PII scrubber redacts ~15 vendor token formats client-side before the report leaves the device. Mirrors `packages/core/src/pii-scrubber.ts` across iOS, Android, Flutter, React Native. |
112
+ | Stolen end-user device with the SDK installed | ⚠️ partial | Offline queue is AsyncStorage / Keychain / SharedPreferences — no app-level encryption beyond the OS default. Reports waiting to flush are vulnerable to a forensic attacker. |
113
+ | Compromised Supabase service-role key | ❌ | Treated as a tier-0 incident; would require key rotation and audit-log forensics. Not defendable in software. |
114
+ | Compromise of `kensaurus@gmail.com` | ❌ | Treated as a project-fork event; downstream consumers should pin to the last known-good version and follow the new release channel. |
115
+ | Physical / OS-level attacker on an end-user device | ❌ | Out of scope. |
116
+ | Malicious fork using the Mushi name to ship malware | ❌ (technical) ✅ (legal) | The MIT/BSL grant lets the fork exist; the trademark policy (`TRADEMARK.md`) makes shipping it under the Mushi name an infringement we will pursue. |
117
+
118
+ ## Data handling and PII
119
+
120
+ ### What the SDK collects by default
121
+
122
+ | Field | Scope | PII risk |
123
+ |-------|-------|----------|
124
+ | URL / route the user was on | Always | Low — strip query strings if your routes encode user IDs. |
125
+ | Browser / OS / device | Always | None |
126
+ | Console errors (last 50) | Opt-in via `captureConsole: true` | Medium — can include user data your code logs. |
127
+ | Network failures (URL + status) | Opt-in via `captureNetwork: true` | Medium — query params logged as-is unless you redact in-app. |
128
+ | User id / email / role | Only if you call `setUser()` | High — only set what you need; we do not auto-discover. |
129
+ | Session replay frames | Off by default | High — handled by the masking layer; passwords / cards / opted-out elements never leave the page. |
130
+ | Free-text bug description | Always | Medium — passed through the PII scrubber (see below). |
131
+
132
+ ### What the PII scrubber redacts before send
133
+
134
+ Implemented identically across `@mushi-mushi/core`, the iOS, Android,
135
+ Flutter, and React Native SDKs. Defaults are below — every category can
136
+ be toggled off, but `secretTokens` is on by default and we recommend
137
+ keeping it that way.
138
+
139
+ | Category | Default | Patterns |
140
+ |----------|---------|----------|
141
+ | `ssns` | on | `123-45-6789` |
142
+ | `creditCards` | on | 12–19 digit Luhn-shaped sequences with optional separators |
143
+ | `secretTokens` | on | AWS access key (`AKIA…` / `ASIA…`), AWS secret (`aws_secret_access_key=…`), Stripe (`sk_live_…`, `sk_test_…`, `rk_…`, `pk_…`), Slack (`xox[abpor]-…`), GitHub PAT (`ghp_…`, `github_pat_…`), OpenAI (`sk-…`, `sk-proj-…`), Anthropic (`sk-ant-…`), Google API (`AIza…`), JWT (`eyJ…` 3-segment) |
144
+ | `emails` | on | RFC-5322 lite |
145
+ | `phones` | on | E.164 with optional country code |
146
+ | `ipAddresses` | off | IPv4 (off because internal IPs are usually not PII and noise hurts triage) |
147
+ | `ipv6` | off | Same |
148
+
149
+ The fields scrubbed are:
150
+
151
+ - `description` — primary free-text field of every bug report
152
+ - `summary` — short summary, in the same composer
153
+ - `breadcrumbs[].message` — auto-captured user-action trail (clicks, route changes, console messages)
154
+
155
+ Structured fields you set explicitly (`metadata.userEmail`,
156
+ `metadata.userId`, etc.) are intentionally **not** scrubbed — those are
157
+ opt-in attribution data, and silently rewriting them would break
158
+ support workflows.
159
+
160
+ ### Where data lives
161
+
162
+ - **Reports & telemetry** — Supabase Postgres in the `us-west-1` region.
163
+ - **Session replays** — Supabase Storage, same region. Lifecycle policy
164
+ trims replays older than 30 days unless explicitly retained from the
165
+ admin console.
166
+ - **Inbound webhook bodies** — only a SHA-256 hash + `delivery_id` of
167
+ the body is persisted (`webhook_audit_log`). The full body is
168
+ processed in memory and discarded.
169
+ - **Outbound integrations** (Slack, Jira, …) — Mushi is a sender only;
170
+ the receiving system's retention applies.
171
+
172
+ ### Encryption
173
+
174
+ | Surface | At rest | In transit |
175
+ |---------|---------|------------|
176
+ | Postgres (Supabase) | AES-256 (Supabase default) | TLS 1.2+ |
177
+ | Supabase Storage (replays) | AES-256 | TLS 1.2+ |
178
+ | Edge Function ↔ Postgres | — | TLS via the Supavisor pooler |
179
+ | SDK ↔ ingest endpoint | — | TLS 1.2+ enforced; HSTS preload on `kensaur.us` |
180
+ | Inbound webhooks | — | TLS terminated at CloudFront / Supabase edge |
181
+ | Audit log integrity | append-only by RLS; no in-row signing | — |
182
+
183
+ ### Cryptographic primitives
184
+
185
+ | Use | Algorithm | Implementation |
186
+ |-----|-----------|---------------|
187
+ | Webhook HMAC verification (Sentry, GitHub, Datadog, Honeycomb, Grafana, Bugsnag, Rollbar, Crashlytics) | HMAC-SHA256, constant-time compare | Web Crypto in Deno; `crypto.subtle.timingSafeEqual` analogue |
188
+ | AWS SNS subscription confirmation | RSA-SHA1 / RSA-SHA256 | Deno `crypto.subtle.verify` with the cert from `SigningCertURL` (URL allow-listed to `*.sns.*.amazonaws.com`) |
189
+ | Opsgenie JWT shared-token | HS256 with `aud` claim verification | `jose` (Deno-compatible) |
190
+ | API-key hashing (database) | SHA-256 prefix + bcrypt secret half | `pgcrypto` |
191
+ | Provenance attestations (npm) | Sigstore (Fulcio + Rekor) | `npm publish --provenance` |
192
+
193
+ We deliberately do not roll our own crypto. If you find an algorithm or
194
+ library above that has been deprecated, please file a security advisory.
195
+
196
+ ### Operator security checklist
197
+
198
+ When you provision a new self-hosted Mushi instance:
199
+
200
+ - [ ] Set `auth_leaked_password_protection = true` in Supabase Auth
201
+ (HaveIBeenPwned blocklist; flagged as `auth_leaked_password_protection`
202
+ in the security advisor).
203
+ - [ ] Enable at least two MFA factors in Supabase Auth (`auth_insufficient_mfa_options`).
204
+ - [ ] Rotate the service-role key on day 1, then quarterly.
205
+ - [ ] Restrict Postgres direct connections to your CI / migration runners
206
+ via Supabase network restrictions.
207
+ - [ ] Run `pnpm dlx supabase advisors --project-ref <ref>` after every
208
+ migration; aim for zero ERROR-level findings.
209
+ - [ ] Configure a Supabase log drain to your SIEM if you are subject to
210
+ SOC 2 / ISO 27001.
211
+ - [ ] Set CSP `frame-ancestors` on the host page if you embed the Mushi
212
+ widget (the widget is iframe-friendly but does not enforce
213
+ framing constraints itself).
51
214
 
52
215
  ## Supply-chain hardening (how this package is protected)
53
216
 
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var MUSHI_CLI_VERSION = true ? "0.7.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ MUSHI_CLI_VERSION
6
+ };
package/dist/index.js CHANGED
@@ -275,7 +275,6 @@ function collectDeps(pkg) {
275
275
  }
276
276
 
277
277
  // src/endpoint.ts
278
- var DEFAULT_ENDPOINT = "https://api.mushimushi.dev";
279
278
  var TEST_REPORT_TIMEOUT_MS = 1e4;
280
279
  var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
281
280
  function assertEndpoint(url) {
@@ -293,10 +292,14 @@ function assertEndpoint(url) {
293
292
  return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname);
294
293
  }
295
294
  function normalizeEndpoint(url) {
296
- const input = url ?? DEFAULT_ENDPOINT;
297
- let end = input.length;
298
- while (end > 0 && input.charCodeAt(end - 1) === 47) end--;
299
- return input.slice(0, end);
295
+ if (!url) {
296
+ throw new Error(
297
+ "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"
298
+ );
299
+ }
300
+ let end = url.length;
301
+ while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
302
+ return url.slice(0, end);
300
303
  }
301
304
 
302
305
  // src/freshness.ts
@@ -454,7 +457,7 @@ function getFrameworkFromPkg(pkg) {
454
457
  }
455
458
 
456
459
  // src/version.ts
457
- var MUSHI_CLI_VERSION = true ? "0.6.1" : "0.0.0-dev";
460
+ var MUSHI_CLI_VERSION = true ? "0.7.0" : "0.0.0-dev";
458
461
 
459
462
  // src/init.ts
460
463
  var ENV_FILES = [".env.local", ".env"];
@@ -490,7 +493,8 @@ async function runInit(options = {}) {
490
493
  }
491
494
  writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
492
495
  persistCliConfig(credentials.apiKey, credentials.projectId);
493
- printNextSteps(framework, credentials.apiKey, credentials.projectId);
496
+ const enableRewards = await maybeEnableRewards(options);
497
+ printNextSteps(framework, credentials.apiKey, credentials.projectId, enableRewards);
494
498
  await maybeSendTestReport(credentials, options);
495
499
  p.outro("Setup complete. Happy bug squashing \u{1F41B}");
496
500
  }
@@ -589,13 +593,13 @@ async function promptText(opts) {
589
593
  }
590
594
  async function installPackages(pm, packages, cwd) {
591
595
  const command = installCommand(pm, packages);
592
- const spinner2 = p.spinner();
593
- spinner2.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
596
+ const spinner3 = p.spinner();
597
+ spinner3.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
594
598
  try {
595
599
  await runCommand(pm, packages, cwd);
596
- spinner2.stop(`Installed ${packages.join(", ")}`);
600
+ spinner3.stop(`Installed ${packages.join(", ")}`);
597
601
  } catch (err) {
598
- spinner2.stop(`Install failed \u2014 run \`${command}\` manually.`);
602
+ spinner3.stop(`Install failed \u2014 run \`${command}\` manually.`);
599
603
  p.log.error(err instanceof Error ? err.name + ": " + err.message : String(err));
600
604
  }
601
605
  }
@@ -669,13 +673,34 @@ function persistCliConfig(apiKey, projectId) {
669
673
  const existing = loadConfig();
670
674
  saveConfig({ ...existing, apiKey, projectId });
671
675
  }
672
- function printNextSteps(framework, apiKey, projectId) {
676
+ function printNextSteps(framework, apiKey, projectId, enableRewards = false) {
673
677
  p.note(framework.snippet(apiKey, projectId), "Add this to your app:");
678
+ if (enableRewards) {
679
+ const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
680
+ import { MushiRewardsBadge } from '@mushi-mushi/react';
681
+
682
+ // Inside your component:
683
+ <MushiRewardsBadge showPoints />` : `// Add to your user menu:
684
+ // import { MushiRewardsBadge } from '@mushi-mushi/react';
685
+ // <MushiRewardsBadge showPoints />`;
686
+ p.note(badgeSnippet, "Rewards badge snippet:");
687
+ p.log.info("Enable rewards in your project settings at https://kensaur.us/mushi-mushi/rewards");
688
+ p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
689
+ }
674
690
  p.log.message("Verify the install:");
675
691
  p.log.message(" \u2022 Start your dev server");
676
692
  p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
677
693
  p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
678
694
  }
695
+ async function maybeEnableRewards(options) {
696
+ if (options.yes) return false;
697
+ const answer = await p.confirm({
698
+ message: "Enable Mushi Rewards? (users earn points for bug reports + app activity)",
699
+ initialValue: false
700
+ });
701
+ if (p.isCancel(answer)) return false;
702
+ return Boolean(answer);
703
+ }
679
704
  async function maybeSendTestReport(credentials, options) {
680
705
  if (options.sendTestReport === false) return;
681
706
  let shouldSend;
@@ -690,8 +715,15 @@ async function maybeSendTestReport(credentials, options) {
690
715
  shouldSend = answer;
691
716
  }
692
717
  if (!shouldSend) return;
693
- const spinner2 = p.spinner();
694
- spinner2.start("Sending test report\u2026");
718
+ if (!options.endpoint) {
719
+ p.note(
720
+ "No endpoint configured \u2014 skipping test report.\nSet endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api",
721
+ "Skipped"
722
+ );
723
+ return;
724
+ }
725
+ const spinner3 = p.spinner();
726
+ spinner3.start("Sending test report\u2026");
695
727
  const endpoint = normalizeEndpoint(options.endpoint);
696
728
  const controller = new AbortController();
697
729
  const timer = setTimeout(() => controller.abort(), TEST_REPORT_FETCH_TIMEOUT_MS);
@@ -723,17 +755,24 @@ async function maybeSendTestReport(credentials, options) {
723
755
  })
724
756
  });
725
757
  if (!res.ok) {
726
- spinner2.stop(`Test report rejected (HTTP ${res.status}).`);
758
+ spinner3.stop(`Test report rejected (HTTP ${res.status}).`);
727
759
  p.log.warn(
728
760
  res.status === 401 || res.status === 403 ? "Credentials did not authenticate \u2014 double-check the project ID and API key." : "Skipping test report. You can retry with `mushi test`."
729
761
  );
730
762
  return;
731
763
  }
732
- spinner2.stop("Test report sent.");
733
- p.log.success("View it at https://kensaur.us/mushi-mushi/reports");
764
+ spinner3.stop("Test report sent.");
765
+ let reportId;
766
+ try {
767
+ const body = await res.json();
768
+ reportId = body.data?.reportId;
769
+ } catch {
770
+ }
771
+ const reportPath = reportId ? `/reports/${reportId}` : "/reports";
772
+ p.log.success(`View it at https://kensaur.us/mushi-mushi/admin${reportPath}`);
734
773
  } catch (err) {
735
774
  const aborted = err instanceof Error && err.name === "AbortError";
736
- spinner2.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
775
+ spinner3.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
737
776
  p.log.warn(err instanceof Error ? err.message : String(err));
738
777
  } finally {
739
778
  clearTimeout(timer);
@@ -828,7 +867,7 @@ var MIGRATE_CATALOG = [
828
867
  category: "competitor",
829
868
  status: "published",
830
869
  detectionLabel: pkgs[0],
831
- match: (d) => pkgs.some((p2) => d.has(p2))
870
+ match: (d) => pkgs.some((p3) => d.has(p3))
832
871
  }))
833
872
  ];
834
873
  function titleForCompetitor(slug) {
@@ -860,10 +899,10 @@ function depsFromPackageJson(pkg) {
860
899
  }
861
900
  function runMigrate(opts = {}) {
862
901
  const cwd = opts.cwd ?? process.cwd();
863
- const log2 = opts.log ?? ((s) => console.log(s));
902
+ const log3 = opts.log ?? ((s) => console.log(s));
864
903
  const pkg = readPackageJson(cwd);
865
904
  if (!pkg) {
866
- log2(
905
+ log3(
867
906
  opts.json ? JSON.stringify({ ok: false, error: "no-package-json", cwd, matches: [] }, null, 2) : `No package.json found in ${cwd}. Run \`mushi migrate\` from your project root.`
868
907
  );
869
908
  return { matches: [] };
@@ -871,7 +910,7 @@ function runMigrate(opts = {}) {
871
910
  const deps = depsFromPackageJson(pkg);
872
911
  const matches = detectMigrations(deps);
873
912
  if (opts.json) {
874
- log2(
913
+ log3(
875
914
  JSON.stringify(
876
915
  {
877
916
  ok: true,
@@ -891,26 +930,173 @@ function runMigrate(opts = {}) {
891
930
  return { matches };
892
931
  }
893
932
  if (matches.length === 0) {
894
- log2("No migrations suggested for this project.");
895
- log2(`Browse the full catalog: ${DOCS_BASE}/migrations`);
933
+ log3("No migrations suggested for this project.");
934
+ log3(`Browse the full catalog: ${DOCS_BASE}/migrations`);
896
935
  return { matches };
897
936
  }
898
- log2(`Suggested migration${matches.length > 1 ? "s" : ""} for this project:`);
899
- log2("");
937
+ log3(`Suggested migration${matches.length > 1 ? "s" : ""} for this project:`);
938
+ log3("");
900
939
  for (const { guide, url } of matches) {
901
- log2(` \u2022 ${guide.title}`);
902
- if (guide.detectionLabel) log2(` detected: ${guide.detectionLabel}`);
903
- log2(` ${guide.summary}`);
904
- log2(` ${url}`);
905
- log2("");
940
+ log3(` \u2022 ${guide.title}`);
941
+ if (guide.detectionLabel) log3(` detected: ${guide.detectionLabel}`);
942
+ log3(` ${guide.summary}`);
943
+ log3(` ${url}`);
944
+ log3("");
906
945
  }
907
- log2(`Browse the full catalog: ${DOCS_BASE}/migrations`);
946
+ log3(`Browse the full catalog: ${DOCS_BASE}/migrations`);
908
947
  return { matches };
909
948
  }
910
949
 
950
+ // src/sourcemaps.ts
951
+ import { createReadStream } from "fs";
952
+ import { readFile, readdir } from "fs/promises";
953
+ import { createHash } from "crypto";
954
+ import { join as join5, relative, basename } from "path";
955
+ import * as p2 from "@clack/prompts";
956
+ async function findMapFiles(dir) {
957
+ const results = [];
958
+ async function walk(current) {
959
+ const entries = await readdir(current, { withFileTypes: true });
960
+ for (const e of entries) {
961
+ const full = join5(current, e.name);
962
+ if (e.isDirectory()) {
963
+ await walk(full);
964
+ } else if (e.isFile() && (e.name.endsWith(".js.map") || e.name.endsWith(".css.map"))) {
965
+ results.push(full);
966
+ }
967
+ }
968
+ }
969
+ await walk(dir);
970
+ return results;
971
+ }
972
+ function fileHash(path) {
973
+ return new Promise((resolve2, reject) => {
974
+ const hash = createHash("sha256");
975
+ const stream = createReadStream(path);
976
+ stream.on("data", (chunk) => hash.update(chunk));
977
+ stream.on("end", () => resolve2(hash.digest("hex")));
978
+ stream.on("error", reject);
979
+ });
980
+ }
981
+ async function uploadFile(filePath, release, endpoint, apiKey) {
982
+ const sha256 = await fileHash(filePath);
983
+ try {
984
+ const checkRes = await fetch(
985
+ `${endpoint}/v1/sourcemaps?sha256=${encodeURIComponent(sha256)}&release=${encodeURIComponent(release)}`,
986
+ { headers: { Authorization: `Bearer ${apiKey}`, "X-Mushi-Api-Key": apiKey } }
987
+ );
988
+ if (checkRes.ok) {
989
+ const json = await checkRes.json();
990
+ if (json.exists) return { ok: true, skipped: true };
991
+ }
992
+ } catch {
993
+ }
994
+ const contents = await readFile(filePath);
995
+ const form = new FormData();
996
+ form.append("file", new Blob([contents]), basename(filePath));
997
+ form.append("filename", relative(process.cwd(), filePath).replaceAll("\\", "/"));
998
+ form.append("release", release);
999
+ form.append("sha256", sha256);
1000
+ const res = await fetch(`${endpoint}/v1/sourcemaps`, {
1001
+ method: "POST",
1002
+ headers: { Authorization: `Bearer ${apiKey}`, "X-Mushi-Api-Key": apiKey },
1003
+ body: form
1004
+ });
1005
+ if (!res.ok) {
1006
+ const text2 = await res.text().catch(() => "");
1007
+ return {
1008
+ ok: false,
1009
+ skipped: false,
1010
+ reason: `HTTP ${res.status}: ${text2.slice(0, 120)}`
1011
+ };
1012
+ }
1013
+ return { ok: true, skipped: false };
1014
+ }
1015
+ async function runSourcemapsUpload(opts) {
1016
+ const endpoint = opts.endpoint ?? process.env["MUSHI_API_ENDPOINT"] ?? "";
1017
+ const apiKey = opts.apiKey ?? process.env["MUSHI_API_KEY"] ?? "";
1018
+ if (!opts.dryRun && !endpoint) {
1019
+ p2.log.error(
1020
+ "No API endpoint configured. Pass --endpoint <url>, set MUSHI_API_ENDPOINT,\n or run `mushi config endpoint <url>` to persist it. For Supabase self-hosting,\n this is your edge-functions URL, e.g. https://xyz.supabase.co/functions/v1/api"
1021
+ );
1022
+ process.exit(1);
1023
+ }
1024
+ if (!opts.dryRun && !apiKey) {
1025
+ p2.log.error("No API key \u2014 set MUSHI_API_KEY or pass --api-key <key>");
1026
+ process.exit(1);
1027
+ }
1028
+ if (!opts.silent) p2.intro(`sourcemaps upload \xB7 release ${opts.release}`);
1029
+ const spin = p2.spinner();
1030
+ spin.start(`Scanning ${opts.dir} for .map files\u2026`);
1031
+ let files;
1032
+ try {
1033
+ files = await findMapFiles(opts.dir);
1034
+ } catch (err) {
1035
+ spin.stop("Scan failed");
1036
+ p2.log.error(
1037
+ `Cannot read ${opts.dir}: ${err instanceof Error ? err.message : String(err)}`
1038
+ );
1039
+ process.exit(1);
1040
+ }
1041
+ spin.stop(
1042
+ `Found ${files.length} map file${files.length === 1 ? "" : "s"}`
1043
+ );
1044
+ if (files.length === 0) {
1045
+ p2.log.warn("No .js.map or .css.map files found \u2014 nothing to upload.");
1046
+ return;
1047
+ }
1048
+ if (opts.dryRun) {
1049
+ p2.log.info("Dry run \u2014 files that would be uploaded:");
1050
+ for (const f of files) {
1051
+ p2.log.message(` ${relative(process.cwd(), f).replaceAll("\\", "/")}`);
1052
+ }
1053
+ p2.outro(`${files.length} file${files.length === 1 ? "" : "s"} would be uploaded`);
1054
+ return;
1055
+ }
1056
+ let uploaded = 0;
1057
+ let skipped = 0;
1058
+ let failed = 0;
1059
+ for (const filePath of files) {
1060
+ const rel = relative(process.cwd(), filePath).replaceAll("\\", "/");
1061
+ const fs = p2.spinner();
1062
+ fs.start(rel);
1063
+ const result = await uploadFile(filePath, opts.release, endpoint, apiKey);
1064
+ if (result.skipped) {
1065
+ fs.stop(`\u21A9 ${rel} (already uploaded)`);
1066
+ skipped++;
1067
+ } else if (result.ok) {
1068
+ fs.stop(`\u2713 ${rel}`);
1069
+ uploaded++;
1070
+ } else {
1071
+ fs.stop(`\u2717 ${rel} \u2014 ${result.reason ?? "unknown error"}`);
1072
+ failed++;
1073
+ }
1074
+ }
1075
+ const total = files.length;
1076
+ const parts = [
1077
+ `Uploaded ${uploaded} / ${total} file${total === 1 ? "" : "s"}`,
1078
+ skipped > 0 ? `(${skipped} already existed)` : "",
1079
+ failed > 0 ? `\u2014 ${failed} failed` : ""
1080
+ ].filter(Boolean);
1081
+ const summary = parts.join(" ");
1082
+ if (!opts.silent) {
1083
+ if (failed > 0) {
1084
+ p2.log.error(summary);
1085
+ } else {
1086
+ p2.outro(summary);
1087
+ }
1088
+ }
1089
+ if (failed > 0) process.exit(1);
1090
+ }
1091
+
911
1092
  // src/index.ts
912
1093
  async function apiCall(path, config, options = {}) {
913
- const endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
1094
+ const endpoint = config.endpoint;
1095
+ if (!endpoint) {
1096
+ throw new Error(
1097
+ "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"
1098
+ );
1099
+ }
914
1100
  const res = await fetch(`${endpoint}${path}`, {
915
1101
  ...options,
916
1102
  headers: {
@@ -1018,7 +1204,13 @@ deploy.command("check").description("Check edge function health").action(async (
1018
1204
  console.error("Run `mushi login` first");
1019
1205
  process.exit(1);
1020
1206
  }
1021
- const endpoint = config.endpoint ?? "https://api.mushimushi.dev";
1207
+ if (!config.endpoint) {
1208
+ console.error(
1209
+ "No API endpoint configured. Run `mushi init` or set MUSHI_API_ENDPOINT.\nSet endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api"
1210
+ );
1211
+ process.exit(1);
1212
+ }
1213
+ const endpoint = config.endpoint;
1022
1214
  try {
1023
1215
  const res = await fetch(`${endpoint}/health`);
1024
1216
  console.log(`Health: ${res.status === 200 ? "OK" : "FAIL"} (${res.status})`);
@@ -1036,12 +1228,12 @@ program.command("index <path>").description("Walk a local repo and upload code c
1036
1228
  console.error("Set projectId via `mushi config projectId <id>`");
1037
1229
  process.exit(1);
1038
1230
  }
1039
- const { readdir, readFile, stat } = await import("fs/promises");
1231
+ const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
1040
1232
  const nodePath = await import("path");
1041
1233
  const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
1042
1234
  const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
1043
1235
  async function* walk(dir) {
1044
- const entries = await readdir(dir, { withFileTypes: true });
1236
+ const entries = await readdir2(dir, { withFileTypes: true });
1045
1237
  for (const e of entries) {
1046
1238
  const full = nodePath.join(dir, e.name);
1047
1239
  if (SKIP.test(full)) continue;
@@ -1057,24 +1249,24 @@ program.command("index <path>").description("Walk a local repo and upload code c
1057
1249
  if (opts.language && opts.language !== lang) continue;
1058
1250
  const stats = await stat(file);
1059
1251
  if (stats.size > 5e5) continue;
1060
- const source = await readFile(file, "utf8");
1061
- const relative = nodePath.relative(root, file).replaceAll("\\", "/");
1252
+ const source = await readFile2(file, "utf8");
1253
+ const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
1062
1254
  count++;
1063
1255
  bytes += source.length;
1064
1256
  if (opts.dryRun) {
1065
- console.log(` ${relative} (${source.length} bytes)`);
1257
+ console.log(` ${relative2} (${source.length} bytes)`);
1066
1258
  continue;
1067
1259
  }
1068
1260
  const res = await apiCall("/v1/admin/codebase/upload", config, {
1069
1261
  method: "POST",
1070
1262
  body: JSON.stringify({
1071
1263
  projectId: config.projectId,
1072
- filePath: relative,
1264
+ filePath: relative2,
1073
1265
  source
1074
1266
  })
1075
1267
  });
1076
- if (!res.ok) console.error(` FAIL ${relative}: ${res.error ?? "unknown"}`);
1077
- else process.stdout.write(` ${relative} \u2192 ${res.chunks ?? 0} chunks
1268
+ if (!res.ok) console.error(` FAIL ${relative2}: ${res.error ?? "unknown"}`);
1269
+ else process.stdout.write(` ${relative2} \u2192 ${res.chunks ?? 0} chunks
1078
1270
  `);
1079
1271
  }
1080
1272
  console.log(`Indexed ${count} files (${(bytes / 1024).toFixed(1)} KB) into project ${config.projectId}`);
@@ -1107,4 +1299,64 @@ program.command("test").description("Submit a test report to verify pipeline").a
1107
1299
  });
1108
1300
  console.log("Test report submitted:", JSON.stringify(data, null, 2));
1109
1301
  });
1302
+ var sourcemaps = program.command("sourcemaps").description("Source map management");
1303
+ sourcemaps.command("upload").description("Upload source map files to the Mushi platform (idempotent, sha256-keyed)").requiredOption("--release <version>", "Release version (e.g. 1.0.0 or git SHA)").option("--dir <path>", "Directory containing source maps", "./dist").option("--dry-run", "List files that would be uploaded without uploading").option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--silent", "Suppress progress output (exit code still reflects failure)").action(async (opts) => {
1304
+ await runSourcemapsUpload({
1305
+ release: opts.release,
1306
+ dir: opts.dir,
1307
+ dryRun: opts.dryRun,
1308
+ endpoint: opts.endpoint,
1309
+ apiKey: opts.apiKey,
1310
+ silent: opts.silent
1311
+ });
1312
+ });
1313
+ program.command("sync-lessons").description("Sync promoted lessons from Mushi into .mushi/lessons.json in this repo").option("--cwd <path>", "Target directory (default: current working dir)").option("--dry-run", "Print what would be written without writing").option("--json", "Machine-readable JSON output").action(async (opts) => {
1314
+ const { writeFile, mkdir } = await import("fs/promises");
1315
+ const nodePath = await import("path");
1316
+ const config = loadConfig();
1317
+ if (!config.apiKey) {
1318
+ console.error("Run `mushi login` first");
1319
+ process.exit(1);
1320
+ }
1321
+ if (!config.projectId) {
1322
+ console.error("Set projectId via `mushi config projectId <id>`");
1323
+ process.exit(1);
1324
+ }
1325
+ const cwd = opts.cwd ?? process.cwd();
1326
+ const target = nodePath.join(cwd, ".mushi", "lessons.json");
1327
+ const res = await apiCall(
1328
+ `/v1/admin/lessons?projectId=${config.projectId}&limit=500`,
1329
+ config
1330
+ );
1331
+ if (!res.ok || !res.data) {
1332
+ console.error("Failed to fetch lessons:", res.error ?? JSON.stringify(res));
1333
+ process.exit(1);
1334
+ }
1335
+ const lessons = res.data.map((l) => ({
1336
+ id: l.id,
1337
+ rule: l.rule_text,
1338
+ anti_pattern: l.anti_pattern ?? void 0,
1339
+ severity: l.severity,
1340
+ frequency: l.frequency,
1341
+ last_reinforced: l.last_reinforced_at?.slice(0, 10) ?? "",
1342
+ cluster_id: l.cluster_id ?? void 0
1343
+ }));
1344
+ const output = {
1345
+ schema_version: "1",
1346
+ project_id: config.projectId,
1347
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1348
+ lessons
1349
+ };
1350
+ if (opts.dryRun) {
1351
+ console.log(JSON.stringify(output, null, 2));
1352
+ return;
1353
+ }
1354
+ await mkdir(nodePath.dirname(target), { recursive: true });
1355
+ await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
1356
+ if (opts.json) {
1357
+ console.log(JSON.stringify({ ok: true, path: target, count: lessons.length }));
1358
+ } else {
1359
+ console.log(`\u2713 Wrote ${lessons.length} lessons to ${target}`);
1360
+ }
1361
+ });
1110
1362
  program.parse();
package/dist/init.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-XHD3H54W.js";
9
9
  import {
10
10
  MUSHI_CLI_VERSION
11
- } from "./chunk-ZL3BTW32.js";
11
+ } from "./chunk-LBQX6RYS.js";
12
12
 
13
13
  // src/init.ts
14
14
  import * as p from "@clack/prompts";
@@ -46,14 +46,17 @@ function tightenPermissions(path) {
46
46
  }
47
47
 
48
48
  // src/endpoint.ts
49
- var DEFAULT_ENDPOINT = "https://api.mushimushi.dev";
50
49
  var TEST_REPORT_TIMEOUT_MS = 1e4;
51
50
  var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
52
51
  function normalizeEndpoint(url) {
53
- const input = url ?? DEFAULT_ENDPOINT;
54
- let end = input.length;
55
- while (end > 0 && input.charCodeAt(end - 1) === 47) end--;
56
- return input.slice(0, end);
52
+ if (!url) {
53
+ throw new Error(
54
+ "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"
55
+ );
56
+ }
57
+ let end = url.length;
58
+ while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
59
+ return url.slice(0, end);
57
60
  }
58
61
 
59
62
  // src/freshness.ts
@@ -244,7 +247,8 @@ async function runInit(options = {}) {
244
247
  }
245
248
  writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
246
249
  persistCliConfig(credentials.apiKey, credentials.projectId);
247
- printNextSteps(framework, credentials.apiKey, credentials.projectId);
250
+ const enableRewards = await maybeEnableRewards(options);
251
+ printNextSteps(framework, credentials.apiKey, credentials.projectId, enableRewards);
248
252
  await maybeSendTestReport(credentials, options);
249
253
  p.outro("Setup complete. Happy bug squashing \u{1F41B}");
250
254
  }
@@ -423,13 +427,34 @@ function persistCliConfig(apiKey, projectId) {
423
427
  const existing = loadConfig();
424
428
  saveConfig({ ...existing, apiKey, projectId });
425
429
  }
426
- function printNextSteps(framework, apiKey, projectId) {
430
+ function printNextSteps(framework, apiKey, projectId, enableRewards = false) {
427
431
  p.note(framework.snippet(apiKey, projectId), "Add this to your app:");
432
+ if (enableRewards) {
433
+ const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
434
+ import { MushiRewardsBadge } from '@mushi-mushi/react';
435
+
436
+ // Inside your component:
437
+ <MushiRewardsBadge showPoints />` : `// Add to your user menu:
438
+ // import { MushiRewardsBadge } from '@mushi-mushi/react';
439
+ // <MushiRewardsBadge showPoints />`;
440
+ p.note(badgeSnippet, "Rewards badge snippet:");
441
+ p.log.info("Enable rewards in your project settings at https://kensaur.us/mushi-mushi/rewards");
442
+ p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
443
+ }
428
444
  p.log.message("Verify the install:");
429
445
  p.log.message(" \u2022 Start your dev server");
430
446
  p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
431
447
  p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
432
448
  }
449
+ async function maybeEnableRewards(options) {
450
+ if (options.yes) return false;
451
+ const answer = await p.confirm({
452
+ message: "Enable Mushi Rewards? (users earn points for bug reports + app activity)",
453
+ initialValue: false
454
+ });
455
+ if (p.isCancel(answer)) return false;
456
+ return Boolean(answer);
457
+ }
433
458
  async function maybeSendTestReport(credentials, options) {
434
459
  if (options.sendTestReport === false) return;
435
460
  let shouldSend;
@@ -444,6 +469,13 @@ async function maybeSendTestReport(credentials, options) {
444
469
  shouldSend = answer;
445
470
  }
446
471
  if (!shouldSend) return;
472
+ if (!options.endpoint) {
473
+ p.note(
474
+ "No endpoint configured \u2014 skipping test report.\nSet endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api",
475
+ "Skipped"
476
+ );
477
+ return;
478
+ }
447
479
  const spinner2 = p.spinner();
448
480
  spinner2.start("Sending test report\u2026");
449
481
  const endpoint = normalizeEndpoint(options.endpoint);
@@ -484,7 +516,14 @@ async function maybeSendTestReport(credentials, options) {
484
516
  return;
485
517
  }
486
518
  spinner2.stop("Test report sent.");
487
- p.log.success("View it at https://kensaur.us/mushi-mushi/reports");
519
+ let reportId;
520
+ try {
521
+ const body = await res.json();
522
+ reportId = body.data?.reportId;
523
+ } catch {
524
+ }
525
+ const reportPath = reportId ? `/reports/${reportId}` : "/reports";
526
+ p.log.success(`View it at https://kensaur.us/mushi-mushi/admin${reportPath}`);
488
527
  } catch (err) {
489
528
  const aborted = err instanceof Error && err.name === "AbortError";
490
529
  spinner2.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-ZL3BTW32.js";
3
+ } from "./chunk-LBQX6RYS.js";
4
4
  export {
5
5
  MUSHI_CLI_VERSION
6
6
  };
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@mushi-mushi/cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.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
9
  "dependencies": {
10
- "@clack/prompts": "^1.2.0",
10
+ "@clack/prompts": "^1.3.0",
11
11
  "commander": "^14.0.3"
12
12
  },
13
13
  "devDependencies": {
14
- "@types/node": "^22.15.3",
15
- "eslint": "^10.2.0",
14
+ "@types/node": "^22.19.17",
15
+ "eslint": "^10.3.0",
16
16
  "tsup": "^8.5.1",
17
- "typescript": "^6.0.2",
18
- "vitest": "^4.1.4",
17
+ "typescript": "^6.0.3",
18
+ "vitest": "^4.1.5",
19
19
  "@mushi-mushi/eslint-config": "0.0.0"
20
20
  },
21
21
  "author": "Kenji Sakuramoto",
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var MUSHI_CLI_VERSION = true ? "0.6.1" : "0.0.0-dev";
3
-
4
- export {
5
- MUSHI_CLI_VERSION
6
- };