aegisnode 0.0.4 → 0.1.0

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/README.md CHANGED
@@ -43,6 +43,7 @@ It keeps the development model simple, but adds enough structure and tooling to
43
43
  Core features:
44
44
 
45
45
  - CLI generators (`startproject`, `createapp`, `runserver`)
46
+ - App scaffold repair command (`fix`)
46
47
  - Startup entry generator (`generateloader`)
47
48
  - Project health checker (`doctor`)
48
49
  - Dependency updater (`updatedeps`)
@@ -61,15 +62,16 @@ Core features:
61
62
  - Root route file `routes.js` (not `routes/` folder)
62
63
  - Automatic default confirmation page on `/` when no custom `/` route exists
63
64
  - App folder uses `views.js` (not `controllers/` folder)
64
- - `createapp` uses file modules: `views.js`, `models.js`, `validators.js`, `routes.js`, `subscribers.js`, `services.js`
65
+ - `createapp` uses file modules: `views.js`, `models.js`, `validators.js`, `routes.js`, `subscribers.js`, `services.js`, `utils.js`
65
66
  - `createapp` also generates app tests in `apps/<app>/tests`
66
67
  - EJS templates configurable in `settings.js` with Django-style base layout flow
67
68
  - Built-in runtime helpers (`money`, `number`, `dateTime`, `timeElapsed`, `toObjectId`) + `jlive` bridge
68
69
 
69
70
  `startproject` creates `app.js`, `loader.cjs`, `.env`, `settings.js`, and `routes.js` without creating any default app.
70
- It does not create `public/` or `logs/`; create your own folders and set them in `settings.js`.
71
+ Use `startproject --typescript` to generate `app.ts`, `settings.ts`, `routes.ts`, app `*.ts` files, and `tsconfig.json` instead.
72
+ It does not create `public/` or `logs/`; create your own folders and set them in `settings.js` or `settings.ts`.
71
73
 
72
- Environment files are loaded automatically before `settings.js` is imported.
74
+ Environment files are loaded automatically before `settings.js` or `settings.ts` is imported.
73
75
  Supported files:
74
76
  - `.env`
75
77
  - `.env.local`
@@ -86,20 +88,36 @@ Shell or hosting-panel environment variables win over values from `.env` files.
86
88
  npm install -g aegisnode
87
89
 
88
90
  aegisnode startproject blog
91
+ aegisnode startproject blog-ts --typescript
89
92
  npm --prefix blog install
90
93
  aegisnode runserver --project blog
91
94
 
92
95
  aegisnode createapp users --project blog
96
+ aegisnode fix --app users --project blog
93
97
  aegisnode generate view profile --app users --project blog
94
98
  aegisnode generate route profile --app users --project blog
95
99
  aegisnode generateloader --project blog
96
100
  aegisnode doctor --project blog
101
+ aegisnode doctor --app users --project blog
97
102
  aegisnode updatedeps --project blog
98
103
  ```
99
104
 
100
105
  `cd blog` is optional. You can run commands from parent folder with `--project blog`.
101
106
 
102
- `createapp`, `generate`, `runserver`, `generateloader`, `doctor`, and `updatedeps` are project-level commands.
107
+ Use `--typescript` on `startproject` when you want a TypeScript scaffold. That generates `app.ts`, `settings.ts`, `routes.ts`, `tsconfig.json`, app files like `views.ts`/`services.ts`, and generated artifacts such as `profile.view.ts`.
108
+
109
+ ### JavaScript vs TypeScript Projects
110
+
111
+ The project type is chosen once at `startproject` time:
112
+ - `aegisnode startproject blog` creates a JavaScript project
113
+ - `aegisnode startproject blog --typescript` creates a TypeScript project
114
+
115
+ After that, the rest of the CLI follows the project automatically:
116
+ - `createapp` generates `views.js` / `services.js` / `routes.js` in JavaScript projects, or `views.ts` / `services.ts` / `routes.ts` in TypeScript projects
117
+ - `generate` creates artifacts with the same extension as the project, for example `profile.view.js` or `profile.view.ts`
118
+ - `fix`, `doctor`, and `generateloader` also check and repair the matching project file type automatically
119
+
120
+ `createapp`, `fix`, `generate`, `runserver`, `generateloader`, `doctor`, and `updatedeps` are project-level commands.
103
121
  Run them from the project root; do not `cd` into `apps/<app>`.
104
122
  Startup mode rules:
105
123
  - Development (`env === development`): start with `aegisnode runserver` only.
@@ -107,6 +125,20 @@ Startup mode rules:
107
125
  - `node app.js` and `node loader.cjs` are rejected in development mode.
108
126
  - `aegisnode runserver` is rejected outside development mode.
109
127
 
128
+ ### Trust Proxy
129
+
130
+ If your app runs behind Nginx, Apache, Passenger, or another reverse proxy that terminates HTTPS before the Node process, set top-level `trustProxy` in `settings.js`:
131
+
132
+ ```js
133
+ export default {
134
+ trustProxy: 1,
135
+ };
136
+ ```
137
+
138
+ This is the AegisNode equivalent of `app.set('trust proxy', 1)` in raw Express. It makes `req.secure`, `req.protocol`, client IP detection, secure cookies, and HTTPS-aware auth logic behave correctly behind the proxy.
139
+
140
+ Prefer an exact value such as `1`, `'loopback'`, or a subnet string instead of `true`.
141
+
110
142
  ### Deploy On Phusion Passenger
111
143
 
112
144
  AegisNode supports Passenger-style startup using the generated `loader.cjs`.
@@ -125,8 +157,8 @@ HTTPS note:
125
157
  - Only enable `https` in `settings.js` when Node itself should serve TLS directly.
126
158
 
127
159
  How it works:
128
- - `loader.cjs` imports `app.js`.
129
- - `app.js` starts AegisNode with project root resolved from its own file location, so it works correctly under Passenger.
160
+ - `loader.cjs` imports `app.js` in JavaScript projects or `app.ts` in TypeScript projects.
161
+ - `app.js` / `app.ts` starts AegisNode with project root resolved from its own file location, so it works correctly under Passenger.
130
162
 
131
163
 
132
164
  Generated routes are auto-wired into `apps/<app>/routes.js`.
@@ -145,10 +177,8 @@ By default, new app routes are API-ready:
145
177
 
146
178
  Default flow is `route -> validator -> service -> model`.
147
179
  Default app tests generated by `createapp`:
148
- - `apps/<app>/tests/models.test.js`
149
- - `apps/<app>/tests/validators.test.js`
150
- - `apps/<app>/tests/services.test.js`
151
- - `apps/<app>/tests/routes.test.js`
180
+ - JavaScript projects: `apps/<app>/tests/models.test.js`, `validators.test.js`, `services.test.js`, `routes.test.js`
181
+ - TypeScript projects: `apps/<app>/tests/models.test.ts`, `validators.test.ts`, `services.test.ts`, `routes.test.ts`
152
182
 
153
183
  Run all project tests:
154
184
 
@@ -170,6 +200,26 @@ aegisnode doctor
170
200
  - Auth safety checks (JWT secret, OAuth2 `allowHttp` in production)
171
201
  - Template directory availability
172
202
 
203
+ Run app-level scaffold checks for one app:
204
+
205
+ ```bash
206
+ aegisnode doctor --app users
207
+ ```
208
+
209
+ App-level doctor focuses on the named app:
210
+ - Missing `views.js`, `models.js`, `services.js`, `validators.js`, `routes.js`, `subscribers.js`, `utils.js`
211
+ - Missing generated test files under `apps/<app>/tests`
212
+ - Missing `settings.apps` declaration for that app
213
+ - Missing central `routes.js` import/mount when `autoMountApps` is off
214
+
215
+ Repair a partially missing app scaffold:
216
+
217
+ ```bash
218
+ aegisnode fix --app users
219
+ ```
220
+
221
+ `fix` recreates missing default app files and tests without overwriting existing files. If the app is missing from `settings.apps` or central `routes.js`, it restores those registrations too.
222
+
173
223
  Regenerate project startup entry files if needed:
174
224
 
175
225
  ```bash
@@ -241,7 +291,7 @@ export default {
241
291
 
242
292
  Injected app layers also receive `env`, so views/services/models/validators/controllers/subscribers/loaders can use `env.MY_NAME` without importing `process.env`.
243
293
 
244
- `settings.js` (generated shape):
294
+ `settings.js` (generated shape, or `settings.ts` in TypeScript mode):
245
295
 
246
296
  ```js
247
297
  export default {
@@ -288,16 +338,26 @@ Each generated app usually contains:
288
338
  - `apps/<app>/views.js`
289
339
  - `apps/<app>/models.js`
290
340
  - `apps/<app>/services.js`
341
+ - `apps/<app>/utils.js`
291
342
  - `apps/<app>/subscribers.js`
292
343
  - `apps/<app>/routes.js`
293
344
 
345
+ If the project was created with `--typescript`, the same generated files use `.ts` instead of `.js`.
346
+
294
347
  Usage by file:
295
348
  - `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
349
+ Keep `views.js` thin: prefer only the view class and its imports. Avoid defining extra local helper/utility functions in the view file. Move reusable pure logic to `utils.js` and app workflows to `services.js`.
296
350
  - `models.js`: data access layer only (SQL/NoSQL operations).
297
- - `services.js`: business logic layer; orchestrates models.
351
+ - `services.js`: business logic layer; orchestrates models and uses injected runtime objects when needed.
352
+ - `utils.js`: app-local pure utility functions. Use this for small reusable helpers that belong only to the app. Do not put DB access, request validation, or business workflows here.
353
+ `utils.js` is a plain module, not an injected runtime layer. If a utility needs `jlive`, `helpers`, `i18n`, or another injected runtime object, inject that object into a view/service/model first and pass it into the utility function as an argument.
298
354
  - `subscribers.js`: event listeners (for example `app.booted`, `ws.connection`, custom events).
299
355
  - `routes.js`: route mapping only (`route.get(...)`, `route.post(...)`, `route.use(...)`) to view handlers.
300
356
 
357
+ Short rule for `utils.js` vs `services.js`:
358
+ - Use `utils.js` for pure app-local helpers such as string formatting, slug generation, payload shaping, or small mappers.
359
+ - Use `services.js` for application behavior: anything that coordinates models, injected runtime objects, or feature rules.
360
+
301
361
  Route modules are mapping-only (`register(route)`).
302
362
  Framework context is injected into handlers as first argument (when handler uses 4 args): `{ service, validator, services, models, validators, auth, mail, helpers, i18n, events, ... }`.
303
363
  `req.aegis` is also available.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegisnode",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
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",
@@ -55,6 +55,7 @@
55
55
  "nodemailer": "^8.0.2",
56
56
  "querymesh": "^0.0.7",
57
57
  "socket.io": "^4.8.1",
58
- "swagger-ui-express": "^5.0.1"
58
+ "swagger-ui-express": "^5.0.1",
59
+ "tsx": "^4.21.0"
59
60
  }
60
61
  }
@@ -7,6 +7,7 @@ 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';
@@ -16,6 +17,7 @@ import { createAuthManager, normalizeAuthConfig } from '../src/runtime/auth.js';
16
17
  import { loadProjectConfig } from '../src/runtime/config.js';
17
18
  import { initializeDatabase, closeDatabase } from '../src/runtime/database.js';
18
19
  import { runDoctor } from '../src/cli/commands/doctor.js';
20
+ import { runFixApp } from '../src/cli/commands/fixapp.js';
19
21
  import { runUpdateDependencies } from '../src/cli/commands/updatedeps.js';
20
22
  import { createHelpers } from '../src/runtime/helpers.js';
21
23
 
@@ -134,6 +136,7 @@ async function main() {
134
136
  const dotenvSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-dotenv-'));
135
137
  const httpsSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-https-'));
136
138
  const proxySandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-proxy-'));
139
+ const typescriptSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-ts-'));
137
140
 
138
141
  await startProject({ projectName, cwd: sandboxRoot });
139
142
  const generatedProjectEnv = await fs.readFile(path.join(projectRoot, '.env'), 'utf8');
@@ -153,6 +156,66 @@ async function main() {
153
156
  /started with "aegisnode runserver"/,
154
157
  );
155
158
 
159
+ const tsProjectName = 'forumts';
160
+ const tsProjectRoot = path.join(typescriptSandboxRoot, tsProjectName);
161
+ await startProject({ projectName: tsProjectName, cwd: typescriptSandboxRoot, typescript: true });
162
+ await fs.access(path.join(tsProjectRoot, 'app.ts'));
163
+ await fs.access(path.join(tsProjectRoot, 'settings.ts'));
164
+ await fs.access(path.join(tsProjectRoot, 'routes.ts'));
165
+ await fs.access(path.join(tsProjectRoot, 'tsconfig.json'));
166
+ const tsPackageJson = JSON.parse(await fs.readFile(path.join(tsProjectRoot, 'package.json'), 'utf8'));
167
+ assert.equal(tsPackageJson.scripts.test, 'node --import tsx/esm --test');
168
+ assert.equal(tsPackageJson.scripts.typecheck, 'tsc --noEmit');
169
+ assert.equal(tsPackageJson.devDependencies.tsx, '^4.21.0');
170
+ assert.equal(tsPackageJson.devDependencies.typescript, '^5.9.3');
171
+ const tsConfig = await loadProjectConfig(tsProjectRoot);
172
+ assert.equal(tsConfig.appName, 'forumts');
173
+ await createApp({
174
+ appName: 'users',
175
+ projectRoot: tsProjectRoot,
176
+ mount: '/users',
177
+ });
178
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'routes.ts'));
179
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'views.ts'));
180
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'models.ts'));
181
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'validators.ts'));
182
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'services.ts'));
183
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'utils.ts'));
184
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'subscribers.ts'));
185
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'models.test.ts'));
186
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'validators.test.ts'));
187
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'services.test.ts'));
188
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'routes.test.ts'));
189
+ await generateArtifact({
190
+ type: 'view',
191
+ name: 'profile',
192
+ appName: 'users',
193
+ projectRoot: tsProjectRoot,
194
+ });
195
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'profile.view.ts'));
196
+ await generateArtifact({
197
+ type: 'route',
198
+ name: 'profile',
199
+ appName: 'users',
200
+ projectRoot: tsProjectRoot,
201
+ });
202
+ const tsUsersRoutesFile = await fs.readFile(path.join(tsProjectRoot, 'apps', 'users', 'routes.ts'), 'utf8');
203
+ assert.match(tsUsersRoutesFile, /import ProfileView from '\.\/profile\.view\.ts';/);
204
+ assert.match(tsUsersRoutesFile, /route\.get\('\/profile', ProfileView\.index\);/);
205
+ const tsDoctorReport = await runDoctor({
206
+ projectRoot: tsProjectRoot,
207
+ failOnError: true,
208
+ output: {
209
+ log() {},
210
+ },
211
+ });
212
+ assert.equal(tsDoctorReport.summary.errors, 0);
213
+ const tsServer = await runServer({
214
+ projectRoot: tsProjectRoot,
215
+ port: 0,
216
+ });
217
+ await tsServer.stop();
218
+
156
219
  const envProjectName = 'envdemo';
157
220
  const envProjectRoot = path.join(envSandboxRoot, envProjectName);
158
221
  await startProject({ projectName: envProjectName, cwd: envSandboxRoot });
@@ -590,6 +653,7 @@ dkcqnJD4SGWVeG+KhA==
590
653
  await fs.access(path.join(projectRoot, 'apps', 'users', 'models.js'));
591
654
  await fs.access(path.join(projectRoot, 'apps', 'users', 'validators.js'));
592
655
  await fs.access(path.join(projectRoot, 'apps', 'users', 'services.js'));
656
+ await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
593
657
  await fs.access(path.join(projectRoot, 'apps', 'users', 'subscribers.js'));
594
658
  await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'models.test.js'));
595
659
  await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'validators.test.js'));
@@ -605,6 +669,90 @@ dkcqnJD4SGWVeG+KhA==
605
669
  assert.equal(doctorReport.summary.errors, 0);
606
670
  const projectRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
607
671
  assert.match(projectRoutesFile, /route\.use\((['"])\/users\1, users\);/);
672
+
673
+ await fs.unlink(path.join(projectRoot, 'apps', 'users', 'utils.js'));
674
+ await fs.unlink(path.join(projectRoot, 'apps', 'users', 'tests', 'routes.test.js'));
675
+ await fs.writeFile(
676
+ path.join(projectRoot, 'routes.js'),
677
+ projectRoutesFile
678
+ .replace(/import users from '\.\/apps\/users\/routes\.js';\n/, '')
679
+ .replace(/\s*route\.use\((['"])\/users\1, users\);\n/, '\n'),
680
+ 'utf8',
681
+ );
682
+ const appDoctorRouteReport = await runDoctor({
683
+ projectRoot,
684
+ appName: 'users',
685
+ failOnError: false,
686
+ output: {
687
+ log() {},
688
+ },
689
+ });
690
+ assert.equal(
691
+ appDoctorRouteReport.entries.some((entry) => entry.message.includes('Project routes.js is missing import for app "users".')),
692
+ true,
693
+ );
694
+ assert.equal(
695
+ appDoctorRouteReport.entries.some((entry) => entry.message.includes('Project routes.js is missing route.use(...) mount for app "users".')),
696
+ true,
697
+ );
698
+
699
+ const settingsBeforeFix = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
700
+ await fs.writeFile(
701
+ path.join(projectRoot, 'settings.js'),
702
+ settingsBeforeFix.replace(/\n\s*\{ name: "users", mount: "\/users" \},/, ''),
703
+ 'utf8',
704
+ );
705
+
706
+ const appDoctorReport = await runDoctor({
707
+ projectRoot,
708
+ appName: 'users',
709
+ failOnError: false,
710
+ output: {
711
+ log() {},
712
+ },
713
+ });
714
+ assert.equal(appDoctorReport.summary.errors, 0);
715
+ assert.equal(
716
+ appDoctorReport.entries.some((entry) => entry.message.includes('App "users" is not declared in settings.apps.')),
717
+ true,
718
+ );
719
+ assert.equal(
720
+ appDoctorReport.entries.some((entry) => entry.message.includes('App "users" missing utils.js.')),
721
+ true,
722
+ );
723
+ assert.equal(
724
+ appDoctorReport.entries.some((entry) => entry.message.includes('App "users" missing tests/routes.test.js.')),
725
+ true,
726
+ );
727
+
728
+ const fixAppResult = await runFixApp({
729
+ appName: 'users',
730
+ projectRoot,
731
+ });
732
+ assert.equal(fixAppResult.registryUpdated, true);
733
+ assert.equal(fixAppResult.routesUpdated, true);
734
+ await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
735
+ await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'routes.test.js'));
736
+ const fixedSettingsFile = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
737
+ assert.match(fixedSettingsFile, /\{ name: "users", mount: "\/users" \},/);
738
+ const fixedRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
739
+ assert.match(fixedRoutesFile, /import users from '\.\/apps\/users\/routes\.js';/);
740
+ assert.match(fixedRoutesFile, /route\.use\((['"])\/users\1, users\);/);
741
+ await assert.doesNotReject(
742
+ () => runCli(['doctor', '--app', 'users', '--project', projectRoot]),
743
+ );
744
+ await assert.rejects(
745
+ () => runCli(['doctor', 'users', '--project', projectRoot]),
746
+ /Doctor app target must use --app <app-name>/,
747
+ );
748
+ await assert.doesNotReject(
749
+ () => runCli(['fix', '--app', 'users', '--project', projectRoot]),
750
+ );
751
+ await assert.rejects(
752
+ () => runCli(['fix', 'users', '--project', projectRoot]),
753
+ /Fix app target must use --app <app-name>/,
754
+ );
755
+
608
756
  await generateArtifact({
609
757
  type: 'view',
610
758
  name: 'profile',
@@ -1,191 +1,29 @@
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';
5
- import { resolveProjectRoot } from '../utils/project.js';
1
+ import { ensureValidName, normalizeMountPath } from '../utils/fs.js';
2
+ import { getProjectSourceExtension, 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
-
15
+ const sourceExtension = getProjectSourceExtension(resolvedRoot);
176
16
  const settingsMode = await detectSettingsMode(resolvedRoot);
177
17
  const normalizedMount = normalizeMountPath(mount || `/${appName}`);
178
18
  const existingApps = await readAppsConfig(settingsMode);
179
19
 
180
20
  if (existingApps.some((entry) => entry.name === appName)) {
181
- throw new Error(`App \"${appName}\" already exists in project settings`);
21
+ throw new Error(`App "${appName}" already exists in project settings`);
182
22
  }
183
23
 
184
- const updatedApps = [...existingApps, { name: appName, mount: normalizedMount }];
185
-
186
- await createScaffold(resolvedRoot, appName);
187
- await updateAppRegistry(resolvedRoot, updatedApps, settingsMode);
188
- await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount);
24
+ await ensureAppScaffold(resolvedRoot, appName, { sourceExtension });
25
+ await updateAppRegistry(resolvedRoot, [...existingApps, { name: appName, mount: normalizedMount }], settingsMode);
26
+ await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount, sourceExtension);
189
27
 
190
- console.log(`App \"${appName}\" created at ${path.join(resolvedRoot, 'apps', appName)}`);
28
+ console.log(`App "${appName}" created at ${resolvedRoot}/apps/${appName}`);
191
29
  }