@lastbrain/app 0.1.34 → 0.1.37

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 (38) hide show
  1. package/README.md +23 -5
  2. package/dist/__tests__/module-registry.test.js +5 -16
  3. package/dist/scripts/init-app.d.ts.map +1 -1
  4. package/dist/scripts/init-app.js +2 -2
  5. package/dist/scripts/module-add.d.ts +0 -11
  6. package/dist/scripts/module-add.d.ts.map +1 -1
  7. package/dist/scripts/module-add.js +45 -22
  8. package/dist/scripts/module-build.d.ts.map +1 -1
  9. package/dist/scripts/module-build.js +90 -1
  10. package/dist/scripts/module-create.d.ts +23 -0
  11. package/dist/scripts/module-create.d.ts.map +1 -1
  12. package/dist/scripts/module-create.js +737 -52
  13. package/dist/scripts/module-delete.d.ts +6 -0
  14. package/dist/scripts/module-delete.d.ts.map +1 -0
  15. package/dist/scripts/module-delete.js +143 -0
  16. package/dist/scripts/module-list.d.ts.map +1 -1
  17. package/dist/scripts/module-list.js +2 -2
  18. package/dist/scripts/module-remove.d.ts.map +1 -1
  19. package/dist/scripts/module-remove.js +20 -4
  20. package/dist/styles.css +1 -1
  21. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  22. package/dist/templates/DefaultDoc.js +170 -30
  23. package/dist/templates/DocPage.d.ts.map +1 -1
  24. package/dist/templates/DocPage.js +25 -8
  25. package/dist/templates/migrations/20201010100000_app_base.sql +23 -24
  26. package/package.json +4 -4
  27. package/src/__tests__/module-registry.test.ts +5 -17
  28. package/src/scripts/db-init.ts +2 -2
  29. package/src/scripts/init-app.ts +5 -2
  30. package/src/scripts/module-add.ts +55 -23
  31. package/src/scripts/module-build.ts +109 -1
  32. package/src/scripts/module-create.ts +885 -63
  33. package/src/scripts/module-delete.ts +202 -0
  34. package/src/scripts/module-list.ts +9 -2
  35. package/src/scripts/module-remove.ts +36 -4
  36. package/src/templates/DefaultDoc.tsx +1163 -753
  37. package/src/templates/DocPage.tsx +28 -11
  38. package/src/templates/migrations/20201010100000_app_base.sql +23 -24
@@ -3,9 +3,9 @@
3
3
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  import { useState, useEffect } from "react";
5
5
  import { Card, CardBody, CardHeader, Listbox, ListboxItem, Chip, Button, Drawer, DrawerContent, DrawerHeader, DrawerBody, Snippet, } from "@lastbrain/ui";
6
- import { Menu, Home, Sparkles, Rocket, Building2, Package, Database, Palette, BookOpen, Link, Blocks, HardDrive, } from "lucide-react";
6
+ import { Menu, Home, Sparkles, Rocket, Building2, Package, Database, Palette, BookOpen, Link, Blocks, HardDrive, RotateCcw, } from "lucide-react";
7
7
  import { DefaultDocumentation } from "./DefaultDoc.js";
8
- const NavigationListbox = ({ navigationItems, selectedModule, scrollToSection, setSelectedModule, }) => (_jsx(Listbox, { "aria-label": "Navigation", selectionMode: "single", selectedKeys: selectedModule ? [selectedModule] : [], onSelectionChange: (keys) => {
8
+ const NavigationListbox = ({ navigationItems, selectedModule, scrollToSection, setSelectedModule, }) => (_jsx(Listbox, { "aria-label": "Navigation", selectionMode: "single", variant: "solid", selectedKeys: selectedModule ? [selectedModule] : [], onSelectionChange: (keys) => {
9
9
  const key = Array.from(keys)[0];
10
10
  if (key) {
11
11
  scrollToSection(key);
@@ -16,8 +16,8 @@ const NavigationListbox = ({ navigationItems, selectedModule, scrollToSection, s
16
16
  }
17
17
  }, items: navigationItems, children: (item) => {
18
18
  const IconComponent = item.icon;
19
- return (_jsx(ListboxItem, { textValue: item.name, description: item.description, color: item.color, variant: "solid", endContent: item.number && (_jsx(Chip, { size: "sm", color: "primary", children: item.number ?? 0 })), className: `${selectedModule === item.id ? "bg-default-200/40" : ""}`, startContent: _jsx(IconComponent, { size: 18, className: "shrink-0" }), children: item.name }, item.id));
20
- } }));
19
+ return (_jsx(ListboxItem, { textValue: item.name, description: item.description, color: "default", variant: "solid", endContent: item.number && (_jsx(Chip, { size: "sm", color: "primary", children: item.number ?? 0 })), startContent: _jsx(IconComponent, { size: 18, className: "shrink-0" }), children: item.name }, item.id));
20
+ } }, `listbox-${selectedModule}`));
21
21
  export function DocPage({ modules = [], defaultContent }) {
22
22
  const [selectedModule, setSelectedModule] = useState("default");
23
23
  const [isDrawerOpen, setIsDrawerOpen] = useState(false);
@@ -92,62 +92,78 @@ export function DocPage({ modules = [], defaultContent }) {
92
92
  name: "Documentation",
93
93
  description: "Accueil",
94
94
  icon: Home,
95
+ color: "default",
95
96
  },
96
97
  {
97
98
  id: "section-welcome",
98
99
  name: "Bienvenue",
99
100
  description: "",
100
101
  icon: Sparkles,
102
+ color: "default",
101
103
  },
102
104
  {
103
105
  id: "section-quickstart",
104
106
  name: "Démarrage rapide",
105
107
  description: "",
106
108
  icon: Rocket,
109
+ color: "default",
107
110
  },
108
111
  {
109
112
  id: "section-architecture",
110
113
  name: "Architecture",
111
114
  description: "",
112
115
  icon: Building2,
116
+ color: "default",
113
117
  },
114
118
  {
115
119
  id: "section-create-module",
116
120
  name: "Créer un module",
117
121
  description: "",
118
122
  icon: Package,
123
+ color: "default",
119
124
  },
120
125
  {
121
126
  id: "section-database",
122
127
  name: "Base de données",
123
128
  description: "",
124
129
  icon: Database,
130
+ color: "default",
125
131
  },
126
132
  {
127
133
  id: "section-storage",
128
134
  name: "Proxy Storage",
129
135
  description: "Gestion des fichiers",
130
136
  icon: HardDrive,
137
+ color: "default",
131
138
  },
132
139
  {
133
140
  id: "section-realtime",
134
141
  name: "Système Realtime",
135
142
  description: "Synchronisation temps réel",
136
- icon: Sparkles,
143
+ icon: RotateCcw,
144
+ color: "default",
137
145
  },
138
146
  {
139
147
  id: "section-ui",
140
148
  name: "Interface utilisateur",
141
149
  description: "",
142
150
  icon: Palette,
151
+ color: "default",
143
152
  },
144
153
  {
145
154
  id: "section-module-docs",
146
155
  name: "Doc des modules",
147
156
  description: "",
148
157
  icon: BookOpen,
158
+ color: "default",
159
+ },
160
+ {
161
+ id: "section-links",
162
+ name: "Liens utiles",
163
+ description: "",
164
+ icon: Link,
165
+ color: "default",
149
166
  },
150
- { id: "section-links", name: "Liens utiles", description: "", icon: Link },
151
167
  ...(modules.length > 0
152
168
  ? [
153
169
  {
@@ -156,6 +172,7 @@ export function DocPage({ modules = [], defaultContent }) {
156
172
  description: "",
157
173
  icon: Blocks,
158
174
  number: modules.length,
175
+ color: "default",
159
176
  },
160
177
  ]
161
178
  : []),
@@ -170,9 +187,9 @@ export function DocPage({ modules = [], defaultContent }) {
170
187
  color: "primary",
171
188
  })),
172
189
  ];
173
- return (_jsx("div", { className: "w-full pt-8 md:pt-12 pb-24 lg:pb-8", children: _jsxs("div", { className: "container mx-auto md:px-4 py-8", children: [_jsx("div", { className: "fixed w-full h-16 left-0 bottom-0 bg-background/20 backdrop-blur-lg z-50 lg:hidden p-2", children: _jsx("div", { className: "flex justify-center", children: _jsx(Button, { isIconOnly: true, variant: "solid", onPress: () => setIsDrawerOpen(true), children: _jsx(Menu, { size: 24 }) }) }) }), _jsx(Drawer, { isOpen: isDrawerOpen, onOpenChange: setIsDrawerOpen, placement: "left", children: _jsxs(DrawerContent, { children: [_jsx(DrawerHeader, { children: _jsx("h2", { className: "text-xl font-semibold", children: "Navigation" }) }), _jsx(DrawerBody, { children: _jsx(NavigationListbox, { navigationItems: navigationItems, selectedModule: selectedModule, scrollToSection: scrollToSection, setSelectedModule: setSelectedModule }) })] }) }), _jsxs("div", { className: "flex gap-8", children: [_jsx("aside", { className: "hidden lg:block w-64 shrink-0 sticky top-18 self-start", children: _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-2", children: _jsx("h2", { className: "text-xl font-semibold", children: "Navigation" }) }), _jsx(CardBody, { children: _jsx(NavigationListbox, { navigationItems: navigationItems, selectedModule: selectedModule, scrollToSection: scrollToSection, setSelectedModule: setSelectedModule }) })] }) }), _jsxs("main", { className: "flex-1 w-full min-w-0 space-y-8", children: [defaultContent ? (_jsx("div", { children: defaultContent })) : (_jsx(DefaultDocumentation, {})), modules.length > 0 && (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { id: "section-modules", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsx("h2", { className: "text-2xl font-semibold", children: "Modules disponibles" }) }), _jsxs(CardBody, { children: [_jsx("p", { className: "text-slate-600 dark:text-slate-400 mb-4", children: "Voici la liste de tous les modules disponibles dans LastBrain. Les modules actifs sont utilis\u00E9s dans votre application." }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: modules.map((module) => (_jsx(Card, { isPressable: module.available, onPress: () => module.available && scrollToSection(module.id), className: `${module.available
190
+ return (_jsx("div", { className: "w-full pt-8 md:pt-12 pb-24 lg:pb-8", children: _jsxs("div", { className: "container mx-auto md:px-4 py-8", children: [_jsx("div", { className: "fixed w-full h-16 left-0 bottom-0 bg-background/20 backdrop-blur-lg z-50 lg:hidden p-2", children: _jsx("div", { className: "flex justify-center", children: _jsx(Button, { isIconOnly: true, variant: "solid", onPress: () => setIsDrawerOpen(true), children: _jsx(Menu, { size: 24 }) }) }) }), _jsx(Drawer, { isOpen: isDrawerOpen, onOpenChange: setIsDrawerOpen, placement: "left", children: _jsxs(DrawerContent, { children: [_jsx(DrawerHeader, { children: _jsx("h2", { className: "text-xl font-semibold", children: "Navigation" }) }), _jsx(DrawerBody, { children: _jsx(NavigationListbox, { navigationItems: navigationItems, selectedModule: selectedModule, scrollToSection: scrollToSection, setSelectedModule: setSelectedModule }) })] }) }), _jsxs("div", { className: "flex gap-4", children: [_jsx("aside", { className: "hidden lg:block w-72 shrink-0 sticky top-18 self-start", children: _jsx(Card, { children: _jsx(CardBody, { children: _jsx(NavigationListbox, { navigationItems: navigationItems, selectedModule: selectedModule, scrollToSection: scrollToSection, setSelectedModule: setSelectedModule }) }) }) }), _jsxs("main", { className: "flex-1 w-full min-w-0 space-y-8", children: [defaultContent ? (_jsx("div", { children: defaultContent })) : (_jsx(DefaultDocumentation, {})), modules.length > 0 && (_jsxs("div", { className: "space-y-6", children: [_jsxs(Card, { id: "section-modules", className: "scroll-mt-32", children: [_jsx(CardHeader, { children: _jsx("h2", { className: "text-2xl font-semibold", children: "Modules disponibles" }) }), _jsxs(CardBody, { children: [_jsx("p", { className: "text-slate-600 dark:text-slate-400 mb-4", children: "Voici la liste de tous les modules disponibles dans LastBrain. Les modules actifs sont utilis\u00E9s dans votre application." }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: modules.map((module) => (_jsx(Card, { isPressable: module.available, onPress: () => module.available && scrollToSection(module.id), className: `${module.available
174
191
  ? "cursor-pointer hover:shadow-lg"
175
- : "opacity-70"} transition-shadow`, children: _jsxs(CardBody, { children: [_jsxs("div", { className: "flex items-start justify-between mb-2", children: [_jsx("h3", { className: "text-lg font-semibold", children: module.name }), _jsx(Chip, { size: "sm", color: module.available ? "success" : "warning", variant: "flat", children: module.available ? "Actif" : "Inactif" })] }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: module.description }), !module.available && (_jsxs("div", { className: "flex justify-between items-center text-xs text-default-700 mt-2", children: [_jsx("span", { children: "Pour activer : " }), _jsx(Snippet, { hideSymbol: true, color: "primary", children: `pnpm lastbrain add-module ${module.id}` })] }))] }) }, module.id))) })] })] }), modules
192
+ : "opacity-70"} transition-shadow`, children: _jsxs(CardBody, { children: [_jsxs("div", { className: "flex items-start justify-between mb-2", children: [_jsxs("h3", { className: "text-lg font-semibold flex flex-inline items-center gap-2", children: [_jsx(Blocks, { size: 20, className: "shrink-0" }), module.name] }), _jsx(Chip, { size: "sm", color: module.available ? "success" : "warning", variant: "flat", children: module.available ? "Actif" : "Inactif" })] }), _jsx("p", { className: "text-sm text-slate-600 dark:text-slate-400 mb-2", children: module.description }), !module.available && (_jsxs("div", { className: "flex justify-between items-center text-xs text-default-700 mt-2", children: [_jsx("span", { children: "Pour activer : " }), _jsx(Snippet, { hideSymbol: true, color: "primary", children: `pnpm lastbrain add-module ${module.id}` })] }))] }) }, module.id))) })] })] }), modules
176
193
  .filter((m) => m.available)
177
194
  .map((module) => (_jsx("div", { id: `module-${module.id}`, className: "scroll-mt-32", children: module.content }, module.id)))] }))] })] })] }) }));
178
195
  }
@@ -177,33 +177,32 @@ BEGIN
177
177
  ), false
178
178
  ) as is_foreign_key,
179
179
  (
180
- SELECT ccu.table_name::text
181
- FROM information_schema.table_constraints tc
182
- JOIN information_schema.key_column_usage kcu
183
- ON tc.constraint_name = kcu.constraint_name
184
- AND tc.table_schema = kcu.table_schema
185
- JOIN information_schema.constraint_column_usage ccu
186
- ON ccu.constraint_name = tc.constraint_name
187
- AND ccu.table_schema = tc.table_schema
188
- WHERE tc.constraint_type = 'FOREIGN KEY'
189
- AND tc.table_schema = 'public'
190
- AND tc.table_name = p_table_name
191
- AND kcu.column_name = c.column_name
180
+ SELECT
181
+ CASE
182
+ WHEN nsp.nspname = 'public' THEN ref_table.relname::text
183
+ ELSE nsp.nspname::text || '.' || ref_table.relname::text
184
+ END
185
+ FROM pg_constraint con
186
+ JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
187
+ JOIN pg_class tbl ON tbl.oid = con.conrelid
188
+ JOIN pg_class ref_table ON ref_table.oid = con.confrelid
189
+ JOIN pg_namespace nsp ON nsp.oid = ref_table.relnamespace
190
+ WHERE con.contype = 'f'
191
+ AND tbl.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
192
+ AND tbl.relname = p_table_name
193
+ AND att.attname = c.column_name
192
194
  LIMIT 1
193
195
  ) as foreign_table,
194
196
  (
195
- SELECT ccu.column_name::text
196
- FROM information_schema.table_constraints tc
197
- JOIN information_schema.key_column_usage kcu
198
- ON tc.constraint_name = kcu.constraint_name
199
- AND tc.table_schema = kcu.table_schema
200
- JOIN information_schema.constraint_column_usage ccu
201
- ON ccu.constraint_name = tc.constraint_name
202
- AND ccu.table_schema = tc.table_schema
203
- WHERE tc.constraint_type = 'FOREIGN KEY'
204
- AND tc.table_schema = 'public'
205
- AND tc.table_name = p_table_name
206
- AND kcu.column_name = c.column_name
197
+ SELECT ref_att.attname::text
198
+ FROM pg_constraint con
199
+ JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
200
+ JOIN pg_class tbl ON tbl.oid = con.conrelid
201
+ JOIN pg_attribute ref_att ON ref_att.attrelid = con.confrelid AND ref_att.attnum = ANY(con.confkey)
202
+ WHERE con.contype = 'f'
203
+ AND tbl.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
204
+ AND tbl.relname = p_table_name
205
+ AND att.attname = c.column_name
207
206
  LIMIT 1
208
207
  ) as foreign_column,
209
208
  pgd.description::text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/app",
3
- "version": "0.1.34",
3
+ "version": "0.1.37",
4
4
  "description": "Framework modulaire Next.js avec CLI et système de modules",
5
5
  "private": false,
6
6
  "type": "module",
@@ -33,11 +33,11 @@
33
33
  "dependencies": {
34
34
  "@lastbrain/core": "^0.1.0",
35
35
  "@lastbrain/ui": "^0.1.4",
36
- "@supabase/supabase-js": "^2.36.0",
36
+ "@supabase/supabase-js": "^2.84.0",
37
37
  "chalk": "^5.3.0",
38
- "commander": "^12.1.0",
38
+ "commander": "^14.0.2",
39
39
  "fs-extra": "^11.2.0",
40
- "inquirer": "^9.2.12",
40
+ "inquirer": "^13.0.1",
41
41
  "lucide-react": "^0.554.0",
42
42
  "next-themes": "^0.4.6",
43
43
  "react": "^19.0.0",
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { AVAILABLE_MODULES } from "../scripts/module-add";
2
+ import { AVAILABLE_MODULES } from "@lastbrain/core/config/modules";
3
3
 
4
4
  describe("Module Registry", () => {
5
5
  describe("AVAILABLE_MODULES", () => {
@@ -11,32 +11,20 @@ describe("Module Registry", () => {
11
11
  const authModule = AVAILABLE_MODULES.find((m) => m.name === "auth");
12
12
  expect(authModule).toBeDefined();
13
13
  expect(authModule?.package).toBe("@lastbrain/module-auth");
14
- expect(authModule?.hasMigrations).toBe(true);
15
14
  });
16
15
 
17
16
  it("should have ai module defined", () => {
18
17
  const aiModule = AVAILABLE_MODULES.find((m) => m.name === "ai");
19
18
  expect(aiModule).toBeDefined();
20
19
  expect(aiModule?.package).toBe("@lastbrain/module-ai");
21
- expect(aiModule?.hasMigrations).toBe(true);
22
20
  });
23
21
 
24
22
  it("should have all required properties for each module", () => {
25
23
  AVAILABLE_MODULES.forEach((module) => {
26
24
  expect(module.name).toBeDefined();
27
25
  expect(module.package).toBeDefined();
28
- expect(module.displayName).toBeDefined();
26
+ expect(module.emoji).toBeDefined();
29
27
  expect(module.description).toBeDefined();
30
- expect(typeof module.hasMigrations).toBe("boolean");
31
- });
32
- });
33
-
34
- it("should have migrations paths when hasMigrations is true", () => {
35
- AVAILABLE_MODULES.forEach((module) => {
36
- if (module.hasMigrations) {
37
- expect(module.migrationsPath).toBeDefined();
38
- expect(module.migrationsDownPath).toBeDefined();
39
- }
40
28
  });
41
29
  });
42
30
 
@@ -52,10 +40,10 @@ describe("Module Registry", () => {
52
40
  expect(packages.length).toBe(uniquePackages.size);
53
41
  });
54
42
 
55
- it("should have display names with emoji", () => {
43
+ it("should have emoji", () => {
56
44
  AVAILABLE_MODULES.forEach((module) => {
57
- // Check if displayName contains at least one emoji (basic check)
58
- expect(module.displayName).toMatch(/[\u{1F300}-\u{1F9FF}]/u);
45
+ // Check if emoji contains at least one emoji (basic check)
46
+ expect(module.emoji).toMatch(/[\u{1F300}-\u{1F9FF}]/u);
59
47
  });
60
48
  });
61
49
 
@@ -117,7 +117,7 @@ function parseEnvFile(filePath: string) {
117
117
  function ensureEnvFile(values: Record<string, string>) {
118
118
  envTargets.forEach((target) => {
119
119
  let existingVars: Record<string, string> = {};
120
-
120
+
121
121
  // Lire le fichier .env.local existant pour préserver les variables personnalisées
122
122
  if (fs.existsSync(target)) {
123
123
  try {
@@ -138,7 +138,7 @@ function ensureEnvFile(values: Record<string, string>) {
138
138
 
139
139
  // Fusionner les nouvelles valeurs avec les existantes (nouvelles valeurs prioritaires)
140
140
  const mergedValues = { ...existingVars, ...values };
141
-
141
+
142
142
  const content = Object.entries(mergedValues)
143
143
  .map(([key, value]) => `${key}=${value}`)
144
144
  .join("\n");
@@ -4,7 +4,10 @@ import { fileURLToPath } from "url";
4
4
  import chalk from "chalk";
5
5
  import inquirer from "inquirer";
6
6
  import { execSync } from "child_process";
7
- import { AVAILABLE_MODULES } from "./module-add.js";
7
+ import {
8
+ AVAILABLE_MODULES,
9
+ type ModuleMetadata,
10
+ } from "@lastbrain/core/config/modules";
8
11
 
9
12
  const __filename = fileURLToPath(import.meta.url);
10
13
  const __dirname = path.dirname(__filename);
@@ -47,7 +50,7 @@ export async function initApp(options: InitAppOptions) {
47
50
  name: "modules",
48
51
  message: "Quels modules voulez-vous installer ?",
49
52
  choices: AVAILABLE_MODULES.map((module) => ({
50
- name: `${module.displayName} - ${module.description}`,
53
+ name: `${module.emoji} ${module.name.charAt(0).toUpperCase() + module.name.slice(1)} - ${module.description}`,
51
54
  value: module.name,
52
55
  checked: false,
53
56
  })),
@@ -3,6 +3,10 @@ import path from "path";
3
3
  import chalk from "chalk";
4
4
  import { execSync } from "child_process";
5
5
  import inquirer from "inquirer";
6
+ import {
7
+ AVAILABLE_MODULES,
8
+ type ModuleMetadata,
9
+ } from "@lastbrain/core/config/modules";
6
10
 
7
11
  interface ModuleDefinition {
8
12
  name: string;
@@ -14,44 +18,35 @@ interface ModuleDefinition {
14
18
  migrationsDownPath?: string;
15
19
  }
16
20
 
17
- // Registre des modules disponibles
18
- export const AVAILABLE_MODULES: ModuleDefinition[] = [
19
- {
20
- name: "auth",
21
- package: "@lastbrain/module-auth",
22
- displayName: "🔐 Authentication",
23
- description:
24
- "Système d'authentification complet (signin, signup, sessions)",
21
+ // Convert core module metadata to local module definition
22
+ function toModuleDefinition(meta: ModuleMetadata): ModuleDefinition {
23
+ return {
24
+ name: meta.name,
25
+ package: meta.package,
26
+ displayName: `${meta.emoji} ${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`,
27
+ description: meta.description,
25
28
  hasMigrations: true,
26
29
  migrationsPath: "supabase/migrations",
27
30
  migrationsDownPath: "supabase/migrations-down",
28
- },
29
- {
30
- name: "ai",
31
- package: "@lastbrain/module-ai",
32
- displayName: "🤖 AI Generation",
33
- description: "Génération de texte et d'images avec gestion de tokens",
34
- hasMigrations: true,
35
- migrationsPath: "supabase/migrations",
36
- migrationsDownPath: "supabase/migrations-down",
37
- },
38
- // Ajouter d'autres modules ici au fur et à mesure
39
- ];
31
+ };
32
+ }
40
33
 
41
34
  export async function addModule(moduleName: string, targetDir: string) {
42
35
  console.log(chalk.blue(`\n🔧 Ajout du module: ${moduleName}\n`));
43
36
 
44
- const module = AVAILABLE_MODULES.find((m) => m.name === moduleName);
45
- if (!module) {
37
+ const moduleMeta = AVAILABLE_MODULES.find((m) => m.name === moduleName);
38
+ if (!moduleMeta) {
46
39
  console.error(
47
40
  chalk.red(`❌ Module "${moduleName}" non trouvé. Modules disponibles:`),
48
41
  );
49
42
  AVAILABLE_MODULES.forEach((m) => {
50
- console.log(chalk.gray(` - ${m.name}: ${m.description}`));
43
+ console.log(chalk.gray(` - ${m.emoji} ${m.name}: ${m.description}`));
51
44
  });
52
45
  process.exit(1);
53
46
  }
54
47
 
48
+ const module = toModuleDefinition(moduleMeta);
49
+
55
50
  // 1. Vérifier qu'on est dans un projet LastBrain
56
51
  const pkgPath = path.join(targetDir, "package.json");
57
52
  if (!fs.existsSync(pkgPath)) {
@@ -85,6 +80,43 @@ export async function addModule(moduleName: string, targetDir: string) {
85
80
  process.exit(1);
86
81
  }
87
82
 
83
+ // 3.1 Build du module ajouté (pour générer dist/ et éviter les erreurs d'import)
84
+ console.log(chalk.yellow(`\n🏗️ Build du module ${module.package}...`));
85
+ try {
86
+ // Trouver la racine du monorepo (chercher pnpm-workspace.yaml en remontant)
87
+ let workspaceRoot = targetDir;
88
+ let attempts = 0;
89
+ const maxAttempts = 6;
90
+ while (attempts < maxAttempts) {
91
+ if (fs.existsSync(path.join(workspaceRoot, "pnpm-workspace.yaml"))) {
92
+ break;
93
+ }
94
+ const parent = path.resolve(workspaceRoot, "..");
95
+ if (parent === workspaceRoot) break;
96
+ workspaceRoot = parent;
97
+ attempts++;
98
+ }
99
+ if (!fs.existsSync(path.join(workspaceRoot, "pnpm-workspace.yaml"))) {
100
+ console.log(
101
+ chalk.gray(
102
+ " ℹ️ Impossible de localiser la racine du monorepo, build ignoré",
103
+ ),
104
+ );
105
+ } else {
106
+ execSync(`pnpm --filter ${module.package} build`, {
107
+ cwd: workspaceRoot,
108
+ stdio: "inherit",
109
+ });
110
+ console.log(chalk.green(" ✓ Module compilé"));
111
+ }
112
+ } catch {
113
+ console.log(
114
+ chalk.yellow(" ⚠️ Build du module échoué, essayez: pnpm --filter"),
115
+ module.package,
116
+ "build",
117
+ );
118
+ }
119
+
88
120
  // 5. Copier les migrations du module
89
121
  const copiedMigrationFiles: string[] = [];
90
122
  if (module.hasMigrations) {
@@ -13,6 +13,10 @@ import type {
13
13
 
14
14
  // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
15
15
  const projectRoot = process.env.PROJECT_ROOT || process.cwd();
16
+ // Si on est dans une app, monter jusqu'à la racine du monorepo
17
+ const monorepoRoot = projectRoot.includes("/apps/")
18
+ ? path.resolve(projectRoot, "..", "..")
19
+ : projectRoot;
16
20
  const appDirectory = path.join(projectRoot, "app");
17
21
 
18
22
  // Créer un require dans le contexte de l'application pour résoudre les modules installés dans l'app
@@ -207,9 +211,40 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
207
211
  page.path.includes("signup") ||
208
212
  page.path.includes("reset-password"));
209
213
 
214
+ // Détecter si c'est la page de détail utilisateur qui a besoin des user tabs
215
+ const isUserDetailPage =
216
+ page.section === "admin" &&
217
+ page.path.includes("users/[id]") &&
218
+ page.componentExport === "UserPage";
219
+
210
220
  let content: string;
211
221
 
212
- if (isPublicAuthPage) {
222
+ if (isUserDetailPage) {
223
+ // Page spéciale SSR avec injection des user tabs
224
+ // On importe directement depuis app/config au lieu de passer via props
225
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD
226
+ import { UserDetailPage } from "${moduleConfig.moduleName}";
227
+
228
+ interface UserPageProps { params: Promise<{ id: string }> }
229
+
230
+ async function getModuleUserTabs() {
231
+ try {
232
+ // Depuis /app/admin/auth/users/[id]/ vers /apps/test-01/config/user-tabs
233
+ const { moduleUserTabs } = await import("../../../../../config/user-tabs");
234
+ return moduleUserTabs || [];
235
+ } catch (e) {
236
+ console.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
237
+ return [];
238
+ }
239
+ }
240
+
241
+ export default async function ${wrapperName}(props: UserPageProps) {
242
+ const { id } = await props.params;
243
+ const moduleUserTabs = await getModuleUserTabs();
244
+ return <UserDetailPage userId={id} moduleUserTabs={moduleUserTabs} />;
245
+ }
246
+ `;
247
+ } else if (isPublicAuthPage) {
213
248
  content = `// GENERATED BY LASTBRAIN MODULE BUILD
214
249
  "use client";
215
250
 
@@ -314,6 +349,8 @@ export interface MenuItem {
314
349
  icon?: string;
315
350
  path: string;
316
351
  order?: number;
352
+ shortcut?: string;
353
+ shortcutDisplay?: string;
317
354
  }
318
355
 
319
356
  export interface MenuConfig {
@@ -838,6 +875,73 @@ export default realtimeConfig;
838
875
  }
839
876
  }
840
877
 
878
+ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
879
+ try {
880
+ // Extraire les configurations user tabs des modules
881
+ const userTabsConfigs = moduleConfigs
882
+ .filter((config) => config.userTabs && config.userTabs.length > 0)
883
+ .flatMap((config) =>
884
+ config.userTabs!.map((tab) => ({
885
+ ...tab,
886
+ moduleName: config.moduleName,
887
+ })),
888
+ )
889
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
890
+
891
+ if (userTabsConfigs.length === 0) {
892
+ console.log("⏭️ No user tabs configuration found in modules");
893
+ return;
894
+ }
895
+
896
+ // Générer les imports statiques (Next/dynamic pour chaque composant)
897
+ const importsForApp = userTabsConfigs
898
+ .map(
899
+ (tab) =>
900
+ `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: true });`,
901
+ )
902
+ .join("\n");
903
+
904
+ // Générer le tableau des tabs
905
+ const tabsArray = userTabsConfigs
906
+ .map(
907
+ (tab) => ` {
908
+ key: "${tab.key}",
909
+ title: "${tab.title}",
910
+ icon: "${tab.icon || ""}",
911
+ component: ${tab.componentExport},
912
+ }`,
913
+ )
914
+ .join(",\n");
915
+
916
+ const timestamp = new Date().toISOString();
917
+ const appContent = `// GENERATED FILE - DO NOT EDIT MANUALLY\n// User tabs configuration\n// Generated at: ${timestamp}\n\n"use client";\n\nimport dynamic from "next/dynamic";\nimport type React from "react";\n\n${importsForApp}\n\nexport interface ModuleUserTab {\n key: string;\n title: string;\n icon?: string;\n component: React.ComponentType<{ userId: string }>;\n}\n\nexport const moduleUserTabs: ModuleUserTab[] = [\n${tabsArray}\n];\n\nexport default moduleUserTabs;\n`;
918
+
919
+ // Créer le fichier de configuration (uniquement dans /config)
920
+ const outputPath = path.join(projectRoot, "config", "user-tabs.ts");
921
+ const configDir = path.dirname(outputPath);
922
+
923
+ // Créer le dossier config s'il n'existe pas
924
+ if (!fs.existsSync(configDir)) {
925
+ fs.mkdirSync(configDir, { recursive: true });
926
+ }
927
+
928
+ // Écrire le fichier TypeScript
929
+ fs.writeFileSync(outputPath, appContent);
930
+
931
+ console.log(`✅ Generated user tabs configuration: ${outputPath}`);
932
+ console.log(`📊 User tabs count: ${userTabsConfigs.length}`);
933
+
934
+ // Afficher un résumé
935
+ userTabsConfigs.forEach((tab) => {
936
+ console.log(` - ${tab.title} (${tab.moduleName})`);
937
+ });
938
+
939
+ // Plus de copie vers app/config ni stub core
940
+ } catch (error) {
941
+ console.error("❌ Error generating user tabs configuration:", error);
942
+ }
943
+ }
944
+
841
945
  export async function runModuleBuild() {
842
946
  ensureDirectory(appDirectory);
843
947
 
@@ -886,4 +990,8 @@ export async function runModuleBuild() {
886
990
  // Générer la configuration realtime
887
991
  console.log("🔄 Generating realtime configuration...");
888
992
  await generateRealtimeConfig(moduleConfigs);
993
+
994
+ // Générer la configuration des user tabs
995
+ console.log("📑 Generating user tabs configuration...");
996
+ await generateUserTabsConfig(moduleConfigs);
889
997
  }