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.
- package/package.json +1 -1
- package/src/cli/add-help.mjs +3 -2
- package/src/core/docs.test.mjs +1 -0
- package/src/core/scaffold.test.mjs +1 -0
- package/src/modules/accounts.mjs +9 -18
- package/src/modules/dependencies.mjs +153 -4
- package/src/modules/dependencies.test.mjs +58 -0
- package/src/modules/executor.test.mjs +544 -515
- package/src/modules/files-access.mjs +375 -375
- package/src/modules/files-image.mjs +512 -510
- package/src/modules/files-quotas.mjs +365 -365
- package/src/modules/files.mjs +5 -6
- package/src/modules/idempotency.test.mjs +3 -2
- package/src/modules/registry.mjs +20 -0
- package/src/modules/shared/files-runtime-wiring.mjs +13 -10
- package/src/run-add-module.mjs +39 -26
- package/src/run-add-module.test.mjs +228 -152
- package/src/run-scan-integrations.mjs +1 -0
- package/templates/base/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
- package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
- package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
- package/templates/module-presets/files/packages/files/package.json +1 -0
- package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
- package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
- package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
- package/templates/module-presets/files/packages/files/src/index.ts +1 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -20
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -118
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +18 -18
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
package/package.json
CHANGED
package/src/cli/add-help.mjs
CHANGED
|
@@ -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.
|
package/src/core/docs.test.mjs
CHANGED
|
@@ -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);
|
package/src/modules/accounts.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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 {
|
|
183
|
+
"import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
|
|
184
184
|
);
|
|
185
|
-
content =
|
|
186
|
-
|
|
187
|
-
|
|
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\(
|
|
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 {
|
|
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
|
|
98
|
-
|
|
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:
|
|
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 {
|