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
@@ -11,6 +11,7 @@ import {
11
11
  ensureLineBefore,
12
12
  ensureNestCommonImport,
13
13
  } from './shared/patch-utils.mjs';
14
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
14
15
 
15
16
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
16
17
  const source = path.join(packageRoot, 'templates', 'module-presets', 'files-access', relativePath);
@@ -159,24 +160,6 @@ function patchFilesController(targetRoot) {
159
160
  }`,
160
161
  );
161
162
 
162
- content = content.replace(
163
- ` async download(@Param('publicId') publicId: string) {
164
- const payload = await this.filesService.openDownload(publicId);
165
- return new StreamableFile(payload.stream, {
166
- disposition: \`inline; filename="\${payload.fileName}"\`,
167
- type: payload.mimeType,
168
- });
169
- }`,
170
- ` async download(@Param('publicId') publicId: string, @Req() req: any) {
171
- const file = await this.filesService.getByPublicId(publicId);
172
- this.filesAccessService.assertCanRead(file, extractFilesAccessSubject(req));
173
- const payload = await this.filesService.openDownload(publicId);
174
- return new StreamableFile(payload.stream, {
175
- disposition: \`inline; filename="\${payload.fileName}"\`,
176
- type: payload.mimeType,
177
- });
178
- }`,
179
- );
180
163
 
181
164
  content = content.replace(
182
165
  ` async remove(@Param('publicId') publicId: string) {
@@ -193,7 +176,11 @@ function patchFilesController(targetRoot) {
193
176
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
194
177
  }
195
178
 
196
- function patchHealthController(targetRoot) {
179
+ function patchHealthController(targetRoot, probeTargets) {
180
+ if (!probeTargets.allowApi) {
181
+ return;
182
+ }
183
+
197
184
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
198
185
  if (!fs.existsSync(filePath)) {
199
186
  return;
@@ -256,83 +243,23 @@ function patchHealthController(targetRoot) {
256
243
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
257
244
  }
258
245
 
259
- function patchWebApp(targetRoot) {
260
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
261
- if (!fs.existsSync(filePath)) {
262
- return;
263
- }
264
-
265
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
266
- content = content
267
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
268
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
269
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
270
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
271
-
272
- if (!content.includes('filesAccessProbeResult')) {
273
- const stateAnchors = [
274
- ' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
275
- ' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
276
- ' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
277
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
278
- ' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
279
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
280
- ];
281
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
282
- if (stateAnchor) {
283
- content = ensureLineAfter(
284
- content,
285
- stateAnchor,
286
- ' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
287
- );
288
- }
289
- }
290
-
291
- if (!content.includes('Check files access')) {
292
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
293
- ? '/health/files-access'
294
- : '/api/health/files-access';
295
- const button = ` <button
296
- onClick={() =>
297
- runProbe(setFilesAccessProbeResult, '${probePath}', {
298
- headers: { 'x-forgeon-user-id': 'probe-owner' },
299
- })
300
- }
301
- >
302
- Check files access
303
- </button>`;
304
-
305
- const actionsStart = content.indexOf('<div className="actions">');
306
- if (actionsStart >= 0) {
307
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
308
- if (actionsEnd >= 0) {
309
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
310
- }
311
- }
312
- }
313
-
314
- if (!content.includes("{renderResult('Files access probe response', filesAccessProbeResult)}")) {
315
- const resultLine = " {renderResult('Files access probe response', filesAccessProbeResult)}";
316
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
317
- if (content.includes(networkLine)) {
318
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
319
- } else {
320
- const anchors = [
321
- " {renderResult('Files probe response', filesProbeResult)}",
322
- " {renderResult('RBAC probe response', rbacProbeResult)}",
323
- " {renderResult('Rate limit probe response', rateLimitProbeResult)}",
324
- " {renderResult('Auth probe response', authProbeResult)}",
325
- " {renderResult('DB probe response', dbProbeResult)}",
326
- " {renderResult('Validation probe response', validationProbeResult)}",
327
- ];
328
- const anchor = anchors.find((line) => content.includes(line));
329
- if (anchor) {
330
- content = ensureLineAfter(content, anchor, resultLine);
331
- }
332
- }
333
- }
334
-
335
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
246
+ function registerWebProbe(targetRoot, probeTargets) {
247
+ ensureWebProbeDefinition({
248
+ targetRoot,
249
+ probeTargets,
250
+ definition: {
251
+ id: 'files-access',
252
+ title: 'Files Access',
253
+ buttonLabel: 'Check files access',
254
+ resultTitle: 'Files access probe response',
255
+ path: '/health/files-access',
256
+ request: {
257
+ headers: {
258
+ 'x-forgeon-user-id': 'probe-owner',
259
+ },
260
+ },
261
+ },
262
+ });
336
263
  }
337
264
 
338
265
  function patchApiDockerfile(targetRoot) {
@@ -435,12 +362,14 @@ Actor context for probe/testing:
435
362
 
436
363
  export function applyFilesAccessModule({ packageRoot, targetRoot }) {
437
364
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-access'));
365
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-access' });
366
+
438
367
  patchApiPackage(targetRoot);
439
368
  patchFilesPackage(targetRoot);
440
369
  patchAppModule(targetRoot);
441
370
  patchFilesController(targetRoot);
442
- patchHealthController(targetRoot);
443
- patchWebApp(targetRoot);
371
+ patchHealthController(targetRoot, probeTargets);
372
+ registerWebProbe(targetRoot, probeTargets);
444
373
  patchApiDockerfile(targetRoot);
445
374
  patchReadme(targetRoot);
446
375
  }
@@ -14,6 +14,7 @@ import {
14
14
  ensureValidatorSchema,
15
15
  upsertEnvLines,
16
16
  } from './shared/patch-utils.mjs';
17
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
17
18
 
18
19
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
19
20
  const source = path.join(packageRoot, 'templates', 'module-presets', 'files-image', relativePath);
@@ -76,27 +77,6 @@ function patchRootPackage(targetRoot) {
76
77
  writeJson(packagePath, packageJson);
77
78
  }
78
79
 
79
- function patchFilesTypes(targetRoot) {
80
- const filePath = path.join(targetRoot, 'packages', 'files', 'src', 'files.types.ts');
81
- if (!fs.existsSync(filePath)) {
82
- return;
83
- }
84
-
85
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
86
- if (!content.includes('auditContext?:')) {
87
- content = content.replace(
88
- /(\s+createdById\?: string;\n)(\};)/m,
89
- `$1 auditContext?: {
90
- requestId?: string | null;
91
- ip?: string | null;
92
- userId?: string | null;
93
- };
94
- $2`,
95
- );
96
- }
97
-
98
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
99
- }
100
80
 
101
81
  function patchAppModule(targetRoot) {
102
82
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
@@ -143,14 +123,10 @@ function patchFilesModule(targetRoot) {
143
123
 
144
124
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
145
125
  content = ensureImportLine(content, "import { ForgeonFilesImageModule } from '@forgeon/files-image';");
146
- if (!content.includes('ForgeonFilesImageModule')) {
147
- content = content.replace('imports: [FilesConfigModule],', 'imports: [FilesConfigModule, ForgeonFilesImageModule],');
148
- } else {
149
- content = content.replace(
150
- 'imports: [FilesConfigModule],',
151
- 'imports: [FilesConfigModule, ForgeonFilesImageModule],',
152
- );
153
- }
126
+ content = content.replace(
127
+ 'imports: [FilesConfigModule],',
128
+ 'imports: [FilesConfigModule, ForgeonFilesImageModule],',
129
+ );
154
130
 
155
131
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
156
132
  }
@@ -298,7 +274,11 @@ function patchFilesService(targetRoot) {
298
274
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
299
275
  }
300
276
 
301
- function patchHealthController(targetRoot) {
277
+ function patchHealthController(targetRoot, probeTargets) {
278
+ if (!probeTargets.allowApi) {
279
+ return;
280
+ }
281
+
302
282
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
303
283
  if (!fs.existsSync(filePath)) {
304
284
  return;
@@ -335,73 +315,18 @@ function patchHealthController(targetRoot) {
335
315
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
336
316
  }
337
317
 
338
- function patchWebApp(targetRoot) {
339
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
340
- if (!fs.existsSync(filePath)) {
341
- return;
342
- }
343
-
344
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
345
- content = content
346
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
347
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
348
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
349
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
350
-
351
- if (!content.includes('filesImageProbeResult')) {
352
- const stateAnchors = [
353
- ' const [filesQuotasProbeResult, setFilesQuotasProbeResult] = useState<ProbeResult | null>(null);',
354
- ' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
355
- ' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
356
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
357
- ];
358
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
359
- if (stateAnchor) {
360
- content = ensureLineAfter(
361
- content,
362
- stateAnchor,
363
- ' const [filesImageProbeResult, setFilesImageProbeResult] = useState<ProbeResult | null>(null);',
364
- );
365
- }
366
- }
367
-
368
- if (!content.includes('Check files image sanitize')) {
369
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
370
- ? '/health/files-image'
371
- : '/api/health/files-image';
372
- const button = ` <button onClick={() => runProbe(setFilesImageProbeResult, '${probePath}')}>
373
- Check files image sanitize
374
- </button>`;
375
-
376
- const actionsStart = content.indexOf('<div className="actions">');
377
- if (actionsStart >= 0) {
378
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
379
- if (actionsEnd >= 0) {
380
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
381
- }
382
- }
383
- }
384
-
385
- if (!content.includes("{renderResult('Files image probe response', filesImageProbeResult)}")) {
386
- const resultLine = " {renderResult('Files image probe response', filesImageProbeResult)}";
387
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
388
- if (content.includes(networkLine)) {
389
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
390
- } else {
391
- const anchors = [
392
- " {renderResult('Files quotas probe response', filesQuotasProbeResult)}",
393
- " {renderResult('Files access probe response', filesAccessProbeResult)}",
394
- " {renderResult('Files probe response', filesProbeResult)}",
395
- " {renderResult('Validation probe response', validationProbeResult)}",
396
- ];
397
- const anchor = anchors.find((line) => content.includes(line));
398
- if (anchor) {
399
- content = ensureLineAfter(content, anchor, resultLine);
400
- }
401
- }
402
- }
403
-
404
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
318
+ function registerWebProbe(targetRoot, probeTargets) {
319
+ ensureWebProbeDefinition({
320
+ targetRoot,
321
+ probeTargets,
322
+ definition: {
323
+ id: 'files-image',
324
+ title: 'Files Image',
325
+ buttonLabel: 'Check files image sanitize',
326
+ resultTitle: 'Files image probe response',
327
+ path: '/health/files-image',
328
+ },
329
+ });
405
330
  }
406
331
 
407
332
  function patchApiDockerfile(targetRoot) {
@@ -547,16 +472,17 @@ Key env:
547
472
 
548
473
  export function applyFilesImageModule({ packageRoot, targetRoot }) {
549
474
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-image'));
475
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-image' });
476
+
550
477
  patchApiPackage(targetRoot);
551
478
  patchFilesPackage(targetRoot);
552
479
  patchRootPackage(targetRoot);
553
- patchFilesTypes(targetRoot);
554
480
  patchAppModule(targetRoot);
555
481
  patchFilesModule(targetRoot);
556
482
  patchFilesController(targetRoot);
557
483
  patchFilesService(targetRoot);
558
- patchHealthController(targetRoot);
559
- patchWebApp(targetRoot);
484
+ patchHealthController(targetRoot, probeTargets);
485
+ registerWebProbe(targetRoot, probeTargets);
560
486
  patchApiDockerfile(targetRoot);
561
487
  patchCompose(targetRoot);
562
488
  patchReadme(targetRoot);
@@ -1,4 +1,4 @@
1
- import fs from 'node:fs';
1
+ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
  import {
@@ -14,6 +14,7 @@ import {
14
14
  ensureValidatorSchema,
15
15
  upsertEnvLines,
16
16
  } from './shared/patch-utils.mjs';
17
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
17
18
 
18
19
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
19
20
  const source = path.join(packageRoot, 'templates', 'module-presets', 'files-quotas', relativePath);
@@ -49,7 +50,7 @@ function patchFilesPackage(targetRoot) {
49
50
  }
50
51
 
51
52
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
52
- ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
53
+ ensureDependency(packageJson, '@nestjs/core', '^11.0.1');
53
54
  writeJson(packagePath, packageJson);
54
55
  }
55
56
 
@@ -99,9 +100,24 @@ function patchFilesController(targetRoot) {
99
100
  }
100
101
 
101
102
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
102
- content = ensureImportLine(content, "import { FilesQuotasService } from '@forgeon/files-quotas';");
103
+ content = ensureImportLine(content, "import { ModuleRef } from '@nestjs/core';");
103
104
 
104
- if (!content.includes('private readonly filesQuotasService: FilesQuotasService')) {
105
+ if (!content.includes("const FORGEON_FILES_UPLOAD_QUOTA_SERVICE = 'FORGEON_FILES_UPLOAD_QUOTA_SERVICE';")) {
106
+ content = content.replace(
107
+ '};\n\n@Controller(\'files\')',
108
+ `};
109
+
110
+ const FORGEON_FILES_UPLOAD_QUOTA_SERVICE = 'FORGEON_FILES_UPLOAD_QUOTA_SERVICE';
111
+
112
+ type FilesUploadQuotaService = {
113
+ assertUploadAllowed(input: { ownerType: string; ownerId: string | null; fileSize: number }): Promise<void>;
114
+ };
115
+
116
+ @Controller('files')`,
117
+ );
118
+ }
119
+
120
+ if (!content.includes('private readonly moduleRef: ModuleRef')) {
105
121
  const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
106
122
  if (constructorMatch) {
107
123
  const original = constructorMatch[0];
@@ -109,29 +125,51 @@ function patchFilesController(targetRoot) {
109
125
  const normalizedInner = inner.replace(/,\s*$/, '');
110
126
  const separator = normalizedInner.length > 0 ? ',' : '';
111
127
  const next = `constructor(${normalizedInner}${separator}
112
- private readonly filesQuotasService: FilesQuotasService,
128
+ private readonly moduleRef: ModuleRef,
113
129
  ) {`;
114
130
  content = content.replace(original, next);
115
131
  }
116
132
  }
117
133
 
118
- if (!content.includes('filesQuotasService.assertUploadAllowed')) {
134
+ if (!content.includes('const filesQuotasService = this.getFilesUploadQuotaService();')) {
119
135
  content = content.replace(
120
136
  ' return this.filesService.create({',
121
- ` await this.filesQuotasService.assertUploadAllowed({
122
- ownerType: body.ownerType ?? 'system',
123
- ownerId: body.ownerId ?? null,
124
- fileSize: file.size,
125
- });
137
+ ` const filesQuotasService = this.getFilesUploadQuotaService();
138
+ if (filesQuotasService) {
139
+ await filesQuotasService.assertUploadAllowed({
140
+ ownerType: body.ownerType ?? 'system',
141
+ ownerId: body.ownerId ?? null,
142
+ fileSize: file.size,
143
+ });
144
+ }
126
145
 
127
146
  return this.filesService.create({`,
128
147
  );
129
148
  }
130
149
 
150
+ if (!content.includes('private getFilesUploadQuotaService(): FilesUploadQuotaService | null {')) {
151
+ content = content.replace(
152
+ ' private parseVariant(variantQuery?: string): FileVariantKey {',
153
+ ` private getFilesUploadQuotaService(): FilesUploadQuotaService | null {
154
+ try {
155
+ return this.moduleRef.get(FORGEON_FILES_UPLOAD_QUOTA_SERVICE, { strict: false });
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ private parseVariant(variantQuery?: string): FileVariantKey {`,
162
+ );
163
+ }
164
+
131
165
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
132
166
  }
133
167
 
134
- function patchHealthController(targetRoot) {
168
+ function patchHealthController(targetRoot, probeTargets) {
169
+ if (!probeTargets.allowApi) {
170
+ return;
171
+ }
172
+
135
173
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
136
174
  if (!fs.existsSync(filePath)) {
137
175
  return;
@@ -177,79 +215,18 @@ function patchHealthController(targetRoot) {
177
215
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
178
216
  }
179
217
 
180
- function patchWebApp(targetRoot) {
181
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
182
- if (!fs.existsSync(filePath)) {
183
- return;
184
- }
185
-
186
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
187
- content = content
188
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
189
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
190
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
191
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
192
-
193
- if (!content.includes('filesQuotasProbeResult')) {
194
- const stateAnchors = [
195
- ' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
196
- ' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
197
- ' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
198
- ' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
199
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
200
- ' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
201
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
202
- ];
203
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
204
- if (stateAnchor) {
205
- content = ensureLineAfter(
206
- content,
207
- stateAnchor,
208
- ' const [filesQuotasProbeResult, setFilesQuotasProbeResult] = useState<ProbeResult | null>(null);',
209
- );
210
- }
211
- }
212
-
213
- if (!content.includes('Check files quotas')) {
214
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
215
- ? '/health/files-quotas'
216
- : '/api/health/files-quotas';
217
- const button = ` <button onClick={() => runProbe(setFilesQuotasProbeResult, '${probePath}')}>
218
- Check files quotas
219
- </button>`;
220
-
221
- const actionsStart = content.indexOf('<div className="actions">');
222
- if (actionsStart >= 0) {
223
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
224
- if (actionsEnd >= 0) {
225
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
226
- }
227
- }
228
- }
229
-
230
- if (!content.includes("{renderResult('Files quotas probe response', filesQuotasProbeResult)}")) {
231
- const resultLine = " {renderResult('Files quotas probe response', filesQuotasProbeResult)}";
232
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
233
- if (content.includes(networkLine)) {
234
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
235
- } else {
236
- const anchors = [
237
- " {renderResult('Files access probe response', filesAccessProbeResult)}",
238
- " {renderResult('Files probe response', filesProbeResult)}",
239
- " {renderResult('RBAC probe response', rbacProbeResult)}",
240
- " {renderResult('Rate limit probe response', rateLimitProbeResult)}",
241
- " {renderResult('Auth probe response', authProbeResult)}",
242
- " {renderResult('DB probe response', dbProbeResult)}",
243
- " {renderResult('Validation probe response', validationProbeResult)}",
244
- ];
245
- const anchor = anchors.find((line) => content.includes(line));
246
- if (anchor) {
247
- content = ensureLineAfter(content, anchor, resultLine);
248
- }
249
- }
250
- }
251
-
252
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
218
+ function registerWebProbe(targetRoot, probeTargets) {
219
+ ensureWebProbeDefinition({
220
+ targetRoot,
221
+ probeTargets,
222
+ definition: {
223
+ id: 'files-quotas',
224
+ title: 'Files Quotas',
225
+ buttonLabel: 'Check files quotas',
226
+ resultTitle: 'Files quotas probe response',
227
+ path: '/health/files-quotas',
228
+ },
229
+ });
253
230
  }
254
231
 
255
232
  function patchApiDockerfile(targetRoot) {
@@ -379,12 +356,14 @@ Key env:
379
356
 
380
357
  export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
381
358
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-quotas'));
359
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-quotas' });
360
+
382
361
  patchApiPackage(targetRoot);
383
362
  patchFilesPackage(targetRoot);
384
363
  patchAppModule(targetRoot);
385
364
  patchFilesController(targetRoot);
386
- patchHealthController(targetRoot);
387
- patchWebApp(targetRoot);
365
+ patchHealthController(targetRoot, probeTargets);
366
+ registerWebProbe(targetRoot, probeTargets);
388
367
  patchApiDockerfile(targetRoot);
389
368
  patchCompose(targetRoot);
390
369
  patchReadme(targetRoot);
@@ -400,3 +379,4 @@ export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
400
379
  'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
401
380
  ]);
402
381
  }
382
+