@mushi-mushi/cli 0.6.0 → 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 +27 -0
- package/README.md +17 -0
- package/SECURITY.md +167 -4
- package/dist/chunk-LBQX6RYS.js +6 -0
- package/dist/index.js +294 -42
- package/dist/init.js +48 -9
- package/dist/version.js +1 -1
- package/package.json +6 -6
- package/dist/chunk-KNU5OWYY.js +0 -6
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
593
|
-
|
|
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
|
-
|
|
600
|
+
spinner3.stop(`Installed ${packages.join(", ")}`);
|
|
597
601
|
} catch (err) {
|
|
598
|
-
|
|
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
|
-
|
|
694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
|
902
|
+
const log3 = opts.log ?? ((s) => console.log(s));
|
|
864
903
|
const pkg = readPackageJson(cwd);
|
|
865
904
|
if (!pkg) {
|
|
866
|
-
|
|
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
|
-
|
|
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
|
-
|
|
895
|
-
|
|
933
|
+
log3("No migrations suggested for this project.");
|
|
934
|
+
log3(`Browse the full catalog: ${DOCS_BASE}/migrations`);
|
|
896
935
|
return { matches };
|
|
897
936
|
}
|
|
898
|
-
|
|
899
|
-
|
|
937
|
+
log3(`Suggested migration${matches.length > 1 ? "s" : ""} for this project:`);
|
|
938
|
+
log3("");
|
|
900
939
|
for (const { guide, url } of matches) {
|
|
901
|
-
|
|
902
|
-
if (guide.detectionLabel)
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1061
|
-
const
|
|
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(` ${
|
|
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:
|
|
1264
|
+
filePath: relative2,
|
|
1073
1265
|
source
|
|
1074
1266
|
})
|
|
1075
1267
|
});
|
|
1076
|
-
if (!res.ok) console.error(` FAIL ${
|
|
1077
|
-
else process.stdout.write(` ${
|
|
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-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
10
|
+
"@clack/prompts": "^1.3.0",
|
|
11
11
|
"commander": "^14.0.3"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@types/node": "^22.
|
|
15
|
-
"eslint": "^10.
|
|
14
|
+
"@types/node": "^22.19.17",
|
|
15
|
+
"eslint": "^10.3.0",
|
|
16
16
|
"tsup": "^8.5.1",
|
|
17
|
-
"typescript": "^6.0.
|
|
18
|
-
"vitest": "^4.1.
|
|
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",
|