@lastbrain/app 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/init-app.js +0 -2
- package/package.json +3 -2
- package/src/app-shell/(admin)/layout.tsx +13 -0
- package/src/app-shell/(auth)/layout.tsx +13 -0
- package/src/app-shell/(public)/page.tsx +11 -0
- package/src/app-shell/layout.tsx +5 -0
- package/src/app-shell/not-found.tsx +28 -0
- package/src/auth/authHelpers.ts +24 -0
- package/src/auth/useAuthSession.ts +54 -0
- package/src/cli.ts +96 -0
- package/src/index.ts +21 -0
- package/src/layouts/AdminLayout.tsx +7 -0
- package/src/layouts/AppProviders.tsx +61 -0
- package/src/layouts/AuthLayout.tsx +7 -0
- package/src/layouts/PublicLayout.tsx +7 -0
- package/src/layouts/RootLayout.tsx +27 -0
- package/src/modules/module-loader.ts +14 -0
- package/src/scripts/README.md +262 -0
- package/src/scripts/db-init.ts +338 -0
- package/src/scripts/db-migrations-sync.ts +86 -0
- package/src/scripts/dev-sync.ts +218 -0
- package/src/scripts/init-app.ts +1077 -0
- package/src/scripts/module-add.ts +242 -0
- package/src/scripts/module-build.ts +502 -0
- package/src/scripts/module-create.ts +809 -0
- package/src/scripts/module-list.ts +37 -0
- package/src/scripts/module-remove.ts +367 -0
- package/src/scripts/readme-build.ts +60 -0
- package/src/styles.css +3 -0
- package/src/templates/AuthGuidePage.tsx +68 -0
- package/src/templates/DefaultDoc.tsx +462 -0
- package/src/templates/DocPage.tsx +381 -0
- package/src/templates/DocsPageWithModules.tsx +22 -0
- package/src/templates/MigrationsGuidePage.tsx +61 -0
- package/src/templates/ModuleGuidePage.tsx +71 -0
- package/src/templates/SimpleDocPage.tsx +587 -0
- package/src/templates/SimpleHomePage.tsx +385 -0
- package/src/templates/env.example/.env.example +6 -0
- package/src/templates/migrations/20201010100000_app_base.sql +228 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
|
|
6
|
+
interface PageConfig {
|
|
7
|
+
section: "public" | "auth" | "admin";
|
|
8
|
+
path: string;
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TableConfig {
|
|
13
|
+
name: string;
|
|
14
|
+
sections: ("public" | "auth" | "admin")[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ModuleConfig {
|
|
18
|
+
slug: string;
|
|
19
|
+
moduleName: string;
|
|
20
|
+
pages: PageConfig[];
|
|
21
|
+
tables: TableConfig[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse une chaîne de pages séparées par des virgules
|
|
26
|
+
* Ex: "legal, privacy, terms" => ["legal", "privacy", "terms"]
|
|
27
|
+
*/
|
|
28
|
+
function parsePagesList(input: string): string[] {
|
|
29
|
+
if (!input || input.trim() === "") return [];
|
|
30
|
+
return input
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((p) => p.trim())
|
|
33
|
+
.filter((p) => p.length > 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse une chaîne de tables séparées par des virgules
|
|
38
|
+
* Ex: "settings, users" => ["settings", "users"]
|
|
39
|
+
*/
|
|
40
|
+
function parseTablesList(input: string): string[] {
|
|
41
|
+
if (!input || input.trim() === "") return [];
|
|
42
|
+
return input
|
|
43
|
+
.split(",")
|
|
44
|
+
.map((t) => t.trim())
|
|
45
|
+
.filter((t) => t.length > 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Génère le contenu du package.json
|
|
50
|
+
*/
|
|
51
|
+
function generatePackageJson(moduleName: string, slug: string): string {
|
|
52
|
+
const buildConfigExport = `./${slug}.build.config`;
|
|
53
|
+
return JSON.stringify(
|
|
54
|
+
{
|
|
55
|
+
name: moduleName,
|
|
56
|
+
version: "0.1.0",
|
|
57
|
+
private: false,
|
|
58
|
+
type: "module",
|
|
59
|
+
main: "dist/index.js",
|
|
60
|
+
types: "dist/index.d.ts",
|
|
61
|
+
files: ["dist", "supabase"],
|
|
62
|
+
scripts: {
|
|
63
|
+
build: "tsc -p tsconfig.json",
|
|
64
|
+
dev: "tsc -p tsconfig.json --watch",
|
|
65
|
+
},
|
|
66
|
+
dependencies: {
|
|
67
|
+
"@lastbrain/core": "workspace:0.1.0",
|
|
68
|
+
"@lastbrain/ui": "workspace:0.1.0",
|
|
69
|
+
react: "^19.0.0",
|
|
70
|
+
"lucide-react": "^0.554.0",
|
|
71
|
+
"react-dom": "^19.0.0",
|
|
72
|
+
},
|
|
73
|
+
devDependencies: {
|
|
74
|
+
typescript: "^5.4.0",
|
|
75
|
+
},
|
|
76
|
+
exports: {
|
|
77
|
+
".": {
|
|
78
|
+
types: "./dist/index.d.ts",
|
|
79
|
+
default: "./dist/index.js",
|
|
80
|
+
},
|
|
81
|
+
"./server": {
|
|
82
|
+
types: "./dist/server.d.ts",
|
|
83
|
+
default: "./dist/server.js",
|
|
84
|
+
},
|
|
85
|
+
[buildConfigExport]: {
|
|
86
|
+
types: `./dist/${slug}.build.config.d.ts`,
|
|
87
|
+
default: `./dist/${slug}.build.config.js`,
|
|
88
|
+
},
|
|
89
|
+
"./api/*": {
|
|
90
|
+
types: "./dist/api/*.d.ts",
|
|
91
|
+
default: "./dist/api/*.js",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
sideEffects: false,
|
|
95
|
+
},
|
|
96
|
+
null,
|
|
97
|
+
2
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Génère le contenu du tsconfig.json
|
|
103
|
+
*/
|
|
104
|
+
function generateTsConfig(): string {
|
|
105
|
+
return JSON.stringify(
|
|
106
|
+
{
|
|
107
|
+
extends: "../../tsconfig.base.json",
|
|
108
|
+
compilerOptions: {
|
|
109
|
+
outDir: "dist",
|
|
110
|
+
rootDir: "src",
|
|
111
|
+
declaration: true,
|
|
112
|
+
},
|
|
113
|
+
include: ["src"],
|
|
114
|
+
},
|
|
115
|
+
null,
|
|
116
|
+
2
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Génère le contenu du fichier build.config.ts
|
|
122
|
+
*/
|
|
123
|
+
function generateBuildConfig(config: ModuleConfig): string {
|
|
124
|
+
const { moduleName, pages, tables } = config;
|
|
125
|
+
|
|
126
|
+
// Générer la liste des pages
|
|
127
|
+
const pagesConfig = pages.map((page) => {
|
|
128
|
+
const componentName = page.name
|
|
129
|
+
.split("-")
|
|
130
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
131
|
+
.join("");
|
|
132
|
+
return ` {
|
|
133
|
+
section: "${page.section}",
|
|
134
|
+
path: "${page.path}",
|
|
135
|
+
componentExport: "${componentName}Page",
|
|
136
|
+
}`;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Générer la liste des APIs
|
|
140
|
+
const apisConfig: string[] = [];
|
|
141
|
+
for (const table of tables) {
|
|
142
|
+
for (const section of table.sections) {
|
|
143
|
+
const methods = ["GET", "POST", "PUT", "DELETE"];
|
|
144
|
+
for (const method of methods) {
|
|
145
|
+
apisConfig.push(` {
|
|
146
|
+
method: "${method}",
|
|
147
|
+
path: "/api/${section}/${table.name}",
|
|
148
|
+
handlerExport: "${method}",
|
|
149
|
+
entryPoint: "api/${section}/${table.name}",
|
|
150
|
+
authRequired: ${section !== "public"},
|
|
151
|
+
}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Générer les menus par section
|
|
157
|
+
const menuSections: { section: string; items: string[] }[] = [];
|
|
158
|
+
|
|
159
|
+
// Menu public
|
|
160
|
+
const publicPages = pages.filter(p => p.section === "public");
|
|
161
|
+
if (publicPages.length > 0) {
|
|
162
|
+
const publicMenuItems = publicPages.map((page, index) => {
|
|
163
|
+
const title = page.name
|
|
164
|
+
.split("-")
|
|
165
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
166
|
+
.join(" ");
|
|
167
|
+
return ` {
|
|
168
|
+
title: "${title}",
|
|
169
|
+
description: "Page ${title}",
|
|
170
|
+
icon: "FileText",
|
|
171
|
+
path: "${page.path}",
|
|
172
|
+
order: ${index + 1},
|
|
173
|
+
}`;
|
|
174
|
+
});
|
|
175
|
+
menuSections.push({ section: "public", items: publicMenuItems });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Menu auth
|
|
179
|
+
const authPages = pages.filter(p => p.section === "auth");
|
|
180
|
+
if (authPages.length > 0) {
|
|
181
|
+
const authMenuItems = authPages.map((page, index) => {
|
|
182
|
+
const title = page.name
|
|
183
|
+
.split("-")
|
|
184
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
185
|
+
.join(" ");
|
|
186
|
+
return ` {
|
|
187
|
+
title: "${title}",
|
|
188
|
+
description: "Page ${title}",
|
|
189
|
+
icon: "FileText",
|
|
190
|
+
path: "${page.path}",
|
|
191
|
+
order: ${index + 1},
|
|
192
|
+
}`;
|
|
193
|
+
});
|
|
194
|
+
menuSections.push({ section: "auth", items: authMenuItems });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Menu admin
|
|
198
|
+
const adminPages = pages.filter(p => p.section === "admin");
|
|
199
|
+
if (adminPages.length > 0) {
|
|
200
|
+
const adminMenuItems = adminPages.map((page, index) => {
|
|
201
|
+
const title = page.name
|
|
202
|
+
.split("-")
|
|
203
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
204
|
+
.join(" ");
|
|
205
|
+
return ` {
|
|
206
|
+
title: "${title}",
|
|
207
|
+
description: "Page ${title}",
|
|
208
|
+
icon: "Settings",
|
|
209
|
+
path: "${page.path}",
|
|
210
|
+
order: ${index + 1},
|
|
211
|
+
}`;
|
|
212
|
+
});
|
|
213
|
+
menuSections.push({ section: "admin", items: adminMenuItems });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Générer la configuration menu
|
|
217
|
+
const menuConfig = menuSections.length > 0
|
|
218
|
+
? `,
|
|
219
|
+
menu: {
|
|
220
|
+
${menuSections.map(({ section, items }) => ` ${section}: [
|
|
221
|
+
${items.join(",\n")}
|
|
222
|
+
]`).join(",\n")}
|
|
223
|
+
}`
|
|
224
|
+
: "";
|
|
225
|
+
|
|
226
|
+
return `import type { ModuleBuildConfig } from "@lastbrain/core";
|
|
227
|
+
|
|
228
|
+
const buildConfig: ModuleBuildConfig = {
|
|
229
|
+
moduleName: "${moduleName}",
|
|
230
|
+
pages: [
|
|
231
|
+
${pagesConfig.join(",\n")}
|
|
232
|
+
],
|
|
233
|
+
apis: [
|
|
234
|
+
${apisConfig.join(",\n")}
|
|
235
|
+
],
|
|
236
|
+
migrations: {
|
|
237
|
+
enabled: true,
|
|
238
|
+
priority: 30,
|
|
239
|
+
path: "supabase/migrations",
|
|
240
|
+
files: ${tables.length > 0 ? `["001_${config.slug}_init.sql"]` : "[]"},
|
|
241
|
+
}${menuConfig},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export default buildConfig;
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Génère le contenu du fichier index.ts
|
|
250
|
+
*/
|
|
251
|
+
function generateIndexTs(pages: PageConfig[]): string {
|
|
252
|
+
const exports = pages.map((page) => {
|
|
253
|
+
const componentName = page.name
|
|
254
|
+
.split("-")
|
|
255
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
256
|
+
.join("");
|
|
257
|
+
const fileName = page.name
|
|
258
|
+
.split("-")
|
|
259
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
260
|
+
.join("");
|
|
261
|
+
return `export { ${componentName}Page } from "./web/${page.section}/${fileName}Page.js";`;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return `// Client Components
|
|
265
|
+
${exports.join("\n")}
|
|
266
|
+
|
|
267
|
+
// Configuration de build
|
|
268
|
+
export { default as buildConfig } from "./build.config.js";
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Génère le contenu du fichier server.ts
|
|
274
|
+
*/
|
|
275
|
+
function generateServerTs(tables: TableConfig[]): string {
|
|
276
|
+
const exports: string[] = [];
|
|
277
|
+
|
|
278
|
+
for (const table of tables) {
|
|
279
|
+
for (const section of table.sections) {
|
|
280
|
+
exports.push(
|
|
281
|
+
`export { GET, POST, PUT, DELETE } from "./api/${section}/${table.name}.js";`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return `// Server-only exports (Route Handlers, Server Actions, etc.)
|
|
287
|
+
${exports.join("\n")}
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Génère le contenu d'une page
|
|
293
|
+
*/
|
|
294
|
+
function generatePageComponent(pageName: string, section: string): string {
|
|
295
|
+
const componentName = pageName
|
|
296
|
+
.split("-")
|
|
297
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
298
|
+
.join("");
|
|
299
|
+
|
|
300
|
+
return `"use client";
|
|
301
|
+
|
|
302
|
+
import { Card, CardBody, CardHeader } from "@lastbrain/ui";
|
|
303
|
+
|
|
304
|
+
export function ${componentName}Page() {
|
|
305
|
+
return (
|
|
306
|
+
<div className="container mx-auto p-6">
|
|
307
|
+
<Card>
|
|
308
|
+
<CardHeader>
|
|
309
|
+
<h1 className="text-2xl font-bold">${componentName}</h1>
|
|
310
|
+
</CardHeader>
|
|
311
|
+
<CardBody>
|
|
312
|
+
<p className="text-default-600">
|
|
313
|
+
Contenu de la page ${pageName} (section: ${section})
|
|
314
|
+
</p>
|
|
315
|
+
</CardBody>
|
|
316
|
+
</Card>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Génère le contenu d'une route API CRUD
|
|
325
|
+
*/
|
|
326
|
+
function generateApiRoute(tableName: string, section: string): string {
|
|
327
|
+
const authRequired = section !== "public";
|
|
328
|
+
|
|
329
|
+
return `import { getSupabaseServerClient } from "@lastbrain/core/server";
|
|
330
|
+
|
|
331
|
+
const jsonResponse = (payload: unknown, status = 200) => {
|
|
332
|
+
return new Response(JSON.stringify(payload), {
|
|
333
|
+
headers: {
|
|
334
|
+
"content-type": "application/json"
|
|
335
|
+
},
|
|
336
|
+
status
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* GET - Liste tous les enregistrements de ${tableName}
|
|
342
|
+
*/
|
|
343
|
+
export async function GET(request: Request) {
|
|
344
|
+
const supabase = await getSupabaseServerClient();
|
|
345
|
+
${authRequired ? `
|
|
346
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
347
|
+
if (authError || !user) {
|
|
348
|
+
return jsonResponse({ error: "Non authentifié" }, 401);
|
|
349
|
+
}
|
|
350
|
+
` : ""}
|
|
351
|
+
|
|
352
|
+
const { data, error } = await supabase
|
|
353
|
+
.from("${tableName}")
|
|
354
|
+
.select("*");
|
|
355
|
+
|
|
356
|
+
if (error) {
|
|
357
|
+
return jsonResponse({ error: error.message }, 400);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return jsonResponse({ data });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* POST - Crée un nouvel enregistrement dans ${tableName}
|
|
365
|
+
*/
|
|
366
|
+
export async function POST(request: Request) {
|
|
367
|
+
const supabase = await getSupabaseServerClient();
|
|
368
|
+
${authRequired ? `
|
|
369
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
370
|
+
if (authError || !user) {
|
|
371
|
+
return jsonResponse({ error: "Non authentifié" }, 401);
|
|
372
|
+
}
|
|
373
|
+
` : ""}
|
|
374
|
+
|
|
375
|
+
const body = await request.json();
|
|
376
|
+
|
|
377
|
+
const { data, error } = await supabase
|
|
378
|
+
.from("${tableName}")
|
|
379
|
+
.insert(body)
|
|
380
|
+
.select()
|
|
381
|
+
.single();
|
|
382
|
+
|
|
383
|
+
if (error) {
|
|
384
|
+
return jsonResponse({ error: error.message }, 400);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return jsonResponse({ data }, 201);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* PUT - Met à jour un enregistrement dans ${tableName}
|
|
392
|
+
*/
|
|
393
|
+
export async function PUT(request: Request) {
|
|
394
|
+
const supabase = await getSupabaseServerClient();
|
|
395
|
+
${authRequired ? `
|
|
396
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
397
|
+
if (authError || !user) {
|
|
398
|
+
return jsonResponse({ error: "Non authentifié" }, 401);
|
|
399
|
+
}
|
|
400
|
+
` : ""}
|
|
401
|
+
|
|
402
|
+
const body = await request.json();
|
|
403
|
+
const { id, ...updateData } = body;
|
|
404
|
+
|
|
405
|
+
if (!id) {
|
|
406
|
+
return jsonResponse({ error: "ID requis pour la mise à jour" }, 400);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const { data, error } = await supabase
|
|
410
|
+
.from("${tableName}")
|
|
411
|
+
.update(updateData)
|
|
412
|
+
.eq("id", id)
|
|
413
|
+
.select()
|
|
414
|
+
.single();
|
|
415
|
+
|
|
416
|
+
if (error) {
|
|
417
|
+
return jsonResponse({ error: error.message }, 400);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return jsonResponse({ data });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* DELETE - Supprime un enregistrement de ${tableName}
|
|
425
|
+
*/
|
|
426
|
+
export async function DELETE(request: Request) {
|
|
427
|
+
const supabase = await getSupabaseServerClient();
|
|
428
|
+
${authRequired ? `
|
|
429
|
+
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
430
|
+
if (authError || !user) {
|
|
431
|
+
return jsonResponse({ error: "Non authentifié" }, 401);
|
|
432
|
+
}
|
|
433
|
+
` : ""}
|
|
434
|
+
|
|
435
|
+
const { searchParams } = new URL(request.url);
|
|
436
|
+
const id = searchParams.get("id");
|
|
437
|
+
|
|
438
|
+
if (!id) {
|
|
439
|
+
return jsonResponse({ error: "ID requis pour la suppression" }, 400);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const { error } = await supabase
|
|
443
|
+
.from("${tableName}")
|
|
444
|
+
.delete()
|
|
445
|
+
.eq("id", id);
|
|
446
|
+
|
|
447
|
+
if (error) {
|
|
448
|
+
return jsonResponse({ error: error.message }, 400);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return jsonResponse({ success: true });
|
|
452
|
+
}
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Génère le contenu d'un fichier de migration SQL
|
|
458
|
+
*/
|
|
459
|
+
function generateMigration(tables: TableConfig[], slug: string): string {
|
|
460
|
+
const timestamp = new Date()
|
|
461
|
+
.toISOString()
|
|
462
|
+
.replace(/[-:]/g, "")
|
|
463
|
+
.split(".")[0]
|
|
464
|
+
.replace("T", "");
|
|
465
|
+
|
|
466
|
+
const tablesSQL = tables
|
|
467
|
+
.map((table) => {
|
|
468
|
+
return `-- ===========================================================================
|
|
469
|
+
-- Table: public.${table.name}
|
|
470
|
+
-- ===========================================================================
|
|
471
|
+
CREATE TABLE IF NOT EXISTS public.${table.name} (
|
|
472
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
473
|
+
owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
474
|
+
title text NOT NULL,
|
|
475
|
+
description text,
|
|
476
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
477
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
-- RLS
|
|
481
|
+
ALTER TABLE public.${table.name} ENABLE ROW LEVEL SECURITY;
|
|
482
|
+
|
|
483
|
+
-- Politique: Les utilisateurs peuvent voir leurs propres enregistrements
|
|
484
|
+
DROP POLICY IF EXISTS ${table.name}_owner_select ON public.${table.name};
|
|
485
|
+
CREATE POLICY ${table.name}_owner_select ON public.${table.name}
|
|
486
|
+
FOR SELECT TO authenticated
|
|
487
|
+
USING (owner_id = auth.uid());
|
|
488
|
+
|
|
489
|
+
-- Politique: Les utilisateurs peuvent créer leurs propres enregistrements
|
|
490
|
+
DROP POLICY IF EXISTS ${table.name}_owner_insert ON public.${table.name};
|
|
491
|
+
CREATE POLICY ${table.name}_owner_insert ON public.${table.name}
|
|
492
|
+
FOR INSERT TO authenticated
|
|
493
|
+
WITH CHECK (owner_id = auth.uid());
|
|
494
|
+
|
|
495
|
+
-- Politique: Les utilisateurs peuvent modifier leurs propres enregistrements
|
|
496
|
+
DROP POLICY IF EXISTS ${table.name}_owner_update ON public.${table.name};
|
|
497
|
+
CREATE POLICY ${table.name}_owner_update ON public.${table.name}
|
|
498
|
+
FOR UPDATE TO authenticated
|
|
499
|
+
USING (owner_id = auth.uid())
|
|
500
|
+
WITH CHECK (owner_id = auth.uid());
|
|
501
|
+
|
|
502
|
+
-- Politique: Les utilisateurs peuvent supprimer leurs propres enregistrements
|
|
503
|
+
DROP POLICY IF EXISTS ${table.name}_owner_delete ON public.${table.name};
|
|
504
|
+
CREATE POLICY ${table.name}_owner_delete ON public.${table.name}
|
|
505
|
+
FOR DELETE TO authenticated
|
|
506
|
+
USING (owner_id = auth.uid());
|
|
507
|
+
|
|
508
|
+
-- Trigger updated_at
|
|
509
|
+
DROP TRIGGER IF EXISTS set_${table.name}_updated_at ON public.${table.name};
|
|
510
|
+
CREATE TRIGGER set_${table.name}_updated_at
|
|
511
|
+
BEFORE UPDATE ON public.${table.name}
|
|
512
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
513
|
+
|
|
514
|
+
-- Index
|
|
515
|
+
CREATE INDEX IF NOT EXISTS idx_${table.name}_owner_id ON public.${table.name}(owner_id);
|
|
516
|
+
|
|
517
|
+
-- Grants
|
|
518
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.${table.name} TO service_role;
|
|
519
|
+
`;
|
|
520
|
+
})
|
|
521
|
+
.join("\n\n");
|
|
522
|
+
|
|
523
|
+
return `-- ${slug} module initial migration
|
|
524
|
+
-- Auto-generated by module-create.ts
|
|
525
|
+
-- NOTE: uses helper function set_updated_at() from base migration
|
|
526
|
+
|
|
527
|
+
-- ===========================================================================
|
|
528
|
+
-- Helper: set_updated_at trigger function (if not already present)
|
|
529
|
+
-- ===========================================================================
|
|
530
|
+
CREATE OR REPLACE FUNCTION public.set_updated_at()
|
|
531
|
+
RETURNS trigger
|
|
532
|
+
LANGUAGE plpgsql
|
|
533
|
+
AS $$
|
|
534
|
+
BEGIN
|
|
535
|
+
NEW.updated_at := now();
|
|
536
|
+
RETURN NEW;
|
|
537
|
+
END;
|
|
538
|
+
$$;
|
|
539
|
+
|
|
540
|
+
${tablesSQL}
|
|
541
|
+
`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Crée la structure du module
|
|
546
|
+
*/
|
|
547
|
+
async function createModuleStructure(config: ModuleConfig, rootDir: string) {
|
|
548
|
+
const moduleDir = path.join(rootDir, "packages", config.slug);
|
|
549
|
+
|
|
550
|
+
console.log(chalk.blue(`\n📦 Création du module ${config.slug}...\n`));
|
|
551
|
+
|
|
552
|
+
// Créer la structure de base
|
|
553
|
+
await fs.ensureDir(moduleDir);
|
|
554
|
+
await fs.ensureDir(path.join(moduleDir, "src"));
|
|
555
|
+
await fs.ensureDir(path.join(moduleDir, "src", "web"));
|
|
556
|
+
await fs.ensureDir(path.join(moduleDir, "src", "api"));
|
|
557
|
+
await fs.ensureDir(path.join(moduleDir, "supabase", "migrations"));
|
|
558
|
+
|
|
559
|
+
// Créer package.json
|
|
560
|
+
console.log(chalk.yellow(" 📄 package.json"));
|
|
561
|
+
await fs.writeFile(
|
|
562
|
+
path.join(moduleDir, "package.json"),
|
|
563
|
+
generatePackageJson(config.moduleName, config.slug)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
// Créer tsconfig.json
|
|
567
|
+
console.log(chalk.yellow(" 📄 tsconfig.json"));
|
|
568
|
+
await fs.writeFile(
|
|
569
|
+
path.join(moduleDir, "tsconfig.json"),
|
|
570
|
+
generateTsConfig()
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// Créer {slug}.build.config.ts
|
|
574
|
+
const buildConfigFileName = `${config.slug}.build.config.ts`;
|
|
575
|
+
console.log(chalk.yellow(` 📄 src/${buildConfigFileName}`));
|
|
576
|
+
await fs.writeFile(
|
|
577
|
+
path.join(moduleDir, "src", buildConfigFileName),
|
|
578
|
+
generateBuildConfig(config)
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
// Créer index.ts
|
|
582
|
+
console.log(chalk.yellow(" 📄 src/index.ts"));
|
|
583
|
+
await fs.writeFile(
|
|
584
|
+
path.join(moduleDir, "src", "index.ts"),
|
|
585
|
+
generateIndexTs(config.pages)
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// Créer server.ts
|
|
589
|
+
console.log(chalk.yellow(" 📄 src/server.ts"));
|
|
590
|
+
await fs.writeFile(
|
|
591
|
+
path.join(moduleDir, "src", "server.ts"),
|
|
592
|
+
generateServerTs(config.tables)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// Créer les pages
|
|
596
|
+
console.log(chalk.blue("\n📄 Création des pages..."));
|
|
597
|
+
for (const page of config.pages) {
|
|
598
|
+
const pagePath = path.join(moduleDir, "src", "web", page.section);
|
|
599
|
+
await fs.ensureDir(pagePath);
|
|
600
|
+
|
|
601
|
+
const componentName = page.name
|
|
602
|
+
.split("-")
|
|
603
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
604
|
+
.join("");
|
|
605
|
+
const fileName = `${componentName}Page.tsx`;
|
|
606
|
+
|
|
607
|
+
console.log(chalk.yellow(` 📄 src/web/${page.section}/${fileName}`));
|
|
608
|
+
await fs.writeFile(
|
|
609
|
+
path.join(pagePath, fileName),
|
|
610
|
+
generatePageComponent(page.name, page.section)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Créer les routes API
|
|
615
|
+
if (config.tables.length > 0) {
|
|
616
|
+
console.log(chalk.blue("\n🔌 Création des routes API..."));
|
|
617
|
+
for (const table of config.tables) {
|
|
618
|
+
for (const section of table.sections) {
|
|
619
|
+
const apiPath = path.join(moduleDir, "src", "api", section);
|
|
620
|
+
await fs.ensureDir(apiPath);
|
|
621
|
+
|
|
622
|
+
const fileName = `${table.name}.ts`;
|
|
623
|
+
console.log(chalk.yellow(` 📄 src/api/${section}/${fileName}`));
|
|
624
|
+
await fs.writeFile(
|
|
625
|
+
path.join(apiPath, fileName),
|
|
626
|
+
generateApiRoute(table.name, section)
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Créer les migrations
|
|
633
|
+
if (config.tables.length > 0) {
|
|
634
|
+
console.log(chalk.blue("\n🗄️ Création des migrations..."));
|
|
635
|
+
const timestamp = new Date()
|
|
636
|
+
.toISOString()
|
|
637
|
+
.replace(/[-:]/g, "")
|
|
638
|
+
.split(".")[0]
|
|
639
|
+
.replace("T", "");
|
|
640
|
+
|
|
641
|
+
const migrationFileName = `${timestamp}_${config.slug}_init.sql`;
|
|
642
|
+
console.log(
|
|
643
|
+
chalk.yellow(` 📄 supabase/migrations/${migrationFileName}`)
|
|
644
|
+
);
|
|
645
|
+
await fs.writeFile(
|
|
646
|
+
path.join(moduleDir, "supabase", "migrations", migrationFileName),
|
|
647
|
+
generateMigration(config.tables, config.slug)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
console.log(chalk.green(`\n✅ Module ${config.slug} créé avec succès!\n`));
|
|
652
|
+
console.log(chalk.gray(`📂 Emplacement: ${moduleDir}\n`));
|
|
653
|
+
console.log(chalk.blue("Prochaines étapes:"));
|
|
654
|
+
console.log(chalk.gray(` 1. cd ${moduleDir}`));
|
|
655
|
+
console.log(chalk.gray(` 2. pnpm install`));
|
|
656
|
+
console.log(chalk.gray(` 3. pnpm build`));
|
|
657
|
+
console.log(
|
|
658
|
+
chalk.gray(
|
|
659
|
+
` 4. Ajouter le module à votre app avec: pnpm lastbrain add ${config.slug.replace("module-", "")}\n`
|
|
660
|
+
)
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Point d'entrée du script
|
|
666
|
+
*/
|
|
667
|
+
export async function createModule() {
|
|
668
|
+
console.log(chalk.blue("\n🚀 Création d'un nouveau module LastBrain\n"));
|
|
669
|
+
|
|
670
|
+
const answers = await inquirer.prompt([
|
|
671
|
+
{
|
|
672
|
+
type: "input",
|
|
673
|
+
name: "slug",
|
|
674
|
+
message: "Nom du module (sera préfixé par 'module-'):",
|
|
675
|
+
validate: (input) => {
|
|
676
|
+
if (!input || input.trim() === "") {
|
|
677
|
+
return "Le nom du module est requis";
|
|
678
|
+
}
|
|
679
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
680
|
+
return "Le nom doit contenir uniquement des lettres minuscules, chiffres et tirets";
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
},
|
|
684
|
+
filter: (input: string) => input.trim().toLowerCase(),
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
type: "input",
|
|
688
|
+
name: "pagesPublic",
|
|
689
|
+
message:
|
|
690
|
+
"Pages publiques (séparées par des virgules, ex: legal, privacy, terms):",
|
|
691
|
+
default: "",
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
type: "input",
|
|
695
|
+
name: "pagesAuth",
|
|
696
|
+
message:
|
|
697
|
+
"Pages authentifiées (séparées par des virgules, ex: dashboard, profile):",
|
|
698
|
+
default: "",
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
type: "input",
|
|
702
|
+
name: "pagesAdmin",
|
|
703
|
+
message:
|
|
704
|
+
"Pages admin (séparées par des virgules, ex: settings, users):",
|
|
705
|
+
default: "",
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
type: "input",
|
|
709
|
+
name: "tables",
|
|
710
|
+
message:
|
|
711
|
+
"Tables (séparées par des virgules, ex: settings, notifications):",
|
|
712
|
+
default: "",
|
|
713
|
+
},
|
|
714
|
+
]);
|
|
715
|
+
|
|
716
|
+
// Construire la configuration du module
|
|
717
|
+
const slug = `module-${answers.slug}`;
|
|
718
|
+
const moduleName = `@lastbrain/${slug}`;
|
|
719
|
+
|
|
720
|
+
const pages: PageConfig[] = [];
|
|
721
|
+
|
|
722
|
+
// Pages publiques
|
|
723
|
+
const publicPages = parsePagesList(answers.pagesPublic);
|
|
724
|
+
for (const pageName of publicPages) {
|
|
725
|
+
pages.push({
|
|
726
|
+
section: "public",
|
|
727
|
+
path: `/${pageName}`,
|
|
728
|
+
name: pageName,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Pages auth
|
|
733
|
+
const authPages = parsePagesList(answers.pagesAuth);
|
|
734
|
+
for (const pageName of authPages) {
|
|
735
|
+
pages.push({
|
|
736
|
+
section: "auth",
|
|
737
|
+
path: `/${pageName}`,
|
|
738
|
+
name: pageName,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Pages admin
|
|
743
|
+
const adminPages = parsePagesList(answers.pagesAdmin);
|
|
744
|
+
for (const pageName of adminPages) {
|
|
745
|
+
pages.push({
|
|
746
|
+
section: "admin",
|
|
747
|
+
path: `/${pageName}`,
|
|
748
|
+
name: pageName,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Tables
|
|
753
|
+
const tableNames = parseTablesList(answers.tables);
|
|
754
|
+
const tables: TableConfig[] = [];
|
|
755
|
+
|
|
756
|
+
for (const tableName of tableNames) {
|
|
757
|
+
// Déterminer dans quelles sections créer les APIs pour cette table
|
|
758
|
+
const sections: ("public" | "auth" | "admin")[] = [];
|
|
759
|
+
|
|
760
|
+
if (publicPages.length > 0) sections.push("public");
|
|
761
|
+
if (authPages.length > 0) sections.push("auth");
|
|
762
|
+
if (adminPages.length > 0) sections.push("admin");
|
|
763
|
+
|
|
764
|
+
// Si aucune page n'est définie, créer au moins les APIs auth
|
|
765
|
+
if (sections.length === 0) sections.push("auth");
|
|
766
|
+
|
|
767
|
+
tables.push({
|
|
768
|
+
name: tableName,
|
|
769
|
+
sections,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const config: ModuleConfig = {
|
|
774
|
+
slug,
|
|
775
|
+
moduleName,
|
|
776
|
+
pages,
|
|
777
|
+
tables,
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// Trouver le répertoire racine du workspace (chercher pnpm-workspace.yaml)
|
|
781
|
+
let rootDir = process.cwd();
|
|
782
|
+
let attempts = 0;
|
|
783
|
+
const maxAttempts = 5;
|
|
784
|
+
|
|
785
|
+
while (attempts < maxAttempts) {
|
|
786
|
+
const workspaceFile = path.join(rootDir, "pnpm-workspace.yaml");
|
|
787
|
+
if (fs.existsSync(workspaceFile)) {
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
rootDir = path.resolve(rootDir, "..");
|
|
791
|
+
attempts++;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (attempts === maxAttempts) {
|
|
795
|
+
console.error(chalk.red("❌ Impossible de trouver le répertoire racine du workspace (pnpm-workspace.yaml non trouvé)"));
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Créer le module
|
|
800
|
+
await createModuleStructure(config, rootDir);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Exécuter le script si appelé directement
|
|
804
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
805
|
+
createModule().catch((error) => {
|
|
806
|
+
console.error(chalk.red("❌ Erreur:"), error);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
});
|
|
809
|
+
}
|