@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.
- package/dist/bin.js +4 -4
- package/dist/bin.js.map +1 -1
- package/dist/{doctor-HTTAR4ZB.js → doctor-K2C6LBR2.js} +2 -2
- package/dist/doctor-K2C6LBR2.js.map +1 -0
- package/dist/{init-64QSAHMB.js → init-RS7BKALE.js} +14 -3
- package/dist/init-RS7BKALE.js.map +1 -0
- package/dist/{install-skill-CIZ3ZQI7.js → install-skill-QJ4ZDVVR.js} +8 -5
- package/dist/{install-skill-CIZ3ZQI7.js.map → install-skill-QJ4ZDVVR.js.map} +1 -1
- package/package.json +1 -1
- package/skills/feedback-close/SKILL.md +58 -0
- package/skills/feedback-fix/SKILL.md +107 -0
- package/skills/feedback-pull/SKILL.md +56 -0
- package/skills/feedback-watch-merges/SKILL.md +62 -0
- package/skills/integrate-feedback/SKILL.md +109 -64
- package/skills/integrate-feedback/references/consumer-install-astro.md +83 -0
- package/skills/integrate-feedback/references/consumer-install-next.md +2 -2
- package/skills/integrate-feedback/references/consumer-install-nuxt.md +108 -0
- package/skills/integrate-feedback/references/consumer-install-plain.md +55 -30
- package/skills/integrate-feedback/references/consumer-install-remix.md +1 -1
- package/skills/integrate-feedback/references/consumer-install-svelte.md +1 -1
- package/skills/integrate-feedback/references/consumer-install-vite.md +2 -2
- package/skills/integrate-feedback/references/consumer-install-vue.md +1 -1
- package/skills/integrate-feedback/references/consumer-install.md +67 -18
- package/skills/integrate-feedback/references/identify-snippets.md +3 -3
- package/skills/integrate-feedback/references/operator-provision.md +77 -33
- package/dist/doctor-HTTAR4ZB.js.map +0 -1
- 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 —
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
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
|
|
35
|
-
|
|
36
|
-
| `vite` + `react` + `@vitejs/plugin-react` | `vite-react`
|
|
37
|
-
| `next`
|
|
38
|
-
|
|
|
39
|
-
| `
|
|
40
|
-
| `@
|
|
41
|
-
|
|
|
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
|
|
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()`
|
|
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: {
|
|
83
|
-
})
|
|
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
|
-
|
|
99
|
+
Authorization: 'Bearer TOKEN',
|
|
95
100
|
'Content-Type': 'application/json',
|
|
96
101
|
},
|
|
97
|
-
body: JSON.stringify({ name: 'COMPANY_NAME', slug: 'company-slug' })
|
|
98
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
})
|
|
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.
|
|
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 —
|
|
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
|
-
|
|
205
|
-
-
|
|
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 —
|
|
208
|
-
- `No — I'll
|
|
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
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
| `fetch` returns 401
|
|
228
|
-
| `fetch` returns 403 on companies POST
|
|
229
|
-
| `fetch` returns 400 on projects POST with `allowed_origins`
|
|
230
|
-
| `fetch` returns 400 with `slug` collision
|
|
231
|
-
| Plaintext key not in response
|
|
232
|
-
| `mcp__claude-in-chrome__javascript_tool` returns null/undefined | Snippet didn't return a value
|
|
233
|
-
| Operator says "I don't see the new project"
|
|
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"]}
|