@lastbrain/app 0.1.36 → 0.1.39
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/__tests__/module-registry.test.js +5 -16
- package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
- package/dist/layouts/AdminLayoutWithSidebar.js +36 -1
- package/dist/layouts/AuthLayoutWithSidebar.d.ts.map +1 -1
- package/dist/layouts/AuthLayoutWithSidebar.js +36 -1
- package/dist/layouts/PublicLayout.js +1 -1
- package/dist/layouts/PublicLayoutWithSidebar.d.ts.map +1 -1
- package/dist/layouts/PublicLayoutWithSidebar.js +36 -1
- package/dist/scripts/init-app.d.ts.map +1 -1
- package/dist/scripts/init-app.js +2 -2
- package/dist/scripts/module-add.d.ts +0 -11
- package/dist/scripts/module-add.d.ts.map +1 -1
- package/dist/scripts/module-add.js +45 -22
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +90 -1
- package/dist/scripts/module-create.d.ts +23 -0
- package/dist/scripts/module-create.d.ts.map +1 -1
- package/dist/scripts/module-create.js +289 -56
- package/dist/scripts/module-delete.d.ts +6 -0
- package/dist/scripts/module-delete.d.ts.map +1 -0
- package/dist/scripts/module-delete.js +143 -0
- package/dist/scripts/module-list.d.ts.map +1 -1
- package/dist/scripts/module-list.js +2 -2
- package/dist/scripts/module-remove.d.ts.map +1 -1
- package/dist/scripts/module-remove.js +20 -4
- package/dist/styles.css +1 -1
- package/dist/templates/DefaultDoc.d.ts.map +1 -1
- package/dist/templates/DefaultDoc.js +132 -9
- package/dist/templates/DocPage.d.ts.map +1 -1
- package/dist/templates/DocPage.js +24 -7
- package/package.json +4 -4
- package/src/__tests__/module-registry.test.ts +5 -17
- package/src/layouts/AdminLayoutWithSidebar.tsx +51 -1
- package/src/layouts/AuthLayoutWithSidebar.tsx +51 -1
- package/src/layouts/PublicLayout.tsx +1 -1
- package/src/layouts/PublicLayoutWithSidebar.tsx +51 -1
- package/src/scripts/init-app.ts +5 -2
- package/src/scripts/module-add.ts +55 -23
- package/src/scripts/module-build.ts +109 -1
- package/src/scripts/module-create.ts +392 -69
- package/src/scripts/module-delete.ts +202 -0
- package/src/scripts/module-list.ts +9 -2
- package/src/scripts/module-remove.ts +36 -4
- package/src/templates/DefaultDoc.tsx +1149 -424
- package/src/templates/DocPage.tsx +26 -10
|
@@ -2,25 +2,39 @@ import fs from "fs-extra";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import inquirer from "inquirer";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
export interface PageConfig {
|
|
7
11
|
section: "public" | "auth" | "admin";
|
|
8
12
|
path: string;
|
|
9
13
|
name: string;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
interface TableConfig {
|
|
16
|
+
export interface TableConfig {
|
|
13
17
|
name: string;
|
|
14
18
|
sections: ("public" | "auth" | "admin")[];
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
interface ModuleConfig {
|
|
21
|
+
export interface ModuleConfig {
|
|
18
22
|
slug: string;
|
|
19
23
|
moduleName: string;
|
|
20
24
|
pages: PageConfig[];
|
|
21
25
|
tables: TableConfig[];
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
// Noms réservés pour éviter les collisions avec le routeur/app
|
|
29
|
+
const RESERVED_PAGE_NAMES = new Set([
|
|
30
|
+
"layout",
|
|
31
|
+
"page",
|
|
32
|
+
"api",
|
|
33
|
+
"admin",
|
|
34
|
+
"auth",
|
|
35
|
+
"public",
|
|
36
|
+
]);
|
|
37
|
+
|
|
24
38
|
/**
|
|
25
39
|
* Parse une chaîne de pages séparées par des virgules
|
|
26
40
|
* Ex: "legal, privacy, terms" => ["legal", "privacy", "terms"]
|
|
@@ -33,6 +47,19 @@ function parsePagesList(input: string): string[] {
|
|
|
33
47
|
.filter((p) => p.length > 0);
|
|
34
48
|
}
|
|
35
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Slugifie un nom de page: minuscules, tirets, alphanum uniquement
|
|
52
|
+
*/
|
|
53
|
+
function slugifyPageName(name: string): string {
|
|
54
|
+
return name
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[\s_]+/g, "-")
|
|
58
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
59
|
+
.replace(/--+/g, "-")
|
|
60
|
+
.replace(/^-+|-+$/g, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
/**
|
|
37
64
|
* Parse une chaîne de tables séparées par des virgules
|
|
38
65
|
* Ex: "settings, users" => ["settings", "users"]
|
|
@@ -92,7 +119,8 @@ function generatePackageJson(
|
|
|
92
119
|
rootDir: string,
|
|
93
120
|
): string {
|
|
94
121
|
const versions = getLastBrainPackageVersions(rootDir);
|
|
95
|
-
const
|
|
122
|
+
const moduleNameOnly = slug.replace("module-", "");
|
|
123
|
+
const buildConfigExport = `./${moduleNameOnly}.build.config`;
|
|
96
124
|
return JSON.stringify(
|
|
97
125
|
{
|
|
98
126
|
name: moduleName,
|
|
@@ -126,8 +154,8 @@ function generatePackageJson(
|
|
|
126
154
|
default: "./dist/server.js",
|
|
127
155
|
},
|
|
128
156
|
[buildConfigExport]: {
|
|
129
|
-
types: `./dist/${
|
|
130
|
-
default: `./dist/${
|
|
157
|
+
types: `./dist/${moduleNameOnly}.build.config.d.ts`,
|
|
158
|
+
default: `./dist/${moduleNameOnly}.build.config.js`,
|
|
131
159
|
},
|
|
132
160
|
"./api/*": {
|
|
133
161
|
types: "./dist/api/*.d.ts",
|
|
@@ -165,6 +193,46 @@ function generateTsConfig(): string {
|
|
|
165
193
|
*/
|
|
166
194
|
function generateBuildConfig(config: ModuleConfig): string {
|
|
167
195
|
const { moduleName, pages, tables } = config;
|
|
196
|
+
const moduleNameOnly = config.slug.replace("module-", "");
|
|
197
|
+
|
|
198
|
+
// Helper pour normaliser les chemins (évite // et trim)
|
|
199
|
+
function normalizePath(...segments: string[]): string {
|
|
200
|
+
return (
|
|
201
|
+
"/" +
|
|
202
|
+
segments
|
|
203
|
+
.map((s) => s.replace(/^\/+/g, "").replace(/\/+$/g, ""))
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.join("/")
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Construit un path de menu selon la section en respectant le pattern attendu
|
|
210
|
+
function buildMenuPath(
|
|
211
|
+
section: string,
|
|
212
|
+
pagePath: string,
|
|
213
|
+
pageName: string,
|
|
214
|
+
): string {
|
|
215
|
+
const cleanedPagePath = pagePath.replace(/^\/+/g, "");
|
|
216
|
+
switch (section) {
|
|
217
|
+
case "public": {
|
|
218
|
+
// Public: /<module>/<page> (si non déjà préfixé)
|
|
219
|
+
if (cleanedPagePath.startsWith(moduleNameOnly + "/")) {
|
|
220
|
+
return normalizePath(cleanedPagePath); // déjà préfixé
|
|
221
|
+
}
|
|
222
|
+
return normalizePath(moduleNameOnly, cleanedPagePath);
|
|
223
|
+
}
|
|
224
|
+
case "auth": {
|
|
225
|
+
// Auth: /auth/<module>/<page>
|
|
226
|
+
return normalizePath("auth", moduleNameOnly, cleanedPagePath);
|
|
227
|
+
}
|
|
228
|
+
case "admin": {
|
|
229
|
+
// Admin: /admin/<module>/<pageName> (utilise le slug brut)
|
|
230
|
+
return normalizePath("admin", moduleNameOnly, pageName);
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
return normalizePath(cleanedPagePath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
168
236
|
|
|
169
237
|
// Générer la liste des pages
|
|
170
238
|
const pagesConfig = pages.map((page) => {
|
|
@@ -207,11 +275,12 @@ function generateBuildConfig(config: ModuleConfig): string {
|
|
|
207
275
|
.split("-")
|
|
208
276
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
209
277
|
.join(" ");
|
|
278
|
+
const menuPath = buildMenuPath("public", page.path, page.name);
|
|
210
279
|
return ` {
|
|
211
280
|
title: "${title}",
|
|
212
281
|
description: "Page ${title}",
|
|
213
282
|
icon: "FileText",
|
|
214
|
-
path: "${
|
|
283
|
+
path: "${menuPath}",
|
|
215
284
|
order: ${index + 1},
|
|
216
285
|
}`;
|
|
217
286
|
});
|
|
@@ -226,11 +295,12 @@ function generateBuildConfig(config: ModuleConfig): string {
|
|
|
226
295
|
.split("-")
|
|
227
296
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
228
297
|
.join(" ");
|
|
298
|
+
const menuPath = buildMenuPath("auth", page.path, page.name);
|
|
229
299
|
return ` {
|
|
230
300
|
title: "${title}",
|
|
231
301
|
description: "Page ${title}",
|
|
232
302
|
icon: "FileText",
|
|
233
|
-
path: "${
|
|
303
|
+
path: "${menuPath}",
|
|
234
304
|
order: ${index + 1},
|
|
235
305
|
}`;
|
|
236
306
|
});
|
|
@@ -245,11 +315,12 @@ function generateBuildConfig(config: ModuleConfig): string {
|
|
|
245
315
|
.split("-")
|
|
246
316
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
247
317
|
.join(" ");
|
|
318
|
+
const menuPath = buildMenuPath("admin", page.path, page.name);
|
|
248
319
|
return ` {
|
|
249
320
|
title: "${title}",
|
|
250
321
|
description: "Page ${title}",
|
|
251
322
|
icon: "Settings",
|
|
252
|
-
path: "${
|
|
323
|
+
path: "${menuPath}",
|
|
253
324
|
order: ${index + 1},
|
|
254
325
|
}`;
|
|
255
326
|
});
|
|
@@ -296,27 +367,32 @@ export default buildConfig;
|
|
|
296
367
|
/**
|
|
297
368
|
* Génère le contenu du fichier index.ts
|
|
298
369
|
*/
|
|
299
|
-
function
|
|
370
|
+
function toPascalCase(value: string): string {
|
|
371
|
+
return value
|
|
372
|
+
.split(/[-_]/g)
|
|
373
|
+
.filter(Boolean)
|
|
374
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
375
|
+
.join("");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function generateIndexTs(pages: PageConfig[], moduleNameOnly: string): string {
|
|
300
379
|
const exports = pages.map((page) => {
|
|
301
|
-
const componentName = page.name
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
.join("");
|
|
305
|
-
const fileName = page.name
|
|
306
|
-
.split("-")
|
|
307
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
308
|
-
.join("");
|
|
309
|
-
return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page.js";`;
|
|
380
|
+
const componentName = toPascalCase(page.name);
|
|
381
|
+
const fileName = toPascalCase(page.name);
|
|
382
|
+
return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page";`;
|
|
310
383
|
});
|
|
311
384
|
|
|
385
|
+
const moduleAlias = toPascalCase(moduleNameOnly);
|
|
386
|
+
|
|
312
387
|
return `// Client Components
|
|
313
388
|
${exports.join("\n")}
|
|
314
389
|
|
|
315
|
-
// Documentation Component
|
|
316
|
-
export { Doc } from "./components/Doc
|
|
390
|
+
// Documentation Component
|
|
391
|
+
export { Doc } from "./components/Doc";
|
|
392
|
+
export { Doc as ${moduleAlias}ModuleDoc } from "./components/Doc";
|
|
317
393
|
|
|
318
394
|
// Configuration de build
|
|
319
|
-
export { default as buildConfig } from "
|
|
395
|
+
export { default as buildConfig } from "./${moduleNameOnly}.build.config";
|
|
320
396
|
`;
|
|
321
397
|
}
|
|
322
398
|
|
|
@@ -376,8 +452,16 @@ export function ${componentName}Page() {
|
|
|
376
452
|
*/
|
|
377
453
|
function generateApiRoute(tableName: string, section: string): string {
|
|
378
454
|
const authRequired = section !== "public";
|
|
455
|
+
const clientImport =
|
|
456
|
+
section === "admin"
|
|
457
|
+
? 'import { getSupabaseServiceClient } from "@lastbrain/core/server"'
|
|
458
|
+
: 'import { getSupabaseServerClient } from "@lastbrain/core/server"';
|
|
459
|
+
const clientGetter =
|
|
460
|
+
section === "admin"
|
|
461
|
+
? "getSupabaseServiceClient"
|
|
462
|
+
: "getSupabaseServerClient";
|
|
379
463
|
|
|
380
|
-
return
|
|
464
|
+
return `${clientImport};
|
|
381
465
|
|
|
382
466
|
const jsonResponse = (payload: unknown, status = 200) => {
|
|
383
467
|
return new Response(JSON.stringify(payload), {
|
|
@@ -392,7 +476,7 @@ const jsonResponse = (payload: unknown, status = 200) => {
|
|
|
392
476
|
* GET - Liste tous les enregistrements de ${tableName}
|
|
393
477
|
*/
|
|
394
478
|
export async function GET(request: Request) {
|
|
395
|
-
const supabase = await
|
|
479
|
+
const supabase = await ${clientGetter}();
|
|
396
480
|
${
|
|
397
481
|
authRequired
|
|
398
482
|
? `
|
|
@@ -419,7 +503,7 @@ export async function GET(request: Request) {
|
|
|
419
503
|
* POST - Crée un nouvel enregistrement dans ${tableName}
|
|
420
504
|
*/
|
|
421
505
|
export async function POST(request: Request) {
|
|
422
|
-
const supabase = await
|
|
506
|
+
const supabase = await ${clientGetter}();
|
|
423
507
|
${
|
|
424
508
|
authRequired
|
|
425
509
|
? `
|
|
@@ -433,9 +517,12 @@ export async function POST(request: Request) {
|
|
|
433
517
|
|
|
434
518
|
const body = await request.json();
|
|
435
519
|
|
|
520
|
+
// Injection côté serveur de owner_id si l'utilisateur est authentifié (sécurité)
|
|
521
|
+
const insertPayload = ${authRequired ? `{ ...body, owner_id: user.id }` : "body"};
|
|
522
|
+
|
|
436
523
|
const { data, error } = await supabase
|
|
437
524
|
.from("${tableName}")
|
|
438
|
-
.insert(
|
|
525
|
+
.insert(insertPayload)
|
|
439
526
|
.select()
|
|
440
527
|
.single();
|
|
441
528
|
|
|
@@ -450,7 +537,7 @@ export async function POST(request: Request) {
|
|
|
450
537
|
* PUT - Met à jour un enregistrement dans ${tableName}
|
|
451
538
|
*/
|
|
452
539
|
export async function PUT(request: Request) {
|
|
453
|
-
const supabase = await
|
|
540
|
+
const supabase = await ${clientGetter}();
|
|
454
541
|
${
|
|
455
542
|
authRequired
|
|
456
543
|
? `
|
|
@@ -487,7 +574,7 @@ export async function PUT(request: Request) {
|
|
|
487
574
|
* DELETE - Supprime un enregistrement de ${tableName}
|
|
488
575
|
*/
|
|
489
576
|
export async function DELETE(request: Request) {
|
|
490
|
-
const supabase = await
|
|
577
|
+
const supabase = await ${clientGetter}();
|
|
491
578
|
${
|
|
492
579
|
authRequired
|
|
493
580
|
? `
|
|
@@ -613,6 +700,7 @@ ${tablesSQL}
|
|
|
613
700
|
*/
|
|
614
701
|
function generateDocComponent(config: ModuleConfig): string {
|
|
615
702
|
const moduleNameClean = config.slug.replace("module-", "");
|
|
703
|
+
const moduleNameOnly = moduleNameClean;
|
|
616
704
|
|
|
617
705
|
// Generate pages sections
|
|
618
706
|
const publicPages = config.pages.filter((p) => p.section === "public");
|
|
@@ -688,7 +776,7 @@ function generateDocComponent(config: ModuleConfig): string {
|
|
|
688
776
|
pagesSection += `
|
|
689
777
|
<div className="flex items-start gap-2">
|
|
690
778
|
<Chip size="sm" color="secondary" variant="flat">GET</Chip>
|
|
691
|
-
<code className="text-sm"
|
|
779
|
+
<code className="text-sm">/admin/${moduleNameOnly}/${page.name}</code>
|
|
692
780
|
<span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
|
|
693
781
|
</div>`;
|
|
694
782
|
}
|
|
@@ -991,7 +1079,7 @@ async function generateModuleReadme(config: ModuleConfig, moduleDir: string) {
|
|
|
991
1079
|
.split("-")
|
|
992
1080
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
993
1081
|
.join("");
|
|
994
|
-
md += `- **GET**
|
|
1082
|
+
md += `- **GET** \`/admin/${config.slug.replace("module-", "")}/${page.name}\` - ${componentName}\n`;
|
|
995
1083
|
}
|
|
996
1084
|
md += `\n`;
|
|
997
1085
|
}
|
|
@@ -1092,10 +1180,157 @@ async function generateModuleReadme(config: ModuleConfig, moduleDir: string) {
|
|
|
1092
1180
|
console.log(chalk.yellow(" 📄 README.md"));
|
|
1093
1181
|
}
|
|
1094
1182
|
|
|
1183
|
+
/**
|
|
1184
|
+
* Met à jour le registre des modules dans core/src/config/modules.ts
|
|
1185
|
+
*/
|
|
1186
|
+
async function updateModuleRegistry(config: ModuleConfig, rootDir: string) {
|
|
1187
|
+
const moduleRegistryPath = path.join(
|
|
1188
|
+
rootDir,
|
|
1189
|
+
"packages",
|
|
1190
|
+
"core",
|
|
1191
|
+
"src",
|
|
1192
|
+
"config",
|
|
1193
|
+
"modules.ts",
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
if (!fs.existsSync(moduleRegistryPath)) {
|
|
1197
|
+
console.log(
|
|
1198
|
+
chalk.yellow(" ⚠️ Fichier de registre non trouvé, création..."),
|
|
1199
|
+
);
|
|
1200
|
+
// Si le fichier n'existe pas, on le crée avec le module actuel
|
|
1201
|
+
const content = `/**
|
|
1202
|
+
* Configuration centralisée des modules LastBrain
|
|
1203
|
+
* Ce fichier est auto-généré et maintenu par les scripts de gestion des modules
|
|
1204
|
+
*/
|
|
1205
|
+
|
|
1206
|
+
export interface ModuleMetadata {
|
|
1207
|
+
name: string;
|
|
1208
|
+
package: string;
|
|
1209
|
+
description: string;
|
|
1210
|
+
emoji: string;
|
|
1211
|
+
version?: string;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export const AVAILABLE_MODULES: ModuleMetadata[] = [
|
|
1215
|
+
{
|
|
1216
|
+
name: "${config.slug.replace("module-", "")}",
|
|
1217
|
+
package: "@lastbrain/${config.slug}",
|
|
1218
|
+
description: "Module ${config.moduleName}",
|
|
1219
|
+
emoji: "📦",
|
|
1220
|
+
},
|
|
1221
|
+
];
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Récupère les métadonnées d'un module par son nom
|
|
1225
|
+
*/
|
|
1226
|
+
export function getModuleMetadata(name: string): ModuleMetadata | undefined {
|
|
1227
|
+
return AVAILABLE_MODULES.find((m) => m.name === name);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Vérifie si un module existe
|
|
1232
|
+
*/
|
|
1233
|
+
export function moduleExists(name: string): boolean {
|
|
1234
|
+
return AVAILABLE_MODULES.some((m) => m.name === name);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Récupère la liste des noms de modules disponibles
|
|
1239
|
+
*/
|
|
1240
|
+
export function getAvailableModuleNames(): string[] {
|
|
1241
|
+
return AVAILABLE_MODULES.map((m) => m.name);
|
|
1242
|
+
}
|
|
1243
|
+
`;
|
|
1244
|
+
await fs.writeFile(moduleRegistryPath, content, "utf-8");
|
|
1245
|
+
console.log(chalk.green(" ✓ Registre créé"));
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
try {
|
|
1250
|
+
let content = await fs.readFile(moduleRegistryPath, "utf-8");
|
|
1251
|
+
|
|
1252
|
+
const moduleName = config.slug.replace("module-", "");
|
|
1253
|
+
const moduleEntry = ` {
|
|
1254
|
+
name: "${moduleName}",
|
|
1255
|
+
package: "@lastbrain/${config.slug}",
|
|
1256
|
+
description: "Module ${config.moduleName}",
|
|
1257
|
+
emoji: "📦",
|
|
1258
|
+
},`;
|
|
1259
|
+
|
|
1260
|
+
// Vérifier si le module existe déjà
|
|
1261
|
+
if (content.includes(`name: "${moduleName}"`)) {
|
|
1262
|
+
console.log(
|
|
1263
|
+
chalk.yellow(
|
|
1264
|
+
` ⚠️ Module ${moduleName} déjà présent dans le registre`,
|
|
1265
|
+
),
|
|
1266
|
+
);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Trouver le tableau AVAILABLE_MODULES et ajouter le module
|
|
1271
|
+
const arrayMatch = content.match(
|
|
1272
|
+
/export const AVAILABLE_MODULES: ModuleMetadata\[\] = \[([\s\S]*?)\];/,
|
|
1273
|
+
);
|
|
1274
|
+
if (arrayMatch) {
|
|
1275
|
+
const arrayContent = arrayMatch[1];
|
|
1276
|
+
const lastItem = arrayContent.trim().split("\n").pop();
|
|
1277
|
+
|
|
1278
|
+
// Ajouter le nouveau module à la fin du tableau
|
|
1279
|
+
const newArrayContent =
|
|
1280
|
+
arrayContent.trimEnd() + "\n" + moduleEntry + "\n";
|
|
1281
|
+
content = content.replace(
|
|
1282
|
+
/export const AVAILABLE_MODULES: ModuleMetadata\[\] = \[([\s\S]*?)\];/,
|
|
1283
|
+
`export const AVAILABLE_MODULES: ModuleMetadata[] = [${newArrayContent}];`,
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
await fs.writeFile(moduleRegistryPath, content, "utf-8");
|
|
1287
|
+
console.log(chalk.green(` ✓ Module ${moduleName} ajouté au registre`));
|
|
1288
|
+
} else {
|
|
1289
|
+
console.log(
|
|
1290
|
+
chalk.yellow(
|
|
1291
|
+
" ⚠️ Format du registre non reconnu, ajout manuel requis",
|
|
1292
|
+
),
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
console.log(
|
|
1297
|
+
chalk.yellow(` ⚠️ Erreur lors de la mise à jour du registre: ${error}`),
|
|
1298
|
+
);
|
|
1299
|
+
console.log(
|
|
1300
|
+
chalk.gray(" Vous devrez ajouter manuellement le module au registre"),
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Trouve le répertoire racine du workspace (avec pnpm-workspace.yaml)
|
|
1307
|
+
*/
|
|
1308
|
+
export function findWorkspaceRoot(): string {
|
|
1309
|
+
let rootDir = process.cwd();
|
|
1310
|
+
let attempts = 0;
|
|
1311
|
+
const maxAttempts = 5;
|
|
1312
|
+
|
|
1313
|
+
while (attempts < maxAttempts) {
|
|
1314
|
+
const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
|
|
1315
|
+
if (fs.existsSync(workspaceFile)) {
|
|
1316
|
+
return rootDir;
|
|
1317
|
+
}
|
|
1318
|
+
rootDir = path.resolve(rootDir, "..");
|
|
1319
|
+
attempts++;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
"Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)",
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1095
1327
|
/**
|
|
1096
1328
|
* Crée la structure du module
|
|
1097
1329
|
*/
|
|
1098
|
-
async function createModuleStructure(
|
|
1330
|
+
export async function createModuleStructure(
|
|
1331
|
+
config: ModuleConfig,
|
|
1332
|
+
rootDir: string,
|
|
1333
|
+
) {
|
|
1099
1334
|
const moduleDir = path.join(rootDir, "packages", config.slug);
|
|
1100
1335
|
|
|
1101
1336
|
console.log(chalk.blue(`\n📦 Création du module ${config.slug}...\n`));
|
|
@@ -1107,6 +1342,7 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
|
1107
1342
|
await fs.ensureDir(path.join(moduleDir, "src", "api"));
|
|
1108
1343
|
await fs.ensureDir(path.join(moduleDir, "src", "components"));
|
|
1109
1344
|
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations"));
|
|
1345
|
+
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations-down"));
|
|
1110
1346
|
|
|
1111
1347
|
// Créer package.json
|
|
1112
1348
|
console.log(chalk.yellow(" 📄 package.json"));
|
|
@@ -1119,8 +1355,9 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
|
1119
1355
|
console.log(chalk.yellow(" 📄 tsconfig.json"));
|
|
1120
1356
|
await fs.writeFile(path.join(moduleDir, "tsconfig.json"), generateTsConfig());
|
|
1121
1357
|
|
|
1122
|
-
// Créer {
|
|
1123
|
-
const
|
|
1358
|
+
// Créer {name}.build.config.ts (sans le préfixe module-)
|
|
1359
|
+
const moduleNameOnly = config.slug.replace("module-", "");
|
|
1360
|
+
const buildConfigFileName = `${moduleNameOnly}.build.config.ts`;
|
|
1124
1361
|
console.log(chalk.yellow(` 📄 src/${buildConfigFileName}`));
|
|
1125
1362
|
await fs.writeFile(
|
|
1126
1363
|
path.join(moduleDir, "src", buildConfigFileName),
|
|
@@ -1131,15 +1368,11 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
|
1131
1368
|
console.log(chalk.yellow(" 📄 src/index.ts"));
|
|
1132
1369
|
await fs.writeFile(
|
|
1133
1370
|
path.join(moduleDir, "src", "index.ts"),
|
|
1134
|
-
generateIndexTs(config.pages),
|
|
1371
|
+
generateIndexTs(config.pages, moduleNameOnly),
|
|
1135
1372
|
);
|
|
1136
1373
|
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
await fs.writeFile(
|
|
1140
|
-
path.join(moduleDir, "src", "server.ts"),
|
|
1141
|
-
generateServerTs(config.tables),
|
|
1142
|
-
);
|
|
1374
|
+
// Note: server.ts n'est plus généré pour éviter les conflits d'exports
|
|
1375
|
+
// Les routes API sont exposées via le build.config et l'app les importe dynamiquement
|
|
1143
1376
|
|
|
1144
1377
|
// Créer Doc.tsx
|
|
1145
1378
|
console.log(chalk.yellow(" 📄 src/components/Doc.tsx"));
|
|
@@ -1200,23 +1433,82 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
|
1200
1433
|
path.join(moduleDir, "supabase", "migrations", migrationFileName),
|
|
1201
1434
|
generateMigration(config.tables, config.slug),
|
|
1202
1435
|
);
|
|
1436
|
+
|
|
1437
|
+
// Génération du fichier DOWN correspondant pour rollback
|
|
1438
|
+
const downFileName = `${timestamp}_${config.slug}_init.sql`;
|
|
1439
|
+
const downContent = config.tables
|
|
1440
|
+
.map(
|
|
1441
|
+
(t) =>
|
|
1442
|
+
`-- 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`,
|
|
1443
|
+
)
|
|
1444
|
+
.join("\n");
|
|
1445
|
+
console.log(chalk.yellow(` 📄 supabase/migrations-down/${downFileName}`));
|
|
1446
|
+
await fs.writeFile(
|
|
1447
|
+
path.join(moduleDir, "supabase", "migrations-down", downFileName),
|
|
1448
|
+
`-- DOWN migration for ${config.slug}\n${downContent}`,
|
|
1449
|
+
);
|
|
1203
1450
|
}
|
|
1204
1451
|
|
|
1205
1452
|
// Générer la documentation du module
|
|
1206
1453
|
console.log(chalk.blue("\n📝 Génération de la documentation..."));
|
|
1207
1454
|
await generateModuleReadme(config, moduleDir);
|
|
1208
1455
|
|
|
1456
|
+
// Installer les dépendances
|
|
1457
|
+
console.log(chalk.blue("\n📦 Installation des dépendances..."));
|
|
1458
|
+
const { execSync } = await import("child_process");
|
|
1459
|
+
const installCmd = process.env.CI
|
|
1460
|
+
? "pnpm install --no-frozen-lockfile"
|
|
1461
|
+
: "pnpm install";
|
|
1462
|
+
try {
|
|
1463
|
+
execSync(installCmd, {
|
|
1464
|
+
cwd: moduleDir,
|
|
1465
|
+
stdio: "inherit",
|
|
1466
|
+
});
|
|
1467
|
+
console.log(chalk.green("\n✅ Dépendances installées avec succès!"));
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
console.log(
|
|
1470
|
+
chalk.yellow(
|
|
1471
|
+
"\n⚠️ Erreur lors de l'installation, veuillez exécuter manuellement:",
|
|
1472
|
+
),
|
|
1473
|
+
);
|
|
1474
|
+
console.log(chalk.gray(` cd ${moduleDir} && pnpm install`));
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Mettre à jour le registre des modules dans le core
|
|
1478
|
+
console.log(chalk.blue("\n📝 Mise à jour du registre des modules..."));
|
|
1479
|
+
await updateModuleRegistry(config, rootDir);
|
|
1480
|
+
|
|
1481
|
+
// Build auto du module pour générer dist immédiatement
|
|
1482
|
+
console.log(chalk.blue("\n🏗️ Build initial du module..."));
|
|
1483
|
+
try {
|
|
1484
|
+
execSync(`pnpm --filter ${config.slug} build`, {
|
|
1485
|
+
cwd: rootDir,
|
|
1486
|
+
stdio: "inherit",
|
|
1487
|
+
});
|
|
1488
|
+
console.log(chalk.green("✓ Module compilé"));
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
console.log(
|
|
1491
|
+
chalk.yellow("⚠️ Build automatique échoué, exécutez: cd"),
|
|
1492
|
+
config.slug,
|
|
1493
|
+
"&& pnpm build",
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1209
1497
|
console.log(chalk.green(`\n✅ Module ${config.slug} créé avec succès!\n`));
|
|
1210
1498
|
console.log(chalk.gray(`📂 Emplacement: ${moduleDir}\n`));
|
|
1211
1499
|
console.log(chalk.blue("Prochaines étapes:"));
|
|
1212
|
-
console.log(chalk.gray(` 1. cd ${moduleDir}`));
|
|
1213
|
-
console.log(chalk.gray(` 2. pnpm install`));
|
|
1214
|
-
console.log(chalk.gray(` 3. pnpm build`));
|
|
1215
1500
|
console.log(
|
|
1216
1501
|
chalk.gray(
|
|
1217
|
-
`
|
|
1502
|
+
` 1. Ajouter à une app: pnpm lastbrain add-module ${config.slug.replace("module-", "")}`,
|
|
1218
1503
|
),
|
|
1219
1504
|
);
|
|
1505
|
+
console.log(
|
|
1506
|
+
chalk.gray(
|
|
1507
|
+
" 2. (Optionnel) Modifier Doc.tsx pour documentation personnalisée",
|
|
1508
|
+
),
|
|
1509
|
+
);
|
|
1510
|
+
console.log(chalk.gray(" 3. Publier: pnpm publish:" + config.slug));
|
|
1511
|
+
console.log(chalk.gray(" 4. Générer docs globales: pnpm generate:all\n"));
|
|
1220
1512
|
}
|
|
1221
1513
|
|
|
1222
1514
|
/**
|
|
@@ -1278,31 +1570,76 @@ export async function createModule() {
|
|
|
1278
1570
|
|
|
1279
1571
|
// Pages publiques
|
|
1280
1572
|
const publicPages = parsePagesList(answers.pagesPublic);
|
|
1573
|
+
const invalidPublic = publicPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1574
|
+
if (invalidPublic.length) {
|
|
1575
|
+
console.error(
|
|
1576
|
+
chalk.red(
|
|
1577
|
+
`❌ Noms de pages publiques réservés détectés: ${invalidPublic.join(", ")}`,
|
|
1578
|
+
),
|
|
1579
|
+
);
|
|
1580
|
+
console.error(
|
|
1581
|
+
chalk.yellow(
|
|
1582
|
+
"Noms interdits: layout, page, api, admin, auth, public. Choisissez des noms métier distincts.",
|
|
1583
|
+
),
|
|
1584
|
+
);
|
|
1585
|
+
process.exit(1);
|
|
1586
|
+
}
|
|
1281
1587
|
for (const pageName of publicPages) {
|
|
1588
|
+
const slugName = slugifyPageName(pageName);
|
|
1282
1589
|
pages.push({
|
|
1283
1590
|
section: "public",
|
|
1284
|
-
path: `/${
|
|
1285
|
-
name:
|
|
1591
|
+
path: `/${slugName}`,
|
|
1592
|
+
name: slugName,
|
|
1286
1593
|
});
|
|
1287
1594
|
}
|
|
1288
1595
|
|
|
1289
1596
|
// Pages auth
|
|
1290
1597
|
const authPages = parsePagesList(answers.pagesAuth);
|
|
1598
|
+
const invalidAuth = authPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1599
|
+
if (invalidAuth.length) {
|
|
1600
|
+
console.error(
|
|
1601
|
+
chalk.red(
|
|
1602
|
+
`❌ Noms de pages auth réservés détectés: ${invalidAuth.join(", ")}`,
|
|
1603
|
+
),
|
|
1604
|
+
);
|
|
1605
|
+
console.error(
|
|
1606
|
+
chalk.yellow(
|
|
1607
|
+
"Noms interdits: layout, page, api, admin, auth, public. Utilisez des noms métier (ex: dashboard, profile).",
|
|
1608
|
+
),
|
|
1609
|
+
);
|
|
1610
|
+
process.exit(1);
|
|
1611
|
+
}
|
|
1291
1612
|
for (const pageName of authPages) {
|
|
1613
|
+
const slugName = slugifyPageName(pageName);
|
|
1292
1614
|
pages.push({
|
|
1293
1615
|
section: "auth",
|
|
1294
|
-
path: `/${
|
|
1295
|
-
name:
|
|
1616
|
+
path: `/${slugName}`,
|
|
1617
|
+
name: slugName,
|
|
1296
1618
|
});
|
|
1297
1619
|
}
|
|
1298
1620
|
|
|
1299
1621
|
// Pages admin
|
|
1300
1622
|
const adminPages = parsePagesList(answers.pagesAdmin);
|
|
1623
|
+
const invalidAdmin = adminPages.filter((n) => RESERVED_PAGE_NAMES.has(n));
|
|
1624
|
+
if (invalidAdmin.length) {
|
|
1625
|
+
console.error(
|
|
1626
|
+
chalk.red(
|
|
1627
|
+
`❌ Noms de pages admin réservés détectés: ${invalidAdmin.join(", ")}`,
|
|
1628
|
+
),
|
|
1629
|
+
);
|
|
1630
|
+
console.error(
|
|
1631
|
+
chalk.yellow(
|
|
1632
|
+
"Noms interdits: layout, page, api, admin, auth, public. Utilisez des noms métier (ex: settings, users).",
|
|
1633
|
+
),
|
|
1634
|
+
);
|
|
1635
|
+
process.exit(1);
|
|
1636
|
+
}
|
|
1301
1637
|
for (const pageName of adminPages) {
|
|
1638
|
+
const slugName = slugifyPageName(pageName);
|
|
1302
1639
|
pages.push({
|
|
1303
1640
|
section: "admin",
|
|
1304
|
-
path: `/${
|
|
1305
|
-
name:
|
|
1641
|
+
path: `/${slugName}`,
|
|
1642
|
+
name: slugName,
|
|
1306
1643
|
});
|
|
1307
1644
|
}
|
|
1308
1645
|
|
|
@@ -1335,25 +1672,11 @@ export async function createModule() {
|
|
|
1335
1672
|
};
|
|
1336
1673
|
|
|
1337
1674
|
// Trouver le répertoire racine du workspace (chercher pnpm-workspace.yaml)
|
|
1338
|
-
let rootDir
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
|
|
1344
|
-
if (fs.existsSync(workspaceFile)) {
|
|
1345
|
-
break;
|
|
1346
|
-
}
|
|
1347
|
-
rootDir = path.resolve(rootDir, "..");
|
|
1348
|
-
attempts++;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
if (attempts === maxAttempts) {
|
|
1352
|
-
console.error(
|
|
1353
|
-
chalk.red(
|
|
1354
|
-
"❌ Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)",
|
|
1355
|
-
),
|
|
1356
|
-
);
|
|
1675
|
+
let rootDir: string;
|
|
1676
|
+
try {
|
|
1677
|
+
rootDir = findWorkspaceRoot();
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
console.error(chalk.red("❌ " + (error as Error).message));
|
|
1357
1680
|
process.exit(1);
|
|
1358
1681
|
}
|
|
1359
1682
|
|