create-forgeon 0.3.15 → 0.3.16

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 (41) hide show
  1. package/package.json +4 -2
  2. package/src/core/docs.test.mjs +79 -40
  3. package/src/core/scaffold.test.mjs +99 -0
  4. package/src/modules/db-prisma.mjs +23 -55
  5. package/src/modules/executor.test.mjs +132 -36
  6. package/src/modules/files-access.mjs +27 -98
  7. package/src/modules/files-image.mjs +26 -100
  8. package/src/modules/files-quotas.mjs +67 -87
  9. package/src/modules/files.mjs +35 -104
  10. package/src/modules/i18n.mjs +17 -121
  11. package/src/modules/idempotency.test.mjs +174 -0
  12. package/src/modules/jwt-auth.mjs +90 -209
  13. package/src/modules/logger.mjs +0 -9
  14. package/src/modules/probes.test.mjs +202 -0
  15. package/src/modules/queue.mjs +325 -443
  16. package/src/modules/rate-limit.mjs +22 -66
  17. package/src/modules/rbac.mjs +27 -67
  18. package/src/modules/scheduler.mjs +44 -167
  19. package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
  20. package/src/modules/shared/probes.mjs +235 -0
  21. package/src/modules/sync-integrations.mjs +54 -21
  22. package/src/modules/sync-integrations.test.mjs +220 -0
  23. package/src/run-add-module.test.mjs +153 -0
  24. package/templates/base/README.md +7 -55
  25. package/templates/base/apps/web/src/App.tsx +70 -42
  26. package/templates/base/apps/web/src/probes.ts +61 -0
  27. package/templates/base/apps/web/src/styles.css +86 -25
  28. package/templates/base/package.json +21 -15
  29. package/templates/base/scripts/forgeon-sync-integrations.mjs +55 -11
  30. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
  31. package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
  32. package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
  33. package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
  34. package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
  35. package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
  36. package/templates/base/docs/AI/PROJECT.md +0 -43
  37. package/templates/base/docs/AI/ROADMAP.md +0 -171
  38. package/templates/base/docs/AI/TASKS.md +0 -60
  39. package/templates/base/docs/AI/VALIDATION.md +0 -31
  40. package/templates/base/docs/README.md +0 -18
  41. package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
7
7
  "type": "module",
8
8
  "scripts": {
9
- "test": "node --test src/cli/options.test.mjs src/cli/add-options.test.mjs src/cli/prompt-select.test.mjs src/core/docs.test.mjs src/core/validate.test.mjs src/modules/executor.test.mjs"
9
+ "test": "node --test --test-concurrency=1 src/cli/options.test.mjs src/cli/add-options.test.mjs src/cli/prompt-select.test.mjs src/core/docs.test.mjs src/core/validate.test.mjs src/core/scaffold.test.mjs src/modules/dependencies.test.mjs src/modules/executor.test.mjs src/modules/probes.test.mjs src/modules/idempotency.test.mjs src/modules/sync-integrations.test.mjs src/run-add-module.test.mjs",
10
+ "smoke:generated-project": "node scripts/generated-project-smoke.mjs",
11
+ "smoke:generated-project:full": "node scripts/generated-project-full-smoke.mjs"
10
12
  },
11
13
  "bin": {
12
14
  "create-forgeon": "bin/create-forgeon.mjs"
@@ -1,29 +1,30 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { generateDocs } from './docs.mjs';
8
-
9
- function makeTempDir(prefix) {
10
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
- }
12
-
13
- function readFile(filePath) {
14
- return fs.readFileSync(filePath, 'utf8');
15
- }
16
-
17
- describe('generateDocs', () => {
18
- const thisDir = path.dirname(fileURLToPath(import.meta.url));
19
- const packageRoot = path.resolve(thisDir, '..', '..');
20
-
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { generateDocs } from './docs.mjs';
8
+ import { scaffoldProject } from './scaffold.mjs';
9
+
10
+ function makeTempDir(prefix) {
11
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
12
+ }
13
+
14
+ function readFile(filePath) {
15
+ return fs.readFileSync(filePath, 'utf8');
16
+ }
17
+
18
+ describe('generateDocs', () => {
19
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
20
+ const packageRoot = path.resolve(thisDir, '..', '..');
21
+
21
22
  it('generates docs for proxy=none without i18n section', () => {
22
- const targetRoot = makeTempDir('forgeon-docs-off-');
23
-
24
- try {
25
- generateDocs(
26
- targetRoot,
23
+ const targetRoot = makeTempDir('forgeon-docs-off-');
24
+
25
+ try {
26
+ generateDocs(
27
+ targetRoot,
27
28
  {
28
29
  frontend: 'react',
29
30
  db: 'prisma',
@@ -31,9 +32,9 @@ describe('generateDocs', () => {
31
32
  dockerEnabled: true,
32
33
  i18nEnabled: false,
33
34
  proxy: 'none',
34
- },
35
- packageRoot,
36
- );
35
+ },
36
+ packageRoot,
37
+ );
37
38
 
38
39
  const readme = readFile(path.join(targetRoot, 'README.md'));
39
40
 
@@ -45,18 +46,20 @@ describe('generateDocs', () => {
45
46
  assert.match(readme, /Module notes index: `modules\/README\.md`/);
46
47
  assert.doesNotMatch(readme, /i18n Configuration/);
47
48
  assert.doesNotMatch(readme, /Prisma In Container Start/);
49
+ assert.doesNotMatch(readme, /docs\/README\.md/);
50
+ assert.doesNotMatch(readme, /docs\/Agents\.md/);
48
51
  assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
49
52
  } finally {
50
53
  fs.rmSync(targetRoot, { recursive: true, force: true });
51
54
  }
52
- });
53
-
54
- it('generates docker and caddy notes when enabled', () => {
55
- const targetRoot = makeTempDir('forgeon-docs-on-');
56
-
57
- try {
58
- generateDocs(
59
- targetRoot,
55
+ });
56
+
57
+ it('generates docker and caddy notes when enabled', () => {
58
+ const targetRoot = makeTempDir('forgeon-docs-on-');
59
+
60
+ try {
61
+ generateDocs(
62
+ targetRoot,
60
63
  {
61
64
  frontend: 'react',
62
65
  db: 'prisma',
@@ -64,9 +67,9 @@ describe('generateDocs', () => {
64
67
  dockerEnabled: true,
65
68
  i18nEnabled: true,
66
69
  proxy: 'caddy',
67
- },
68
- packageRoot,
69
- );
70
+ },
71
+ packageRoot,
72
+ );
70
73
 
71
74
  const readme = readFile(path.join(targetRoot, 'README.md'));
72
75
 
@@ -77,9 +80,45 @@ describe('generateDocs', () => {
77
80
  assert.match(readme, /Prisma In Container Start/);
78
81
  assert.match(readme, /Error Handling \(`core-errors`\)/);
79
82
  assert.match(readme, /Module-specific notes: `modules\/<module-id>\/README\.md`/);
83
+ assert.doesNotMatch(readme, /docs\/README\.md/);
84
+ assert.doesNotMatch(readme, /docs\/Agents\.md/);
80
85
  assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
81
86
  } finally {
82
87
  fs.rmSync(targetRoot, { recursive: true, force: true });
83
88
  }
84
- });
89
+ });
90
+
91
+ it('scaffolds a generated project without copying internal docs payload', () => {
92
+ const tempRoot = makeTempDir('forgeon-scaffold-doc-boundary-');
93
+ const targetRoot = path.join(tempRoot, 'demo-doc-boundary');
94
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
95
+
96
+ try {
97
+ scaffoldProject({
98
+ templateRoot,
99
+ packageRoot,
100
+ targetRoot,
101
+ projectName: 'demo-doc-boundary',
102
+ frontend: 'react',
103
+ db: 'prisma',
104
+ dbPrismaEnabled: true,
105
+ i18nEnabled: true,
106
+ proxy: 'caddy',
107
+ });
108
+
109
+ const readme = readFile(path.join(targetRoot, 'README.md'));
110
+ const packageJson = readFile(path.join(targetRoot, 'package.json'));
111
+
112
+ assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
113
+ assert.match(readme, /Module notes index: `modules\/README\.md`/);
114
+ assert.doesNotMatch(readme, /temporary template placeholder/i);
115
+ assert.doesNotMatch(readme, /built-in docs/i);
116
+ assert.doesNotMatch(readme, /docs\/README\.md/);
117
+ assert.doesNotMatch(readme, /docs\/Agents\.md/);
118
+ assert.doesNotMatch(packageJson, /"create:forgeon"/);
119
+ assert.match(packageJson, /"forgeon:sync-integrations"/);
120
+ } finally {
121
+ fs.rmSync(tempRoot, { recursive: true, force: true });
122
+ }
123
+ });
85
124
  });
@@ -0,0 +1,99 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { scaffoldProject } from './scaffold.mjs';
8
+
9
+ function makeTempDir(prefix) {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
+ }
12
+
13
+ function readFile(filePath) {
14
+ return fs.readFileSync(filePath, 'utf8');
15
+ }
16
+
17
+ function assertProxyPreset(targetRoot, proxy) {
18
+ const dockerDir = path.join(targetRoot, 'infra', 'docker');
19
+ const compose = readFile(path.join(dockerDir, 'compose.yml'));
20
+ const packageJson = readFile(path.join(targetRoot, 'package.json'));
21
+ const appTsx = readFile(path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx'));
22
+ const probesTs = readFile(path.join(targetRoot, 'apps', 'web', 'src', 'probes.ts'));
23
+
24
+ assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
25
+ assert.equal(fs.existsSync(path.join(dockerDir, 'compose.caddy.yml')), false);
26
+ assert.equal(fs.existsSync(path.join(dockerDir, 'compose.nginx.yml')), false);
27
+ assert.equal(fs.existsSync(path.join(dockerDir, 'compose.none.yml')), false);
28
+ assert.match(packageJson, /"forgeon:sync-integrations"/);
29
+ assert.doesNotMatch(packageJson, /"create:forgeon"/);
30
+ assert.match(compose, /^services:\s*$/m);
31
+ assert.match(compose, /^\s{2}api:\s*$/m);
32
+
33
+ if (proxy === 'caddy') {
34
+ assert.match(compose, /^\s{2}caddy:\s*$/m);
35
+ assert.doesNotMatch(compose, /^\s{2}nginx:\s*$/m);
36
+ assert.equal(fs.existsSync(path.join(dockerDir, 'caddy.Dockerfile')), true);
37
+ assert.equal(fs.existsSync(path.join(dockerDir, 'nginx.Dockerfile')), false);
38
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'caddy')), true);
39
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'nginx')), false);
40
+ return;
41
+ }
42
+
43
+ if (proxy === 'nginx') {
44
+ assert.match(compose, /^\s{2}nginx:\s*$/m);
45
+ assert.doesNotMatch(compose, /^\s{2}caddy:\s*$/m);
46
+ assert.equal(fs.existsSync(path.join(dockerDir, 'nginx.Dockerfile')), true);
47
+ assert.equal(fs.existsSync(path.join(dockerDir, 'caddy.Dockerfile')), false);
48
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'nginx')), true);
49
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'caddy')), false);
50
+ return;
51
+ }
52
+
53
+ assert.doesNotMatch(compose, /^\s{2}caddy:\s*$/m);
54
+ assert.doesNotMatch(compose, /^\s{2}nginx:\s*$/m);
55
+ assert.match(compose, /- "3000:3000"/);
56
+ assert.equal(fs.existsSync(path.join(dockerDir, 'nginx.Dockerfile')), false);
57
+ assert.equal(fs.existsSync(path.join(dockerDir, 'caddy.Dockerfile')), false);
58
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'nginx')), false);
59
+ assert.equal(fs.existsSync(path.join(targetRoot, 'infra', 'caddy')), false);
60
+ }
61
+
62
+ describe('scaffoldProject', () => {
63
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
64
+ const packageRoot = path.resolve(thisDir, '..', '..');
65
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
66
+ const cases = [
67
+ { proxy: 'caddy', readmePattern: /Proxy Preset: Caddy/ },
68
+ { proxy: 'nginx', readmePattern: /Proxy Preset: Nginx/ },
69
+ { proxy: 'none', readmePattern: /Proxy Preset: none/ },
70
+ ];
71
+
72
+ it('applies proxy presets without leftover reverse-proxy assets', () => {
73
+ for (const testCase of cases) {
74
+ const tempRoot = makeTempDir(`forgeon-scaffold-proxy-${testCase.proxy}-`);
75
+ const targetRoot = path.join(tempRoot, `demo-${testCase.proxy}`);
76
+
77
+ try {
78
+ scaffoldProject({
79
+ templateRoot,
80
+ packageRoot,
81
+ targetRoot,
82
+ projectName: `demo-${testCase.proxy}`,
83
+ frontend: 'react',
84
+ db: 'prisma',
85
+ dbPrismaEnabled: false,
86
+ i18nEnabled: false,
87
+ proxy: testCase.proxy,
88
+ });
89
+
90
+ const readme = readFile(path.join(targetRoot, 'README.md'));
91
+ assert.match(readme, testCase.readmePattern);
92
+ assert.match(readme, /Module notes index: `modules\/README\.md`/);
93
+ assertProxyPreset(targetRoot, testCase.proxy);
94
+ } finally {
95
+ fs.rmSync(tempRoot, { recursive: true, force: true });
96
+ }
97
+ }
98
+ });
99
+ });
@@ -15,6 +15,7 @@ import {
15
15
  ensureValidatorSchema,
16
16
  upsertEnvLines,
17
17
  } from './shared/patch-utils.mjs';
18
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
18
19
 
19
20
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
20
21
  const source = path.join(packageRoot, 'templates', 'module-presets', 'db-prisma', relativePath);
@@ -110,7 +111,11 @@ function patchAppModule(targetRoot) {
110
111
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
111
112
  }
112
113
 
113
- function patchHealthController(targetRoot) {
114
+ function patchHealthController(targetRoot, probeTargets) {
115
+ if (!probeTargets.allowApi) {
116
+ return;
117
+ }
118
+
114
119
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
115
120
  if (!fs.existsSync(filePath)) {
116
121
  return;
@@ -171,58 +176,19 @@ function patchHealthController(targetRoot) {
171
176
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
172
177
  }
173
178
 
174
- function patchWebApp(targetRoot) {
175
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
176
- if (!fs.existsSync(filePath)) {
177
- return;
178
- }
179
-
180
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
181
- content = content
182
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
183
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
184
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
185
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
186
-
187
- if (!content.includes('dbProbeResult')) {
188
- const stateAnchor = ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);';
189
- if (content.includes(stateAnchor)) {
190
- content = ensureLineAfter(
191
- content,
192
- stateAnchor,
193
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
194
- );
195
- }
196
- }
197
-
198
- if (!content.includes('Check database (create user)')) {
199
- const dbButton = content.includes("runProbe(setHealthResult, '/health')")
200
- ? " <button onClick={() => runProbe(setDbProbeResult, '/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>"
201
- : " <button onClick={() => runProbe(setDbProbeResult, '/api/health/db', { method: 'POST' })}>\n Check database (create user)\n </button>";
202
-
203
- const actionsStart = content.indexOf('<div className="actions">');
204
- if (actionsStart >= 0) {
205
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
206
- if (actionsEnd >= 0) {
207
- content = `${content.slice(0, actionsEnd)}\n${dbButton}${content.slice(actionsEnd)}`;
208
- }
209
- }
210
- }
211
-
212
- if (!content.includes("{renderResult('DB probe response', dbProbeResult)}")) {
213
- const dbResultLine = " {renderResult('DB probe response', dbProbeResult)}";
214
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
215
- if (content.includes(networkLine)) {
216
- content = content.replace(networkLine, `${dbResultLine}\n${networkLine}`);
217
- } else {
218
- const resultAnchor = "{renderResult('Validation probe response', validationProbeResult)}";
219
- if (content.includes(resultAnchor)) {
220
- content = ensureLineAfter(content, resultAnchor, dbResultLine);
221
- }
222
- }
223
- }
224
-
225
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
179
+ function registerWebProbe(targetRoot, probeTargets) {
180
+ ensureWebProbeDefinition({
181
+ targetRoot,
182
+ probeTargets,
183
+ definition: {
184
+ id: 'db',
185
+ title: 'Database',
186
+ buttonLabel: 'Check database (create user)',
187
+ resultTitle: 'DB probe response',
188
+ path: '/health/db',
189
+ request: { method: 'POST' },
190
+ },
191
+ });
226
192
  }
227
193
 
228
194
  function patchApiDockerfile(targetRoot) {
@@ -349,11 +315,13 @@ export function applyDbPrismaModule({ packageRoot, targetRoot }) {
349
315
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'db-prisma'));
350
316
  copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'prisma'));
351
317
 
318
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'db-prisma' });
319
+
352
320
  patchApiPackage(targetRoot);
353
321
  patchRootPackage(targetRoot);
354
322
  patchAppModule(targetRoot);
355
- patchHealthController(targetRoot);
356
- patchWebApp(targetRoot);
323
+ patchHealthController(targetRoot, probeTargets);
324
+ registerWebProbe(targetRoot, probeTargets);
357
325
  patchApiDockerfile(targetRoot);
358
326
  patchCompose(targetRoot);
359
327
  patchReadme(targetRoot);
@@ -1,4 +1,4 @@
1
- import { describe, it } from 'node:test';
1
+ import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
@@ -18,6 +18,21 @@ function createMinimalForgeonProject(targetRoot) {
18
18
  fs.writeFileSync(path.join(targetRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n', 'utf8');
19
19
  }
20
20
 
21
+ function readFile(filePath) {
22
+ return fs.readFileSync(filePath, 'utf8');
23
+ }
24
+
25
+ function readWebProbes(projectRoot) {
26
+ return readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts'));
27
+ }
28
+
29
+ function assertWebProbeShell(projectRoot) {
30
+ const appTsx = readFile(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'));
31
+ assert.match(appTsx, /id="probes"/);
32
+ assert.match(appTsx, /from '\.\/probes'/);
33
+ return appTsx;
34
+ }
35
+
21
36
  function assertDbPrismaWiring(projectRoot) {
22
37
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
23
38
  assert.match(appModule, /dbPrismaConfig/);
@@ -75,10 +90,12 @@ function assertRateLimitWiring(projectRoot) {
75
90
  assert.match(healthController, /@Get\('rate-limit'\)/);
76
91
  assert.match(healthController, /TOO_MANY_REQUESTS/);
77
92
 
78
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
93
+ const appTsx = assertWebProbeShell(projectRoot);
94
+ const probesTs = readWebProbes(projectRoot);
79
95
  assert.match(appTsx, /cache: 'no-store'/);
80
- assert.match(appTsx, /Check rate limit \(click repeatedly\)/);
81
- assert.match(appTsx, /Rate limit probe response/);
96
+ assert.match(probesTs, /"id": "rate-limit"/);
97
+ assert.match(probesTs, /"buttonLabel": "Check rate limit \(click repeatedly\)"/);
98
+ assert.match(probesTs, /"resultTitle": "Rate limit probe response"/);
82
99
 
83
100
  const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
84
101
  assert.match(readme, /## Rate Limit Module/);
@@ -109,9 +126,11 @@ function assertQueueWiring(projectRoot) {
109
126
  assert.match(healthController, /@Get\('queue'\)/);
110
127
  assert.match(healthController, /queueService\.getProbeStatus/);
111
128
 
112
- const webApp = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
113
- assert.match(webApp, /Check queue health/);
114
- assert.match(webApp, /Queue probe response/);
129
+ assertWebProbeShell(projectRoot);
130
+ const probesTs = readWebProbes(projectRoot);
131
+ assert.match(probesTs, /"id": "queue"/);
132
+ assert.match(probesTs, /"buttonLabel": "Check queue health"/);
133
+ assert.match(probesTs, /"resultTitle": "Queue probe response"/);
115
134
 
116
135
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
117
136
  assert.match(apiEnv, /QUEUE_ENABLED=true/);
@@ -156,9 +175,11 @@ function assertSchedulerWiring(projectRoot) {
156
175
  assert.match(healthController, /@Get\('scheduler'\)/);
157
176
  assert.match(healthController, /schedulerService\.getProbeStatus/);
158
177
 
159
- const webApp = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
160
- assert.match(webApp, /Check scheduler health/);
161
- assert.match(webApp, /Scheduler probe response/);
178
+ assertWebProbeShell(projectRoot);
179
+ const probesTs = readWebProbes(projectRoot);
180
+ assert.match(probesTs, /"id": "scheduler"/);
181
+ assert.match(probesTs, /"buttonLabel": "Check scheduler health"/);
182
+ assert.match(probesTs, /"resultTitle": "Scheduler probe response"/);
162
183
 
163
184
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
164
185
  assert.match(apiEnv, /SCHEDULER_ENABLED=true/);
@@ -200,10 +221,12 @@ function assertRbacWiring(projectRoot) {
200
221
  assert.match(healthController, /@Get\('rbac'\)/);
201
222
  assert.match(healthController, /@Permissions\('health\.rbac'\)/);
202
223
 
203
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
204
- assert.match(appTsx, /Check RBAC access/);
205
- assert.match(appTsx, /RBAC probe response/);
206
- assert.match(appTsx, /x-forgeon-permissions/);
224
+ assertWebProbeShell(projectRoot);
225
+ const probesTs = readWebProbes(projectRoot);
226
+ assert.match(probesTs, /"id": "rbac"/);
227
+ assert.match(probesTs, /"buttonLabel": "Check RBAC access"/);
228
+ assert.match(probesTs, /"resultTitle": "RBAC probe response"/);
229
+ assert.match(probesTs, /x-forgeon-permissions/);
207
230
 
208
231
  const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
209
232
  assert.match(readme, /## RBAC \/ Permissions Module/);
@@ -262,11 +285,14 @@ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
262
285
  assert.match(filesService, /variants:\s*\{[\s\S]*?none:\s*\{[\s\S]*?\}/);
263
286
  assert.match(filesService, /prisma\.fileBlob/);
264
287
 
265
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
266
- assert.match(appTsx, /Check files probe \(create metadata\)/);
267
- assert.match(appTsx, /Check files variants capability/);
268
- assert.match(appTsx, /Files probe response/);
269
- assert.match(appTsx, /Files variants probe response/);
288
+ assertWebProbeShell(projectRoot);
289
+ const probesTs = readWebProbes(projectRoot);
290
+ assert.match(probesTs, /"id": "files"/);
291
+ assert.match(probesTs, /"buttonLabel": "Check files probe \(create metadata\)"/);
292
+ assert.match(probesTs, /"resultTitle": "Files probe response"/);
293
+ assert.match(probesTs, /"id": "files-variants"/);
294
+ assert.match(probesTs, /"buttonLabel": "Check files variants capability"/);
295
+ assert.match(probesTs, /"resultTitle": "Files variants probe response"/);
270
296
 
271
297
  const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
272
298
  assert.match(schema, /model FileRecord \{/);
@@ -410,10 +436,12 @@ function assertFilesAccessWiring(projectRoot) {
410
436
  assert.match(healthController, /extractFilesAccessSubject/);
411
437
  assert.match(healthController, /filesAccessService\.canRead/);
412
438
 
413
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
414
- assert.match(appTsx, /Check files access/);
415
- assert.match(appTsx, /Files access probe response/);
416
- assert.match(appTsx, /x-forgeon-user-id/);
439
+ assertWebProbeShell(projectRoot);
440
+ const probesTs = readWebProbes(projectRoot);
441
+ assert.match(probesTs, /"id": "files-access"/);
442
+ assert.match(probesTs, /"buttonLabel": "Check files access"/);
443
+ assert.match(probesTs, /"resultTitle": "Files access probe response"/);
444
+ assert.match(probesTs, /x-forgeon-user-id/);
417
445
 
418
446
  const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
419
447
  assert.match(readme, /## Files Access Module/);
@@ -436,7 +464,8 @@ function assertFilesQuotasWiring(projectRoot) {
436
464
  );
437
465
 
438
466
  const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
439
- assert.match(filesPackage, /@forgeon\/files-quotas/);
467
+ assert.doesNotMatch(filesPackage, /@forgeon\/files-quotas/);
468
+ assert.match(filesPackage, /@nestjs\/core/);
440
469
 
441
470
  const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
442
471
  assert.match(
@@ -455,7 +484,9 @@ function assertFilesQuotasWiring(projectRoot) {
455
484
  path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
456
485
  'utf8',
457
486
  );
458
- assert.match(filesController, /FilesQuotasService/);
487
+ assert.match(filesController, /ModuleRef/);
488
+ assert.match(filesController, /FORGEON_FILES_UPLOAD_QUOTA_SERVICE/);
489
+ assert.match(filesController, /getFilesUploadQuotaService/);
459
490
  assert.match(filesController, /filesQuotasService\.assertUploadAllowed/);
460
491
 
461
492
  const healthController = fs.readFileSync(
@@ -465,9 +496,11 @@ function assertFilesQuotasWiring(projectRoot) {
465
496
  assert.match(healthController, /@Get\('files-quotas'\)/);
466
497
  assert.match(healthController, /filesQuotasService\.getProbeStatus/);
467
498
 
468
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
469
- assert.match(appTsx, /Check files quotas/);
470
- assert.match(appTsx, /Files quotas probe response/);
499
+ assertWebProbeShell(projectRoot);
500
+ const probesTs = readWebProbes(projectRoot);
501
+ assert.match(probesTs, /"id": "files-quotas"/);
502
+ assert.match(probesTs, /"buttonLabel": "Check files quotas"/);
503
+ assert.match(probesTs, /"resultTitle": "Files quotas probe response"/);
471
504
 
472
505
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
473
506
  assert.match(apiEnv, /FILES_QUOTAS_ENABLED=true/);
@@ -552,9 +585,11 @@ function assertFilesImageWiring(projectRoot) {
552
585
  assert.match(healthController, /@Get\('files-image'\)/);
553
586
  assert.match(healthController, /filesImageService\.getProbeStatus/);
554
587
 
555
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
556
- assert.match(appTsx, /Check files image sanitize/);
557
- assert.match(appTsx, /Files image probe response/);
588
+ assertWebProbeShell(projectRoot);
589
+ const probesTs = readWebProbes(projectRoot);
590
+ assert.match(probesTs, /"id": "files-image"/);
591
+ assert.match(probesTs, /"buttonLabel": "Check files image sanitize"/);
592
+ assert.match(probesTs, /"resultTitle": "Files image probe response"/);
558
593
 
559
594
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
560
595
  assert.match(apiEnv, /FILES_IMAGE_ENABLED=true/);
@@ -617,9 +652,11 @@ function assertJwtAuthWiring(projectRoot, withPrismaStore) {
617
652
  assert.match(healthController, /authService\.getProbeStatus/);
618
653
  assert.doesNotMatch(healthController, /,\s*,/);
619
654
 
620
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
621
- assert.match(appTsx, /Check JWT auth probe/);
622
- assert.match(appTsx, /Auth probe response/);
655
+ assertWebProbeShell(projectRoot);
656
+ const probesTs = readWebProbes(projectRoot);
657
+ assert.match(probesTs, /"id": "auth"/);
658
+ assert.match(probesTs, /"buttonLabel": "Check JWT auth probe"/);
659
+ assert.match(probesTs, /"resultTitle": "Auth probe response"/);
623
660
 
624
661
  const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
625
662
  assert.match(
@@ -1188,6 +1225,16 @@ describe('addModule', () => {
1188
1225
  assert.match(loggerModule, /ForgeonHttpLoggingMiddleware/);
1189
1226
  assert.match(loggerModule, /consumer\.apply\(RequestIdMiddleware, ForgeonHttpLoggingMiddleware\)\.forRoutes\('\*'\);/);
1190
1227
 
1228
+ const loggerIndex = fs.readFileSync(
1229
+ path.join(projectRoot, 'packages', 'logger', 'src', 'index.ts'),
1230
+ 'utf8',
1231
+ );
1232
+ assert.doesNotMatch(loggerIndex, /http-logging\.interceptor/);
1233
+ assert.equal(
1234
+ fs.existsSync(path.join(projectRoot, 'packages', 'logger', 'src', 'http-logging.interceptor.ts')),
1235
+ false,
1236
+ );
1237
+
1191
1238
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
1192
1239
  assert.match(apiEnv, /LOGGER_LEVEL=log/);
1193
1240
  assert.match(apiEnv, /LOGGER_HTTP_ENABLED=true/);
@@ -1604,8 +1651,9 @@ describe('addModule', () => {
1604
1651
  assert.match(healthController, /@Get\('files-access'\)/);
1605
1652
  assert.match(healthController, /@Get\('files-quotas'\)/);
1606
1653
 
1607
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
1608
- const filesChecks = appTsx.match(/Check files /g) ?? [];
1654
+ assertWebProbeShell(projectRoot);
1655
+ const probesTs = readWebProbes(projectRoot);
1656
+ const filesChecks = probesTs.match(/"buttonLabel": "Check files /g) ?? [];
1609
1657
  assert.equal(filesChecks.length, 5);
1610
1658
  } finally {
1611
1659
  fs.rmSync(targetRoot, { recursive: true, force: true });
@@ -1925,6 +1973,51 @@ describe('addModule', () => {
1925
1973
  }
1926
1974
  });
1927
1975
 
1976
+
1977
+ it('applies i18n after probe modules and preserves managed probe registry entries', () => {
1978
+ const targetRoot = mkTmp('forgeon-module-i18n-probes-');
1979
+ const projectRoot = path.join(targetRoot, 'demo-i18n-probes');
1980
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
1981
+
1982
+ try {
1983
+ scaffoldProject({
1984
+ templateRoot,
1985
+ packageRoot,
1986
+ targetRoot: projectRoot,
1987
+ projectName: 'demo-i18n-probes',
1988
+ frontend: 'react',
1989
+ db: 'prisma',
1990
+ dbPrismaEnabled: true,
1991
+ i18nEnabled: false,
1992
+ proxy: 'caddy',
1993
+ });
1994
+
1995
+ addModule({ moduleId: 'rate-limit', targetRoot: projectRoot, packageRoot });
1996
+ addModule({ moduleId: 'rbac', targetRoot: projectRoot, packageRoot });
1997
+
1998
+ const probesBeforeI18n = readWebProbes(projectRoot);
1999
+ assert.match(probesBeforeI18n, /"id": "rate-limit"/);
2000
+ assert.match(probesBeforeI18n, /"id": "rbac"/);
2001
+
2002
+ const i18nResult = addModule({
2003
+ moduleId: 'i18n',
2004
+ targetRoot: projectRoot,
2005
+ packageRoot,
2006
+ });
2007
+ assert.equal(i18nResult.applied, true);
2008
+
2009
+ const probesAfterI18n = readWebProbes(projectRoot);
2010
+ assert.match(probesAfterI18n, /"id": "rate-limit"/);
2011
+ assert.match(probesAfterI18n, /"id": "rbac"/);
2012
+
2013
+ const rateLimitIndex = probesAfterI18n.indexOf('"id": "rate-limit"');
2014
+ const rbacIndex = probesAfterI18n.indexOf('"id": "rbac"');
2015
+ assert.equal(rbacIndex >= 0 && rateLimitIndex > rbacIndex, true);
2016
+ } finally {
2017
+ fs.rmSync(targetRoot, { recursive: true, force: true });
2018
+ }
2019
+ });
2020
+
1928
2021
  it('applies swagger -> logger -> i18n and keeps all module wiring', () => {
1929
2022
  const targetRoot = mkTmp('forgeon-module-mixed-order-');
1930
2023
  const projectRoot = path.join(targetRoot, 'demo-mixed-order');
@@ -2503,3 +2596,6 @@ describe('addModule', () => {
2503
2596
  });
2504
2597
 
2505
2598
 
2599
+
2600
+
2601
+