@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.
- package/AGENTS.md +161 -46
- package/CHANGELOG.md +128 -0
- package/README.md +238 -48
- 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 +7 -2
- package/dist/ai/actions.d.ts.map +1 -1
- package/dist/ai/actions.js +9 -1
- package/dist/ai/actions.js.map +1 -1
- 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 +10 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/client/init.d.ts.map +1 -1
- package/dist/client/init.js +51 -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/index.d.ts +34 -5
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +145 -17
- package/dist/dashboard/index.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 +2601 -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/ingest.d.ts +33 -2
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +61 -7
- 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/docs/SETUP.md +308 -51
- package/package.json +14 -7
package/bin/init.mjs
CHANGED
|
@@ -1,224 +1,251 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
2
|
* `npx @polishd/next init` — scaffold the glue files a Next.js app needs to wire
|
|
4
3
|
* up Polishd. Node builtins only; zero runtime dependencies.
|
|
5
4
|
*
|
|
6
5
|
* Flags:
|
|
7
|
-
* --force
|
|
6
|
+
* --force regenerate files this CLI wrote (backed up first). Files it did
|
|
7
|
+
* not write are never overwritten, with or without this flag.
|
|
8
8
|
* --dry-run print what would happen, write nothing
|
|
9
9
|
* --js emit .js/.jsx instead of TypeScript
|
|
10
10
|
* --config also write a starter polishd.config.ts
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
15
|
+
import {
|
|
16
|
+
c,
|
|
17
|
+
detectLayout,
|
|
18
|
+
detectNextMajor,
|
|
19
|
+
nodeVersionOk,
|
|
20
|
+
proxyShape,
|
|
21
|
+
readText,
|
|
22
|
+
} from "./shared.mjs";
|
|
23
|
+
|
|
24
|
+
// Every generated file carries this marker. It is what lets `--force` tell "a
|
|
25
|
+
// file I wrote and may regenerate" apart from "the user's own code", which is
|
|
26
|
+
// the difference between a convenience flag and a data-loss bug.
|
|
27
|
+
const SENTINEL = "@polishd/next init";
|
|
28
|
+
const STAMP =
|
|
29
|
+
`// Generated by \`npx ${SENTINEL}\`. Edit freely — \`--force\` backs up\n` +
|
|
30
|
+
`// this file before regenerating it.\n`;
|
|
31
|
+
|
|
32
|
+
export function runInit(argv, cwd) {
|
|
33
|
+
const args = new Set(argv);
|
|
34
|
+
const FORCE = args.has("--force");
|
|
35
|
+
const DRY = args.has("--dry-run");
|
|
36
|
+
const JS = args.has("--js");
|
|
37
|
+
const WANT_CONFIG = args.has("--config");
|
|
38
|
+
const codeExt = JS ? "js" : "ts";
|
|
39
|
+
const pageExt = JS ? "jsx" : "tsx";
|
|
40
|
+
|
|
41
|
+
// ── Detect project layout ──────────────────────────────────────────────────
|
|
42
|
+
const layout = detectLayout(cwd);
|
|
43
|
+
if (!layout.found) {
|
|
44
|
+
console.error(
|
|
45
|
+
c.warn("✗ Could not find an `app/` or `src/app/` directory.\n") +
|
|
46
|
+
" Run this from the root of a Next.js App Router project.",
|
|
47
|
+
);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const { base, appDir } = layout;
|
|
51
|
+
console.log(
|
|
52
|
+
c.ok(`✔ Detected Next.js App Router`) +
|
|
53
|
+
c.dim(` (${layout.hasSrcApp ? "src/ layout" : "root layout"})`),
|
|
36
54
|
);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
const base = hasSrcApp ? "src" : ""; // where proxy/instrumentation live
|
|
40
|
-
const appDir = hasSrcApp ? join("src", "app") : "app";
|
|
41
|
-
const ext = JS ? "tsx" : "tsx"; // page is always JSX
|
|
42
|
-
const codeExt = JS ? "js" : "ts";
|
|
43
|
-
const pageExt = JS ? "jsx" : "tsx";
|
|
44
|
-
|
|
45
|
-
console.log(
|
|
46
|
-
c.ok(`✔ Detected Next.js App Router`) +
|
|
47
|
-
c.dim(` (${hasSrcApp ? "src/ layout" : "root layout"})`),
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// ── File templates ───────────────────────────────────────────────────────────
|
|
51
|
-
const files = [
|
|
52
|
-
{
|
|
53
|
-
path: join(base, `proxy.${codeExt}`),
|
|
54
|
-
merge: join(base, `middleware.${codeExt}`),
|
|
55
|
-
body:
|
|
56
|
-
(JS ? "" : `import type { NextRequest } from "next/server";\n`) +
|
|
57
|
-
`import { withPolishdSession } from "@polishd/next/proxy";\n\n` +
|
|
58
|
-
`export function proxy(request${JS ? "" : ": NextRequest"}) {\n` +
|
|
59
|
-
` return withPolishdSession(request);\n` +
|
|
60
|
-
`}\n\n` +
|
|
61
|
-
`// Next statically parses config.matcher, so it must be an inline literal\n` +
|
|
62
|
-
`// here. Excluding all of /api is required — under Next 16 + Turbopack a\n` +
|
|
63
|
-
`// matcher that touches /api breaks the whole /api segment.\n` +
|
|
64
|
-
`export const config = {\n` +
|
|
65
|
-
` matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],\n` +
|
|
66
|
-
`};\n`,
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
path: join(base, `instrumentation-client.${codeExt}`),
|
|
70
|
-
body: `import { initPolishd } from "@polishd/next/client";\n\ninitPolishd();\n`,
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
path: join(appDir, "api", "polishd", `route.${codeExt}`),
|
|
74
|
-
// runtime/dynamic must be declared here, not re-exported: Next statically
|
|
75
|
-
// parses route segment config and rejects `export { runtime } from ...`
|
|
76
|
-
// ("Next.js can't recognize the exported `runtime` field in route").
|
|
77
|
-
body:
|
|
78
|
-
`import { createPolishdRoute } from "@polishd/next/route";\n\n` +
|
|
79
|
-
`// node:sqlite / pg need the Node runtime — never the Edge runtime.\n` +
|
|
80
|
-
`export const runtime = "nodejs";\n` +
|
|
81
|
-
`// This route mutates per-request; it must never be statically cached.\n` +
|
|
82
|
-
`export const dynamic = "force-dynamic";\n\n` +
|
|
83
|
-
`export const POST = createPolishdRoute();\n`,
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
path: join(appDir, "polishd", `page.${pageExt}`),
|
|
87
|
-
body:
|
|
88
|
-
`import { createPolishdPage } from "@polishd/next/dashboard";\n\n` +
|
|
89
|
-
`// Next requires these to be declared inline in the page module.\n` +
|
|
90
|
-
`export const runtime = "nodejs";\n` +
|
|
91
|
-
`export const dynamic = "force-dynamic";\n\n` +
|
|
92
|
-
`// Unguarded by default. To protect it, pass an authenticate callback:\n` +
|
|
93
|
-
`// export default createPolishdPage({ authenticate: isAuthenticated });\n` +
|
|
94
|
-
`export default createPolishdPage();\n`,
|
|
95
|
-
},
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
if (WANT_CONFIG) {
|
|
99
|
-
files.push({
|
|
100
|
-
path: `polishd.config.${codeExt}`,
|
|
101
|
-
body:
|
|
102
|
-
`import { definePolishdConfig } from "@polishd/next";\n\n` +
|
|
103
|
-
`export default definePolishdConfig({\n` +
|
|
104
|
-
` // sampleRate: 1,\n` +
|
|
105
|
-
` // rageClick: { count: 3, windowMs: 500 },\n` +
|
|
106
|
-
`});\n`,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
55
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (existsSync(abs) && !FORCE) {
|
|
114
|
-
console.log(c.warn(`⚠ ${file.path} exists`) + c.dim(" — skipped (use --force)"));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
// If a middleware.ts already exists for the proxy file, print a merge snippet.
|
|
118
|
-
if (file.merge && existsSync(join(cwd, file.merge)) && !FORCE) {
|
|
56
|
+
// The dev store uses `node:sqlite`, which landed in 22.5.0. Checked here
|
|
57
|
+
// rather than left to a confusing "cannot find module" at first request.
|
|
58
|
+
if (!nodeVersionOk()) {
|
|
119
59
|
console.log(
|
|
120
|
-
c.warn(`⚠ ${
|
|
121
|
-
c.dim(" —
|
|
122
|
-
` import { withPolishdSession, polishdMatcher } from "@polishd/next/proxy";\n` +
|
|
123
|
-
` // call withPolishdSession(request) in your handler;\n` +
|
|
124
|
-
` // and merge polishdMatcher into your config.matcher`,
|
|
60
|
+
c.warn(`⚠ Node ${process.versions.node}`) +
|
|
61
|
+
c.dim(" — Polishd needs >= 22.5.0 (the dev store uses node:sqlite)."),
|
|
125
62
|
);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (DRY) {
|
|
129
|
-
console.log(c.dim(`• would write ${file.path}`));
|
|
130
|
-
return;
|
|
131
63
|
}
|
|
132
|
-
mkdirSync(dirname(abs), { recursive: true });
|
|
133
|
-
writeFileSync(abs, file.body);
|
|
134
|
-
console.log(c.ok(`✔ Created ${file.path}`));
|
|
135
|
-
}
|
|
136
64
|
|
|
137
|
-
|
|
65
|
+
// ── Detect the installed Next major ────────────────────────────────────────
|
|
66
|
+
const next = detectNextMajor(cwd);
|
|
67
|
+
const shape = proxyShape(next, base, codeExt);
|
|
138
68
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// ── Tailwind ─────────────────────────────────────────────────────────────────
|
|
150
|
-
// The dashboard is styled entirely with Tailwind utility classes and ships no
|
|
151
|
-
// stylesheet of its own, so Tailwind has to be told to scan the package's dist.
|
|
152
|
-
// Neither version finds it by default: v4's automatic content detection skips
|
|
153
|
-
// node_modules, and v3 only scans the globs it is given. Without this the
|
|
154
|
-
// dashboard renders completely unstyled.
|
|
155
|
-
const DIST_GLOB = "node_modules/@polishd/next/dist";
|
|
156
|
-
|
|
157
|
-
function wireTailwind() {
|
|
158
|
-
const v3Config = ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs"]
|
|
159
|
-
.map((f) => join(cwd, f))
|
|
160
|
-
.find((f) => existsSync(f));
|
|
161
|
-
|
|
162
|
-
if (v3Config) {
|
|
69
|
+
if (next.major === null) {
|
|
70
|
+
// An unannounced guess here is exactly the silent failure this detection
|
|
71
|
+
// exists to prevent, so say so.
|
|
72
|
+
console.log(
|
|
73
|
+
c.warn("⚠ Could not determine the installed Next.js version") +
|
|
74
|
+
c.dim(`\n Assuming 16+ and writing ${shape.path}.`) +
|
|
75
|
+
c.dim(`\n On Next 15 rename it to ${shape.otherPath} and rename the exported`) +
|
|
76
|
+
c.dim(`\n function to \`${shape.other}\` — Next 15 does not load ${shape.path}.`),
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
163
79
|
console.log(
|
|
164
|
-
c.
|
|
165
|
-
c.dim(
|
|
166
|
-
|
|
80
|
+
c.ok(`✔ Detected Next.js ${next.version}`) +
|
|
81
|
+
c.dim(` — using ${shape.path}`) +
|
|
82
|
+
(next.source === "package.json" ? c.dim(" (from package.json range)") : ""),
|
|
167
83
|
);
|
|
168
|
-
return;
|
|
169
84
|
}
|
|
170
85
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
86
|
+
// ── File templates ─────────────────────────────────────────────────────────
|
|
87
|
+
const mergeHint =
|
|
88
|
+
c.dim(` Add Polishd to your existing handler instead:\n`) +
|
|
89
|
+
` import { composePolishd } from "@polishd/next/proxy";\n` +
|
|
90
|
+
` export const ${shape.name} = composePolishd(myHandler, { matcher: "/your/scope" });\n` +
|
|
91
|
+
c.dim(` // Then widen your inline config.matcher to the union of both.\n`) +
|
|
92
|
+
c.dim(` // Declaring your original scope keeps your handler running only where\n`) +
|
|
93
|
+
c.dim(` // it ran before, even though the matcher now feeds Polishd everything.\n`) +
|
|
94
|
+
c.dim(` // See README → "Composing with an existing proxy".`);
|
|
95
|
+
|
|
96
|
+
const files = [
|
|
97
|
+
{
|
|
98
|
+
path: shape.path,
|
|
99
|
+
// The other spelling of this same file. A host on Next 16 that still has
|
|
100
|
+
// a middleware.ts (or vice versa) has a real collision worth flagging.
|
|
101
|
+
counterpart: shape.otherPath,
|
|
102
|
+
mergeHint,
|
|
103
|
+
body:
|
|
104
|
+
(JS ? "" : `import type { NextRequest } from "next/server";\n`) +
|
|
105
|
+
`import { withPolishdSession } from "@polishd/next/proxy";\n\n` +
|
|
106
|
+
`export function ${shape.name}(request${JS ? "" : ": NextRequest"}) {\n` +
|
|
107
|
+
` return withPolishdSession(request);\n` +
|
|
108
|
+
`}\n\n` +
|
|
109
|
+
`// Next statically parses config.matcher, so it must be an inline literal\n` +
|
|
110
|
+
`// here. Excluding all of /api is required — under Next 16 + Turbopack a\n` +
|
|
111
|
+
`// matcher that touches /api breaks the whole /api segment.\n` +
|
|
112
|
+
`export const config = {\n` +
|
|
113
|
+
` matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],\n` +
|
|
114
|
+
`};\n`,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
path: join(base, `instrumentation-client.${codeExt}`),
|
|
118
|
+
body: `import { initPolishd } from "@polishd/next/client";\n\ninitPolishd();\n`,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
path: join(appDir, "api", "polishd", `route.${codeExt}`),
|
|
122
|
+
// runtime/dynamic must be declared here, not re-exported: Next statically
|
|
123
|
+
// parses route segment config and rejects `export { runtime } from ...`
|
|
124
|
+
// ("Next.js can't recognize the exported `runtime` field in route").
|
|
125
|
+
body:
|
|
126
|
+
`import { createPolishdRoute } from "@polishd/next/route";\n\n` +
|
|
127
|
+
`// node:sqlite / pg need the Node runtime — never the Edge runtime.\n` +
|
|
128
|
+
`export const runtime = "nodejs";\n` +
|
|
129
|
+
`// This route mutates per-request; it must never be statically cached.\n` +
|
|
130
|
+
`export const dynamic = "force-dynamic";\n\n` +
|
|
131
|
+
`export const POST = createPolishdRoute();\n`,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
path: join(appDir, "polishd", `page.${pageExt}`),
|
|
135
|
+
body:
|
|
136
|
+
`// The dashboard's own stylesheet. Self-contained and scoped to\n` +
|
|
137
|
+
`// .polishd-root, so it needs no Tailwind in this app and cannot\n` +
|
|
138
|
+
`// affect anything outside the dashboard.\n` +
|
|
139
|
+
`import "@polishd/next/dashboard.css";\n` +
|
|
140
|
+
`import { createPolishdPage } from "@polishd/next/dashboard";\n\n` +
|
|
141
|
+
`// Next requires these to be declared inline in the page module.\n` +
|
|
142
|
+
`export const runtime = "nodejs";\n` +
|
|
143
|
+
`export const dynamic = "force-dynamic";\n\n` +
|
|
144
|
+
`// In production the dashboard refuses to render until you either set\n` +
|
|
145
|
+
`// POLISHD_DASHBOARD_TOKEN, pass your own \`authenticate\`, or opt out\n` +
|
|
146
|
+
`// explicitly with POLISHD_DASHBOARD_PUBLIC=true. It can change your AI\n` +
|
|
147
|
+
`// settings and spend your model tokens, so it is not public by default.\n` +
|
|
148
|
+
`export default createPolishdPage();\n`,
|
|
149
|
+
},
|
|
178
150
|
];
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
151
|
+
|
|
152
|
+
if (WANT_CONFIG) {
|
|
153
|
+
files.push({
|
|
154
|
+
path: `polishd.config.${codeExt}`,
|
|
155
|
+
body:
|
|
156
|
+
`import { definePolishdConfig } from "@polishd/next";\n\n` +
|
|
157
|
+
`// NOTE: nothing imports this for you. Pass it to initPolishd(),\n` +
|
|
158
|
+
`// createPolishdRoute() and your proxy explicitly, or the layers will\n` +
|
|
159
|
+
`// disagree. For the session cookie name prefer POLISHD_SESSION_COOKIE,\n` +
|
|
160
|
+
`// which every runtime can read.\n` +
|
|
161
|
+
`export default definePolishdConfig({\n` +
|
|
162
|
+
` // sampleRate: 1,\n` +
|
|
163
|
+
` // rageClick: { count: 3, windowMs: 500 },\n` +
|
|
164
|
+
`});\n`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Write ──────────────────────────────────────────────────────────────────
|
|
169
|
+
/** Did this CLI write the file currently at `abs`? */
|
|
170
|
+
const isOurs = (abs) => (readText(abs) ?? "").includes(SENTINEL);
|
|
171
|
+
|
|
172
|
+
function write(file) {
|
|
173
|
+
const abs = join(cwd, file.path);
|
|
174
|
+
const body = STAMP + file.body;
|
|
175
|
+
|
|
176
|
+
// A proxy under the *other* name is a collision even when our target is
|
|
177
|
+
// free: the host already has a handler, and Next loads only one of these.
|
|
178
|
+
const counterpartAbs = file.counterpart ? join(cwd, file.counterpart) : null;
|
|
179
|
+
if (counterpartAbs && existsSync(counterpartAbs) && !isOurs(counterpartAbs)) {
|
|
180
|
+
console.log(
|
|
181
|
+
c.warn(`⚠ ${file.counterpart} already exists`) +
|
|
182
|
+
c.dim(` — Next loads only one proxy module, and on this version it is ${file.path}.\n`) +
|
|
183
|
+
mergeHint,
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (existsSync(abs)) {
|
|
189
|
+
// Never overwrite code this CLI did not author — not even with --force.
|
|
190
|
+
// Losing a hand-written proxy (auth redirects, i18n, rate limiting) to a
|
|
191
|
+
// flag someone passed for an unrelated skipped file is not a tradeoff.
|
|
192
|
+
if (!isOurs(abs)) {
|
|
193
|
+
console.log(
|
|
194
|
+
c.warn(`⚠ ${file.path} exists and was not written by Polishd`) +
|
|
195
|
+
c.dim(" — left untouched.\n") +
|
|
196
|
+
(file.mergeHint ?? c.dim(` Rename or delete it to have a fresh copy scaffolded.`)),
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!FORCE) {
|
|
201
|
+
console.log(
|
|
202
|
+
c.dim(`• ${file.path} already scaffolded — skipped (use --force to regenerate)`),
|
|
203
|
+
);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (DRY) {
|
|
207
|
+
console.log(c.dim(`• would back up ${file.path} → ${file.path}.bak and regenerate`));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
copyFileSync(abs, `${abs}.bak`);
|
|
211
|
+
writeFileSync(abs, body);
|
|
212
|
+
console.log(
|
|
213
|
+
c.ok(`✔ Regenerated ${file.path}`) + c.dim(` (previous copy saved as ${file.path}.bak)`),
|
|
214
|
+
);
|
|
186
215
|
return;
|
|
187
216
|
}
|
|
188
|
-
|
|
189
|
-
const depth = dirname(rel).split(/[\\/]/).filter(Boolean).length;
|
|
190
|
-
const source = `${"../".repeat(depth)}${DIST_GLOB}`;
|
|
217
|
+
|
|
191
218
|
if (DRY) {
|
|
192
|
-
console.log(c.dim(`• would
|
|
219
|
+
console.log(c.dim(`• would write ${file.path}`));
|
|
193
220
|
return;
|
|
194
221
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
/(@import\s+["']tailwindcss["'];?)/,
|
|
199
|
-
`$1\n/* Generate the Polishd dashboard's utility classes (Tailwind skips node_modules). */\n@source "${source}";`,
|
|
200
|
-
),
|
|
201
|
-
);
|
|
202
|
-
console.log(c.ok(`✔ Added @source "${source}" to ${rel}`));
|
|
203
|
-
return;
|
|
222
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
223
|
+
writeFileSync(abs, body);
|
|
224
|
+
console.log(c.ok(`✔ Created ${file.path}`));
|
|
204
225
|
}
|
|
205
226
|
|
|
206
|
-
|
|
207
|
-
c.warn("⚠ No Tailwind entry stylesheet found") +
|
|
208
|
-
c.dim(" — the dashboard needs Tailwind to render styled.\n") +
|
|
209
|
-
c.dim(" v4: add to your CSS → ") +
|
|
210
|
-
`@source "<path-to>/${DIST_GLOB}";\n` +
|
|
211
|
-
c.dim(" v3: add to content → ") +
|
|
212
|
-
`"./${DIST_GLOB}/**/*.js"`,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
227
|
+
for (const f of files) write(f);
|
|
215
228
|
|
|
216
|
-
|
|
229
|
+
// ── .gitignore ─────────────────────────────────────────────────────────────
|
|
230
|
+
const giPath = join(cwd, ".gitignore");
|
|
231
|
+
const gi = readText(giPath) ?? "";
|
|
232
|
+
if (!gi.includes(".polishd")) {
|
|
233
|
+
if (DRY) {
|
|
234
|
+
console.log(c.dim("• would add .polishd/ to .gitignore"));
|
|
235
|
+
} else {
|
|
236
|
+
appendFileSync(giPath, `${gi.endsWith("\n") || gi === "" ? "" : "\n"}.polishd/\n`);
|
|
237
|
+
console.log(c.ok("✔ Added .polishd/ to .gitignore"));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
217
240
|
|
|
218
|
-
|
|
219
|
-
console.log(`
|
|
241
|
+
console.log(`
|
|
220
242
|
${c.bold("Next steps:")}
|
|
221
243
|
1. Local dev needs nothing — events write to ${c.dim(".polishd/analytics.db")} (SQLite).
|
|
222
244
|
2. For production, set ${c.dim("POLISHD_DATABASE_URL")} (pooled Postgres). See DATABASE.md.
|
|
223
|
-
3.
|
|
245
|
+
3. Set ${c.dim("POLISHD_DASHBOARD_TOKEN")} to protect ${c.bold("/polishd")} in production.
|
|
246
|
+
4. Visit ${c.bold("/polishd")} to see the dashboard.
|
|
247
|
+
5. Check the wiring with ${c.bold("npx @polishd/next doctor")}.
|
|
224
248
|
${DRY ? c.warn("\n(dry run — nothing was written)") : ""}`);
|
|
249
|
+
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
package/bin/polishd.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `polishd` CLI.
|
|
4
|
+
*
|
|
5
|
+
* npx @polishd/next init scaffold the glue files
|
|
6
|
+
* npx @polishd/next doctor check that an install is actually wired up
|
|
7
|
+
*
|
|
8
|
+
* `init` is the default so the historical `npx polishd` keeps working.
|
|
9
|
+
* Node builtins only; zero runtime dependencies.
|
|
10
|
+
*/
|
|
11
|
+
import { c } from "./shared.mjs";
|
|
12
|
+
|
|
13
|
+
const argv = process.argv.slice(2);
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "init";
|
|
16
|
+
const rest = argv.filter((a) => a !== command);
|
|
17
|
+
|
|
18
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
19
|
+
console.log(`
|
|
20
|
+
${c.bold("polishd")} — drop-in product analytics for Next.js
|
|
21
|
+
|
|
22
|
+
${c.bold("Usage")}
|
|
23
|
+
npx @polishd/next init [options] scaffold the files a Next.js app needs
|
|
24
|
+
npx @polishd/next doctor [options] check an existing install
|
|
25
|
+
|
|
26
|
+
${c.bold("init options")}
|
|
27
|
+
--force regenerate files this CLI wrote (backed up to .bak first).
|
|
28
|
+
Files it did not write are never overwritten.
|
|
29
|
+
--dry-run print what would happen, write nothing
|
|
30
|
+
--js emit .js/.jsx instead of TypeScript
|
|
31
|
+
--config also write a starter polishd.config.ts
|
|
32
|
+
|
|
33
|
+
${c.bold("doctor options")}
|
|
34
|
+
--url <origin> also run live checks against a running server
|
|
35
|
+
--js inspect .js/.jsx files instead of TypeScript
|
|
36
|
+
|
|
37
|
+
${c.bold("Installing with a coding agent")}
|
|
38
|
+
Polishd ships a machine-readable install procedure. Paste this to Claude Code,
|
|
39
|
+
Cursor, Copilot, or any other coding agent and let it do the whole install:
|
|
40
|
+
|
|
41
|
+
${c.dim(" ───────────────────────────────────────────────────────────────────────")}
|
|
42
|
+
Install @polishd/next into this Next.js app.
|
|
43
|
+
|
|
44
|
+
Follow the procedure in AGENTS.md from the package exactly — after
|
|
45
|
+
installing, read node_modules/@polishd/next/AGENTS.md. If the package
|
|
46
|
+
isn't installed yet, read it at
|
|
47
|
+
https://github.com/muskanvaswan/polishd/blob/main/AGENTS.md
|
|
48
|
+
|
|
49
|
+
Work through its phases in order and do not skip Phase 7 (verification).
|
|
50
|
+
Several of its constraints fail silently at runtime rather than at build
|
|
51
|
+
time, so a passing build does not mean the install worked.
|
|
52
|
+
|
|
53
|
+
Finish by running \`npx @polishd/next doctor\` and fixing everything it
|
|
54
|
+
reports.
|
|
55
|
+
${c.dim(" ───────────────────────────────────────────────────────────────────────")}
|
|
56
|
+
|
|
57
|
+
${c.bold("Docs")}
|
|
58
|
+
AGENTS.md the install procedure (agent-oriented, and the most precise)
|
|
59
|
+
README.md overview, protecting the dashboard, configuration
|
|
60
|
+
docs/SETUP.md full walkthrough, every variant, troubleshooting
|
|
61
|
+
DATABASE.md production database setup, schema, retention
|
|
62
|
+
`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let code = 0;
|
|
67
|
+
switch (command) {
|
|
68
|
+
case "init": {
|
|
69
|
+
const { runInit } = await import("./init.mjs");
|
|
70
|
+
code = runInit(rest, cwd);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "doctor": {
|
|
74
|
+
const { runDoctor } = await import("./doctor.mjs");
|
|
75
|
+
code = await runDoctor(rest, cwd);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
console.error(
|
|
80
|
+
c.err(`✗ Unknown command "${command}"`) + c.dim("\n Try `init` or `doctor`, or --help."),
|
|
81
|
+
);
|
|
82
|
+
code = 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.exit(code);
|
package/bin/shared.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the `polishd` CLI. Node builtins only.
|
|
3
|
+
*
|
|
4
|
+
* `init` and `doctor` have to agree exactly about where things live and which
|
|
5
|
+
* Next major is installed — a doctor that inspected different paths than init
|
|
6
|
+
* wrote would be worse than no doctor at all.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
export const c = {
|
|
13
|
+
ok: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
14
|
+
warn: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
15
|
+
err: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
16
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
17
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function readJSON(path) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readText(path) {
|
|
29
|
+
try {
|
|
30
|
+
return readFileSync(path, "utf8");
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Where the app's code lives: `src/app` layout or root `app`. */
|
|
37
|
+
export function detectLayout(cwd) {
|
|
38
|
+
const hasSrcApp = existsSync(join(cwd, "src", "app"));
|
|
39
|
+
const hasRootApp = existsSync(join(cwd, "app"));
|
|
40
|
+
return {
|
|
41
|
+
found: hasSrcApp || hasRootApp,
|
|
42
|
+
hasSrcApp,
|
|
43
|
+
base: hasSrcApp ? "src" : "",
|
|
44
|
+
appDir: hasSrcApp ? join("src", "app") : "app",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Which Next major is actually installed.
|
|
50
|
+
*
|
|
51
|
+
* Load-bearing: 16+ loads `proxy.ts`, 15 loads `middleware.ts`, and writing
|
|
52
|
+
* the wrong one means the file never runs — no session cookie, every beacon
|
|
53
|
+
* still 200, nothing stored, and no error to follow.
|
|
54
|
+
*/
|
|
55
|
+
export function detectNextMajor(cwd) {
|
|
56
|
+
// Prefer the resolved install. A declared range ("^16", "latest", "canary")
|
|
57
|
+
// does not tell you what is actually in node_modules.
|
|
58
|
+
const direct = readJSON(join(cwd, "node_modules", "next", "package.json"));
|
|
59
|
+
if (direct?.version) {
|
|
60
|
+
const major = Number.parseInt(direct.version, 10);
|
|
61
|
+
if (Number.isFinite(major)) {
|
|
62
|
+
return { major, version: direct.version, source: "node_modules" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Hoisted / pnpm / workspace installs won't be at that path.
|
|
66
|
+
try {
|
|
67
|
+
const require = createRequire(join(cwd, "package.json"));
|
|
68
|
+
const resolved = readJSON(require.resolve("next/package.json"));
|
|
69
|
+
if (resolved?.version) {
|
|
70
|
+
const major = Number.parseInt(resolved.version, 10);
|
|
71
|
+
if (Number.isFinite(major)) {
|
|
72
|
+
return { major, version: resolved.version, source: "node_modules" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// `next` not installed yet, or its exports map hides package.json.
|
|
77
|
+
}
|
|
78
|
+
// Last resort: the declared range. Better than guessing blind.
|
|
79
|
+
const pkg = readJSON(join(cwd, "package.json"));
|
|
80
|
+
const range =
|
|
81
|
+
pkg?.dependencies?.next ?? pkg?.devDependencies?.next ?? pkg?.peerDependencies?.next;
|
|
82
|
+
if (typeof range === "string") {
|
|
83
|
+
const major = Number.parseInt(range.replace(/^[^\d]*/, ""), 10);
|
|
84
|
+
if (Number.isFinite(major)) return { major, version: range, source: "package.json" };
|
|
85
|
+
}
|
|
86
|
+
return { major: null, version: null, source: null };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The proxy file this app should have, given its Next major.
|
|
91
|
+
* An undetermined version is treated as modern, which is also what init writes.
|
|
92
|
+
*/
|
|
93
|
+
export function proxyShape(next, base, codeExt) {
|
|
94
|
+
const useMiddleware = next.major !== null && next.major <= 15;
|
|
95
|
+
const name = useMiddleware ? "middleware" : "proxy";
|
|
96
|
+
const other = useMiddleware ? "proxy" : "middleware";
|
|
97
|
+
return {
|
|
98
|
+
useMiddleware,
|
|
99
|
+
name,
|
|
100
|
+
other,
|
|
101
|
+
path: join(base, `${name}.${codeExt}`),
|
|
102
|
+
otherPath: join(base, `${other}.${codeExt}`),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const MIN_NODE = { major: 22, minor: 5 };
|
|
107
|
+
|
|
108
|
+
export function nodeVersionOk() {
|
|
109
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
110
|
+
return major > MIN_NODE.major || (major === MIN_NODE.major && minor >= MIN_NODE.minor);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The dist path Tailwind must be told to scan (v4 `@source`, v3 `content`). */
|
|
114
|
+
export const DIST_GLOB = "node_modules/@polishd/next/dist";
|
package/dist/ai/actions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type SaveAISettingsInput } from "./settings";
|
|
2
|
-
import type { PolishdAISettingsPublic, PolishdLossItem, CreateIssueResult, GenerateProfileResult, GenerateSummaryResult, VerifyGithubResult } from "./types";
|
|
1
|
+
import { type ListModelsInput, type SaveAISettingsInput } from "./settings";
|
|
2
|
+
import type { PolishdAISettingsPublic, PolishdLossItem, CreateIssueResult, GenerateProfileResult, GenerateSummaryResult, ListModelsResult, VerifyGithubResult } from "./types";
|
|
3
3
|
/** Generate or refresh the narrative. `force` re-asks the model even if data is unchanged. */
|
|
4
4
|
export declare function generateSummaryAction(force?: boolean): Promise<GenerateSummaryResult>;
|
|
5
5
|
/** Persist provider/model/key/instructions/context; returns the no-secret view. */
|
|
@@ -9,6 +9,11 @@ export declare function saveAISettingsAction(input: SaveAISettingsInput): Promis
|
|
|
9
9
|
* step. Passes current analytics so known component names steer the scan.
|
|
10
10
|
*/
|
|
11
11
|
export declare function generateProfileAction(): Promise<GenerateProfileResult>;
|
|
12
|
+
/**
|
|
13
|
+
* List the models available to the given (or already-stored) key, so the
|
|
14
|
+
* dashboard can render a dropdown instead of a free-text model id.
|
|
15
|
+
*/
|
|
16
|
+
export declare function listModelsAction(input: ListModelsInput): Promise<ListModelsResult>;
|
|
12
17
|
/**
|
|
13
18
|
* Check the saved GitHub repo + token live against the API — called by the
|
|
14
19
|
* onboarding step right after saving, so a bad token fails at setup.
|