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.
@@ -1,6 +1,18 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
+ import {
5
+ ensureBuildSteps,
6
+ ensureClassMember,
7
+ ensureDependency,
8
+ ensureImportLine,
9
+ ensureLineAfter,
10
+ ensureLineBefore,
11
+ ensureLoadItem,
12
+ ensureScript,
13
+ ensureValidatorSchema,
14
+ upsertEnvLines,
15
+ } from './shared/patch-utils.mjs';
4
16
 
5
17
  function copyFromBase(packageRoot, targetRoot, relativePath) {
6
18
  const source = path.join(packageRoot, 'templates', 'base', relativePath);
@@ -20,140 +32,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
20
32
  copyRecursive(source, destination);
21
33
  }
22
34
 
23
- function ensureDependency(packageJson, name, version) {
24
- if (!packageJson.dependencies) {
25
- packageJson.dependencies = {};
26
- }
27
- packageJson.dependencies[name] = version;
28
- }
29
-
30
- function ensureScript(packageJson, name, command) {
31
- if (!packageJson.scripts) {
32
- packageJson.scripts = {};
33
- }
34
- packageJson.scripts[name] = command;
35
- }
36
-
37
- function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
38
- if (!packageJson.scripts) {
39
- packageJson.scripts = {};
40
- }
41
-
42
- const current = packageJson.scripts[scriptName];
43
- const steps =
44
- typeof current === 'string' && current.trim().length > 0
45
- ? current
46
- .split('&&')
47
- .map((item) => item.trim())
48
- .filter(Boolean)
49
- : [];
50
-
51
- for (const command of requiredCommands) {
52
- if (!steps.includes(command)) {
53
- steps.push(command);
54
- }
55
- }
56
-
57
- if (steps.length > 0) {
58
- packageJson.scripts[scriptName] = steps.join(' && ');
59
- }
60
- }
61
-
62
- function upsertEnvLines(filePath, lines) {
63
- let content = '';
64
- if (fs.existsSync(filePath)) {
65
- content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
66
- }
67
-
68
- const keys = new Set(
69
- content
70
- .split('\n')
71
- .filter(Boolean)
72
- .map((line) => line.split('=')[0]),
73
- );
74
-
75
- const append = [];
76
- for (const line of lines) {
77
- const key = line.split('=')[0];
78
- if (!keys.has(key)) {
79
- append.push(line);
80
- }
81
- }
82
-
83
- const next =
84
- append.length > 0 ? `${content.trimEnd()}\n${append.join('\n')}\n` : `${content.trimEnd()}\n`;
85
- fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
86
- }
87
-
88
- function ensureLineAfter(content, anchorLine, lineToInsert) {
89
- if (content.includes(lineToInsert)) {
90
- return content;
91
- }
92
-
93
- const index = content.indexOf(anchorLine);
94
- if (index < 0) {
95
- return `${content.trimEnd()}\n${lineToInsert}\n`;
96
- }
97
-
98
- const insertAt = index + anchorLine.length;
99
- return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
100
- }
101
-
102
- function ensureLineBefore(content, anchorLine, lineToInsert) {
103
- if (content.includes(lineToInsert)) {
104
- return content;
105
- }
106
-
107
- const index = content.indexOf(anchorLine);
108
- if (index < 0) {
109
- return `${content.trimEnd()}\n${lineToInsert}\n`;
110
- }
111
-
112
- return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
113
- }
114
-
115
- function ensureLoadItem(content, itemName) {
116
- const pattern = /load:\s*\[([^\]]*)\]/m;
117
- const match = content.match(pattern);
118
- if (!match) {
119
- return content;
120
- }
121
-
122
- const rawList = match[1];
123
- const items = rawList
124
- .split(',')
125
- .map((item) => item.trim())
126
- .filter(Boolean);
127
-
128
- if (!items.includes(itemName)) {
129
- items.push(itemName);
130
- }
131
-
132
- const next = `load: [${items.join(', ')}]`;
133
- return content.replace(pattern, next);
134
- }
135
-
136
- function ensureValidatorSchema(content, schemaName) {
137
- const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
138
- const match = content.match(pattern);
139
- if (!match) {
140
- return content;
141
- }
142
-
143
- const rawList = match[1];
144
- const items = rawList
145
- .split(',')
146
- .map((item) => item.trim())
147
- .filter(Boolean);
148
-
149
- if (!items.includes(schemaName)) {
150
- items.push(schemaName);
151
- }
152
-
153
- const next = `validate: createEnvValidator([${items.join(', ')}])`;
154
- return content.replace(pattern, next);
155
- }
156
-
157
35
  function patchApiDockerfile(targetRoot) {
158
36
  const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
159
37
  if (!fs.existsSync(dockerfilePath)) {
@@ -412,19 +290,7 @@ function patchHealthController(targetRoot) {
412
290
 
413
291
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
414
292
  if (!content.includes("from 'nestjs-i18n';")) {
415
- if (content.includes("import { PrismaService } from '@forgeon/db-prisma';")) {
416
- content = ensureLineAfter(
417
- content,
418
- "import { PrismaService } from '@forgeon/db-prisma';",
419
- "import { I18nService } from 'nestjs-i18n';",
420
- );
421
- } else {
422
- content = ensureLineAfter(
423
- content,
424
- "import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';",
425
- "import { I18nService } from 'nestjs-i18n';",
426
- );
427
- }
293
+ content = ensureImportLine(content, "import { I18nService } from 'nestjs-i18n';");
428
294
  }
429
295
 
430
296
  if (!content.includes('private readonly i18n: I18nService')) {
@@ -432,11 +298,22 @@ function patchHealthController(targetRoot) {
432
298
  if (constructorMatch) {
433
299
  const original = constructorMatch[0];
434
300
  const inner = constructorMatch[1].trimEnd();
435
- const separator = inner.length > 0 ? ',' : '';
436
- const next = `constructor(${inner}${separator}
301
+ const normalizedInner = inner.replace(/,\s*$/, '');
302
+ const separator = normalizedInner.length > 0 ? ',' : '';
303
+ const next = `constructor(${normalizedInner}${separator}
437
304
  private readonly i18n: I18nService,
438
305
  ) {`;
439
306
  content = content.replace(original, next);
307
+ } else {
308
+ const classAnchor = 'export class HealthController {';
309
+ if (content.includes(classAnchor)) {
310
+ content = content.replace(
311
+ classAnchor,
312
+ `${classAnchor}
313
+ constructor(private readonly i18n: I18nService) {}
314
+ `,
315
+ );
316
+ }
440
317
  }
441
318
  }
442
319
 
@@ -447,9 +324,10 @@ function patchHealthController(targetRoot) {
447
324
  return typeof value === 'string' ? value : key;
448
325
  }
449
326
  `;
450
- content = `${content.trimEnd()}\n${translateMethod}\n`;
327
+ content = ensureClassMember(content, 'HealthController', translateMethod);
451
328
  }
452
329
 
330
+ content = content.replace(/getHealth\(\)/g, "getHealth(@Query('lang') lang?: string)");
453
331
  content = content.replace(
454
332
  /getHealth\(@Query\('lang'\)\s*_lang\?:\s*string\)/g,
455
333
  "getHealth(@Query('lang') lang?: string)",
@@ -463,6 +341,7 @@ function patchHealthController(targetRoot) {
463
341
  "getValidationProbe(@Query('value') value?: string, @Query('lang') lang?: string)",
464
342
  );
465
343
  content = content.replace(/message:\s*'OK',/g, "message: this.translate('common.actions.ok', lang),");
344
+ content = content.replace(/i18n:\s*'disabled',/g, "i18n: 'en',");
466
345
  content = content.replace(/i18n:\s*'English',/g, "i18n: 'en',");
467
346
 
468
347
  content = content.replace(
@@ -1,6 +1,17 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
+ import {
5
+ ensureBuildSteps,
6
+ ensureClassMember,
7
+ ensureDependency,
8
+ ensureImportLine,
9
+ ensureLineAfter,
10
+ ensureLineBefore,
11
+ ensureLoadItem,
12
+ ensureValidatorSchema,
13
+ upsertEnvLines,
14
+ } from './shared/patch-utils.mjs';
4
15
 
5
16
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
6
17
  const source = path.join(packageRoot, 'templates', 'module-presets', 'jwt-auth', relativePath);
@@ -12,177 +23,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
12
23
  copyRecursive(source, destination);
13
24
  }
14
25
 
15
- function ensureDependency(packageJson, name, version) {
16
- if (!packageJson.dependencies) {
17
- packageJson.dependencies = {};
18
- }
19
- packageJson.dependencies[name] = version;
20
- }
21
-
22
- function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
23
- if (!packageJson.scripts) {
24
- packageJson.scripts = {};
25
- }
26
-
27
- const current = packageJson.scripts[scriptName];
28
- const steps =
29
- typeof current === 'string' && current.trim().length > 0
30
- ? current
31
- .split('&&')
32
- .map((item) => item.trim())
33
- .filter(Boolean)
34
- : [];
35
-
36
- for (const command of requiredCommands) {
37
- if (!steps.includes(command)) {
38
- steps.push(command);
39
- }
40
- }
41
-
42
- if (steps.length > 0) {
43
- packageJson.scripts[scriptName] = steps.join(' && ');
44
- }
45
- }
46
-
47
- function ensureLineAfter(content, anchorLine, lineToInsert) {
48
- if (content.includes(lineToInsert)) {
49
- return content;
50
- }
51
-
52
- const index = content.indexOf(anchorLine);
53
- if (index < 0) {
54
- return `${content.trimEnd()}\n${lineToInsert}\n`;
55
- }
56
-
57
- const insertAt = index + anchorLine.length;
58
- return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
59
- }
60
-
61
- function ensureLineBefore(content, anchorLine, lineToInsert) {
62
- if (content.includes(lineToInsert)) {
63
- return content;
64
- }
65
-
66
- const index = content.indexOf(anchorLine);
67
- if (index < 0) {
68
- return `${content.trimEnd()}\n${lineToInsert}\n`;
69
- }
70
-
71
- return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
72
- }
73
-
74
- function ensureLoadItem(content, itemName) {
75
- const pattern = /load:\s*\[([^\]]*)\]/m;
76
- const match = content.match(pattern);
77
- if (!match) {
78
- return content;
79
- }
80
-
81
- const rawList = match[1];
82
- const items = rawList
83
- .split(',')
84
- .map((item) => item.trim())
85
- .filter(Boolean);
86
-
87
- if (!items.includes(itemName)) {
88
- items.push(itemName);
89
- }
90
-
91
- const next = `load: [${items.join(', ')}]`;
92
- return content.replace(pattern, next);
93
- }
94
-
95
- function ensureValidatorSchema(content, schemaName) {
96
- const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
97
- const match = content.match(pattern);
98
- if (!match) {
99
- return content;
100
- }
101
-
102
- const rawList = match[1];
103
- const items = rawList
104
- .split(',')
105
- .map((item) => item.trim())
106
- .filter(Boolean);
107
-
108
- if (!items.includes(schemaName)) {
109
- items.push(schemaName);
110
- }
111
-
112
- const next = `validate: createEnvValidator([${items.join(', ')}])`;
113
- return content.replace(pattern, next);
114
- }
115
-
116
- function upsertEnvLines(filePath, lines) {
117
- let content = '';
118
- if (fs.existsSync(filePath)) {
119
- content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
120
- }
121
-
122
- const keys = new Set(
123
- content
124
- .split('\n')
125
- .filter(Boolean)
126
- .map((line) => line.split('=')[0]),
127
- );
128
-
129
- const append = [];
130
- for (const line of lines) {
131
- const key = line.split('=')[0];
132
- if (!keys.has(key)) {
133
- append.push(line);
134
- }
135
- }
136
-
137
- const next =
138
- append.length > 0 ? `${content.trimEnd()}\n${append.join('\n')}\n` : `${content.trimEnd()}\n`;
139
- fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
140
- }
141
-
142
- function detectDbAdapter(targetRoot) {
143
- const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
144
- let deps = {};
145
- if (fs.existsSync(apiPackagePath)) {
146
- const packageJson = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
147
- deps = {
148
- ...(packageJson.dependencies ?? {}),
149
- ...(packageJson.devDependencies ?? {}),
150
- };
151
- }
152
-
153
- if (
154
- deps['@forgeon/db-prisma'] ||
155
- fs.existsSync(path.join(targetRoot, 'packages', 'db-prisma', 'package.json'))
156
- ) {
157
- return { id: 'db-prisma', supported: true, tokenStore: 'prisma' };
158
- }
159
-
160
- const dbDeps = Object.keys(deps).filter((name) => name.startsWith('@forgeon/db-'));
161
- if (dbDeps.length > 0) {
162
- return { id: dbDeps[0], supported: false, tokenStore: 'none' };
163
- }
164
-
165
- const packagesPath = path.join(targetRoot, 'packages');
166
- if (fs.existsSync(packagesPath)) {
167
- const localDbPackages = fs
168
- .readdirSync(packagesPath, { withFileTypes: true })
169
- .filter((entry) => entry.isDirectory() && entry.name.startsWith('db-'))
170
- .map((entry) => entry.name);
171
- if (localDbPackages.includes('db-prisma')) {
172
- return { id: 'db-prisma', supported: true, tokenStore: 'prisma' };
173
- }
174
- if (localDbPackages.length > 0) {
175
- return { id: `@forgeon/${localDbPackages[0]}`, supported: false, tokenStore: 'none' };
176
- }
177
- }
178
-
179
- return null;
180
- }
181
-
182
- function printDbWarning(message) {
183
- console.error(`\x1b[31m[create-forgeon add jwt-auth] ${message}\x1b[0m`);
184
- }
185
-
186
26
  function patchApiPackage(targetRoot) {
187
27
  const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
188
28
  if (!fs.existsSync(packagePath)) {
@@ -201,21 +41,19 @@ function patchApiPackage(targetRoot) {
201
41
  writeJson(packagePath, packageJson);
202
42
  }
203
43
 
204
- function patchAppModule(targetRoot, dbAdapter) {
44
+ function patchAppModule(targetRoot) {
205
45
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
206
46
  if (!fs.existsSync(filePath)) {
207
47
  return;
208
48
  }
209
49
 
210
- const withPrismaStore = dbAdapter?.supported === true && dbAdapter?.id === 'db-prisma';
211
-
212
50
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
213
51
  if (!content.includes("from '@forgeon/auth-api';")) {
214
52
  if (content.includes("import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';")) {
215
53
  content = ensureLineAfter(
216
54
  content,
217
55
  "import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';",
218
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
56
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
219
57
  );
220
58
  } else if (
221
59
  content.includes("import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';")
@@ -223,7 +61,7 @@ function patchAppModule(targetRoot, dbAdapter) {
223
61
  content = ensureLineAfter(
224
62
  content,
225
63
  "import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
226
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
64
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
227
65
  );
228
66
  } else if (
229
67
  content.includes("import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';")
@@ -231,7 +69,7 @@ function patchAppModule(targetRoot, dbAdapter) {
231
69
  content = ensureLineAfter(
232
70
  content,
233
71
  "import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';",
234
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
72
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
235
73
  );
236
74
  } else if (
237
75
  content.includes("import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';")
@@ -239,38 +77,22 @@ function patchAppModule(targetRoot, dbAdapter) {
239
77
  content = ensureLineAfter(
240
78
  content,
241
79
  "import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';",
242
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
80
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
243
81
  );
244
82
  } else {
245
83
  content = ensureLineAfter(
246
84
  content,
247
85
  "import { ConfigModule } from '@nestjs/config';",
248
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
86
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
249
87
  );
250
88
  }
251
89
  }
252
90
 
253
- if (withPrismaStore && !content.includes("./auth/prisma-auth-refresh-token.store")) {
254
- content = ensureLineBefore(
255
- content,
256
- "import { HealthController } from './health/health.controller';",
257
- "import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';",
258
- );
259
- }
260
-
261
91
  content = ensureLoadItem(content, 'authConfig');
262
92
  content = ensureValidatorSchema(content, 'authEnvSchema');
263
93
 
264
94
  if (!content.includes('ForgeonAuthModule.register(')) {
265
- const moduleBlock = withPrismaStore
266
- ? ` ForgeonAuthModule.register({
267
- imports: [DbPrismaModule],
268
- refreshTokenStoreProvider: {
269
- provide: AUTH_REFRESH_TOKEN_STORE,
270
- useClass: PrismaAuthRefreshTokenStore,
271
- },
272
- }),`
273
- : ` ForgeonAuthModule.register(),`;
95
+ const moduleBlock = ' ForgeonAuthModule.register(),';
274
96
 
275
97
  if (content.includes(' ForgeonI18nModule.register({')) {
276
98
  content = ensureLineBefore(content, ' ForgeonI18nModule.register({', moduleBlock);
@@ -297,19 +119,7 @@ function patchHealthController(targetRoot) {
297
119
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
298
120
 
299
121
  if (!content.includes("from '@forgeon/auth-api';")) {
300
- if (content.includes("import { PrismaService } from '@forgeon/db-prisma';")) {
301
- content = ensureLineAfter(
302
- content,
303
- "import { PrismaService } from '@forgeon/db-prisma';",
304
- "import { AuthService } from '@forgeon/auth-api';",
305
- );
306
- } else {
307
- content = ensureLineAfter(
308
- content,
309
- "import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';",
310
- "import { AuthService } from '@forgeon/auth-api';",
311
- );
312
- }
122
+ content = ensureImportLine(content, "import { AuthService } from '@forgeon/auth-api';");
313
123
  }
314
124
 
315
125
  if (!content.includes('private readonly authService: AuthService')) {
@@ -323,6 +133,16 @@ function patchHealthController(targetRoot) {
323
133
  private readonly authService: AuthService,
324
134
  ) {`;
325
135
  content = content.replace(original, next);
136
+ } else {
137
+ const classAnchor = 'export class HealthController {';
138
+ if (content.includes(classAnchor)) {
139
+ content = content.replace(
140
+ classAnchor,
141
+ `${classAnchor}
142
+ constructor(private readonly authService: AuthService) {}
143
+ `,
144
+ );
145
+ }
326
146
  }
327
147
  }
328
148
 
@@ -333,14 +153,8 @@ function patchHealthController(targetRoot) {
333
153
  return this.authService.getProbeStatus();
334
154
  }
335
155
  `;
336
- if (content.includes("@Post('db')")) {
337
- content = content.replace("@Post('db')", `${method}\n @Post('db')`);
338
- } else if (content.includes('private translate(')) {
339
- const index = content.indexOf('private translate(');
340
- content = `${content.slice(0, index).trimEnd()}\n\n${method}\n${content.slice(index)}`;
341
- } else {
342
- content = `${content.trimEnd()}\n${method}\n`;
343
- }
156
+ const beforeNeedle = content.includes("@Post('db')") ? "@Post('db')" : 'private translate(';
157
+ content = ensureClassMember(content, 'HealthController', method, { beforeNeedle });
344
158
  }
345
159
 
346
160
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
@@ -488,22 +302,17 @@ function patchCompose(targetRoot) {
488
302
  fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
489
303
  }
490
304
 
491
- function patchReadme(targetRoot, dbAdapter) {
305
+ function patchReadme(targetRoot) {
492
306
  const readmePath = path.join(targetRoot, 'README.md');
493
307
  if (!fs.existsSync(readmePath)) {
494
308
  return;
495
309
  }
496
310
 
497
311
  const persistenceSummary =
498
- dbAdapter?.supported && dbAdapter.id === 'db-prisma'
499
- ? '- refresh token persistence: enabled (`db-prisma` adapter)'
500
- : '- refresh token persistence: disabled (no supported DB adapter found)';
501
- const dbFollowUp =
502
- dbAdapter?.supported && dbAdapter.id === 'db-prisma'
503
- ? '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`'
504
- : `- to enable persistence later:
312
+ '- refresh token persistence: disabled by default (stateless mode)';
313
+ const dbFollowUp = `- to enable persistence later:
505
314
  1. install a DB module first (for now: \`create-forgeon add db-prisma --project .\`);
506
- 2. run \`create-forgeon add jwt-auth --project .\` again to auto-wire the adapter.`;
315
+ 2. run \`pnpm forgeon:sync-integrations\` to auto-wire pair integrations.`;
507
316
 
508
317
  const section = `## JWT Auth Module
509
318
 
@@ -547,86 +356,17 @@ Default routes:
547
356
  fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
548
357
  }
549
358
 
550
- function patchPrismaSchema(targetRoot) {
551
- const schemaPath = path.join(targetRoot, 'apps', 'api', 'prisma', 'schema.prisma');
552
- if (!fs.existsSync(schemaPath)) {
553
- return;
554
- }
555
-
556
- let content = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
557
- if (!content.includes('refreshTokenHash')) {
558
- content = content.replace(
559
- /email\s+String\s+@unique/g,
560
- 'email String @unique\n refreshTokenHash String?',
561
- );
562
- fs.writeFileSync(schemaPath, `${content.trimEnd()}\n`, 'utf8');
563
- }
564
- }
565
-
566
- function patchPrismaMigration(packageRoot, targetRoot) {
567
- const migrationSource = path.join(
568
- packageRoot,
569
- 'templates',
570
- 'module-presets',
571
- 'jwt-auth',
572
- 'apps',
573
- 'api',
574
- 'prisma',
575
- 'migrations',
576
- '0002_auth_refresh_token_hash',
577
- );
578
- const migrationTarget = path.join(
579
- targetRoot,
580
- 'apps',
581
- 'api',
582
- 'prisma',
583
- 'migrations',
584
- '0002_auth_refresh_token_hash',
585
- );
586
-
587
- if (!fs.existsSync(migrationTarget) && fs.existsSync(migrationSource)) {
588
- copyRecursive(migrationSource, migrationTarget);
589
- }
590
- }
591
-
592
359
  export function applyJwtAuthModule({ packageRoot, targetRoot }) {
593
- const dbAdapter = detectDbAdapter(targetRoot);
594
- const supportsPrismaStore = dbAdapter?.supported === true && dbAdapter?.id === 'db-prisma';
595
-
596
360
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-contracts'));
597
361
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-api'));
598
362
 
599
- const swaggerPackagePath = path.join(targetRoot, 'packages', 'swagger', 'package.json');
600
- const authApiPackagePath = path.join(targetRoot, 'packages', 'auth-api', 'package.json');
601
- if (fs.existsSync(swaggerPackagePath) && fs.existsSync(authApiPackagePath)) {
602
- const authApiPackage = JSON.parse(fs.readFileSync(authApiPackagePath, 'utf8'));
603
- ensureDependency(authApiPackage, '@nestjs/swagger', '^11.2.0');
604
- writeJson(authApiPackagePath, authApiPackage);
605
- }
606
-
607
- if (supportsPrismaStore) {
608
- copyFromPreset(
609
- packageRoot,
610
- targetRoot,
611
- path.join('apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts'),
612
- );
613
- patchPrismaSchema(targetRoot);
614
- patchPrismaMigration(packageRoot, targetRoot);
615
- } else {
616
- const detected = dbAdapter?.id ? `detected: ${dbAdapter.id}` : 'no DB adapter detected';
617
- printDbWarning(
618
- `jwt-auth installed without persistent refresh token store (${detected}). ` +
619
- 'Login/refresh works in stateless mode. Re-run add after supported DB module is installed.',
620
- );
621
- }
622
-
623
363
  patchApiPackage(targetRoot);
624
- patchAppModule(targetRoot, dbAdapter);
364
+ patchAppModule(targetRoot);
625
365
  patchHealthController(targetRoot);
626
366
  patchWebApp(targetRoot);
627
367
  patchApiDockerfile(targetRoot);
628
368
  patchCompose(targetRoot);
629
- patchReadme(targetRoot, dbAdapter);
369
+ patchReadme(targetRoot);
630
370
 
631
371
  upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
632
372
  'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',