millas 0.2.13 → 0.2.15
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 +6 -3
- package/src/admin/Admin.js +107 -1027
- package/src/admin/AdminAuth.js +1 -1
- package/src/admin/ViewContext.js +1 -1
- package/src/admin/handlers/ActionHandler.js +103 -0
- package/src/admin/handlers/ApiHandler.js +113 -0
- package/src/admin/handlers/AuthHandler.js +76 -0
- package/src/admin/handlers/ExportHandler.js +70 -0
- package/src/admin/handlers/InlineHandler.js +71 -0
- package/src/admin/handlers/PageHandler.js +351 -0
- package/src/admin/resources/AdminResource.js +22 -1
- package/src/admin/static/SelectFilter2.js +34 -0
- package/src/admin/static/actions.js +201 -0
- package/src/admin/static/admin.css +7 -0
- package/src/admin/static/change_form.js +585 -0
- package/src/admin/static/core.js +128 -0
- package/src/admin/static/login.js +76 -0
- package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
- package/src/admin/static/vendor/jquery.min.js +2 -0
- package/src/admin/views/layouts/base.njk +30 -113
- package/src/admin/views/pages/detail.njk +10 -9
- package/src/admin/views/pages/form.njk +4 -4
- package/src/admin/views/pages/list.njk +11 -193
- package/src/admin/views/pages/login.njk +19 -64
- package/src/admin/views/partials/form-field.njk +1 -1
- package/src/admin/views/partials/form-scripts.njk +4 -478
- package/src/admin/views/partials/form-widget.njk +10 -10
- package/src/ai/AITokenBudget.js +1 -1
- package/src/auth/Auth.js +112 -3
- package/src/auth/AuthMiddleware.js +18 -15
- package/src/auth/Hasher.js +15 -43
- package/src/cli.js +3 -0
- package/src/commands/call.js +190 -0
- package/src/commands/createsuperuser.js +3 -4
- package/src/commands/key.js +97 -0
- package/src/commands/make.js +16 -2
- package/src/commands/new.js +16 -1
- package/src/commands/serve.js +5 -5
- package/src/console/Command.js +337 -0
- package/src/console/CommandLoader.js +165 -0
- package/src/console/index.js +6 -0
- package/src/container/AppInitializer.js +48 -1
- package/src/container/Application.js +3 -1
- package/src/container/HttpServer.js +0 -1
- package/src/container/MillasConfig.js +48 -0
- package/src/controller/Controller.js +13 -11
- package/src/core/docs.js +6 -0
- package/src/core/foundation.js +8 -0
- package/src/core/http.js +20 -10
- package/src/core/validation.js +58 -27
- package/src/docs/Docs.js +268 -0
- package/src/docs/DocsServiceProvider.js +80 -0
- package/src/docs/SchemaInferrer.js +131 -0
- package/src/docs/handlers/ApiHandler.js +305 -0
- package/src/docs/handlers/PageHandler.js +47 -0
- package/src/docs/index.js +13 -0
- package/src/docs/resources/ApiResource.js +402 -0
- package/src/docs/static/docs.css +723 -0
- package/src/docs/static/docs.js +1181 -0
- package/src/encryption/Encrypter.js +381 -0
- package/src/facades/Auth.js +5 -2
- package/src/facades/Crypt.js +166 -0
- package/src/facades/Docs.js +43 -0
- package/src/facades/Mail.js +1 -1
- package/src/http/MillasRequest.js +7 -31
- package/src/http/RequestContext.js +11 -7
- package/src/http/SecurityBootstrap.js +24 -2
- package/src/http/Shape.js +168 -0
- package/src/http/adapters/ExpressAdapter.js +9 -5
- package/src/middleware/CorsMiddleware.js +3 -0
- package/src/middleware/ThrottleMiddleware.js +10 -7
- package/src/orm/model/Model.js +20 -2
- package/src/providers/EncryptionServiceProvider.js +66 -0
- package/src/router/MiddlewareRegistry.js +79 -54
- package/src/router/Route.js +9 -4
- package/src/router/RouteEntry.js +91 -0
- package/src/router/Router.js +71 -1
- package/src/scaffold/maker.js +138 -1
- package/src/scaffold/templates.js +12 -0
- package/src/serializer/Serializer.js +239 -0
- package/src/support/Str.js +1080 -0
- package/src/validation/BaseValidator.js +45 -5
- package/src/validation/Validator.js +67 -61
- package/src/validation/types.js +490 -0
- package/src/middleware/AuthMiddleware.js +0 -46
- package/src/middleware/MiddlewareRegistry.js +0 -106
package/src/commands/serve.js
CHANGED
|
@@ -198,13 +198,15 @@ module.exports = function (program) {
|
|
|
198
198
|
program
|
|
199
199
|
.command('serve')
|
|
200
200
|
.description('Start the development server with hot reload')
|
|
201
|
-
.option('-p, --port <port>', 'Port to listen on'
|
|
201
|
+
.option('-p, --port <port>', 'Port to listen on')
|
|
202
202
|
.option('-h, --host <host>', 'Host to bind to', 'localhost')
|
|
203
203
|
.option('--no-reload', 'Disable hot reload (run once, like production)')
|
|
204
204
|
.action((options) => {
|
|
205
205
|
|
|
206
|
+
require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
|
|
207
|
+
|
|
206
208
|
const restoreAfterPatch = patchConsole(Logger,"SystemOut")
|
|
207
|
-
|
|
209
|
+
let appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
208
210
|
|
|
209
211
|
if (!fs.existsSync(appBootstrap)) {
|
|
210
212
|
process.stderr.write(chalk.red('\n ✖ No Millas project found here.\n'));
|
|
@@ -212,8 +214,7 @@ module.exports = function (program) {
|
|
|
212
214
|
process.exit(1);
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
|
|
216
|
-
const publicPort = parseInt(options.port, 10) || 3000;
|
|
217
|
+
const publicPort = parseInt(options.port ||process.env.APP_PORT, 10) || 3000;
|
|
217
218
|
const publicHost = options.host || 'localhost';
|
|
218
219
|
|
|
219
220
|
const env = {
|
|
@@ -227,7 +228,6 @@ module.exports = function (program) {
|
|
|
227
228
|
printBanner(publicHost, publicPort);
|
|
228
229
|
|
|
229
230
|
if (options.reload !== false) {
|
|
230
|
-
|
|
231
231
|
new HotReloader(appBootstrap, publicPort, publicHost).start();
|
|
232
232
|
} else {
|
|
233
233
|
try {
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Command
|
|
7
|
+
*
|
|
8
|
+
* Base class for all custom Millas console commands.
|
|
9
|
+
* Mirrors Laravel's Artisan command / Django's BaseCommand.
|
|
10
|
+
*
|
|
11
|
+
* ── Defining a command ────────────────────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* // app/commands/SendDigestCommand.js
|
|
14
|
+
* const { Command } = require('millas/console');
|
|
15
|
+
*
|
|
16
|
+
* class SendDigestCommand extends Command {
|
|
17
|
+
* static signature = 'email:digest';
|
|
18
|
+
* static description = 'Send the weekly digest email to all subscribers';
|
|
19
|
+
*
|
|
20
|
+
* // Optional: define arguments and options
|
|
21
|
+
* static args = [
|
|
22
|
+
* { name: 'type', description: 'Digest type (weekly|daily)', default: 'weekly' },
|
|
23
|
+
* ];
|
|
24
|
+
*
|
|
25
|
+
* static options = [
|
|
26
|
+
* { flag: '--dry-run', description: 'Preview without sending' },
|
|
27
|
+
* { flag: '--limit <n>', description: 'Max recipients', default: '100' },
|
|
28
|
+
* ];
|
|
29
|
+
*
|
|
30
|
+
* async handle() {
|
|
31
|
+
* const type = this.argument('type');
|
|
32
|
+
* const limit = this.option('limit');
|
|
33
|
+
* const dry = this.option('dryRun');
|
|
34
|
+
*
|
|
35
|
+
* this.info(`Sending ${type} digest to up to ${limit} users…`);
|
|
36
|
+
*
|
|
37
|
+
* if (dry) {
|
|
38
|
+
* this.warn('Dry run — no emails sent.');
|
|
39
|
+
* return;
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* // … your logic …
|
|
43
|
+
* this.success('Done!');
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* module.exports = SendDigestCommand;
|
|
48
|
+
*
|
|
49
|
+
* ── Running ───────────────────────────────────────────────────────────────────
|
|
50
|
+
*
|
|
51
|
+
* millas call email:digest
|
|
52
|
+
* millas call email:digest weekly --limit 50 --dry-run
|
|
53
|
+
*
|
|
54
|
+
* ── Output helpers ────────────────────────────────────────────────────────────
|
|
55
|
+
*
|
|
56
|
+
* this.line(msg) — plain output
|
|
57
|
+
* this.info(msg) — cyan
|
|
58
|
+
* this.success(msg) — green ✔
|
|
59
|
+
* this.warn(msg) — yellow ⚠
|
|
60
|
+
* this.error(msg) — red ✖
|
|
61
|
+
* this.comment(msg) — dim / gray
|
|
62
|
+
* this.newLine() — blank line
|
|
63
|
+
* this.table(headers, rows) — formatted table
|
|
64
|
+
*
|
|
65
|
+
* ── Input helpers ─────────────────────────────────────────────────────────────
|
|
66
|
+
*
|
|
67
|
+
* this.argument(name) — positional arg value
|
|
68
|
+
* this.option(name) — option value (camelCase: --dry-run → dryRun)
|
|
69
|
+
* this.ask(question) — interactive prompt, returns Promise<string>
|
|
70
|
+
* this.secret(question) — hidden input (passwords), returns Promise<string>
|
|
71
|
+
* this.confirm(question, default?) — yes/no prompt, returns Promise<boolean>
|
|
72
|
+
*
|
|
73
|
+
* ── Exit ──────────────────────────────────────────────────────────────────────
|
|
74
|
+
*
|
|
75
|
+
* return; — success (exit 0)
|
|
76
|
+
* this.fail(msg) — print error + exit 1
|
|
77
|
+
*/
|
|
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
|
+
|
|
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
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Populate the command with parsed CLI values.
|
|
120
|
+
* Called by CommandLoader before handle().
|
|
121
|
+
*
|
|
122
|
+
* @param {object} args — { argName: value, … }
|
|
123
|
+
* @param {object} options — { optName: value, … }
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
_hydrate(args, options) {
|
|
127
|
+
this._args = args || {};
|
|
128
|
+
this._opts = options || {};
|
|
129
|
+
}
|
|
130
|
+
|
|
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
|
+
/**
|
|
160
|
+
* Prompt the user for input.
|
|
161
|
+
*
|
|
162
|
+
* const name = await this.ask('What is your name?');
|
|
163
|
+
*
|
|
164
|
+
* @param {string} question
|
|
165
|
+
* @returns {Promise<string>}
|
|
166
|
+
*/
|
|
167
|
+
ask(question) {
|
|
168
|
+
return _prompt(question + ' ');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Prompt the user for a secret (input hidden — for passwords).
|
|
173
|
+
*
|
|
174
|
+
* const pass = await this.secret('Password:');
|
|
175
|
+
*
|
|
176
|
+
* @param {string} question
|
|
177
|
+
* @returns {Promise<string>}
|
|
178
|
+
*/
|
|
179
|
+
secret(question) {
|
|
180
|
+
return _promptSecret(question + ' ');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Prompt for a yes/no confirmation.
|
|
185
|
+
*
|
|
186
|
+
* const ok = await this.confirm('Are you sure?');
|
|
187
|
+
* const ok = await this.confirm('Delete all records?', false);
|
|
188
|
+
*
|
|
189
|
+
* @param {string} question
|
|
190
|
+
* @param {boolean} [defaultValue=true]
|
|
191
|
+
* @returns {Promise<boolean>}
|
|
192
|
+
*/
|
|
193
|
+
async confirm(question, defaultValue = true) {
|
|
194
|
+
const hint = defaultValue ? '(Y/n)' : '(y/N)';
|
|
195
|
+
const answer = await _prompt(`${question} ${hint} `);
|
|
196
|
+
const trimmed = (answer || '').trim().toLowerCase();
|
|
197
|
+
if (!trimmed) return defaultValue;
|
|
198
|
+
return trimmed === 'y' || trimmed === 'yes';
|
|
199
|
+
}
|
|
200
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Render a simple table.
|
|
269
|
+
*
|
|
270
|
+
* this.table(
|
|
271
|
+
* ['ID', 'Name', 'Email'],
|
|
272
|
+
* [[1, 'Alice', 'alice@example.com'],
|
|
273
|
+
* [2, 'Bob', 'bob@example.com']],
|
|
274
|
+
* );
|
|
275
|
+
*
|
|
276
|
+
* @param {string[]} headers
|
|
277
|
+
* @param {Array[]} rows
|
|
278
|
+
*/
|
|
279
|
+
table(headers, rows) {
|
|
280
|
+
if (!headers.length) return;
|
|
281
|
+
|
|
282
|
+
// Compute column widths
|
|
283
|
+
const widths = headers.map((h, i) => {
|
|
284
|
+
const colValues = [String(h), ...rows.map(r => String(r[i] ?? ''))];
|
|
285
|
+
return Math.max(...colValues.map(v => v.length));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const hr = ' ' + widths.map(w => '─'.repeat(w + 2)).join('┼') ;
|
|
289
|
+
const fmt = (cells, color) => ' ' + cells
|
|
290
|
+
.map((c, i) => color(String(c ?? '').padEnd(widths[i])))
|
|
291
|
+
.join(chalk.dim(' │ '));
|
|
292
|
+
|
|
293
|
+
this.newLine();
|
|
294
|
+
this.line(fmt(headers, chalk.bold));
|
|
295
|
+
this.line(chalk.dim(hr));
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
this.line(fmt(row, v => v));
|
|
298
|
+
}
|
|
299
|
+
this.newLine();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
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
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Prompt helpers ────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
function _prompt(question) {
|
|
317
|
+
const readline = require('readline');
|
|
318
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
319
|
+
return new Promise(resolve => {
|
|
320
|
+
rl.question(question, answer => { rl.close(); resolve(answer); });
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _promptSecret(question) {
|
|
325
|
+
if (!process.stdin.isTTY) return _prompt(question);
|
|
326
|
+
return new Promise(resolve => {
|
|
327
|
+
process.stdout.write(question);
|
|
328
|
+
const rl = require('readline').createInterface({
|
|
329
|
+
input: process.stdin,
|
|
330
|
+
output: new (require('stream').Writable)({ write(c, e, cb) { cb(); } }),
|
|
331
|
+
terminal: true,
|
|
332
|
+
});
|
|
333
|
+
rl.question('', answer => { rl.close(); process.stdout.write('\n'); resolve(answer); });
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = Command;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CommandLoader
|
|
8
|
+
*
|
|
9
|
+
* Discovers all custom commands in app/commands/ and registers
|
|
10
|
+
* them as Commander sub-commands under `millas call <signature>`.
|
|
11
|
+
*
|
|
12
|
+
* Each file must export a class that extends Command:
|
|
13
|
+
*
|
|
14
|
+
* module.exports = SendDigestCommand;
|
|
15
|
+
*
|
|
16
|
+
* CommandLoader reads the static `signature`, `description`, `args`,
|
|
17
|
+
* and `options` properties to wire up Commander automatically —
|
|
18
|
+
* no manual registration needed.
|
|
19
|
+
*/
|
|
20
|
+
class CommandLoader {
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} commandsDir — absolute path to app/commands/
|
|
23
|
+
*/
|
|
24
|
+
constructor(commandsDir) {
|
|
25
|
+
this._dir = commandsDir;
|
|
26
|
+
this._commands = new Map(); // signature → CommandClass
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Scan commandsDir and load all valid Command subclasses.
|
|
31
|
+
* Files that throw on require() are reported as warnings — never silently skipped.
|
|
32
|
+
* Files that don't export a valid Command subclass are skipped with a clear message.
|
|
33
|
+
*
|
|
34
|
+
* @returns {Map<string, typeof Command>}
|
|
35
|
+
*/
|
|
36
|
+
load() {
|
|
37
|
+
if (!fs.existsSync(this._dir)) return this._commands;
|
|
38
|
+
|
|
39
|
+
const chalk = require('chalk');
|
|
40
|
+
|
|
41
|
+
const files = fs.readdirSync(this._dir)
|
|
42
|
+
.filter(f => f.endsWith('.js') && !f.startsWith('.') && !f.startsWith('_'));
|
|
43
|
+
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const filePath = path.join(this._dir, file);
|
|
46
|
+
|
|
47
|
+
let CommandClass;
|
|
48
|
+
try {
|
|
49
|
+
CommandClass = require(filePath);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
chalk.red(`\n ✖ Failed to load command file: ${chalk.bold(file)}\n`) +
|
|
53
|
+
chalk.dim(` ${err.message}\n`) +
|
|
54
|
+
(process.env.DEBUG ? chalk.dim(err.stack + '\n') : '') +
|
|
55
|
+
'\n'
|
|
56
|
+
);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!CommandClass || typeof CommandClass !== 'function') {
|
|
61
|
+
process.stderr.write(
|
|
62
|
+
chalk.yellow(` ⚠ Skipping ${chalk.bold(file)} — does not export a class.\n`)
|
|
63
|
+
);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!CommandClass.signature) {
|
|
68
|
+
process.stderr.write(
|
|
69
|
+
chalk.yellow(` ⚠ Skipping ${chalk.bold(file)} — missing static signature.\n`)
|
|
70
|
+
);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof CommandClass.prototype.handle !== 'function') {
|
|
75
|
+
process.stderr.write(
|
|
76
|
+
chalk.yellow(` ⚠ Skipping ${chalk.bold(file)} — missing handle() method.\n`)
|
|
77
|
+
);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this._commands.set(CommandClass.signature, CommandClass);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this._commands;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Register all loaded commands onto a Commander `program` instance.
|
|
89
|
+
* Called by the `millas call` command to dynamically attach sub-commands.
|
|
90
|
+
*
|
|
91
|
+
* @param {import('commander').Command} program
|
|
92
|
+
*/
|
|
93
|
+
register(program) {
|
|
94
|
+
this.load();
|
|
95
|
+
|
|
96
|
+
for (const [, CommandClass] of this._commands) {
|
|
97
|
+
this._registerOne(program, CommandClass);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Internal ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
_registerOne(program, CommandClass) {
|
|
104
|
+
const sig = CommandClass.signature;
|
|
105
|
+
const desc = CommandClass.description || '';
|
|
106
|
+
const args = CommandClass.args || [];
|
|
107
|
+
const opts = CommandClass.options || [];
|
|
108
|
+
|
|
109
|
+
// Build Commander argument string: <required> or [optional]
|
|
110
|
+
const argStr = args
|
|
111
|
+
.map(a => a.default !== undefined ? `[${a.name}]` : `<${a.name}>`)
|
|
112
|
+
.join(' ');
|
|
113
|
+
|
|
114
|
+
const fullSig = argStr ? `${sig} ${argStr}` : sig;
|
|
115
|
+
|
|
116
|
+
let cmd = program.command(fullSig).description(desc);
|
|
117
|
+
|
|
118
|
+
// Register each option
|
|
119
|
+
for (const opt of opts) {
|
|
120
|
+
if (opt.default !== undefined) {
|
|
121
|
+
cmd = cmd.option(opt.flag, opt.description || '', opt.default);
|
|
122
|
+
} else {
|
|
123
|
+
cmd = cmd.option(opt.flag, opt.description || '');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Action handler
|
|
128
|
+
cmd.action(async (...cliArgs) => {
|
|
129
|
+
// Commander passes positional args first, then the options object last
|
|
130
|
+
const options = cliArgs[cliArgs.length - 1];
|
|
131
|
+
const posArgs = cliArgs.slice(0, cliArgs.length - 1);
|
|
132
|
+
|
|
133
|
+
// Build named arg map
|
|
134
|
+
const argMap = {};
|
|
135
|
+
for (let i = 0; i < args.length; i++) {
|
|
136
|
+
argMap[args[i].name] = posArgs[i] !== undefined
|
|
137
|
+
? posArgs[i]
|
|
138
|
+
: args[i].default;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const instance = new CommandClass();
|
|
142
|
+
instance._hydrate(argMap, options);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await instance.handle();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const chalk = require('chalk');
|
|
148
|
+
process.stderr.write(chalk.red(`\n ✖ ${sig} failed: ${err.message}\n`));
|
|
149
|
+
if (process.env.DEBUG) process.stderr.write(err.stack + '\n');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Return all loaded command signatures.
|
|
157
|
+
* @returns {string[]}
|
|
158
|
+
*/
|
|
159
|
+
signatures() {
|
|
160
|
+
this.load();
|
|
161
|
+
return [...this._commands.keys()];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = CommandLoader;
|
|
@@ -55,7 +55,10 @@ class AppInitializer {
|
|
|
55
55
|
*/
|
|
56
56
|
async boot() {
|
|
57
57
|
await this.bootKernel();
|
|
58
|
-
|
|
58
|
+
if (!process.env.MILLAS_CLI_MODE) {
|
|
59
|
+
await this._serve();
|
|
60
|
+
}
|
|
61
|
+
return this._kernel;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
/**
|
|
@@ -82,6 +85,18 @@ class AppInitializer {
|
|
|
82
85
|
const basePath = cfg.basePath || process.cwd();
|
|
83
86
|
const appConfig = SecurityBootstrap.loadConfig(basePath + '/config/app');
|
|
84
87
|
SecurityBootstrap.apply(this._adapter.nativeApp || expressApp, appConfig);
|
|
88
|
+
|
|
89
|
+
// ── CORS — applied immediately after security, before routes ──────────
|
|
90
|
+
// Only active when .withCors() was called in bootstrap/app.js.
|
|
91
|
+
// Config is read from the cors: {} block in config/app.js (already loaded
|
|
92
|
+
// above as appConfig). No cors key = CorsMiddleware class defaults apply.
|
|
93
|
+
if (cfg.cors !== null && cfg.cors !== undefined) {
|
|
94
|
+
const CorsMiddleware = require('../middleware/CorsMiddleware');
|
|
95
|
+
const corsMiddleware = new CorsMiddleware(appConfig.cors || {});
|
|
96
|
+
(this._adapter.nativeApp || expressApp).use(
|
|
97
|
+
this._adapter.wrapMiddleware(corsMiddleware, null)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
85
100
|
// ─────────────────────────────────────────────────────────────────────
|
|
86
101
|
|
|
87
102
|
for (const mw of (cfg.adapterMiddleware || [])) {
|
|
@@ -130,6 +145,23 @@ class AppInitializer {
|
|
|
130
145
|
}
|
|
131
146
|
}
|
|
132
147
|
|
|
148
|
+
// ── Docs panel ────────────────────────────────────────────────
|
|
149
|
+
if (cfg.docs !== null && cfg.docs !== undefined) {
|
|
150
|
+
try {
|
|
151
|
+
const Docs = require('../docs/Docs');
|
|
152
|
+
// Wire live RouteRegistry now that routes are fully mounted
|
|
153
|
+
try {
|
|
154
|
+
Docs.setRouteRegistry(this._kernel.route.getRegistry());
|
|
155
|
+
} catch {}
|
|
156
|
+
if (cfg.docs && Object.keys(cfg.docs).length) {
|
|
157
|
+
Docs.configure(cfg.docs);
|
|
158
|
+
}
|
|
159
|
+
Docs.mount(this._adapter.nativeApp);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
process.stderr.write(`[millas] Docs mount failed: ${err.message}\n`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
133
165
|
this._kernel.mountFallbacks();
|
|
134
166
|
|
|
135
167
|
const server = new HttpServer(this._kernel, {
|
|
@@ -178,6 +210,12 @@ class AppInitializer {
|
|
|
178
210
|
if (p) providers.push(p);
|
|
179
211
|
}
|
|
180
212
|
|
|
213
|
+
// ── 4b. Docs — on when .withDocs() was called ────────────────────────
|
|
214
|
+
if (cfg.docs !== null && cfg.docs !== undefined) {
|
|
215
|
+
const p = load('../docs/DocsServiceProvider');
|
|
216
|
+
if (p) providers.push(p);
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
// ── 5. Cache + Storage ───────────────────────────────────────────────
|
|
182
220
|
if (cfg.cache !== false || cfg.storage !== false) {
|
|
183
221
|
const p = load('../providers/CacheStorageServiceProvider');
|
|
@@ -208,6 +246,15 @@ class AppInitializer {
|
|
|
208
246
|
// ── 9. i18n — opt-in via config/app.js use_i18n: true ───────────────
|
|
209
247
|
// Mirrors Django's USE_I18N = True in settings.py.
|
|
210
248
|
// Booted last so translations are available in all request handlers.
|
|
249
|
+
// ── 9. Encryption — always on (APP_KEY drives it) ────────────────────
|
|
250
|
+
// Mirrors Laravel: the encrypter is always bound so Crypt / Encrypt
|
|
251
|
+
// facades work out of the box. If APP_KEY is absent a clear error is
|
|
252
|
+
// thrown on first use, not at boot — apps without encryption still start.
|
|
253
|
+
{
|
|
254
|
+
const p = load('../providers/EncryptionServiceProvider');
|
|
255
|
+
if (p) providers.push(p);
|
|
256
|
+
}
|
|
257
|
+
|
|
211
258
|
if (this._resolveI18nEnabled(cfg)) {
|
|
212
259
|
const p = load('../i18n/I18nServiceProvider');
|
|
213
260
|
if (p) providers.push(p);
|
|
@@ -304,7 +304,9 @@ class Application {
|
|
|
304
304
|
this._container.instance('url', urlGenerator);
|
|
305
305
|
|
|
306
306
|
const { HashManager } = require('../hashing/Hash');
|
|
307
|
-
|
|
307
|
+
let _hashRounds = 12;
|
|
308
|
+
try { const _appCfg = require(basePath + '/config/app'); if (_appCfg.hashing?.rounds) _hashRounds = _appCfg.hashing.rounds; } catch {}
|
|
309
|
+
const hashManager = new HashManager({ default: 'bcrypt', bcrypt: { rounds: _hashRounds } });
|
|
308
310
|
this._container.instance('hash', hashManager);
|
|
309
311
|
|
|
310
312
|
const ProcessManager = require('../process/Process').ProcessManager;
|