create-forgeon 0.3.18 → 0.3.20

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add-help.mjs +3 -2
  3. package/src/core/docs.test.mjs +1 -0
  4. package/src/core/scaffold.test.mjs +1 -0
  5. package/src/modules/accounts.mjs +28 -22
  6. package/src/modules/dependencies.mjs +153 -4
  7. package/src/modules/dependencies.test.mjs +58 -0
  8. package/src/modules/executor.test.mjs +73 -37
  9. package/src/modules/files-image.mjs +6 -4
  10. package/src/modules/files.mjs +5 -6
  11. package/src/modules/idempotency.test.mjs +3 -2
  12. package/src/modules/registry.mjs +20 -0
  13. package/src/modules/shared/files-runtime-wiring.mjs +13 -10
  14. package/src/run-add-module.mjs +39 -26
  15. package/src/run-add-module.test.mjs +76 -0
  16. package/src/run-scan-integrations.mjs +1 -0
  17. package/templates/base/package.json +1 -0
  18. package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
  19. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
  20. package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
  21. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
  22. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
  23. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
  24. package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
  25. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
  26. package/templates/module-presets/files/packages/files/package.json +1 -0
  27. package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
  28. package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
  29. package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
  30. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
  31. package/templates/module-presets/files/packages/files/src/index.ts +1 -0
  32. package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +0 -17
  33. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
  34. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
@@ -1,8 +1,8 @@
1
1
  export function printAddHelp() {
2
2
  console.log(`create-forgeon add
3
3
 
4
- Usage:
5
- npx create-forgeon@latest add <module-id> [options]
4
+ Usage:
5
+ npx create-forgeon@latest add <module-id|all> [options]
6
6
 
7
7
  Options:
8
8
  --project <path> Target project path (default: current directory)
@@ -14,6 +14,7 @@ Options:
14
14
  -h, --help Show this help
15
15
 
16
16
  Note:
17
+ Use "add all" to install every implemented module and choose one provider per provider-choice capability.
17
18
  Hard prerequisites are resolved explicitly.
18
19
  Pair integrations remain explicit follow-up actions.
19
20
  Run "pnpm forgeon:sync-integrations" in the target project after add-module steps.
@@ -117,6 +117,7 @@ describe('generateDocs', () => {
117
117
  assert.doesNotMatch(readme, /docs\/Agents\.md/);
118
118
  assert.doesNotMatch(packageJson, /"create:forgeon"/);
119
119
  assert.match(packageJson, /"forgeon:sync-integrations"/);
120
+ assert.match(packageJson, /"add-all": "npx create-forgeon@latest add all --project \."?/);
120
121
  } finally {
121
122
  fs.rmSync(tempRoot, { recursive: true, force: true });
122
123
  }
@@ -26,6 +26,7 @@ function assertProxyPreset(targetRoot, proxy) {
26
26
  assert.equal(fs.existsSync(path.join(dockerDir, 'compose.nginx.yml')), false);
27
27
  assert.equal(fs.existsSync(path.join(dockerDir, 'compose.none.yml')), false);
28
28
  assert.match(packageJson, /"forgeon:sync-integrations"/);
29
+ assert.match(packageJson, /"add-all": "npx create-forgeon@latest add all --project \."?/);
29
30
  assert.doesNotMatch(packageJson, /"create:forgeon"/);
30
31
  assert.match(compose, /^services:\s*$/m);
31
32
  assert.match(compose, /^\s{2}api:\s*$/m);
@@ -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 {
@@ -170,6 +170,10 @@ function patchAppModule(targetRoot) {
170
170
  }
171
171
 
172
172
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
173
+ content = content.replace(
174
+ "import { ACCOUNTS_PERSISTENCE_PORT, authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
175
+ "import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
176
+ );
173
177
  content = content.replace(
174
178
  "import { authConfig, authEnvSchema, ForgeonAccountsModule } from '@forgeon/accounts-api';",
175
179
  "import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
@@ -178,37 +182,37 @@ function patchAppModule(targetRoot) {
178
182
  content,
179
183
  "import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
180
184
  );
181
- content = ensureImportLine(
182
- content,
183
- "import { ForgeonAccountsDbPrismaModule } from './accounts/forgeon-accounts-db-prisma.module';",
185
+ content = content.replace(
186
+ /^import \{ PrismaAccountsPersistenceStore \} from '\.\/accounts\/prisma-accounts-persistence\.store';\r?\n/m,
187
+ '',
188
+ );
189
+ content = content.replace(
190
+ /^import \{ ForgeonAccountsDbPrismaModule \} from '\.\/accounts\/forgeon-accounts-db-prisma\.module';\r?\n/m,
191
+ '',
184
192
  );
185
193
  content = ensureLoadItem(content, 'authConfig');
186
194
  content = ensureValidatorSchema(content, 'authEnvSchema');
187
-
188
- if (!content.includes(' ForgeonAccountsDbPrismaModule,')) {
189
- if (content.includes(' DbPrismaModule,')) {
190
- content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonAccountsDbPrismaModule,');
191
- } else {
192
- content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonAccountsDbPrismaModule,');
193
- }
194
- }
195
+ content = content.replace(/^\s*ForgeonAccountsDbPrismaModule,\r?\n/gm, '');
195
196
 
196
197
  const accountsModuleLine = ` ForgeonAccountsModule.register({
197
198
  users: UsersModule.register({}),
198
199
  }),`;
199
- if (!content.includes('ForgeonAccountsModule.register({')) {
200
- if (content.includes(' ForgeonI18nModule.register({')) {
201
- content = ensureLineBefore(content, ' ForgeonI18nModule.register({', accountsModuleLine);
202
- } else if (content.includes(' ForgeonAccountsDbPrismaModule,')) {
203
- content = ensureLineAfter(content, ' ForgeonAccountsDbPrismaModule,', accountsModuleLine);
204
- } else {
205
- content = ensureLineAfter(content, ' CoreErrorsModule,', accountsModuleLine);
206
- }
200
+
201
+ if (content.includes(' ForgeonAccountsModule.register({')) {
202
+ content = content.replace(
203
+ / {4}ForgeonAccountsModule\.register\([\s\S]*? {4}\}\),/m,
204
+ accountsModuleLine,
205
+ );
206
+ } else if (content.includes(' ForgeonI18nModule.register({')) {
207
+ content = ensureLineBefore(content, ' ForgeonI18nModule.register({', accountsModuleLine);
208
+ } else if (content.includes(' DbPrismaModule,')) {
209
+ content = ensureLineAfter(content, ' DbPrismaModule,', accountsModuleLine);
210
+ } else {
211
+ content = ensureLineAfter(content, ' CoreErrorsModule,', accountsModuleLine);
207
212
  }
208
213
 
209
214
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
210
215
  }
211
-
212
216
  function patchHealthController(targetRoot, probeTargets) {
213
217
  patchHealthControllerServiceProbe(targetRoot, probeTargets, {
214
218
  importLine: "import { AuthService } from '@forgeon/accounts-api';",
@@ -373,7 +377,6 @@ function patchReadme(targetRoot) {
373
377
  export function applyAccountsModule({ packageRoot, targetRoot }) {
374
378
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-contracts'));
375
379
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-api'));
376
- copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'accounts'));
377
380
  copyFromPreset(
378
381
  packageRoot,
379
382
  targetRoot,
@@ -414,3 +417,6 @@ export function applyAccountsModule({ packageRoot, targetRoot }) {
414
417
  }
415
418
 
416
419
 
420
+
421
+
422
+
@@ -1,7 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { promptSelect } from '../cli/prompt-select.mjs';
4
- import { getCapabilityProviders, listModulePresets } from './registry.mjs';
4
+ import {
5
+ getCapabilityProviders,
6
+ getRecommendedCapabilityProvider,
7
+ listModulePresets,
8
+ } from './registry.mjs';
5
9
 
6
10
  function getPresetMap(presets) {
7
11
  return new Map(presets.map((preset) => [preset.id, preset]));
@@ -94,15 +98,16 @@ async function selectProviderForCapability({
94
98
  );
95
99
  }
96
100
 
97
- const choices = providers.map((provider, index) => ({
98
- label: index === 0 ? `${provider.id} (Recommended)` : provider.id,
101
+ const recommendedProvider = getRecommendedCapabilityProvider(capabilityId, providers) ?? providers[0];
102
+ const choices = providers.map((provider) => ({
103
+ label: provider.id === recommendedProvider.id ? `${provider.id} (Recommended)` : provider.id,
99
104
  value: provider.id,
100
105
  }));
101
106
  choices.push({ label: 'Cancel', value: '__cancel' });
102
107
 
103
108
  const picked = await promptSelectImpl({
104
109
  message: `Module "${moduleId}" requires capability: ${capabilityId}`,
105
- defaultValue: providers[0].id,
110
+ defaultValue: recommendedProvider.id,
106
111
  choices,
107
112
  });
108
113
 
@@ -113,6 +118,53 @@ async function selectProviderForCapability({
113
118
  return picked;
114
119
  }
115
120
 
121
+ function getAmbiguousCapabilityProviders(presets = listModulePresets()) {
122
+ const providersByCapability = new Map();
123
+
124
+ for (const preset of presets) {
125
+ if (preset.implemented === false || !Array.isArray(preset.provides)) {
126
+ continue;
127
+ }
128
+
129
+ for (const capabilityId of preset.provides) {
130
+ const providers = providersByCapability.get(capabilityId) ?? [];
131
+ providers.push(preset);
132
+ providersByCapability.set(capabilityId, providers);
133
+ }
134
+ }
135
+
136
+ return new Map(
137
+ [...providersByCapability.entries()].filter(([, providers]) => providers.length > 1),
138
+ );
139
+ }
140
+
141
+ function getAllModulesInstallTargets({ presets = listModulePresets(), selectedProviders = {} }) {
142
+ const ambiguousCapabilityProviders = getAmbiguousCapabilityProviders(presets);
143
+
144
+ return presets
145
+ .filter((preset) => preset.implemented !== false)
146
+ .filter((preset) => {
147
+ const providedCapabilities = Array.isArray(preset.provides) ? preset.provides : [];
148
+ const ambiguousCapabilities = providedCapabilities.filter((capabilityId) =>
149
+ ambiguousCapabilityProviders.has(capabilityId),
150
+ );
151
+
152
+ if (ambiguousCapabilities.length === 0) {
153
+ return true;
154
+ }
155
+
156
+ const hasNonAmbiguousCapability = providedCapabilities.some(
157
+ (capabilityId) => !ambiguousCapabilityProviders.has(capabilityId),
158
+ );
159
+ if (hasNonAmbiguousCapability) {
160
+ return true;
161
+ }
162
+
163
+ return ambiguousCapabilities.some((capabilityId) => selectedProviders[capabilityId] === preset.id);
164
+ })
165
+ .map((preset) => preset.id);
166
+ }
167
+
116
168
  export async function resolveModuleInstallPlan({
117
169
  moduleId,
118
170
  targetRoot,
@@ -229,6 +281,103 @@ export async function resolveModuleInstallPlan({
229
281
  };
230
282
  }
231
283
 
284
+ export async function resolveAllModulesInstallPlan({
285
+ targetRoot,
286
+ presets = listModulePresets(),
287
+ providerSelections = {},
288
+ promptSelectImpl = promptSelect,
289
+ isInteractive = process.stdin.isTTY && process.stdout.isTTY,
290
+ }) {
291
+ const ambiguousCapabilityProviders = getAmbiguousCapabilityProviders(presets);
292
+ const selectedProviders = { ...providerSelections };
293
+
294
+ for (const [capabilityId, providers] of ambiguousCapabilityProviders.entries()) {
295
+ const explicitProvider = selectedProviders[capabilityId];
296
+ if (explicitProvider) {
297
+ const matchedProvider = providers.find((provider) => provider.id === explicitProvider);
298
+ if (!matchedProvider) {
299
+ throw createResolutionError('all', { type: 'capability', id: capabilityId }, providers, explicitProvider);
300
+ }
301
+ continue;
302
+ }
303
+
304
+ if (isInteractive) {
305
+ const pickedProvider = await selectProviderForCapability({
306
+ moduleId: 'all',
307
+ capabilityId,
308
+ providers,
309
+ promptSelectImpl,
310
+ });
311
+ if (!pickedProvider) {
312
+ return {
313
+ cancelled: true,
314
+ moduleSequence: [],
315
+ selectedProviders: {},
316
+ rootModuleIds: [],
317
+ };
318
+ }
319
+ selectedProviders[capabilityId] = pickedProvider;
320
+ continue;
321
+ }
322
+
323
+ const recommendedProvider = getRecommendedCapabilityProvider(capabilityId, providers);
324
+ if (!recommendedProvider) {
325
+ throw createResolutionError('all', { type: 'capability', id: capabilityId }, providers);
326
+ }
327
+ selectedProviders[capabilityId] = recommendedProvider.id;
328
+ }
329
+
330
+ const rootModuleIds = getAllModulesInstallTargets({
331
+ presets,
332
+ selectedProviders,
333
+ });
334
+
335
+ const moduleSequence = [];
336
+ const plannedSet = new Set();
337
+ let accumulatedProviderSelections = { ...selectedProviders };
338
+
339
+ for (const moduleId of rootModuleIds) {
340
+ const plan = await resolveModuleInstallPlan({
341
+ moduleId,
342
+ targetRoot,
343
+ presets,
344
+ withRequired: true,
345
+ providerSelections: accumulatedProviderSelections,
346
+ promptSelectImpl,
347
+ isInteractive: false,
348
+ });
349
+
350
+ if (plan.cancelled) {
351
+ return {
352
+ cancelled: true,
353
+ moduleSequence,
354
+ selectedProviders: accumulatedProviderSelections,
355
+ rootModuleIds,
356
+ };
357
+ }
358
+
359
+ accumulatedProviderSelections = {
360
+ ...accumulatedProviderSelections,
361
+ ...plan.selectedProviders,
362
+ };
363
+
364
+ for (const plannedModuleId of plan.moduleSequence) {
365
+ if (plannedSet.has(plannedModuleId)) {
366
+ continue;
367
+ }
368
+ plannedSet.add(plannedModuleId);
369
+ moduleSequence.push(plannedModuleId);
370
+ }
371
+ }
372
+
373
+ return {
374
+ cancelled: false,
375
+ moduleSequence,
376
+ selectedProviders: accumulatedProviderSelections,
377
+ rootModuleIds,
378
+ };
379
+ }
380
+
232
381
  function requirementIsMissing(requirement, installedModules, providedCapabilities) {
233
382
  if (requirement.type === 'capability') {
234
383
  return !providedCapabilities.has(requirement.id);
@@ -8,6 +8,7 @@ import {
8
8
  detectInstalledModules,
9
9
  getPendingOptionalIntegrations,
10
10
  getPendingRecommendedCompanions,
11
+ resolveAllModulesInstallPlan,
11
12
  resolveModuleInstallPlan,
12
13
  } from './dependencies.mjs';
13
14
 
@@ -361,6 +362,63 @@ describe('module dependency helpers', () => {
361
362
  }
362
363
  });
363
364
 
365
+ it('builds a full install plan with the recommended provider for ambiguous capabilities', async () => {
366
+ const targetRoot = mkTmp('forgeon-deps-all-plan-');
367
+
368
+ try {
369
+ const result = await resolveAllModulesInstallPlan({
370
+ targetRoot,
371
+ presets: TEST_PRESETS,
372
+ isInteractive: false,
373
+ });
374
+
375
+ assert.equal(result.cancelled, false);
376
+ assert.deepEqual(result.moduleSequence, [
377
+ 'db-prisma',
378
+ 'accounts',
379
+ 'rbac',
380
+ 'files-local',
381
+ 'files',
382
+ 'files-access',
383
+ 'files-quotas',
384
+ 'files-image',
385
+ 'queue',
386
+ 'scheduler',
387
+ ]);
388
+ assert.deepEqual(result.selectedProviders, {
389
+ 'files-storage-adapter': 'files-local',
390
+ 'db-adapter': 'db-prisma',
391
+ 'files-runtime': 'files',
392
+ 'queue-runtime': 'queue',
393
+ });
394
+ assert.equal(result.rootModuleIds.includes('files-s3'), false);
395
+ } finally {
396
+ fs.rmSync(targetRoot, { recursive: true, force: true });
397
+ }
398
+ });
399
+
400
+ it('builds a full install plan with an explicit provider override', async () => {
401
+ const targetRoot = mkTmp('forgeon-deps-all-provider-');
402
+
403
+ try {
404
+ const result = await resolveAllModulesInstallPlan({
405
+ targetRoot,
406
+ presets: TEST_PRESETS,
407
+ isInteractive: false,
408
+ providerSelections: {
409
+ 'files-storage-adapter': 'files-s3',
410
+ },
411
+ });
412
+
413
+ assert.equal(result.cancelled, false);
414
+ assert.equal(result.moduleSequence.includes('files-s3'), true);
415
+ assert.equal(result.moduleSequence.includes('files-local'), false);
416
+ assert.equal(result.selectedProviders['files-storage-adapter'], 'files-s3');
417
+ } finally {
418
+ fs.rmSync(targetRoot, { recursive: true, force: true });
419
+ }
420
+ });
421
+
364
422
  it('reports pending optional integrations for accounts when rbac is missing', () => {
365
423
  const targetRoot = mkTmp('forgeon-deps-optional-');
366
424
  try {
@@ -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';
@@ -239,10 +239,10 @@ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
239
239
  assert.match(appModule, /filesConfig/);
240
240
  assert.match(appModule, /filesEnvSchema/);
241
241
  assert.match(appModule, /ForgeonFilesModule\.register\(\{/);
242
- assert.match(appModule, /ForgeonFilesDbPrismaModule/);
242
+ assert.doesNotMatch(appModule, /ForgeonFilesDbPrismaModule/);
243
243
  if (expectedStorageDriver === 's3') {
244
244
  assert.match(appModule, /ForgeonFilesS3StorageModule/);
245
- assert.doesNotMatch(appModule, /imports: \[ForgeonFilesDbPrismaModule, ForgeonFilesLocalStorageModule\]/);
245
+ assert.doesNotMatch(appModule, /ForgeonFilesLocalStorageModule/);
246
246
  } else {
247
247
  assert.match(appModule, /ForgeonFilesLocalStorageModule/);
248
248
  }
@@ -285,14 +285,16 @@ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
285
285
  path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
286
286
  'utf8',
287
287
  );
288
- assert.match(filesService, /FILES_PERSISTENCE_PORT/);
288
+ assert.match(filesService, /FilesStore/);
289
289
  assert.match(filesService, /FILES_STORAGE_ADAPTER/);
290
+ assert.match(filesService, /requireStorageAdapter/);
290
291
  assert.match(filesService, /getOrCreateBlob/);
291
292
  assert.match(filesService, /cleanupReferencedBlobs/);
292
293
  assert.match(filesService, /isUniqueConstraintError/);
293
294
  assert.match(filesService, /storageAdapter\.put/);
294
- assert.match(filesService, /persistence\.createBlob/);
295
- assert.match(filesService, /persistence\.deleteBlobIfUnreferenced/);
295
+ assert.match(filesService, /filesStore\.createBlob/);
296
+ assert.match(filesService, /filesStore\.deleteBlobIfUnreferenced/);
297
+ assert.doesNotMatch(filesService, /FILES_PERSISTENCE_PORT/);
296
298
  assert.doesNotMatch(filesService, /PrismaService/);
297
299
  assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
298
300
 
@@ -300,38 +302,34 @@ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
300
302
  path.join(projectRoot, 'packages', 'files', 'src', 'files.ports.ts'),
301
303
  'utf8',
302
304
  );
303
- assert.match(filesPorts, /FILES_PERSISTENCE_PORT/);
305
+ assert.doesNotMatch(filesPorts, /FILES_PERSISTENCE_PORT/);
306
+ assert.doesNotMatch(filesPorts, /interface FilesPersistencePort/);
304
307
  assert.match(filesPorts, /FILES_STORAGE_ADAPTER/);
305
- assert.match(filesPorts, /interface FilesPersistencePort/);
306
308
  assert.match(filesPorts, /interface FilesStorageAdapter/);
307
309
 
310
+ const filesStore = fs.readFileSync(
311
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.store.ts'),
312
+ 'utf8',
313
+ );
314
+ assert.match(filesStore, /PrismaService/);
315
+ assert.match(filesStore, /fileBlob\.deleteMany/);
316
+
308
317
  const filesModule = fs.readFileSync(
309
318
  path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
310
319
  'utf8',
311
320
  );
312
321
  assert.match(filesModule, /ForgeonFilesModuleOptions/);
313
322
  assert.match(filesModule, /static register\(options: ForgeonFilesModuleOptions = \{\}\)/);
314
- assert.match(filesModule, /FILES_PERSISTENCE_PORT/);
315
- assert.match(filesModule, /FILES_STORAGE_ADAPTER/);
323
+ assert.match(filesModule, /DbPrismaModule/);
324
+ assert.match(filesModule, /FilesStore/);
325
+ assert.doesNotMatch(filesModule, /FILES_PERSISTENCE_PORT/);
316
326
 
317
327
  const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
318
- assert.doesNotMatch(filesPackage, /@forgeon\/db-prisma/);
319
-
320
- const prismaFilesStore = fs.readFileSync(
321
- path.join(projectRoot, 'apps', 'api', 'src', 'files', 'prisma-files-persistence.store.ts'),
322
- 'utf8',
323
- );
324
- assert.match(prismaFilesStore, /PrismaService/);
325
- assert.match(prismaFilesStore, /FILES_PERSISTENCE_PORT/);
326
- assert.match(prismaFilesStore, /fileBlob\.deleteMany/);
328
+ assert.match(filesPackage, /@forgeon\/db-prisma/);
327
329
 
328
- const prismaFilesModule = fs.readFileSync(
329
- path.join(projectRoot, 'apps', 'api', 'src', 'files', 'forgeon-files-db-prisma.module.ts'),
330
- 'utf8',
331
- );
332
- assert.match(prismaFilesModule, /ForgeonFilesDbPrismaModule/);
333
- assert.match(prismaFilesModule, /DbPrismaModule/);
334
- assert.match(prismaFilesModule, /FILES_PERSISTENCE_PORT/);
330
+ const prismaFilesDir = path.join(projectRoot, 'apps', 'api', 'src', 'files');
331
+ assert.equal(fs.existsSync(path.join(prismaFilesDir, 'prisma-files-persistence.store.ts')), false);
332
+ assert.equal(fs.existsSync(path.join(prismaFilesDir, 'forgeon-files-db-prisma.module.ts')), false);
335
333
 
336
334
  assertWebProbeShell(projectRoot);
337
335
  const probesTs = readWebProbes(projectRoot);
@@ -619,14 +617,19 @@ function assertFilesImageWiring(projectRoot) {
619
617
  );
620
618
  assert.match(filesModule, /ForgeonFilesImageModule/);
621
619
 
622
- const filesService = fs.readFileSync(
623
- path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
624
- 'utf8',
625
- );
626
- assert.match(filesService, /FilesImageService/);
627
- assert.match(filesService, /filesImageService\.sanitizeForStorage/);
628
- assert.match(filesService, /sanitizeForStorage\({/);
629
- assert.match(filesService, /auditContext: input\.auditContext/);
620
+ const filesService = fs.readFileSync(
621
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
622
+ 'utf8',
623
+ );
624
+ assert.match(filesService, /FilesImageService/);
625
+ assert.match(filesService, /filesImageService\.sanitizeForStorage/);
626
+ assert.match(filesService, /sanitizeForStorage\({/);
627
+ assert.match(filesService, /auditContext: input\.auditContext/);
628
+ assert.equal(
629
+ (filesService.match(/protected normalizeFileName\(originalName: string, extension: string, suffix\?: string\): string \{/g) ?? [])
630
+ .length,
631
+ 1,
632
+ );
630
633
 
631
634
  const filesController = fs.readFileSync(
632
635
  path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
@@ -716,9 +719,10 @@ function assertAccountsWiring(projectRoot) {
716
719
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
717
720
  assert.match(appModule, /authConfig/);
718
721
  assert.match(appModule, /authEnvSchema/);
719
- assert.match(appModule, /ForgeonAccountsDbPrismaModule/);
720
722
  assert.match(appModule, /ForgeonAccountsModule\.register\(\{/);
721
723
  assert.match(appModule, /UsersModule\.register\(\{\}\)/);
724
+ assert.doesNotMatch(appModule, /ACCOUNTS_PERSISTENCE_PORT/);
725
+ assert.doesNotMatch(appModule, /PrismaAccountsPersistenceStore/);
722
726
  assert.doesNotMatch(appModule, /AUTH_REFRESH_TOKEN_STORE/);
723
727
 
724
728
  const healthController = fs.readFileSync(
@@ -770,6 +774,33 @@ function assertAccountsWiring(projectRoot) {
770
774
  );
771
775
  assert.match(authServiceSource, /import type \{ RegisterRequest \} from '@forgeon\/accounts-contracts';/);
772
776
 
777
+ const authCoreSource = fs.readFileSync(
778
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth-core.service.ts'),
779
+ 'utf8',
780
+ );
781
+ assert.match(authCoreSource, /AuthStore/);
782
+ assert.doesNotMatch(authCoreSource, /ACCOUNTS_PERSISTENCE_PORT/);
783
+
784
+ const accountsApiPackage = fs.readFileSync(
785
+ path.join(projectRoot, 'packages', 'accounts-api', 'package.json'),
786
+ 'utf8',
787
+ );
788
+ assert.match(accountsApiPackage, /@forgeon\/db-prisma/);
789
+
790
+ const authStoreSource = fs.readFileSync(
791
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.store.ts'),
792
+ 'utf8',
793
+ );
794
+ assert.match(authStoreSource, /PrismaService/);
795
+ assert.match(authStoreSource, /authRefreshToken\.updateMany/);
796
+
797
+ const usersStoreSource = fs.readFileSync(
798
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'users.store.ts'),
799
+ 'utf8',
800
+ );
801
+ assert.match(usersStoreSource, /PrismaService/);
802
+ assert.match(usersStoreSource, /userProfile\.upsert/);
803
+
773
804
  const prismaStorePath = path.join(
774
805
  projectRoot,
775
806
  'apps',
@@ -778,7 +809,7 @@ function assertAccountsWiring(projectRoot) {
778
809
  'accounts',
779
810
  'prisma-accounts-persistence.store.ts',
780
811
  );
781
- assert.equal(fs.existsSync(prismaStorePath), true);
812
+ assert.equal(fs.existsSync(prismaStorePath), false);
782
813
  }
783
814
  function stripDbPrismaArtifacts(projectRoot) {
784
815
  const dbPackageDir = path.join(projectRoot, 'packages', 'db-prisma');
@@ -1547,7 +1578,7 @@ describe('addModule', () => {
1547
1578
 
1548
1579
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
1549
1580
  assert.match(appModule, /ForgeonFilesS3StorageModule/);
1550
- assert.doesNotMatch(appModule, /imports: \[ForgeonFilesDbPrismaModule, ForgeonFilesLocalStorageModule\]/);
1581
+ assert.doesNotMatch(appModule, /ForgeonFilesLocalStorageModule/);
1551
1582
 
1552
1583
  const filesService = fs.readFileSync(
1553
1584
  path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
@@ -2643,3 +2674,8 @@ describe('addModule', () => {
2643
2674
 
2644
2675
 
2645
2676
 
2677
+
2678
+
2679
+
2680
+
2681
+
@@ -258,9 +258,10 @@ function patchFilesService(targetRoot) {
258
258
  }`,
259
259
  );
260
260
 
261
- content = content.replace(
262
- ` private extensionFromMime(mimeType: string): string | null {`,
263
- ` protected normalizeFileName(originalName: string, extension: string, suffix?: string): string {
261
+ if (!content.includes('protected normalizeFileName(originalName: string, extension: string, suffix?: string): string')) {
262
+ content = content.replace(
263
+ ` private extensionFromMime(mimeType: string): string | null {`,
264
+ ` protected normalizeFileName(originalName: string, extension: string, suffix?: string): string {
264
265
  const parsed = path.parse(originalName);
265
266
  const safeExtension = extension.startsWith('.') ? extension : \`.\${extension}\`;
266
267
  const base = suffix ? \`\${parsed.name}-\${suffix}\` : parsed.name;
@@ -268,7 +269,8 @@ function patchFilesService(targetRoot) {
268
269
  }
269
270
 
270
271
  private extensionFromMime(mimeType: string): string | null {`,
271
- );
272
+ );
273
+ }
272
274
  }
273
275
 
274
276
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
@@ -170,9 +170,9 @@ function patchAppModule(targetRoot) {
170
170
  content,
171
171
  "import { filesConfig, filesEnvSchema, ForgeonFilesModule } from '@forgeon/files';",
172
172
  );
173
- content = ensureImportLine(
174
- content,
175
- "import { ForgeonFilesDbPrismaModule } from './files/forgeon-files-db-prisma.module';",
173
+ content = content.replace(
174
+ /^import \{ ForgeonFilesDbPrismaModule \} from '\.\/files\/forgeon-files-db-prisma\.module';\r?\n/m,
175
+ '',
176
176
  );
177
177
 
178
178
  if (fs.existsSync(path.join(targetRoot, 'packages', 'files-local', 'package.json'))) {
@@ -188,7 +188,6 @@ function patchAppModule(targetRoot) {
188
188
 
189
189
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
190
190
  }
191
-
192
191
  function patchApiDockerfile(targetRoot) {
193
192
  const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
194
193
  if (!fs.existsSync(dockerfilePath)) {
@@ -378,7 +377,7 @@ function patchReadme(targetRoot) {
378
377
 
379
378
  const section = `## Files Module
380
379
 
381
- The files module adds runtime file endpoints with DB-backed metadata and storage-driver-aware behavior.
380
+ The files module adds runtime file endpoints with Prisma-backed metadata and storage-driver-aware behavior.
382
381
 
383
382
  What it currently adds:
384
383
  - \`@forgeon/files\` package with:
@@ -425,7 +424,6 @@ Key env:
425
424
 
426
425
  export function applyFilesModule({ packageRoot, targetRoot }) {
427
426
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files'));
428
- copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'files'));
429
427
  const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files' });
430
428
 
431
429
 
@@ -456,3 +454,4 @@ export function applyFilesModule({ packageRoot, targetRoot }) {
456
454
  }
457
455
 
458
456
 
457
+