@lastbrain/app 2.0.1 → 2.0.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.
Files changed (131) hide show
  1. package/dist/config/version.d.ts +7 -0
  2. package/dist/config/version.d.ts.map +1 -0
  3. package/dist/config/version.js +25 -0
  4. package/dist/src/__tests__/module-registry.test.d.ts +2 -0
  5. package/dist/src/__tests__/module-registry.test.d.ts.map +1 -0
  6. package/dist/src/__tests__/module-registry.test.js +53 -0
  7. package/dist/src/app-shell/(admin)/layout.d.ts +4 -0
  8. package/dist/src/app-shell/(admin)/layout.d.ts.map +1 -0
  9. package/dist/src/app-shell/(admin)/layout.js +5 -0
  10. package/dist/src/app-shell/(auth)/layout.d.ts +4 -0
  11. package/dist/src/app-shell/(auth)/layout.d.ts.map +1 -0
  12. package/dist/src/app-shell/(auth)/layout.js +5 -0
  13. package/dist/src/app-shell/(public)/page.d.ts +2 -0
  14. package/dist/src/app-shell/(public)/page.d.ts.map +1 -0
  15. package/dist/src/app-shell/(public)/page.js +5 -0
  16. package/dist/src/app-shell/layout.d.ts +3 -0
  17. package/dist/src/app-shell/layout.d.ts.map +1 -0
  18. package/dist/src/app-shell/layout.js +3 -0
  19. package/dist/src/app-shell/not-found.d.ts +2 -0
  20. package/dist/src/app-shell/not-found.d.ts.map +1 -0
  21. package/dist/src/app-shell/not-found.js +10 -0
  22. package/dist/src/auth/authHelpers.d.ts +7 -0
  23. package/dist/src/auth/authHelpers.d.ts.map +1 -0
  24. package/dist/src/auth/authHelpers.js +19 -0
  25. package/dist/src/auth/useAuthSession.d.ts +7 -0
  26. package/dist/src/auth/useAuthSession.d.ts.map +1 -0
  27. package/dist/src/auth/useAuthSession.js +49 -0
  28. package/dist/src/cli.d.ts +3 -0
  29. package/dist/src/cli.d.ts.map +1 -0
  30. package/dist/src/cli.js +143 -0
  31. package/dist/src/components/NotificationContainer.d.ts +2 -0
  32. package/dist/src/components/NotificationContainer.d.ts.map +1 -0
  33. package/dist/src/components/NotificationContainer.js +8 -0
  34. package/dist/src/hooks/useNotifications.d.ts +30 -0
  35. package/dist/src/hooks/useNotifications.d.ts.map +1 -0
  36. package/dist/src/hooks/useNotifications.js +165 -0
  37. package/dist/src/index.d.ts +22 -0
  38. package/dist/src/index.d.ts.map +1 -0
  39. package/dist/src/index.js +22 -0
  40. package/dist/src/layouts/AdminLayout.d.ts +4 -0
  41. package/dist/src/layouts/AdminLayout.d.ts.map +1 -0
  42. package/dist/src/layouts/AdminLayout.js +4 -0
  43. package/dist/src/layouts/AdminLayoutWithSidebar.d.ts +10 -0
  44. package/dist/src/layouts/AdminLayoutWithSidebar.d.ts.map +1 -0
  45. package/dist/src/layouts/AdminLayoutWithSidebar.js +62 -0
  46. package/dist/src/layouts/AppProviders.d.ts +27 -0
  47. package/dist/src/layouts/AppProviders.d.ts.map +1 -0
  48. package/dist/src/layouts/AppProviders.js +48 -0
  49. package/dist/src/layouts/AuthLayout.d.ts +4 -0
  50. package/dist/src/layouts/AuthLayout.d.ts.map +1 -0
  51. package/dist/src/layouts/AuthLayout.js +4 -0
  52. package/dist/src/layouts/AuthLayoutWithSidebar.d.ts +12 -0
  53. package/dist/src/layouts/AuthLayoutWithSidebar.d.ts.map +1 -0
  54. package/dist/src/layouts/AuthLayoutWithSidebar.js +60 -0
  55. package/dist/src/layouts/PublicLayout.d.ts +8 -0
  56. package/dist/src/layouts/PublicLayout.d.ts.map +1 -0
  57. package/dist/src/layouts/PublicLayout.js +6 -0
  58. package/dist/src/layouts/PublicLayoutWithSidebar.d.ts +9 -0
  59. package/dist/src/layouts/PublicLayoutWithSidebar.d.ts.map +1 -0
  60. package/dist/src/layouts/PublicLayoutWithSidebar.js +60 -0
  61. package/dist/src/layouts/RootLayout.d.ts +6 -0
  62. package/dist/src/layouts/RootLayout.d.ts.map +1 -0
  63. package/dist/src/layouts/RootLayout.js +9 -0
  64. package/dist/src/modules/module-loader.d.ts +5 -0
  65. package/dist/src/modules/module-loader.d.ts.map +1 -0
  66. package/dist/src/modules/module-loader.js +10 -0
  67. package/dist/src/scripts/db-init.d.ts +2 -0
  68. package/dist/src/scripts/db-init.d.ts.map +1 -0
  69. package/dist/src/scripts/db-init.js +300 -0
  70. package/dist/src/scripts/db-migrations-sync.d.ts +2 -0
  71. package/dist/src/scripts/db-migrations-sync.d.ts.map +1 -0
  72. package/dist/src/scripts/db-migrations-sync.js +84 -0
  73. package/dist/src/scripts/dev-sync.d.ts +2 -0
  74. package/dist/src/scripts/dev-sync.d.ts.map +1 -0
  75. package/dist/src/scripts/dev-sync.js +194 -0
  76. package/dist/src/scripts/init-app.d.ts +12 -0
  77. package/dist/src/scripts/init-app.d.ts.map +1 -0
  78. package/dist/src/scripts/init-app.js +2175 -0
  79. package/dist/src/scripts/module-add.d.ts +2 -0
  80. package/dist/src/scripts/module-add.d.ts.map +1 -0
  81. package/dist/src/scripts/module-add.js +232 -0
  82. package/dist/src/scripts/module-build.d.ts +2 -0
  83. package/dist/src/scripts/module-build.d.ts.map +1 -0
  84. package/dist/src/scripts/module-build.js +1280 -0
  85. package/dist/src/scripts/module-create.d.ts +28 -0
  86. package/dist/src/scripts/module-create.d.ts.map +1 -0
  87. package/dist/src/scripts/module-create.js +1429 -0
  88. package/dist/src/scripts/module-delete.d.ts +6 -0
  89. package/dist/src/scripts/module-delete.d.ts.map +1 -0
  90. package/dist/src/scripts/module-delete.js +147 -0
  91. package/dist/src/scripts/module-list.d.ts +2 -0
  92. package/dist/src/scripts/module-list.d.ts.map +1 -0
  93. package/dist/src/scripts/module-list.js +61 -0
  94. package/dist/src/scripts/module-remove.d.ts +2 -0
  95. package/dist/src/scripts/module-remove.d.ts.map +1 -0
  96. package/dist/src/scripts/module-remove.js +311 -0
  97. package/dist/src/scripts/readme-build.d.ts +2 -0
  98. package/dist/src/scripts/readme-build.d.ts.map +1 -0
  99. package/dist/src/scripts/readme-build.js +39 -0
  100. package/dist/src/scripts/script-runner.d.ts +5 -0
  101. package/dist/src/scripts/script-runner.d.ts.map +1 -0
  102. package/dist/src/scripts/script-runner.js +25 -0
  103. package/dist/src/templates/AuthGuidePage.d.ts +2 -0
  104. package/dist/src/templates/AuthGuidePage.d.ts.map +1 -0
  105. package/dist/src/templates/AuthGuidePage.js +9 -0
  106. package/dist/src/templates/DefaultDoc.d.ts +2 -0
  107. package/dist/src/templates/DefaultDoc.d.ts.map +1 -0
  108. package/dist/src/templates/DefaultDoc.js +240 -0
  109. package/dist/src/templates/DocPage.d.ts +17 -0
  110. package/dist/src/templates/DocPage.d.ts.map +1 -0
  111. package/dist/src/templates/DocPage.js +193 -0
  112. package/dist/src/templates/DocsPageWithModules.d.ts +2 -0
  113. package/dist/src/templates/DocsPageWithModules.d.ts.map +1 -0
  114. package/dist/src/templates/DocsPageWithModules.js +8 -0
  115. package/dist/src/templates/MigrationsGuidePage.d.ts +2 -0
  116. package/dist/src/templates/MigrationsGuidePage.d.ts.map +1 -0
  117. package/dist/src/templates/MigrationsGuidePage.js +11 -0
  118. package/dist/src/templates/ModuleGuidePage.d.ts +2 -0
  119. package/dist/src/templates/ModuleGuidePage.d.ts.map +1 -0
  120. package/dist/src/templates/ModuleGuidePage.js +14 -0
  121. package/dist/src/templates/SimpleDocPage.d.ts +2 -0
  122. package/dist/src/templates/SimpleDocPage.d.ts.map +1 -0
  123. package/dist/src/templates/SimpleDocPage.js +28 -0
  124. package/dist/src/templates/SimpleHomePage.d.ts +6 -0
  125. package/dist/src/templates/SimpleHomePage.d.ts.map +1 -0
  126. package/dist/src/templates/SimpleHomePage.js +7 -0
  127. package/dist/src/types/menu.d.ts +23 -0
  128. package/dist/src/types/menu.d.ts.map +1 -0
  129. package/dist/src/types/menu.js +1 -0
  130. package/package.json +4 -5
  131. package/src/scripts/init-app.ts +7 -76
@@ -0,0 +1,2175 @@
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 "@lastbrain/core/config/modules";
8
+ import { PACKAGE_VERSIONS } from "../../config/version.js";
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ export async function initApp(options) {
12
+ const { targetDir, force, projectName, useHeroUI, interactive = true, } = options;
13
+ let { withAuth = false, isMonorepoProject = false } = options;
14
+ console.log(chalk.blue("\n🚀 LastBrain App Init\n"));
15
+ console.log(chalk.gray(`📁 Dossier cible: ${targetDir}`));
16
+ if (useHeroUI) {
17
+ console.log(chalk.magenta(`🎨 Mode: HeroUI\n`));
18
+ }
19
+ else {
20
+ console.log(chalk.gray(`🎨 Mode: Tailwind CSS only\n`));
21
+ }
22
+ // Sélection interactive des modules
23
+ const selectedModules = [];
24
+ if (interactive && !withAuth) {
25
+ console.log(chalk.blue("📦 Sélection des modules:\n"));
26
+ const answers = await inquirer.prompt([
27
+ {
28
+ type: "checkbox",
29
+ name: "modules",
30
+ message: "Quels modules voulez-vous installer ?",
31
+ choices: AVAILABLE_MODULES.map((module) => ({
32
+ name: `${module.emoji} ${module.name.charAt(0).toUpperCase() + module.name.slice(1)} - ${module.description}`,
33
+ value: module.name,
34
+ checked: false,
35
+ })),
36
+ },
37
+ {
38
+ type: "rawlist",
39
+ name: "projectType",
40
+ message: "Type de projet ?",
41
+ choices: [
42
+ {
43
+ name: "📦 Indépendant (créer une nouvelle BDD Supabase)",
44
+ value: "independent",
45
+ },
46
+ {
47
+ name: "🏢 Monorepo (utiliser la BDD existante)",
48
+ value: "monorepo",
49
+ },
50
+ ],
51
+ default: "independent",
52
+ },
53
+ ]);
54
+ selectedModules.push(...answers.modules);
55
+ withAuth = selectedModules.includes("auth");
56
+ isMonorepoProject = answers.projectType === "monorepo";
57
+ if (isMonorepoProject) {
58
+ console.log(chalk.cyan("\n💡 Mode monorepo activé:"), chalk.gray("Votre app utilisera la base de données centralisée du monorepo."));
59
+ console.log(chalk.gray(" Aucune configuration Supabase locale ne sera créée.\n"));
60
+ }
61
+ else {
62
+ console.log(chalk.cyan("\n💡 Mode projet indépendant activé:"), chalk.gray("Votre app aura sa propre instance Supabase locale."));
63
+ console.log(chalk.gray(" Configuration complète sera créée.\n"));
64
+ }
65
+ console.log();
66
+ }
67
+ else if (!interactive) {
68
+ // En mode non-interactif, installer auth et ai par défaut
69
+ selectedModules.push("auth", "ai");
70
+ withAuth = true;
71
+ isMonorepoProject = false;
72
+ console.log(chalk.blue("📦 Modules installés par défaut : Authentication, AI Generation\n"));
73
+ }
74
+ // Créer le dossier s'il n'existe pas
75
+ await fs.ensureDir(targetDir);
76
+ // 1. Vérifier/créer package.json
77
+ await ensurePackageJson(targetDir, projectName);
78
+ // 2. Installer les dépendances
79
+ await addDependencies(targetDir, useHeroUI, withAuth, selectedModules);
80
+ // 3. Créer la structure Next.js
81
+ await createNextStructure(targetDir, force, useHeroUI, withAuth);
82
+ // 4. Créer les fichiers de configuration
83
+ await createConfigFiles(targetDir, force, useHeroUI, projectName);
84
+ // 5. Créer le système de proxy storage
85
+ await createStorageProxy(targetDir, force);
86
+ // 6. Créer .gitignore et .env.local.example
87
+ await createGitIgnore(targetDir, force);
88
+ await createEnvExample(targetDir, force);
89
+ await createEnvLocal(targetDir, force, isMonorepoProject);
90
+ await createEnvProd(targetDir, force);
91
+ // 7. Créer la structure Supabase avec migrations (seulement pour projets indépendants)
92
+ if (!isMonorepoProject) {
93
+ console.log(chalk.blue("🗄️ Création de la structure Supabase locale...\n"));
94
+ await createSupabaseStructure(targetDir, force);
95
+ }
96
+ else {
97
+ console.log(chalk.yellow("⏭️ Projet monorepo détecté - pas de configuration Supabase locale requise\n"));
98
+ }
99
+ // 8. Ajouter les scripts NPM
100
+ await addScriptsToPackageJson(targetDir);
101
+ // 9. Enregistrer les modules sélectionnés
102
+ if (withAuth && !selectedModules.includes("auth")) {
103
+ selectedModules.push("auth");
104
+ }
105
+ if (selectedModules.length > 0) {
106
+ await saveModulesConfig(targetDir, selectedModules, withAuth);
107
+ }
108
+ console.log(chalk.green("\n✅ Application LastBrain initialisée avec succès!\n"));
109
+ const relativePath = path.relative(process.cwd(), targetDir);
110
+ // Demander si l'utilisateur veut lancer l'installation et le dev server
111
+ const { launchNow } = await inquirer.prompt([
112
+ {
113
+ type: "confirm",
114
+ name: "launchNow",
115
+ message: "Voulez-vous installer les dépendances et lancer le serveur de développement maintenant ?",
116
+ default: true,
117
+ },
118
+ ]);
119
+ if (launchNow) {
120
+ console.log(chalk.yellow("\n📦 Installation des dépendances...\n"));
121
+ try {
122
+ execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
123
+ console.log(chalk.green("\n✓ Dépendances installées\n"));
124
+ console.log(chalk.yellow("🔧 Génération des routes des modules...\n"));
125
+ execSync("pnpm build:modules", { cwd: targetDir, stdio: "inherit" });
126
+ console.log(chalk.green("\n✓ Routes des modules générées\n"));
127
+ console.log(chalk.yellow("📜 Synchronisation des migrations des modules...\n"));
128
+ try {
129
+ execSync("pnpm db:migrations:sync", {
130
+ cwd: targetDir,
131
+ stdio: "inherit",
132
+ });
133
+ console.log(chalk.green("\n✓ Migrations synchronisées\n"));
134
+ }
135
+ catch (_error) {
136
+ console.log(chalk.yellow("\n⚠️ Erreur de synchronisation des migrations\n"));
137
+ }
138
+ // Ne pas initialiser la BDD en mode monorepo
139
+ if (!isMonorepoProject) {
140
+ console.log(chalk.yellow("🗄️ Initialisation de la base de données...\n"));
141
+ try {
142
+ execSync("pnpm db:init", { cwd: targetDir, stdio: "inherit" });
143
+ console.log(chalk.green("\n✓ Base de données initialisée\n"));
144
+ }
145
+ catch {
146
+ console.log(chalk.yellow("\n⚠️ Erreur d'initialisation de la DB (normal si Supabase pas configuré)\n"));
147
+ }
148
+ }
149
+ else {
150
+ console.log(chalk.cyan("⏭️ Mode monorepo détecté - BDD centralisée, pas d'initialisation locale requise\n"));
151
+ }
152
+ // Détecter le port (par défaut 3000 pour Next.js)
153
+ const port = 3000;
154
+ const url = `http://127.0.0.1:${port}`;
155
+ console.log(chalk.yellow("🚀 Lancement du serveur de développement...\n"));
156
+ console.log(chalk.cyan(`📱 Ouvrez votre navigateur sur : ${url}\n`));
157
+ console.log(chalk.blue("\n📋 Prochaines étapes après le démarrage :"));
158
+ console.log(chalk.white(" 1. Cliquez sur 'Get Started' sur la page d'accueil"));
159
+ if (isMonorepoProject) {
160
+ console.log(chalk.white(" 2. La BDD est centralisée dans le monorepo\n"));
161
+ }
162
+ else {
163
+ console.log(chalk.white(" 2. Lancez Docker Desktop"));
164
+ console.log(chalk.white(" 3. Installez Supabase CLI si nécessaire : brew install supabase/tap/supabase"));
165
+ console.log(chalk.white(" 4. Exécutez : pnpm db:init"));
166
+ console.log(chalk.white(" 5. Rechargez la page\n"));
167
+ }
168
+ // Ouvrir le navigateur
169
+ const openCommand = process.platform === "darwin"
170
+ ? "open"
171
+ : process.platform === "win32"
172
+ ? "start"
173
+ : "xdg-open";
174
+ setTimeout(() => {
175
+ try {
176
+ execSync(`${openCommand} ${url}`, { stdio: "ignore" });
177
+ }
178
+ catch {
179
+ // Ignorer les erreurs d'ouverture du navigateur
180
+ }
181
+ }, 2000);
182
+ // Lancer pnpm dev
183
+ execSync("pnpm dev:local", { cwd: targetDir, stdio: "inherit" });
184
+ }
185
+ catch {
186
+ console.error(chalk.red("\n❌ Erreur lors du lancement\n"));
187
+ console.log(chalk.cyan("\nVous pouvez lancer manuellement avec :"));
188
+ console.log(chalk.white(` cd ${relativePath}`));
189
+ console.log(chalk.white(" pnpm install"));
190
+ console.log(chalk.white(" pnpm build:modules"));
191
+ console.log(chalk.white(" pnpm db:migrations:sync"));
192
+ if (!isMonorepoProject) {
193
+ console.log(chalk.white(" pnpm db:init"));
194
+ }
195
+ console.log(chalk.white(" pnpm dev:local (ou pnpm dev:prod)\n"));
196
+ }
197
+ }
198
+ else {
199
+ console.log(chalk.cyan("\n📋 Prochaines étapes:"));
200
+ console.log(chalk.white(" 1. cd " + relativePath));
201
+ console.log(chalk.white(" 2. pnpm install (installer les dépendances)"));
202
+ console.log(chalk.white(" 3. pnpm build:modules (générer les routes des modules)"));
203
+ console.log(chalk.white(" 4. pnpm db:migrations:sync (synchroniser les migrations)"));
204
+ console.log(chalk.white(" 5. pnpm db:init (initialiser la base de données)"));
205
+ console.log(chalk.white(" 6. pnpm dev:local (ou pnpm dev:prod)\n"));
206
+ console.log(chalk.cyan("\n💡 Scripts disponibles:"));
207
+ console.log(chalk.white(" • pnpm dev:local - Lance avec .env.local"));
208
+ console.log(chalk.white(" • pnpm dev:prod - Lance avec .env.prod"));
209
+ console.log(chalk.white(" • pnpm dev - Lance avec .env actuel\n"));
210
+ console.log(chalk.gray("Prérequis pour Supabase :"));
211
+ console.log(chalk.white(" - Docker Desktop installé et lancé"));
212
+ console.log(chalk.white(" - Supabase CLI : brew install supabase/tap/supabase\n"));
213
+ // Afficher la commande cd pour faciliter la copie
214
+ console.log(chalk.gray("Pour vous déplacer dans le projet :"));
215
+ console.log(chalk.cyan(` cd ${relativePath}\n`));
216
+ }
217
+ }
218
+ async function ensurePackageJson(targetDir, projectName) {
219
+ const pkgPath = path.join(targetDir, "package.json");
220
+ if (!fs.existsSync(pkgPath)) {
221
+ console.log(chalk.yellow("📦 Création de package.json..."));
222
+ const pkg = {
223
+ name: projectName || path.basename(targetDir),
224
+ version: "0.1.0",
225
+ private: true,
226
+ type: "module",
227
+ scripts: {},
228
+ dependencies: {},
229
+ devDependencies: {},
230
+ };
231
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
232
+ console.log(chalk.green("✓ package.json créé"));
233
+ }
234
+ else {
235
+ console.log(chalk.green("✓ package.json existe"));
236
+ }
237
+ }
238
+ /**
239
+ * Récupère les versions actuelles des packages LastBrain depuis npm ou le monorepo
240
+ */
241
+ function getLastBrainVersions(targetDir) {
242
+ const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
243
+ fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
244
+ if (targetIsInMonorepo) {
245
+ // Dans le monorepo, utiliser workspace:*
246
+ return {
247
+ app: "workspace:*",
248
+ core: "workspace:*",
249
+ ui: "workspace:*",
250
+ moduleAuth: "workspace:*",
251
+ moduleAi: "workspace:*",
252
+ };
253
+ }
254
+ // Hors monorepo, utiliser les versions publiées
255
+ return {
256
+ app: PACKAGE_VERSIONS["@lastbrain/app"],
257
+ core: PACKAGE_VERSIONS["@lastbrain/core"],
258
+ ui: PACKAGE_VERSIONS["@lastbrain/ui"],
259
+ moduleAuth: PACKAGE_VERSIONS["@lastbrain/module-auth"],
260
+ moduleAi: PACKAGE_VERSIONS["@lastbrain/module-ai"],
261
+ };
262
+ }
263
+ async function addDependencies(targetDir, useHeroUI, withAuth, selectedModules = []) {
264
+ const pkgPath = path.join(targetDir, "package.json");
265
+ const pkg = await fs.readJson(pkgPath);
266
+ console.log(chalk.yellow("\n📦 Configuration des dépendances..."));
267
+ const versions = getLastBrainVersions(targetDir);
268
+ // Dependencies
269
+ const requiredDeps = {
270
+ next: "^15.0.3",
271
+ react: "^18.3.1",
272
+ "react-dom": "^18.3.1",
273
+ "@lastbrain/app": versions.app,
274
+ "@lastbrain/core": versions.core,
275
+ "@lastbrain/ui": versions.ui,
276
+ "next-themes": "^0.4.6",
277
+ pino: "^10.1.0",
278
+ "pino-pretty": "^13.1.2",
279
+ };
280
+ // Ajouter module-auth si demandé
281
+ if (withAuth) {
282
+ requiredDeps["@lastbrain/module-auth"] = versions.moduleAuth;
283
+ }
284
+ // Ajouter les autres modules sélectionnés
285
+ for (const moduleName of selectedModules) {
286
+ const moduleInfo = AVAILABLE_MODULES.find((m) => m.name === moduleName);
287
+ if (moduleInfo && moduleInfo.package !== "@lastbrain/module-auth") {
288
+ // Utiliser les vraies versions au lieu de "latest"
289
+ if (moduleInfo.package === "@lastbrain/module-ai") {
290
+ requiredDeps[moduleInfo.package] = versions.moduleAi;
291
+ }
292
+ else {
293
+ // Pour les futurs modules, utiliser "latest" en attendant
294
+ requiredDeps[moduleInfo.package] = "latest";
295
+ }
296
+ }
297
+ }
298
+ // Ajouter les dépendances HeroUI si nécessaire
299
+ if (useHeroUI) {
300
+ // Tous les packages HeroUI nécessaires (car @lastbrain/ui les ré-exporte)
301
+ requiredDeps["@heroui/system"] = "^2.4.23";
302
+ requiredDeps["@heroui/theme"] = "^2.4.23";
303
+ requiredDeps["@heroui/accordion"] = "^2.2.24";
304
+ requiredDeps["@heroui/alert"] = "^2.2.27";
305
+ requiredDeps["@heroui/autocomplete"] = "^2.3.29";
306
+ requiredDeps["@heroui/avatar"] = "^2.2.22";
307
+ requiredDeps["@heroui/badge"] = "^2.2.17";
308
+ requiredDeps["@heroui/breadcrumbs"] = "^2.2.22";
309
+ requiredDeps["@heroui/button"] = "^2.2.27";
310
+ requiredDeps["@heroui/card"] = "^2.2.25";
311
+ requiredDeps["@heroui/checkbox"] = "^2.3.27";
312
+ requiredDeps["@heroui/chip"] = "^2.2.22";
313
+ requiredDeps["@heroui/code"] = "^2.2.21";
314
+ requiredDeps["@heroui/divider"] = "^2.2.20";
315
+ requiredDeps["@heroui/drawer"] = "^2.2.24";
316
+ requiredDeps["@heroui/dropdown"] = "^2.3.27";
317
+ requiredDeps["@heroui/form"] = "^2.1.27";
318
+ requiredDeps["@heroui/input"] = "^2.4.28";
319
+ requiredDeps["@heroui/link"] = "^2.2.23";
320
+ requiredDeps["@heroui/listbox"] = "^2.3.26";
321
+ requiredDeps["@heroui/modal"] = "^2.2.24";
322
+ requiredDeps["@heroui/navbar"] = "^2.2.25";
323
+ requiredDeps["@heroui/pagination"] = "^2.2.24";
324
+ requiredDeps["@heroui/popover"] = "^2.3.27";
325
+ requiredDeps["@heroui/progress"] = "^2.2.22";
326
+ requiredDeps["@heroui/radio"] = "^2.3.27";
327
+ requiredDeps["@heroui/scroll-shadow"] = "^2.3.18";
328
+ requiredDeps["@heroui/select"] = "^2.4.28";
329
+ requiredDeps["@heroui/skeleton"] = "^2.2.17";
330
+ requiredDeps["@heroui/snippet"] = "^2.2.28";
331
+ requiredDeps["@heroui/spacer"] = "^2.2.21";
332
+ requiredDeps["@heroui/spinner"] = "^2.2.24";
333
+ requiredDeps["@heroui/switch"] = "^2.2.24";
334
+ requiredDeps["@heroui/table"] = "^2.2.27";
335
+ requiredDeps["@heroui/tabs"] = "^2.2.24";
336
+ requiredDeps["@heroui/toast"] = "^2.0.17";
337
+ requiredDeps["@heroui/tooltip"] = "^2.2.24";
338
+ requiredDeps["@heroui/user"] = "^2.2.22";
339
+ // Dependencies
340
+ requiredDeps["lucide-react"] = "^0.554.0";
341
+ requiredDeps["framer-motion"] = "^11.18.2";
342
+ requiredDeps["clsx"] = "^2.1.1";
343
+ requiredDeps["env-cmd"] = "^11.0.0";
344
+ }
345
+ // DevDependencies
346
+ const requiredDevDeps = {
347
+ tailwindcss: "^4.1.0",
348
+ "@tailwindcss/postcss": "^4.1.0",
349
+ postcss: "^8.4.0",
350
+ typescript: "^5.4.0",
351
+ "@types/node": "^20.0.0",
352
+ "@types/react": "^18.3.0",
353
+ "@types/react-dom": "^18.3.0",
354
+ };
355
+ pkg.dependencies = { ...pkg.dependencies, ...requiredDeps };
356
+ pkg.devDependencies = { ...pkg.devDependencies, ...requiredDevDeps };
357
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
358
+ console.log(chalk.green("✓ Dépendances configurées"));
359
+ }
360
+ async function createNextStructure(targetDir, force, useHeroUI, withAuth) {
361
+ console.log(chalk.yellow("\n📁 Création de la structure Next.js..."));
362
+ const appDir = path.join(targetDir, "app");
363
+ const stylesDir = path.join(targetDir, "styles");
364
+ await fs.ensureDir(appDir);
365
+ await fs.ensureDir(stylesDir);
366
+ // Générer le layout principal
367
+ const layoutDest = path.join(appDir, "layout.tsx");
368
+ if (!fs.existsSync(layoutDest) || force) {
369
+ let layoutContent = "";
370
+ if (useHeroUI) {
371
+ // Layout avec HeroUI - Server Component
372
+ layoutContent = `// GENERATED BY LASTBRAIN APP-SHELL
373
+ // Server Component pour permettre le SSR des pages enfants
374
+
375
+ import "../styles/globals.css";
376
+ import { ClientLayout } from "../components/ClientLayout";
377
+ import type { PropsWithChildren } from "react";
378
+
379
+ export default function RootLayout({ children }: PropsWithChildren<{}>) {
380
+ return (
381
+ <html lang="fr" suppressHydrationWarning>
382
+ <body className="min-h-screen">
383
+ <ClientLayout>{children}</ClientLayout>
384
+ </body>
385
+ </html>
386
+ );
387
+ }
388
+ `;
389
+ }
390
+ else {
391
+ // Layout Tailwind CSS uniquement - Server Component
392
+ layoutContent = `// GENERATED BY LASTBRAIN APP-SHELL
393
+ // Server Component pour permettre le SSR des pages enfants
394
+
395
+ import "../styles/globals.css";
396
+ import { ClientLayout } from "../components/ClientLayout";
397
+ import type { PropsWithChildren } from "react";
398
+
399
+ export default function RootLayout({ children }: PropsWithChildren<{}>) {
400
+ return (
401
+ <html lang="fr" suppressHydrationWarning>
402
+ <body className="min-h-screen">
403
+ <ClientLayout>{children}</ClientLayout>
404
+ </body>
405
+ </html>
406
+ );
407
+ }
408
+ `;
409
+ }
410
+ await fs.writeFile(layoutDest, layoutContent);
411
+ console.log(chalk.green("✓ app/layout.tsx créé"));
412
+ }
413
+ else {
414
+ console.log(chalk.gray(" app/layout.tsx existe déjà (utilisez --force pour écraser)"));
415
+ }
416
+ // Créer globals.css
417
+ const globalsPath = path.join(stylesDir, "globals.css");
418
+ if (!fs.existsSync(globalsPath) || force) {
419
+ const globalsContent = `@import "tailwindcss";
420
+
421
+ @config "../tailwind.config.mjs";
422
+ `;
423
+ await fs.writeFile(globalsPath, globalsContent);
424
+ console.log(chalk.green("✓ styles/globals.css créé"));
425
+ }
426
+ // Créer la page d'accueil publique (racine)
427
+ const homePagePath = path.join(appDir, "page.tsx");
428
+ if (!fs.existsSync(homePagePath) || force) {
429
+ const homePageContent = `// GENERATED BY LASTBRAIN APP-SHELL
430
+
431
+ import { SimpleHomePage } from "@lastbrain/app";
432
+ import { Footer } from "@lastbrain/ui";
433
+ import { footerConfig } from "../config/footer";
434
+
435
+ export default function RootPage() {
436
+ return (
437
+ <>
438
+ <SimpleHomePage showAuth={${withAuth}} />
439
+ <Footer config={footerConfig} />
440
+ </>
441
+ );
442
+ }
443
+ `;
444
+ await fs.writeFile(homePagePath, homePageContent);
445
+ console.log(chalk.green("✓ app/page.tsx créé"));
446
+ }
447
+ // Créer la page not-found.tsx
448
+ const notFoundPath = path.join(appDir, "not-found.tsx");
449
+ if (!fs.existsSync(notFoundPath) || force) {
450
+ const notFoundContent = `"use client";
451
+ import { Button } from "@lastbrain/ui";
452
+ import { useRouter } from "next/navigation";
453
+
454
+ export default function NotFound() {
455
+ const router = useRouter();
456
+ return (
457
+ <div className="flex min-h-screen items-center justify-center bg-background">
458
+ <div className="mx-auto max-w-md text-center">
459
+ <h1 className="mb-4 text-6xl font-bold text-foreground">404</h1>
460
+ <h2 className="mb-4 text-2xl font-semibold text-foreground">
461
+ Page non trouvée
462
+ </h2>
463
+ <p className="mb-8 text-muted-foreground">
464
+ La page que vous recherchez n'existe pas ou a été déplacée.
465
+ </p>
466
+ <Button
467
+ onPress={() => {
468
+ router.back();
469
+ }}
470
+ 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"
471
+ >
472
+ Retour à l'accueil
473
+ </Button>
474
+ </div>
475
+ </div>
476
+ );
477
+ }
478
+ `;
479
+ await fs.writeFile(notFoundPath, notFoundContent);
480
+ console.log(chalk.green("✓ app/not-found.tsx créé"));
481
+ }
482
+ // Créer les routes avec leurs layouts
483
+ await createRoute(appDir, "admin", "admin", force);
484
+ await createRoute(appDir, "auth", "auth", force);
485
+ await createRoute(appDir, "docs", "public", force);
486
+ // Créer le composant ClientLayout (wrapper client avec providers)
487
+ await createClientLayout(targetDir, force, useHeroUI);
488
+ // Créer le composant AppHeader
489
+ await createAppHeader(targetDir, force);
490
+ // Créer le composant AppAside
491
+ await createAppAside(targetDir, force);
492
+ // Créer le wrapper AppProviders avec configuration realtime
493
+ await createAppProvidersWrapper(targetDir, force);
494
+ }
495
+ async function createClientLayout(targetDir, force, useHeroUI) {
496
+ const componentsDir = path.join(targetDir, "components");
497
+ await fs.ensureDir(componentsDir);
498
+ const clientLayoutPath = path.join(componentsDir, "ClientLayout.tsx");
499
+ if (!fs.existsSync(clientLayoutPath) || force) {
500
+ let clientLayoutContent = "";
501
+ if (useHeroUI) {
502
+ // ClientLayout avec HeroUI
503
+ clientLayoutContent = `"use client";
504
+
505
+ import { HeroUIProvider } from "@heroui/system";
506
+ import { ThemeProvider } from "next-themes";
507
+ import { useRouter } from "next/navigation";
508
+ import { AppProviders } from "./AppProviders";
509
+ import { AppHeader } from "./AppHeader";
510
+ import type { ReactNode } from "react";
511
+
512
+ export function ClientLayout({ children }: { children: ReactNode }) {
513
+ const router = useRouter();
514
+
515
+ return (
516
+ <HeroUIProvider navigate={router.push}>
517
+ <ThemeProvider
518
+ attribute="class"
519
+ defaultTheme="dark"
520
+ enableSystem={false}
521
+ storageKey="lastbrain-theme"
522
+ >
523
+ <AppProviders>
524
+ <AppHeader />
525
+ <div className="min-h-screen text-foreground bg-background">
526
+ {children}
527
+ </div>
528
+ </AppProviders>
529
+ </ThemeProvider>
530
+ </HeroUIProvider>
531
+ );
532
+ }
533
+ `;
534
+ }
535
+ else {
536
+ // ClientLayout Tailwind CSS uniquement
537
+ clientLayoutContent = `"use client";
538
+
539
+ import { ThemeProvider } from "next-themes";
540
+ import { AppProviders } from "./AppProviders";
541
+ import { AppHeader } from "./AppHeader";
542
+ import type { ReactNode } from "react";
543
+
544
+ export function ClientLayout({ children }: { children: ReactNode }) {
545
+ return (
546
+ <ThemeProvider
547
+ attribute="class"
548
+ defaultTheme="light"
549
+ enableSystem={false}
550
+ storageKey="lastbrain-theme"
551
+ >
552
+ <AppProviders>
553
+ <AppHeader />
554
+ <div className="min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-white">
555
+ {children}
556
+ </div>
557
+ </AppProviders>
558
+ </ThemeProvider>
559
+ );
560
+ }
561
+ `;
562
+ }
563
+ await fs.writeFile(clientLayoutPath, clientLayoutContent);
564
+ console.log(chalk.green("✓ components/ClientLayout.tsx créé"));
565
+ }
566
+ else {
567
+ console.log(chalk.gray(" components/ClientLayout.tsx existe déjà (utilisez --force pour écraser)"));
568
+ }
569
+ }
570
+ async function createRoute(appDir, routeName, layoutType, force) {
571
+ const routeDir = path.join(appDir, routeName);
572
+ await fs.ensureDir(routeDir);
573
+ // Layout pour la route
574
+ const layoutPath = path.join(routeDir, "layout.tsx");
575
+ if (!fs.existsSync(layoutPath) || force) {
576
+ let layoutContent = "";
577
+ if (routeName === "admin") {
578
+ // Layout admin avec sidebar
579
+ layoutContent = `import { AdminLayoutWithSidebar } from "@lastbrain/app";
580
+ import { menuConfig } from "../../config/menu";
581
+
582
+ export default function AdminLayout({
583
+ children,
584
+ }: {
585
+ children: React.ReactNode;
586
+ }) {
587
+ return (
588
+ <AdminLayoutWithSidebar menuConfig={menuConfig}>
589
+ {children}
590
+ </AdminLayoutWithSidebar>
591
+ );
592
+ }`;
593
+ }
594
+ else if (routeName === "auth") {
595
+ // Layout auth avec sidebar
596
+ layoutContent = `import { AuthLayoutWithSidebar } from "@lastbrain/app";
597
+ import { menuConfig } from "../../config/menu";
598
+
599
+ export default function AuthLayout({
600
+ children,
601
+ }: {
602
+ children: React.ReactNode;
603
+ }) {
604
+ return (
605
+ <AuthLayoutWithSidebar menuConfig={menuConfig}>
606
+ {children}
607
+ </AuthLayoutWithSidebar>
608
+ );
609
+ }`;
610
+ }
611
+ else {
612
+ // Layout standard pour les autres routes (ex: docs = public)
613
+ const layoutComponent = layoutType.charAt(0).toUpperCase() + layoutType.slice(1) + "Layout";
614
+ // Layout docs avec footer
615
+ layoutContent = `import { ${layoutComponent} } from "@lastbrain/app";
616
+ import { footerConfig } from "../../config/footer";
617
+
618
+ export default function DocsLayout({
619
+ children,
620
+ }: {
621
+ children: React.ReactNode;
622
+ }) {
623
+ return <${layoutComponent} footerConfig={footerConfig}>{children}</${layoutComponent}>;
624
+ }`;
625
+ }
626
+ await fs.writeFile(layoutPath, layoutContent);
627
+ console.log(chalk.green(`✓ app/${routeName}/layout.tsx créé`));
628
+ }
629
+ // Page par défaut
630
+ const pagePath = path.join(routeDir, "page.tsx");
631
+ if (!fs.existsSync(pagePath) || force) {
632
+ let templateImport = "";
633
+ let componentName = null;
634
+ // Choisir le template approprié selon la route
635
+ switch (routeName) {
636
+ case "admin":
637
+ templateImport = 'import { ModuleGuidePage } from "@lastbrain/app";';
638
+ componentName = "ModuleGuidePage";
639
+ break;
640
+ case "docs":
641
+ templateImport = 'import { SimpleDocPage } from "@lastbrain/app";';
642
+ componentName = "SimpleDocPage";
643
+ break;
644
+ default:
645
+ // Template générique pour les autres routes
646
+ templateImport = `// Generic page for ${routeName}`;
647
+ componentName = null;
648
+ }
649
+ const pageContent = componentName
650
+ ? `// GENERATED BY LASTBRAIN APP-SHELL
651
+
652
+ ${templateImport}
653
+
654
+ export default function ${routeName.charAt(0).toUpperCase() + routeName.slice(1)}Page() {
655
+ return <${componentName} />;
656
+ }
657
+ `
658
+ : `// GENERATED BY LASTBRAIN APP-SHELL
659
+
660
+ export default function ${routeName.charAt(0).toUpperCase() + routeName.slice(1)}Page() {
661
+ return (
662
+ <div className="container mx-auto px-4 py-8">
663
+ <h1 className="text-3xl font-bold mb-4">${routeName.charAt(0).toUpperCase() + routeName.slice(1)} Page</h1>
664
+ <p className="text-slate-600 dark:text-slate-400">
665
+ Cette page a été générée par LastBrain Init.
666
+ </p>
667
+ <p className="text-sm text-slate-500 dark:text-slate-500 mt-4">
668
+ Route: /${routeName}
669
+ </p>
670
+ </div>
671
+ );
672
+ }
673
+ `;
674
+ await fs.writeFile(pagePath, pageContent);
675
+ console.log(chalk.green(`✓ app/${routeName}/page.tsx créé`));
676
+ }
677
+ }
678
+ async function createAppHeader(targetDir, force) {
679
+ const componentsDir = path.join(targetDir, "components");
680
+ await fs.ensureDir(componentsDir);
681
+ const headerPath = path.join(componentsDir, "AppHeader.tsx");
682
+ if (!fs.existsSync(headerPath) || force) {
683
+ const headerContent = `"use client";
684
+
685
+ import { Header, type MenuItem } from "@lastbrain/ui";
686
+ import { menuConfig } from "../config/menu";
687
+ import { supabaseBrowserClient } from "@lastbrain/core";
688
+ import { useRouter } from "next/navigation";
689
+ import { useAuthSession, useNotificationsContext } from "@lastbrain/app";
690
+ import { useState, useEffect } from "react";
691
+
692
+ interface MenuIgnored {
693
+ public: { title: string; path: string }[];
694
+ auth: { title: string; path: string }[];
695
+ }
696
+
697
+ export function AppHeader() {
698
+ const router = useRouter();
699
+ const { user, isSuperAdmin } = useAuthSession();
700
+ const { data, loading, markAsRead, markAllAsRead, deleteNotification } =
701
+ useNotificationsContext();
702
+ const [menuIgnored, setMenuIgnored] = useState<MenuIgnored | undefined>();
703
+ const [menuCustom, setMenuCustom] = useState<MenuItem[] | undefined>();
704
+ const [isLoading, setIsLoading] = useState(true);
705
+
706
+ // Charger menuIgnored et menuCustom s'ils existent
707
+ useEffect(() => {
708
+ let loadedIgnored = false;
709
+ let loadedCustom = false;
710
+
711
+ // Charger menu-ignored
712
+ import("../config/menu-ignored")
713
+ .then((mod) => setMenuIgnored(mod.menuIgnored))
714
+ .catch(() => setMenuIgnored(undefined))
715
+ .finally(() => {
716
+ loadedIgnored = true;
717
+ if (loadedIgnored && loadedCustom) setIsLoading(false);
718
+ });
719
+
720
+ // Charger menu-custom
721
+ import("../config/menu-custom")
722
+ .then((mod) => {
723
+ // Combiner les menus custom publics et auth
724
+ const customMenus: MenuItem[] = [];
725
+ if (mod.menuCustom?.public) customMenus.push(...mod.menuCustom.public);
726
+ if (mod.menuCustom?.auth) customMenus.push(...mod.menuCustom.auth);
727
+ setMenuCustom(customMenus.length > 0 ? customMenus : undefined);
728
+ })
729
+ .catch(() => setMenuCustom(undefined))
730
+ .finally(() => {
731
+ loadedCustom = true;
732
+ if (loadedIgnored && loadedCustom) setIsLoading(false);
733
+ });
734
+ }, []);
735
+
736
+ const handleLogout = async () => {
737
+ await supabaseBrowserClient.auth.signOut();
738
+ router.push("/");
739
+ router.refresh();
740
+ };
741
+
742
+ return (
743
+ <Header
744
+ user={user}
745
+ onLogout={handleLogout}
746
+ menuConfig={menuConfig}
747
+ accountMenu={menuConfig.account}
748
+ brandName="LastBrain App"
749
+ brandHref="/"
750
+ isSuperAdmin={isSuperAdmin}
751
+ notifications={data.notifications}
752
+ unreadCount={data.unreadCount}
753
+ notificationsLoading={loading}
754
+ onMarkAsRead={markAsRead}
755
+ onMarkAllAsRead={markAllAsRead}
756
+ onDeleteNotification={deleteNotification}
757
+ {...(menuIgnored ? { menuIgnored } : {})}
758
+ {...(menuCustom ? { menuCustom } : {})}
759
+ {...{ isLoadingMenus: isLoading }}
760
+ />
761
+ );
762
+ }
763
+ `;
764
+ await fs.writeFile(headerPath, headerContent);
765
+ console.log(chalk.green("✓ components/AppHeader.tsx créé"));
766
+ }
767
+ else {
768
+ console.log(chalk.gray(" components/AppHeader.tsx existe déjà (utilisez --force pour écraser)"));
769
+ }
770
+ }
771
+ async function createAppAside(targetDir, force) {
772
+ const componentsDir = path.join(targetDir, "components");
773
+ await fs.ensureDir(componentsDir);
774
+ const asidePath = path.join(componentsDir, "AppAside.tsx");
775
+ if (!fs.existsSync(asidePath) || force) {
776
+ const asideContent = `"use client";
777
+
778
+ import { AppAside as UIAppAside } from "@lastbrain/app";
779
+ import { useAuthSession } from "@lastbrain/app";
780
+ import { menuConfig } from "../config/menu";
781
+
782
+ interface AppAsideProps {
783
+ className?: string;
784
+ isVisible?: boolean;
785
+ }
786
+
787
+ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
788
+ const { isSuperAdmin } = useAuthSession();
789
+
790
+ return (
791
+ <UIAppAside
792
+ className={className}
793
+ menuConfig={menuConfig}
794
+ isSuperAdmin={isSuperAdmin}
795
+ isVisible={isVisible}
796
+ />
797
+ );
798
+ }`;
799
+ await fs.writeFile(asidePath, asideContent);
800
+ console.log(chalk.green("✓ components/AppAside.tsx créé"));
801
+ }
802
+ else {
803
+ console.log(chalk.gray(" components/AppAside.tsx existe déjà (utilisez --force pour écraser)"));
804
+ }
805
+ }
806
+ async function createAppProvidersWrapper(targetDir, force) {
807
+ const componentsDir = path.join(targetDir, "components");
808
+ await fs.ensureDir(componentsDir);
809
+ const providersPath = path.join(componentsDir, "AppProviders.tsx");
810
+ if (!fs.existsSync(providersPath) || force) {
811
+ const providersContent = `"use client";
812
+
813
+ import { AppProviders as BaseAppProviders } from "@lastbrain/app";
814
+ import { realtimeConfig } from "../config/realtime";
815
+ import type { ReactNode } from "react";
816
+
817
+ export function AppProviders({ children }: { children: ReactNode }) {
818
+ return (
819
+ <BaseAppProviders realtimeConfig={realtimeConfig}>
820
+ {children}
821
+ </BaseAppProviders>
822
+ );
823
+ }`;
824
+ await fs.writeFile(providersPath, providersContent);
825
+ console.log(chalk.green("✓ components/AppProviders.tsx créé"));
826
+ }
827
+ else {
828
+ console.log(chalk.gray(" components/AppProviders.tsx existe déjà (utilisez --force pour écraser)"));
829
+ }
830
+ }
831
+ async function createConfigFiles(targetDir, force, useHeroUI, projectName) {
832
+ console.log(chalk.yellow("\n⚙️ Création des fichiers de configuration..."));
833
+ // middleware.ts - Protection des routes /auth/*, /admin/* et /api/admin/*
834
+ const middlewarePath = path.join(targetDir, "middleware.ts");
835
+ if (!fs.existsSync(middlewarePath) || force) {
836
+ const middleware = `import { type NextRequest, NextResponse } from "next/server";
837
+ import { createServerClient, type CookieOptions } from "@supabase/ssr";
838
+
839
+ /**
840
+ * Crée un client Supabase pour le middleware
841
+ */
842
+ function createMiddlewareClient(request: NextRequest) {
843
+ let response = NextResponse.next({
844
+ request: {
845
+ headers: request.headers,
846
+ },
847
+ });
848
+
849
+ const supabase = createServerClient(
850
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
851
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
852
+ {
853
+ cookies: {
854
+ get(name: string) {
855
+ return request.cookies.get(name)?.value;
856
+ },
857
+ set(name: string, value: string, options: CookieOptions) {
858
+ request.cookies.set({
859
+ name,
860
+ value,
861
+ ...options,
862
+ });
863
+ response = NextResponse.next({
864
+ request: {
865
+ headers: request.headers,
866
+ },
867
+ });
868
+ response.cookies.set({
869
+ name,
870
+ value,
871
+ ...options,
872
+ });
873
+ },
874
+ remove(name: string, options: CookieOptions) {
875
+ request.cookies.delete(name);
876
+ response = NextResponse.next({
877
+ request: {
878
+ headers: request.headers,
879
+ },
880
+ });
881
+ response.cookies.delete(name);
882
+ },
883
+ },
884
+ }
885
+ );
886
+
887
+ return { supabase, response };
888
+ }
889
+
890
+ export async function middleware(request: NextRequest) {
891
+ const { pathname } = request.nextUrl;
892
+ const isApi = pathname.startsWith("/api/");
893
+
894
+ // Pages publiques d'authentification (ne pas protéger)
895
+ const publicAuthPages = [
896
+ "/signin",
897
+ "/signup",
898
+ "/reset-password",
899
+ "/forgot-password",
900
+ "/callback",
901
+ ];
902
+
903
+ if(process.env.MAINTENANCE_MODE === "true" && !pathname.startsWith("/maintenance")) {
904
+ const redirectUrl = new URL("/maintenance", request.url);
905
+ return NextResponse.redirect(redirectUrl);
906
+ }
907
+
908
+ const isPublicAuthPage = publicAuthPages.some((page) =>
909
+ pathname.startsWith(page)
910
+ );
911
+
912
+ // Ne pas protéger les pages publiques d'authentification
913
+ if (isPublicAuthPage) {
914
+ return NextResponse.next();
915
+ }
916
+
917
+ // Protéger les routes /auth/* (espace membre)
918
+ if (pathname.startsWith("/auth")) {
919
+ try {
920
+ const { supabase, response } = createMiddlewareClient(request);
921
+ const {
922
+ data: { session },
923
+ } = await supabase.auth.getSession();
924
+
925
+ // Pas de session → nettoyage des cookies + redirection vers /signin
926
+ if (!session) {
927
+ const redirectUrl = new URL("/signin", request.url);
928
+ redirectUrl.searchParams.set("redirect", pathname);
929
+ const res = NextResponse.redirect(redirectUrl);
930
+ res.cookies.delete("sb-access-token");
931
+ res.cookies.delete("sb-refresh-token");
932
+ res.cookies.delete("sb:token");
933
+ res.cookies.delete("sb:refresh-token");
934
+ return res;
935
+ }
936
+
937
+ return response;
938
+ } catch (error) {
939
+ console.error("Middleware auth error:", error);
940
+ const res = NextResponse.redirect(new URL("/signin", request.url));
941
+ res.cookies.delete("sb-access-token");
942
+ res.cookies.delete("sb-refresh-token");
943
+ res.cookies.delete("sb:token");
944
+ res.cookies.delete("sb:refresh-token");
945
+ return res;
946
+ }
947
+ }
948
+
949
+ // Protéger les routes /admin/* et /api/admin/* (superadmin uniquement)
950
+ if (pathname.startsWith("/admin") || pathname.startsWith("/api/admin")) {
951
+ try {
952
+ const { supabase, response } = createMiddlewareClient(request);
953
+ const {
954
+ data: { session },
955
+ } = await supabase.auth.getSession();
956
+
957
+ // Pas de session → 401 JSON pour API, sinon redirection vers /signin avec nettoyage cookies
958
+ if (!session) {
959
+ if (isApi) {
960
+ const res = NextResponse.json({ error: "Non authentifié" }, { status: 401 });
961
+ res.cookies.delete("sb-access-token");
962
+ res.cookies.delete("sb-refresh-token");
963
+ res.cookies.delete("sb:token");
964
+ res.cookies.delete("sb:refresh-token");
965
+ return res;
966
+ }
967
+ const redirectUrl = new URL("/signin", request.url);
968
+ redirectUrl.searchParams.set("redirect", pathname);
969
+ const res = NextResponse.redirect(redirectUrl);
970
+ res.cookies.delete("sb-access-token");
971
+ res.cookies.delete("sb-refresh-token");
972
+ res.cookies.delete("sb:token");
973
+ res.cookies.delete("sb:refresh-token");
974
+ return res;
975
+ }
976
+
977
+ // Vérifier si l'utilisateur est superadmin
978
+ const { data: isSuperAdmin, error } = await supabase.rpc(
979
+ "is_superadmin",
980
+ { user_id: session.user.id }
981
+ );
982
+
983
+ if (error || !isSuperAdmin) {
984
+ console.error("Access denied: not a superadmin", error);
985
+ if (isApi) {
986
+ return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
987
+ }
988
+ return NextResponse.redirect(new URL("/", request.url));
989
+ }
990
+
991
+ return response;
992
+ } catch (error) {
993
+ console.error("Middleware admin error:", error);
994
+ if (isApi) {
995
+ return NextResponse.json({ error: "Erreur middleware" }, { status: 500 });
996
+ }
997
+ const res = NextResponse.redirect(new URL("/", request.url));
998
+ res.cookies.delete("sb-access-token");
999
+ res.cookies.delete("sb-refresh-token");
1000
+ res.cookies.delete("sb:token");
1001
+ res.cookies.delete("sb:refresh-token");
1002
+ return res;
1003
+ }
1004
+ }
1005
+
1006
+ return NextResponse.next();
1007
+ }
1008
+
1009
+ export const config = {
1010
+ matcher: [
1011
+ /*
1012
+ * Match all request paths except:
1013
+ * - _next/static (static files)
1014
+ * - _next/image (image optimization files)
1015
+ * - favicon.ico (favicon file)
1016
+ * - public folder
1017
+ */
1018
+ "/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
1019
+ ],
1020
+ };
1021
+ `;
1022
+ await fs.writeFile(middlewarePath, middleware);
1023
+ console.log(chalk.green("✓ middleware.ts créé (protection /auth/*, /admin/* et /api/admin/*)"));
1024
+ }
1025
+ // next.config.mjs
1026
+ const nextConfigPath = path.join(targetDir, "next.config.mjs");
1027
+ if (!fs.existsSync(nextConfigPath) || force) {
1028
+ const nextConfig = `/** @type {import('next').NextConfig} */
1029
+ const nextConfig = {
1030
+ reactStrictMode: true,
1031
+ devIndicators: {
1032
+ position: 'bottom-right',
1033
+ }
1034
+ };
1035
+
1036
+ export default nextConfig;
1037
+ `;
1038
+ await fs.writeFile(nextConfigPath, nextConfig);
1039
+ console.log(chalk.green("✓ next.config.mjs créé"));
1040
+ }
1041
+ // tailwind.config.mjs
1042
+ const tailwindConfigPath = path.join(targetDir, "tailwind.config.mjs");
1043
+ if (!fs.existsSync(tailwindConfigPath) || force) {
1044
+ let tailwindConfig = "";
1045
+ if (useHeroUI) {
1046
+ // Configuration avec HeroUI - complète avec thèmes
1047
+ tailwindConfig = `import { heroui } from "@heroui/theme";
1048
+
1049
+ /** @type {import('tailwindcss').Config} */
1050
+ const config = {
1051
+ content: [
1052
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
1053
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
1054
+ "./node_modules/@lastbrain/*/src/**/*.{js,jsx,ts,tsx}",
1055
+ "./node_modules/@heroui/theme/dist/**/*.{js,mjs}",
1056
+ ],
1057
+ darkMode: "class",
1058
+ plugins: [
1059
+ heroui({
1060
+ themes: {
1061
+ light: {
1062
+ colors: {
1063
+ default: {
1064
+ 950: "#0d0d0e",
1065
+ 900: "#19191c",
1066
+ 800: "#26262a",
1067
+ 700: "#323238",
1068
+ 600: "#3f3f46",
1069
+ 500: "#65656b",
1070
+ 400: "#8c8c90",
1071
+ 300: "#b2b2b5",
1072
+ 200: "#d9d9da",
1073
+ 100: "#ffffff",
1074
+ foreground: "#000000",
1075
+ DEFAULT: "#d4d4d8",
1076
+ },
1077
+ primary: {
1078
+ 50: "#eef2ff",
1079
+ 100: "#e0e7ff",
1080
+ 200: "#c7d2fe",
1081
+ 300: "#a5b4fc",
1082
+ 400: "#818cf8",
1083
+ 500: "#6366f1",
1084
+ 600: "#4f46e5",
1085
+ 700: "#4338ca",
1086
+ 800: "#3730a3",
1087
+ 900: "#312e81",
1088
+ foreground: "#ffffff",
1089
+ DEFAULT: "#6366f1",
1090
+ },
1091
+ secondary: {
1092
+ 50: "#f5e8ff",
1093
+ 100: "#e8ccff",
1094
+ 200: "#d7a6ff",
1095
+ 300: "#c57fff",
1096
+ 400: "#b359ff",
1097
+ 500: "#9b37ff",
1098
+ 600: "#7d1fcc",
1099
+ 700: "#5f1799",
1100
+ 800: "#410f66",
1101
+ 900: "#240833",
1102
+ foreground: "#ffffff",
1103
+ DEFAULT: "#9b37ff",
1104
+ },
1105
+ success: {
1106
+ 50: "#e7fff7",
1107
+ 100: "#c3ffec",
1108
+ 200: "#9affdf",
1109
+ 300: "#70ffd2",
1110
+ 400: "#4bffca",
1111
+ 500: "#22e6b0",
1112
+ 600: "#18b38a",
1113
+ 700: "#108066",
1114
+ 800: "#074d40",
1115
+ 900: "#012b22",
1116
+ foreground: "#000",
1117
+ DEFAULT: "#22e6b0",
1118
+ },
1119
+ warning: {
1120
+ 50: "#fff8e1",
1121
+ 100: "#ffecb3",
1122
+ 200: "#ffe082",
1123
+ 300: "#ffd54f",
1124
+ 400: "#ffca28",
1125
+ 500: "#ffc107",
1126
+ 600: "#ffb300",
1127
+ 700: "#ffa000",
1128
+ 800: "#ff8f00",
1129
+ 900: "#ff6f00",
1130
+ foreground: "#000",
1131
+ DEFAULT: "#ffca28",
1132
+ },
1133
+ danger: {
1134
+ 50: "#ffe6e9",
1135
+ 100: "#ffbac1",
1136
+ 200: "#ff8f99",
1137
+ 300: "#ff6471",
1138
+ 400: "#ff3949",
1139
+ 500: "#ff112a",
1140
+ 600: "#d80c22",
1141
+ 700: "#b1081b",
1142
+ 800: "#890514",
1143
+ 900: "#62020d",
1144
+ foreground: "#fff",
1145
+ DEFAULT: "#ff112a",
1146
+ },
1147
+ background: "#f7f8fc",
1148
+ foreground: "#0e0e10",
1149
+ content1: "#ffffff",
1150
+ },
1151
+ },
1152
+ dark: {
1153
+ colors: {
1154
+ default: {
1155
+ 50: "#1a1c1f",
1156
+ 100: "#232529",
1157
+ 200: "#2c2f34",
1158
+ 300: "#363940",
1159
+ 400: "#40444d",
1160
+ 500: "#4b4f59",
1161
+ 600: "#6c707b",
1162
+ 700: "#8d919b",
1163
+ 800: "#afb2bb",
1164
+ 900: "#d1d3d9",
1165
+ foreground: "#ffffff",
1166
+ DEFAULT: "#363940",
1167
+ },
1168
+ primary: {
1169
+ 50: "#312e81",
1170
+ 100: "#3730a3",
1171
+ 200: "#4338ca",
1172
+ 300: "#4f46e5",
1173
+ 400: "#6366f1",
1174
+ 500: "#818cf8",
1175
+ 600: "#a5b4fc",
1176
+ 700: "#c7d2fe",
1177
+ 800: "#e0e7ff",
1178
+ 900: "#eef2ff",
1179
+ foreground: "#ffffff",
1180
+ DEFAULT: "#6366f1",
1181
+ },
1182
+ secondary: {
1183
+ 50: "#240833",
1184
+ 100: "#410f66",
1185
+ 200: "#5f1799",
1186
+ 300: "#7d1fcc",
1187
+ 400: "#9b37ff",
1188
+ 500: "#b359ff",
1189
+ 600: "#c57fff",
1190
+ 700: "#d7a6ff",
1191
+ 800: "#e8ccff",
1192
+ 900: "#f5e8ff",
1193
+ foreground: "#ffffff",
1194
+ DEFAULT: "#9b37ff",
1195
+ },
1196
+ success: {
1197
+ 50: "#012b22",
1198
+ 100: "#074d40",
1199
+ 200: "#108066",
1200
+ 300: "#18b38a",
1201
+ 400: "#22e6b0",
1202
+ 500: "#4bffca",
1203
+ 600: "#70ffd2",
1204
+ 700: "#9affdf",
1205
+ 800: "#c3ffec",
1206
+ 900: "#e7fff7",
1207
+ foreground: "#0e0e0e",
1208
+ DEFAULT: "#22e6b0",
1209
+ },
1210
+ warning: {
1211
+ 50: "#3a2c00",
1212
+ 100: "#5e4700",
1213
+ 200: "#836300",
1214
+ 300: "#a77e00",
1215
+ 400: "#cc9900",
1216
+ 500: "#ffc107",
1217
+ 600: "#ffd54f",
1218
+ 700: "#ffe082",
1219
+ 800: "#ffecb3",
1220
+ 900: "#fff8e1",
1221
+ foreground: "#000",
1222
+ DEFAULT: "#ffc107",
1223
+ },
1224
+ danger: {
1225
+ 50: "#4a000a",
1226
+ 100: "#6e000f",
1227
+ 200: "#920014",
1228
+ 300: "#b60019",
1229
+ 400: "#da001f",
1230
+ 500: "#ff112a",
1231
+ 600: "#ff3949",
1232
+ 700: "#ff6471",
1233
+ 800: "#ff8f99",
1234
+ 900: "#ffbac1",
1235
+ foreground: "#ffffff",
1236
+ DEFAULT: "#ff112a",
1237
+ },
1238
+ background: "#0f1114",
1239
+ foreground: "#f8f8f8",
1240
+ content1: "#15181d",
1241
+ },
1242
+ },
1243
+ },
1244
+ }),
1245
+ ],
1246
+ };
1247
+
1248
+ export default config;
1249
+ `;
1250
+ }
1251
+ else {
1252
+ // Configuration Tailwind CSS uniquement
1253
+ tailwindConfig = `/** @type {import('tailwindcss').Config} */
1254
+ const config = {
1255
+ content: [
1256
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
1257
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
1258
+ "./node_modules/@lastbrain/*/src/**/*.{js,jsx,ts,tsx}",
1259
+ ],
1260
+ theme: {
1261
+ extend: {
1262
+ colors: {
1263
+ primary: {
1264
+ 50: "#eff6ff",
1265
+ 100: "#dbeafe",
1266
+ 200: "#bfdbfe",
1267
+ 300: "#93c5fd",
1268
+ 400: "#60a5fa",
1269
+ 500: "#3b82f6",
1270
+ 600: "#2563eb",
1271
+ 700: "#1d4ed8",
1272
+ 800: "#1e40af",
1273
+ 900: "#1e3a8a",
1274
+ 950: "#172554",
1275
+ },
1276
+ },
1277
+ },
1278
+ },
1279
+ plugins: [],
1280
+ }
1281
+
1282
+ export default config;
1283
+ `;
1284
+ }
1285
+ await fs.writeFile(tailwindConfigPath, tailwindConfig);
1286
+ console.log(chalk.green("✓ tailwind.config.mjs créé"));
1287
+ }
1288
+ // postcss.config.mjs
1289
+ const postcssConfigPath = path.join(targetDir, "postcss.config.mjs");
1290
+ if (!fs.existsSync(postcssConfigPath) || force) {
1291
+ const postcssConfig = `export default {
1292
+ plugins: {
1293
+ '@tailwindcss/postcss': {},
1294
+ },
1295
+ };
1296
+ `;
1297
+ await fs.writeFile(postcssConfigPath, postcssConfig);
1298
+ console.log(chalk.green("✓ postcss.config.mjs créé"));
1299
+ }
1300
+ // tsconfig.json
1301
+ const tsconfigPath = path.join(targetDir, "tsconfig.json");
1302
+ if (!fs.existsSync(tsconfigPath) || force) {
1303
+ // Détecter si le projet cible est dans un monorepo
1304
+ const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
1305
+ fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
1306
+ // Générer les chemins en fonction du contexte
1307
+ const paths = {
1308
+ "@/*": ["./*"],
1309
+ };
1310
+ if (targetIsInMonorepo) {
1311
+ // En monorepo, pointer sur les sources locales
1312
+ paths["@lastbrain/ui"] = ["../../packages/ui/src/index.ts"];
1313
+ paths["@lastbrain/ui/*"] = ["../../packages/ui/src/*"];
1314
+ paths["@lastbrain/core"] = ["../../packages/core/src/index.ts"];
1315
+ paths["@lastbrain/core/*"] = ["../../packages/core/src/*"];
1316
+ paths["@lastbrain/app"] = ["../../packages/app/src/index.ts"];
1317
+ paths["@lastbrain/app/*"] = ["../../packages/app/src/*"];
1318
+ paths["@lastbrain/module-auth"] = [
1319
+ "../../packages/module-auth/src/index.ts",
1320
+ ];
1321
+ paths["@lastbrain/module-auth/*"] = ["../../packages/module-auth/src/*"];
1322
+ paths["@lastbrain/module-ai"] = ["../../packages/module-ai/src/index.ts"];
1323
+ paths["@lastbrain/module-ai/*"] = ["../../packages/module-ai/src/*"];
1324
+ paths["@lastbrain/module-legal"] = [
1325
+ "../../packages/module-legal/src/index.ts",
1326
+ ];
1327
+ paths["@lastbrain/module-legal/*"] = [
1328
+ "../../packages/module-legal/src/*",
1329
+ ];
1330
+ paths["@lastbrain/module-project-board"] = [
1331
+ "../../packages/module-project-board/src/index.ts",
1332
+ ];
1333
+ paths["@lastbrain/module-project-board/*"] = [
1334
+ "../../packages/module-project-board/src/*",
1335
+ ];
1336
+ paths["@lastbrain/module-tasks"] = [
1337
+ "../../packages/module-tasks/src/index.ts",
1338
+ ];
1339
+ paths["@lastbrain/module-tasks/*"] = [
1340
+ "../../packages/module-tasks/src/*",
1341
+ ];
1342
+ }
1343
+ else {
1344
+ // Hors monorepo (npm install), ne pas utiliser de paths aliases
1345
+ delete paths["@/*"];
1346
+ }
1347
+ const tsconfig = {
1348
+ compilerOptions: {
1349
+ target: "ES2020",
1350
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
1351
+ jsx: "preserve",
1352
+ module: "ESNext",
1353
+ moduleResolution: "bundler",
1354
+ resolveJsonModule: true,
1355
+ allowJs: true,
1356
+ strict: true,
1357
+ noEmit: true,
1358
+ esModuleInterop: true,
1359
+ skipLibCheck: true,
1360
+ forceConsistentCasingInFileNames: true,
1361
+ incremental: true,
1362
+ isolatedModules: true,
1363
+ plugins: [{ name: "next" }],
1364
+ paths,
1365
+ types: [],
1366
+ },
1367
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1368
+ exclude: ["node_modules", "dist", ".next", "out"],
1369
+ };
1370
+ // Ajouter extends seulement en monorepo
1371
+ if (targetIsInMonorepo) {
1372
+ tsconfig.extends = "../../tsconfig.base.json";
1373
+ }
1374
+ await fs.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
1375
+ console.log(chalk.green("✓ tsconfig.json créé"));
1376
+ }
1377
+ // config/menu.ts
1378
+ const configDir = path.join(targetDir, "config");
1379
+ await fs.ensureDir(configDir);
1380
+ const menuConfigPath = path.join(configDir, "menu.ts");
1381
+ if (!fs.existsSync(menuConfigPath) || force) {
1382
+ const menuConfig = `import type { MenuConfig } from "@lastbrain/ui";
1383
+
1384
+ export const menuConfig: MenuConfig = {
1385
+ public: [
1386
+ { label: "Accueil", href: "/" },
1387
+ { label: "Documentation", href: "/docs" },
1388
+ ],
1389
+ auth: [
1390
+ { label: "Dashboard", href: "/auth/dashboard" },
1391
+ { label: "Profil", href: "/auth/profile" },
1392
+ ],
1393
+ admin: [
1394
+ { label: "Admin", href: "/admin" },
1395
+ { label: "Utilisateurs", href: "/admin/users" },
1396
+ ],
1397
+ };
1398
+ `;
1399
+ await fs.writeFile(menuConfigPath, menuConfig);
1400
+ console.log(chalk.green("✓ config/menu.ts créé"));
1401
+ }
1402
+ // config/menu-ignored.ts - Optional file to hide menus and block routes
1403
+ const menuIgnoredPath = path.join(configDir, "menu-ignored.ts");
1404
+ if (!fs.existsSync(menuIgnoredPath) || force) {
1405
+ const menuIgnored = `import type { MenuIgnored } from "@lastbrain/app";
1406
+
1407
+ /**
1408
+ * Menu items to hide and routes to block
1409
+ * This file is optional - if it doesn't exist, all menus are shown
1410
+ *
1411
+ * Example:
1412
+ * export const menuIgnored: MenuIgnored = {
1413
+ * public: [
1414
+ * { title: "Documentation", path: "/docs" }
1415
+ * ],
1416
+ * auth: [
1417
+ * { title: "Profile", path: "/auth/profile" }
1418
+ * ]
1419
+ * };
1420
+ */
1421
+
1422
+ export const menuIgnored: MenuIgnored = {
1423
+ public: [],
1424
+ auth: [],
1425
+ };
1426
+ `;
1427
+ await fs.writeFile(menuIgnoredPath, menuIgnored);
1428
+ console.log(chalk.green("✓ config/menu-ignored.ts créé"));
1429
+ }
1430
+ // config/menu-custom.ts - Optional file to add custom menus
1431
+ const menuCustomPath = path.join(configDir, "menu-custom.ts");
1432
+ if (!fs.existsSync(menuCustomPath) || force) {
1433
+ const menuCustom = `import type { MenuCustom } from "@lastbrain/app";
1434
+
1435
+ /**
1436
+ * Custom menu items to add without creating a module
1437
+ * This file is optional - if it doesn't exist, no custom menus are added
1438
+ *
1439
+ * Example:
1440
+ * export const menuCustom: MenuCustom = {
1441
+ * public: [
1442
+ * { label: "Blog", href: "/blog" },
1443
+ * { label: "Pricing", href: "/pricing" }
1444
+ * ],
1445
+ * auth: [
1446
+ * { label: "Invoices", href: "/auth/invoices" }
1447
+ * ],
1448
+ * admin: [
1449
+ * { label: "Reports", href: "/admin/reports" }
1450
+ * ]
1451
+ * };
1452
+ */
1453
+
1454
+ export const menuCustom: MenuCustom = {
1455
+ public: [],
1456
+ auth: [],
1457
+ admin: [],
1458
+ };
1459
+ `;
1460
+ await fs.writeFile(menuCustomPath, menuCustom);
1461
+ console.log(chalk.green("✓ config/menu-custom.ts créé"));
1462
+ }
1463
+ // config/footer.ts
1464
+ const footerConfigPath = path.join(configDir, "footer.ts");
1465
+ if (!fs.existsSync(footerConfigPath) || force) {
1466
+ const footerConfig = `// Auto-generated footer configuration
1467
+ // Run "node ../../scripts/generate-footer-config.js ./apps/[your-app]" to regenerate
1468
+ "use client";
1469
+
1470
+ import type { FooterConfig } from "@lastbrain/ui";
1471
+
1472
+ export const footerConfig: FooterConfig = {
1473
+ companyName: "${projectName}",
1474
+ companyDescription: "Application LastBrain",
1475
+ links: [],
1476
+ social: [],
1477
+ };
1478
+ `;
1479
+ await fs.writeFile(footerConfigPath, footerConfig);
1480
+ console.log(chalk.green("✓ config/footer.ts créé"));
1481
+ }
1482
+ // config/user-tabs.ts - User detail page tabs configuration
1483
+ const userTabsConfigPath = path.join(configDir, "user-tabs.ts");
1484
+ if (!fs.existsSync(userTabsConfigPath) || force) {
1485
+ const userTabsConfig = `// User tabs configuration
1486
+ // This file is generated and can be regenerated by running: pnpm build:modules
1487
+ "use client";
1488
+
1489
+ import type React from "react";
1490
+
1491
+ export interface ModuleUserTab {
1492
+ id: string;
1493
+ label: string;
1494
+ component: React.ComponentType<{ userId: string }>;
1495
+ }
1496
+
1497
+ // Module user tabs - dynamically imported from active modules
1498
+ // To add tabs from modules, import them here and add to the array below
1499
+ export const moduleUserTabs: ModuleUserTab[] = [];
1500
+ `;
1501
+ await fs.writeFile(userTabsConfigPath, userTabsConfig);
1502
+ console.log(chalk.green("✓ config/user-tabs.ts créé"));
1503
+ }
1504
+ // Créer les hooks
1505
+ await createHooksDirectory(targetDir, force);
1506
+ }
1507
+ async function createHooksDirectory(targetDir, force) {
1508
+ const hooksDir = path.join(targetDir, "hooks");
1509
+ await fs.ensureDir(hooksDir);
1510
+ // Créer le hook useNotifications
1511
+ const useNotificationsPath = path.join(hooksDir, "useNotifications.ts");
1512
+ if (!fs.existsSync(useNotificationsPath) || force) {
1513
+ const hookContent = `"use client";
1514
+
1515
+ import { useEffect, useState } from "react";
1516
+ import { useNotificationRealtime } from "@lastbrain/core";
1517
+ import { supabaseBrowserClient } from "@lastbrain/core";
1518
+ import { useAuth } from "@lastbrain/app";
1519
+
1520
+ export interface Notification {
1521
+ id: string;
1522
+ title: string;
1523
+ message: string;
1524
+ read: boolean;
1525
+ created_at: string;
1526
+ owner_id: string;
1527
+ }
1528
+
1529
+ export function useNotifications() {
1530
+ const { user } = useAuth();
1531
+ const [notifications, setNotifications] = useState<Notification[]>([]);
1532
+ const [loading, setLoading] = useState(true);
1533
+ const [error, setError] = useState<Error | null>(null);
1534
+
1535
+ // Écouter les changements en temps réel
1536
+ const realtimeSignal = useNotificationRealtime();
1537
+
1538
+ // Charger les notifications initiales
1539
+ const fetchNotifications = async () => {
1540
+ if (!user?.id) return;
1541
+
1542
+ try {
1543
+ setLoading(true);
1544
+ const { data, error } = await supabaseBrowserClient
1545
+ .from("user_notifications")
1546
+ .select("*")
1547
+ .eq("owner_id", user.id)
1548
+ .order("created_at", { ascending: false });
1549
+
1550
+ if (error) throw error;
1551
+ setNotifications(data || []);
1552
+ setError(null);
1553
+ } catch (err) {
1554
+ setError(err as Error);
1555
+ } finally {
1556
+ setLoading(false);
1557
+ }
1558
+ };
1559
+
1560
+ // Charger au montage et quand l'utilisateur change
1561
+ useEffect(() => {
1562
+ fetchNotifications();
1563
+ }, [user?.id]);
1564
+
1565
+ // Recharger quand les données changent en temps réel
1566
+ useEffect(() => {
1567
+ if (realtimeSignal.hasUpdates && user?.id) {
1568
+ console.log("🔔 Rechargement des notifications suite à un changement");
1569
+ fetchNotifications();
1570
+ }
1571
+ }, [realtimeSignal.tick, user?.id]);
1572
+
1573
+ return {
1574
+ notifications,
1575
+ loading,
1576
+ error,
1577
+ refetch: fetchNotifications,
1578
+ hasUpdates: realtimeSignal.hasUpdates,
1579
+ };
1580
+ }
1581
+ `;
1582
+ await fs.writeFile(useNotificationsPath, hookContent);
1583
+ console.log(chalk.green("✓ hooks/useNotifications.ts créé"));
1584
+ }
1585
+ // Créer un placeholder pour realtime.ts qui sera généré par build:modules
1586
+ const realtimePath = path.join(targetDir, "config", "realtime.ts");
1587
+ if (!fs.existsSync(realtimePath) || force) {
1588
+ const realtimeContent = `import type { ModuleRealtimeConfig } from "@lastbrain/core";
1589
+
1590
+ // GENERATED FILE - DO NOT EDIT MANUALLY
1591
+ // Ce fichier sera automatiquement généré lors du build:modules
1592
+ // Exécutez "pnpm build:modules" pour créer la configuration realtime
1593
+
1594
+ export const realtimeConfig: ModuleRealtimeConfig[] = [];
1595
+
1596
+ export default realtimeConfig;
1597
+ `;
1598
+ await fs.writeFile(realtimePath, realtimeContent);
1599
+ console.log(chalk.green("✓ config/realtime.ts placeholder créé"));
1600
+ }
1601
+ }
1602
+ async function createGitIgnore(targetDir, force) {
1603
+ const gitignorePath = path.join(targetDir, ".gitignore");
1604
+ if (!fs.existsSync(gitignorePath) || force) {
1605
+ console.log(chalk.yellow("\n📝 Création de .gitignore..."));
1606
+ // Copier le fichier .gitignore depuis le template
1607
+ const templateGitignorePath = path.join(__dirname, "../templates/gitignore/.gitignore");
1608
+ if (fs.existsSync(templateGitignorePath)) {
1609
+ await fs.copyFile(templateGitignorePath, gitignorePath);
1610
+ }
1611
+ else {
1612
+ // Fallback si le template n'existe pas
1613
+ const gitignoreContent = `# ===========================================
1614
+ # GENERATED BY LASTBRAIN
1615
+ # ===========================================
1616
+
1617
+ # Node
1618
+ node_modules/
1619
+ npm-debug.log*
1620
+ pnpm-debug.log*
1621
+ yarn-debug.log*
1622
+ yarn-error.log*
1623
+
1624
+ # Environment
1625
+ .env
1626
+ .env.*
1627
+ !.env.example
1628
+
1629
+ # Next.js
1630
+ .next/
1631
+ out/
1632
+ dist/
1633
+ build/
1634
+
1635
+ # Supabase
1636
+ supabase/.temp/
1637
+ supabase/functions/.netlify/
1638
+ supabase/functions/node_modules/
1639
+ supabase/.branches/
1640
+
1641
+ # Logs
1642
+ logs
1643
+ *.log
1644
+ *.log.*
1645
+
1646
+ # OS / Editor
1647
+ .DS_Store
1648
+ Thumbs.db
1649
+ .idea/
1650
+ .vscode/
1651
+ *.swp
1652
+
1653
+ # Local backups
1654
+ *.sql
1655
+ *.sql.zip
1656
+ backup/
1657
+ tmp/
1658
+
1659
+ # Coverage
1660
+ coverage/
1661
+ *.lcov
1662
+
1663
+
1664
+ `;
1665
+ await fs.writeFile(gitignorePath, gitignoreContent);
1666
+ }
1667
+ console.log(chalk.green("✓ .gitignore créé"));
1668
+ }
1669
+ }
1670
+ async function createEnvExample(targetDir, force) {
1671
+ const envExamplePath = path.join(targetDir, ".env.local.example");
1672
+ if (!fs.existsSync(envExamplePath) || force) {
1673
+ console.log(chalk.yellow("\n🔐 Création de .env.local.example..."));
1674
+ const envContent = `# Supabase Configuration
1675
+ # Exécutez 'pnpm db:init' pour initialiser Supabase local et générer le vrai .env.local
1676
+
1677
+ # Supabase Local (par défaut)
1678
+ NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
1679
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_LOCAL_ANON_KEY
1680
+ SUPABASE_SERVICE_ROLE_KEY=YOUR_LOCAL_SERVICE_ROLE_KEY
1681
+
1682
+ # Supabase Production
1683
+ # NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
1684
+ # NEXT_PUBLIC_SUPABASE_ANON_KEY=your_production_anon_key
1685
+ # SUPABASE_SERVICE_ROLE_KEY=your_production_service_role_key
1686
+ `;
1687
+ await fs.writeFile(envExamplePath, envContent);
1688
+ console.log(chalk.green("✓ .env.local.example créé"));
1689
+ }
1690
+ }
1691
+ async function createEnvLocal(targetDir, force, isMonorepoProject = false) {
1692
+ const envLocalPath = path.join(targetDir, ".env.local");
1693
+ if (!fs.existsSync(envLocalPath) || force) {
1694
+ console.log(chalk.yellow("\n🔐 Création de .env.local..."));
1695
+ let envContent;
1696
+ if (isMonorepoProject) {
1697
+ // Pour les projets monorepo, utiliser les variables du monorepo
1698
+ envContent = `# Supabase Configuration (Monorepo - Centralisé)
1699
+ # Les variables Supabase sont gérées au niveau du monorepo
1700
+
1701
+ # OpenAI Configuration (clé factice pour éviter les erreurs de build)
1702
+ OPENAI_API_KEY=sk-fake-openai-key-for-development-replace-with-real-key
1703
+
1704
+ # Note: Les variables Supabase sont fournies par le monorepo parent
1705
+ `;
1706
+ }
1707
+ else {
1708
+ // Pour les projets indépendants
1709
+ envContent = `# Supabase Configuration
1710
+ # Valeurs par défaut pour le développement local
1711
+
1712
+ # Supabase Local (par défaut)
1713
+ NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
1714
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
1715
+ SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
1716
+
1717
+ # OpenAI Configuration (clé factice pour éviter les erreurs de build)
1718
+ OPENAI_API_KEY=sk-fake-openai-key-for-development-replace-with-real-key
1719
+ `;
1720
+ }
1721
+ await fs.writeFile(envLocalPath, envContent);
1722
+ console.log(chalk.green("✓ .env.local créé"));
1723
+ }
1724
+ }
1725
+ async function createEnvProd(targetDir, force) {
1726
+ const envProdPath = path.join(targetDir, ".env.prod");
1727
+ if (!fs.existsSync(envProdPath) || force) {
1728
+ console.log(chalk.yellow("\n🔐 Création de .env.prod..."));
1729
+ const envContent = `# Production Environment Configuration
1730
+ # Copy your production values here
1731
+
1732
+ # Supabase Production
1733
+ NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
1734
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=your-prod-anon-key
1735
+ SUPABASE_SERVICE_ROLE_KEY=your-prod-service-role-key
1736
+
1737
+ # OpenAI Production
1738
+ OPENAI_API_KEY=sk-proj-your-prod-api-key
1739
+
1740
+ # Note: Update these values with your actual production credentials
1741
+ `;
1742
+ await fs.writeFile(envProdPath, envContent);
1743
+ console.log(chalk.green("✓ .env.prod créé"));
1744
+ }
1745
+ }
1746
+ async function createSupabaseStructure(targetDir, force) {
1747
+ console.log(chalk.yellow("\n🗄️ Création de la structure Supabase..."));
1748
+ const supabaseDir = path.join(targetDir, "supabase");
1749
+ const migrationsDir = path.join(supabaseDir, "migrations");
1750
+ await fs.ensureDir(migrationsDir);
1751
+ // Copier le fichier de migration initial depuis le template
1752
+ const templateMigrationPath = path.join(__dirname, "../templates/migrations/20201010100000_app_base.sql");
1753
+ const migrationDestPath = path.join(migrationsDir, "20201010100000_app_base.sql");
1754
+ if (!fs.existsSync(migrationDestPath) || force) {
1755
+ try {
1756
+ if (fs.existsSync(templateMigrationPath)) {
1757
+ await fs.copy(templateMigrationPath, migrationDestPath);
1758
+ console.log(chalk.green("✓ supabase/migrations/20201010100000_app_base.sql créé"));
1759
+ }
1760
+ else {
1761
+ console.log(chalk.yellow("⚠ Template de migration introuvable, création d'un fichier vide"));
1762
+ await fs.writeFile(migrationDestPath, "-- Initial migration\n-- Add your database schema here\n");
1763
+ console.log(chalk.green("✓ supabase/migrations/20201010100000_app_base.sql créé (vide)"));
1764
+ }
1765
+ }
1766
+ catch (_error) {
1767
+ console.error(chalk.red("✗ Erreur lors de la création de la migration:"), _error);
1768
+ }
1769
+ }
1770
+ else {
1771
+ console.log(chalk.gray(" supabase/migrations/20201010100000_app_base.sql existe déjà"));
1772
+ }
1773
+ }
1774
+ async function addScriptsToPackageJson(targetDir) {
1775
+ console.log(chalk.yellow("\n🔧 Ajout des scripts NPM..."));
1776
+ const pkgPath = path.join(targetDir, "package.json");
1777
+ const pkg = await fs.readJson(pkgPath);
1778
+ // Détecter si le projet cible est dans un workspace
1779
+ const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
1780
+ fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
1781
+ // Utiliser le CLI depuis le monorepo si présent, sinon depuis node_modules
1782
+ const scriptsPrefix = targetIsInMonorepo
1783
+ ? "node ../../packages/app/dist/cli.js"
1784
+ : "node node_modules/@lastbrain/app/dist/cli.js";
1785
+ const scripts = {
1786
+ predev: targetIsInMonorepo
1787
+ ? "pnpm --filter @lastbrain/core build && pnpm --filter @lastbrain/ui build && pnpm --filter @lastbrain/app build && pnpm --filter @lastbrain/module-auth build && pnpm --filter @lastbrain/module-ai build"
1788
+ : "echo 'No prebuild needed'",
1789
+ dev: "next dev",
1790
+ "dev:local": "env-cmd -f .env.local next dev",
1791
+ "dev:prod": "env-cmd -f .env.prod next dev",
1792
+ build: "next build",
1793
+ start: "next start",
1794
+ lint: "next lint",
1795
+ lastbrain: scriptsPrefix,
1796
+ "build:modules": `${scriptsPrefix} module:build`,
1797
+ "db:migrations:sync": `${scriptsPrefix} db:migrations:sync`,
1798
+ "db:init": `${scriptsPrefix} db:init`,
1799
+ "readme:create": `${scriptsPrefix} readme:create`,
1800
+ };
1801
+ pkg.scripts = { ...pkg.scripts, ...scripts };
1802
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1803
+ console.log(chalk.green("✓ Scripts ajoutés au package.json"));
1804
+ }
1805
+ async function saveModulesConfig(targetDir, selectedModules, withAuth) {
1806
+ const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
1807
+ await fs.ensureDir(path.dirname(modulesConfigPath));
1808
+ const modules = [];
1809
+ // Ajouter TOUS les modules disponibles
1810
+ for (const availableModule of AVAILABLE_MODULES) {
1811
+ const isSelected = selectedModules.includes(availableModule.name) ||
1812
+ (withAuth && availableModule.name === "auth");
1813
+ // Vérifier si le module a des migrations
1814
+ const modulePath = path.join(targetDir, "node_modules", ...availableModule.package.split("/"));
1815
+ const migrationsDir = path.join(modulePath, "supabase", "migrations");
1816
+ const moduleConfig = {
1817
+ package: availableModule.package,
1818
+ active: isSelected,
1819
+ };
1820
+ if (fs.existsSync(migrationsDir)) {
1821
+ const migrationFiles = fs
1822
+ .readdirSync(migrationsDir)
1823
+ .filter((f) => f.endsWith(".sql"));
1824
+ if (migrationFiles.length > 0) {
1825
+ moduleConfig.migrations = isSelected ? migrationFiles : [];
1826
+ }
1827
+ }
1828
+ modules.push(moduleConfig);
1829
+ }
1830
+ await fs.writeJson(modulesConfigPath, { modules }, { spaces: 2 });
1831
+ console.log(chalk.green("✓ Configuration des modules sauvegardée"));
1832
+ }
1833
+ async function createStorageProxy(targetDir, force) {
1834
+ console.log(chalk.yellow("\n🗂️ Création du système de proxy storage..."));
1835
+ // Créer le dossier lib
1836
+ const libDir = path.join(targetDir, "lib");
1837
+ await fs.ensureDir(libDir);
1838
+ // 1. Créer lib/bucket-config.ts
1839
+ const bucketConfigPath = path.join(libDir, "bucket-config.ts");
1840
+ if (!fs.existsSync(bucketConfigPath) || force) {
1841
+ const bucketConfigContent = `/**
1842
+ * Storage configuration for buckets and access control
1843
+ */
1844
+
1845
+ export interface BucketConfig {
1846
+ name: string;
1847
+ isPublic: boolean;
1848
+ description: string;
1849
+ allowedFileTypes?: string[];
1850
+ maxFileSize?: number; // in bytes
1851
+ customAccessControl?: (userId: string, filePath: string) => boolean;
1852
+ }
1853
+
1854
+ export const BUCKET_CONFIGS: Record<string, BucketConfig> = {
1855
+ avatar: {
1856
+ name: "avatar",
1857
+ isPublic: true,
1858
+ description: "User profile pictures and avatars",
1859
+ allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
1860
+ maxFileSize: 10 * 1024 * 1024, // 10MB
1861
+ },
1862
+ app: {
1863
+ name: "app",
1864
+ isPublic: false,
1865
+ description: "Private user files and documents",
1866
+ maxFileSize: 100 * 1024 * 1024, // 100MB
1867
+ customAccessControl: (userId: string, filePath: string) => {
1868
+ // Users can only access files in their own folder (app/{userId}/...)
1869
+ return filePath.startsWith(\`\${userId}/\`);
1870
+ },
1871
+ },
1872
+ recipes: {
1873
+ name: "recipes",
1874
+ isPublic: true,
1875
+ description: "Public recipe images accessible by slug",
1876
+ allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
1877
+ maxFileSize: 50 * 1024 * 1024, // 50MB
1878
+ },
1879
+ // Example for future buckets:
1880
+ // public: {
1881
+ // name: "public",
1882
+ // isPublic: true,
1883
+ // description: "Publicly accessible files like logos, banners",
1884
+ // allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
1885
+ // maxFileSize: 50 * 1024 * 1024, // 50MB
1886
+ // },
1887
+ // documents: {
1888
+ // name: "documents",
1889
+ // isPublic: false,
1890
+ // description: "Private documents requiring authentication",
1891
+ // allowedFileTypes: ["application/pdf", "application/msword", "text/plain"],
1892
+ // maxFileSize: 25 * 1024 * 1024, // 25MB
1893
+ // },
1894
+ };
1895
+
1896
+ /**
1897
+ * Get bucket configuration
1898
+ */
1899
+ export function getBucketConfig(bucketName: string): BucketConfig | null {
1900
+ return BUCKET_CONFIGS[bucketName] || null;
1901
+ }
1902
+
1903
+ /**
1904
+ * Check if bucket is public
1905
+ */
1906
+ export function isPublicBucket(bucketName: string): boolean {
1907
+ const config = getBucketConfig(bucketName);
1908
+ return config?.isPublic ?? false;
1909
+ }
1910
+
1911
+ /**
1912
+ * Check if user has access to a specific file
1913
+ */
1914
+ export function hasFileAccess(bucketName: string, userId: string, filePath: string): boolean {
1915
+ const config = getBucketConfig(bucketName);
1916
+ if (!config) return false;
1917
+
1918
+ // Public buckets are accessible to everyone
1919
+ if (config.isPublic) return true;
1920
+
1921
+ // Private buckets require authentication
1922
+ if (!userId) return false;
1923
+
1924
+ // Apply custom access control if defined
1925
+ if (config.customAccessControl) {
1926
+ return config.customAccessControl(userId, filePath);
1927
+ }
1928
+
1929
+ return true;
1930
+ }
1931
+
1932
+ /**
1933
+ * Validate file type for bucket
1934
+ */
1935
+ export function isValidFileType(bucketName: string, contentType: string): boolean {
1936
+ const config = getBucketConfig(bucketName);
1937
+ if (!config || !config.allowedFileTypes) return true;
1938
+
1939
+ return config.allowedFileTypes.includes(contentType);
1940
+ }
1941
+
1942
+ /**
1943
+ * Check if file size is within bucket limits
1944
+ */
1945
+ export function isValidFileSize(bucketName: string, fileSize: number): boolean {
1946
+ const config = getBucketConfig(bucketName);
1947
+ if (!config || !config.maxFileSize) return true;
1948
+
1949
+ return fileSize <= config.maxFileSize;
1950
+ }`;
1951
+ await fs.writeFile(bucketConfigPath, bucketConfigContent);
1952
+ console.log(chalk.green("✓ lib/bucket-config.ts créé"));
1953
+ }
1954
+ // 2. Créer lib/storage.ts
1955
+ const storagePath = path.join(libDir, "storage.ts");
1956
+ if (!fs.existsSync(storagePath) || force) {
1957
+ const storageContent = `/**
1958
+ * Build storage proxy URL for files in Supabase buckets
1959
+ *
1960
+ * @param bucket - The bucket name (e.g., "avatar", "app")
1961
+ * @param path - The file path within the bucket
1962
+ * @returns Proxied URL (e.g., "/api/storage/avatar/user_128_123456.webp")
1963
+ */
1964
+ export function buildStorageUrl(bucket: string, path: string): string {
1965
+ // Remove leading slash if present
1966
+ const cleanPath = path.startsWith("/") ? path.slice(1) : path;
1967
+
1968
+ // Remove bucket prefix from path if present (e.g., "avatar/file.jpg" -> "file.jpg")
1969
+ const pathWithoutBucket = cleanPath.startsWith(bucket + "/")
1970
+ ? cleanPath.slice(bucket.length + 1)
1971
+ : cleanPath;
1972
+
1973
+ return \`/api/storage/\${bucket}/\${pathWithoutBucket}\`;
1974
+ }
1975
+
1976
+ /**
1977
+ * Extract bucket and path from a storage URL
1978
+ *
1979
+ * @param url - Storage URL (can be proxied URL or Supabase public URL)
1980
+ * @returns Object with bucket and path, or null if not a valid storage URL
1981
+ */
1982
+ export function parseStorageUrl(url: string): { bucket: string; path: string } | null {
1983
+ // Handle proxy URLs like "/api/storage/avatar/file.jpg"
1984
+ const proxyMatch = url.match(/^\\/api\\/storage\\/([^\\/]+)\\/(.+)$/);
1985
+ if (proxyMatch) {
1986
+ return {
1987
+ bucket: proxyMatch[1],
1988
+ path: proxyMatch[2]
1989
+ };
1990
+ }
1991
+
1992
+ // Handle Supabase public URLs
1993
+ const supabaseMatch = url.match(/\\/storage\\/v1\\/object\\/public\\/([^\\/]+)\\/(.+)$/);
1994
+ if (supabaseMatch) {
1995
+ return {
1996
+ bucket: supabaseMatch[1],
1997
+ path: supabaseMatch[2]
1998
+ };
1999
+ }
2000
+
2001
+ return null;
2002
+ }
2003
+
2004
+ /**
2005
+ * Convert a Supabase storage path to proxy URL
2006
+ *
2007
+ * @param storagePath - Path like "avatar/file.jpg" or "app/user/file.pdf"
2008
+ * @returns Proxied URL
2009
+ */
2010
+ export function storagePathToProxyUrl(storagePath: string): string {
2011
+ const parts = storagePath.split("/");
2012
+ if (parts.length < 2) {
2013
+ throw new Error("Invalid storage path format");
2014
+ }
2015
+
2016
+ const bucket = parts[0];
2017
+ const path = parts.slice(1).join("/");
2018
+
2019
+ return buildStorageUrl(bucket, path);
2020
+ }
2021
+
2022
+ /**
2023
+ * List of public buckets that don't require authentication
2024
+ */
2025
+ export const PUBLIC_BUCKETS = ["avatar"];
2026
+
2027
+ /**
2028
+ * List of private buckets that require authentication
2029
+ */
2030
+ export const PRIVATE_BUCKETS = ["app"];
2031
+
2032
+ /**
2033
+ * Check if a bucket is public
2034
+ */
2035
+ export function isPublicBucket(bucket: string): boolean {
2036
+ return PUBLIC_BUCKETS.includes(bucket);
2037
+ }
2038
+
2039
+ /**
2040
+ * Check if a bucket is private
2041
+ */
2042
+ export function isPrivateBucket(bucket: string): boolean {
2043
+ return PRIVATE_BUCKETS.includes(bucket);
2044
+ }`;
2045
+ await fs.writeFile(storagePath, storageContent);
2046
+ console.log(chalk.green("✓ lib/storage.ts créé"));
2047
+ }
2048
+ // 3. Créer app/api/storage/[bucket]/[...path]/route.ts
2049
+ const apiStorageDir = path.join(targetDir, "app", "api", "storage", "[bucket]", "[...path]");
2050
+ await fs.ensureDir(apiStorageDir);
2051
+ const routePath = path.join(apiStorageDir, "route.ts");
2052
+ if (!fs.existsSync(routePath) || force) {
2053
+ const routeContent = `import { NextRequest, NextResponse } from "next/server";
2054
+ import { getSupabaseServerClient } from "@lastbrain/core/server";
2055
+ import { getBucketConfig, hasFileAccess } from "@/lib/bucket-config";
2056
+
2057
+ /**
2058
+ * GET /api/storage/[bucket]/[...path]
2059
+ * Proxy for Supabase Storage files with clean URLs and access control
2060
+ *
2061
+ * Examples:
2062
+ * - /api/storage/avatar/user_128_123456.webp
2063
+ * - /api/storage/app/user/documents/file.pdf
2064
+ */
2065
+ export async function GET(
2066
+ request: NextRequest,
2067
+ props: { params: Promise<{ bucket: string; path: string[] }> }
2068
+ ) {
2069
+ try {
2070
+ const { bucket, path } = await props.params;
2071
+ const filePath = path.join("/");
2072
+
2073
+ // Check if bucket exists in our configuration
2074
+ const bucketConfig = getBucketConfig(bucket);
2075
+ if (!bucketConfig) {
2076
+ return new NextResponse("Bucket not allowed", { status: 403 });
2077
+ }
2078
+
2079
+ const supabase = await getSupabaseServerClient();
2080
+ let userId: string | null = null;
2081
+
2082
+ // Get user for private buckets or custom access control
2083
+ if (!bucketConfig.isPublic) {
2084
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
2085
+
2086
+ if (authError || !user) {
2087
+ return new NextResponse("Unauthorized", { status: 401 });
2088
+ }
2089
+
2090
+ userId = user.id;
2091
+ }
2092
+
2093
+ // Check file access permissions
2094
+ if (!hasFileAccess(bucket, userId || "", filePath)) {
2095
+ return new NextResponse("Forbidden - Access denied to this file", { status: 403 });
2096
+ }
2097
+
2098
+ // Get file from Supabase Storage
2099
+ const { data: file, error } = await supabase.storage
2100
+ .from(bucket)
2101
+ .download(filePath);
2102
+
2103
+ if (error) {
2104
+ console.error("Storage download error:", error);
2105
+ if (error.message.includes("not found")) {
2106
+ return new NextResponse("File not found", { status: 404 });
2107
+ }
2108
+ return new NextResponse("Storage error", { status: 500 });
2109
+ }
2110
+
2111
+ if (!file) {
2112
+ return new NextResponse("File not found", { status: 404 });
2113
+ }
2114
+
2115
+ // Convert blob to array buffer
2116
+ const arrayBuffer = await file.arrayBuffer();
2117
+
2118
+ // Determine content type from file extension
2119
+ const getContentType = (filename: string): string => {
2120
+ const ext = filename.toLowerCase().split(".").pop();
2121
+ const mimeTypes: Record<string, string> = {
2122
+ // Images
2123
+ jpg: "image/jpeg",
2124
+ jpeg: "image/jpeg",
2125
+ png: "image/png",
2126
+ gif: "image/gif",
2127
+ webp: "image/webp",
2128
+ svg: "image/svg+xml",
2129
+ // Documents
2130
+ pdf: "application/pdf",
2131
+ doc: "application/msword",
2132
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2133
+ xls: "application/vnd.ms-excel",
2134
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2135
+ // Text
2136
+ txt: "text/plain",
2137
+ csv: "text/csv",
2138
+ // Videos
2139
+ mp4: "video/mp4",
2140
+ avi: "video/x-msvideo",
2141
+ mov: "video/quicktime",
2142
+ // Audio
2143
+ mp3: "audio/mpeg",
2144
+ wav: "audio/wav",
2145
+ // Archives
2146
+ zip: "application/zip",
2147
+ rar: "application/x-rar-compressed",
2148
+ };
2149
+ return mimeTypes[ext || ""] || "application/octet-stream";
2150
+ };
2151
+
2152
+ const contentType = getContentType(filePath);
2153
+
2154
+ // Create response with proper headers
2155
+ const response = new NextResponse(arrayBuffer, {
2156
+ status: 200,
2157
+ headers: {
2158
+ "Content-Type": contentType,
2159
+ "Cache-Control": "public, max-age=31536000, immutable", // Cache for 1 year
2160
+ "Content-Length": arrayBuffer.byteLength.toString(),
2161
+ },
2162
+ });
2163
+
2164
+ return response;
2165
+
2166
+ } catch (error) {
2167
+ console.error("Storage proxy error:", error);
2168
+ return new NextResponse("Internal server error", { status: 500 });
2169
+ }
2170
+ }`;
2171
+ await fs.writeFile(routePath, routeContent);
2172
+ console.log(chalk.green("✓ app/api/storage/[bucket]/[...path]/route.ts créé"));
2173
+ }
2174
+ console.log(chalk.green("✓ Système de proxy storage configuré"));
2175
+ }