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.
- 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/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,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;
|
package/src/console/index.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const Command
|
|
4
|
-
const 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 = {
|
|
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
package/src/scaffold/maker.js
CHANGED
|
@@ -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
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
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(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
346
|
+
let content;
|
|
347
|
+
|
|
348
|
+
if (isGroup) {
|
|
349
|
+
// Generate subcommand group template
|
|
350
|
+
content = `'use strict';
|
|
358
351
|
|
|
359
|
-
const {
|
|
352
|
+
const { BaseCommand } = require('millas/console');
|
|
360
353
|
|
|
361
354
|
/**
|
|
362
355
|
* ${className}
|
|
363
356
|
*
|
|
364
|
-
*
|
|
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
|
|
367
|
-
|
|
368
|
-
static
|
|
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
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
//
|
|
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
|
-
// {
|
|
378
|
-
// {
|
|
421
|
+
// { flags: '--dry-run', description: 'Preview without making changes' },
|
|
422
|
+
// { flags: '--limit <n>', description: 'Max items', defaultValue: '50' },
|
|
379
423
|
// ];
|
|
380
424
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
}
|