@lastbrain/app 0.1.34 → 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 (38) hide show
  1. package/README.md +23 -5
  2. package/dist/__tests__/module-registry.test.js +5 -16
  3. package/dist/scripts/init-app.d.ts.map +1 -1
  4. package/dist/scripts/init-app.js +2 -2
  5. package/dist/scripts/module-add.d.ts +0 -11
  6. package/dist/scripts/module-add.d.ts.map +1 -1
  7. package/dist/scripts/module-add.js +45 -22
  8. package/dist/scripts/module-build.d.ts.map +1 -1
  9. package/dist/scripts/module-build.js +90 -1
  10. package/dist/scripts/module-create.d.ts +23 -0
  11. package/dist/scripts/module-create.d.ts.map +1 -1
  12. package/dist/scripts/module-create.js +737 -52
  13. package/dist/scripts/module-delete.d.ts +6 -0
  14. package/dist/scripts/module-delete.d.ts.map +1 -0
  15. package/dist/scripts/module-delete.js +143 -0
  16. package/dist/scripts/module-list.d.ts.map +1 -1
  17. package/dist/scripts/module-list.js +2 -2
  18. package/dist/scripts/module-remove.d.ts.map +1 -1
  19. package/dist/scripts/module-remove.js +20 -4
  20. package/dist/styles.css +1 -1
  21. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  22. package/dist/templates/DefaultDoc.js +170 -30
  23. package/dist/templates/DocPage.d.ts.map +1 -1
  24. package/dist/templates/DocPage.js +25 -8
  25. package/dist/templates/migrations/20201010100000_app_base.sql +23 -24
  26. package/package.json +4 -4
  27. package/src/__tests__/module-registry.test.ts +5 -17
  28. package/src/scripts/db-init.ts +2 -2
  29. package/src/scripts/init-app.ts +5 -2
  30. package/src/scripts/module-add.ts +55 -23
  31. package/src/scripts/module-build.ts +109 -1
  32. package/src/scripts/module-create.ts +885 -63
  33. package/src/scripts/module-delete.ts +202 -0
  34. package/src/scripts/module-list.ts +9 -2
  35. package/src/scripts/module-remove.ts +36 -4
  36. package/src/templates/DefaultDoc.tsx +1163 -753
  37. package/src/templates/DocPage.tsx +28 -11
  38. package/src/templates/migrations/20201010100000_app_base.sql +23 -24
@@ -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,24 +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
 
390
+ // Documentation Component
391
+ export { Doc } from "./components/Doc";
392
+ export { Doc as ${moduleAlias}ModuleDoc } from "./components/Doc";
393
+
315
394
  // Configuration de build
316
- export { default as buildConfig } from "./build.config.js";
395
+ export { default as buildConfig } from "./${moduleNameOnly}.build.config";
317
396
  `;
318
397
  }
319
398
 
@@ -373,8 +452,16 @@ export function ${componentName}Page() {
373
452
  */
374
453
  function generateApiRoute(tableName: string, section: string): string {
375
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";
376
463
 
377
- return `import { getSupabaseServerClient } from "@lastbrain/core/server";
464
+ return `${clientImport};
378
465
 
379
466
  const jsonResponse = (payload: unknown, status = 200) => {
380
467
  return new Response(JSON.stringify(payload), {
@@ -389,7 +476,7 @@ const jsonResponse = (payload: unknown, status = 200) => {
389
476
  * GET - Liste tous les enregistrements de ${tableName}
390
477
  */
391
478
  export async function GET(request: Request) {
392
- const supabase = await getSupabaseServerClient();
479
+ const supabase = await ${clientGetter}();
393
480
  ${
394
481
  authRequired
395
482
  ? `
@@ -416,7 +503,7 @@ export async function GET(request: Request) {
416
503
  * POST - Crée un nouvel enregistrement dans ${tableName}
417
504
  */
418
505
  export async function POST(request: Request) {
419
- const supabase = await getSupabaseServerClient();
506
+ const supabase = await ${clientGetter}();
420
507
  ${
421
508
  authRequired
422
509
  ? `
@@ -430,9 +517,12 @@ export async function POST(request: Request) {
430
517
 
431
518
  const body = await request.json();
432
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
+
433
523
  const { data, error } = await supabase
434
524
  .from("${tableName}")
435
- .insert(body)
525
+ .insert(insertPayload)
436
526
  .select()
437
527
  .single();
438
528
 
@@ -447,7 +537,7 @@ export async function POST(request: Request) {
447
537
  * PUT - Met à jour un enregistrement dans ${tableName}
448
538
  */
449
539
  export async function PUT(request: Request) {
450
- const supabase = await getSupabaseServerClient();
540
+ const supabase = await ${clientGetter}();
451
541
  ${
452
542
  authRequired
453
543
  ? `
@@ -484,7 +574,7 @@ export async function PUT(request: Request) {
484
574
  * DELETE - Supprime un enregistrement de ${tableName}
485
575
  */
486
576
  export async function DELETE(request: Request) {
487
- const supabase = await getSupabaseServerClient();
577
+ const supabase = await ${clientGetter}();
488
578
  ${
489
579
  authRequired
490
580
  ? `
@@ -605,10 +695,642 @@ ${tablesSQL}
605
695
  `;
606
696
  }
607
697
 
698
+ /**
699
+ * Generate Doc.tsx component for the module
700
+ */
701
+ function generateDocComponent(config: ModuleConfig): string {
702
+ const moduleNameClean = config.slug.replace("module-", "");
703
+ const moduleNameOnly = moduleNameClean;
704
+
705
+ // Generate pages sections
706
+ const publicPages = config.pages.filter((p) => p.section === "public");
707
+ const authPages = config.pages.filter((p) => p.section === "auth");
708
+ const adminPages = config.pages.filter((p) => p.section === "admin");
709
+
710
+ let pagesSection = "";
711
+ if (config.pages.length > 0) {
712
+ pagesSection = `
713
+ <Card>
714
+ <CardHeader>
715
+ <h2 className="text-2xl font-semibold flex items-center gap-2">
716
+ <FileText size={24} />
717
+ Pages Disponibles
718
+ </h2>
719
+ </CardHeader>
720
+ <CardBody className="space-y-4">`;
721
+
722
+ if (publicPages.length > 0) {
723
+ pagesSection += `
724
+ <div>
725
+ <h3 className="text-lg font-semibold mb-2">Pages Publiques</h3>
726
+ <div className="space-y-2">`;
727
+ for (const page of publicPages) {
728
+ const componentName = page.name
729
+ .split("-")
730
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
731
+ .join("");
732
+ pagesSection += `
733
+ <div className="flex items-start gap-2">
734
+ <Chip size="sm" color="success" variant="flat">GET</Chip>
735
+ <code className="text-sm">${page.path}</code>
736
+ <span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
737
+ </div>`;
738
+ }
739
+ pagesSection += `
740
+ </div>
741
+ </div>`;
742
+ }
743
+
744
+ if (authPages.length > 0) {
745
+ pagesSection += `
746
+ <div>
747
+ <h3 className="text-lg font-semibold mb-2">Pages Protégées (Auth)</h3>
748
+ <div className="space-y-2">`;
749
+ for (const page of authPages) {
750
+ const componentName = page.name
751
+ .split("-")
752
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
753
+ .join("");
754
+ pagesSection += `
755
+ <div className="flex items-start gap-2">
756
+ <Chip size="sm" color="primary" variant="flat">GET</Chip>
757
+ <code className="text-sm">${page.path}</code>
758
+ <span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
759
+ </div>`;
760
+ }
761
+ pagesSection += `
762
+ </div>
763
+ </div>`;
764
+ }
765
+
766
+ if (adminPages.length > 0) {
767
+ pagesSection += `
768
+ <div>
769
+ <h3 className="text-lg font-semibold mb-2">Pages Admin</h3>
770
+ <div className="space-y-2">`;
771
+ for (const page of adminPages) {
772
+ const componentName = page.name
773
+ .split("-")
774
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
775
+ .join("");
776
+ pagesSection += `
777
+ <div className="flex items-start gap-2">
778
+ <Chip size="sm" color="secondary" variant="flat">GET</Chip>
779
+ <code className="text-sm">/admin/${moduleNameOnly}/${page.name}</code>
780
+ <span className="text-sm text-slate-600 dark:text-slate-400">- ${componentName}</span>
781
+ </div>`;
782
+ }
783
+ pagesSection += `
784
+ </div>
785
+ </div>`;
786
+ }
787
+
788
+ pagesSection += `
789
+ </CardBody>
790
+ </Card>
791
+ `;
792
+ }
793
+
794
+ // Generate APIs section
795
+ let apisSection = "";
796
+ if (config.tables.length > 0) {
797
+ apisSection = `
798
+ <Card>
799
+ <CardHeader>
800
+ <h2 className="text-2xl font-semibold flex items-center gap-2">
801
+ <Zap size={24} />
802
+ API Routes
803
+ </h2>
804
+ </CardHeader>
805
+ <CardBody className="space-y-4">`;
806
+
807
+ for (const table of config.tables) {
808
+ for (const section of table.sections) {
809
+ apisSection += `
810
+ <div>
811
+ <h3 className="text-lg font-semibold mb-2">
812
+ <code>/api/${section}/${table.name}</code>
813
+ </h3>
814
+ <div className="flex gap-2">
815
+ <Chip size="sm" color="success" variant="flat">GET</Chip>
816
+ <Chip size="sm" color="primary" variant="flat">POST</Chip>
817
+ <Chip size="sm" color="warning" variant="flat">PUT</Chip>
818
+ <Chip size="sm" color="danger" variant="flat">DELETE</Chip>
819
+ </div>
820
+ </div>`;
821
+ }
822
+ }
823
+
824
+ apisSection += `
825
+ </CardBody>
826
+ </Card>
827
+ `;
828
+ }
829
+
830
+ // Generate tables section
831
+ let tablesSection = "";
832
+ if (config.tables.length > 0) {
833
+ tablesSection = `
834
+ <Card>
835
+ <CardHeader>
836
+ <h2 className="text-2xl font-semibold flex items-center gap-2">
837
+ <Database size={24} />
838
+ Base de Données
839
+ </h2>
840
+ </CardHeader>
841
+ <CardBody className="space-y-6">`;
842
+
843
+ for (const table of config.tables) {
844
+ tablesSection += `
845
+ <TableStructure
846
+ tableName="${table.name}"
847
+ title="${table.name}"
848
+ description="Table ${table.name} du module ${moduleNameClean}"
849
+ />`;
850
+ }
851
+
852
+ tablesSection += `
853
+ </CardBody>
854
+ </Card>
855
+ `;
856
+ }
857
+
858
+ // Installation commands
859
+ const installSection = `
860
+ <Card>
861
+ <CardHeader>
862
+ <h2 className="text-2xl font-semibold flex items-center gap-2">
863
+ <Package size={24} />
864
+ Installation
865
+ </h2>
866
+ </CardHeader>
867
+ <CardBody className="space-y-4">
868
+ <div>
869
+ <h3 className="text-lg font-semibold mb-2">Ajouter le module</h3>
870
+ <Snippet symbol="" hideSymbol className="text-sm mb-2">
871
+ pnpm lastbrain add-module ${moduleNameClean}
872
+ </Snippet>
873
+ <Snippet symbol="" hideSymbol className="text-sm mb-2">
874
+ pnpm build:modules
875
+ </Snippet>
876
+ </div>
877
+
878
+ <div>
879
+ <h3 className="text-lg font-semibold mb-2">Appliquer les migrations</h3>
880
+ <Snippet symbol="" hideSymbol className="text-sm mb-2">
881
+ cd apps/votre-app
882
+ </Snippet>
883
+ <Snippet symbol="" hideSymbol className="text-sm mb-2">
884
+ supabase migration up
885
+ </Snippet>
886
+ </div>
887
+ </CardBody>
888
+ </Card>
889
+ `;
890
+
891
+ // Usage section with placeholder
892
+ const usageSection = `
893
+ <Card>
894
+ <CardHeader>
895
+ <h2 className="text-2xl font-semibold flex items-center gap-2">
896
+ <BookOpen size={24} />
897
+ Utilisation
898
+ </h2>
899
+ </CardHeader>
900
+ <CardBody className="space-y-4">
901
+ <Alert color="default" className="mb-4">
902
+ <p className="text-sm">
903
+ 📝 <strong>Section à compléter par l'auteur du module</strong>
904
+ </p>
905
+ <p className="text-sm text-slate-600 dark:text-slate-400 mt-2">
906
+ Ajoutez ici des exemples d'utilisation, des configurations spécifiques,
907
+ et toute information utile pour les développeurs utilisant ce module.
908
+ </p>
909
+ </Alert>
910
+
911
+ <div>
912
+ <h3 className="text-lg font-semibold mb-2">Exemple d'utilisation</h3>
913
+ <Alert color="primary" className="p-4 mb-4">
914
+ <pre className="whitespace-pre-wrap">{\`// Importez les composants depuis le module
915
+ import { ${
916
+ config.pages.length > 0
917
+ ? config.pages[0].name
918
+ .split("-")
919
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
920
+ .join("") + "Page"
921
+ : "Component"
922
+ } } from "${config.moduleName}";
923
+
924
+ // Utilisez-les dans votre application
925
+ <${
926
+ config.pages.length > 0
927
+ ? config.pages[0].name
928
+ .split("-")
929
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
930
+ .join("") + "Page"
931
+ : "Component"
932
+ } />\`}</pre>
933
+ </Alert>
934
+ </div>
935
+ </CardBody>
936
+ </Card>
937
+ `;
938
+
939
+ // Danger zone
940
+ const dangerSection = `
941
+ <Card>
942
+ <CardHeader>
943
+ <h2 className="text-2xl font-semibold flex items-center gap-2 text-danger">
944
+ <AlertTriangle size={24} />
945
+ Danger Zone
946
+ </h2>
947
+ </CardHeader>
948
+ <CardBody className="space-y-4">
949
+ <Alert color="danger" className="mb-4">
950
+ <p className="text-sm font-semibold">
951
+ ⚠️ Cette action est irréversible
952
+ </p>
953
+ <p className="text-sm mt-2">
954
+ La suppression du module supprimera toutes les pages, routes API et migrations associées.
955
+ </p>
956
+ </Alert>
957
+
958
+ <div>
959
+ <h3 className="text-lg font-semibold mb-2">Supprimer le module</h3>
960
+ <Snippet symbol="" hideSymbol color="danger" className="text-sm mb-2">
961
+ pnpm lastbrain remove-module ${moduleNameClean}
962
+ </Snippet>
963
+ <Snippet symbol="" hideSymbol color="danger" className="text-sm mb-2">
964
+ pnpm build:modules
965
+ </Snippet>
966
+ </div>
967
+ </CardBody>
968
+ </Card>
969
+ `;
970
+
971
+ return `"use client";
972
+
973
+ import { Card, CardBody, CardHeader } from "@lastbrain/ui";
974
+ import { Chip } from "@lastbrain/ui";
975
+ import { Snippet } from "@lastbrain/ui";
976
+ import { Alert } from "@lastbrain/ui";
977
+ import { TableStructure } from "@lastbrain/ui";
978
+ import {
979
+ FileText,
980
+ Zap,
981
+ Database,
982
+ Package,
983
+ BookOpen,
984
+ AlertTriangle
985
+ } from "lucide-react";
986
+
987
+ /**
988
+ * Documentation component for ${config.moduleName}
989
+ * Auto-generated from ${config.slug}.build.config.ts
990
+ *
991
+ * To regenerate this file, run:
992
+ * pnpm generate:module-docs
993
+ */
994
+ export function Doc() {
995
+ return (
996
+ <div className="container mx-auto p-6 space-y-6">
997
+ <Card>
998
+ <CardHeader>
999
+ <div>
1000
+ <h1 className="text-3xl font-bold mb-2">📦 Module ${moduleNameClean}</h1>
1001
+ <p className="text-slate-600 dark:text-slate-400">${config.moduleName}</p>
1002
+ </div>
1003
+ </CardHeader>
1004
+ <CardBody>
1005
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1006
+ <div>
1007
+ <p className="text-sm text-slate-600 dark:text-slate-400">Package</p>
1008
+ <code className="text-sm font-semibold">${config.moduleName}</code>
1009
+ </div>
1010
+ <div>
1011
+ <p className="text-sm text-slate-600 dark:text-slate-400">Slug</p>
1012
+ <code className="text-sm font-semibold">${config.slug}</code>
1013
+ </div>
1014
+ <div>
1015
+ <p className="text-sm text-slate-600 dark:text-slate-400">Type</p>
1016
+ <code className="text-sm font-semibold">Module LastBrain</code>
1017
+ </div>
1018
+ </div>
1019
+ </CardBody>
1020
+ </Card>
1021
+ ${pagesSection}${apisSection}${tablesSection}${installSection}${usageSection}${dangerSection}
1022
+ </div>
1023
+ );
1024
+ }
1025
+ `;
1026
+ }
1027
+
1028
+ /**
1029
+ * Generate README.md for the module
1030
+ */
1031
+ async function generateModuleReadme(config: ModuleConfig, moduleDir: string) {
1032
+ const moduleNameClean = config.slug.replace("module-", "");
1033
+
1034
+ let md = `# 📦 Module ${moduleNameClean}\n\n`;
1035
+ md += `> ${config.moduleName}\n\n`;
1036
+
1037
+ // Information section
1038
+ md += `## 📋 Informations\n\n`;
1039
+ md += `- **Nom du package**: \`${config.moduleName}\`\n`;
1040
+ md += `- **Slug**: \`${config.slug}\`\n`;
1041
+ md += `- **Type**: Module LastBrain\n\n`;
1042
+
1043
+ // Pages section
1044
+ if (config.pages.length > 0) {
1045
+ md += `## 📄 Pages Disponibles\n\n`;
1046
+
1047
+ const publicPages = config.pages.filter((p) => p.section === "public");
1048
+ const authPages = config.pages.filter((p) => p.section === "auth");
1049
+ const adminPages = config.pages.filter((p) => p.section === "admin");
1050
+
1051
+ if (publicPages.length > 0) {
1052
+ md += `### Pages Publiques\n\n`;
1053
+ for (const page of publicPages) {
1054
+ const componentName = page.name
1055
+ .split("-")
1056
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1057
+ .join("");
1058
+ md += `- **GET** \`${page.path}\` - ${componentName}\n`;
1059
+ }
1060
+ md += `\n`;
1061
+ }
1062
+
1063
+ if (authPages.length > 0) {
1064
+ md += `### Pages Protégées (Auth)\n\n`;
1065
+ for (const page of authPages) {
1066
+ const componentName = page.name
1067
+ .split("-")
1068
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1069
+ .join("");
1070
+ md += `- **GET** \`${page.path}\` - ${componentName}\n`;
1071
+ }
1072
+ md += `\n`;
1073
+ }
1074
+
1075
+ if (adminPages.length > 0) {
1076
+ md += `### Pages Admin\n\n`;
1077
+ for (const page of adminPages) {
1078
+ const componentName = page.name
1079
+ .split("-")
1080
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1081
+ .join("");
1082
+ md += `- **GET** \`/admin/${config.slug.replace("module-", "")}/${page.name}\` - ${componentName}\n`;
1083
+ }
1084
+ md += `\n`;
1085
+ }
1086
+ }
1087
+
1088
+ // APIs section
1089
+ if (config.tables.length > 0) {
1090
+ md += `## 🔌 API Routes\n\n`;
1091
+
1092
+ for (const table of config.tables) {
1093
+ for (const section of table.sections) {
1094
+ md += `### \`/api/${section}/${table.name}\`\n\n`;
1095
+ md += `**Méthodes supportées**: GET, POST, PUT, DELETE\n\n`;
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ // Database section
1101
+ if (config.tables.length > 0) {
1102
+ md += `## 🗄️ Base de Données\n\n`;
1103
+ md += `### Tables\n\n`;
1104
+
1105
+ for (const table of config.tables) {
1106
+ md += `#### \`${table.name}\`\n\n`;
1107
+ md += `\`\`\`tsx\n`;
1108
+ md += `<TableStructure\n`;
1109
+ md += ` tableName="${table.name}"\n`;
1110
+ md += ` title="${table.name}"\n`;
1111
+ md += ` description="Table ${table.name} du module ${moduleNameClean}"\n`;
1112
+ md += `/>\n`;
1113
+ md += `\`\`\`\n\n`;
1114
+ }
1115
+
1116
+ // Get migration files
1117
+ const migrationsPath = path.join(moduleDir, "supabase", "migrations");
1118
+ if (fs.existsSync(migrationsPath)) {
1119
+ const migrationFiles = fs
1120
+ .readdirSync(migrationsPath)
1121
+ .filter((f) => f.endsWith(".sql"))
1122
+ .sort();
1123
+
1124
+ if (migrationFiles.length > 0) {
1125
+ md += `### Migrations\n\n`;
1126
+ for (const migration of migrationFiles) {
1127
+ md += `- \`${migration}\`\n`;
1128
+ }
1129
+ md += `\n`;
1130
+ }
1131
+ }
1132
+ }
1133
+
1134
+ // Installation section
1135
+ md += `## 📦 Installation\n\n`;
1136
+ md += `\`\`\`bash\n`;
1137
+ md += `pnpm lastbrain add-module ${moduleNameClean}\n`;
1138
+ md += `pnpm build:modules\n`;
1139
+ md += `\`\`\`\n\n`;
1140
+
1141
+ md += `### Appliquer les migrations\n\n`;
1142
+ md += `\`\`\`bash\n`;
1143
+ md += `cd apps/votre-app\n`;
1144
+ md += `supabase migration up\n`;
1145
+ md += `\`\`\`\n\n`;
1146
+
1147
+ // Usage section
1148
+ md += `## 💡 Utilisation\n\n`;
1149
+ md += `<!-- 📝 Section à compléter par l'auteur du module -->\n\n`;
1150
+ md += `### Exemple d'utilisation\n\n`;
1151
+ md += `\`\`\`tsx\n`;
1152
+ md += `// Importez les composants depuis le module\n`;
1153
+ if (config.pages.length > 0) {
1154
+ const firstPage = config.pages[0];
1155
+ const componentName = firstPage.name
1156
+ .split("-")
1157
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1158
+ .join("");
1159
+ md += `import { ${componentName}Page } from "${config.moduleName}";\n\n`;
1160
+ md += `// Utilisez-les dans votre application\n`;
1161
+ md += `<${componentName}Page />\n`;
1162
+ }
1163
+ md += `\`\`\`\n\n`;
1164
+
1165
+ md += `### Configuration\n\n`;
1166
+ md += `<!-- Ajoutez ici les détails de configuration spécifiques -->\n\n`;
1167
+
1168
+ // Danger zone
1169
+ md += `## ⚠️ Danger Zone\n\n`;
1170
+ md += `La suppression du module supprimera toutes les pages, routes API et migrations associées. **Cette action est irréversible.**\n\n`;
1171
+ md += `\`\`\`bash\n`;
1172
+ md += `pnpm lastbrain remove-module ${moduleNameClean}\n`;
1173
+ md += `pnpm build:modules\n`;
1174
+ md += `\`\`\`\n\n`;
1175
+
1176
+ // Write README.md
1177
+ const readmePath = path.join(moduleDir, "README.md");
1178
+ await fs.writeFile(readmePath, md);
1179
+
1180
+ console.log(chalk.yellow(" 📄 README.md"));
1181
+ }
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
+
608
1327
  /**
609
1328
  * Crée la structure du module
610
1329
  */
611
- async function createModuleStructure(config: ModuleConfig, rootDir: string) {
1330
+ export async function createModuleStructure(
1331
+ config: ModuleConfig,
1332
+ rootDir: string,
1333
+ ) {
612
1334
  const moduleDir = path.join(rootDir, "packages", config.slug);
613
1335
 
614
1336
  console.log(chalk.blue(`\n📦 Création du module ${config.slug}...\n`));
@@ -618,7 +1340,9 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
618
1340
  await fs.ensureDir(path.join(moduleDir, "src"));
619
1341
  await fs.ensureDir(path.join(moduleDir, "src", "web"));
620
1342
  await fs.ensureDir(path.join(moduleDir, "src", "api"));
1343
+ await fs.ensureDir(path.join(moduleDir, "src", "components"));
621
1344
  await fs.ensureDir(path.join(moduleDir, "supabase", "migrations"));
1345
+ await fs.ensureDir(path.join(moduleDir, "supabase", "migrations-down"));
622
1346
 
623
1347
  // Créer package.json
624
1348
  console.log(chalk.yellow(" 📄 package.json"));
@@ -631,8 +1355,9 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
631
1355
  console.log(chalk.yellow(" 📄 tsconfig.json"));
632
1356
  await fs.writeFile(path.join(moduleDir, "tsconfig.json"), generateTsConfig());
633
1357
 
634
- // Créer {slug}.build.config.ts
635
- 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`;
636
1361
  console.log(chalk.yellow(` 📄 src/${buildConfigFileName}`));
637
1362
  await fs.writeFile(
638
1363
  path.join(moduleDir, "src", buildConfigFileName),
@@ -643,14 +1368,17 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
643
1368
  console.log(chalk.yellow(" 📄 src/index.ts"));
644
1369
  await fs.writeFile(
645
1370
  path.join(moduleDir, "src", "index.ts"),
646
- generateIndexTs(config.pages),
1371
+ generateIndexTs(config.pages, moduleNameOnly),
647
1372
  );
648
1373
 
649
- // Créer server.ts
650
- console.log(chalk.yellow(" 📄 src/server.ts"));
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
1376
+
1377
+ // Créer Doc.tsx
1378
+ console.log(chalk.yellow(" 📄 src/components/Doc.tsx"));
651
1379
  await fs.writeFile(
652
- path.join(moduleDir, "src", "server.ts"),
653
- generateServerTs(config.tables),
1380
+ path.join(moduleDir, "src", "components", "Doc.tsx"),
1381
+ generateDocComponent(config),
654
1382
  );
655
1383
 
656
1384
  // Créer les pages
@@ -705,19 +1433,82 @@ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
705
1433
  path.join(moduleDir, "supabase", "migrations", migrationFileName),
706
1434
  generateMigration(config.tables, config.slug),
707
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
+ );
1450
+ }
1451
+
1452
+ // Générer la documentation du module
1453
+ console.log(chalk.blue("\n📝 Génération de la documentation..."));
1454
+ await generateModuleReadme(config, moduleDir);
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
+ );
708
1495
  }
709
1496
 
710
1497
  console.log(chalk.green(`\n✅ Module ${config.slug} créé avec succès!\n`));
711
1498
  console.log(chalk.gray(`📂 Emplacement: ${moduleDir}\n`));
712
1499
  console.log(chalk.blue("Prochaines étapes:"));
713
- console.log(chalk.gray(` 1. cd ${moduleDir}`));
714
- console.log(chalk.gray(` 2. pnpm install`));
715
- console.log(chalk.gray(` 3. pnpm build`));
716
1500
  console.log(
717
1501
  chalk.gray(
718
- ` 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-", "")}`,
1503
+ ),
1504
+ );
1505
+ console.log(
1506
+ chalk.gray(
1507
+ " 2. (Optionnel) Modifier Doc.tsx pour documentation personnalisée",
719
1508
  ),
720
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"));
721
1512
  }
722
1513
 
723
1514
  /**
@@ -779,31 +1570,76 @@ export async function createModule() {
779
1570
 
780
1571
  // Pages publiques
781
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
+ }
782
1587
  for (const pageName of publicPages) {
1588
+ const slugName = slugifyPageName(pageName);
783
1589
  pages.push({
784
1590
  section: "public",
785
- path: `/${pageName}`,
786
- name: pageName,
1591
+ path: `/${slugName}`,
1592
+ name: slugName,
787
1593
  });
788
1594
  }
789
1595
 
790
1596
  // Pages auth
791
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
+ }
792
1612
  for (const pageName of authPages) {
1613
+ const slugName = slugifyPageName(pageName);
793
1614
  pages.push({
794
1615
  section: "auth",
795
- path: `/${pageName}`,
796
- name: pageName,
1616
+ path: `/${slugName}`,
1617
+ name: slugName,
797
1618
  });
798
1619
  }
799
1620
 
800
1621
  // Pages admin
801
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
+ }
802
1637
  for (const pageName of adminPages) {
1638
+ const slugName = slugifyPageName(pageName);
803
1639
  pages.push({
804
1640
  section: "admin",
805
- path: `/${pageName}`,
806
- name: pageName,
1641
+ path: `/${slugName}`,
1642
+ name: slugName,
807
1643
  });
808
1644
  }
809
1645
 
@@ -836,25 +1672,11 @@ export async function createModule() {
836
1672
  };
837
1673
 
838
1674
  // Trouver le répertoire racine du workspace (chercher pnpm-workspace.yaml)
839
- let rootDir = process.cwd();
840
- let attempts = 0;
841
- const maxAttempts = 5;
842
-
843
- while (attempts < maxAttempts) {
844
- const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
845
- if (fs.existsSync(workspaceFile)) {
846
- break;
847
- }
848
- rootDir = path.resolve(rootDir, "..");
849
- attempts++;
850
- }
851
-
852
- if (attempts === maxAttempts) {
853
- console.error(
854
- chalk.red(
855
- "❌ Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)",
856
- ),
857
- );
1675
+ let rootDir: string;
1676
+ try {
1677
+ rootDir = findWorkspaceRoot();
1678
+ } catch (error) {
1679
+ console.error(chalk.red("❌ " + (error as Error).message));
858
1680
  process.exit(1);
859
1681
  }
860
1682