create-forgeon 0.2.2 → 0.2.4
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/bin/create-forgeon.mjs +9 -6
- package/package.json +1 -1
- package/src/cli/add-help.mjs +10 -6
- package/src/cli/help.mjs +13 -9
- package/src/integrations/flow.mjs +118 -0
- package/src/modules/db-prisma.mjs +26 -172
- package/src/modules/executor.test.mjs +150 -10
- package/src/modules/i18n.mjs +29 -150
- package/src/modules/jwt-auth.mjs +37 -297
- package/src/modules/logger.mjs +10 -118
- package/src/modules/shared/patch-utils.mjs +237 -0
- package/src/modules/swagger.mjs +10 -121
- package/src/modules/sync-integrations.mjs +269 -0
- package/src/run-add-module.mjs +8 -42
- package/src/run-scan-integrations.mjs +93 -0
- package/templates/base/docs/AI/ARCHITECTURE.md +17 -0
- package/templates/base/docs/AI/MODULE_SPEC.md +4 -0
- package/templates/base/package.json +0 -3
- package/templates/base/scripts/forgeon-sync-integrations.mjs +44 -241
|
@@ -1,176 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { Project, QuoteKind } from 'ts-morph';
|
|
6
|
-
|
|
7
|
-
function hasAnyImport(sourceFile, moduleSpecifier) {
|
|
8
|
-
return sourceFile.getImportDeclarations().some((item) => item.getModuleSpecifierValue() === moduleSpecifier);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function ensureNamedImports(sourceFile, moduleSpecifier, names) {
|
|
12
|
-
const declaration = sourceFile
|
|
13
|
-
.getImportDeclarations()
|
|
14
|
-
.find((item) => item.getModuleSpecifierValue() === moduleSpecifier);
|
|
15
|
-
|
|
16
|
-
if (!declaration) {
|
|
17
|
-
sourceFile.addImportDeclaration({
|
|
18
|
-
moduleSpecifier,
|
|
19
|
-
namedImports: [...names].sort(),
|
|
20
|
-
});
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const existing = new Set(declaration.getNamedImports().map((item) => item.getName()));
|
|
25
|
-
for (const name of names) {
|
|
26
|
-
if (!existing.has(name)) {
|
|
27
|
-
declaration.addNamedImport(name);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function hasDecorator(node, decoratorName) {
|
|
33
|
-
return node.getDecorators().some((item) => item.getName() === decoratorName);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function addDecoratorIfMissing(node, decoratorName, args = []) {
|
|
37
|
-
if (hasDecorator(node, decoratorName)) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
node.addDecorator({
|
|
41
|
-
name: decoratorName,
|
|
42
|
-
arguments: args,
|
|
43
|
-
});
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function syncJwtSwagger({ rootDir, changedFiles }) {
|
|
48
|
-
const controllerPath = path.join(
|
|
49
|
-
rootDir,
|
|
50
|
-
'packages',
|
|
51
|
-
'auth-api',
|
|
52
|
-
'src',
|
|
53
|
-
'auth.controller.ts',
|
|
54
|
-
);
|
|
55
|
-
const loginDtoPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'dto', 'login.dto.ts');
|
|
56
|
-
const refreshDtoPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'dto', 'refresh.dto.ts');
|
|
57
|
-
const authApiPackagePath = path.join(rootDir, 'packages', 'auth-api', 'package.json');
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
!fs.existsSync(controllerPath) ||
|
|
61
|
-
!fs.existsSync(loginDtoPath) ||
|
|
62
|
-
!fs.existsSync(refreshDtoPath) ||
|
|
63
|
-
!fs.existsSync(authApiPackagePath)
|
|
64
|
-
) {
|
|
65
|
-
return { applied: false, reason: 'jwt-auth source files are missing' };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const project = new Project({
|
|
69
|
-
manipulationSettings: { quoteKind: QuoteKind.Single },
|
|
70
|
-
skipAddingFilesFromTsConfig: true,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const controller = project.addSourceFileAtPath(controllerPath);
|
|
74
|
-
const loginDto = project.addSourceFileAtPath(loginDtoPath);
|
|
75
|
-
const refreshDto = project.addSourceFileAtPath(refreshDtoPath);
|
|
76
|
-
|
|
77
|
-
const controllerDecorators = [
|
|
78
|
-
'ApiBearerAuth',
|
|
79
|
-
'ApiBody',
|
|
80
|
-
'ApiOkResponse',
|
|
81
|
-
'ApiOperation',
|
|
82
|
-
'ApiTags',
|
|
83
|
-
'ApiUnauthorizedResponse',
|
|
84
|
-
];
|
|
85
|
-
const dtoDecorators = ['ApiProperty'];
|
|
86
|
-
|
|
87
|
-
ensureNamedImports(controller, '@nestjs/swagger', controllerDecorators);
|
|
88
|
-
ensureNamedImports(loginDto, '@nestjs/swagger', dtoDecorators);
|
|
89
|
-
ensureNamedImports(refreshDto, '@nestjs/swagger', dtoDecorators);
|
|
90
|
-
|
|
91
|
-
const authController = controller.getClass('AuthController');
|
|
92
|
-
if (!authController) {
|
|
93
|
-
return { applied: false, reason: 'AuthController not found' };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
addDecoratorIfMissing(authController, 'ApiTags', ["'auth'"]);
|
|
97
|
-
|
|
98
|
-
const loginMethod = authController.getMethod('login');
|
|
99
|
-
const refreshMethod = authController.getMethod('refresh');
|
|
100
|
-
const logoutMethod = authController.getMethod('logout');
|
|
101
|
-
const meMethod = authController.getMethod('me');
|
|
102
|
-
|
|
103
|
-
if (loginMethod) {
|
|
104
|
-
addDecoratorIfMissing(loginMethod, 'ApiOperation', ["{ summary: 'Authenticate demo user' }"]);
|
|
105
|
-
addDecoratorIfMissing(loginMethod, 'ApiBody', ['{ type: LoginDto }']);
|
|
106
|
-
addDecoratorIfMissing(loginMethod, 'ApiOkResponse', ["{ description: 'JWT token pair' }"]);
|
|
107
|
-
addDecoratorIfMissing(loginMethod, 'ApiUnauthorizedResponse', ["{ description: 'Invalid credentials' }"]);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (refreshMethod) {
|
|
111
|
-
addDecoratorIfMissing(refreshMethod, 'ApiOperation', ["{ summary: 'Refresh access token' }"]);
|
|
112
|
-
addDecoratorIfMissing(refreshMethod, 'ApiBody', ['{ type: RefreshDto }']);
|
|
113
|
-
addDecoratorIfMissing(refreshMethod, 'ApiOkResponse', ["{ description: 'New JWT token pair' }"]);
|
|
114
|
-
addDecoratorIfMissing(refreshMethod, 'ApiUnauthorizedResponse', [
|
|
115
|
-
"{ description: 'Refresh token is invalid or expired' }",
|
|
116
|
-
]);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (logoutMethod) {
|
|
120
|
-
addDecoratorIfMissing(logoutMethod, 'ApiBearerAuth');
|
|
121
|
-
addDecoratorIfMissing(logoutMethod, 'ApiOperation', ["{ summary: 'Logout and clear refresh token state' }"]);
|
|
122
|
-
addDecoratorIfMissing(logoutMethod, 'ApiOkResponse', ["{ description: 'Logout accepted' }"]);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (meMethod) {
|
|
126
|
-
addDecoratorIfMissing(meMethod, 'ApiBearerAuth');
|
|
127
|
-
addDecoratorIfMissing(meMethod, 'ApiOperation', ["{ summary: 'Get current authenticated user' }"]);
|
|
128
|
-
addDecoratorIfMissing(meMethod, 'ApiOkResponse', ["{ description: 'Current user payload' }"]);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const loginDtoClass = loginDto.getClass('LoginDto');
|
|
132
|
-
if (loginDtoClass) {
|
|
133
|
-
const emailProp = loginDtoClass.getProperty('email');
|
|
134
|
-
const passwordProp = loginDtoClass.getProperty('password');
|
|
135
|
-
if (emailProp) {
|
|
136
|
-
addDecoratorIfMissing(emailProp, 'ApiProperty', [
|
|
137
|
-
"{ example: 'demo@forgeon.local', description: 'Demo account email' }",
|
|
138
|
-
]);
|
|
139
|
-
}
|
|
140
|
-
if (passwordProp) {
|
|
141
|
-
addDecoratorIfMissing(passwordProp, 'ApiProperty', [
|
|
142
|
-
"{ example: 'forgeon-demo-password', description: 'Demo account password' }",
|
|
143
|
-
]);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const refreshDtoClass = refreshDto.getClass('RefreshDto');
|
|
148
|
-
if (refreshDtoClass) {
|
|
149
|
-
const tokenProp = refreshDtoClass.getProperty('refreshToken');
|
|
150
|
-
if (tokenProp) {
|
|
151
|
-
addDecoratorIfMissing(tokenProp, 'ApiProperty', [
|
|
152
|
-
"{ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', description: 'Refresh token' }",
|
|
153
|
-
]);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
project.saveSync();
|
|
158
|
-
changedFiles.add(controllerPath);
|
|
159
|
-
changedFiles.add(loginDtoPath);
|
|
160
|
-
changedFiles.add(refreshDtoPath);
|
|
161
|
-
|
|
162
|
-
const authApiPackage = JSON.parse(fs.readFileSync(authApiPackagePath, 'utf8'));
|
|
163
|
-
if (!authApiPackage.dependencies) {
|
|
164
|
-
authApiPackage.dependencies = {};
|
|
165
|
-
}
|
|
166
|
-
if (!authApiPackage.dependencies['@nestjs/swagger']) {
|
|
167
|
-
authApiPackage.dependencies['@nestjs/swagger'] = '^11.2.0';
|
|
168
|
-
fs.writeFileSync(authApiPackagePath, `${JSON.stringify(authApiPackage, null, 2)}\n`, 'utf8');
|
|
169
|
-
changedFiles.add(authApiPackagePath);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return { applied: true };
|
|
173
|
-
}
|
|
174
4
|
|
|
175
5
|
const PRISMA_AUTH_STORE_CONTENT = `import {
|
|
176
6
|
AuthRefreshTokenStore,
|
|
@@ -215,6 +45,32 @@ ALTER TABLE "User"
|
|
|
215
45
|
ADD COLUMN "refreshTokenHash" TEXT;
|
|
216
46
|
`;
|
|
217
47
|
|
|
48
|
+
function detectModules(rootDir) {
|
|
49
|
+
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
50
|
+
const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
jwtAuth:
|
|
54
|
+
fs.existsSync(path.join(rootDir, 'packages', 'auth-api', 'package.json')) ||
|
|
55
|
+
appModuleText.includes("from '@forgeon/auth-api'"),
|
|
56
|
+
dbPrisma:
|
|
57
|
+
fs.existsSync(path.join(rootDir, 'packages', 'db-prisma', 'package.json')) ||
|
|
58
|
+
appModuleText.includes("from '@forgeon/db-prisma'"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
63
|
+
if (content.includes(lineToInsert)) {
|
|
64
|
+
return content;
|
|
65
|
+
}
|
|
66
|
+
const index = content.indexOf(anchorLine);
|
|
67
|
+
if (index < 0) {
|
|
68
|
+
return `${content.trimEnd()}\n${lineToInsert}\n`;
|
|
69
|
+
}
|
|
70
|
+
const insertAt = index + anchorLine.length;
|
|
71
|
+
return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
218
74
|
function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
219
75
|
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
220
76
|
const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
@@ -246,48 +102,33 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
|
246
102
|
let appModule = fs.readFileSync(appModulePath, 'utf8').replace(/\r\n/g, '\n');
|
|
247
103
|
const originalAppModule = appModule;
|
|
248
104
|
|
|
249
|
-
if (appModule.includes("
|
|
250
|
-
// already includes token symbol
|
|
251
|
-
} else {
|
|
105
|
+
if (!appModule.includes("AUTH_REFRESH_TOKEN_STORE, authConfig")) {
|
|
252
106
|
appModule = appModule.replace(
|
|
253
|
-
/import\s*\{
|
|
254
|
-
|
|
255
|
-
const names = namesRaw
|
|
256
|
-
.split(',')
|
|
257
|
-
.map((item) => item.trim())
|
|
258
|
-
.filter(Boolean);
|
|
259
|
-
if (!names.includes('AUTH_REFRESH_TOKEN_STORE')) {
|
|
260
|
-
names.unshift('AUTH_REFRESH_TOKEN_STORE');
|
|
261
|
-
}
|
|
262
|
-
return `import { ${names.join(', ')} } from '@forgeon/auth-api';`;
|
|
263
|
-
},
|
|
107
|
+
/import\s*\{\s*authConfig,\s*authEnvSchema,\s*ForgeonAuthModule\s*\}\s*from '@forgeon\/auth-api';/m,
|
|
108
|
+
"import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
|
|
264
109
|
);
|
|
265
110
|
}
|
|
266
111
|
|
|
267
112
|
const storeImportLine = "import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';";
|
|
268
113
|
if (!appModule.includes(storeImportLine)) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
114
|
+
appModule = ensureLineAfter(
|
|
115
|
+
appModule,
|
|
116
|
+
"import { HealthController } from './health/health.controller';",
|
|
117
|
+
storeImportLine,
|
|
118
|
+
);
|
|
275
119
|
}
|
|
276
120
|
|
|
277
|
-
|
|
121
|
+
if (!appModule.includes('refreshTokenStoreProvider')) {
|
|
122
|
+
appModule = appModule.replace(
|
|
123
|
+
/ForgeonAuthModule\.register\(\),/m,
|
|
124
|
+
`ForgeonAuthModule.register({
|
|
278
125
|
imports: [DbPrismaModule],
|
|
279
126
|
refreshTokenStoreProvider: {
|
|
280
127
|
provide: AUTH_REFRESH_TOKEN_STORE,
|
|
281
128
|
useClass: PrismaAuthRefreshTokenStore,
|
|
282
129
|
},
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
if (!appModule.includes('refreshTokenStoreProvider')) {
|
|
286
|
-
if (/ForgeonAuthModule\.register\(\s*\),/.test(appModule)) {
|
|
287
|
-
appModule = appModule.replace(/ForgeonAuthModule\.register\(\s*\),/, authRegisterWithPrisma);
|
|
288
|
-
} else if (/ForgeonAuthModule\.register\(\{[\s\S]*?\}\),/m.test(appModule)) {
|
|
289
|
-
appModule = appModule.replace(/ForgeonAuthModule\.register\(\{[\s\S]*?\}\),/m, authRegisterWithPrisma);
|
|
290
|
-
}
|
|
130
|
+
}),`,
|
|
131
|
+
);
|
|
291
132
|
}
|
|
292
133
|
|
|
293
134
|
if (appModule !== originalAppModule) {
|
|
@@ -299,10 +140,7 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
|
299
140
|
let schema = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
|
|
300
141
|
const originalSchema = schema;
|
|
301
142
|
if (!schema.includes('refreshTokenHash')) {
|
|
302
|
-
schema = schema.replace(
|
|
303
|
-
/email\s+String\s+@unique/g,
|
|
304
|
-
'email String @unique\n refreshTokenHash String?',
|
|
305
|
-
);
|
|
143
|
+
schema = schema.replace(/email\s+String\s+@unique/g, 'email String @unique\n refreshTokenHash String?');
|
|
306
144
|
}
|
|
307
145
|
if (schema !== originalSchema) {
|
|
308
146
|
fs.writeFileSync(schemaPath, `${schema.trimEnd()}\n`, 'utf8');
|
|
@@ -321,11 +159,11 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
|
321
159
|
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
322
160
|
const originalReadme = readme;
|
|
323
161
|
readme = readme.replace(
|
|
324
|
-
'- refresh token persistence: disabled
|
|
162
|
+
'- refresh token persistence: disabled by default (stateless mode)',
|
|
325
163
|
'- refresh token persistence: enabled (`db-prisma` adapter)',
|
|
326
164
|
);
|
|
327
165
|
readme = readme.replace(
|
|
328
|
-
/- to enable persistence later:[\s\S]*?2\. run `
|
|
166
|
+
/- to enable persistence later:[\s\S]*?2\. run `pnpm forgeon:sync-integrations` to auto-wire pair integrations\./m,
|
|
329
167
|
'- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
|
|
330
168
|
);
|
|
331
169
|
if (readme !== originalReadme) {
|
|
@@ -341,41 +179,12 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
|
341
179
|
return { applied: true };
|
|
342
180
|
}
|
|
343
181
|
|
|
344
|
-
function detectModules(rootDir) {
|
|
345
|
-
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
346
|
-
const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
swagger:
|
|
350
|
-
fs.existsSync(path.join(rootDir, 'packages', 'swagger', 'package.json')) ||
|
|
351
|
-
appModuleText.includes("from '@forgeon/swagger'"),
|
|
352
|
-
jwtAuth:
|
|
353
|
-
fs.existsSync(path.join(rootDir, 'packages', 'auth-api', 'package.json')) ||
|
|
354
|
-
appModuleText.includes("from '@forgeon/auth-api'"),
|
|
355
|
-
dbPrisma:
|
|
356
|
-
fs.existsSync(path.join(rootDir, 'packages', 'db-prisma', 'package.json')) ||
|
|
357
|
-
appModuleText.includes("from '@forgeon/db-prisma'"),
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
182
|
function run() {
|
|
362
183
|
const rootDir = process.cwd();
|
|
363
184
|
const changedFiles = new Set();
|
|
364
185
|
const detected = detectModules(rootDir);
|
|
365
186
|
const summary = [];
|
|
366
187
|
|
|
367
|
-
if (detected.swagger && detected.jwtAuth) {
|
|
368
|
-
summary.push({
|
|
369
|
-
feature: 'jwt-auth + swagger',
|
|
370
|
-
result: syncJwtSwagger({ rootDir, changedFiles }),
|
|
371
|
-
});
|
|
372
|
-
} else {
|
|
373
|
-
summary.push({
|
|
374
|
-
feature: 'jwt-auth + swagger',
|
|
375
|
-
result: { applied: false, reason: 'required modules are not both installed' },
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
188
|
if (detected.jwtAuth && detected.dbPrisma) {
|
|
380
189
|
summary.push({
|
|
381
190
|
feature: 'jwt-auth + db-prisma',
|
|
@@ -406,10 +215,4 @@ function run() {
|
|
|
406
215
|
}
|
|
407
216
|
}
|
|
408
217
|
|
|
409
|
-
|
|
410
|
-
process.argv[1] &&
|
|
411
|
-
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
412
|
-
|
|
413
|
-
if (isMain) {
|
|
414
|
-
run();
|
|
415
|
-
}
|
|
218
|
+
run();
|