create-forgeon 0.1.2 → 0.1.6

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 (143) hide show
  1. package/README.md +19 -17
  2. package/bin/create-forgeon.mjs +22 -22
  3. package/package.json +1 -1
  4. package/src/cli/add-help.mjs +12 -12
  5. package/src/cli/add-options.mjs +54 -54
  6. package/src/cli/add-options.test.mjs +24 -24
  7. package/src/cli/help.mjs +20 -20
  8. package/src/cli/options.mjs +121 -121
  9. package/src/cli/options.test.mjs +41 -41
  10. package/src/cli/prompt-select.mjs +94 -94
  11. package/src/cli/prompt-select.test.mjs +148 -148
  12. package/src/constants.mjs +13 -13
  13. package/src/core/docs.mjs +128 -128
  14. package/src/core/docs.test.mjs +91 -91
  15. package/src/core/install.mjs +14 -14
  16. package/src/core/scaffold.mjs +48 -45
  17. package/src/core/validate.mjs +12 -12
  18. package/src/core/validate.test.mjs +73 -73
  19. package/src/databases/index.mjs +26 -26
  20. package/src/frameworks/index.mjs +32 -32
  21. package/src/infrastructure/proxy.mjs +12 -12
  22. package/src/modules/docs.mjs +70 -70
  23. package/src/modules/executor.mjs +39 -21
  24. package/src/modules/executor.test.mjs +95 -45
  25. package/src/modules/i18n.mjs +283 -0
  26. package/src/modules/registry.mjs +43 -35
  27. package/src/presets/i18n.mjs +228 -180
  28. package/src/presets/index.mjs +2 -2
  29. package/src/presets/proxy.mjs +32 -32
  30. package/src/run-add-module.mjs +47 -47
  31. package/src/run-create-forgeon.mjs +72 -72
  32. package/src/utils/fs.mjs +26 -26
  33. package/src/utils/values.mjs +20 -20
  34. package/templates/base/.dockerignore +7 -7
  35. package/templates/base/.editorconfig +11 -11
  36. package/templates/base/README.md +46 -46
  37. package/templates/base/apps/api/Dockerfile +24 -24
  38. package/templates/base/apps/api/package.json +39 -39
  39. package/templates/base/apps/api/prisma/migrations/0001_init/migration.sql +11 -11
  40. package/templates/base/apps/api/prisma/schema.prisma +14 -14
  41. package/templates/base/apps/api/prisma/seed.ts +19 -19
  42. package/templates/base/apps/api/src/app.module.ts +32 -32
  43. package/templates/base/apps/api/src/common/dto/echo-query.dto.ts +5 -5
  44. package/templates/base/apps/api/src/common/filters/app-exception.filter.ts +129 -129
  45. package/templates/base/apps/api/src/config/app.config.ts +12 -12
  46. package/templates/base/apps/api/src/health/health.controller.ts +30 -30
  47. package/templates/base/apps/api/src/main.ts +25 -25
  48. package/templates/base/apps/api/src/prisma/prisma.module.ts +8 -8
  49. package/templates/base/apps/api/src/prisma/prisma.service.ts +26 -26
  50. package/templates/base/apps/api/tsconfig.build.json +8 -8
  51. package/templates/base/apps/api/tsconfig.json +8 -8
  52. package/templates/base/apps/web/Dockerfile +12 -12
  53. package/templates/base/apps/web/index.html +11 -11
  54. package/templates/base/apps/web/package.json +21 -21
  55. package/templates/base/apps/web/src/App.tsx +35 -35
  56. package/templates/base/apps/web/src/main.tsx +8 -8
  57. package/templates/base/apps/web/src/styles.css +32 -32
  58. package/templates/base/apps/web/tsconfig.json +17 -17
  59. package/templates/base/apps/web/vite.config.ts +14 -14
  60. package/templates/base/docs/AI/ARCHITECTURE.md +37 -37
  61. package/templates/base/docs/AI/MODULE_SPEC.md +56 -56
  62. package/templates/base/docs/AI/PROJECT.md +31 -31
  63. package/templates/base/docs/AI/TASKS.md +57 -57
  64. package/templates/base/docs/README.md +6 -6
  65. package/templates/base/infra/caddy/Caddyfile +15 -15
  66. package/templates/base/infra/docker/.env.example +9 -9
  67. package/templates/base/infra/docker/caddy.Dockerfile +15 -15
  68. package/templates/base/infra/docker/compose.caddy.yml +44 -44
  69. package/templates/base/infra/docker/compose.nginx.yml +44 -44
  70. package/templates/base/infra/docker/compose.none.yml +37 -37
  71. package/templates/base/infra/docker/compose.yml +44 -44
  72. package/templates/base/infra/docker/nginx.Dockerfile +15 -15
  73. package/templates/base/infra/nginx/nginx.conf +31 -31
  74. package/templates/base/package.json +23 -23
  75. package/templates/base/packages/core/README.md +2 -2
  76. package/templates/base/packages/core/package.json +13 -13
  77. package/templates/base/packages/core/tsconfig.json +7 -7
  78. package/templates/base/packages/i18n/package.json +18 -18
  79. package/templates/base/packages/i18n/src/forgeon-i18n.module.ts +45 -45
  80. package/templates/base/packages/i18n/tsconfig.json +8 -8
  81. package/templates/base/pnpm-workspace.yaml +2 -2
  82. package/templates/base/resources/i18n/en/common.json +4 -4
  83. package/templates/base/resources/i18n/en/validation.json +2 -2
  84. package/templates/base/resources/i18n/uk/common.json +4 -4
  85. package/templates/base/resources/i18n/uk/validation.json +2 -2
  86. package/templates/base/tsconfig.base.json +16 -16
  87. package/templates/docs-fragments/AI_ARCHITECTURE/00_title.md +1 -1
  88. package/templates/docs-fragments/AI_ARCHITECTURE/10_layout_base.md +6 -6
  89. package/templates/docs-fragments/AI_ARCHITECTURE/11_layout_infra.md +1 -1
  90. package/templates/docs-fragments/AI_ARCHITECTURE/12_layout_i18n_resources.md +1 -1
  91. package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +4 -4
  92. package/templates/docs-fragments/AI_ARCHITECTURE/21_env_i18n.md +3 -3
  93. package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +7 -7
  94. package/templates/docs-fragments/AI_ARCHITECTURE/31_docker_runtime.md +5 -5
  95. package/templates/docs-fragments/AI_ARCHITECTURE/32_scope_freeze.md +5 -5
  96. package/templates/docs-fragments/AI_ARCHITECTURE/40_docs_generation.md +9 -9
  97. package/templates/docs-fragments/AI_ARCHITECTURE/50_extension_points.md +8 -8
  98. package/templates/docs-fragments/AI_PROJECT/00_title.md +1 -1
  99. package/templates/docs-fragments/AI_PROJECT/10_what_is.md +3 -3
  100. package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +5 -5
  101. package/templates/docs-fragments/AI_PROJECT/21_structure_i18n.md +2 -0
  102. package/templates/docs-fragments/AI_PROJECT/22_structure_docker.md +1 -1
  103. package/templates/docs-fragments/AI_PROJECT/23_structure_docs.md +1 -1
  104. package/templates/docs-fragments/AI_PROJECT/30_run_dev.md +8 -8
  105. package/templates/docs-fragments/AI_PROJECT/31_run_docker.md +5 -5
  106. package/templates/docs-fragments/AI_PROJECT/32_proxy_notes.md +5 -5
  107. package/templates/docs-fragments/AI_PROJECT/32_proxy_notes_none.md +5 -5
  108. package/templates/docs-fragments/AI_PROJECT/33_i18n_notes.md +2 -0
  109. package/templates/docs-fragments/AI_PROJECT/40_change_boundaries_base.md +3 -3
  110. package/templates/docs-fragments/AI_PROJECT/41_change_boundaries_docker.md +1 -1
  111. package/templates/docs-fragments/README/00_title.md +3 -3
  112. package/templates/docs-fragments/README/10_stack.md +8 -8
  113. package/templates/docs-fragments/README/20_quick_start_dev_intro.md +6 -6
  114. package/templates/docs-fragments/README/21_quick_start_dev_db_docker.md +4 -4
  115. package/templates/docs-fragments/README/21_quick_start_dev_db_local.md +1 -1
  116. package/templates/docs-fragments/README/22_quick_start_dev_outro.md +7 -7
  117. package/templates/docs-fragments/README/30_quick_start_docker.md +7 -7
  118. package/templates/docs-fragments/README/30_quick_start_docker_none.md +9 -9
  119. package/templates/docs-fragments/README/31_proxy_preset_caddy.md +9 -9
  120. package/templates/docs-fragments/README/31_proxy_preset_nginx.md +8 -8
  121. package/templates/docs-fragments/README/31_proxy_preset_none.md +6 -6
  122. package/templates/docs-fragments/README/32_prisma_container_start.md +5 -5
  123. package/templates/docs-fragments/README/40_i18n.md +14 -8
  124. package/templates/docs-fragments/README/90_next_steps.md +7 -7
  125. package/templates/module-fragments/i18n/00_title.md +5 -0
  126. package/templates/module-fragments/i18n/10_overview.md +9 -0
  127. package/templates/module-fragments/i18n/20_scope.md +7 -0
  128. package/templates/module-fragments/i18n/90_status_implemented.md +3 -0
  129. package/templates/module-fragments/jwt-auth/00_title.md +1 -1
  130. package/templates/module-fragments/jwt-auth/10_overview.md +6 -6
  131. package/templates/module-fragments/jwt-auth/20_scope.md +7 -7
  132. package/templates/module-fragments/jwt-auth/90_status_planned.md +3 -3
  133. package/templates/module-fragments/queue/00_title.md +1 -1
  134. package/templates/module-fragments/queue/10_overview.md +6 -6
  135. package/templates/module-fragments/queue/20_scope.md +7 -7
  136. package/templates/module-fragments/queue/90_status_planned.md +3 -3
  137. package/templates/module-presets/i18n/apps/web/src/App.tsx +61 -0
  138. package/templates/module-presets/i18n/packages/i18n-contracts/package.json +14 -0
  139. package/templates/module-presets/i18n/packages/i18n-contracts/src/index.ts +7 -0
  140. package/templates/module-presets/i18n/packages/i18n-contracts/tsconfig.json +8 -0
  141. package/templates/module-presets/i18n/packages/i18n-web/package.json +17 -0
  142. package/templates/module-presets/i18n/packages/i18n-web/src/index.ts +50 -0
  143. package/templates/module-presets/i18n/packages/i18n-web/tsconfig.json +8 -0
@@ -1,203 +1,251 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { removeIfExists, writeJson } from '../utils/fs.mjs';
4
-
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { removeIfExists, writeJson } from '../utils/fs.mjs';
4
+
5
5
  export function applyI18nDisabled(targetRoot) {
6
6
  removeIfExists(path.join(targetRoot, 'packages', 'i18n'));
7
+ removeIfExists(path.join(targetRoot, 'packages', 'i18n-contracts'));
8
+ removeIfExists(path.join(targetRoot, 'packages', 'i18n-web'));
7
9
  removeIfExists(path.join(targetRoot, 'resources', 'i18n'));
8
-
9
- const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
10
- if (fs.existsSync(apiPackagePath)) {
11
- const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
12
-
13
- if (apiPackage.scripts) {
14
- delete apiPackage.scripts.predev;
15
- }
16
-
10
+
11
+ const apiPackagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
12
+ if (fs.existsSync(apiPackagePath)) {
13
+ const apiPackage = JSON.parse(fs.readFileSync(apiPackagePath, 'utf8'));
14
+
15
+ if (apiPackage.scripts) {
16
+ delete apiPackage.scripts.predev;
17
+ }
18
+
17
19
  if (apiPackage.dependencies) {
18
20
  delete apiPackage.dependencies['@forgeon/i18n'];
21
+ delete apiPackage.dependencies['@forgeon/i18n-contracts'];
19
22
  delete apiPackage.dependencies['nestjs-i18n'];
20
23
  }
21
-
22
- writeJson(apiPackagePath, apiPackage);
23
- }
24
-
25
- const apiDockerfile = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
24
+
25
+ writeJson(apiPackagePath, apiPackage);
26
+ }
27
+
28
+ const apiDockerfile = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
26
29
  if (fs.existsSync(apiDockerfile)) {
27
30
  let content = fs.readFileSync(apiDockerfile, 'utf8');
28
31
  content = content
29
32
  .replace(/^COPY packages\/i18n\/package\.json packages\/i18n\/package\.json\r?\n/gm, '')
33
+ .replace(
34
+ /^COPY packages\/i18n-contracts\/package\.json packages\/i18n-contracts\/package\.json\r?\n/gm,
35
+ '',
36
+ )
30
37
  .replace(/^COPY packages\/i18n packages\/i18n\r?\n/gm, '')
31
- .replace(/^RUN pnpm --filter @forgeon\/i18n build\r?\n/gm, '');
38
+ .replace(/^COPY packages\/i18n-contracts packages\/i18n-contracts\r?\n/gm, '')
39
+ .replace(/^RUN pnpm --filter @forgeon\/i18n build\r?\n/gm, '')
40
+ .replace(/^RUN pnpm --filter @forgeon\/i18n-contracts build\r?\n/gm, '');
32
41
  fs.writeFileSync(apiDockerfile, content, 'utf8');
33
42
  }
34
43
 
35
- const appModulePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
36
- fs.writeFileSync(
37
- appModulePath,
38
- `import { Module } from '@nestjs/common';
39
- import { ConfigModule } from '@nestjs/config';
40
- import appConfig from './config/app.config';
41
- import { HealthController } from './health/health.controller';
42
- import { PrismaModule } from './prisma/prisma.module';
43
- import { AppExceptionFilter } from './common/filters/app-exception.filter';
44
-
45
- @Module({
46
- imports: [
47
- ConfigModule.forRoot({
48
- isGlobal: true,
49
- load: [appConfig],
50
- envFilePath: '.env',
51
- }),
52
- PrismaModule,
53
- ],
54
- controllers: [HealthController],
55
- providers: [AppExceptionFilter],
56
- })
57
- export class AppModule {}
58
- `,
59
- 'utf8',
60
- );
61
-
62
- const healthControllerPath = path.join(
63
- targetRoot,
64
- 'apps',
65
- 'api',
66
- 'src',
67
- 'health',
68
- 'health.controller.ts',
69
- );
70
- fs.writeFileSync(
71
- healthControllerPath,
72
- `import { Controller, Get, Query } from '@nestjs/common';
73
- import { EchoQueryDto } from '../common/dto/echo-query.dto';
74
-
75
- @Controller('health')
76
- export class HealthController {
77
- @Get()
78
- getHealth(@Query('lang') _lang?: string) {
79
- return {
80
- status: 'ok',
81
- message: 'OK',
82
- };
83
- }
44
+ const proxyDockerfiles = [
45
+ path.join(targetRoot, 'infra', 'docker', 'caddy.Dockerfile'),
46
+ path.join(targetRoot, 'infra', 'docker', 'nginx.Dockerfile'),
47
+ ];
48
+ for (const dockerfilePath of proxyDockerfiles) {
49
+ if (!fs.existsSync(dockerfilePath)) {
50
+ continue;
51
+ }
84
52
 
85
- @Get('echo')
86
- getEcho(@Query() query: EchoQueryDto) {
87
- return { value: query.value };
53
+ const content = fs
54
+ .readFileSync(dockerfilePath, 'utf8')
55
+ .replace(
56
+ /^COPY packages\/i18n-contracts\/package\.json packages\/i18n-contracts\/package\.json\r?\n/gm,
57
+ '',
58
+ )
59
+ .replace(/^COPY packages\/i18n-web\/package\.json packages\/i18n-web\/package\.json\r?\n/gm, '')
60
+ .replace(/^COPY packages\/i18n-contracts packages\/i18n-contracts\r?\n/gm, '')
61
+ .replace(/^COPY packages\/i18n-web packages\/i18n-web\r?\n/gm, '');
62
+
63
+ fs.writeFileSync(dockerfilePath, content, 'utf8');
88
64
  }
89
- }
90
- `,
91
- 'utf8',
92
- );
93
65
 
94
- const filterPath = path.join(
95
- targetRoot,
96
- 'apps',
97
- 'api',
98
- 'src',
99
- 'common',
100
- 'filters',
101
- 'app-exception.filter.ts',
102
- );
103
- fs.writeFileSync(
104
- filterPath,
105
- `import {
106
- ArgumentsHost,
107
- Catch,
108
- ExceptionFilter,
109
- HttpException,
110
- HttpStatus,
111
- Injectable,
112
- } from '@nestjs/common';
113
- import { Response } from 'express';
66
+ const webPackagePath = path.join(targetRoot, 'apps', 'web', 'package.json');
67
+ if (fs.existsSync(webPackagePath)) {
68
+ const webPackage = JSON.parse(fs.readFileSync(webPackagePath, 'utf8'));
114
69
 
115
- @Injectable()
116
- @Catch()
117
- export class AppExceptionFilter implements ExceptionFilter {
118
- catch(exception: unknown, host: ArgumentsHost): void {
119
- const context = host.switchToHttp();
120
- const response = context.getResponse<Response>();
121
-
122
- const status =
123
- exception instanceof HttpException
124
- ? exception.getStatus()
125
- : HttpStatus.INTERNAL_SERVER_ERROR;
126
-
127
- const payload =
128
- exception instanceof HttpException
129
- ? exception.getResponse()
130
- : { message: 'Internal server error' };
131
-
132
- const message =
133
- typeof payload === 'object' && payload !== null && 'message' in payload
134
- ? Array.isArray((payload as { message?: unknown }).message)
135
- ? String((payload as { message: unknown[] }).message[0] ?? 'Internal server error')
136
- : String((payload as { message?: unknown }).message ?? 'Internal server error')
137
- : typeof payload === 'string'
138
- ? payload
139
- : 'Internal server error';
140
-
141
- response.status(status).json({
142
- error: {
143
- code: this.resolveCode(status),
144
- message,
145
- },
146
- });
147
- }
148
-
149
- private resolveCode(status: number): string {
150
- switch (status) {
151
- case HttpStatus.BAD_REQUEST:
152
- return 'validation_error';
153
- case HttpStatus.UNAUTHORIZED:
154
- return 'unauthorized';
155
- case HttpStatus.FORBIDDEN:
156
- return 'forbidden';
157
- case HttpStatus.NOT_FOUND:
158
- return 'not_found';
159
- case HttpStatus.CONFLICT:
160
- return 'conflict';
161
- default:
162
- return 'internal_error';
70
+ if (webPackage.scripts) {
71
+ delete webPackage.scripts.predev;
72
+ delete webPackage.scripts.prebuild;
163
73
  }
164
- }
165
- }
166
- `,
167
- 'utf8',
168
- );
169
74
 
170
- const appConfigPath = path.join(targetRoot, 'apps', 'api', 'src', 'config', 'app.config.ts');
171
- fs.writeFileSync(
172
- appConfigPath,
173
- `import { registerAs } from '@nestjs/config';
174
-
175
- export default registerAs('app', () => ({
176
- port: Number(process.env.PORT ?? 3000),
177
- }));
178
- `,
179
- 'utf8',
180
- );
181
- }
182
-
183
- export function patchDockerEnvForI18n(targetRoot, i18nEnabled) {
184
- const dockerEnvPath = path.join(targetRoot, 'infra', 'docker', '.env.example');
185
- if (fs.existsSync(dockerEnvPath) && !i18nEnabled) {
186
- const content = fs
187
- .readFileSync(dockerEnvPath, 'utf8')
188
- .replace(/^I18N_ENABLED=.*\r?\n/gm, '')
189
- .replace(/^I18N_DEFAULT_LANG=.*\r?\n/gm, '')
190
- .replace(/^I18N_FALLBACK_LANG=.*\r?\n/gm, '');
191
- fs.writeFileSync(dockerEnvPath, content.trimEnd() + '\n', 'utf8');
192
- }
75
+ if (webPackage.dependencies) {
76
+ delete webPackage.dependencies['@forgeon/i18n-contracts'];
77
+ delete webPackage.dependencies['@forgeon/i18n-web'];
78
+ }
193
79
 
194
- const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
195
- if (fs.existsSync(composePath) && !i18nEnabled) {
196
- const content = fs
197
- .readFileSync(composePath, 'utf8')
198
- .replace(/^\s+I18N_ENABLED:.*\r?\n/gm, '')
199
- .replace(/^\s+I18N_DEFAULT_LANG:.*\r?\n/gm, '')
200
- .replace(/^\s+I18N_FALLBACK_LANG:.*\r?\n/gm, '');
201
- fs.writeFileSync(composePath, content, 'utf8');
80
+ writeJson(webPackagePath, webPackage);
202
81
  }
203
- }
82
+
83
+ const appModulePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
84
+ fs.writeFileSync(
85
+ appModulePath,
86
+ `import { Module } from '@nestjs/common';
87
+ import { ConfigModule } from '@nestjs/config';
88
+ import appConfig from './config/app.config';
89
+ import { HealthController } from './health/health.controller';
90
+ import { PrismaModule } from './prisma/prisma.module';
91
+ import { AppExceptionFilter } from './common/filters/app-exception.filter';
92
+
93
+ @Module({
94
+ imports: [
95
+ ConfigModule.forRoot({
96
+ isGlobal: true,
97
+ load: [appConfig],
98
+ envFilePath: '.env',
99
+ }),
100
+ PrismaModule,
101
+ ],
102
+ controllers: [HealthController],
103
+ providers: [AppExceptionFilter],
104
+ })
105
+ export class AppModule {}
106
+ `,
107
+ 'utf8',
108
+ );
109
+
110
+ const healthControllerPath = path.join(
111
+ targetRoot,
112
+ 'apps',
113
+ 'api',
114
+ 'src',
115
+ 'health',
116
+ 'health.controller.ts',
117
+ );
118
+ fs.writeFileSync(
119
+ healthControllerPath,
120
+ `import { Controller, Get, Query } from '@nestjs/common';
121
+ import { EchoQueryDto } from '../common/dto/echo-query.dto';
122
+
123
+ @Controller('health')
124
+ export class HealthController {
125
+ @Get()
126
+ getHealth(@Query('lang') _lang?: string) {
127
+ return {
128
+ status: 'ok',
129
+ message: 'OK',
130
+ };
131
+ }
132
+
133
+ @Get('echo')
134
+ getEcho(@Query() query: EchoQueryDto) {
135
+ return { value: query.value };
136
+ }
137
+ }
138
+ `,
139
+ 'utf8',
140
+ );
141
+
142
+ const filterPath = path.join(
143
+ targetRoot,
144
+ 'apps',
145
+ 'api',
146
+ 'src',
147
+ 'common',
148
+ 'filters',
149
+ 'app-exception.filter.ts',
150
+ );
151
+ fs.writeFileSync(
152
+ filterPath,
153
+ `import {
154
+ ArgumentsHost,
155
+ Catch,
156
+ ExceptionFilter,
157
+ HttpException,
158
+ HttpStatus,
159
+ Injectable,
160
+ } from '@nestjs/common';
161
+ import { Response } from 'express';
162
+
163
+ @Injectable()
164
+ @Catch()
165
+ export class AppExceptionFilter implements ExceptionFilter {
166
+ catch(exception: unknown, host: ArgumentsHost): void {
167
+ const context = host.switchToHttp();
168
+ const response = context.getResponse<Response>();
169
+
170
+ const status =
171
+ exception instanceof HttpException
172
+ ? exception.getStatus()
173
+ : HttpStatus.INTERNAL_SERVER_ERROR;
174
+
175
+ const payload =
176
+ exception instanceof HttpException
177
+ ? exception.getResponse()
178
+ : { message: 'Internal server error' };
179
+
180
+ const message =
181
+ typeof payload === 'object' && payload !== null && 'message' in payload
182
+ ? Array.isArray((payload as { message?: unknown }).message)
183
+ ? String((payload as { message: unknown[] }).message[0] ?? 'Internal server error')
184
+ : String((payload as { message?: unknown }).message ?? 'Internal server error')
185
+ : typeof payload === 'string'
186
+ ? payload
187
+ : 'Internal server error';
188
+
189
+ response.status(status).json({
190
+ error: {
191
+ code: this.resolveCode(status),
192
+ message,
193
+ },
194
+ });
195
+ }
196
+
197
+ private resolveCode(status: number): string {
198
+ switch (status) {
199
+ case HttpStatus.BAD_REQUEST:
200
+ return 'validation_error';
201
+ case HttpStatus.UNAUTHORIZED:
202
+ return 'unauthorized';
203
+ case HttpStatus.FORBIDDEN:
204
+ return 'forbidden';
205
+ case HttpStatus.NOT_FOUND:
206
+ return 'not_found';
207
+ case HttpStatus.CONFLICT:
208
+ return 'conflict';
209
+ default:
210
+ return 'internal_error';
211
+ }
212
+ }
213
+ }
214
+ `,
215
+ 'utf8',
216
+ );
217
+
218
+ const appConfigPath = path.join(targetRoot, 'apps', 'api', 'src', 'config', 'app.config.ts');
219
+ fs.writeFileSync(
220
+ appConfigPath,
221
+ `import { registerAs } from '@nestjs/config';
222
+
223
+ export default registerAs('app', () => ({
224
+ port: Number(process.env.PORT ?? 3000),
225
+ }));
226
+ `,
227
+ 'utf8',
228
+ );
229
+ }
230
+
231
+ export function patchDockerEnvForI18n(targetRoot, i18nEnabled) {
232
+ const dockerEnvPath = path.join(targetRoot, 'infra', 'docker', '.env.example');
233
+ if (fs.existsSync(dockerEnvPath) && !i18nEnabled) {
234
+ const content = fs
235
+ .readFileSync(dockerEnvPath, 'utf8')
236
+ .replace(/^I18N_ENABLED=.*\r?\n/gm, '')
237
+ .replace(/^I18N_DEFAULT_LANG=.*\r?\n/gm, '')
238
+ .replace(/^I18N_FALLBACK_LANG=.*\r?\n/gm, '');
239
+ fs.writeFileSync(dockerEnvPath, content.trimEnd() + '\n', 'utf8');
240
+ }
241
+
242
+ const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
243
+ if (fs.existsSync(composePath) && !i18nEnabled) {
244
+ const content = fs
245
+ .readFileSync(composePath, 'utf8')
246
+ .replace(/^\s+I18N_ENABLED:.*\r?\n/gm, '')
247
+ .replace(/^\s+I18N_DEFAULT_LANG:.*\r?\n/gm, '')
248
+ .replace(/^\s+I18N_FALLBACK_LANG:.*\r?\n/gm, '');
249
+ fs.writeFileSync(composePath, content, 'utf8');
250
+ }
251
+ }
@@ -1,2 +1,2 @@
1
- export { applyProxyPreset } from './proxy.mjs';
2
- export { applyI18nDisabled, patchDockerEnvForI18n } from './i18n.mjs';
1
+ export { applyProxyPreset } from './proxy.mjs';
2
+ export { applyI18nDisabled, patchDockerEnvForI18n } from './i18n.mjs';
@@ -1,32 +1,32 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { removeIfExists } from '../utils/fs.mjs';
4
-
5
- export function applyProxyPreset(targetRoot, proxy) {
6
- const dockerDir = path.join(targetRoot, 'infra', 'docker');
7
- const composeTarget = path.join(dockerDir, 'compose.yml');
8
- const composeSource = path.join(dockerDir, `compose.${proxy}.yml`);
9
-
10
- if (!fs.existsSync(composeSource)) {
11
- throw new Error(`Missing proxy compose preset: ${composeSource}`);
12
- }
13
-
14
- fs.copyFileSync(composeSource, composeTarget);
15
-
16
- removeIfExists(path.join(dockerDir, 'compose.nginx.yml'));
17
- removeIfExists(path.join(dockerDir, 'compose.caddy.yml'));
18
- removeIfExists(path.join(dockerDir, 'compose.none.yml'));
19
-
20
- if (proxy === 'nginx') {
21
- removeIfExists(path.join(dockerDir, 'caddy.Dockerfile'));
22
- removeIfExists(path.join(targetRoot, 'infra', 'caddy'));
23
- } else if (proxy === 'caddy') {
24
- removeIfExists(path.join(dockerDir, 'nginx.Dockerfile'));
25
- removeIfExists(path.join(targetRoot, 'infra', 'nginx'));
26
- } else if (proxy === 'none') {
27
- removeIfExists(path.join(dockerDir, 'nginx.Dockerfile'));
28
- removeIfExists(path.join(dockerDir, 'caddy.Dockerfile'));
29
- removeIfExists(path.join(targetRoot, 'infra', 'nginx'));
30
- removeIfExists(path.join(targetRoot, 'infra', 'caddy'));
31
- }
32
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { removeIfExists } from '../utils/fs.mjs';
4
+
5
+ export function applyProxyPreset(targetRoot, proxy) {
6
+ const dockerDir = path.join(targetRoot, 'infra', 'docker');
7
+ const composeTarget = path.join(dockerDir, 'compose.yml');
8
+ const composeSource = path.join(dockerDir, `compose.${proxy}.yml`);
9
+
10
+ if (!fs.existsSync(composeSource)) {
11
+ throw new Error(`Missing proxy compose preset: ${composeSource}`);
12
+ }
13
+
14
+ fs.copyFileSync(composeSource, composeTarget);
15
+
16
+ removeIfExists(path.join(dockerDir, 'compose.nginx.yml'));
17
+ removeIfExists(path.join(dockerDir, 'compose.caddy.yml'));
18
+ removeIfExists(path.join(dockerDir, 'compose.none.yml'));
19
+
20
+ if (proxy === 'nginx') {
21
+ removeIfExists(path.join(dockerDir, 'caddy.Dockerfile'));
22
+ removeIfExists(path.join(targetRoot, 'infra', 'caddy'));
23
+ } else if (proxy === 'caddy') {
24
+ removeIfExists(path.join(dockerDir, 'nginx.Dockerfile'));
25
+ removeIfExists(path.join(targetRoot, 'infra', 'nginx'));
26
+ } else if (proxy === 'none') {
27
+ removeIfExists(path.join(dockerDir, 'nginx.Dockerfile'));
28
+ removeIfExists(path.join(dockerDir, 'caddy.Dockerfile'));
29
+ removeIfExists(path.join(targetRoot, 'infra', 'nginx'));
30
+ removeIfExists(path.join(targetRoot, 'infra', 'caddy'));
31
+ }
32
+ }
@@ -1,47 +1,47 @@
1
- import path from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
- import { printAddHelp } from './cli/add-help.mjs';
4
- import { parseAddCliArgs } from './cli/add-options.mjs';
5
- import { addModule } from './modules/executor.mjs';
6
- import { listModulePresets } from './modules/registry.mjs';
7
-
8
- function printModuleList() {
9
- const modules = listModulePresets();
10
- console.log('Available modules:');
11
- for (const moduleItem of modules) {
12
- const status = moduleItem.implemented ? 'implemented' : 'planned';
13
- console.log(`- ${moduleItem.id} (${status}) - ${moduleItem.description}`);
14
- }
15
- }
16
-
17
- export async function runAddModule(argv = process.argv.slice(2)) {
18
- const options = parseAddCliArgs(argv);
19
-
20
- if (options.help) {
21
- printAddHelp();
22
- return;
23
- }
24
-
25
- if (options.list) {
26
- printModuleList();
27
- return;
28
- }
29
-
30
- if (!options.moduleId) {
31
- throw new Error('Module id is required. Use `create-forgeon add --list` to see available modules.');
32
- }
33
-
34
- const srcDir = path.dirname(fileURLToPath(import.meta.url));
35
- const packageRoot = path.resolve(srcDir, '..');
36
- const targetRoot = path.resolve(process.cwd(), options.project);
37
-
38
- const result = addModule({
39
- moduleId: options.moduleId,
40
- targetRoot,
41
- packageRoot,
42
- });
43
-
44
- console.log(result.message);
45
- console.log(`- module: ${result.preset.id}`);
46
- console.log(`- docs: ${result.docsPath}`);
47
- }
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { printAddHelp } from './cli/add-help.mjs';
4
+ import { parseAddCliArgs } from './cli/add-options.mjs';
5
+ import { addModule } from './modules/executor.mjs';
6
+ import { listModulePresets } from './modules/registry.mjs';
7
+
8
+ function printModuleList() {
9
+ const modules = listModulePresets();
10
+ console.log('Available modules:');
11
+ for (const moduleItem of modules) {
12
+ const status = moduleItem.implemented ? 'implemented' : 'planned';
13
+ console.log(`- ${moduleItem.id} (${status}) - ${moduleItem.description}`);
14
+ }
15
+ }
16
+
17
+ export async function runAddModule(argv = process.argv.slice(2)) {
18
+ const options = parseAddCliArgs(argv);
19
+
20
+ if (options.help) {
21
+ printAddHelp();
22
+ return;
23
+ }
24
+
25
+ if (options.list) {
26
+ printModuleList();
27
+ return;
28
+ }
29
+
30
+ if (!options.moduleId) {
31
+ throw new Error('Module id is required. Use `create-forgeon add --list` to see available modules.');
32
+ }
33
+
34
+ const srcDir = path.dirname(fileURLToPath(import.meta.url));
35
+ const packageRoot = path.resolve(srcDir, '..');
36
+ const targetRoot = path.resolve(process.cwd(), options.project);
37
+
38
+ const result = addModule({
39
+ moduleId: options.moduleId,
40
+ targetRoot,
41
+ packageRoot,
42
+ });
43
+
44
+ console.log(result.message);
45
+ console.log(`- module: ${result.preset.id}`);
46
+ console.log(`- docs: ${result.docsPath}`);
47
+ }