@lastbrain/app 0.1.0 → 0.1.2

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