nowebcli 0.2.3 → 0.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nowebcli",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Create, preview and publish community templates for Noweb",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,8 @@
35
35
  "screenshots": "tsx scripts/generate-template-screenshots.ts",
36
36
  "template": "node scripts/noweb.mjs",
37
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",
38
+ "migrate": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/migrate-database.ts",
40
39
  "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
40
  "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
41
  },
44
42
  "dependencies": {
@@ -60,8 +58,6 @@
60
58
  "better-auth": "1.7.2",
61
59
  "clsx": "^2.1.1",
62
60
  "esbuild": "^0.28.1",
63
- "fumadocs-core": "^16.15.11",
64
- "fumadocs-ui": "^16.15.11",
65
61
  "geist": "^1.7.2",
66
62
  "gsap": "^3.15.0",
67
63
  "html2canvas-pro": "^2.3.3",
package/scripts/noweb.mjs CHANGED
@@ -7,6 +7,7 @@ import { createServer } from "node:http";
7
7
  import { access, chmod, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
8
8
  import { homedir } from "node:os";
9
9
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
10
+ import { createInterface } from "node:readline/promises";
10
11
  import { fileURLToPath, pathToFileURL } from "node:url";
11
12
  import { register } from "tsx/esm/api";
12
13
 
@@ -34,15 +35,15 @@ function usage() {
34
35
  check validate and compile this template
35
36
  login connect the CLI to your Noweb account
36
37
  list list your uploaded templates
37
- download <slug|id> restore one into the current folder
38
+ download <slug> restore one into the current folder
38
39
  upload publish this template directly to Noweb
40
+ delete [slug] delete one of your templates
39
41
  publish [slug] publish validated templates (deployment only)`);
40
42
  }
41
43
 
42
44
  async function createTemplate(value) {
43
45
  const validSlug = requireSlug(value || basename(root));
44
46
  const directory = root;
45
- const name = validSlug.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
46
47
 
47
48
  for (const file of ["Template.tsx", "customizations.json", "template.json"]) {
48
49
  if (existsSync(join(directory, file))) throw new Error(`Template file already exists: ${file}`);
@@ -50,7 +51,7 @@ async function createTemplate(value) {
50
51
 
51
52
  await mkdir(join(directory, "public"), { recursive: true });
52
53
  await Promise.all([
53
- writeFile(join(directory, "template.json"), `${JSON.stringify({ slug: validSlug, name, type: "other" }, null, 2)}\n`),
54
+ writeFile(join(directory, "template.json"), `${JSON.stringify({ slug: validSlug, type: "other" }, null, 2)}\n`),
54
55
  writeFile(join(directory, "Template.tsx"), starterTemplate),
55
56
  writeFile(join(directory, "customizations.json"), starterCustomizations),
56
57
  writeFile(join(directory, "public/.gitkeep"), ""),
@@ -101,9 +102,7 @@ async function buildTemplate(directory, slugOverride, assetBase = "/assets") {
101
102
  ]);
102
103
  const metadata = JSON.parse(metadataSource);
103
104
  const currentSlug = requireSlug(slugOverride || metadata?.slug);
104
- if (!metadata || typeof metadata.name !== "string" || !metadata.name.trim() || !types.has(metadata.type)) {
105
- throw new Error(`${currentSlug}: template.json needs a name and type (${[...types].join(", ")}).`);
106
- }
105
+ if (!metadata || !types.has(metadata.type)) throw new Error(`${currentSlug}: template.json needs a type (${[...types].join(", ")}).`);
107
106
 
108
107
  const [{ compileTemplateAssets }, { validateTemplateCode }, { validateTemplateCustomizations }] = await Promise.all([
109
108
  load("templates/templateCompiler.ts"),
@@ -167,7 +166,6 @@ async function upload() {
167
166
  const credentials = await requireCredentials();
168
167
  const form = new FormData();
169
168
  form.set("slug", template.slug);
170
- form.set("name", template.name);
171
169
  form.set("type", template.type);
172
170
  form.set("source", template.source);
173
171
  form.set("customizations", JSON.stringify(template.customizations));
@@ -185,6 +183,29 @@ async function upload() {
185
183
  console.log(`Published ${template.slug}: ${new URL(result.url, apiBase)}`);
186
184
  }
187
185
 
186
+ async function deleteTemplate(value) {
187
+ const selectedSlug = value
188
+ ? requireSlug(value)
189
+ : requireSlug(JSON.parse(await readFile(join(root, "template.json"), "utf8")).slug);
190
+
191
+ if (!cliArgs.includes("--yes")) {
192
+ if (!process.stdin.isTTY) throw new Error("Pass --yes to confirm deletion.");
193
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
194
+ const answer = await prompt.question(`Delete ${selectedSlug} permanently? [y/N] `);
195
+ prompt.close();
196
+ if (!/^y(?:es)?$/i.test(answer.trim())) return;
197
+ }
198
+
199
+ const credentials = await requireCredentials();
200
+ const response = await fetch(`${apiBase}/api/templates/download?slug=${encodeURIComponent(selectedSlug)}`, {
201
+ method: "DELETE",
202
+ headers: { Authorization: `Bearer ${credentials.token}` },
203
+ });
204
+ const result = await response.json().catch(() => ({}));
205
+ if (!response.ok) throw new Error(result.error || `Delete failed (${response.status}).`);
206
+ console.log(`Deleted ${selectedSlug}`);
207
+ }
208
+
188
209
  async function listTemplates() {
189
210
  const credentials = await requireCredentials();
190
211
  const response = await fetch(`${apiBase}/api/templates/download`, {
@@ -223,7 +244,7 @@ async function downloadTemplate(value) {
223
244
  if (!response.ok) throw new Error(result.error || `Download failed (${response.status}).`);
224
245
  const template = result.template;
225
246
  if (
226
- !template || typeof template.metadata?.slug !== "string" || typeof template.metadata?.name !== "string" ||
247
+ !template || typeof template.metadata?.slug !== "string" ||
227
248
  !types.has(template.metadata?.type) || typeof template.source !== "string" ||
228
249
  !Array.isArray(template.customizations) || !Array.isArray(template.assets) || template.assets.length > 200
229
250
  ) throw new Error("The server returned an invalid template.");
@@ -369,28 +390,22 @@ async function publish(value) {
369
390
  await uploadTemplateAssets(template, storageApi.createObjectStorageClient(storage), storage.bucketName);
370
391
  const [published] = await sql`
371
392
  INSERT INTO templates (
372
- name, type, component, compiled_js, compiled_css, customizations,
373
- author_id, approved_by, source_slug
393
+ slug, type, component, compiled_js, compiled_css, customizations, author_id
374
394
  ) VALUES (
375
- ${template.name}, ${template.type}, ${template.source}, ${template.js},
376
- ${template.css}, ${sql.json(template.customizations)}, 'github', 'github',
377
- ${template.slug}
395
+ ${template.slug}, ${template.type}, ${template.source}, ${template.js},
396
+ ${template.css}, ${sql.json(template.customizations)}, 'github'
378
397
  )
379
- ON CONFLICT (source_slug) DO UPDATE SET
380
- name = EXCLUDED.name,
398
+ ON CONFLICT (slug) DO UPDATE SET
381
399
  type = EXCLUDED.type,
382
400
  component = EXCLUDED.component,
383
401
  compiled_js = EXCLUDED.compiled_js,
384
402
  compiled_css = EXCLUDED.compiled_css,
385
- customizations = EXCLUDED.customizations,
386
- approved_by = 'github'
387
- WHERE templates.name IS DISTINCT FROM EXCLUDED.name
388
- OR templates.type IS DISTINCT FROM EXCLUDED.type
403
+ customizations = EXCLUDED.customizations
404
+ WHERE templates.type IS DISTINCT FROM EXCLUDED.type
389
405
  OR templates.component IS DISTINCT FROM EXCLUDED.component
390
406
  OR templates.compiled_js IS DISTINCT FROM EXCLUDED.compiled_js
391
407
  OR templates.compiled_css IS DISTINCT FROM EXCLUDED.compiled_css
392
408
  OR templates.customizations::jsonb IS DISTINCT FROM EXCLUDED.customizations::jsonb
393
- OR templates.approved_by IS DISTINCT FROM 'github'
394
409
  RETURNING id
395
410
  `;
396
411
  if (!published) {
@@ -498,7 +513,7 @@ async function uploadTemplateAssets(template, client, bucketName) {
498
513
  }
499
514
 
500
515
  function previewHtml(template) {
501
- return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(template.name)}</title><style>${escapeClosingTag(template.css, "style")}</style></head><body><div id="root"></div><script>${escapeClosingTag(template.js, "script")}</script><script>window.__nowebRenderTemplate(document.getElementById("root"), {});</script></body></html>`;
516
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(template.slug)}</title><style>${escapeClosingTag(template.css, "style")}</style></head><body><div id="root"></div><script>${escapeClosingTag(template.js, "script")}</script><script>window.__nowebRenderTemplate(document.getElementById("root"), {});</script></body></html>`;
502
517
  }
503
518
 
504
519
  function requireSlug(value) {
@@ -617,6 +632,7 @@ try {
617
632
  else if (command === "list") await listTemplates();
618
633
  else if (command === "download") await downloadTemplate(slug);
619
634
  else if (command === "upload") await upload();
635
+ else if (command === "delete") await deleteTemplate(slug);
620
636
  else if (command === "publish") await publish(slug);
621
637
  else if (command === "help" || command === "--help" || command === "-h") usage();
622
638
  else throw new Error(`Unknown command: ${command}`);
package/README.md DELETED
@@ -1,188 +0,0 @@
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.