@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 +151 -0
- package/bin/create-vefk-app.js +305 -0
- package/lib/ersetzen.js +404 -0
- package/lib/farben.js +66 -0
- package/lib/fragen.js +265 -0
- package/lib/git.js +33 -0
- package/lib/klonen.js +80 -0
- package/lib/lokaleEnv.js +34 -0
- package/lib/prozesse.js +21 -0
- package/lib/validieren.js +85 -0
- package/package.json +34 -0
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
|
+
});
|