@polishd/next 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/AGENTS.md +161 -46
  2. package/CHANGELOG.md +128 -0
  3. package/README.md +238 -48
  4. package/bin/doctor.mjs +374 -0
  5. package/bin/init.mjs +215 -188
  6. package/bin/polishd.mjs +85 -0
  7. package/bin/shared.mjs +114 -0
  8. package/dist/ai/actions.d.ts +7 -2
  9. package/dist/ai/actions.d.ts.map +1 -1
  10. package/dist/ai/actions.js +9 -1
  11. package/dist/ai/actions.js.map +1 -1
  12. package/dist/ai/guard.d.ts +44 -3
  13. package/dist/ai/guard.d.ts.map +1 -1
  14. package/dist/ai/guard.js +13 -2
  15. package/dist/ai/guard.js.map +1 -1
  16. package/dist/ai/providers.d.ts +2 -0
  17. package/dist/ai/providers.d.ts.map +1 -1
  18. package/dist/ai/providers.js +70 -0
  19. package/dist/ai/providers.js.map +1 -1
  20. package/dist/ai/settings.d.ts +15 -1
  21. package/dist/ai/settings.d.ts.map +1 -1
  22. package/dist/ai/settings.js +38 -1
  23. package/dist/ai/settings.js.map +1 -1
  24. package/dist/ai/types.d.ts +10 -0
  25. package/dist/ai/types.d.ts.map +1 -1
  26. package/dist/client/init.d.ts.map +1 -1
  27. package/dist/client/init.js +51 -2
  28. package/dist/client/init.js.map +1 -1
  29. package/dist/config.d.ts +57 -3
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +62 -3
  32. package/dist/config.js.map +1 -1
  33. package/dist/dashboard/index.d.ts +34 -5
  34. package/dist/dashboard/index.d.ts.map +1 -1
  35. package/dist/dashboard/index.js +145 -17
  36. package/dist/dashboard/index.js.map +1 -1
  37. package/dist/dashboard/summary.d.ts.map +1 -1
  38. package/dist/dashboard/summary.js +59 -5
  39. package/dist/dashboard/summary.js.map +1 -1
  40. package/dist/dashboard/token-auth.d.ts +56 -0
  41. package/dist/dashboard/token-auth.d.ts.map +1 -0
  42. package/dist/dashboard/token-auth.js +158 -0
  43. package/dist/dashboard/token-auth.js.map +1 -0
  44. package/dist/dashboard/unlock.d.ts +2 -0
  45. package/dist/dashboard/unlock.d.ts.map +1 -0
  46. package/dist/dashboard/unlock.js +54 -0
  47. package/dist/dashboard/unlock.js.map +1 -0
  48. package/dist/dashboard.css +2601 -0
  49. package/dist/proxy.d.ts +53 -5
  50. package/dist/proxy.d.ts.map +1 -1
  51. package/dist/proxy.js +73 -3
  52. package/dist/proxy.js.map +1 -1
  53. package/dist/route.d.ts.map +1 -1
  54. package/dist/route.js +11 -3
  55. package/dist/route.js.map +1 -1
  56. package/dist/server/ingest.d.ts +33 -2
  57. package/dist/server/ingest.d.ts.map +1 -1
  58. package/dist/server/ingest.js +61 -7
  59. package/dist/server/ingest.js.map +1 -1
  60. package/dist/server/queries.d.ts +16 -0
  61. package/dist/server/queries.d.ts.map +1 -1
  62. package/dist/server/queries.js +80 -16
  63. package/dist/server/queries.js.map +1 -1
  64. package/dist/session.d.ts.map +1 -1
  65. package/dist/session.js +4 -2
  66. package/dist/session.js.map +1 -1
  67. package/docs/SETUP.md +308 -51
  68. package/package.json +14 -7
package/README.md CHANGED
@@ -13,6 +13,32 @@ one httpOnly UUID cookie per session, no fingerprinting, no PII.
13
13
 
14
14
  ---
15
15
 
16
+ ## Installing with a coding agent
17
+
18
+ Polishd ships a machine-readable install procedure. If you use Claude Code,
19
+ Cursor, Copilot, or any other coding agent, **paste this prompt** and let it do
20
+ the whole install:
21
+
22
+ ```text
23
+ Install @polishd/next into this Next.js app.
24
+
25
+ Follow the procedure in AGENTS.md from the package exactly — after installing,
26
+ read node_modules/@polishd/next/AGENTS.md. If the package isn't installed yet,
27
+ read it at https://github.com/muskanvaswan/polishd/blob/main/AGENTS.md
28
+
29
+ Work through its phases in order and do not skip Phase 7 (verification).
30
+ Several of its constraints fail silently at runtime rather than at build time,
31
+ so a passing build does not mean the install worked.
32
+
33
+ Finish by running `npx @polishd/next doctor` and fixing everything it reports.
34
+ ```
35
+
36
+ `npx @polishd/next doctor` is the ground truth: it checks the wiring that fails
37
+ quietly — the proxy named for the wrong Next major, route segment config
38
+ re-exported instead of declared, a matcher that isn't an inline literal, the
39
+ dashboard stylesheet not imported. Add `--url http://localhost:3000` to also
40
+ verify live that the session cookie is minted and events actually store.
41
+
16
42
  ## Documentation
17
43
 
18
44
  | | |
@@ -20,27 +46,36 @@ one httpOnly UUID cookie per session, no fingerprinting, no PII.
20
46
  | **[docs/SETUP.md](./docs/SETUP.md)** | Full walkthrough — every variant, verification, troubleshooting |
21
47
  | **[AGENTS.md](./AGENTS.md)** | Install procedure written for AI coding agents |
22
48
  | **[DATABASE.md](./DATABASE.md)** | Production database setup, schema, retention |
49
+ | **[CHANGELOG.md](./CHANGELOG.md)** | Release notes — **start here if you're upgrading from 0.1.x** |
23
50
 
24
51
  ## Requirements
25
52
 
26
53
  - Next.js **15+**, App Router (no Pages Router support)
27
54
  - Node **22.5+** (the dev store uses the built-in `node:sqlite`)
28
- - Tailwind CSS v3 or v4, if you want the dashboard styled — see [Styling](#styling-required)
29
55
  - Postgres in production; nothing extra for local development
30
56
 
31
57
  ## Quickstart
32
58
 
33
59
  ```bash
34
60
  npm install @polishd/next
35
- npx polishd init
36
- npm run dev # then visit /polishd
61
+ npx @polishd/next init
62
+ npm run dev # then visit /polishd
63
+ npx @polishd/next doctor # confirm the wiring
37
64
  ```
38
65
 
66
+ (`npx polishd init` is the shorter form once the package is installed;
67
+ `npx @polishd/next init` also works before it is.)
68
+
39
69
  That's it locally — events write to `.polishd/analytics.db` with no
40
- configuration. `init` detects your layout (`src/` or root), writes the four
41
- files below, adds `.polishd/` to `.gitignore`, wires Tailwind, and skips
42
- anything that already exists (`--force` to overwrite, `--dry-run` to preview,
43
- `--js` for JavaScript, `--config` to also emit a `polishd.config.ts`).
70
+ configuration. `init` detects your layout (`src/` or root), detects your
71
+ installed Next major to name the proxy file correctly, writes the four files
72
+ below, and adds `.polishd/` to `.gitignore`.
73
+
74
+ Files it already wrote are skipped unless you pass `--force`, which backs them
75
+ up to `.bak` first. **Files it did not write are never overwritten**, with or
76
+ without `--force` — if you already have a proxy, it prints how to merge instead
77
+ of clobbering your code. Other flags: `--dry-run` to preview, `--js` for
78
+ JavaScript, `--config` to also emit a `polishd.config.ts`.
44
79
 
45
80
  `pg` installs automatically as an optional dependency and is loaded only when
46
81
  you point Polishd at a Postgres URL.
@@ -70,11 +105,12 @@ export const runtime = "nodejs"; // must be declared here, not re-exporte
70
105
  export const dynamic = "force-dynamic";
71
106
  export const POST = createPolishdRoute();
72
107
 
73
- // src/app/polishd/page.tsx — the dashboard (unguarded by default)
108
+ // src/app/polishd/page.tsx — the dashboard
109
+ import "@polishd/next/dashboard.css"; // self-contained; no Tailwind needed
74
110
  import { createPolishdPage } from "@polishd/next/dashboard";
75
111
  export const runtime = "nodejs"; // Next requires these inline in the page
76
112
  export const dynamic = "force-dynamic";
77
- export default createPolishdPage();
113
+ export default createPolishdPage(); // see Protecting the dashboard
78
114
  ```
79
115
 
80
116
  > **Route segment config can't be re-exported.** Next statically parses
@@ -88,35 +124,127 @@ export default createPolishdPage();
88
124
  > under Next 16 + Turbopack, a proxy matcher that touches any `/api/*` route
89
125
  > breaks resolution for the entire `/api` segment.
90
126
 
91
- ## Styling (required)
127
+ ## Composing with an existing proxy
92
128
 
93
- The dashboard is built from Tailwind utility classes and ships **no stylesheet
94
- of its own**, so Tailwind has to scan the package's `dist` — it won't find it by
95
- default. `init` wires this up for you; if you're doing it by hand:
129
+ Next allows exactly **one** proxy module per app (`proxy.ts` on Next 16+,
130
+ `middleware.ts` on Next 15), so if you already have one, Polishd has to go
131
+ inside it. `init` detects this and prints instructions rather than overwriting.
96
132
 
97
- ```css
98
- /* Tailwind v4 — in the CSS file that imports tailwind */
99
- @import "tailwindcss";
100
- @source "../../node_modules/@polishd/next/dist"; /* relative to this file */
101
- ```
133
+ The subtle part is the matcher. One file means one `config.matcher`, so adding
134
+ Polishd means widening it to the union — and Polishd's matcher is near-total.
135
+ Your handler, which you deliberately scoped to a couple of routes, would
136
+ silently start running on every request in the app. Nothing errors; your
137
+ redirects and rewrites just begin firing where they never did before.
138
+
139
+ Declare your original scope and `composePolishd` re-applies it as an internal
140
+ guard:
102
141
 
103
142
  ```ts
104
- /* Tailwind v3 — in tailwind.config.ts */
105
- content: [
106
- "./src/**/*.{ts,tsx}",
107
- "./node_modules/@polishd/next/dist/**/*.js",
108
- ],
143
+ // proxy.ts (middleware.ts on Next 15, with the function named `middleware`)
144
+ import { composePolishd } from "@polishd/next/proxy";
145
+ import type { NextRequest } from "next/server";
146
+
147
+ function myProxy(request: NextRequest) {
148
+ if (!hasSession(request)) return NextResponse.redirect(new URL("/login", request.url));
149
+ }
150
+
151
+ // Your handler still only runs on /admin/*, even though the matcher below
152
+ // now feeds Polishd every page request.
153
+ export const proxy = composePolishd(myProxy, { matcher: "/admin/:path*" });
154
+
155
+ // Written by hand: Next statically parses this and it must be an inline
156
+ // literal, so no helper can generate it. The union of your paths and Polishd's.
157
+ export const config = {
158
+ matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
159
+ };
160
+ ```
161
+
162
+ Your handler runs first and its response is threaded through, so a redirect or
163
+ rewrite you return **carries the analytics cookie** rather than losing it.
164
+ Returning nothing is fine. `matcher` accepts Next's matcher strings
165
+ (`"/admin/:path*"`, `"/((?!api).*)"`) or a `RegExp` when you want to be exact;
166
+ omit it only if your handler genuinely did run on everything.
167
+
168
+ Widening the matcher is safe from Polishd's side: `withPolishdSession` returns
169
+ early once the cookie is present, so the extra paths cost a cookie lookup and
170
+ nothing else.
171
+
172
+ ## Styling
173
+
174
+ The dashboard ships its own stylesheet. One import, no configuration:
175
+
176
+ ```tsx
177
+ // app/polishd/page.tsx — `init` writes this for you
178
+ import "@polishd/next/dashboard.css";
109
179
  ```
110
180
 
111
- Skip this and `/polishd` renders as unstyled HTML. Tailwind v4's automatic content
112
- detection deliberately ignores `node_modules`, so the `@source` line is not
113
- optional.
181
+ **Tailwind is not required in your app.** The stylesheet is compiled at publish
182
+ time, so it doesn't depend on your build setup, your Tailwind version, or
183
+ whether you use Tailwind at all.
184
+
185
+ It is built not to interfere with the app it lands in:
186
+
187
+ - **No global preflight.** Tailwind's reset is what would restyle every element
188
+ on your page; it's left out. A scoped equivalent applies to the dashboard's
189
+ own subtree instead, so your headings, lists and buttons are untouched.
190
+ - **Every rule is scoped to `.polishd-root`**, verified at build time — nothing
191
+ in the file can match your markup.
192
+ - **The scope is doubled (`.polishd-root.polishd-root`)** so the dashboard wins
193
+ specificity ties against a host that happens to have its own `.flex`,
194
+ `.hidden` or `.container`, rather than the winner depending on stylesheet
195
+ order.
196
+ - **No cascade layers**, because unlayered host CSS beats layered CSS outright —
197
+ a plain `h1 { color: … }` in your app would otherwise override the dashboard's
198
+ own typography.
199
+
200
+ The one thing it can't win is a host `!important` rule aimed at the same
201
+ property. Nothing short of `!important` on our side would, and a package
202
+ shouting over its host is worse than a rare collision.
203
+
204
+ > **Upgrading from 0.1.x?** Nothing to do — your existing `@source
205
+ > "…/@polishd/next/dist"` keeps compiling the dashboard's utilities exactly as
206
+ > before, so the dashboard stays styled whether or not you add the import.
207
+ >
208
+ > To move over at your own pace, **add the import first, then** delete the
209
+ > `@source` line — in that order, so the dashboard is never unstyled in between.
210
+ > `polishd doctor` reports which setup it finds and won't call a working 0.1.x
211
+ > install broken.
114
212
 
115
213
  ## Protecting the dashboard
116
214
 
117
- By default `/polishd` is **public** (a dev-only console warning reminds you).
118
- Gate it with an `authenticate` callback, and optionally render your own sign-in
119
- UI when it fails:
215
+ The dashboard is not a read-only report. Its settings change the configured
216
+ model and **API base URL**, so anyone who can reach it can point your requests
217
+ at their own endpoint and spend your key. Treat it as a credential-adjacent
218
+ surface.
219
+
220
+ In **development** it is open and says so once in the console. In
221
+ **production** Polishd refuses to serve it until you have made a decision.
222
+ There are three ways to make one.
223
+
224
+ ### 1. A token — no code, works with no auth of your own
225
+
226
+ ```bash
227
+ POLISHD_DASHBOARD_TOKEN=$(openssl rand -hex 32)
228
+ ```
229
+
230
+ That is the whole integration. The locked screen shows a password field; the
231
+ token is exchanged for an httpOnly, path-scoped cookie by a server action, so
232
+ it never appears in a URL, in browser history, in server logs, or in a
233
+ `Referer` header. The cookie holds an HMAC of the token rather than the token
234
+ itself, comparison is constant-time, expiry is enforced server-side, and the
235
+ unlock form is rate-limited per IP.
236
+
237
+ Tune it if you moved the dashboard off `/polishd`:
238
+
239
+ ```tsx
240
+ import { createPolishdPage, polishdTokenAuth } from "@polishd/next/dashboard";
241
+
242
+ export default createPolishdPage({
243
+ authenticate: polishdTokenAuth({ basePath: "/admin/polishd" }),
244
+ });
245
+ ```
246
+
247
+ ### 2. Your app's own auth
120
248
 
121
249
  ```tsx
122
250
  import { createPolishdPage } from "@polishd/next/dashboard";
@@ -127,21 +255,37 @@ export const runtime = "nodejs";
127
255
  export const dynamic = "force-dynamic";
128
256
 
129
257
  export default createPolishdPage({
130
- authenticate: isAuthenticated, // () => boolean | Promise<boolean>
131
- unauthorized: <MyLogin />, // optional; a minimal screen otherwise
258
+ authenticate: isAuthenticated, // (ctx) => boolean | Promise<boolean>
259
+ unauthorized: <MyLogin />, // optional; a minimal screen otherwise
132
260
  });
133
261
  ```
134
262
 
135
- The same callback also guards the AI **server actions** (generate summary, save
136
- settings, scan codebase, file GitHub issues). That matters: a server action is
137
- an addressable POST endpoint whose id ships in the public client bundle, so
138
- gating only the page would leave those actions callable by anyone. They re-run
139
- `authenticate` against the caller's cookies on every call, and deny when no
140
- dashboard page has registered a policy.
263
+ `authenticate` receives the request's `cookies` and `headers`, so it is an
264
+ ordinary function of its inputs and can be unit tested without a live request:
265
+
266
+ ```ts
267
+ const authenticate = (ctx) => ctx.cookies.get("session")?.value === expected;
268
+ ```
269
+
270
+ A zero-argument callback still works unchanged.
271
+
272
+ ### 3. Public on purpose
273
+
274
+ ```bash
275
+ POLISHD_DASHBOARD_PUBLIC=true
276
+ ```
277
+
278
+ An explicit, grep-able opt-in. You will still get one console warning per boot,
279
+ in production, because that is the only place it is worth acting on.
141
280
 
142
- If you pass no `authenticate`, the dashboard **and** its actions are public —
143
- anyone who finds the URL can read your analytics, change the configured model
144
- and API base URL, and spend your model tokens. Always gate it in production.
281
+ > **The gate covers the server actions, not just the page.** The AI actions
282
+ > (generate summary, save settings, scan codebase, file GitHub issues) are
283
+ > independently addressable POST endpoints whose ids ship in the public client
284
+ > bundle, so gating only the page would leave them callable by anyone. They
285
+ > re-run your check against the caller's cookies on every call, and deny when
286
+ > no dashboard page has registered a policy — including when you delete
287
+ > `app/polishd/page.tsx` but keep the package, in which case the actions stay
288
+ > routable and always deny.
145
289
 
146
290
  ## AI summary
147
291
 
@@ -236,8 +380,7 @@ import { PolishdMonitor } from "@polishd/next/client";
236
380
 
237
381
  ## Configuration
238
382
 
239
- Defaults suit a low-traffic site. Override via `polishd.config.ts` and pass it to
240
- `initPolishd`:
383
+ Defaults suit a low-traffic site. Override via `polishd.config.ts`:
241
384
 
242
385
  ```ts
243
386
  // polishd.config.ts
@@ -248,11 +391,32 @@ export default definePolishdConfig({
248
391
  });
249
392
  ```
250
393
 
394
+ The dashboard never measures itself: nothing on `/polishd` (or below it) is
395
+ captured, so reading your analytics can't show up as your site's most-used
396
+ feature. Moved the dashboard elsewhere? Set `dashboardRoute`.
397
+
398
+ > **Nothing imports this file for you.** It cannot be auto-loaded: the three
399
+ > places that read config run in three different runtimes — the browser, the
400
+ > Edge proxy, and the Node route handler — and the Edge one has no filesystem.
401
+ > Pass it to each explicitly:
402
+
251
403
  ```ts
252
- // src/instrumentation-client.ts
253
- import { initPolishd } from "@polishd/next/client";
254
404
  import config from "../polishd.config";
255
- initPolishd(config);
405
+
406
+ initPolishd(config); // instrumentation-client.ts
407
+ export const POST = createPolishdRoute(config); // app/api/polishd/route.ts
408
+ const { proxy } = createPolishdProxy(config); // proxy.ts
409
+ ```
410
+
411
+ Thread it into one but not another and the layers disagree silently — a proxy
412
+ minting cookie `a` while ingest looks for `b` produces exactly the empty
413
+ dashboard described below.
414
+
415
+ For **`sessionCookie` specifically, prefer the environment variable**. It is the
416
+ one channel every runtime can read, so it cannot go half-applied:
417
+
418
+ ```bash
419
+ POLISHD_SESSION_COOKIE=my_session
256
420
  ```
257
421
 
258
422
  ## Environment variables
@@ -261,6 +425,9 @@ initPolishd(config);
261
425
  |---|---|---|
262
426
  | `POLISHD_DATABASE_URL` | production | Pooled Postgres connection string — enables capture |
263
427
  | `POLISHD_DB_PATH` | dev (optional) | Custom SQLite path (default `.polishd/analytics.db`) |
428
+ | `POLISHD_DASHBOARD_TOKEN` | production | Protects `/polishd` with the built-in gate. Generate with `openssl rand -hex 32` |
429
+ | `POLISHD_DASHBOARD_PUBLIC` | production (optional) | `true` — serve the dashboard unguarded on purpose |
430
+ | `POLISHD_SESSION_COOKIE` | optional | Rename the anonymous session cookie. Read by both the proxy and ingest, so they cannot disagree |
264
431
  | `POLISHD_AI_PROVIDER` | AI (optional) | `anthropic` \| `openai` \| `openai-compatible` \| `google` |
265
432
  | `POLISHD_AI_MODEL` | AI (optional) | Model id (defaults per provider, e.g. `claude-opus-4-8`) |
266
433
  | `POLISHD_AI_API_KEY` | AI (optional) | Model API key — preset instead of using the dashboard |
@@ -280,16 +447,39 @@ Local dev needs nothing. If no writable store is available the package degrades
280
447
  to a safe no-op (a console warning, dashboard notice) — your app never breaks.
281
448
  See [DATABASE.md](./DATABASE.md) for production setup.
282
449
 
450
+ ## Troubleshooting
451
+
452
+ Run `npx @polishd/next doctor` first — it catches most of these by name, and
453
+ `--url http://localhost:3000` checks the live path as well.
454
+
455
+ | Symptom | Cause | Fix |
456
+ |---|---|---|
457
+ | Dashboard empty, beacons all return 200 | The proxy isn't running, so no session cookie is minted and ingest drops every batch | Check the file is named for your Next major — `proxy.ts` on 16+, `middleware.ts` on 15 — and exports a function of the same name. The dashboard shows a banner when it detects this |
458
+ | Every `/api/*` route 404s | The proxy matcher touches `/api` | Restore the `(?!api…)` exclusion. Under Next 16 + Turbopack this breaks the whole `/api` segment |
459
+ | Build fails: *"can't recognize the exported `runtime` field"* | Route segment config re-exported | Declare `runtime`/`dynamic` inline in the module |
460
+ | Dashboard renders unstyled | `@polishd/next/dashboard.css` isn't imported | See [Styling](#styling) |
461
+ | Dashboard content cut off below the fold | A host `body` that doesn't scroll | Fixed by default; if an ancestor sets `transform`/`filter`/`contain`, pass `shell: false` and see [docs/SETUP.md](./docs/SETUP.md) |
462
+ | `ExperimentalWarning: SQLite is an experimental feature` | Node's built-in `node:sqlite` | **Expected and harmless.** Node prints this for its own SQLite module on 22.x. Not a sign the package is unstable, and it disappears in production where Postgres is used |
463
+ | `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite` | Node < 22.5 | Upgrade Node, or configure Postgres |
464
+ | Server actions fail with an opaque digest | No dashboard page registered a policy | Expected if you deleted `app/polishd/page.tsx` — the actions fail closed by design |
465
+
283
466
  ## Entry points
284
467
 
285
468
  | Import | Contents |
286
469
  |---|---|
287
- | `@polishd/next` | config, `definePolishdConfig`, event types (isomorphic) |
470
+ | `@polishd/next` | config, `definePolishdConfig`, `resolveSessionCookie`, event types (isomorphic) |
288
471
  | `@polishd/next/client` | `initPolishd`, `PolishdMonitor` |
289
472
  | `@polishd/next/server` | store, ingest, queries, `withPolishdSession` (Node only) |
290
473
  | `@polishd/next/route` | `POST`, `createPolishdRoute` |
291
- | `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher` |
292
- | `@polishd/next/dashboard` | `createPolishdPage`, `PolishdDashboard`, `loadPolishdDashboardData` |
474
+ | `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher`, `composePolishd` |
475
+ | `@polishd/next/dashboard` | `createPolishdPage`, `polishdTokenAuth`, `PolishdDashboard`, `loadPolishdDashboardData` |
476
+
477
+ ## CLI
478
+
479
+ | Command | Purpose |
480
+ |---|---|
481
+ | `npx @polishd/next init` | Scaffold the glue files. `--dry-run`, `--force`, `--js`, `--config` |
482
+ | `npx @polishd/next doctor` | Check an install. `--url <origin>` adds live checks |
293
483
 
294
484
  ## Development
295
485