create-gef 1.0.0
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/.github/workflows/release-please.yml +18 -0
- package/ENGINEERING_PLAYBOOK.md +395 -0
- package/PROJECT_CONFIG.template.md +34 -0
- package/README.md +229 -0
- package/ci-templates/main.yml +76 -0
- package/generator/index.js +700 -0
- package/hooks/commit-msg +27 -0
- package/hooks/pre-commit +46 -0
- package/hooks/pre-push +25 -0
- package/package.json +18 -0
- package/prompts/adr_writing.md +14 -0
- package/prompts/bugfix.md +15 -0
- package/prompts/code_review.md +14 -0
- package/prompts/feature_development.md +29 -0
- package/prompts/new_project_kickoff.md +14 -0
- package/prompts/system_prompt.md +24 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { execSync, spawnSync } from 'child_process';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const GEF_DIR = path.resolve(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
async function run() {
|
|
15
|
+
console.log(chalk.cyan.bold('\n🚀 Bienvenue dans le générateur GEF Intelligent\n'));
|
|
16
|
+
if (process.argv[2] === 'update') {
|
|
17
|
+
console.log(chalk.blue('Mise à jour du projet courant vers la dernière version du GEF...'));
|
|
18
|
+
if (!fs.existsSync('.git')) {
|
|
19
|
+
console.log(chalk.red('Erreur: Ce dossier ne semble pas être un dépôt Git valide.'));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// MAJ Playbook
|
|
24
|
+
if (fs.existsSync(path.join(GEF_DIR, 'ENGINEERING_PLAYBOOK.md'))) {
|
|
25
|
+
fs.mkdirSync('.gef', { recursive: true });
|
|
26
|
+
fs.copyFileSync(path.join(GEF_DIR, 'ENGINEERING_PLAYBOOK.md'), '.gef/ENGINEERING_PLAYBOOK.md');
|
|
27
|
+
console.log(chalk.green('✅ ENGINEERING_PLAYBOOK.md mis à jour.'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MAJ Prompts
|
|
31
|
+
if (fs.existsSync(path.join(GEF_DIR, 'prompts'))) {
|
|
32
|
+
fs.mkdirSync('.gef/prompts', { recursive: true });
|
|
33
|
+
fs.readdirSync(path.join(GEF_DIR, 'prompts')).forEach(p => {
|
|
34
|
+
fs.copyFileSync(path.join(GEF_DIR, 'prompts', p), path.join('.gef/prompts', p));
|
|
35
|
+
});
|
|
36
|
+
console.log(chalk.green('✅ Prompts IA mis à jour.'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MAJ Hooks
|
|
40
|
+
const hooksSrc = path.join(GEF_DIR, 'hooks');
|
|
41
|
+
if (fs.existsSync(hooksSrc)) {
|
|
42
|
+
const hooksDest = path.join(process.cwd(), '.git', 'hooks');
|
|
43
|
+
fs.cpSync(hooksSrc, hooksDest, { recursive: true });
|
|
44
|
+
try {
|
|
45
|
+
execSync(`chmod +x .git/hooks/commit-msg .git/hooks/pre-commit .git/hooks/pre-push`, { stdio: 'ignore' });
|
|
46
|
+
} catch(e) {}
|
|
47
|
+
console.log(chalk.green('✅ Hooks Git mis à jour.'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(chalk.green.bold('\n🎉 Projet mis à jour avec succès !'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const answers = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'projectName',
|
|
58
|
+
message: 'Quel est le nom de votre nouveau projet ?',
|
|
59
|
+
validate: input => input ? true : 'Le nom du projet est requis.'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'list',
|
|
63
|
+
name: 'phase',
|
|
64
|
+
message: 'Dans quelle phase se situe ce projet ?',
|
|
65
|
+
choices: ['Prototype (R&D)', 'Développement contractuel / Production']
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'list',
|
|
69
|
+
name: 'stack',
|
|
70
|
+
message: 'Quel Framework principal voulez-vous installer ?',
|
|
71
|
+
choices: [
|
|
72
|
+
'Next.js (React)',
|
|
73
|
+
'React (Vite)',
|
|
74
|
+
'API Node.js (Express)',
|
|
75
|
+
'API Python (FastAPI)',
|
|
76
|
+
'Projet vide'
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'list',
|
|
81
|
+
name: 'database',
|
|
82
|
+
message: 'Quelle Base de données principale ?',
|
|
83
|
+
choices: ['PostgreSQL', 'MongoDB', 'Supabase', 'Aucune']
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'list',
|
|
87
|
+
name: 'cloud',
|
|
88
|
+
message: 'Quel Cloud Provider principal ?',
|
|
89
|
+
choices: ['AWS', 'GCP', 'Azure', 'Vercel', 'Aucun']
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'confirm',
|
|
93
|
+
name: 'includeDocker',
|
|
94
|
+
message: 'Voulez-vous préparer un dossier Docker ?',
|
|
95
|
+
default: true,
|
|
96
|
+
when: (ans) => ans.cloud !== 'Vercel'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'confirm',
|
|
100
|
+
name: 'includeCI',
|
|
101
|
+
message: 'Voulez-vous inclure le template de CI/CD GEF ?',
|
|
102
|
+
default: true
|
|
103
|
+
}
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const { projectName, phase, stack, database, cloud, includeDocker, includeCI } = answers;
|
|
107
|
+
const projectPath = path.resolve(process.cwd(), projectName);
|
|
108
|
+
|
|
109
|
+
if (fs.existsSync(projectPath)) {
|
|
110
|
+
console.log(chalk.red(`\nErreur: Le dossier "${projectName}" existe déjà.`));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(chalk.blue(`\nCréation du dossier projet : ${projectPath}`));
|
|
115
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
116
|
+
process.chdir(projectPath);
|
|
117
|
+
|
|
118
|
+
// --- SCAFFOLDING INTELLIGENT ---
|
|
119
|
+
if (stack !== 'Projet vide') {
|
|
120
|
+
try {
|
|
121
|
+
if (stack === 'Next.js (React)') {
|
|
122
|
+
console.log(chalk.magenta(`\n▲ Lancement de create-next-app — répondez aux questions ci-dessous :\n`));
|
|
123
|
+
// On sort du dossier créé : create-next-app crée lui-même le sous-dossier
|
|
124
|
+
process.chdir(path.resolve(projectPath, '..'));
|
|
125
|
+
fs.rmdirSync(projectPath); // on enlève le dossier vide pour laisser create-next-app le créer
|
|
126
|
+
execSync(`npx create-next-app@latest ${projectName}`, { stdio: 'inherit' });
|
|
127
|
+
process.chdir(projectPath);
|
|
128
|
+
}
|
|
129
|
+
else if (stack === 'React (Vite)') {
|
|
130
|
+
console.log(chalk.magenta(`\n⚡ Lancement de create-vite — répondez aux questions ci-dessous :\n`));
|
|
131
|
+
process.chdir(path.resolve(projectPath, '..'));
|
|
132
|
+
fs.rmdirSync(projectPath);
|
|
133
|
+
execSync(`npm create vite@latest ${projectName}`, { stdio: 'inherit' });
|
|
134
|
+
process.chdir(projectPath);
|
|
135
|
+
// Vite ne lance pas npm install automatiquement
|
|
136
|
+
console.log(chalk.yellow('\nInstallation des dépendances...'));
|
|
137
|
+
execSync('npm install', { stdio: 'inherit' });
|
|
138
|
+
}
|
|
139
|
+
else if (stack === 'API Node.js (Express)') {
|
|
140
|
+
console.log(chalk.magenta(`\n📦 Initialisation de l'API Node.js...\n`));
|
|
141
|
+
execSync('npm init -y', { stdio: 'ignore' });
|
|
142
|
+
execSync('npm install express cors dotenv', { stdio: 'inherit' });
|
|
143
|
+
if (!fs.existsSync('src')) fs.mkdirSync('src');
|
|
144
|
+
fs.writeFileSync('src/index.js', `const express = require('express');
|
|
145
|
+
const cors = require('cors');
|
|
146
|
+
require('dotenv').config();
|
|
147
|
+
|
|
148
|
+
const app = express();
|
|
149
|
+
const PORT = process.env.PORT || 3000;
|
|
150
|
+
|
|
151
|
+
app.use(cors());
|
|
152
|
+
app.use(express.json());
|
|
153
|
+
|
|
154
|
+
app.get('/', (req, res) => res.json({ message: 'API prête.' }));
|
|
155
|
+
|
|
156
|
+
app.listen(PORT, () => console.log(\`Serveur lancé sur http://localhost:\${PORT}\`));
|
|
157
|
+
`);
|
|
158
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
159
|
+
pkg.scripts = { start: 'node src/index.js', dev: 'node --watch src/index.js' };
|
|
160
|
+
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
|
161
|
+
}
|
|
162
|
+
else if (stack === 'API Python (FastAPI)') {
|
|
163
|
+
console.log(chalk.magenta(`\n🐍 Initialisation de l'API Python (FastAPI)...\n`));
|
|
164
|
+
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
|
|
165
|
+
execSync(`${pythonCmd} -m venv venv`, { stdio: 'inherit' });
|
|
166
|
+
fs.writeFileSync('requirements.txt', 'fastapi\nuvicorn[standard]\npython-dotenv\n');
|
|
167
|
+
if (!fs.existsSync('src')) fs.mkdirSync('src');
|
|
168
|
+
fs.writeFileSync('src/__init__.py', '');
|
|
169
|
+
fs.writeFileSync('src/main.py', `from fastapi import FastAPI
|
|
170
|
+
|
|
171
|
+
app = FastAPI(title="${projectName}")
|
|
172
|
+
|
|
173
|
+
@app.get("/")
|
|
174
|
+
def read_root():
|
|
175
|
+
return {"message": "API prête."}
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
console.log(chalk.green('\n✅ Framework installé.'));
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.log(chalk.red('\nErreur lors du scaffolding. Continuation avec la structure de base GEF.'));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- SURCOUCHE GEF ---
|
|
185
|
+
console.log(chalk.yellow('\n📁 Application de la surcouche GEF (Architecture & Config)...'));
|
|
186
|
+
|
|
187
|
+
// Création des dossiers manquants
|
|
188
|
+
const dirs = [
|
|
189
|
+
'docs/adr', 'docs/research', 'src', 'tests', 'scripts',
|
|
190
|
+
'assets', 'infra', 'database'
|
|
191
|
+
];
|
|
192
|
+
if (includeCI) dirs.push('.github/workflows');
|
|
193
|
+
|
|
194
|
+
dirs.forEach(d => {
|
|
195
|
+
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Template ADR
|
|
199
|
+
const adrTemplatePath = 'docs/adr/0000-template.md';
|
|
200
|
+
if (!fs.existsSync(adrTemplatePath)) {
|
|
201
|
+
fs.writeFileSync(adrTemplatePath, `# Titre de l'Architecture Decision Record\n\n## Contexte\nPourquoi prenons-nous cette décision ?\n\n## Décision\nQu'avons-nous décidé de faire ?\n\n## Conséquences\nQuels sont les impacts positifs et négatifs ?`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Playwright natif (Test E2E)
|
|
205
|
+
if (stack.includes('React') || stack.includes('Next')) {
|
|
206
|
+
console.log(chalk.yellow('🎭 Installation de Playwright pour le TDD E2E...'));
|
|
207
|
+
try {
|
|
208
|
+
execSync('npm init playwright@latest --yes -- --quiet --browser=chromium --browser=firefox --browser=webkit', { stdio: 'ignore' });
|
|
209
|
+
console.log(chalk.green('✅ Playwright configuré.'));
|
|
210
|
+
} catch(err) {
|
|
211
|
+
console.log(chalk.red('Impossible d\'installer Playwright automatiquement.'));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- DOCKER INTELLIGENT ---
|
|
216
|
+
if (includeDocker) {
|
|
217
|
+
console.log(chalk.yellow('🐳 Génération des fichiers Docker adaptés à votre stack...'));
|
|
218
|
+
if (!fs.existsSync('docker')) fs.mkdirSync('docker');
|
|
219
|
+
|
|
220
|
+
// .dockerignore (toujours présent)
|
|
221
|
+
const dockerIgnoreLines = [
|
|
222
|
+
'node_modules', 'npm-debug.log', '.git', '.gitignore',
|
|
223
|
+
'.env', '*.log', 'dist', 'build', '__pycache__', 'venv', '.venv'
|
|
224
|
+
];
|
|
225
|
+
fs.writeFileSync('.dockerignore', dockerIgnoreLines.join('\n') + '\n');
|
|
226
|
+
|
|
227
|
+
if (stack === 'Next.js (React)') {
|
|
228
|
+
fs.writeFileSync('docker/Dockerfile', `# Étape 1 : Build
|
|
229
|
+
FROM node:20-alpine AS builder
|
|
230
|
+
WORKDIR /app
|
|
231
|
+
COPY package*.json ./
|
|
232
|
+
RUN npm ci
|
|
233
|
+
COPY . .
|
|
234
|
+
RUN npm run build
|
|
235
|
+
|
|
236
|
+
# Étape 2 : Production
|
|
237
|
+
FROM node:20-alpine AS runner
|
|
238
|
+
WORKDIR /app
|
|
239
|
+
ENV NODE_ENV=production
|
|
240
|
+
COPY --from=builder /app/.next/standalone ./
|
|
241
|
+
COPY --from=builder /app/.next/static ./.next/static
|
|
242
|
+
COPY --from=builder /app/public ./public
|
|
243
|
+
EXPOSE 3000
|
|
244
|
+
CMD ["node", "server.js"]
|
|
245
|
+
`);
|
|
246
|
+
fs.writeFileSync('docker/docker-compose.yml', `version: '3.8'
|
|
247
|
+
services:
|
|
248
|
+
app:
|
|
249
|
+
build:
|
|
250
|
+
context: ..
|
|
251
|
+
dockerfile: docker/Dockerfile
|
|
252
|
+
ports:
|
|
253
|
+
- "3000:3000"
|
|
254
|
+
environment:
|
|
255
|
+
- NODE_ENV=production
|
|
256
|
+
`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
else if (stack === 'React (Vite)') {
|
|
260
|
+
fs.writeFileSync('docker/Dockerfile', `# Étape 1 : Build
|
|
261
|
+
FROM node:20-alpine AS builder
|
|
262
|
+
WORKDIR /app
|
|
263
|
+
COPY package*.json ./
|
|
264
|
+
RUN npm ci
|
|
265
|
+
COPY . .
|
|
266
|
+
RUN npm run build
|
|
267
|
+
|
|
268
|
+
# Étape 2 : Serveur statique Nginx
|
|
269
|
+
FROM nginx:alpine AS runner
|
|
270
|
+
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
271
|
+
EXPOSE 80
|
|
272
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
273
|
+
`);
|
|
274
|
+
fs.writeFileSync('docker/docker-compose.yml', `version: '3.8'
|
|
275
|
+
services:
|
|
276
|
+
app:
|
|
277
|
+
build:
|
|
278
|
+
context: ..
|
|
279
|
+
dockerfile: docker/Dockerfile
|
|
280
|
+
ports:
|
|
281
|
+
- "80:80"
|
|
282
|
+
`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
else if (stack === 'API Node.js (Express)') {
|
|
286
|
+
fs.writeFileSync('docker/Dockerfile', `FROM node:20-alpine
|
|
287
|
+
WORKDIR /app
|
|
288
|
+
COPY package*.json ./
|
|
289
|
+
RUN npm ci --only=production
|
|
290
|
+
COPY . .
|
|
291
|
+
EXPOSE 3000
|
|
292
|
+
CMD ["node", "src/index.js"]
|
|
293
|
+
`);
|
|
294
|
+
// docker-compose avec DB optionnelle
|
|
295
|
+
let dbService = '';
|
|
296
|
+
if (database === 'PostgreSQL') {
|
|
297
|
+
dbService = `
|
|
298
|
+
db:
|
|
299
|
+
image: postgres:16-alpine
|
|
300
|
+
environment:
|
|
301
|
+
POSTGRES_USER: user
|
|
302
|
+
POSTGRES_PASSWORD: password
|
|
303
|
+
POSTGRES_DB: ${projectName.toLowerCase().replace(/\s+/g, '_')}
|
|
304
|
+
ports:
|
|
305
|
+
- "5432:5432"
|
|
306
|
+
volumes:
|
|
307
|
+
- db_data:/var/lib/postgresql/data
|
|
308
|
+
|
|
309
|
+
volumes:
|
|
310
|
+
db_data:`;
|
|
311
|
+
} else if (database === 'MongoDB') {
|
|
312
|
+
dbService = `
|
|
313
|
+
db:
|
|
314
|
+
image: mongo:7
|
|
315
|
+
ports:
|
|
316
|
+
- "27017:27017"
|
|
317
|
+
volumes:
|
|
318
|
+
- db_data:/data/db
|
|
319
|
+
|
|
320
|
+
volumes:
|
|
321
|
+
db_data:`;
|
|
322
|
+
}
|
|
323
|
+
fs.writeFileSync('docker/docker-compose.yml', `version: '3.8'
|
|
324
|
+
services:
|
|
325
|
+
app:
|
|
326
|
+
build:
|
|
327
|
+
context: ..
|
|
328
|
+
dockerfile: docker/Dockerfile
|
|
329
|
+
ports:
|
|
330
|
+
- "3000:3000"
|
|
331
|
+
env_file:
|
|
332
|
+
- ../.env
|
|
333
|
+
depends_on:
|
|
334
|
+
- db${dbService}
|
|
335
|
+
`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
else if (stack === 'API Python (FastAPI)') {
|
|
339
|
+
fs.writeFileSync('docker/Dockerfile', `FROM python:3.12-slim
|
|
340
|
+
WORKDIR /app
|
|
341
|
+
COPY requirements.txt ./
|
|
342
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
343
|
+
COPY . .
|
|
344
|
+
EXPOSE 8000
|
|
345
|
+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
346
|
+
`);
|
|
347
|
+
let dbService = '';
|
|
348
|
+
if (database === 'PostgreSQL') {
|
|
349
|
+
dbService = `
|
|
350
|
+
db:
|
|
351
|
+
image: postgres:16-alpine
|
|
352
|
+
environment:
|
|
353
|
+
POSTGRES_USER: user
|
|
354
|
+
POSTGRES_PASSWORD: password
|
|
355
|
+
POSTGRES_DB: ${projectName.toLowerCase().replace(/\s+/g, '_')}
|
|
356
|
+
ports:
|
|
357
|
+
- "5432:5432"
|
|
358
|
+
volumes:
|
|
359
|
+
- db_data:/var/lib/postgresql/data
|
|
360
|
+
|
|
361
|
+
volumes:
|
|
362
|
+
db_data:`;
|
|
363
|
+
} else if (database === 'MongoDB') {
|
|
364
|
+
dbService = `
|
|
365
|
+
db:
|
|
366
|
+
image: mongo:7
|
|
367
|
+
ports:
|
|
368
|
+
- "27017:27017"
|
|
369
|
+
volumes:
|
|
370
|
+
- db_data:/data/db
|
|
371
|
+
|
|
372
|
+
volumes:
|
|
373
|
+
db_data:`;
|
|
374
|
+
}
|
|
375
|
+
fs.writeFileSync('docker/docker-compose.yml', `version: '3.8'
|
|
376
|
+
services:
|
|
377
|
+
app:
|
|
378
|
+
build:
|
|
379
|
+
context: ..
|
|
380
|
+
dockerfile: docker/Dockerfile
|
|
381
|
+
ports:
|
|
382
|
+
- "8000:8000"
|
|
383
|
+
env_file:
|
|
384
|
+
- ../.env
|
|
385
|
+
depends_on:
|
|
386
|
+
- db${dbService}
|
|
387
|
+
`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
else {
|
|
391
|
+
// Projet vide — Dockerfile générique
|
|
392
|
+
fs.writeFileSync('docker/Dockerfile', `FROM alpine:latest
|
|
393
|
+
WORKDIR /app
|
|
394
|
+
COPY . .
|
|
395
|
+
CMD ["sh"]
|
|
396
|
+
`);
|
|
397
|
+
}
|
|
398
|
+
console.log(chalk.green('✅ Fichiers Docker générés.'));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// --- VERCEL ---
|
|
402
|
+
if (cloud === 'Vercel') {
|
|
403
|
+
console.log(chalk.yellow('▲ Génération de la configuration Vercel...'));
|
|
404
|
+
const isApi = stack.includes('Node') || stack.includes('Python');
|
|
405
|
+
const vercelConfig = {
|
|
406
|
+
version: 2,
|
|
407
|
+
name: projectName,
|
|
408
|
+
...(isApi ? {
|
|
409
|
+
builds: [{ src: 'src/index.js', use: '@vercel/node' }],
|
|
410
|
+
routes: [{ src: '/(.*)', dest: 'src/index.js' }]
|
|
411
|
+
} : {}),
|
|
412
|
+
regions: ['cdg1']
|
|
413
|
+
};
|
|
414
|
+
fs.writeFileSync('vercel.json', JSON.stringify(vercelConfig, null, 2));
|
|
415
|
+
console.log(chalk.green('✅ vercel.json généré.'));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// --- SUPABASE ---
|
|
419
|
+
if (database === 'Supabase') {
|
|
420
|
+
console.log(chalk.yellow('⚡ Génération de la configuration Supabase...'));
|
|
421
|
+
fs.mkdirSync('supabase/migrations', { recursive: true });
|
|
422
|
+
fs.writeFileSync('supabase/config.toml', `[api]
|
|
423
|
+
enabled = true
|
|
424
|
+
port = 54321
|
|
425
|
+
schemas = ["public", "storage", "graphql_public"]
|
|
426
|
+
|
|
427
|
+
[db]
|
|
428
|
+
port = 54322
|
|
429
|
+
shadow_port = 54320
|
|
430
|
+
major_version = 15
|
|
431
|
+
|
|
432
|
+
[studio]
|
|
433
|
+
enabled = true
|
|
434
|
+
port = 54323
|
|
435
|
+
|
|
436
|
+
[auth]
|
|
437
|
+
enabled = true
|
|
438
|
+
site_url = "http://localhost:3000"
|
|
439
|
+
|
|
440
|
+
[storage]
|
|
441
|
+
enabled = true
|
|
442
|
+
`);
|
|
443
|
+
fs.writeFileSync('supabase/migrations/.gitkeep', '');
|
|
444
|
+
fs.writeFileSync('supabase/seed.sql', `-- Seed data pour ${projectName}
|
|
445
|
+
-- Ajoutez ici vos données initiales
|
|
446
|
+
`);
|
|
447
|
+
console.log(chalk.green('✅ Dossier supabase/ généré (config.toml + migrations/).'));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Mise à jour du README existant ou création
|
|
451
|
+
const readmeHeader = `# ${projectName}\n\n## Fonctionnalités\n<À COMPLÉTER>\n\n## Installation\n<À COMPLÉTER>\n\n## Architecture\nStack: ${stack}\nCloud: ${cloud}\nDB: ${database}\n`;
|
|
452
|
+
if (fs.existsSync('README.md')) {
|
|
453
|
+
const existing = fs.readFileSync('README.md', 'utf8');
|
|
454
|
+
fs.writeFileSync('README.md', readmeHeader + '\n---\n*Généré initialement par le framework:*\n' + existing);
|
|
455
|
+
} else {
|
|
456
|
+
fs.writeFileSync('README.md', readmeHeader);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// PROJECT_CONFIG.md
|
|
460
|
+
let configContent = `# PROJECT_CONFIG.md\n- Projet : ${projectName}\n- Langage : ${stack}\n`;
|
|
461
|
+
const templatePath = path.join(GEF_DIR, 'PROJECT_CONFIG.template.md');
|
|
462
|
+
if (fs.existsSync(templatePath)) {
|
|
463
|
+
let tpl = fs.readFileSync(templatePath, 'utf-8');
|
|
464
|
+
tpl = tpl.replace(/{{PROJECT_NAME}}/g, projectName);
|
|
465
|
+
tpl = tpl.replace(/{{STACK}}/g, stack);
|
|
466
|
+
tpl = tpl.replace(/{{PHASE}}/g, phase);
|
|
467
|
+
tpl = tpl.replace(/{{DATABASE}}/g, database);
|
|
468
|
+
tpl = tpl.replace(/{{CLOUD}}/g, cloud);
|
|
469
|
+
const dateStr = new Date().toLocaleString('fr-FR', { month: 'long', year: 'numeric' });
|
|
470
|
+
tpl = tpl.replace(/{{DATE}}/g, dateStr);
|
|
471
|
+
configContent = tpl;
|
|
472
|
+
}
|
|
473
|
+
fs.writeFileSync('PROJECT_CONFIG.md', configContent);
|
|
474
|
+
|
|
475
|
+
// RESEARCH_LOG.md
|
|
476
|
+
if (!fs.existsSync('docs/research/RESEARCH_LOG.md')) {
|
|
477
|
+
fs.writeFileSync('docs/research/RESEARCH_LOG.md', `# Research Log — Journal de bord scientifique\n> Documentez ici la résolution des bugs bloquants.\n\n## 1. <Titre>\n- **Contexte :** \n- **Cause :** \n- **Résolution :** \n`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// .gitignore dynamique additionnel
|
|
481
|
+
let gitignoreAdditions = `\n# GEF Standard\n.env\n.DS_Store\n`;
|
|
482
|
+
if (stack.includes('Python')) gitignoreAdditions += `__pycache__/\nvenv/\n.venv/\n.pytest_cache/\n`;
|
|
483
|
+
if (fs.existsSync('.gitignore')) {
|
|
484
|
+
fs.appendFileSync('.gitignore', gitignoreAdditions);
|
|
485
|
+
} else {
|
|
486
|
+
fs.writeFileSync('.gitignore', gitignoreAdditions);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Copie de la documentation GEF locale (.gef/)
|
|
490
|
+
console.log(chalk.cyan('📚 Ajout du Playbook et des Prompts IA locaux...'));
|
|
491
|
+
fs.mkdirSync('.gef/prompts', { recursive: true });
|
|
492
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
493
|
+
if (fs.existsSync(path.join(rootDir, 'ENGINEERING_PLAYBOOK.md'))) {
|
|
494
|
+
fs.copyFileSync(path.join(rootDir, 'ENGINEERING_PLAYBOOK.md'), '.gef/ENGINEERING_PLAYBOOK.md');
|
|
495
|
+
}
|
|
496
|
+
if (fs.existsSync(path.join(rootDir, 'prompts'))) {
|
|
497
|
+
const prompts = fs.readdirSync(path.join(rootDir, 'prompts'));
|
|
498
|
+
prompts.forEach(p => {
|
|
499
|
+
fs.copyFileSync(path.join(rootDir, 'prompts', p), path.join('.gef/prompts', p));
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Git et Hooks
|
|
504
|
+
console.log(chalk.yellow('🔗 Initialisation Git et installation des hooks de sécurité...'));
|
|
505
|
+
try {
|
|
506
|
+
if (!fs.existsSync('.git')) {
|
|
507
|
+
execSync('git init', { stdio: 'ignore' });
|
|
508
|
+
}
|
|
509
|
+
const hooksSrc = path.join(GEF_DIR, 'hooks');
|
|
510
|
+
if (fs.existsSync(hooksSrc)) {
|
|
511
|
+
const hooksDest = path.join(process.cwd(), '.git', 'hooks');
|
|
512
|
+
fs.cpSync(hooksSrc, hooksDest, { recursive: true });
|
|
513
|
+
try {
|
|
514
|
+
execSync(`chmod +x .git/hooks/commit-msg .git/hooks/pre-commit .git/hooks/pre-push`, { stdio: 'ignore' });
|
|
515
|
+
} catch (e) { /* ignore on windows */ }
|
|
516
|
+
}
|
|
517
|
+
} catch (err) {
|
|
518
|
+
console.log(chalk.red('Erreur lors de l\'installation des hooks Git.'));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// CI/CD — Généré selon la stack
|
|
522
|
+
if (includeCI) {
|
|
523
|
+
console.log(chalk.yellow('🤖 Génération du pipeline CI/CD adapté à votre stack...'));
|
|
524
|
+
if (!fs.existsSync('.github/workflows')) fs.mkdirSync('.github/workflows', { recursive: true });
|
|
525
|
+
|
|
526
|
+
const isNode = stack.includes('Node') || stack.includes('React') || stack.includes('Next');
|
|
527
|
+
const isPython = stack.includes('Python');
|
|
528
|
+
|
|
529
|
+
// Bloc setup runtime
|
|
530
|
+
const setupRuntime = isNode
|
|
531
|
+
? ` - name: Setup Node.js
|
|
532
|
+
uses: actions/setup-node@v4
|
|
533
|
+
with:
|
|
534
|
+
node-version: '20'
|
|
535
|
+
cache: 'npm'
|
|
536
|
+
|
|
537
|
+
- name: Installer les dépendances
|
|
538
|
+
run: npm ci`
|
|
539
|
+
: isPython
|
|
540
|
+
? ` - name: Setup Python
|
|
541
|
+
uses: actions/setup-python@v5
|
|
542
|
+
with:
|
|
543
|
+
python-version: '3.12'
|
|
544
|
+
cache: 'pip'
|
|
545
|
+
|
|
546
|
+
- name: Installer les dépendances
|
|
547
|
+
run: pip install -r requirements.txt`
|
|
548
|
+
: ` - name: Setup
|
|
549
|
+
run: echo "Configurez votre runtime ici"`;
|
|
550
|
+
|
|
551
|
+
// Bloc tests
|
|
552
|
+
const testBlock = isNode
|
|
553
|
+
? ` - name: Lint
|
|
554
|
+
run: npm run lint --if-present
|
|
555
|
+
|
|
556
|
+
- name: Tests
|
|
557
|
+
run: npm test --if-present`
|
|
558
|
+
: isPython
|
|
559
|
+
? ` - name: Lint (flake8)
|
|
560
|
+
run: |
|
|
561
|
+
pip install flake8
|
|
562
|
+
flake8 src/ --max-line-length=120 --ignore=E501 || true
|
|
563
|
+
|
|
564
|
+
- name: Tests (pytest)
|
|
565
|
+
run: |
|
|
566
|
+
pip install pytest
|
|
567
|
+
pytest tests/ -v || true`
|
|
568
|
+
: ` - name: Tests
|
|
569
|
+
run: echo "Ajoutez vos commandes de tests ici"`;
|
|
570
|
+
|
|
571
|
+
// Bloc déploiement
|
|
572
|
+
let deployBlock = '';
|
|
573
|
+
if (cloud === 'Vercel') {
|
|
574
|
+
deployBlock = `
|
|
575
|
+
deploy:
|
|
576
|
+
name: Déploiement Vercel
|
|
577
|
+
needs: quality-gate
|
|
578
|
+
if: github.ref == 'refs/heads/main'
|
|
579
|
+
runs-on: ubuntu-latest
|
|
580
|
+
steps:
|
|
581
|
+
- uses: actions/checkout@v4
|
|
582
|
+
- name: Deploy to Vercel
|
|
583
|
+
uses: amondnet/vercel-action@v25
|
|
584
|
+
with:
|
|
585
|
+
vercel-token: \${{ secrets.VERCEL_TOKEN }}
|
|
586
|
+
vercel-org-id: \${{ secrets.VERCEL_ORG_ID }}
|
|
587
|
+
vercel-project-id: \${{ secrets.VERCEL_PROJECT_ID }}
|
|
588
|
+
vercel-args: '--prod'`;
|
|
589
|
+
} else if (cloud === 'AWS') {
|
|
590
|
+
deployBlock = `
|
|
591
|
+
deploy:
|
|
592
|
+
name: Déploiement AWS
|
|
593
|
+
needs: quality-gate
|
|
594
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
595
|
+
runs-on: ubuntu-latest
|
|
596
|
+
steps:
|
|
597
|
+
- uses: actions/checkout@v4
|
|
598
|
+
- name: Configure AWS credentials
|
|
599
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
600
|
+
with:
|
|
601
|
+
aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
602
|
+
aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
603
|
+
aws-region: eu-west-3
|
|
604
|
+
- name: Deploy
|
|
605
|
+
run: echo "Ajoutez ici votre commande de déploiement AWS (ex: eb deploy, ecs update, etc.)"`;
|
|
606
|
+
} else {
|
|
607
|
+
deployBlock = `
|
|
608
|
+
release:
|
|
609
|
+
name: Release Automatique
|
|
610
|
+
needs: quality-gate
|
|
611
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
612
|
+
runs-on: ubuntu-latest
|
|
613
|
+
steps:
|
|
614
|
+
- uses: actions/checkout@v4
|
|
615
|
+
- name: Create GitHub Release
|
|
616
|
+
uses: softprops/action-gh-release@v1
|
|
617
|
+
with:
|
|
618
|
+
generate_release_notes: true`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const ciContent = `name: GEF CI/CD — ${projectName}
|
|
622
|
+
|
|
623
|
+
on:
|
|
624
|
+
push:
|
|
625
|
+
branches:
|
|
626
|
+
- main
|
|
627
|
+
- 'feat/**'
|
|
628
|
+
- 'fix/**'
|
|
629
|
+
tags:
|
|
630
|
+
- 'v*.*.*'
|
|
631
|
+
pull_request:
|
|
632
|
+
branches:
|
|
633
|
+
- main
|
|
634
|
+
|
|
635
|
+
jobs:
|
|
636
|
+
quality-gate:
|
|
637
|
+
name: Contrôle Qualité
|
|
638
|
+
runs-on: ubuntu-latest
|
|
639
|
+
steps:
|
|
640
|
+
- name: Checkout code
|
|
641
|
+
uses: actions/checkout@v4
|
|
642
|
+
|
|
643
|
+
${setupRuntime}
|
|
644
|
+
|
|
645
|
+
${testBlock}
|
|
646
|
+
|
|
647
|
+
- name: Analyse de sécurité
|
|
648
|
+
run: echo "Ajoutez ici Trivy, Gitleaks ou équivalent"
|
|
649
|
+
${deployBlock}
|
|
650
|
+
`;
|
|
651
|
+
fs.writeFileSync('.github/workflows/main.yml', ciContent);
|
|
652
|
+
console.log(chalk.green('✅ Pipeline CI/CD généré.'));
|
|
653
|
+
|
|
654
|
+
console.log(chalk.yellow('📦 Intégration native de release-please...'));
|
|
655
|
+
const releasePleaseContent = `on:
|
|
656
|
+
push:
|
|
657
|
+
branches:
|
|
658
|
+
- main
|
|
659
|
+
|
|
660
|
+
permissions:
|
|
661
|
+
contents: write
|
|
662
|
+
pull-requests: write
|
|
663
|
+
|
|
664
|
+
name: release-please
|
|
665
|
+
|
|
666
|
+
jobs:
|
|
667
|
+
release-please:
|
|
668
|
+
runs-on: ubuntu-latest
|
|
669
|
+
steps:
|
|
670
|
+
- uses: googleapis/release-please-action@v4
|
|
671
|
+
with:
|
|
672
|
+
release-type: node
|
|
673
|
+
`;
|
|
674
|
+
fs.writeFileSync('.github/workflows/release-please.yml', releasePleaseContent);
|
|
675
|
+
console.log(chalk.green('✅ Workflow release-please.yml généré.'));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!fs.existsSync('CHANGELOG.md')) fs.writeFileSync('CHANGELOG.md', '');
|
|
679
|
+
if (!fs.existsSync('LICENSE')) fs.writeFileSync('LICENSE', '');
|
|
680
|
+
|
|
681
|
+
console.log(chalk.green.bold(`\n✅ Projet "${projectName}" scaffoldé avec succès !`));
|
|
682
|
+
console.log(chalk.cyan(`\nPour commencer :`));
|
|
683
|
+
console.log(chalk.white(` cd ${projectName}`));
|
|
684
|
+
if (stack.includes('React') || stack.includes('Node')) {
|
|
685
|
+
console.log(chalk.white(` npm run dev`));
|
|
686
|
+
} else if (stack.includes('Python')) {
|
|
687
|
+
console.log(chalk.white(` .\\venv\\Scripts\\activate (ou source venv/bin/activate)`));
|
|
688
|
+
console.log(chalk.white(` uvicorn src.main:app --reload`));
|
|
689
|
+
}
|
|
690
|
+
console.log(chalk.white(`\nN'oubliez pas d'éditer PROJECT_CONFIG.md et de faire votre premier commit !`));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Verrou anti-double-exécution (npm charge parfois le fichier via 'main' ET via 'bin')
|
|
694
|
+
if (!process.env._GEF_RUNNING) {
|
|
695
|
+
process.env._GEF_RUNNING = '1';
|
|
696
|
+
run().catch(err => {
|
|
697
|
+
console.error(chalk.red('Une erreur est survenue :'), err);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
});
|
|
700
|
+
}
|