@lastbrain/app 0.1.39 → 0.1.40
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/cli.js +6 -1
- package/dist/scripts/init-app.js +54 -12
- package/dist/scripts/module-add.d.ts.map +1 -1
- package/dist/scripts/module-add.js +54 -6
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +102 -39
- package/dist/scripts/module-list.d.ts.map +1 -1
- package/dist/scripts/module-list.js +38 -8
- package/package.json +2 -2
- package/src/cli.ts +6 -1
- package/src/scripts/init-app.ts +56 -12
- package/src/scripts/module-add.ts +86 -7
- package/src/scripts/module-build.ts +111 -41
- package/src/scripts/module-list.ts +45 -8
package/dist/cli.js
CHANGED
|
@@ -89,8 +89,13 @@ program
|
|
|
89
89
|
program
|
|
90
90
|
.command("module:build")
|
|
91
91
|
.description("Build les configurations de modules")
|
|
92
|
-
.
|
|
92
|
+
.option("--debug", "Affiche tous les logs détaillés")
|
|
93
|
+
.action(async (options) => {
|
|
93
94
|
try {
|
|
95
|
+
// Passer le flag debug via process.argv pour que module-build puisse le lire
|
|
96
|
+
if (options.debug && !process.argv.includes("--debug")) {
|
|
97
|
+
process.argv.push("--debug");
|
|
98
|
+
}
|
|
94
99
|
const { runModuleBuild } = await import("./scripts/module-build.js");
|
|
95
100
|
await runModuleBuild();
|
|
96
101
|
}
|
package/dist/scripts/init-app.js
CHANGED
|
@@ -723,7 +723,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
|
|
723
723
|
}
|
|
724
724
|
async function createConfigFiles(targetDir, force, useHeroUI) {
|
|
725
725
|
console.log(chalk.yellow("\n⚙️ Création des fichiers de configuration..."));
|
|
726
|
-
// middleware.ts - Protection des routes /auth/* et /admin/*
|
|
726
|
+
// middleware.ts - Protection des routes /auth/*, /admin/* et /api/admin/*
|
|
727
727
|
const middlewarePath = path.join(targetDir, "middleware.ts");
|
|
728
728
|
if (!fs.existsSync(middlewarePath) || force) {
|
|
729
729
|
const middleware = `import { type NextRequest, NextResponse } from "next/server";
|
|
@@ -731,6 +731,7 @@ import { createMiddlewareClient } from "@lastbrain/core/server";
|
|
|
731
731
|
|
|
732
732
|
export async function middleware(request: NextRequest) {
|
|
733
733
|
const { pathname } = request.nextUrl;
|
|
734
|
+
const isApi = pathname.startsWith("/api/");
|
|
734
735
|
|
|
735
736
|
// Protéger les routes /auth/* (espace membre)
|
|
736
737
|
if (pathname.startsWith("/auth")) {
|
|
@@ -740,33 +741,56 @@ export async function middleware(request: NextRequest) {
|
|
|
740
741
|
data: { session },
|
|
741
742
|
} = await supabase.auth.getSession();
|
|
742
743
|
|
|
743
|
-
// Pas de session → redirection vers /signin
|
|
744
|
+
// Pas de session → nettoyage des cookies + redirection vers /auth/signin
|
|
744
745
|
if (!session) {
|
|
745
|
-
const redirectUrl = new URL("/signin", request.url);
|
|
746
|
+
const redirectUrl = new URL("/auth/signin", request.url);
|
|
746
747
|
redirectUrl.searchParams.set("redirect", pathname);
|
|
747
|
-
|
|
748
|
+
const res = NextResponse.redirect(redirectUrl);
|
|
749
|
+
res.cookies.delete("sb-access-token");
|
|
750
|
+
res.cookies.delete("sb-refresh-token");
|
|
751
|
+
res.cookies.delete("sb:token");
|
|
752
|
+
res.cookies.delete("sb:refresh-token");
|
|
753
|
+
return res;
|
|
748
754
|
}
|
|
749
755
|
|
|
750
756
|
return response;
|
|
751
757
|
} catch (error) {
|
|
752
758
|
console.error("Middleware auth error:", error);
|
|
753
|
-
|
|
759
|
+
const res = NextResponse.redirect(new URL("/auth/signin", request.url));
|
|
760
|
+
res.cookies.delete("sb-access-token");
|
|
761
|
+
res.cookies.delete("sb-refresh-token");
|
|
762
|
+
res.cookies.delete("sb:token");
|
|
763
|
+
res.cookies.delete("sb:refresh-token");
|
|
764
|
+
return res;
|
|
754
765
|
}
|
|
755
766
|
}
|
|
756
767
|
|
|
757
|
-
// Protéger les routes /admin/* (superadmin uniquement)
|
|
758
|
-
if (pathname.startsWith("/admin")) {
|
|
768
|
+
// Protéger les routes /admin/* et /api/admin/* (superadmin uniquement)
|
|
769
|
+
if (pathname.startsWith("/admin") || pathname.startsWith("/api/admin")) {
|
|
759
770
|
try {
|
|
760
771
|
const { supabase, response } = createMiddlewareClient(request);
|
|
761
772
|
const {
|
|
762
773
|
data: { session },
|
|
763
774
|
} = await supabase.auth.getSession();
|
|
764
775
|
|
|
765
|
-
// Pas de session → redirection vers /signin
|
|
776
|
+
// Pas de session → 401 JSON pour API, sinon redirection vers /auth/signin avec nettoyage cookies
|
|
766
777
|
if (!session) {
|
|
767
|
-
|
|
778
|
+
if (isApi) {
|
|
779
|
+
const res = NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
|
780
|
+
res.cookies.delete("sb-access-token");
|
|
781
|
+
res.cookies.delete("sb-refresh-token");
|
|
782
|
+
res.cookies.delete("sb:token");
|
|
783
|
+
res.cookies.delete("sb:refresh-token");
|
|
784
|
+
return res;
|
|
785
|
+
}
|
|
786
|
+
const redirectUrl = new URL("/auth/signin", request.url);
|
|
768
787
|
redirectUrl.searchParams.set("redirect", pathname);
|
|
769
|
-
|
|
788
|
+
const res = NextResponse.redirect(redirectUrl);
|
|
789
|
+
res.cookies.delete("sb-access-token");
|
|
790
|
+
res.cookies.delete("sb-refresh-token");
|
|
791
|
+
res.cookies.delete("sb:token");
|
|
792
|
+
res.cookies.delete("sb:refresh-token");
|
|
793
|
+
return res;
|
|
770
794
|
}
|
|
771
795
|
|
|
772
796
|
// Vérifier si l'utilisateur est superadmin
|
|
@@ -777,13 +801,24 @@ export async function middleware(request: NextRequest) {
|
|
|
777
801
|
|
|
778
802
|
if (error || !isSuperAdmin) {
|
|
779
803
|
console.error("Access denied: not a superadmin", error);
|
|
804
|
+
if (isApi) {
|
|
805
|
+
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
|
806
|
+
}
|
|
780
807
|
return NextResponse.redirect(new URL("/", request.url));
|
|
781
808
|
}
|
|
782
809
|
|
|
783
810
|
return response;
|
|
784
811
|
} catch (error) {
|
|
785
812
|
console.error("Middleware admin error:", error);
|
|
786
|
-
|
|
813
|
+
if (isApi) {
|
|
814
|
+
return NextResponse.json({ error: "Erreur middleware" }, { status: 500 });
|
|
815
|
+
}
|
|
816
|
+
const res = NextResponse.redirect(new URL("/", request.url));
|
|
817
|
+
res.cookies.delete("sb-access-token");
|
|
818
|
+
res.cookies.delete("sb-refresh-token");
|
|
819
|
+
res.cookies.delete("sb:token");
|
|
820
|
+
res.cookies.delete("sb:refresh-token");
|
|
821
|
+
return res;
|
|
787
822
|
}
|
|
788
823
|
}
|
|
789
824
|
|
|
@@ -804,7 +839,7 @@ export const config = {
|
|
|
804
839
|
};
|
|
805
840
|
`;
|
|
806
841
|
await fs.writeFile(middlewarePath, middleware);
|
|
807
|
-
console.log(chalk.green("✓ middleware.ts créé (protection /auth/* et /admin/*)"));
|
|
842
|
+
console.log(chalk.green("✓ middleware.ts créé (protection /auth/*, /admin/* et /api/admin/*)"));
|
|
808
843
|
}
|
|
809
844
|
// next.config.mjs
|
|
810
845
|
const nextConfigPath = path.join(targetDir, "next.config.mjs");
|
|
@@ -1290,6 +1325,13 @@ export const BUCKET_CONFIGS: Record<string, BucketConfig> = {
|
|
|
1290
1325
|
return filePath.startsWith(\`\${userId}/\`);
|
|
1291
1326
|
},
|
|
1292
1327
|
},
|
|
1328
|
+
recipes: {
|
|
1329
|
+
name: "recipes",
|
|
1330
|
+
isPublic: true,
|
|
1331
|
+
description: "Public recipe images accessible by slug",
|
|
1332
|
+
allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
|
|
1333
|
+
maxFileSize: 50 * 1024 * 1024, // 50MB
|
|
1334
|
+
},
|
|
1293
1335
|
// Example for future buckets:
|
|
1294
1336
|
// public: {
|
|
1295
1337
|
// name: "public",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-add.d.ts","sourceRoot":"","sources":["../../src/scripts/module-add.ts"],"names":[],"mappings":"AAiCA,wBAAsB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"module-add.d.ts","sourceRoot":"","sources":["../../src/scripts/module-add.ts"],"names":[],"mappings":"AAiCA,wBAAsB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,iBA6VpE"}
|
|
@@ -146,14 +146,62 @@ export async function addModule(moduleName, targetDir) {
|
|
|
146
146
|
else if (migrationAction === "push") {
|
|
147
147
|
console.log(chalk.yellow("\n⬆️ Application des nouvelles migrations..."));
|
|
148
148
|
try {
|
|
149
|
-
|
|
149
|
+
// Essayer en capturant la sortie pour diagnostiquer les erreurs fréquentes
|
|
150
|
+
execSync("supabase migration up --local", {
|
|
150
151
|
cwd: targetDir,
|
|
151
|
-
stdio: "
|
|
152
|
+
stdio: "pipe",
|
|
152
153
|
});
|
|
153
154
|
console.log(chalk.green("✓ Migrations appliquées"));
|
|
154
155
|
}
|
|
155
|
-
catch {
|
|
156
|
-
|
|
156
|
+
catch (error) {
|
|
157
|
+
const output = String(error?.stderr || error?.stdout || error?.message || "");
|
|
158
|
+
const mismatch = output.includes("Remote migration versions not found in local migrations directory");
|
|
159
|
+
if (mismatch) {
|
|
160
|
+
console.warn(chalk.yellow("⚠️ Historique des migrations désynchronisé."));
|
|
161
|
+
// Extraire TOUTES les dates de migration depuis le message d'erreur
|
|
162
|
+
const repairMatch = output.match(/supabase migration repair --status reverted ([\s\S]+?)\n/);
|
|
163
|
+
let migrationDates = [];
|
|
164
|
+
if (repairMatch && repairMatch[1]) {
|
|
165
|
+
// Extraire toutes les dates de la commande suggérée
|
|
166
|
+
migrationDates = repairMatch[1].trim().split(/\s+/);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Fallback: chercher toutes les dates dans le message
|
|
170
|
+
const allDates = output.match(/20\d{6,14}/g);
|
|
171
|
+
migrationDates = allDates ? [...new Set(allDates)] : [];
|
|
172
|
+
}
|
|
173
|
+
if (migrationDates.length === 0) {
|
|
174
|
+
console.error(chalk.red("❌ Impossible d'extraire les dates de migration"));
|
|
175
|
+
console.log(chalk.gray("\nFaites un reset complet:\n supabase db reset\n"));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Réparation automatique de l'historique
|
|
179
|
+
const datesStr = migrationDates.join(" ");
|
|
180
|
+
console.log(chalk.yellow(`\n🔧 Réparation automatique de l'historique...`));
|
|
181
|
+
console.log(chalk.gray(` Dates: ${datesStr}`));
|
|
182
|
+
try {
|
|
183
|
+
execSync(`supabase migration repair --status reverted ${datesStr} --local`, {
|
|
184
|
+
cwd: targetDir,
|
|
185
|
+
stdio: "inherit",
|
|
186
|
+
});
|
|
187
|
+
console.log(chalk.green("✓ Historique réparé"));
|
|
188
|
+
console.log(chalk.yellow("\n⬆️ Application des nouvelles migrations..."));
|
|
189
|
+
execSync("supabase migration up --local", {
|
|
190
|
+
cwd: targetDir,
|
|
191
|
+
stdio: "inherit",
|
|
192
|
+
});
|
|
193
|
+
console.log(chalk.green("✓ Migrations appliquées"));
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
console.error(chalk.red("❌ Échec de la réparation automatique"));
|
|
197
|
+
console.log(chalk.gray(`\nVous pouvez essayer manuellement:\n supabase migration repair --status reverted ${datesStr} --local\n supabase migration up --local`));
|
|
198
|
+
console.log(chalk.gray(`\nOu faire un reset complet:\n supabase db reset\n`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.error(chalk.red("❌ Erreur lors de l'application des migrations"));
|
|
204
|
+
}
|
|
157
205
|
}
|
|
158
206
|
}
|
|
159
207
|
else {
|
|
@@ -203,12 +251,12 @@ export async function addModule(moduleName, targetDir) {
|
|
|
203
251
|
// 7. Générer automatiquement les routes du module
|
|
204
252
|
console.log(chalk.yellow("🔧 Génération des routes du module..."));
|
|
205
253
|
try {
|
|
206
|
-
execSync("pnpm build:modules", { cwd: targetDir, stdio: "inherit" });
|
|
254
|
+
execSync("pnpm run build:modules", { cwd: targetDir, stdio: "inherit" });
|
|
207
255
|
console.log(chalk.green("✓ Routes du module générées"));
|
|
208
256
|
}
|
|
209
257
|
catch {
|
|
210
258
|
console.error(chalk.red("❌ Erreur lors de la génération des routes"));
|
|
211
|
-
console.error(chalk.gray("Vous pouvez les générer manuellement avec: pnpm build:modules"));
|
|
259
|
+
console.error(chalk.gray("Vous pouvez les générer manuellement avec: cd apps/<votre-app> && pnpm run build:modules"));
|
|
212
260
|
}
|
|
213
261
|
console.log(chalk.gray("\nLe serveur de développement redémarrera automatiquement.\n"));
|
|
214
262
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-build.d.ts","sourceRoot":"","sources":["../../src/scripts/module-build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"module-build.d.ts","sourceRoot":"","sources":["../../src/scripts/module-build.ts"],"names":[],"mappings":"AA69BA,wBAAsB,cAAc,kBA6EnC"}
|
|
@@ -8,6 +8,8 @@ const monorepoRoot = projectRoot.includes("/apps/")
|
|
|
8
8
|
? path.resolve(projectRoot, "..", "..")
|
|
9
9
|
: projectRoot;
|
|
10
10
|
const appDirectory = path.join(projectRoot, "app");
|
|
11
|
+
// Mode debug activé via --debug
|
|
12
|
+
const isDebugMode = process.argv.includes("--debug");
|
|
11
13
|
// Créer un require dans le contexte de l'application pour résoudre les modules installés dans l'app
|
|
12
14
|
const projectRequire = createRequire(path.join(projectRoot, "package.json"));
|
|
13
15
|
// Charger les modules depuis modules.json
|
|
@@ -121,7 +123,9 @@ function buildPage(moduleConfig, page) {
|
|
|
121
123
|
const modulePrefix = moduleConfig.moduleName
|
|
122
124
|
.replace(/^@lastbrain\/module-/, "")
|
|
123
125
|
.toLowerCase();
|
|
124
|
-
|
|
126
|
+
if (isDebugMode) {
|
|
127
|
+
console.log(`🔄 Building page for module ${modulePrefix}: ${page.path}`);
|
|
128
|
+
}
|
|
125
129
|
// Ajouter le préfixe du module au path pour les sections admin et auth,
|
|
126
130
|
// MAIS seulement quand la section ne correspond PAS au module lui-même
|
|
127
131
|
let effectivePath = page.path;
|
|
@@ -130,13 +134,17 @@ function buildPage(moduleConfig, page) {
|
|
|
130
134
|
// Éviter les doublons si le préfixe est déjà présent
|
|
131
135
|
if (!page.path.startsWith(`/${modulePrefix}/`)) {
|
|
132
136
|
effectivePath = `/${modulePrefix}${page.path}`;
|
|
133
|
-
|
|
137
|
+
if (isDebugMode) {
|
|
138
|
+
console.log(`📂 Added module prefix: ${page.path} -> ${effectivePath}`);
|
|
139
|
+
}
|
|
134
140
|
}
|
|
135
|
-
else {
|
|
141
|
+
else if (isDebugMode) {
|
|
136
142
|
console.log(`✅ Module prefix already present: ${page.path}`);
|
|
137
143
|
}
|
|
138
144
|
}
|
|
139
|
-
else if (page.section === "auth" &&
|
|
145
|
+
else if (page.section === "auth" &&
|
|
146
|
+
modulePrefix === "auth" &&
|
|
147
|
+
isDebugMode) {
|
|
140
148
|
console.log(`🏠 Auth module in auth section, no prefix needed: ${page.path}`);
|
|
141
149
|
}
|
|
142
150
|
const segments = effectivePath.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
@@ -215,7 +223,9 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: any)" : "()"
|
|
|
215
223
|
`;
|
|
216
224
|
}
|
|
217
225
|
fs.writeFileSync(filePath, content);
|
|
218
|
-
|
|
226
|
+
if (isDebugMode) {
|
|
227
|
+
console.log(`⭐ Generated page: ${filePath}`);
|
|
228
|
+
}
|
|
219
229
|
const entry = {
|
|
220
230
|
label: `Module:${moduleConfig.moduleName} ${page.componentExport}`,
|
|
221
231
|
path: effectivePath,
|
|
@@ -238,7 +248,9 @@ export const navigation = ${JSON.stringify(navigation, null, 2)};
|
|
|
238
248
|
export const userMenu = ${JSON.stringify(userMenu, null, 2)};
|
|
239
249
|
`;
|
|
240
250
|
fs.writeFileSync(navPath, content);
|
|
241
|
-
|
|
251
|
+
if (isDebugMode) {
|
|
252
|
+
console.log(`🧭 Generated navigation metadata: ${navPath}`);
|
|
253
|
+
}
|
|
242
254
|
}
|
|
243
255
|
/**
|
|
244
256
|
* Génère le fichier config/menu.ts à partir des configurations de menu des modules
|
|
@@ -305,7 +317,9 @@ export const menuConfig: MenuConfig = {
|
|
|
305
317
|
};
|
|
306
318
|
`;
|
|
307
319
|
fs.writeFileSync(menuPath, content);
|
|
308
|
-
|
|
320
|
+
if (isDebugMode) {
|
|
321
|
+
console.log(`🍔 Generated menu configuration: ${menuPath}`);
|
|
322
|
+
}
|
|
309
323
|
}
|
|
310
324
|
function copyModuleMigrations(moduleConfigs) {
|
|
311
325
|
const supabaseMigrationsDir = path.join(projectRoot, "supabase", "migrations");
|
|
@@ -441,7 +455,9 @@ ${moduleConfigurations.join(",\n")}
|
|
|
441
455
|
}
|
|
442
456
|
`;
|
|
443
457
|
fs.writeFileSync(docsPagePath, docsContent);
|
|
444
|
-
|
|
458
|
+
if (isDebugMode) {
|
|
459
|
+
console.log(`📚 Generated docs page: ${docsPagePath}`);
|
|
460
|
+
}
|
|
445
461
|
}
|
|
446
462
|
function buildGroupedApi(apis, routePath) {
|
|
447
463
|
const segments = routePath.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
@@ -468,7 +484,9 @@ function buildGroupedApi(apis, routePath) {
|
|
|
468
484
|
const contentWithMarker = `// GENERATED BY LASTBRAIN MODULE BUILD
|
|
469
485
|
${content}`;
|
|
470
486
|
fs.writeFileSync(filePath, contentWithMarker);
|
|
471
|
-
|
|
487
|
+
if (isDebugMode) {
|
|
488
|
+
console.log(`🔌 Generated API route: ${filePath}`);
|
|
489
|
+
}
|
|
472
490
|
}
|
|
473
491
|
function getModuleDescription(moduleConfig) {
|
|
474
492
|
// Essayer de déduire la description depuis les pages ou retourner une description par défaut
|
|
@@ -523,7 +541,9 @@ function cleanGeneratedFiles() {
|
|
|
523
541
|
try {
|
|
524
542
|
if (!isProtected(itemPath) && fs.readdirSync(itemPath).length === 0) {
|
|
525
543
|
fs.rmdirSync(itemPath);
|
|
526
|
-
|
|
544
|
+
if (isDebugMode) {
|
|
545
|
+
console.log(`📁 Removed empty directory: ${itemPath}`);
|
|
546
|
+
}
|
|
527
547
|
}
|
|
528
548
|
}
|
|
529
549
|
catch {
|
|
@@ -541,14 +561,16 @@ function cleanGeneratedFiles() {
|
|
|
541
561
|
(content.includes("export {") &&
|
|
542
562
|
content.includes('} from "@lastbrain/module-'))) {
|
|
543
563
|
fs.unlinkSync(itemPath);
|
|
544
|
-
|
|
564
|
+
if (isDebugMode) {
|
|
565
|
+
console.log(`🗑️ Removed: ${itemPath}`);
|
|
566
|
+
}
|
|
545
567
|
}
|
|
546
568
|
}
|
|
547
569
|
catch {
|
|
548
570
|
// Ignorer les erreurs de lecture/suppression
|
|
549
571
|
}
|
|
550
572
|
}
|
|
551
|
-
else {
|
|
573
|
+
else if (isDebugMode) {
|
|
552
574
|
console.log(`🔒 Protected file skipped: ${itemPath}`);
|
|
553
575
|
}
|
|
554
576
|
}
|
|
@@ -568,16 +590,22 @@ function cleanGeneratedFiles() {
|
|
|
568
590
|
const filePath = path.join(appDirectory, file);
|
|
569
591
|
if (fs.existsSync(filePath)) {
|
|
570
592
|
fs.unlinkSync(filePath);
|
|
571
|
-
|
|
593
|
+
if (isDebugMode) {
|
|
594
|
+
console.log(`🗑️ Removed: ${filePath}`);
|
|
595
|
+
}
|
|
572
596
|
}
|
|
573
597
|
});
|
|
574
|
-
|
|
598
|
+
if (isDebugMode) {
|
|
599
|
+
console.log("🧹 Cleanup completed");
|
|
600
|
+
}
|
|
575
601
|
}
|
|
576
602
|
function generateAppAside() {
|
|
577
603
|
const targetPath = path.join(appDirectory, "components", "AppAside.tsx");
|
|
578
604
|
// Ne pas écraser si le fichier existe déjà
|
|
579
605
|
if (fs.existsSync(targetPath)) {
|
|
580
|
-
|
|
606
|
+
if (isDebugMode) {
|
|
607
|
+
console.log(`⏭️ AppAside already exists, skipping: ${targetPath}`);
|
|
608
|
+
}
|
|
581
609
|
return;
|
|
582
610
|
}
|
|
583
611
|
const templateContent = `"use client";
|
|
@@ -606,7 +634,9 @@ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
|
|
|
606
634
|
try {
|
|
607
635
|
ensureDirectory(path.dirname(targetPath));
|
|
608
636
|
fs.writeFileSync(targetPath, templateContent, "utf-8");
|
|
609
|
-
|
|
637
|
+
if (isDebugMode) {
|
|
638
|
+
console.log(`✅ Generated AppAside component: ${targetPath}`);
|
|
639
|
+
}
|
|
610
640
|
}
|
|
611
641
|
catch (error) {
|
|
612
642
|
console.error(`❌ Error generating AppAside component: ${error}`);
|
|
@@ -633,13 +663,15 @@ export default function SectionLayout({
|
|
|
633
663
|
try {
|
|
634
664
|
ensureDirectory(path.dirname(authLayoutPath));
|
|
635
665
|
fs.writeFileSync(authLayoutPath, authLayoutContent, "utf-8");
|
|
636
|
-
|
|
666
|
+
if (isDebugMode) {
|
|
667
|
+
console.log(`✅ Generated auth layout with sidebar: ${authLayoutPath}`);
|
|
668
|
+
}
|
|
637
669
|
}
|
|
638
670
|
catch (error) {
|
|
639
671
|
console.error(`❌ Error generating auth layout: ${error}`);
|
|
640
672
|
}
|
|
641
673
|
}
|
|
642
|
-
else {
|
|
674
|
+
else if (isDebugMode) {
|
|
643
675
|
console.log(`⏭️ Auth layout already exists, skipping: ${authLayoutPath}`);
|
|
644
676
|
}
|
|
645
677
|
// Générer layout admin avec sidebar
|
|
@@ -662,13 +694,15 @@ export default function AdminLayout({
|
|
|
662
694
|
try {
|
|
663
695
|
ensureDirectory(path.dirname(adminLayoutPath));
|
|
664
696
|
fs.writeFileSync(adminLayoutPath, adminLayoutContent, "utf-8");
|
|
665
|
-
|
|
697
|
+
if (isDebugMode) {
|
|
698
|
+
console.log(`✅ Generated admin layout with sidebar: ${adminLayoutPath}`);
|
|
699
|
+
}
|
|
666
700
|
}
|
|
667
701
|
catch (error) {
|
|
668
702
|
console.error(`❌ Error generating admin layout: ${error}`);
|
|
669
703
|
}
|
|
670
704
|
}
|
|
671
|
-
else {
|
|
705
|
+
else if (isDebugMode) {
|
|
672
706
|
console.log(`⏭️ Admin layout already exists, skipping: ${adminLayoutPath}`);
|
|
673
707
|
}
|
|
674
708
|
}
|
|
@@ -707,15 +741,17 @@ export default realtimeConfig;
|
|
|
707
741
|
}
|
|
708
742
|
// Écrire le fichier TypeScript
|
|
709
743
|
fs.writeFileSync(outputPath, content);
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
744
|
+
if (isDebugMode) {
|
|
745
|
+
console.log(`✅ Generated realtime configuration: ${outputPath}`);
|
|
746
|
+
console.log(`📊 Modules with realtime: ${realtimeConfigs.length}`);
|
|
747
|
+
// Afficher un résumé
|
|
748
|
+
realtimeConfigs.forEach((module) => {
|
|
749
|
+
console.log(` - ${module.moduleId}: ${module.tables.length} table(s)`);
|
|
750
|
+
module.tables.forEach((table) => {
|
|
751
|
+
console.log(` • ${table.schema}.${table.table} (${table.event})`);
|
|
752
|
+
});
|
|
717
753
|
});
|
|
718
|
-
}
|
|
754
|
+
}
|
|
719
755
|
}
|
|
720
756
|
catch (error) {
|
|
721
757
|
console.error("❌ Error generating realtime configuration:", error);
|
|
@@ -759,12 +795,14 @@ async function generateUserTabsConfig(moduleConfigs) {
|
|
|
759
795
|
}
|
|
760
796
|
// Écrire le fichier TypeScript
|
|
761
797
|
fs.writeFileSync(outputPath, appContent);
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
798
|
+
if (isDebugMode) {
|
|
799
|
+
console.log(`✅ Generated user tabs configuration: ${outputPath}`);
|
|
800
|
+
console.log(`📊 User tabs count: ${userTabsConfigs.length}`);
|
|
801
|
+
// Afficher un résumé
|
|
802
|
+
userTabsConfigs.forEach((tab) => {
|
|
803
|
+
console.log(` - ${tab.title} (${tab.moduleName})`);
|
|
804
|
+
});
|
|
805
|
+
}
|
|
768
806
|
// Plus de copie vers app/config ni stub core
|
|
769
807
|
}
|
|
770
808
|
catch (error) {
|
|
@@ -774,16 +812,28 @@ async function generateUserTabsConfig(moduleConfigs) {
|
|
|
774
812
|
export async function runModuleBuild() {
|
|
775
813
|
ensureDirectory(appDirectory);
|
|
776
814
|
// Nettoyer les fichiers générés précédemment
|
|
777
|
-
|
|
815
|
+
if (isDebugMode) {
|
|
816
|
+
console.log("🧹 Cleaning previously generated files...");
|
|
817
|
+
}
|
|
778
818
|
cleanGeneratedFiles();
|
|
779
819
|
const moduleConfigs = await loadModuleConfigs();
|
|
780
|
-
|
|
820
|
+
if (isDebugMode) {
|
|
821
|
+
console.log(`🔍 Loaded ${moduleConfigs.length} module configurations`);
|
|
822
|
+
}
|
|
781
823
|
// Générer les pages
|
|
824
|
+
if (isDebugMode) {
|
|
825
|
+
console.log("\n📝 Generating pages...");
|
|
826
|
+
}
|
|
782
827
|
moduleConfigs.forEach((moduleConfig) => {
|
|
783
|
-
|
|
828
|
+
if (isDebugMode) {
|
|
829
|
+
console.log(`📦 Processing module: ${moduleConfig.moduleName} with ${moduleConfig.pages.length} pages`);
|
|
830
|
+
}
|
|
784
831
|
moduleConfig.pages.forEach((page) => buildPage(moduleConfig, page));
|
|
785
832
|
});
|
|
786
833
|
// Grouper les APIs par chemin pour éviter les écrasements de fichier
|
|
834
|
+
if (isDebugMode) {
|
|
835
|
+
console.log("\n🔌 Generating API routes...");
|
|
836
|
+
}
|
|
787
837
|
const apisByPath = new Map();
|
|
788
838
|
moduleConfigs.forEach((moduleConfig) => {
|
|
789
839
|
moduleConfig.apis.forEach((api) => {
|
|
@@ -804,9 +854,22 @@ export async function runModuleBuild() {
|
|
|
804
854
|
generateLayouts();
|
|
805
855
|
copyModuleMigrations(moduleConfigs);
|
|
806
856
|
// Générer la configuration realtime
|
|
807
|
-
|
|
857
|
+
if (isDebugMode) {
|
|
858
|
+
console.log("🔄 Generating realtime configuration...");
|
|
859
|
+
}
|
|
808
860
|
await generateRealtimeConfig(moduleConfigs);
|
|
809
861
|
// Générer la configuration des user tabs
|
|
810
|
-
|
|
862
|
+
if (isDebugMode) {
|
|
863
|
+
console.log("📑 Generating user tabs configuration...");
|
|
864
|
+
}
|
|
811
865
|
await generateUserTabsConfig(moduleConfigs);
|
|
866
|
+
// Message de succès final
|
|
867
|
+
if (!isDebugMode) {
|
|
868
|
+
console.log("\n✅ Module build completed successfully!");
|
|
869
|
+
console.log(`📦 ${moduleConfigs.length} module(s) processed`);
|
|
870
|
+
console.log("💡 Use --debug flag for detailed logs\n");
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
console.log("\n✅ Module build completed (debug mode)\n");
|
|
874
|
+
}
|
|
812
875
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-list.d.ts","sourceRoot":"","sources":["../../src/scripts/module-list.ts"],"names":[],"mappings":"AAQA,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"module-list.d.ts","sourceRoot":"","sources":["../../src/scripts/module-list.ts"],"names":[],"mappings":"AAQA,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,iBAwElD"}
|
|
@@ -6,26 +6,56 @@ export async function listModules(targetDir) {
|
|
|
6
6
|
console.log(chalk.blue("\n📦 Modules disponibles:\n"));
|
|
7
7
|
// Lire la config des modules installés
|
|
8
8
|
const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
|
|
9
|
-
let
|
|
9
|
+
let installedPackages = [];
|
|
10
|
+
let inactivePackages = [];
|
|
10
11
|
if (fs.existsSync(modulesConfigPath)) {
|
|
11
12
|
const modulesConfig = await fs.readJson(modulesConfigPath);
|
|
12
|
-
|
|
13
|
+
const modules = modulesConfig.modules || [];
|
|
14
|
+
// Supporte deux formats:
|
|
15
|
+
// - Ancien: string[] des noms de packages
|
|
16
|
+
// - Actuel: { package: string; active?: boolean; migrations?: string[] }[]
|
|
17
|
+
if (Array.isArray(modules)) {
|
|
18
|
+
if (modules.length > 0 && typeof modules[0] === "string") {
|
|
19
|
+
installedPackages = modules;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const entries = modules;
|
|
23
|
+
installedPackages = entries
|
|
24
|
+
.filter((m) => m && m.package && m.active !== false)
|
|
25
|
+
.map((m) => m.package);
|
|
26
|
+
inactivePackages = entries
|
|
27
|
+
.filter((m) => m && m.package && m.active === false)
|
|
28
|
+
.map((m) => m.package);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
13
31
|
}
|
|
14
32
|
// Afficher tous les modules disponibles
|
|
15
33
|
AVAILABLE_MODULES.forEach((module) => {
|
|
16
|
-
const isInstalled =
|
|
34
|
+
const isInstalled = installedPackages.includes(module.package);
|
|
35
|
+
const isInactive = inactivePackages.includes(module.package);
|
|
17
36
|
const status = isInstalled
|
|
18
37
|
? chalk.green("✓ installé")
|
|
19
|
-
:
|
|
38
|
+
: isInactive
|
|
39
|
+
? chalk.yellow("⤳ inactif")
|
|
40
|
+
: chalk.gray(" disponible");
|
|
20
41
|
console.log(chalk.bold(`${module.emoji} ${module.name.charAt(0).toUpperCase() + module.name.slice(1)}`));
|
|
21
42
|
console.log(chalk.gray(` Nom: ${module.name}`));
|
|
22
43
|
console.log(chalk.gray(` Package: ${module.package}`));
|
|
23
44
|
console.log(chalk.gray(` Description: ${module.description}`));
|
|
24
45
|
console.log(` Statut: ${status}`);
|
|
46
|
+
// Afficher la commande appropriée selon le statut
|
|
47
|
+
if (isInstalled) {
|
|
48
|
+
console.log(chalk.cyan(` → pnpm lastbrain remove-module ${module.name}`));
|
|
49
|
+
}
|
|
50
|
+
else if (isInactive || !isInstalled) {
|
|
51
|
+
console.log(chalk.cyan(` → pnpm lastbrain add-module ${module.name}`));
|
|
52
|
+
}
|
|
25
53
|
console.log();
|
|
26
54
|
});
|
|
27
|
-
console.log(chalk.gray("
|
|
28
|
-
console.log(chalk.
|
|
29
|
-
console.log(chalk.
|
|
30
|
-
|
|
55
|
+
console.log(chalk.gray("─".repeat(50)));
|
|
56
|
+
console.log(chalk.gray("\nCommandes disponibles:"));
|
|
57
|
+
console.log(chalk.cyan(" pnpm lastbrain add-module <nom> ") +
|
|
58
|
+
chalk.gray("- Ajouter un module"));
|
|
59
|
+
console.log(chalk.cyan(" pnpm lastbrain remove-module <nom> ") +
|
|
60
|
+
chalk.gray("- Supprimer un module\n"));
|
|
31
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lastbrain/app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
4
4
|
"description": "Framework modulaire Next.js avec CLI et système de modules",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@tailwindcss/postcss": "^4.1.17",
|
|
52
52
|
"@types/fs-extra": "^11.0.4",
|
|
53
53
|
"@types/inquirer": "^9.0.9",
|
|
54
|
-
"@types/node": "^
|
|
54
|
+
"@types/node": "^24.10.1",
|
|
55
55
|
"inquirer": "^13.0.1",
|
|
56
56
|
"postcss": "^8.4.0",
|
|
57
57
|
"postcss-cli": "^11.0.0",
|
package/src/cli.ts
CHANGED
|
@@ -97,8 +97,13 @@ program
|
|
|
97
97
|
program
|
|
98
98
|
.command("module:build")
|
|
99
99
|
.description("Build les configurations de modules")
|
|
100
|
-
.
|
|
100
|
+
.option("--debug", "Affiche tous les logs détaillés")
|
|
101
|
+
.action(async (options) => {
|
|
101
102
|
try {
|
|
103
|
+
// Passer le flag debug via process.argv pour que module-build puisse le lire
|
|
104
|
+
if (options.debug && !process.argv.includes("--debug")) {
|
|
105
|
+
process.argv.push("--debug");
|
|
106
|
+
}
|
|
102
107
|
const { runModuleBuild } = await import("./scripts/module-build.js");
|
|
103
108
|
await runModuleBuild();
|
|
104
109
|
} catch (error) {
|
package/src/scripts/init-app.ts
CHANGED
|
@@ -903,7 +903,7 @@ async function createConfigFiles(
|
|
|
903
903
|
) {
|
|
904
904
|
console.log(chalk.yellow("\n⚙️ Création des fichiers de configuration..."));
|
|
905
905
|
|
|
906
|
-
// middleware.ts - Protection des routes /auth/* et /admin/*
|
|
906
|
+
// middleware.ts - Protection des routes /auth/*, /admin/* et /api/admin/*
|
|
907
907
|
const middlewarePath = path.join(targetDir, "middleware.ts");
|
|
908
908
|
if (!fs.existsSync(middlewarePath) || force) {
|
|
909
909
|
const middleware = `import { type NextRequest, NextResponse } from "next/server";
|
|
@@ -911,6 +911,7 @@ import { createMiddlewareClient } from "@lastbrain/core/server";
|
|
|
911
911
|
|
|
912
912
|
export async function middleware(request: NextRequest) {
|
|
913
913
|
const { pathname } = request.nextUrl;
|
|
914
|
+
const isApi = pathname.startsWith("/api/");
|
|
914
915
|
|
|
915
916
|
// Protéger les routes /auth/* (espace membre)
|
|
916
917
|
if (pathname.startsWith("/auth")) {
|
|
@@ -920,33 +921,56 @@ export async function middleware(request: NextRequest) {
|
|
|
920
921
|
data: { session },
|
|
921
922
|
} = await supabase.auth.getSession();
|
|
922
923
|
|
|
923
|
-
// Pas de session → redirection vers /signin
|
|
924
|
+
// Pas de session → nettoyage des cookies + redirection vers /auth/signin
|
|
924
925
|
if (!session) {
|
|
925
|
-
const redirectUrl = new URL("/signin", request.url);
|
|
926
|
+
const redirectUrl = new URL("/auth/signin", request.url);
|
|
926
927
|
redirectUrl.searchParams.set("redirect", pathname);
|
|
927
|
-
|
|
928
|
+
const res = NextResponse.redirect(redirectUrl);
|
|
929
|
+
res.cookies.delete("sb-access-token");
|
|
930
|
+
res.cookies.delete("sb-refresh-token");
|
|
931
|
+
res.cookies.delete("sb:token");
|
|
932
|
+
res.cookies.delete("sb:refresh-token");
|
|
933
|
+
return res;
|
|
928
934
|
}
|
|
929
935
|
|
|
930
936
|
return response;
|
|
931
937
|
} catch (error) {
|
|
932
938
|
console.error("Middleware auth error:", error);
|
|
933
|
-
|
|
939
|
+
const res = NextResponse.redirect(new URL("/auth/signin", request.url));
|
|
940
|
+
res.cookies.delete("sb-access-token");
|
|
941
|
+
res.cookies.delete("sb-refresh-token");
|
|
942
|
+
res.cookies.delete("sb:token");
|
|
943
|
+
res.cookies.delete("sb:refresh-token");
|
|
944
|
+
return res;
|
|
934
945
|
}
|
|
935
946
|
}
|
|
936
947
|
|
|
937
|
-
// Protéger les routes /admin/* (superadmin uniquement)
|
|
938
|
-
if (pathname.startsWith("/admin")) {
|
|
948
|
+
// Protéger les routes /admin/* et /api/admin/* (superadmin uniquement)
|
|
949
|
+
if (pathname.startsWith("/admin") || pathname.startsWith("/api/admin")) {
|
|
939
950
|
try {
|
|
940
951
|
const { supabase, response } = createMiddlewareClient(request);
|
|
941
952
|
const {
|
|
942
953
|
data: { session },
|
|
943
954
|
} = await supabase.auth.getSession();
|
|
944
955
|
|
|
945
|
-
// Pas de session → redirection vers /signin
|
|
956
|
+
// Pas de session → 401 JSON pour API, sinon redirection vers /auth/signin avec nettoyage cookies
|
|
946
957
|
if (!session) {
|
|
947
|
-
|
|
958
|
+
if (isApi) {
|
|
959
|
+
const res = NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
|
960
|
+
res.cookies.delete("sb-access-token");
|
|
961
|
+
res.cookies.delete("sb-refresh-token");
|
|
962
|
+
res.cookies.delete("sb:token");
|
|
963
|
+
res.cookies.delete("sb:refresh-token");
|
|
964
|
+
return res;
|
|
965
|
+
}
|
|
966
|
+
const redirectUrl = new URL("/auth/signin", request.url);
|
|
948
967
|
redirectUrl.searchParams.set("redirect", pathname);
|
|
949
|
-
|
|
968
|
+
const res = NextResponse.redirect(redirectUrl);
|
|
969
|
+
res.cookies.delete("sb-access-token");
|
|
970
|
+
res.cookies.delete("sb-refresh-token");
|
|
971
|
+
res.cookies.delete("sb:token");
|
|
972
|
+
res.cookies.delete("sb:refresh-token");
|
|
973
|
+
return res;
|
|
950
974
|
}
|
|
951
975
|
|
|
952
976
|
// Vérifier si l'utilisateur est superadmin
|
|
@@ -957,13 +981,24 @@ export async function middleware(request: NextRequest) {
|
|
|
957
981
|
|
|
958
982
|
if (error || !isSuperAdmin) {
|
|
959
983
|
console.error("Access denied: not a superadmin", error);
|
|
984
|
+
if (isApi) {
|
|
985
|
+
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
|
986
|
+
}
|
|
960
987
|
return NextResponse.redirect(new URL("/", request.url));
|
|
961
988
|
}
|
|
962
989
|
|
|
963
990
|
return response;
|
|
964
991
|
} catch (error) {
|
|
965
992
|
console.error("Middleware admin error:", error);
|
|
966
|
-
|
|
993
|
+
if (isApi) {
|
|
994
|
+
return NextResponse.json({ error: "Erreur middleware" }, { status: 500 });
|
|
995
|
+
}
|
|
996
|
+
const res = NextResponse.redirect(new URL("/", request.url));
|
|
997
|
+
res.cookies.delete("sb-access-token");
|
|
998
|
+
res.cookies.delete("sb-refresh-token");
|
|
999
|
+
res.cookies.delete("sb:token");
|
|
1000
|
+
res.cookies.delete("sb:refresh-token");
|
|
1001
|
+
return res;
|
|
967
1002
|
}
|
|
968
1003
|
}
|
|
969
1004
|
|
|
@@ -985,7 +1020,9 @@ export const config = {
|
|
|
985
1020
|
`;
|
|
986
1021
|
await fs.writeFile(middlewarePath, middleware);
|
|
987
1022
|
console.log(
|
|
988
|
-
chalk.green(
|
|
1023
|
+
chalk.green(
|
|
1024
|
+
"✓ middleware.ts créé (protection /auth/*, /admin/* et /api/admin/*)",
|
|
1025
|
+
),
|
|
989
1026
|
);
|
|
990
1027
|
}
|
|
991
1028
|
|
|
@@ -1561,6 +1598,13 @@ export const BUCKET_CONFIGS: Record<string, BucketConfig> = {
|
|
|
1561
1598
|
return filePath.startsWith(\`\${userId}/\`);
|
|
1562
1599
|
},
|
|
1563
1600
|
},
|
|
1601
|
+
recipes: {
|
|
1602
|
+
name: "recipes",
|
|
1603
|
+
isPublic: true,
|
|
1604
|
+
description: "Public recipe images accessible by slug",
|
|
1605
|
+
allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
|
|
1606
|
+
maxFileSize: 50 * 1024 * 1024, // 50MB
|
|
1607
|
+
},
|
|
1564
1608
|
// Example for future buckets:
|
|
1565
1609
|
// public: {
|
|
1566
1610
|
// name: "public",
|
|
@@ -205,15 +205,94 @@ export async function addModule(moduleName: string, targetDir: string) {
|
|
|
205
205
|
chalk.yellow("\n⬆️ Application des nouvelles migrations..."),
|
|
206
206
|
);
|
|
207
207
|
try {
|
|
208
|
-
|
|
208
|
+
// Essayer en capturant la sortie pour diagnostiquer les erreurs fréquentes
|
|
209
|
+
execSync("supabase migration up --local", {
|
|
209
210
|
cwd: targetDir,
|
|
210
|
-
stdio: "
|
|
211
|
+
stdio: "pipe",
|
|
211
212
|
});
|
|
212
213
|
console.log(chalk.green("✓ Migrations appliquées"));
|
|
213
|
-
} catch {
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
} catch (error: any) {
|
|
215
|
+
const output = String(
|
|
216
|
+
error?.stderr || error?.stdout || error?.message || "",
|
|
217
|
+
);
|
|
218
|
+
const mismatch = output.includes(
|
|
219
|
+
"Remote migration versions not found in local migrations directory",
|
|
216
220
|
);
|
|
221
|
+
|
|
222
|
+
if (mismatch) {
|
|
223
|
+
console.warn(
|
|
224
|
+
chalk.yellow("⚠️ Historique des migrations désynchronisé."),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Extraire TOUTES les dates de migration depuis le message d'erreur
|
|
228
|
+
const repairMatch = output.match(
|
|
229
|
+
/supabase migration repair --status reverted ([\s\S]+?)\n/,
|
|
230
|
+
);
|
|
231
|
+
let migrationDates: string[] = [];
|
|
232
|
+
|
|
233
|
+
if (repairMatch && repairMatch[1]) {
|
|
234
|
+
// Extraire toutes les dates de la commande suggérée
|
|
235
|
+
migrationDates = repairMatch[1].trim().split(/\s+/);
|
|
236
|
+
} else {
|
|
237
|
+
// Fallback: chercher toutes les dates dans le message
|
|
238
|
+
const allDates = output.match(/20\d{6,14}/g);
|
|
239
|
+
migrationDates = allDates ? [...new Set(allDates)] : [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (migrationDates.length === 0) {
|
|
243
|
+
console.error(
|
|
244
|
+
chalk.red("❌ Impossible d'extraire les dates de migration"),
|
|
245
|
+
);
|
|
246
|
+
console.log(
|
|
247
|
+
chalk.gray("\nFaites un reset complet:\n supabase db reset\n"),
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
// Réparation automatique de l'historique
|
|
251
|
+
const datesStr = migrationDates.join(" ");
|
|
252
|
+
console.log(
|
|
253
|
+
chalk.yellow(`\n🔧 Réparation automatique de l'historique...`),
|
|
254
|
+
);
|
|
255
|
+
console.log(chalk.gray(` Dates: ${datesStr}`));
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
execSync(
|
|
259
|
+
`supabase migration repair --status reverted ${datesStr} --local`,
|
|
260
|
+
{
|
|
261
|
+
cwd: targetDir,
|
|
262
|
+
stdio: "inherit",
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
console.log(chalk.green("✓ Historique réparé"));
|
|
266
|
+
|
|
267
|
+
console.log(
|
|
268
|
+
chalk.yellow("\n⬆️ Application des nouvelles migrations..."),
|
|
269
|
+
);
|
|
270
|
+
execSync("supabase migration up --local", {
|
|
271
|
+
cwd: targetDir,
|
|
272
|
+
stdio: "inherit",
|
|
273
|
+
});
|
|
274
|
+
console.log(chalk.green("✓ Migrations appliquées"));
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.error(
|
|
277
|
+
chalk.red("❌ Échec de la réparation automatique"),
|
|
278
|
+
);
|
|
279
|
+
console.log(
|
|
280
|
+
chalk.gray(
|
|
281
|
+
`\nVous pouvez essayer manuellement:\n supabase migration repair --status reverted ${datesStr} --local\n supabase migration up --local`,
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
console.log(
|
|
285
|
+
chalk.gray(
|
|
286
|
+
`\nOu faire un reset complet:\n supabase db reset\n`,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
console.error(
|
|
293
|
+
chalk.red("❌ Erreur lors de l'application des migrations"),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
217
296
|
}
|
|
218
297
|
} else {
|
|
219
298
|
console.log(
|
|
@@ -287,13 +366,13 @@ export async function addModule(moduleName: string, targetDir: string) {
|
|
|
287
366
|
// 7. Générer automatiquement les routes du module
|
|
288
367
|
console.log(chalk.yellow("🔧 Génération des routes du module..."));
|
|
289
368
|
try {
|
|
290
|
-
execSync("pnpm build:modules", { cwd: targetDir, stdio: "inherit" });
|
|
369
|
+
execSync("pnpm run build:modules", { cwd: targetDir, stdio: "inherit" });
|
|
291
370
|
console.log(chalk.green("✓ Routes du module générées"));
|
|
292
371
|
} catch {
|
|
293
372
|
console.error(chalk.red("❌ Erreur lors de la génération des routes"));
|
|
294
373
|
console.error(
|
|
295
374
|
chalk.gray(
|
|
296
|
-
"Vous pouvez les générer manuellement avec: pnpm build:modules",
|
|
375
|
+
"Vous pouvez les générer manuellement avec: cd apps/<votre-app> && pnpm run build:modules",
|
|
297
376
|
),
|
|
298
377
|
);
|
|
299
378
|
}
|
|
@@ -19,6 +19,9 @@ const monorepoRoot = projectRoot.includes("/apps/")
|
|
|
19
19
|
: projectRoot;
|
|
20
20
|
const appDirectory = path.join(projectRoot, "app");
|
|
21
21
|
|
|
22
|
+
// Mode debug activé via --debug
|
|
23
|
+
const isDebugMode = process.argv.includes("--debug");
|
|
24
|
+
|
|
22
25
|
// Créer un require dans le contexte de l'application pour résoudre les modules installés dans l'app
|
|
23
26
|
const projectRequire = createRequire(path.join(projectRoot, "package.json"));
|
|
24
27
|
|
|
@@ -161,7 +164,9 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
|
|
|
161
164
|
.replace(/^@lastbrain\/module-/, "")
|
|
162
165
|
.toLowerCase();
|
|
163
166
|
|
|
164
|
-
|
|
167
|
+
if (isDebugMode) {
|
|
168
|
+
console.log(`🔄 Building page for module ${modulePrefix}: ${page.path}`);
|
|
169
|
+
}
|
|
165
170
|
|
|
166
171
|
// Ajouter le préfixe du module au path pour les sections admin et auth,
|
|
167
172
|
// MAIS seulement quand la section ne correspond PAS au module lui-même
|
|
@@ -173,11 +178,17 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
|
|
|
173
178
|
// Éviter les doublons si le préfixe est déjà présent
|
|
174
179
|
if (!page.path.startsWith(`/${modulePrefix}/`)) {
|
|
175
180
|
effectivePath = `/${modulePrefix}${page.path}`;
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
if (isDebugMode) {
|
|
182
|
+
console.log(`📂 Added module prefix: ${page.path} -> ${effectivePath}`);
|
|
183
|
+
}
|
|
184
|
+
} else if (isDebugMode) {
|
|
178
185
|
console.log(`✅ Module prefix already present: ${page.path}`);
|
|
179
186
|
}
|
|
180
|
-
} else if (
|
|
187
|
+
} else if (
|
|
188
|
+
page.section === "auth" &&
|
|
189
|
+
modulePrefix === "auth" &&
|
|
190
|
+
isDebugMode
|
|
191
|
+
) {
|
|
181
192
|
console.log(
|
|
182
193
|
`🏠 Auth module in auth section, no prefix needed: ${page.path}`,
|
|
183
194
|
);
|
|
@@ -270,7 +281,9 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: any)" : "()"
|
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
fs.writeFileSync(filePath, content);
|
|
273
|
-
|
|
284
|
+
if (isDebugMode) {
|
|
285
|
+
console.log(`⭐ Generated page: ${filePath}`);
|
|
286
|
+
}
|
|
274
287
|
const entry: MenuEntry = {
|
|
275
288
|
label: `Module:${moduleConfig.moduleName} ${page.componentExport}`,
|
|
276
289
|
path: effectivePath,
|
|
@@ -294,7 +307,9 @@ export const navigation = ${JSON.stringify(navigation, null, 2)};
|
|
|
294
307
|
export const userMenu = ${JSON.stringify(userMenu, null, 2)};
|
|
295
308
|
`;
|
|
296
309
|
fs.writeFileSync(navPath, content);
|
|
297
|
-
|
|
310
|
+
if (isDebugMode) {
|
|
311
|
+
console.log(`🧭 Generated navigation metadata: ${navPath}`);
|
|
312
|
+
}
|
|
298
313
|
}
|
|
299
314
|
|
|
300
315
|
/**
|
|
@@ -369,7 +384,9 @@ export const menuConfig: MenuConfig = {
|
|
|
369
384
|
`;
|
|
370
385
|
|
|
371
386
|
fs.writeFileSync(menuPath, content);
|
|
372
|
-
|
|
387
|
+
if (isDebugMode) {
|
|
388
|
+
console.log(`🍔 Generated menu configuration: ${menuPath}`);
|
|
389
|
+
}
|
|
373
390
|
}
|
|
374
391
|
|
|
375
392
|
function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
|
|
@@ -557,7 +574,9 @@ ${moduleConfigurations.join(",\n")}
|
|
|
557
574
|
`;
|
|
558
575
|
|
|
559
576
|
fs.writeFileSync(docsPagePath, docsContent);
|
|
560
|
-
|
|
577
|
+
if (isDebugMode) {
|
|
578
|
+
console.log(`📚 Generated docs page: ${docsPagePath}`);
|
|
579
|
+
}
|
|
561
580
|
}
|
|
562
581
|
|
|
563
582
|
function buildGroupedApi(
|
|
@@ -595,7 +614,9 @@ function buildGroupedApi(
|
|
|
595
614
|
${content}`;
|
|
596
615
|
|
|
597
616
|
fs.writeFileSync(filePath, contentWithMarker);
|
|
598
|
-
|
|
617
|
+
if (isDebugMode) {
|
|
618
|
+
console.log(`🔌 Generated API route: ${filePath}`);
|
|
619
|
+
}
|
|
599
620
|
}
|
|
600
621
|
|
|
601
622
|
function getModuleDescription(moduleConfig: ModuleBuildConfig): string {
|
|
@@ -663,7 +684,9 @@ function cleanGeneratedFiles() {
|
|
|
663
684
|
try {
|
|
664
685
|
if (!isProtected(itemPath) && fs.readdirSync(itemPath).length === 0) {
|
|
665
686
|
fs.rmdirSync(itemPath);
|
|
666
|
-
|
|
687
|
+
if (isDebugMode) {
|
|
688
|
+
console.log(`📁 Removed empty directory: ${itemPath}`);
|
|
689
|
+
}
|
|
667
690
|
}
|
|
668
691
|
} catch {
|
|
669
692
|
// Ignorer les erreurs de suppression de fichiers
|
|
@@ -681,12 +704,14 @@ function cleanGeneratedFiles() {
|
|
|
681
704
|
content.includes('} from "@lastbrain/module-'))
|
|
682
705
|
) {
|
|
683
706
|
fs.unlinkSync(itemPath);
|
|
684
|
-
|
|
707
|
+
if (isDebugMode) {
|
|
708
|
+
console.log(`🗑️ Removed: ${itemPath}`);
|
|
709
|
+
}
|
|
685
710
|
}
|
|
686
711
|
} catch {
|
|
687
712
|
// Ignorer les erreurs de lecture/suppression
|
|
688
713
|
}
|
|
689
|
-
} else {
|
|
714
|
+
} else if (isDebugMode) {
|
|
690
715
|
console.log(`🔒 Protected file skipped: ${itemPath}`);
|
|
691
716
|
}
|
|
692
717
|
}
|
|
@@ -708,11 +733,15 @@ function cleanGeneratedFiles() {
|
|
|
708
733
|
const filePath = path.join(appDirectory, file);
|
|
709
734
|
if (fs.existsSync(filePath)) {
|
|
710
735
|
fs.unlinkSync(filePath);
|
|
711
|
-
|
|
736
|
+
if (isDebugMode) {
|
|
737
|
+
console.log(`🗑️ Removed: ${filePath}`);
|
|
738
|
+
}
|
|
712
739
|
}
|
|
713
740
|
});
|
|
714
741
|
|
|
715
|
-
|
|
742
|
+
if (isDebugMode) {
|
|
743
|
+
console.log("🧹 Cleanup completed");
|
|
744
|
+
}
|
|
716
745
|
}
|
|
717
746
|
|
|
718
747
|
function generateAppAside() {
|
|
@@ -720,7 +749,9 @@ function generateAppAside() {
|
|
|
720
749
|
|
|
721
750
|
// Ne pas écraser si le fichier existe déjà
|
|
722
751
|
if (fs.existsSync(targetPath)) {
|
|
723
|
-
|
|
752
|
+
if (isDebugMode) {
|
|
753
|
+
console.log(`⏭️ AppAside already exists, skipping: ${targetPath}`);
|
|
754
|
+
}
|
|
724
755
|
return;
|
|
725
756
|
}
|
|
726
757
|
|
|
@@ -751,7 +782,9 @@ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
|
|
|
751
782
|
try {
|
|
752
783
|
ensureDirectory(path.dirname(targetPath));
|
|
753
784
|
fs.writeFileSync(targetPath, templateContent, "utf-8");
|
|
754
|
-
|
|
785
|
+
if (isDebugMode) {
|
|
786
|
+
console.log(`✅ Generated AppAside component: ${targetPath}`);
|
|
787
|
+
}
|
|
755
788
|
} catch (error) {
|
|
756
789
|
console.error(`❌ Error generating AppAside component: ${error}`);
|
|
757
790
|
}
|
|
@@ -779,11 +812,13 @@ export default function SectionLayout({
|
|
|
779
812
|
try {
|
|
780
813
|
ensureDirectory(path.dirname(authLayoutPath));
|
|
781
814
|
fs.writeFileSync(authLayoutPath, authLayoutContent, "utf-8");
|
|
782
|
-
|
|
815
|
+
if (isDebugMode) {
|
|
816
|
+
console.log(`✅ Generated auth layout with sidebar: ${authLayoutPath}`);
|
|
817
|
+
}
|
|
783
818
|
} catch (error) {
|
|
784
819
|
console.error(`❌ Error generating auth layout: ${error}`);
|
|
785
820
|
}
|
|
786
|
-
} else {
|
|
821
|
+
} else if (isDebugMode) {
|
|
787
822
|
console.log(`⏭️ Auth layout already exists, skipping: ${authLayoutPath}`);
|
|
788
823
|
}
|
|
789
824
|
|
|
@@ -808,11 +843,15 @@ export default function AdminLayout({
|
|
|
808
843
|
try {
|
|
809
844
|
ensureDirectory(path.dirname(adminLayoutPath));
|
|
810
845
|
fs.writeFileSync(adminLayoutPath, adminLayoutContent, "utf-8");
|
|
811
|
-
|
|
846
|
+
if (isDebugMode) {
|
|
847
|
+
console.log(
|
|
848
|
+
`✅ Generated admin layout with sidebar: ${adminLayoutPath}`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
812
851
|
} catch (error) {
|
|
813
852
|
console.error(`❌ Error generating admin layout: ${error}`);
|
|
814
853
|
}
|
|
815
|
-
} else {
|
|
854
|
+
} else if (isDebugMode) {
|
|
816
855
|
console.log(`⏭️ Admin layout already exists, skipping: ${adminLayoutPath}`);
|
|
817
856
|
}
|
|
818
857
|
}
|
|
@@ -860,16 +899,20 @@ export default realtimeConfig;
|
|
|
860
899
|
// Écrire le fichier TypeScript
|
|
861
900
|
fs.writeFileSync(outputPath, content);
|
|
862
901
|
|
|
863
|
-
|
|
864
|
-
|
|
902
|
+
if (isDebugMode) {
|
|
903
|
+
console.log(`✅ Generated realtime configuration: ${outputPath}`);
|
|
904
|
+
console.log(`📊 Modules with realtime: ${realtimeConfigs.length}`);
|
|
865
905
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
906
|
+
// Afficher un résumé
|
|
907
|
+
realtimeConfigs.forEach((module) => {
|
|
908
|
+
console.log(
|
|
909
|
+
` - ${module.moduleId}: ${module.tables.length} table(s)`,
|
|
910
|
+
);
|
|
911
|
+
module.tables.forEach((table) => {
|
|
912
|
+
console.log(` • ${table.schema}.${table.table} (${table.event})`);
|
|
913
|
+
});
|
|
871
914
|
});
|
|
872
|
-
}
|
|
915
|
+
}
|
|
873
916
|
} catch (error) {
|
|
874
917
|
console.error("❌ Error generating realtime configuration:", error);
|
|
875
918
|
}
|
|
@@ -928,13 +971,15 @@ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
|
|
|
928
971
|
// Écrire le fichier TypeScript
|
|
929
972
|
fs.writeFileSync(outputPath, appContent);
|
|
930
973
|
|
|
931
|
-
|
|
932
|
-
|
|
974
|
+
if (isDebugMode) {
|
|
975
|
+
console.log(`✅ Generated user tabs configuration: ${outputPath}`);
|
|
976
|
+
console.log(`📊 User tabs count: ${userTabsConfigs.length}`);
|
|
933
977
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
978
|
+
// Afficher un résumé
|
|
979
|
+
userTabsConfigs.forEach((tab) => {
|
|
980
|
+
console.log(` - ${tab.title} (${tab.moduleName})`);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
938
983
|
|
|
939
984
|
// Plus de copie vers app/config ni stub core
|
|
940
985
|
} catch (error) {
|
|
@@ -946,21 +991,33 @@ export async function runModuleBuild() {
|
|
|
946
991
|
ensureDirectory(appDirectory);
|
|
947
992
|
|
|
948
993
|
// Nettoyer les fichiers générés précédemment
|
|
949
|
-
|
|
994
|
+
if (isDebugMode) {
|
|
995
|
+
console.log("🧹 Cleaning previously generated files...");
|
|
996
|
+
}
|
|
950
997
|
cleanGeneratedFiles();
|
|
951
998
|
|
|
952
999
|
const moduleConfigs = await loadModuleConfigs();
|
|
953
|
-
|
|
1000
|
+
if (isDebugMode) {
|
|
1001
|
+
console.log(`🔍 Loaded ${moduleConfigs.length} module configurations`);
|
|
1002
|
+
}
|
|
954
1003
|
|
|
955
1004
|
// Générer les pages
|
|
1005
|
+
if (isDebugMode) {
|
|
1006
|
+
console.log("\n📝 Generating pages...");
|
|
1007
|
+
}
|
|
956
1008
|
moduleConfigs.forEach((moduleConfig) => {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1009
|
+
if (isDebugMode) {
|
|
1010
|
+
console.log(
|
|
1011
|
+
`📦 Processing module: ${moduleConfig.moduleName} with ${moduleConfig.pages.length} pages`,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
960
1014
|
moduleConfig.pages.forEach((page) => buildPage(moduleConfig, page));
|
|
961
1015
|
});
|
|
962
1016
|
|
|
963
1017
|
// Grouper les APIs par chemin pour éviter les écrasements de fichier
|
|
1018
|
+
if (isDebugMode) {
|
|
1019
|
+
console.log("\n🔌 Generating API routes...");
|
|
1020
|
+
}
|
|
964
1021
|
const apisByPath = new Map<
|
|
965
1022
|
string,
|
|
966
1023
|
Array<{ moduleConfig: ModuleBuildConfig; api: ModuleApiConfig }>
|
|
@@ -988,10 +1045,23 @@ export async function runModuleBuild() {
|
|
|
988
1045
|
copyModuleMigrations(moduleConfigs);
|
|
989
1046
|
|
|
990
1047
|
// Générer la configuration realtime
|
|
991
|
-
|
|
1048
|
+
if (isDebugMode) {
|
|
1049
|
+
console.log("🔄 Generating realtime configuration...");
|
|
1050
|
+
}
|
|
992
1051
|
await generateRealtimeConfig(moduleConfigs);
|
|
993
1052
|
|
|
994
1053
|
// Générer la configuration des user tabs
|
|
995
|
-
|
|
1054
|
+
if (isDebugMode) {
|
|
1055
|
+
console.log("📑 Generating user tabs configuration...");
|
|
1056
|
+
}
|
|
996
1057
|
await generateUserTabsConfig(moduleConfigs);
|
|
1058
|
+
|
|
1059
|
+
// Message de succès final
|
|
1060
|
+
if (!isDebugMode) {
|
|
1061
|
+
console.log("\n✅ Module build completed successfully!");
|
|
1062
|
+
console.log(`📦 ${moduleConfigs.length} module(s) processed`);
|
|
1063
|
+
console.log("💡 Use --debug flag for detailed logs\n");
|
|
1064
|
+
} else {
|
|
1065
|
+
console.log("\n✅ Module build completed (debug mode)\n");
|
|
1066
|
+
}
|
|
997
1067
|
}
|
|
@@ -11,19 +11,40 @@ export async function listModules(targetDir: string) {
|
|
|
11
11
|
|
|
12
12
|
// Lire la config des modules installés
|
|
13
13
|
const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
|
|
14
|
-
let
|
|
14
|
+
let installedPackages: string[] = [];
|
|
15
|
+
let inactivePackages: string[] = [];
|
|
15
16
|
|
|
16
17
|
if (fs.existsSync(modulesConfigPath)) {
|
|
17
18
|
const modulesConfig = await fs.readJson(modulesConfigPath);
|
|
18
|
-
|
|
19
|
+
const modules = modulesConfig.modules || [];
|
|
20
|
+
|
|
21
|
+
// Supporte deux formats:
|
|
22
|
+
// - Ancien: string[] des noms de packages
|
|
23
|
+
// - Actuel: { package: string; active?: boolean; migrations?: string[] }[]
|
|
24
|
+
if (Array.isArray(modules)) {
|
|
25
|
+
if (modules.length > 0 && typeof modules[0] === "string") {
|
|
26
|
+
installedPackages = modules as string[];
|
|
27
|
+
} else {
|
|
28
|
+
const entries = modules as Array<{ package: string; active?: boolean }>;
|
|
29
|
+
installedPackages = entries
|
|
30
|
+
.filter((m) => m && m.package && m.active !== false)
|
|
31
|
+
.map((m) => m.package);
|
|
32
|
+
inactivePackages = entries
|
|
33
|
+
.filter((m) => m && m.package && m.active === false)
|
|
34
|
+
.map((m) => m.package);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
// Afficher tous les modules disponibles
|
|
22
40
|
AVAILABLE_MODULES.forEach((module) => {
|
|
23
|
-
const isInstalled =
|
|
41
|
+
const isInstalled = installedPackages.includes(module.package);
|
|
42
|
+
const isInactive = inactivePackages.includes(module.package);
|
|
24
43
|
const status = isInstalled
|
|
25
44
|
? chalk.green("✓ installé")
|
|
26
|
-
:
|
|
45
|
+
: isInactive
|
|
46
|
+
? chalk.yellow("⤳ inactif")
|
|
47
|
+
: chalk.gray(" disponible");
|
|
27
48
|
|
|
28
49
|
console.log(
|
|
29
50
|
chalk.bold(
|
|
@@ -34,11 +55,27 @@ export async function listModules(targetDir: string) {
|
|
|
34
55
|
console.log(chalk.gray(` Package: ${module.package}`));
|
|
35
56
|
console.log(chalk.gray(` Description: ${module.description}`));
|
|
36
57
|
console.log(` Statut: ${status}`);
|
|
58
|
+
|
|
59
|
+
// Afficher la commande appropriée selon le statut
|
|
60
|
+
if (isInstalled) {
|
|
61
|
+
console.log(
|
|
62
|
+
chalk.cyan(` → pnpm lastbrain remove-module ${module.name}`),
|
|
63
|
+
);
|
|
64
|
+
} else if (isInactive || !isInstalled) {
|
|
65
|
+
console.log(chalk.cyan(` → pnpm lastbrain add-module ${module.name}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
console.log();
|
|
38
69
|
});
|
|
39
70
|
|
|
40
|
-
console.log(chalk.gray("
|
|
41
|
-
console.log(chalk.
|
|
42
|
-
console.log(
|
|
43
|
-
|
|
71
|
+
console.log(chalk.gray("─".repeat(50)));
|
|
72
|
+
console.log(chalk.gray("\nCommandes disponibles:"));
|
|
73
|
+
console.log(
|
|
74
|
+
chalk.cyan(" pnpm lastbrain add-module <nom> ") +
|
|
75
|
+
chalk.gray("- Ajouter un module"),
|
|
76
|
+
);
|
|
77
|
+
console.log(
|
|
78
|
+
chalk.cyan(" pnpm lastbrain remove-module <nom> ") +
|
|
79
|
+
chalk.gray("- Supprimer un module\n"),
|
|
80
|
+
);
|
|
44
81
|
}
|