@lastbrain/app 0.1.8 → 0.1.10

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 (39) hide show
  1. package/dist/scripts/init-app.js +7 -10
  2. package/package.json +3 -2
  3. package/src/app-shell/(admin)/layout.tsx +13 -0
  4. package/src/app-shell/(auth)/layout.tsx +13 -0
  5. package/src/app-shell/(public)/page.tsx +11 -0
  6. package/src/app-shell/layout.tsx +5 -0
  7. package/src/app-shell/not-found.tsx +28 -0
  8. package/src/auth/authHelpers.ts +24 -0
  9. package/src/auth/useAuthSession.ts +54 -0
  10. package/src/cli.ts +96 -0
  11. package/src/index.ts +21 -0
  12. package/src/layouts/AdminLayout.tsx +7 -0
  13. package/src/layouts/AppProviders.tsx +61 -0
  14. package/src/layouts/AuthLayout.tsx +7 -0
  15. package/src/layouts/PublicLayout.tsx +7 -0
  16. package/src/layouts/RootLayout.tsx +27 -0
  17. package/src/modules/module-loader.ts +14 -0
  18. package/src/scripts/README.md +262 -0
  19. package/src/scripts/db-init.ts +338 -0
  20. package/src/scripts/db-migrations-sync.ts +86 -0
  21. package/src/scripts/dev-sync.ts +218 -0
  22. package/src/scripts/init-app.ts +1076 -0
  23. package/src/scripts/module-add.ts +242 -0
  24. package/src/scripts/module-build.ts +502 -0
  25. package/src/scripts/module-create.ts +809 -0
  26. package/src/scripts/module-list.ts +37 -0
  27. package/src/scripts/module-remove.ts +367 -0
  28. package/src/scripts/readme-build.ts +60 -0
  29. package/src/styles.css +3 -0
  30. package/src/templates/AuthGuidePage.tsx +68 -0
  31. package/src/templates/DefaultDoc.tsx +462 -0
  32. package/src/templates/DocPage.tsx +381 -0
  33. package/src/templates/DocsPageWithModules.tsx +22 -0
  34. package/src/templates/MigrationsGuidePage.tsx +61 -0
  35. package/src/templates/ModuleGuidePage.tsx +71 -0
  36. package/src/templates/SimpleDocPage.tsx +587 -0
  37. package/src/templates/SimpleHomePage.tsx +385 -0
  38. package/src/templates/env.example/.env.example +6 -0
  39. package/src/templates/migrations/20201010100000_app_base.sql +228 -0
@@ -0,0 +1,1076 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import chalk from "chalk";
5
+ import inquirer from "inquirer";
6
+ import { execSync } from "child_process";
7
+ import { AVAILABLE_MODULES } from "./module-add.js";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ interface InitAppOptions {
13
+ targetDir: string;
14
+ force: boolean;
15
+ projectName: string;
16
+ useHeroUI: boolean;
17
+ withAuth?: boolean;
18
+ interactive?: boolean;
19
+ }
20
+
21
+ export async function initApp(options: InitAppOptions) {
22
+ const {
23
+ targetDir,
24
+ force,
25
+ projectName,
26
+ useHeroUI,
27
+ interactive = true,
28
+ } = options;
29
+ let { withAuth = false } = options;
30
+
31
+ console.log(chalk.blue("\n🚀 LastBrain App Init\n"));
32
+ console.log(chalk.gray(`📁 Dossier cible: ${targetDir}`));
33
+ if (useHeroUI) {
34
+ console.log(chalk.magenta(`🎨 Mode: HeroUI\n`));
35
+ } else {
36
+ console.log(chalk.gray(`🎨 Mode: Tailwind CSS only\n`));
37
+ }
38
+
39
+ // Sélection interactive des modules
40
+ const selectedModules: string[] = [];
41
+ if (interactive && !withAuth) {
42
+ console.log(chalk.blue("📦 Sélection des modules:\n"));
43
+
44
+ const answers = await inquirer.prompt([
45
+ {
46
+ type: "checkbox",
47
+ name: "modules",
48
+ message: "Quels modules voulez-vous installer ?",
49
+ choices: AVAILABLE_MODULES.map((module) => ({
50
+ name: `${module.displayName} - ${module.description}`,
51
+ value: module.name,
52
+ checked: false,
53
+ })),
54
+ },
55
+ ]);
56
+
57
+ selectedModules.push(...answers.modules);
58
+ withAuth = selectedModules.includes("auth");
59
+
60
+ console.log();
61
+ }
62
+
63
+ // Créer le dossier s'il n'existe pas
64
+ await fs.ensureDir(targetDir);
65
+
66
+ // 1. Vérifier/créer package.json
67
+ await ensurePackageJson(targetDir, projectName);
68
+
69
+ // 2. Installer les dépendances
70
+ await addDependencies(targetDir, useHeroUI, withAuth, selectedModules);
71
+
72
+ // 3. Créer la structure Next.js
73
+ await createNextStructure(targetDir, force, useHeroUI, withAuth);
74
+
75
+ // 4. Créer les fichiers de configuration
76
+ await createConfigFiles(targetDir, force, useHeroUI);
77
+
78
+ // 5. Créer .gitignore et .env.local.example
79
+ await createGitIgnore(targetDir, force);
80
+ await createEnvExample(targetDir, force);
81
+
82
+ // 6. Créer la structure Supabase avec migrations
83
+ await createSupabaseStructure(targetDir, force);
84
+
85
+ // 7. Ajouter les scripts NPM
86
+ await addScriptsToPackageJson(targetDir);
87
+
88
+ // 8. Enregistrer les modules sélectionnés
89
+ if (withAuth || selectedModules.length > 0) {
90
+ await saveModulesConfig(targetDir, selectedModules, withAuth);
91
+ }
92
+
93
+ console.log(
94
+ chalk.green("\n✅ Application LastBrain initialisée avec succès!\n")
95
+ );
96
+
97
+ const relativePath = path.relative(process.cwd(), targetDir);
98
+
99
+ // Demander si l'utilisateur veut lancer l'installation et le dev server
100
+ const { launchNow } = await inquirer.prompt([
101
+ {
102
+ type: "confirm",
103
+ name: "launchNow",
104
+ message: "Voulez-vous installer les dépendances et lancer le serveur de développement maintenant ?",
105
+ default: true,
106
+ },
107
+ ]);
108
+
109
+ if (launchNow) {
110
+ console.log(chalk.yellow("\n📦 Installation des dépendances...\n"));
111
+
112
+ try {
113
+ execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
114
+ console.log(chalk.green("\n✓ Dépendances installées\n"));
115
+
116
+ console.log(chalk.yellow("🔧 Génération des routes des modules...\n"));
117
+ execSync("pnpm build:modules", { cwd: targetDir, stdio: "inherit" });
118
+ console.log(chalk.green("\n✓ Routes des modules générées\n"));
119
+
120
+ // Détecter le port (par défaut 3000 pour Next.js)
121
+ const port = 3000;
122
+ const url = `http://127.0.0.1:${port}`;
123
+
124
+ console.log(chalk.yellow("🚀 Lancement du serveur de développement...\n"));
125
+ console.log(chalk.cyan(`📱 Ouvrez votre navigateur sur : ${url}\n`));
126
+
127
+ console.log(chalk.blue("\n📋 Prochaines étapes après le démarrage :"));
128
+ console.log(chalk.white(" 1. Cliquez sur 'Get Started' sur la page d'accueil"));
129
+ console.log(chalk.white(" 2. Lancez Docker Desktop"));
130
+ console.log(chalk.white(" 3. Installez Supabase CLI si nécessaire : brew install supabase/tap/supabase"));
131
+ console.log(chalk.white(" 4. Exécutez : pnpm db:init"));
132
+ console.log(chalk.white(" 5. Rechargez la page\n"));
133
+
134
+ // Ouvrir le navigateur
135
+ const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
136
+ setTimeout(() => {
137
+ try {
138
+ execSync(`${openCommand} ${url}`, { stdio: "ignore" });
139
+ } catch (error) {
140
+ // Ignorer les erreurs d'ouverture du navigateur
141
+ }
142
+ }, 2000);
143
+
144
+ // Lancer pnpm dev
145
+ execSync("pnpm dev", { cwd: targetDir, stdio: "inherit" });
146
+ } catch (error) {
147
+ console.error(chalk.red("\n❌ Erreur lors du lancement\n"));
148
+ console.log(chalk.cyan("\nVous pouvez lancer manuellement avec :"));
149
+ console.log(chalk.white(` cd ${relativePath}`));
150
+ console.log(chalk.white(" pnpm install"));
151
+ console.log(chalk.white(" pnpm build:modules"));
152
+ console.log(chalk.white(" pnpm dev\n"));
153
+ }
154
+ } else {
155
+ console.log(chalk.cyan("\n📋 Prochaines étapes:"));
156
+ console.log(chalk.white(" 1. cd " + relativePath));
157
+ console.log(chalk.white(" 2. pnpm install"));
158
+ console.log(chalk.white(" 3. pnpm build:modules (générer les routes des modules)"));
159
+ console.log(chalk.white(" 4. pnpm dev (démarrer le serveur de développement)"));
160
+ console.log(chalk.white("\n Puis sur la page d'accueil :"));
161
+ console.log(chalk.white(" 5. Cliquez sur 'Get Started'"));
162
+ console.log(chalk.white(" 6. Lancez Docker Desktop"));
163
+ console.log(chalk.white(" 7. Installez Supabase CLI : brew install supabase/tap/supabase"));
164
+ console.log(chalk.white(" 8. Exécutez : pnpm db:init"));
165
+ console.log(chalk.white(" 9. Rechargez la page\n"));
166
+
167
+ // Afficher la commande cd pour faciliter la copie
168
+ console.log(chalk.gray("Pour vous déplacer dans le projet :"));
169
+ console.log(chalk.cyan(` cd ${relativePath}\n`));
170
+ }
171
+ }
172
+
173
+ async function ensurePackageJson(targetDir: string, projectName: string) {
174
+ const pkgPath = path.join(targetDir, "package.json");
175
+
176
+ if (!fs.existsSync(pkgPath)) {
177
+ console.log(chalk.yellow("📦 Création de package.json..."));
178
+
179
+ const pkg = {
180
+ name: projectName || path.basename(targetDir),
181
+ version: "0.1.0",
182
+ private: true,
183
+ type: "module",
184
+ scripts: {},
185
+ dependencies: {},
186
+ devDependencies: {},
187
+ };
188
+
189
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
190
+ console.log(chalk.green("✓ package.json créé"));
191
+ } else {
192
+ console.log(chalk.green("✓ package.json existe"));
193
+ }
194
+ }
195
+
196
+ async function addDependencies(
197
+ targetDir: string,
198
+ useHeroUI: boolean,
199
+ withAuth: boolean,
200
+ selectedModules: string[] = []
201
+ ) {
202
+ const pkgPath = path.join(targetDir, "package.json");
203
+ const pkg = await fs.readJson(pkgPath);
204
+
205
+ console.log(chalk.yellow("\n📦 Configuration des dépendances..."));
206
+
207
+ // Détecter si on est dans le monorepo (développement local)
208
+ // Vérifier si le projet cible est à l'intérieur du monorepo
209
+ const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
210
+ fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
211
+ const latestVersion = targetIsInMonorepo ? "workspace:*" : "latest";
212
+
213
+ // Dependencies
214
+ const requiredDeps: Record<string, string> = {
215
+ next: "^15.0.3",
216
+ react: "^18.3.1",
217
+ "react-dom": "^18.3.1",
218
+ "@lastbrain/app": latestVersion,
219
+ "@lastbrain/core": latestVersion,
220
+ "@lastbrain/ui": latestVersion,
221
+ "next-themes": "^0.4.6",
222
+ };
223
+
224
+ // Ajouter module-auth si demandé
225
+ if (withAuth) {
226
+ requiredDeps["@lastbrain/module-auth"] = latestVersion;
227
+ }
228
+
229
+ // Ajouter les autres modules sélectionnés
230
+ for (const moduleName of selectedModules) {
231
+ const moduleInfo = AVAILABLE_MODULES.find(m => m.name === moduleName);
232
+ if (moduleInfo && moduleInfo.package !== "@lastbrain/module-auth") {
233
+ requiredDeps[moduleInfo.package] = latestVersion;
234
+ }
235
+ }
236
+
237
+ // Ajouter les dépendances HeroUI si nécessaire
238
+ if (useHeroUI) {
239
+ // Core
240
+ requiredDeps["@heroui/system"] = "^2.4.23";
241
+ requiredDeps["@heroui/theme"] = "^2.4.23";
242
+ requiredDeps["@heroui/react-utils"] = "^2.1.14";
243
+ requiredDeps["@heroui/framer-utils"] = "^2.1.23";
244
+
245
+ // Buttons & Actions
246
+ requiredDeps["@heroui/button"] = "^2.2.27";
247
+ requiredDeps["@heroui/link"] = "^2.2.23";
248
+
249
+ // Forms
250
+ requiredDeps["@heroui/input"] = "^2.4.28";
251
+ requiredDeps["@heroui/checkbox"] = "^2.3.27";
252
+ requiredDeps["@heroui/radio"] = "^2.3.27";
253
+ requiredDeps["@heroui/select"] = "^2.4.28";
254
+ requiredDeps["@heroui/switch"] = "^2.2.24";
255
+ requiredDeps["@heroui/form"] = "^2.1.27";
256
+ requiredDeps["@heroui/autocomplete"] = "^2.3.29";
257
+
258
+ // Layout
259
+ requiredDeps["@heroui/card"] = "^2.2.25";
260
+ requiredDeps["@heroui/navbar"] = "^2.2.25";
261
+ requiredDeps["@heroui/divider"] = "^2.2.20";
262
+ requiredDeps["@heroui/spacer"] = "^2.2.21";
263
+
264
+ // Navigation
265
+ requiredDeps["@heroui/tabs"] = "^2.2.24";
266
+ requiredDeps["@heroui/breadcrumbs"] = "^2.2.19";
267
+ requiredDeps["@heroui/pagination"] = "^2.2.24";
268
+ requiredDeps["@heroui/listbox"] = "^2.3.26";
269
+
270
+ // Feedback
271
+ requiredDeps["@heroui/spinner"] = "^2.2.24";
272
+ requiredDeps["@heroui/progress"] = "^2.2.22";
273
+ requiredDeps["@heroui/skeleton"] = "^2.2.17";
274
+ requiredDeps["@heroui/alert"] = "^2.2.27";
275
+ requiredDeps["@heroui/toast"] = "^2.0.17";
276
+
277
+ // Overlays
278
+ requiredDeps["@heroui/modal"] = "^2.2.24";
279
+ requiredDeps["@heroui/tooltip"] = "^2.2.24";
280
+ requiredDeps["@heroui/popover"] = "^2.3.27";
281
+ requiredDeps["@heroui/dropdown"] = "^2.3.27";
282
+ requiredDeps["@heroui/drawer"] = "^2.2.24";
283
+
284
+ // Data Display
285
+ requiredDeps["@heroui/avatar"] = "^2.2.22";
286
+ requiredDeps["@heroui/badge"] = "^2.2.17";
287
+ requiredDeps["@heroui/chip"] = "^2.2.22";
288
+ requiredDeps["@heroui/code"] = "^2.2.21";
289
+ requiredDeps["@heroui/image"] = "^2.2.17";
290
+ requiredDeps["@heroui/kbd"] = "^2.2.22";
291
+ requiredDeps["@heroui/snippet"] = "^2.2.28";
292
+ requiredDeps["@heroui/table"] = "^2.2.27";
293
+ requiredDeps["@heroui/user"] = "^2.2.22";
294
+ requiredDeps["@heroui/accordion"] = "^2.2.24";
295
+
296
+ // Utilities
297
+ requiredDeps["@heroui/scroll-shadow"] = "^2.3.18";
298
+ requiredDeps["@react-aria/ssr"] = "^3.9.10";
299
+ requiredDeps["@react-aria/visually-hidden"] = "^3.8.28";
300
+
301
+ // Dependencies
302
+ requiredDeps["lucide-react"] = "^0.554.0";
303
+ requiredDeps["framer-motion"] = "^11.18.2";
304
+ requiredDeps["clsx"] = "^2.1.1";
305
+ }
306
+
307
+ // DevDependencies
308
+ const requiredDevDeps = {
309
+ tailwindcss: "^3.4.0",
310
+ postcss: "^8.4.0",
311
+ autoprefixer: "^10.4.20",
312
+ typescript: "^5.4.0",
313
+ "@types/node": "^20.0.0",
314
+ "@types/react": "^18.3.0",
315
+ "@types/react-dom": "^18.3.0",
316
+ };
317
+
318
+ pkg.dependencies = { ...pkg.dependencies, ...requiredDeps };
319
+ pkg.devDependencies = { ...pkg.devDependencies, ...requiredDevDeps };
320
+
321
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
322
+ console.log(chalk.green("✓ Dépendances configurées"));
323
+ }
324
+
325
+ async function createNextStructure(
326
+ targetDir: string,
327
+ force: boolean,
328
+ useHeroUI: boolean,
329
+ withAuth: boolean
330
+ ) {
331
+ console.log(chalk.yellow("\n📁 Création de la structure Next.js..."));
332
+
333
+ const appDir = path.join(targetDir, "app");
334
+ const stylesDir = path.join(targetDir, "styles");
335
+
336
+ await fs.ensureDir(appDir);
337
+ await fs.ensureDir(stylesDir);
338
+
339
+ // Générer le layout principal
340
+ const layoutDest = path.join(appDir, "layout.tsx");
341
+
342
+ if (!fs.existsSync(layoutDest) || force) {
343
+ let layoutContent = "";
344
+
345
+ if (useHeroUI) {
346
+ // Layout avec HeroUI
347
+ layoutContent = `// GENERATED BY LASTBRAIN APP-SHELL
348
+
349
+ "use client";
350
+
351
+ import "../styles/globals.css";
352
+ import { HeroUIProvider } from "@heroui/system";
353
+
354
+ import { ThemeProvider } from "next-themes";
355
+ import { useRouter } from "next/navigation";
356
+ import { AppProviders } from "@lastbrain/app";
357
+ import type { PropsWithChildren } from "react";
358
+ import { AppHeader } from "../components/AppHeader";
359
+
360
+ export default function RootLayout({ children }: PropsWithChildren<{}>) {
361
+ const router = useRouter();
362
+
363
+ return (
364
+ <html lang="fr" suppressHydrationWarning>
365
+ <body className="min-h-screen">
366
+ <HeroUIProvider navigate={router.push}>
367
+ <ThemeProvider
368
+ attribute="class"
369
+ defaultTheme="dark"
370
+ enableSystem={false}
371
+ storageKey="lastbrain-theme"
372
+ >
373
+ <AppProviders>
374
+ <AppHeader />
375
+ <div className="min-h-screen text-foreground bg-background">
376
+ {children}
377
+ </div>
378
+ </AppProviders>
379
+ </ThemeProvider>
380
+ </HeroUIProvider>
381
+ </body>
382
+ </html>
383
+ );
384
+ }
385
+ `;
386
+ } else {
387
+ // Layout Tailwind CSS uniquement
388
+ layoutContent = `// GENERATED BY LASTBRAIN APP-SHELL
389
+
390
+ "use client";
391
+
392
+ import "../styles/globals.css";
393
+ import { ThemeProvider } from "next-themes";
394
+ import { AppProviders } from "@lastbrain/app";
395
+ import type { PropsWithChildren } from "react";
396
+ import { AppHeader } from "../components/AppHeader";
397
+
398
+ export default function RootLayout({ children }: PropsWithChildren<{}>) {
399
+ return (
400
+ <html lang="fr" suppressHydrationWarning>
401
+ <body className="min-h-screen">
402
+ <ThemeProvider
403
+ attribute="class"
404
+ defaultTheme="light"
405
+ enableSystem={false}
406
+ storageKey="lastbrain-theme"
407
+ >
408
+ <AppProviders>
409
+ <AppHeader />
410
+ <div className="min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-white">
411
+ {children}
412
+ </div>
413
+ </AppProviders>
414
+ </ThemeProvider>
415
+ </body>
416
+ </html>
417
+ );
418
+ }
419
+ `;
420
+ }
421
+
422
+ await fs.writeFile(layoutDest, layoutContent);
423
+ console.log(chalk.green("✓ app/layout.tsx créé"));
424
+ } else {
425
+ console.log(
426
+ chalk.gray(" app/layout.tsx existe déjà (utilisez --force pour écraser)")
427
+ );
428
+ }
429
+
430
+ // Créer globals.css
431
+ const globalsPath = path.join(stylesDir, "globals.css");
432
+ if (!fs.existsSync(globalsPath) || force) {
433
+ const globalsContent = `@tailwind base;
434
+ @tailwind components;
435
+ @tailwind utilities;
436
+ `;
437
+ await fs.writeFile(globalsPath, globalsContent);
438
+ console.log(chalk.green("✓ styles/globals.css créé"));
439
+ }
440
+
441
+ // Créer la page d'accueil publique (racine)
442
+ const homePagePath = path.join(appDir, "page.tsx");
443
+ if (!fs.existsSync(homePagePath) || force) {
444
+ const homePageContent = `// GENERATED BY LASTBRAIN APP-SHELL
445
+
446
+ import { SimpleHomePage } from "@lastbrain/app";
447
+
448
+ export default function RootPage() {
449
+ return <SimpleHomePage showAuth={${withAuth}} />;
450
+ }
451
+ `;
452
+ await fs.writeFile(homePagePath, homePageContent);
453
+ console.log(chalk.green("✓ app/page.tsx créé"));
454
+ }
455
+
456
+ // Créer la page not-found.tsx
457
+ const notFoundPath = path.join(appDir, "not-found.tsx");
458
+ if (!fs.existsSync(notFoundPath) || force) {
459
+ const notFoundContent = `"use client";
460
+ import { Button } from "@lastbrain/ui";
461
+ import { useRouter } from "next/navigation";
462
+
463
+ export default function NotFound() {
464
+ const router = useRouter();
465
+ return (
466
+ <div className="flex min-h-screen items-center justify-center bg-background">
467
+ <div className="mx-auto max-w-md text-center">
468
+ <h1 className="mb-4 text-6xl font-bold text-foreground">404</h1>
469
+ <h2 className="mb-4 text-2xl font-semibold text-foreground">
470
+ Page non trouvée
471
+ </h2>
472
+ <p className="mb-8 text-muted-foreground">
473
+ La page que vous recherchez n'existe pas ou a été déplacée.
474
+ </p>
475
+ <Button
476
+ onPress={() => {
477
+ router.back();
478
+ }}
479
+ className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
480
+ >
481
+ Retour à l'accueil
482
+ </Button>
483
+ </div>
484
+ </div>
485
+ );
486
+ }
487
+ `;
488
+ await fs.writeFile(notFoundPath, notFoundContent);
489
+ console.log(chalk.green("✓ app/not-found.tsx créé"));
490
+ }
491
+
492
+ // Créer les routes avec leurs layouts
493
+ await createRoute(appDir, "admin", "admin", force);
494
+ await createRoute(appDir, "docs", "public", force);
495
+
496
+ // Créer le composant AppHeader
497
+ await createAppHeader(targetDir, force);
498
+ }
499
+
500
+ async function createRoute(
501
+ appDir: string,
502
+ routeName: string,
503
+ layoutType: string,
504
+ force: boolean
505
+ ) {
506
+ const routeDir = path.join(appDir, routeName);
507
+ await fs.ensureDir(routeDir);
508
+
509
+ // Layout pour la route
510
+ const layoutPath = path.join(routeDir, "layout.tsx");
511
+ if (!fs.existsSync(layoutPath) || force) {
512
+ const layoutComponent =
513
+ layoutType.charAt(0).toUpperCase() + layoutType.slice(1) + "Layout";
514
+
515
+ const layoutContent = `import { ${layoutComponent} } from "@lastbrain/app";
516
+
517
+ export default ${layoutComponent};
518
+ `;
519
+ await fs.writeFile(layoutPath, layoutContent);
520
+ console.log(chalk.green(`✓ app/${routeName}/layout.tsx créé`));
521
+ }
522
+
523
+ // Page par défaut
524
+ const pagePath = path.join(routeDir, "page.tsx");
525
+ if (!fs.existsSync(pagePath) || force) {
526
+ let templateImport = "";
527
+ let componentName: string | null = null;
528
+
529
+ // Choisir le template approprié selon la route
530
+ switch (routeName) {
531
+ case "admin":
532
+ templateImport = 'import { ModuleGuidePage } from "@lastbrain/app";';
533
+ componentName = "ModuleGuidePage";
534
+ break;
535
+ case "docs":
536
+ templateImport = 'import { SimpleDocPage } from "@lastbrain/app";';
537
+ componentName = "SimpleDocPage";
538
+ break;
539
+ default:
540
+ // Template générique pour les autres routes
541
+ templateImport = `// Generic page for ${routeName}`;
542
+ componentName = null;
543
+ }
544
+
545
+ const pageContent = componentName
546
+ ? `// GENERATED BY LASTBRAIN APP-SHELL
547
+
548
+ ${templateImport}
549
+
550
+ export default function ${
551
+ routeName.charAt(0).toUpperCase() + routeName.slice(1)
552
+ }Page() {
553
+ return <${componentName} />;
554
+ }
555
+ `
556
+ : `// GENERATED BY LASTBRAIN APP-SHELL
557
+
558
+ export default function ${
559
+ routeName.charAt(0).toUpperCase() + routeName.slice(1)
560
+ }Page() {
561
+ return (
562
+ <div className="container mx-auto px-4 py-8">
563
+ <h1 className="text-3xl font-bold mb-4">${
564
+ routeName.charAt(0).toUpperCase() + routeName.slice(1)
565
+ } Page</h1>
566
+ <p className="text-slate-600 dark:text-slate-400">
567
+ Cette page a été générée par LastBrain Init.
568
+ </p>
569
+ <p className="text-sm text-slate-500 dark:text-slate-500 mt-4">
570
+ Route: /${routeName}
571
+ </p>
572
+ </div>
573
+ );
574
+ }
575
+ `;
576
+ await fs.writeFile(pagePath, pageContent);
577
+ console.log(chalk.green(`✓ app/${routeName}/page.tsx créé`));
578
+ }
579
+ }
580
+
581
+ async function createAppHeader(targetDir: string, force: boolean) {
582
+ const componentsDir = path.join(targetDir, "components");
583
+ await fs.ensureDir(componentsDir);
584
+
585
+ const headerPath = path.join(componentsDir, "AppHeader.tsx");
586
+
587
+ if (!fs.existsSync(headerPath) || force) {
588
+ const headerContent = `"use client";
589
+
590
+ import { Header } from "@lastbrain/ui";
591
+ import { menuConfig } from "../config/menu";
592
+ import { supabaseBrowserClient } from "@lastbrain/core";
593
+ import { useRouter } from "next/navigation";
594
+ import { useAuthSession } from "@lastbrain/app";
595
+
596
+ export function AppHeader() {
597
+ const router = useRouter();
598
+ const { user, isSuperAdmin } = useAuthSession();
599
+
600
+ const handleLogout = async () => {
601
+ await supabaseBrowserClient.auth.signOut();
602
+ router.push("/");
603
+ router.refresh();
604
+ };
605
+
606
+ return (
607
+ <Header
608
+ user={user}
609
+ onLogout={handleLogout}
610
+ menuConfig={menuConfig}
611
+ brandName="LastBrain App"
612
+ brandHref="/"
613
+ isSuperAdmin={isSuperAdmin}
614
+ />
615
+ );
616
+ }
617
+ `;
618
+ await fs.writeFile(headerPath, headerContent);
619
+ console.log(chalk.green("✓ components/AppHeader.tsx créé"));
620
+ } else {
621
+ console.log(
622
+ chalk.gray(" components/AppHeader.tsx existe déjà (utilisez --force pour écraser)")
623
+ );
624
+ }
625
+ }
626
+
627
+ async function createConfigFiles(
628
+ targetDir: string,
629
+ force: boolean,
630
+ useHeroUI: boolean
631
+ ) {
632
+ console.log(chalk.yellow("\n⚙️ Création des fichiers de configuration..."));
633
+
634
+ // middleware.ts - Protection des routes /auth/* et /admin/*
635
+ const middlewarePath = path.join(targetDir, "middleware.ts");
636
+ if (!fs.existsSync(middlewarePath) || force) {
637
+ const middleware = `import { type NextRequest, NextResponse } from "next/server";
638
+ import { createMiddlewareClient } from "@lastbrain/core/server";
639
+
640
+ export async function middleware(request: NextRequest) {
641
+ const { pathname } = request.nextUrl;
642
+
643
+ // Protéger les routes /auth/* (espace membre)
644
+ if (pathname.startsWith("/auth")) {
645
+ try {
646
+ const { supabase, response } = createMiddlewareClient(request);
647
+ const {
648
+ data: { session },
649
+ } = await supabase.auth.getSession();
650
+
651
+ // Pas de session → redirection vers /signin
652
+ if (!session) {
653
+ const redirectUrl = new URL("/signin", request.url);
654
+ redirectUrl.searchParams.set("redirect", pathname);
655
+ return NextResponse.redirect(redirectUrl);
656
+ }
657
+
658
+ return response;
659
+ } catch (error) {
660
+ console.error("Middleware auth error:", error);
661
+ return NextResponse.redirect(new URL("/signin", request.url));
662
+ }
663
+ }
664
+
665
+ // Protéger les routes /admin/* (superadmin uniquement)
666
+ if (pathname.startsWith("/admin")) {
667
+ try {
668
+ const { supabase, response } = createMiddlewareClient(request);
669
+ const {
670
+ data: { session },
671
+ } = await supabase.auth.getSession();
672
+
673
+ // Pas de session → redirection vers /signin
674
+ if (!session) {
675
+ const redirectUrl = new URL("/signin", request.url);
676
+ redirectUrl.searchParams.set("redirect", pathname);
677
+ return NextResponse.redirect(redirectUrl);
678
+ }
679
+
680
+ // Vérifier si l'utilisateur est superadmin
681
+ const { data: isSuperAdmin, error } = await supabase.rpc(
682
+ "is_superadmin",
683
+ { user_id: session.user.id }
684
+ );
685
+
686
+ if (error || !isSuperAdmin) {
687
+ console.error("Access denied: not a superadmin", error);
688
+ return NextResponse.redirect(new URL("/", request.url));
689
+ }
690
+
691
+ return response;
692
+ } catch (error) {
693
+ console.error("Middleware admin error:", error);
694
+ return NextResponse.redirect(new URL("/", request.url));
695
+ }
696
+ }
697
+
698
+ return NextResponse.next();
699
+ }
700
+
701
+ export const config = {
702
+ matcher: [
703
+ /*
704
+ * Match all request paths except:
705
+ * - _next/static (static files)
706
+ * - _next/image (image optimization files)
707
+ * - favicon.ico (favicon file)
708
+ * - public folder
709
+ */
710
+ "/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
711
+ ],
712
+ };
713
+ `;
714
+ await fs.writeFile(middlewarePath, middleware);
715
+ console.log(chalk.green("✓ middleware.ts créé (protection /auth/* et /admin/*)"));
716
+ }
717
+
718
+ // next.config.mjs
719
+ const nextConfigPath = path.join(targetDir, "next.config.mjs");
720
+ if (!fs.existsSync(nextConfigPath) || force) {
721
+ const nextConfig = `/** @type {import('next').NextConfig} */
722
+ const nextConfig = {
723
+ reactStrictMode: true,
724
+ };
725
+
726
+ export default nextConfig;
727
+ `;
728
+ await fs.writeFile(nextConfigPath, nextConfig);
729
+ console.log(chalk.green("✓ next.config.mjs créé"));
730
+ }
731
+
732
+ // tailwind.config.mjs
733
+ const tailwindConfigPath = path.join(targetDir, "tailwind.config.mjs");
734
+ if (!fs.existsSync(tailwindConfigPath) || force) {
735
+ let tailwindConfig = "";
736
+
737
+ if (useHeroUI) {
738
+ // Configuration avec HeroUI
739
+ tailwindConfig = `import {heroui} from "@heroui/theme"
740
+
741
+ /** @type {import('tailwindcss').Config} */
742
+ const config = {
743
+ content: [
744
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
745
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
746
+ "./node_modules/@lastbrain/**/*.{js,jsx,ts,tsx}",
747
+ "./node_modules/@heroui/**/*.{js,jsx,ts,tsx}"
748
+ ],
749
+ theme: {
750
+ extend: {},
751
+ },
752
+ darkMode: "class",
753
+ plugins: [heroui()],
754
+ }
755
+
756
+ export default config;
757
+ `;
758
+ } else {
759
+ // Configuration Tailwind CSS uniquement
760
+ tailwindConfig = `module.exports = {
761
+ content: [
762
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
763
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
764
+ "./node_modules/@lastbrain/**/*.{js,jsx,ts,tsx}",
765
+ ],
766
+ theme: {
767
+ extend: {
768
+ colors: {
769
+ primary: {
770
+ 50: "#eff6ff",
771
+ 100: "#dbeafe",
772
+ 200: "#bfdbfe",
773
+ 300: "#93c5fd",
774
+ 400: "#60a5fa",
775
+ 500: "#3b82f6",
776
+ 600: "#2563eb",
777
+ 700: "#1d4ed8",
778
+ 800: "#1e40af",
779
+ 900: "#1e3a8a",
780
+ 950: "#172554",
781
+ },
782
+ },
783
+ },
784
+ },
785
+ plugins: [],
786
+ };
787
+ `;
788
+ }
789
+
790
+ await fs.writeFile(tailwindConfigPath, tailwindConfig);
791
+ console.log(chalk.green("✓ tailwind.config.mjs créé"));
792
+ }
793
+
794
+ // postcss.config.mjs
795
+ const postcssConfigPath = path.join(targetDir, "postcss.config.mjs");
796
+ if (!fs.existsSync(postcssConfigPath) || force) {
797
+ const postcssConfig = `export default {
798
+ plugins: {
799
+ tailwindcss: {},
800
+ autoprefixer: {},
801
+ },
802
+ };
803
+ `;
804
+ await fs.writeFile(postcssConfigPath, postcssConfig);
805
+ console.log(chalk.green("✓ postcss.config.mjs créé"));
806
+ }
807
+
808
+ // tsconfig.json
809
+ const tsconfigPath = path.join(targetDir, "tsconfig.json");
810
+ if (!fs.existsSync(tsconfigPath) || force) {
811
+ const tsconfig = {
812
+ compilerOptions: {
813
+ target: "ES2020",
814
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
815
+ jsx: "preserve",
816
+ module: "ESNext",
817
+ moduleResolution: "bundler",
818
+ resolveJsonModule: true,
819
+ allowJs: true,
820
+ strict: true,
821
+ noEmit: true,
822
+ esModuleInterop: true,
823
+ skipLibCheck: true,
824
+ forceConsistentCasingInFileNames: true,
825
+ incremental: true,
826
+ isolatedModules: true,
827
+ plugins: [{ name: "next" }],
828
+ paths: {
829
+ "@/*": ["./*"],
830
+ },
831
+ },
832
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
833
+ exclude: ["node_modules"],
834
+ };
835
+ await fs.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
836
+ console.log(chalk.green("✓ tsconfig.json créé"));
837
+ }
838
+
839
+ // config/menu.ts
840
+ const configDir = path.join(targetDir, "config");
841
+ await fs.ensureDir(configDir);
842
+ const menuConfigPath = path.join(configDir, "menu.ts");
843
+ if (!fs.existsSync(menuConfigPath) || force) {
844
+ const menuConfig = `import type { MenuConfig } from "@lastbrain/ui";
845
+
846
+ export const menuConfig: MenuConfig = {
847
+ public: [
848
+ { label: "Accueil", href: "/" },
849
+ { label: "Documentation", href: "/docs" },
850
+ ],
851
+ auth: [
852
+ { label: "Dashboard", href: "/auth/dashboard" },
853
+ { label: "Profil", href: "/auth/profile" },
854
+ ],
855
+ admin: [
856
+ { label: "Admin", href: "/admin" },
857
+ { label: "Utilisateurs", href: "/admin/users" },
858
+ ],
859
+ };
860
+ `;
861
+ await fs.writeFile(menuConfigPath, menuConfig);
862
+ console.log(chalk.green("✓ config/menu.ts créé"));
863
+ }
864
+ }
865
+
866
+ async function createGitIgnore(targetDir: string, force: boolean) {
867
+ const gitignorePath = path.join(targetDir, ".gitignore");
868
+
869
+ if (!fs.existsSync(gitignorePath) || force) {
870
+ console.log(chalk.yellow("\n📝 Création de .gitignore..."));
871
+
872
+ const gitignoreContent = `# Dependencies
873
+ node_modules/
874
+ .pnp
875
+ .pnp.js
876
+
877
+ # Testing
878
+ coverage/
879
+
880
+ # Next.js
881
+ .next/
882
+ out/
883
+ build/
884
+ dist/
885
+
886
+ # Production
887
+ *.log*
888
+
889
+ # Misc
890
+ .DS_Store
891
+ *.pem
892
+
893
+ # Debug
894
+ npm-debug.log*
895
+ yarn-debug.log*
896
+ yarn-error.log*
897
+
898
+ # Local env files
899
+ .env
900
+ .env*.local
901
+ .env.production
902
+
903
+ # Vercel
904
+ .vercel
905
+
906
+ # Typescript
907
+ *.tsbuildinfo
908
+ next-env.d.ts
909
+
910
+ # Supabase
911
+ supabase/.temp/
912
+ supabase/.branches/
913
+
914
+ # LastBrain generated
915
+ **/navigation.generated.ts
916
+ **/routes.generated.ts
917
+ `;
918
+ await fs.writeFile(gitignorePath, gitignoreContent);
919
+ console.log(chalk.green("✓ .gitignore créé"));
920
+ }
921
+ }
922
+
923
+ async function createEnvExample(targetDir: string, force: boolean) {
924
+ const envExamplePath = path.join(targetDir, ".env.local.example");
925
+
926
+ if (!fs.existsSync(envExamplePath) || force) {
927
+ console.log(chalk.yellow("\n🔐 Création de .env.local.example..."));
928
+
929
+ const envContent = `# Supabase Configuration
930
+ # Exécutez 'pnpm db:init' pour initialiser Supabase local et générer le vrai .env.local
931
+
932
+ # Supabase Local (par défaut)
933
+ NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
934
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_LOCAL_ANON_KEY
935
+ SUPABASE_SERVICE_ROLE_KEY=YOUR_LOCAL_SERVICE_ROLE_KEY
936
+
937
+ # Supabase Production
938
+ # NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
939
+ # NEXT_PUBLIC_SUPABASE_ANON_KEY=your_production_anon_key
940
+ # SUPABASE_SERVICE_ROLE_KEY=your_production_service_role_key
941
+ `;
942
+ await fs.writeFile(envExamplePath, envContent);
943
+ console.log(chalk.green("✓ .env.local.example créé"));
944
+ }
945
+ }
946
+
947
+ async function createSupabaseStructure(targetDir: string, force: boolean) {
948
+ console.log(chalk.yellow("\n🗄️ Création de la structure Supabase..."));
949
+
950
+ const supabaseDir = path.join(targetDir, "supabase");
951
+ const migrationsDir = path.join(supabaseDir, "migrations");
952
+ await fs.ensureDir(migrationsDir);
953
+
954
+ // Copier le fichier de migration initial depuis le template
955
+ const templateMigrationPath = path.join(
956
+ __dirname,
957
+ "../templates/migrations/20201010100000_app_base.sql"
958
+ );
959
+ const migrationDestPath = path.join(
960
+ migrationsDir,
961
+ "20201010100000_app_base.sql"
962
+ );
963
+
964
+ if (!fs.existsSync(migrationDestPath) || force) {
965
+ try {
966
+ if (fs.existsSync(templateMigrationPath)) {
967
+ await fs.copy(templateMigrationPath, migrationDestPath);
968
+ console.log(chalk.green("✓ supabase/migrations/20201010100000_app_base.sql créé"));
969
+ } else {
970
+ console.log(
971
+ chalk.yellow(
972
+ "⚠ Template de migration introuvable, création d'un fichier vide"
973
+ )
974
+ );
975
+ await fs.writeFile(
976
+ migrationDestPath,
977
+ "-- Initial migration\n-- Add your database schema here\n"
978
+ );
979
+ console.log(chalk.green("✓ supabase/migrations/20201010100000_app_base.sql créé (vide)"));
980
+ }
981
+ } catch (error) {
982
+ console.error(chalk.red("✗ Erreur lors de la création de la migration:"), error);
983
+ }
984
+ } else {
985
+ console.log(chalk.gray(" supabase/migrations/20201010100000_app_base.sql existe déjà"));
986
+ }
987
+ }
988
+
989
+ async function addScriptsToPackageJson(targetDir: string) {
990
+ console.log(chalk.yellow("\n🔧 Ajout des scripts NPM..."));
991
+
992
+ const pkgPath = path.join(targetDir, "package.json");
993
+ const pkg = await fs.readJson(pkgPath);
994
+
995
+ // Détecter si le projet cible est dans un workspace
996
+ const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
997
+ fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
998
+
999
+ let scriptsPrefix = "node node_modules/@lastbrain/app/dist/scripts/";
1000
+
1001
+ if (targetIsInMonorepo) {
1002
+ // Dans un monorepo, utiliser pnpm exec pour résoudre correctement les workspace packages
1003
+ scriptsPrefix = "pnpm exec lastbrain ";
1004
+ }
1005
+
1006
+ const scripts = {
1007
+ dev: "next dev",
1008
+ build: "next build",
1009
+ start: "next start",
1010
+ lint: "next lint",
1011
+ lastbrain: targetIsInMonorepo
1012
+ ? "pnpm exec lastbrain"
1013
+ : "node node_modules/@lastbrain/app/dist/cli.js",
1014
+ "build:modules": targetIsInMonorepo
1015
+ ? "pnpm exec lastbrain module:build"
1016
+ : "node node_modules/@lastbrain/app/dist/scripts/module-build.js",
1017
+ "db:migrations:sync": targetIsInMonorepo
1018
+ ? "pnpm exec lastbrain db:migrations:sync"
1019
+ : "node node_modules/@lastbrain/app/dist/scripts/db-migrations-sync.js",
1020
+ "db:init": targetIsInMonorepo
1021
+ ? "pnpm exec lastbrain db:init"
1022
+ : "node node_modules/@lastbrain/app/dist/scripts/db-init.js",
1023
+ "readme:create": targetIsInMonorepo
1024
+ ? "pnpm exec lastbrain readme:create"
1025
+ : "node node_modules/@lastbrain/app/dist/scripts/readme-build.js",
1026
+ };
1027
+
1028
+ pkg.scripts = { ...pkg.scripts, ...scripts };
1029
+
1030
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1031
+ console.log(chalk.green("✓ Scripts ajoutés au package.json"));
1032
+ }
1033
+
1034
+ async function saveModulesConfig(
1035
+ targetDir: string,
1036
+ selectedModules: string[],
1037
+ withAuth: boolean
1038
+ ) {
1039
+ const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
1040
+ await fs.ensureDir(path.dirname(modulesConfigPath));
1041
+
1042
+ const modules: Array<{ package: string; active: boolean; migrations?: string[] }> = [];
1043
+
1044
+ // Ajouter TOUS les modules disponibles
1045
+ for (const availableModule of AVAILABLE_MODULES) {
1046
+ const isSelected = selectedModules.includes(availableModule.name) || (withAuth && availableModule.name === 'auth');
1047
+
1048
+ // Vérifier si le module a des migrations
1049
+ const modulePath = path.join(
1050
+ targetDir,
1051
+ "node_modules",
1052
+ ...availableModule.package.split("/")
1053
+ );
1054
+ const migrationsDir = path.join(modulePath, "supabase", "migrations");
1055
+
1056
+ const moduleConfig: { package: string; active: boolean; migrations?: string[] } = {
1057
+ package: availableModule.package,
1058
+ active: isSelected,
1059
+ };
1060
+
1061
+ if (fs.existsSync(migrationsDir)) {
1062
+ const migrationFiles = fs
1063
+ .readdirSync(migrationsDir)
1064
+ .filter((f) => f.endsWith(".sql"));
1065
+
1066
+ if (migrationFiles.length > 0) {
1067
+ moduleConfig.migrations = isSelected ? migrationFiles : [];
1068
+ }
1069
+ }
1070
+
1071
+ modules.push(moduleConfig);
1072
+ }
1073
+
1074
+ await fs.writeJson(modulesConfigPath, { modules }, { spaces: 2 });
1075
+ console.log(chalk.green("✓ Configuration des modules sauvegardée"));
1076
+ }