@mhosaic/feedback-cli 0.14.0 → 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 (27) hide show
  1. package/dist/bin.js +4 -4
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{doctor-HTTAR4ZB.js → doctor-K2C6LBR2.js} +2 -2
  4. package/dist/doctor-K2C6LBR2.js.map +1 -0
  5. package/dist/{init-64QSAHMB.js → init-RS7BKALE.js} +14 -3
  6. package/dist/init-RS7BKALE.js.map +1 -0
  7. package/dist/{install-skill-CIZ3ZQI7.js → install-skill-QJ4ZDVVR.js} +8 -5
  8. package/dist/{install-skill-CIZ3ZQI7.js.map → install-skill-QJ4ZDVVR.js.map} +1 -1
  9. package/package.json +1 -1
  10. package/skills/feedback-close/SKILL.md +58 -0
  11. package/skills/feedback-fix/SKILL.md +107 -0
  12. package/skills/feedback-pull/SKILL.md +56 -0
  13. package/skills/feedback-watch-merges/SKILL.md +62 -0
  14. package/skills/integrate-feedback/SKILL.md +109 -64
  15. package/skills/integrate-feedback/references/consumer-install-astro.md +83 -0
  16. package/skills/integrate-feedback/references/consumer-install-next.md +2 -2
  17. package/skills/integrate-feedback/references/consumer-install-nuxt.md +108 -0
  18. package/skills/integrate-feedback/references/consumer-install-plain.md +55 -30
  19. package/skills/integrate-feedback/references/consumer-install-remix.md +1 -1
  20. package/skills/integrate-feedback/references/consumer-install-svelte.md +1 -1
  21. package/skills/integrate-feedback/references/consumer-install-vite.md +2 -2
  22. package/skills/integrate-feedback/references/consumer-install-vue.md +1 -1
  23. package/skills/integrate-feedback/references/consumer-install.md +67 -18
  24. package/skills/integrate-feedback/references/identify-snippets.md +3 -3
  25. package/skills/integrate-feedback/references/operator-provision.md +77 -33
  26. package/dist/doctor-HTTAR4ZB.js.map +0 -1
  27. package/dist/init-64QSAHMB.js.map +0 -1
@@ -15,7 +15,7 @@ Read `src/main.tsx` and confirm you see:
15
15
 
16
16
  ```typescript
17
17
  // === mhosaic-feedback:import:start ===
18
- import { FeedbackProvider } from '@mhosaic/feedback/react'
18
+ import { FeedbackProvider } from '@mhosaic/feedback/loader/react'
19
19
  // === mhosaic-feedback:import:end ===
20
20
 
21
21
  // ... existing imports
@@ -45,7 +45,7 @@ The default wrap uses `<FeedbackProvider>`, which doesn't expose the wrapper-cha
45
45
 
46
46
  ```typescript
47
47
  // src/feedback.ts
48
- import { createFeedback } from '@mhosaic/feedback'
48
+ import { createFeedback } from '@mhosaic/feedback/loader'
49
49
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
50
50
  import { withReplay } from '@mhosaic/feedback/replay'
51
51
  import { withWebVitals } from '@mhosaic/feedback/webvitals'
@@ -23,7 +23,7 @@ Edit `src/main.ts`:
23
23
  import { createApp } from 'vue'
24
24
  import App from './App.vue'
25
25
 
26
- import { createFeedback } from '@mhosaic/feedback'
26
+ import { createFeedback } from '@mhosaic/feedback/loader'
27
27
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
28
28
  import { withReplay } from '@mhosaic/feedback/replay'
29
29
  import { withWebVitals } from '@mhosaic/feedback/webvitals'
@@ -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:
@@ -17,28 +47,44 @@ If the user can't find the payload, walk them through asking the operator for it
17
47
 
18
48
  ---
19
49
 
20
- ## Step 2 — Verify we're in a JS/TS project
50
+ ## Step 2 — Classify the host app
51
+
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:
21
55
 
22
- `Read` the user's `package.json`. If the file doesn't exist, stop and ask:
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)'."
23
57
 
24
- > "I don't see a `package.json` in `<cwd>`. Are you in your host app's project root? cd there and re-run `/integrate-feedback`."
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:
63
+
64
+ 1. **package.json exists with `dependencies`** → continue to Step 3 (framework detection).
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."
66
+ 3. **Neither package.json nor any obvious template root** → ask the user with plain text: "I don't see a `package.json` or a template folder. Are you in your host app's project root? What kind of app is this — JS framework, server-rendered (Django/Rails/Hugo), or something else?" Branch based on the answer.
25
67
 
26
68
  ---
27
69
 
28
70
  ## Step 3 — Detect the framework
29
71
 
30
- 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.
31
73
 
32
74
  Read `package.json`'s `dependencies` + `devDependencies` and classify:
33
75
 
34
- | Found | Framework | Reference doc | CLI auto-wires entry? |
35
- |---|---|---|---|
36
- | `vite` + `react` + `@vitejs/plugin-react` | `vite-react` | `consumer-install-vite.md` | ✅ yes |
37
- | `next` | `nextjs` (App or Pages Router — sniff `app/` vs `pages/`) | `consumer-install-next.md` | ❌ paste snippet |
38
- | `@remix-run/react` | `remix` | `consumer-install-remix.md` | ❌ paste snippet |
39
- | `vue` ≥ 3 | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
40
- | `@sveltejs/kit` | `sveltekit` | `consumer-install-svelte.md` | ❌ paste snippet |
41
- | 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 |
86
+
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).
42
88
 
43
89
  Tell the user which framework you detected. Confirm with `AskUserQuestion` (especially if Next.js, since App vs Pages Router branches differently).
44
90
 
@@ -56,12 +102,14 @@ npx @mhosaic/feedback-cli@latest init \
56
102
  ```
57
103
 
58
104
  This will:
105
+
59
106
  - `npm install @mhosaic/feedback` (using the project's package manager)
60
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)
61
108
  - Ensure `.env.local` is in `.gitignore`
62
109
  - If `vite-react`: auto-wrap `src/main.tsx` with `<FeedbackProvider>`
63
110
 
64
111
  Surface the CLI's output verbatim. If install fails, stop and read the error message; common causes:
112
+
65
113
  - No internet
66
114
  - Lockfile conflict (offer to delete `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json` and retry)
67
115
  - Wrong directory (no `package.json`)
@@ -80,6 +128,7 @@ Open the per-framework reference document and follow it verbatim:
80
128
  - Plain HTML / CDN: `consumer-install-plain.md`
81
129
 
82
130
  Each reference doc gives:
131
+
83
132
  1. The env-var prefix (e.g. `NEXT_PUBLIC_FEEDBACK_API_KEY` for Next.js — the CLI's default `VITE_FEEDBACK_*` needs renaming)
84
133
  2. The entry-point snippet to paste (with import + provider/init call)
85
134
  3. Where to put it (exact file path)
@@ -100,6 +149,7 @@ If you can't detect the auth provider from `package.json`, ask the user explicit
100
149
  ## Step 7 — (Optional but recommended) Wire the auto-capture modules
101
150
 
102
151
  `AskUserQuestion`:
152
+
103
153
  - question: `Wire the auto-capture modules?`
104
154
  - options:
105
155
  - `Yes — all three (error-tracking, web-vitals, replay)`, description: `Captures runtime errors, Core Web Vitals, and session replay. ~30 KiB gzip overhead.`
@@ -109,16 +159,12 @@ If you can't detect the auth provider from `package.json`, ask the user explicit
109
159
  For each `Yes`, modify the consumer's widget init to chain the wrappers. The pattern:
110
160
 
111
161
  ```typescript
112
- import { createFeedback } from '@mhosaic/feedback'
162
+ import { createFeedback } from '@mhosaic/feedback/loader'
113
163
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
114
164
  import { withReplay } from '@mhosaic/feedback/replay'
115
165
  import { withWebVitals } from '@mhosaic/feedback/webvitals'
116
166
 
117
- const fb = withErrorTracking(
118
- withReplay(
119
- withWebVitals(createFeedback({ apiKey, endpoint }))
120
- )
121
- )
167
+ const fb = withErrorTracking(withReplay(withWebVitals(createFeedback({ apiKey, endpoint }))))
122
168
  ```
123
169
 
124
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).
@@ -134,12 +180,14 @@ npx @mhosaic/feedback-cli@latest verify \
134
180
  ```
135
181
 
136
182
  This is a lightweight check; it confirms:
183
+
137
184
  - `.env.local` has the right shape
138
185
  - The endpoint resolves and responds
139
186
  - CORS allows the consumer's origin
140
187
  - The public key is accepted (POSTs a `[mhosaic-feedback verify]` synthetic report)
141
188
 
142
189
  If anything is red, the verify command prints a hint. Common hints:
190
+
143
191
  - `add this origin to the project's allowed_origins (admin SPA: Edit project)` → operator needs to add the dev/prod origin.
144
192
  - `key invalid or revoked` → operator needs to mint a new key.
145
193
 
@@ -172,6 +220,7 @@ Summarize for the user:
172
220
  - Next: deploy to staging, add the staging URL to `allowed_origins` (operator action — DM them the URL)
173
221
 
174
222
  Encourage them to:
223
+
175
224
  - Commit the change (sans `.env.local`)
176
225
  - Tell the operator the smoke test passed
177
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,13 +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
- Print this to the chat. Encourage the operator to share it via a secure channel (1Password, Slack DM, signed email) — **not in public channels or git**.
206
+ Print this to the chat.
207
+
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.
191
209
 
192
210
  ---
193
211
 
@@ -199,15 +217,41 @@ If the project doesn't appear: something silently failed (browser cache, deploy
199
217
 
200
218
  ---
201
219
 
202
- ## 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.
223
+
224
+ Check whether the skill is already installed globally:
203
225
 
204
- `AskUserQuestion`:
205
- - question: `Want to install the widget into a consumer app right now?`
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?`
206
235
  - options:
207
- - `Yes — continue here`, description: `Move to consumer mode using the handoff payload we just built.`
208
- - `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."
209
253
 
210
- 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.
211
255
 
212
256
  ---
213
257
 
@@ -222,12 +266,12 @@ If the operator wants to undo what was just provisioned:
222
266
 
223
267
  ## Error catalog
224
268
 
225
- | Symptom | Cause | Fix |
226
- |---|---|---|
227
- | `fetch` returns 401 | JWT expired | Re-do step 2 (re-login via SSO) |
228
- | `fetch` returns 403 on companies POST | Not staff | Operator's user lacks `is_staff` — provision via `/admin/` or `python manage.py shell` |
229
- | `fetch` returns 400 on projects POST with `allowed_origins` | Origin format invalid | Parse server error, walk operator through fixing the URL, retry |
230
- | `fetch` returns 400 with `slug` collision | Slug already exists in this company | Append `-2`, confirm with operator, retry |
231
- | 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` |
232
- | `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 |
233
- | 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 } 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 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","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"]}