@lastbrain/app 2.0.1 → 2.0.3
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/dist/config/version.d.ts +7 -0
- package/dist/config/version.d.ts.map +1 -0
- package/dist/config/version.js +25 -0
- package/dist/src/__tests__/module-registry.test.d.ts +2 -0
- package/dist/src/__tests__/module-registry.test.d.ts.map +1 -0
- package/dist/src/__tests__/module-registry.test.js +53 -0
- package/dist/src/app-shell/(admin)/layout.d.ts +4 -0
- package/dist/src/app-shell/(admin)/layout.d.ts.map +1 -0
- package/dist/src/app-shell/(admin)/layout.js +5 -0
- package/dist/src/app-shell/(auth)/layout.d.ts +4 -0
- package/dist/src/app-shell/(auth)/layout.d.ts.map +1 -0
- package/dist/src/app-shell/(auth)/layout.js +5 -0
- package/dist/src/app-shell/(public)/page.d.ts +2 -0
- package/dist/src/app-shell/(public)/page.d.ts.map +1 -0
- package/dist/src/app-shell/(public)/page.js +5 -0
- package/dist/src/app-shell/layout.d.ts +3 -0
- package/dist/src/app-shell/layout.d.ts.map +1 -0
- package/dist/src/app-shell/layout.js +3 -0
- package/dist/src/app-shell/not-found.d.ts +2 -0
- package/dist/src/app-shell/not-found.d.ts.map +1 -0
- package/dist/src/app-shell/not-found.js +10 -0
- package/dist/src/auth/authHelpers.d.ts +7 -0
- package/dist/src/auth/authHelpers.d.ts.map +1 -0
- package/dist/src/auth/authHelpers.js +19 -0
- package/dist/src/auth/useAuthSession.d.ts +7 -0
- package/dist/src/auth/useAuthSession.d.ts.map +1 -0
- package/dist/src/auth/useAuthSession.js +49 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +143 -0
- package/dist/src/components/NotificationContainer.d.ts +2 -0
- package/dist/src/components/NotificationContainer.d.ts.map +1 -0
- package/dist/src/components/NotificationContainer.js +8 -0
- package/dist/src/hooks/useNotifications.d.ts +30 -0
- package/dist/src/hooks/useNotifications.d.ts.map +1 -0
- package/dist/src/hooks/useNotifications.js +165 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +22 -0
- package/dist/src/layouts/AdminLayout.d.ts +4 -0
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -0
- package/dist/src/layouts/AdminLayout.js +4 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.d.ts +10 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/AdminLayoutWithSidebar.js +62 -0
- package/dist/src/layouts/AppProviders.d.ts +27 -0
- package/dist/src/layouts/AppProviders.d.ts.map +1 -0
- package/dist/src/layouts/AppProviders.js +48 -0
- package/dist/src/layouts/AuthLayout.d.ts +4 -0
- package/dist/src/layouts/AuthLayout.d.ts.map +1 -0
- package/dist/src/layouts/AuthLayout.js +4 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.d.ts +12 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/AuthLayoutWithSidebar.js +60 -0
- package/dist/src/layouts/PublicLayout.d.ts +8 -0
- package/dist/src/layouts/PublicLayout.d.ts.map +1 -0
- package/dist/src/layouts/PublicLayout.js +6 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.d.ts +9 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.d.ts.map +1 -0
- package/dist/src/layouts/PublicLayoutWithSidebar.js +60 -0
- package/dist/src/layouts/RootLayout.d.ts +6 -0
- package/dist/src/layouts/RootLayout.d.ts.map +1 -0
- package/dist/src/layouts/RootLayout.js +9 -0
- package/dist/src/modules/module-loader.d.ts +5 -0
- package/dist/src/modules/module-loader.d.ts.map +1 -0
- package/dist/src/modules/module-loader.js +10 -0
- package/dist/src/scripts/db-init.d.ts +2 -0
- package/dist/src/scripts/db-init.d.ts.map +1 -0
- package/dist/src/scripts/db-init.js +300 -0
- package/dist/src/scripts/db-migrations-sync.d.ts +2 -0
- package/dist/src/scripts/db-migrations-sync.d.ts.map +1 -0
- package/dist/src/scripts/db-migrations-sync.js +84 -0
- package/dist/src/scripts/dev-sync.d.ts +2 -0
- package/dist/src/scripts/dev-sync.d.ts.map +1 -0
- package/dist/src/scripts/dev-sync.js +194 -0
- package/dist/src/scripts/init-app.d.ts +12 -0
- package/dist/src/scripts/init-app.d.ts.map +1 -0
- package/dist/src/scripts/init-app.js +2175 -0
- package/dist/src/scripts/module-add.d.ts +2 -0
- package/dist/src/scripts/module-add.d.ts.map +1 -0
- package/dist/src/scripts/module-add.js +232 -0
- package/dist/src/scripts/module-build.d.ts +2 -0
- package/dist/src/scripts/module-build.d.ts.map +1 -0
- package/dist/src/scripts/module-build.js +1280 -0
- package/dist/src/scripts/module-create.d.ts +28 -0
- package/dist/src/scripts/module-create.d.ts.map +1 -0
- package/dist/src/scripts/module-create.js +1429 -0
- package/dist/src/scripts/module-delete.d.ts +6 -0
- package/dist/src/scripts/module-delete.d.ts.map +1 -0
- package/dist/src/scripts/module-delete.js +147 -0
- package/dist/src/scripts/module-list.d.ts +2 -0
- package/dist/src/scripts/module-list.d.ts.map +1 -0
- package/dist/src/scripts/module-list.js +61 -0
- package/dist/src/scripts/module-remove.d.ts +2 -0
- package/dist/src/scripts/module-remove.d.ts.map +1 -0
- package/dist/src/scripts/module-remove.js +311 -0
- package/dist/src/scripts/readme-build.d.ts +2 -0
- package/dist/src/scripts/readme-build.d.ts.map +1 -0
- package/dist/src/scripts/readme-build.js +39 -0
- package/dist/src/scripts/script-runner.d.ts +5 -0
- package/dist/src/scripts/script-runner.d.ts.map +1 -0
- package/dist/src/scripts/script-runner.js +25 -0
- package/dist/src/templates/AuthGuidePage.d.ts +2 -0
- package/dist/src/templates/AuthGuidePage.d.ts.map +1 -0
- package/dist/src/templates/AuthGuidePage.js +9 -0
- package/dist/src/templates/DefaultDoc.d.ts +2 -0
- package/dist/src/templates/DefaultDoc.d.ts.map +1 -0
- package/dist/src/templates/DefaultDoc.js +240 -0
- package/dist/src/templates/DocPage.d.ts +17 -0
- package/dist/src/templates/DocPage.d.ts.map +1 -0
- package/dist/src/templates/DocPage.js +193 -0
- package/dist/src/templates/DocsPageWithModules.d.ts +2 -0
- package/dist/src/templates/DocsPageWithModules.d.ts.map +1 -0
- package/dist/src/templates/DocsPageWithModules.js +8 -0
- package/dist/src/templates/MigrationsGuidePage.d.ts +2 -0
- package/dist/src/templates/MigrationsGuidePage.d.ts.map +1 -0
- package/dist/src/templates/MigrationsGuidePage.js +11 -0
- package/dist/src/templates/ModuleGuidePage.d.ts +2 -0
- package/dist/src/templates/ModuleGuidePage.d.ts.map +1 -0
- package/dist/src/templates/ModuleGuidePage.js +14 -0
- package/dist/src/templates/SimpleDocPage.d.ts +2 -0
- package/dist/src/templates/SimpleDocPage.d.ts.map +1 -0
- package/dist/src/templates/SimpleDocPage.js +28 -0
- package/dist/src/templates/SimpleHomePage.d.ts +6 -0
- package/dist/src/templates/SimpleHomePage.d.ts.map +1 -0
- package/dist/src/templates/SimpleHomePage.js +7 -0
- package/dist/src/types/menu.d.ts +23 -0
- package/dist/src/types/menu.d.ts.map +1 -0
- package/dist/src/types/menu.js +1 -0
- package/package.json +4 -5
- package/src/scripts/init-app.ts +7 -76
|
@@ -0,0 +1,1429 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
// Noms réservés pour éviter les collisions avec le routeur/app
|
|
9
|
+
const RESERVED_PAGE_NAMES = new Set([
|
|
10
|
+
"layout",
|
|
11
|
+
"page",
|
|
12
|
+
"api",
|
|
13
|
+
"admin",
|
|
14
|
+
"auth",
|
|
15
|
+
"public",
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Parse une chaîne de pages séparées par des virgules
|
|
19
|
+
* Ex: "legal, privacy, terms" => ["legal", "privacy", "terms"]
|
|
20
|
+
*/
|
|
21
|
+
function parsePagesList(input) {
|
|
22
|
+
if (!input || input.trim() === "")
|
|
23
|
+
return [];
|
|
24
|
+
return input
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((p) => p.trim())
|
|
27
|
+
.filter((p) => p.length > 0);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Slugifie un nom de page: minuscules, tirets, alphanum uniquement
|
|
31
|
+
*/
|
|
32
|
+
function slugifyPageName(name) {
|
|
33
|
+
return name
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[\s_]+/g, "-")
|
|
37
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
38
|
+
.replace(/--+/g, "-")
|
|
39
|
+
.replace(/^-+|-+$/g, "");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse une chaîne de tables séparées par des virgules
|
|
43
|
+
* Ex: "settings, users" => ["settings", "users"]
|
|
44
|
+
*/
|
|
45
|
+
function parseTablesList(input) {
|
|
46
|
+
if (!input || input.trim() === "")
|
|
47
|
+
return [];
|
|
48
|
+
return input
|
|
49
|
+
.split(",")
|
|
50
|
+
.map((t) => t.trim())
|
|
51
|
+
.filter((t) => t.length > 0);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Récupère les versions actuelles des packages LastBrain
|
|
55
|
+
*/
|
|
56
|
+
function getLastBrainPackageVersions(rootDir) {
|
|
57
|
+
try {
|
|
58
|
+
const corePackageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "packages", "core", "package.json"), "utf-8"));
|
|
59
|
+
const uiPackageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "packages", "ui", "package.json"), "utf-8"));
|
|
60
|
+
return {
|
|
61
|
+
core: `^${corePackageJson.version}`,
|
|
62
|
+
ui: `^${uiPackageJson.version}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
console.warn(chalk.yellow("⚠️ Impossible de lire les versions des packages, utilisation des versions par défaut"));
|
|
67
|
+
return {
|
|
68
|
+
core: "^0.1.0",
|
|
69
|
+
ui: "^0.1.4",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Génère le contenu du package.json
|
|
75
|
+
*/
|
|
76
|
+
function generatePackageJson(moduleName, slug, rootDir) {
|
|
77
|
+
const versions = getLastBrainPackageVersions(rootDir);
|
|
78
|
+
const moduleNameOnly = slug.replace("module-", "");
|
|
79
|
+
const buildConfigExport = `./${moduleNameOnly}.build.config`;
|
|
80
|
+
return JSON.stringify({
|
|
81
|
+
name: moduleName,
|
|
82
|
+
version: "0.1.0",
|
|
83
|
+
private: false,
|
|
84
|
+
type: "module",
|
|
85
|
+
main: "dist/index.js",
|
|
86
|
+
types: "dist/index.d.ts",
|
|
87
|
+
files: ["dist", "src", "supabase"],
|
|
88
|
+
scripts: {
|
|
89
|
+
build: "tsc -p tsconfig.json",
|
|
90
|
+
dev: "tsc -p tsconfig.json --watch",
|
|
91
|
+
},
|
|
92
|
+
dependencies: {
|
|
93
|
+
"@lastbrain/core": versions.core,
|
|
94
|
+
"@lastbrain/ui": versions.ui,
|
|
95
|
+
react: "^19.0.0",
|
|
96
|
+
"lucide-react": "^0.554.0",
|
|
97
|
+
"react-dom": "^19.0.0",
|
|
98
|
+
},
|
|
99
|
+
devDependencies: {
|
|
100
|
+
typescript: "^5.4.0",
|
|
101
|
+
},
|
|
102
|
+
exports: {
|
|
103
|
+
".": {
|
|
104
|
+
types: "./dist/index.d.ts",
|
|
105
|
+
default: "./dist/index.js",
|
|
106
|
+
},
|
|
107
|
+
"./server": {
|
|
108
|
+
types: "./dist/server.d.ts",
|
|
109
|
+
default: "./dist/server.js",
|
|
110
|
+
},
|
|
111
|
+
[buildConfigExport]: {
|
|
112
|
+
types: `./dist/${moduleNameOnly}.build.config.d.ts`,
|
|
113
|
+
default: `./dist/${moduleNameOnly}.build.config.js`,
|
|
114
|
+
},
|
|
115
|
+
"./api/*": {
|
|
116
|
+
types: "./dist/api/*.d.ts",
|
|
117
|
+
default: "./dist/api/*.js",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
sideEffects: false,
|
|
121
|
+
}, null, 2);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Génère le contenu du tsconfig.json
|
|
125
|
+
*/
|
|
126
|
+
function generateTsConfig() {
|
|
127
|
+
return JSON.stringify({
|
|
128
|
+
extends: "../../tsconfig.base.json",
|
|
129
|
+
compilerOptions: {
|
|
130
|
+
outDir: "dist",
|
|
131
|
+
rootDir: "src",
|
|
132
|
+
declaration: true,
|
|
133
|
+
},
|
|
134
|
+
include: ["src"],
|
|
135
|
+
}, null, 2);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Génère le contenu du fichier build.config.ts
|
|
139
|
+
*/
|
|
140
|
+
function generateBuildConfig(config) {
|
|
141
|
+
const { moduleName, pages, tables } = config;
|
|
142
|
+
const moduleNameOnly = config.slug.replace("module-", "");
|
|
143
|
+
// Helper pour normaliser les chemins (évite // et trim)
|
|
144
|
+
function normalizePath(...segments) {
|
|
145
|
+
return ("/" +
|
|
146
|
+
segments
|
|
147
|
+
.map((s) => s.replace(/^\/+/g, "").replace(/\/+$/g, ""))
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.join("/"));
|
|
150
|
+
}
|
|
151
|
+
// Construit un path de menu selon la section en respectant le pattern attendu
|
|
152
|
+
function buildMenuPath(section, pagePath, pageName) {
|
|
153
|
+
const cleanedPagePath = pagePath.replace(/^\/+/g, "");
|
|
154
|
+
switch (section) {
|
|
155
|
+
case "public": {
|
|
156
|
+
// Public: /<module>/<page> (si non déjà préfixé)
|
|
157
|
+
if (cleanedPagePath.startsWith(moduleNameOnly + "/")) {
|
|
158
|
+
return normalizePath(cleanedPagePath); // déjà préfixé
|
|
159
|
+
}
|
|
160
|
+
return normalizePath(moduleNameOnly, cleanedPagePath);
|
|
161
|
+
}
|
|
162
|
+
case "auth": {
|
|
163
|
+
// Auth: /auth/<module>/<page>
|
|
164
|
+
return normalizePath("auth", moduleNameOnly, cleanedPagePath);
|
|
165
|
+
}
|
|
166
|
+
case "admin": {
|
|
167
|
+
// Admin: /admin/<module>/<pageName> (utilise le slug brut)
|
|
168
|
+
return normalizePath("admin", moduleNameOnly, pageName);
|
|
169
|
+
}
|
|
170
|
+
default:
|
|
171
|
+
return normalizePath(cleanedPagePath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Générer la liste des pages
|
|
175
|
+
const pagesConfig = pages.map((page) => {
|
|
176
|
+
const componentName = page.name
|
|
177
|
+
.split("-")
|
|
178
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
179
|
+
.join("");
|
|
180
|
+
return ` {
|
|
181
|
+
section: "${page.section}",
|
|
182
|
+
path: "${page.path}",
|
|
183
|
+
componentExport: "${componentName}Page",
|
|
184
|
+
}`;
|
|
185
|
+
});
|
|
186
|
+
// Générer la liste des APIs
|
|
187
|
+
const apisConfig = [];
|
|
188
|
+
for (const table of tables) {
|
|
189
|
+
for (const section of table.sections) {
|
|
190
|
+
const methods = ["GET", "POST", "PUT", "DELETE"];
|
|
191
|
+
for (const method of methods) {
|
|
192
|
+
apisConfig.push(` {
|
|
193
|
+
method: "${method}",
|
|
194
|
+
path: "/api/${section}/${table.name}",
|
|
195
|
+
handlerExport: "${method}",
|
|
196
|
+
entryPoint: "api/${section}/${table.name}",
|
|
197
|
+
authRequired: ${section !== "public"},
|
|
198
|
+
}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Générer les menus par section
|
|
203
|
+
const menuSections = [];
|
|
204
|
+
// Menu public
|
|
205
|
+
const publicPages = pages.filter((p) => p.section === "public");
|
|
206
|
+
if (publicPages.length > 0) {
|
|
207
|
+
const publicMenuItems = publicPages.map((page, index) => {
|
|
208
|
+
const title = page.name
|
|
209
|
+
.split("-")
|
|
210
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
211
|
+
.join(" ");
|
|
212
|
+
const menuPath = buildMenuPath("public", page.path, page.name);
|
|
213
|
+
return ` {
|
|
214
|
+
title: "${title}",
|
|
215
|
+
description: "Page ${title}",
|
|
216
|
+
icon: "FileText",
|
|
217
|
+
path: "${menuPath}",
|
|
218
|
+
order: ${index + 1},
|
|
219
|
+
}`;
|
|
220
|
+
});
|
|
221
|
+
menuSections.push({ section: "public", items: publicMenuItems });
|
|
222
|
+
}
|
|
223
|
+
// Menu auth
|
|
224
|
+
const authPages = pages.filter((p) => p.section === "auth");
|
|
225
|
+
if (authPages.length > 0) {
|
|
226
|
+
const authMenuItems = authPages.map((page, index) => {
|
|
227
|
+
const title = page.name
|
|
228
|
+
.split("-")
|
|
229
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
230
|
+
.join(" ");
|
|
231
|
+
const menuPath = buildMenuPath("auth", page.path, page.name);
|
|
232
|
+
return ` {
|
|
233
|
+
title: "${title}",
|
|
234
|
+
description: "Page ${title}",
|
|
235
|
+
icon: "FileText",
|
|
236
|
+
path: "${menuPath}",
|
|
237
|
+
order: ${index + 1},
|
|
238
|
+
}`;
|
|
239
|
+
});
|
|
240
|
+
menuSections.push({ section: "auth", items: authMenuItems });
|
|
241
|
+
}
|
|
242
|
+
// Menu admin
|
|
243
|
+
const adminPages = pages.filter((p) => p.section === "admin");
|
|
244
|
+
if (adminPages.length > 0) {
|
|
245
|
+
const adminMenuItems = adminPages.map((page, index) => {
|
|
246
|
+
const title = page.name
|
|
247
|
+
.split("-")
|
|
248
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
249
|
+
.join(" ");
|
|
250
|
+
const menuPath = buildMenuPath("admin", page.path, page.name);
|
|
251
|
+
return ` {
|
|
252
|
+
title: "${title}",
|
|
253
|
+
description: "Page ${title}",
|
|
254
|
+
icon: "Settings",
|
|
255
|
+
path: "${menuPath}",
|
|
256
|
+
order: ${index + 1},
|
|
257
|
+
}`;
|
|
258
|
+
});
|
|
259
|
+
menuSections.push({ section: "admin", items: adminMenuItems });
|
|
260
|
+
}
|
|
261
|
+
// Générer la configuration menu
|
|
262
|
+
const menuConfig = menuSections.length > 0
|
|
263
|
+
? `,
|
|
264
|
+
menu: {
|
|
265
|
+
${menuSections
|
|
266
|
+
.map(({ section, items }) => ` ${section}: [
|
|
267
|
+
${items.join(",\n")}
|
|
268
|
+
]`)
|
|
269
|
+
.join(",\n")}
|
|
270
|
+
}`
|
|
271
|
+
: "";
|
|
272
|
+
return `import type { ModuleBuildConfig } from "@lastbrain/core";
|
|
273
|
+
|
|
274
|
+
const buildConfig: ModuleBuildConfig = {
|
|
275
|
+
moduleName: "${moduleName}",
|
|
276
|
+
pages: [
|
|
277
|
+
${pagesConfig.join(",\n")}
|
|
278
|
+
],
|
|
279
|
+
apis: [
|
|
280
|
+
${apisConfig.join(",\n")}
|
|
281
|
+
],
|
|
282
|
+
migrations: {
|
|
283
|
+
enabled: true,
|
|
284
|
+
priority: 30,
|
|
285
|
+
path: "supabase/migrations",
|
|
286
|
+
files: ${tables.length > 0 ? `["001_${config.slug}_init.sql"]` : "[]"},
|
|
287
|
+
}${menuConfig},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export default buildConfig;
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Génère le contenu du fichier index.ts
|
|
295
|
+
*/
|
|
296
|
+
function toPascalCase(value) {
|
|
297
|
+
return value
|
|
298
|
+
.split(/[-_]/g)
|
|
299
|
+
.filter(Boolean)
|
|
300
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
301
|
+
.join("");
|
|
302
|
+
}
|
|
303
|
+
function generateIndexTs(pages, moduleNameOnly) {
|
|
304
|
+
const exports = pages.map((page) => {
|
|
305
|
+
const componentName = toPascalCase(page.name);
|
|
306
|
+
const fileName = toPascalCase(page.name);
|
|
307
|
+
return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page";`;
|
|
308
|
+
});
|
|
309
|
+
const moduleAlias = toPascalCase(moduleNameOnly);
|
|
310
|
+
return `// Client Components
|
|
311
|
+
${exports.join("\n")}
|
|
312
|
+
|
|
313
|
+
// Documentation Component
|
|
314
|
+
export { Doc } from "./components/Doc";
|
|
315
|
+
export { Doc as ${moduleAlias}ModuleDoc } from "./components/Doc";
|
|
316
|
+
|
|
317
|
+
// Configuration de build
|
|
318
|
+
export { default as buildConfig } from "./${moduleNameOnly}.build.config";
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Génère le contenu du fichier server.ts
|
|
323
|
+
*/
|
|
324
|
+
function _generateServerTs(tables) {
|
|
325
|
+
const exports = [];
|
|
326
|
+
for (const table of tables) {
|
|
327
|
+
for (const section of table.sections) {
|
|
328
|
+
exports.push(`export { GET, POST, PUT, DELETE } from "./api/${section}/${table.name}.js";`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return `// Server-only exports (Route Handlers, Server Actions, etc.)
|
|
332
|
+
${exports.join("\n")}
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Génère le contenu d'une page
|
|
337
|
+
*/
|
|
338
|
+
function generatePageComponent(pageName, section) {
|
|
339
|
+
const componentName = pageName
|
|
340
|
+
.split("-")
|
|
341
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
342
|
+
.join("");
|
|
343
|
+
return `"use client";
|
|
344
|
+
|
|
345
|
+
import { Card, CardBody, CardHeader } from "@lastbrain/ui";
|
|
346
|
+
|
|
347
|
+
export function ${componentName}Page() {
|
|
348
|
+
return (
|
|
349
|
+
<div className="container mx-auto p-6">
|
|
350
|
+
<Card>
|
|
351
|
+
<CardHeader>
|
|
352
|
+
<h1 className="text-2xl font-bold">${componentName}</h1>
|
|
353
|
+
</CardHeader>
|
|
354
|
+
<CardBody>
|
|
355
|
+
<p className="text-default-600">
|
|
356
|
+
Contenu de la page ${pageName} (section: ${section})
|
|
357
|
+
</p>
|
|
358
|
+
</CardBody>
|
|
359
|
+
</Card>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
`;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Génère le contenu d'une route API CRUD
|
|
367
|
+
*/
|
|
368
|
+
function generateApiRoute(tableName, section) {
|
|
369
|
+
const authRequired = section !== "public";
|
|
370
|
+
const clientImport = section === "admin"
|
|
371
|
+
? 'import { getSupabaseServiceClient } from "@lastbrain/core/server"'
|
|
372
|
+
: 'import { getSupabaseServerClient } from "@lastbrain/core/server"';
|
|
373
|
+
const clientGetter = section === "admin"
|
|
374
|
+
? "getSupabaseServiceClient"
|
|
375
|
+
: "getSupabaseServerClient";
|
|
376
|
+
return `${clientImport};
|
|
377
|
+
|
|
378
|
+
const jsonResponse = (payload: unknown, status = 200) => {
|
|
379
|
+
return new Response(JSON.stringify(payload), {
|
|
380
|
+
headers: {
|
|
381
|
+
"content-type": "application/json"
|
|
382
|
+
},
|
|
383
|
+
status
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* GET - Liste tous les enregistrements de ${tableName}
|
|
389
|
+
*/
|
|
390
|
+
export async function GET(request: Request) {
|
|
391
|
+
const supabase = await ${clientGetter}();
|
|
392
|
+
${authRequired
|
|
393
|
+
? `
|
|
394
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
395
|
+
if (authError || !user) {
|
|
396
|
+
return jsonResponse({ error: "Non authentifié" }, 401);
|
|
397
|
+
}
|
|
398
|
+
`
|
|
399
|
+
: ""}
|
|
400
|
+
|
|
401
|
+
const { data, error } = await supabase
|
|
402
|
+
.from("${tableName}")
|
|
403
|
+
.select("*");
|
|
404
|
+
|
|
405
|
+
if (error) {
|
|
406
|
+
return jsonResponse({ error: error.message }, 400);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return jsonResponse({ data });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* POST - Crée un nouvel enregistrement dans ${tableName}
|
|
414
|
+
*/
|
|
415
|
+
export async function POST(request: Request) {
|
|
416
|
+
const supabase = await ${clientGetter}();
|
|
417
|
+
${authRequired
|
|
418
|
+
? `
|
|
419
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
420
|
+
if (authError || !user) {
|
|
421
|
+
return jsonResponse({ error: "Non authentifié" }, 401);
|
|
422
|
+
}
|
|
423
|
+
`
|
|
424
|
+
: ""}
|
|
425
|
+
|
|
426
|
+
const body = await request.json();
|
|
427
|
+
|
|
428
|
+
// Injection côté serveur de owner_id si l'utilisateur est authentifié (sécurité)
|
|
429
|
+
const insertPayload = ${authRequired ? `{ ...body, owner_id: user.id }` : "body"};
|
|
430
|
+
|
|
431
|
+
const { data, error } = await supabase
|
|
432
|
+
.from("${tableName}")
|
|
433
|
+
.insert(insertPayload)
|
|
434
|
+
.select()
|
|
435
|
+
.single();
|
|
436
|
+
|
|
437
|
+
if (error) {
|
|
438
|
+
return jsonResponse({ error: error.message }, 400);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return jsonResponse({ data }, 201);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* PUT - Met à jour un enregistrement dans ${tableName}
|
|
446
|
+
*/
|
|
447
|
+
export async function PUT(request: Request) {
|
|
448
|
+
const supabase = await ${clientGetter}();
|
|
449
|
+
${authRequired
|
|
450
|
+
? `
|
|
451
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
452
|
+
if (authError || !user) {
|
|
453
|
+
return jsonResponse({ error: "Non authentifié" }, 401);
|
|
454
|
+
}
|
|
455
|
+
`
|
|
456
|
+
: ""}
|
|
457
|
+
|
|
458
|
+
const body = await request.json();
|
|
459
|
+
const { id, ...updateData } = body;
|
|
460
|
+
|
|
461
|
+
if (!id) {
|
|
462
|
+
return jsonResponse({ error: "ID requis pour la mise à jour" }, 400);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const { data, error } = await supabase
|
|
466
|
+
.from("${tableName}")
|
|
467
|
+
.update(updateData)
|
|
468
|
+
.eq("id", id)
|
|
469
|
+
.select()
|
|
470
|
+
.single();
|
|
471
|
+
|
|
472
|
+
if (error) {
|
|
473
|
+
return jsonResponse({ error: error.message }, 400);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return jsonResponse({ data });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* DELETE - Supprime un enregistrement de ${tableName}
|
|
481
|
+
*/
|
|
482
|
+
export async function DELETE(request: Request) {
|
|
483
|
+
const supabase = await ${clientGetter}();
|
|
484
|
+
${authRequired
|
|
485
|
+
? `
|
|
486
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
487
|
+
if (authError || !user) {
|
|
488
|
+
return jsonResponse({ error: "Non authentifié" }, 401);
|
|
489
|
+
}
|
|
490
|
+
`
|
|
491
|
+
: ""}
|
|
492
|
+
|
|
493
|
+
const { searchParams } = new URL(request.url);
|
|
494
|
+
const id = searchParams.get("id");
|
|
495
|
+
|
|
496
|
+
if (!id) {
|
|
497
|
+
return jsonResponse({ error: "ID requis pour la suppression" }, 400);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const { error } = await supabase
|
|
501
|
+
.from("${tableName}")
|
|
502
|
+
.delete()
|
|
503
|
+
.eq("id", id);
|
|
504
|
+
|
|
505
|
+
if (error) {
|
|
506
|
+
return jsonResponse({ error: error.message }, 400);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return jsonResponse({ success: true });
|
|
510
|
+
}
|
|
511
|
+
`;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Génère le contenu d'un fichier de migration SQL
|
|
515
|
+
*/
|
|
516
|
+
function generateMigration(tables, slug) {
|
|
517
|
+
const _timestamp = new Date()
|
|
518
|
+
.toISOString()
|
|
519
|
+
.replace(/[-:]/g, "")
|
|
520
|
+
.split(".")[0]
|
|
521
|
+
.replace("T", "");
|
|
522
|
+
const tablesSQL = tables
|
|
523
|
+
.map((table) => {
|
|
524
|
+
return `-- ===========================================================================
|
|
525
|
+
-- Table: public.${table.name}
|
|
526
|
+
-- ===========================================================================
|
|
527
|
+
CREATE TABLE IF NOT EXISTS public.${table.name} (
|
|
528
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
529
|
+
owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
530
|
+
title text NOT NULL,
|
|
531
|
+
description text,
|
|
532
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
533
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
-- RLS
|
|
537
|
+
ALTER TABLE public.${table.name} ENABLE ROW LEVEL SECURITY;
|
|
538
|
+
|
|
539
|
+
-- Politique: Les utilisateurs peuvent voir leurs propres enregistrements
|
|
540
|
+
DROP POLICY IF EXISTS ${table.name}_owner_select ON public.${table.name};
|
|
541
|
+
CREATE POLICY ${table.name}_owner_select ON public.${table.name}
|
|
542
|
+
FOR SELECT TO authenticated
|
|
543
|
+
USING (owner_id = auth.uid());
|
|
544
|
+
|
|
545
|
+
-- Politique: Les utilisateurs peuvent créer leurs propres enregistrements
|
|
546
|
+
DROP POLICY IF EXISTS ${table.name}_owner_insert ON public.${table.name};
|
|
547
|
+
CREATE POLICY ${table.name}_owner_insert ON public.${table.name}
|
|
548
|
+
FOR INSERT TO authenticated
|
|
549
|
+
WITH CHECK (owner_id = auth.uid());
|
|
550
|
+
|
|
551
|
+
-- Politique: Les utilisateurs peuvent modifier leurs propres enregistrements
|
|
552
|
+
DROP POLICY IF EXISTS ${table.name}_owner_update ON public.${table.name};
|
|
553
|
+
CREATE POLICY ${table.name}_owner_update ON public.${table.name}
|
|
554
|
+
FOR UPDATE TO authenticated
|
|
555
|
+
USING (owner_id = auth.uid())
|
|
556
|
+
WITH CHECK (owner_id = auth.uid());
|
|
557
|
+
|
|
558
|
+
-- Politique: Les utilisateurs peuvent supprimer leurs propres enregistrements
|
|
559
|
+
DROP POLICY IF EXISTS ${table.name}_owner_delete ON public.${table.name};
|
|
560
|
+
CREATE POLICY ${table.name}_owner_delete ON public.${table.name}
|
|
561
|
+
FOR DELETE TO authenticated
|
|
562
|
+
USING (owner_id = auth.uid());
|
|
563
|
+
|
|
564
|
+
-- Trigger updated_at
|
|
565
|
+
DROP TRIGGER IF EXISTS set_${table.name}_updated_at ON public.${table.name};
|
|
566
|
+
CREATE TRIGGER set_${table.name}_updated_at
|
|
567
|
+
BEFORE UPDATE ON public.${table.name}
|
|
568
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
569
|
+
|
|
570
|
+
-- Index
|
|
571
|
+
CREATE INDEX IF NOT EXISTS idx_${table.name}_owner_id ON public.${table.name}(owner_id);
|
|
572
|
+
|
|
573
|
+
-- Grants
|
|
574
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.${table.name} TO service_role;
|
|
575
|
+
`;
|
|
576
|
+
})
|
|
577
|
+
.join("\n\n");
|
|
578
|
+
return `-- ${slug} module initial migration
|
|
579
|
+
-- Auto-generated by module-create.ts
|
|
580
|
+
-- NOTE: uses helper function set_updated_at() from base migration
|
|
581
|
+
|
|
582
|
+
-- ===========================================================================
|
|
583
|
+
-- Helper: set_updated_at trigger function (if not already present)
|
|
584
|
+
-- ===========================================================================
|
|
585
|
+
CREATE OR REPLACE FUNCTION public.set_updated_at()
|
|
586
|
+
RETURNS trigger
|
|
587
|
+
LANGUAGE plpgsql
|
|
588
|
+
AS $$
|
|
589
|
+
BEGIN
|
|
590
|
+
NEW.updated_at := now();
|
|
591
|
+
RETURN NEW;
|
|
592
|
+
END;
|
|
593
|
+
$$;
|
|
594
|
+
|
|
595
|
+
${tablesSQL}
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Generate Doc.tsx component for the module
|
|
600
|
+
*/
|
|
601
|
+
function generateDocComponent(config) {
|
|
602
|
+
const moduleNameClean = config.slug.replace("module-", "");
|
|
603
|
+
const moduleNameOnly = moduleNameClean;
|
|
604
|
+
// Generate pages sections
|
|
605
|
+
const publicPages = config.pages.filter((p) => p.section === "public");
|
|
606
|
+
const authPages = config.pages.filter((p) => p.section === "auth");
|
|
607
|
+
const adminPages = config.pages.filter((p) => p.section === "admin");
|
|
608
|
+
let pagesSection = "";
|
|
609
|
+
if (config.pages.length > 0) {
|
|
610
|
+
pagesSection = `
|
|
611
|
+
<Card>
|
|
612
|
+
<CardHeader>
|
|
613
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
614
|
+
<FileText size={24} />
|
|
615
|
+
Pages Disponibles
|
|
616
|
+
</h2>
|
|
617
|
+
</CardHeader>
|
|
618
|
+
<CardBody className="space-y-4">`;
|
|
619
|
+
if (publicPages.length > 0) {
|
|
620
|
+
pagesSection += `
|
|
621
|
+
<div>
|
|
622
|
+
<h3 className="text-lg font-semibold mb-2">Pages Publiques</h3>
|
|
623
|
+
<div className="space-y-2">`;
|
|
624
|
+
for (const page of publicPages) {
|
|
625
|
+
const componentName = page.name
|
|
626
|
+
.split("-")
|
|
627
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
628
|
+
.join("");
|
|
629
|
+
pagesSection += `
|
|
630
|
+
<div className="flex items-start gap-2">
|
|
631
|
+
<Chip size="sm" color="success" variant="flat">GET</Chip>
|
|
632
|
+
<code className="text-sm">${page.path}</code>
|
|
633
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
634
|
+
</div>`;
|
|
635
|
+
}
|
|
636
|
+
pagesSection += `
|
|
637
|
+
</div>
|
|
638
|
+
</div>`;
|
|
639
|
+
}
|
|
640
|
+
if (authPages.length > 0) {
|
|
641
|
+
pagesSection += `
|
|
642
|
+
<div>
|
|
643
|
+
<h3 className="text-lg font-semibold mb-2">Pages Protégées (Auth)</h3>
|
|
644
|
+
<div className="space-y-2">`;
|
|
645
|
+
for (const page of authPages) {
|
|
646
|
+
const componentName = page.name
|
|
647
|
+
.split("-")
|
|
648
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
649
|
+
.join("");
|
|
650
|
+
pagesSection += `
|
|
651
|
+
<div className="flex items-start gap-2">
|
|
652
|
+
<Chip size="sm" color="primary" variant="flat">GET</Chip>
|
|
653
|
+
<code className="text-sm">${page.path}</code>
|
|
654
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
655
|
+
</div>`;
|
|
656
|
+
}
|
|
657
|
+
pagesSection += `
|
|
658
|
+
</div>
|
|
659
|
+
</div>`;
|
|
660
|
+
}
|
|
661
|
+
if (adminPages.length > 0) {
|
|
662
|
+
pagesSection += `
|
|
663
|
+
<div>
|
|
664
|
+
<h3 className="text-lg font-semibold mb-2">Pages Admin</h3>
|
|
665
|
+
<div className="space-y-2">`;
|
|
666
|
+
for (const page of adminPages) {
|
|
667
|
+
const componentName = page.name
|
|
668
|
+
.split("-")
|
|
669
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
670
|
+
.join("");
|
|
671
|
+
pagesSection += `
|
|
672
|
+
<div className="flex items-start gap-2">
|
|
673
|
+
<Chip size="sm" color="secondary" variant="flat">GET</Chip>
|
|
674
|
+
<code className="text-sm">/admin/${moduleNameOnly}/${page.name}</code>
|
|
675
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
676
|
+
</div>`;
|
|
677
|
+
}
|
|
678
|
+
pagesSection += `
|
|
679
|
+
</div>
|
|
680
|
+
</div>`;
|
|
681
|
+
}
|
|
682
|
+
pagesSection += `
|
|
683
|
+
</CardBody>
|
|
684
|
+
</Card>
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
// Generate APIs section
|
|
688
|
+
let apisSection = "";
|
|
689
|
+
if (config.tables.length > 0) {
|
|
690
|
+
apisSection = `
|
|
691
|
+
<Card>
|
|
692
|
+
<CardHeader>
|
|
693
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
694
|
+
<Zap size={24} />
|
|
695
|
+
API Routes
|
|
696
|
+
</h2>
|
|
697
|
+
</CardHeader>
|
|
698
|
+
<CardBody className="space-y-4">`;
|
|
699
|
+
for (const table of config.tables) {
|
|
700
|
+
for (const section of table.sections) {
|
|
701
|
+
apisSection += `
|
|
702
|
+
<div>
|
|
703
|
+
<h3 className="text-lg font-semibold mb-2">
|
|
704
|
+
<code>/api/${section}/${table.name}</code>
|
|
705
|
+
</h3>
|
|
706
|
+
<div className="flex gap-2">
|
|
707
|
+
<Chip size="sm" color="success" variant="flat">GET</Chip>
|
|
708
|
+
<Chip size="sm" color="primary" variant="flat">POST</Chip>
|
|
709
|
+
<Chip size="sm" color="warning" variant="flat">PUT</Chip>
|
|
710
|
+
<Chip size="sm" color="danger" variant="flat">DELETE</Chip>
|
|
711
|
+
</div>
|
|
712
|
+
</div>`;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
apisSection += `
|
|
716
|
+
</CardBody>
|
|
717
|
+
</Card>
|
|
718
|
+
`;
|
|
719
|
+
}
|
|
720
|
+
// Generate tables section
|
|
721
|
+
let tablesSection = "";
|
|
722
|
+
if (config.tables.length > 0) {
|
|
723
|
+
tablesSection = `
|
|
724
|
+
<Card>
|
|
725
|
+
<CardHeader>
|
|
726
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
727
|
+
<Database size={24} />
|
|
728
|
+
Base de Données
|
|
729
|
+
</h2>
|
|
730
|
+
</CardHeader>
|
|
731
|
+
<CardBody className="space-y-6">`;
|
|
732
|
+
for (const table of config.tables) {
|
|
733
|
+
tablesSection += `
|
|
734
|
+
<TableStructure
|
|
735
|
+
tableName="${table.name}"
|
|
736
|
+
title="${table.name}"
|
|
737
|
+
description="Table ${table.name} du module ${moduleNameClean}"
|
|
738
|
+
/>`;
|
|
739
|
+
}
|
|
740
|
+
tablesSection += `
|
|
741
|
+
</CardBody>
|
|
742
|
+
</Card>
|
|
743
|
+
`;
|
|
744
|
+
}
|
|
745
|
+
// Installation commands
|
|
746
|
+
const installSection = `
|
|
747
|
+
<Card>
|
|
748
|
+
<CardHeader>
|
|
749
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
750
|
+
<Package size={24} />
|
|
751
|
+
Installation
|
|
752
|
+
</h2>
|
|
753
|
+
</CardHeader>
|
|
754
|
+
<CardBody className="space-y-4">
|
|
755
|
+
<div>
|
|
756
|
+
<h3 className="text-lg font-semibold mb-2">Ajouter le module</h3>
|
|
757
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
758
|
+
pnpm lastbrain add-module ${moduleNameClean}
|
|
759
|
+
</Snippet>
|
|
760
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
761
|
+
pnpm build:modules
|
|
762
|
+
</Snippet>
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
<div>
|
|
766
|
+
<h3 className="text-lg font-semibold mb-2">Appliquer les migrations</h3>
|
|
767
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
768
|
+
cd apps/votre-app
|
|
769
|
+
</Snippet>
|
|
770
|
+
<Snippet symbol="" hideSymbol className="text-sm mb-2">
|
|
771
|
+
supabase migration up
|
|
772
|
+
</Snippet>
|
|
773
|
+
</div>
|
|
774
|
+
</CardBody>
|
|
775
|
+
</Card>
|
|
776
|
+
`;
|
|
777
|
+
// Usage section with placeholder
|
|
778
|
+
const usageSection = `
|
|
779
|
+
<Card>
|
|
780
|
+
<CardHeader>
|
|
781
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
782
|
+
<BookOpen size={24} />
|
|
783
|
+
Utilisation
|
|
784
|
+
</h2>
|
|
785
|
+
</CardHeader>
|
|
786
|
+
<CardBody className="space-y-4">
|
|
787
|
+
<Alert color="default" className="mb-4">
|
|
788
|
+
<p className="text-sm">
|
|
789
|
+
📝 <strong>Section à compléter par l'auteur du module</strong>
|
|
790
|
+
</p>
|
|
791
|
+
<p className="text-sm text-slate-600 dark:text-slate-400 mt-2">
|
|
792
|
+
Ajoutez ici des exemples d'utilisation, des configurations spécifiques,
|
|
793
|
+
et toute information utile pour les développeurs utilisant ce module.
|
|
794
|
+
</p>
|
|
795
|
+
</Alert>
|
|
796
|
+
|
|
797
|
+
<div>
|
|
798
|
+
<h3 className="text-lg font-semibold mb-2">Exemple d'utilisation</h3>
|
|
799
|
+
<Alert color="primary" className="p-4 mb-4">
|
|
800
|
+
<pre className="whitespace-pre-wrap">{\`// Importez les composants depuis le module
|
|
801
|
+
import { ${config.pages.length > 0
|
|
802
|
+
? config.pages[0].name
|
|
803
|
+
.split("-")
|
|
804
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
805
|
+
.join("") + "Page"
|
|
806
|
+
: "Component"} } from "${config.moduleName}";
|
|
807
|
+
|
|
808
|
+
// Utilisez-les dans votre application
|
|
809
|
+
<${config.pages.length > 0
|
|
810
|
+
? config.pages[0].name
|
|
811
|
+
.split("-")
|
|
812
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
813
|
+
.join("") + "Page"
|
|
814
|
+
: "Component"} />\`}</pre>
|
|
815
|
+
</Alert>
|
|
816
|
+
</div>
|
|
817
|
+
</CardBody>
|
|
818
|
+
</Card>
|
|
819
|
+
`;
|
|
820
|
+
// Danger zone
|
|
821
|
+
const dangerSection = `
|
|
822
|
+
<Card>
|
|
823
|
+
<CardHeader>
|
|
824
|
+
<h2 className="text-2xl font-semibold flex items-center gap-2 text-danger">
|
|
825
|
+
<AlertTriangle size={24} />
|
|
826
|
+
Danger Zone
|
|
827
|
+
</h2>
|
|
828
|
+
</CardHeader>
|
|
829
|
+
<CardBody className="space-y-4">
|
|
830
|
+
<Alert color="danger" className="mb-4">
|
|
831
|
+
<p className="text-sm font-semibold">
|
|
832
|
+
⚠️ Cette action est irréversible
|
|
833
|
+
</p>
|
|
834
|
+
<p className="text-sm mt-2">
|
|
835
|
+
La suppression du module supprimera toutes les pages, routes API et migrations associées.
|
|
836
|
+
</p>
|
|
837
|
+
</Alert>
|
|
838
|
+
|
|
839
|
+
<div>
|
|
840
|
+
<h3 className="text-lg font-semibold mb-2">Supprimer le module</h3>
|
|
841
|
+
<Snippet symbol="" hideSymbol color="danger" className="text-sm mb-2">
|
|
842
|
+
pnpm lastbrain remove-module ${moduleNameClean}
|
|
843
|
+
</Snippet>
|
|
844
|
+
<Snippet symbol="" hideSymbol color="danger" className="text-sm mb-2">
|
|
845
|
+
pnpm build:modules
|
|
846
|
+
</Snippet>
|
|
847
|
+
</div>
|
|
848
|
+
</CardBody>
|
|
849
|
+
</Card>
|
|
850
|
+
`;
|
|
851
|
+
return `"use client";
|
|
852
|
+
|
|
853
|
+
import { Card, CardBody, CardHeader } from "@lastbrain/ui";
|
|
854
|
+
import { Chip } from "@lastbrain/ui";
|
|
855
|
+
import { Snippet } from "@lastbrain/ui";
|
|
856
|
+
import { Alert } from "@lastbrain/ui";
|
|
857
|
+
import { TableStructure } from "@lastbrain/ui";
|
|
858
|
+
import {
|
|
859
|
+
FileText,
|
|
860
|
+
Zap,
|
|
861
|
+
Database,
|
|
862
|
+
Package,
|
|
863
|
+
BookOpen,
|
|
864
|
+
AlertTriangle
|
|
865
|
+
} from "lucide-react";
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Documentation component for ${config.moduleName}
|
|
869
|
+
* Auto-generated from ${config.slug}.build.config.ts
|
|
870
|
+
*
|
|
871
|
+
* To regenerate this file, run:
|
|
872
|
+
* pnpm generate:module-docs
|
|
873
|
+
*/
|
|
874
|
+
export function Doc() {
|
|
875
|
+
return (
|
|
876
|
+
<div className="container mx-auto p-6 space-y-6">
|
|
877
|
+
<Card>
|
|
878
|
+
<CardHeader>
|
|
879
|
+
<div>
|
|
880
|
+
<h1 className="text-3xl font-bold mb-2">📦 Module ${moduleNameClean}</h1>
|
|
881
|
+
<p className="text-slate-600 dark:text-slate-400">${config.moduleName}</p>
|
|
882
|
+
</div>
|
|
883
|
+
</CardHeader>
|
|
884
|
+
<CardBody>
|
|
885
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
886
|
+
<div>
|
|
887
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Package</p>
|
|
888
|
+
<code className="text-sm font-semibold">${config.moduleName}</code>
|
|
889
|
+
</div>
|
|
890
|
+
<div>
|
|
891
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Slug</p>
|
|
892
|
+
<code className="text-sm font-semibold">${config.slug}</code>
|
|
893
|
+
</div>
|
|
894
|
+
<div>
|
|
895
|
+
<p className="text-sm text-slate-600 dark:text-slate-400">Type</p>
|
|
896
|
+
<code className="text-sm font-semibold">Module LastBrain</code>
|
|
897
|
+
</div>
|
|
898
|
+
</div>
|
|
899
|
+
</CardBody>
|
|
900
|
+
</Card>
|
|
901
|
+
${pagesSection}${apisSection}${tablesSection}${installSection}${usageSection}${dangerSection}
|
|
902
|
+
</div>
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
`;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Generate README.md for the module
|
|
909
|
+
*/
|
|
910
|
+
async function generateModuleReadme(config, moduleDir) {
|
|
911
|
+
const moduleNameClean = config.slug.replace("module-", "");
|
|
912
|
+
let md = `# 📦 Module ${moduleNameClean}\n\n`;
|
|
913
|
+
md += `> ${config.moduleName}\n\n`;
|
|
914
|
+
// Description section
|
|
915
|
+
if (config.description) {
|
|
916
|
+
md += `## 📝 Description\n\n`;
|
|
917
|
+
md += `${config.description}\n\n`;
|
|
918
|
+
}
|
|
919
|
+
// Information section
|
|
920
|
+
md += `## 📋 Informations\n\n`;
|
|
921
|
+
md += `- **Nom du package**: \`${config.moduleName}\`\n`;
|
|
922
|
+
md += `- **Slug**: \`${config.slug}\`\n`;
|
|
923
|
+
md += `- **Type**: Module LastBrain\n\n`;
|
|
924
|
+
// Pages section
|
|
925
|
+
if (config.pages.length > 0) {
|
|
926
|
+
md += `## 📄 Pages Disponibles\n\n`;
|
|
927
|
+
const publicPages = config.pages.filter((p) => p.section === "public");
|
|
928
|
+
const authPages = config.pages.filter((p) => p.section === "auth");
|
|
929
|
+
const adminPages = config.pages.filter((p) => p.section === "admin");
|
|
930
|
+
if (publicPages.length > 0) {
|
|
931
|
+
md += `### Pages Publiques\n\n`;
|
|
932
|
+
for (const page of publicPages) {
|
|
933
|
+
const componentName = page.name
|
|
934
|
+
.split("-")
|
|
935
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
936
|
+
.join("");
|
|
937
|
+
md += `- **GET** \`${page.path}\` - ${componentName}\n`;
|
|
938
|
+
}
|
|
939
|
+
md += `\n`;
|
|
940
|
+
}
|
|
941
|
+
if (authPages.length > 0) {
|
|
942
|
+
md += `### Pages Protégées (Auth)\n\n`;
|
|
943
|
+
for (const page of authPages) {
|
|
944
|
+
const componentName = page.name
|
|
945
|
+
.split("-")
|
|
946
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
947
|
+
.join("");
|
|
948
|
+
md += `- **GET** \`${page.path}\` - ${componentName}\n`;
|
|
949
|
+
}
|
|
950
|
+
md += `\n`;
|
|
951
|
+
}
|
|
952
|
+
if (adminPages.length > 0) {
|
|
953
|
+
md += `### Pages Admin\n\n`;
|
|
954
|
+
for (const page of adminPages) {
|
|
955
|
+
const componentName = page.name
|
|
956
|
+
.split("-")
|
|
957
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
958
|
+
.join("");
|
|
959
|
+
md += `- **GET** \`/admin/${config.slug.replace("module-", "")}/${page.name}\` - ${componentName}\n`;
|
|
960
|
+
}
|
|
961
|
+
md += `\n`;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// APIs section
|
|
965
|
+
if (config.tables.length > 0) {
|
|
966
|
+
md += `## 🔌 API Routes\n\n`;
|
|
967
|
+
for (const table of config.tables) {
|
|
968
|
+
for (const section of table.sections) {
|
|
969
|
+
md += `### \`/api/${section}/${table.name}\`\n\n`;
|
|
970
|
+
md += `**Méthodes supportées**: GET, POST, PUT, DELETE\n\n`;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// Database section
|
|
975
|
+
if (config.tables.length > 0) {
|
|
976
|
+
md += `## 🗄️ Base de Données\n\n`;
|
|
977
|
+
md += `### Tables\n\n`;
|
|
978
|
+
for (const table of config.tables) {
|
|
979
|
+
md += `#### \`${table.name}\`\n\n`;
|
|
980
|
+
md += `\`\`\`tsx\n`;
|
|
981
|
+
md += `<TableStructure\n`;
|
|
982
|
+
md += ` tableName="${table.name}"\n`;
|
|
983
|
+
md += ` title="${table.name}"\n`;
|
|
984
|
+
md += ` description="Table ${table.name} du module ${moduleNameClean}"\n`;
|
|
985
|
+
md += `/>\n`;
|
|
986
|
+
md += `\`\`\`\n\n`;
|
|
987
|
+
}
|
|
988
|
+
// Get migration files
|
|
989
|
+
const migrationsPath = path.join(moduleDir, "supabase", "migrations");
|
|
990
|
+
if (fs.existsSync(migrationsPath)) {
|
|
991
|
+
const migrationFiles = fs
|
|
992
|
+
.readdirSync(migrationsPath)
|
|
993
|
+
.filter((f) => f.endsWith(".sql"))
|
|
994
|
+
.sort();
|
|
995
|
+
if (migrationFiles.length > 0) {
|
|
996
|
+
md += `### Migrations\n\n`;
|
|
997
|
+
for (const migration of migrationFiles) {
|
|
998
|
+
md += `- \`${migration}\`\n`;
|
|
999
|
+
}
|
|
1000
|
+
md += `\n`;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// Installation section
|
|
1005
|
+
md += `## 📦 Installation\n\n`;
|
|
1006
|
+
md += `\`\`\`bash\n`;
|
|
1007
|
+
md += `pnpm lastbrain add-module ${moduleNameClean}\n`;
|
|
1008
|
+
md += `pnpm build:modules\n`;
|
|
1009
|
+
md += `\`\`\`\n\n`;
|
|
1010
|
+
md += `### Appliquer les migrations\n\n`;
|
|
1011
|
+
md += `\`\`\`bash\n`;
|
|
1012
|
+
md += `cd apps/votre-app\n`;
|
|
1013
|
+
md += `supabase migration up\n`;
|
|
1014
|
+
md += `\`\`\`\n\n`;
|
|
1015
|
+
// Usage section
|
|
1016
|
+
md += `## 💡 Utilisation\n\n`;
|
|
1017
|
+
md += `<!-- 📝 Section à compléter par l'auteur du module -->\n\n`;
|
|
1018
|
+
md += `### Exemple d'utilisation\n\n`;
|
|
1019
|
+
md += `\`\`\`tsx\n`;
|
|
1020
|
+
md += `// Importez les composants depuis le module\n`;
|
|
1021
|
+
if (config.pages.length > 0) {
|
|
1022
|
+
const firstPage = config.pages[0];
|
|
1023
|
+
const componentName = firstPage.name
|
|
1024
|
+
.split("-")
|
|
1025
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1026
|
+
.join("");
|
|
1027
|
+
md += `import { ${componentName}Page } from "${config.moduleName}";\n\n`;
|
|
1028
|
+
md += `// Utilisez-les dans votre application\n`;
|
|
1029
|
+
md += `<${componentName}Page />\n`;
|
|
1030
|
+
}
|
|
1031
|
+
md += `\`\`\`\n\n`;
|
|
1032
|
+
md += `### Configuration\n\n`;
|
|
1033
|
+
md += `<!-- Ajoutez ici les détails de configuration spécifiques -->\n\n`;
|
|
1034
|
+
// Danger zone
|
|
1035
|
+
md += `## ⚠️ Danger Zone\n\n`;
|
|
1036
|
+
md += `La suppression du module supprimera toutes les pages, routes API et migrations associées. **Cette action est irréversible.**\n\n`;
|
|
1037
|
+
md += `\`\`\`bash\n`;
|
|
1038
|
+
md += `pnpm lastbrain remove-module ${moduleNameClean}\n`;
|
|
1039
|
+
md += `pnpm build:modules\n`;
|
|
1040
|
+
md += `\`\`\`\n\n`;
|
|
1041
|
+
// Write README.md
|
|
1042
|
+
const readmePath = path.join(moduleDir, "README.md");
|
|
1043
|
+
await fs.writeFile(readmePath, md);
|
|
1044
|
+
console.log(chalk.yellow(" 📄 README.md"));
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Met à jour le registre des modules dans core/src/config/modules.ts
|
|
1048
|
+
*/
|
|
1049
|
+
async function updateModuleRegistry(config, rootDir) {
|
|
1050
|
+
const moduleRegistryPath = path.join(rootDir, "packages", "core", "src", "config", "modules.ts");
|
|
1051
|
+
if (!fs.existsSync(moduleRegistryPath)) {
|
|
1052
|
+
console.log(chalk.yellow(" ⚠️ Fichier de registre non trouvé, création..."));
|
|
1053
|
+
// Si le fichier n'existe pas, on le crée avec le module actuel
|
|
1054
|
+
const content = `/**
|
|
1055
|
+
* Configuration centralisée des modules LastBrain
|
|
1056
|
+
* Ce fichier est auto-généré et maintenu par les scripts de gestion des modules
|
|
1057
|
+
*/
|
|
1058
|
+
|
|
1059
|
+
export interface ModuleMetadata {
|
|
1060
|
+
name: string;
|
|
1061
|
+
package: string;
|
|
1062
|
+
description: string;
|
|
1063
|
+
emoji: string;
|
|
1064
|
+
version?: string;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
export const AVAILABLE_MODULES: ModuleMetadata[] = [
|
|
1068
|
+
{
|
|
1069
|
+
name: "${config.slug.replace("module-", "")}",
|
|
1070
|
+
package: "@lastbrain/${config.slug}",
|
|
1071
|
+
description: "Module ${config.moduleName}",
|
|
1072
|
+
emoji: "📦",
|
|
1073
|
+
},
|
|
1074
|
+
];
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Récupère les métadonnées d'un module par son nom
|
|
1078
|
+
*/
|
|
1079
|
+
export function getModuleMetadata(name: string): ModuleMetadata | undefined {
|
|
1080
|
+
return AVAILABLE_MODULES.find((m) => m.name === name);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Vérifie si un module existe
|
|
1085
|
+
*/
|
|
1086
|
+
export function moduleExists(name: string): boolean {
|
|
1087
|
+
return AVAILABLE_MODULES.some((m) => m.name === name);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Récupère la liste des noms de modules disponibles
|
|
1092
|
+
*/
|
|
1093
|
+
export function getAvailableModuleNames(): string[] {
|
|
1094
|
+
return AVAILABLE_MODULES.map((m) => m.name);
|
|
1095
|
+
}
|
|
1096
|
+
`;
|
|
1097
|
+
await fs.writeFile(moduleRegistryPath, content, "utf-8");
|
|
1098
|
+
console.log(chalk.green(" ✓ Registre créé"));
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
let content = await fs.readFile(moduleRegistryPath, "utf-8");
|
|
1103
|
+
const moduleName = config.slug.replace("module-", "");
|
|
1104
|
+
const moduleEntry = ` {
|
|
1105
|
+
name: "${moduleName}",
|
|
1106
|
+
package: "@lastbrain/${config.slug}",
|
|
1107
|
+
description: "Module ${config.moduleName}",
|
|
1108
|
+
emoji: "📦",
|
|
1109
|
+
},`;
|
|
1110
|
+
// Vérifier si le module existe déjà
|
|
1111
|
+
if (content.includes(`name: "${moduleName}"`)) {
|
|
1112
|
+
console.log(chalk.yellow(` ⚠️ Module ${moduleName} déjà présent dans le registre`));
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
// Trouver le tableau AVAILABLE_MODULES et ajouter le module
|
|
1116
|
+
const arrayMatch = content.match(/export const AVAILABLE_MODULES: ModuleMetadata\[\] = \[([\s\S]*?)\];/);
|
|
1117
|
+
if (arrayMatch) {
|
|
1118
|
+
const arrayContent = arrayMatch[1];
|
|
1119
|
+
const _lastItem = arrayContent.trim().split("\n").pop();
|
|
1120
|
+
// Ajouter le nouveau module à la fin du tableau
|
|
1121
|
+
const newArrayContent = arrayContent.trimEnd() + "\n" + moduleEntry + "\n";
|
|
1122
|
+
content = content.replace(/export const AVAILABLE_MODULES: ModuleMetadata\[\] = \[([\s\S]*?)\];/, `export const AVAILABLE_MODULES: ModuleMetadata[] = [${newArrayContent}];`);
|
|
1123
|
+
await fs.writeFile(moduleRegistryPath, content, "utf-8");
|
|
1124
|
+
console.log(chalk.green(` ✓ Module ${moduleName} ajouté au registre`));
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
console.log(chalk.yellow(" ⚠️ Format du registre non reconnu, ajout manuel requis"));
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
catch (_error) {
|
|
1131
|
+
console.log(chalk.yellow(` ⚠️ Erreur lors de la mise à jour du registre: ${_error}`));
|
|
1132
|
+
console.log(chalk.gray(" Vous devrez ajouter manuellement le module au registre"));
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Trouve le répertoire racine du workspace (avec pnpm-workspace.yaml)
|
|
1137
|
+
*/
|
|
1138
|
+
export function findWorkspaceRoot() {
|
|
1139
|
+
let rootDir = process.cwd();
|
|
1140
|
+
let attempts = 0;
|
|
1141
|
+
const maxAttempts = 5;
|
|
1142
|
+
while (attempts < maxAttempts) {
|
|
1143
|
+
const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
|
|
1144
|
+
if (fs.existsSync(workspaceFile)) {
|
|
1145
|
+
return rootDir;
|
|
1146
|
+
}
|
|
1147
|
+
rootDir = path.resolve(rootDir, "..");
|
|
1148
|
+
attempts++;
|
|
1149
|
+
}
|
|
1150
|
+
throw new Error("Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)");
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Crée la structure du module
|
|
1154
|
+
*/
|
|
1155
|
+
export async function createModuleStructure(config, rootDir) {
|
|
1156
|
+
const moduleDir = path.join(rootDir, "packages", config.slug);
|
|
1157
|
+
console.log(chalk.blue(`\n📦 Création du module ${config.slug}...\n`));
|
|
1158
|
+
// Créer la structure de base
|
|
1159
|
+
await fs.ensureDir(moduleDir);
|
|
1160
|
+
await fs.ensureDir(path.join(moduleDir, "src"));
|
|
1161
|
+
await fs.ensureDir(path.join(moduleDir, "src", "web"));
|
|
1162
|
+
await fs.ensureDir(path.join(moduleDir, "src", "api"));
|
|
1163
|
+
await fs.ensureDir(path.join(moduleDir, "src", "components"));
|
|
1164
|
+
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations"));
|
|
1165
|
+
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations-down"));
|
|
1166
|
+
// Créer package.json
|
|
1167
|
+
console.log(chalk.yellow(" 📄 package.json"));
|
|
1168
|
+
await fs.writeFile(path.join(moduleDir, "package.json"), generatePackageJson(config.moduleName, config.slug, rootDir));
|
|
1169
|
+
// Créer tsconfig.json
|
|
1170
|
+
console.log(chalk.yellow(" 📄 tsconfig.json"));
|
|
1171
|
+
await fs.writeFile(path.join(moduleDir, "tsconfig.json"), generateTsConfig());
|
|
1172
|
+
// Créer {name}.build.config.ts (sans le préfixe module-)
|
|
1173
|
+
const moduleNameOnly = config.slug.replace("module-", "");
|
|
1174
|
+
const buildConfigFileName = `${moduleNameOnly}.build.config.ts`;
|
|
1175
|
+
console.log(chalk.yellow(` 📄 src/${buildConfigFileName}`));
|
|
1176
|
+
await fs.writeFile(path.join(moduleDir, "src", buildConfigFileName), generateBuildConfig(config));
|
|
1177
|
+
// Créer index.ts
|
|
1178
|
+
console.log(chalk.yellow(" 📄 src/index.ts"));
|
|
1179
|
+
await fs.writeFile(path.join(moduleDir, "src", "index.ts"), generateIndexTs(config.pages, moduleNameOnly));
|
|
1180
|
+
// Note: server.ts n'est plus généré pour éviter les conflits d'exports
|
|
1181
|
+
// Les routes API sont exposées via le build.config et l'app les importe dynamiquement
|
|
1182
|
+
// Créer Doc.tsx
|
|
1183
|
+
console.log(chalk.yellow(" 📄 src/components/Doc.tsx"));
|
|
1184
|
+
await fs.writeFile(path.join(moduleDir, "src", "components", "Doc.tsx"), generateDocComponent(config));
|
|
1185
|
+
// Créer les pages
|
|
1186
|
+
console.log(chalk.blue("\n📄 Création des pages..."));
|
|
1187
|
+
for (const page of config.pages) {
|
|
1188
|
+
const pagePath = path.join(moduleDir, "src", "web", page.section);
|
|
1189
|
+
await fs.ensureDir(pagePath);
|
|
1190
|
+
const componentName = page.name
|
|
1191
|
+
.split("-")
|
|
1192
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1193
|
+
.join("");
|
|
1194
|
+
const fileName = `${componentName}Page.tsx`;
|
|
1195
|
+
console.log(chalk.yellow(` 📄 src/web/${page.section}/${fileName}`));
|
|
1196
|
+
await fs.writeFile(path.join(pagePath, fileName), generatePageComponent(page.name, page.section));
|
|
1197
|
+
}
|
|
1198
|
+
// Créer les routes API
|
|
1199
|
+
if (config.tables.length > 0) {
|
|
1200
|
+
console.log(chalk.blue("\n🔌 Création des routes API..."));
|
|
1201
|
+
for (const table of config.tables) {
|
|
1202
|
+
for (const section of table.sections) {
|
|
1203
|
+
const apiPath = path.join(moduleDir, "src", "api", section);
|
|
1204
|
+
await fs.ensureDir(apiPath);
|
|
1205
|
+
const fileName = `${table.name}.ts`;
|
|
1206
|
+
console.log(chalk.yellow(` 📄 src/api/${section}/${fileName}`));
|
|
1207
|
+
await fs.writeFile(path.join(apiPath, fileName), generateApiRoute(table.name, section));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// Créer les migrations
|
|
1212
|
+
if (config.tables.length > 0) {
|
|
1213
|
+
console.log(chalk.blue("\n🗄️ Création des migrations..."));
|
|
1214
|
+
const timestamp = new Date()
|
|
1215
|
+
.toISOString()
|
|
1216
|
+
.replace(/[-:]/g, "")
|
|
1217
|
+
.split(".")[0]
|
|
1218
|
+
.replace("T", "");
|
|
1219
|
+
const migrationFileName = `${timestamp}_${config.slug}_init.sql`;
|
|
1220
|
+
console.log(chalk.yellow(` 📄 supabase/migrations/${migrationFileName}`));
|
|
1221
|
+
await fs.writeFile(path.join(moduleDir, "supabase", "migrations", migrationFileName), generateMigration(config.tables, config.slug));
|
|
1222
|
+
// Génération du fichier DOWN correspondant pour rollback
|
|
1223
|
+
const downFileName = `${timestamp}_${config.slug}_init.sql`;
|
|
1224
|
+
const downContent = config.tables
|
|
1225
|
+
.map((t) => `-- Rollback for table ${t.name}\nDROP POLICY IF EXISTS ${t.name}_owner_delete ON public.${t.name};\nDROP POLICY IF EXISTS ${t.name}_owner_update ON public.${t.name};\nDROP POLICY IF EXISTS ${t.name}_owner_insert ON public.${t.name};\nDROP POLICY IF EXISTS ${t.name}_owner_select ON public.${t.name};\nDROP TRIGGER IF EXISTS set_${t.name}_updated_at ON public.${t.name};\nDROP INDEX IF EXISTS idx_${t.name}_owner_id;\nDROP TABLE IF EXISTS public.${t.name};\n`)
|
|
1226
|
+
.join("\n");
|
|
1227
|
+
console.log(chalk.yellow(` 📄 supabase/migrations-down/${downFileName}`));
|
|
1228
|
+
await fs.writeFile(path.join(moduleDir, "supabase", "migrations-down", downFileName), `-- DOWN migration for ${config.slug}\n${downContent}`);
|
|
1229
|
+
}
|
|
1230
|
+
// Générer la documentation du module
|
|
1231
|
+
console.log(chalk.blue("\n📝 Génération de la documentation..."));
|
|
1232
|
+
await generateModuleReadme(config, moduleDir);
|
|
1233
|
+
// Installer les dépendances
|
|
1234
|
+
console.log(chalk.blue("\n📦 Installation des dépendances..."));
|
|
1235
|
+
const { execSync } = await import("child_process");
|
|
1236
|
+
const installCmd = process.env.CI
|
|
1237
|
+
? "pnpm install --no-frozen-lockfile"
|
|
1238
|
+
: "pnpm install";
|
|
1239
|
+
try {
|
|
1240
|
+
execSync(installCmd, {
|
|
1241
|
+
cwd: moduleDir,
|
|
1242
|
+
stdio: "inherit",
|
|
1243
|
+
});
|
|
1244
|
+
console.log(chalk.green("\n✅ Dépendances installées avec succès!"));
|
|
1245
|
+
}
|
|
1246
|
+
catch (_error) {
|
|
1247
|
+
console.log(chalk.yellow("\n⚠️ Erreur lors de l'installation, veuillez exécuter manuellement:"));
|
|
1248
|
+
console.log(chalk.gray(` cd ${moduleDir} && pnpm install`));
|
|
1249
|
+
}
|
|
1250
|
+
// Mettre à jour le registre des modules dans le core
|
|
1251
|
+
console.log(chalk.blue("\n📝 Mise à jour du registre des modules..."));
|
|
1252
|
+
await updateModuleRegistry(config, rootDir);
|
|
1253
|
+
// Build auto du module pour générer dist immédiatement
|
|
1254
|
+
console.log(chalk.blue("\n🏗️ Build initial du module..."));
|
|
1255
|
+
try {
|
|
1256
|
+
execSync(`pnpm --filter ${config.slug} build`, {
|
|
1257
|
+
cwd: rootDir,
|
|
1258
|
+
stdio: "inherit",
|
|
1259
|
+
});
|
|
1260
|
+
console.log(chalk.green("✓ Module compilé"));
|
|
1261
|
+
}
|
|
1262
|
+
catch (_error) {
|
|
1263
|
+
console.log(chalk.yellow("⚠️ Build automatique échoué, exécutez: cd"), config.slug, "&& pnpm build");
|
|
1264
|
+
}
|
|
1265
|
+
console.log(chalk.green(`\n✅ Module ${config.slug} créé avec succès!\n`));
|
|
1266
|
+
console.log(chalk.gray(`📂 Emplacement: ${moduleDir}\n`));
|
|
1267
|
+
console.log(chalk.blue("Prochaines étapes:"));
|
|
1268
|
+
console.log(chalk.gray(` 1. Ajouter à une app: pnpm lastbrain add-module ${config.slug.replace("module-", "")}`));
|
|
1269
|
+
console.log(chalk.gray(" 2. (Optionnel) Modifier Doc.tsx pour documentation personnalisée"));
|
|
1270
|
+
console.log(chalk.gray(" 3. Publier: pnpm publish:" + config.slug));
|
|
1271
|
+
console.log(chalk.gray(" 4. Générer docs globales: pnpm generate:all\n"));
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Point d'entrée du script
|
|
1275
|
+
*/
|
|
1276
|
+
export async function createModule() {
|
|
1277
|
+
console.log(chalk.blue("\n🚀 Création d'un nouveau module LastBrain\n"));
|
|
1278
|
+
const answers = await inquirer.prompt([
|
|
1279
|
+
{
|
|
1280
|
+
type: "input",
|
|
1281
|
+
name: "slug",
|
|
1282
|
+
message: "Nom du module (sera préfixé par 'module-'):",
|
|
1283
|
+
validate: (input) => {
|
|
1284
|
+
if (!input || input.trim() === "") {
|
|
1285
|
+
return "Le nom du module est requis";
|
|
1286
|
+
}
|
|
1287
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
1288
|
+
return "Le nom doit contenir uniquement des lettres minuscules, chiffres et tirets";
|
|
1289
|
+
}
|
|
1290
|
+
return true;
|
|
1291
|
+
},
|
|
1292
|
+
filter: (input) => input.trim().toLowerCase(),
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
type: "input",
|
|
1296
|
+
name: "description",
|
|
1297
|
+
message: "Description du module (une ligne):",
|
|
1298
|
+
default: "Module LastBrain",
|
|
1299
|
+
validate: (input) => {
|
|
1300
|
+
if (!input || input.trim() === "") {
|
|
1301
|
+
return "La description est requise";
|
|
1302
|
+
}
|
|
1303
|
+
return true;
|
|
1304
|
+
},
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
type: "input",
|
|
1308
|
+
name: "pagesPublic",
|
|
1309
|
+
message: "Pages publiques (séparées par des virgules, ex: legal, privacy, terms):",
|
|
1310
|
+
default: "",
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
type: "input",
|
|
1314
|
+
name: "pagesAuth",
|
|
1315
|
+
message: "Pages authentifiées (séparées par des virgules, ex: dashboard, profile):",
|
|
1316
|
+
default: "",
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
type: "input",
|
|
1320
|
+
name: "pagesAdmin",
|
|
1321
|
+
message: "Pages admin (séparées par des virgules, ex: settings, users):",
|
|
1322
|
+
default: "",
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
type: "input",
|
|
1326
|
+
name: "tables",
|
|
1327
|
+
message: "Tables (séparées par des virgules, ex: settings, notifications):",
|
|
1328
|
+
default: "",
|
|
1329
|
+
},
|
|
1330
|
+
]);
|
|
1331
|
+
// Construire la configuration du module
|
|
1332
|
+
const slug = `module-${answers.slug}`;
|
|
1333
|
+
const moduleName = `@lastbrain/${slug}`;
|
|
1334
|
+
const description = answers.description;
|
|
1335
|
+
const pages = [];
|
|
1336
|
+
// Pages publiques
|
|
1337
|
+
const publicPages = parsePagesList(answers.pagesPublic);
|
|
1338
|
+
const invalidPublic = publicPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1339
|
+
if (invalidPublic.length) {
|
|
1340
|
+
console.error(chalk.red(`❌ Noms de pages publiques réservés détectés: ${invalidPublic.join(", ")}`));
|
|
1341
|
+
console.error(chalk.yellow("Noms interdits: layout, page, api, admin, auth, public. Choisissez des noms métier distincts."));
|
|
1342
|
+
process.exit(1);
|
|
1343
|
+
}
|
|
1344
|
+
for (const pageName of publicPages) {
|
|
1345
|
+
const slugName = slugifyPageName(pageName);
|
|
1346
|
+
pages.push({
|
|
1347
|
+
section: "public",
|
|
1348
|
+
path: `/${slugName}`,
|
|
1349
|
+
name: slugName,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
// Pages auth
|
|
1353
|
+
const authPages = parsePagesList(answers.pagesAuth);
|
|
1354
|
+
const invalidAuth = authPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1355
|
+
if (invalidAuth.length) {
|
|
1356
|
+
console.error(chalk.red(`❌ Noms de pages auth réservés détectés: ${invalidAuth.join(", ")}`));
|
|
1357
|
+
console.error(chalk.yellow("Noms interdits: layout, page, api, admin, auth, public. Utilisez des noms métier (ex: dashboard, profile)."));
|
|
1358
|
+
process.exit(1);
|
|
1359
|
+
}
|
|
1360
|
+
for (const pageName of authPages) {
|
|
1361
|
+
const slugName = slugifyPageName(pageName);
|
|
1362
|
+
pages.push({
|
|
1363
|
+
section: "auth",
|
|
1364
|
+
path: `/${slugName}`,
|
|
1365
|
+
name: slugName,
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
// Pages admin
|
|
1369
|
+
const adminPages = parsePagesList(answers.pagesAdmin);
|
|
1370
|
+
const invalidAdmin = adminPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1371
|
+
if (invalidAdmin.length) {
|
|
1372
|
+
console.error(chalk.red(`❌ Noms de pages admin réservés détectés: ${invalidAdmin.join(", ")}`));
|
|
1373
|
+
console.error(chalk.yellow("Noms interdits: layout, page, api, admin, auth, public. Utilisez des noms métier (ex: settings, users)."));
|
|
1374
|
+
process.exit(1);
|
|
1375
|
+
}
|
|
1376
|
+
for (const pageName of adminPages) {
|
|
1377
|
+
const slugName = slugifyPageName(pageName);
|
|
1378
|
+
pages.push({
|
|
1379
|
+
section: "admin",
|
|
1380
|
+
path: `/${slugName}`,
|
|
1381
|
+
name: slugName,
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
// Tables
|
|
1385
|
+
const tableNames = parseTablesList(answers.tables);
|
|
1386
|
+
const tables = [];
|
|
1387
|
+
for (const tableName of tableNames) {
|
|
1388
|
+
// Déterminer dans quelles sections créer les APIs pour cette table
|
|
1389
|
+
const sections = [];
|
|
1390
|
+
if (publicPages.length > 0)
|
|
1391
|
+
sections.push("public");
|
|
1392
|
+
if (authPages.length > 0)
|
|
1393
|
+
sections.push("auth");
|
|
1394
|
+
if (adminPages.length > 0)
|
|
1395
|
+
sections.push("admin");
|
|
1396
|
+
// Si aucune page n'est définie, créer au moins les APIs auth
|
|
1397
|
+
if (sections.length === 0)
|
|
1398
|
+
sections.push("auth");
|
|
1399
|
+
tables.push({
|
|
1400
|
+
name: tableName,
|
|
1401
|
+
sections,
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
const config = {
|
|
1405
|
+
slug,
|
|
1406
|
+
moduleName,
|
|
1407
|
+
pages,
|
|
1408
|
+
tables,
|
|
1409
|
+
description,
|
|
1410
|
+
};
|
|
1411
|
+
// Trouver le répertoire racine du workspace (chercher pnpm-workspace.yaml)
|
|
1412
|
+
let rootDir;
|
|
1413
|
+
try {
|
|
1414
|
+
rootDir = findWorkspaceRoot();
|
|
1415
|
+
}
|
|
1416
|
+
catch (_error) {
|
|
1417
|
+
console.error(chalk.red("❌ " + _error.message));
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
// Créer le module
|
|
1421
|
+
await createModuleStructure(config, rootDir);
|
|
1422
|
+
}
|
|
1423
|
+
// Exécuter le script si appelé directement
|
|
1424
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1425
|
+
createModule().catch((error) => {
|
|
1426
|
+
console.error(chalk.red("❌ Erreur:"), error);
|
|
1427
|
+
process.exit(1);
|
|
1428
|
+
});
|
|
1429
|
+
}
|