@lastbrain/app 0.1.8 → 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 (39) hide show
  1. package/dist/scripts/init-app.js +0 -2
  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 +1077 -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,1077 @@
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/postcss": "^4.0.0",
310
+ tailwindcss: "^4.0.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 = `@config "../tailwind.config.mjs";
434
+ @import "tailwindcss";
435
+ `;
436
+ await fs.writeFile(globalsPath, globalsContent);
437
+ console.log(chalk.green("✓ styles/globals.css créé"));
438
+ }
439
+
440
+ // Créer la page d'accueil publique (racine)
441
+ const homePagePath = path.join(appDir, "page.tsx");
442
+ if (!fs.existsSync(homePagePath) || force) {
443
+ const homePageContent = `// GENERATED BY LASTBRAIN APP-SHELL
444
+
445
+ import { SimpleHomePage } from "@lastbrain/app";
446
+
447
+ export default function RootPage() {
448
+ return <SimpleHomePage showAuth={${withAuth}} />;
449
+ }
450
+ `;
451
+ await fs.writeFile(homePagePath, homePageContent);
452
+ console.log(chalk.green("✓ app/page.tsx créé"));
453
+ }
454
+
455
+ // Créer la page not-found.tsx
456
+ const notFoundPath = path.join(appDir, "not-found.tsx");
457
+ if (!fs.existsSync(notFoundPath) || force) {
458
+ const notFoundContent = `"use client";
459
+ import { Button } from "@lastbrain/ui";
460
+ import { useRouter } from "next/navigation";
461
+
462
+ export default function NotFound() {
463
+ const router = useRouter();
464
+ return (
465
+ <div className="flex min-h-screen items-center justify-center bg-background">
466
+ <div className="mx-auto max-w-md text-center">
467
+ <h1 className="mb-4 text-6xl font-bold text-foreground">404</h1>
468
+ <h2 className="mb-4 text-2xl font-semibold text-foreground">
469
+ Page non trouvée
470
+ </h2>
471
+ <p className="mb-8 text-muted-foreground">
472
+ La page que vous recherchez n'existe pas ou a été déplacée.
473
+ </p>
474
+ <Button
475
+ onPress={() => {
476
+ router.back();
477
+ }}
478
+ 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"
479
+ >
480
+ Retour à l'accueil
481
+ </Button>
482
+ </div>
483
+ </div>
484
+ );
485
+ }
486
+ `;
487
+ await fs.writeFile(notFoundPath, notFoundContent);
488
+ console.log(chalk.green("✓ app/not-found.tsx créé"));
489
+ }
490
+
491
+ // Créer les routes avec leurs layouts
492
+ await createRoute(appDir, "admin", "admin", force);
493
+ await createRoute(appDir, "docs", "public", force);
494
+
495
+ // Créer le composant AppHeader
496
+ await createAppHeader(targetDir, force);
497
+ }
498
+
499
+ async function createRoute(
500
+ appDir: string,
501
+ routeName: string,
502
+ layoutType: string,
503
+ force: boolean
504
+ ) {
505
+ const routeDir = path.join(appDir, routeName);
506
+ await fs.ensureDir(routeDir);
507
+
508
+ // Layout pour la route
509
+ const layoutPath = path.join(routeDir, "layout.tsx");
510
+ if (!fs.existsSync(layoutPath) || force) {
511
+ const layoutComponent =
512
+ layoutType.charAt(0).toUpperCase() + layoutType.slice(1) + "Layout";
513
+
514
+ const layoutContent = `import { ${layoutComponent} } from "@lastbrain/app";
515
+
516
+ export default ${layoutComponent};
517
+ `;
518
+ await fs.writeFile(layoutPath, layoutContent);
519
+ console.log(chalk.green(`✓ app/${routeName}/layout.tsx créé`));
520
+ }
521
+
522
+ // Page par défaut
523
+ const pagePath = path.join(routeDir, "page.tsx");
524
+ if (!fs.existsSync(pagePath) || force) {
525
+ let templateImport = "";
526
+ let componentName: string | null = null;
527
+
528
+ // Choisir le template approprié selon la route
529
+ switch (routeName) {
530
+ case "admin":
531
+ templateImport = 'import { ModuleGuidePage } from "@lastbrain/app";';
532
+ componentName = "ModuleGuidePage";
533
+ break;
534
+ case "docs":
535
+ templateImport = 'import { SimpleDocPage } from "@lastbrain/app";';
536
+ componentName = "SimpleDocPage";
537
+ break;
538
+ default:
539
+ // Template générique pour les autres routes
540
+ templateImport = `// Generic page for ${routeName}`;
541
+ componentName = null;
542
+ }
543
+
544
+ const pageContent = componentName
545
+ ? `// GENERATED BY LASTBRAIN APP-SHELL
546
+
547
+ ${templateImport}
548
+
549
+ export default function ${
550
+ routeName.charAt(0).toUpperCase() + routeName.slice(1)
551
+ }Page() {
552
+ return <${componentName} />;
553
+ }
554
+ `
555
+ : `// GENERATED BY LASTBRAIN APP-SHELL
556
+
557
+ export default function ${
558
+ routeName.charAt(0).toUpperCase() + routeName.slice(1)
559
+ }Page() {
560
+ return (
561
+ <div className="container mx-auto px-4 py-8">
562
+ <h1 className="text-3xl font-bold mb-4">${
563
+ routeName.charAt(0).toUpperCase() + routeName.slice(1)
564
+ } Page</h1>
565
+ <p className="text-slate-600 dark:text-slate-400">
566
+ Cette page a été générée par LastBrain Init.
567
+ </p>
568
+ <p className="text-sm text-slate-500 dark:text-slate-500 mt-4">
569
+ Route: /${routeName}
570
+ </p>
571
+ </div>
572
+ );
573
+ }
574
+ `;
575
+ await fs.writeFile(pagePath, pageContent);
576
+ console.log(chalk.green(`✓ app/${routeName}/page.tsx créé`));
577
+ }
578
+ }
579
+
580
+ async function createAppHeader(targetDir: string, force: boolean) {
581
+ const componentsDir = path.join(targetDir, "components");
582
+ await fs.ensureDir(componentsDir);
583
+
584
+ const headerPath = path.join(componentsDir, "AppHeader.tsx");
585
+
586
+ if (!fs.existsSync(headerPath) || force) {
587
+ const headerContent = `"use client";
588
+
589
+ import { Header } from "@lastbrain/ui";
590
+ import { menuConfig } from "../config/menu";
591
+ import { supabaseBrowserClient } from "@lastbrain/core";
592
+ import { useRouter } from "next/navigation";
593
+ import { useAuthSession } from "@lastbrain/app";
594
+
595
+ export function AppHeader() {
596
+ const router = useRouter();
597
+ const { user, isSuperAdmin } = useAuthSession();
598
+
599
+ const handleLogout = async () => {
600
+ await supabaseBrowserClient.auth.signOut();
601
+ router.push("/");
602
+ router.refresh();
603
+ };
604
+
605
+ return (
606
+ <Header
607
+ user={user}
608
+ onLogout={handleLogout}
609
+ menuConfig={menuConfig}
610
+ brandName="LastBrain App"
611
+ brandHref="/"
612
+ isSuperAdmin={isSuperAdmin}
613
+ />
614
+ );
615
+ }
616
+ `;
617
+ await fs.writeFile(headerPath, headerContent);
618
+ console.log(chalk.green("✓ components/AppHeader.tsx créé"));
619
+ } else {
620
+ console.log(
621
+ chalk.gray(" components/AppHeader.tsx existe déjà (utilisez --force pour écraser)")
622
+ );
623
+ }
624
+ }
625
+
626
+ async function createConfigFiles(
627
+ targetDir: string,
628
+ force: boolean,
629
+ useHeroUI: boolean
630
+ ) {
631
+ console.log(chalk.yellow("\n⚙️ Création des fichiers de configuration..."));
632
+
633
+ // middleware.ts - Protection des routes /auth/* et /admin/*
634
+ const middlewarePath = path.join(targetDir, "middleware.ts");
635
+ if (!fs.existsSync(middlewarePath) || force) {
636
+ const middleware = `import { type NextRequest, NextResponse } from "next/server";
637
+ import { createMiddlewareClient } from "@lastbrain/core/server";
638
+
639
+ export async function middleware(request: NextRequest) {
640
+ const { pathname } = request.nextUrl;
641
+
642
+ // Protéger les routes /auth/* (espace membre)
643
+ if (pathname.startsWith("/auth")) {
644
+ try {
645
+ const { supabase, response } = createMiddlewareClient(request);
646
+ const {
647
+ data: { session },
648
+ } = await supabase.auth.getSession();
649
+
650
+ // Pas de session → redirection vers /signin
651
+ if (!session) {
652
+ const redirectUrl = new URL("/signin", request.url);
653
+ redirectUrl.searchParams.set("redirect", pathname);
654
+ return NextResponse.redirect(redirectUrl);
655
+ }
656
+
657
+ return response;
658
+ } catch (error) {
659
+ console.error("Middleware auth error:", error);
660
+ return NextResponse.redirect(new URL("/signin", request.url));
661
+ }
662
+ }
663
+
664
+ // Protéger les routes /admin/* (superadmin uniquement)
665
+ if (pathname.startsWith("/admin")) {
666
+ try {
667
+ const { supabase, response } = createMiddlewareClient(request);
668
+ const {
669
+ data: { session },
670
+ } = await supabase.auth.getSession();
671
+
672
+ // Pas de session → redirection vers /signin
673
+ if (!session) {
674
+ const redirectUrl = new URL("/signin", request.url);
675
+ redirectUrl.searchParams.set("redirect", pathname);
676
+ return NextResponse.redirect(redirectUrl);
677
+ }
678
+
679
+ // Vérifier si l'utilisateur est superadmin
680
+ const { data: isSuperAdmin, error } = await supabase.rpc(
681
+ "is_superadmin",
682
+ { user_id: session.user.id }
683
+ );
684
+
685
+ if (error || !isSuperAdmin) {
686
+ console.error("Access denied: not a superadmin", error);
687
+ return NextResponse.redirect(new URL("/", request.url));
688
+ }
689
+
690
+ return response;
691
+ } catch (error) {
692
+ console.error("Middleware admin error:", error);
693
+ return NextResponse.redirect(new URL("/", request.url));
694
+ }
695
+ }
696
+
697
+ return NextResponse.next();
698
+ }
699
+
700
+ export const config = {
701
+ matcher: [
702
+ /*
703
+ * Match all request paths except:
704
+ * - _next/static (static files)
705
+ * - _next/image (image optimization files)
706
+ * - favicon.ico (favicon file)
707
+ * - public folder
708
+ */
709
+ "/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
710
+ ],
711
+ };
712
+ `;
713
+ await fs.writeFile(middlewarePath, middleware);
714
+ console.log(chalk.green("✓ middleware.ts créé (protection /auth/* et /admin/*)"));
715
+ }
716
+
717
+ // next.config.mjs
718
+ const nextConfigPath = path.join(targetDir, "next.config.mjs");
719
+ if (!fs.existsSync(nextConfigPath) || force) {
720
+ const nextConfig = `/** @type {import('next').NextConfig} */
721
+ const nextConfig = {
722
+ reactStrictMode: true,
723
+ };
724
+
725
+ export default nextConfig;
726
+ `;
727
+ await fs.writeFile(nextConfigPath, nextConfig);
728
+ console.log(chalk.green("✓ next.config.mjs créé"));
729
+ }
730
+
731
+ // tailwind.config.mjs
732
+ const tailwindConfigPath = path.join(targetDir, "tailwind.config.mjs");
733
+ if (!fs.existsSync(tailwindConfigPath) || force) {
734
+ let tailwindConfig = "";
735
+
736
+ if (useHeroUI) {
737
+ // Configuration avec HeroUI
738
+ tailwindConfig = `import {heroui} from "@heroui/theme"
739
+
740
+ /** @type {import('tailwindcss').Config} */
741
+ const config = {
742
+ content: [
743
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
744
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
745
+ "./node_modules/@lastbrain/**/*.{js,jsx,ts,tsx}",
746
+ "./node_modules/@heroui/**/*.{js,jsx,ts,tsx}"
747
+ ],
748
+ theme: {
749
+ extend: {},
750
+ },
751
+ darkMode: "class",
752
+ plugins: [heroui()],
753
+ }
754
+
755
+ export default config;
756
+ `;
757
+ } else {
758
+ // Configuration Tailwind CSS uniquement
759
+ tailwindConfig = `module.exports = {
760
+ content: [
761
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
762
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
763
+ "./node_modules/@lastbrain/**/*.{js,jsx,ts,tsx}",
764
+ ],
765
+ theme: {
766
+ extend: {
767
+ colors: {
768
+ primary: {
769
+ 50: "#eff6ff",
770
+ 100: "#dbeafe",
771
+ 200: "#bfdbfe",
772
+ 300: "#93c5fd",
773
+ 400: "#60a5fa",
774
+ 500: "#3b82f6",
775
+ 600: "#2563eb",
776
+ 700: "#1d4ed8",
777
+ 800: "#1e40af",
778
+ 900: "#1e3a8a",
779
+ 950: "#172554",
780
+ },
781
+ },
782
+ },
783
+ },
784
+ plugins: [],
785
+ };
786
+ `;
787
+ }
788
+
789
+ await fs.writeFile(tailwindConfigPath, tailwindConfig);
790
+ console.log(chalk.green("✓ tailwind.config.mjs créé"));
791
+ }
792
+
793
+ // postcss.config.mjs
794
+ const postcssConfigPath = path.join(targetDir, "postcss.config.mjs");
795
+ if (!fs.existsSync(postcssConfigPath) || force) {
796
+ const postcssConfig = `const config = {
797
+ plugins: {
798
+ '@tailwindcss/postcss': {},
799
+ autoprefixer: {},
800
+ },
801
+ };
802
+
803
+ export default config;
804
+ `;
805
+ await fs.writeFile(postcssConfigPath, postcssConfig);
806
+ console.log(chalk.green("✓ postcss.config.mjs créé"));
807
+ }
808
+
809
+ // tsconfig.json
810
+ const tsconfigPath = path.join(targetDir, "tsconfig.json");
811
+ if (!fs.existsSync(tsconfigPath) || force) {
812
+ const tsconfig = {
813
+ compilerOptions: {
814
+ target: "ES2020",
815
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
816
+ jsx: "preserve",
817
+ module: "ESNext",
818
+ moduleResolution: "bundler",
819
+ resolveJsonModule: true,
820
+ allowJs: true,
821
+ strict: true,
822
+ noEmit: true,
823
+ esModuleInterop: true,
824
+ skipLibCheck: true,
825
+ forceConsistentCasingInFileNames: true,
826
+ incremental: true,
827
+ isolatedModules: true,
828
+ plugins: [{ name: "next" }],
829
+ paths: {
830
+ "@/*": ["./*"],
831
+ },
832
+ },
833
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
834
+ exclude: ["node_modules"],
835
+ };
836
+ await fs.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
837
+ console.log(chalk.green("✓ tsconfig.json créé"));
838
+ }
839
+
840
+ // config/menu.ts
841
+ const configDir = path.join(targetDir, "config");
842
+ await fs.ensureDir(configDir);
843
+ const menuConfigPath = path.join(configDir, "menu.ts");
844
+ if (!fs.existsSync(menuConfigPath) || force) {
845
+ const menuConfig = `import type { MenuConfig } from "@lastbrain/ui";
846
+
847
+ export const menuConfig: MenuConfig = {
848
+ public: [
849
+ { label: "Accueil", href: "/" },
850
+ { label: "Documentation", href: "/docs" },
851
+ ],
852
+ auth: [
853
+ { label: "Dashboard", href: "/auth/dashboard" },
854
+ { label: "Profil", href: "/auth/profile" },
855
+ ],
856
+ admin: [
857
+ { label: "Admin", href: "/admin" },
858
+ { label: "Utilisateurs", href: "/admin/users" },
859
+ ],
860
+ };
861
+ `;
862
+ await fs.writeFile(menuConfigPath, menuConfig);
863
+ console.log(chalk.green("✓ config/menu.ts créé"));
864
+ }
865
+ }
866
+
867
+ async function createGitIgnore(targetDir: string, force: boolean) {
868
+ const gitignorePath = path.join(targetDir, ".gitignore");
869
+
870
+ if (!fs.existsSync(gitignorePath) || force) {
871
+ console.log(chalk.yellow("\n📝 Création de .gitignore..."));
872
+
873
+ const gitignoreContent = `# Dependencies
874
+ node_modules/
875
+ .pnp
876
+ .pnp.js
877
+
878
+ # Testing
879
+ coverage/
880
+
881
+ # Next.js
882
+ .next/
883
+ out/
884
+ build/
885
+ dist/
886
+
887
+ # Production
888
+ *.log*
889
+
890
+ # Misc
891
+ .DS_Store
892
+ *.pem
893
+
894
+ # Debug
895
+ npm-debug.log*
896
+ yarn-debug.log*
897
+ yarn-error.log*
898
+
899
+ # Local env files
900
+ .env
901
+ .env*.local
902
+ .env.production
903
+
904
+ # Vercel
905
+ .vercel
906
+
907
+ # Typescript
908
+ *.tsbuildinfo
909
+ next-env.d.ts
910
+
911
+ # Supabase
912
+ supabase/.temp/
913
+ supabase/.branches/
914
+
915
+ # LastBrain generated
916
+ **/navigation.generated.ts
917
+ **/routes.generated.ts
918
+ `;
919
+ await fs.writeFile(gitignorePath, gitignoreContent);
920
+ console.log(chalk.green("✓ .gitignore créé"));
921
+ }
922
+ }
923
+
924
+ async function createEnvExample(targetDir: string, force: boolean) {
925
+ const envExamplePath = path.join(targetDir, ".env.local.example");
926
+
927
+ if (!fs.existsSync(envExamplePath) || force) {
928
+ console.log(chalk.yellow("\n🔐 Création de .env.local.example..."));
929
+
930
+ const envContent = `# Supabase Configuration
931
+ # Exécutez 'pnpm db:init' pour initialiser Supabase local et générer le vrai .env.local
932
+
933
+ # Supabase Local (par défaut)
934
+ NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
935
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_LOCAL_ANON_KEY
936
+ SUPABASE_SERVICE_ROLE_KEY=YOUR_LOCAL_SERVICE_ROLE_KEY
937
+
938
+ # Supabase Production
939
+ # NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
940
+ # NEXT_PUBLIC_SUPABASE_ANON_KEY=your_production_anon_key
941
+ # SUPABASE_SERVICE_ROLE_KEY=your_production_service_role_key
942
+ `;
943
+ await fs.writeFile(envExamplePath, envContent);
944
+ console.log(chalk.green("✓ .env.local.example créé"));
945
+ }
946
+ }
947
+
948
+ async function createSupabaseStructure(targetDir: string, force: boolean) {
949
+ console.log(chalk.yellow("\n🗄️ Création de la structure Supabase..."));
950
+
951
+ const supabaseDir = path.join(targetDir, "supabase");
952
+ const migrationsDir = path.join(supabaseDir, "migrations");
953
+ await fs.ensureDir(migrationsDir);
954
+
955
+ // Copier le fichier de migration initial depuis le template
956
+ const templateMigrationPath = path.join(
957
+ __dirname,
958
+ "../templates/migrations/20201010100000_app_base.sql"
959
+ );
960
+ const migrationDestPath = path.join(
961
+ migrationsDir,
962
+ "20201010100000_app_base.sql"
963
+ );
964
+
965
+ if (!fs.existsSync(migrationDestPath) || force) {
966
+ try {
967
+ if (fs.existsSync(templateMigrationPath)) {
968
+ await fs.copy(templateMigrationPath, migrationDestPath);
969
+ console.log(chalk.green("✓ supabase/migrations/20201010100000_app_base.sql créé"));
970
+ } else {
971
+ console.log(
972
+ chalk.yellow(
973
+ "⚠ Template de migration introuvable, création d'un fichier vide"
974
+ )
975
+ );
976
+ await fs.writeFile(
977
+ migrationDestPath,
978
+ "-- Initial migration\n-- Add your database schema here\n"
979
+ );
980
+ console.log(chalk.green("✓ supabase/migrations/20201010100000_app_base.sql créé (vide)"));
981
+ }
982
+ } catch (error) {
983
+ console.error(chalk.red("✗ Erreur lors de la création de la migration:"), error);
984
+ }
985
+ } else {
986
+ console.log(chalk.gray(" supabase/migrations/20201010100000_app_base.sql existe déjà"));
987
+ }
988
+ }
989
+
990
+ async function addScriptsToPackageJson(targetDir: string) {
991
+ console.log(chalk.yellow("\n🔧 Ajout des scripts NPM..."));
992
+
993
+ const pkgPath = path.join(targetDir, "package.json");
994
+ const pkg = await fs.readJson(pkgPath);
995
+
996
+ // Détecter si le projet cible est dans un workspace
997
+ const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
998
+ fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
999
+
1000
+ let scriptsPrefix = "node node_modules/@lastbrain/app/dist/scripts/";
1001
+
1002
+ if (targetIsInMonorepo) {
1003
+ // Dans un monorepo, utiliser pnpm exec pour résoudre correctement les workspace packages
1004
+ scriptsPrefix = "pnpm exec lastbrain ";
1005
+ }
1006
+
1007
+ const scripts = {
1008
+ dev: "next dev",
1009
+ build: "next build",
1010
+ start: "next start",
1011
+ lint: "next lint",
1012
+ lastbrain: targetIsInMonorepo
1013
+ ? "pnpm exec lastbrain"
1014
+ : "node node_modules/@lastbrain/app/dist/cli.js",
1015
+ "build:modules": targetIsInMonorepo
1016
+ ? "pnpm exec lastbrain module:build"
1017
+ : "node node_modules/@lastbrain/app/dist/scripts/module-build.js",
1018
+ "db:migrations:sync": targetIsInMonorepo
1019
+ ? "pnpm exec lastbrain db:migrations:sync"
1020
+ : "node node_modules/@lastbrain/app/dist/scripts/db-migrations-sync.js",
1021
+ "db:init": targetIsInMonorepo
1022
+ ? "pnpm exec lastbrain db:init"
1023
+ : "node node_modules/@lastbrain/app/dist/scripts/db-init.js",
1024
+ "readme:create": targetIsInMonorepo
1025
+ ? "pnpm exec lastbrain readme:create"
1026
+ : "node node_modules/@lastbrain/app/dist/scripts/readme-build.js",
1027
+ };
1028
+
1029
+ pkg.scripts = { ...pkg.scripts, ...scripts };
1030
+
1031
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1032
+ console.log(chalk.green("✓ Scripts ajoutés au package.json"));
1033
+ }
1034
+
1035
+ async function saveModulesConfig(
1036
+ targetDir: string,
1037
+ selectedModules: string[],
1038
+ withAuth: boolean
1039
+ ) {
1040
+ const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
1041
+ await fs.ensureDir(path.dirname(modulesConfigPath));
1042
+
1043
+ const modules: Array<{ package: string; active: boolean; migrations?: string[] }> = [];
1044
+
1045
+ // Ajouter TOUS les modules disponibles
1046
+ for (const availableModule of AVAILABLE_MODULES) {
1047
+ const isSelected = selectedModules.includes(availableModule.name) || (withAuth && availableModule.name === 'auth');
1048
+
1049
+ // Vérifier si le module a des migrations
1050
+ const modulePath = path.join(
1051
+ targetDir,
1052
+ "node_modules",
1053
+ ...availableModule.package.split("/")
1054
+ );
1055
+ const migrationsDir = path.join(modulePath, "supabase", "migrations");
1056
+
1057
+ const moduleConfig: { package: string; active: boolean; migrations?: string[] } = {
1058
+ package: availableModule.package,
1059
+ active: isSelected,
1060
+ };
1061
+
1062
+ if (fs.existsSync(migrationsDir)) {
1063
+ const migrationFiles = fs
1064
+ .readdirSync(migrationsDir)
1065
+ .filter((f) => f.endsWith(".sql"));
1066
+
1067
+ if (migrationFiles.length > 0) {
1068
+ moduleConfig.migrations = isSelected ? migrationFiles : [];
1069
+ }
1070
+ }
1071
+
1072
+ modules.push(moduleConfig);
1073
+ }
1074
+
1075
+ await fs.writeJson(modulesConfigPath, { modules }, { spaces: 2 });
1076
+ console.log(chalk.green("✓ Configuration des modules sauvegardée"));
1077
+ }