@lastbrain/app 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/scripts/init-app.js +7 -10
  2. package/package.json +3 -2
  3. package/src/app-shell/(admin)/layout.tsx +13 -0
  4. package/src/app-shell/(auth)/layout.tsx +13 -0
  5. package/src/app-shell/(public)/page.tsx +11 -0
  6. package/src/app-shell/layout.tsx +5 -0
  7. package/src/app-shell/not-found.tsx +28 -0
  8. package/src/auth/authHelpers.ts +24 -0
  9. package/src/auth/useAuthSession.ts +54 -0
  10. package/src/cli.ts +96 -0
  11. package/src/index.ts +21 -0
  12. package/src/layouts/AdminLayout.tsx +7 -0
  13. package/src/layouts/AppProviders.tsx +61 -0
  14. package/src/layouts/AuthLayout.tsx +7 -0
  15. package/src/layouts/PublicLayout.tsx +7 -0
  16. package/src/layouts/RootLayout.tsx +27 -0
  17. package/src/modules/module-loader.ts +14 -0
  18. package/src/scripts/README.md +262 -0
  19. package/src/scripts/db-init.ts +338 -0
  20. package/src/scripts/db-migrations-sync.ts +86 -0
  21. package/src/scripts/dev-sync.ts +218 -0
  22. package/src/scripts/init-app.ts +1076 -0
  23. package/src/scripts/module-add.ts +242 -0
  24. package/src/scripts/module-build.ts +502 -0
  25. package/src/scripts/module-create.ts +809 -0
  26. package/src/scripts/module-list.ts +37 -0
  27. package/src/scripts/module-remove.ts +367 -0
  28. package/src/scripts/readme-build.ts +60 -0
  29. package/src/styles.css +3 -0
  30. package/src/templates/AuthGuidePage.tsx +68 -0
  31. package/src/templates/DefaultDoc.tsx +462 -0
  32. package/src/templates/DocPage.tsx +381 -0
  33. package/src/templates/DocsPageWithModules.tsx +22 -0
  34. package/src/templates/MigrationsGuidePage.tsx +61 -0
  35. package/src/templates/ModuleGuidePage.tsx +71 -0
  36. package/src/templates/SimpleDocPage.tsx +587 -0
  37. package/src/templates/SimpleHomePage.tsx +385 -0
  38. package/src/templates/env.example/.env.example +6 -0
  39. package/src/templates/migrations/20201010100000_app_base.sql +228 -0
@@ -0,0 +1,262 @@
1
+ # Scripts LastBrain
2
+
3
+ Ce répertoire contient tous les scripts utilitaires pour gérer les applications et modules LastBrain.
4
+
5
+ ## Scripts disponibles
6
+
7
+ ### `init-app.ts`
8
+
9
+ Initialise une nouvelle application Next.js LastBrain avec toute la structure nécessaire.
10
+
11
+ **Usage :**
12
+
13
+ ```bash
14
+ pnpm lastbrain init [directory] [options]
15
+ ```
16
+
17
+ **Options :**
18
+
19
+ - `--force` : Écrase les fichiers existants
20
+ - `--heroui` : Utilise HeroUI au lieu de NextUI
21
+ - `--with-auth` : Inclut le module d'authentification
22
+ - `--no-interactive` : Mode non-interactif
23
+
24
+ ### `module-create.ts`
25
+
26
+ Crée un nouveau module dans `packages/module-{nom}` avec toute la structure nécessaire.
27
+
28
+ **Usage :**
29
+
30
+ ```bash
31
+ pnpm lastbrain create-module
32
+ ```
33
+
34
+ **Fonctionnalités :**
35
+
36
+ - Génération interactive du module
37
+ - Création des pages (public/auth/admin)
38
+ - Création des routes API CRUD
39
+ - Génération des migrations SQL
40
+ - Configuration complète (package.json, tsconfig.json, build.config.ts)
41
+
42
+ **Voir :** [MODULE_CREATION.md](../../../../docs/MODULE_CREATION.md)
43
+
44
+ ### `module-add.ts`
45
+
46
+ Ajoute un module existant à l'application courante.
47
+
48
+ **Usage :**
49
+
50
+ ```bash
51
+ pnpm lastbrain add-module <nom-du-module>
52
+ ```
53
+
54
+ **Exemple :**
55
+
56
+ ```bash
57
+ pnpm lastbrain add-module auth
58
+ ```
59
+
60
+ **Actions effectuées :**
61
+
62
+ - Ajout de la dépendance au package.json
63
+ - Installation via pnpm
64
+ - Copie des migrations du module
65
+ - Application des migrations (avec choix reset/push/skip)
66
+ - Enregistrement dans `.lastbrain/modules.json`
67
+
68
+ ### `module-remove.ts`
69
+
70
+ Supprime un module de l'application courante.
71
+
72
+ **Usage :**
73
+
74
+ ```bash
75
+ pnpm lastbrain remove-module <nom-du-module>
76
+ ```
77
+
78
+ **Actions effectuées :**
79
+
80
+ - Suppression de la dépendance du package.json
81
+ - Suppression du module de `.lastbrain/modules.json`
82
+ - Nettoyage via pnpm
83
+
84
+ **Note :** Les migrations ne sont pas automatiquement inversées.
85
+
86
+ ### `module-list.ts`
87
+
88
+ Liste tous les modules disponibles et ceux installés dans l'application courante.
89
+
90
+ **Usage :**
91
+
92
+ ```bash
93
+ pnpm lastbrain list-modules
94
+ ```
95
+
96
+ **Affiche :**
97
+
98
+ - ✅ Modules installés
99
+ - 📦 Modules disponibles
100
+
101
+ ### `module-build.ts`
102
+
103
+ Génère automatiquement les routes et pages de l'application à partir des modules installés.
104
+
105
+ **Usage :**
106
+
107
+ ```bash
108
+ pnpm build:modules
109
+ ```
110
+
111
+ **Actions effectuées :**
112
+
113
+ - Lecture des `build.config.ts` de tous les modules installés
114
+ - Génération de `app/navigation.generated.ts`
115
+ - Création des pages dans les sections appropriées (public/auth/admin)
116
+ - Création des routes API
117
+
118
+ **Note :** Utilisé par le script `dev:sync` en mode watch.
119
+
120
+ ### `db-init.ts`
121
+
122
+ Initialise la base de données Supabase locale.
123
+
124
+ **Usage :**
125
+
126
+ ```bash
127
+ pnpm db:init
128
+ ```
129
+
130
+ **Actions effectuées :**
131
+
132
+ - Démarre Supabase local (si pas déjà démarré)
133
+ - Génère le fichier `.env.local` avec les clés Supabase
134
+ - Applique les migrations de base
135
+ - Reset de la base de données
136
+
137
+ ### `db-migrations-sync.ts`
138
+
139
+ Synchronise les migrations de tous les modules avec la base de données.
140
+
141
+ **Usage :**
142
+
143
+ ```bash
144
+ pnpm db:migrations:sync
145
+ ```
146
+
147
+ **Actions effectuées :**
148
+
149
+ - Collecte toutes les migrations des modules installés
150
+ - Copie dans `supabase/migrations/`
151
+ - Applique les migrations (via `supabase db push` ou `supabase db reset`)
152
+
153
+ ### `dev-sync.ts`
154
+
155
+ Lance le mode développement avec synchronisation automatique des modules.
156
+
157
+ **Usage :**
158
+
159
+ ```bash
160
+ pnpm dev:sync
161
+ ```
162
+
163
+ **Actions effectuées :**
164
+
165
+ - Watch des changements dans les modules
166
+ - Régénération automatique des routes
167
+ - Synchronisation des migrations
168
+ - Redémarrage automatique du serveur Next.js
169
+
170
+ ### `readme-build.ts`
171
+
172
+ Génère la documentation README.md de l'application.
173
+
174
+ **Usage :**
175
+
176
+ ```bash
177
+ pnpm readme:create
178
+ ```
179
+
180
+ **Actions effectuées :**
181
+
182
+ - Analyse la structure de l'application
183
+ - Collecte les informations des modules installés
184
+ - Génère un README complet avec la documentation
185
+
186
+ ## Architecture des scripts
187
+
188
+ ### Dépendances communes
189
+
190
+ Tous les scripts utilisent :
191
+
192
+ - `fs-extra` : Gestion des fichiers
193
+ - `chalk` : Affichage coloré dans le terminal
194
+ - `inquirer` : Prompts interactifs (pour certains scripts)
195
+ - `commander` : Parsing des arguments CLI
196
+
197
+ ### Structure type d'un script
198
+
199
+ ```typescript
200
+ import fs from "fs-extra";
201
+ import path from "path";
202
+ import chalk from "chalk";
203
+
204
+ export async function monScript(targetDir: string, options?: any) {
205
+ console.log(chalk.blue("\n🚀 Début du script...\n"));
206
+
207
+ try {
208
+ // Logique du script
209
+ console.log(chalk.green("✅ Succès!"));
210
+ } catch (error) {
211
+ console.error(chalk.red("❌ Erreur:"), error);
212
+ process.exit(1);
213
+ }
214
+ }
215
+ ```
216
+
217
+ ## Ajouter un nouveau script
218
+
219
+ 1. Créer le fichier TypeScript dans `src/scripts/`
220
+ 2. Exporter une fonction principale
221
+ 3. Ajouter la commande dans `src/cli.ts`
222
+ 4. Ajouter éventuellement un script NPM dans `package.json`
223
+ 5. Documenter ici et dans les docs principales
224
+
225
+ ## Bonnes pratiques
226
+
227
+ 1. **Messages clairs** : Utilisez `chalk` pour des messages colorés et informatifs
228
+ 2. **Gestion d'erreurs** : Toujours catch les erreurs et donner un message explicite
229
+ 3. **Confirmations** : Pour les opérations destructives, demander confirmation
230
+ 4. **Idempotence** : Les scripts doivent pouvoir être exécutés plusieurs fois
231
+ 5. **Logs** : Logger les actions importantes pour faciliter le debug
232
+ 6. **Exit codes** : Utiliser `process.exit(1)` en cas d'erreur
233
+
234
+ ## Tests
235
+
236
+ Pour tester un script :
237
+
238
+ ```bash
239
+ # 1. Compiler le package
240
+ pnpm build
241
+
242
+ # 2. Exécuter la commande
243
+ pnpm lastbrain <commande> [args]
244
+ ```
245
+
246
+ ## Contribution
247
+
248
+ Pour contribuer un nouveau script :
249
+
250
+ 1. Fork le projet
251
+ 2. Créer une branche feature
252
+ 3. Implémenter le script avec tests
253
+ 4. Documenter dans ce README
254
+ 5. Créer une Pull Request
255
+
256
+ ## Support
257
+
258
+ Pour toute question ou problème :
259
+
260
+ - Ouvrir une issue sur GitHub
261
+ - Consulter la documentation dans `/docs`
262
+ - Rejoindre le Discord LastBrain
@@ -0,0 +1,338 @@
1
+ import { spawn, spawnSync, execSync, execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
7
+ const projectRoot = process.env.PROJECT_ROOT || process.cwd();
8
+ const supabaseDir = path.join(projectRoot, "supabase");
9
+
10
+ const envTargets = [path.join(projectRoot, ".env.local")];
11
+
12
+ // Trouver le template dans le package @lastbrain/app
13
+ const packageAppDir = path
14
+ .dirname(fileURLToPath(import.meta.url))
15
+ .replace(/dist\/scripts$/, "src");
16
+ const envExampleTemplate = path.join(
17
+ packageAppDir,
18
+ "templates/env.example/.env.example"
19
+ );
20
+
21
+ function ensureDirectory(dir: string) {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+
25
+ function runSupabase(...args: string[]): Buffer | null;
26
+ function runSupabase(captureOutput: boolean, ...args: string[]): Buffer | null;
27
+ function runSupabase(...args: any[]): Buffer | null {
28
+ const bin = path.join(projectRoot, "node_modules", ".bin", "supabase");
29
+
30
+ // Ensure PATH includes common binary locations
31
+ const PATH = process.env.PATH || "";
32
+ const enhancedPath = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${PATH}`;
33
+
34
+ // Check if first argument is a boolean (captureOutput flag)
35
+ const captureOutput = typeof args[0] === "boolean" ? args.shift() : false;
36
+ const stdio = captureOutput ? "pipe" : "inherit";
37
+
38
+ const runners = [
39
+ () =>
40
+ spawnSync("supabase", args, {
41
+ cwd: projectRoot,
42
+ stdio: stdio as any,
43
+ env: { ...process.env, PATH: enhancedPath },
44
+ shell: true,
45
+ }),
46
+ () =>
47
+ spawnSync(bin, args, {
48
+ cwd: projectRoot,
49
+ stdio: stdio as any,
50
+ env: { ...process.env, PATH: enhancedPath },
51
+ }),
52
+ () =>
53
+ spawnSync("pnpm", ["exec", "supabase", ...args], {
54
+ cwd: projectRoot,
55
+ stdio: stdio as any,
56
+ env: { ...process.env, PATH: enhancedPath },
57
+ shell: true,
58
+ }),
59
+ ];
60
+
61
+ for (let i = 0; i < runners.length; i++) {
62
+ const result = runners[i]();
63
+
64
+ if (result.error) {
65
+ // Si c'est une erreur ENOENT (commande non trouvée), essayer la suivante
66
+ if ((result.error as any).code === "ENOENT") {
67
+ continue;
68
+ }
69
+ // Si c'est une autre erreur, la propager
70
+ throw result.error;
71
+ }
72
+
73
+ // Si le processus s'est terminé avec un code de sortie non nul, propager l'erreur
74
+ if (result.status !== 0) {
75
+ const error: any = new Error(
76
+ `Command failed with exit code ${result.status}`
77
+ );
78
+ error.status = result.status;
79
+ throw error;
80
+ }
81
+
82
+ // Succès !
83
+ return result.stdout || null;
84
+ }
85
+
86
+ throw new Error(
87
+ "Unable to locate Supabase CLI (install it globally or via pnpm)."
88
+ );
89
+ }
90
+
91
+ function ensureSupabaseInit() {
92
+ const configPath = path.join(supabaseDir, "config.toml");
93
+ if (!fs.existsSync(configPath)) {
94
+ runSupabase("init");
95
+ }
96
+ }
97
+
98
+ function parseEnvFile(filePath: string) {
99
+ const data = fs.readFileSync(filePath, "utf-8");
100
+ return Object.fromEntries(
101
+ data
102
+ .split("\n")
103
+ .map((line) => line.trim())
104
+ .filter((line) => line && !line.startsWith("#"))
105
+ .map((line) => {
106
+ const [key, ...value] = line.split("=");
107
+ return [key, value.join("=")];
108
+ })
109
+ ) as Record<string, string>;
110
+ }
111
+
112
+ function ensureEnvFile(values: Record<string, string>) {
113
+ const content = Object.entries(values)
114
+ .map(([key, value]) => `${key}=${value}`)
115
+ .join("\n");
116
+
117
+ envTargets.forEach((target) => {
118
+ fs.writeFileSync(target, content);
119
+ console.log(`📝 Wrote env to ${target}`);
120
+ });
121
+ }
122
+
123
+ function buildEnvPayload(creds: Record<string, string | undefined>) {
124
+ return {
125
+ NEXT_PUBLIC_SUPABASE_URL: creds.url ?? "",
126
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: creds.anon ?? "",
127
+ SUPABASE_URL: creds.url ?? "",
128
+ SUPABASE_ANON_KEY: creds.anon ?? "",
129
+ SUPABASE_SERVICE_ROLE_KEY: creds.service ?? "",
130
+ SUPABASE_DB_URL: creds.db ?? "",
131
+ SUPABASE_JWT_SECRET: creds.jwt ?? "",
132
+ };
133
+ }
134
+
135
+ function applyEnvToProcess(values: Record<string, string>) {
136
+ process.env.NEXT_PUBLIC_SUPABASE_URL = values.NEXT_PUBLIC_SUPABASE_URL;
137
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY =
138
+ values.NEXT_PUBLIC_SUPABASE_ANON_KEY;
139
+ process.env.SUPABASE_URL = values.SUPABASE_URL;
140
+ process.env.SUPABASE_ANON_KEY = values.SUPABASE_ANON_KEY;
141
+ process.env.SUPABASE_SERVICE_ROLE_KEY = values.SUPABASE_SERVICE_ROLE_KEY;
142
+ process.env.SUPABASE_DB_URL = values.SUPABASE_DB_URL;
143
+ process.env.SUPABASE_JWT_SECRET = values.SUPABASE_JWT_SECRET;
144
+ }
145
+
146
+ function copyEnvExample() {
147
+ if (!fs.existsSync(envExampleTemplate)) {
148
+ return null;
149
+ }
150
+ const target = envTargets[0];
151
+ ensureDirectory(path.dirname(target));
152
+ fs.copyFileSync(envExampleTemplate, target);
153
+ return target;
154
+ }
155
+
156
+ function readSupabaseEnv() {
157
+ const candidate = path.join(supabaseDir, ".env");
158
+ if (fs.existsSync(candidate)) {
159
+ return parseEnvFile(candidate);
160
+ }
161
+ const candidateLocal = path.join(supabaseDir, ".env.local");
162
+ if (fs.existsSync(candidateLocal)) {
163
+ return parseEnvFile(candidateLocal);
164
+ }
165
+ return {} as Record<string, string>;
166
+ }
167
+
168
+ function getSupabaseStatusEnv() {
169
+ try {
170
+ const result = runSupabase(true, "status", "-o", "env");
171
+ if (!result) return null;
172
+
173
+ const envString = result.toString();
174
+ const lines = envString
175
+ .split("\n")
176
+ .map((line) => line.trim())
177
+ .filter((line) => line && !line.startsWith("#") && line.includes("="));
178
+
179
+ const env: Record<string, string> = {};
180
+ for (const line of lines) {
181
+ const equalIndex = line.indexOf("=");
182
+ if (equalIndex > 0) {
183
+ const key = line.substring(0, equalIndex);
184
+ const value = line
185
+ .substring(equalIndex + 1)
186
+ .replace(/^["']|["']$/g, "");
187
+ env[key] = value;
188
+ }
189
+ }
190
+
191
+ return Object.keys(env).length > 0 ? env : null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ function buildCredsFromStatus(status: Record<string, any>) {
198
+ if (!status) {
199
+ return null;
200
+ }
201
+ return {
202
+ url: status.API_URL ?? status.SUPABASE_URL ?? "",
203
+ anon: status.ANON_KEY ?? "",
204
+ service: status.SERVICE_ROLE_KEY ?? status.SECRET_KEY ?? "",
205
+ jwt: status.JWT_SECRET ?? "",
206
+ db: status.DB_URL ?? status.DATABASE_URL ?? "",
207
+ };
208
+ }
209
+
210
+ function findLocalKeysFallback() {
211
+ const env = readSupabaseEnv();
212
+ if (Object.keys(env).length === 0) {
213
+ return null;
214
+ }
215
+
216
+ return {
217
+ url: env.SUPABASE_URL ?? env.API_URL ?? null,
218
+ anon: env.SUPABASE_ANON_KEY ?? env.ANON_KEY ?? null,
219
+ service: env.SUPABASE_SERVICE_ROLE_KEY ?? env.SERVICE_ROLE_KEY ?? null,
220
+ jwt: env.JWT_SECRET ?? null,
221
+ db: env.SUPABASE_DB_URL ?? env.DB_URL ?? null,
222
+ };
223
+ }
224
+
225
+ async function wait(ms: number) {
226
+ await new Promise((resolve) => setTimeout(resolve, ms));
227
+ }
228
+
229
+ async function waitForStatus() {
230
+ const maxAttempts = 10;
231
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
232
+ const status = getSupabaseStatusEnv();
233
+ if (status && Object.keys(status).length > 0) {
234
+ return status;
235
+ }
236
+ await wait(1000);
237
+ }
238
+ return null;
239
+ }
240
+
241
+ async function gatherCredentials() {
242
+ const envStatus = await waitForStatus();
243
+ let creds = envStatus ? buildCredsFromStatus(envStatus) : null;
244
+
245
+ if (!creds || !creds.url || !creds.anon) {
246
+ const fallback = findLocalKeysFallback();
247
+ if (fallback) {
248
+ creds = {
249
+ url: fallback.url ?? "",
250
+ anon: fallback.anon ?? "",
251
+ service: fallback.service ?? "",
252
+ jwt: fallback.jwt ?? "",
253
+ db: fallback.db ?? "",
254
+ };
255
+ }
256
+ }
257
+
258
+ return creds;
259
+ }
260
+
261
+ async function main() {
262
+ ensureSupabaseInit();
263
+
264
+ const PATH = process.env.PATH || "";
265
+ const enhancedPath = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${PATH}`;
266
+
267
+ const migrationResult = spawnSync(
268
+ "pnpm",
269
+ ["--filter", "web", "db:migrations:sync"],
270
+ {
271
+ cwd: projectRoot,
272
+ stdio: "inherit",
273
+ env: { ...process.env, PATH: enhancedPath },
274
+ shell: true,
275
+ }
276
+ );
277
+
278
+ if (migrationResult.error || migrationResult.status !== 0) {
279
+ console.warn("⚠️ Migration sync failed, continuing anyway...");
280
+ }
281
+
282
+ console.log("⚙️ Starting Supabase...");
283
+ try {
284
+ runSupabase("start");
285
+ } catch (error) {
286
+ console.warn("⚠️ Supabase start had issues, continuing...");
287
+ }
288
+
289
+ console.log("🔄 Resetting database...");
290
+ try {
291
+ runSupabase("db", "reset");
292
+ console.log("✅ Database reset complete.");
293
+ } catch (error) {
294
+ console.error("⚠️ supabase db reset failed:", error);
295
+ throw error;
296
+ }
297
+
298
+ console.log("🛠 Waiting for services to restart...");
299
+ await wait(3000);
300
+
301
+ const creds = await gatherCredentials();
302
+ if (creds && creds.url && creds.anon) {
303
+ const payload = buildEnvPayload(creds);
304
+ ensureEnvFile(payload);
305
+ applyEnvToProcess(payload);
306
+ console.log("✅ Environment configured successfully!");
307
+ } else {
308
+ const example = copyEnvExample();
309
+ if (example) {
310
+ console.log("ℹ️ Copied .env.example into .env.local as fallback.");
311
+ }
312
+ console.warn(
313
+ "⚠️ Impossible de récupérer automatiquement les accès Supabase."
314
+ );
315
+ }
316
+
317
+ if (process.argv.includes("--with-dev")) {
318
+ console.log("🚀 Launching Next dev server...");
319
+ const devProcess = spawn("pnpm", ["--filter", "web", "dev"], {
320
+ cwd: projectRoot,
321
+ stdio: "inherit",
322
+ });
323
+ devProcess.on("exit", (code) => {
324
+ console.log("🧹 Stopping Supabase");
325
+ try {
326
+ runSupabase("stop");
327
+ } catch {
328
+ // Ignore errors when stopping
329
+ }
330
+ process.exit(code ?? 0);
331
+ });
332
+ }
333
+ }
334
+
335
+ main().catch((error) => {
336
+ console.error(error);
337
+ process.exit(1);
338
+ });
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { getModuleConfigs } from "../modules/module-loader.js";
6
+ import type { ModuleBuildConfig } from "@lastbrain/core";
7
+
8
+ // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
9
+ const projectRoot = process.env.PROJECT_ROOT || process.cwd();
10
+ const migrationsDir = path.join(projectRoot, "supabase/migrations");
11
+
12
+ function ensureDirectory(dir: string) {
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ }
15
+
16
+ enum CopyStrategy {
17
+ overwrite,
18
+ skip,
19
+ }
20
+
21
+ function getModulePackageDir(moduleName: string) {
22
+ return path.join(projectRoot, "node_modules", ...moduleName.split("/"));
23
+ }
24
+
25
+ interface ModuleMigrationEntry {
26
+ enabled: boolean;
27
+ priority?: number;
28
+ path?: string;
29
+ }
30
+
31
+ function copyMigration(
32
+ moduleConfig: ModuleBuildConfig,
33
+ migration: ModuleMigrationEntry,
34
+ file: string,
35
+ index: number
36
+ ) {
37
+ const moduleDir = getModulePackageDir(moduleConfig.moduleName);
38
+ const migrationsPath = migration.path ?? "supabase/migrations";
39
+ const sourceDir = path.join(moduleDir, migrationsPath);
40
+ const sourceFile = path.join(sourceDir, file);
41
+
42
+ if (!fs.existsSync(sourceFile)) {
43
+ console.warn(`⚠️ Missing migration file ${sourceFile}`);
44
+ return;
45
+ }
46
+
47
+ ensureDirectory(migrationsDir);
48
+ const moduleSlug = moduleConfig.moduleName.replace("@lastbrain/", "module-");
49
+ const prefix = String((migration.priority ?? 0) * 10 + index + 1).padStart(
50
+ 3,
51
+ "0"
52
+ );
53
+ const fileName = `${prefix}_${moduleSlug}_${file}`;
54
+ const targetFile = path.join(migrationsDir, fileName);
55
+
56
+ fs.copyFileSync(sourceFile, targetFile);
57
+ console.log(`📜 Copied migration ${fileName}`);
58
+ }
59
+
60
+ function syncModuleMigrations() {
61
+ const moduleConfigs = getModuleConfigs();
62
+ moduleConfigs.forEach((moduleConfig) => {
63
+ const migrationBlock = moduleConfig.migrations;
64
+ if (!migrationBlock || !migrationBlock.enabled) {
65
+ return;
66
+ }
67
+
68
+ const moduleMigrationPath = path.join(
69
+ getModulePackageDir(moduleConfig.moduleName),
70
+ migrationBlock.path ?? "supabase/migrations"
71
+ );
72
+ let files = migrationBlock.files;
73
+ if (!files?.length && fs.existsSync(moduleMigrationPath)) {
74
+ files = fs
75
+ .readdirSync(moduleMigrationPath)
76
+ .filter((file) => file.endsWith(".sql"))
77
+ .sort();
78
+ }
79
+
80
+ files?.forEach((file, index) => {
81
+ copyMigration(moduleConfig, migrationBlock, file, index);
82
+ });
83
+ });
84
+ }
85
+
86
+ syncModuleMigrations();