@mhosaic/feedback-cli 0.13.0 → 0.14.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/README.md +109 -0
- package/dist/bin.js +15 -5
- package/dist/bin.js.map +1 -1
- package/dist/{init-64QSAHMB.js → init-P45CEXDP.js} +12 -1
- package/dist/init-P45CEXDP.js.map +1 -0
- package/dist/install-skill-3OPVFMTK.js +89 -0
- package/dist/install-skill-3OPVFMTK.js.map +1 -0
- package/dist/verify-LCXL3WOX.js +188 -0
- package/dist/verify-LCXL3WOX.js.map +1 -0
- package/package.json +4 -2
- package/skills/integrate-feedback/SKILL.md +163 -0
- package/skills/integrate-feedback/references/consumer-install-astro.md +83 -0
- package/skills/integrate-feedback/references/consumer-install-next.md +133 -0
- package/skills/integrate-feedback/references/consumer-install-nuxt.md +108 -0
- package/skills/integrate-feedback/references/consumer-install-plain.md +92 -0
- package/skills/integrate-feedback/references/consumer-install-remix.md +93 -0
- package/skills/integrate-feedback/references/consumer-install-svelte.md +87 -0
- package/skills/integrate-feedback/references/consumer-install-vite.md +100 -0
- package/skills/integrate-feedback/references/consumer-install-vue.md +92 -0
- package/skills/integrate-feedback/references/consumer-install.md +185 -0
- package/skills/integrate-feedback/references/identify-snippets.md +218 -0
- package/skills/integrate-feedback/references/operator-provision.md +235 -0
- package/skills/integrate-feedback/references/verify-install.md +151 -0
- package/dist/init-64QSAHMB.js.map +0 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Operator mode — provisioning a new project
|
|
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.
|
|
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.
|
|
6
|
+
|
|
7
|
+
The admin SPA is at https://software-factory-3tbbu.ondigitalocean.app.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Step 1 — Confirm Chrome MCP is available
|
|
12
|
+
|
|
13
|
+
Before anything else, call `mcp__claude-in-chrome__tabs_context_mcp` to discover what tabs exist. If you don't see a tab pointed at `software-factory-3tbbu.ondigitalocean.app`, create one with `mcp__claude-in-chrome__tabs_create_mcp`.
|
|
14
|
+
|
|
15
|
+
If `tabs_context_mcp` errors with "no MCP tab group", call it again with `createIfEmpty: true` — that opens a fresh window for the skill to use without disturbing the operator's existing browsing.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Step 2 — Verify operator is logged in
|
|
20
|
+
|
|
21
|
+
Navigate the chosen tab to `https://software-factory-3tbbu.ondigitalocean.app/`. Take a screenshot. If the page shows `/login`, walk the operator through the SSO flow:
|
|
22
|
+
|
|
23
|
+
> "You're not logged in. Click 'Continuer avec Google' and pick your @mhosaic.com account. When you land on /, tell me 'ready'."
|
|
24
|
+
|
|
25
|
+
Wait for the operator to confirm. Then re-screenshot to verify the dashboard is showing.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Step 3 — Collect the project inputs
|
|
30
|
+
|
|
31
|
+
Use `AskUserQuestion` for **each** of these, one at a time. Always confirm before proceeding.
|
|
32
|
+
|
|
33
|
+
1. **Company:** existing or new?
|
|
34
|
+
- Use `AskUserQuestion` with options `Use existing company` and `Create new company`.
|
|
35
|
+
- If existing: fetch the list with the JS snippet in step 4 and present a multi-choice question.
|
|
36
|
+
- 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`).
|
|
37
|
+
|
|
38
|
+
2. **Project name** (plain text, e.g. `"NoRag Production"`).
|
|
39
|
+
|
|
40
|
+
3. **Project slug** (plain text, auto-slugified default from name). Must be unique within the company. Lowercase, hyphens, ≤ 80 chars.
|
|
41
|
+
|
|
42
|
+
4. **Allowed origins** (plain text, comma- or space-separated list). Each must be a full `https?://host[:port]` URL. Rules:
|
|
43
|
+
- Wildcards (`*`) are rejected.
|
|
44
|
+
- `http://` is only permitted for localhost / 127.0.0.1 / ::1.
|
|
45
|
+
- Path is forbidden (e.g. `https://app.example.com/widget` — drop the path).
|
|
46
|
+
- 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
|
+
|
|
48
|
+
5. **Share reports with widget board?** (`AskUserQuestion`, default No).
|
|
49
|
+
- `Yes — every authenticated end-user sees every report` (good for internal tools)
|
|
50
|
+
- `No — strict submitter-only view` (default, safer for public-facing apps)
|
|
51
|
+
|
|
52
|
+
6. **(Optional) Repo/staging/prod URLs** — surface in admin UI but not load-bearing. Skip unless the operator volunteers.
|
|
53
|
+
|
|
54
|
+
Display the collected values as a summary table and confirm with `AskUserQuestion` "Proceed?" before mutating anything.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 4 — Provision via the browser's fetch()
|
|
59
|
+
|
|
60
|
+
You'll execute small JavaScript snippets in the admin SPA tab via `mcp__claude-in-chrome__javascript_tool`. The SPA's session cookies authenticate the requests automatically.
|
|
61
|
+
|
|
62
|
+
### Get a fresh access token
|
|
63
|
+
|
|
64
|
+
The SPA holds its access token in memory (not localStorage — see `apps/admin/src/api/tokenStore.ts`). To get one from outside the SPA's React state, hit the cookie-backed refresh endpoint: the browser auto-sends the HttpOnly refresh cookie, and the response body returns a fresh access token.
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
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 }))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
For the rest of this procedure, treat the captured token as a shell variable `TOKEN`. Each snippet below uses `TOKEN` literally — substitute it before sending to `mcp__claude-in-chrome__javascript_tool`.
|
|
75
|
+
|
|
76
|
+
### Look up or create the company
|
|
77
|
+
|
|
78
|
+
To list companies (response wraps them in `{items: [...]}`):
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
fetch('/api/feedback/v1/companies/', {
|
|
82
|
+
headers: { 'Authorization': 'Bearer TOKEN' }
|
|
83
|
+
}).then(r => r.json()).then(d => JSON.stringify(d.items ?? d))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each item has `{id, slug, name}`. Filter client-side by slug if you're looking up an existing company.
|
|
87
|
+
|
|
88
|
+
To create a new company:
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
fetch('/api/feedback/v1/companies/', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'Authorization': 'Bearer TOKEN',
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({ name: 'COMPANY_NAME', slug: 'company-slug' })
|
|
98
|
+
}).then(r => r.json()).then(d => JSON.stringify(d))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Capture the company `id` (UUID) from the response.
|
|
102
|
+
|
|
103
|
+
### Create the project
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
fetch('/api/feedback/v1/projects/', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Authorization': 'Bearer TOKEN',
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
company: 'COMPANY_UUID',
|
|
114
|
+
name: 'PROJECT_NAME',
|
|
115
|
+
slug: 'project-slug',
|
|
116
|
+
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))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
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:
|
|
123
|
+
- `http://` in prod (use `https://`)
|
|
124
|
+
- trailing slash or path on the URL (strip it)
|
|
125
|
+
- wildcard `*` (not supported by design)
|
|
126
|
+
|
|
127
|
+
If you get a 400 with slug collision: append `-2`, `-3`, … and retry. Confirm the new slug with the operator first.
|
|
128
|
+
|
|
129
|
+
Capture the project `id` from the response.
|
|
130
|
+
|
|
131
|
+
### Mint the public widget key
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
fetch('/api/feedback/v1/project-keys/create/', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'Authorization': 'Bearer TOKEN',
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
project_id: 'PROJECT_UUID',
|
|
142
|
+
kind: 'public',
|
|
143
|
+
label: 'Production widget key — minted by /integrate-feedback'
|
|
144
|
+
})
|
|
145
|
+
}).then(r => r.json()).then(d => JSON.stringify(d))
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
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).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Step 5 — Assemble the handoff payload
|
|
153
|
+
|
|
154
|
+
Construct this Markdown block and present it to the operator as the deliverable:
|
|
155
|
+
|
|
156
|
+
````markdown
|
|
157
|
+
## Mhosaic Feedback handoff
|
|
158
|
+
|
|
159
|
+
**Project slug:** `<project-slug>`
|
|
160
|
+
**Project ID:** `<project-uuid>`
|
|
161
|
+
**Endpoint:** `https://software-factory-3tbbu.ondigitalocean.app`
|
|
162
|
+
**Public widget key:** `<pk_proj_…>`
|
|
163
|
+
**Allowed origin(s):** `<origin1>`, `<origin2>`, …
|
|
164
|
+
**Reports visibility:** `<submitter-only | shared with widget board>`
|
|
165
|
+
|
|
166
|
+
### Send to your teammate
|
|
167
|
+
|
|
168
|
+
To install:
|
|
169
|
+
```bash
|
|
170
|
+
npx @mhosaic/feedback-cli@latest init \
|
|
171
|
+
--api-key <pk_proj_…> \
|
|
172
|
+
--endpoint https://software-factory-3tbbu.ondigitalocean.app \
|
|
173
|
+
--yes
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
To verify after install:
|
|
177
|
+
```bash
|
|
178
|
+
npx @mhosaic/feedback-cli@latest verify \
|
|
179
|
+
--origin <consumer dev origin> \
|
|
180
|
+
--with-test-report
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or guide them with Claude Code:
|
|
184
|
+
```
|
|
185
|
+
/integrate-feedback
|
|
186
|
+
```
|
|
187
|
+
(then paste the block above as their first message)
|
|
188
|
+
````
|
|
189
|
+
|
|
190
|
+
Print this to the chat.
|
|
191
|
+
|
|
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.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Step 6 — Verify the project really exists
|
|
197
|
+
|
|
198
|
+
Drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/companies/<company-slug>` and screenshot. The operator should see the project listed. Then drive to `/settings/widget-keys` and confirm the key shows up there (masked — the plaintext is gone after this point).
|
|
199
|
+
|
|
200
|
+
If the project doesn't appear: something silently failed (browser cache, deploy mid-rollout, race condition). Open browser DevTools network tab via `mcp__claude-in-chrome__read_network_requests` filtered to `/api/feedback/v1/projects/` and inspect the actual POST response. Surface the diagnostic to the operator and stop.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Step 7 — Offer to continue into consumer mode
|
|
205
|
+
|
|
206
|
+
`AskUserQuestion`:
|
|
207
|
+
- question: `Want to install the widget into a consumer app right now?`
|
|
208
|
+
- 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.`
|
|
211
|
+
|
|
212
|
+
If yes, follow `references/consumer-install.md` from step 1, pre-filling the handoff payload values.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Rollback / undo
|
|
217
|
+
|
|
218
|
+
If the operator wants to undo what was just provisioned:
|
|
219
|
+
|
|
220
|
+
1. **Revoke the key** — admin SPA at `/settings/widget-keys` → find the row → click "Révoquer". This breaks the widget immediately for anyone using it.
|
|
221
|
+
2. **Delete the project** — currently no UI button; instruct the operator to use the DO Console → backend pod → `python manage.py shell -c "from mhosaic_feedback.models import Project; Project.objects.filter(slug='<slug>').delete()"`. This cascades to keys, reports, etc. — only do this if no real reports have been created against the project.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Error catalog
|
|
226
|
+
|
|
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 |
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Verify install — live smoke test
|
|
2
|
+
|
|
3
|
+
After the consumer has installed the widget, run this end-to-end smoke test to confirm everything actually works. Don't claim "done" until this is green.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Run the CLI verify
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @mhosaic/feedback-cli@latest verify \
|
|
11
|
+
--origin <consumer-dev-origin> \
|
|
12
|
+
--with-test-report
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`<consumer-dev-origin>` is the URL where the consumer's app runs locally — `http://localhost:5173` for Vite, `http://localhost:3000` for Next/Remix, etc.
|
|
16
|
+
|
|
17
|
+
Expected output (all green):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
✓ .env.local exists with both keys
|
|
21
|
+
✓ API key starts with pk_proj_
|
|
22
|
+
✓ Endpoint is a valid http(s) URL
|
|
23
|
+
✓ Endpoint reachable over network
|
|
24
|
+
✓ CORS allows POST from <origin>
|
|
25
|
+
✓ POST /api/feedback/v1/reports/ accepts the public key (submitted (report id: …) — delete it from /reports if you don't want noise)
|
|
26
|
+
|
|
27
|
+
All checks passed.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If any check is red, **STOP**. Don't proceed to step 2 until they're all green. The hint column tells you what to fix; the diagnostic table at the bottom of this doc covers every failure mode.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Step 2 — Start the consumer's dev server
|
|
35
|
+
|
|
36
|
+
In the consumer's repo:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm dev # or npm run dev / yarn dev / bun dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Wait for the server to print its URL (e.g. `Local: http://localhost:5173`).
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Step 3 — Drive Chrome to the dev URL
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
mcp__claude-in-chrome__tabs_create_mcp # or reuse an existing tab
|
|
50
|
+
mcp__claude-in-chrome__navigate(tabId, url=<dev-url>)
|
|
51
|
+
mcp__claude-in-chrome__computer(action="screenshot")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Inspect the screenshot. You should see:
|
|
55
|
+
|
|
56
|
+
1. The consumer's actual UI rendered (their app loaded successfully)
|
|
57
|
+
2. A small floating action button (FAB) in the bottom-right corner — a circle with a chat-bubble or megaphone icon
|
|
58
|
+
|
|
59
|
+
If the FAB is missing, see the diagnostic table at the bottom.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Step 4 — Open DevTools console; verify no widget errors
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
mcp__claude-in-chrome__read_console_messages(pattern="mhosaic|feedback|widget")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Acceptable: a "widget mounted" or "init complete" debug line. Not acceptable: any `error` or `unhandledrejection` mentioning `@mhosaic/feedback`, `pk_proj_`, CORS, or fingerprint.
|
|
70
|
+
|
|
71
|
+
If errors appear, surface them to the user before clicking the FAB — they may already explain a failure that the visual test wouldn't catch.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Step 5 — Click the FAB and submit a test report
|
|
76
|
+
|
|
77
|
+
Use `mcp__claude-in-chrome__find(query="feedback FAB or feedback button")` to locate the element. Click it.
|
|
78
|
+
|
|
79
|
+
A modal/sheet should open inviting the user to write a description. Fill in:
|
|
80
|
+
|
|
81
|
+
> `[INTEGRATE-FEEDBACK SMOKE TEST] integration verified at <ISO timestamp>`
|
|
82
|
+
|
|
83
|
+
…and submit. The widget should close itself + show a success toast.
|
|
84
|
+
|
|
85
|
+
If the form rejects the submit with a 4xx error, capture the network response via:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
mcp__claude-in-chrome__read_network_requests(urlPattern="/api/feedback/v1/reports/")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
and surface the status code + response body.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Step 6 — Confirm the report landed in admin
|
|
96
|
+
|
|
97
|
+
Drive Chrome to `https://software-factory-3tbbu.ondigitalocean.app/reports`. You may need to log in if the operator's session expired.
|
|
98
|
+
|
|
99
|
+
Filter by description containing `[INTEGRATE-FEEDBACK SMOKE TEST]`. The new report should appear at the top of the list.
|
|
100
|
+
|
|
101
|
+
Click into it. Verify:
|
|
102
|
+
- Project = the project the operator just provisioned
|
|
103
|
+
- Submitter = whatever identity was wired (anonymous-id, real user, etc.)
|
|
104
|
+
- Page URL = the consumer's dev URL
|
|
105
|
+
- Status = new
|
|
106
|
+
|
|
107
|
+
Screenshot this to give the user proof-of-life.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Step 7 — (Optional cleanup) delete the smoke-test reports
|
|
112
|
+
|
|
113
|
+
After confirming, the operator (not the consumer) can clean up the noise:
|
|
114
|
+
|
|
115
|
+
1. Admin SPA `/reports`, filter by description containing `[INTEGRATE-FEEDBACK SMOKE TEST]` and `[mhosaic-feedback verify]`
|
|
116
|
+
2. Bulk select and delete
|
|
117
|
+
|
|
118
|
+
Skip this step if the consumer wants to keep the smoke-test reports as a paper trail.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Diagnostic table — when checks fail
|
|
123
|
+
|
|
124
|
+
| Symptom | Likely cause | Fix |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `✗ .env.local exists with both keys` | CLI init never ran, or ran in a different directory | Re-run `mhosaic-feedback init` from the project root |
|
|
127
|
+
| `✗ API key starts with pk_proj_` | Operator pasted the wrong key (e.g. `sk_proj_…`, an MCP key) | Operator mints a *public* widget key (`kind: "public"`) in admin SPA |
|
|
128
|
+
| `✗ Endpoint reachable over network` (network error) | DNS, firewall, offline, or wrong endpoint URL | Verify the URL in `.env.local`; try `curl -I <endpoint>/admin/login/` |
|
|
129
|
+
| `✗ Endpoint reachable` (5xx) | Backend is down or mid-deploy | Wait 1–2 minutes; check DO App Platform deploy status |
|
|
130
|
+
| `✗ CORS allows POST from <origin>` | Operator forgot to add this origin to `allowed_origins` | Operator opens admin SPA → Edit project → adds the dev URL (e.g. `http://localhost:5173`) |
|
|
131
|
+
| `✗ POST /api/feedback/v1/reports/` returns 401 | Key invalid or revoked | Operator mints a new key |
|
|
132
|
+
| `✗ POST /api/feedback/v1/reports/` returns 403 with "origin not allowed" | Same as CORS row above | Operator adds the origin |
|
|
133
|
+
| FAB doesn't render in dev | `identify()` not called with a non-empty `id` | Wire identify per `identify-snippets.md`, OR for testing pass `{id: 'test-user'}` |
|
|
134
|
+
| FAB renders but form submit fails with 403 | Origin not in `allowed_origins` (Vite dev URL differs from prod URL) | Operator adds the dev URL |
|
|
135
|
+
| FAB renders but form submit fails with 400 | Serializer rejected the payload (description empty, env unknown, etc.) | Read DevTools network response — usually a clear field-level error |
|
|
136
|
+
| Console error: `Failed to load module` for `@mhosaic/feedback` | Vite cache mismatch | Stop dev server, delete `node_modules/.vite`, restart |
|
|
137
|
+
| Console error: `Unhandled CORS preflight failure` | Origin not allowlisted on the backend side | Operator adds the origin; consumer hard-reloads |
|
|
138
|
+
| Widget submits anonymous reports despite identify() running | identify() called in a stale closure (effect dep array missing the user) | Re-read identify-snippets.md — every snippet has the right deps |
|
|
139
|
+
| `verify --with-test-report` succeeds but the report doesn't appear in admin | Browser cache; report is filtered by default (e.g. `synthetic=true` hides it) | Refresh admin /reports; toggle "show synthetic" filter |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## When to escalate
|
|
144
|
+
|
|
145
|
+
If after 3 fix-iterations the smoke test still fails, escalate to the operator with:
|
|
146
|
+
|
|
147
|
+
1. The CLI verify output (full text)
|
|
148
|
+
2. A DevTools network request showing the actual server response
|
|
149
|
+
3. A screenshot of the dev UI showing FAB state
|
|
150
|
+
|
|
151
|
+
The operator can correlate the failure against the project's `allowed_origins`, key status, and `share_reports_with_widget` flag in the admin SPA — that catches >90% of "weird" failures the consumer can't debug on their own.
|
|
@@ -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"]}
|