@polishd/next 0.1.0 → 0.2.1-dev.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.
- package/AGENTS.md +161 -46
- package/CHANGELOG.md +163 -0
- package/README.md +275 -49
- package/bin/doctor.mjs +374 -0
- package/bin/init.mjs +215 -188
- package/bin/polishd.mjs +85 -0
- package/bin/shared.mjs +114 -0
- package/dist/ai/actions.d.ts +13 -2
- package/dist/ai/actions.d.ts.map +1 -1
- package/dist/ai/actions.js +19 -1
- package/dist/ai/actions.js.map +1 -1
- package/dist/ai/design.d.ts +28 -0
- package/dist/ai/design.d.ts.map +1 -0
- package/dist/ai/design.js +245 -0
- package/dist/ai/design.js.map +1 -0
- package/dist/ai/guard.d.ts +44 -3
- package/dist/ai/guard.d.ts.map +1 -1
- package/dist/ai/guard.js +13 -2
- package/dist/ai/guard.js.map +1 -1
- package/dist/ai/providers.d.ts +2 -0
- package/dist/ai/providers.d.ts.map +1 -1
- package/dist/ai/providers.js +70 -0
- package/dist/ai/providers.js.map +1 -1
- package/dist/ai/settings.d.ts +15 -1
- package/dist/ai/settings.d.ts.map +1 -1
- package/dist/ai/settings.js +38 -1
- package/dist/ai/settings.js.map +1 -1
- package/dist/ai/types.d.ts +57 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/client/design-scan.d.ts +37 -0
- package/dist/client/design-scan.d.ts.map +1 -0
- package/dist/client/design-scan.js +319 -0
- package/dist/client/design-scan.js.map +1 -0
- package/dist/client/init.d.ts.map +1 -1
- package/dist/client/init.js +67 -2
- package/dist/client/init.js.map +1 -1
- package/dist/config.d.ts +57 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +62 -3
- package/dist/config.js.map +1 -1
- package/dist/dashboard/design-review-card.d.ts +15 -0
- package/dist/dashboard/design-review-card.d.ts.map +1 -0
- package/dist/dashboard/design-review-card.js +82 -0
- package/dist/dashboard/design-review-card.js.map +1 -0
- package/dist/dashboard/design.d.ts +10 -0
- package/dist/dashboard/design.d.ts.map +1 -0
- package/dist/dashboard/design.js +83 -0
- package/dist/dashboard/design.js.map +1 -0
- package/dist/dashboard/index.d.ts +35 -5
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +176 -18
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/journeys.d.ts.map +1 -1
- package/dist/dashboard/journeys.js +1 -0
- package/dist/dashboard/journeys.js.map +1 -1
- package/dist/dashboard/summary.d.ts.map +1 -1
- package/dist/dashboard/summary.js +59 -5
- package/dist/dashboard/summary.js.map +1 -1
- package/dist/dashboard/token-auth.d.ts +56 -0
- package/dist/dashboard/token-auth.d.ts.map +1 -0
- package/dist/dashboard/token-auth.js +158 -0
- package/dist/dashboard/token-auth.js.map +1 -0
- package/dist/dashboard/unlock.d.ts +2 -0
- package/dist/dashboard/unlock.d.ts.map +1 -0
- package/dist/dashboard/unlock.js +54 -0
- package/dist/dashboard/unlock.js.map +1 -0
- package/dist/dashboard.css +2740 -0
- package/dist/proxy.d.ts +53 -5
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +73 -3
- package/dist/proxy.js.map +1 -1
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js +11 -3
- package/dist/route.js.map +1 -1
- package/dist/server/design.d.ts +93 -0
- package/dist/server/design.d.ts.map +1 -0
- package/dist/server/design.js +430 -0
- package/dist/server/design.js.map +1 -0
- package/dist/server/ingest.d.ts +33 -2
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +70 -8
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/queries.d.ts +16 -0
- package/dist/server/queries.d.ts.map +1 -1
- package/dist/server/queries.js +80 -16
- package/dist/server/queries.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +4 -2
- package/dist/session.js.map +1 -1
- package/dist/shared/types.d.ts +31 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +1 -0
- package/dist/shared/types.js.map +1 -1
- package/docs/SETUP.md +308 -51
- package/package.json +14 -7
package/bin/doctor.mjs
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npx @polishd/next doctor` — check that an install is actually wired up.
|
|
3
|
+
*
|
|
4
|
+
* Every failure this looks for is one that produces no error at runtime. A
|
|
5
|
+
* proxy named for the wrong Next major, a matcher that doesn't cover the pages
|
|
6
|
+
* being captured, a route missing its `runtime` declaration, Tailwind not
|
|
7
|
+
* scanning the package — each of these yields a working-looking app, HTTP 200s
|
|
8
|
+
* on every beacon, and an empty dashboard. This turns that class of problem
|
|
9
|
+
* into a list of sentences.
|
|
10
|
+
*
|
|
11
|
+
* Static checks run anywhere. `--url <origin>` adds live checks against a
|
|
12
|
+
* running server, which is the only way to confirm the cookie is really minted
|
|
13
|
+
* and the ingest route really stores.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
DIST_GLOB,
|
|
20
|
+
c,
|
|
21
|
+
detectLayout,
|
|
22
|
+
detectNextMajor,
|
|
23
|
+
nodeVersionOk,
|
|
24
|
+
proxyShape,
|
|
25
|
+
readJSON,
|
|
26
|
+
readText,
|
|
27
|
+
} from "./shared.mjs";
|
|
28
|
+
|
|
29
|
+
const PASS = "pass";
|
|
30
|
+
const WARN = "warn";
|
|
31
|
+
const FAIL = "fail";
|
|
32
|
+
|
|
33
|
+
export async function runDoctor(argv, cwd) {
|
|
34
|
+
const results = [];
|
|
35
|
+
const add = (status, title, detail) => results.push({ status, title, detail });
|
|
36
|
+
|
|
37
|
+
const urlIdx = argv.indexOf("--url");
|
|
38
|
+
const origin = urlIdx !== -1 ? argv[urlIdx + 1] : null;
|
|
39
|
+
const js = argv.includes("--js");
|
|
40
|
+
const codeExt = js ? "js" : "ts";
|
|
41
|
+
const pageExt = js ? "jsx" : "tsx";
|
|
42
|
+
|
|
43
|
+
// ── Environment ────────────────────────────────────────────────────────────
|
|
44
|
+
if (nodeVersionOk()) {
|
|
45
|
+
add(PASS, `Node ${process.versions.node}`, "meets the >= 22.5.0 requirement");
|
|
46
|
+
} else {
|
|
47
|
+
add(
|
|
48
|
+
FAIL,
|
|
49
|
+
`Node ${process.versions.node} is too old`,
|
|
50
|
+
"Polishd needs >= 22.5.0 — the dev store uses node:sqlite, added in 22.5.0.",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const layout = detectLayout(cwd);
|
|
55
|
+
if (!layout.found) {
|
|
56
|
+
add(FAIL, "No app/ or src/app/ directory", "Run this from a Next.js App Router project root.");
|
|
57
|
+
return report(results, origin);
|
|
58
|
+
}
|
|
59
|
+
const { base, appDir } = layout;
|
|
60
|
+
|
|
61
|
+
const next = detectNextMajor(cwd);
|
|
62
|
+
if (next.major === null) {
|
|
63
|
+
add(
|
|
64
|
+
WARN,
|
|
65
|
+
"Could not determine the installed Next.js version",
|
|
66
|
+
"Checks below assume Next 16+. Install dependencies first for an exact answer.",
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
add(PASS, `Next.js ${next.version}`, `detected from ${next.source}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const shape = proxyShape(next, base, codeExt);
|
|
73
|
+
|
|
74
|
+
// ── The proxy: right file, right export, usable matcher ────────────────────
|
|
75
|
+
const proxyAbs = join(cwd, shape.path);
|
|
76
|
+
const otherAbs = join(cwd, shape.otherPath);
|
|
77
|
+
const proxySrc = readText(proxyAbs);
|
|
78
|
+
const otherSrc = readText(otherAbs);
|
|
79
|
+
|
|
80
|
+
if (!proxySrc && otherSrc) {
|
|
81
|
+
add(
|
|
82
|
+
FAIL,
|
|
83
|
+
`Proxy is named ${shape.otherPath}, but this Next major loads ${shape.path}`,
|
|
84
|
+
`Next ${next.major ?? "16+"} does not load ${shape.otherPath}. The file never runs, so no ` +
|
|
85
|
+
`session cookie is minted and every event is silently dropped. Rename it to ` +
|
|
86
|
+
`${shape.path} and rename the exported function to \`${shape.name}\`.`,
|
|
87
|
+
);
|
|
88
|
+
} else if (!proxySrc) {
|
|
89
|
+
add(
|
|
90
|
+
FAIL,
|
|
91
|
+
`Missing ${shape.path}`,
|
|
92
|
+
"Without it no session cookie is minted and ingest drops every batch. Run `npx @polishd/next init`.",
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
add(PASS, `${shape.path} present`, `correct name for Next ${next.major ?? "16+"}`);
|
|
96
|
+
|
|
97
|
+
if (new RegExp(`export\\s+(async\\s+)?function\\s+${shape.name}\\b`).test(proxySrc) ||
|
|
98
|
+
new RegExp(`export\\s+const\\s+${shape.name}\\b`).test(proxySrc)) {
|
|
99
|
+
add(PASS, `Exports \`${shape.name}\``, "matches what this Next major looks for");
|
|
100
|
+
} else {
|
|
101
|
+
add(
|
|
102
|
+
FAIL,
|
|
103
|
+
`${shape.path} does not export \`${shape.name}\``,
|
|
104
|
+
`Next ${next.major ?? "16+"} calls the export named \`${shape.name}\`. An export under any ` +
|
|
105
|
+
"other name is ignored, silently.",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!/withPolishdSession|composePolishd/.test(proxySrc)) {
|
|
110
|
+
add(
|
|
111
|
+
FAIL,
|
|
112
|
+
"Proxy never calls withPolishdSession",
|
|
113
|
+
"The file runs but mints no cookie. Call `withPolishdSession(request)`, or wrap your own " +
|
|
114
|
+
"handler with `composePolishd(handler, { matcher })`.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const matcher = proxySrc.match(/matcher\s*:\s*(\[[^\]]*\]|["'`][^"'`]*["'`])/);
|
|
119
|
+
// Order matters: a referenced matcher never matches the literal pattern, so
|
|
120
|
+
// checking "missing" first would report the vaguer of the two diagnoses for
|
|
121
|
+
// the more specific and more common mistake.
|
|
122
|
+
if (/\bmatcher\s*:\s*[A-Za-z_$]/.test(proxySrc)) {
|
|
123
|
+
add(
|
|
124
|
+
FAIL,
|
|
125
|
+
"config.matcher is not an inline literal",
|
|
126
|
+
"Next statically parses this value, so an imported or referenced matcher — including " +
|
|
127
|
+
"Polishd's own `polishdMatcher` — is not read. The proxy then runs with no matcher at " +
|
|
128
|
+
"all, which under Next 16 + Turbopack breaks route resolution for the entire /api " +
|
|
129
|
+
"segment. Paste the array in literally.",
|
|
130
|
+
);
|
|
131
|
+
} else if (!matcher) {
|
|
132
|
+
add(
|
|
133
|
+
WARN,
|
|
134
|
+
"No config.matcher found in the proxy",
|
|
135
|
+
"Without one the proxy runs everywhere, including /api — which under Next 16 + Turbopack " +
|
|
136
|
+
"breaks route resolution for the entire /api segment.",
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
add(PASS, "config.matcher is an inline literal", matcher[1].replace(/\s+/g, " ").slice(0, 60));
|
|
140
|
+
if (!/\(\?!.*\bapi\b/.test(matcher[1])) {
|
|
141
|
+
add(
|
|
142
|
+
WARN,
|
|
143
|
+
"config.matcher does not exclude /api",
|
|
144
|
+
"Under Next 16 + Turbopack a matcher that touches /api breaks route resolution for the " +
|
|
145
|
+
"whole /api segment — every API route 404s.",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Capture and ingest ─────────────────────────────────────────────────────
|
|
152
|
+
const clientPath = join(base, `instrumentation-client.${codeExt}`);
|
|
153
|
+
const clientSrc = readText(join(cwd, clientPath));
|
|
154
|
+
if (!clientSrc) {
|
|
155
|
+
add(FAIL, `Missing ${clientPath}`, "Nothing initialises capture, so no events are ever sent.");
|
|
156
|
+
} else if (!/initPolishd\s*\(/.test(clientSrc)) {
|
|
157
|
+
add(WARN, `${clientPath} does not call initPolishd()`, "Capture never starts.");
|
|
158
|
+
} else {
|
|
159
|
+
add(PASS, `${clientPath} present`, "capture is initialised");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const routePath = join(appDir, "api", "polishd", `route.${codeExt}`);
|
|
163
|
+
const routeSrc = readText(join(cwd, routePath));
|
|
164
|
+
if (!routeSrc) {
|
|
165
|
+
add(FAIL, `Missing ${routePath}`, "Beacons have nowhere to POST to.");
|
|
166
|
+
} else {
|
|
167
|
+
add(PASS, `${routePath} present`, "ingest endpoint exists");
|
|
168
|
+
for (const [decl, value, why] of [
|
|
169
|
+
["runtime", "nodejs", "node:sqlite and pg cannot run on the Edge runtime"],
|
|
170
|
+
["dynamic", "force-dynamic", "the route mutates per request and must never be cached"],
|
|
171
|
+
]) {
|
|
172
|
+
if (new RegExp(`export\\s+const\\s+${decl}\\s*=\\s*["']${value}["']`).test(routeSrc)) {
|
|
173
|
+
add(PASS, `${routePath} declares ${decl} = "${value}"`, why);
|
|
174
|
+
} else if (new RegExp(`export\\s*\\{[^}]*\\b${decl}\\b`).test(routeSrc)) {
|
|
175
|
+
add(
|
|
176
|
+
FAIL,
|
|
177
|
+
`${routePath} re-exports \`${decl}\` instead of declaring it`,
|
|
178
|
+
"Next statically parses route segment config and rejects a re-export — it must be " +
|
|
179
|
+
`written inline as \`export const ${decl} = "${value}";\`.`,
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
add(WARN, `${routePath} does not declare ${decl}`, why);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const pagePath = join(appDir, "polishd", `page.${pageExt}`);
|
|
188
|
+
const pageSrc = readText(join(cwd, pagePath));
|
|
189
|
+
if (!pageSrc) {
|
|
190
|
+
add(WARN, `Missing ${pagePath}`, "No dashboard. Note the AI server actions fail closed without it.");
|
|
191
|
+
} else {
|
|
192
|
+
add(PASS, `${pagePath} present`, "dashboard route exists");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Styling ────────────────────────────────────────────────────────────────
|
|
196
|
+
// Two things can style the dashboard: the stylesheet it now ships, or the
|
|
197
|
+
// host's own Tailwind scanning our dist, which is how 0.1.x worked. Either
|
|
198
|
+
// alone is fine. What matters is that at least one is present — reporting a
|
|
199
|
+
// 0.1.x app as broken when it renders perfectly would be worse than useless,
|
|
200
|
+
// since the obvious "fix" is to change something that already works.
|
|
201
|
+
const hasImport = pageSrc ? /@polishd\/next\/dashboard\.css/.test(pageSrc) : false;
|
|
202
|
+
const legacySources = [
|
|
203
|
+
"tailwind.config.ts",
|
|
204
|
+
"tailwind.config.js",
|
|
205
|
+
"tailwind.config.mjs",
|
|
206
|
+
join("src", "app", "globals.css"),
|
|
207
|
+
join("app", "globals.css"),
|
|
208
|
+
join("src", "styles", "globals.css"),
|
|
209
|
+
join("styles", "globals.css"),
|
|
210
|
+
].filter((rel) => readText(join(cwd, rel))?.includes(DIST_GLOB));
|
|
211
|
+
|
|
212
|
+
if (hasImport && legacySources.length > 0) {
|
|
213
|
+
add(PASS, "Dashboard stylesheet is imported", "the dashboard will render styled");
|
|
214
|
+
add(
|
|
215
|
+
WARN,
|
|
216
|
+
`Tailwind is also still scanning our dist (${legacySources.join(", ")})`,
|
|
217
|
+
`Now redundant — the shipped stylesheet covers it. Safe to remove that ${DIST_GLOB} line ` +
|
|
218
|
+
"and stop compiling utilities you don't use. Remove it only now that the import is in " +
|
|
219
|
+
"place, not before.",
|
|
220
|
+
);
|
|
221
|
+
} else if (hasImport) {
|
|
222
|
+
add(PASS, "Dashboard stylesheet is imported", "the dashboard will render styled");
|
|
223
|
+
} else if (legacySources.length > 0) {
|
|
224
|
+
add(
|
|
225
|
+
WARN,
|
|
226
|
+
`Dashboard is styled the 0.1.x way, via ${legacySources.join(", ")}`,
|
|
227
|
+
"This still works — your Tailwind compiles the dashboard's utilities from our dist, so " +
|
|
228
|
+
"nothing is broken. To modernise, add `import \"@polishd/next/dashboard.css\";` to " +
|
|
229
|
+
`${pagePath} FIRST, then remove the ${DIST_GLOB} line. Doing it in that order means ` +
|
|
230
|
+
"the dashboard is never unstyled in between.",
|
|
231
|
+
);
|
|
232
|
+
} else if (pageSrc) {
|
|
233
|
+
add(
|
|
234
|
+
FAIL,
|
|
235
|
+
"Nothing is styling the dashboard",
|
|
236
|
+
`It will render as unstyled HTML. Add \`import "@polishd/next/dashboard.css";\` to ` +
|
|
237
|
+
`${pagePath}. The stylesheet is self-contained and scoped to .polishd-root, so it needs ` +
|
|
238
|
+
"no Tailwind in this app and cannot affect anything outside the dashboard.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Access posture ─────────────────────────────────────────────────────────
|
|
243
|
+
if (process.env.POLISHD_DASHBOARD_TOKEN) {
|
|
244
|
+
add(PASS, "POLISHD_DASHBOARD_TOKEN is set", "the dashboard is protected by the built-in gate");
|
|
245
|
+
} else if (process.env.POLISHD_DASHBOARD_PUBLIC === "true") {
|
|
246
|
+
add(
|
|
247
|
+
WARN,
|
|
248
|
+
"Dashboard is explicitly public",
|
|
249
|
+
"POLISHD_DASHBOARD_PUBLIC=true — anyone who finds the URL can change the configured model " +
|
|
250
|
+
"and API base URL, and spend your model tokens.",
|
|
251
|
+
);
|
|
252
|
+
} else if (pageSrc && /authenticate\s*:/.test(pageSrc)) {
|
|
253
|
+
add(PASS, "Dashboard passes its own `authenticate`", "access is gated by your app");
|
|
254
|
+
} else {
|
|
255
|
+
add(
|
|
256
|
+
WARN,
|
|
257
|
+
"No dashboard protection configured in this environment",
|
|
258
|
+
"Fine locally. In production Polishd refuses to serve the dashboard until you set " +
|
|
259
|
+
"POLISHD_DASHBOARD_TOKEN, pass `authenticate`, or set POLISHD_DASHBOARD_PUBLIC=true.",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const gitignore = readText(join(cwd, ".gitignore")) ?? "";
|
|
264
|
+
if (gitignore.includes(".polishd")) {
|
|
265
|
+
add(PASS, ".polishd/ is gitignored", "the local analytics DB won't be committed");
|
|
266
|
+
} else {
|
|
267
|
+
add(WARN, ".polishd/ is not gitignored", "the local SQLite analytics DB may get committed.");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Live checks ────────────────────────────────────────────────────────────
|
|
271
|
+
if (origin) await liveChecks(origin, add, cwd);
|
|
272
|
+
|
|
273
|
+
return report(results, origin);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Confirm against a running server what static inspection can only infer: that
|
|
278
|
+
* the proxy actually mints a cookie, and that ingest actually stores a row.
|
|
279
|
+
*/
|
|
280
|
+
async function liveChecks(origin, add, cwd) {
|
|
281
|
+
const base = origin.replace(/\/$/, "");
|
|
282
|
+
let cookie = null;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const res = await fetch(base, { redirect: "manual" });
|
|
286
|
+
const setCookie = res.headers.getSetCookie?.() ?? [];
|
|
287
|
+
const session = setCookie.find((v) => /^polishd_session=|^[^=]*session=/.test(v));
|
|
288
|
+
if (session) {
|
|
289
|
+
cookie = session.split(";")[0];
|
|
290
|
+
add(PASS, "Proxy mints a session cookie", `${base} responded with ${cookie.split("=")[0]}`);
|
|
291
|
+
} else {
|
|
292
|
+
add(
|
|
293
|
+
FAIL,
|
|
294
|
+
"No session cookie on a page request",
|
|
295
|
+
`${base} returned ${res.status} without setting one. The proxy is not running on this ` +
|
|
296
|
+
"path — check its filename, its exported function name, and that the matcher covers `/`.",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
add(WARN, `Could not reach ${base}`, String(err.message ?? err));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const route = `${base}/api/polishd`;
|
|
305
|
+
const body = JSON.stringify({
|
|
306
|
+
events: [{ type: "page_view", ts: Date.now(), path: "/__polishd_doctor" }],
|
|
307
|
+
});
|
|
308
|
+
try {
|
|
309
|
+
const res = await fetch(route, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: { "Content-Type": "application/json", ...(cookie ? { cookie } : {}) },
|
|
312
|
+
body,
|
|
313
|
+
});
|
|
314
|
+
const json = await res.json().catch(() => null);
|
|
315
|
+
if (!res.ok) {
|
|
316
|
+
add(FAIL, `Ingest route returned ${res.status}`, JSON.stringify(json));
|
|
317
|
+
} else if (json?.reason === "no_session") {
|
|
318
|
+
add(
|
|
319
|
+
FAIL,
|
|
320
|
+
"Ingest is dropping events: no_session",
|
|
321
|
+
"The route works but the request carried no recognised session cookie. Either the proxy " +
|
|
322
|
+
"isn't running, or the proxy and the route disagree about the cookie name — set " +
|
|
323
|
+
"POLISHD_SESSION_COOKIE so both read the same value.",
|
|
324
|
+
);
|
|
325
|
+
} else if (json?.stored > 0) {
|
|
326
|
+
add(PASS, "Ingest stored a test event", `${route} → ${JSON.stringify(json)}`);
|
|
327
|
+
} else {
|
|
328
|
+
add(WARN, "Ingest accepted but stored nothing", JSON.stringify(json));
|
|
329
|
+
}
|
|
330
|
+
} catch (err) {
|
|
331
|
+
add(FAIL, `Could not POST to ${route}`, String(err.message ?? err));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function report(results, origin) {
|
|
336
|
+
const icon = { pass: c.ok("✔"), warn: c.warn("⚠"), fail: c.err("✖") };
|
|
337
|
+
console.log(`\n${c.bold("polishd doctor")}\n`);
|
|
338
|
+
for (const r of results) {
|
|
339
|
+
console.log(`${icon[r.status]} ${r.title}`);
|
|
340
|
+
if (r.detail && r.status !== PASS) console.log(c.dim(` ${wrap(r.detail)}`));
|
|
341
|
+
else if (r.detail) console.log(c.dim(` ${r.detail}`));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const fails = results.filter((r) => r.status === FAIL).length;
|
|
345
|
+
const warns = results.filter((r) => r.status === WARN).length;
|
|
346
|
+
console.log(
|
|
347
|
+
`\n${fails ? c.err(`${fails} problem${fails === 1 ? "" : "s"}`) : c.ok("no problems")}` +
|
|
348
|
+
`${warns ? c.warn(`, ${warns} warning${warns === 1 ? "" : "s"}`) : ""}` +
|
|
349
|
+
c.dim(`, ${results.filter((r) => r.status === PASS).length} checks passed`),
|
|
350
|
+
);
|
|
351
|
+
if (!origin) {
|
|
352
|
+
console.log(
|
|
353
|
+
c.dim("\nRun with --url http://localhost:3000 to also verify the cookie and ingest live."),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return fails > 0 ? 1 : 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Indent continuation lines so multi-sentence details stay readable. */
|
|
360
|
+
function wrap(text, width = 76) {
|
|
361
|
+
const words = text.split(/\s+/);
|
|
362
|
+
const lines = [];
|
|
363
|
+
let line = "";
|
|
364
|
+
for (const w of words) {
|
|
365
|
+
if (line.length + w.length + 1 > width) {
|
|
366
|
+
lines.push(line);
|
|
367
|
+
line = w;
|
|
368
|
+
} else {
|
|
369
|
+
line = line ? `${line} ${w}` : w;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (line) lines.push(line);
|
|
373
|
+
return lines.join("\n ");
|
|
374
|
+
}
|