@lastbrain/app 0.1.36 → 0.1.37

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.
Files changed (34) hide show
  1. package/dist/__tests__/module-registry.test.js +5 -16
  2. package/dist/scripts/init-app.d.ts.map +1 -1
  3. package/dist/scripts/init-app.js +2 -2
  4. package/dist/scripts/module-add.d.ts +0 -11
  5. package/dist/scripts/module-add.d.ts.map +1 -1
  6. package/dist/scripts/module-add.js +45 -22
  7. package/dist/scripts/module-build.d.ts.map +1 -1
  8. package/dist/scripts/module-build.js +90 -1
  9. package/dist/scripts/module-create.d.ts +23 -0
  10. package/dist/scripts/module-create.d.ts.map +1 -1
  11. package/dist/scripts/module-create.js +289 -56
  12. package/dist/scripts/module-delete.d.ts +6 -0
  13. package/dist/scripts/module-delete.d.ts.map +1 -0
  14. package/dist/scripts/module-delete.js +143 -0
  15. package/dist/scripts/module-list.d.ts.map +1 -1
  16. package/dist/scripts/module-list.js +2 -2
  17. package/dist/scripts/module-remove.d.ts.map +1 -1
  18. package/dist/scripts/module-remove.js +20 -4
  19. package/dist/styles.css +1 -1
  20. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  21. package/dist/templates/DefaultDoc.js +132 -9
  22. package/dist/templates/DocPage.d.ts.map +1 -1
  23. package/dist/templates/DocPage.js +24 -7
  24. package/package.json +4 -4
  25. package/src/__tests__/module-registry.test.ts +5 -17
  26. package/src/scripts/init-app.ts +5 -2
  27. package/src/scripts/module-add.ts +55 -23
  28. package/src/scripts/module-build.ts +109 -1
  29. package/src/scripts/module-create.ts +392 -69
  30. package/src/scripts/module-delete.ts +202 -0
  31. package/src/scripts/module-list.ts +9 -2
  32. package/src/scripts/module-remove.ts +36 -4
  33. package/src/templates/DefaultDoc.tsx +1121 -424
  34. 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
- interface PageConfig {
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 buildConfigExport = `./${slug}.build.config`;
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/${slug}.build.config.d.ts`,
130
- default: `./dist/${slug}.build.config.js`,
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: "${page.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: "${page.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: "${page.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 generateIndexTs(pages: PageConfig[]): string {
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
- .split("-")
303
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
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.js";
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 "./build.config.js";
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 `import { getSupabaseServerClient } from "@lastbrain/core/server";
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 getSupabaseServerClient();
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 getSupabaseServerClient();
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(body)
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 getSupabaseServerClient();
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 getSupabaseServerClient();
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">${page.path}</code>
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** \`${page.path}\` - ${componentName}\n`;
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(config: ModuleConfig, rootDir: string) {
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 {slug}.build.config.ts
1123
- const buildConfigFileName = `${config.slug}.build.config.ts`;
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
- // Créer server.ts
1138
- console.log(chalk.yellow(" 📄 src/server.ts"));
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
- ` 4. Ajouter le module à votre app avec: pnpm lastbrain add ${config.slug.replace("module-", "")}\n`,
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: `/${pageName}`,
1285
- name: pageName,
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: `/${pageName}`,
1295
- name: pageName,
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: `/${pageName}`,
1305
- name: pageName,
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 = process.cwd();
1339
- let attempts = 0;
1340
- const maxAttempts = 5;
1341
-
1342
- while (attempts < maxAttempts) {
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