millas 0.2.29 → 0.2.31
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/package.json +1 -1
- package/src/cli.js +22 -3
- package/src/commands/{createsuperuser.js → admin.js} +40 -63
- package/src/commands/key.js +11 -18
- package/src/commands/lang.js +14 -14
- package/src/commands/make.js +5 -5
- package/src/commands/migrate.js +2 -2
- package/src/commands/new.js +2 -2
- package/src/commands/route.js +2 -2
- package/src/commands/schedule.js +2 -2
- package/src/commands/serve.js +3 -2
- package/src/console/BaseCommand.js +74 -19
- package/src/console/Command.js +55 -143
- package/src/console/CommandContext.js +1 -15
- package/src/console/CommandRegistry.js +56 -19
- package/src/console/index.js +0 -4
- package/src/container/AppInitializer.js +1 -1
- package/src/providers/ProviderRegistry.js +3 -0
- package/src/scheduler/TaskScheduler.js +1 -1
- package/src/templates/command/default.template.js +9 -6
- package/src/commands/call.js +0 -190
- package/src/console/CommandLoader.js +0 -165
package/src/console/Command.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
+
const BaseCommand = require("./BaseCommand");
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Command
|
|
@@ -75,45 +76,12 @@ const chalk = require('chalk');
|
|
|
75
76
|
* return; — success (exit 0)
|
|
76
77
|
* this.fail(msg) — print error + exit 1
|
|
77
78
|
*/
|
|
78
|
-
class Command {
|
|
79
|
-
/**
|
|
80
|
-
* The CLI signature — used as the command name.
|
|
81
|
-
* Use colons for namespacing: 'email:digest', 'cache:clear', 'db:seed'
|
|
82
|
-
*
|
|
83
|
-
* @type {string}
|
|
84
|
-
*/
|
|
85
|
-
static signature = '';
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Short description shown in `millas --help` and `millas list`.
|
|
89
|
-
*
|
|
90
|
-
* @type {string}
|
|
91
|
-
*/
|
|
92
|
-
static description = '';
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Positional arguments.
|
|
96
|
-
* Each entry: { name: string, description?: string, default?: * }
|
|
97
|
-
*
|
|
98
|
-
* @type {Array<{ name: string, description?: string, default?: * }>}
|
|
99
|
-
*/
|
|
100
|
-
static args = [];
|
|
101
79
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* @type {Array<{ flag: string, description?: string, default?: * }>}
|
|
108
|
-
*/
|
|
109
|
-
static options = [];
|
|
110
|
-
|
|
111
|
-
// ── Internal ───────────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
constructor() {
|
|
114
|
-
this._args = {};
|
|
115
|
-
this._opts = {};
|
|
116
|
-
}
|
|
80
|
+
/**
|
|
81
|
+
* @typedef {Object} SecreteOptions
|
|
82
|
+
* @property {{mesage?:string,error?:string,retry?:boolean}} [confirm] - Needs secrete Confirmation
|
|
83
|
+
*/
|
|
84
|
+
class Command extends BaseCommand{
|
|
117
85
|
|
|
118
86
|
/**
|
|
119
87
|
* Populate the command with parsed CLI values.
|
|
@@ -128,56 +96,72 @@ class Command {
|
|
|
128
96
|
this._opts = options || {};
|
|
129
97
|
}
|
|
130
98
|
|
|
131
|
-
// ── Input ──────────────────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get a positional argument value by name.
|
|
135
|
-
*
|
|
136
|
-
* const name = this.argument('name');
|
|
137
|
-
*
|
|
138
|
-
* @param {string} name
|
|
139
|
-
* @returns {*}
|
|
140
|
-
*/
|
|
141
|
-
argument(name) {
|
|
142
|
-
return this._args[name];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Get an option value by name (camelCase).
|
|
147
|
-
* Flags like --dry-run become dryRun.
|
|
148
|
-
*
|
|
149
|
-
* const limit = this.option('limit');
|
|
150
|
-
* const dry = this.option('dryRun');
|
|
151
|
-
*
|
|
152
|
-
* @param {string} name
|
|
153
|
-
* @returns {*}
|
|
154
|
-
*/
|
|
155
|
-
option(name) {
|
|
156
|
-
return this._opts[name];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
99
|
/**
|
|
160
100
|
* Prompt the user for input.
|
|
161
101
|
*
|
|
162
102
|
* const name = await this.ask('What is your name?');
|
|
103
|
+
* const name = await this.ask('What is your name?', 'Anonymous');
|
|
104
|
+
* const age = await this.ask('Your age?', null, v => Number.isInteger(+v) || 'Must be a number');
|
|
163
105
|
*
|
|
164
|
-
* @param {string}
|
|
106
|
+
* @param {string} question
|
|
107
|
+
* @param {string|null} [defaultValue=null]
|
|
108
|
+
* @param {(v:string)=>true|string} [validate] — return true to accept, or an error string
|
|
165
109
|
* @returns {Promise<string>}
|
|
166
110
|
*/
|
|
167
|
-
ask(question) {
|
|
168
|
-
|
|
111
|
+
async ask(question, defaultValue = null, validate = null) {
|
|
112
|
+
const hint = defaultValue != null ? chalk.dim(` [${defaultValue}]`) : '';
|
|
113
|
+
const prompt = `${question}${hint} `;
|
|
114
|
+
|
|
115
|
+
while (true) {
|
|
116
|
+
const raw = await _prompt(prompt);
|
|
117
|
+
const answer = raw.trim() || (defaultValue ?? '');
|
|
118
|
+
|
|
119
|
+
if (validate) {
|
|
120
|
+
const result = validate(answer);
|
|
121
|
+
if (result !== true) {
|
|
122
|
+
this.error(typeof result === 'string' ? result : 'Invalid input.');
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return answer;
|
|
128
|
+
}
|
|
169
129
|
}
|
|
170
130
|
|
|
171
131
|
/**
|
|
172
132
|
* Prompt the user for a secret (input hidden — for passwords).
|
|
173
133
|
*
|
|
174
134
|
* const pass = await this.secret('Password:');
|
|
135
|
+
* const pass = await this.secret('Password:', { confirm: { message: 'Confirm:' } });
|
|
136
|
+
* const pass = await this.secret('Password:', { validate: v => v.length >= 8 || 'Min 8 chars' });
|
|
175
137
|
*
|
|
176
138
|
* @param {string} question
|
|
139
|
+
* @param {{ confirm?: { message?: string, error?: string, retry?: boolean }, validate?: (v: string) => true|string }} [options]
|
|
177
140
|
* @returns {Promise<string>}
|
|
178
141
|
*/
|
|
179
|
-
secret(question) {
|
|
180
|
-
|
|
142
|
+
async secret(question, options = {}) {
|
|
143
|
+
while (true) {
|
|
144
|
+
const first = await _promptSecret(question + ' ');
|
|
145
|
+
|
|
146
|
+
if (options.validate) {
|
|
147
|
+
const result = options.validate(first);
|
|
148
|
+
if (result !== true) {
|
|
149
|
+
this.error(typeof result === 'string' ? result : 'Invalid input.');
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (options.confirm) {
|
|
155
|
+
const second = await _promptSecret(options.confirm.message ?? 'Confirm: ');
|
|
156
|
+
if (first !== second) {
|
|
157
|
+
this.error(options.confirm.error ?? "Inputs don't match!");
|
|
158
|
+
if (options.confirm.retry) continue;
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return first;
|
|
164
|
+
}
|
|
181
165
|
}
|
|
182
166
|
|
|
183
167
|
/**
|
|
@@ -198,71 +182,8 @@ class Command {
|
|
|
198
182
|
return trimmed === 'y' || trimmed === 'yes';
|
|
199
183
|
}
|
|
200
184
|
|
|
201
|
-
// ── Output ─────────────────────────────────────────────────────────────────
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Write a plain line to stdout.
|
|
205
|
-
* @param {string} [msg='']
|
|
206
|
-
*/
|
|
207
|
-
line(msg = '') {
|
|
208
|
-
process.stdout.write(msg + '\n');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Write a blank line.
|
|
213
|
-
*/
|
|
214
|
-
newLine() {
|
|
215
|
-
process.stdout.write('\n');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Informational message (cyan).
|
|
220
|
-
* @param {string} msg
|
|
221
|
-
*/
|
|
222
|
-
info(msg) {
|
|
223
|
-
this.line(chalk.cyan(` ${msg}`));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Success message (green ✔).
|
|
228
|
-
* @param {string} msg
|
|
229
|
-
*/
|
|
230
|
-
success(msg) {
|
|
231
|
-
this.line(chalk.green(` ✔ ${msg}`));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Warning message (yellow ⚠).
|
|
236
|
-
* @param {string} msg
|
|
237
|
-
*/
|
|
238
|
-
warn(msg) {
|
|
239
|
-
this.line(chalk.yellow(` ⚠ ${msg}`));
|
|
240
|
-
}
|
|
241
185
|
|
|
242
|
-
/**
|
|
243
|
-
* Error message (red ✖). Does NOT exit — use fail() to exit.
|
|
244
|
-
* @param {string} msg
|
|
245
|
-
*/
|
|
246
|
-
error(msg) {
|
|
247
|
-
this.line(chalk.red(` ✖ ${msg}`));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Dimmed / comment message.
|
|
252
|
-
* @param {string} msg
|
|
253
|
-
*/
|
|
254
|
-
comment(msg) {
|
|
255
|
-
this.line(chalk.dim(` // ${msg}`));
|
|
256
|
-
}
|
|
257
186
|
|
|
258
|
-
/**
|
|
259
|
-
* Print an error and exit with code 1.
|
|
260
|
-
* @param {string} msg
|
|
261
|
-
*/
|
|
262
|
-
fail(msg) {
|
|
263
|
-
this.error(msg);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
187
|
|
|
267
188
|
/**
|
|
268
189
|
* Render a simple table.
|
|
@@ -299,16 +220,7 @@ class Command {
|
|
|
299
220
|
this.newLine();
|
|
300
221
|
}
|
|
301
222
|
|
|
302
|
-
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
303
223
|
|
|
304
|
-
/**
|
|
305
|
-
* The command's entry point. Override this in your command.
|
|
306
|
-
*
|
|
307
|
-
* @returns {Promise<void>}
|
|
308
|
-
*/
|
|
309
|
-
async handle() {
|
|
310
|
-
throw new Error(`[Command] "${this.constructor.signature}" must implement handle().`);
|
|
311
|
-
}
|
|
312
224
|
}
|
|
313
225
|
|
|
314
226
|
// ── Prompt helpers ────────────────────────────────────────────────────────────
|
|
@@ -45,21 +45,7 @@ class CommandContext {
|
|
|
45
45
|
isMillasProject() {
|
|
46
46
|
const fs = require('fs');
|
|
47
47
|
const path = require('path');
|
|
48
|
-
|
|
49
|
-
const pkgPath = path.join(this.cwd, 'package.json');
|
|
50
|
-
if (!fs.existsSync(pkgPath)) {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
56
|
-
// Check for millas marker in package.json
|
|
57
|
-
return pkg.millas === true ||
|
|
58
|
-
(pkg.dependencies && 'millas' in pkg.dependencies) ||
|
|
59
|
-
(pkg.devDependencies && 'millas' in pkg.devDependencies);
|
|
60
|
-
} catch {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
48
|
+
return fs.existsSync(path.join(this.cwd, 'millas.config.js'));
|
|
63
49
|
}
|
|
64
50
|
}
|
|
65
51
|
|
|
@@ -45,26 +45,63 @@ class CommandRegistry {
|
|
|
45
45
|
*/
|
|
46
46
|
async loadCommand(commandPath) {
|
|
47
47
|
const CommandClass = require(commandPath);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// }
|
|
48
|
+
const Command = require('./Command');
|
|
49
|
+
|
|
50
|
+
if (typeof CommandClass !== 'function' || !(CommandClass.prototype instanceof Command)) return;
|
|
51
|
+
|
|
52
|
+
const commandInstance = new CommandClass(this.context);
|
|
53
|
+
commandInstance._filename = path.basename(commandPath, '.js');
|
|
54
|
+
|
|
55
|
+
// Collect subcommand names the user intends to register
|
|
56
|
+
// by running onInit against a dry registrar that only records names
|
|
57
|
+
const namespace = commandInstance.constructor.namespace
|
|
58
|
+
|| commandInstance._filename.toLowerCase();
|
|
59
|
+
|
|
60
|
+
const userSubcommandNames = await this.#collectSubcommandNames(commandInstance, namespace);
|
|
61
|
+
|
|
62
|
+
// Remove any conflicting built-in subcommands from Commander
|
|
63
|
+
for (const fullName of userSubcommandNames) {
|
|
64
|
+
const existing = this.context.program.commands.findIndex(c => c.name() === fullName);
|
|
65
|
+
if (existing !== -1) this.context.program.commands.splice(existing, 1);
|
|
67
66
|
}
|
|
67
|
+
|
|
68
|
+
await commandInstance.register();
|
|
69
|
+
this.commands.set(namespace, commandInstance);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async #collectSubcommandNames(instance, namespace) {
|
|
73
|
+
if (typeof instance.onInit !== 'function') return [namespace];
|
|
74
|
+
|
|
75
|
+
const names = [];
|
|
76
|
+
const dryRegistrar = {
|
|
77
|
+
_last: null,
|
|
78
|
+
command(fn) { this._last = { name: fn.name || 'anonymous' }; return this; },
|
|
79
|
+
name(n) { if (this._last) this._last.name = n; return this; },
|
|
80
|
+
// absorb all chaining methods
|
|
81
|
+
...Object.fromEntries(
|
|
82
|
+
['arg','str','num','bool','email','enum','option','argument',
|
|
83
|
+
'description','aliases','before','after','validate','onError']
|
|
84
|
+
.map(m => [m, function() { return this; }])
|
|
85
|
+
),
|
|
86
|
+
_flush(ns) {
|
|
87
|
+
if (this._last) {
|
|
88
|
+
names.push(this._last.name === ns ? ns : `${ns}:${this._last.name}`);
|
|
89
|
+
this._last = null;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Wrap command() to flush previous before starting next
|
|
95
|
+
const origCommand = dryRegistrar.command.bind(dryRegistrar);
|
|
96
|
+
dryRegistrar.command = function(fn) {
|
|
97
|
+
this._flush(namespace);
|
|
98
|
+
return origCommand(fn);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await instance.onInit(dryRegistrar);
|
|
102
|
+
dryRegistrar._flush(namespace);
|
|
103
|
+
|
|
104
|
+
return names;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
107
|
/**
|
package/src/console/index.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const Command = require('./Command');
|
|
4
|
-
const CommandLoader = require('./CommandLoader');
|
|
5
|
-
const BaseCommand = require('./BaseCommand');
|
|
6
4
|
const CommandContext = require('./CommandContext');
|
|
7
5
|
const CommandRegistry = require('./CommandRegistry');
|
|
8
6
|
|
|
9
7
|
module.exports = {
|
|
10
8
|
Command,
|
|
11
|
-
CommandLoader,
|
|
12
|
-
BaseCommand,
|
|
13
9
|
CommandContext,
|
|
14
10
|
CommandRegistry
|
|
15
11
|
};
|
|
@@ -236,7 +236,7 @@ class AppInitializer {
|
|
|
236
236
|
} else {
|
|
237
237
|
env = nunjucks.configure(viewsDir, {
|
|
238
238
|
autoescape: viewsConfig.autoescape ?? true,
|
|
239
|
-
watch: viewsConfig.watch ?? (process.env.NODE_ENV !== 'production'),
|
|
239
|
+
watch: viewsConfig.watch ?? (process.env.NODE_ENV !== 'production' && !process.env.MILLAS_CLI_MODE),
|
|
240
240
|
noCache: viewsConfig.noCache ?? (process.env.NODE_ENV !== 'production'),
|
|
241
241
|
throwOnUndefined: viewsConfig.throwOnUndefined ?? false,
|
|
242
242
|
express: expressApp,
|
|
@@ -77,8 +77,11 @@ class ProviderRegistry {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// Phase 2: boot all providers (async-safe)
|
|
80
|
+
// Providers with static cli = false are skipped in CLI mode
|
|
81
|
+
const isCli = !!process.env.MILLAS_CLI_MODE;
|
|
80
82
|
for (const provider of this._providers) {
|
|
81
83
|
if (typeof provider.boot === 'function') {
|
|
84
|
+
if (isCli && provider.constructor.cli === false) continue;
|
|
82
85
|
await provider.boot(this._container, this._app);
|
|
83
86
|
}
|
|
84
87
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
module.exports = `const {
|
|
1
|
+
module.exports = `const { Command } = require('millas/console');
|
|
2
2
|
|
|
3
|
-
class {{ name | pascalCase }}Command extends
|
|
4
|
-
static signature = '{{ name | kebabCase }}';
|
|
3
|
+
class {{ name | pascalCase }}Command extends Command {
|
|
5
4
|
static description = '{{ name | pascalCase }} command description';
|
|
6
5
|
|
|
7
|
-
async
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
async onInit(register) {
|
|
7
|
+
register
|
|
8
|
+
.command(async () => {
|
|
9
|
+
this.info('Running {{ name | pascalCase }}');
|
|
10
|
+
// Command logic here
|
|
11
|
+
})
|
|
12
|
+
.description('{{ name | pascalCase }} command description');
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
|
package/src/commands/call.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const chalk = require('chalk');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `millas call <signature> [args] [options]`
|
|
9
|
-
*
|
|
10
|
-
* Discovers all commands in app/commands/, then either:
|
|
11
|
-
* - Runs the matched command directly, OR
|
|
12
|
-
* - Lists all available commands when no signature is given
|
|
13
|
-
*
|
|
14
|
-
* Each command in app/commands/ must extend Command and have a static `signature`.
|
|
15
|
-
*
|
|
16
|
-
* ── Examples ─────────────────────────────────────────────────────────────────
|
|
17
|
-
*
|
|
18
|
-
* millas call — list all custom commands
|
|
19
|
-
* millas call email:digest — run with defaults
|
|
20
|
-
* millas call email:digest weekly — positional arg
|
|
21
|
-
* millas call email:digest --dry-run — flag
|
|
22
|
-
*/
|
|
23
|
-
module.exports = function (program) {
|
|
24
|
-
const commandsDir = path.resolve(process.cwd(), 'app/commands');
|
|
25
|
-
const CommandLoader = require('../console/CommandLoader');
|
|
26
|
-
|
|
27
|
-
// ── Bootstrap helpers ──────────────────────────────────────────────────────
|
|
28
|
-
async function bootstrapApp() {
|
|
29
|
-
const bootstrapPath = path.join(process.cwd(), 'bootstrap/app.js');
|
|
30
|
-
if (!fs.existsSync(bootstrapPath)) return;
|
|
31
|
-
|
|
32
|
-
// require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
|
|
33
|
-
|
|
34
|
-
const basePath = process.cwd();
|
|
35
|
-
const AppServiceProvider = require(path.join(basePath, 'providers/AppServiceProvider'));
|
|
36
|
-
const AppInitializer = require('../container/AppInitializer');
|
|
37
|
-
|
|
38
|
-
const config = {
|
|
39
|
-
basePath,
|
|
40
|
-
providers: [AppServiceProvider],
|
|
41
|
-
routes: null,
|
|
42
|
-
middleware: [],
|
|
43
|
-
logging: true,
|
|
44
|
-
database: true,
|
|
45
|
-
auth: true,
|
|
46
|
-
cache: true,
|
|
47
|
-
storage: true,
|
|
48
|
-
mail: true,
|
|
49
|
-
queue: true,
|
|
50
|
-
events: true,
|
|
51
|
-
admin: null,
|
|
52
|
-
docs: null,
|
|
53
|
-
adapterMiddleware: [],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const initializer = new AppInitializer(config);
|
|
57
|
-
await initializer.bootKernel();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function closeDb() {
|
|
61
|
-
try {
|
|
62
|
-
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
63
|
-
await DatabaseManager.closeAll();
|
|
64
|
-
} catch {}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ── millas list ────────────────────────────────────────────────────────────
|
|
68
|
-
program
|
|
69
|
-
.command('list')
|
|
70
|
-
.description('List all available custom commands')
|
|
71
|
-
.action(() => {
|
|
72
|
-
if (!fs.existsSync(commandsDir)) {
|
|
73
|
-
console.log(chalk.yellow('\n No app/commands/ directory found.\n'));
|
|
74
|
-
console.log(chalk.dim(' Run: millas make:command <Name> to create your first command.\n'));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const loader = new CommandLoader(commandsDir);
|
|
79
|
-
loader.load();
|
|
80
|
-
const sigs = loader.signatures();
|
|
81
|
-
|
|
82
|
-
if (sigs.length === 0) {
|
|
83
|
-
console.log(chalk.yellow('\n No custom commands found in app/commands/.\n'));
|
|
84
|
-
console.log(chalk.dim(' Run: millas make:command <Name> to create one.\n'));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const maxLen = Math.max(...sigs.map(s => s.length));
|
|
89
|
-
|
|
90
|
-
console.log(chalk.bold('\n Available commands:\n'));
|
|
91
|
-
for (const [sig, CommandClass] of loader._commands) {
|
|
92
|
-
const desc = CommandClass.description || '';
|
|
93
|
-
console.log(
|
|
94
|
-
' ' + chalk.cyan(sig.padEnd(maxLen + 2)) +
|
|
95
|
-
chalk.dim(desc)
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
console.log();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// ── millas call <signature> ────────────────────────────────────────────────
|
|
102
|
-
// Uses Commander's allowUnknownOption + passThroughOptions so we can
|
|
103
|
-
// forward all args/options to the matched command.
|
|
104
|
-
const callCmd = program
|
|
105
|
-
.command('call <signature> [args...]')
|
|
106
|
-
.description('Run a custom command from app/commands/')
|
|
107
|
-
.allowUnknownOption(true)
|
|
108
|
-
.passThroughOptions(true)
|
|
109
|
-
.action((signature, extraArgs, options, cmd) => {
|
|
110
|
-
if (!fs.existsSync(commandsDir)) {
|
|
111
|
-
console.error(chalk.red('\n ✖ No app/commands/ directory found.\n'));
|
|
112
|
-
console.error(chalk.dim(' Run: millas make:command <Name> to create your first command.\n'));
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const loader = new CommandLoader(commandsDir);
|
|
117
|
-
loader.load();
|
|
118
|
-
|
|
119
|
-
const CommandClass = loader._commands.get(signature);
|
|
120
|
-
|
|
121
|
-
if (!CommandClass) {
|
|
122
|
-
const sigs = loader.signatures();
|
|
123
|
-
console.error(chalk.red(`\n ✖ Unknown command: ${chalk.bold(signature)}\n`));
|
|
124
|
-
if (sigs.length) {
|
|
125
|
-
console.log(chalk.dim(' Available commands:'));
|
|
126
|
-
for (const s of sigs) console.log(chalk.dim(` ${s}`));
|
|
127
|
-
} else {
|
|
128
|
-
console.log(chalk.dim(' No custom commands found. Run: millas make:command <Name>'));
|
|
129
|
-
}
|
|
130
|
-
console.log();
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Re-parse args + options against the command's own definition
|
|
135
|
-
const { Command: CommanderCmd } = require('commander');
|
|
136
|
-
const sub = new CommanderCmd(signature);
|
|
137
|
-
|
|
138
|
-
const argsDef = CommandClass.args || [];
|
|
139
|
-
const optsDef = CommandClass.options || [];
|
|
140
|
-
|
|
141
|
-
for (const a of argsDef) {
|
|
142
|
-
sub.argument(
|
|
143
|
-
a.default !== undefined ? `[${a.name}]` : `<${a.name}>`,
|
|
144
|
-
a.description || '',
|
|
145
|
-
a.default
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
for (const o of optsDef) {
|
|
149
|
-
if (o.default !== undefined) {
|
|
150
|
-
sub.option(o.flag, o.description || '', o.default);
|
|
151
|
-
} else {
|
|
152
|
-
sub.option(o.flag, o.description || '');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Combine extraArgs back with the raw unknown options Commander captured
|
|
157
|
-
const rawArgs = [...(extraArgs || []), ...(cmd.args || [])];
|
|
158
|
-
|
|
159
|
-
// Re-deduplicate (Commander may put some into cmd.args already)
|
|
160
|
-
const allRaw = [...new Set([...extraArgs, ...rawArgs])];
|
|
161
|
-
|
|
162
|
-
sub.parse([process.execPath, signature, ...allRaw]);
|
|
163
|
-
|
|
164
|
-
const parsedOpts = sub.opts();
|
|
165
|
-
const parsedPosArgs = sub.args;
|
|
166
|
-
|
|
167
|
-
const argMap = {};
|
|
168
|
-
for (let i = 0; i < argsDef.length; i++) {
|
|
169
|
-
argMap[argsDef[i].name] = parsedPosArgs[i] !== undefined
|
|
170
|
-
? parsedPosArgs[i]
|
|
171
|
-
: argsDef[i].default;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const instance = new CommandClass();
|
|
175
|
-
instance._hydrate(argMap, parsedOpts);
|
|
176
|
-
|
|
177
|
-
// Prevent HTTP server from starting during CLI commands
|
|
178
|
-
process.env.MILLAS_CLI_MODE = '1';
|
|
179
|
-
|
|
180
|
-
Promise.resolve()
|
|
181
|
-
.then(() => bootstrapApp())
|
|
182
|
-
.then(() => instance.handle())
|
|
183
|
-
.catch(err => {
|
|
184
|
-
console.error(chalk.red(`\n ✖ ${signature} failed: ${err.message}\n`));
|
|
185
|
-
if (process.env.DEBUG) console.error(err.stack);
|
|
186
|
-
process.exit(1);
|
|
187
|
-
})
|
|
188
|
-
.finally(() => closeDb());
|
|
189
|
-
});
|
|
190
|
-
};
|