@mhosaic/feedback-cli 0.14.1 → 0.16.1

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.
Files changed (26) hide show
  1. package/dist/bin.js +3 -3
  2. package/dist/{doctor-HTTAR4ZB.js → doctor-K2C6LBR2.js} +2 -2
  3. package/dist/doctor-K2C6LBR2.js.map +1 -0
  4. package/dist/{init-P45CEXDP.js → init-RS7BKALE.js} +3 -3
  5. package/dist/init-RS7BKALE.js.map +1 -0
  6. package/dist/{install-skill-3OPVFMTK.js → install-skill-QJ4ZDVVR.js} +4 -2
  7. package/dist/{install-skill-3OPVFMTK.js.map → install-skill-QJ4ZDVVR.js.map} +1 -1
  8. package/package.json +1 -1
  9. package/skills/feedback-close/SKILL.md +58 -0
  10. package/skills/feedback-fix/SKILL.md +107 -0
  11. package/skills/feedback-pull/SKILL.md +56 -0
  12. package/skills/feedback-watch-merges/SKILL.md +62 -0
  13. package/skills/integrate-feedback/SKILL.md +99 -75
  14. package/skills/integrate-feedback/references/consumer-install-astro.md +1 -1
  15. package/skills/integrate-feedback/references/consumer-install-next.md +2 -2
  16. package/skills/integrate-feedback/references/consumer-install-nuxt.md +1 -1
  17. package/skills/integrate-feedback/references/consumer-install-plain.md +55 -30
  18. package/skills/integrate-feedback/references/consumer-install-remix.md +1 -1
  19. package/skills/integrate-feedback/references/consumer-install-svelte.md +1 -1
  20. package/skills/integrate-feedback/references/consumer-install-vite.md +2 -2
  21. package/skills/integrate-feedback/references/consumer-install-vue.md +1 -1
  22. package/skills/integrate-feedback/references/consumer-install.md +62 -19
  23. package/skills/integrate-feedback/references/identify-snippets.md +3 -3
  24. package/skills/integrate-feedback/references/operator-provision.md +75 -33
  25. package/dist/doctor-HTTAR4ZB.js.map +0 -1
  26. package/dist/init-P45CEXDP.js.map +0 -1
@@ -4,6 +4,36 @@ You are guiding a teammate through installing the @mhosaic/feedback widget into
4
4
 
5
5
  ---
6
6
 
7
+ ## CRITICAL: API key handling — read this before doing anything
8
+
9
+ The `pk_proj_…` public widget key is **NOT safe to commit to git**. Even though it's a "public" key (it ends up in the browser-side bundle at runtime), committing it is wrong for several concrete reasons:
10
+
11
+ - Git history is permanent. If the key needs rotation (compromise, audit finding, project transfer), you can't actually remove it — every clone of the repo still has it.
12
+ - GitHub's secret scanners flag any `pk_proj_…` literal in a public repo and notify the org. Even private repos can leak via accidental fork or contributor offboarding.
13
+ - The key is tied to a specific project's `allowed_origins` allow-list — if it leaks and gets used from a domain the operator forgot to lock down, you get garbage reports from the wild.
14
+
15
+ **Rules. NO exceptions.**
16
+
17
+ 1. **NEVER commit the API key to any tracked file.** Not `.env`, not `.env.example`, not `config.ts`, not a comment, not a README. Not even "temporarily, I'll rotate later."
18
+ 2. **If the user asks "can I commit this?" — the answer is NO.** Don't rationalize "it's public anyway" or "it's bundled into the JS at build time so what's the harm." Both are true; neither matters. Refer them to this section.
19
+ 3. **If the host's deploy pipeline excludes `.env.local`** (common with Docker builds — Dockerfile `COPY` of `frontend/` typically skips `.env*.local` via `.dockerignore`, or the build context only includes specific files), the key MUST come from **deploy-platform env vars** (DO App Platform > component > Environment Variables; Vercel > Project Settings > Env Vars; etc.), NOT from a tracked file. Pause and tell the user. Do NOT switch to `.env` (which IS tracked) as a workaround.
20
+ 4. **`.env.local` (or framework equivalent) must be in `.gitignore`.** The CLI's `init` command verifies this. If you're editing files manually because the CLI failed, you verify it yourself before writing the key anywhere.
21
+ 5. **The endpoint URL (`https://software-factory-…`) IS safe to commit** — it's a public URL with no auth on its surface. Only the API key matters.
22
+
23
+ **Per-environment-variable destination table** (which file gets the key for which scenario):
24
+
25
+ | Scenario | API key goes in | Tracked by git? |
26
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------- |
27
+ | Local dev, Vite/Next/Nuxt/etc. without Docker | `.env.local` | No (gitignored) |
28
+ | Local dev with Docker Compose (host-side build) | `.env.local` | No |
29
+ | Production deploy (DO App Platform, Vercel, Netlify, etc.) | Platform env vars UI | No (lives only on the platform) |
30
+ | Docker build where `.env.local` is excluded from context | Platform env vars (passed in at build time via `--build-arg` or runtime injection) | No |
31
+ | CI build for staging/prod | CI secret (GH Actions secrets, etc.), surfaced as env var to the build | No |
32
+
33
+ If a scenario isn't in the table and you're unsure, **stop and ask the operator**. Do not improvise toward a tracked file.
34
+
35
+ ---
36
+
7
37
  ## Step 1 — Parse the handoff payload
8
38
 
9
39
  Ask the user to paste the handoff payload as plain Markdown. Extract:
@@ -19,7 +49,17 @@ If the user can't find the payload, walk them through asking the operator for it
19
49
 
20
50
  ## Step 2 — Classify the host app
21
51
 
22
- `Read` the user's `package.json`. Three branches:
52
+ Before anything else, **confirm the cwd is actually the target.** This is the most common operator footgun: someone runs `/integrate-feedback` inside `feedback-tool-mhosaic` (where the skill is conveniently invokable) and the consumer phase silently writes the widget into our own monorepo. Two cheap checks:
53
+
54
+ - Read `package.json`. If its `name` is `@mhosaic/feedback-tool-mhosaic`, `@mhosaic/feedback`, `@mhosaic/feedback-admin`, `@mhosaic/feedback-cli`, or contains the literal string `mhosaic/feedback`: **STOP**. The user is inside our own monorepo, not their host app. Tell them:
55
+
56
+ > "Looks like we're inside `feedback-tool-mhosaic` itself. The consumer install writes to the current directory; running it here would install the widget into our own monorepo. Either pass `--cwd <target-app-path>` to the CLI, or close Claude and re-open it inside the target app's root, then re-run `/integrate-feedback` and pick 'Install (consumer)'."
57
+
58
+ Wait for the user to confirm they've moved before continuing.
59
+
60
+ - If `package.json` is missing AND the cwd contains `packages/cli/skills/integrate-feedback/` (we're inside the skill's own source), same stop-and-redirect.
61
+
62
+ Now `Read` the user's `package.json`. Three branches:
23
63
 
24
64
  1. **package.json exists with `dependencies`** → continue to Step 3 (framework detection).
25
65
  2. **No package.json, but `index.html` / `templates/` / `app/views/` / similar template directory exists** → this is a non-JS host app (Django, Rails, Hugo, plain HTML/static site, etc.). Skip Steps 3-5 and go directly to **`references/consumer-install-plain.md`** (CDN `<script>` flow). Tell the user: "I see this isn't a JS/TS app — the CDN path is the right one. The npm-package + framework-snippet flow doesn't apply."
@@ -29,22 +69,22 @@ If the user can't find the payload, walk them through asking the operator for it
29
69
 
30
70
  ## Step 3 — Detect the framework
31
71
 
32
- The CLI does framework detection itself, but you should know the result *before* running it so you can decide which post-install snippet (if any) the user needs to apply.
72
+ The CLI does framework detection itself, but you should know the result _before_ running it so you can decide which post-install snippet (if any) the user needs to apply.
33
73
 
34
74
  Read `package.json`'s `dependencies` + `devDependencies` and classify:
35
75
 
36
- | Found | Framework | Reference doc | CLI auto-wires entry? |
37
- |---|---|---|---|
38
- | `vite` + `react` + `@vitejs/plugin-react` | `vite-react` | `consumer-install-vite.md` | ✅ yes |
39
- | `next` | `nextjs` (App or Pages Router — sniff `app/` vs `pages/`) | `consumer-install-next.md` | ❌ paste snippet |
40
- | `nuxt` | `nuxt` | `consumer-install-nuxt.md` | ❌ paste snippet |
41
- | `astro` | `astro` | `consumer-install-astro.md` | ❌ paste snippet |
42
- | `@remix-run/react` | `remix` | `consumer-install-remix.md` | ❌ paste snippet |
43
- | `vue` ≥ 3 (no `nuxt`) | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
44
- | `@sveltejs/kit` | `sveltekit` | `consumer-install-svelte.md` | ❌ paste snippet |
45
- | none of the above | `plain` / `unknown` | `consumer-install-plain.md` | ❌ CDN snippet |
76
+ | Found | Framework | Reference doc | CLI auto-wires entry? |
77
+ | ----------------------------------------- | --------------------------------------------------------- | ---------------------------- | --------------------- |
78
+ | `vite` + `react` + `@vitejs/plugin-react` | `vite-react` | `consumer-install-vite.md` | ✅ yes |
79
+ | `next` | `nextjs` (App or Pages Router — sniff `app/` vs `pages/`) | `consumer-install-next.md` | ❌ paste snippet |
80
+ | `nuxt` | `nuxt` | `consumer-install-nuxt.md` | ❌ paste snippet |
81
+ | `astro` | `astro` | `consumer-install-astro.md` | ❌ paste snippet |
82
+ | `@remix-run/react` | `remix` | `consumer-install-remix.md` | ❌ paste snippet |
83
+ | `vue` ≥ 3 (no `nuxt`) | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
84
+ | `@sveltejs/kit` | `sveltekit` | `consumer-install-svelte.md` | ❌ paste snippet |
85
+ | none of the above | `plain` / `unknown` | `consumer-install-plain.md` | ❌ CDN snippet |
46
86
 
47
- Order matters: check for `nuxt` *before* generic `vue`, since Nuxt apps have both. Same for `astro` vs the underlying renderer (Astro apps can also have React/Vue/Svelte).
87
+ Order matters: check for `nuxt` _before_ generic `vue`, since Nuxt apps have both. Same for `astro` vs the underlying renderer (Astro apps can also have React/Vue/Svelte).
48
88
 
49
89
  Tell the user which framework you detected. Confirm with `AskUserQuestion` (especially if Next.js, since App vs Pages Router branches differently).
50
90
 
@@ -62,12 +102,14 @@ npx @mhosaic/feedback-cli@latest init \
62
102
  ```
63
103
 
64
104
  This will:
105
+
65
106
  - `npm install @mhosaic/feedback` (using the project's package manager)
66
107
  - Write `.env.local` with `VITE_FEEDBACK_API_KEY=…` + `VITE_FEEDBACK_ENDPOINT=…` (Vite-flavored names; rename per framework if Next/Vue/etc. — see framework references)
67
108
  - Ensure `.env.local` is in `.gitignore`
68
109
  - If `vite-react`: auto-wrap `src/main.tsx` with `<FeedbackProvider>`
69
110
 
70
111
  Surface the CLI's output verbatim. If install fails, stop and read the error message; common causes:
112
+
71
113
  - No internet
72
114
  - Lockfile conflict (offer to delete `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json` and retry)
73
115
  - Wrong directory (no `package.json`)
@@ -86,6 +128,7 @@ Open the per-framework reference document and follow it verbatim:
86
128
  - Plain HTML / CDN: `consumer-install-plain.md`
87
129
 
88
130
  Each reference doc gives:
131
+
89
132
  1. The env-var prefix (e.g. `NEXT_PUBLIC_FEEDBACK_API_KEY` for Next.js — the CLI's default `VITE_FEEDBACK_*` needs renaming)
90
133
  2. The entry-point snippet to paste (with import + provider/init call)
91
134
  3. Where to put it (exact file path)
@@ -106,6 +149,7 @@ If you can't detect the auth provider from `package.json`, ask the user explicit
106
149
  ## Step 7 — (Optional but recommended) Wire the auto-capture modules
107
150
 
108
151
  `AskUserQuestion`:
152
+
109
153
  - question: `Wire the auto-capture modules?`
110
154
  - options:
111
155
  - `Yes — all three (error-tracking, web-vitals, replay)`, description: `Captures runtime errors, Core Web Vitals, and session replay. ~30 KiB gzip overhead.`
@@ -115,16 +159,12 @@ If you can't detect the auth provider from `package.json`, ask the user explicit
115
159
  For each `Yes`, modify the consumer's widget init to chain the wrappers. The pattern:
116
160
 
117
161
  ```typescript
118
- import { createFeedback } from '@mhosaic/feedback'
162
+ import { createFeedback } from '@mhosaic/feedback/loader'
119
163
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
120
164
  import { withReplay } from '@mhosaic/feedback/replay'
121
165
  import { withWebVitals } from '@mhosaic/feedback/webvitals'
122
166
 
123
- const fb = withErrorTracking(
124
- withReplay(
125
- withWebVitals(createFeedback({ apiKey, endpoint }))
126
- )
127
- )
167
+ const fb = withErrorTracking(withReplay(withWebVitals(createFeedback({ apiKey, endpoint }))))
128
168
  ```
129
169
 
130
170
  The CLI's auto-wrap uses `<FeedbackProvider>` (a React-specific helper) — for non-Vite-React frameworks the snippet above is the direct equivalent. Match the framework's import style (CommonJS vs ESM, top-level await vs inside an effect).
@@ -140,12 +180,14 @@ npx @mhosaic/feedback-cli@latest verify \
140
180
  ```
141
181
 
142
182
  This is a lightweight check; it confirms:
183
+
143
184
  - `.env.local` has the right shape
144
185
  - The endpoint resolves and responds
145
186
  - CORS allows the consumer's origin
146
187
  - The public key is accepted (POSTs a `[mhosaic-feedback verify]` synthetic report)
147
188
 
148
189
  If anything is red, the verify command prints a hint. Common hints:
190
+
149
191
  - `add this origin to the project's allowed_origins (admin SPA: Edit project)` → operator needs to add the dev/prod origin.
150
192
  - `key invalid or revoked` → operator needs to mint a new key.
151
193
 
@@ -178,6 +220,7 @@ Summarize for the user:
178
220
  - Next: deploy to staging, add the staging URL to `allowed_origins` (operator action — DM them the URL)
179
221
 
180
222
  Encourage them to:
223
+
181
224
  - Commit the change (sans `.env.local`)
182
225
  - Tell the operator the smoke test passed
183
226
  - Delete the smoke-test reports from `/reports` in admin once it lands
@@ -14,7 +14,7 @@ The widget's FAB is hidden until `fb.identify()` is called with a non-empty `id`
14
14
 
15
15
  import { useEffect } from 'react'
16
16
  import { useAuth0 } from '@auth0/auth0-react'
17
- import { useFeedback } from '@mhosaic/feedback/react'
17
+ import { useFeedback } from '@mhosaic/feedback/loader/react'
18
18
 
19
19
  export function FeedbackIdentity() {
20
20
  const { user, isAuthenticated, isLoading } = useAuth0()
@@ -40,7 +40,7 @@ Mount `<FeedbackIdentity />` once inside both `<Auth0Provider>` and `<FeedbackPr
40
40
  ```tsx
41
41
  import { useEffect } from 'react'
42
42
  import { useUser } from '@clerk/clerk-react'
43
- import { useFeedback } from '@mhosaic/feedback/react'
43
+ import { useFeedback } from '@mhosaic/feedback/loader/react'
44
44
 
45
45
  export function FeedbackIdentity() {
46
46
  const { isLoaded, isSignedIn, user } = useUser()
@@ -118,7 +118,7 @@ Place after `initializeApp()` and `createFeedback()`. The callback fires immedia
118
118
 
119
119
  import { useEffect } from 'react'
120
120
  import { useSession } from 'next-auth/react'
121
- import { useFeedback } from '@mhosaic/feedback/react'
121
+ import { useFeedback } from '@mhosaic/feedback/loader/react'
122
122
 
123
123
  export function FeedbackIdentity() {
124
124
  const { data: session, status } = useSession()
@@ -1,8 +1,8 @@
1
1
  # Operator mode — provisioning a new project
2
2
 
3
- You are guiding the platform operator (Victor or a delegate) through creating a new **Company → Project → public widget key** on the Mhosaic backend, then assembling a handoff payload the consumer pastes into their own integration flow.
3
+ You are guiding the platform operator through creating a new **Company → Project → public widget key** on the Mhosaic backend, then assembling a handoff payload the consumer pastes into their own integration flow.
4
4
 
5
- **Strategy:** drive the operator's already-authenticated Chrome session at the software-factory admin SPA. For the API calls themselves, use `mcp__claude-in-chrome__javascript_tool` to invoke `fetch()` *from within* the browser tab — that way every call goes through the SPA's auth cookies, hits the DRF stack with full middleware (CSRF, permissions, audit), and never requires copying tokens into shell `curl` commands.
5
+ **Strategy:** drive the operator's already-authenticated Chrome session at the software-factory admin SPA. For the API calls themselves, use `mcp__claude-in-chrome__javascript_tool` to invoke `fetch()` _from within_ the browser tab — that way every call goes through the SPA's auth cookies, hits the DRF stack with full middleware (CSRF, permissions, audit), and never requires copying tokens into shell `curl` commands.
6
6
 
7
7
  The admin SPA is at https://software-factory-3tbbu.ondigitalocean.app.
8
8
 
@@ -31,6 +31,7 @@ Wait for the operator to confirm. Then re-screenshot to verify the dashboard is
31
31
  Use `AskUserQuestion` for **each** of these, one at a time. Always confirm before proceeding.
32
32
 
33
33
  1. **Company:** existing or new?
34
+
34
35
  - Use `AskUserQuestion` with options `Use existing company` and `Create new company`.
35
36
  - If existing: fetch the list with the JS snippet in step 4 and present a multi-choice question.
36
37
  - If new: ask for the company name (plain text). Auto-slugify it (lowercase, hyphens, strip accents — see `apps/admin/src/features/companies/NewCompanyForm.tsx:14-22`).
@@ -40,12 +41,14 @@ Use `AskUserQuestion` for **each** of these, one at a time. Always confirm befor
40
41
  3. **Project slug** (plain text, auto-slugified default from name). Must be unique within the company. Lowercase, hyphens, ≤ 80 chars.
41
42
 
42
43
  4. **Allowed origins** (plain text, comma- or space-separated list). Each must be a full `https?://host[:port]` URL. Rules:
44
+
43
45
  - Wildcards (`*`) are rejected.
44
46
  - `http://` is only permitted for localhost / 127.0.0.1 / ::1.
45
47
  - Path is forbidden (e.g. `https://app.example.com/widget` — drop the path).
46
48
  - Strongly recommend including BOTH the prod URL **and** the dev URL (e.g. `https://app.acme.com, http://localhost:5173`) so the teammate can test locally before deploying.
47
49
 
48
50
  5. **Share reports with widget board?** (`AskUserQuestion`, default No).
51
+
49
52
  - `Yes — every authenticated end-user sees every report` (good for internal tools)
50
53
  - `No — strict submitter-only view` (default, safer for public-facing apps)
51
54
 
@@ -65,8 +68,8 @@ The SPA holds its access token in memory (not localStorage — see `apps/admin/s
65
68
 
66
69
  ```javascript
67
70
  fetch('/api/auth/token/refresh/', { method: 'POST', credentials: 'include' })
68
- .then(r => r.json())
69
- .then(d => JSON.stringify({ access: d.access ?? d.access_token ?? null }))
71
+ .then((r) => r.json())
72
+ .then((d) => JSON.stringify({ access: d.access ?? d.access_token ?? null }))
70
73
  ```
71
74
 
72
75
  If `access` is null and the response was 401, the operator's session is fully expired — re-do step 2 (re-SSO). Otherwise capture the token and use it for `Authorization: Bearer <token>` on subsequent calls.
@@ -79,8 +82,10 @@ To list companies (response wraps them in `{items: [...]}`):
79
82
 
80
83
  ```javascript
81
84
  fetch('/api/feedback/v1/companies/', {
82
- headers: { 'Authorization': 'Bearer TOKEN' }
83
- }).then(r => r.json()).then(d => JSON.stringify(d.items ?? d))
85
+ headers: { Authorization: 'Bearer TOKEN' },
86
+ })
87
+ .then((r) => r.json())
88
+ .then((d) => JSON.stringify(d.items ?? d))
84
89
  ```
85
90
 
86
91
  Each item has `{id, slug, name}`. Filter client-side by slug if you're looking up an existing company.
@@ -91,11 +96,13 @@ To create a new company:
91
96
  fetch('/api/feedback/v1/companies/', {
92
97
  method: 'POST',
93
98
  headers: {
94
- 'Authorization': 'Bearer TOKEN',
99
+ Authorization: 'Bearer TOKEN',
95
100
  'Content-Type': 'application/json',
96
101
  },
97
- body: JSON.stringify({ name: 'COMPANY_NAME', slug: 'company-slug' })
98
- }).then(r => r.json()).then(d => JSON.stringify(d))
102
+ body: JSON.stringify({ name: 'COMPANY_NAME', slug: 'company-slug' }),
103
+ })
104
+ .then((r) => r.json())
105
+ .then((d) => JSON.stringify(d))
99
106
  ```
100
107
 
101
108
  Capture the company `id` (UUID) from the response.
@@ -106,7 +113,7 @@ Capture the company `id` (UUID) from the response.
106
113
  fetch('/api/feedback/v1/projects/', {
107
114
  method: 'POST',
108
115
  headers: {
109
- 'Authorization': 'Bearer TOKEN',
116
+ Authorization: 'Bearer TOKEN',
110
117
  'Content-Type': 'application/json',
111
118
  },
112
119
  body: JSON.stringify({
@@ -114,12 +121,15 @@ fetch('/api/feedback/v1/projects/', {
114
121
  name: 'PROJECT_NAME',
115
122
  slug: 'project-slug',
116
123
  allowed_origins: ['https://app.example.com', 'http://localhost:5173'],
117
- share_reports_with_widget: false
118
- })
119
- }).then(r => r.json()).then(d => JSON.stringify(d))
124
+ share_reports_with_widget: false,
125
+ }),
126
+ })
127
+ .then((r) => r.json())
128
+ .then((d) => JSON.stringify(d))
120
129
  ```
121
130
 
122
131
  If you get a 400 with `allowed_origins` validation error: parse the message, walk the operator through fixing the offending URL, and retry. Common reasons:
132
+
123
133
  - `http://` in prod (use `https://`)
124
134
  - trailing slash or path on the URL (strip it)
125
135
  - wildcard `*` (not supported by design)
@@ -134,15 +144,17 @@ Capture the project `id` from the response.
134
144
  fetch('/api/feedback/v1/project-keys/create/', {
135
145
  method: 'POST',
136
146
  headers: {
137
- 'Authorization': 'Bearer TOKEN',
147
+ Authorization: 'Bearer TOKEN',
138
148
  'Content-Type': 'application/json',
139
149
  },
140
150
  body: JSON.stringify({
141
151
  project_id: 'PROJECT_UUID',
142
152
  kind: 'public',
143
- label: 'Production widget key — minted by /integrate-feedback'
144
- })
145
- }).then(r => r.json()).then(d => JSON.stringify(d))
153
+ label: 'Production widget key — minted by /integrate-feedback',
154
+ }),
155
+ })
156
+ .then((r) => r.json())
157
+ .then((d) => JSON.stringify(d))
146
158
  ```
147
159
 
148
160
  The response includes `key: "pk_proj_…"` in plaintext. **This is the only time the plaintext key is ever returned.** Capture it immediately and treat it as a secret in your subsequent skill state (don't echo it more than necessary).
@@ -166,6 +178,7 @@ Construct this Markdown block and present it to the operator as the deliverable:
166
178
  ### Send to your teammate
167
179
 
168
180
  To install:
181
+
169
182
  ```bash
170
183
  npx @mhosaic/feedback-cli@latest init \
171
184
  --api-key <pk_proj_…> \
@@ -174,6 +187,7 @@ npx @mhosaic/feedback-cli@latest init \
174
187
  ```
175
188
 
176
189
  To verify after install:
190
+
177
191
  ```bash
178
192
  npx @mhosaic/feedback-cli@latest verify \
179
193
  --origin <consumer dev origin> \
@@ -181,15 +195,17 @@ npx @mhosaic/feedback-cli@latest verify \
181
195
  ```
182
196
 
183
197
  Or guide them with Claude Code:
198
+
184
199
  ```
185
200
  /integrate-feedback
186
201
  ```
202
+
187
203
  (then paste the block above as their first message)
188
204
  ````
189
205
 
190
206
  Print this to the chat.
191
207
 
192
- **On sharing the payload:** the `pk_proj_…` is a *public* key (it ships in every consumer's widget bundle, just like a Stripe publishable key). The real threat is "someone with the key + a whitelisted origin can submit junk reports against your project" — noise pollution, not a security breach. So **DM it rather than posting it in a public channel**, but you don't need 1Password levels of caution. The actual secrets in this flow (your SSO session, the backend JWT) never leave your browser; only the public artifacts are in the handoff.
208
+ **On sharing the payload:** the `pk_proj_…` is a _public_ key (it ships in every consumer's widget bundle, just like a Stripe publishable key). The real threat is "someone with the key + a whitelisted origin can submit junk reports against your project" — noise pollution, not a security breach. So **DM it rather than posting it in a public channel**, but you don't need 1Password levels of caution. The actual secrets in this flow (your SSO session, the backend JWT) never leave your browser; only the public artifacts are in the handoff.
193
209
 
194
210
  ---
195
211
 
@@ -201,15 +217,41 @@ If the project doesn't appear: something silently failed (browser cache, deploy
201
217
 
202
218
  ---
203
219
 
204
- ## Step 7 — Offer to continue into consumer mode
220
+ ## Step 7 — Set up the consumer side
221
+
222
+ The handoff payload is built. The consumer phase has to run in the client app's directory (where `.env.local` and the entry file live), NOT here. The operator now needs `/integrate-feedback` invokable from the client's repo so they can pick "Install (consumer)" there.
205
223
 
206
- `AskUserQuestion`:
207
- - question: `Want to install the widget into a consumer app right now?`
224
+ Check whether the skill is already installed globally:
225
+
226
+ ```bash
227
+ test -f ~/.claude/skills/integrate-feedback/SKILL.md && echo "INSTALLED" || echo "NOT-INSTALLED"
228
+ ```
229
+
230
+ If `INSTALLED`, skip to the closing instructions below.
231
+
232
+ If `NOT-INSTALLED`, ask via `AskUserQuestion`:
233
+
234
+ - question: `Install /integrate-feedback globally so it works in the client's repo?`
208
235
  - options:
209
- - `Yes — continue here`, description: `Move to consumer mode using the handoff payload we just built.`
210
- - `No — I'll send the payload to my teammate`, description: `Stop here.`
236
+ - label: `Yes — install it now`, description: `Runs "npx @mhosaic/feedback-cli@latest install-skill --force". ~10 seconds.`
237
+ - label: `No — I'll just run "npx @mhosaic/feedback-cli@latest init" directly`, description: `Skip the skill on the consumer side. CLI alone handles install + Vite+React auto-wrap; non-Vite frameworks need a manual snippet from references/.`
238
+
239
+ On "Yes", run via Bash:
240
+
241
+ ```bash
242
+ npx @mhosaic/feedback-cli@latest install-skill --force
243
+ ```
244
+
245
+ Surface the output verbatim. On success, tell the operator:
246
+
247
+ > "Done. Skill is at `~/.claude/skills/`. Next:
248
+ >
249
+ > 1. Close this Claude Code session (skills are discovered at session start).
250
+ > 2. `cd ~/path/to/client-app`
251
+ > 3. `claude`
252
+ > 4. `/integrate-feedback` → pick **Install (consumer)** → paste the handoff payload as your first message."
211
253
 
212
- If yes, follow `references/consumer-install.md` from step 1, pre-filling the handoff payload values.
254
+ On "No", print the exact CLI command with the operator's `pk_proj_…` and endpoint filled in, plus a pointer to `references/consumer-install-<framework>.md` for whatever framework the client uses.
213
255
 
214
256
  ---
215
257
 
@@ -224,12 +266,12 @@ If the operator wants to undo what was just provisioned:
224
266
 
225
267
  ## Error catalog
226
268
 
227
- | Symptom | Cause | Fix |
228
- |---|---|---|
229
- | `fetch` returns 401 | JWT expired | Re-do step 2 (re-login via SSO) |
230
- | `fetch` returns 403 on companies POST | Not staff | Operator's user lacks `is_staff` — provision via `/admin/` or `python manage.py shell` |
231
- | `fetch` returns 400 on projects POST with `allowed_origins` | Origin format invalid | Parse server error, walk operator through fixing the URL, retry |
232
- | `fetch` returns 400 with `slug` collision | Slug already exists in this company | Append `-2`, confirm with operator, retry |
233
- | Plaintext key not in response | Wrong endpoint (used `/api/feedback/v1/projects/<id>/keys/` instead of `/api/feedback/v1/project-keys/create/`) | Use the correct endpoint per `packages/backend/mhosaic_feedback/views_project_keys.py:36` |
234
- | `mcp__claude-in-chrome__javascript_tool` returns null/undefined | Snippet didn't return a value | All snippets above end with `JSON.stringify(...)` — verify the snippet's last expression is a JSON-serializable value |
235
- | Operator says "I don't see the new project" | Browser cache | Hard reload (Cmd+Shift+R). If still missing, network tab → check POST returned 201 |
269
+ | Symptom | Cause | Fix |
270
+ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
271
+ | `fetch` returns 401 | JWT expired | Re-do step 2 (re-login via SSO) |
272
+ | `fetch` returns 403 on companies POST | Not staff | Operator's user lacks `is_staff` — provision via `/admin/` or `python manage.py shell` |
273
+ | `fetch` returns 400 on projects POST with `allowed_origins` | Origin format invalid | Parse server error, walk operator through fixing the URL, retry |
274
+ | `fetch` returns 400 with `slug` collision | Slug already exists in this company | Append `-2`, confirm with operator, retry |
275
+ | Plaintext key not in response | Wrong endpoint (used `/api/feedback/v1/projects/<id>/keys/` instead of `/api/feedback/v1/project-keys/create/`) | Use the correct endpoint per `packages/backend/mhosaic_feedback/views_project_keys.py:36` |
276
+ | `mcp__claude-in-chrome__javascript_tool` returns null/undefined | Snippet didn't return a value | All snippets above end with `JSON.stringify(...)` — verify the snippet's last expression is a JSON-serializable value |
277
+ | Operator says "I don't see the new project" | Browser cache | Hard reload (Cmd+Shift+R). If still missing, network tab → check POST returned 201 |
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/doctor.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport kleur from 'kleur'\n\nimport { detectFramework } from '../detect'\n\nexport async function runDoctor(argv: string[]): Promise<void> {\n const cwd = argv.includes('--cwd') ? argv[argv.indexOf('--cwd') + 1] ?? process.cwd() : process.cwd()\n\n const checks: Array<{ name: string; ok: boolean; hint?: string }> = []\n\n const framework = await detectFramework(cwd)\n checks.push({ name: `framework detected: ${framework.kind}`, ok: framework.kind !== 'unknown' && framework.kind !== 'plain' })\n\n const envPath = join(cwd, '.env.local')\n const envOk = existsSync(envPath) && readFileSync(envPath, 'utf8').includes('VITE_FEEDBACK_API_KEY=')\n checks.push({ name: '.env.local has VITE_FEEDBACK_API_KEY', ok: envOk, hint: 'run `mhosaic-feedback init`' })\n\n const giPath = join(cwd, '.gitignore')\n const giOk = existsSync(giPath) && readFileSync(giPath, 'utf8').includes('.env.local')\n checks.push({ name: '.gitignore ignores .env.local', ok: giOk, hint: 'add `.env.local` to .gitignore' })\n\n let wrapOk = false\n if (framework.entry) {\n const entryPath = join(cwd, framework.entry)\n if (existsSync(entryPath)) {\n const src = readFileSync(entryPath, 'utf8')\n wrapOk = src.includes('<FeedbackProvider') && src.includes(\"@mhosaic/feedback/react\")\n }\n }\n checks.push({ name: '<FeedbackProvider> wired in entry', ok: wrapOk, hint: 'run `mhosaic-feedback init`' })\n\n let hasFailure = false\n for (const c of checks) {\n const icon = c.ok ? kleur.green('✓') : kleur.red('✗')\n process.stdout.write(`${icon} ${c.name}${!c.ok && c.hint ? kleur.gray(' — ' + c.hint) : ''}\\n`)\n if (!c.ok) hasFailure = true\n }\n if (hasFailure) process.exitCode = 1\n}\n"],"mappings":";;;;;;AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AAErB,OAAO,WAAW;AAIlB,eAAsB,UAAU,MAA+B;AAC7D,QAAM,MAAM,KAAK,SAAS,OAAO,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,CAAC,KAAK,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAEpG,QAAM,SAA8D,CAAC;AAErE,QAAM,YAAY,MAAM,gBAAgB,GAAG;AAC3C,SAAO,KAAK,EAAE,MAAM,uBAAuB,UAAU,IAAI,IAAI,IAAI,UAAU,SAAS,aAAa,UAAU,SAAS,QAAQ,CAAC;AAE7H,QAAM,UAAU,KAAK,KAAK,YAAY;AACtC,QAAM,QAAQ,WAAW,OAAO,KAAK,aAAa,SAAS,MAAM,EAAE,SAAS,wBAAwB;AACpG,SAAO,KAAK,EAAE,MAAM,wCAAwC,IAAI,OAAO,MAAM,8BAA8B,CAAC;AAE5G,QAAM,SAAS,KAAK,KAAK,YAAY;AACrC,QAAM,OAAO,WAAW,MAAM,KAAK,aAAa,QAAQ,MAAM,EAAE,SAAS,YAAY;AACrF,SAAO,KAAK,EAAE,MAAM,iCAAiC,IAAI,MAAM,MAAM,iCAAiC,CAAC;AAEvG,MAAI,SAAS;AACb,MAAI,UAAU,OAAO;AACnB,UAAM,YAAY,KAAK,KAAK,UAAU,KAAK;AAC3C,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,MAAM,aAAa,WAAW,MAAM;AAC1C,eAAS,IAAI,SAAS,mBAAmB,KAAK,IAAI,SAAS,yBAAyB;AAAA,IACtF;AAAA,EACF;AACA,SAAO,KAAK,EAAE,MAAM,qCAAqC,IAAI,QAAQ,MAAM,8BAA8B,CAAC;AAE1G,MAAI,aAAa;AACjB,aAAW,KAAK,QAAQ;AACtB,UAAM,OAAO,EAAE,KAAK,MAAM,MAAM,QAAG,IAAI,MAAM,IAAI,QAAG;AACpD,YAAQ,OAAO,MAAM,GAAG,IAAI,IAAI,EAAE,IAAI,GAAG,CAAC,EAAE,MAAM,EAAE,OAAO,MAAM,KAAK,aAAQ,EAAE,IAAI,IAAI,EAAE;AAAA,CAAI;AAC9F,QAAI,CAAC,EAAE,GAAI,cAAa;AAAA,EAC1B;AACA,MAAI,WAAY,SAAQ,WAAW;AACrC;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/init.ts","../src/edits/env.ts","../src/edits/gitignore.ts","../src/edits/react.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { spawnSync } from 'node:child_process'\n\nimport kleur from 'kleur'\nimport prompts from 'prompts'\n\nimport { detectFramework } from '../detect'\nimport { writeEnvLocal, writeEnvLocalExample } from '../edits/env'\nimport { ensureGitignore } from '../edits/gitignore'\nimport { wrapReactEntry } from '../edits/react'\n\ninterface InitArgs {\n apiKey?: string\n endpoint?: string\n yes: boolean\n install: boolean\n cwd: string\n}\n\nfunction parseArgs(argv: string[]): InitArgs {\n const out: InitArgs = { yes: false, install: true, cwd: process.cwd() }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--api-key') { const v = argv[++i]; if (v !== undefined) out.apiKey = v }\n else if (a === '--endpoint') { const v = argv[++i]; if (v !== undefined) out.endpoint = v }\n else if (a === '--yes' || a === '-y') out.yes = true\n else if (a === '--no-install') out.install = false\n else if (a === '--cwd') out.cwd = argv[++i] ?? process.cwd()\n }\n return out\n}\n\nfunction detectPackageManager(cwd: string): 'pnpm' | 'yarn' | 'npm' {\n if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'\n if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'\n return 'npm'\n}\n\nexport async function runInit(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const { cwd } = args\n\n process.stdout.write(kleur.bold('\\n⚡ Mhosaic Feedback setup\\n\\n'))\n\n const framework = await detectFramework(cwd)\n process.stdout.write(kleur.gray(`Detected: ${framework.kind}${framework.entry ? ' (' + framework.entry + ')' : ''}\\n`))\n\n let apiKey = args.apiKey\n let endpoint = args.endpoint ?? 'http://localhost:8000'\n if (!args.yes && !apiKey) {\n const answers = await prompts([\n { type: 'text', name: 'endpoint', message: 'Backend endpoint', initial: endpoint },\n { type: 'text', name: 'apiKey', message: 'Public API key (pk_proj_…)', validate: (v: string) => v.startsWith('pk_proj_') || 'must start with pk_proj_' },\n ])\n if (!answers.apiKey) {\n process.stdout.write(kleur.red('Aborted.\\n'))\n process.exitCode = 1\n return\n }\n endpoint = answers.endpoint\n apiKey = answers.apiKey\n }\n if (!apiKey) {\n process.stderr.write('api-key is required (flag or prompt)\\n')\n process.exitCode = 1\n return\n }\n\n const pm = detectPackageManager(cwd)\n if (args.install) {\n process.stdout.write(kleur.gray(`Installing @mhosaic/feedback via ${pm}…\\n`))\n const cmd = pm === 'pnpm' ? ['pnpm', 'add', '@mhosaic/feedback'] : pm === 'yarn' ? ['yarn', 'add', '@mhosaic/feedback'] : ['npm', 'install', '@mhosaic/feedback']\n const res = spawnSync(cmd[0]!, cmd.slice(1), { cwd, stdio: 'inherit' })\n if (res.status !== 0) {\n process.stderr.write(kleur.red('install failed\\n'))\n process.exitCode = res.status ?? 1\n return\n }\n process.stdout.write(kleur.green('✓ Installed @mhosaic/feedback\\n'))\n }\n\n await writeEnvLocal(join(cwd, '.env.local'), { apiKey, endpoint })\n process.stdout.write(kleur.green('✓ Wrote .env.local\\n'))\n\n await writeEnvLocalExample(join(cwd, '.env.local.example'))\n process.stdout.write(kleur.green('✓ Wrote .env.local.example (commit this — your next teammate will thank you)\\n'))\n\n await ensureGitignore(join(cwd, '.gitignore'))\n process.stdout.write(kleur.green('✓ Updated .gitignore\\n'))\n\n if (framework.entry && framework.kind === 'vite-react') {\n const entryPath = join(cwd, framework.entry)\n if (existsSync(entryPath)) {\n await wrapReactEntry(entryPath, {\n apiKeyEnv: 'VITE_FEEDBACK_API_KEY',\n endpointEnv: 'VITE_FEEDBACK_ENDPOINT',\n })\n process.stdout.write(kleur.green(`✓ Wrapped ${framework.entry}\\n`))\n } else {\n process.stdout.write(kleur.yellow(`⚠ Expected entry ${framework.entry} not found — wire <FeedbackProvider> manually.\\n`))\n }\n } else if (framework.kind !== 'vite-react') {\n process.stdout.write(kleur.yellow(`⚠ Auto-wiring is only supported for Vite + React today. For ${framework.kind}, add <FeedbackProvider> manually.\\n`))\n }\n\n process.stdout.write(kleur.bold('\\nDone.\\n'))\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\n\nconst MARK = '# === mhosaic-feedback ==='\n\nexport interface EnvEntries {\n apiKey: string\n endpoint: string\n}\n\n/**\n * Reject values that could escape the `KEY=value` line in a .env file.\n *\n * Audit R5/M3: the previous version interpolated user input verbatim,\n * so a paste containing `\\n` would inject arbitrary additional\n * variables, and `\"` would unbalance any later parser. The user owns\n * their own machine — this is a defense-in-depth guard against\n * accidental misuse (e.g. paste from a multi-line message) more than\n * an attack surface.\n */\nfunction ensureEnvSafe(field: 'apiKey' | 'endpoint', value: string): string {\n if (/[\\r\\n]/.test(value)) {\n throw new Error(`${field} contains a newline; refusing to write .env.local.`)\n }\n if (value.includes('\"')) {\n throw new Error(`${field} contains a double-quote; refusing to write .env.local.`)\n }\n if (value.includes('\\0')) {\n throw new Error(`${field} contains a NUL byte; refusing to write .env.local.`)\n }\n return value\n}\n\nfunction ensureKnownEndpoint(value: string): string {\n // http(s) only — a `javascript:` or `file:` URL in .env would silently\n // become the configured backend after `pnpm dev`.\n if (!/^https?:\\/\\/[^\\s]+$/i.test(value)) {\n throw new Error(\n `endpoint must be an http(s) URL with no whitespace; got ${JSON.stringify(value)}`,\n )\n }\n return value\n}\n\nfunction ensureApiKey(value: string): string {\n // Trim accidental leading/trailing whitespace — pasted keys frequently\n // arrive with a trailing newline that survives split-on-tab.\n const trimmed = value.trim()\n if (!trimmed.startsWith('pk_proj_')) {\n throw new Error(\n 'apiKey must look like a widget public key (starts with `pk_proj_`).',\n )\n }\n return trimmed\n}\n\nexport function renderEnv(existing: string, entries: EnvEntries): string {\n const apiKey = ensureEnvSafe('apiKey', ensureApiKey(entries.apiKey))\n const endpoint = ensureEnvSafe('endpoint', ensureKnownEndpoint(entries.endpoint))\n const lines = existing.split(/\\r?\\n/)\n const filtered: string[] = []\n let inBlock = false\n for (const line of lines) {\n if (line.trim() === MARK) { inBlock = !inBlock; continue }\n if (inBlock) continue\n if (line.startsWith('VITE_FEEDBACK_API_KEY=') || line.startsWith('VITE_FEEDBACK_ENDPOINT=')) continue\n filtered.push(line)\n }\n // Trim trailing empty lines\n while (filtered.length > 0 && filtered[filtered.length - 1] === '') filtered.pop()\n const block = [\n MARK,\n `VITE_FEEDBACK_API_KEY=${apiKey}`,\n `VITE_FEEDBACK_ENDPOINT=${endpoint}`,\n MARK,\n ].join('\\n')\n return (filtered.length > 0 ? filtered.join('\\n') + '\\n\\n' : '') + block + '\\n'\n}\n\nexport async function writeEnvLocal(path: string, entries: EnvEntries): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n await writeFile(path, renderEnv(existing, entries))\n}\n\n/**\n * Write a `.env.local.example` template alongside `.env.local`. This file\n * IS checked into git (it lives next to .env.local, but only .env.local\n * gets gitignored) — so the second developer joining the repo sees which\n * keys they need to set, with placeholder values, without having to ask.\n *\n * Idempotent: re-running `init` overwrites the marker block but leaves\n * any custom keys the team added between markers untouched.\n */\nexport async function writeEnvLocalExample(path: string): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n // Reuse renderEnv with placeholder values so the file shape exactly\n // matches `.env.local` (same marker, same key order, same encoding rules).\n const rendered = renderEnv(existing, {\n apiKey: 'pk_proj_REPLACE_ME_WITH_YOUR_PROJECT_KEY',\n endpoint: 'https://software-factory-3tbbu.ondigitalocean.app',\n })\n // Prepend a one-line header so a developer who opens the file knows\n // it's a template (and that .env.local is the real file).\n const header = '# Copy this file to .env.local and replace the placeholder.\\n# .env.local is gitignored; this template stays in git so the next\\n# developer onboarding the repo sees which keys are required.\\n'\n await writeFile(path, header + rendered)\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\n\nconst REQUIRED = ['.env.local']\n\nexport function renderGitignore(existing: string): string {\n const lines = new Set(existing.split(/\\r?\\n/).map((l) => l.trim()))\n const missing = REQUIRED.filter((entry) => !lines.has(entry))\n if (missing.length === 0) return existing\n const trailingNewline = existing.endsWith('\\n') || existing === ''\n return existing + (trailingNewline ? '' : '\\n') + missing.join('\\n') + '\\n'\n}\n\nexport async function ensureGitignore(path: string): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n const next = renderGitignore(existing)\n if (next !== existing) await writeFile(path, next)\n}\n","import { readFile, writeFile } from 'node:fs/promises'\n\nconst IMPORT_MARK_START = '// === mhosaic-feedback:import:start ==='\nconst IMPORT_MARK_END = '// === mhosaic-feedback:import:end ==='\nconst WRAP_MARK_START = '{/* === mhosaic-feedback:wrap:start === */}'\nconst WRAP_MARK_END = '{/* === mhosaic-feedback:wrap:end === */}'\n\nexport interface WrapOptions {\n apiKeyEnv: string // 'VITE_FEEDBACK_API_KEY'\n endpointEnv: string // 'VITE_FEEDBACK_ENDPOINT'\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction buildImportBlock(): string {\n return [\n IMPORT_MARK_START,\n \"import { FeedbackProvider } from '@mhosaic/feedback/react'\",\n IMPORT_MARK_END,\n ].join('\\n')\n}\n\nfunction buildWrapOpen(opts: WrapOptions): string {\n return [\n WRAP_MARK_START,\n `<FeedbackProvider apiKey={import.meta.env.${opts.apiKeyEnv} as string} endpoint={import.meta.env.${opts.endpointEnv} as string} env=\"prod\">`,\n ].join('\\n ')\n}\n\nfunction buildWrapClose(): string {\n return ['</FeedbackProvider>', WRAP_MARK_END].join('\\n ')\n}\n\nexport function transformReactEntry(source: string, opts: WrapOptions): string {\n let out = source\n\n // If markers are already present, do an in-place update of the FeedbackProvider\n // props (import block + wrap tag) without restructuring anything — this is what\n // makes re-runs idempotent without needing to parse/unwrap the JSX tree.\n const alreadyWrapped =\n out.includes(IMPORT_MARK_START) && out.includes(WRAP_MARK_START)\n\n if (alreadyWrapped) {\n // Update import block (replace old import line inside the markers)\n out = out.replace(\n new RegExp(`(${escapeRegex(IMPORT_MARK_START)}\\\\n)[^\\\\n]*(\\\\n${escapeRegex(IMPORT_MARK_END)})`),\n `$1import { FeedbackProvider } from '@mhosaic/feedback/react'$2`,\n )\n // Update FeedbackProvider opening tag props\n out = out.replace(\n /<FeedbackProvider[^>]*>/,\n `<FeedbackProvider apiKey={import.meta.env.${opts.apiKeyEnv} as string} endpoint={import.meta.env.${opts.endpointEnv} as string} env=\"prod\">`,\n )\n return out\n }\n\n // Fresh insert: strip any partial markers that may exist (safety), then insert.\n out = out.replace(\n new RegExp(`${escapeRegex(IMPORT_MARK_START)}[\\\\s\\\\S]*?${escapeRegex(IMPORT_MARK_END)}\\\\n?`, 'g'),\n '',\n )\n out = out.replace(\n new RegExp(`${escapeRegex(WRAP_MARK_START)}[\\\\s\\\\S]*?${escapeRegex(WRAP_MARK_END)}\\\\n?`, 'g'),\n '',\n )\n\n // Insert import block after the last existing import statement.\n const importRegex = /^(import[^\\n]+\\n)+/m\n const importMatch = out.match(importRegex)\n if (importMatch) {\n const idx = importMatch.index! + importMatch[0].length\n out = out.slice(0, idx) + buildImportBlock() + '\\n\\n' + out.slice(idx)\n } else {\n out = buildImportBlock() + '\\n\\n' + out\n }\n\n // Wrap the App component inside the render call.\n // Pattern A: createRoot(...).render(<jsx>) — covers both single-line and multi-line.\n // Pattern B: ReactDOM.render(<jsx>, document.getElementById(...))\n const renderPatterns: Array<RegExp> = [\n /\\.render\\(\\s*([\\s\\S]*?)\\s*\\)\\s*[,;]?\\s*\\n/,\n /ReactDOM\\.render\\(\\s*([\\s\\S]*?),\\s*document\\.getElementById/,\n ]\n for (const re of renderPatterns) {\n const m = out.match(re)\n if (m && m.index !== undefined) {\n // Strip any trailing argument-list punctuation from the captured JSX —\n // `render(<App />,)` is valid JS but wrapping a trailing `,` inside the\n // FeedbackProvider children would render a literal \",\" as a text node.\n const jsx = m[1]!.replace(/[,;]\\s*$/, '')\n const wrapped = `\\n ${buildWrapOpen(opts)}\\n ${jsx.replace(/\\n/g, '\\n ')}\\n ${buildWrapClose()}\\n `\n const startIdx = m.index + m[0].indexOf(m[1]!)\n out = out.slice(0, startIdx) + wrapped + out.slice(startIdx + m[1]!.length)\n break\n }\n }\n\n return out\n}\n\nexport async function wrapReactEntry(path: string, opts: WrapOptions): Promise<void> {\n const source = await readFile(path, 'utf8')\n const next = transformReactEntry(source, opts)\n if (next !== source) await writeFile(path, next)\n}\n"],"mappings":"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","names":["existsSync","readFile","writeFile","existsSync","readFile","writeFile","existsSync"]}