@lastbrain/app 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/scripts/init-app.js +3 -3
  2. package/dist/styles.css +2 -0
  3. package/package.json +11 -3
  4. package/src/app-shell/(admin)/layout.tsx +13 -0
  5. package/src/app-shell/(auth)/layout.tsx +13 -0
  6. package/src/app-shell/(public)/page.tsx +11 -0
  7. package/src/app-shell/layout.tsx +5 -0
  8. package/src/app-shell/not-found.tsx +28 -0
  9. package/src/auth/authHelpers.ts +24 -0
  10. package/src/auth/useAuthSession.ts +54 -0
  11. package/src/cli.ts +96 -0
  12. package/src/index.ts +21 -0
  13. package/src/layouts/AdminLayout.tsx +7 -0
  14. package/src/layouts/AppProviders.tsx +61 -0
  15. package/src/layouts/AuthLayout.tsx +7 -0
  16. package/src/layouts/PublicLayout.tsx +7 -0
  17. package/src/layouts/RootLayout.tsx +27 -0
  18. package/src/modules/module-loader.ts +14 -0
  19. package/src/scripts/README.md +262 -0
  20. package/src/scripts/db-init.ts +338 -0
  21. package/src/scripts/db-migrations-sync.ts +86 -0
  22. package/src/scripts/dev-sync.ts +218 -0
  23. package/src/scripts/init-app.ts +1077 -0
  24. package/src/scripts/module-add.ts +242 -0
  25. package/src/scripts/module-build.ts +502 -0
  26. package/src/scripts/module-create.ts +809 -0
  27. package/src/scripts/module-list.ts +37 -0
  28. package/src/scripts/module-remove.ts +367 -0
  29. package/src/scripts/readme-build.ts +60 -0
  30. package/src/styles.css +3 -0
  31. package/src/templates/AuthGuidePage.tsx +68 -0
  32. package/src/templates/DefaultDoc.tsx +462 -0
  33. package/src/templates/DocPage.tsx +381 -0
  34. package/src/templates/DocsPageWithModules.tsx +22 -0
  35. package/src/templates/MigrationsGuidePage.tsx +61 -0
  36. package/src/templates/ModuleGuidePage.tsx +71 -0
  37. package/src/templates/SimpleDocPage.tsx +587 -0
  38. package/src/templates/SimpleHomePage.tsx +385 -0
  39. package/src/templates/env.example/.env.example +6 -0
  40. package/src/templates/migrations/20201010100000_app_base.sql +228 -0
@@ -0,0 +1,502 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createRequire } from "node:module";
5
+
6
+ import type {
7
+ ModuleApiConfig,
8
+ ModuleBuildConfig,
9
+ ModuleMenuConfig,
10
+ ModuleMenuItemConfig,
11
+ ModulePageConfig,
12
+ ModuleSection,
13
+ } from "@lastbrain/core";
14
+
15
+ // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
16
+ const projectRoot = process.env.PROJECT_ROOT || process.cwd();
17
+ const appDirectory = path.join(projectRoot, "app");
18
+
19
+ // Créer un require dans le contexte de l'application pour résoudre les modules installés dans l'app
20
+ const projectRequire = createRequire(path.join(projectRoot, "package.json"));
21
+
22
+ // Charger les modules depuis modules.json
23
+ async function loadModuleConfigs(): Promise<ModuleBuildConfig[]> {
24
+ const moduleConfigs: ModuleBuildConfig[] = [];
25
+ const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
26
+
27
+ if (!fs.existsSync(modulesJsonPath)) {
28
+ return moduleConfigs;
29
+ }
30
+
31
+ try {
32
+ const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
33
+ const modules = modulesData.modules || [];
34
+
35
+ for (const module of modules) {
36
+ // Ne charger que les modules actifs
37
+ if (module.active === false) {
38
+ continue;
39
+ }
40
+
41
+ const packageName = module.package;
42
+
43
+ try {
44
+ const moduleSuffix = packageName.replace("@lastbrain/module-", "");
45
+ const possibleConfigNames = [
46
+ `${moduleSuffix}.build.config`,
47
+ "build.config",
48
+ ];
49
+
50
+ let loaded = false;
51
+ for (const configName of possibleConfigNames) {
52
+ try {
53
+ // Résoudre le chemin du module depuis l'application
54
+ const modulePath = projectRequire.resolve(`${packageName}/${configName}`);
55
+ // Convertir en URL file:// pour l'import dynamique
56
+ const moduleUrl = `file://${modulePath}`;
57
+ const moduleImport = await import(moduleUrl);
58
+
59
+ if (moduleImport.default) {
60
+ moduleConfigs.push(moduleImport.default);
61
+ loaded = true;
62
+ break;
63
+ }
64
+ } catch (err) {
65
+ // Essayer le nom suivant
66
+ }
67
+ }
68
+
69
+ if (!loaded) {
70
+ console.warn(`⚠️ Could not load build config for ${packageName}`);
71
+ }
72
+ } catch (error) {
73
+ console.warn(`⚠️ Failed to load module ${packageName}:`, error);
74
+ }
75
+ }
76
+ } catch (error) {
77
+ console.error("❌ Error loading modules.json:", error);
78
+ }
79
+
80
+ return moduleConfigs;
81
+ }
82
+
83
+ const sectionDirectoryMap: Record<ModuleSection, string[]> = {
84
+ public: ["(public)"],
85
+ auth: ["auth"],
86
+ admin: ["admin"],
87
+ user: ["auth", "user"],
88
+ };
89
+
90
+ const sectionLayoutMap: Record<string, string> = {
91
+ auth: "AuthLayout",
92
+ admin: "AdminLayout",
93
+ "(public)": "PublicLayout",
94
+ };
95
+
96
+ const generatedSections = new Set<string>();
97
+
98
+ const navigation: Record<string, MenuEntry[]> = {
99
+ public: [],
100
+ auth: [],
101
+ admin: [],
102
+ };
103
+ const userMenu: MenuEntry[] = [];
104
+
105
+ interface MenuEntry {
106
+ label: string;
107
+ path: string;
108
+ module: string;
109
+ section: ModuleSection;
110
+ }
111
+
112
+ function ensureDirectory(dir: string) {
113
+ fs.mkdirSync(dir, { recursive: true });
114
+ }
115
+
116
+ function ensureSectionLayout(sectionPath: string[]) {
117
+ const sectionDir = path.join(appDirectory, ...sectionPath);
118
+ const sectionKey = sectionPath[0];
119
+
120
+ // Éviter de générer plusieurs fois le même layout
121
+ if (generatedSections.has(sectionKey)) {
122
+ return;
123
+ }
124
+
125
+ ensureDirectory(sectionDir);
126
+
127
+ const layoutName = sectionLayoutMap[sectionKey];
128
+ if (layoutName) {
129
+ const layoutPath = path.join(sectionDir, "layout.tsx");
130
+ if (!fs.existsSync(layoutPath)) {
131
+ const layoutContent = `import { ${layoutName} } from "@lastbrain/app";
132
+
133
+ export default function SectionLayout({ children }: { children: React.ReactNode }) {
134
+ return <${layoutName}>{children}</${layoutName}>;
135
+ }
136
+ `;
137
+ fs.writeFileSync(layoutPath, layoutContent);
138
+ console.log(`📐 Generated section layout: ${layoutPath}`);
139
+ }
140
+ }
141
+
142
+ generatedSections.add(sectionKey);
143
+ }
144
+
145
+ function toPascalCase(value: string) {
146
+ return value
147
+ .split(/[^a-zA-Z0-9]+/)
148
+ .filter(Boolean)
149
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
150
+ .join("");
151
+ }
152
+
153
+ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
154
+ const segments = page.path.replace(/^\/+/, "").split("/").filter(Boolean);
155
+ const sectionPath = sectionDirectoryMap[page.section] ?? ["(public)"];
156
+
157
+ // Générer le layout de section si nécessaire
158
+ ensureSectionLayout(sectionPath);
159
+
160
+ const routeDir = path.join(appDirectory, ...sectionPath, ...segments);
161
+ const filePath = path.join(routeDir, "page.tsx");
162
+ ensureDirectory(routeDir);
163
+
164
+ const wrapperSuffix = segments.length
165
+ ? toPascalCase(segments.join("-"))
166
+ : "Root";
167
+ const wrapperName = `${page.componentExport}${wrapperSuffix}Route`;
168
+
169
+ // Vérifier si la route a des paramètres dynamiques (segments avec [])
170
+ const hasDynamicParams = segments.some(seg => seg.startsWith('[') && seg.endsWith(']'));
171
+
172
+ // Pour les pages publiques (signin, signup, etc.), utiliser dynamic import sans SSR
173
+ // pour éviter les erreurs d'hydratation avec les IDs HeroUI/React Aria
174
+ const isPublicAuthPage = page.section === "public" &&
175
+ (page.path.includes("signin") || page.path.includes("signup") || page.path.includes("reset-password"));
176
+
177
+ let content: string;
178
+
179
+ if (isPublicAuthPage) {
180
+ content = `"use client";
181
+
182
+ import dynamic from "next/dynamic";
183
+
184
+ const ${page.componentExport} = dynamic(
185
+ () => import("${moduleConfig.moduleName}").then((mod) => ({ default: mod.${page.componentExport} })),
186
+ { ssr: false }
187
+ );
188
+
189
+ export default function ${wrapperName}${hasDynamicParams ? '(props: any)' : '()'} {
190
+ return <${page.componentExport} ${hasDynamicParams ? '{...props}' : ''} />;
191
+ }
192
+ `;
193
+ } else {
194
+ content = `import { ${page.componentExport} } from "${moduleConfig.moduleName}";
195
+
196
+ export default function ${wrapperName}${hasDynamicParams ? '(props: any)' : '()'} {
197
+ return <${page.componentExport} ${hasDynamicParams ? '{...props}' : ''} />;
198
+ }
199
+ `;
200
+ }
201
+
202
+ fs.writeFileSync(filePath, content);
203
+ console.log(`⭐ Generated page: ${filePath}`); const entry: MenuEntry = {
204
+ label: `Module:${moduleConfig.moduleName} ${page.componentExport}`,
205
+ path: page.path,
206
+ module: moduleConfig.moduleName,
207
+ section: page.section,
208
+ };
209
+
210
+ if (page.section === "user") {
211
+ userMenu.push(entry);
212
+ } else if (page.section) {
213
+ navigation[page.section]?.push(entry);
214
+ }
215
+ }
216
+
217
+ function buildApi(moduleConfig: ModuleBuildConfig, api: ModuleApiConfig) {
218
+ const segments = api.path.replace(/^\/+/, "").split("/").filter(Boolean);
219
+ const sanitizedSegments =
220
+ segments[0] === "api" ? segments.slice(1) : segments;
221
+ const routeDir = path.join(appDirectory, "api", ...sanitizedSegments);
222
+ const filePath = path.join(routeDir, "route.ts");
223
+ ensureDirectory(routeDir);
224
+
225
+ const handler = `${moduleConfig.moduleName}/${api.entryPoint}`;
226
+ const content = `export { ${api.handlerExport} } from "${handler}";
227
+ `;
228
+
229
+ fs.writeFileSync(filePath, content);
230
+ console.log(`🔌 Generated API route: ${filePath}`);
231
+ }
232
+
233
+ function dumpNavigation() {
234
+ const navPath = path.join(appDirectory, "navigation.generated.ts");
235
+ const content = `export type MenuEntry = { label: string; path: string; module: string; section: string };
236
+
237
+ export const navigation = ${JSON.stringify(navigation, null, 2)};
238
+
239
+ export const userMenu = ${JSON.stringify(userMenu, null, 2)};
240
+ `;
241
+ fs.writeFileSync(navPath, content);
242
+ console.log(`🧭 Generated navigation metadata: ${navPath}`);
243
+ }
244
+
245
+ /**
246
+ * Génère le fichier config/menu.ts à partir des configurations de menu des modules
247
+ */
248
+ function generateMenuConfig(moduleConfigs: ModuleBuildConfig[]) {
249
+ const configDir = path.join(projectRoot, "config");
250
+ ensureDirectory(configDir);
251
+
252
+ const menuPath = path.join(configDir, "menu.ts");
253
+
254
+ // Collecter les menus de tous les modules
255
+ const publicMenus: ModuleMenuItemConfig[] = [];
256
+ const authMenus: ModuleMenuItemConfig[] = [];
257
+ const adminMenus: ModuleMenuItemConfig[] = [];
258
+ const accountMenus: ModuleMenuItemConfig[] = [];
259
+
260
+ moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
261
+ if (moduleConfig.menu) {
262
+ if (moduleConfig.menu.public) {
263
+ publicMenus.push(...moduleConfig.menu.public);
264
+ }
265
+ if (moduleConfig.menu.auth) {
266
+ authMenus.push(...moduleConfig.menu.auth);
267
+ }
268
+ if (moduleConfig.menu.admin) {
269
+ adminMenus.push(...moduleConfig.menu.admin);
270
+ }
271
+ if (moduleConfig.menu.account) {
272
+ accountMenus.push(...moduleConfig.menu.account);
273
+ }
274
+ }
275
+ });
276
+
277
+ // Trier par ordre
278
+ const sortByOrder = (a: ModuleMenuItemConfig, b: ModuleMenuItemConfig) => {
279
+ return (a.order ?? 999) - (b.order ?? 999);
280
+ };
281
+
282
+ publicMenus.sort(sortByOrder);
283
+ authMenus.sort(sortByOrder);
284
+ adminMenus.sort(sortByOrder);
285
+ accountMenus.sort(sortByOrder);
286
+
287
+ // Générer le contenu du fichier
288
+ const content = `// Auto-generated menu configuration
289
+ // Generated from module build configs
290
+
291
+ export interface MenuItem {
292
+ title: string;
293
+ description?: string;
294
+ icon?: string;
295
+ path: string;
296
+ order?: number;
297
+ }
298
+
299
+ export interface MenuConfig {
300
+ public: MenuItem[];
301
+ auth: MenuItem[];
302
+ admin: MenuItem[];
303
+ account: MenuItem[];
304
+ }
305
+
306
+ export const menuConfig: MenuConfig = {
307
+ public: ${JSON.stringify(publicMenus, null, 2)},
308
+ auth: ${JSON.stringify(authMenus, null, 2)},
309
+ admin: ${JSON.stringify(adminMenus, null, 2)},
310
+ account: ${JSON.stringify(accountMenus, null, 2)},
311
+ };
312
+ `;
313
+
314
+ fs.writeFileSync(menuPath, content);
315
+ console.log(`🍔 Generated menu configuration: ${menuPath}`);
316
+ }
317
+
318
+ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
319
+ const supabaseMigrationsDir = path.join(projectRoot, "supabase", "migrations");
320
+
321
+ // S'assurer que le dossier migrations existe
322
+ if (!fs.existsSync(supabaseMigrationsDir)) {
323
+ ensureDirectory(supabaseMigrationsDir);
324
+ }
325
+
326
+ const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
327
+
328
+ // Charger modules.json
329
+ let modulesConfig: { modules: Array<{ package: string; migrations?: string[] }> } = { modules: [] };
330
+ if (fs.existsSync(modulesJsonPath)) {
331
+ modulesConfig = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
332
+ }
333
+
334
+ moduleConfigs.forEach((moduleConfig: ModuleBuildConfig) => {
335
+ try {
336
+ const moduleName = moduleConfig.moduleName;
337
+ const copiedMigrations: string[] = [];
338
+
339
+ // Essayer plusieurs chemins possibles pour trouver le module
340
+ const possiblePaths = [
341
+ // Dans node_modules local de l'app
342
+ path.join(projectRoot, "node_modules", moduleName),
343
+ // Dans node_modules à la racine du workspace
344
+ path.join(projectRoot, "..", "..", "node_modules", moduleName),
345
+ // Directement dans packages/ (pour monorepo)
346
+ path.join(projectRoot, "..", "..", "packages", moduleName.replace("@lastbrain/", "")),
347
+ ];
348
+
349
+ let moduleBasePath: string | null = null;
350
+
351
+ for (const possiblePath of possiblePaths) {
352
+ if (fs.existsSync(possiblePath)) {
353
+ moduleBasePath = possiblePath;
354
+ break;
355
+ }
356
+ }
357
+
358
+ if (!moduleBasePath) {
359
+ return;
360
+ }
361
+
362
+ const moduleMigrationsDir = path.join(moduleBasePath, "supabase", "migrations");
363
+
364
+ if (fs.existsSync(moduleMigrationsDir)) {
365
+ const migrationFiles = fs.readdirSync(moduleMigrationsDir);
366
+
367
+ migrationFiles.forEach((file) => {
368
+ if (file.endsWith(".sql")) {
369
+ const sourcePath = path.join(moduleMigrationsDir, file);
370
+ const destPath = path.join(supabaseMigrationsDir, file);
371
+
372
+ // Copier seulement si le fichier n'existe pas déjà
373
+ if (!fs.existsSync(destPath)) {
374
+ fs.copyFileSync(sourcePath, destPath);
375
+ console.log(`📦 Copied migration: ${file} from ${moduleName}`);
376
+ }
377
+ copiedMigrations.push(file);
378
+ }
379
+ });
380
+ }
381
+
382
+ // Mettre à jour modules.json avec les migrations
383
+ if (copiedMigrations.length > 0) {
384
+ const moduleIndex = modulesConfig.modules.findIndex(
385
+ (m) => m.package === moduleName
386
+ );
387
+
388
+ if (moduleIndex >= 0) {
389
+ modulesConfig.modules[moduleIndex].migrations = copiedMigrations;
390
+ } else {
391
+ modulesConfig.modules.push({
392
+ package: moduleName,
393
+ migrations: copiedMigrations,
394
+ });
395
+ }
396
+ }
397
+ } catch (error) {
398
+ console.warn(`⚠️ Could not copy migrations from ${moduleConfig.moduleName}:`, error);
399
+ }
400
+ });
401
+
402
+ // Sauvegarder modules.json avec les migrations mises à jour
403
+ ensureDirectory(path.dirname(modulesJsonPath));
404
+ fs.writeFileSync(modulesJsonPath, JSON.stringify(modulesConfig, null, 2));
405
+ }
406
+
407
+ function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
408
+ const docsDir = path.join(appDirectory, "docs");
409
+ ensureDirectory(docsDir);
410
+
411
+ const docsPagePath = path.join(docsDir, "page.tsx");
412
+
413
+ // Charger tous les modules depuis modules.json (actifs ET inactifs)
414
+ const modulesJsonPath = path.join(projectRoot, ".lastbrain", "modules.json");
415
+ let allModules: Array<{ package: string; active: boolean; migrations?: string[] }> = [];
416
+
417
+ if (fs.existsSync(modulesJsonPath)) {
418
+ try {
419
+ const modulesData = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
420
+ allModules = modulesData.modules || [];
421
+ } catch (error) {
422
+ console.error("❌ Error reading modules.json:", error);
423
+ }
424
+ }
425
+
426
+ // Générer les imports des composants Doc de chaque module
427
+ const docImports: string[] = [];
428
+ const moduleConfigurations: string[] = [];
429
+
430
+ allModules.forEach((moduleEntry) => {
431
+ const moduleName = moduleEntry.package;
432
+ const moduleId = moduleName.replace("@lastbrain/module-", "");
433
+ const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
434
+
435
+ // Trouver la config du module pour obtenir la description
436
+ const moduleConfig = moduleConfigs.find(mc => mc.moduleName === moduleName);
437
+ const description = moduleConfig ? getModuleDescription(moduleConfig) : "Module non configuré";
438
+
439
+ // Importer le composant Doc seulement pour les modules actifs
440
+ if (moduleEntry.active) {
441
+ docImports.push(`import { ${docComponentName} } from "${moduleName}";`);
442
+ }
443
+
444
+ const config = {
445
+ id: moduleId,
446
+ name: `Module ${moduleId.charAt(0).toUpperCase() + moduleId.slice(1)}`,
447
+ description: description,
448
+ component: docComponentName,
449
+ active: moduleEntry.active,
450
+ };
451
+
452
+ moduleConfigurations.push(` {
453
+ id: "${config.id}",
454
+ name: "${config.name}",
455
+ description: "${config.description}",
456
+ ${config.active ? `content: <${config.component} />,` : 'content: null,'}
457
+ available: ${config.active},
458
+ }`);
459
+ });
460
+
461
+ const docsContent = `// Auto-generated docs page with module documentation
462
+ import React from "react";
463
+ import { DocPage } from "@lastbrain/app";
464
+ ${docImports.join('\n')}
465
+
466
+ export default function DocsPage() {
467
+ const modules = [
468
+ ${moduleConfigurations.join(',\n')}
469
+ ];
470
+
471
+ return <DocPage modules={modules} />;
472
+ }
473
+ `;
474
+
475
+ fs.writeFileSync(docsPagePath, docsContent);
476
+ console.log(`📚 Generated docs page: ${docsPagePath}`);
477
+ }
478
+
479
+ function getModuleDescription(moduleConfig: ModuleBuildConfig): string {
480
+ // Essayer de déduire la description depuis les pages ou retourner une description par défaut
481
+ if (moduleConfig.pages.length > 0) {
482
+ return `${moduleConfig.pages.length} page(s), ${moduleConfig.apis.length} API(s)`;
483
+ }
484
+ return "Module documentation";
485
+ }
486
+
487
+ export async function runModuleBuild() {
488
+ ensureDirectory(appDirectory);
489
+
490
+ const moduleConfigs = await loadModuleConfigs();
491
+ moduleConfigs.forEach((moduleConfig) => {
492
+ moduleConfig.pages.forEach((page) => buildPage(moduleConfig, page));
493
+ moduleConfig.apis.forEach((api) => buildApi(moduleConfig, api));
494
+ });
495
+
496
+ dumpNavigation();
497
+ generateMenuConfig(moduleConfigs);
498
+ generateDocsPage(moduleConfigs);
499
+ copyModuleMigrations(moduleConfigs);
500
+ }
501
+
502
+ runModuleBuild();