nowebcli 0.2.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 ADDED
@@ -0,0 +1,188 @@
1
+ # Noweb
2
+
3
+ Noweb ist eine Next.js-App fuer Website-Templates, Projekte, Auth, Checkout und
4
+ Deployment. Nutzer koennen Templates auswaehlen, Inhalte im Editor anpassen,
5
+ Bilder hochladen und Projekte mit eigener Domain veroeffentlichen.
6
+
7
+ ## Schnellstart
8
+
9
+ ```bash
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ Die App laeuft lokal unter `http://localhost:3000`.
15
+
16
+ Vor einem Pull Request oder Deploy:
17
+
18
+ ```bash
19
+ npm run ci
20
+ ```
21
+
22
+ ## Community-Templates
23
+
24
+ Die vollständige Anleitung für Erstellung, Assets, lokale Vorschau und
25
+ automatische Veröffentlichung steht in [`templates/README.md`](templates/README.md).
26
+
27
+ ## Wichtige Befehle
28
+
29
+ - `npm run dev` startet Next lokal mit Webpack.
30
+ - `npm run build` baut die Produktionsversion.
31
+ - `npm run start` startet den gebauten Next-Server.
32
+ - `npm run lint` prueft den Code mit ESLint und erlaubt keine Warnings.
33
+ - `npm run typecheck` prueft TypeScript ohne Emit.
34
+ - `npm run test` fuehrt Unit-Tests aus.
35
+ - `npm run test:e2e` fuehrt den Playwright-Smoke gegen `npm run start` aus.
36
+ - `npx nowebcli create` erstellt ein Community-Template; danach laufen die Befehle lokal ueber `npx noweb`.
37
+ - `npm run ci` fuehrt Audit, Typecheck, Lint, Tests, Build und E2E aus.
38
+
39
+ ## Struktur
40
+
41
+ - `app/` enthaelt Seiten, Routen und Server Actions.
42
+ - `app/page.tsx`, `app/HomeSections.tsx`, `app/Footer.tsx` und `app/menu.tsx`
43
+ bilden die Startseite und Navigation.
44
+ - `app/docs/` enthaelt die oeffentliche Doku im Produkt.
45
+ - `lib/auth/` konfiguriert Better Auth.
46
+ - `lib/db.ts` stellt die Postgres-Verbindung bereit.
47
+ - `lib/object-storage.ts` kapselt RustFS/R2-kompatiblen Object Storage.
48
+ - `lib/env.ts` validiert serverseitige Runtime-Konfiguration.
49
+ - `lib/domain.ts` normalisiert und validiert Domains.
50
+ - `lib/templates/` enthaelt Template-Typen, Runtime-Helfer und lokale Templates.
51
+ - `templates/index.ts` ist generiert und soll nicht per Hand editiert werden.
52
+ - `db.txt` dokumentiert das aktuelle Datenbankschema.
53
+ - `scripts/deploy-worker.mjs` veroeffentlicht Projekte als statische Sites.
54
+ - `.github/workflows/deploy.yaml` deployed `main` per SSH auf den Server.
55
+
56
+ ## Umgebung
57
+
58
+ Mindestens benoetigt:
59
+
60
+ ```env
61
+ DATABASE_URL=postgresql://...
62
+ BETTER_AUTH_SECRET=...
63
+ ```
64
+
65
+ Je nach Feature zusaetzlich:
66
+
67
+ ```env
68
+ RESEND_API_KEY=...
69
+ EMAIL_FROM=Noweb <mail@example.com>
70
+
71
+ GOOGLE_CLIENT_ID=...
72
+ GOOGLE_CLIENT_SECRET=...
73
+ GITHUB_CLIENT_ID=...
74
+ GITHUB_CLIENT_SECRET=...
75
+
76
+ POLAR_ACCESS_TOKEN=...
77
+ POLAR_PRODUCT_ID=...
78
+ POLAR_SERVER=sandbox
79
+
80
+ RUSTFS_ACCESS_KEY=...
81
+ RUSTFS_SECRET_KEY=...
82
+ RUSTFS_BUCKET_NAME=...
83
+ RUSTFS_ENDPOINT=...
84
+ RUSTFS_PUBLIC_URL=...
85
+
86
+ SENTRY_DSN=...
87
+ ```
88
+
89
+ Object Storage kann alternativ ueber die `R2_*` Variablen konfiguriert werden.
90
+ Details stehen in [docs/object-storage.md](docs/object-storage.md).
91
+
92
+ ## Wie die Hauptteile zusammenhaengen
93
+
94
+ Auth laeuft ueber Better Auth in `lib/auth/server.ts`. Die Daten liegen in
95
+ Postgres, E-Mails werden ueber Resend verschickt, OAuth ist optional ueber
96
+ Google und GitHub aktiv.
97
+
98
+ Projekte und Templates werden in Postgres gespeichert. Das aktuelle Schema
99
+ steht in `db.txt`; Schema-Aenderungen werden direkt in Postgres ausgefuehrt und
100
+ danach dort dokumentiert.
101
+
102
+ Checkout laeuft ueber Polar. Der kanonische Einstieg ist
103
+ `app/checkout/route.ts`, nicht direkt der Better-Auth-Checkout. Details stehen
104
+ in [docs/checkout.md](docs/checkout.md).
105
+
106
+ Bild-Uploads gehen ueber `app/api/uploads/r2/route.ts` in einen S3-kompatiblen
107
+ Storage. Die oeffentliche URL kommt aus `RUSTFS_PUBLIC_URL`, `R2_PUBLIC_URL`
108
+ oder `PUBLIC_DEV_URL`.
109
+
110
+ Template-Previews laufen in einer sandboxed `iframe/srcDoc` Runtime. Oeffentlich
111
+ sichtbar werden Community-Templates nach erfolgreicher automatischer Validierung.
112
+
113
+ Der Deploy Worker nimmt unveroeffentlichte Projekte aus der Datenbank, liest die
114
+ kompilierten Template-Dateien, validiert Domains und schreibt statische Sites
115
+ nach `SITES_DIR` Standard: `/etc/caddy/sites`.
116
+
117
+ ## Deployment
118
+
119
+ Bei jedem Push auf `main` fuehrt GitHub Actions `.github/workflows/deploy.yaml`
120
+ aus:
121
+
122
+ 1. Per SSH auf den Server verbinden und `origin/main` auschecken.
123
+ 2. `npm ci` ausfuehren.
124
+ 3. Repository-Templates in die Datenbank und den Preview-Storage publizieren.
125
+ 4. `npm run build` ausfuehren.
126
+ 5. Den `noweb`-Service neu starten.
127
+
128
+ Die GitHub-Secrets muessen gesetzt sein:
129
+
130
+ - `SERVER_HOST`
131
+ - `SERVER_USER`
132
+ - `SSH_PRIVATE_KEY`
133
+
134
+ Auf dem Server muessen ausserdem Node, npm, die Runtime-Env-Variablen und der
135
+ `noweb` systemd-Service vorhanden sein.
136
+
137
+ ## Aenderungs-Checkliste
138
+
139
+ Wenn du etwas aenderst, pruefe die abhaengigen Stellen mit:
140
+
141
+ - Startseite, Preise oder Marketingtext:
142
+ `app/page.tsx`, `app/HomeSections.tsx`, `app/Footer.tsx`, `app/menu.tsx`,
143
+ `docs/checkout.md` bei Preis-/Checkout-Aenderungen.
144
+ - Polar-Produkte oder Checkout:
145
+ `app/checkout/route.ts`, `lib/auth/server.ts`, `lib/polar.ts`,
146
+ `docs/checkout.md`, Env-Variablen auf Server/GitHub.
147
+ - Auth, Login, Verifizierung oder OAuth:
148
+ `lib/auth/server.ts`, `app/auth/**`, Datenbanktabellen und Resend/OAuth-Env.
149
+ - Datenbank-Schema oder Query-Format:
150
+ `lib/db.ts`, Server Actions in `app/**/actions.ts`, `scripts/deploy-worker.mjs`,
151
+ `db.txt`.
152
+ - Template-Felder oder Customizations:
153
+ Template-Definition, Editor-Komponenten in `lib/editor/` und
154
+ `app/my-templates/[id]/`, Deploy Worker, `templates/index.ts` nur ueber den
155
+ Sync-Prozess aktualisieren.
156
+ - Template-Doku:
157
+ passende Seite in `app/docs/` aktualisieren und den Eintrag in
158
+ `app/docs/guides.ts` pruefen.
159
+ - Bild-Uploads oder Storage:
160
+ `lib/object-storage.ts`, `app/api/uploads/r2/route.ts`,
161
+ `docs/object-storage.md`, Storage-Env auf dem Server.
162
+ - Projekt-Deployment oder Domains:
163
+ `scripts/deploy-worker.mjs`, Caddy/systemd-Konfiguration, `docs/deploy-worker.md`.
164
+ - GitHub Deploy:
165
+ `.github/workflows/deploy.yaml`, GitHub-Secrets, Serverpfad `~/noweb`,
166
+ systemd-Service `noweb`.
167
+ - Neue oder geaenderte Produktfunktion:
168
+ vorhandene Doku aktualisieren oder eine kleine neue Seite in `docs/` oder
169
+ `app/docs/` anlegen.
170
+
171
+ ## Launch-Blocker
172
+
173
+ - Echte Betreiberangaben in `app/imprint/page.tsx`, `app/privacy/page.tsx` und
174
+ `app/terms/page.tsx` eintragen.
175
+ - Lokale und Server-Env pruefen. `POLAR_SERVER` darf nur `sandbox` oder
176
+ `production` sein.
177
+ - Monitoring setzen: `SENTRY_DSN` oder dokumentierte Alternative.
178
+ - `npm run ci` muss vor Production gruen sein.
179
+
180
+ ## Wichtige Hinweise
181
+
182
+ - Dieses Projekt nutzt Next.js 16. Vor groesseren Next-Aenderungen die lokalen
183
+ Docs in `node_modules/next/dist/docs/` lesen.
184
+ - `templates/index.ts` ist generiert. Nicht manuell pflegen.
185
+ - JSON-Spalten muessen beim Schreiben mit `JSON.stringify(...)` gespeichert
186
+ werden.
187
+ - Secrets gehoeren nicht ins Repository.
188
+ - Vor einem Deploy `npm run ci` ausfuehren.
@@ -0,0 +1,252 @@
1
+ export type ScalarType =
2
+ | "text"
3
+ | "markdown"
4
+ | "image"
5
+ | "icon"
6
+ | "color"
7
+ | "number"
8
+ | "range"
9
+ | "boolean"
10
+ | "select";
11
+
12
+ export type ScalarValue = string | number | boolean;
13
+ export type CollectionItem = Record<string, ScalarValue>;
14
+ export type CustomizationValue = ScalarValue | string[] | CollectionItem[];
15
+ export type CustomizationValues = Record<string, CustomizationValue>;
16
+
17
+ type FieldOptions = {
18
+ min?: number;
19
+ max?: number;
20
+ step?: number;
21
+ options?: string[];
22
+ };
23
+
24
+ export type CollectionField = FieldOptions & {
25
+ key: string;
26
+ name: string;
27
+ type: ScalarType;
28
+ defaultValue: ScalarValue;
29
+ };
30
+
31
+ export type TemplateCustomization = FieldOptions & {
32
+ fieldId: number;
33
+ name: string;
34
+ type: ScalarType | "array" | "collection";
35
+ defaultValue: CustomizationValue;
36
+ fields?: CollectionField[];
37
+ };
38
+
39
+ const SCALAR_TYPES: ScalarType[] = [
40
+ "text", "markdown", "image", "icon", "color",
41
+ "number", "range", "boolean", "select",
42
+ ];
43
+
44
+ export function normalizeTemplateCustomizations(value: unknown) {
45
+ const parsed = typeof value === "string" ? parseJson(value, []) : value;
46
+ return Array.isArray(parsed)
47
+ ? parsed.flatMap((item) => {
48
+ const field = readField(item, true);
49
+ return field ? [field] : [];
50
+ })
51
+ : [];
52
+ }
53
+
54
+ export function validateTemplateCustomizations(value: unknown) {
55
+ if (!Array.isArray(value)) throw new Error("Template customizations must be an array.");
56
+ return value.map((item, index) => {
57
+ const field = readField(item, false);
58
+ if (!field) throw new Error(`Customization ${index + 1} is invalid.`);
59
+ return field;
60
+ });
61
+ }
62
+
63
+ export function normalizeCustomizationValues(value: unknown): CustomizationValues | null {
64
+ const parsed = typeof value === "string" ? parseJson(value, null) : value;
65
+ return isRecord(parsed) && Object.values(parsed).every(isCustomizationValue)
66
+ ? parsed as CustomizationValues
67
+ : null;
68
+ }
69
+
70
+ export function restoreCustomizationValues(
71
+ current: CustomizationValues,
72
+ previous: CustomizationValues,
73
+ fieldIds: string[],
74
+ ) {
75
+ const restored = { ...current };
76
+ for (const fieldId of fieldIds) {
77
+ if (Object.hasOwn(previous, fieldId)) restored[fieldId] = previous[fieldId];
78
+ else delete restored[fieldId];
79
+ }
80
+ return restored;
81
+ }
82
+
83
+ export function normalizeCustomizationValue(
84
+ field: TemplateCustomization,
85
+ value: unknown,
86
+ ): CustomizationValue | undefined {
87
+ if (field.type === "array") {
88
+ return Array.isArray(value) && value.length <= 50 &&
89
+ value.every((item) => typeof item === "string" && item.length <= 20_000)
90
+ ? value
91
+ : undefined;
92
+ }
93
+
94
+ if (field.type === "collection") {
95
+ if (!Array.isArray(value) || value.length > 50 || !field.fields?.length) return undefined;
96
+ const items = value.map((item) => {
97
+ if (!isRecord(item)) return null;
98
+ const normalized: CollectionItem = {};
99
+ for (const child of field.fields!) {
100
+ const childValue = normalizeScalarValue(child, item[child.key]);
101
+ if (childValue === undefined) return null;
102
+ normalized[child.key] = childValue;
103
+ }
104
+ return normalized;
105
+ });
106
+ return items.some((item) => item === null) ? undefined : items as CollectionItem[];
107
+ }
108
+
109
+ const type = field.type;
110
+ if (!isScalarType(type)) return undefined;
111
+ return normalizeScalarValue({ ...field, type }, value);
112
+ }
113
+
114
+ function readField(value: unknown, loose: boolean): TemplateCustomization | null {
115
+ if (!isRecord(value)) return null;
116
+ const fieldId = Number(value.fieldId);
117
+ const name = text(value.name);
118
+ const type = value.type === "option" ? "select" : value.type;
119
+ if (!Number.isInteger(fieldId) || fieldId < 1 || !name) return null;
120
+
121
+ if (type === "array") {
122
+ const defaultValue = strings(value.defaultValue);
123
+ if (!loose && !defaultValue) return null;
124
+ return { fieldId, name, type, defaultValue: defaultValue ?? [] };
125
+ }
126
+
127
+ if (type === "collection") {
128
+ const fields = Array.isArray(value.fields)
129
+ ? value.fields.flatMap((item) => {
130
+ const field = readCollectionField(item, loose);
131
+ return field ? [field] : [];
132
+ })
133
+ : [];
134
+ if (!loose && new Set(fields.map((field) => field.key)).size !== fields.length) return null;
135
+ const defaultValue = collection(value.defaultValue, fields);
136
+ if (!loose && (!Array.isArray(value.fields) || !defaultValue)) return null;
137
+ return { fieldId, name, type, fields, defaultValue: defaultValue ?? [] };
138
+ }
139
+
140
+ if (!isScalarType(type)) return null;
141
+ const defaultValue = scalar(value.defaultValue, type);
142
+ if (!loose && defaultValue === null) return null;
143
+ return { fieldId, name, type, defaultValue: defaultValue ?? scalarDefault(type), ...fieldOptions(value, type) };
144
+ }
145
+
146
+ function readCollectionField(value: unknown, loose: boolean): CollectionField | null {
147
+ if (!isRecord(value) || !isScalarType(value.type)) return null;
148
+ const key = text(value.key);
149
+ const name = text(value.name);
150
+ const defaultValue = scalar(value.defaultValue, value.type);
151
+ if (!key || !name || (!loose && defaultValue === null)) return null;
152
+ return { key, name, type: value.type, defaultValue: defaultValue ?? scalarDefault(value.type), ...fieldOptions(value, value.type) };
153
+ }
154
+
155
+ function fieldOptions(value: Record<string, unknown>, type: ScalarType) {
156
+ return {
157
+ ...(number(value.min) !== null ? { min: number(value.min)! } : {}),
158
+ ...(number(value.max) !== null ? { max: number(value.max)! } : {}),
159
+ ...(number(value.step) !== null && number(value.step)! > 0 ? { step: number(value.step)! } : {}),
160
+ ...(type === "select" ? { options: strings(value.options) ?? [] } : {}),
161
+ };
162
+ }
163
+
164
+ function collection(value: unknown, fields: CollectionField[]) {
165
+ if (!Array.isArray(value)) return null;
166
+ const items = value.flatMap((item) => {
167
+ if (!isRecord(item)) return [];
168
+ return [Object.fromEntries(fields.map((field) => [
169
+ field.key,
170
+ scalar(item[field.key], field.type) ?? field.defaultValue,
171
+ ])) as CollectionItem];
172
+ });
173
+ return items.length === value.length ? items : null;
174
+ }
175
+
176
+ function isCustomizationValue(value: unknown): value is CustomizationValue {
177
+ if (typeof value === "string" || typeof value === "boolean") return true;
178
+ if (typeof value === "number") return Number.isFinite(value);
179
+ if (!Array.isArray(value)) return false;
180
+ return value.every((item) => typeof item === "string") ||
181
+ value.every((item) => isRecord(item) && Object.values(item).every(isScalarValue));
182
+ }
183
+
184
+ function scalar(value: unknown, type: ScalarType): ScalarValue | null {
185
+ if (type === "number" || type === "range") return number(value);
186
+ if (type === "boolean") return typeof value === "boolean" ? value : null;
187
+ return typeof value === "string" ? value : null;
188
+ }
189
+
190
+ function normalizeScalarValue(
191
+ field: { type: ScalarType; min?: number; max?: number; options?: string[] },
192
+ value: unknown,
193
+ ): ScalarValue | undefined {
194
+ if (field.type === "number" || field.type === "range") {
195
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
196
+ if (field.min !== undefined && value < field.min) return undefined;
197
+ if (field.max !== undefined && value > field.max) return undefined;
198
+ return value;
199
+ }
200
+ if (field.type === "boolean") return typeof value === "boolean" ? value : undefined;
201
+ if (typeof value !== "string" || value.length > 20_000) return undefined;
202
+ if (field.type === "select" && field.options?.length && !field.options.includes(value)) return undefined;
203
+ if (field.type === "color" && !/^#[\da-f]{3}([\da-f]{3})?$/i.test(value)) return undefined;
204
+ if (field.type === "icon" && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) return undefined;
205
+ return value;
206
+ }
207
+
208
+ function scalarDefault(type: ScalarType): ScalarValue {
209
+ if (type === "number" || type === "range") return 0;
210
+ if (type === "boolean") return false;
211
+ if (type === "icon") return "circle";
212
+ return "";
213
+ }
214
+
215
+ function isScalarValue(value: unknown): value is ScalarValue {
216
+ return typeof value === "string" || typeof value === "boolean" ||
217
+ (typeof value === "number" && Number.isFinite(value));
218
+ }
219
+
220
+ function isScalarType(value: unknown): value is ScalarType {
221
+ return SCALAR_TYPES.includes(value as ScalarType);
222
+ }
223
+
224
+ function number(value: unknown) {
225
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
226
+ }
227
+
228
+ function strings(value: unknown) {
229
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
230
+ }
231
+
232
+ function text(value: unknown) {
233
+ return typeof value === "string" ? value.trim() : "";
234
+ }
235
+
236
+ function isRecord(value: unknown): value is Record<string, unknown> {
237
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
238
+ }
239
+
240
+ function parseJson<T>(value: string, fallback: T) {
241
+ let parsed: unknown = value;
242
+
243
+ for (let i = 0; i < 4 && typeof parsed === "string"; i++) {
244
+ try {
245
+ parsed = JSON.parse(parsed) as unknown;
246
+ } catch {
247
+ return fallback;
248
+ }
249
+ }
250
+
251
+ return parsed;
252
+ }
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "nowebcli",
3
+ "version": "0.2.1",
4
+ "description": "Create, preview and publish community templates for Noweb",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/lnny8/noweb.git"
9
+ },
10
+ "files": [
11
+ "lib/customizations.ts",
12
+ "scripts/noweb.mjs",
13
+ "templates/compileTailwind.ts",
14
+ "templates/shadersRuntime.tsx",
15
+ "templates/templateCompiler.ts",
16
+ "templates/templateRuntime.tsx",
17
+ "templates/validateTemplateCode.ts",
18
+ "tsconfig.json"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "bin": {
24
+ "noweb": "scripts/noweb.mjs",
25
+ "nowebcli": "scripts/noweb.mjs"
26
+ },
27
+ "scripts": {
28
+ "dev": "next dev --webpack",
29
+ "build": "next build",
30
+ "start": "next start",
31
+ "lint": "eslint --max-warnings=0",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "vitest run",
34
+ "test:e2e": "playwright test",
35
+ "screenshots": "tsx scripts/generate-template-screenshots.ts",
36
+ "template": "node scripts/noweb.mjs",
37
+ "template:publish": "node --env-file-if-exists=.env --env-file-if-exists=.env.local scripts/noweb.mjs publish",
38
+ "migrate:cli-auth": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-cli-auth.ts",
39
+ "migrate:project-snapshots": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-project-snapshots.ts",
40
+ "migrate:project-data": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-project-data.ts",
41
+ "migrate:template-ids": "tsx scripts/migrate-positive-template-ids.ts",
42
+ "ci": "npm audit --omit=dev --audit-level=high && npm run typecheck && npm run lint && npm run test && npm run build && npm run test:e2e"
43
+ },
44
+ "dependencies": {
45
+ "@ai-sdk/openai": "^4.0.24",
46
+ "@ai-sdk/react": "^4.0.45",
47
+ "@aws-sdk/client-s3": "^3.1055.0",
48
+ "@heroicons/react": "^2.2.0",
49
+ "@monaco-editor/react": "^4.7.0",
50
+ "@polar-sh/better-auth": "^1.8.4",
51
+ "@polar-sh/nextjs": "^0.9.6",
52
+ "@polar-sh/sdk": "^0.47.1",
53
+ "@react-three/drei": "^10.7.7",
54
+ "@react-three/fiber": "^9.6.1",
55
+ "@react-three/rapier": "^2.2.0",
56
+ "@tailwindcss/postcss": "^4",
57
+ "@types/pg": "^8.20.0",
58
+ "@uiw/react-textarea-code-editor": "^3.1.1",
59
+ "ai": "^7.0.42",
60
+ "better-auth": "1.7.2",
61
+ "clsx": "^2.1.1",
62
+ "esbuild": "^0.28.1",
63
+ "fumadocs-core": "^16.15.11",
64
+ "fumadocs-ui": "^16.15.11",
65
+ "geist": "^1.7.2",
66
+ "gsap": "^3.15.0",
67
+ "html2canvas-pro": "^2.3.3",
68
+ "lucide-react": "^1.14.0",
69
+ "monaco-editor": "^0.53.0",
70
+ "motion": "^12.40.0",
71
+ "next": "^16.2.10",
72
+ "ogl": "^1.0.11",
73
+ "pg": "^8.22.0",
74
+ "playwright": "^1.61.1",
75
+ "postcss": "^8.5.6",
76
+ "postgres": "^3.4.9",
77
+ "react": "19.2.4",
78
+ "react-dom": "19.2.4",
79
+ "react-markdown": "^10.1.0",
80
+ "remark-gfm": "^4.0.1",
81
+ "shaders": "^3.0.445",
82
+ "shiki": "^4.3.0",
83
+ "tailwindcss": "^4",
84
+ "three": "^0.184.0",
85
+ "tsx": "^4.22.4",
86
+ "typescript": "^5",
87
+ "zod": "^4.4.3"
88
+ },
89
+ "devDependencies": {
90
+ "@playwright/test": "^1.61.1",
91
+ "@tailwindcss/vite": "^4.3.1",
92
+ "@types/node": "^20",
93
+ "@types/react": "^19",
94
+ "@types/react-dom": "^19",
95
+ "@types/three": "^0.184.1",
96
+ "@vitejs/plugin-react": "^6.0.3",
97
+ "eslint": "^9",
98
+ "eslint-config-next": "^16.2.10",
99
+ "shadcn": "^4.16.1",
100
+ "vite": "^8.1.0",
101
+ "vitest": "^4.1.10"
102
+ }
103
+ }