create-forgeon 0.3.19 → 0.3.21

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 (38) 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 +9 -18
  6. package/src/modules/dependencies.mjs +153 -4
  7. package/src/modules/dependencies.test.mjs +58 -0
  8. package/src/modules/executor.test.mjs +544 -515
  9. package/src/modules/files-access.mjs +375 -375
  10. package/src/modules/files-image.mjs +512 -510
  11. package/src/modules/files-quotas.mjs +365 -365
  12. package/src/modules/files.mjs +5 -6
  13. package/src/modules/idempotency.test.mjs +3 -2
  14. package/src/modules/registry.mjs +20 -0
  15. package/src/modules/shared/files-runtime-wiring.mjs +13 -10
  16. package/src/run-add-module.mjs +39 -26
  17. package/src/run-add-module.test.mjs +228 -152
  18. package/src/run-scan-integrations.mjs +1 -0
  19. package/templates/base/package.json +1 -0
  20. package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
  21. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
  22. package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
  23. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
  24. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
  25. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
  26. package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
  27. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
  28. package/templates/module-presets/files/packages/files/package.json +1 -0
  29. package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
  30. package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
  31. package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
  32. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
  33. package/templates/module-presets/files/packages/files/src/index.ts +1 -0
  34. package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -20
  35. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -118
  36. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +18 -18
  37. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
  38. 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.19",
3
+ "version": "0.3.21",
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 {
@@ -171,20 +171,20 @@ function patchAppModule(targetRoot) {
171
171
 
172
172
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
173
173
  content = content.replace(
174
- "import { authConfig, authEnvSchema, ForgeonAccountsModule } from '@forgeon/accounts-api';",
175
174
  "import { ACCOUNTS_PERSISTENCE_PORT, authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
175
+ "import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
176
176
  );
177
177
  content = content.replace(
178
+ "import { authConfig, authEnvSchema, ForgeonAccountsModule } from '@forgeon/accounts-api';",
178
179
  "import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
179
- "import { ACCOUNTS_PERSISTENCE_PORT, authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
180
180
  );
181
181
  content = ensureImportLine(
182
182
  content,
183
- "import { ACCOUNTS_PERSISTENCE_PORT, authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
183
+ "import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
184
184
  );
185
- content = ensureImportLine(
186
- content,
187
- "import { PrismaAccountsPersistenceStore } from './accounts/prisma-accounts-persistence.store';",
185
+ content = content.replace(
186
+ /^import \{ PrismaAccountsPersistenceStore \} from '\.\/accounts\/prisma-accounts-persistence\.store';\r?\n/m,
187
+ '',
188
188
  );
189
189
  content = content.replace(
190
190
  /^import \{ ForgeonAccountsDbPrismaModule \} from '\.\/accounts\/forgeon-accounts-db-prisma\.module';\r?\n/m,
@@ -195,20 +195,12 @@ function patchAppModule(targetRoot) {
195
195
  content = content.replace(/^\s*ForgeonAccountsDbPrismaModule,\r?\n/gm, '');
196
196
 
197
197
  const accountsModuleLine = ` ForgeonAccountsModule.register({
198
- imports: [DbPrismaModule],
199
- providers: [
200
- PrismaAccountsPersistenceStore,
201
- {
202
- provide: ACCOUNTS_PERSISTENCE_PORT,
203
- useExisting: PrismaAccountsPersistenceStore,
204
- },
205
- ],
206
198
  users: UsersModule.register({}),
207
199
  }),`;
208
200
 
209
201
  if (content.includes(' ForgeonAccountsModule.register({')) {
210
202
  content = content.replace(
211
- / {4}ForgeonAccountsModule\.register\(\{[\s\S]*? {4}\}\),/m,
203
+ / {4}ForgeonAccountsModule\.register\([\s\S]*? {4}\}\),/m,
212
204
  accountsModuleLine,
213
205
  );
214
206
  } else if (content.includes(' ForgeonI18nModule.register({')) {
@@ -221,7 +213,6 @@ function patchAppModule(targetRoot) {
221
213
 
222
214
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
223
215
  }
224
-
225
216
  function patchHealthController(targetRoot, probeTargets) {
226
217
  patchHealthControllerServiceProbe(targetRoot, probeTargets, {
227
218
  importLine: "import { AuthService } from '@forgeon/accounts-api';",
@@ -386,7 +377,6 @@ function patchReadme(targetRoot) {
386
377
  export function applyAccountsModule({ packageRoot, targetRoot }) {
387
378
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-contracts'));
388
379
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-api'));
389
- copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'accounts'));
390
380
  copyFromPreset(
391
381
  packageRoot,
392
382
  targetRoot,
@@ -429,3 +419,4 @@ export function applyAccountsModule({ packageRoot, targetRoot }) {
429
419
 
430
420
 
431
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 {