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/package.json
CHANGED
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.
|
|
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
|
|
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.
|
|
17
|
+
this.info('Create Millas Superuser');
|
|
18
|
+
this.newLine()
|
|
18
19
|
|
|
19
20
|
// Email
|
|
20
|
-
if (!email) email = await this
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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.#
|
|
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
|
|
81
|
-
|
|
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.
|
|
83
|
+
this.info(`Changing password for: ${user.email}`);
|
|
84
|
+
this.newLine();
|
|
87
85
|
|
|
88
|
-
const plain = await this.#
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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 =
|
|
198
|
+
module.exports = AdminCommand;
|
package/src/commands/key.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const Command = require('../console/Command');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
class KeyCommand extends
|
|
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
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.
|
|
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.
|
|
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.
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
64
|
+
this.comment(this.style.muted('APP_KEY=') + this.style.info(key));
|
|
72
65
|
}
|
|
73
66
|
}
|
|
74
67
|
|
package/src/commands/lang.js
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
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
|
|
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
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.
|
|
19
|
-
.
|
|
20
|
-
.
|
|
21
|
-
.
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
package/src/commands/make.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const
|
|
4
|
+
const Command = require('../console/Command');
|
|
5
5
|
const SchematicEngine = require('../schematics/SchematicEngine');
|
|
6
6
|
|
|
7
|
-
class MakeCommand extends
|
|
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
|
-
.
|
|
20
|
-
.
|
|
21
|
-
.
|
|
19
|
+
.str('name')
|
|
20
|
+
.str('--resource')
|
|
21
|
+
.str('--model')
|
|
22
22
|
.description('Generate a new controller');
|
|
23
23
|
|
|
24
24
|
register
|
package/src/commands/migrate.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs-extra');
|
|
5
|
-
const
|
|
5
|
+
const Command = require('../console/Command');
|
|
6
6
|
const DB = require('../facades/DB');
|
|
7
7
|
|
|
8
|
-
class MigrateCommand extends
|
|
8
|
+
class MigrateCommand extends Command {
|
|
9
9
|
static description = 'Database migration commands';
|
|
10
10
|
static namespace = 'db';
|
|
11
11
|
|
package/src/commands/new.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
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
|
|
9
|
+
class NewCommand extends Command {
|
|
10
10
|
static description = 'Create a new Millas project';
|
|
11
11
|
|
|
12
12
|
async onInit(register) {
|
package/src/commands/route.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const Command = require('../console/Command');
|
|
4
4
|
|
|
5
|
-
class RouteCommand extends
|
|
5
|
+
class RouteCommand extends Command {
|
|
6
6
|
static description = 'Manage application routes';
|
|
7
7
|
|
|
8
8
|
async onInit(register) {
|
package/src/commands/schedule.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const Command = require('../console/Command');
|
|
4
4
|
|
|
5
|
-
class ScheduleCommand extends
|
|
5
|
+
class ScheduleCommand extends Command {
|
|
6
6
|
static description = 'Manage scheduled tasks';
|
|
7
7
|
|
|
8
8
|
async onInit(register) {
|
package/src/commands/serve.js
CHANGED
|
@@ -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
|
|
7
|
+
const Command = require('../console/Command');
|
|
8
8
|
const patchConsole = require('../logger/patchConsole');
|
|
9
9
|
const Logger = require('../logger/internal');
|
|
10
10
|
|
|
@@ -46,6 +46,7 @@ class HotReloader {
|
|
|
46
46
|
...process.env,
|
|
47
47
|
...extra,
|
|
48
48
|
MILLAS_CHILD: '1',
|
|
49
|
+
MILLAS_CLI_MODE: true,
|
|
49
50
|
},
|
|
50
51
|
stdio: 'inherit',
|
|
51
52
|
});
|
|
@@ -136,7 +137,7 @@ class HotReloader {
|
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
class ServeCommand extends
|
|
140
|
+
class ServeCommand extends Command {
|
|
140
141
|
static description = 'Start the development server with hot reload';
|
|
141
142
|
|
|
142
143
|
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
|
|
51
|
-
|
|
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
|
-
*
|
|
72
|
-
*
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
334
|
-
|
|
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,
|