millas 0.2.12-beta-1 → 0.2.13

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 (120) hide show
  1. package/package.json +3 -2
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +516 -199
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +318 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +393 -97
  12. package/src/admin/static/admin.css +1422 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +87 -1046
  19. package/src/admin/views/pages/detail.njk +56 -21
  20. package/src/admin/views/pages/error.njk +65 -0
  21. package/src/admin/views/pages/form.njk +47 -599
  22. package/src/admin/views/pages/list.njk +270 -62
  23. package/src/admin/views/partials/form-field.njk +53 -0
  24. package/src/admin/views/partials/form-footer.njk +28 -0
  25. package/src/admin/views/partials/form-readonly.njk +114 -0
  26. package/src/admin/views/partials/form-scripts.njk +480 -0
  27. package/src/admin/views/partials/form-widget.njk +297 -0
  28. package/src/admin/views/partials/icons.njk +64 -0
  29. package/src/admin/views/partials/json-dialog.njk +80 -0
  30. package/src/admin/views/partials/json-editor.njk +37 -0
  31. package/src/ai/AIManager.js +954 -0
  32. package/src/ai/AITokenBudget.js +250 -0
  33. package/src/ai/PromptGuard.js +216 -0
  34. package/src/ai/agents.js +218 -0
  35. package/src/ai/conversation.js +213 -0
  36. package/src/ai/drivers.js +734 -0
  37. package/src/ai/files.js +249 -0
  38. package/src/ai/media.js +303 -0
  39. package/src/ai/pricing.js +152 -0
  40. package/src/ai/provider_tools.js +114 -0
  41. package/src/ai/types.js +356 -0
  42. package/src/auth/Auth.js +18 -2
  43. package/src/auth/AuthUser.js +65 -44
  44. package/src/cli.js +3 -1
  45. package/src/commands/createsuperuser.js +267 -0
  46. package/src/commands/lang.js +589 -0
  47. package/src/commands/migrate.js +154 -81
  48. package/src/commands/serve.js +3 -4
  49. package/src/container/AppInitializer.js +101 -20
  50. package/src/container/Application.js +31 -1
  51. package/src/container/MillasApp.js +10 -3
  52. package/src/container/MillasConfig.js +35 -6
  53. package/src/core/admin.js +5 -0
  54. package/src/core/db.js +2 -1
  55. package/src/core/foundation.js +2 -10
  56. package/src/core/lang.js +1 -0
  57. package/src/errors/HttpError.js +32 -16
  58. package/src/facades/AI.js +411 -0
  59. package/src/facades/Hash.js +67 -0
  60. package/src/facades/Process.js +144 -0
  61. package/src/hashing/Hash.js +262 -0
  62. package/src/http/HtmlEscape.js +162 -0
  63. package/src/http/MillasRequest.js +63 -7
  64. package/src/http/MillasResponse.js +70 -4
  65. package/src/http/ResponseDispatcher.js +21 -27
  66. package/src/http/SafeFilePath.js +195 -0
  67. package/src/http/SafeRedirect.js +62 -0
  68. package/src/http/SecurityBootstrap.js +70 -0
  69. package/src/http/helpers.js +40 -125
  70. package/src/http/index.js +10 -1
  71. package/src/http/middleware/CsrfMiddleware.js +258 -0
  72. package/src/http/middleware/RateLimiter.js +314 -0
  73. package/src/http/middleware/SecurityHeaders.js +281 -0
  74. package/src/i18n/I18nServiceProvider.js +91 -0
  75. package/src/i18n/Translator.js +643 -0
  76. package/src/i18n/defaults.js +122 -0
  77. package/src/i18n/index.js +164 -0
  78. package/src/i18n/locales/en.js +55 -0
  79. package/src/i18n/locales/sw.js +48 -0
  80. package/src/logger/LogRedactor.js +247 -0
  81. package/src/logger/Logger.js +1 -1
  82. package/src/logger/formatters/JsonFormatter.js +11 -4
  83. package/src/logger/formatters/PrettyFormatter.js +103 -65
  84. package/src/logger/formatters/SimpleFormatter.js +14 -3
  85. package/src/middleware/ThrottleMiddleware.js +27 -4
  86. package/src/migrations/system/0001_users.js +21 -0
  87. package/src/migrations/system/0002_admin_log.js +25 -0
  88. package/src/migrations/system/0003_sessions.js +23 -0
  89. package/src/orm/fields/index.js +210 -188
  90. package/src/orm/migration/DefaultValueParser.js +325 -0
  91. package/src/orm/migration/InteractiveResolver.js +191 -0
  92. package/src/orm/migration/Makemigrations.js +312 -0
  93. package/src/orm/migration/MigrationGraph.js +227 -0
  94. package/src/orm/migration/MigrationRunner.js +202 -108
  95. package/src/orm/migration/MigrationWriter.js +463 -0
  96. package/src/orm/migration/ModelInspector.js +143 -74
  97. package/src/orm/migration/ModelScanner.js +225 -0
  98. package/src/orm/migration/ProjectState.js +213 -0
  99. package/src/orm/migration/RenameDetector.js +175 -0
  100. package/src/orm/migration/SchemaBuilder.js +8 -81
  101. package/src/orm/migration/operations/base.js +57 -0
  102. package/src/orm/migration/operations/column.js +191 -0
  103. package/src/orm/migration/operations/fields.js +252 -0
  104. package/src/orm/migration/operations/index.js +55 -0
  105. package/src/orm/migration/operations/models.js +152 -0
  106. package/src/orm/migration/operations/registry.js +131 -0
  107. package/src/orm/migration/operations/special.js +51 -0
  108. package/src/orm/migration/utils.js +208 -0
  109. package/src/orm/model/Model.js +81 -13
  110. package/src/process/Process.js +333 -0
  111. package/src/providers/AdminServiceProvider.js +66 -9
  112. package/src/providers/AuthServiceProvider.js +40 -5
  113. package/src/providers/CacheStorageServiceProvider.js +2 -2
  114. package/src/providers/DatabaseServiceProvider.js +3 -2
  115. package/src/providers/LogServiceProvider.js +4 -1
  116. package/src/providers/MailServiceProvider.js +1 -1
  117. package/src/providers/QueueServiceProvider.js +1 -1
  118. package/src/router/MiddlewareRegistry.js +27 -2
  119. package/src/scaffold/templates.js +80 -21
  120. package/src/validation/Validator.js +348 -607
@@ -9,40 +9,81 @@ module.exports = function (program) {
9
9
  // ── makemigrations ──────────────────────────────────────────────────────────
10
10
  program
11
11
  .command('makemigrations')
12
- .description('Scan model files, detect schema changes, generate migration files')
13
- .action(async () => {
12
+ .description('Detect model changes and generate migration files (never touches DB)')
13
+ .option('--dry-run', 'Show what would be generated without writing files')
14
+ .option('--noinput', 'Non-interactive mode — fails if dangerous fields need resolution')
15
+ .action(async (options) => {
14
16
  try {
15
- const ctx = getProjectContext();
16
- const ModelInspector = require('../orm/migration/ModelInspector');
17
- const inspector = new ModelInspector(
18
- ctx.modelsPath,
19
- ctx.migrationsPath,
20
- ctx.snapshotPath,
21
- );
22
- const result = await inspector.makeMigrations();
17
+ const ctx = getProjectContext();
18
+ const Mk = require('../orm/migration/Makemigrations');
19
+ const mk = new Mk(ctx.modelsPath, ctx.appMigPath, ctx.systemMigPath, {
20
+ nonInteractive: options.noinput || false,
21
+ });
22
+ const result = await mk.run({ dryRun: options.dryRun });
23
23
 
24
24
  if (result.files.length === 0) {
25
- console.log(chalk.yellow(`\n ${result.message}\n`));
25
+ console.log(chalk.yellow(`\n No changes detected.\n`));
26
26
  } else {
27
- console.log(chalk.green(`\n ✔ ${result.message}`));
28
- result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
29
- console.log(chalk.gray('\n Run: millas migrate to apply these migrations.\n'));
27
+ if (options.dryRun) {
28
+ console.log(chalk.cyan('\n Would generate:'));
29
+ } else {
30
+ console.log(chalk.green('\n Migrations generated:'));
31
+ }
32
+ result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
33
+ result.ops.forEach(op => {
34
+ // Match Django's +/-/~ prefix style exactly
35
+ let prefix, label;
36
+ switch (op.type) {
37
+ case 'CreateModel':
38
+ prefix = chalk.green('+'); label = `Create model ${op.table}`; break;
39
+ case 'DeleteModel':
40
+ prefix = chalk.red('-'); label = `Delete model ${op.table}`; break;
41
+ case 'AddField':
42
+ prefix = chalk.green('+'); label = `Add field ${op.column} to ${op.table}`; break;
43
+ case 'RemoveField':
44
+ prefix = chalk.red('-'); label = `Remove field ${op.column} from ${op.table}`; break;
45
+ case 'AlterField':
46
+ prefix = chalk.yellow('~'); label = `Alter field ${op.column} on ${op.table}`; break;
47
+ case 'RenameField':
48
+ prefix = chalk.yellow('~'); label = `Rename field ${op.oldColumn} on ${op.table} to ${op.newColumn}`; break;
49
+ case 'RenameModel':
50
+ prefix = chalk.yellow('~'); label = `Rename model ${op.oldTable} to ${op.newTable}`; break;
51
+ default:
52
+ prefix = chalk.gray(' '); label = op.type;
53
+ }
54
+ console.log(chalk.gray(` ${prefix} ${label}`));
55
+ });
56
+ if (!options.dryRun) {
57
+ console.log(chalk.gray('\n Run: millas migrate to apply.\n'));
58
+ } else {
59
+ console.log();
60
+ }
30
61
  }
31
62
  } catch (err) {
32
63
  bail('makemigrations', err);
33
64
  }
34
- // makemigrations doesn't open a DB connection so no closeDb() needed
35
65
  });
36
66
 
37
67
  // ── migrate ─────────────────────────────────────────────────────────────────
38
68
  program
39
69
  .command('migrate')
40
- .description('Run all pending migrations')
41
- .action(async () => {
70
+ .description('Apply pending migrations in dependency order (never generates migrations)')
71
+ .option('--fake <name>', 'Mark a migration as applied without running it')
72
+ .action(async (options) => {
42
73
  try {
43
74
  const runner = await getRunner();
75
+
76
+ if (options.fake) {
77
+ const [source, name] = options.fake.includes(':')
78
+ ? options.fake.split(':')
79
+ : ['app', options.fake];
80
+ const result = await runner.fake(source, name);
81
+ console.log(chalk.green(`\n Marked "${result.key}" as applied (fake).\n`));
82
+ return;
83
+ }
84
+
44
85
  const result = await runner.migrate();
45
- printMigrationResult(result, 'Ran');
86
+ printResult(result.ran, 'Applying');
46
87
  } catch (err) {
47
88
  bail('migrate', err);
48
89
  } finally {
@@ -50,18 +91,69 @@ module.exports = function (program) {
50
91
  }
51
92
  });
52
93
 
53
- // ── migrate:fresh ────────────────────────────────────────────────────────────
94
+ // ── migrate:plan ─────────────────────────────────────────────────────────────
54
95
  program
55
- .command('migrate:fresh')
56
- .description('Drop ALL tables then re-run every migration from scratch')
96
+ .command('migrate:plan')
97
+ .description('Preview which migrations would run without applying them')
98
+ .action(async () => {
99
+ try {
100
+ const runner = await getRunner();
101
+ const pending = await runner.plan();
102
+
103
+ if (pending.length === 0) {
104
+ console.log(chalk.yellow('\n Nothing to migrate.\n'));
105
+ return;
106
+ }
107
+
108
+ console.log(chalk.cyan('\n Migration plan:\n'));
109
+ pending.forEach(p => {
110
+ const color = p.source === 'system' ? chalk.gray : chalk.cyan;
111
+ console.log(color(` ${p.key}`));
112
+ });
113
+ console.log();
114
+ } catch (err) {
115
+ bail('migrate:plan', err);
116
+ } finally {
117
+ await closeDb();
118
+ }
119
+ });
120
+
121
+ // ── migrate:status ───────────────────────────────────────────────────────────
122
+ program
123
+ .command('migrate:status')
124
+ .description('Show the status of all migrations')
57
125
  .action(async () => {
58
126
  try {
59
- console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
60
127
  const runner = await getRunner();
61
- const result = await runner.fresh();
62
- printMigrationResult(result, 'Ran');
128
+ const rows = await runner.status();
129
+
130
+ if (rows.length === 0) {
131
+ console.log(chalk.yellow('\n No migrations found.\n'));
132
+ return;
133
+ }
134
+
135
+ const colW = Math.max(...rows.map(r => r.key.length)) + 2;
136
+ console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
137
+ console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
138
+
139
+ let lastSource = null;
140
+ for (const row of rows) {
141
+ if (row.source !== lastSource) {
142
+ if (lastSource !== null) console.log();
143
+ lastSource = row.source;
144
+ }
145
+ const status = row.status === 'Applied'
146
+ ? chalk.green(row.status.padEnd(10))
147
+ : chalk.yellow(row.status.padEnd(10));
148
+ const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
149
+ const label = row.source === 'system'
150
+ ? chalk.gray(row.key.padEnd(colW))
151
+ : chalk.cyan(row.key.padEnd(colW));
152
+ console.log(` ${label} ${status} ${batch}`);
153
+ }
154
+ console.log();
63
155
  } catch (err) {
64
- bail('migrate:fresh', err);
156
+ bail('migrate:status', err);
65
157
  } finally {
66
158
  await closeDb();
67
159
  }
@@ -76,7 +168,7 @@ module.exports = function (program) {
76
168
  try {
77
169
  const runner = await getRunner();
78
170
  const result = await runner.rollback(Number(options.steps));
79
- printMigrationResult(result, 'Rolled back');
171
+ printResult(result.rolledBack, 'Reverting');
80
172
  } catch (err) {
81
173
  bail('migrate:rollback', err);
82
174
  } finally {
@@ -84,66 +176,50 @@ module.exports = function (program) {
84
176
  }
85
177
  });
86
178
 
87
- // ── migrate:reset ────────────────────────────────────────────────────────────
179
+ // ── migrate:fresh ────────────────────────────────────────────────────────────
88
180
  program
89
- .command('migrate:reset')
90
- .description('Rollback ALL migrations')
181
+ .command('migrate:fresh')
182
+ .description('Drop all tables and re-run every migration from scratch')
91
183
  .action(async () => {
92
184
  try {
185
+ console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
93
186
  const runner = await getRunner();
94
- const result = await runner.reset();
95
- printMigrationResult(result, 'Rolled back');
187
+ const result = await runner.fresh();
188
+ printResult(result.ran, 'Applying');
96
189
  } catch (err) {
97
- bail('migrate:reset', err);
190
+ bail('migrate:fresh', err);
98
191
  } finally {
99
192
  await closeDb();
100
193
  }
101
194
  });
102
195
 
103
- // ── migrate:refresh ──────────────────────────────────────────────────────────
196
+ // ── migrate:reset ────────────────────────────────────────────────────────────
104
197
  program
105
- .command('migrate:refresh')
106
- .description('Rollback all then re-run all migrations')
198
+ .command('migrate:reset')
199
+ .description('Rollback ALL migrations')
107
200
  .action(async () => {
108
201
  try {
109
202
  const runner = await getRunner();
110
- const result = await runner.refresh();
111
- printMigrationResult(result, 'Ran');
203
+ const result = await runner.reset();
204
+ printResult(result.rolledBack, 'Reverting');
112
205
  } catch (err) {
113
- bail('migrate:refresh', err);
206
+ bail('migrate:reset', err);
114
207
  } finally {
115
208
  await closeDb();
116
209
  }
117
210
  });
118
211
 
119
- // ── migrate:status ───────────────────────────────────────────────────────────
212
+ // ── migrate:refresh ──────────────────────────────────────────────────────────
120
213
  program
121
- .command('migrate:status')
122
- .description('Show the status of all migration files')
214
+ .command('migrate:refresh')
215
+ .description('Rollback all then re-run all migrations')
123
216
  .action(async () => {
124
217
  try {
125
218
  const runner = await getRunner();
126
- const rows = await runner.status();
127
-
128
- if (rows.length === 0) {
129
- console.log(chalk.yellow('\n No migration files found.\n'));
130
- return;
131
- }
132
-
133
- const colW = Math.max(...rows.map(r => r.name.length)) + 2;
134
- console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
135
- console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
136
-
137
- for (const row of rows) {
138
- const status = row.status === 'Ran'
139
- ? chalk.green(row.status.padEnd(10))
140
- : chalk.yellow(row.status.padEnd(10));
141
- const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
142
- console.log(` ${chalk.cyan(row.name.padEnd(colW))} ${status} ${batch}`);
143
- }
144
- console.log();
219
+ const result = await runner.refresh();
220
+ printResult(result.ran, 'Applying');
145
221
  } catch (err) {
146
- bail('migrate:status', err);
222
+ bail('migrate:refresh', err);
147
223
  } finally {
148
224
  await closeDb();
149
225
  }
@@ -193,10 +269,10 @@ module.exports = function (program) {
193
269
  function getProjectContext() {
194
270
  const cwd = process.cwd();
195
271
  return {
196
- migrationsPath: path.join(cwd, 'database/migrations'),
197
- seedersPath: path.join(cwd, 'database/seeders'),
198
- modelsPath: path.join(cwd, 'app/models'),
199
- snapshotPath: path.join(cwd, '.millas/schema.json'),
272
+ appMigPath: path.join(cwd, 'database/migrations'),
273
+ systemMigPath: path.join(__dirname, '../migrations/system'),
274
+ seedersPath: path.join(cwd, 'database/seeders'),
275
+ modelsPath: path.join(cwd, 'app/models'),
200
276
  };
201
277
  }
202
278
 
@@ -215,31 +291,28 @@ async function getRunner() {
215
291
  const MigrationRunner = require('../orm/migration/MigrationRunner');
216
292
  const ctx = getProjectContext();
217
293
  const db = await getDbConnection();
218
- return new MigrationRunner(db, ctx.migrationsPath);
294
+ return new MigrationRunner(db, ctx.appMigPath, ctx.systemMigPath);
219
295
  }
220
296
 
221
- /**
222
- * Destroy all open knex connection pools so the CLI process exits cleanly.
223
- * Without this, knex keeps the event loop alive indefinitely after the
224
- * command finishes, causing the terminal to appear to hang.
225
- */
226
297
  async function closeDb() {
227
298
  try {
228
299
  const DatabaseManager = require('../orm/drivers/DatabaseManager');
229
300
  await DatabaseManager.closeAll();
230
- } catch { /* already closed or never opened — safe to ignore */ }
301
+ } catch {}
231
302
  }
232
303
 
233
- function printMigrationResult(result, verb) {
234
- const list = result.ran || result.rolledBack || [];
235
- if (list.length === 0) {
236
- console.log(chalk.yellow(`\n ${result.message}\n`));
304
+ function printResult(list, verb) {
305
+ if (!list || list.length === 0) {
306
+ console.log(chalk.yellow('\n Nothing to do.\n'));
237
307
  return;
238
308
  }
239
- console.log(chalk.green(`\n ✔ ${result.message}`));
240
- list.forEach(f =>
241
- console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`))
242
- );
309
+ console.log(chalk.green(`\n Running migrations:`));
310
+ for (const entry of list) {
311
+ const label = typeof entry === 'object' ? entry.label || entry.key : entry;
312
+ const source = typeof entry === 'object' ? entry.source : null;
313
+ const color = source === 'system' ? chalk.gray : chalk.cyan;
314
+ console.log(color(` ${verb} ${label}... OK`));
315
+ }
243
316
  console.log();
244
317
  }
245
318
 
@@ -247,4 +320,4 @@ function bail(cmd, err) {
247
320
  console.error(chalk.red(`\n ✖ ${cmd} failed: ${err.message}\n`));
248
321
  if (process.env.DEBUG) console.error(err.stack);
249
322
  closeDb().finally(() => process.exit(1));
250
- }
323
+ }
@@ -85,6 +85,7 @@ class HotReloader {
85
85
  env: {
86
86
  ...extra,
87
87
  MILLAS_CHILD: '1',
88
+ DEBUG: process.env.APP_DEBUG,
88
89
  },
89
90
  stdio: 'inherit',
90
91
  });
@@ -120,13 +121,11 @@ class HotReloader {
120
121
  }
121
122
 
122
123
  _restart(changedFile) {
123
- const link = changedFile
124
- ? `\x1b]8;;file://${changedFile}\x07${path.relative(process.cwd(), changedFile)}\x1b]8;;\x07`
125
- : '';
124
+
126
125
  console.warn(
127
126
  chalk.yellow('↺') + ' ' +
128
127
  chalk.white('Reloading') +
129
- (link ? chalk.blueBright(' ' + link) : '')
128
+ (changedFile ? chalk.blueBright(' ' + changedFile) : '')
130
129
  );
131
130
 
132
131
  this._restarts++;
@@ -23,12 +23,15 @@ const HttpServer = require('./HttpServer');
23
23
  * bootstrap/app.js → MillasInstance → AppInitialiser.boot()
24
24
  * ├─ builds ExpressAdapter
25
25
  * ├─ builds Application
26
- * ├─ registers providers
26
+ * ├─ registers core providers
27
+ * │ Log → Database → Auth → Admin
28
+ * │ → Cache → Mail → Queue → Events
29
+ * ├─ registers app providers
27
30
  * ├─ registers routes
28
31
  * ├─ registers middleware aliases
29
- * ├─ boots providers
32
+ * ├─ boots all providers
30
33
  * ├─ mounts routes
31
- * ├─ mounts admin (if configured)
34
+ * ├─ mounts admin panel
32
35
  * ├─ mounts fallbacks
33
36
  * └─ starts HttpServer
34
37
  */
@@ -46,52 +49,82 @@ class AppInitializer {
46
49
  * Boot the full application and start the HTTP server.
47
50
  * Returns a Promise that resolves once the server is listening.
48
51
  */
52
+ /**
53
+ * Boot the full application and start the HTTP server.
54
+ * Called by millas serve — no changes to the developer API.
55
+ */
49
56
  async boot() {
57
+ await this.bootKernel();
58
+ await this._serve();
59
+ }
60
+
61
+ /**
62
+ * Boot the application kernel only — DI container, providers, DB, auth,
63
+ * cache, mail, queue. No HTTP server, no routes, no listen().
64
+ *
65
+ * Used internally by CLI commands (millas migrate, millas createsuperuser, etc.)
66
+ * via MILLAS_CLI_BOOT=1 environment variable. Developers never call this directly.
67
+ *
68
+ * @returns {Application} the booted kernel
69
+ */
70
+ async bootKernel() {
50
71
  const cfg = this._config;
51
72
 
52
- // ── Build the HTTP adapter ───────────────────────────────────────────────
53
73
  const ExpressAdapter = require('../http/adapters/ExpressAdapter');
54
74
  const expressApp = express();
55
75
  this._adapter = new ExpressAdapter(expressApp);
56
76
  this._adapter.applyBodyParsers();
57
77
 
58
- // Raw adapter middleware (helmet, compression, etc.)
78
+ // ── Security applied before any routes or developer middleware ──────
79
+ // Reads config/app.js for overrides. All protections are on by default:
80
+ // security headers, CSRF, rate limiting, cookie defaults, allowed hosts.
81
+ const SecurityBootstrap = require('../http/SecurityBootstrap');
82
+ const basePath = cfg.basePath || process.cwd();
83
+ const appConfig = SecurityBootstrap.loadConfig(basePath + '/config/app');
84
+ SecurityBootstrap.apply(this._adapter.nativeApp || expressApp, appConfig);
85
+ // ─────────────────────────────────────────────────────────────────────
86
+
59
87
  for (const mw of (cfg.adapterMiddleware || [])) {
60
88
  this._adapter.applyMiddleware(mw);
61
89
  }
62
90
 
63
- // ── Build the Application kernel ─────────────────────────────────────────
64
91
  this._kernel = new Application(this._adapter);
65
92
 
66
- // Core providers (auto-enabled unless disabled in config)
93
+ this._kernel._container.instance('basePath', basePath);
94
+
67
95
  const coreProviders = this._buildCoreProviders(cfg);
68
96
  this._kernel.providers([...coreProviders, ...cfg.providers]);
69
97
 
70
- // Named middleware aliases
71
98
  for (const {alias, handler} of (cfg.middleware || [])) {
72
99
  this._kernel.middleware(alias, handler);
73
100
  }
74
101
 
75
- // Route definitions
76
102
  if (cfg.routes) {
77
103
  this._kernel.routes(cfg.routes);
78
104
  }
79
105
 
80
- // ── Boot providers ───────────────────────────────────────────────────────
81
106
  await this._kernel.boot();
82
107
 
83
- // ── Mount routes ─────────────────────────────────────────────────────────
108
+ return this._kernel;
109
+ }
110
+
111
+ /**
112
+ * Mount routes and start the HTTP server.
113
+ * Internal — called only by boot(). CLI commands stop after bootKernel().
114
+ */
115
+ async _serve() {
116
+ const cfg = this._config;
117
+
84
118
  if (!process.env.MILLAS_ROUTE_LIST) {
85
119
  this._kernel.mountRoutes();
86
120
 
87
- // Admin panel — mounted between routes and fallbacks
88
121
  if (cfg.admin !== null) {
89
122
  try {
90
123
  const Admin = require('../admin/Admin');
91
124
  if (cfg.admin && Object.keys(cfg.admin).length) {
92
125
  Admin.configure(cfg.admin);
93
126
  }
94
- Admin.mount(expressApp);
127
+ Admin.mount(this._adapter.nativeApp);
95
128
  } catch (err) {
96
129
  process.stderr.write(`[millas] Admin mount failed: ${err.message}\n`);
97
130
  }
@@ -99,9 +132,8 @@ class AppInitializer {
99
132
 
100
133
  this._kernel.mountFallbacks();
101
134
 
102
- // ── Start the HTTP server ──────────────────────────────────────────────
103
135
  const server = new HttpServer(this._kernel, {
104
- onStart: cfg.onStart || undefined,
136
+ onStart: cfg.onStart || undefined,
105
137
  onShutdown: cfg.onShutdown || undefined,
106
138
  });
107
139
 
@@ -114,22 +146,39 @@ class AppInitializer {
114
146
  _buildCoreProviders(cfg) {
115
147
  const providers = [];
116
148
  const load = (p) => {
117
- try {
118
- return require(p);
119
- } catch {
120
- return null;
121
- }
149
+ try { return require(p); } catch { return null; }
122
150
  };
123
151
 
152
+ // ── 1. Logging ───────────────────────────────────────────────────────
124
153
  if (cfg.logging !== false) {
125
154
  const p = load('../providers/LogServiceProvider');
126
155
  if (p) providers.push(p);
127
156
  }
157
+
158
+ // ── 2. Database ──────────────────────────────────────────────────────
128
159
  if (cfg.database !== false) {
129
160
  const p = load('../providers/DatabaseServiceProvider');
130
161
  if (p) providers.push(p);
131
162
  }
132
163
 
164
+ // ── 3. Auth — always on unless explicitly disabled ───────────────────
165
+ // Mirrors Django: django.contrib.auth is in INSTALLED_APPS by default.
166
+ // Provides Auth.login/register, JWT middleware, the User model.
167
+ // Requires Database to be booted first.
168
+ if (cfg.auth !== false) {
169
+ const p = load('../providers/AuthServiceProvider');
170
+ if (p) providers.push(p);
171
+ }
172
+
173
+ // ── 4. Admin — on when .withAdmin() was called ───────────────────────
174
+ // Mirrors Django: django.contrib.admin is in INSTALLED_APPS by default.
175
+ // Requires Auth to be booted first (needs the resolved User model).
176
+ if (cfg.admin !== null && cfg.admin !== undefined) {
177
+ const p = load('../providers/AdminServiceProvider');
178
+ if (p) providers.push(p);
179
+ }
180
+
181
+ // ── 5. Cache + Storage ───────────────────────────────────────────────
133
182
  if (cfg.cache !== false || cfg.storage !== false) {
134
183
  const p = load('../providers/CacheStorageServiceProvider');
135
184
  if (p) {
@@ -138,21 +187,53 @@ class AppInitializer {
138
187
  }
139
188
  }
140
189
 
190
+ // ── 6. Mail ──────────────────────────────────────────────────────────
141
191
  if (cfg.mail !== false) {
142
192
  const p = load('../providers/MailServiceProvider');
143
193
  if (p) providers.push(p);
144
194
  }
195
+
196
+ // ── 7. Queue ─────────────────────────────────────────────────────────
145
197
  if (cfg.queue !== false) {
146
198
  const p = load('../providers/QueueServiceProvider');
147
199
  if (p) providers.push(p);
148
200
  }
201
+
202
+ // ── 8. Events ────────────────────────────────────────────────────────
149
203
  if (cfg.events !== false) {
150
204
  const p = load('../providers/EventServiceProvider');
151
205
  if (p) providers.push(p);
152
206
  }
153
207
 
208
+ // ── 9. i18n — opt-in via config/app.js use_i18n: true ───────────────
209
+ // Mirrors Django's USE_I18N = True in settings.py.
210
+ // Booted last so translations are available in all request handlers.
211
+ if (this._resolveI18nEnabled(cfg)) {
212
+ const p = load('../i18n/I18nServiceProvider');
213
+ if (p) providers.push(p);
214
+ }
215
+
154
216
  return providers;
155
217
  }
218
+
219
+ /**
220
+ * Resolve whether i18n should be enabled.
221
+ * Reads use_i18n from config/app.js — the single source of truth.
222
+ *
223
+ * // config/app.js
224
+ * module.exports = {
225
+ * use_i18n: true,
226
+ * locale: 'sw',
227
+ * fallback: 'en',
228
+ * };
229
+ */
230
+ _resolveI18nEnabled(cfg) {
231
+ try {
232
+ const basePath = cfg.basePath || process.cwd();
233
+ const appConfig = require(basePath + '/config/app');
234
+ return appConfig.use_i18n === true;
235
+ } catch { return false; }
236
+ }
156
237
  }
157
238
 
158
239
  module.exports = AppInitializer;
@@ -98,6 +98,22 @@ class Application {
98
98
  await this._providers.boot();
99
99
  this._booted = true;
100
100
 
101
+ // Wire cache, db, and storage into AI manager now that providers are booted
102
+ try {
103
+ const ai = this._container.make('ai');
104
+ if (ai) {
105
+ try { const cache = this._container.make('cache'); if (cache) ai.setCache(cache); } catch {}
106
+ try { const db = this._container.make('db'); if (db) ai.setDb(db); } catch {}
107
+ try { const store = this._container.make('storage'); if (store) ai.setStorage(store); } catch {}
108
+ // Attach files and stores API as properties
109
+ try {
110
+ const { AIFilesAPI, AIStoresAPI } = require('../ai/files');
111
+ if (!ai.files) Object.defineProperty(ai, 'files', { get: () => new AIFilesAPI(ai), configurable: true });
112
+ if (!ai.stores) Object.defineProperty(ai, 'stores', { get: () => new AIStoresAPI(ai), configurable: true });
113
+ } catch {}
114
+ }
115
+ } catch { /* ai not registered — skip */ }
116
+
101
117
  this._emitSync('platform.booted', {providers: this._providers.list()});
102
118
 
103
119
  return this;
@@ -287,9 +303,23 @@ class Application {
287
303
  });
288
304
  this._container.instance('url', urlGenerator);
289
305
 
306
+ const { HashManager } = require('../hashing/Hash');
307
+ const hashManager = new HashManager({ default: 'bcrypt', bcrypt: { rounds: 12 } });
308
+ this._container.instance('hash', hashManager);
309
+
310
+ const ProcessManager = require('../process/Process').ProcessManager;
311
+ this._container.instance('process', new ProcessManager());
312
+
313
+ const { AIManager } = require('../ai/AIManager');
314
+ const basePath = (() => { try { return this._container.make('basePath'); } catch { return process.cwd(); } })();
315
+ let aiConfig = { default: process.env.AI_PROVIDER || 'anthropic', providers: {} };
316
+ try { aiConfig = require(basePath + '/config/ai'); } catch { /* no config — use env vars */ }
317
+ const aiManager = new AIManager(aiConfig);
318
+ this._container.instance('ai', aiManager);
319
+
290
320
 
291
321
  this._mwRegistry.register('cors', new CorsMiddleware());
292
- this._mwRegistry.register('throttle', new ThrottleMiddleware({max: 60, window: 60}));
322
+ this._mwRegistry.register('throttle', ThrottleMiddleware);
293
323
  this._mwRegistry.register('log', new LogMiddleware());
294
324
  this._mwRegistry.register('auth', AuthMiddleware);
295
325
  }
@@ -30,12 +30,19 @@ const MillasConfig = require('./MillasConfig');
30
30
  const Millas = {
31
31
  /**
32
32
  * Start building an application config.
33
- * Returns a MillasConfig chain ending in .create().
33
+ * Equivalent to Millas.config().configure(basePath).
34
34
  *
35
+ * Usage (bootstrap/app.js):
36
+ * module.exports = Millas.configure(__dirname)
37
+ * .withAdmin()
38
+ * .routes(Route => { ... })
39
+ * .create();
40
+ *
41
+ * @param {string} basePath — pass __dirname from bootstrap/app.js
35
42
  * @returns {MillasConfig}
36
43
  */
37
- config() {
38
- return new MillasConfig();
44
+ configure(basePath) {
45
+ return new MillasConfig().configure(basePath);
39
46
  },
40
47
  };
41
48