@neohm/nh-cli 1.1.5 → 1.3.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.
@@ -0,0 +1,357 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const inquirer = require('inquirer');
6
+ const chalk = require('chalk');
7
+
8
+ const { projectPaths, MODULE_SUBFOLDERS } = require('../lib/paths');
9
+ const { assertFlavourMatchesProject, cli } = require('../lib/flavour');
10
+ const naming = require('../lib/naming');
11
+ const tpl = require('../lib/templates');
12
+ const {
13
+ writeFile,
14
+ report: reportFile,
15
+ log,
16
+ ensureDir,
17
+ } = require('../lib/fs.utils');
18
+ const sync = require('../lib/sync.engine');
19
+ const { formatFiles } = require('../lib/format');
20
+ const { registerJob, registerEntity } = require('../lib/registry');
21
+ const { findConflict, describeConflict } = require('../lib/collision');
22
+
23
+ /**
24
+ * Single-artifact generation: `nh gen -c`, `-s`, `-dp`, `-lp`, `-e`, `-j`, and
25
+ * the `-x` (external, API-key surface) variants of the first four.
26
+ *
27
+ * Bare `nh gen` still runs the full wizard — this path exists for adding one
28
+ * more file to a module that already exists, which is most of the work after
29
+ * the initial scaffold.
30
+ */
31
+
32
+ // Short noun, used in the name prompt and error messages.
33
+ const LABELS = {
34
+ controller: 'controller',
35
+ service: 'service',
36
+ dataProcessor: 'data processor',
37
+ listProcessor: 'list processor',
38
+ entity: 'entity',
39
+ job: 'job',
40
+ };
41
+
42
+ // What the target actually writes, for the header line.
43
+ const DESCRIBES = {
44
+ controller: 'controller',
45
+ service: 'service',
46
+ dataProcessor: 'data processor (+ its payload DTO)',
47
+ listProcessor: 'list processor',
48
+ entity: 'entity, its job and subscriber, and a migration',
49
+ job: 'job',
50
+ };
51
+
52
+ // `-x` mirrors a target under <module>/external/. Entities are tables and jobs
53
+ // are queue workers — neither has an inward/outward face, so neither takes -x.
54
+ const EXTERNAL_CAPABLE = new Set([
55
+ 'controller',
56
+ 'service',
57
+ 'dataProcessor',
58
+ 'listProcessor',
59
+ ]);
60
+
61
+ const FLAG = {
62
+ controller: '-c',
63
+ service: '-s',
64
+ dataProcessor: '-dp',
65
+ listProcessor: '-lp',
66
+ entity: '-e',
67
+ job: '-j',
68
+ };
69
+
70
+ /**
71
+ * The files a target owns, as {className, file} pairs.
72
+ *
73
+ * "Owns" excludes anything reused rather than claimed: the payload DTO for
74
+ * -dp, the attributes DTO for -e, and the timestamped migration. Those are
75
+ * shared or unique-by-construction, so they are created when absent and left
76
+ * alone when present — only the artifacts listed here are name-checked.
77
+ *
78
+ * An entity owns three files, not one: TypeORM drives the job through the
79
+ * subscriber, so an entity without them is a table nothing reacts to. The
80
+ * wizard has always written all three together; -e does the same.
81
+ */
82
+ function primaryArtifacts(target, moduleDir, n, external) {
83
+ const at = (...parts) =>
84
+ external
85
+ ? path.join(moduleDir, 'external', ...parts)
86
+ : path.join(moduleDir, ...parts);
87
+ const one = (className, ...parts) => [{ className, file: at(...parts) }];
88
+
89
+ switch (target) {
90
+ case 'controller':
91
+ return external
92
+ ? one(
93
+ n.externalControllerClass,
94
+ 'controllers',
95
+ n.externalControllerFile,
96
+ )
97
+ : one(n.controllerClass, 'controllers', n.controllerFile);
98
+ case 'service':
99
+ return external
100
+ ? one(n.externalServiceClass, 'services', n.externalServiceFile)
101
+ : one(n.serviceClass, 'services', n.serviceFile);
102
+ case 'dataProcessor':
103
+ return external
104
+ ? one(n.externalProcessorClass, 'libraries', n.externalProcessorFile)
105
+ : one(n.processorClass, 'libraries', n.processorFile);
106
+ case 'listProcessor':
107
+ return external
108
+ ? one(
109
+ n.externalListProcessorClass,
110
+ 'libraries',
111
+ n.externalListProcessorFile,
112
+ )
113
+ : one(n.listProcessorClass, 'libraries', n.listProcessorFile);
114
+ case 'entity':
115
+ return [
116
+ {
117
+ className: n.entityClass,
118
+ file: path.join(moduleDir, 'entities', n.entityFile),
119
+ },
120
+ {
121
+ className: n.jobClass,
122
+ file: path.join(moduleDir, 'jobs', n.jobFile),
123
+ },
124
+ {
125
+ className: n.subscriberClass,
126
+ file: path.join(moduleDir, 'subscribers', n.subscriberFile),
127
+ },
128
+ ];
129
+ case 'job':
130
+ return [
131
+ {
132
+ className: n.jobClass,
133
+ file: path.join(moduleDir, 'jobs', n.jobFile),
134
+ },
135
+ ];
136
+ default:
137
+ throw new Error(`Unknown target "${target}".`);
138
+ }
139
+ }
140
+
141
+ /** Create the module skeleton if this is the first file going into it. */
142
+ function ensureModule(paths, moduleDot, external, report) {
143
+ const moduleDir = paths.moduleDir(moduleDot);
144
+ const existed = fs.existsSync(moduleDir);
145
+
146
+ for (const sub of MODULE_SUBFOLDERS) ensureDir(path.join(moduleDir, sub));
147
+ if (external) {
148
+ for (const sub of ['controllers', 'libraries', 'services']) {
149
+ ensureDir(path.join(moduleDir, 'external', sub));
150
+ }
151
+ }
152
+
153
+ const modulePath = path.join(moduleDir, `${moduleDot}.module.ts`);
154
+ const es6Path = path.join(moduleDir, 'es6.classes.ts');
155
+ const a = writeFile(modulePath, tpl.moduleFile(moduleDot));
156
+ const b = writeFile(es6Path, tpl.emptyEs6Classes());
157
+ if (a !== 'skipped') report(a, modulePath, paths.root);
158
+ if (b !== 'skipped') report(b, es6Path, paths.root);
159
+
160
+ if (!existed) log.info(`Created module "${moduleDot}".`);
161
+ return moduleDir;
162
+ }
163
+
164
+ async function genFlag(target, { external = false } = {}) {
165
+ if (!LABELS[target]) throw new Error(`Unknown target "${target}".`);
166
+ if (external && !EXTERNAL_CAPABLE.has(target)) {
167
+ throw new Error(
168
+ `-x is not supported for ${LABELS[target]} — external applies to ` +
169
+ `-c, -s, -dp and -lp only. Run \`nh gen ${FLAG[target]}\` without -x.`,
170
+ );
171
+ }
172
+
173
+ const paths = projectPaths();
174
+ assertFlavourMatchesProject(paths.root);
175
+ log.info(`Project: ${paths.root}\n`);
176
+
177
+ const touched = [];
178
+ const report = (result, filePath, root) => {
179
+ if (result !== 'skipped') touched.push(filePath);
180
+ reportFile(result, filePath, root);
181
+ };
182
+
183
+ log.info(`Generating: ${external ? 'external ' : ''}${DESCRIBES[target]}.`);
184
+
185
+ // 1. Module ---------------------------------------------------------------
186
+ const known = sync.scanModules(paths).map((m) => m.moduleDot);
187
+ const { moduleNameRaw } = await inquirer.prompt([
188
+ {
189
+ type: 'input',
190
+ name: 'moduleNameRaw',
191
+ message: known.length
192
+ ? `Module name (existing: ${known.join(', ')}):`
193
+ : 'Module name:',
194
+ validate: (v) => (v && v.trim() ? true : 'Module name is required'),
195
+ },
196
+ ]);
197
+ const moduleDot = naming.toDotCase(moduleNameRaw);
198
+
199
+ // 2. Name -----------------------------------------------------------------
200
+ const { rawName } = await inquirer.prompt([
201
+ {
202
+ type: 'input',
203
+ name: 'rawName',
204
+ message: `${LABELS[target].replace(/^./, (c) => c.toUpperCase())} name (PascalCase):`,
205
+ validate: (v) => (v && v.trim() ? true : 'Name is required'),
206
+ },
207
+ ]);
208
+ const n = tpl.names(rawName);
209
+
210
+ // 3. Target-specific answers ---------------------------------------------
211
+ let routePath;
212
+ if (target === 'controller') {
213
+ ({ routePath } = await inquirer.prompt([
214
+ {
215
+ type: 'input',
216
+ name: 'routePath',
217
+ message: external
218
+ ? 'Route path (mounted at api/e/v1/<path>):'
219
+ : 'Route path (mounted at api/v1/<path>):',
220
+ default: n.kebab,
221
+ validate: (v) => (v && v.trim() ? true : 'Route path is required'),
222
+ },
223
+ ]));
224
+ routePath = routePath.trim().replace(/^\/+|\/+$/g, '');
225
+ }
226
+
227
+ let tableName;
228
+ if (target === 'listProcessor' || target === 'entity') {
229
+ ({ tableName } = await inquirer.prompt([
230
+ {
231
+ type: 'input',
232
+ name: 'tableName',
233
+ message:
234
+ target === 'entity' ? 'Table name:' : 'Table to list (table name):',
235
+ default: `nh_${naming.toWords(rawName).join('_')}s`,
236
+ validate: (v) => (v && v.trim() ? true : 'Table name is required'),
237
+ },
238
+ ]));
239
+ tableName = tableName.trim();
240
+ }
241
+
242
+ // 4. Refuse to shadow a name that is already taken ------------------------
243
+ // Checked for every artifact up front, so a target that writes three files
244
+ // does not create the first and then abort on the second.
245
+ const artifacts = primaryArtifacts(
246
+ target,
247
+ paths.moduleDir(moduleDot),
248
+ n,
249
+ external,
250
+ );
251
+ for (const artifact of artifacts) {
252
+ const conflict = findConflict(paths.srcDir, {
253
+ className: artifact.className,
254
+ fileName: path.basename(artifact.file),
255
+ });
256
+ if (conflict) {
257
+ throw new Error(
258
+ `${describeConflict(conflict, paths.root)}.\n` +
259
+ ` Pick a different name, or edit that file instead — ` +
260
+ `nothing was generated.`,
261
+ );
262
+ }
263
+ }
264
+ const dest = artifacts[0].file;
265
+
266
+ // 5. Write ----------------------------------------------------------------
267
+ const moduleDir = ensureModule(paths, moduleDot, external, report);
268
+ const write = (filePath, content) =>
269
+ report(writeFile(filePath, content), filePath, paths.root);
270
+
271
+ switch (target) {
272
+ case 'controller':
273
+ write(
274
+ dest,
275
+ external
276
+ ? tpl.bareExternalController(rawName, routePath)
277
+ : tpl.bareController(rawName, routePath),
278
+ );
279
+ break;
280
+
281
+ case 'service':
282
+ write(dest, tpl.bareService(rawName, { external }));
283
+ break;
284
+
285
+ case 'dataProcessor':
286
+ write(dest, tpl.processor(rawName, { external }));
287
+ // The processor's payload DTO lives in the module's dtos/ either way —
288
+ // created here only if it is not already there.
289
+ write(path.join(moduleDir, 'dtos', n.dtoFile), tpl.dto(rawName));
290
+ break;
291
+
292
+ case 'listProcessor':
293
+ write(dest, tpl.listProcessor(rawName, tableName, { external }));
294
+ break;
295
+
296
+ case 'entity': {
297
+ write(
298
+ path.join(moduleDir, 'dtos', n.attributesDtoFile),
299
+ tpl.attributesDto(rawName),
300
+ );
301
+ write(dest, tpl.entity(rawName, tableName));
302
+
303
+ // The entity's lifecycle hook and the job it drives. Same pair the
304
+ // wizard writes: the subscriber listens to the entity and hands each
305
+ // event to the job, which is why the job takes a DatabaseEventDto here
306
+ // rather than the loose payload `nh gen -j` scaffolds.
307
+ write(path.join(moduleDir, 'jobs', n.jobFile), tpl.job(rawName));
308
+ write(
309
+ path.join(moduleDir, 'subscribers', n.subscriberFile),
310
+ tpl.subscriber(rawName),
311
+ );
312
+
313
+ const ts = Date.now();
314
+ const migrationPath = path.join(
315
+ paths.migrationsDir,
316
+ naming.migrationFileName(n.migrationName, ts),
317
+ );
318
+ write(migrationPath, tpl.genMigration(rawName, tableName, ts));
319
+
320
+ registerEntity(paths, rawName, moduleDot, report);
321
+ registerJob(paths, rawName, moduleDot, report);
322
+ break;
323
+ }
324
+
325
+ case 'job':
326
+ write(dest, tpl.additionalJob(rawName));
327
+ registerJob(paths, rawName, moduleDot, report);
328
+ break;
329
+ }
330
+
331
+ // 6. Wire it into the barrel + app.module, then format -------------------
332
+ log.info('\nWiring imports...');
333
+ const mod = sync.scanModule(
334
+ moduleDir,
335
+ path.join(moduleDir, `${moduleDot}.module.ts`),
336
+ );
337
+ const written = sync.applyPlan(sync.buildPlan(paths, [mod]));
338
+ for (const w of written) report('overwritten', w, paths.root);
339
+ if (!written.length) log.info(' (already in sync)');
340
+
341
+ const formatted = await formatFiles(touched, paths.root);
342
+ if (formatted === null) {
343
+ log.info(' (prettier not installed — files left unformatted)');
344
+ } else if (formatted) {
345
+ log.info(` formatted ${formatted} file(s)`);
346
+ }
347
+
348
+ const made = artifacts.map((a) => a.className).join(', ');
349
+ log.done(`${made} generated in "${moduleDot}".`);
350
+ console.log(
351
+ chalk.dim(
352
+ `Run \`${cli()} sync\` anytime to re-wire imports after manual edits.`,
353
+ ),
354
+ );
355
+ }
356
+
357
+ module.exports = genFlag;
@@ -6,21 +6,28 @@ const inquirer = require("inquirer");
6
6
  const chalk = require("chalk");
7
7
 
8
8
  const { projectPaths, MODULE_SUBFOLDERS } = require("../lib/paths");
9
+ const { assertFlavourMatchesProject, cli } = require("../lib/flavour");
9
10
  const naming = require("../lib/naming");
10
11
  const tpl = require("../lib/templates");
11
- const { writeFile, report, log, ensureDir } = require("../lib/fs.utils");
12
+ const { writeFile, report: reportFile, log, ensureDir } = require("../lib/fs.utils");
12
13
  const sync = require("../lib/sync.engine");
13
- const {
14
- ensureConstantEntry,
15
- ensureMapEntry,
16
- detectExportName,
17
- entityConstantsPath,
18
- } = require("../lib/constants.engine");
14
+ const { formatFiles } = require("../lib/format");
15
+ const { registerJob, registerEntity } = require("../lib/registry");
19
16
 
20
17
  async function gen() {
21
18
  const paths = projectPaths();
19
+ assertFlavourMatchesProject(paths.root);
22
20
  log.info(`Project: ${paths.root}\n`);
23
21
 
22
+ // Every file this run writes or rewrites, formatted in one pass at the end.
23
+ // Each write is announced through report(), so recording paths here catches
24
+ // templates, the constant registries, the es6 barrel and app.module alike.
25
+ const touched = [];
26
+ const report = (result, filePath, root) => {
27
+ if (result !== "skipped") touched.push(filePath);
28
+ reportFile(result, filePath, root);
29
+ };
30
+
24
31
  // 1. Module ---------------------------------------------------------------
25
32
  const { moduleNameRaw } = await inquirer.prompt([
26
33
  {
@@ -94,74 +101,8 @@ async function gen() {
94
101
 
95
102
  // Register the entity + job identifiers in the project constant registries
96
103
  // (the generated job references JobConstants.<name>Job, so the entry must exist).
97
- const jobConstantsPath = path.join(paths.constantsDir, "job.constants.ts");
98
- const jobEntry = ensureConstantEntry(
99
- jobConstantsPath,
100
- tpl.jobConstantsFile(entityName),
101
- `${n.camel}Job`,
102
- `${n.dot}.job`,
103
- );
104
- if (jobEntry === "failed") {
105
- log.warn(`Could not update ${path.relative(paths.root, jobConstantsPath)} — add ${n.camel}Job: '${n.dot}.job' manually.`);
106
- } else {
107
- report(jobEntry, jobConstantsPath, paths.root);
108
- }
109
-
110
- const entityConstants = entityConstantsPath(paths.constantsDir);
111
- const entityEntry = ensureConstantEntry(
112
- entityConstants,
113
- tpl.entityConstantsFile(entityName),
114
- n.camel,
115
- n.dot,
116
- );
117
- if (entityEntry === "failed") {
118
- log.warn(`Could not update ${path.relative(paths.root, entityConstants)} — add ${n.camel}: '${n.dot}' manually.`);
119
- } else {
120
- report(entityEntry, entityConstants, paths.root);
121
- }
122
-
123
- // ...and mirror them in the identifier → class map registries.
124
- const jobConstantsRef = {
125
- exportName: detectExportName(jobConstantsPath) || "JobConstants",
126
- importPath: "./job.constants",
127
- };
128
- const jobMapsPath = path.join(paths.constantsDir, "job.maps.ts");
129
- const jobMapEntry = ensureMapEntry(
130
- jobMapsPath,
131
- tpl.jobMapsFile(entityName, moduleDot, jobConstantsRef),
132
- `[${jobConstantsRef.exportName}.${n.camel}Job]`,
133
- n.jobClass,
134
- [
135
- { className: n.jobClass, importPath: `../${moduleDot}/jobs/${n.dot}.job` },
136
- { className: jobConstantsRef.exportName, importPath: jobConstantsRef.importPath },
137
- ],
138
- );
139
- if (jobMapEntry === "failed") {
140
- log.warn(`Could not update ${path.relative(paths.root, jobMapsPath)} — map ${n.jobClass} manually.`);
141
- } else {
142
- report(jobMapEntry, jobMapsPath, paths.root);
143
- }
144
-
145
- const entityConstantsRef = {
146
- exportName: detectExportName(entityConstants) || "EntityConstants",
147
- importPath: `./${path.basename(entityConstants, ".ts")}`,
148
- };
149
- const entityMapsPath = path.join(paths.constantsDir, "entity.maps.ts");
150
- const entityMapEntry = ensureMapEntry(
151
- entityMapsPath,
152
- tpl.entityMapsFile(entityName, moduleDot, entityConstantsRef),
153
- `[${entityConstantsRef.exportName}.${n.camel}]`,
154
- n.entityClass,
155
- [
156
- { className: n.entityClass, importPath: `../${moduleDot}/entities/${n.dot}.entity` },
157
- { className: entityConstantsRef.exportName, importPath: entityConstantsRef.importPath },
158
- ],
159
- );
160
- if (entityMapEntry === "failed") {
161
- log.warn(`Could not update ${path.relative(paths.root, entityMapsPath)} — map ${n.entityClass} manually.`);
162
- } else {
163
- report(entityMapEntry, entityMapsPath, paths.root);
164
- }
104
+ registerJob(paths, entityName, moduleDot, report);
105
+ registerEntity(paths, entityName, moduleDot, report);
165
106
 
166
107
  // 4. Optional: controller -------------------------------------------------
167
108
  const { controller } = await inquirer.prompt([
@@ -173,8 +114,11 @@ async function gen() {
173
114
  },
174
115
  ]);
175
116
 
176
- // (written after the listing-processor prompt — the answer decides whether
177
- // the controller ships with a POST list endpoint)
117
+ // The service is always generated — it is the layer the controller and any
118
+ // job, subscriber or sibling service goes through, never an opt-in. It is
119
+ // written after the processor prompts, because those answers decide which
120
+ // methods it (and the controller) ships with: `get()` always, `upsert()`
121
+ // with a data processor, `list()` with a listing processor.
178
122
 
179
123
  // 5. Optional: data processor (includes Add*DataDto) ----------------------
180
124
  const { processor } = await inquirer.prompt([
@@ -213,10 +157,17 @@ async function gen() {
213
157
  );
214
158
  }
215
159
 
160
+ const servicePath = path.join(moduleDir, "services", n.serviceFile);
161
+ report(
162
+ writeFile(servicePath, tpl.service(entityName, { processor, listProcessor })),
163
+ servicePath,
164
+ paths.root,
165
+ );
166
+
216
167
  if (controller) {
217
168
  const p = path.join(moduleDir, "controllers", n.controllerFile);
218
169
  report(
219
- writeFile(p, tpl.controller(entityName, { listProcessor })),
170
+ writeFile(p, tpl.controller(entityName, { processor, listProcessor })),
220
171
  p,
221
172
  paths.root,
222
173
  );
@@ -255,9 +206,20 @@ async function gen() {
255
206
  for (const w of written) report("overwritten", w, paths.root);
256
207
  if (!written.length) log.info(" (already in sync)");
257
208
 
209
+ // The boilerplate lints with `prettier/prettier: error`, so hand the
210
+ // generated files to the project's own prettier before declaring done.
211
+ const formatted = await formatFiles(touched, paths.root);
212
+ if (formatted === null) {
213
+ log.info(" (prettier not installed — files left unformatted)");
214
+ } else if (formatted) {
215
+ log.info(` formatted ${formatted} file(s)`);
216
+ }
217
+
258
218
  log.done(`Module "${moduleDot}" scaffolded.`);
259
219
  console.log(
260
- chalk.dim("Run `nh sync` anytime to re-wire imports after manual edits."),
220
+ chalk.dim(
221
+ `Run \`${cli()} sync\` anytime to re-wire imports after manual edits.`,
222
+ ),
261
223
  );
262
224
  }
263
225
 
@@ -4,12 +4,14 @@ const path = require("path");
4
4
  const inquirer = require("inquirer");
5
5
 
6
6
  const { projectPaths } = require("../lib/paths");
7
+ const { assertFlavourMatchesProject } = require("../lib/flavour");
7
8
  const naming = require("../lib/naming");
8
9
  const tpl = require("../lib/templates");
9
10
  const { writeFile, report, log } = require("../lib/fs.utils");
10
11
 
11
12
  async function migration() {
12
13
  const paths = projectPaths();
14
+ assertFlavourMatchesProject(paths.root);
13
15
 
14
16
  const { tableName, migrationName } = await inquirer.prompt([
15
17
  {
@@ -4,12 +4,14 @@ const path = require("path");
4
4
  const inquirer = require("inquirer");
5
5
 
6
6
  const { projectPaths } = require("../lib/paths");
7
+ const { assertFlavourMatchesProject } = require("../lib/flavour");
7
8
  const naming = require("../lib/naming");
8
9
  const tpl = require("../lib/templates");
9
10
  const { writeFile, report, log } = require("../lib/fs.utils");
10
11
 
11
12
  async function seed() {
12
13
  const paths = projectPaths();
14
+ assertFlavourMatchesProject(paths.root);
13
15
 
14
16
  const { tableName, seedName } = await inquirer.prompt([
15
17
  {
package/src/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { Command } = require('commander');
4
4
  const chalk = require('chalk');
5
5
  const pkg = require('../package.json');
6
+ const flavour = require('./lib/flavour');
6
7
 
7
8
  const program = new Command();
8
9
 
@@ -17,15 +18,77 @@ function run(action) {
17
18
  };
18
19
  }
19
20
 
21
+ // `nh` and `nhx` are the same program against different packages: the bin
22
+ // entrypoint picks the flavour before this module is required, and everything
23
+ // that generates code reads it from there.
24
+ const { cli, package: packageName } = flavour.current();
25
+
20
26
  program
21
- .name('nh')
22
- .description('nh — scaffolding CLI for neohm. NestJS projects (built on @neohm/nestend)')
27
+ .name(cli)
28
+ .description(
29
+ `${cli} — scaffolding CLI for neohm. NestJS projects (built on ${packageName})`,
30
+ )
23
31
  .version(pkg.version, '-v, --version', 'output the current version');
24
32
 
33
+ // `gen` with no target flag runs the full wizard; a target flag generates just
34
+ // that one artifact. -x mirrors a target onto the module's external (API-key)
35
+ // surface and applies to -c, -s, -dp and -lp only.
36
+ const GEN_TARGETS = [
37
+ ['controller', 'controller'],
38
+ ['service', 'service'],
39
+ ['dataProcessor', 'dataProcessor'],
40
+ ['listProcessor', 'listProcessor'],
41
+ ['entity', 'entity'],
42
+ ['job', 'job'],
43
+ ];
44
+
25
45
  program
26
46
  .command('gen')
27
- .description('interactive scaffolding wizard (module, entity, controller, dto, processor, migration)')
28
- .action(run(() => require('./commands/gen')()));
47
+ .description(
48
+ 'scaffolding wizard; with a flag, generate one artifact instead',
49
+ )
50
+ .option('-c, --controller', 'generate only a controller')
51
+ .option('-s, --service', 'generate only a service')
52
+ .option('-dp, --data-processor', 'generate only a data processor')
53
+ .option('-lp, --list-processor', 'generate only a listing processor')
54
+ .option('-e, --entity', 'generate an entity and its migration')
55
+ .option('-j, --job', 'generate only a job')
56
+ .option('-x, --external', 'put it on the external (api/e/v1) surface')
57
+ .addHelpText(
58
+ 'after',
59
+ `
60
+ Examples:
61
+ ${cli} gen full wizard: module, entity, controller, service, ...
62
+ ${cli} gen -c controller only (asks module, name, route path)
63
+ ${cli} gen -s bare injectable service
64
+ ${cli} gen -dp data processor (+ its Add<Name>DataDto if missing)
65
+ ${cli} gen -lp listing processor
66
+ ${cli} gen -e entity + migration, registered in the constants/maps
67
+ ${cli} gen -j job, registered in job.constants/job.maps
68
+ ${cli} gen -x -c external controller under <module>/external/controllers
69
+ ${cli} gen -x -dp external data processor under <module>/external/libraries`,
70
+ )
71
+ .action(
72
+ run((opts) => {
73
+ const chosen = GEN_TARGETS.filter(([key]) => opts[key]).map(
74
+ ([, target]) => target,
75
+ );
76
+ if (chosen.length > 1) {
77
+ throw new Error(
78
+ 'Pick one target flag at a time (-c, -s, -dp, -lp, -e or -j).',
79
+ );
80
+ }
81
+ if (!chosen.length) {
82
+ if (opts.external) {
83
+ throw new Error('-x needs a target: -c, -s, -dp or -lp.');
84
+ }
85
+ return require('./commands/gen')();
86
+ }
87
+ return require('./commands/gen.flag')(chosen[0], {
88
+ external: Boolean(opts.external),
89
+ });
90
+ }),
91
+ );
29
92
 
30
93
  program
31
94
  .command('migration')