@lastbrain/app 0.1.23 → 0.1.25

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 (51) hide show
  1. package/dist/__tests__/module-registry.test.d.ts +2 -0
  2. package/dist/__tests__/module-registry.test.d.ts.map +1 -0
  3. package/dist/__tests__/module-registry.test.js +64 -0
  4. package/dist/app-shell/(admin)/layout.d.ts +3 -2
  5. package/dist/app-shell/(admin)/layout.d.ts.map +1 -1
  6. package/dist/app-shell/(admin)/layout.js +1 -1
  7. package/dist/app-shell/(auth)/layout.d.ts +3 -2
  8. package/dist/app-shell/(auth)/layout.d.ts.map +1 -1
  9. package/dist/app-shell/(auth)/layout.js +1 -1
  10. package/dist/cli.js +50 -0
  11. package/dist/layouts/AdminLayout.d.ts +3 -2
  12. package/dist/layouts/AdminLayout.d.ts.map +1 -1
  13. package/dist/layouts/AppProviders.d.ts +3 -2
  14. package/dist/layouts/AppProviders.d.ts.map +1 -1
  15. package/dist/layouts/AuthLayout.d.ts +3 -2
  16. package/dist/layouts/AuthLayout.d.ts.map +1 -1
  17. package/dist/layouts/PublicLayout.d.ts +3 -2
  18. package/dist/layouts/PublicLayout.d.ts.map +1 -1
  19. package/dist/layouts/RootLayout.d.ts +3 -2
  20. package/dist/layouts/RootLayout.d.ts.map +1 -1
  21. package/dist/scripts/db-init.js +1 -1
  22. package/dist/scripts/db-migrations-sync.js +5 -5
  23. package/dist/scripts/init-app.d.ts.map +1 -1
  24. package/dist/scripts/init-app.js +355 -23
  25. package/dist/scripts/module-add.d.ts.map +1 -1
  26. package/dist/scripts/module-add.js +1 -1
  27. package/dist/scripts/script-runner.d.ts +5 -0
  28. package/dist/scripts/script-runner.d.ts.map +1 -0
  29. package/dist/scripts/script-runner.js +25 -0
  30. package/dist/styles.css +1 -1
  31. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  32. package/dist/templates/DefaultDoc.js +16 -1
  33. package/dist/templates/DocPage.d.ts.map +1 -1
  34. package/dist/templates/DocPage.js +23 -17
  35. package/package.json +19 -20
  36. package/src/__tests__/module-registry.test.ts +74 -0
  37. package/src/app-shell/(admin)/layout.tsx +5 -3
  38. package/src/app-shell/(auth)/layout.tsx +5 -3
  39. package/src/cli.ts +50 -0
  40. package/src/layouts/AdminLayout.tsx +1 -3
  41. package/src/layouts/AppProviders.tsx +2 -4
  42. package/src/layouts/AuthLayout.tsx +1 -3
  43. package/src/layouts/PublicLayout.tsx +1 -3
  44. package/src/layouts/RootLayout.tsx +1 -2
  45. package/src/scripts/db-init.ts +2 -2
  46. package/src/scripts/db-migrations-sync.ts +1 -1
  47. package/src/scripts/init-app.ts +366 -23
  48. package/src/scripts/module-add.ts +1 -3
  49. package/src/scripts/script-runner.ts +28 -0
  50. package/src/templates/DefaultDoc.tsx +127 -0
  51. package/src/templates/DocPage.tsx +83 -49
@@ -1,7 +1,5 @@
1
1
  "use client";
2
2
 
3
- import type { PropsWithChildren } from "react";
4
-
5
- export function AdminLayout({ children }: PropsWithChildren<{}>) {
3
+ export function AdminLayout({ children }: { children: React.ReactNode }) {
6
4
  return <div className="pt-18 px-2 md:px-5">{children}</div>;
7
5
  }
@@ -1,12 +1,10 @@
1
1
  "use client";
2
2
 
3
- import type { PropsWithChildren } from "react";
4
3
  import { createContext, useContext, useMemo } from "react";
5
4
  import { getModuleConfigs } from "../modules/module-loader.js";
6
- import { Header, ToastProvider } from "@lastbrain/ui";
5
+ import { ToastProvider } from "@lastbrain/ui";
7
6
  import { useAuthSession } from "../auth/useAuthSession.js";
8
7
  import type { User } from "@supabase/supabase-js";
9
- import { useRouter } from "next/navigation.js";
10
8
 
11
9
  const ModuleContext = createContext(getModuleConfigs());
12
10
  const NotificationContext = createContext({ messages: [] as string[] });
@@ -32,7 +30,7 @@ export function useAuth() {
32
30
  return useContext(AuthContext);
33
31
  }
34
32
 
35
- export function AppProviders({ children }: PropsWithChildren<{}>) {
33
+ export function AppProviders({ children }: { children: React.ReactNode }) {
36
34
  const modules = useMemo(() => getModuleConfigs(), []);
37
35
  const notifications = useMemo(() => ({ messages: [] as string[] }), []);
38
36
  const { user, loading, isSuperAdmin } = useAuthSession();
@@ -1,7 +1,5 @@
1
1
  "use client";
2
2
 
3
- import type { PropsWithChildren } from "react";
4
-
5
- export function AuthLayout({ children }: PropsWithChildren<{}>) {
3
+ export function AuthLayout({ children }: { children: React.ReactNode }) {
6
4
  return <div className="pt-18 px-2 md:px-5 ">{children}</div>;
7
5
  }
@@ -1,7 +1,5 @@
1
1
  "use client";
2
2
 
3
- import type { PropsWithChildren } from "react";
4
-
5
- export function PublicLayout({ children }: PropsWithChildren<{}>) {
3
+ export function PublicLayout({ children }: { children: React.ReactNode }) {
6
4
  return <section className=" px-4 ">{children}</section>;
7
5
  }
@@ -1,11 +1,10 @@
1
1
  "use client";
2
2
 
3
- import type { PropsWithChildren } from "react";
4
3
  import { ThemeProvider } from "next-themes";
5
4
  import { AppProviders } from "./AppProviders.js";
6
5
 
7
6
  // Note: L'app Next.js doit importer son propre globals.css dans son layout
8
- export function RootLayout({ children }: PropsWithChildren<{}>) {
7
+ export function RootLayout({ children }: { children: React.ReactNode }) {
9
8
  return (
10
9
  <html lang="fr" suppressHydrationWarning>
11
10
  <body className="min-h-screen">
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync, execSync, execFileSync } from "node:child_process";
1
+ import { spawn, spawnSync, execSync as _execSync, execFileSync as _execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -282,7 +282,7 @@ async function main() {
282
282
  console.log("⚙️ Starting Supabase...");
283
283
  try {
284
284
  runSupabase("start");
285
- } catch (error) {
285
+ } catch (_error) {
286
286
  console.warn("⚠️ Supabase start had issues, continuing...");
287
287
  }
288
288
 
@@ -13,7 +13,7 @@ function ensureDirectory(dir: string) {
13
13
  fs.mkdirSync(dir, { recursive: true });
14
14
  }
15
15
 
16
- enum CopyStrategy {
16
+ enum _CopyStrategy {
17
17
  overwrite,
18
18
  skip,
19
19
  }
@@ -75,17 +75,20 @@ export async function initApp(options: InitAppOptions) {
75
75
  // 4. Créer les fichiers de configuration
76
76
  await createConfigFiles(targetDir, force, useHeroUI);
77
77
 
78
- // 5. Créer .gitignore et .env.local.example
78
+ // 5. Créer le système de proxy storage
79
+ await createStorageProxy(targetDir, force);
80
+
81
+ // 6. Créer .gitignore et .env.local.example
79
82
  await createGitIgnore(targetDir, force);
80
83
  await createEnvExample(targetDir, force);
81
84
 
82
- // 6. Créer la structure Supabase avec migrations
85
+ // 7. Créer la structure Supabase avec migrations
83
86
  await createSupabaseStructure(targetDir, force);
84
87
 
85
- // 7. Ajouter les scripts NPM
88
+ // 8. Ajouter les scripts NPM
86
89
  await addScriptsToPackageJson(targetDir);
87
90
 
88
- // 8. Enregistrer les modules sélectionnés
91
+ // 9. Enregistrer les modules sélectionnés
89
92
  if (withAuth || selectedModules.length > 0) {
90
93
  await saveModulesConfig(targetDir, selectedModules, withAuth);
91
94
  }
@@ -289,6 +292,7 @@ async function addDependencies(
289
292
  // Ajouter les dépendances HeroUI si nécessaire
290
293
  if (useHeroUI) {
291
294
  // Tous les packages HeroUI nécessaires (car @lastbrain/ui les ré-exporte)
295
+ requiredDeps["@heroui/system"] = "^2.4.23";
292
296
  requiredDeps["@heroui/theme"] = "^2.4.23";
293
297
  requiredDeps["@heroui/accordion"] = "^2.2.24";
294
298
  requiredDeps["@heroui/alert"] = "^2.2.27";
@@ -1050,33 +1054,26 @@ async function addScriptsToPackageJson(targetDir: string) {
1050
1054
  const targetIsInMonorepo = fs.existsSync(path.join(targetDir, "../../../packages/core/package.json")) ||
1051
1055
  fs.existsSync(path.join(targetDir, "../../packages/core/package.json"));
1052
1056
 
1053
- let scriptsPrefix = "node node_modules/@lastbrain/app/dist/scripts/";
1057
+ let scriptsPrefix = "lastbrain";
1054
1058
 
1055
- if (targetIsInMonorepo) {
1056
- // Dans un monorepo, utiliser pnpm exec pour résoudre correctement les workspace packages
1057
- scriptsPrefix = "pnpm exec lastbrain ";
1059
+ if (!targetIsInMonorepo) {
1060
+ // Hors monorepo, utiliser le chemin direct vers le CLI
1061
+ scriptsPrefix = "node node_modules/@lastbrain/app/dist/cli.js";
1058
1062
  }
1059
1063
 
1060
1064
  const scripts = {
1065
+ predev: targetIsInMonorepo
1066
+ ? "pnpm --filter @lastbrain/core build && pnpm --filter @lastbrain/ui build && pnpm --filter @lastbrain/module-auth build && pnpm --filter @lastbrain/module-ai build"
1067
+ : "echo 'No prebuild needed outside monorepo'",
1061
1068
  dev: "next dev",
1062
1069
  build: "next build",
1063
1070
  start: "next start",
1064
1071
  lint: "next lint",
1065
- lastbrain: targetIsInMonorepo
1066
- ? "pnpm exec lastbrain"
1067
- : "node node_modules/@lastbrain/app/dist/cli.js",
1068
- "build:modules": targetIsInMonorepo
1069
- ? "pnpm exec lastbrain module:build"
1070
- : "node node_modules/@lastbrain/app/dist/scripts/module-build.js",
1071
- "db:migrations:sync": targetIsInMonorepo
1072
- ? "pnpm exec lastbrain db:migrations:sync"
1073
- : "node node_modules/@lastbrain/app/dist/scripts/db-migrations-sync.js",
1074
- "db:init": targetIsInMonorepo
1075
- ? "pnpm exec lastbrain db:init"
1076
- : "node node_modules/@lastbrain/app/dist/scripts/db-init.js",
1077
- "readme:create": targetIsInMonorepo
1078
- ? "pnpm exec lastbrain readme:create"
1079
- : "node node_modules/@lastbrain/app/dist/scripts/readme-build.js",
1072
+ lastbrain: targetIsInMonorepo ? "lastbrain" : "node node_modules/@lastbrain/app/dist/cli.js",
1073
+ "build:modules": `${scriptsPrefix} module:build`,
1074
+ "db:migrations:sync": `${scriptsPrefix} db:migrations:sync`,
1075
+ "db:init": `${scriptsPrefix} db:init`,
1076
+ "readme:create": `${scriptsPrefix} readme:create`,
1080
1077
  };
1081
1078
 
1082
1079
  pkg.scripts = { ...pkg.scripts, ...scripts };
@@ -1128,3 +1125,349 @@ async function saveModulesConfig(
1128
1125
  await fs.writeJson(modulesConfigPath, { modules }, { spaces: 2 });
1129
1126
  console.log(chalk.green("✓ Configuration des modules sauvegardée"));
1130
1127
  }
1128
+
1129
+ async function createStorageProxy(targetDir: string, force: boolean) {
1130
+ console.log(chalk.yellow("\n🗂️ Création du système de proxy storage..."));
1131
+
1132
+ // Créer le dossier lib
1133
+ const libDir = path.join(targetDir, "lib");
1134
+ await fs.ensureDir(libDir);
1135
+
1136
+ // 1. Créer lib/bucket-config.ts
1137
+ const bucketConfigPath = path.join(libDir, "bucket-config.ts");
1138
+ if (!fs.existsSync(bucketConfigPath) || force) {
1139
+ const bucketConfigContent = `/**
1140
+ * Storage configuration for buckets and access control
1141
+ */
1142
+
1143
+ export interface BucketConfig {
1144
+ name: string;
1145
+ isPublic: boolean;
1146
+ description: string;
1147
+ allowedFileTypes?: string[];
1148
+ maxFileSize?: number; // in bytes
1149
+ customAccessControl?: (userId: string, filePath: string) => boolean;
1150
+ }
1151
+
1152
+ export const BUCKET_CONFIGS: Record<string, BucketConfig> = {
1153
+ avatar: {
1154
+ name: "avatar",
1155
+ isPublic: true,
1156
+ description: "User profile pictures and avatars",
1157
+ allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
1158
+ maxFileSize: 10 * 1024 * 1024, // 10MB
1159
+ },
1160
+ app: {
1161
+ name: "app",
1162
+ isPublic: false,
1163
+ description: "Private user files and documents",
1164
+ maxFileSize: 100 * 1024 * 1024, // 100MB
1165
+ customAccessControl: (userId: string, filePath: string) => {
1166
+ // Users can only access files in their own folder (app/{userId}/...)
1167
+ return filePath.startsWith(\`\${userId}/\`);
1168
+ },
1169
+ },
1170
+ // Example for future buckets:
1171
+ // public: {
1172
+ // name: "public",
1173
+ // isPublic: true,
1174
+ // description: "Publicly accessible files like logos, banners",
1175
+ // allowedFileTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
1176
+ // maxFileSize: 50 * 1024 * 1024, // 50MB
1177
+ // },
1178
+ // documents: {
1179
+ // name: "documents",
1180
+ // isPublic: false,
1181
+ // description: "Private documents requiring authentication",
1182
+ // allowedFileTypes: ["application/pdf", "application/msword", "text/plain"],
1183
+ // maxFileSize: 25 * 1024 * 1024, // 25MB
1184
+ // },
1185
+ };
1186
+
1187
+ /**
1188
+ * Get bucket configuration
1189
+ */
1190
+ export function getBucketConfig(bucketName: string): BucketConfig | null {
1191
+ return BUCKET_CONFIGS[bucketName] || null;
1192
+ }
1193
+
1194
+ /**
1195
+ * Check if bucket is public
1196
+ */
1197
+ export function isPublicBucket(bucketName: string): boolean {
1198
+ const config = getBucketConfig(bucketName);
1199
+ return config?.isPublic ?? false;
1200
+ }
1201
+
1202
+ /**
1203
+ * Check if user has access to a specific file
1204
+ */
1205
+ export function hasFileAccess(bucketName: string, userId: string, filePath: string): boolean {
1206
+ const config = getBucketConfig(bucketName);
1207
+ if (!config) return false;
1208
+
1209
+ // Public buckets are accessible to everyone
1210
+ if (config.isPublic) return true;
1211
+
1212
+ // Private buckets require authentication
1213
+ if (!userId) return false;
1214
+
1215
+ // Apply custom access control if defined
1216
+ if (config.customAccessControl) {
1217
+ return config.customAccessControl(userId, filePath);
1218
+ }
1219
+
1220
+ return true;
1221
+ }
1222
+
1223
+ /**
1224
+ * Validate file type for bucket
1225
+ */
1226
+ export function isValidFileType(bucketName: string, contentType: string): boolean {
1227
+ const config = getBucketConfig(bucketName);
1228
+ if (!config || !config.allowedFileTypes) return true;
1229
+
1230
+ return config.allowedFileTypes.includes(contentType);
1231
+ }
1232
+
1233
+ /**
1234
+ * Check if file size is within bucket limits
1235
+ */
1236
+ export function isValidFileSize(bucketName: string, fileSize: number): boolean {
1237
+ const config = getBucketConfig(bucketName);
1238
+ if (!config || !config.maxFileSize) return true;
1239
+
1240
+ return fileSize <= config.maxFileSize;
1241
+ }`;
1242
+
1243
+ await fs.writeFile(bucketConfigPath, bucketConfigContent);
1244
+ console.log(chalk.green("✓ lib/bucket-config.ts créé"));
1245
+ }
1246
+
1247
+ // 2. Créer lib/storage.ts
1248
+ const storagePath = path.join(libDir, "storage.ts");
1249
+ if (!fs.existsSync(storagePath) || force) {
1250
+ const storageContent = `/**
1251
+ * Build storage proxy URL for files in Supabase buckets
1252
+ *
1253
+ * @param bucket - The bucket name (e.g., "avatar", "app")
1254
+ * @param path - The file path within the bucket
1255
+ * @returns Proxied URL (e.g., "/api/storage/avatar/user_128_123456.webp")
1256
+ */
1257
+ export function buildStorageUrl(bucket: string, path: string): string {
1258
+ // Remove leading slash if present
1259
+ const cleanPath = path.startsWith("/") ? path.slice(1) : path;
1260
+
1261
+ // Remove bucket prefix from path if present (e.g., "avatar/file.jpg" -> "file.jpg")
1262
+ const pathWithoutBucket = cleanPath.startsWith(bucket + "/")
1263
+ ? cleanPath.slice(bucket.length + 1)
1264
+ : cleanPath;
1265
+
1266
+ return \`/api/storage/\${bucket}/\${pathWithoutBucket}\`;
1267
+ }
1268
+
1269
+ /**
1270
+ * Extract bucket and path from a storage URL
1271
+ *
1272
+ * @param url - Storage URL (can be proxied URL or Supabase public URL)
1273
+ * @returns Object with bucket and path, or null if not a valid storage URL
1274
+ */
1275
+ export function parseStorageUrl(url: string): { bucket: string; path: string } | null {
1276
+ // Handle proxy URLs like "/api/storage/avatar/file.jpg"
1277
+ const proxyMatch = url.match(/^\\/api\\/storage\\/([^\\/]+)\\/(.+)$/);
1278
+ if (proxyMatch) {
1279
+ return {
1280
+ bucket: proxyMatch[1],
1281
+ path: proxyMatch[2]
1282
+ };
1283
+ }
1284
+
1285
+ // Handle Supabase public URLs
1286
+ const supabaseMatch = url.match(/\\/storage\\/v1\\/object\\/public\\/([^\\/]+)\\/(.+)$/);
1287
+ if (supabaseMatch) {
1288
+ return {
1289
+ bucket: supabaseMatch[1],
1290
+ path: supabaseMatch[2]
1291
+ };
1292
+ }
1293
+
1294
+ return null;
1295
+ }
1296
+
1297
+ /**
1298
+ * Convert a Supabase storage path to proxy URL
1299
+ *
1300
+ * @param storagePath - Path like "avatar/file.jpg" or "app/user/file.pdf"
1301
+ * @returns Proxied URL
1302
+ */
1303
+ export function storagePathToProxyUrl(storagePath: string): string {
1304
+ const parts = storagePath.split("/");
1305
+ if (parts.length < 2) {
1306
+ throw new Error("Invalid storage path format");
1307
+ }
1308
+
1309
+ const bucket = parts[0];
1310
+ const path = parts.slice(1).join("/");
1311
+
1312
+ return buildStorageUrl(bucket, path);
1313
+ }
1314
+
1315
+ /**
1316
+ * List of public buckets that don't require authentication
1317
+ */
1318
+ export const PUBLIC_BUCKETS = ["avatar"];
1319
+
1320
+ /**
1321
+ * List of private buckets that require authentication
1322
+ */
1323
+ export const PRIVATE_BUCKETS = ["app"];
1324
+
1325
+ /**
1326
+ * Check if a bucket is public
1327
+ */
1328
+ export function isPublicBucket(bucket: string): boolean {
1329
+ return PUBLIC_BUCKETS.includes(bucket);
1330
+ }
1331
+
1332
+ /**
1333
+ * Check if a bucket is private
1334
+ */
1335
+ export function isPrivateBucket(bucket: string): boolean {
1336
+ return PRIVATE_BUCKETS.includes(bucket);
1337
+ }`;
1338
+
1339
+ await fs.writeFile(storagePath, storageContent);
1340
+ console.log(chalk.green("✓ lib/storage.ts créé"));
1341
+ }
1342
+
1343
+ // 3. Créer app/api/storage/[bucket]/[...path]/route.ts
1344
+ const apiStorageDir = path.join(targetDir, "app", "api", "storage", "[bucket]", "[...path]");
1345
+ await fs.ensureDir(apiStorageDir);
1346
+
1347
+ const routePath = path.join(apiStorageDir, "route.ts");
1348
+ if (!fs.existsSync(routePath) || force) {
1349
+ const routeContent = `import { NextRequest, NextResponse } from "next/server";
1350
+ import { getSupabaseServerClient } from "@lastbrain/core/server";
1351
+ import { getBucketConfig, hasFileAccess } from "@/lib/bucket-config";
1352
+
1353
+ /**
1354
+ * GET /api/storage/[bucket]/[...path]
1355
+ * Proxy for Supabase Storage files with clean URLs and access control
1356
+ *
1357
+ * Examples:
1358
+ * - /api/storage/avatar/user_128_123456.webp
1359
+ * - /api/storage/app/user/documents/file.pdf
1360
+ */
1361
+ export async function GET(
1362
+ request: NextRequest,
1363
+ props: { params: Promise<{ bucket: string; path: string[] }> }
1364
+ ) {
1365
+ try {
1366
+ const { bucket, path } = await props.params;
1367
+ const filePath = path.join("/");
1368
+
1369
+ // Check if bucket exists in our configuration
1370
+ const bucketConfig = getBucketConfig(bucket);
1371
+ if (!bucketConfig) {
1372
+ return new NextResponse("Bucket not allowed", { status: 403 });
1373
+ }
1374
+
1375
+ const supabase = await getSupabaseServerClient();
1376
+ let userId: string | null = null;
1377
+
1378
+ // Get user for private buckets or custom access control
1379
+ if (!bucketConfig.isPublic) {
1380
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
1381
+
1382
+ if (authError || !user) {
1383
+ return new NextResponse("Unauthorized", { status: 401 });
1384
+ }
1385
+
1386
+ userId = user.id;
1387
+ }
1388
+
1389
+ // Check file access permissions
1390
+ if (!hasFileAccess(bucket, userId || "", filePath)) {
1391
+ return new NextResponse("Forbidden - Access denied to this file", { status: 403 });
1392
+ }
1393
+
1394
+ // Get file from Supabase Storage
1395
+ const { data: file, error } = await supabase.storage
1396
+ .from(bucket)
1397
+ .download(filePath);
1398
+
1399
+ if (error) {
1400
+ console.error("Storage download error:", error);
1401
+ if (error.message.includes("not found")) {
1402
+ return new NextResponse("File not found", { status: 404 });
1403
+ }
1404
+ return new NextResponse("Storage error", { status: 500 });
1405
+ }
1406
+
1407
+ if (!file) {
1408
+ return new NextResponse("File not found", { status: 404 });
1409
+ }
1410
+
1411
+ // Convert blob to array buffer
1412
+ const arrayBuffer = await file.arrayBuffer();
1413
+
1414
+ // Determine content type from file extension
1415
+ const getContentType = (filename: string): string => {
1416
+ const ext = filename.toLowerCase().split(".").pop();
1417
+ const mimeTypes: Record<string, string> = {
1418
+ // Images
1419
+ jpg: "image/jpeg",
1420
+ jpeg: "image/jpeg",
1421
+ png: "image/png",
1422
+ gif: "image/gif",
1423
+ webp: "image/webp",
1424
+ svg: "image/svg+xml",
1425
+ // Documents
1426
+ pdf: "application/pdf",
1427
+ doc: "application/msword",
1428
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1429
+ xls: "application/vnd.ms-excel",
1430
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1431
+ // Text
1432
+ txt: "text/plain",
1433
+ csv: "text/csv",
1434
+ // Videos
1435
+ mp4: "video/mp4",
1436
+ avi: "video/x-msvideo",
1437
+ mov: "video/quicktime",
1438
+ // Audio
1439
+ mp3: "audio/mpeg",
1440
+ wav: "audio/wav",
1441
+ // Archives
1442
+ zip: "application/zip",
1443
+ rar: "application/x-rar-compressed",
1444
+ };
1445
+ return mimeTypes[ext || ""] || "application/octet-stream";
1446
+ };
1447
+
1448
+ const contentType = getContentType(filePath);
1449
+
1450
+ // Create response with proper headers
1451
+ const response = new NextResponse(arrayBuffer, {
1452
+ status: 200,
1453
+ headers: {
1454
+ "Content-Type": contentType,
1455
+ "Cache-Control": "public, max-age=31536000, immutable", // Cache for 1 year
1456
+ "Content-Length": arrayBuffer.byteLength.toString(),
1457
+ },
1458
+ });
1459
+
1460
+ return response;
1461
+
1462
+ } catch (error) {
1463
+ console.error("Storage proxy error:", error);
1464
+ return new NextResponse("Internal server error", { status: 500 });
1465
+ }
1466
+ }`;
1467
+
1468
+ await fs.writeFile(routePath, routeContent);
1469
+ console.log(chalk.green("✓ app/api/storage/[bucket]/[...path]/route.ts créé"));
1470
+ }
1471
+
1472
+ console.log(chalk.green("✓ Système de proxy storage configuré"));
1473
+ }
@@ -87,9 +87,7 @@ export async function addModule(moduleName: string, targetDir: string) {
87
87
  }
88
88
 
89
89
  // 5. Copier les migrations du module
90
- let copiedMigrationFiles: string[] = [];
91
-
92
- if (module.hasMigrations) {
90
+ const copiedMigrationFiles: string[] = []; if (module.hasMigrations) {
93
91
  console.log(chalk.yellow("\n📋 Copie des migrations du module..."));
94
92
 
95
93
  // Trouver le chemin du module installé
@@ -0,0 +1,28 @@
1
+ import { spawn } from "child_process";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Exécute un script via node en ajustant le chemin
6
+ */
7
+ export async function runScript(scriptName: string, args: string[] = []) {
8
+ return new Promise<void>((resolve, reject) => {
9
+ const scriptPath = path.join(__dirname, `${scriptName}.js`);
10
+
11
+ const child = spawn("node", [scriptPath, ...args], {
12
+ stdio: "inherit",
13
+ env: { ...process.env, PROJECT_ROOT: process.cwd() }
14
+ });
15
+
16
+ child.on("close", (code) => {
17
+ if (code === 0) {
18
+ resolve();
19
+ } else {
20
+ reject(new Error(`Script ${scriptName} exited with code ${code}`));
21
+ }
22
+ });
23
+
24
+ child.on("error", (error) => {
25
+ reject(error);
26
+ });
27
+ });
28
+ }