nestgen-cli 0.0.7 → 0.2.1
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/engine/architecture-profile.js +10 -0
- package/dist/engine/file-transaction.js +166 -0
- package/dist/engine/generated-code.js +26 -0
- package/dist/engine/generation-manifest.js +118 -0
- package/dist/engine/migration-plan.js +78 -0
- package/dist/engine/module-generator.js +157 -0
- package/dist/engine/project-config.js +42 -0
- package/dist/engine/project-path.js +25 -0
- package/dist/engine/project-preflight.js +96 -0
- package/dist/engine/resource-generator.js +1153 -0
- package/dist/engine/resource-spec.js +287 -0
- package/examples/store-api/README.md +76 -0
- package/examples/store-api/definitions/customers.resource.json +14 -0
- package/examples/store-api/definitions/order-items.resource.json +26 -0
- package/examples/store-api/definitions/orders.resource.json +22 -0
- package/examples/store-api/definitions/products.resource.json +19 -0
- package/examples/store-api/nestgen.example.json +7 -0
- package/nestgen.js +700 -86
- package/nestjs-generator/features/add_module.sh +68 -24
- package/nestjs-generator/features/configure_bootstrap.mjs +42 -0
- package/nestjs-generator/features/docker.sh +169 -36
- package/nestjs-generator/features/git.sh +4 -2
- package/nestjs-generator/features/inject_module_to_app.sh +22 -69
- package/nestjs-generator/features/operational_foundation.mjs +210 -0
- package/nestjs-generator/features/operations.sh +16 -0
- package/nestjs-generator/features/preflight.mjs +13 -0
- package/nestjs-generator/features/prisma.sh +48 -8
- package/nestjs-generator/features/resource_name.mjs +57 -0
- package/nestjs-generator/features/swagger.sh +7 -20
- package/nestjs-generator/features/typeorm.sh +68 -7
- package/nestjs-generator/features/update_app_module.mjs +146 -0
- package/nestjs-generator/features/utils.sh +52 -14
- package/nestjs-generator/generate_project.sh +46 -70
- package/package.json +63 -31
- package/readme.md +298 -120
package/nestgen.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
+
import { inspectProject } from './nestjs-generator/features/preflight.mjs';
|
|
10
|
+
import { generateModule, planModuleGeneration } from './dist/engine/module-generator.js';
|
|
11
|
+
import { generateResource, planResourceGeneration } from './dist/engine/resource-generator.js';
|
|
12
|
+
import {
|
|
13
|
+
parseResourceFields,
|
|
14
|
+
parseResourceIndexes,
|
|
15
|
+
parseResourceListOptions,
|
|
16
|
+
parseResourceRelations,
|
|
17
|
+
} from './dist/engine/resource-spec.js';
|
|
18
|
+
import { configFileName, defaultConfig, loadConfigIfPresent, writeConfig } from './dist/engine/project-config.js';
|
|
19
|
+
import { parseArchitectureProfile } from './dist/engine/architecture-profile.js';
|
|
20
|
+
import { planResourceMigration } from './dist/engine/migration-plan.js';
|
|
9
21
|
|
|
10
22
|
// ────── Resolve __dirname compatible ES Modules
|
|
11
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -15,30 +27,216 @@ const __dirname = path.dirname(__filename);
|
|
|
15
27
|
const ROOT_PATH = process.env.NESTGEN_ROOT || path.resolve(__dirname, './nestjs-generator');
|
|
16
28
|
const GENERATE_SCRIPT = path.join(ROOT_PATH, 'generate_project.sh');
|
|
17
29
|
const ADD_MODULE_SCRIPT = path.join(ROOT_PATH, './features/add_module.sh');
|
|
30
|
+
const SUPPORTED_ORMS = new Set(['typeorm', 'prisma']);
|
|
31
|
+
const MODULE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/i;
|
|
18
32
|
|
|
19
33
|
// ────── Logo CLI
|
|
20
34
|
function printLogo() {
|
|
21
|
-
console.log(
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.magentaBright(`
|
|
22
37
|
███╗ ██╗███████╗███████╗████████╗ ██████╗ ███████╗███╗ ██╗
|
|
23
38
|
████╗ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝ ██╔════╝████╗ ██║
|
|
24
39
|
██╔██╗ ██║█████╗ ███████╗ ██║ ██║ ███╗█████╗ ██╔██╗ ██║
|
|
25
40
|
██║╚██╗██║██╔══╝ ╚════██║ ██║ ██║ ██║██╔══╝ ██║╚██╗██║
|
|
26
41
|
██║ ╚████║███████╗███████║ ██║ ╚██████╔╝███████╗██║ ╚████║
|
|
27
42
|
╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝
|
|
28
|
-
`)
|
|
43
|
+
`),
|
|
44
|
+
);
|
|
29
45
|
console.log(chalk.cyan.bold('✨ NestGen CLI — Générateur modulaire NestJS'));
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
// ────── Helpers
|
|
33
|
-
function isNestProject() {
|
|
49
|
+
export function isNestProject() {
|
|
34
50
|
return fs.existsSync(path.resolve('./src/app.module.ts'));
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
function
|
|
38
|
-
const moduleName =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
export function validateModuleName(value) {
|
|
54
|
+
const moduleName = String(value ?? '').trim();
|
|
55
|
+
|
|
56
|
+
if (!MODULE_NAME_PATTERN.test(moduleName)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'Le nom du module doit commencer par une lettre et ne contenir que des lettres, chiffres, tirets ou underscores.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return moduleName.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateOrm(value) {
|
|
66
|
+
const orm = String(value ?? '')
|
|
67
|
+
.trim()
|
|
68
|
+
.toLowerCase();
|
|
69
|
+
|
|
70
|
+
if (!SUPPORTED_ORMS.has(orm)) {
|
|
71
|
+
throw new Error(`ORM non supporté : ${value}. Valeurs acceptées : ${[...SUPPORTED_ORMS].join(', ')}.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return orm;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validatePackageManager(value) {
|
|
78
|
+
if (!['npm', 'pnpm', 'yarn'].includes(value)) throw new Error('Package manager invalide.');
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveProjectPath(value) {
|
|
83
|
+
const projectPath = String(value ?? '').trim();
|
|
84
|
+
|
|
85
|
+
if (!projectPath || projectPath.includes('\0')) {
|
|
86
|
+
throw new Error('Le dossier cible doit être un chemin non vide valide.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return path.resolve(projectPath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseModuleArgs(args) {
|
|
93
|
+
const parsed = parseCliArgs(args);
|
|
94
|
+
if (parsed.command !== 'module' || !parsed.positionals[0])
|
|
95
|
+
throw new Error('La commande module requiert un nom de module.');
|
|
96
|
+
return { moduleName: validateModuleName(parsed.positionals[0]), orm: validateOrm(parsed.options.orm ?? 'typeorm') };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseCliArgs(args) {
|
|
100
|
+
const options = {
|
|
101
|
+
orm: undefined,
|
|
102
|
+
profile: undefined,
|
|
103
|
+
packageManager: undefined,
|
|
104
|
+
projectPath: undefined,
|
|
105
|
+
docker: undefined,
|
|
106
|
+
swagger: undefined,
|
|
107
|
+
operations: undefined,
|
|
108
|
+
git: undefined,
|
|
109
|
+
modules: undefined,
|
|
110
|
+
application: undefined,
|
|
111
|
+
indexes: undefined,
|
|
112
|
+
definitionFile: undefined,
|
|
113
|
+
noInteractive: false,
|
|
114
|
+
quiet: false,
|
|
115
|
+
verbose: false,
|
|
116
|
+
color: true,
|
|
117
|
+
help: false,
|
|
118
|
+
version: false,
|
|
119
|
+
dryRun: false,
|
|
120
|
+
update: false,
|
|
121
|
+
migrationName: undefined,
|
|
122
|
+
json: false,
|
|
123
|
+
};
|
|
124
|
+
const positionals = [];
|
|
125
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
126
|
+
const argument = args[index];
|
|
127
|
+
if (argument === '--orm') {
|
|
128
|
+
const value = args[++index];
|
|
129
|
+
if (!value || value.startsWith('-')) throw new Error('--orm requiert une valeur.');
|
|
130
|
+
options.orm = value;
|
|
131
|
+
} else if (argument.startsWith('--orm=')) options.orm = argument.slice(6);
|
|
132
|
+
else if (argument === '--profile') options.profile = args[++index];
|
|
133
|
+
else if (argument.startsWith('--profile=')) options.profile = argument.slice(10);
|
|
134
|
+
else if (argument === '--package-manager') options.packageManager = args[++index];
|
|
135
|
+
else if (argument.startsWith('--package-manager=')) options.packageManager = argument.slice(18);
|
|
136
|
+
else if (argument === '--project-path') options.projectPath = args[++index];
|
|
137
|
+
else if (argument.startsWith('--project-path=')) options.projectPath = argument.slice(15);
|
|
138
|
+
else if (argument === '--docker') options.docker = true;
|
|
139
|
+
else if (argument === '--no-docker') options.docker = false;
|
|
140
|
+
else if (argument === '--swagger') options.swagger = true;
|
|
141
|
+
else if (argument === '--no-swagger') options.swagger = false;
|
|
142
|
+
else if (argument === '--operations') options.operations = true;
|
|
143
|
+
else if (argument === '--no-operations') options.operations = false;
|
|
144
|
+
else if (argument === '--git') options.git = true;
|
|
145
|
+
else if (argument === '--no-git') options.git = false;
|
|
146
|
+
else if (argument === '--modules') options.modules = args[++index];
|
|
147
|
+
else if (argument.startsWith('--modules=')) options.modules = argument.slice(10);
|
|
148
|
+
else if (argument === '--application') {
|
|
149
|
+
const value = args[++index];
|
|
150
|
+
if (!value || value.startsWith('-')) throw new Error('--application requiert une valeur.');
|
|
151
|
+
options.application = value;
|
|
152
|
+
} else if (argument.startsWith('--application=')) options.application = argument.slice(14);
|
|
153
|
+
else if (argument === '--fields') options.fields = args[++index];
|
|
154
|
+
else if (argument.startsWith('--fields=')) options.fields = argument.slice(9);
|
|
155
|
+
else if (argument === '--indexes') options.indexes = args[++index];
|
|
156
|
+
else if (argument.startsWith('--indexes=')) options.indexes = argument.slice(10);
|
|
157
|
+
else if (argument === '--file') options.definitionFile = args[++index];
|
|
158
|
+
else if (argument.startsWith('--file=')) options.definitionFile = argument.slice(7);
|
|
159
|
+
else if (argument === '--route') options.route = args[++index];
|
|
160
|
+
else if (argument.startsWith('--route=')) options.route = argument.slice(8);
|
|
161
|
+
else if (argument === '--table') options.table = args[++index];
|
|
162
|
+
else if (argument.startsWith('--table=')) options.table = argument.slice(8);
|
|
163
|
+
else if (argument === '--no-interactive') options.noInteractive = true;
|
|
164
|
+
else if (argument === '--quiet') options.quiet = true;
|
|
165
|
+
else if (argument === '--verbose') options.verbose = true;
|
|
166
|
+
else if (argument === '--dry-run') options.dryRun = true;
|
|
167
|
+
else if (argument === '--update') options.update = true;
|
|
168
|
+
else if (argument === '--migration-name') options.migrationName = args[++index];
|
|
169
|
+
else if (argument.startsWith('--migration-name=')) options.migrationName = argument.slice(17);
|
|
170
|
+
else if (argument === '--json') options.json = true;
|
|
171
|
+
else if (argument === '--no-color') options.color = false;
|
|
172
|
+
else if (argument === '--help' || argument === '-h') options.help = true;
|
|
173
|
+
else if (argument === '--version' || argument === '-V') options.version = true;
|
|
174
|
+
else if (argument.startsWith('-')) throw new Error(`Option inconnue : ${argument}`);
|
|
175
|
+
else positionals.push(argument);
|
|
176
|
+
}
|
|
177
|
+
return { command: positionals.shift(), positionals, options };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createModulePlan(projectRoot, moduleName, orm, sourceRoot = 'src') {
|
|
181
|
+
const normalizedName = validateModuleName(moduleName);
|
|
182
|
+
const normalizedOrm = validateOrm(orm);
|
|
183
|
+
const resourceRoot = `${sourceRoot}/app/${normalizedName}`;
|
|
184
|
+
return {
|
|
185
|
+
operation: 'module',
|
|
186
|
+
module: normalizedName,
|
|
187
|
+
orm: normalizedOrm,
|
|
188
|
+
projectRoot,
|
|
189
|
+
files: [
|
|
190
|
+
`${resourceRoot}/core/domain/entities/${normalizedName}.entity.ts`,
|
|
191
|
+
`${resourceRoot}/core/domain/ports/${normalizedName}.repository.ts`,
|
|
192
|
+
`${resourceRoot}/core/application/commands/create-${normalizedName}.command.ts`,
|
|
193
|
+
`${resourceRoot}/core/application/commands/create-${normalizedName}.handler.ts`,
|
|
194
|
+
`${resourceRoot}/interfaces/dtos/create-${normalizedName}.dto.ts`,
|
|
195
|
+
`${resourceRoot}/interfaces/controllers/${normalizedName}.controller.ts`,
|
|
196
|
+
`${resourceRoot}/${normalizedName}.module.ts`,
|
|
197
|
+
],
|
|
198
|
+
mutations: [
|
|
199
|
+
`Ajoute le module dans @Module({ imports }) de ${sourceRoot}/app.module.ts.`,
|
|
200
|
+
`Ajoute CqrsModule dans ${sourceRoot}/app.module.ts si nécessaire.`,
|
|
201
|
+
...(normalizedOrm === 'typeorm'
|
|
202
|
+
? ['Ajoute la configuration TypeORM racine seulement si elle est absente.']
|
|
203
|
+
: []),
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function dryRunOutput(operation, plan) {
|
|
209
|
+
const conflicts = [
|
|
210
|
+
...(plan.featureConflict ? [{ path: '', reason: plan.featureConflict }] : []),
|
|
211
|
+
...plan.preview.filter((change) => change.status === 'conflict').map(({ path, reason }) => ({ path, reason })),
|
|
212
|
+
];
|
|
213
|
+
return {
|
|
214
|
+
version: 1,
|
|
215
|
+
operation,
|
|
216
|
+
changes: plan.preview.map(({ path, operation: fileOperation, status, diff, reason }) => ({
|
|
217
|
+
path,
|
|
218
|
+
operation: fileOperation,
|
|
219
|
+
status,
|
|
220
|
+
...(diff ? { diff } : {}),
|
|
221
|
+
...(reason ? { reason } : {}),
|
|
222
|
+
})),
|
|
223
|
+
conflicts,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function runBashScript(scriptPath, args = [], env = {}, quiet = false) {
|
|
228
|
+
const result = spawnSync('bash', [scriptPath, ...args], {
|
|
229
|
+
env: { ...process.env, ...env },
|
|
230
|
+
stdio: quiet ? 'pipe' : 'inherit',
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (result.error) {
|
|
234
|
+
throw result.error;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (result.status !== 0) {
|
|
238
|
+
throw new Error(`Le script ${path.basename(scriptPath)} a échoué avec le code ${result.status ?? 'inconnu'}.`);
|
|
239
|
+
}
|
|
42
240
|
}
|
|
43
241
|
|
|
44
242
|
// ────── Commande : INIT
|
|
@@ -49,12 +247,14 @@ async function askInitQuestions() {
|
|
|
49
247
|
name: 'projectName',
|
|
50
248
|
message: '📛 Nom du projet :',
|
|
51
249
|
default: 'my-app',
|
|
250
|
+
validate: validatePromptValue(validateModuleName),
|
|
52
251
|
},
|
|
53
252
|
{
|
|
54
253
|
type: 'input',
|
|
55
254
|
name: 'projectPath',
|
|
56
255
|
message: '📁 Dossier cible :',
|
|
57
256
|
default: './',
|
|
257
|
+
validate: validatePromptValue(resolveProjectPath),
|
|
58
258
|
},
|
|
59
259
|
{
|
|
60
260
|
type: 'list',
|
|
@@ -82,6 +282,12 @@ async function askInitQuestions() {
|
|
|
82
282
|
message: '🐳 Activer Docker ?',
|
|
83
283
|
default: false,
|
|
84
284
|
},
|
|
285
|
+
{
|
|
286
|
+
type: 'confirm',
|
|
287
|
+
name: 'withOperations',
|
|
288
|
+
message: '🩺 Activer le socle d’exploitation (santé, logs, arrêt propre) ?',
|
|
289
|
+
default: false,
|
|
290
|
+
},
|
|
85
291
|
{
|
|
86
292
|
type: 'confirm',
|
|
87
293
|
name: 'withGit',
|
|
@@ -93,76 +299,106 @@ async function askInitQuestions() {
|
|
|
93
299
|
name: 'modules',
|
|
94
300
|
message: '📦 Modules à générer (séparés par des espaces) :',
|
|
95
301
|
default: 'user',
|
|
96
|
-
filter: (input) =>
|
|
97
|
-
|
|
302
|
+
filter: (input) =>
|
|
303
|
+
input
|
|
304
|
+
.split(' ')
|
|
305
|
+
.map((s) => s.trim())
|
|
306
|
+
.filter(Boolean),
|
|
307
|
+
validate: validatePromptValue((modules) => modules.forEach(validateModuleName)),
|
|
308
|
+
},
|
|
98
309
|
]);
|
|
99
310
|
}
|
|
100
311
|
|
|
101
|
-
|
|
102
|
-
|
|
312
|
+
function validatePromptValue(validator) {
|
|
313
|
+
return (value) => {
|
|
314
|
+
try {
|
|
315
|
+
validator(value);
|
|
316
|
+
return true;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return error.message;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
103
322
|
|
|
323
|
+
async function runInteractiveInit(options) {
|
|
324
|
+
if (!options.quiet) printLogo();
|
|
104
325
|
if (!fs.existsSync(GENERATE_SCRIPT)) {
|
|
105
|
-
|
|
106
|
-
process.exit(1);
|
|
326
|
+
throw new Error(`Script introuvable : ${GENERATE_SCRIPT}`);
|
|
107
327
|
}
|
|
108
328
|
|
|
109
|
-
const answers =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
329
|
+
const answers = options.noInteractive
|
|
330
|
+
? (() => {
|
|
331
|
+
const missing = [
|
|
332
|
+
['nom du projet', options.projectName],
|
|
333
|
+
['--project-path', options.projectPath],
|
|
334
|
+
['--package-manager', options.packageManager],
|
|
335
|
+
['--orm', options.orm],
|
|
336
|
+
['--docker ou --no-docker', options.docker],
|
|
337
|
+
['--swagger ou --no-swagger', options.swagger],
|
|
338
|
+
['--operations ou --no-operations', options.operations],
|
|
339
|
+
['--git ou --no-git', options.git],
|
|
340
|
+
['--modules', options.modules],
|
|
341
|
+
]
|
|
342
|
+
.filter(([, value]) => value === undefined)
|
|
343
|
+
.map(([name]) => name);
|
|
344
|
+
if (missing.length) throw new Error(`init --no-interactive requiert : ${missing.join(', ')}.`);
|
|
345
|
+
return {
|
|
346
|
+
projectName: options.projectName,
|
|
347
|
+
projectPath: options.projectPath,
|
|
348
|
+
packageManager: options.packageManager,
|
|
349
|
+
orm: options.orm,
|
|
350
|
+
withDocker: options.docker,
|
|
351
|
+
withSwagger: options.swagger,
|
|
352
|
+
withOperations: options.operations,
|
|
353
|
+
withGit: options.git,
|
|
354
|
+
modules: options.modules ? options.modules.split(',').filter(Boolean) : [],
|
|
355
|
+
};
|
|
356
|
+
})()
|
|
357
|
+
: await askInitQuestions();
|
|
358
|
+
const { projectName, projectPath, packageManager, orm, withSwagger, withDocker, withOperations, withGit, modules } =
|
|
359
|
+
answers;
|
|
120
360
|
|
|
121
361
|
const env = {
|
|
122
|
-
APP_NAME: projectName,
|
|
123
|
-
PROJECT_PATH:
|
|
124
|
-
PM: packageManager,
|
|
125
|
-
ORM: orm,
|
|
362
|
+
APP_NAME: validateModuleName(projectName),
|
|
363
|
+
PROJECT_PATH: resolveProjectPath(projectPath),
|
|
364
|
+
PM: validatePackageManager(packageManager),
|
|
365
|
+
ORM: validateOrm(orm),
|
|
126
366
|
WITH_SWAGGER: withSwagger ? 'y' : 'n',
|
|
367
|
+
WITH_OPERATIONS: withOperations ? 'y' : 'n',
|
|
127
368
|
WITH_DOCKER: withDocker ? 'y' : 'n',
|
|
128
369
|
WITH_GIT: withGit ? 'y' : 'n',
|
|
129
|
-
MODULES: modules.join(' '),
|
|
370
|
+
MODULES: modules.map(validateModuleName).join(' '),
|
|
130
371
|
};
|
|
131
372
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
} catch (err) {
|
|
142
|
-
console.error(chalk.red('❌ Une erreur est survenue pendant la génération.'));
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
373
|
+
if (!options.quiet) console.log('\n🚀 Lancement de la génération du projet...\n');
|
|
374
|
+
runBashScript(GENERATE_SCRIPT, [], env, options.quiet);
|
|
375
|
+
return {
|
|
376
|
+
projectName: env.APP_NAME,
|
|
377
|
+
projectPath: env.PROJECT_PATH,
|
|
378
|
+
packageManager: env.PM,
|
|
379
|
+
orm: env.ORM,
|
|
380
|
+
operations: env.WITH_OPERATIONS === 'y',
|
|
381
|
+
};
|
|
145
382
|
}
|
|
146
383
|
|
|
147
384
|
// ────── Commande : MODULE
|
|
148
|
-
async function runModuleGeneration(
|
|
149
|
-
printLogo();
|
|
385
|
+
async function runModuleGeneration(parsed) {
|
|
386
|
+
if (!parsed.options.quiet) printLogo();
|
|
387
|
+
const config = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
|
|
150
388
|
|
|
151
389
|
let moduleName, orm;
|
|
152
390
|
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
console.log(chalk.red('❌ Tu dois fournir un nom de module.'));
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
391
|
+
if (parsed.positionals[0]) {
|
|
392
|
+
moduleName = validateModuleName(parsed.positionals[0]);
|
|
393
|
+
orm = validateOrm(parsed.options.orm ?? config.orm);
|
|
159
394
|
} else {
|
|
395
|
+
if (parsed.options.noInteractive) throw new Error('module --no-interactive requiert un nom de module.');
|
|
160
396
|
const answers = await inquirer.prompt([
|
|
161
397
|
{
|
|
162
398
|
type: 'input',
|
|
163
399
|
name: 'moduleName',
|
|
164
400
|
message: '📦 Nom du module :',
|
|
165
|
-
validate:
|
|
401
|
+
validate: validatePromptValue(validateModuleName),
|
|
166
402
|
},
|
|
167
403
|
{
|
|
168
404
|
type: 'list',
|
|
@@ -172,47 +408,425 @@ async function runModuleGeneration(args) {
|
|
|
172
408
|
default: 'typeorm',
|
|
173
409
|
},
|
|
174
410
|
]);
|
|
175
|
-
moduleName = answers.moduleName;
|
|
176
|
-
orm = answers.orm;
|
|
411
|
+
moduleName = validateModuleName(answers.moduleName);
|
|
412
|
+
orm = validateOrm(answers.orm);
|
|
177
413
|
}
|
|
178
414
|
|
|
179
415
|
if (!isNestProject()) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
416
|
+
throw new Error('Aucun projet NestJS détecté dans ce dossier. Lance cette commande depuis un projet NestJS.');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (parsed.options.dryRun) {
|
|
420
|
+
const plan = await planModuleGeneration(process.cwd(), moduleName, orm, parsed.options.application);
|
|
421
|
+
const output = dryRunOutput('module', plan);
|
|
422
|
+
if (!parsed.options.json) console.log(JSON.stringify(output, null, 2));
|
|
423
|
+
return output;
|
|
183
424
|
}
|
|
184
425
|
|
|
426
|
+
if (!parsed.options.quiet) console.log(chalk.cyan(`\n⚙️ Génération du module ${moduleName}...\n`));
|
|
427
|
+
if (process.env.NESTGEN_ROOT) {
|
|
428
|
+
runBashScript(ADD_MODULE_SCRIPT, [moduleName, orm], {}, parsed.options.quiet);
|
|
429
|
+
return { module: moduleName, orm, generated: true };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await generateModule(process.cwd(), moduleName, orm, parsed.options.application);
|
|
433
|
+
return { module: moduleName, orm, generated: true };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function loadResourceDefinition(file, projectRoot = process.cwd()) {
|
|
437
|
+
const candidate = path.resolve(projectRoot, String(file ?? ''));
|
|
438
|
+
if (!candidate.startsWith(`${path.resolve(projectRoot)}${path.sep}`))
|
|
439
|
+
throw new Error('--file doit désigner un fichier situé dans le projet courant.');
|
|
440
|
+
let value;
|
|
441
|
+
try {
|
|
442
|
+
value = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
443
|
+
} catch (error) {
|
|
444
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : ${error.message}`);
|
|
445
|
+
}
|
|
446
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
447
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : un objet JSON est requis.`);
|
|
448
|
+
if (value.version !== 1) throw new Error(`Définition de ressource invalide (${candidate}) : version 1 requise.`);
|
|
449
|
+
if (!Array.isArray(value.fields) || value.fields.some((field) => typeof field !== 'string'))
|
|
450
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : fields doit être un tableau de chaînes.`);
|
|
451
|
+
for (const [index, field] of value.fields.entries()) {
|
|
452
|
+
try {
|
|
453
|
+
parseResourceFields([field]);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : fields[${index}] — ${error.message}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const fields = parseResourceFields(value.fields);
|
|
459
|
+
if (value.relations !== undefined && !Array.isArray(value.relations))
|
|
460
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : relations doit être un tableau.`);
|
|
461
|
+
let relations;
|
|
462
|
+
try {
|
|
463
|
+
relations = parseResourceRelations(value.relations ?? []);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : relations — ${error.message}`);
|
|
466
|
+
}
|
|
467
|
+
const relationFields = relationIndexFields(relations);
|
|
468
|
+
try {
|
|
469
|
+
parseResourceListOptions(value.list, [...fields, ...relationFields]);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : list — ${error.message}`);
|
|
472
|
+
}
|
|
473
|
+
if (
|
|
474
|
+
value.indexes !== undefined &&
|
|
475
|
+
(!Array.isArray(value.indexes) || value.indexes.some((index) => typeof index !== 'string'))
|
|
476
|
+
)
|
|
477
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : indexes doit être un tableau de chaînes.`);
|
|
478
|
+
try {
|
|
479
|
+
if (
|
|
480
|
+
value.uniqueIndexes !== undefined &&
|
|
481
|
+
(!Array.isArray(value.uniqueIndexes) || value.uniqueIndexes.some((index) => typeof index !== 'string'))
|
|
482
|
+
)
|
|
483
|
+
throw new Error(
|
|
484
|
+
`Définition de ressource invalide (${candidate}) : uniqueIndexes doit être un tableau de chaînes.`,
|
|
485
|
+
);
|
|
486
|
+
parseResourceIndexes(value.indexes ?? [], [...fields, ...relationFields], value.uniqueIndexes ?? []);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw new Error(`Définition de ressource invalide (${candidate}) : indexes — ${error.message}`);
|
|
489
|
+
}
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function relationIndexFields(relations) {
|
|
494
|
+
return relations
|
|
495
|
+
.filter((relation) => relation.type === 'belongsTo')
|
|
496
|
+
.map((relation) => ({
|
|
497
|
+
name: `${relation.field ?? relation.target.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}Id`,
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function runResourceGeneration(parsed) {
|
|
502
|
+
const definition = parsed.options.definitionFile ? loadResourceDefinition(parsed.options.definitionFile) : {};
|
|
503
|
+
const name = validateModuleName(parsed.positionals[0] ?? definition.name ?? '');
|
|
504
|
+
const fieldValues = parsed.options.fields?.split(',').filter(Boolean) ?? definition.fields;
|
|
505
|
+
if (!fieldValues) throw new Error('resource requiert --fields ou --file avec fields.');
|
|
506
|
+
const config = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
|
|
507
|
+
const orm = validateOrm(parsed.options.orm ?? definition.orm ?? config.orm);
|
|
508
|
+
const profile = parseArchitectureProfile(parsed.options.profile ?? definition.profile ?? config.profile);
|
|
509
|
+
const fields = parseResourceFields(fieldValues);
|
|
510
|
+
const relations = parseResourceRelations(definition.relations ?? []);
|
|
511
|
+
const relationFields = relationIndexFields(relations);
|
|
512
|
+
const list = parseResourceListOptions(definition.list, [...fields, ...relationFields]);
|
|
513
|
+
const indexes = parseResourceIndexes(
|
|
514
|
+
parsed.options.indexes?.split(',').filter(Boolean) ?? definition.indexes ?? [],
|
|
515
|
+
[...fields, ...relationFields],
|
|
516
|
+
definition.uniqueIndexes ?? [],
|
|
517
|
+
);
|
|
518
|
+
const route = parsed.options.route ?? definition.route ?? `${name}s`;
|
|
519
|
+
const table = parsed.options.table ?? definition.table ?? `${name}s`;
|
|
520
|
+
if (!/^[a-z][a-z0-9/-]*$/.test(route) || !/^[a-z][a-z0-9_]*$/.test(table))
|
|
521
|
+
throw new Error('Route ou table invalide.');
|
|
522
|
+
if (parsed.options.migrationName && !parsed.options.update) throw new Error('--migration-name requiert --update.');
|
|
523
|
+
const resourceOptions = {
|
|
524
|
+
name,
|
|
525
|
+
route,
|
|
526
|
+
table,
|
|
527
|
+
fields,
|
|
528
|
+
indexes,
|
|
529
|
+
relations,
|
|
530
|
+
list,
|
|
531
|
+
orm,
|
|
532
|
+
profile,
|
|
533
|
+
application: parsed.options.application,
|
|
534
|
+
update: parsed.options.update,
|
|
535
|
+
};
|
|
536
|
+
const migration = parsed.options.migrationName
|
|
537
|
+
? planResourceMigration(
|
|
538
|
+
process.cwd(),
|
|
539
|
+
inspectProject(process.cwd(), orm, { application: parsed.options.application }).sourceRoot,
|
|
540
|
+
name,
|
|
541
|
+
orm,
|
|
542
|
+
parsed.options.migrationName,
|
|
543
|
+
fields,
|
|
544
|
+
indexes,
|
|
545
|
+
relations,
|
|
546
|
+
)
|
|
547
|
+
: undefined;
|
|
548
|
+
if (parsed.options.dryRun) {
|
|
549
|
+
const plan = await planResourceGeneration(process.cwd(), resourceOptions);
|
|
550
|
+
const output = { ...dryRunOutput('resource', plan), ...(migration ? { migration } : {}) };
|
|
551
|
+
if (!parsed.options.json) console.log(JSON.stringify(output, null, 2));
|
|
552
|
+
return output;
|
|
553
|
+
}
|
|
554
|
+
await generateResource(process.cwd(), resourceOptions);
|
|
555
|
+
return {
|
|
556
|
+
resource: name,
|
|
557
|
+
orm,
|
|
558
|
+
profile,
|
|
559
|
+
generated: true,
|
|
560
|
+
...(parsed.options.update ? { updated: true } : {}),
|
|
561
|
+
...(migration ? { migration } : {}),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function printUsage() {
|
|
566
|
+
console.log(
|
|
567
|
+
`Usage: nestgen <commande> [options]\n\nCommandes:\n init [nom] Génère un projet NestJS\n module <nom> [--orm <orm>] Génère un module\n resource <nom> --fields ... Génère un CRUD TypeORM ou Prisma
|
|
568
|
+
config init|show Gère nestgen.config.json
|
|
569
|
+
doctor Vérifie l'installation\n\nOptions:\n -h, --help Affiche cette aide\n -V, --version Affiche la version\n --no-interactive Refuse les prompts\n --quiet Supprime les sorties non essentielles\n --verbose Active les diagnostics\n --no-color Désactive les couleurs\n --profile <simple|advanced> Choisit le profil d'architecture
|
|
570
|
+
--package-manager <pm> Définit npm, pnpm ou yarn
|
|
571
|
+
--project-path <chemin> Définit le dossier parent du projet init
|
|
572
|
+
--docker, --no-docker Active ou désactive Docker pour init
|
|
573
|
+
--swagger, --no-swagger Active ou désactive Swagger pour init
|
|
574
|
+
--operations, --no-operations Active ou désactive le socle d’exploitation pour init
|
|
575
|
+
--git, --no-git Active ou désactive Git pour init
|
|
576
|
+
--modules <a,b> Définit les modules init, séparés par des virgules
|
|
577
|
+
--application <nom> Cible une application d’un workspace Nest
|
|
578
|
+
--update Met à jour une ressource déjà générée sans écraser les fichiers modifiés
|
|
579
|
+
--migration-name <nom> Prépare une commande de migration officielle avec --update, sans l’appliquer
|
|
580
|
+
--dry-run Affiche le plan sans écrire
|
|
581
|
+
--json Retourne un résultat JSON versionné
|
|
582
|
+
--indexes <a+b,c> Ajoute des index composites ou simples\n\nChamps resource : string, number, integer, decimal(precision;scale), enum(VALEUR|VALEUR), boolean, date, uuid.`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function runDoctor(machine = false) {
|
|
587
|
+
const diagnostics = [];
|
|
588
|
+
const add = (id, severity, cause, action, detail) => diagnostics.push({ id, severity, cause, action, detail });
|
|
589
|
+
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
|
|
590
|
+
add(
|
|
591
|
+
'NODE_VERSION',
|
|
592
|
+
nodeMajor === 24 && nodeMinor >= 15 ? 'INFO' : 'ERROR',
|
|
593
|
+
nodeMajor === 24 && nodeMinor >= 15 ? 'Version Node.js compatible.' : 'Node.js doit être >=24.15.0 et <27.',
|
|
594
|
+
'Installe une version Node.js supportée puis relance doctor.',
|
|
595
|
+
process.versions.node,
|
|
596
|
+
);
|
|
597
|
+
add(
|
|
598
|
+
'GENERATOR_SCRIPTS',
|
|
599
|
+
fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT) ? 'INFO' : 'ERROR',
|
|
600
|
+
fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT)
|
|
601
|
+
? 'Scripts du générateur disponibles.'
|
|
602
|
+
: 'Un script du générateur est introuvable.',
|
|
603
|
+
'Réinstalle NestGen.',
|
|
604
|
+
`${GENERATE_SCRIPT}, ${ADD_MODULE_SCRIPT}`,
|
|
605
|
+
);
|
|
606
|
+
const projectRoot = process.cwd();
|
|
607
|
+
const packagePath = path.join(projectRoot, 'package.json');
|
|
608
|
+
let manifest;
|
|
609
|
+
if (!fs.existsSync(packagePath)) {
|
|
610
|
+
add(
|
|
611
|
+
'PROJECT_PACKAGE',
|
|
612
|
+
'WARNING',
|
|
613
|
+
'Aucun package.json de projet détecté.',
|
|
614
|
+
'Exécute doctor à la racine du projet Nest.',
|
|
615
|
+
packagePath,
|
|
616
|
+
);
|
|
617
|
+
} else {
|
|
618
|
+
try {
|
|
619
|
+
manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
620
|
+
const dependencies = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) };
|
|
621
|
+
for (const dependency of ['@nestjs/common', '@nestjs/core'])
|
|
622
|
+
add(
|
|
623
|
+
`DEPENDENCY_${dependency.replace(/^@/, '').replaceAll(/[/-]/g, '_').toUpperCase()}`,
|
|
624
|
+
dependencies[dependency] ? 'INFO' : 'ERROR',
|
|
625
|
+
dependencies[dependency] ? `${dependency} est déclaré.` : `${dependency} est absent.`,
|
|
626
|
+
`Installe ${dependency} avec le package manager du projet.`,
|
|
627
|
+
dependencies[dependency] ?? '',
|
|
628
|
+
);
|
|
629
|
+
const nestVersion = String(dependencies['@nestjs/common'] ?? '');
|
|
630
|
+
if (nestVersion)
|
|
631
|
+
add(
|
|
632
|
+
'NEST_VERSION',
|
|
633
|
+
/(?:\^|~)?12(?:\.|$)/.test(nestVersion) ? 'INFO' : 'WARNING',
|
|
634
|
+
/(?:\^|~)?12(?:\.|$)/.test(nestVersion)
|
|
635
|
+
? 'La version Nest déclarée est supportée.'
|
|
636
|
+
: 'La version Nest déclarée est hors de la matrice testée.',
|
|
637
|
+
'Utilise Nest 12 ou vérifie cette combinaison avec la matrice de compatibilité.',
|
|
638
|
+
nestVersion,
|
|
639
|
+
);
|
|
640
|
+
if (dependencies['@nestjs/typeorm'] || dependencies.typeorm)
|
|
641
|
+
add(
|
|
642
|
+
'TYPEORM_COHERENCE',
|
|
643
|
+
dependencies['@nestjs/typeorm'] && dependencies.typeorm ? 'INFO' : 'WARNING',
|
|
644
|
+
dependencies['@nestjs/typeorm'] && dependencies.typeorm
|
|
645
|
+
? 'Les dépendances TypeORM sont cohérentes.'
|
|
646
|
+
: 'Le support TypeORM est incomplet.',
|
|
647
|
+
'Installe @nestjs/typeorm et typeorm ensemble, ou utilise Prisma.',
|
|
648
|
+
'',
|
|
649
|
+
);
|
|
650
|
+
if (dependencies['@prisma/client'])
|
|
651
|
+
add(
|
|
652
|
+
'PRISMA_CLIENT',
|
|
653
|
+
'INFO',
|
|
654
|
+
'@prisma/client est déclaré.',
|
|
655
|
+
'Exécute npx prisma generate après une modification du schéma.',
|
|
656
|
+
dependencies['@prisma/client'],
|
|
657
|
+
);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
add(
|
|
660
|
+
'PROJECT_PACKAGE',
|
|
661
|
+
'ERROR',
|
|
662
|
+
'package.json est invalide.',
|
|
663
|
+
'Corrige le JSON du manifeste.',
|
|
664
|
+
error.message,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
185
668
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
669
|
+
const config = loadConfigIfPresent(projectRoot);
|
|
670
|
+
add(
|
|
671
|
+
'NESTGEN_CONFIG',
|
|
672
|
+
'INFO',
|
|
673
|
+
config
|
|
674
|
+
? 'Configuration NestGen valide.'
|
|
675
|
+
: 'Configuration NestGen absente ; les valeurs par défaut seront utilisées.',
|
|
676
|
+
config ? 'Aucune action requise.' : 'Exécute nestgen config init pour figer les choix du projet.',
|
|
677
|
+
config ? `orm=${config.orm}; profile=${config.profile}; packageManager=${config.packageManager}` : '',
|
|
678
|
+
);
|
|
679
|
+
if (config) {
|
|
680
|
+
const lockfile =
|
|
681
|
+
config.packageManager === 'npm'
|
|
682
|
+
? 'package-lock.json'
|
|
683
|
+
: config.packageManager === 'pnpm'
|
|
684
|
+
? 'pnpm-lock.yaml'
|
|
685
|
+
: 'yarn.lock';
|
|
686
|
+
add(
|
|
687
|
+
'PACKAGE_MANAGER_LOCKFILE',
|
|
688
|
+
fs.existsSync(path.join(projectRoot, lockfile)) ? 'INFO' : 'WARNING',
|
|
689
|
+
fs.existsSync(path.join(projectRoot, lockfile))
|
|
690
|
+
? `Le lockfile ${lockfile} correspond à la configuration.`
|
|
691
|
+
: `Le lockfile ${lockfile} est absent.`,
|
|
692
|
+
`Installe les dépendances avec ${config.packageManager} pour créer le lockfile attendu.`,
|
|
693
|
+
lockfile,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
} catch (error) {
|
|
697
|
+
add(
|
|
698
|
+
'NESTGEN_CONFIG',
|
|
699
|
+
'ERROR',
|
|
700
|
+
'Configuration NestGen invalide.',
|
|
701
|
+
'Corrige nestgen.config.json ou recrée-la.',
|
|
702
|
+
error.message,
|
|
703
|
+
);
|
|
193
704
|
}
|
|
705
|
+
let project;
|
|
706
|
+
try {
|
|
707
|
+
project = inspectProject(projectRoot, 'typeorm', { cqrs: false });
|
|
708
|
+
add(
|
|
709
|
+
'NEST_INTEGRATION',
|
|
710
|
+
'INFO',
|
|
711
|
+
'Structure Nest et intégration TypeORM valides.',
|
|
712
|
+
'Aucune action requise.',
|
|
713
|
+
project.sourceRoot,
|
|
714
|
+
);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
project = { error: error.message };
|
|
717
|
+
add(
|
|
718
|
+
'NEST_INTEGRATION',
|
|
719
|
+
'WARNING',
|
|
720
|
+
'Le projet ne remplit pas toutes les préconditions TypeORM.',
|
|
721
|
+
'Lis le diagnostic puis complète la structure ou les dépendances indiquées.',
|
|
722
|
+
error.message,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
if (!machine)
|
|
726
|
+
for (const diagnostic of diagnostics)
|
|
727
|
+
console.log(`${diagnostic.severity} ${diagnostic.id}: ${diagnostic.cause} — ${diagnostic.action}`);
|
|
728
|
+
return { diagnostics, project };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function machineResult(command, result) {
|
|
732
|
+
return { version: 1, ok: true, command, result };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function errorCode(error) {
|
|
736
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
737
|
+
if (/^(Option inconnue|Commande inconnue|.* requiert |Route ou table invalide|Nom de )/.test(message))
|
|
738
|
+
return 'USAGE';
|
|
739
|
+
if (message.startsWith('Préflight échoué')) return 'PROJECT_INVALID';
|
|
740
|
+
if (/(existe déjà|Destination |Conflit |Transaction interrompue)/.test(message)) return 'CONFLICT';
|
|
741
|
+
return 'INTERNAL';
|
|
194
742
|
}
|
|
195
743
|
|
|
196
744
|
// ────── Entrée CLI
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
745
|
+
export async function main(args = process.argv.slice(2)) {
|
|
746
|
+
const parsed = parseCliArgs(args);
|
|
747
|
+
const machine = parsed.options.json;
|
|
748
|
+
if (machine) {
|
|
749
|
+
parsed.options.quiet = true;
|
|
750
|
+
parsed.options.color = false;
|
|
751
|
+
parsed.options.noInteractive = true;
|
|
752
|
+
}
|
|
753
|
+
if (!parsed.options.color) chalk.level = 0;
|
|
754
|
+
if (parsed.options.version) {
|
|
755
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
756
|
+
const result = { cliVersion: packageJson.version };
|
|
757
|
+
console.log(machine ? JSON.stringify(machineResult('version', result)) : packageJson.version);
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
if (parsed.options.help || !parsed.command) {
|
|
761
|
+
if (machine)
|
|
762
|
+
console.log(
|
|
763
|
+
JSON.stringify(machineResult('help', { commands: ['init', 'module', 'resource', 'config', 'doctor'] })),
|
|
764
|
+
);
|
|
765
|
+
else printUsage();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
let result;
|
|
770
|
+
switch (parsed.command) {
|
|
771
|
+
case 'init':
|
|
772
|
+
if (parsed.positionals.length > 1) throw new Error('init accepte au plus un nom de projet.');
|
|
773
|
+
result = await runInteractiveInit({ ...parsed.options, projectName: parsed.positionals[0] });
|
|
774
|
+
break;
|
|
775
|
+
|
|
776
|
+
case 'module':
|
|
777
|
+
result = await runModuleGeneration(parsed);
|
|
778
|
+
break;
|
|
779
|
+
|
|
780
|
+
case 'resource':
|
|
781
|
+
result = await runResourceGeneration(parsed);
|
|
782
|
+
break;
|
|
783
|
+
|
|
784
|
+
case 'config': {
|
|
785
|
+
const action = parsed.positionals[0];
|
|
786
|
+
if (action === 'show') {
|
|
787
|
+
result = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
|
|
788
|
+
if (!machine) console.log(JSON.stringify(result, null, 2));
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
if (action === 'init') {
|
|
792
|
+
const config = {
|
|
793
|
+
...defaultConfig,
|
|
794
|
+
orm: validateOrm(parsed.options.orm ?? defaultConfig.orm),
|
|
795
|
+
profile: parseArchitectureProfile(parsed.options.profile),
|
|
796
|
+
packageManager: parsed.options.packageManager ?? defaultConfig.packageManager,
|
|
797
|
+
};
|
|
798
|
+
if (!['npm', 'pnpm', 'yarn'].includes(config.packageManager))
|
|
799
|
+
throw new Error('Package manager invalide.');
|
|
800
|
+
if (parsed.options.dryRun) {
|
|
801
|
+
result = { operation: 'config-init', file: configFileName, config };
|
|
802
|
+
if (!machine) console.log(JSON.stringify(result, null, 2));
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
writeConfig(process.cwd(), config);
|
|
806
|
+
result = { file: configFileName, created: true, config };
|
|
807
|
+
if (!machine) console.log(configFileName);
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
throw new Error('config requiert init ou show.');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
case 'doctor':
|
|
814
|
+
result = runDoctor(machine);
|
|
815
|
+
break;
|
|
816
|
+
|
|
817
|
+
default:
|
|
818
|
+
throw new Error(`Commande inconnue : ${parsed.command}. Utilise --help.`);
|
|
819
|
+
}
|
|
820
|
+
if (machine) console.log(JSON.stringify(machineResult(parsed.command, result)));
|
|
821
|
+
return result;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === __filename) {
|
|
825
|
+
main().catch((error) => {
|
|
826
|
+
if (process.argv.includes('--json')) {
|
|
827
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
828
|
+
console.error(JSON.stringify({ version: 1, ok: false, error: { code: errorCode(error), message } }));
|
|
829
|
+
} else console.error(chalk.red(`❌ ${error.message}`));
|
|
830
|
+
process.exitCode = 1;
|
|
831
|
+
});
|
|
218
832
|
}
|