@iddp/create-vefk-app 0.7.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/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # create-vefk-app
2
+
3
+ Scaffolder der **VEFK-Tools-Suite**: erzeugt aus `vefk-app-template` ein neues,
4
+ sofort startbares Tool — Template klonen, Platzhalter ersetzen (Toolname,
5
+ Markenfarbe, Auth-Profil, 2FA/Mail, erster Mandant, Server-Slug, Port-Paar,
6
+ lokale Dev-Ports), `backend/.env` anlegen, `git init` + Initial-Commit. Das Ergebnis ist für die **Zwei-Server-Welt** (test/prod,
7
+ `deploy`-User, `127.0.0.1`) vorbereitet — `deploy.sh`, `ecosystem.config.cjs` und
8
+ `CLAUDE-DEPLOYMENT.md` sind ausgefüllt.
9
+
10
+ ```bash
11
+ npm create @iddp/vefk-app@latest <ordner>
12
+ npm create @iddp/vefk-app@latest . # in den aktuellen, leeren Ordner
13
+ # fragt interaktiv: Zielordner (. = aktueller Ordner), Anzeigename, Auth-Profil
14
+ # (Standard both), Login ohne Mandant (E-Mail + Passwort oder Magic-Code), 2FA,
15
+ # Markenfarbe, erster Mandant, Server-Slug, Prod-Port (aus dem Port-Register — dort
16
+ # auch eintragen!), npm install (Standard ja) …
17
+ ```
18
+
19
+ Login-Wege, die das Ergebnis kennt (Kit ≥ 0.6.0): je Mandant E-Mail + Passwort (+ 2FA),
20
+ E-Mail-Code für Nutzer, die `create-user` **ohne Passwort** angelegt hat, und ein
21
+ **Mandanten-Zugang** nur mit Passwort (`npm run zugang`); ohne Mandant E-Mail + Passwort
22
+ (`--login password`) oder Magic-Code mit Selbst-Registrierung (`--login magic`).
23
+
24
+ **Ordnername = Server-Slug** (`/opt/<slug>`, PM2 `<slug>`, `<slug>.<domain>`, Paketnamen):
25
+ nur `a-z`, `0-9`, `-`, kein Präfix-Zwang — `gbu`, `seminar`, `vefkev-user` sind gute
26
+ Namen. `--slug` übersteuert, wenn der Ordner lokal anders heißen soll. Ein schon
27
+ vorhandener Ordner ist als Ziel erlaubt, solange er leer ist; sonst bricht das CLI ab,
28
+ ohne etwas anzufassen.
29
+
30
+ Solange das Paket noch nicht auf npm liegt, geht derselbe Aufruf direkt vom Repo:
31
+
32
+ ```bash
33
+ npx github:esgoet/create-vefk-app <ordner>
34
+ ```
35
+
36
+ ## Voraussetzungen
37
+
38
+ - Node ≥ 20.11; die Suite läuft lokal und auf den Servern auf **Node 24** (`.nvmrc`). Vor dem Aufruf
39
+ dieselbe Version aktiv haben (`fnm use 24` / `nvm use 24`): native Module (better-sqlite3, bcrypt) werden für das aktive Node gebaut;
40
+ wechselt nvm nach dem `cd` ins Projekt auf eine andere Version, startet das Backend mit
41
+ `ERR_DLOPEN_FAILED`. Der Scaffolder warnt bei Abweichung und nennt `npm --prefix backend rebuild`.
42
+ - `git` mit Zugriff auf das private Repo `esgoet/vefk-app-template`
43
+ (dieselben Credentials, die auch `npm install` für die `@iddp`-git-Dependencies braucht)
44
+
45
+ ## Optionen (für Skripte / nicht-interaktiv)
46
+
47
+ ```
48
+ create-vefk-app [<ordner>] [Optionen]
49
+
50
+ --dir <pfad> Zielordner (statt des Positionsarguments; `.` erlaubt)
51
+ --name <text> Anzeigename (Header, Mail-Betreff)
52
+ --auth <profil> tenant | generic | both (Standard: both)
53
+ --login <verfahren> Login ohne Mandant (generic/both): magic (Code per E-Mail,
54
+ Nutzer legen sich selbst an; Standard) | password (E-Mail + Passwort,
55
+ Nutzer legt der Admin an — der „normale" Login)
56
+ --brand <#rrggbb> Markenfarbe (helle Variante wird abgeleitet; --brand-light überschreibt)
57
+ --two-factor / --no-two-factor E-Mail-2FA nach dem Passwort für Mandanten-Logins (tenant/both)
58
+ bzw. den Passwort-Login (--login password); gilt nicht für Magic-Code
59
+ und Mandanten-Zugang; pro Mandant später in /admin umschaltbar
60
+ --mail / --no-mail SMTP-Block in .env.example einkommentieren
61
+ --tenant <slug> erster Mandant (tenant/both)
62
+ --anmeldung <art> erster Mandant: konto (E-Mail + Passwort / E-Mail-Code, Standard) |
63
+ zugang (nur gemeinsames Mandanten-Passwort ohne E-Mail; Passwort danach per
64
+ npm run zugang oder /admin setzen); später in /admin → Einstellungen umschaltbar
65
+ --generic-tenant <slug> optional: interner Slug des offenen Bereichs (Standard app, wird nicht gefragt)
66
+ --owner-email <mail> optional (--login magic): weitere Adresse, die beim Auto-Anlegen admin wird
67
+ (die Adressen aus --admin-email sind es ohnehin; wird nicht gefragt)
68
+ --admin-email <mail> Admin-Bereich /admin (Google-Konto oder E-Mail-Code): darf sich anmelden;
69
+ mehrfach oder Komma-getrennt (Standard: Betreiber-Adressen aus dem Template)
70
+ --daten <adapter> sqlite (Standard: eine Datei backend/data/app.sqlite) | json (Datei je Collection)
71
+ --slug <slug> Server-Slug → /opt/<slug>, PM2 <slug>, <slug>.<domain> (Standard: Ordnername)
72
+ --port <nnnn> Prod-Port aus dem Port-Register, test = +1. Kein Standardwert:
73
+ ohne --port bleiben die @@PORT_*@@-Token stehen und werden gemeldet.
74
+ Lokal: Backend = Prod-Port, Vite = 51 + letzte zwei Stellen (3130 → 3130/5130)
75
+ --dev-port <nnnn> lokaler Backend-Port, falls der Prod-Port lokal belegt ist (Vite folgt)
76
+ --template <pfad|url> andere Template-Quelle (auch lokaler Pfad — gut zum Testen)
77
+ --install nach dem Anlegen npm run install:all (Standard, auch mit --yes)
78
+ --no-install … überspringen
79
+ --no-git kein git init / Initial-Commit
80
+ --no-local-env keine backend/.env anlegen (Standard: Kopie von .env.example mit
81
+ NODE_ENV=development, VEFK_DEV_LOGIN=true, zufälligem SESSION_SECRET)
82
+ --yes nichts fragen, Standardwerte nehmen
83
+ ```
84
+
85
+ Beispiele:
86
+
87
+ ```bash
88
+ npx @iddp/create-vefk-app protokolle --name "Prüfprotokolle" \
89
+ --auth tenant --two-factor --brand "#0b5b52" --tenant kunde-a --port 3090 --yes
90
+
91
+ # mandantenlos mit „normalem" Login (E-Mail + Passwort, kein Magic-Code):
92
+ npx @iddp/create-vefk-app intern --name "Intern" --auth generic --login password --port 3092 --yes
93
+
94
+ # in einen schon angelegten, leeren Ordner (Slug = Ordnername):
95
+ mkdir vefkev-user && cd vefkev-user
96
+ npx @iddp/create-vefk-app . --name "VEFK e.V. Mitglieder" --auth tenant --port 3130 --install --yes
97
+ ```
98
+
99
+ ## Was das CLI ersetzt
100
+
101
+ | Stelle | Wert |
102
+ |---|---|
103
+ | `app.config.js` | Name, `authMode` (`tenant`/`generic`/`both`), `zweiFaktorDefault`, generic-Slug/`login`/`zweiFaktor`/Owner, Markenfarben |
104
+ | `package.json` × 3 **+ beide `package-lock.json`** | Paketnamen **aus dem Slug** (`npm ci` prüft die Lock-Namen mit) |
105
+ | `frontend/index.html`, `frontend/src/index.css` | `<title>`, `--iddp` / `--iddp-light` |
106
+ | `.env.example` | `APP_NAME`, `SMTP_FROM` (+ SMTP-Block einkommentieren bei `--mail`), `PORT` = lokaler Dev-Port |
107
+ | `frontend/vite.config.js`, `docs/08`, `CLAUDE-DEPLOYMENT.md` (Dev-Block) | lokale Dev-Ports (nur mit `--port`/`--dev-port`; sonst bleibt 3001/5173) |
108
+ | `backend/.env` (neu, gitignored) | Kopie von `.env.example` mit Dev-Flags + zufälligem `SESSION_SECRET` — nie ausgegeben |
109
+ | `deploy.sh`, `ecosystem.config.cjs`, `CLAUDE-DEPLOYMENT.md`, `docs/01`, `docs/07` | `@@APP@@` (Server-Slug), `@@APPNAME@@`, `@@PORT_PROD@@`/`@@PORT_TEST@@`, `@@DATUM@@`; Rest-Scan meldet vergessene Token in Wurzel + `docs/` |
110
+ | `backend/data/tenants/<slug>/` | erster Mandant aus `_muster`, `zweiFaktor` explizit gesetzt |
111
+ | `backend/data/tenants/beispiel/`, `.gitignore`, `frontend/src/App.jsx` | mit erstem Mandanten (oder `generic`): Demo-Mandant `beispiel` entfernt samt `.gitignore`-Whitelist (der erste Mandant ist ein echter Kunde und bleibt ungetrackt), Wurzel-Redirect `/` → `/<slug>` |
112
+ | `README.md` | wird durch ein Projekt-README des neuen Tools ersetzt |
113
+
114
+ Trifft eine Ersetzung nicht (weil sich das Template weiterentwickelt hat), bricht
115
+ nichts ab — das CLI listet die Stelle am Ende als Warnung zum Nachprüfen.
116
+
117
+ ## Entwurfsentscheidungen
118
+
119
+ - **`git clone` statt degit:** degit lädt GitHub-Tarballs und scheitert am privaten
120
+ Template-Repo ohne eigene Token-Konfiguration. `git clone --depth 1` + `.git`
121
+ entfernen nutzt die ohnehin eingerichteten Git-Credentials und kann zusätzlich
122
+ lokale Pfade (`--template ../vefk-app-template`).
123
+ - **Zero-Dependency:** nur Node-Bordmittel (`node:util` parseArgs,
124
+ `node:readline/promises`, `node:test`) — Suite-Linie „nativ statt Extra-Deps".
125
+
126
+ ## Entwickeln & Testen
127
+
128
+ ```bash
129
+ npm test # node:test — Unit + End-to-End gegen die Fixture
130
+ node bin/create-vefk-app.js probe --yes --no-install --port 3090 --template ../vefk-app-template
131
+ ```
132
+
133
+ Die Fixture (`test/fixtures/mini-template/`) friert die Template-Stellen ein, die
134
+ das CLI anfasst. Ändert sich das Template an diesen Stellen, Fixture nachziehen.
135
+
136
+ ## Auf npm veröffentlichen
137
+
138
+ Das Paket heißt `@iddp/create-vefk-app` (npm-Org `iddp`, `publishConfig.access: public`;
139
+ `files`-Whitelist: nur `bin/`, `lib/`, README). Öffentlich sind der Scaffolder und
140
+ `@iddp/vefk-app-kit`; Template und `@iddp/vefk-ui` bleiben private Repos.
141
+
142
+ ```bash
143
+ npm login
144
+ npm publish # Browser-Verifizierung → läuft nur im eigenen Terminal
145
+ npm view @iddp/create-vefk-app version
146
+ ```
147
+
148
+ Danach funktioniert `npm create @iddp/vefk-app@latest` für alle, die Git-Zugriff auf das
149
+ Template-Repo haben. Der frühere Name `create-vefk-app` (bis 0.6.0) ist per `npm deprecate`
150
+ auf das neue Paket verwiesen. Falle: `npm create` nutzt den npx-Cache (`~/.npm/_npx`) —
151
+ zum Testen immer `@latest` anhängen.
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+ // create-vefk-app — Scaffolder der VEFK-Tools-Suite.
3
+ //
4
+ // Erzeugt ein neues Tool aus vefk-app-template:
5
+ // 1. Fragen stellen (oder Flags lesen)
6
+ // 2. Template klonen (git clone --depth 1, .git entfernen)
7
+ // 3. Platzhalter ersetzen (Name, Marke, Auth-Profil, 2FA/Mail, Mandant,
8
+ // Server-Slug + Port-Paar für deploy.sh/ecosystem.config.cjs/docs)
9
+ // 4. git init + Initial-Commit (abwählbar), npm install (Standard, --no-install)
10
+ //
11
+ // Aufruf: npm create @iddp/vefk-app@latest <ordner>
12
+ // oder: npx @iddp/create-vefk-app <ordner> [Flags]
13
+ // oder: npx @iddp/create-vefk-app . [Flags] (in einen leeren Ordner)
14
+
15
+ import { parseArgs } from "node:util";
16
+ import { readFile } from "node:fs/promises";
17
+ import { resolve, basename, dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import process from "node:process";
20
+
21
+ import { stelleFragen } from "../lib/fragen.js";
22
+ import { kloneTemplate, loeseTemplateQuelle, pruefeZielordner, STANDARD_TEMPLATE } from "../lib/klonen.js";
23
+ import { wendeAntwortenAn } from "../lib/ersetzen.js";
24
+ import { initialisiereRepo } from "../lib/git.js";
25
+ import { legeLokaleEnvAn } from "../lib/lokaleEnv.js";
26
+ import { fuehreAus } from "../lib/prozesse.js";
27
+ import { leiteDevPortsAb } from "../lib/validieren.js";
28
+
29
+ const HILFE = `create-vefk-app — neues Tool der VEFK-Tools-Suite anlegen
30
+
31
+ Aufruf:
32
+ npm create @iddp/vefk-app@latest <ordner>
33
+ npx @iddp/create-vefk-app <ordner> [Optionen]
34
+ npx @iddp/create-vefk-app . [Optionen] # in den aktuellen (leeren) Ordner
35
+
36
+ Der Ordnername ist zugleich der Server-Slug (/opt/<slug>, PM2, Domain) — nur
37
+ a-z, 0-9, Bindestrich, kein Präfix-Zwang. Ein vorhandener Ordner muss leer sein.
38
+
39
+ Ohne Optionen fragt das CLI interaktiv (bei der Ordner-Frage ist . der aktuelle
40
+ Ordner). Optionen (für Skripte/CI):
41
+ --dir <pfad> Zielordner (statt des Positionsarguments)
42
+ --name <text> Anzeigename des Tools (Header, Mail-Betreff)
43
+ --auth <profil> tenant | generic | both (Standard: both)
44
+ --login <verfahren> Login ohne Mandant (generic/both):
45
+ magic (Code per E-Mail, Standard) | password (E-Mail + Passwort)
46
+ --brand <#rrggbb> Markenfarbe (Standard: #6667ab)
47
+ --brand-light <#hex> helle Markenvariante (Standard: automatisch abgeleitet)
48
+ --two-factor E-Mail-2FA nach dem Passwort (Mandanten-Logins bzw. --login password);
49
+ nicht für Magic-Code und Mandanten-Zugang; pro Mandant in /admin änderbar
50
+ --no-two-factor … explizit aus
51
+ --mail | --no-mail SMTP-Block in .env.example einkommentieren
52
+ --tenant <slug> erster Mandant (tenant/both)
53
+ --anmeldung <art> erster Mandant: konto (E-Mail + Passwort / E-Mail-Code, Standard)
54
+ | zugang (nur gemeinsames Mandanten-Passwort, ohne E-Mail)
55
+ --generic-tenant <slug> optional: interner Slug des offenen Bereichs (Standard: app)
56
+ --owner-email <mail> optional (--login magic): weitere Adresse, die beim Auto-Anlegen
57
+ admin wird — die Adressen aus --admin-email sind es ohnehin
58
+ --admin-email <mail> Admin-Bereich (/admin): darf sich anmelden; mehrfach oder
59
+ Komma-getrennt (Standard: die Betreiber-Adressen aus dem Template)
60
+ --daten <adapter> sqlite (eine Datei app.sqlite, Standard) | json (Datei je Collection)
61
+ --slug <slug> Server-Slug: /opt/<slug>, PM2 <slug>, <slug>.<domain>
62
+ (Standard: Ordnername)
63
+ --port <nnnn> Prod-Port aus dem Port-Register, test = +1 (kein Standard!)
64
+ Lokal: Backend = Prod-Port, Vite = 51 + letzte zwei Stellen
65
+ --dev-port <nnnn> lokaler Backend-Port, falls der Prod-Port lokal belegt ist
66
+ --template <pfad|url> Template-Quelle (Standard: ${STANDARD_TEMPLATE})
67
+ --install nach dem Anlegen npm run install:all ausführen (Standard)
68
+ --no-install … nicht ausführen
69
+ --no-git kein git init / Initial-Commit
70
+ --no-local-env keine backend/.env anlegen (Standard: Kopie von .env.example
71
+ mit NODE_ENV=development, VEFK_DEV_LOGIN=true, zufälligem SESSION_SECRET)
72
+ --yes nichts fragen, Standardwerte nehmen
73
+ --version, --help
74
+ `;
75
+
76
+ function leseFlags() {
77
+ const { values, positionals } = parseArgs({
78
+ allowPositionals: true,
79
+ options: {
80
+ dir: { type: "string" },
81
+ name: { type: "string" },
82
+ auth: { type: "string" },
83
+ login: { type: "string" },
84
+ brand: { type: "string" },
85
+ "brand-light": { type: "string" },
86
+ "two-factor": { type: "boolean" },
87
+ "no-two-factor": { type: "boolean" },
88
+ mail: { type: "boolean" },
89
+ "no-mail": { type: "boolean" },
90
+ tenant: { type: "string" },
91
+ anmeldung: { type: "string" },
92
+ "generic-tenant": { type: "string" },
93
+ "owner-email": { type: "string" },
94
+ "admin-email": { type: "string", multiple: true },
95
+ daten: { type: "string" },
96
+ slug: { type: "string" },
97
+ port: { type: "string" },
98
+ "dev-port": { type: "string" },
99
+ template: { type: "string" },
100
+ install: { type: "boolean" },
101
+ "no-install": { type: "boolean" },
102
+ "no-git": { type: "boolean" },
103
+ "no-local-env": { type: "boolean" },
104
+ yes: { type: "boolean" },
105
+ version: { type: "boolean" },
106
+ help: { type: "boolean", short: "h" },
107
+ },
108
+ });
109
+ return { values, positionals };
110
+ }
111
+
112
+ /** Flags → Vorgaben für stelleFragen (undefined = wird ggf. gefragt). */
113
+ function vorgabenAusFlags(values, positionals) {
114
+ const jaNeinPaar = (ja, nein) => (values[ja] ? true : values[nein] ? false : undefined);
115
+ if (values.auth && !["tenant", "generic", "both"].includes(values.auth)) {
116
+ throw new Error(`--auth muss tenant, generic oder both sein (war: ${values.auth})`);
117
+ }
118
+ if (values.login && !["magic", "password"].includes(values.login)) {
119
+ throw new Error(`--login muss magic oder password sein (war: ${values.login})`);
120
+ }
121
+ if (values.anmeldung && !["konto", "zugang"].includes(values.anmeldung)) {
122
+ throw new Error(`--anmeldung muss konto oder zugang sein (war: ${values.anmeldung})`);
123
+ }
124
+ if (values.daten && !["sqlite", "json"].includes(values.daten)) {
125
+ throw new Error(`--daten muss sqlite oder json sein (war: ${values.daten})`);
126
+ }
127
+ // Ziel: --dir vor Positionsargument; beides darf ein Pfad sein (auch `.`).
128
+ // Der Ordnername (Basis für Slug + Anzeigename) kommt aus dem aufgelösten Pfad,
129
+ // damit `.` den echten Ordnernamen liefert.
130
+ const zielPfad = values.dir ?? positionals[0];
131
+ return {
132
+ ziel: zielPfad ? basename(resolve(zielPfad)) : undefined,
133
+ zielPfad,
134
+ name: values.name,
135
+ authMode: values.auth,
136
+ genericLogin: values.login,
137
+ zweiFaktor: jaNeinPaar("two-factor", "no-two-factor"),
138
+ mail: jaNeinPaar("mail", "no-mail"),
139
+ marke: values.brand,
140
+ markeHell: values["brand-light"],
141
+ tenantSlug: values.tenant,
142
+ anmeldung: values.anmeldung,
143
+ genericSlug: values["generic-tenant"],
144
+ ownerEmail: values["owner-email"],
145
+ // --admin-email a@x.de --admin-email b@x.de oder --admin-email "a@x.de, b@x.de"
146
+ adminEmails: values["admin-email"]
147
+ ? values["admin-email"].flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean)
148
+ : undefined,
149
+ daten: values.daten,
150
+ slug: values.slug,
151
+ port: values.port,
152
+ devPort: values["dev-port"],
153
+ gitInit: values["no-git"] ? false : undefined,
154
+ // Standard: installieren (auch mit --yes); --no-install schaltet ab.
155
+ install: values["no-install"] ? false : values.install,
156
+ localEnv: values["no-local-env"] ? false : undefined,
157
+ };
158
+ }
159
+
160
+ async function main() {
161
+ const { values, positionals } = leseFlags();
162
+
163
+ if (values.help) {
164
+ process.stdout.write(HILFE);
165
+ return;
166
+ }
167
+ if (values.version) {
168
+ const eigenePkg = JSON.parse(
169
+ await readFile(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8"),
170
+ );
171
+ process.stdout.write(`${eigenePkg.version}\n`);
172
+ return;
173
+ }
174
+
175
+ const vorgaben = vorgabenAusFlags(values, positionals);
176
+ const interaktiv = !values.yes && process.stdin.isTTY === true;
177
+ const antworten = await stelleFragen(vorgaben, interaktiv);
178
+
179
+ // Zielpfad: --dir / Positionsargument (kann ein Pfad sein) oder der erfragte
180
+ // Ordnername. Vorhanden + leer ist ok (wie `npm create vite@latest .`),
181
+ // nicht leer → Abbruch, bevor irgendetwas geschrieben wird.
182
+ const zielPfad = resolve(vorgaben.zielPfad ?? antworten.zielPfad ?? antworten.ziel);
183
+ await pruefeZielordner(zielPfad);
184
+ const { devPort, vitePort } = leiteDevPortsAb(antworten.port, antworten.devPort);
185
+
186
+ if ((antworten.authMode === "generic" || antworten.authMode === "both")
187
+ && antworten.genericLogin === "magic" && antworten.mail === false) {
188
+ process.stdout.write(
189
+ `⚠ ${antworten.authMode}-Profil ohne Mail: der Magic-Code-Login braucht SMTP. ` +
190
+ "Lokal hilft VEFK_DEV_LOGIN=true, für den Betrieb docs/07 folgen.\n",
191
+ );
192
+ }
193
+
194
+ // --- 1) Klonen ------------------------------------------------------------
195
+ const quelle = await loeseTemplateQuelle(values.template ?? STANDARD_TEMPLATE);
196
+ process.stdout.write(`\nKlone Template (${quelle}) …\n`);
197
+ await kloneTemplate(quelle, zielPfad);
198
+
199
+ // --- 2) Platzhalter ersetzen ----------------------------------------------
200
+ const warnungen = await wendeAntwortenAn(zielPfad, { ...antworten, devPort, vitePort });
201
+
202
+ // --- 2b) lokale backend/.env (Zufalls-SESSION_SECRET, Dev-Flags) ----------
203
+ let envErgebnis = "uebersprungen";
204
+ if (antworten.localEnv) {
205
+ envErgebnis = await legeLokaleEnvAn(zielPfad);
206
+ if (envErgebnis === "keine-vorlage") warnungen.push(".env.example fehlt — backend/.env nicht angelegt");
207
+ }
208
+
209
+ // --- 3) git init + Initial-Commit -----------------------------------------
210
+ let gitErgebnis = { commit: false };
211
+ if (antworten.gitInit) {
212
+ gitErgebnis = await initialisiereRepo(zielPfad);
213
+ }
214
+
215
+ // --- 4) optional npm install ----------------------------------------------
216
+ // Native Module (better-sqlite3, bcrypt) werden für die Node-Version gebaut,
217
+ // die JETZT läuft. Wechselt nvm nach dem cd wegen .nvmrc auf eine andere,
218
+ // passt das Binary nicht mehr (ERR_DLOPEN_FAILED). Deshalb vorher vergleichen.
219
+ const nodeHinweis = await pruefeNodeVersion(zielPfad);
220
+ if (antworten.install) {
221
+ if (nodeHinweis) process.stdout.write(`\n⚠ ${nodeHinweis}\n`);
222
+ process.stdout.write("\nInstalliere Dependencies (npm run install:all) …\n");
223
+ await fuehreAus("npm", ["run", "install:all"], {
224
+ cwd: zielPfad,
225
+ shell: process.platform === "win32",
226
+ });
227
+ }
228
+
229
+ // --- Abschluss -------------------------------------------------------------
230
+ process.stdout.write(`\n✔ ${antworten.ziel} angelegt (${zielPfad})\n`);
231
+ process.stdout.write(` Profil: ${antworten.authMode}`);
232
+ if (antworten.authMode === "tenant" || antworten.authMode === "both") {
233
+ process.stdout.write(`, 2FA-Default: ${antworten.zweiFaktor ? "an" : "aus"}`);
234
+ if (antworten.tenantSlug) process.stdout.write(`, erster Mandant: ${antworten.tenantSlug}`);
235
+ }
236
+ if (antworten.authMode === "generic" || antworten.authMode === "both") {
237
+ process.stdout.write(`, offener Bereich: ${antworten.genericSlug} (${
238
+ antworten.genericLogin === "password" ? "E-Mail + Passwort" : "Magic-Code"})`);
239
+ if (antworten.genericLogin === "password" && antworten.authMode === "generic") {
240
+ process.stdout.write(`, 2FA: ${antworten.zweiFaktor ? "an" : "aus"}`);
241
+ }
242
+ }
243
+ process.stdout.write(`, Marke: ${antworten.marke} / ${antworten.markeHell}\n`);
244
+ process.stdout.write(` Server: /opt/${antworten.slug} + /opt/${antworten.slug}-test, Ports ${
245
+ antworten.port ? `${antworten.port}/${antworten.port + 1}` : "NICHT gesetzt (--port)"}\n`);
246
+ process.stdout.write(` Lokal: Backend :${devPort ?? 3001}, Vite :${vitePort ?? 5173}${
247
+ devPort ? "" : " (Standard — mit --port werden eigene Dev-Ports abgeleitet)"}\n`);
248
+ if (envErgebnis === "angelegt") {
249
+ process.stdout.write(" backend/.env: angelegt (NODE_ENV=development, VEFK_DEV_LOGIN=true, zufälliges SESSION_SECRET)\n");
250
+ }
251
+ if (antworten.gitInit) {
252
+ process.stdout.write(gitErgebnis.commit
253
+ ? " git: initialisiert + Initial-Commit\n"
254
+ : ` git: ${gitErgebnis.hinweis ?? "übersprungen"}\n`);
255
+ }
256
+
257
+ if (warnungen.length > 0) {
258
+ process.stdout.write("\n⚠ Nicht automatisch ersetzt (Template neuer als der Scaffolder?):\n");
259
+ for (const w of warnungen) process.stdout.write(` – ${w}\n`);
260
+ process.stdout.write(" Bitte diese Stellen kurz von Hand prüfen.\n");
261
+ }
262
+
263
+ const cdZeile = zielPfad === process.cwd() ? "" : ` cd ${vorgaben.zielPfad ?? antworten.zielPfad ?? antworten.ziel}\n`;
264
+ const rebuildZeile = antworten.install && nodeHinweis
265
+ ? " npm --prefix backend rebuild # native Module für die Node-Version aus .nvmrc neu bauen (s. Warnung oben)\n"
266
+ : "";
267
+ const envZeile = envErgebnis === "angelegt" ? "" : " cp .env.example backend/.env # ausfüllen (SESSION_SECRET!)\n";
268
+ const zugangZeile = antworten.authMode === "generic"
269
+ ? ""
270
+ : antworten.anmeldung === "zugang"
271
+ ? ` npm run zugang # Mandanten-Passwort für „${antworten.tenantSlug}“ setzen — vorher kann sich dort niemand anmelden\n`
272
+ : " npm run zugang # optional: Mandanten-Zugang (nur Passwort, kein Konto)\n";
273
+ process.stdout.write(`\nNächste Schritte:
274
+ ${cdZeile}${envZeile}${antworten.install ? "" : " npm run install:all\n"}${rebuildZeile} npm run create-user # ersten Nutzer anlegen (ohne Passwort = Login per E-Mail-Code)
275
+ ${zugangZeile} npm run dev # Backend + Frontend zusammen, Ctrl-C beendet beide
276
+
277
+ Vor dem ersten Deploy:
278
+ Port-Paar ${antworten.port ? `${antworten.port}/${antworten.port + 1} ` : ""}im Port-Register (DEPLOYMENT.md) eintragen,
279
+ dann CLAUDE-DEPLOYMENT.md lesen — ./deploy.sh test|prod
280
+ `);
281
+ }
282
+
283
+ /**
284
+ * Aktives Node gegen die .nvmrc des Projekts vergleichen (nur die Major-Version).
285
+ * Liefert einen Hinweistext bei Abweichung, sonst "".
286
+ */
287
+ async function pruefeNodeVersion(dir) {
288
+ let erwartet;
289
+ try {
290
+ erwartet = (await readFile(join(dir, ".nvmrc"), "utf8")).trim().replace(/^v/, "");
291
+ } catch {
292
+ return "";
293
+ }
294
+ const erwartetMajor = Number.parseInt(erwartet, 10);
295
+ const aktivMajor = Number.parseInt(process.versions.node, 10);
296
+ if (!Number.isInteger(erwartetMajor) || erwartetMajor === aktivMajor) return "";
297
+ return `Node ${process.versions.node} ist aktiv, das Projekt erwartet Node ${erwartet} (.nvmrc). ` +
298
+ `Native Module (better-sqlite3, bcrypt) werden jetzt für Node ${aktivMajor} gebaut — wechselt nvm beim cd ` +
299
+ `auf Node ${erwartetMajor}, danach im Projekt „npm --prefix backend rebuild" ausführen (oder vorher „nvm use ${erwartetMajor}").`;
300
+ }
301
+
302
+ main().catch((fehler) => {
303
+ process.stderr.write(`\n✗ ${fehler.message}\n`);
304
+ process.exit(1);
305
+ });