@mostajs/setup 1.4.14 → 1.5.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 CHANGED
@@ -25,7 +25,8 @@ Part of the [@mosta suite](https://mostajs.dev).
25
25
  6. [Systeme de modules](#6-systeme-de-modules)
26
26
  7. [Exemples avances](#7-exemples-avances)
27
27
  8. [Reconfiguration (post-installation)](#8-reconfiguration-post-installation)
28
- 9. [FAQ / Troubleshooting](#9-faq--troubleshooting)
28
+ 9. [Mode declaratif : setup.json](#9-mode-declaratif--setupjson)
29
+ 10. [FAQ / Troubleshooting](#10-faq--troubleshooting)
29
30
 
30
31
  ---
31
32
 
@@ -497,6 +498,13 @@ npm run dev
497
498
  | `createDetectModulesHandler()` | `/api/setup/detect-modules` | GET | Liste modules (statiques + npm) + installes |
498
499
  | `createInstallModulesHandler(needsSetup)` | `/api/setup/install-modules` | POST | Installe les modules npm selectionnes |
499
500
 
501
+ ### setup.json (declaratif)
502
+
503
+ | Export | Signature | Description |
504
+ |--------|-----------|-------------|
505
+ | `loadSetupJson` | `(source?, repoFactory?) => Promise<MostaSetupConfig>` | Charge `setup.json` et retourne un config complet |
506
+ | `createSetupJsonHandler` | `(needsSetup) => { GET, POST }` | Route API pour verifier/uploader `setup.json` |
507
+
500
508
  ### Data exports
501
509
 
502
510
  | Export | Description |
@@ -853,7 +861,202 @@ Un package npm ne peut pas injecter de pages dans le routeur — c'est donc au p
853
861
 
854
862
  ---
855
863
 
856
- ## 9. FAQ / Troubleshooting
864
+ ## 9. Mode declaratif : setup.json
865
+
866
+ > **Nouveau** — Depuis v1.5, le setup peut etre entierement configure via un fichier JSON declaratif.
867
+
868
+ ### Principe
869
+
870
+ Au lieu d'ecrire du TypeScript pour definir les categories, permissions, roles et seeds,
871
+ vous declarez tout dans un fichier `setup.json` a la racine du projet. Le module le lit
872
+ et genere automatiquement les callbacks `seedRBAC`, `createAdmin` et `optionalSeeds`.
873
+
874
+ ### Creer un setup.json
875
+
876
+ **3 methodes :**
877
+
878
+ | Methode | Commande | Pour qui |
879
+ |---------|----------|----------|
880
+ | **Studio visuel** | Ouvrir [MostaSetup Studio](https://github.com/apolocine/mosta-setup-studio) | Non-developpeurs, design RBAC |
881
+ | **CLI interactif** | `npx mosta-setup` | Developpeurs en terminal |
882
+ | **CLI rapide** | `npx mosta-setup --quick --name MonApp --port 3000` | CI/CD, scripts |
883
+
884
+ ### Structure du fichier
885
+
886
+ ```json
887
+ {
888
+ "$schema": "https://mostajs.dev/schemas/setup.v1.json",
889
+ "app": {
890
+ "name": "MonApp",
891
+ "port": 3000,
892
+ "dbNamePrefix": "monappdb"
893
+ },
894
+ "env": {
895
+ "MOSTAJS_MODULES": "orm,auth,audit,rbac,settings,setup"
896
+ },
897
+ "rbac": {
898
+ "categories": [
899
+ { "name": "admin", "label": "Administration", "icon": "Settings", "order": 0 }
900
+ ],
901
+ "permissions": [
902
+ { "code": "admin:access", "description": "Acceder au panneau", "category": "admin" }
903
+ ],
904
+ "roles": [
905
+ { "name": "admin", "description": "Administrateur", "permissions": ["*"] }
906
+ ]
907
+ },
908
+ "seeds": [
909
+ {
910
+ "key": "products",
911
+ "label": "Produits demo",
912
+ "collection": "product",
913
+ "match": "slug",
914
+ "default": true,
915
+ "data": [
916
+ { "name": "Produit A", "slug": "produit-a", "price": 1000 }
917
+ ]
918
+ }
919
+ ]
920
+ }
921
+ ```
922
+
923
+ Le champ `$schema` active l'**autocompletion dans VS Code** (types, descriptions, exemples).
924
+
925
+ ### Utiliser loadSetupJson()
926
+
927
+ ```typescript
928
+ // src/lib/setup-config.ts
929
+ import { loadSetupJson } from '@mostajs/setup'
930
+ import type { MostaSetupConfig } from '@mostajs/setup'
931
+
932
+ // repoFactory : adapte a votre couche d'acces aux donnees
933
+ async function repoFactory(collection: string) {
934
+ const service = await import('@/dal/service')
935
+ const factories: Record<string, () => Promise<unknown>> = {
936
+ permissionCategory: service.permissionCategoryRepo,
937
+ permission: service.permissionRepo,
938
+ role: service.roleRepo,
939
+ user: service.userRepo,
940
+ activity: service.activityRepo,
941
+ }
942
+ return factories[collection]() as Promise<any>
943
+ }
944
+
945
+ export async function getSetupConfig(): Promise<MostaSetupConfig> {
946
+ return loadSetupJson('./setup.json', repoFactory)
947
+ }
948
+ ```
949
+
950
+ ```typescript
951
+ // src/app/api/setup/install/route.ts
952
+ import { runInstall } from '@mostajs/setup'
953
+ import type { InstallConfig } from '@mostajs/setup'
954
+ import { appNeedsSetup, getSetupConfig } from '@/lib/setup-config'
955
+
956
+ export async function POST(req: Request) {
957
+ if (!(await appNeedsSetup())) {
958
+ return Response.json({ error: 'Already installed' }, { status: 400 })
959
+ }
960
+ const body: InstallConfig = await req.json()
961
+ const config = await getSetupConfig()
962
+ return Response.json(await runInstall(body, config))
963
+ }
964
+ ```
965
+
966
+ ### Fonctionnalites des seeds JSON
967
+
968
+ | Champ | Type | Description |
969
+ |-------|------|-------------|
970
+ | `key` | `string` | Identifiant unique du seed |
971
+ | `label` | `string` | Label affiche dans le wizard (checkbox) |
972
+ | `collection` | `string` | Collection/table cible (doit matcher un schema enregistre) |
973
+ | `match` | `string` | Champ pour upsert idempotent (ex: `slug`, `email`) |
974
+ | `hashField` | `string` | Champ a hasher avec bcrypt avant insertion (ex: `password`) |
975
+ | `roleField` | `string` | Champ contenant un nom de role — resolu en ID a l'execution |
976
+ | `defaults` | `object` | Valeurs par defaut fusionnees dans chaque ligne |
977
+ | `default` | `boolean` | Si `true`, la checkbox est cochee par defaut dans le wizard |
978
+ | `data` | `array` | Tableau d'objets a seeder |
979
+
980
+ **Exemple : seed utilisateurs avec hash + resolution de role :**
981
+ ```json
982
+ {
983
+ "key": "demoUsers",
984
+ "collection": "user",
985
+ "match": "email",
986
+ "hashField": "password",
987
+ "roleField": "role",
988
+ "defaults": { "status": "active" },
989
+ "data": [
990
+ { "email": "agent@app.dz", "password": "Agent@123", "firstName": "Karim", "role": "agent_accueil" }
991
+ ]
992
+ }
993
+ ```
994
+
995
+ A l'execution :
996
+ 1. `password` est hashe avec bcrypt (12 rounds)
997
+ 2. `role: "agent_accueil"` est resolu en `roles: ["<id-du-role>"]`
998
+ 3. `defaults.status` est fusionne → `status: "active"`
999
+ 4. Si `match: "email"` et l'email existe deja → upsert (pas de doublon)
1000
+
1001
+ ### setup.json manquant : upload automatique
1002
+
1003
+ Si le projet accede a `/setup` et que `setup.json` n'existe pas, la page affiche
1004
+ automatiquement un formulaire d'upload (drag & drop ou selection de fichier).
1005
+
1006
+ Pour activer cette fonctionnalite, ajoutez la route API :
1007
+
1008
+ ```typescript
1009
+ // src/app/api/setup/setup-json/route.ts
1010
+ import { createSetupJsonHandler } from '@mostajs/setup'
1011
+ import { appNeedsSetup } from '@/lib/setup-config'
1012
+
1013
+ export const { GET, POST } = createSetupJsonHandler(appNeedsSetup)
1014
+ ```
1015
+
1016
+ - **GET** `/api/setup/setup-json` → `{ exists: boolean, config?: {...} }`
1017
+ - **POST** `/api/setup/setup-json` → recoit le JSON, ecrit `./setup.json`
1018
+
1019
+ ### Mixer JSON + code TypeScript
1020
+
1021
+ Les seeds simples (insert de donnees) vont dans `setup.json`. Les seeds complexes
1022
+ (relations, logique conditionnelle) restent en TypeScript :
1023
+
1024
+ ```typescript
1025
+ const config = await loadSetupJson('./setup.json', repoFactory)
1026
+ // Ajouter un seed code-only
1027
+ config.optionalSeeds = [
1028
+ ...(config.optionalSeeds ?? []),
1029
+ { key: 'demoData', label: 'Donnees complexes', run: async () => { /* ... */ } },
1030
+ ]
1031
+ ```
1032
+
1033
+ ### Validation
1034
+
1035
+ `loadSetupJson()` valide automatiquement :
1036
+ - `app.name` est requis
1037
+ - Chaque permission reference une categorie existante
1038
+ - Chaque role reference des permissions existantes (sauf `*`)
1039
+ - Erreur descriptive en cas de reference croisee invalide
1040
+
1041
+ ### CLI : npx mosta-setup
1042
+
1043
+ ```bash
1044
+ # Mode interactif (terminal)
1045
+ npx mosta-setup
1046
+
1047
+ # Mode rapide (CI, scripts, Dockerfile)
1048
+ npx mosta-setup --quick --name MonApp --port 4567 --db monappdb
1049
+
1050
+ # Avec modules
1051
+ npx mosta-setup --quick --name MonApp --modules "orm,auth,audit,rbac,settings,setup"
1052
+
1053
+ # Sortie stdout (pour pipe)
1054
+ npx mosta-setup --quick --name MonApp --stdout | jq .
1055
+ ```
1056
+
1057
+ ---
1058
+
1059
+ ## 10. FAQ / Troubleshooting
857
1060
 
858
1061
  ### L'installation tourne en boucle (GET /setup se repete)
859
1062
 
package/cli/init.ts ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ // Author: Dr Hamid MADANI drmdh@msn.com
3
+ // CLI: npx @mostajs/setup init — interactive setup.json generator
4
+ //
5
+ // Usage:
6
+ // npx mosta-setup # interactive mode
7
+ // npx mosta-setup --quick # generate minimal setup.json with defaults
8
+ // npx mosta-setup --name MyApp --port 4567 --db myappdb # non-interactive
9
+
10
+ import * as readline from 'readline'
11
+ import * as fs from 'fs'
12
+ import * as path from 'path'
13
+
14
+ const CYAN = '\x1b[36m'
15
+ const GREEN = '\x1b[32m'
16
+ const NC = '\x1b[0m'
17
+
18
+ // ── Parse CLI args ───────────────────────────────────────
19
+
20
+ const args = process.argv.slice(2)
21
+ const getArg = (flag: string): string | undefined => {
22
+ const i = args.indexOf(flag)
23
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined
24
+ }
25
+ const hasFlag = (flag: string) => args.includes(flag)
26
+
27
+ // ── Quick mode (non-interactive) ─────────────────────────
28
+
29
+ if (hasFlag('--quick') || hasFlag('-q')) {
30
+ const name = getArg('--name') || 'MonApp'
31
+ const port = parseInt(getArg('--port') || '3000')
32
+ const dbPrefix = getArg('--db') || name.toLowerCase().replace(/[^a-z0-9]/g, '') + 'db'
33
+ const modules = getArg('--modules')
34
+
35
+ const setup: Record<string, unknown> = {
36
+ $schema: 'https://mostajs.dev/schemas/setup.v1.json',
37
+ app: { name, ...(port !== 3000 ? { port } : {}), ...(dbPrefix ? { dbNamePrefix: dbPrefix } : {}) },
38
+ }
39
+ if (modules) setup.env = { MOSTAJS_MODULES: modules }
40
+
41
+ const json = JSON.stringify(setup, null, 2)
42
+ const outPath = path.resolve(process.cwd(), 'setup.json')
43
+
44
+ if (hasFlag('--stdout')) {
45
+ process.stdout.write(json + '\n')
46
+ } else {
47
+ fs.writeFileSync(outPath, json + '\n', 'utf-8')
48
+ console.log(`${GREEN}setup.json cree: ${outPath}${NC}`)
49
+ }
50
+ process.exit(0)
51
+ }
52
+
53
+ // ── Interactive mode ─────────────────────────────────────
54
+
55
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY !== false })
56
+ const ask = (q: string, def?: string): Promise<string> =>
57
+ new Promise(r => rl.question(`${q}${def ? ` (${def})` : ''}: `, a => r(a.trim() || def || '')))
58
+
59
+ async function main() {
60
+ console.log(`\n${CYAN} MostaSetup — Generateur de setup.json${NC}\n`)
61
+
62
+ // ── App ────────────────────────────────────────────────
63
+ const appName = await ask(' Nom de l\'application', 'MonApp')
64
+ const portStr = await ask(' Port HTTP', '3000')
65
+ const port = parseInt(portStr) || 3000
66
+ const dbPrefix = await ask(' Prefix DB', appName.toLowerCase().replace(/[^a-z0-9]/g, '') + 'db')
67
+
68
+ // ── Env ────────────────────────────────────────────────
69
+ const addModules = (await ask(' Ajouter MOSTAJS_MODULES ? (o/n)', 'o')).toLowerCase() === 'o'
70
+ const modules = addModules
71
+ ? await ask(' Modules', 'orm,auth,audit,rbac,settings,setup')
72
+ : ''
73
+
74
+ // ── RBAC ───────────────────────────────────────────────
75
+ const addRbac = (await ask('\n Generer un RBAC de base ? (o/n)', 'o')).toLowerCase() === 'o'
76
+ const categories: { name: string; label: string }[] = []
77
+ const permissions: { code: string; description: string; category: string }[] = []
78
+ const roles: { name: string; description: string; permissions: string[] }[] = []
79
+
80
+ if (addRbac) {
81
+ console.log(`\n ${CYAN}Categories${NC} (entree vide pour terminer)`)
82
+ while (true) {
83
+ const name = await ask(' Nom categorie (ex: admin)')
84
+ if (!name) break
85
+ const label = await ask(` Label pour "${name}"`, name.charAt(0).toUpperCase() + name.slice(1))
86
+ categories.push({ name, label })
87
+ }
88
+
89
+ console.log(`\n ${CYAN}Permissions${NC} (entree vide pour terminer)`)
90
+ while (true) {
91
+ const code = await ask(' Code permission (ex: client:view)')
92
+ if (!code) break
93
+ const desc = await ask(` Description pour "${code}"`, code)
94
+ const cat = categories.length > 0
95
+ ? await ask(` Categorie (${categories.map(c => c.name).join(', ')})`, categories[0].name)
96
+ : ''
97
+ permissions.push({ code, description: desc, category: cat })
98
+ }
99
+
100
+ console.log(`\n ${CYAN}Roles${NC} (entree vide pour terminer)`)
101
+ while (true) {
102
+ const name = await ask(' Nom role (ex: admin)')
103
+ if (!name) break
104
+ const desc = await ask(` Description`, name)
105
+ const allPerms = (await ask(' Toutes les permissions ? (o/n)', name === 'admin' ? 'o' : 'n')).toLowerCase() === 'o'
106
+ let rolePerms: string[] = []
107
+ if (allPerms) {
108
+ rolePerms = ['*']
109
+ } else {
110
+ const permsStr = await ask(` Permissions (virgules: ${permissions.map(p => p.code).slice(0, 5).join(', ')}...)`)
111
+ rolePerms = permsStr.split(',').map(s => s.trim()).filter(Boolean)
112
+ }
113
+ roles.push({ name, description: desc, permissions: rolePerms })
114
+ }
115
+ }
116
+
117
+ // ── Build JSON ─────────────────────────────────────────
118
+ const setup: Record<string, unknown> = {
119
+ $schema: 'https://mostajs.dev/schemas/setup.v1.json',
120
+ app: { name: appName, ...(port !== 3000 ? { port } : {}), ...(dbPrefix ? { dbNamePrefix: dbPrefix } : {}) },
121
+ }
122
+ if (modules) {
123
+ setup.env = { MOSTAJS_MODULES: modules }
124
+ }
125
+ if (addRbac && (categories.length || permissions.length || roles.length)) {
126
+ const rbac: Record<string, unknown> = {}
127
+ if (categories.length) rbac.categories = categories
128
+ if (permissions.length) rbac.permissions = permissions
129
+ if (roles.length) rbac.roles = roles
130
+ setup.rbac = rbac
131
+ }
132
+
133
+ const json = JSON.stringify(setup, null, 2)
134
+ const outPath = path.resolve(process.cwd(), 'setup.json')
135
+
136
+ console.log(`\n${CYAN} Preview:${NC}`)
137
+ console.log(json)
138
+
139
+ const write = (await ask(`\n Ecrire dans ${outPath} ? (o/n)`, 'o')).toLowerCase() === 'o'
140
+ if (write) {
141
+ fs.writeFileSync(outPath, json + '\n', 'utf-8')
142
+ console.log(`\n ${GREEN}setup.json cree avec succes${NC}\n`)
143
+ } else {
144
+ console.log(`\n Annule.\n`)
145
+ }
146
+
147
+ rl.close()
148
+ }
149
+
150
+ main().catch(err => {
151
+ console.error(err)
152
+ process.exit(1)
153
+ })
@@ -0,0 +1,12 @@
1
+ type NeedsSetupFn = () => Promise<boolean>;
2
+ /**
3
+ * Creates handlers for checking and uploading setup.json.
4
+ *
5
+ * GET /api/setup/setup-json → { exists: boolean, config?: { appName, seeds, rbac } }
6
+ * POST /api/setup/setup-json → receives JSON body, writes to ./setup.json
7
+ */
8
+ export declare function createSetupJsonHandler(needsSetup: NeedsSetupFn): {
9
+ GET: () => Promise<Response>;
10
+ POST: (req: Request) => Promise<Response>;
11
+ };
12
+ export {};
@@ -0,0 +1,64 @@
1
+ // @mostajs/setup — API Route for uploading setup.json
2
+ // Author: Dr Hamid MADANI drmdh@msn.com
3
+ //
4
+ // When a project uses loadSetupJson() but setup.json is missing,
5
+ // this route allows uploading it via the setup wizard UI.
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ /**
9
+ * Creates handlers for checking and uploading setup.json.
10
+ *
11
+ * GET /api/setup/setup-json → { exists: boolean, config?: { appName, seeds, rbac } }
12
+ * POST /api/setup/setup-json → receives JSON body, writes to ./setup.json
13
+ */
14
+ export function createSetupJsonHandler(needsSetup) {
15
+ const setupJsonPath = () => path.resolve(process.cwd(), 'setup.json');
16
+ async function GET() {
17
+ const filePath = setupJsonPath();
18
+ if (!fs.existsSync(filePath)) {
19
+ return Response.json({ exists: false });
20
+ }
21
+ try {
22
+ const raw = fs.readFileSync(filePath, 'utf-8');
23
+ const json = JSON.parse(raw);
24
+ return Response.json({
25
+ exists: true,
26
+ config: {
27
+ appName: json.app?.name,
28
+ hasRbac: !!(json.rbac?.roles?.length || json.rbac?.permissions?.length),
29
+ seedCount: json.seeds?.length ?? 0,
30
+ },
31
+ });
32
+ }
33
+ catch (err) {
34
+ const msg = err instanceof Error ? err.message : 'Invalid JSON';
35
+ return Response.json({ exists: false, error: msg });
36
+ }
37
+ }
38
+ async function POST(req) {
39
+ if (!(await needsSetup())) {
40
+ return Response.json({ error: 'Already installed' }, { status: 400 });
41
+ }
42
+ let body;
43
+ try {
44
+ body = await req.json();
45
+ }
46
+ catch {
47
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
48
+ }
49
+ if (!body.app?.name) {
50
+ return Response.json({ error: 'setup.json must have app.name' }, { status: 400 });
51
+ }
52
+ const filePath = setupJsonPath();
53
+ fs.writeFileSync(filePath, JSON.stringify(body, null, 2) + '\n', 'utf-8');
54
+ return Response.json({
55
+ ok: true,
56
+ config: {
57
+ appName: body.app.name,
58
+ hasRbac: !!(body.rbac?.roles?.length || body.rbac?.permissions?.length),
59
+ seedCount: body.seeds?.length ?? 0,
60
+ },
61
+ });
62
+ }
63
+ return { GET, POST };
64
+ }
@@ -91,22 +91,26 @@ export function createWireModuleHandler() {
91
91
  const wireFile = path.join(mostaDir, dir, `${dir}.wire.json`);
92
92
  if (fs.existsSync(wireFile)) {
93
93
  const manifest = JSON.parse(fs.readFileSync(wireFile, 'utf8'));
94
- // Check if already wired by looking at schemas or permissions in host files
95
- const permFile = path.join(root, 'src/lib/permissions.ts');
96
- let installed = false;
97
- if (manifest.permissions?.permissionsConst && fs.existsSync(permFile)) {
98
- installed = fs.readFileSync(permFile, 'utf8').includes(manifest.permissions.permissionsConst);
99
- }
100
- else if (manifest.schemas?.exports?.[0]) {
101
- const regFile = path.join(root, 'src/dal/registry.ts');
102
- if (fs.existsSync(regFile)) {
103
- installed = fs.readFileSync(regFile, 'utf8').includes(manifest.schemas.exports[0]);
94
+ // Check if already wired primary: package in dependencies
95
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
96
+ let installed = !!pkg.dependencies?.[manifest.package];
97
+ // Fallback: check host source files for legacy codegen markers
98
+ if (!installed) {
99
+ const permFile = path.join(root, 'src/lib/permissions.ts');
100
+ if (manifest.permissions?.permissionsConst && fs.existsSync(permFile)) {
101
+ installed = fs.readFileSync(permFile, 'utf8').includes(manifest.permissions.permissionsConst);
104
102
  }
105
- }
106
- else if (manifest.menu?.name) {
107
- const sidebarFile = path.join(root, 'src/components/layout/Sidebar.tsx');
108
- if (fs.existsSync(sidebarFile)) {
109
- installed = fs.readFileSync(sidebarFile, 'utf8').includes(manifest.menu.name);
103
+ else if (manifest.schemas?.exports?.[0]) {
104
+ const regFile = path.join(root, 'src/dal/registry.ts');
105
+ if (fs.existsSync(regFile)) {
106
+ installed = fs.readFileSync(regFile, 'utf8').includes(manifest.schemas.exports[0]);
107
+ }
108
+ }
109
+ else if (manifest.menu?.name) {
110
+ const sidebarFile = path.join(root, 'src/components/layout/Sidebar.tsx');
111
+ if (fs.existsSync(sidebarFile)) {
112
+ installed = fs.readFileSync(sidebarFile, 'utf8').includes(manifest.menu.name);
113
+ }
110
114
  }
111
115
  }
112
116
  found.push({
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // Author: Dr Hamid MADANI drmdh@msn.com
3
+ // CLI: npx @mostajs/setup init — interactive setup.json generator
4
+ //
5
+ // Usage:
6
+ // npx mosta-setup # interactive mode
7
+ // npx mosta-setup --quick # generate minimal setup.json with defaults
8
+ // npx mosta-setup --name MyApp --port 4567 --db myappdb # non-interactive
9
+ import * as readline from 'readline';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ const CYAN = '\x1b[36m';
13
+ const GREEN = '\x1b[32m';
14
+ const NC = '\x1b[0m';
15
+ // ── Parse CLI args ───────────────────────────────────────
16
+ const args = process.argv.slice(2);
17
+ const getArg = (flag) => {
18
+ const i = args.indexOf(flag);
19
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
20
+ };
21
+ const hasFlag = (flag) => args.includes(flag);
22
+ // ── Quick mode (non-interactive) ─────────────────────────
23
+ if (hasFlag('--quick') || hasFlag('-q')) {
24
+ const name = getArg('--name') || 'MonApp';
25
+ const port = parseInt(getArg('--port') || '3000');
26
+ const dbPrefix = getArg('--db') || name.toLowerCase().replace(/[^a-z0-9]/g, '') + 'db';
27
+ const modules = getArg('--modules');
28
+ const setup = {
29
+ $schema: 'https://mostajs.dev/schemas/setup.v1.json',
30
+ app: { name, ...(port !== 3000 ? { port } : {}), ...(dbPrefix ? { dbNamePrefix: dbPrefix } : {}) },
31
+ };
32
+ if (modules)
33
+ setup.env = { MOSTAJS_MODULES: modules };
34
+ const json = JSON.stringify(setup, null, 2);
35
+ const outPath = path.resolve(process.cwd(), 'setup.json');
36
+ if (hasFlag('--stdout')) {
37
+ process.stdout.write(json + '\n');
38
+ }
39
+ else {
40
+ fs.writeFileSync(outPath, json + '\n', 'utf-8');
41
+ console.log(`${GREEN}setup.json cree: ${outPath}${NC}`);
42
+ }
43
+ process.exit(0);
44
+ }
45
+ // ── Interactive mode ─────────────────────────────────────
46
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY !== false });
47
+ const ask = (q, def) => new Promise(r => rl.question(`${q}${def ? ` (${def})` : ''}: `, a => r(a.trim() || def || '')));
48
+ async function main() {
49
+ console.log(`\n${CYAN} MostaSetup — Generateur de setup.json${NC}\n`);
50
+ // ── App ────────────────────────────────────────────────
51
+ const appName = await ask(' Nom de l\'application', 'MonApp');
52
+ const portStr = await ask(' Port HTTP', '3000');
53
+ const port = parseInt(portStr) || 3000;
54
+ const dbPrefix = await ask(' Prefix DB', appName.toLowerCase().replace(/[^a-z0-9]/g, '') + 'db');
55
+ // ── Env ────────────────────────────────────────────────
56
+ const addModules = (await ask(' Ajouter MOSTAJS_MODULES ? (o/n)', 'o')).toLowerCase() === 'o';
57
+ const modules = addModules
58
+ ? await ask(' Modules', 'orm,auth,audit,rbac,settings,setup')
59
+ : '';
60
+ // ── RBAC ───────────────────────────────────────────────
61
+ const addRbac = (await ask('\n Generer un RBAC de base ? (o/n)', 'o')).toLowerCase() === 'o';
62
+ const categories = [];
63
+ const permissions = [];
64
+ const roles = [];
65
+ if (addRbac) {
66
+ console.log(`\n ${CYAN}Categories${NC} (entree vide pour terminer)`);
67
+ while (true) {
68
+ const name = await ask(' Nom categorie (ex: admin)');
69
+ if (!name)
70
+ break;
71
+ const label = await ask(` Label pour "${name}"`, name.charAt(0).toUpperCase() + name.slice(1));
72
+ categories.push({ name, label });
73
+ }
74
+ console.log(`\n ${CYAN}Permissions${NC} (entree vide pour terminer)`);
75
+ while (true) {
76
+ const code = await ask(' Code permission (ex: client:view)');
77
+ if (!code)
78
+ break;
79
+ const desc = await ask(` Description pour "${code}"`, code);
80
+ const cat = categories.length > 0
81
+ ? await ask(` Categorie (${categories.map(c => c.name).join(', ')})`, categories[0].name)
82
+ : '';
83
+ permissions.push({ code, description: desc, category: cat });
84
+ }
85
+ console.log(`\n ${CYAN}Roles${NC} (entree vide pour terminer)`);
86
+ while (true) {
87
+ const name = await ask(' Nom role (ex: admin)');
88
+ if (!name)
89
+ break;
90
+ const desc = await ask(` Description`, name);
91
+ const allPerms = (await ask(' Toutes les permissions ? (o/n)', name === 'admin' ? 'o' : 'n')).toLowerCase() === 'o';
92
+ let rolePerms = [];
93
+ if (allPerms) {
94
+ rolePerms = ['*'];
95
+ }
96
+ else {
97
+ const permsStr = await ask(` Permissions (virgules: ${permissions.map(p => p.code).slice(0, 5).join(', ')}...)`);
98
+ rolePerms = permsStr.split(',').map(s => s.trim()).filter(Boolean);
99
+ }
100
+ roles.push({ name, description: desc, permissions: rolePerms });
101
+ }
102
+ }
103
+ // ── Build JSON ─────────────────────────────────────────
104
+ const setup = {
105
+ $schema: 'https://mostajs.dev/schemas/setup.v1.json',
106
+ app: { name: appName, ...(port !== 3000 ? { port } : {}), ...(dbPrefix ? { dbNamePrefix: dbPrefix } : {}) },
107
+ };
108
+ if (modules) {
109
+ setup.env = { MOSTAJS_MODULES: modules };
110
+ }
111
+ if (addRbac && (categories.length || permissions.length || roles.length)) {
112
+ const rbac = {};
113
+ if (categories.length)
114
+ rbac.categories = categories;
115
+ if (permissions.length)
116
+ rbac.permissions = permissions;
117
+ if (roles.length)
118
+ rbac.roles = roles;
119
+ setup.rbac = rbac;
120
+ }
121
+ const json = JSON.stringify(setup, null, 2);
122
+ const outPath = path.resolve(process.cwd(), 'setup.json');
123
+ console.log(`\n${CYAN} Preview:${NC}`);
124
+ console.log(json);
125
+ const write = (await ask(`\n Ecrire dans ${outPath} ? (o/n)`, 'o')).toLowerCase() === 'o';
126
+ if (write) {
127
+ fs.writeFileSync(outPath, json + '\n', 'utf-8');
128
+ console.log(`\n ${GREEN}setup.json cree avec succes${NC}\n`);
129
+ }
130
+ else {
131
+ console.log(`\n Annule.\n`);
132
+ }
133
+ rl.close();
134
+ }
135
+ main().catch(err => {
136
+ console.error(err);
137
+ process.exit(1);
138
+ });
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export { writeEnvLocal } from './lib/env-writer';
5
5
  export { DIALECT_INFO, ALL_DIALECTS } from './data/dialects';
6
6
  export { MODULES, resolveModuleDependencies } from './data/module-definitions';
7
7
  export { discoverNpmModules } from './lib/discover-modules';
8
+ export { loadSetupJson } from './lib/load-setup-json';
9
+ export type { SetupJson, SetupJsonRbac, SetupJsonSeed, SetupJsonCategory, SetupJsonPermission, SetupJsonRole } from './lib/load-setup-json';
8
10
  export { createTestDbHandler } from './api/test-db.route';
9
11
  export { createInstallHandler } from './api/install.route';
10
12
  export { createStatusHandler } from './api/status.route';
@@ -13,6 +15,7 @@ export { createInstallModulesHandler } from './api/install-modules.route';
13
15
  export { createReconfigHandlers } from './api/reconfig.route';
14
16
  export { createUploadJarHandlers } from './api/upload-jar.route';
15
17
  export { createWireModuleHandler } from './api/wire-module.route';
18
+ export { createSetupJsonHandler } from './api/upload-setup-json.route';
16
19
  export { default as ReconfigPanel } from './components/ReconfigPanel';
17
20
  export { default as SetupWizard } from './components/SetupWizard';
18
21
  export { setupMenuContribution } from './lib/menu';
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export { DIALECT_INFO, ALL_DIALECTS } from './data/dialects';
10
10
  export { MODULES, resolveModuleDependencies } from './data/module-definitions';
11
11
  // Lib
12
12
  export { discoverNpmModules } from './lib/discover-modules';
13
+ export { loadSetupJson } from './lib/load-setup-json';
13
14
  // API route factories
14
15
  export { createTestDbHandler } from './api/test-db.route';
15
16
  export { createInstallHandler } from './api/install.route';
@@ -19,6 +20,7 @@ export { createInstallModulesHandler } from './api/install-modules.route';
19
20
  export { createReconfigHandlers } from './api/reconfig.route';
20
21
  export { createUploadJarHandlers } from './api/upload-jar.route';
21
22
  export { createWireModuleHandler } from './api/wire-module.route';
23
+ export { createSetupJsonHandler } from './api/upload-setup-json.route';
22
24
  // Components
23
25
  export { default as ReconfigPanel } from './components/ReconfigPanel';
24
26
  export { default as SetupWizard } from './components/SetupWizard';
@@ -8,7 +8,7 @@ export function composeDbUri(dialect, config) {
8
8
  switch (dialect) {
9
9
  case 'mongodb':
10
10
  if (user && password)
11
- return `mongodb://${eu}:${ep}@${host}:${port}/${name}`;
11
+ return `mongodb://${eu}:${ep}@${host}:${port}/${name}?authSource=admin`;
12
12
  return `mongodb://${host}:${port}/${name}`;
13
13
  case 'sqlite':
14
14
  return `./data/${name}.db`;
@@ -0,0 +1,72 @@
1
+ import type { MostaSetupConfig } from '../types/index';
2
+ export interface SetupJsonCategory {
3
+ name: string;
4
+ label: string;
5
+ description?: string;
6
+ icon?: string;
7
+ order?: number;
8
+ system?: boolean;
9
+ }
10
+ export interface SetupJsonPermission {
11
+ code: string;
12
+ name?: string;
13
+ description: string;
14
+ category: string;
15
+ }
16
+ export interface SetupJsonRole {
17
+ name: string;
18
+ description?: string;
19
+ system?: boolean;
20
+ permissions: string[];
21
+ }
22
+ export interface SetupJsonRbac {
23
+ categories?: SetupJsonCategory[];
24
+ permissions?: SetupJsonPermission[];
25
+ roles?: SetupJsonRole[];
26
+ }
27
+ export interface SetupJsonSeed {
28
+ key: string;
29
+ label: string;
30
+ description?: string;
31
+ icon?: string;
32
+ default?: boolean;
33
+ collection: string;
34
+ match?: string;
35
+ hashField?: string;
36
+ roleField?: string;
37
+ defaults?: Record<string, unknown>;
38
+ data: Record<string, unknown>[];
39
+ }
40
+ export interface SetupJson {
41
+ $schema?: string;
42
+ app: {
43
+ name: string;
44
+ port?: number;
45
+ dbNamePrefix?: string;
46
+ };
47
+ env?: Record<string, string>;
48
+ rbac?: SetupJsonRbac;
49
+ seeds?: SetupJsonSeed[];
50
+ }
51
+ /**
52
+ * Load a setup.json and return a MostaSetupConfig.
53
+ *
54
+ * @param source - File path (default: './setup.json'), or an already-parsed object.
55
+ * @param repoFactory - Optional factory to get a repository by collection name.
56
+ * Signature: `(collection: string) => Promise<{ upsert, create, findOne, count }>`
57
+ * If omitted, uses `@mostajs/orm` getRepository() at runtime.
58
+ */
59
+ export declare function loadSetupJson(source?: string | SetupJson, repoFactory?: (collection: string) => Promise<GenericRepo>): Promise<MostaSetupConfig>;
60
+ type GenericRepo = {
61
+ upsert?: (where: Record<string, unknown>, data: Record<string, unknown>) => Promise<{
62
+ id: string;
63
+ }>;
64
+ create?: (data: Record<string, unknown>) => Promise<{
65
+ id: string;
66
+ }>;
67
+ findOne?: (where: Record<string, unknown>) => Promise<{
68
+ id: string;
69
+ } | null>;
70
+ count?: () => Promise<number>;
71
+ };
72
+ export {};
@@ -0,0 +1,163 @@
1
+ // @mostajs/setup — Declarative setup.json loader
2
+ // Author: Dr Hamid MADANI drmdh@msn.com
3
+ //
4
+ // Reads a setup.json file and converts it into a MostaSetupConfig
5
+ // that can be passed to createInstallHandler() / runInstall().
6
+ //
7
+ // Usage:
8
+ // import { loadSetupJson } from '@mostajs/setup'
9
+ // const config = await loadSetupJson() // reads ./setup.json
10
+ // const config = await loadSetupJson('./my-setup.json') // custom path
11
+ // const config = await loadSetupJson(jsonObject) // already-parsed object
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ // ── Loader ───────────────────────────────────────────────
15
+ /**
16
+ * Load a setup.json and return a MostaSetupConfig.
17
+ *
18
+ * @param source - File path (default: './setup.json'), or an already-parsed object.
19
+ * @param repoFactory - Optional factory to get a repository by collection name.
20
+ * Signature: `(collection: string) => Promise<{ upsert, create, findOne, count }>`
21
+ * If omitted, uses `@mostajs/orm` getRepository() at runtime.
22
+ */
23
+ export async function loadSetupJson(source, repoFactory) {
24
+ const json = typeof source === 'object' && source !== null
25
+ ? source
26
+ : readJsonFile(typeof source === 'string' ? source : './setup.json');
27
+ validate(json);
28
+ return buildConfig(json, repoFactory);
29
+ }
30
+ function readJsonFile(filePath) {
31
+ const resolved = path.resolve(process.cwd(), filePath);
32
+ if (!fs.existsSync(resolved)) {
33
+ throw new Error(`setup.json not found at ${resolved}`);
34
+ }
35
+ const raw = fs.readFileSync(resolved, 'utf-8');
36
+ return JSON.parse(raw);
37
+ }
38
+ function validate(json) {
39
+ if (!json.app?.name) {
40
+ throw new Error('setup.json: "app.name" is required');
41
+ }
42
+ // Validate RBAC cross-references
43
+ if (json.rbac) {
44
+ const categoryNames = new Set((json.rbac.categories ?? []).map(c => c.name));
45
+ const permCodes = new Set((json.rbac.permissions ?? []).map(p => p.code));
46
+ for (const perm of json.rbac.permissions ?? []) {
47
+ if (categoryNames.size > 0 && !categoryNames.has(perm.category)) {
48
+ throw new Error(`setup.json: permission "${perm.code}" references unknown category "${perm.category}"`);
49
+ }
50
+ }
51
+ for (const role of json.rbac.roles ?? []) {
52
+ for (const code of role.permissions) {
53
+ if (code !== '*' && permCodes.size > 0 && !permCodes.has(code)) {
54
+ throw new Error(`setup.json: role "${role.name}" references unknown permission "${code}"`);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ function buildConfig(json, repoFactory) {
61
+ const config = {
62
+ appName: json.app.name,
63
+ defaultPort: json.app.port,
64
+ extraEnvVars: json.env ? { ...json.env } : undefined,
65
+ };
66
+ // ── seedRBAC ───────────────────────────────────────────
67
+ if (json.rbac) {
68
+ config.seedRBAC = async () => {
69
+ const getRepo = repoFactory ?? defaultRepoFactory;
70
+ // 1. Upsert categories
71
+ if (json.rbac.categories?.length) {
72
+ const catRepo = await getRepo('permissionCategory');
73
+ for (const cat of json.rbac.categories) {
74
+ await catRepo.upsert({ name: cat.name }, { name: cat.name, label: cat.label, description: cat.description ?? '', icon: cat.icon ?? '', order: cat.order ?? 0, system: cat.system ?? true });
75
+ }
76
+ }
77
+ // 2. Upsert permissions — build code→ID map
78
+ const permissionMap = {};
79
+ if (json.rbac.permissions?.length) {
80
+ const permRepo = await getRepo('permission');
81
+ for (const pDef of json.rbac.permissions) {
82
+ const displayName = pDef.name ?? pDef.code;
83
+ const perm = await permRepo.upsert({ name: displayName }, { name: displayName, description: pDef.description, category: pDef.category });
84
+ permissionMap[pDef.code] = perm.id;
85
+ }
86
+ }
87
+ // 3. Upsert roles with resolved permission IDs
88
+ if (json.rbac.roles?.length) {
89
+ const roleRepo = await getRepo('role');
90
+ const allPermIds = Object.values(permissionMap);
91
+ for (const roleDef of json.rbac.roles) {
92
+ const permissionIds = roleDef.permissions.includes('*')
93
+ ? allPermIds
94
+ : roleDef.permissions.map(code => permissionMap[code]).filter(Boolean);
95
+ await roleRepo.upsert({ name: roleDef.name }, { name: roleDef.name, description: roleDef.description ?? '', permissions: permissionIds });
96
+ }
97
+ }
98
+ };
99
+ }
100
+ // ── optionalSeeds ──────────────────────────────────────
101
+ if (json.seeds?.length) {
102
+ config.optionalSeeds = json.seeds.map(seedDef => buildSeedDefinition(seedDef, repoFactory));
103
+ }
104
+ return config;
105
+ }
106
+ function buildSeedDefinition(seedDef, repoFactory) {
107
+ return {
108
+ key: seedDef.key,
109
+ label: seedDef.label,
110
+ description: seedDef.description ?? '',
111
+ icon: seedDef.icon,
112
+ default: seedDef.default,
113
+ run: async () => {
114
+ const getRepo = repoFactory ?? defaultRepoFactory;
115
+ const repo = await getRepo(seedDef.collection);
116
+ for (const rawItem of seedDef.data) {
117
+ const item = { ...(seedDef.defaults ?? {}), ...rawItem };
118
+ // Hash field if configured (e.g. password)
119
+ if (seedDef.hashField && item[seedDef.hashField]) {
120
+ const bcryptModule = await import('bcryptjs');
121
+ const bcrypt = bcryptModule.default || bcryptModule;
122
+ item[seedDef.hashField] = await bcrypt.hash(String(item[seedDef.hashField]), 12);
123
+ }
124
+ // Resolve role name → role ID if configured
125
+ if (seedDef.roleField && item[seedDef.roleField]) {
126
+ const roleRepo = await getRepo('role');
127
+ const roleName = String(item[seedDef.roleField]);
128
+ const role = await roleRepo.findOne({ name: roleName });
129
+ if (role) {
130
+ item.roles = [role.id];
131
+ }
132
+ delete item[seedDef.roleField];
133
+ }
134
+ // Upsert (idempotent) or create
135
+ if (seedDef.match && repo.upsert) {
136
+ const matchValue = item[seedDef.match];
137
+ await repo.upsert({ [seedDef.match]: matchValue }, item);
138
+ }
139
+ else if (repo.create) {
140
+ // Skip if already exists (check match field if available)
141
+ if (seedDef.match && repo.findOne) {
142
+ const existing = await repo.findOne({ [seedDef.match]: item[seedDef.match] });
143
+ if (existing)
144
+ continue;
145
+ }
146
+ await repo.create(item);
147
+ }
148
+ }
149
+ },
150
+ };
151
+ }
152
+ async function defaultRepoFactory(collection) {
153
+ // Try to import a getRepository from @mostajs/orm if available
154
+ try {
155
+ const orm = await import('@mostajs/orm');
156
+ if ('getRepository' in orm && typeof orm.getRepository === 'function') {
157
+ return await orm.getRepository(collection);
158
+ }
159
+ }
160
+ catch { /* fallback below */ }
161
+ throw new Error(`No repository factory provided for collection "${collection}". ` +
162
+ `Pass a repoFactory argument to loadSetupJson().`);
163
+ }
package/dist/lib/setup.js CHANGED
@@ -69,7 +69,8 @@ export async function runInstall(installConfig, setupConfig) {
69
69
  }
70
70
  // 5. Create admin user
71
71
  if (setupConfig.createAdmin) {
72
- const bcrypt = await import('bcryptjs');
72
+ const bcryptModule = await import('bcryptjs');
73
+ const bcrypt = bcryptModule.default || bcryptModule;
73
74
  const hashedPassword = await bcrypt.hash(installConfig.admin.password, 12);
74
75
  await setupConfig.createAdmin({
75
76
  email: installConfig.admin.email,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/setup",
3
- "version": "1.4.14",
3
+ "version": "1.5.0",
4
4
  "description": "Reusable setup wizard module — multi-dialect DB configuration, .env.local writer, seed runner",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "MIT",
@@ -67,10 +67,13 @@
67
67
  "types": "./dist/register.d.ts",
68
68
  "import": "./dist/register.js",
69
69
  "default": "./dist/register.js"
70
- }
70
+ },
71
+ "./schemas/*": "./schemas/*"
71
72
  },
72
73
  "files": [
73
74
  "dist",
75
+ "schemas",
76
+ "cli",
74
77
  "wire.json",
75
78
  "setup.wire.json",
76
79
  "LICENSE",
@@ -93,6 +96,9 @@
93
96
  "engines": {
94
97
  "node": ">=18.0.0"
95
98
  },
99
+ "bin": {
100
+ "mosta-setup": "./dist/cli/init.js"
101
+ },
96
102
  "scripts": {
97
103
  "build": "tsc",
98
104
  "prepublishOnly": "npm run build"
@@ -0,0 +1,218 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://mostajs.dev/schemas/setup.v1.json",
4
+ "title": "MostaJS Setup Configuration",
5
+ "description": "Declarative setup manifest for @mostajs/setup — defines app info, RBAC, seeds, and env vars for the install wizard.",
6
+ "type": "object",
7
+ "required": ["app"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "description": "JSON Schema reference for IDE autocompletion"
13
+ },
14
+ "app": {
15
+ "type": "object",
16
+ "description": "Application metadata",
17
+ "required": ["name"],
18
+ "additionalProperties": false,
19
+ "properties": {
20
+ "name": {
21
+ "type": "string",
22
+ "description": "Application name displayed in the setup wizard",
23
+ "minLength": 1,
24
+ "examples": ["SecuAccessPro", "MostaGare", "MonApp"]
25
+ },
26
+ "port": {
27
+ "type": "integer",
28
+ "description": "Default HTTP port",
29
+ "default": 3000,
30
+ "minimum": 1,
31
+ "maximum": 65535
32
+ },
33
+ "dbNamePrefix": {
34
+ "type": "string",
35
+ "description": "Default database name prefix used in the wizard",
36
+ "pattern": "^[a-z][a-z0-9_]*$",
37
+ "examples": ["secuaccessdb", "mostagaredb"]
38
+ }
39
+ }
40
+ },
41
+ "env": {
42
+ "type": "object",
43
+ "description": "Extra environment variables written to .env.local during install",
44
+ "additionalProperties": {
45
+ "type": "string"
46
+ },
47
+ "examples": [
48
+ { "MOSTAJS_MODULES": "orm,auth,audit,rbac,settings,setup" }
49
+ ]
50
+ },
51
+ "rbac": {
52
+ "type": "object",
53
+ "description": "Role-Based Access Control definitions seeded at install time",
54
+ "additionalProperties": false,
55
+ "properties": {
56
+ "categories": {
57
+ "type": "array",
58
+ "description": "Permission categories (grouping)",
59
+ "items": {
60
+ "type": "object",
61
+ "required": ["name", "label"],
62
+ "additionalProperties": false,
63
+ "properties": {
64
+ "name": {
65
+ "type": "string",
66
+ "description": "Unique key (e.g. 'admin', 'client')",
67
+ "pattern": "^[a-z][a-z0-9_]*$"
68
+ },
69
+ "label": {
70
+ "type": "string",
71
+ "description": "Display label"
72
+ },
73
+ "description": {
74
+ "type": "string"
75
+ },
76
+ "icon": {
77
+ "type": "string",
78
+ "description": "Lucide icon name (e.g. 'Settings', 'Users')"
79
+ },
80
+ "order": {
81
+ "type": "integer",
82
+ "minimum": 0,
83
+ "description": "Display order"
84
+ },
85
+ "system": {
86
+ "type": "boolean",
87
+ "default": true,
88
+ "description": "Whether this category is system-defined (not user-deletable)"
89
+ }
90
+ }
91
+ }
92
+ },
93
+ "permissions": {
94
+ "type": "array",
95
+ "description": "Permission definitions",
96
+ "items": {
97
+ "type": "object",
98
+ "required": ["code", "description", "category"],
99
+ "additionalProperties": false,
100
+ "properties": {
101
+ "code": {
102
+ "type": "string",
103
+ "description": "Unique permission code (e.g. 'client:view')",
104
+ "pattern": "^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$"
105
+ },
106
+ "name": {
107
+ "type": "string",
108
+ "description": "Display name (defaults to code if omitted)"
109
+ },
110
+ "description": {
111
+ "type": "string",
112
+ "description": "Human-readable description"
113
+ },
114
+ "category": {
115
+ "type": "string",
116
+ "description": "Category name reference"
117
+ }
118
+ }
119
+ }
120
+ },
121
+ "roles": {
122
+ "type": "array",
123
+ "description": "Role definitions with permission assignments",
124
+ "items": {
125
+ "type": "object",
126
+ "required": ["name", "permissions"],
127
+ "additionalProperties": false,
128
+ "properties": {
129
+ "name": {
130
+ "type": "string",
131
+ "description": "Unique role name (e.g. 'admin', 'agent_accueil')",
132
+ "pattern": "^[a-z][a-z0-9_]*$"
133
+ },
134
+ "description": {
135
+ "type": "string"
136
+ },
137
+ "system": {
138
+ "type": "boolean",
139
+ "default": true,
140
+ "description": "Whether this role is system-defined"
141
+ },
142
+ "permissions": {
143
+ "type": "array",
144
+ "description": "Permission codes assigned to this role. Use ['*'] for all permissions.",
145
+ "items": {
146
+ "type": "string"
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ },
154
+ "seeds": {
155
+ "type": "array",
156
+ "description": "Optional seed datasets shown as checkboxes in the install wizard",
157
+ "items": {
158
+ "type": "object",
159
+ "required": ["key", "label", "collection", "data"],
160
+ "additionalProperties": false,
161
+ "properties": {
162
+ "key": {
163
+ "type": "string",
164
+ "description": "Unique seed identifier",
165
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$"
166
+ },
167
+ "label": {
168
+ "type": "string",
169
+ "description": "Label shown in the wizard checkbox"
170
+ },
171
+ "description": {
172
+ "type": "string",
173
+ "description": "Description shown below the label"
174
+ },
175
+ "icon": {
176
+ "type": "string",
177
+ "description": "Lucide icon name"
178
+ },
179
+ "default": {
180
+ "type": "boolean",
181
+ "default": false,
182
+ "description": "Whether this seed is checked by default"
183
+ },
184
+ "collection": {
185
+ "type": "string",
186
+ "description": "Target collection/table name (must match a registered schema)"
187
+ },
188
+ "match": {
189
+ "type": "string",
190
+ "description": "Field used for upsert matching (idempotent seeding). If omitted, uses 'create' (non-idempotent)."
191
+ },
192
+ "hashField": {
193
+ "type": "string",
194
+ "description": "Field to bcrypt-hash before insert (e.g. 'password')"
195
+ },
196
+ "roleField": {
197
+ "type": "string",
198
+ "description": "Field containing a role name — resolved to role ID at runtime"
199
+ },
200
+ "defaults": {
201
+ "type": "object",
202
+ "description": "Default values merged into every data item",
203
+ "additionalProperties": true
204
+ },
205
+ "data": {
206
+ "type": "array",
207
+ "description": "Array of records to seed",
208
+ "items": {
209
+ "type": "object",
210
+ "additionalProperties": true
211
+ },
212
+ "minItems": 1
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }