millas 0.2.30 → 0.2.32

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "millas",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/src/cli.js CHANGED
@@ -31,10 +31,28 @@ program.configureHelp({
31
31
  sortOptions: true,
32
32
  });
33
33
 
34
- // Handle --debug flag globally
35
- program.hook('preAction', (thisCommand) => {
34
+ // Handle --debug flag globally and boot app before any command runs
35
+ program.hook('preAction', async (thisCommand) => {
36
36
  if (thisCommand.opts().debug) {
37
- process.env.DEBUG = 'true';
37
+ process.env.APP_DEBUG = 'true';
38
+ }
39
+
40
+ if (context.isMillasProject()) {
41
+ const bootstrapPath = require('path').join(context.cwd, 'bootstrap/app.js');
42
+ if (require('fs').existsSync(bootstrapPath)) {
43
+ await require(bootstrapPath);
44
+ }
45
+ }
46
+ });
47
+
48
+ // Close DB connection pool after CLI command finishes
49
+ program.hook('postAction', async () => {
50
+ try {
51
+
52
+ const DatabaseManager = require('./orm/drivers/DatabaseManager');
53
+ await DatabaseManager.closeAll();
54
+ } catch(e) {
55
+ console.log(e)
38
56
  }
39
57
  });
40
58
 
@@ -56,6 +74,7 @@ async function bootstrap() {
56
74
 
57
75
  // Auto-discover user-defined commands (if inside a Millas project)
58
76
  if (context.isMillasProject()) {
77
+
59
78
  await registry.discoverUserCommands();
60
79
  }
61
80
  }
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const BaseCommand = require('../console/BaseCommand');
4
3
  const path = require('path');
5
4
  const fs = require('fs-extra');
6
- const readline = require('readline');
7
5
  const Hasher = require('../auth/Hasher');
6
+ const Command = require("../console/Command");
7
+ const DB = require("../facades/DB");
8
8
 
9
- class CreateSuperUserCommand extends BaseCommand {
9
+ class AdminCommand extends Command {
10
10
  static description = 'Manage superusers and admin accounts';
11
11
 
12
12
  async onInit(register) {
@@ -14,14 +14,15 @@ class CreateSuperUserCommand extends BaseCommand {
14
14
  .command(async (email, name, noinput) => {
15
15
  const { User } = await this.#resolveUserModel();
16
16
 
17
- this.logger.log(this.style.info('\n Create Millas Superuser\n'));
17
+ this.info('Create Millas Superuser');
18
+ this.newLine()
18
19
 
19
20
  // Email
20
- if (!email) email = await this.#prompt(' Email address: ');
21
- email = (email || '').trim().toLowerCase();
22
- if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
23
- throw new Error('Enter a valid email address.');
24
- }
21
+ if (!email) email = await this.ask('Email address:', null, v => {
22
+ const t = v.trim().toLowerCase();
23
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t) || 'Enter a valid email address.';
24
+ });
25
+ email = email.trim().toLowerCase();
25
26
 
26
27
  const existing = await User.findBy('email', email);
27
28
  if (existing) {
@@ -32,10 +33,8 @@ class CreateSuperUserCommand extends BaseCommand {
32
33
  );
33
34
  }
34
35
 
35
- // Name
36
- if (!name && !noinput) {
37
- name = await this.#prompt(' Display name (optional, press Enter to skip): ');
38
- }
36
+ if (!name && !noinput)
37
+ name = await this.ask('Display name (optional):', email.split('@')[0]);
39
38
  name = (name || '').trim() || email.split('@')[0];
40
39
 
41
40
  // Password
@@ -46,13 +45,9 @@ class CreateSuperUserCommand extends BaseCommand {
46
45
  throw new Error('--noinput requires ADMIN_PASSWORD to be set in the environment.');
47
46
  }
48
47
  } else {
49
- plainPassword = await this.#promptPassword(' Password: ');
50
- const confirm = await this.#promptPassword(' Password (again): ');
51
- if (plainPassword !== confirm) throw new Error('Passwords do not match.');
48
+ plainPassword = await this.#promptPasswordWithBypass('Password:');
52
49
  }
53
50
 
54
- this.#validatePassword(plainPassword);
55
-
56
51
  const hash = await Hasher.make(plainPassword);
57
52
 
58
53
  await User.create({
@@ -77,18 +72,18 @@ class CreateSuperUserCommand extends BaseCommand {
77
72
  .command(async (email) => {
78
73
  const { User } = await this.#resolveUserModel();
79
74
 
80
- if (!email) email = await this.#prompt('\n Email address: ');
81
- email = (email || '').trim().toLowerCase();
75
+ if (!email) email = await this.ask('Email address:', null, v =>
76
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) || 'Enter a valid email address.'
77
+ );
78
+ email = email.trim().toLowerCase();
82
79
 
83
80
  const user = await User.findBy('email', email);
84
81
  if (!user) throw new Error(`No user found with email "${email}".`);
85
82
 
86
- this.logger.log(this.style.info(`\n Changing password for: ${user.email}\n`));
83
+ this.info(`Changing password for: ${user.email}`);
84
+ this.newLine();
87
85
 
88
- const plain = await this.#promptPassword(' New password: ');
89
- const confirm = await this.#promptPassword(' New password (again): ');
90
- if (plain !== confirm) throw new Error('Passwords do not match.');
91
- this.#validatePassword(plain);
86
+ const plain = await this.#promptPasswordWithBypass('New password:');
92
87
 
93
88
  const hash = await Hasher.make(plain);
94
89
 
@@ -101,7 +96,7 @@ class CreateSuperUserCommand extends BaseCommand {
101
96
  })
102
97
  .name('changepassword')
103
98
  .str('[email]', v => v.email().optional(), 'Email address of the user')
104
- .description("Change a user's password in the users table");
99
+ .description("Change a user's password in the users table")
105
100
 
106
101
  register
107
102
  .command(async () => {
@@ -131,21 +126,27 @@ class CreateSuperUserCommand extends BaseCommand {
131
126
  .description('List all staff/superusers from the users table');
132
127
  }
133
128
 
134
- async #resolveUserModel() {
135
- const configPath = path.join(this.cwd, 'config/database.js');
136
- if (!fs.existsSync(configPath)) {
137
- throw new Error('config/database.js not found. Are you inside a Millas project?');
129
+ async after(...args) {
130
+ await DB.closeAll()
138
131
  }
139
132
 
140
- const dbConfig = require(configPath);
141
- let DatabaseManager;
142
- try {
143
- DatabaseManager = require(path.join(this.cwd, 'node_modules/millas/src/orm/drivers/DatabaseManager'));
144
- } catch {
145
- DatabaseManager = require('../orm/drivers/DatabaseManager');
133
+ async #promptPasswordWithBypass(question) {
134
+ while (true) {
135
+ const pw = await this.secret(question, {
136
+ confirm: { message: `${question.replace(':', '')} (again):`, error: 'Passwords do not match.', retry: true },
137
+ });
138
+
139
+ if (pw.length >= 8) return pw;
140
+
141
+ this.warn('This password is too short. It must contain at least 8 characters.');
142
+ const bypass = await this.confirm('Bypass password validation and create user anyway?', false);
143
+ if (bypass) return pw;
146
144
  }
147
- DatabaseManager.configure(dbConfig);
148
- const db = DatabaseManager.connection();
145
+ }
146
+
147
+ async #resolveUserModel() {
148
+
149
+ const db = DB.connection()
149
150
 
150
151
  let User;
151
152
  let authUserName = null;
@@ -192,30 +193,6 @@ class CreateSuperUserCommand extends BaseCommand {
192
193
  return { User, db };
193
194
  }
194
195
 
195
- #validatePassword(pw) {
196
- if (!pw || pw.length < 8) {
197
- throw new Error('This password is too short. It must contain at least 8 characters.');
198
- }
199
- }
200
-
201
- #prompt(question) {
202
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
203
- return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans); }));
204
- }
205
-
206
- #promptPassword(question) {
207
- return new Promise(resolve => {
208
- if (!process.stdin.isTTY) return this.#prompt(question).then(resolve);
209
-
210
- process.stdout.write(question);
211
- const rl = readline.createInterface({
212
- input: process.stdin,
213
- output: new (require('stream').Writable)({ write(c, e, cb) { cb(); } }),
214
- terminal: true,
215
- });
216
- rl.question('', ans => { rl.close(); process.stdout.write('\n'); resolve(ans); });
217
- });
218
- }
219
196
  }
220
197
 
221
- module.exports = CreateSuperUserCommand;
198
+ module.exports = AdminCommand;
@@ -1,18 +1,18 @@
1
1
  'use strict';
2
2
 
3
- const BaseCommand = require('../console/BaseCommand');
3
+ const Command = require('../console/Command');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
- class KeyCommand extends BaseCommand {
7
+ class KeyCommand extends Command {
8
8
  static description = 'Manage application encryption keys';
9
9
 
10
10
  async onInit(register) {
11
11
  register
12
12
  .command(this.generate)
13
- .arg('--show', 'Print the key without writing to .env')
14
- .arg('--force', 'Overwrite existing APP_KEY without confirmation')
15
- .arg('--cipher', v=>v.string(),'Cipher to use (default: AES-256-CBC)')
13
+ .bool('show', 'Print the key without writing to .env')
14
+ .bool('force', 'Overwrite existing APP_KEY without confirmation')
15
+ .str('--cipher', v => v.optional(), 'Cipher to use (default: AES-256-CBC)')
16
16
  .description('Generate a new application key and write it to .env');
17
17
  }
18
18
 
@@ -27,7 +27,7 @@ class KeyCommand extends BaseCommand {
27
27
  }
28
28
 
29
29
  if (show) {
30
- this.logger.log('\n ' + this.style.info(key) + '\n');
30
+ this.info(key);
31
31
  return;
32
32
  }
33
33
 
@@ -35,7 +35,7 @@ class KeyCommand extends BaseCommand {
35
35
 
36
36
  if (!fs.existsSync(envPath)) {
37
37
  this.error('.env file not found.');
38
- this.logger.error(this.style.muted(' Run: millas new <project> or create a .env file first.\n\n'));
38
+ this.error(this.style.muted('Run: millas new <project> or create a .env file first.\n\n'));
39
39
  throw new Error('.env file not found');
40
40
  }
41
41
 
@@ -45,16 +45,9 @@ class KeyCommand extends BaseCommand {
45
45
  const hasValue = existing && existing[1] && existing[1].trim() !== '';
46
46
 
47
47
  if (hasValue && !force) {
48
- const readline = require('readline');
49
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
50
- const answer = await new Promise(resolve => {
51
- rl.question(
52
- this.style.warning('\n ⚠ APP_KEY already set. Overwrite? (y/N) '),
53
- ans => { rl.close(); resolve(ans); }
54
- );
55
- });
56
- if ((answer || '').trim().toLowerCase() !== 'y') {
57
- this.logger.log(this.style.muted('\n Key not changed.\n'));
48
+ const ok = await this.confirm('APP_KEY already set. Overwrite?', false);
49
+ if (!ok) {
50
+ this.comment('Key not changed.');
58
51
  return;
59
52
  }
60
53
  }
@@ -68,7 +61,7 @@ class KeyCommand extends BaseCommand {
68
61
  fs.writeFileSync(envPath, envContent, 'utf8');
69
62
 
70
63
  this.success('Application key set.');
71
- this.logger.log(' ' + this.style.muted('APP_KEY=') + this.style.info(key) + '\n');
64
+ this.comment(this.style.muted('APP_KEY=') + this.style.info(key));
72
65
  }
73
66
  }
74
67
 
@@ -1,32 +1,31 @@
1
1
  'use strict';
2
2
 
3
- const BaseCommand = require('../console/BaseCommand');
3
+ const Command = require('../console/Command');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
- const {string} = require("../core/validation");
7
6
 
8
7
  const DEFAULT_NS = 'messages';
9
8
 
10
- class LangCommand extends BaseCommand {
9
+ class LangCommand extends Command {
11
10
  static description = 'Manage application translations';
12
11
 
13
12
  async onInit(register) {
14
13
  register
15
14
  .command(this.publish)
16
- .arg('locale', 'Target locale (e.g., sw, fr)')
17
- .arg('namespace',v=>v.string(), 'Specific namespace to publish')
18
- .arg('--defaults', 'Include built-in Millas framework strings')
19
- .arg('--fresh', 'Clear namespace files and rebuild from scratch')
20
- .arg('--all', 'Publish to every locale in lang/')
21
- .arg('--list', 'List available locales and exit')
22
- .arg('--dry-run', 'Preview changes without writing')
23
- .arg('--format',v=>v.string(), 'File format: js or json (default: js)')
24
- .arg('--src',v=>v.string(), 'Extra directory to scan')
15
+ .str('[locale]', 'Target locale (e.g., sw, fr)')
16
+ .str('[namespace]', 'Specific namespace to publish')
17
+ .bool('defaults', 'Include built-in Millas framework strings')
18
+ .bool('fresh', 'Clear namespace files and rebuild from scratch')
19
+ .bool('all', 'Publish to every locale in lang/')
20
+ .bool('list', 'List available locales and exit')
21
+ .bool('dry-run', 'Preview changes without writing')
22
+ .str('[--format]', 'File format: js or json (default: js)')
23
+ .str('[--src]', 'Extra directory to scan')
25
24
  .description('Extract _() strings from app/ and write to lang/<locale>/<namespace>.js');
26
25
 
27
26
  register
28
27
  .command(this.missing)
29
- .arg('locale', 'Target locale to check')
28
+ .str('[locale]', 'Target locale to check')
30
29
  .description('Show untranslated keys in locale files');
31
30
 
32
31
  register
@@ -35,7 +34,8 @@ class LangCommand extends BaseCommand {
35
34
 
36
35
  register
37
36
  .command(this.keys)
38
- .arg('--src',v=>v.string(), 'Extra directory to scan')
37
+ .str('[--src]', 'Extra directory to scan')
38
+ .bool('ns', 'Group by namespace')
39
39
  .description('List all _() keys found in source files, grouped by namespace');
40
40
  }
41
41
 
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
- const BaseCommand = require('../console/BaseCommand');
4
+ const Command = require('../console/Command');
5
5
  const SchematicEngine = require('../schematics/SchematicEngine');
6
6
 
7
- class MakeCommand extends BaseCommand {
7
+ class MakeCommand extends Command {
8
8
  static description = 'Generate application scaffolding';
9
9
 
10
10
  #engine = new SchematicEngine(path.join(__dirname, '../templates'));
@@ -16,9 +16,9 @@ class MakeCommand extends BaseCommand {
16
16
  this.success(`Created: ${result.path}`);
17
17
  })
18
18
  .name('controller')
19
- .arg('name')
20
- .arg('--resource')
21
- .arg('--model', v => v.string())
19
+ .str('name')
20
+ .str('--resource')
21
+ .str('--model')
22
22
  .description('Generate a new controller');
23
23
 
24
24
  register
@@ -2,10 +2,10 @@
2
2
 
3
3
  const path = require('path');
4
4
  const fs = require('fs-extra');
5
- const BaseCommand = require('../console/BaseCommand');
5
+ const Command = require('../console/Command');
6
6
  const DB = require('../facades/DB');
7
7
 
8
- class MigrateCommand extends BaseCommand {
8
+ class MigrateCommand extends Command {
9
9
  static description = 'Database migration commands';
10
10
  static namespace = 'db';
11
11
 
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const BaseCommand = require('../console/BaseCommand');
3
+ const Command = require('../console/Command');
4
4
  const fs = require('fs-extra');
5
5
  const path = require('path');
6
6
  const ora = require('ora');
7
7
  const { generateProject } = require('../scaffold/generator');
8
8
 
9
- class NewCommand extends BaseCommand {
9
+ class NewCommand extends Command {
10
10
  static description = 'Create a new Millas project';
11
11
 
12
12
  async onInit(register) {
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const BaseCommand = require('../console/BaseCommand');
3
+ const Command = require('../console/Command');
4
4
 
5
- class RouteCommand extends BaseCommand {
5
+ class RouteCommand extends Command {
6
6
  static description = 'Manage application routes';
7
7
 
8
8
  async onInit(register) {
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const BaseCommand = require('../console/BaseCommand');
3
+ const Command = require('../console/Command');
4
4
 
5
- class ScheduleCommand extends BaseCommand {
5
+ class ScheduleCommand extends Command {
6
6
  static description = 'Manage scheduled tasks';
7
7
 
8
8
  async onInit(register) {
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const fs = require('fs-extra');
5
5
  const { fork } = require('child_process');
6
6
  const chokidar = require('chokidar');
7
- const BaseCommand = require('../console/BaseCommand');
7
+ const Command = require('../console/Command');
8
8
  const patchConsole = require('../logger/patchConsole');
9
9
  const Logger = require('../logger/internal');
10
10
 
@@ -136,7 +136,7 @@ class HotReloader {
136
136
  }
137
137
  }
138
138
 
139
- class ServeCommand extends BaseCommand {
139
+ class ServeCommand extends Command {
140
140
  static description = 'Start the development server with hot reload';
141
141
 
142
142
  async onInit(register) {
@@ -47,8 +47,13 @@ const v = require("../core/validation");
47
47
  * Base class for all CLI commands
48
48
  */
49
49
  class BaseCommand extends AppCommand {
50
- static command = '';
51
- static namespace = ''; // Override to set custom namespace
50
+ static namespace = ''; // Override to set a fixed command name
51
+
52
+ /**
53
+ * Short description shown in `millas --help` and `millas list`.
54
+ *
55
+ * @type {string}
56
+ */
52
57
  static description = '';
53
58
  static aliases = [];
54
59
  static options = [];
@@ -68,20 +73,15 @@ class BaseCommand extends AppCommand {
68
73
  }
69
74
 
70
75
  /**
71
- * Auto-derive command name from class name
72
- * Supports CamelCase, underscores, and hyphens
76
+ * Derive command name from filename (set by CommandRegistry).
77
+ * Override with `static namespace = 'name'` to use a fixed name.
73
78
  */
74
79
  static getCommandName() {
75
- // Use custom namespace if provided
76
80
  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
81
+
82
+ if (!this._filenameHint) throw new Error(`Cannot derive command name for ${this.name} — no filename hint set.`);
83
+
84
+ return this._filenameHint
85
85
  .replace(/[-_]/g, ':')
86
86
  .replace(/([A-Z]+)([A-Z][a-z])/g, '$1:$2')
87
87
  .replace(/([a-z\d])([A-Z])/g, '$1:$2')
@@ -104,6 +104,8 @@ class BaseCommand extends AppCommand {
104
104
  * Calls onInit() for subcommands or registerSimpleCommand() for simple commands
105
105
  */
106
106
  async register() {
107
+ // Expose filename to static getCommandName()
108
+ if (this._filename) this.constructor._filenameHint = this._filename;
107
109
  const commandName = this.constructor.getCommandName();
108
110
  const subcommands = this.getSubcommands();
109
111
 
@@ -230,22 +232,68 @@ class BaseCommand extends AppCommand {
230
232
 
231
233
  process.exitCode = 1;
232
234
  }
233
-
235
+ /**
236
+ * Success message (green ✔).
237
+ * @param {string} message
238
+ */
234
239
  success(message) {
235
240
  this.logger.log(this.style.success(`\n ✔ ${message}\n`));
236
241
  }
237
-
242
+ /**
243
+ * Informational message (cyan).
244
+ * @param {string} message
245
+ */
238
246
  info(message) {
239
247
  this.logger.log(this.style.info(` ${message}`));
240
248
  }
241
-
249
+ /**
250
+ * Warning message (yellow ⚠).
251
+ * @param {string} message
252
+ */
242
253
  warn(message) {
243
254
  this.logger.log(this.style.warning(` ⚠ ${message}`));
244
255
  }
245
-
256
+ /**
257
+ * Error message (red ✖). Does NOT exit — use fail() to exit.
258
+ * @param {string} message
259
+ */
246
260
  error(message) {
247
261
  this.logger.error(this.style.danger(` ✖ ${message}`));
248
262
  }
263
+
264
+ /**
265
+ * Write a plain line to stdout.
266
+ * @param {string} [msg='']
267
+ */
268
+ line(msg = '') {
269
+ process.stdout.write(msg + '\n');
270
+ }
271
+
272
+ /**
273
+ * Write a blank line.
274
+ */
275
+ newLine() {
276
+ process.stdout.write('\n');
277
+ }
278
+
279
+
280
+
281
+
282
+ /**
283
+ * Dimmed / comment message.
284
+ * @param {string} msg
285
+ */
286
+ comment(msg) {
287
+ this.line(chalk.dim(`// ${msg}`));
288
+ }
289
+ /**
290
+ * Print an error and exit with code 1.
291
+ * @param {string} msg
292
+ */
293
+ fail(msg) {
294
+ this.error(msg);
295
+ process.exit(1);
296
+ }
249
297
  }
250
298
 
251
299
  /**
@@ -330,9 +378,13 @@ class CommandRegistrar {
330
378
  }
331
379
 
332
380
  const isFlag = name.startsWith('--') || name.startsWith('-');
333
- const cleanName = name.replace(/^--?/, '').replace(/^\[|\]$/g, '');
334
- let validator = null;
381
+ const isOptional = /^\[.*\]$/.test(name);
382
+ const cleanName = name
383
+ .replace(/^--?/, '')
384
+ .replace(/^\[|]$/g, '');let validator = null;
335
385
  let desc = description;
386
+
387
+
336
388
 
337
389
  if (typeof validatorOrDescription === 'string') {
338
390
  desc = validatorOrDescription;
@@ -346,6 +398,9 @@ class CommandRegistrar {
346
398
  desc = cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
347
399
  }
348
400
  }
401
+ if (isOptional && validator && typeof validator.optional === 'function') {
402
+ validator = validator.optional();
403
+ }
349
404
 
350
405
  this.lastCommand.args.push({
351
406
  name: cleanName,