millas 0.2.28 → 0.2.30

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.
Files changed (47) hide show
  1. package/bin/millas.js +12 -2
  2. package/package.json +2 -1
  3. package/src/cli.js +117 -20
  4. package/src/commands/call.js +1 -1
  5. package/src/commands/createsuperuser.js +137 -182
  6. package/src/commands/key.js +61 -83
  7. package/src/commands/lang.js +423 -515
  8. package/src/commands/make.js +88 -62
  9. package/src/commands/migrate.js +200 -279
  10. package/src/commands/new.js +55 -50
  11. package/src/commands/route.js +78 -80
  12. package/src/commands/schedule.js +52 -150
  13. package/src/commands/serve.js +158 -191
  14. package/src/console/AppCommand.js +106 -0
  15. package/src/console/BaseCommand.js +726 -0
  16. package/src/console/CommandContext.js +66 -0
  17. package/src/console/CommandRegistry.js +88 -0
  18. package/src/console/Style.js +123 -0
  19. package/src/console/index.js +12 -3
  20. package/src/container/AppInitializer.js +10 -0
  21. package/src/facades/DB.js +195 -0
  22. package/src/index.js +2 -1
  23. package/src/scaffold/maker.js +102 -42
  24. package/src/schematics/Collection.js +28 -0
  25. package/src/schematics/SchematicEngine.js +122 -0
  26. package/src/schematics/Template.js +99 -0
  27. package/src/schematics/index.js +7 -0
  28. package/src/templates/command/default.template.js +14 -0
  29. package/src/templates/command/schema.json +19 -0
  30. package/src/templates/controller/default.template.js +10 -0
  31. package/src/templates/controller/resource.template.js +59 -0
  32. package/src/templates/controller/schema.json +30 -0
  33. package/src/templates/job/default.template.js +11 -0
  34. package/src/templates/job/schema.json +19 -0
  35. package/src/templates/middleware/default.template.js +11 -0
  36. package/src/templates/middleware/schema.json +19 -0
  37. package/src/templates/migration/default.template.js +14 -0
  38. package/src/templates/migration/schema.json +19 -0
  39. package/src/templates/model/default.template.js +14 -0
  40. package/src/templates/model/migration.template.js +17 -0
  41. package/src/templates/model/schema.json +30 -0
  42. package/src/templates/service/default.template.js +12 -0
  43. package/src/templates/service/schema.json +19 -0
  44. package/src/templates/shape/default.template.js +11 -0
  45. package/src/templates/shape/schema.json +19 -0
  46. package/src/validation/BaseValidator.js +3 -0
  47. package/src/validation/types.js +3 -3
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Command Context
5
+ * Encapsulates all dependencies and configuration for CLI commands
6
+ */
7
+ class CommandContext {
8
+ constructor(options = {}) {
9
+ this.program = options.program;
10
+ this.container = options.container || null;
11
+ this.config = options.config || {};
12
+ this.logger = options.logger || console;
13
+ this.cwd = options.cwd || process.cwd();
14
+ }
15
+
16
+ /**
17
+ * Set the DI container (lazy loaded after app bootstrap)
18
+ */
19
+ setContainer(container) {
20
+ this.container = container;
21
+ return this;
22
+ }
23
+
24
+ /**
25
+ * Set configuration
26
+ */
27
+ setConfig(config) {
28
+ this.config = config;
29
+ return this;
30
+ }
31
+
32
+ /**
33
+ * Get a service from the container
34
+ */
35
+ resolve(serviceName) {
36
+ if (!this.container) {
37
+ throw new Error('Container not initialized. Cannot resolve services.');
38
+ }
39
+ return this.container.resolve(serviceName);
40
+ }
41
+
42
+ /**
43
+ * Check if running inside a Millas project
44
+ */
45
+ isMillasProject() {
46
+ const fs = require('fs');
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
+ }
63
+ }
64
+ }
65
+
66
+ module.exports = CommandContext;
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ /**
8
+ * Command Registry
9
+ * Auto-discovers and registers all commands
10
+ */
11
+ class CommandRegistry {
12
+ constructor(context) {
13
+ this.context = context;
14
+ this.commands = new Map();
15
+ }
16
+
17
+ /**
18
+ * Discover and load all commands from a directory
19
+ */
20
+ async discoverCommands(commandsDir) {
21
+ if (!require('fs').existsSync(commandsDir)) {
22
+ return;
23
+ }
24
+
25
+ const fs = require('fs').promises;
26
+ const files = (await fs.readdir(commandsDir))
27
+ .filter(file => file.endsWith('.js') && file !== 'index.js');
28
+
29
+ for (const file of files) {
30
+ const commandPath = path.join(commandsDir, file);
31
+ try {
32
+ await this.loadCommand(commandPath);
33
+ } catch (err) {
34
+ console.log(err)
35
+ console.error(chalk.yellow(` ⚠ Failed to load command: ${file}`));
36
+ if (process.env.APP_DEBUG) {
37
+ console.error(err);
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Load a single command file
45
+ */
46
+ async loadCommand(commandPath) {
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
+ // }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Register commands from app/commands/ (user-defined commands)
72
+ */
73
+ async discoverUserCommands() {
74
+ const userCommandsDir = path.join(this.context.cwd, 'app', 'commands');
75
+ if (require('fs').existsSync(userCommandsDir)) {
76
+ await this.discoverCommands(userCommandsDir);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get all registered commands
82
+ */
83
+ getCommands() {
84
+ return Array.from(this.commands.values());
85
+ }
86
+ }
87
+
88
+ module.exports = CommandRegistry;
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * Bootstrap-inspired styling system for CLI output
7
+ * Provides consistent theming across all commands
8
+ */
9
+ class Style {
10
+ // Bootstrap-style variants
11
+ success(text) {
12
+ return chalk.green(text);
13
+ }
14
+
15
+ danger(text) {
16
+ return chalk.red(text);
17
+ }
18
+
19
+ warning(text) {
20
+ return chalk.yellow(text);
21
+ }
22
+
23
+ info(text) {
24
+ return chalk.cyan(text);
25
+ }
26
+
27
+ primary(text) {
28
+ return chalk.blue(text);
29
+ }
30
+
31
+ secondary(text) {
32
+ return chalk.gray(text);
33
+ }
34
+
35
+ muted(text) {
36
+ return chalk.dim(text);
37
+ }
38
+
39
+ light(text) {
40
+ return chalk.white(text);
41
+ }
42
+
43
+ dark(text) {
44
+ return chalk.black(text);
45
+ }
46
+
47
+ // Text styles
48
+ bold(text) {
49
+ return chalk.bold(text);
50
+ }
51
+
52
+ italic(text) {
53
+ return chalk.italic(text);
54
+ }
55
+
56
+ underline(text) {
57
+ return chalk.underline(text);
58
+ }
59
+
60
+ // HTTP method colors
61
+ method(verb) {
62
+ const colors = {
63
+ GET: chalk.green,
64
+ POST: chalk.blue,
65
+ PUT: chalk.yellow,
66
+ PATCH: chalk.magenta,
67
+ DELETE: chalk.red,
68
+ };
69
+ return colors[verb] || chalk.white;
70
+ }
71
+
72
+ // Status indicators
73
+ checkmark(text = '') {
74
+ return chalk.green(`✔ ${text}`);
75
+ }
76
+
77
+ cross(text = '') {
78
+ return chalk.red(`✖ ${text}`);
79
+ }
80
+
81
+ bullet(text = '') {
82
+ return chalk.cyan(`• ${text}`);
83
+ }
84
+
85
+ arrow(text = '') {
86
+ return chalk.cyan(`→ ${text}`);
87
+ }
88
+
89
+ // Borders and separators
90
+ line(length = 80, char = '─') {
91
+ return chalk.gray(char.repeat(length));
92
+ }
93
+
94
+ // Badges
95
+ badge(text, variant = 'primary') {
96
+ const styles = {
97
+ success: chalk.bgGreen.black,
98
+ danger: chalk.bgRed.white,
99
+ warning: chalk.bgYellow.black,
100
+ info: chalk.bgCyan.black,
101
+ primary: chalk.bgBlue.white,
102
+ secondary: chalk.bgGray.white,
103
+ };
104
+ const style = styles[variant] || styles.primary;
105
+ return style(` ${text} `);
106
+ }
107
+
108
+ // Code/path highlighting
109
+ code(text) {
110
+ return chalk.cyan(text);
111
+ }
112
+
113
+ path(text) {
114
+ return chalk.cyan(text);
115
+ }
116
+
117
+ // Key-value pairs
118
+ kv(key, value) {
119
+ return `${chalk.dim(key)}: ${chalk.white(value)}`;
120
+ }
121
+ }
122
+
123
+ module.exports = Style;
@@ -1,6 +1,15 @@
1
1
  'use strict';
2
2
 
3
- const Command = require('./Command');
4
- const CommandLoader = require('./CommandLoader');
3
+ const Command = require('./Command');
4
+ const CommandLoader = require('./CommandLoader');
5
+ const BaseCommand = require('./BaseCommand');
6
+ const CommandContext = require('./CommandContext');
7
+ const CommandRegistry = require('./CommandRegistry');
5
8
 
6
- module.exports = { Command, CommandLoader };
9
+ module.exports = {
10
+ Command,
11
+ CommandLoader,
12
+ BaseCommand,
13
+ CommandContext,
14
+ CommandRegistry
15
+ };
@@ -72,6 +72,16 @@ class AppInitializer {
72
72
  * @returns {Application} the booted kernel
73
73
  */
74
74
  async bootKernel() {
75
+ // Load .env if not running via CLI (CLI loads it in src/cli.js)
76
+ if (!process.env.MILLAS_CLI_MODE) {
77
+ const path = require('path');
78
+ const fs = require('fs');
79
+ const envPath = path.resolve(process.cwd(), '.env');
80
+ if (fs.existsSync(envPath)) {
81
+ require('dotenv').config({ path: envPath, override: false });
82
+ }
83
+ }
84
+
75
85
  const cfg = this._config;
76
86
  const basePath = cfg.basePath || process.cwd();
77
87
 
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ /**
7
+ * DB Facade - Django-style database access
8
+ *
9
+ * Auto-configures on first use by loading config/database.js
10
+ *
11
+ * Usage:
12
+ * const DB = require('millas/src/facades/DB');
13
+ *
14
+ * // Query builder
15
+ * const users = await DB.table('users').where('active', true).get();
16
+ *
17
+ * // Raw queries
18
+ * const result = await DB.select('SELECT * FROM users WHERE id = ?', [1]);
19
+ *
20
+ * // Transactions
21
+ * await DB.transaction(async (trx) => {
22
+ * await trx('users').insert({ name: 'John' });
23
+ * await trx('posts').insert({ title: 'Hello' });
24
+ * });
25
+ *
26
+ * // Direct connection
27
+ * const db = DB.connection();
28
+ * await db('users').select('*');
29
+ */
30
+ class DBFacade {
31
+ constructor() {
32
+ this._manager = null;
33
+ this._configured = false;
34
+ }
35
+
36
+ /**
37
+ * Get the DatabaseManager instance (auto-configures if needed)
38
+ */
39
+ _getManager() {
40
+ if (!this._configured) {
41
+ this._configure();
42
+ }
43
+ return this._manager;
44
+ }
45
+
46
+ /**
47
+ * Auto-configure by loading config/database.js
48
+ */
49
+ _configure() {
50
+ if (this._configured) return;
51
+
52
+ // Try to find config/database.js from current working directory
53
+ const configPath = path.resolve(process.cwd(), 'config/database.js');
54
+
55
+ if (!fs.existsSync(configPath)) {
56
+ throw new Error(
57
+ 'config/database.js not found. Make sure you are in a Millas project directory.'
58
+ );
59
+ }
60
+
61
+ const config = require(configPath);
62
+ this._manager = require('../orm/drivers/DatabaseManager');
63
+ this._manager.configure(config);
64
+ this._configured = true;
65
+ }
66
+
67
+ /**
68
+ * Get a database connection
69
+ * @param {string} name - Connection name (optional, uses default if not provided)
70
+ */
71
+ connection(name) {
72
+ return this._getManager().connection(name);
73
+ }
74
+
75
+ /**
76
+ * Get the default connection
77
+ */
78
+ get db() {
79
+ return this._getManager().db;
80
+ }
81
+
82
+ /**
83
+ * Query builder for a table (Laravel: DB::table('users'))
84
+ * @param {string} tableName
85
+ */
86
+ table(tableName) {
87
+ return this._getManager().table(tableName);
88
+ }
89
+
90
+ /**
91
+ * Execute raw SQL SELECT
92
+ * @param {string} sql
93
+ * @param {Array} bindings
94
+ */
95
+ async select(sql, bindings = []) {
96
+ return this._getManager().select(sql, bindings);
97
+ }
98
+
99
+ /**
100
+ * Execute INSERT
101
+ * @param {string} sql
102
+ * @param {Array} bindings
103
+ */
104
+ async insert(sql, bindings = []) {
105
+ return this._getManager().insert(sql, bindings);
106
+ }
107
+
108
+ /**
109
+ * Execute UPDATE
110
+ * @param {string} sql
111
+ * @param {Array} bindings
112
+ */
113
+ async update(sql, bindings = []) {
114
+ return this._getManager().update(sql, bindings);
115
+ }
116
+
117
+ /**
118
+ * Execute DELETE
119
+ * @param {string} sql
120
+ * @param {Array} bindings
121
+ */
122
+ async delete(sql, bindings = []) {
123
+ return this._getManager().delete(sql, bindings);
124
+ }
125
+
126
+ /**
127
+ * Execute raw SQL
128
+ * @param {string} sql
129
+ * @param {Array} bindings
130
+ */
131
+ async raw(sql, bindings = []) {
132
+ return this._getManager().raw(sql, bindings);
133
+ }
134
+
135
+ /**
136
+ * Run queries in a transaction
137
+ * @param {Function} callback
138
+ */
139
+ async transaction(callback) {
140
+ return this._getManager().transaction(callback);
141
+ }
142
+
143
+ /**
144
+ * Begin a transaction manually
145
+ */
146
+ async beginTransaction() {
147
+ return this._getManager().beginTransaction();
148
+ }
149
+
150
+ /**
151
+ * Execute a statement
152
+ * @param {string} sql
153
+ * @param {Array} bindings
154
+ */
155
+ async statement(sql, bindings = []) {
156
+ return this._getManager().statement(sql, bindings);
157
+ }
158
+
159
+ /**
160
+ * Execute unprepared statement
161
+ * @param {string} sql
162
+ */
163
+ async unprepared(sql) {
164
+ return this._getManager().unprepared(sql);
165
+ }
166
+
167
+ /**
168
+ * Get schema builder
169
+ */
170
+ get schema() {
171
+ return this._getManager().schema;
172
+ }
173
+
174
+ /**
175
+ * Close all database connections
176
+ */
177
+ async closeAll() {
178
+ if (this._manager) {
179
+ await this._manager.closeAll();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Close a specific connection
185
+ * @param {string} name
186
+ */
187
+ async close(name) {
188
+ if (this._manager) {
189
+ await this._manager.close(name);
190
+ }
191
+ }
192
+ }
193
+
194
+ // Export singleton instance
195
+ module.exports = new DBFacade();
package/src/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const Millas = require('./container/MillasApp');
4
+ const DB = require('./facades/DB');
4
5
 
5
6
  /**
6
7
  * @module millas
7
8
  */
8
- module.exports = { Millas };
9
+ module.exports = { Millas, DB };
@@ -325,74 +325,134 @@ async function makeShape(name) {
325
325
  return write(filePath, lines.join('\n'));
326
326
  }
327
327
  async function makeCommand(name) {
328
- // If name contains ':' it's a namespaced signature like 'email:SendDigest'.
329
- // Split off the namespace prefix and use only the last segment for the
330
- // class name / filename, but keep the full input as the signature.
331
- const parts = name.split(':');
332
- const basePart = parts[parts.length - 1]; // e.g. 'SendDigest'
333
- const cleanBase = basePart.replace(/Command$/i, ''); // strip trailing Command
334
- const className = pascalCase(cleanBase) + 'Command'; // e.g. 'SendDigestCommand'
335
-
336
- // Build signature from the full name:
337
- // 'email:SendDigest' → 'email:digest'
338
- // 'SendDigest' → 'send-digest'
339
- // 'send-digest' → 'send-digest'
340
- const signatureParts = name
328
+ // Convert command signature to class name
329
+ // Examples:
330
+ // 'user' → UserCommand (subcommand group)
331
+ // 'send:newsletter' → SendNewsletterCommand (simple)
332
+ // 'email' → EmailCommand (subcommand group)
333
+
334
+ const isGroup = !name.includes(':');
335
+
336
+ const parts = name
341
337
  .replace(/Command$/i, '')
342
338
  .split(':')
343
- .map((seg, i, arr) => {
344
- // Last segment: strip camelCase — 'SendDigest' → 'send-digest'
345
- if (i === arr.length - 1) {
346
- return seg
347
- .replace(/([a-z])([A-Z])/g, '$1-$2')
348
- .toLowerCase();
349
- }
350
- // Namespace segments: lowercase as-is
351
- return seg.toLowerCase();
352
- });
353
- const signature = signatureParts.join(':');
339
+ .map(part => pascalCase(part));
340
+
341
+ const className = parts.join('') + 'Command';
342
+ const signature = name.toLowerCase();
354
343
 
355
344
  const filePath = resolveAppPath('app/commands', `${className}.js`);
356
345
 
357
- const content = `'use strict';
346
+ let content;
347
+
348
+ if (isGroup) {
349
+ // Generate subcommand group template
350
+ content = `'use strict';
358
351
 
359
- const { Command } = require('millas/console');
352
+ const { BaseCommand } = require('millas/console');
360
353
 
361
354
  /**
362
355
  * ${className}
363
356
  *
364
- * Run with: millas call ${signature}
357
+ * Command group - methods become subcommands automatically.
358
+ * Class name auto-derived: ${className} → ${signature}:*
359
+ *
360
+ * Examples:
361
+ * millas ${signature}:create --name <value>
362
+ * millas ${signature}:update <id> --name <value>
363
+ * millas ${signature}:delete <id>
365
364
  */
366
- class ${className} extends Command {
367
- static signature = '${signature}';
368
- static description = 'Description of ${signature}';
365
+ class ${className} extends BaseCommand {
366
+ // Optional: Add descriptions for subcommands
367
+ // static commands = {
368
+ // create: 'Create a new ${signature}',
369
+ // update: 'Update a ${signature}',
370
+ // delete: 'Delete a ${signature}',
371
+ // };
372
+
373
+ // Private helper (underscore = not a command)
374
+ // async _get${pascalCase(signature)}(id) {
375
+ // return this.container.resolve('Database')
376
+ // .table('${signature}s').find(id);
377
+ // }
378
+
379
+ // Subcommand: Only options
380
+ async create({ name }) {
381
+ // millas ${signature}:create --name <value>
382
+ this.info('Creating ${signature}...');
383
+ this.success('${pascalCase(signature)} created!');
384
+ }
369
385
 
370
- // Optional: positional arguments
371
- // static args = [
372
- // { name: 'target', description: 'The target to act on', default: 'all' },
373
- // ];
386
+ // Subcommand: Positional + options
387
+ async update(id, { name }) {
388
+ // millas ${signature}:update <id> --name <value>
389
+ this.info(\`Updating ${signature} \${id}...\`);
390
+ this.success('${pascalCase(signature)} updated!');
391
+ }
374
392
 
375
- // Optional: named options / flags
393
+ // Subcommand: Only positional
394
+ async delete(id) {
395
+ // millas ${signature}:delete <id>
396
+ this.info(\`Deleting ${signature} \${id}...\`);
397
+ this.success('${pascalCase(signature)} deleted!');
398
+ }
399
+ }
400
+
401
+ module.exports = ${className};
402
+ `;
403
+ } else {
404
+ // Generate simple command template
405
+ content = `'use strict';
406
+
407
+ const { BaseCommand } = require('millas/console');
408
+
409
+ /**
410
+ * ${className}
411
+ *
412
+ * Simple command - uses handle() method.
413
+ * Command name auto-derived: ${className} → ${signature}
414
+ * Run with: millas ${signature}
415
+ */
416
+ class ${className} extends BaseCommand {
417
+ static description = 'Description of ${signature}';
418
+
419
+ // Optional: simple options (declarative)
376
420
  // static options = [
377
- // { flag: '--dry-run', description: 'Preview without making changes' },
378
- // { flag: '--limit <n>', description: 'Max items to process', default: '50' },
421
+ // { flags: '--dry-run', description: 'Preview without making changes' },
422
+ // { flags: '--limit <n>', description: 'Max items', defaultValue: '50' },
379
423
  // ];
380
424
 
381
- async handle() {
382
- // const target = this.argument('target');
383
- // const limit = this.option('limit');
384
- // const dry = this.option('dryRun');
425
+ // Optional: custom arguments (advanced)
426
+ // addArguments(parser) {
427
+ // parser
428
+ // .argument('<file>', 'Input file path')
429
+ // .argument('[output]', 'Output file path', 'output.txt')
430
+ // .option('--force', 'Force overwrite');
431
+ // }
432
+
433
+ // Optional: validate inputs before execution
434
+ // async validate(...args) {
435
+ // if (!args[0]) {
436
+ // throw new Error('Missing required argument');
437
+ // }
438
+ // }
439
+
440
+ async handle(options) {
441
+ // Access options: options.dryRun, options.limit, etc.
442
+ // Access DI container: this.container.resolve('ServiceName')
443
+ // Logging helpers: this.info(), this.success(), this.warn(), this.error()
385
444
 
386
445
  this.info('Running ${signature}...');
387
446
 
388
447
  // Your command logic here
389
448
 
390
- this.success('Done.');
449
+ this.success('Done!');
391
450
  }
392
451
  }
393
452
 
394
453
  module.exports = ${className};
395
454
  `;
455
+ }
396
456
 
397
457
  return write(filePath, content);
398
458
  }