millas 0.2.27 → 0.2.29
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/bin/millas.js +12 -2
- package/package.json +2 -1
- package/src/cli.js +117 -20
- package/src/commands/call.js +1 -1
- package/src/commands/createsuperuser.js +137 -182
- package/src/commands/key.js +61 -83
- package/src/commands/lang.js +423 -515
- package/src/commands/make.js +88 -62
- package/src/commands/migrate.js +200 -279
- package/src/commands/new.js +55 -50
- package/src/commands/route.js +78 -80
- package/src/commands/schedule.js +52 -150
- package/src/commands/serve.js +158 -191
- package/src/console/AppCommand.js +106 -0
- package/src/console/BaseCommand.js +726 -0
- package/src/console/CommandContext.js +66 -0
- package/src/console/CommandRegistry.js +88 -0
- package/src/console/Style.js +123 -0
- package/src/console/index.js +12 -3
- package/src/container/AppInitializer.js +10 -0
- package/src/container/Application.js +2 -0
- package/src/facades/DB.js +195 -0
- package/src/index.js +2 -1
- package/src/scaffold/maker.js +102 -42
- package/src/schematics/Collection.js +28 -0
- package/src/schematics/SchematicEngine.js +122 -0
- package/src/schematics/Template.js +99 -0
- package/src/schematics/index.js +7 -0
- package/src/templates/command/default.template.js +14 -0
- package/src/templates/command/schema.json +19 -0
- package/src/templates/controller/default.template.js +10 -0
- package/src/templates/controller/resource.template.js +59 -0
- package/src/templates/controller/schema.json +30 -0
- package/src/templates/job/default.template.js +11 -0
- package/src/templates/job/schema.json +19 -0
- package/src/templates/middleware/default.template.js +11 -0
- package/src/templates/middleware/schema.json +19 -0
- package/src/templates/migration/default.template.js +14 -0
- package/src/templates/migration/schema.json +19 -0
- package/src/templates/model/default.template.js +14 -0
- package/src/templates/model/migration.template.js +17 -0
- package/src/templates/model/schema.json +30 -0
- package/src/templates/service/default.template.js +12 -0
- package/src/templates/service/schema.json +19 -0
- package/src/templates/shape/default.template.js +11 -0
- package/src/templates/shape/schema.json +19 -0
- package/src/validation/BaseValidator.js +3 -0
- package/src/validation/types.js +3 -3
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const Style = require('./Style');
|
|
5
|
+
const AppCommand = require('./AppCommand');
|
|
6
|
+
const v = require("../core/validation");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} CommandContext
|
|
10
|
+
* @property {Object} program - Commander program instance
|
|
11
|
+
* @property {Object} container - DI container
|
|
12
|
+
* @property {Object} logger - Logger instance
|
|
13
|
+
* @property {string} cwd - Current working directory
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @callback StringValidatorCallback
|
|
18
|
+
* @param {import('../validation/types').StringValidator} validator - String validator instance
|
|
19
|
+
* @returns {import('../validation/types').StringValidator}
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @callback NumberValidatorCallback
|
|
24
|
+
* @param {import('../validation/types').NumberValidator} validator - Number validator instance
|
|
25
|
+
* @returns {import('../validation/types').NumberValidator}
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @callback EmailValidatorCallback
|
|
30
|
+
* @param {import('../validation/types').EmailValidator} validator - Email validator instance
|
|
31
|
+
* @returns {import('../validation/types').EmailValidator}
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @callback BooleanValidatorCallback
|
|
36
|
+
* @param {import('../validation/types').BooleanValidator} validator - Boolean validator instance
|
|
37
|
+
* @returns {import('../validation/types').BooleanValidator}
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @callback ValidatorCallback
|
|
42
|
+
* @param {typeof import('../core/validation')} v - Validation module with factory functions
|
|
43
|
+
* @returns {import('../validation/BaseValidator').BaseValidator}
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Base class for all CLI commands
|
|
48
|
+
*/
|
|
49
|
+
class BaseCommand extends AppCommand {
|
|
50
|
+
static command = '';
|
|
51
|
+
static namespace = ''; // Override to set custom namespace
|
|
52
|
+
static description = '';
|
|
53
|
+
static aliases = [];
|
|
54
|
+
static options = [];
|
|
55
|
+
|
|
56
|
+
constructor(context) {
|
|
57
|
+
super(context);
|
|
58
|
+
this.style = new Style();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize commands - override in subclasses
|
|
63
|
+
* @param {CommandRegistrar} register - Command registration helper
|
|
64
|
+
* @returns {Promise<void>}
|
|
65
|
+
*/
|
|
66
|
+
async onInit(register) {
|
|
67
|
+
// Override in subclasses to register commands
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Auto-derive command name from class name
|
|
72
|
+
* Supports CamelCase, underscores, and hyphens
|
|
73
|
+
*/
|
|
74
|
+
static getCommandName() {
|
|
75
|
+
// Use custom namespace if provided
|
|
76
|
+
if (this.namespace) return this.namespace;
|
|
77
|
+
if (this.command) return this.command;
|
|
78
|
+
|
|
79
|
+
const className = this.name.replace(/Command$/i, '');
|
|
80
|
+
if (!className) {
|
|
81
|
+
throw new Error(`Cannot derive command name from class ${this.name}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return className
|
|
85
|
+
.replace(/[-_]/g, ':')
|
|
86
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1:$2')
|
|
87
|
+
.replace(/([a-z\d])([A-Z])/g, '$1:$2')
|
|
88
|
+
.toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getSubcommands() {
|
|
92
|
+
const proto = Object.getPrototypeOf(this);
|
|
93
|
+
return Object.getOwnPropertyNames(proto)
|
|
94
|
+
.filter(name => {
|
|
95
|
+
return typeof proto[name] === 'function' &&
|
|
96
|
+
name !== 'constructor' &&
|
|
97
|
+
!['register', 'handle', 'validate', 'before', 'after', 'onError', 'onInit', 'addArguments'].includes(name) &&
|
|
98
|
+
!name.startsWith('_');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register command with Commander
|
|
104
|
+
* Calls onInit() for subcommands or registerSimpleCommand() for simple commands
|
|
105
|
+
*/
|
|
106
|
+
async register() {
|
|
107
|
+
const commandName = this.constructor.getCommandName();
|
|
108
|
+
const subcommands = this.getSubcommands();
|
|
109
|
+
|
|
110
|
+
if (typeof this.onInit === 'function') {
|
|
111
|
+
const registrar = new CommandRegistrar(this, commandName);
|
|
112
|
+
await this.onInit(registrar);
|
|
113
|
+
|
|
114
|
+
// Find subcommand matching base name and make it the default
|
|
115
|
+
for (const cmd of registrar.commands) {
|
|
116
|
+
const subName = cmd.fullCommand.split(':')[1];
|
|
117
|
+
if (subName === commandName) {
|
|
118
|
+
// Register as base command (index/default)
|
|
119
|
+
cmd.fullCommand = commandName;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
registrar.finalizeAll();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (subcommands.length === 0 || subcommands.includes('handle')) {
|
|
129
|
+
this.registerSimpleCommand(commandName);
|
|
130
|
+
} else {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Command ${commandName} has subcommands but no onInit() method. ` +
|
|
133
|
+
`Add: async onInit(register) { register.command(this.${subcommands[0]}); }`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
registerSimpleCommand(commandName) {
|
|
139
|
+
const cmd = this.program
|
|
140
|
+
.command(commandName)
|
|
141
|
+
.description(this.constructor.description || 'No description provided');
|
|
142
|
+
|
|
143
|
+
if (this.constructor.aliases && this.constructor.aliases.length) {
|
|
144
|
+
cmd.aliases(this.constructor.aliases);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.constructor.options && this.constructor.options.length) {
|
|
148
|
+
for (const opt of this.constructor.options) {
|
|
149
|
+
if (opt.flags) {
|
|
150
|
+
cmd.option(opt.flags, opt.description || '', opt.defaultValue);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof this.addArguments === 'function') {
|
|
156
|
+
this.addArguments(cmd);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
cmd.action(this.asyncHandler(this.handle.bind(this)));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Override to add custom arguments/options
|
|
164
|
+
* @param {Command} parser - Commander command instance
|
|
165
|
+
*/
|
|
166
|
+
addArguments(parser) {}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Main command handler - must be implemented by subclasses
|
|
170
|
+
*/
|
|
171
|
+
async handle(...args) {
|
|
172
|
+
throw new Error(`Command ${this.constructor.name} must implement handle() method`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
asyncHandler(fn) {
|
|
176
|
+
return async (...args) => {
|
|
177
|
+
const startTime = Date.now();
|
|
178
|
+
try {
|
|
179
|
+
await this.before(...args);
|
|
180
|
+
await this.validate(...args);
|
|
181
|
+
const result = await fn.call(this, ...args);
|
|
182
|
+
await this.after(...args);
|
|
183
|
+
|
|
184
|
+
if (process.env.DEBUG) {
|
|
185
|
+
this.info(`Completed in ${Date.now() - startTime}ms`);
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
await this.onError(err);
|
|
190
|
+
this.handleError(err);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Lifecycle hook: runs before validation and handler
|
|
197
|
+
*/
|
|
198
|
+
async before(...args) {}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Lifecycle hook: runs after successful execution
|
|
202
|
+
*/
|
|
203
|
+
async after(...args) {}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Lifecycle hook: runs when error occurs
|
|
207
|
+
*/
|
|
208
|
+
async onError(err) {}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate command inputs before execution
|
|
212
|
+
*/
|
|
213
|
+
async validate(...args) {}
|
|
214
|
+
|
|
215
|
+
handleError(err) {
|
|
216
|
+
if (err.code === 'ENOENT') {
|
|
217
|
+
this.logger.error(this.style.danger(`\n ✖ File not found: ${err.path}\n`));
|
|
218
|
+
} else if (err.code === 'EEXIST') {
|
|
219
|
+
this.logger.error(this.style.danger(`\n ✖ File already exists: ${err.path}\n`));
|
|
220
|
+
} else {
|
|
221
|
+
this.logger.error(this.style.danger(`\n ✖ Error: ${err.message}\n`));
|
|
222
|
+
if (process.env.DEBUG) {
|
|
223
|
+
this.logger.error(err.stack);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.program?.exitOverride || process.env.NODE_ENV === 'test') {
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
success(message) {
|
|
235
|
+
this.logger.log(this.style.success(`\n ✔ ${message}\n`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
info(message) {
|
|
239
|
+
this.logger.log(this.style.info(` ${message}`));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
warn(message) {
|
|
243
|
+
this.logger.log(this.style.warning(` ⚠ ${message}`));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
error(message) {
|
|
247
|
+
this.logger.error(this.style.danger(` ✖ ${message}`));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* CommandRegistrar - Helper for explicit command registration
|
|
253
|
+
*/
|
|
254
|
+
class CommandRegistrar {
|
|
255
|
+
constructor(baseCommand, baseName) {
|
|
256
|
+
this.baseCommand = baseCommand;
|
|
257
|
+
this.baseName = baseName;
|
|
258
|
+
this.program = baseCommand.program;
|
|
259
|
+
this.lastCommand = null;
|
|
260
|
+
this.commands = [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Start registering a command
|
|
265
|
+
*
|
|
266
|
+
* @param {Function} fn - The method to register (can be arrow function or method)
|
|
267
|
+
* @returns {CommandRegistrar} - For chaining
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* // Using a method
|
|
271
|
+
* register.command(this.create)
|
|
272
|
+
* .arg('name', string().required(), 'User name')
|
|
273
|
+
*
|
|
274
|
+
* // Using inline arrow function
|
|
275
|
+
* register.command(async (name) => {
|
|
276
|
+
* this.success(`Created ${name}`);
|
|
277
|
+
* })
|
|
278
|
+
* .arg('name', 'Item name')
|
|
279
|
+
* .name('create')
|
|
280
|
+
*/
|
|
281
|
+
command(fn) {
|
|
282
|
+
if (this.lastCommand) {
|
|
283
|
+
this.#finalize();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Derive method name from function name, or use 'anonymous' for arrow functions
|
|
287
|
+
const methodName = fn.name || 'anonymous';
|
|
288
|
+
const fullCommand = `${this.baseName}:${methodName}`;
|
|
289
|
+
const defaultDescription = methodName !== 'anonymous'
|
|
290
|
+
? `${methodName.charAt(0).toUpperCase()}${methodName.slice(1)} command`
|
|
291
|
+
: 'Command';
|
|
292
|
+
|
|
293
|
+
this.lastCommand = {
|
|
294
|
+
fn,
|
|
295
|
+
methodName,
|
|
296
|
+
fullCommand,
|
|
297
|
+
description: defaultDescription,
|
|
298
|
+
args: [],
|
|
299
|
+
aliases: [],
|
|
300
|
+
hooks: {
|
|
301
|
+
before: null,
|
|
302
|
+
after: null,
|
|
303
|
+
validate: null,
|
|
304
|
+
onError: null
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
this.commands.push(this.lastCommand);
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Add an argument or flag
|
|
315
|
+
*
|
|
316
|
+
* @param {string} name - Argument name (use '--' prefix for flags)
|
|
317
|
+
* @param {import('../validation/BaseValidator').BaseValidator|ValidatorCallback|string} [validatorOrDescription] - Validator instance, callback, or description
|
|
318
|
+
* @param {string} [description] - Optional description
|
|
319
|
+
* @returns {CommandRegistrar} - For chaining
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* .arg('name', v => v.string().required().min(2), 'User full name')
|
|
323
|
+
* .arg('email', v => v.email().required())
|
|
324
|
+
* .arg('--admin', v => v.boolean(), 'Create as admin')
|
|
325
|
+
* .arg('--force', 'Force creation') // boolean by default
|
|
326
|
+
*/
|
|
327
|
+
arg(name, validatorOrDescription, description) {
|
|
328
|
+
if (!this.lastCommand) {
|
|
329
|
+
throw new Error('No command to add arg to. Call command() first.');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const isFlag = name.startsWith('--') || name.startsWith('-');
|
|
333
|
+
const cleanName = name.replace(/^--?/, '').replace(/^\[|\]$/g, '');
|
|
334
|
+
let validator = null;
|
|
335
|
+
let desc = description;
|
|
336
|
+
|
|
337
|
+
if (typeof validatorOrDescription === 'string') {
|
|
338
|
+
desc = validatorOrDescription;
|
|
339
|
+
} else if (typeof validatorOrDescription === 'function') {
|
|
340
|
+
// Callback receives validation module
|
|
341
|
+
validator = validatorOrDescription(require("../core/validation"));
|
|
342
|
+
} else if (validatorOrDescription && typeof validatorOrDescription === 'object') {
|
|
343
|
+
// Direct validator object
|
|
344
|
+
validator = validatorOrDescription;
|
|
345
|
+
if (!desc) {
|
|
346
|
+
desc = cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.lastCommand.args.push({
|
|
351
|
+
name: cleanName,
|
|
352
|
+
originalName: name,
|
|
353
|
+
isFlag,
|
|
354
|
+
validator,
|
|
355
|
+
description: desc || cleanName
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Alias for arg() - adds an argument
|
|
363
|
+
* @param {string} name - Argument name
|
|
364
|
+
* @param {import('../validation/BaseValidator').BaseValidator|ValidatorCallback|string} [validatorOrDescription] - Validator instance, callback, or description
|
|
365
|
+
* @param {string} [description] - Optional description
|
|
366
|
+
* @returns {CommandRegistrar}
|
|
367
|
+
*/
|
|
368
|
+
argument(name, validatorOrDescription, description) {
|
|
369
|
+
return this.arg(name, validatorOrDescription, description);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Alias for arg() with '--' prefix - adds an option/flag
|
|
374
|
+
* @param {string} name - Option name (without --)
|
|
375
|
+
* @param {import('../validation/BaseValidator').BaseValidator|ValidatorCallback|string} [validatorOrDescription] - Validator instance, callback, or description
|
|
376
|
+
* @param {string} [description] - Optional description
|
|
377
|
+
* @returns {CommandRegistrar}
|
|
378
|
+
*/
|
|
379
|
+
option(name, validatorOrDescription, description) {
|
|
380
|
+
if (!name.startsWith('--') && !name.startsWith('-')) {
|
|
381
|
+
name = '--' + name;
|
|
382
|
+
}
|
|
383
|
+
return this.arg(name, validatorOrDescription, description);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Add a string argument with optional validator
|
|
389
|
+
* @param {string} name - Argument name
|
|
390
|
+
* @param {StringValidatorCallback|string} [validatorOrDescription] - Validator callback or description
|
|
391
|
+
* @param {string} [description] - Optional description
|
|
392
|
+
* @returns {CommandRegistrar}
|
|
393
|
+
* @example
|
|
394
|
+
* .str('name', 'User name')
|
|
395
|
+
* .str('email', v => v.min(5).max(100), 'Email address')
|
|
396
|
+
*/
|
|
397
|
+
str(name, validatorOrDescription, description) {
|
|
398
|
+
if (typeof validatorOrDescription === 'function') {
|
|
399
|
+
return this.arg(name, v => {
|
|
400
|
+
const base = v.string().required();
|
|
401
|
+
return validatorOrDescription(base);
|
|
402
|
+
}, description);
|
|
403
|
+
}
|
|
404
|
+
return this.arg(name, v => v.string().required(), validatorOrDescription || description);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Add a number argument with optional validator
|
|
409
|
+
* @param {string} name - Argument name
|
|
410
|
+
* @param {NumberValidatorCallback|string} [validatorOrDescription] - Validator callback or description
|
|
411
|
+
* @param {string} [description] - Optional description
|
|
412
|
+
* @returns {CommandRegistrar}
|
|
413
|
+
* @example
|
|
414
|
+
* .num('age', 'User age')
|
|
415
|
+
* .num('port', v => v.min(1000).max(9999), 'Port number')
|
|
416
|
+
*/
|
|
417
|
+
num(name, validatorOrDescription, description) {
|
|
418
|
+
if (typeof validatorOrDescription === 'function') {
|
|
419
|
+
return this.arg(name, v => {
|
|
420
|
+
const base = v.number().required();
|
|
421
|
+
return validatorOrDescription(base);
|
|
422
|
+
}, description);
|
|
423
|
+
}
|
|
424
|
+
return this.arg(name, v => v.number().required(), validatorOrDescription || description);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Add a boolean flag
|
|
429
|
+
* @param {string} name - Flag name (without --)
|
|
430
|
+
* @param {BooleanValidatorCallback|string} [validatorOrDescription] - Validator callback or description
|
|
431
|
+
* @param {string} [description] - Optional description
|
|
432
|
+
* @returns {CommandRegistrar}
|
|
433
|
+
* @example
|
|
434
|
+
* .bool('force', 'Force operation')
|
|
435
|
+
* .bool('verbose', 'Verbose output')
|
|
436
|
+
*/
|
|
437
|
+
bool(name, validatorOrDescription, description) {
|
|
438
|
+
if (!name.startsWith('--') && !name.startsWith('-')) {
|
|
439
|
+
name = '--' + name;
|
|
440
|
+
}
|
|
441
|
+
if (typeof validatorOrDescription === 'function') {
|
|
442
|
+
return this.arg(name, v => {
|
|
443
|
+
const base = v.boolean();
|
|
444
|
+
return validatorOrDescription(base);
|
|
445
|
+
}, description);
|
|
446
|
+
}
|
|
447
|
+
return this.arg(name, v => v.boolean(), validatorOrDescription || description);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Add an email argument
|
|
452
|
+
* @param {string} name - Argument name
|
|
453
|
+
* @param {EmailValidatorCallback|string} [validatorOrDescription] - Validator callback or description
|
|
454
|
+
* @param {string} [description] - Optional description
|
|
455
|
+
* @returns {CommandRegistrar}
|
|
456
|
+
* @example
|
|
457
|
+
* .email('email', 'User email address')
|
|
458
|
+
* .email('email', v => v.domain('example.com'), 'Company email')
|
|
459
|
+
*/
|
|
460
|
+
email(name, validatorOrDescription, description) {
|
|
461
|
+
if (typeof validatorOrDescription === 'function') {
|
|
462
|
+
return this.arg(name, v => {
|
|
463
|
+
const base = v.email().required();
|
|
464
|
+
return validatorOrDescription(base);
|
|
465
|
+
}, description);
|
|
466
|
+
}
|
|
467
|
+
return this.arg(name, v => v.email().required(), validatorOrDescription || description);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Add an enum argument
|
|
472
|
+
* @param {string} name - Argument name
|
|
473
|
+
* @param {Array<string>} values - Allowed values
|
|
474
|
+
* @param {string} [description] - Optional description
|
|
475
|
+
* @returns {CommandRegistrar}
|
|
476
|
+
* @example
|
|
477
|
+
* .enum('role', ['admin', 'user', 'guest'], 'User role')
|
|
478
|
+
*/
|
|
479
|
+
enum(name, values, description) {
|
|
480
|
+
return this.arg(name, v => v.enum(values).required(), description);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Set description for the command
|
|
485
|
+
*/
|
|
486
|
+
description(desc) {
|
|
487
|
+
if (!this.lastCommand) {
|
|
488
|
+
throw new Error('No command to set description for. Call command() first.');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.lastCommand.description = desc;
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Set aliases for the subcommand
|
|
497
|
+
*
|
|
498
|
+
* @param {string[]} aliases - Array of alias names
|
|
499
|
+
* @returns {CommandRegistrar} - For chaining
|
|
500
|
+
*
|
|
501
|
+
* @example
|
|
502
|
+
* register.command(this.create)
|
|
503
|
+
* .aliases(['c', 'new'])
|
|
504
|
+
*/
|
|
505
|
+
aliases(aliasArray) {
|
|
506
|
+
if (!this.lastCommand) {
|
|
507
|
+
throw new Error('No command to set aliases for. Call command() first.');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
this.lastCommand.aliases = aliasArray;
|
|
511
|
+
return this;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Override the command name
|
|
516
|
+
*
|
|
517
|
+
* @param {string} customName - Custom command name (without base)
|
|
518
|
+
* @returns {CommandRegistrar} - For chaining
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* register.command(this.update)
|
|
522
|
+
* .name('modify')
|
|
523
|
+
* // Results in: user:modify instead of user:update
|
|
524
|
+
*/
|
|
525
|
+
name(customName) {
|
|
526
|
+
if (!this.lastCommand) {
|
|
527
|
+
throw new Error('No command to set name for. Call command() first.');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.lastCommand.fullCommand = `${this.baseName}:${customName}`;
|
|
531
|
+
return this;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
before(fn) {
|
|
535
|
+
if (!this.lastCommand) {
|
|
536
|
+
throw new Error('No command to set before hook for. Call command() first.');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this.lastCommand.hooks.before = fn;
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
after(fn) {
|
|
544
|
+
if (!this.lastCommand) {
|
|
545
|
+
throw new Error('No command to set after hook for. Call command() first.');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
this.lastCommand.hooks.after = fn;
|
|
549
|
+
return this;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
validate(fn) {
|
|
553
|
+
if (!this.lastCommand) {
|
|
554
|
+
throw new Error('No command to set validate hook for. Call command() first.');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
this.lastCommand.hooks.validate = fn;
|
|
558
|
+
return this;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
onError(fn) {
|
|
562
|
+
if (!this.lastCommand) {
|
|
563
|
+
throw new Error('No command to set onError hook for. Call command() first.');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this.lastCommand.hooks.onError = fn;
|
|
567
|
+
return this;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
finalizeAll() {
|
|
571
|
+
this.#finalizeAll();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#finalizeAll() {
|
|
575
|
+
for (const cmd of this.commands) {
|
|
576
|
+
if (!cmd._finalized) {
|
|
577
|
+
this.lastCommand = cmd;
|
|
578
|
+
this.#finalize();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
#finalize() {
|
|
584
|
+
|
|
585
|
+
if (!this.lastCommand || this.lastCommand._finalized) return;
|
|
586
|
+
|
|
587
|
+
const { fn, fullCommand, description, args, hooks, aliases } = this.lastCommand;
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
let commandStr = fullCommand;
|
|
591
|
+
const positionalArgs = args.filter(a => !a.isFlag);
|
|
592
|
+
|
|
593
|
+
const flagArgs = args.filter(a => a.isFlag);
|
|
594
|
+
|
|
595
|
+
// Build command string with positional args - all optional, validators handle required checks
|
|
596
|
+
for (const arg of positionalArgs) {
|
|
597
|
+
commandStr += ` [${arg.name}]`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const cmd = this.program
|
|
601
|
+
.command(commandStr)
|
|
602
|
+
.description(description);
|
|
603
|
+
|
|
604
|
+
// Add subcommand aliases
|
|
605
|
+
if (aliases && aliases.length > 0) {
|
|
606
|
+
cmd.aliases(aliases);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
for (const arg of flagArgs) {
|
|
610
|
+
const flagName = `--${arg.name}`;
|
|
611
|
+
const negatedFlagName = `--no-${arg.name}`;
|
|
612
|
+
|
|
613
|
+
// Default validator for flags without one: boolean()
|
|
614
|
+
if (!arg.validator) {
|
|
615
|
+
arg.validator = v.boolean();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Detect boolean validator
|
|
619
|
+
const isBoolean = arg.validator._type === 'boolean';
|
|
620
|
+
|
|
621
|
+
if (isBoolean) {
|
|
622
|
+
cmd.option(flagName, arg.description);
|
|
623
|
+
cmd.option(negatedFlagName, `Disable ${arg.name}`);
|
|
624
|
+
} else {
|
|
625
|
+
cmd.option(`${flagName} [value]`, arg.description);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (process.env.AP_DEBUG) {
|
|
630
|
+
console.log(`Registered: ${commandStr}`);
|
|
631
|
+
console.log(` Args: ${args.map(a => a.originalName).join(', ')}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
cmd.action(async (...cmdArgs) => {
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
const cmdOptions = cmdArgs[cmdArgs.length - 1];
|
|
638
|
+
const options = cmdOptions.opts ? cmdOptions.opts() : cmdOptions;
|
|
639
|
+
const positionalValues = cmdArgs.slice(0, -1);
|
|
640
|
+
|
|
641
|
+
const handlerArgs = [];
|
|
642
|
+
const validationData = {};
|
|
643
|
+
let positionalIndex = 0;
|
|
644
|
+
|
|
645
|
+
for (const arg of args) {
|
|
646
|
+
let value = undefined;
|
|
647
|
+
|
|
648
|
+
if (arg.isFlag) {
|
|
649
|
+
value = options[arg.name];
|
|
650
|
+
} else {
|
|
651
|
+
value = positionalValues[positionalIndex++];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
handlerArgs.push(value);
|
|
656
|
+
validationData[arg.name] = value;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const startTime = Date.now();
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
if (hooks.before) {
|
|
663
|
+
await hooks.before.call(this.baseCommand, ...handlerArgs);
|
|
664
|
+
} else if (typeof this.baseCommand.before === 'function') {
|
|
665
|
+
await this.baseCommand.before(...handlerArgs);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
for (const arg of args) {
|
|
669
|
+
// Default validator for positional args without one: string().required()
|
|
670
|
+
if (!arg.validator && !arg.isFlag) {
|
|
671
|
+
arg.validator = v.string().required();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let value = validationData[arg.name];
|
|
675
|
+
|
|
676
|
+
// Apply default value if undefined and validator has default
|
|
677
|
+
if (value === undefined && arg.validator && arg.validator._default !== undefined) {
|
|
678
|
+
value = arg.validator._default;
|
|
679
|
+
validationData[arg.name] = value;
|
|
680
|
+
handlerArgs[args.indexOf(arg)] = value;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
const res = await arg.validator.run(value, arg.name);
|
|
685
|
+
if (res.error)
|
|
686
|
+
throw new Error(res.error);
|
|
687
|
+
} catch (err) {
|
|
688
|
+
throw new Error(`Validation failed for '${arg.name}': ${err.message}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (hooks.validate) {
|
|
693
|
+
await hooks.validate.call(this.baseCommand, ...handlerArgs);
|
|
694
|
+
} else if (typeof this.baseCommand.validate === 'function') {
|
|
695
|
+
await this.baseCommand.validate(...handlerArgs);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const result = await fn.call(this.baseCommand, ...handlerArgs);
|
|
699
|
+
|
|
700
|
+
if (hooks.after) {
|
|
701
|
+
await hooks.after.call(this.baseCommand, ...handlerArgs);
|
|
702
|
+
} else if (typeof this.baseCommand.after === 'function') {
|
|
703
|
+
await this.baseCommand.after(...handlerArgs);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (process.env.DEBUG) {
|
|
707
|
+
this.baseCommand.info(`Completed in ${Date.now() - startTime}ms`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return result;
|
|
711
|
+
} catch (err) {
|
|
712
|
+
if (hooks.onError) {
|
|
713
|
+
await hooks.onError.call(this.baseCommand, err);
|
|
714
|
+
} else if (typeof this.baseCommand.onError === 'function') {
|
|
715
|
+
await this.baseCommand.onError(err);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
this.baseCommand.handleError(err);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
this.lastCommand._finalized = true;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
module.exports = BaseCommand;
|