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.
@@ -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
- * Named options / flags.
104
- * Each entry: { flag: string, description?: string, default?: * }
105
- * flag examples: '--dry-run', '--limit <n>', '-f, --force'
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} question
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
- return _prompt(question + ' ');
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
- return _promptSecret(question + ' ');
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 BaseCommand = require('./BaseCommand');
49
-
50
- // Support both class-based and function-based commands
51
- if (typeof CommandClass === 'function') {
52
- // Check if it's a class extending BaseCommand
53
- if (CommandClass.prototype instanceof BaseCommand) {
54
- const commandInstance = new CommandClass(this.context);
55
- await commandInstance.register(); // Now async
56
- this.commands.set(commandPath, commandInstance);
57
- }
58
- // Legacy function-based command (backward compatibility)
59
- // else if (CommandClass.length === 1) { // expects (program) argument
60
- // CommandClass(this.context.program);
61
- // this.commands.set(commandPath, CommandClass);
62
- // }
63
- // Invalid command export
64
- // else {
65
- // throw new Error(`Invalid command export in ${commandPath}. Must extend BaseCommand or export function(program).`);
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
  /**
@@ -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
  }
@@ -69,7 +69,7 @@ class TaskScheduler {
69
69
  * Start the scheduler
70
70
  */
71
71
  start() {
72
- if (!this._config.enabled || this._running) return;
72
+ if (!this._config.enabled || this._running || process.env.MILLAS_CLI_MODE) return;
73
73
 
74
74
  this._running = true;
75
75
 
@@ -1,12 +1,15 @@
1
- module.exports = `const { BaseCommand } = require('millas/src/console');
1
+ module.exports = `const { Command } = require('millas/console');
2
2
 
3
- class {{ name | pascalCase }}Command extends BaseCommand {
4
- static signature = '{{ name | kebabCase }}';
3
+ class {{ name | pascalCase }}Command extends Command {
5
4
  static description = '{{ name | pascalCase }} command description';
6
5
 
7
- async run(args, opts) {
8
- this.info('Running {{ name | pascalCase }}Command');
9
- // Command logic here
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
 
@@ -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
- };