@lastbrain/app 0.1.7 → 0.1.9

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 (40) hide show
  1. package/dist/scripts/init-app.js +3 -3
  2. package/dist/styles.css +2 -0
  3. package/package.json +11 -3
  4. package/src/app-shell/(admin)/layout.tsx +13 -0
  5. package/src/app-shell/(auth)/layout.tsx +13 -0
  6. package/src/app-shell/(public)/page.tsx +11 -0
  7. package/src/app-shell/layout.tsx +5 -0
  8. package/src/app-shell/not-found.tsx +28 -0
  9. package/src/auth/authHelpers.ts +24 -0
  10. package/src/auth/useAuthSession.ts +54 -0
  11. package/src/cli.ts +96 -0
  12. package/src/index.ts +21 -0
  13. package/src/layouts/AdminLayout.tsx +7 -0
  14. package/src/layouts/AppProviders.tsx +61 -0
  15. package/src/layouts/AuthLayout.tsx +7 -0
  16. package/src/layouts/PublicLayout.tsx +7 -0
  17. package/src/layouts/RootLayout.tsx +27 -0
  18. package/src/modules/module-loader.ts +14 -0
  19. package/src/scripts/README.md +262 -0
  20. package/src/scripts/db-init.ts +338 -0
  21. package/src/scripts/db-migrations-sync.ts +86 -0
  22. package/src/scripts/dev-sync.ts +218 -0
  23. package/src/scripts/init-app.ts +1077 -0
  24. package/src/scripts/module-add.ts +242 -0
  25. package/src/scripts/module-build.ts +502 -0
  26. package/src/scripts/module-create.ts +809 -0
  27. package/src/scripts/module-list.ts +37 -0
  28. package/src/scripts/module-remove.ts +367 -0
  29. package/src/scripts/readme-build.ts +60 -0
  30. package/src/styles.css +3 -0
  31. package/src/templates/AuthGuidePage.tsx +68 -0
  32. package/src/templates/DefaultDoc.tsx +462 -0
  33. package/src/templates/DocPage.tsx +381 -0
  34. package/src/templates/DocsPageWithModules.tsx +22 -0
  35. package/src/templates/MigrationsGuidePage.tsx +61 -0
  36. package/src/templates/ModuleGuidePage.tsx +71 -0
  37. package/src/templates/SimpleDocPage.tsx +587 -0
  38. package/src/templates/SimpleHomePage.tsx +385 -0
  39. package/src/templates/env.example/.env.example +6 -0
  40. package/src/templates/migrations/20201010100000_app_base.sql +228 -0
@@ -0,0 +1,809 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import inquirer from "inquirer";
5
+
6
+ interface PageConfig {
7
+ section: "public" | "auth" | "admin";
8
+ path: string;
9
+ name: string;
10
+ }
11
+
12
+ interface TableConfig {
13
+ name: string;
14
+ sections: ("public" | "auth" | "admin")[];
15
+ }
16
+
17
+ interface ModuleConfig {
18
+ slug: string;
19
+ moduleName: string;
20
+ pages: PageConfig[];
21
+ tables: TableConfig[];
22
+ }
23
+
24
+ /**
25
+ * Parse une chaîne de pages séparées par des virgules
26
+ * Ex: "legal, privacy, terms" => ["legal", "privacy", "terms"]
27
+ */
28
+ function parsePagesList(input: string): string[] {
29
+ if (!input || input.trim() === "") return [];
30
+ return input
31
+ .split(",")
32
+ .map((p) => p.trim())
33
+ .filter((p) => p.length > 0);
34
+ }
35
+
36
+ /**
37
+ * Parse une chaîne de tables séparées par des virgules
38
+ * Ex: "settings, users" => ["settings", "users"]
39
+ */
40
+ function parseTablesList(input: string): string[] {
41
+ if (!input || input.trim() === "") return [];
42
+ return input
43
+ .split(",")
44
+ .map((t) => t.trim())
45
+ .filter((t) => t.length > 0);
46
+ }
47
+
48
+ /**
49
+ * Génère le contenu du package.json
50
+ */
51
+ function generatePackageJson(moduleName: string, slug: string): string {
52
+ const buildConfigExport = `./${slug}.build.config`;
53
+ return JSON.stringify(
54
+ {
55
+ name: moduleName,
56
+ version: "0.1.0",
57
+ private: false,
58
+ type: "module",
59
+ main: "dist/index.js",
60
+ types: "dist/index.d.ts",
61
+ files: ["dist", "supabase"],
62
+ scripts: {
63
+ build: "tsc -p tsconfig.json",
64
+ dev: "tsc -p tsconfig.json --watch",
65
+ },
66
+ dependencies: {
67
+ "@lastbrain/core": "workspace:0.1.0",
68
+ "@lastbrain/ui": "workspace:0.1.0",
69
+ react: "^19.0.0",
70
+ "lucide-react": "^0.554.0",
71
+ "react-dom": "^19.0.0",
72
+ },
73
+ devDependencies: {
74
+ typescript: "^5.4.0",
75
+ },
76
+ exports: {
77
+ ".": {
78
+ types: "./dist/index.d.ts",
79
+ default: "./dist/index.js",
80
+ },
81
+ "./server": {
82
+ types: "./dist/server.d.ts",
83
+ default: "./dist/server.js",
84
+ },
85
+ [buildConfigExport]: {
86
+ types: `./dist/${slug}.build.config.d.ts`,
87
+ default: `./dist/${slug}.build.config.js`,
88
+ },
89
+ "./api/*": {
90
+ types: "./dist/api/*.d.ts",
91
+ default: "./dist/api/*.js",
92
+ },
93
+ },
94
+ sideEffects: false,
95
+ },
96
+ null,
97
+ 2
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Génère le contenu du tsconfig.json
103
+ */
104
+ function generateTsConfig(): string {
105
+ return JSON.stringify(
106
+ {
107
+ extends: "../../tsconfig.base.json",
108
+ compilerOptions: {
109
+ outDir: "dist",
110
+ rootDir: "src",
111
+ declaration: true,
112
+ },
113
+ include: ["src"],
114
+ },
115
+ null,
116
+ 2
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Génère le contenu du fichier build.config.ts
122
+ */
123
+ function generateBuildConfig(config: ModuleConfig): string {
124
+ const { moduleName, pages, tables } = config;
125
+
126
+ // Générer la liste des pages
127
+ const pagesConfig = pages.map((page) => {
128
+ const componentName = page.name
129
+ .split("-")
130
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
131
+ .join("");
132
+ return ` {
133
+ section: "${page.section}",
134
+ path: "${page.path}",
135
+ componentExport: "${componentName}Page",
136
+ }`;
137
+ });
138
+
139
+ // Générer la liste des APIs
140
+ const apisConfig: string[] = [];
141
+ for (const table of tables) {
142
+ for (const section of table.sections) {
143
+ const methods = ["GET", "POST", "PUT", "DELETE"];
144
+ for (const method of methods) {
145
+ apisConfig.push(` {
146
+ method: "${method}",
147
+ path: "/api/${section}/${table.name}",
148
+ handlerExport: "${method}",
149
+ entryPoint: "api/${section}/${table.name}",
150
+ authRequired: ${section !== "public"},
151
+ }`);
152
+ }
153
+ }
154
+ }
155
+
156
+ // Générer les menus par section
157
+ const menuSections: { section: string; items: string[] }[] = [];
158
+
159
+ // Menu public
160
+ const publicPages = pages.filter(p => p.section === "public");
161
+ if (publicPages.length > 0) {
162
+ const publicMenuItems = publicPages.map((page, index) => {
163
+ const title = page.name
164
+ .split("-")
165
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
166
+ .join(" ");
167
+ return ` {
168
+ title: "${title}",
169
+ description: "Page ${title}",
170
+ icon: "FileText",
171
+ path: "${page.path}",
172
+ order: ${index + 1},
173
+ }`;
174
+ });
175
+ menuSections.push({ section: "public", items: publicMenuItems });
176
+ }
177
+
178
+ // Menu auth
179
+ const authPages = pages.filter(p => p.section === "auth");
180
+ if (authPages.length > 0) {
181
+ const authMenuItems = authPages.map((page, index) => {
182
+ const title = page.name
183
+ .split("-")
184
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
185
+ .join(" ");
186
+ return ` {
187
+ title: "${title}",
188
+ description: "Page ${title}",
189
+ icon: "FileText",
190
+ path: "${page.path}",
191
+ order: ${index + 1},
192
+ }`;
193
+ });
194
+ menuSections.push({ section: "auth", items: authMenuItems });
195
+ }
196
+
197
+ // Menu admin
198
+ const adminPages = pages.filter(p => p.section === "admin");
199
+ if (adminPages.length > 0) {
200
+ const adminMenuItems = adminPages.map((page, index) => {
201
+ const title = page.name
202
+ .split("-")
203
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
204
+ .join(" ");
205
+ return ` {
206
+ title: "${title}",
207
+ description: "Page ${title}",
208
+ icon: "Settings",
209
+ path: "${page.path}",
210
+ order: ${index + 1},
211
+ }`;
212
+ });
213
+ menuSections.push({ section: "admin", items: adminMenuItems });
214
+ }
215
+
216
+ // Générer la configuration menu
217
+ const menuConfig = menuSections.length > 0
218
+ ? `,
219
+ menu: {
220
+ ${menuSections.map(({ section, items }) => ` ${section}: [
221
+ ${items.join(",\n")}
222
+ ]`).join(",\n")}
223
+ }`
224
+ : "";
225
+
226
+ return `import type { ModuleBuildConfig } from "@lastbrain/core";
227
+
228
+ const buildConfig: ModuleBuildConfig = {
229
+ moduleName: "${moduleName}",
230
+ pages: [
231
+ ${pagesConfig.join(",\n")}
232
+ ],
233
+ apis: [
234
+ ${apisConfig.join(",\n")}
235
+ ],
236
+ migrations: {
237
+ enabled: true,
238
+ priority: 30,
239
+ path: "supabase/migrations",
240
+ files: ${tables.length > 0 ? `["001_${config.slug}_init.sql"]` : "[]"},
241
+ }${menuConfig},
242
+ };
243
+
244
+ export default buildConfig;
245
+ `;
246
+ }
247
+
248
+ /**
249
+ * Génère le contenu du fichier index.ts
250
+ */
251
+ function generateIndexTs(pages: PageConfig[]): string {
252
+ const exports = pages.map((page) => {
253
+ const componentName = page.name
254
+ .split("-")
255
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
256
+ .join("");
257
+ const fileName = page.name
258
+ .split("-")
259
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
260
+ .join("");
261
+ return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page.js";`;
262
+ });
263
+
264
+ return `// Client Components
265
+ ${exports.join("\n")}
266
+
267
+ // Configuration de build
268
+ export { default as buildConfig } from "./build.config.js";
269
+ `;
270
+ }
271
+
272
+ /**
273
+ * Génère le contenu du fichier server.ts
274
+ */
275
+ function generateServerTs(tables: TableConfig[]): string {
276
+ const exports: string[] = [];
277
+
278
+ for (const table of tables) {
279
+ for (const section of table.sections) {
280
+ exports.push(
281
+ `export { GET, POST, PUT, DELETE } from "./api/${section}/${table.name}.js";`
282
+ );
283
+ }
284
+ }
285
+
286
+ return `// Server-only exports (Route Handlers, Server Actions, etc.)
287
+ ${exports.join("\n")}
288
+ `;
289
+ }
290
+
291
+ /**
292
+ * Génère le contenu d'une page
293
+ */
294
+ function generatePageComponent(pageName: string, section: string): string {
295
+ const componentName = pageName
296
+ .split("-")
297
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
298
+ .join("");
299
+
300
+ return `"use client";
301
+
302
+ import { Card, CardBody, CardHeader } from "@lastbrain/ui";
303
+
304
+ export function ${componentName}Page() {
305
+ return (
306
+ <div className="container mx-auto p-6">
307
+ <Card>
308
+ <CardHeader>
309
+ <h1 className="text-2xl font-bold">${componentName}</h1>
310
+ </CardHeader>
311
+ <CardBody>
312
+ <p className="text-default-600">
313
+ Contenu de la page ${pageName} (section: ${section})
314
+ </p>
315
+ </CardBody>
316
+ </Card>
317
+ </div>
318
+ );
319
+ }
320
+ `;
321
+ }
322
+
323
+ /**
324
+ * Génère le contenu d'une route API CRUD
325
+ */
326
+ function generateApiRoute(tableName: string, section: string): string {
327
+ const authRequired = section !== "public";
328
+
329
+ return `import { getSupabaseServerClient } from "@lastbrain/core/server";
330
+
331
+ const jsonResponse = (payload: unknown, status = 200) => {
332
+ return new Response(JSON.stringify(payload), {
333
+ headers: {
334
+ "content-type": "application/json"
335
+ },
336
+ status
337
+ });
338
+ };
339
+
340
+ /**
341
+ * GET - Liste tous les enregistrements de ${tableName}
342
+ */
343
+ export async function GET(request: Request) {
344
+ const supabase = await getSupabaseServerClient();
345
+ ${authRequired ? `
346
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
347
+ if (authError || !user) {
348
+ return jsonResponse({ error: "Non authentifié" }, 401);
349
+ }
350
+ ` : ""}
351
+
352
+ const { data, error } = await supabase
353
+ .from("${tableName}")
354
+ .select("*");
355
+
356
+ if (error) {
357
+ return jsonResponse({ error: error.message }, 400);
358
+ }
359
+
360
+ return jsonResponse({ data });
361
+ }
362
+
363
+ /**
364
+ * POST - Crée un nouvel enregistrement dans ${tableName}
365
+ */
366
+ export async function POST(request: Request) {
367
+ const supabase = await getSupabaseServerClient();
368
+ ${authRequired ? `
369
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
370
+ if (authError || !user) {
371
+ return jsonResponse({ error: "Non authentifié" }, 401);
372
+ }
373
+ ` : ""}
374
+
375
+ const body = await request.json();
376
+
377
+ const { data, error } = await supabase
378
+ .from("${tableName}")
379
+ .insert(body)
380
+ .select()
381
+ .single();
382
+
383
+ if (error) {
384
+ return jsonResponse({ error: error.message }, 400);
385
+ }
386
+
387
+ return jsonResponse({ data }, 201);
388
+ }
389
+
390
+ /**
391
+ * PUT - Met à jour un enregistrement dans ${tableName}
392
+ */
393
+ export async function PUT(request: Request) {
394
+ const supabase = await getSupabaseServerClient();
395
+ ${authRequired ? `
396
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
397
+ if (authError || !user) {
398
+ return jsonResponse({ error: "Non authentifié" }, 401);
399
+ }
400
+ ` : ""}
401
+
402
+ const body = await request.json();
403
+ const { id, ...updateData } = body;
404
+
405
+ if (!id) {
406
+ return jsonResponse({ error: "ID requis pour la mise à jour" }, 400);
407
+ }
408
+
409
+ const { data, error } = await supabase
410
+ .from("${tableName}")
411
+ .update(updateData)
412
+ .eq("id", id)
413
+ .select()
414
+ .single();
415
+
416
+ if (error) {
417
+ return jsonResponse({ error: error.message }, 400);
418
+ }
419
+
420
+ return jsonResponse({ data });
421
+ }
422
+
423
+ /**
424
+ * DELETE - Supprime un enregistrement de ${tableName}
425
+ */
426
+ export async function DELETE(request: Request) {
427
+ const supabase = await getSupabaseServerClient();
428
+ ${authRequired ? `
429
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
430
+ if (authError || !user) {
431
+ return jsonResponse({ error: "Non authentifié" }, 401);
432
+ }
433
+ ` : ""}
434
+
435
+ const { searchParams } = new URL(request.url);
436
+ const id = searchParams.get("id");
437
+
438
+ if (!id) {
439
+ return jsonResponse({ error: "ID requis pour la suppression" }, 400);
440
+ }
441
+
442
+ const { error } = await supabase
443
+ .from("${tableName}")
444
+ .delete()
445
+ .eq("id", id);
446
+
447
+ if (error) {
448
+ return jsonResponse({ error: error.message }, 400);
449
+ }
450
+
451
+ return jsonResponse({ success: true });
452
+ }
453
+ `;
454
+ }
455
+
456
+ /**
457
+ * Génère le contenu d'un fichier de migration SQL
458
+ */
459
+ function generateMigration(tables: TableConfig[], slug: string): string {
460
+ const timestamp = new Date()
461
+ .toISOString()
462
+ .replace(/[-:]/g, "")
463
+ .split(".")[0]
464
+ .replace("T", "");
465
+
466
+ const tablesSQL = tables
467
+ .map((table) => {
468
+ return `-- ===========================================================================
469
+ -- Table: public.${table.name}
470
+ -- ===========================================================================
471
+ CREATE TABLE IF NOT EXISTS public.${table.name} (
472
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
473
+ owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
474
+ title text NOT NULL,
475
+ description text,
476
+ created_at timestamptz NOT NULL DEFAULT now(),
477
+ updated_at timestamptz NOT NULL DEFAULT now()
478
+ );
479
+
480
+ -- RLS
481
+ ALTER TABLE public.${table.name} ENABLE ROW LEVEL SECURITY;
482
+
483
+ -- Politique: Les utilisateurs peuvent voir leurs propres enregistrements
484
+ DROP POLICY IF EXISTS ${table.name}_owner_select ON public.${table.name};
485
+ CREATE POLICY ${table.name}_owner_select ON public.${table.name}
486
+ FOR SELECT TO authenticated
487
+ USING (owner_id = auth.uid());
488
+
489
+ -- Politique: Les utilisateurs peuvent créer leurs propres enregistrements
490
+ DROP POLICY IF EXISTS ${table.name}_owner_insert ON public.${table.name};
491
+ CREATE POLICY ${table.name}_owner_insert ON public.${table.name}
492
+ FOR INSERT TO authenticated
493
+ WITH CHECK (owner_id = auth.uid());
494
+
495
+ -- Politique: Les utilisateurs peuvent modifier leurs propres enregistrements
496
+ DROP POLICY IF EXISTS ${table.name}_owner_update ON public.${table.name};
497
+ CREATE POLICY ${table.name}_owner_update ON public.${table.name}
498
+ FOR UPDATE TO authenticated
499
+ USING (owner_id = auth.uid())
500
+ WITH CHECK (owner_id = auth.uid());
501
+
502
+ -- Politique: Les utilisateurs peuvent supprimer leurs propres enregistrements
503
+ DROP POLICY IF EXISTS ${table.name}_owner_delete ON public.${table.name};
504
+ CREATE POLICY ${table.name}_owner_delete ON public.${table.name}
505
+ FOR DELETE TO authenticated
506
+ USING (owner_id = auth.uid());
507
+
508
+ -- Trigger updated_at
509
+ DROP TRIGGER IF EXISTS set_${table.name}_updated_at ON public.${table.name};
510
+ CREATE TRIGGER set_${table.name}_updated_at
511
+ BEFORE UPDATE ON public.${table.name}
512
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
513
+
514
+ -- Index
515
+ CREATE INDEX IF NOT EXISTS idx_${table.name}_owner_id ON public.${table.name}(owner_id);
516
+
517
+ -- Grants
518
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.${table.name} TO service_role;
519
+ `;
520
+ })
521
+ .join("\n\n");
522
+
523
+ return `-- ${slug} module initial migration
524
+ -- Auto-generated by module-create.ts
525
+ -- NOTE: uses helper function set_updated_at() from base migration
526
+
527
+ -- ===========================================================================
528
+ -- Helper: set_updated_at trigger function (if not already present)
529
+ -- ===========================================================================
530
+ CREATE OR REPLACE FUNCTION public.set_updated_at()
531
+ RETURNS trigger
532
+ LANGUAGE plpgsql
533
+ AS $$
534
+ BEGIN
535
+ NEW.updated_at := now();
536
+ RETURN NEW;
537
+ END;
538
+ $$;
539
+
540
+ ${tablesSQL}
541
+ `;
542
+ }
543
+
544
+ /**
545
+ * Crée la structure du module
546
+ */
547
+ async function createModuleStructure(config: ModuleConfig, rootDir: string) {
548
+ const moduleDir = path.join(rootDir, "packages", config.slug);
549
+
550
+ console.log(chalk.blue(`\n📦 Création du module ${config.slug}...\n`));
551
+
552
+ // Créer la structure de base
553
+ await fs.ensureDir(moduleDir);
554
+ await fs.ensureDir(path.join(moduleDir, "src"));
555
+ await fs.ensureDir(path.join(moduleDir, "src", "web"));
556
+ await fs.ensureDir(path.join(moduleDir, "src", "api"));
557
+ await fs.ensureDir(path.join(moduleDir, "supabase", "migrations"));
558
+
559
+ // Créer package.json
560
+ console.log(chalk.yellow(" 📄 package.json"));
561
+ await fs.writeFile(
562
+ path.join(moduleDir, "package.json"),
563
+ generatePackageJson(config.moduleName, config.slug)
564
+ );
565
+
566
+ // Créer tsconfig.json
567
+ console.log(chalk.yellow(" 📄 tsconfig.json"));
568
+ await fs.writeFile(
569
+ path.join(moduleDir, "tsconfig.json"),
570
+ generateTsConfig()
571
+ );
572
+
573
+ // Créer {slug}.build.config.ts
574
+ const buildConfigFileName = `${config.slug}.build.config.ts`;
575
+ console.log(chalk.yellow(` 📄 src/${buildConfigFileName}`));
576
+ await fs.writeFile(
577
+ path.join(moduleDir, "src", buildConfigFileName),
578
+ generateBuildConfig(config)
579
+ );
580
+
581
+ // Créer index.ts
582
+ console.log(chalk.yellow(" 📄 src/index.ts"));
583
+ await fs.writeFile(
584
+ path.join(moduleDir, "src", "index.ts"),
585
+ generateIndexTs(config.pages)
586
+ );
587
+
588
+ // Créer server.ts
589
+ console.log(chalk.yellow(" 📄 src/server.ts"));
590
+ await fs.writeFile(
591
+ path.join(moduleDir, "src", "server.ts"),
592
+ generateServerTs(config.tables)
593
+ );
594
+
595
+ // Créer les pages
596
+ console.log(chalk.blue("\n📄 Création des pages..."));
597
+ for (const page of config.pages) {
598
+ const pagePath = path.join(moduleDir, "src", "web", page.section);
599
+ await fs.ensureDir(pagePath);
600
+
601
+ const componentName = page.name
602
+ .split("-")
603
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
604
+ .join("");
605
+ const fileName = `${componentName}Page.tsx`;
606
+
607
+ console.log(chalk.yellow(` 📄 src/web/${page.section}/${fileName}`));
608
+ await fs.writeFile(
609
+ path.join(pagePath, fileName),
610
+ generatePageComponent(page.name, page.section)
611
+ );
612
+ }
613
+
614
+ // Créer les routes API
615
+ if (config.tables.length > 0) {
616
+ console.log(chalk.blue("\n🔌 Création des routes API..."));
617
+ for (const table of config.tables) {
618
+ for (const section of table.sections) {
619
+ const apiPath = path.join(moduleDir, "src", "api", section);
620
+ await fs.ensureDir(apiPath);
621
+
622
+ const fileName = `${table.name}.ts`;
623
+ console.log(chalk.yellow(` 📄 src/api/${section}/${fileName}`));
624
+ await fs.writeFile(
625
+ path.join(apiPath, fileName),
626
+ generateApiRoute(table.name, section)
627
+ );
628
+ }
629
+ }
630
+ }
631
+
632
+ // Créer les migrations
633
+ if (config.tables.length > 0) {
634
+ console.log(chalk.blue("\n🗄️ Création des migrations..."));
635
+ const timestamp = new Date()
636
+ .toISOString()
637
+ .replace(/[-:]/g, "")
638
+ .split(".")[0]
639
+ .replace("T", "");
640
+
641
+ const migrationFileName = `${timestamp}_${config.slug}_init.sql`;
642
+ console.log(
643
+ chalk.yellow(` 📄 supabase/migrations/${migrationFileName}`)
644
+ );
645
+ await fs.writeFile(
646
+ path.join(moduleDir, "supabase", "migrations", migrationFileName),
647
+ generateMigration(config.tables, config.slug)
648
+ );
649
+ }
650
+
651
+ console.log(chalk.green(`\n✅ Module ${config.slug} créé avec succès!\n`));
652
+ console.log(chalk.gray(`📂 Emplacement: ${moduleDir}\n`));
653
+ console.log(chalk.blue("Prochaines étapes:"));
654
+ console.log(chalk.gray(` 1. cd ${moduleDir}`));
655
+ console.log(chalk.gray(` 2. pnpm install`));
656
+ console.log(chalk.gray(` 3. pnpm build`));
657
+ console.log(
658
+ chalk.gray(
659
+ ` 4. Ajouter le module à votre app avec: pnpm lastbrain add ${config.slug.replace("module-", "")}\n`
660
+ )
661
+ );
662
+ }
663
+
664
+ /**
665
+ * Point d'entrée du script
666
+ */
667
+ export async function createModule() {
668
+ console.log(chalk.blue("\n🚀 Création d'un nouveau module LastBrain\n"));
669
+
670
+ const answers = await inquirer.prompt([
671
+ {
672
+ type: "input",
673
+ name: "slug",
674
+ message: "Nom du module (sera préfixé par 'module-'):",
675
+ validate: (input) => {
676
+ if (!input || input.trim() === "") {
677
+ return "Le nom du module est requis";
678
+ }
679
+ if (!/^[a-z0-9-]+$/.test(input)) {
680
+ return "Le nom doit contenir uniquement des lettres minuscules, chiffres et tirets";
681
+ }
682
+ return true;
683
+ },
684
+ filter: (input: string) => input.trim().toLowerCase(),
685
+ },
686
+ {
687
+ type: "input",
688
+ name: "pagesPublic",
689
+ message:
690
+ "Pages publiques (séparées par des virgules, ex: legal, privacy, terms):",
691
+ default: "",
692
+ },
693
+ {
694
+ type: "input",
695
+ name: "pagesAuth",
696
+ message:
697
+ "Pages authentifiées (séparées par des virgules, ex: dashboard, profile):",
698
+ default: "",
699
+ },
700
+ {
701
+ type: "input",
702
+ name: "pagesAdmin",
703
+ message:
704
+ "Pages admin (séparées par des virgules, ex: settings, users):",
705
+ default: "",
706
+ },
707
+ {
708
+ type: "input",
709
+ name: "tables",
710
+ message:
711
+ "Tables (séparées par des virgules, ex: settings, notifications):",
712
+ default: "",
713
+ },
714
+ ]);
715
+
716
+ // Construire la configuration du module
717
+ const slug = `module-${answers.slug}`;
718
+ const moduleName = `@lastbrain/${slug}`;
719
+
720
+ const pages: PageConfig[] = [];
721
+
722
+ // Pages publiques
723
+ const publicPages = parsePagesList(answers.pagesPublic);
724
+ for (const pageName of publicPages) {
725
+ pages.push({
726
+ section: "public",
727
+ path: `/${pageName}`,
728
+ name: pageName,
729
+ });
730
+ }
731
+
732
+ // Pages auth
733
+ const authPages = parsePagesList(answers.pagesAuth);
734
+ for (const pageName of authPages) {
735
+ pages.push({
736
+ section: "auth",
737
+ path: `/${pageName}`,
738
+ name: pageName,
739
+ });
740
+ }
741
+
742
+ // Pages admin
743
+ const adminPages = parsePagesList(answers.pagesAdmin);
744
+ for (const pageName of adminPages) {
745
+ pages.push({
746
+ section: "admin",
747
+ path: `/${pageName}`,
748
+ name: pageName,
749
+ });
750
+ }
751
+
752
+ // Tables
753
+ const tableNames = parseTablesList(answers.tables);
754
+ const tables: TableConfig[] = [];
755
+
756
+ for (const tableName of tableNames) {
757
+ // Déterminer dans quelles sections créer les APIs pour cette table
758
+ const sections: ("public" | "auth" | "admin")[] = [];
759
+
760
+ if (publicPages.length > 0) sections.push("public");
761
+ if (authPages.length > 0) sections.push("auth");
762
+ if (adminPages.length > 0) sections.push("admin");
763
+
764
+ // Si aucune page n'est définie, créer au moins les APIs auth
765
+ if (sections.length === 0) sections.push("auth");
766
+
767
+ tables.push({
768
+ name: tableName,
769
+ sections,
770
+ });
771
+ }
772
+
773
+ const config: ModuleConfig = {
774
+ slug,
775
+ moduleName,
776
+ pages,
777
+ tables,
778
+ };
779
+
780
+ // Trouver le répertoire racine du workspace (chercher pnpm-workspace.yaml)
781
+ let rootDir = process.cwd();
782
+ let attempts = 0;
783
+ const maxAttempts = 5;
784
+
785
+ while (attempts < maxAttempts) {
786
+ const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
787
+ if (fs.existsSync(workspaceFile)) {
788
+ break;
789
+ }
790
+ rootDir = path.resolve(rootDir, "..");
791
+ attempts++;
792
+ }
793
+
794
+ if (attempts === maxAttempts) {
795
+ console.error(chalk.red("❌ Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)"));
796
+ process.exit(1);
797
+ }
798
+
799
+ // Créer le module
800
+ await createModuleStructure(config, rootDir);
801
+ }
802
+
803
+ // Exécuter le script si appelé directement
804
+ if (import.meta.url === `file://${process.argv[1]}`) {
805
+ createModule().catch((error) => {
806
+ console.error(chalk.red("❌ Erreur:"), error);
807
+ process.exit(1);
808
+ });
809
+ }