create-forgeon 0.2.1 → 0.2.3

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,16 @@
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
+ ensureDependency,
7
+ ensureLineAfter,
8
+ ensureLineBefore,
9
+ ensureLoadItem,
10
+ ensureScript,
11
+ ensureValidatorSchema,
12
+ upsertEnvLines,
13
+ } from './shared/patch-utils.mjs';
4
14
 
5
15
  function copyFromBase(packageRoot, targetRoot, relativePath) {
6
16
  const source = path.join(packageRoot, 'templates', 'base', relativePath);
@@ -20,140 +30,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
20
30
  copyRecursive(source, destination);
21
31
  }
22
32
 
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
33
  function patchApiDockerfile(targetRoot) {
158
34
  const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
159
35
  if (!fs.existsSync(dockerfilePath)) {
@@ -1,9 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
-
5
- const JWT_README_START = '<!-- forgeon:jwt-auth:start -->';
6
- const JWT_README_END = '<!-- forgeon:jwt-auth:end -->';
4
+ import {
5
+ ensureBuildSteps,
6
+ ensureDependency,
7
+ ensureLineAfter,
8
+ ensureLineBefore,
9
+ ensureLoadItem,
10
+ ensureValidatorSchema,
11
+ upsertEnvLines,
12
+ } from './shared/patch-utils.mjs';
7
13
 
8
14
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
9
15
  const source = path.join(packageRoot, 'templates', 'module-presets', 'jwt-auth', relativePath);
@@ -15,177 +21,6 @@ function copyFromPreset(packageRoot, targetRoot, relativePath) {
15
21
  copyRecursive(source, destination);
16
22
  }
17
23
 
18
- function ensureDependency(packageJson, name, version) {
19
- if (!packageJson.dependencies) {
20
- packageJson.dependencies = {};
21
- }
22
- packageJson.dependencies[name] = version;
23
- }
24
-
25
- function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
26
- if (!packageJson.scripts) {
27
- packageJson.scripts = {};
28
- }
29
-
30
- const current = packageJson.scripts[scriptName];
31
- const steps =
32
- typeof current === 'string' && current.trim().length > 0
33
- ? current
34
- .split('&&')
35
- .map((item) => item.trim())
36
- .filter(Boolean)
37
- : [];
38
-
39
- for (const command of requiredCommands) {
40
- if (!steps.includes(command)) {
41
- steps.push(command);
42
- }
43
- }
44
-
45
- if (steps.length > 0) {
46
- packageJson.scripts[scriptName] = steps.join(' && ');
47
- }
48
- }
49
-
50
- function ensureLineAfter(content, anchorLine, lineToInsert) {
51
- if (content.includes(lineToInsert)) {
52
- return content;
53
- }
54
-
55
- const index = content.indexOf(anchorLine);
56
- if (index < 0) {
57
- return `${content.trimEnd()}\n${lineToInsert}\n`;
58
- }
59
-
60
- const insertAt = index + anchorLine.length;
61
- return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
62
- }
63
-
64
- function ensureLineBefore(content, anchorLine, lineToInsert) {
65
- if (content.includes(lineToInsert)) {
66
- return content;
67
- }
68
-
69
- const index = content.indexOf(anchorLine);
70
- if (index < 0) {
71
- return `${content.trimEnd()}\n${lineToInsert}\n`;
72
- }
73
-
74
- return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
75
- }
76
-
77
- function ensureLoadItem(content, itemName) {
78
- const pattern = /load:\s*\[([^\]]*)\]/m;
79
- const match = content.match(pattern);
80
- if (!match) {
81
- return content;
82
- }
83
-
84
- const rawList = match[1];
85
- const items = rawList
86
- .split(',')
87
- .map((item) => item.trim())
88
- .filter(Boolean);
89
-
90
- if (!items.includes(itemName)) {
91
- items.push(itemName);
92
- }
93
-
94
- const next = `load: [${items.join(', ')}]`;
95
- return content.replace(pattern, next);
96
- }
97
-
98
- function ensureValidatorSchema(content, schemaName) {
99
- const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
100
- const match = content.match(pattern);
101
- if (!match) {
102
- return content;
103
- }
104
-
105
- const rawList = match[1];
106
- const items = rawList
107
- .split(',')
108
- .map((item) => item.trim())
109
- .filter(Boolean);
110
-
111
- if (!items.includes(schemaName)) {
112
- items.push(schemaName);
113
- }
114
-
115
- const next = `validate: createEnvValidator([${items.join(', ')}])`;
116
- return content.replace(pattern, next);
117
- }
118
-
119
- function upsertEnvLines(filePath, lines) {
120
- let content = '';
121
- if (fs.existsSync(filePath)) {
122
- content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
123
- }
124
-
125
- const keys = new Set(
126
- content
127
- .split('\n')
128
- .filter(Boolean)
129
- .map((line) => line.split('=')[0]),
130
- );
131
-
132
- const append = [];
133
- for (const line of lines) {
134
- const key = line.split('=')[0];
135
- if (!keys.has(key)) {
136
- append.push(line);
137
- }
138
- }
139
-
140
- const next =
141
- append.length > 0 ? `${content.trimEnd()}\n${append.join('\n')}\n` : `${content.trimEnd()}\n`;
142
- fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
143
- }
144
-
145
- function detectDbAdapter(targetRoot) {
146
- const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
147
- let deps = {};
148
- if (fs.existsSync(apiPackagePath)) {
149
- const packageJson = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
150
- deps = {
151
- ...(packageJson.dependencies ?? {}),
152
- ...(packageJson.devDependencies ?? {}),
153
- };
154
- }
155
-
156
- if (
157
- deps['@forgeon/db-prisma'] ||
158
- fs.existsSync(path.join(targetRoot, 'packages', 'db-prisma', 'package.json'))
159
- ) {
160
- return { id: 'db-prisma', supported: true, tokenStore: 'prisma' };
161
- }
162
-
163
- const dbDeps = Object.keys(deps).filter((name) => name.startsWith('@forgeon/db-'));
164
- if (dbDeps.length > 0) {
165
- return { id: dbDeps[0], supported: false, tokenStore: 'none' };
166
- }
167
-
168
- const packagesPath = path.join(targetRoot, 'packages');
169
- if (fs.existsSync(packagesPath)) {
170
- const localDbPackages = fs
171
- .readdirSync(packagesPath, { withFileTypes: true })
172
- .filter((entry) => entry.isDirectory() && entry.name.startsWith('db-'))
173
- .map((entry) => entry.name);
174
- if (localDbPackages.includes('db-prisma')) {
175
- return { id: 'db-prisma', supported: true, tokenStore: 'prisma' };
176
- }
177
- if (localDbPackages.length > 0) {
178
- return { id: `@forgeon/${localDbPackages[0]}`, supported: false, tokenStore: 'none' };
179
- }
180
- }
181
-
182
- return null;
183
- }
184
-
185
- function printDbWarning(message) {
186
- console.error(`\x1b[31m[create-forgeon add jwt-auth] ${message}\x1b[0m`);
187
- }
188
-
189
24
  function patchApiPackage(targetRoot) {
190
25
  const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
191
26
  if (!fs.existsSync(packagePath)) {
@@ -204,21 +39,19 @@ function patchApiPackage(targetRoot) {
204
39
  writeJson(packagePath, packageJson);
205
40
  }
206
41
 
207
- function patchAppModule(targetRoot, dbAdapter) {
42
+ function patchAppModule(targetRoot) {
208
43
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
209
44
  if (!fs.existsSync(filePath)) {
210
45
  return;
211
46
  }
212
47
 
213
- const withPrismaStore = dbAdapter?.supported === true && dbAdapter?.id === 'db-prisma';
214
-
215
48
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
216
49
  if (!content.includes("from '@forgeon/auth-api';")) {
217
50
  if (content.includes("import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';")) {
218
51
  content = ensureLineAfter(
219
52
  content,
220
53
  "import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';",
221
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
54
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
222
55
  );
223
56
  } else if (
224
57
  content.includes("import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';")
@@ -226,7 +59,7 @@ function patchAppModule(targetRoot, dbAdapter) {
226
59
  content = ensureLineAfter(
227
60
  content,
228
61
  "import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
229
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
62
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
230
63
  );
231
64
  } else if (
232
65
  content.includes("import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';")
@@ -234,7 +67,7 @@ function patchAppModule(targetRoot, dbAdapter) {
234
67
  content = ensureLineAfter(
235
68
  content,
236
69
  "import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';",
237
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
70
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
238
71
  );
239
72
  } else if (
240
73
  content.includes("import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';")
@@ -242,38 +75,22 @@ function patchAppModule(targetRoot, dbAdapter) {
242
75
  content = ensureLineAfter(
243
76
  content,
244
77
  "import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';",
245
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
78
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
246
79
  );
247
80
  } else {
248
81
  content = ensureLineAfter(
249
82
  content,
250
83
  "import { ConfigModule } from '@nestjs/config';",
251
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
84
+ "import { authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
252
85
  );
253
86
  }
254
87
  }
255
88
 
256
- if (withPrismaStore && !content.includes("./auth/prisma-auth-refresh-token.store")) {
257
- content = ensureLineBefore(
258
- content,
259
- "import { HealthController } from './health/health.controller';",
260
- "import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';",
261
- );
262
- }
263
-
264
89
  content = ensureLoadItem(content, 'authConfig');
265
90
  content = ensureValidatorSchema(content, 'authEnvSchema');
266
91
 
267
92
  if (!content.includes('ForgeonAuthModule.register(')) {
268
- const moduleBlock = withPrismaStore
269
- ? ` ForgeonAuthModule.register({
270
- imports: [DbPrismaModule],
271
- refreshTokenStoreProvider: {
272
- provide: AUTH_REFRESH_TOKEN_STORE,
273
- useClass: PrismaAuthRefreshTokenStore,
274
- },
275
- }),`
276
- : ` ForgeonAuthModule.register(),`;
93
+ const moduleBlock = ' ForgeonAuthModule.register(),';
277
94
 
278
95
  if (content.includes(' ForgeonI18nModule.register({')) {
279
96
  content = ensureLineBefore(content, ' ForgeonI18nModule.register({', moduleBlock);
@@ -300,6 +117,7 @@ function patchHealthController(targetRoot) {
300
117
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
301
118
 
302
119
  if (!content.includes("from '@forgeon/auth-api';")) {
120
+ const nestCommonImport = content.match(/import\s*\{[^}]*\}\s*from '@nestjs\/common';/m)?.[0];
303
121
  if (content.includes("import { PrismaService } from '@forgeon/db-prisma';")) {
304
122
  content = ensureLineAfter(
305
123
  content,
@@ -309,7 +127,7 @@ function patchHealthController(targetRoot) {
309
127
  } else {
310
128
  content = ensureLineAfter(
311
129
  content,
312
- "import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';",
130
+ nestCommonImport ?? "import { Controller, Get } from '@nestjs/common';",
313
131
  "import { AuthService } from '@forgeon/auth-api';",
314
132
  );
315
133
  }
@@ -326,6 +144,16 @@ function patchHealthController(targetRoot) {
326
144
  private readonly authService: AuthService,
327
145
  ) {`;
328
146
  content = content.replace(original, next);
147
+ } else {
148
+ const classAnchor = 'export class HealthController {';
149
+ if (content.includes(classAnchor)) {
150
+ content = content.replace(
151
+ classAnchor,
152
+ `${classAnchor}
153
+ constructor(private readonly authService: AuthService) {}
154
+ `,
155
+ );
156
+ }
329
157
  }
330
158
  }
331
159
 
@@ -342,7 +170,12 @@ function patchHealthController(targetRoot) {
342
170
  const index = content.indexOf('private translate(');
343
171
  content = `${content.slice(0, index).trimEnd()}\n\n${method}\n${content.slice(index)}`;
344
172
  } else {
345
- content = `${content.trimEnd()}\n${method}\n`;
173
+ const classEnd = content.lastIndexOf('\n}');
174
+ if (classEnd >= 0) {
175
+ content = `${content.slice(0, classEnd).trimEnd()}\n\n${method}\n${content.slice(classEnd)}`;
176
+ } else {
177
+ content = `${content.trimEnd()}\n${method}\n`;
178
+ }
346
179
  }
347
180
  }
348
181
 
@@ -356,30 +189,58 @@ function patchWebApp(targetRoot) {
356
189
  }
357
190
 
358
191
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
192
+ content = content
193
+ .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
194
+ .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
195
+ .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
196
+ .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
197
+
359
198
  if (!content.includes('authProbeResult')) {
360
- content = content.replace(
361
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
362
- ` const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
199
+ if (content.includes(' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);')) {
200
+ content = content.replace(
201
+ ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
202
+ ` const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);
363
203
  const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);`,
364
- );
204
+ );
205
+ } else if (content.includes(' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);')) {
206
+ content = content.replace(
207
+ ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
208
+ ` const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
209
+ const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);`,
210
+ );
211
+ }
365
212
  }
366
213
 
367
214
  if (!content.includes('Check JWT auth probe')) {
368
215
  const path = content.includes("runProbe(setHealthResult, '/health')") ? '/health/auth' : '/api/health/auth';
369
- content = content.replace(
370
- /<button onClick=\{\(\) => runProbe\(setErrorProbeResult,[\s\S]*?<\/button>/m,
371
- (match) =>
372
- `${match}
373
- <button onClick={() => runProbe(setAuthProbeResult, '${path}')}>Check JWT auth probe</button>`,
374
- );
216
+ const authButton = ` <button onClick={() => runProbe(setAuthProbeResult, '${path}')}>Check JWT auth probe</button>`;
217
+ const actionsStart = content.indexOf('<div className="actions">');
218
+ if (actionsStart >= 0) {
219
+ const actionsEnd = content.indexOf('\n </div>', actionsStart);
220
+ if (actionsEnd >= 0) {
221
+ content = `${content.slice(0, actionsEnd)}\n${authButton}${content.slice(actionsEnd)}`;
222
+ }
223
+ }
375
224
  }
376
225
 
377
226
  if (!content.includes("renderResult('Auth probe response', authProbeResult)")) {
378
- content = content.replace(
379
- "{renderResult('DB probe response', dbProbeResult)}",
380
- `{renderResult('DB probe response', dbProbeResult)}
227
+ const authResultLine = " {renderResult('Auth probe response', authProbeResult)}";
228
+ const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
229
+ if (content.includes(networkLine)) {
230
+ content = content.replace(networkLine, `${authResultLine}\n${networkLine}`);
231
+ } else if (content.includes("{renderResult('DB probe response', dbProbeResult)}")) {
232
+ content = content.replace(
233
+ "{renderResult('DB probe response', dbProbeResult)}",
234
+ `{renderResult('DB probe response', dbProbeResult)}
381
235
  {renderResult('Auth probe response', authProbeResult)}`,
382
- );
236
+ );
237
+ } else if (content.includes("{renderResult('Validation probe response', validationProbeResult)}")) {
238
+ content = content.replace(
239
+ "{renderResult('Validation probe response', validationProbeResult)}",
240
+ `{renderResult('Validation probe response', validationProbeResult)}
241
+ {renderResult('Auth probe response', authProbeResult)}`,
242
+ );
243
+ }
383
244
  }
384
245
 
385
246
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
@@ -463,25 +324,19 @@ function patchCompose(targetRoot) {
463
324
  fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
464
325
  }
465
326
 
466
- function patchReadme(targetRoot, dbAdapter) {
327
+ function patchReadme(targetRoot) {
467
328
  const readmePath = path.join(targetRoot, 'README.md');
468
329
  if (!fs.existsSync(readmePath)) {
469
330
  return;
470
331
  }
471
332
 
472
333
  const persistenceSummary =
473
- dbAdapter?.supported && dbAdapter.id === 'db-prisma'
474
- ? '- refresh token persistence: enabled (`db-prisma` adapter)'
475
- : '- refresh token persistence: disabled (no supported DB adapter found)';
476
- const dbFollowUp =
477
- dbAdapter?.supported && dbAdapter.id === 'db-prisma'
478
- ? '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`'
479
- : `- to enable persistence later:
334
+ '- refresh token persistence: disabled by default (stateless mode)';
335
+ const dbFollowUp = `- to enable persistence later:
480
336
  1. install a DB module first (for now: \`create-forgeon add db-prisma --project .\`);
481
- 2. run \`create-forgeon add jwt-auth --project .\` again to auto-wire the adapter.`;
337
+ 2. run \`pnpm forgeon:sync-integrations\` to auto-wire pair integrations.`;
482
338
 
483
- const section = `${JWT_README_START}
484
- ## JWT Auth Module
339
+ const section = `## JWT Auth Module
485
340
 
486
341
  The jwt-auth add-module provides:
487
342
  - \`@forgeon/auth-contracts\` shared auth routes/types/error codes
@@ -501,13 +356,19 @@ Default routes:
501
356
  - \`POST /api/auth/login\`
502
357
  - \`POST /api/auth/refresh\`
503
358
  - \`POST /api/auth/logout\`
504
- - \`GET /api/auth/me\`
505
- ${JWT_README_END}`;
359
+ - \`GET /api/auth/me\``;
506
360
 
507
361
  let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
508
- const sectionPattern = new RegExp(`${JWT_README_START}[\\s\\S]*?${JWT_README_END}`, 'm');
509
- if (sectionPattern.test(content)) {
510
- content = content.replace(sectionPattern, section);
362
+ const sectionHeading = '## JWT Auth Module';
363
+ if (content.includes(sectionHeading)) {
364
+ const start = content.indexOf(sectionHeading);
365
+ const tail = content.slice(start + sectionHeading.length);
366
+ const nextHeadingMatch = tail.match(/\n##\s+/);
367
+ const end =
368
+ nextHeadingMatch && nextHeadingMatch.index !== undefined
369
+ ? start + sectionHeading.length + nextHeadingMatch.index + 1
370
+ : content.length;
371
+ content = `${content.slice(0, start)}${section}\n\n${content.slice(end).replace(/^\n+/, '')}`;
511
372
  } else if (content.includes('## Prisma In Docker Start')) {
512
373
  content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
513
374
  } else {
@@ -517,78 +378,17 @@ ${JWT_README_END}`;
517
378
  fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
518
379
  }
519
380
 
520
- function patchPrismaSchema(targetRoot) {
521
- const schemaPath = path.join(targetRoot, 'apps', 'api', 'prisma', 'schema.prisma');
522
- if (!fs.existsSync(schemaPath)) {
523
- return;
524
- }
525
-
526
- let content = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
527
- if (!content.includes('refreshTokenHash')) {
528
- content = content.replace(
529
- /email\s+String\s+@unique/g,
530
- 'email String @unique\n refreshTokenHash String?',
531
- );
532
- fs.writeFileSync(schemaPath, `${content.trimEnd()}\n`, 'utf8');
533
- }
534
- }
535
-
536
- function patchPrismaMigration(packageRoot, targetRoot) {
537
- const migrationSource = path.join(
538
- packageRoot,
539
- 'templates',
540
- 'module-presets',
541
- 'jwt-auth',
542
- 'apps',
543
- 'api',
544
- 'prisma',
545
- 'migrations',
546
- '0002_auth_refresh_token_hash',
547
- );
548
- const migrationTarget = path.join(
549
- targetRoot,
550
- 'apps',
551
- 'api',
552
- 'prisma',
553
- 'migrations',
554
- '0002_auth_refresh_token_hash',
555
- );
556
-
557
- if (!fs.existsSync(migrationTarget) && fs.existsSync(migrationSource)) {
558
- copyRecursive(migrationSource, migrationTarget);
559
- }
560
- }
561
-
562
381
  export function applyJwtAuthModule({ packageRoot, targetRoot }) {
563
- const dbAdapter = detectDbAdapter(targetRoot);
564
- const supportsPrismaStore = dbAdapter?.supported === true && dbAdapter?.id === 'db-prisma';
565
-
566
382
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-contracts'));
567
383
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'auth-api'));
568
384
 
569
- if (supportsPrismaStore) {
570
- copyFromPreset(
571
- packageRoot,
572
- targetRoot,
573
- path.join('apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts'),
574
- );
575
- patchPrismaSchema(targetRoot);
576
- patchPrismaMigration(packageRoot, targetRoot);
577
- } else {
578
- const detected = dbAdapter?.id ? `detected: ${dbAdapter.id}` : 'no DB adapter detected';
579
- printDbWarning(
580
- `jwt-auth installed without persistent refresh token store (${detected}). ` +
581
- 'Login/refresh works in stateless mode. Re-run add after supported DB module is installed.',
582
- );
583
- }
584
-
585
385
  patchApiPackage(targetRoot);
586
- patchAppModule(targetRoot, dbAdapter);
386
+ patchAppModule(targetRoot);
587
387
  patchHealthController(targetRoot);
588
388
  patchWebApp(targetRoot);
589
389
  patchApiDockerfile(targetRoot);
590
390
  patchCompose(targetRoot);
591
- patchReadme(targetRoot, dbAdapter);
391
+ patchReadme(targetRoot);
592
392
 
593
393
  upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
594
394
  'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',