aegisnode 0.0.3 → 0.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegisnode",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A view-first Node.js framework for modular web apps and JSON APIs with CLI scaffolding, runtime injection, auth, uploads, i18n, mail, and WebSocket support.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -7,14 +7,17 @@ import path from 'path';
7
7
  import fs from 'fs/promises';
8
8
  import { startProject } from '../src/cli/commands/startproject.js';
9
9
  import { createApp } from '../src/cli/commands/createapp.js';
10
+ import { runCli } from '../src/cli/index.js';
10
11
  import { generateArtifact } from '../src/cli/commands/generate.js';
11
12
  import { createKernel } from '../src/runtime/kernel.js';
12
13
  import { runServer } from '../src/cli/commands/runserver.js';
14
+ import { runGenerateLoader } from '../src/cli/commands/generateloader.js';
13
15
  import { runProject } from '../src/index.js';
14
16
  import { createAuthManager, normalizeAuthConfig } from '../src/runtime/auth.js';
15
17
  import { loadProjectConfig } from '../src/runtime/config.js';
16
18
  import { initializeDatabase, closeDatabase } from '../src/runtime/database.js';
17
19
  import { runDoctor } from '../src/cli/commands/doctor.js';
20
+ import { runFixApp } from '../src/cli/commands/fixapp.js';
18
21
  import { runUpdateDependencies } from '../src/cli/commands/updatedeps.js';
19
22
  import { createHelpers } from '../src/runtime/helpers.js';
20
23
 
@@ -137,8 +140,10 @@ async function main() {
137
140
  await startProject({ projectName, cwd: sandboxRoot });
138
141
  const generatedProjectEnv = await fs.readFile(path.join(projectRoot, '.env'), 'utf8');
139
142
  assert.match(generatedProjectEnv, /^APP_SECRET=.{16,}$/m);
143
+ const generatedAppSecret = generatedProjectEnv.match(/^APP_SECRET=(.+)$/m)?.[1]?.trim();
144
+ assert.ok(generatedAppSecret);
140
145
  const generatedSettings = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
141
- assert.match(generatedSettings, /appSecret:\s*process\.env\.APP_SECRET\s*\|\|\s*''/);
146
+ assert.ok(generatedSettings.includes(`appSecret: process.env.APP_SECRET || ${JSON.stringify(generatedAppSecret)}`));
142
147
  await assert.rejects(
143
148
  () => runProject({
144
149
  rootDir: projectRoot,
@@ -404,6 +409,74 @@ dkcqnJD4SGWVeG+KhA==
404
409
  await fs.access(filePath);
405
410
  }
406
411
 
412
+ await fs.unlink(path.join(projectRoot, 'app.js'));
413
+ await fs.unlink(path.join(projectRoot, 'loader.cjs'));
414
+ const restoredStartupEntries = await runGenerateLoader({
415
+ projectRoot,
416
+ output: {
417
+ log() {},
418
+ },
419
+ });
420
+ assert.equal(restoredStartupEntries.createdApp, true);
421
+ assert.equal(restoredStartupEntries.createdLoader, true);
422
+ await fs.access(path.join(projectRoot, 'app.js'));
423
+ await fs.access(path.join(projectRoot, 'loader.cjs'));
424
+
425
+ await fs.writeFile(
426
+ path.join(envProjectRoot, 'settings.js'),
427
+ `export default {
428
+ appName: 'envdemo',
429
+ env: 'production',
430
+ logging: {
431
+ level: 'info',
432
+ },
433
+ security: {
434
+ appSecret: process.env.APP_SECRET || '',
435
+ ddos: {
436
+ maxRequests: 120,
437
+ },
438
+ },
439
+ environments: {
440
+ default: {
441
+ security: {
442
+ ddos: {
443
+ windowMs: 45000,
444
+ },
445
+ },
446
+ },
447
+ production: {
448
+ logging: { level: 'warn' },
449
+ security: { ddos: { maxRequests: 80 } },
450
+ },
451
+ },
452
+ };
453
+ `,
454
+ 'utf8',
455
+ );
456
+ await fs.unlink(path.join(envProjectRoot, 'loader.cjs'));
457
+ const missingLoaderDoctorReport = await runDoctor({
458
+ projectRoot: envProjectRoot,
459
+ failOnError: false,
460
+ output: {
461
+ log() {},
462
+ },
463
+ });
464
+ assert.equal(missingLoaderDoctorReport.summary.errors, 1);
465
+ assert.ok(
466
+ missingLoaderDoctorReport.entries.some((entry) => (
467
+ entry.level === 'ERROR'
468
+ && /loader\.cjs is missing for production startup/.test(entry.message)
469
+ )),
470
+ );
471
+ const restoredProductionLoader = await runGenerateLoader({
472
+ projectRoot: envProjectRoot,
473
+ output: {
474
+ log() {},
475
+ },
476
+ });
477
+ assert.equal(restoredProductionLoader.createdLoader, true);
478
+ await fs.access(path.join(envProjectRoot, 'loader.cjs'));
479
+
407
480
  const registryPackages = new Map([
408
481
  ['alpha', '2.0.0'],
409
482
  ['@scope/bravo', '3.4.0'],
@@ -519,6 +592,7 @@ dkcqnJD4SGWVeG+KhA==
519
592
  await fs.access(path.join(projectRoot, 'apps', 'users', 'models.js'));
520
593
  await fs.access(path.join(projectRoot, 'apps', 'users', 'validators.js'));
521
594
  await fs.access(path.join(projectRoot, 'apps', 'users', 'services.js'));
595
+ await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
522
596
  await fs.access(path.join(projectRoot, 'apps', 'users', 'subscribers.js'));
523
597
  await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'models.test.js'));
524
598
  await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'validators.test.js'));
@@ -534,6 +608,90 @@ dkcqnJD4SGWVeG+KhA==
534
608
  assert.equal(doctorReport.summary.errors, 0);
535
609
  const projectRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
536
610
  assert.match(projectRoutesFile, /route\.use\((['"])\/users\1, users\);/);
611
+
612
+ await fs.unlink(path.join(projectRoot, 'apps', 'users', 'utils.js'));
613
+ await fs.unlink(path.join(projectRoot, 'apps', 'users', 'tests', 'routes.test.js'));
614
+ await fs.writeFile(
615
+ path.join(projectRoot, 'routes.js'),
616
+ projectRoutesFile
617
+ .replace(/import users from '\.\/apps\/users\/routes\.js';\n/, '')
618
+ .replace(/\s*route\.use\((['"])\/users\1, users\);\n/, '\n'),
619
+ 'utf8',
620
+ );
621
+ const appDoctorRouteReport = await runDoctor({
622
+ projectRoot,
623
+ appName: 'users',
624
+ failOnError: false,
625
+ output: {
626
+ log() {},
627
+ },
628
+ });
629
+ assert.equal(
630
+ appDoctorRouteReport.entries.some((entry) => entry.message.includes('Project routes.js is missing import for app "users".')),
631
+ true,
632
+ );
633
+ assert.equal(
634
+ appDoctorRouteReport.entries.some((entry) => entry.message.includes('Project routes.js is missing route.use(...) mount for app "users".')),
635
+ true,
636
+ );
637
+
638
+ const settingsBeforeFix = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
639
+ await fs.writeFile(
640
+ path.join(projectRoot, 'settings.js'),
641
+ settingsBeforeFix.replace(/\n\s*\{ name: "users", mount: "\/users" \},/, ''),
642
+ 'utf8',
643
+ );
644
+
645
+ const appDoctorReport = await runDoctor({
646
+ projectRoot,
647
+ appName: 'users',
648
+ failOnError: false,
649
+ output: {
650
+ log() {},
651
+ },
652
+ });
653
+ assert.equal(appDoctorReport.summary.errors, 0);
654
+ assert.equal(
655
+ appDoctorReport.entries.some((entry) => entry.message.includes('App "users" is not declared in settings.apps.')),
656
+ true,
657
+ );
658
+ assert.equal(
659
+ appDoctorReport.entries.some((entry) => entry.message.includes('App "users" missing utils.js.')),
660
+ true,
661
+ );
662
+ assert.equal(
663
+ appDoctorReport.entries.some((entry) => entry.message.includes('App "users" missing tests/routes.test.js.')),
664
+ true,
665
+ );
666
+
667
+ const fixAppResult = await runFixApp({
668
+ appName: 'users',
669
+ projectRoot,
670
+ });
671
+ assert.equal(fixAppResult.registryUpdated, true);
672
+ assert.equal(fixAppResult.routesUpdated, true);
673
+ await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
674
+ await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'routes.test.js'));
675
+ const fixedSettingsFile = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
676
+ assert.match(fixedSettingsFile, /\{ name: "users", mount: "\/users" \},/);
677
+ const fixedRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
678
+ assert.match(fixedRoutesFile, /import users from '\.\/apps\/users\/routes\.js';/);
679
+ assert.match(fixedRoutesFile, /route\.use\((['"])\/users\1, users\);/);
680
+ await assert.doesNotReject(
681
+ () => runCli(['doctor', '--app', 'users', '--project', projectRoot]),
682
+ );
683
+ await assert.rejects(
684
+ () => runCli(['doctor', 'users', '--project', projectRoot]),
685
+ /Doctor app target must use --app <app-name>/,
686
+ );
687
+ await assert.doesNotReject(
688
+ () => runCli(['fix', '--app', 'users', '--project', projectRoot]),
689
+ );
690
+ await assert.rejects(
691
+ () => runCli(['fix', 'users', '--project', projectRoot]),
692
+ /Fix app target must use --app <app-name>/,
693
+ );
694
+
537
695
  await generateArtifact({
538
696
  type: 'view',
539
697
  name: 'profile',
@@ -1288,7 +1446,7 @@ export default {
1288
1446
  );
1289
1447
  await fs.writeFile(
1290
1448
  path.join(projectRoot, 'routes.js'),
1291
- `export default {\n register(route) {\n route.get('/csrf-token', (req, res) => {\n res.json({ token: req.csrfToken() });\n });\n\n route.get('/csrf-form', (req, res) => {\n return res.render('csrf-form', { layout: false });\n });\n\n route.post('/submit', (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n },\n};\n`,
1449
+ `export default {\n register(route) {\n route.get('/csrf-token', (req, res) => {\n res.json({ token: req.csrfToken() });\n });\n\n route.get('/csrf-form', (req, res) => {\n return res.render('csrf-form', { layout: false });\n });\n\n route.post('/submit', (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n\n route.post('/submit-upload', route.upload.none(), (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n },\n};\n`,
1292
1450
  'utf8',
1293
1451
  );
1294
1452
 
@@ -1361,6 +1519,32 @@ export default {
1361
1519
  assert.equal(validJsonTokenJson.ok, true);
1362
1520
  assert.equal(validJsonTokenJson.body.name, 'json-with-token');
1363
1521
 
1522
+ const missingMultipartTokenBody = new FormData();
1523
+ missingMultipartTokenBody.set('name', 'multipart-without-token');
1524
+ const missingMultipartTokenResponse = await fetch(`http://127.0.0.1:${csrfPort}/submit-upload`, {
1525
+ method: 'POST',
1526
+ headers: {
1527
+ cookie: csrfCookie,
1528
+ },
1529
+ body: missingMultipartTokenBody,
1530
+ });
1531
+ assert.equal(missingMultipartTokenResponse.status, 403);
1532
+
1533
+ const validMultipartTokenBody = new FormData();
1534
+ validMultipartTokenBody.set('_csrf', csrfTokenJson.token);
1535
+ validMultipartTokenBody.set('name', 'multipart-with-token');
1536
+ const validMultipartTokenResponse = await fetch(`http://127.0.0.1:${csrfPort}/submit-upload`, {
1537
+ method: 'POST',
1538
+ headers: {
1539
+ cookie: csrfCookie,
1540
+ },
1541
+ body: validMultipartTokenBody,
1542
+ });
1543
+ assert.equal(validMultipartTokenResponse.status, 200);
1544
+ const validMultipartTokenJson = await validMultipartTokenResponse.json();
1545
+ assert.equal(validMultipartTokenJson.ok, true);
1546
+ assert.equal(validMultipartTokenJson.body.name, 'multipart-with-token');
1547
+
1364
1548
  const csrfFormResponse = await fetch(`http://127.0.0.1:${csrfPort}/csrf-form`, {
1365
1549
  headers: {
1366
1550
  cookie: csrfCookie,
@@ -1,191 +1,28 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { pathToFileURL } from 'url';
4
- import { ensureDir, ensureValidName, exists, normalizeMountPath, writeFile } from '../utils/fs.js';
1
+ import { ensureValidName, normalizeMountPath } from '../utils/fs.js';
5
2
  import { resolveProjectRoot } from '../utils/project.js';
6
3
  import {
7
- renderAppRoutes,
8
- renderAppViewsFile,
9
- renderAppModelsFile,
10
- renderAppValidatorsFile,
11
- renderAppServicesFile,
12
- renderAppSubscribersFile,
13
- renderAppModelTest,
14
- renderAppValidatorTest,
15
- renderAppServiceTest,
16
- renderAppRoutesTest,
17
- renderSettingsApps,
18
- } from '../utils/scaffolds.js';
19
-
20
- const APPS_START = '// AEGIS_APPS_START';
21
- const APPS_END = '// AEGIS_APPS_END';
22
-
23
- function renderAppEntries(apps) {
24
- return apps
25
- .map((app) => ` { name: ${JSON.stringify(app.name)}, mount: ${JSON.stringify(app.mount)} },`)
26
- .join('\n');
27
- }
28
-
29
- function toImportName(appName) {
30
- const safe = appName
31
- .split(/[-_\s]+/)
32
- .filter(Boolean)
33
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
34
- .join('');
35
-
36
- if (!safe) {
37
- return 'appRoutes';
38
- }
39
-
40
- return `${safe.charAt(0).toLowerCase()}${safe.slice(1)}`;
41
- }
42
-
43
- async function readDefaultExport(filePath) {
44
- const moduleUrl = `${pathToFileURL(filePath).href}?t=${Date.now()}`;
45
- const loaded = await import(moduleUrl);
46
- return loaded?.default;
47
- }
48
-
49
- async function detectSettingsMode(projectRoot) {
50
- const single = path.join(projectRoot, 'settings.js');
51
- const split = path.join(projectRoot, 'settings', 'apps.js');
52
-
53
- if (await exists(single)) {
54
- return { mode: 'single', file: single };
55
- }
56
-
57
- if (await exists(split)) {
58
- return { mode: 'split', file: split };
59
- }
60
-
61
- throw new Error(`Not an AegisNode project root: missing ${single} (or legacy ${split})`);
62
- }
63
-
64
- async function readAppsConfig(settingsMode) {
65
- const normalizeApp = (entry) => {
66
- if (!entry || typeof entry !== 'object' || typeof entry.name !== 'string') {
67
- return null;
68
- }
69
-
70
- ensureValidName(entry.name, 'app');
71
- return {
72
- name: entry.name,
73
- mount: normalizeMountPath(entry.mount || `/${entry.name}`),
74
- };
75
- };
76
-
77
- if (settingsMode.mode === 'single') {
78
- const settings = await readDefaultExport(settingsMode.file);
79
- const apps = settings?.apps;
80
-
81
- if (!Array.isArray(apps)) {
82
- throw new Error(`settings.js must export { apps: [] }. File: ${settingsMode.file}`);
83
- }
84
-
85
- return apps.map(normalizeApp).filter(Boolean);
86
- }
87
-
88
- const apps = await readDefaultExport(settingsMode.file);
89
- if (!Array.isArray(apps)) {
90
- throw new Error(`settings/apps.js must export an array. File: ${settingsMode.file}`);
91
- }
92
-
93
- return apps.map(normalizeApp).filter(Boolean);
94
- }
95
-
96
- async function updateSingleSettingsApps(settingsFile, apps) {
97
- const current = await fs.readFile(settingsFile, 'utf8');
98
-
99
- if (!current.includes(APPS_START) || !current.includes(APPS_END)) {
100
- throw new Error(`settings.js is missing ${APPS_START}/${APPS_END} markers: ${settingsFile}`);
101
- }
102
-
103
- const replacement = `${APPS_START}\n${renderAppEntries(apps)}\n ${APPS_END}`;
104
- const updated = current.replace(/\/\/ AEGIS_APPS_START[\s\S]*?\/\/ AEGIS_APPS_END/m, replacement);
105
-
106
- await writeFile(settingsFile, updated);
107
- }
108
-
109
- async function updateAppRegistry(projectRoot, apps, settingsMode) {
110
- if (settingsMode.mode === 'single') {
111
- await updateSingleSettingsApps(settingsMode.file, apps);
112
- return;
113
- }
114
-
115
- await writeFile(path.join(projectRoot, 'settings', 'apps.js'), renderSettingsApps(apps));
116
- }
117
-
118
- async function updateProjectRoutesFile(projectRoot, appName, mountPath) {
119
- const routesFile = path.join(projectRoot, 'routes.js');
120
- if (!(await exists(routesFile))) {
121
- // Keep backward compatibility for legacy projects that still use routes/index.js.
122
- return;
123
- }
124
-
125
- const importName = toImportName(appName);
126
- const importLine = `import ${importName} from './apps/${appName}/routes.js';`;
127
- const routeLine = ` route.use(${JSON.stringify(mountPath)}, ${importName});`;
128
-
129
- let content = await fs.readFile(routesFile, 'utf8');
130
-
131
- if (!content.includes(importLine)) {
132
- if (content.includes('// AEGIS_APP_IMPORTS_START') && content.includes('// AEGIS_APP_IMPORTS_END')) {
133
- content = content.replace('// AEGIS_APP_IMPORTS_END', `${importLine}\n// AEGIS_APP_IMPORTS_END`);
134
- } else {
135
- content = `${importLine}\n${content}`;
136
- }
137
- }
138
-
139
- if (!content.includes(routeLine)) {
140
- if (content.includes('// AEGIS_PROJECT_APP_ROUTES_START') && content.includes('// AEGIS_PROJECT_APP_ROUTES_END')) {
141
- content = content.replace(' // AEGIS_PROJECT_APP_ROUTES_END', `${routeLine}\n // AEGIS_PROJECT_APP_ROUTES_END`);
142
- } else {
143
- const match = content.match(/register\s*\([^)]*\)\s*{[\s\S]*?\n\s*}/m);
144
- if (match) {
145
- content = content.replace(match[0], `${match[0]}\n${routeLine}`);
146
- }
147
- }
148
- }
149
-
150
- await writeFile(routesFile, content);
151
- }
152
-
153
- async function createScaffold(projectRoot, appName) {
154
- const appRoot = path.join(projectRoot, 'apps', appName);
155
-
156
- await ensureDir(appRoot);
157
- await ensureDir(path.join(appRoot, 'tests'));
158
-
159
- await writeFile(path.join(appRoot, 'views.js'), renderAppViewsFile(appName));
160
- await writeFile(path.join(appRoot, 'models.js'), renderAppModelsFile(appName));
161
- await writeFile(path.join(appRoot, 'validators.js'), renderAppValidatorsFile(appName));
162
- await writeFile(path.join(appRoot, 'services.js'), renderAppServicesFile(appName));
163
- await writeFile(path.join(appRoot, 'subscribers.js'), renderAppSubscribersFile(appName));
164
- await writeFile(path.join(appRoot, 'routes.js'), renderAppRoutes(appName));
165
- await writeFile(path.join(appRoot, 'tests', 'models.test.js'), renderAppModelTest(appName));
166
- await writeFile(path.join(appRoot, 'tests', 'validators.test.js'), renderAppValidatorTest(appName));
167
- await writeFile(path.join(appRoot, 'tests', 'services.test.js'), renderAppServiceTest(appName));
168
- await writeFile(path.join(appRoot, 'tests', 'routes.test.js'), renderAppRoutesTest(appName));
169
- }
4
+ detectSettingsMode,
5
+ ensureAppScaffold,
6
+ readAppsConfig,
7
+ updateAppRegistry,
8
+ updateProjectRoutesFile,
9
+ } from '../utils/apps.js';
170
10
 
171
11
  export async function createApp({ appName, projectRoot, mount }) {
172
12
  ensureValidName(appName, 'app');
173
13
 
174
14
  const resolvedRoot = await resolveProjectRoot(projectRoot);
175
-
176
15
  const settingsMode = await detectSettingsMode(resolvedRoot);
177
16
  const normalizedMount = normalizeMountPath(mount || `/${appName}`);
178
17
  const existingApps = await readAppsConfig(settingsMode);
179
18
 
180
19
  if (existingApps.some((entry) => entry.name === appName)) {
181
- throw new Error(`App \"${appName}\" already exists in project settings`);
20
+ throw new Error(`App "${appName}" already exists in project settings`);
182
21
  }
183
22
 
184
- const updatedApps = [...existingApps, { name: appName, mount: normalizedMount }];
185
-
186
- await createScaffold(resolvedRoot, appName);
187
- await updateAppRegistry(resolvedRoot, updatedApps, settingsMode);
23
+ await ensureAppScaffold(resolvedRoot, appName);
24
+ await updateAppRegistry(resolvedRoot, [...existingApps, { name: appName, mount: normalizedMount }], settingsMode);
188
25
  await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount);
189
26
 
190
- console.log(`App \"${appName}\" created at ${path.join(resolvedRoot, 'apps', appName)}`);
27
+ console.log(`App "${appName}" created at ${resolvedRoot}/apps/${appName}`);
191
28
  }
@@ -1,7 +1,9 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { loadProjectConfig } from '../../runtime/config.js';
4
+ import { ensureValidName } from '../utils/fs.js';
4
5
  import { resolveProjectRoot } from '../utils/project.js';
6
+ import { getAppScaffoldEntries, toImportName } from '../utils/apps.js';
5
7
 
6
8
  function createCollector() {
7
9
  const entries = [];
@@ -26,25 +28,47 @@ async function fileExists(filePath) {
26
28
  }
27
29
  }
28
30
 
29
- async function runAppChecks(rootDir, config, collector) {
31
+ function escapeRegExp(value) {
32
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33
+ }
34
+
35
+ async function runAppChecks(rootDir, config, collector, targetAppName = null) {
30
36
  const apps = Array.isArray(config.apps) ? config.apps : [];
37
+ const declaredApps = new Map();
31
38
 
32
- if (apps.length === 0) {
39
+ for (const app of apps) {
40
+ const appName = app?.name;
41
+ if (typeof appName === 'string' && appName.trim().length > 0) {
42
+ declaredApps.set(appName, app);
43
+ }
44
+ }
45
+
46
+ if (!targetAppName && apps.length === 0) {
33
47
  collector.warn('No apps declared in settings.apps.');
34
48
  return;
35
49
  }
36
50
 
37
- collector.ok(`Declared apps: ${apps.map((app) => app.name).join(', ')}`);
51
+ if (!targetAppName) {
52
+ collector.ok(`Declared apps: ${apps.map((app) => app.name).join(', ')}`);
53
+ }
38
54
 
39
- for (const app of apps) {
40
- const appName = app?.name;
55
+ const routesFile = path.join(rootDir, 'routes.js');
56
+ const routesFileExists = await fileExists(routesFile);
57
+ const routesContent = routesFileExists ? await fs.readFile(routesFile, 'utf8') : '';
58
+ const targetApps = targetAppName ? [targetAppName] : apps;
59
+
60
+ for (const appEntry of targetApps) {
61
+ const appName = targetAppName || appEntry?.name;
62
+ const app = targetAppName ? declaredApps.get(appName) || null : appEntry;
41
63
  const mount = app?.mount;
42
64
  if (typeof appName !== 'string' || appName.trim().length === 0) {
43
- collector.error(`Invalid app entry: ${JSON.stringify(app)}`);
65
+ collector.error(`Invalid app entry: ${JSON.stringify(appEntry)}`);
44
66
  continue;
45
67
  }
46
68
 
47
- if (typeof mount !== 'string' || !mount.startsWith('/')) {
69
+ if (!app) {
70
+ collector.warn(`App "${appName}" is not declared in settings.apps.`);
71
+ } else if (typeof mount !== 'string' || !mount.startsWith('/')) {
48
72
  collector.error(`App "${appName}" has invalid mount "${String(mount)}" (must start with /).`);
49
73
  }
50
74
 
@@ -55,21 +79,65 @@ async function runAppChecks(rootDir, config, collector) {
55
79
  continue;
56
80
  }
57
81
 
58
- const requiredFiles = ['routes.js', 'views.js', 'services.js', 'models.js', 'validators.js'];
59
- for (const fileName of requiredFiles) {
60
- const target = path.join(appRoot, fileName);
82
+ for (const entry of getAppScaffoldEntries(appName)) {
83
+ const target = path.join(rootDir, entry.target);
61
84
  if (!(await fileExists(target))) {
62
- collector.warn(`App "${appName}" missing ${fileName}.`);
85
+ collector.warn(`App "${appName}" missing ${path.relative(appRoot, target)}.`);
63
86
  }
64
87
  }
65
88
 
66
- const subscribersFile = path.join(appRoot, 'subscribers.js');
67
- if (!(await fileExists(subscribersFile))) {
68
- collector.warn(`App "${appName}" missing subscribers.js.`);
89
+ if (!app) {
90
+ continue;
91
+ }
92
+
93
+ if (config.autoMountApps === true) {
94
+ collector.ok(`App "${appName}" will be mounted automatically from settings.apps.`);
95
+ continue;
96
+ }
97
+
98
+ if (!routesFileExists) {
99
+ collector.warn(`Project routes.js is missing; app "${appName}" cannot be mounted centrally.`);
100
+ continue;
101
+ }
102
+
103
+ const importPath = `./apps/${appName}/routes.js`;
104
+ const importName = toImportName(appName);
105
+ const routePattern = new RegExp(`route\\.use\\([^\\n]*,\\s*${escapeRegExp(importName)}\\s*\\);`);
106
+
107
+ if (!routesContent.includes(importPath)) {
108
+ collector.warn(`Project routes.js is missing import for app "${appName}".`);
109
+ }
110
+
111
+ if (!routePattern.test(routesContent)) {
112
+ collector.warn(`Project routes.js is missing route.use(...) mount for app "${appName}".`);
69
113
  }
70
114
  }
71
115
  }
72
116
 
117
+ async function runStartupEntryChecks(rootDir, config, collector) {
118
+ const env = String(config.env || process.env.NODE_ENV || 'development').trim().toLowerCase();
119
+ const appEntryPath = path.join(rootDir, 'app.js');
120
+ const loaderEntryPath = path.join(rootDir, 'loader.cjs');
121
+ const appEntryExists = await fileExists(appEntryPath);
122
+ const loaderEntryExists = await fileExists(loaderEntryPath);
123
+
124
+ if (appEntryExists) {
125
+ collector.ok('app.js exists.');
126
+ } else if (env === 'production') {
127
+ collector.error('app.js is missing for production startup. Run "aegisnode generateloader" to restore startup entry files.');
128
+ } else {
129
+ collector.warn('app.js is missing. Run "aegisnode generateloader" to restore startup entry files.');
130
+ }
131
+
132
+ if (loaderEntryExists) {
133
+ collector.ok('loader.cjs exists.');
134
+ } else if (env === 'production') {
135
+ collector.error('loader.cjs is missing for production startup. Run "aegisnode generateloader" to restore it.');
136
+ } else {
137
+ collector.warn('loader.cjs is missing. Generate it with "aegisnode generateloader" before non-development startup.');
138
+ }
139
+ }
140
+
73
141
  function runSecurityChecks(config, collector) {
74
142
  const env = String(config.env || process.env.NODE_ENV || 'development');
75
143
  const security = config.security || {};
@@ -169,7 +237,12 @@ export async function runDoctor({
169
237
  projectRoot,
170
238
  failOnError = true,
171
239
  output = console,
240
+ appName = null,
172
241
  } = {}) {
242
+ if (appName) {
243
+ ensureValidName(appName, 'app');
244
+ }
245
+
173
246
  const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
174
247
  const collector = createCollector();
175
248
 
@@ -178,11 +251,17 @@ export async function runDoctor({
178
251
  const config = await loadProjectConfig(resolvedRoot);
179
252
  collector.ok(`Environment: ${config.env || 'development'}`);
180
253
 
181
- await runAppChecks(resolvedRoot, config, collector);
182
- runSecurityChecks(config, collector);
183
- runAuthChecks(config, collector);
184
- runApiChecks(config, collector);
185
- await runTemplateChecks(resolvedRoot, config, collector);
254
+ if (appName) {
255
+ collector.ok(`Doctor scope: app "${appName}"`);
256
+ await runAppChecks(resolvedRoot, config, collector, appName);
257
+ } else {
258
+ await runAppChecks(resolvedRoot, config, collector);
259
+ await runStartupEntryChecks(resolvedRoot, config, collector);
260
+ runSecurityChecks(config, collector);
261
+ runAuthChecks(config, collector);
262
+ runApiChecks(config, collector);
263
+ await runTemplateChecks(resolvedRoot, config, collector);
264
+ }
186
265
 
187
266
  const summary = printSummary(collector.entries, output);
188
267