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