create-forgeon 0.1.2 → 0.1.5

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 +246 -0
  26. package/src/modules/registry.mjs +43 -35
  27. package/src/presets/i18n.mjs +211 -185
  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
@@ -0,0 +1,246 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
+
5
+ function copyFromBase(packageRoot, targetRoot, relativePath) {
6
+ const source = path.join(packageRoot, 'templates', 'base', relativePath);
7
+ if (!fs.existsSync(source)) {
8
+ throw new Error(`Missing i18n source template: ${source}`);
9
+ }
10
+ const destination = path.join(targetRoot, relativePath);
11
+ copyRecursive(source, destination);
12
+ }
13
+
14
+ function copyFromPreset(packageRoot, targetRoot, relativePath) {
15
+ const source = path.join(packageRoot, 'templates', 'module-presets', 'i18n', relativePath);
16
+ if (!fs.existsSync(source)) {
17
+ throw new Error(`Missing i18n preset template: ${source}`);
18
+ }
19
+ const destination = path.join(targetRoot, relativePath);
20
+ copyRecursive(source, destination);
21
+ }
22
+
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 upsertEnvLines(filePath, lines) {
38
+ let content = '';
39
+ if (fs.existsSync(filePath)) {
40
+ content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
41
+ }
42
+
43
+ const keys = new Set(
44
+ content
45
+ .split('\n')
46
+ .filter(Boolean)
47
+ .map((line) => line.split('=')[0]),
48
+ );
49
+
50
+ const append = [];
51
+ for (const line of lines) {
52
+ const key = line.split('=')[0];
53
+ if (!keys.has(key)) {
54
+ append.push(line);
55
+ }
56
+ }
57
+
58
+ const next =
59
+ append.length > 0 ? `${content.trimEnd()}\n${append.join('\n')}\n` : `${content.trimEnd()}\n`;
60
+ fs.writeFileSync(filePath, next.replace(/^\n/, ''), 'utf8');
61
+ }
62
+
63
+ function ensureLineAfter(content, anchorLine, lineToInsert) {
64
+ if (content.includes(lineToInsert)) {
65
+ return content;
66
+ }
67
+
68
+ const index = content.indexOf(anchorLine);
69
+ if (index < 0) {
70
+ return `${content.trimEnd()}\n${lineToInsert}\n`;
71
+ }
72
+
73
+ const insertAt = index + anchorLine.length;
74
+ return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
75
+ }
76
+
77
+ function ensureLineBefore(content, anchorLine, lineToInsert) {
78
+ if (content.includes(lineToInsert)) {
79
+ return content;
80
+ }
81
+
82
+ const index = content.indexOf(anchorLine);
83
+ if (index < 0) {
84
+ return `${content.trimEnd()}\n${lineToInsert}\n`;
85
+ }
86
+
87
+ return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
88
+ }
89
+
90
+ function patchApiDockerfile(targetRoot) {
91
+ const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
92
+ if (!fs.existsSync(dockerfilePath)) {
93
+ return;
94
+ }
95
+
96
+ let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
97
+
98
+ content = ensureLineAfter(
99
+ content,
100
+ 'COPY packages/core/package.json packages/core/package.json',
101
+ 'COPY packages/i18n-contracts/package.json packages/i18n-contracts/package.json',
102
+ );
103
+ content = ensureLineAfter(
104
+ content,
105
+ 'COPY packages/i18n-contracts/package.json packages/i18n-contracts/package.json',
106
+ 'COPY packages/i18n/package.json packages/i18n/package.json',
107
+ );
108
+ content = ensureLineAfter(
109
+ content,
110
+ 'COPY packages/core packages/core',
111
+ 'COPY packages/i18n-contracts packages/i18n-contracts',
112
+ );
113
+ content = ensureLineAfter(
114
+ content,
115
+ 'COPY packages/i18n-contracts packages/i18n-contracts',
116
+ 'COPY packages/i18n packages/i18n',
117
+ );
118
+
119
+ content = content
120
+ .replace(/^RUN pnpm --filter @forgeon\/i18n-contracts build\r?\n?/gm, '')
121
+ .replace(/^RUN pnpm --filter @forgeon\/i18n build\r?\n?/gm, '');
122
+
123
+ content = ensureLineBefore(
124
+ content,
125
+ 'RUN pnpm --filter @forgeon/api prisma:generate',
126
+ 'RUN pnpm --filter @forgeon/i18n-contracts build',
127
+ );
128
+ content = ensureLineBefore(
129
+ content,
130
+ 'RUN pnpm --filter @forgeon/api prisma:generate',
131
+ 'RUN pnpm --filter @forgeon/i18n build',
132
+ );
133
+
134
+ fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
135
+ }
136
+
137
+ function patchCompose(targetRoot) {
138
+ const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
139
+ if (!fs.existsSync(composePath)) {
140
+ return;
141
+ }
142
+
143
+ let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
144
+ if (!content.includes('I18N_ENABLED: ${I18N_ENABLED}')) {
145
+ content = content.replace(
146
+ /^(\s+DATABASE_URL:.*)$/m,
147
+ `$1
148
+ I18N_ENABLED: \${I18N_ENABLED}
149
+ I18N_DEFAULT_LANG: \${I18N_DEFAULT_LANG}
150
+ I18N_FALLBACK_LANG: \${I18N_FALLBACK_LANG}`,
151
+ );
152
+ }
153
+
154
+ fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
155
+ }
156
+
157
+ function patchApiPackage(targetRoot) {
158
+ const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
159
+ if (!fs.existsSync(packagePath)) {
160
+ return;
161
+ }
162
+
163
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
164
+ ensureScript(
165
+ packageJson,
166
+ 'predev',
167
+ 'pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n build',
168
+ );
169
+ ensureDependency(packageJson, '@forgeon/i18n', 'workspace:*');
170
+ ensureDependency(packageJson, '@forgeon/i18n-contracts', 'workspace:*');
171
+ ensureDependency(packageJson, 'nestjs-i18n', '^10.5.1');
172
+ writeJson(packagePath, packageJson);
173
+ }
174
+
175
+ function patchWebPackage(targetRoot) {
176
+ const packagePath = path.join(targetRoot, 'apps', 'web', 'package.json');
177
+ if (!fs.existsSync(packagePath)) {
178
+ return;
179
+ }
180
+
181
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
182
+ ensureScript(
183
+ packageJson,
184
+ 'predev',
185
+ 'pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n-web build',
186
+ );
187
+ ensureScript(
188
+ packageJson,
189
+ 'prebuild',
190
+ 'pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n-web build',
191
+ );
192
+ ensureDependency(packageJson, '@forgeon/i18n-contracts', 'workspace:*');
193
+ ensureDependency(packageJson, '@forgeon/i18n-web', 'workspace:*');
194
+ writeJson(packagePath, packageJson);
195
+ }
196
+
197
+ function patchI18nPackage(targetRoot) {
198
+ const packagePath = path.join(targetRoot, 'packages', 'i18n', 'package.json');
199
+ if (!fs.existsSync(packagePath)) {
200
+ return;
201
+ }
202
+
203
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
204
+ ensureDependency(packageJson, '@forgeon/i18n-contracts', 'workspace:*');
205
+ writeJson(packagePath, packageJson);
206
+ }
207
+
208
+ export function applyI18nModule({ packageRoot, targetRoot }) {
209
+ copyFromBase(packageRoot, targetRoot, path.join('packages', 'i18n'));
210
+ copyFromBase(packageRoot, targetRoot, path.join('resources', 'i18n'));
211
+
212
+ copyFromPreset(packageRoot, targetRoot, path.join('packages', 'i18n-contracts'));
213
+ copyFromPreset(packageRoot, targetRoot, path.join('packages', 'i18n-web'));
214
+ copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'App.tsx'));
215
+
216
+ copyFromBase(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'app.module.ts'));
217
+ copyFromBase(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'config', 'app.config.ts'));
218
+ copyFromBase(
219
+ packageRoot,
220
+ targetRoot,
221
+ path.join('apps', 'api', 'src', 'health', 'health.controller.ts'),
222
+ );
223
+ copyFromBase(
224
+ packageRoot,
225
+ targetRoot,
226
+ path.join('apps', 'api', 'src', 'common', 'filters', 'app-exception.filter.ts'),
227
+ );
228
+
229
+ patchI18nPackage(targetRoot);
230
+ patchApiPackage(targetRoot);
231
+ patchWebPackage(targetRoot);
232
+ patchApiDockerfile(targetRoot);
233
+
234
+ upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
235
+ 'I18N_ENABLED=true',
236
+ 'I18N_DEFAULT_LANG=en',
237
+ 'I18N_FALLBACK_LANG=en',
238
+ ]);
239
+ upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
240
+ 'I18N_ENABLED=true',
241
+ 'I18N_DEFAULT_LANG=en',
242
+ 'I18N_FALLBACK_LANG=en',
243
+ ]);
244
+
245
+ patchCompose(targetRoot);
246
+ }
@@ -1,37 +1,45 @@
1
1
  const MODULE_PRESETS = {
2
- 'jwt-auth': {
3
- id: 'jwt-auth',
4
- label: 'JWT Auth',
5
- category: 'auth-security',
6
- implemented: false,
7
- description: 'JWT auth preset with guards and passport strategy wiring.',
8
- docFragments: ['00_title', '10_overview', '20_scope', '90_status_planned'],
9
- },
10
- queue: {
11
- id: 'queue',
12
- label: 'Queue Worker',
13
- category: 'background-jobs',
14
- implemented: false,
15
- description: 'Queue processing preset (BullMQ-style app wiring).',
16
- docFragments: ['00_title', '10_overview', '20_scope', '90_status_planned'],
2
+ i18n: {
3
+ id: 'i18n',
4
+ label: 'I18n',
5
+ category: 'localization',
6
+ implemented: true,
7
+ description: 'Backend/frontend i18n wiring with locale contracts and translation resources.',
8
+ docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
17
9
  },
18
- };
19
-
20
- export function listModulePresets() {
21
- return Object.values(MODULE_PRESETS);
22
- }
23
-
24
- export function getModulePreset(moduleId) {
25
- return MODULE_PRESETS[moduleId] ?? null;
26
- }
27
-
28
- export function ensureModuleExists(moduleId) {
29
- const preset = getModulePreset(moduleId);
30
- if (!preset) {
31
- const available = listModulePresets()
32
- .map((item) => item.id)
33
- .join(', ');
34
- throw new Error(`Unknown module "${moduleId}". Available modules: ${available}`);
35
- }
36
- return preset;
37
- }
10
+ 'jwt-auth': {
11
+ id: 'jwt-auth',
12
+ label: 'JWT Auth',
13
+ category: 'auth-security',
14
+ implemented: false,
15
+ description: 'JWT auth preset with guards and passport strategy wiring.',
16
+ docFragments: ['00_title', '10_overview', '20_scope', '90_status_planned'],
17
+ },
18
+ queue: {
19
+ id: 'queue',
20
+ label: 'Queue Worker',
21
+ category: 'background-jobs',
22
+ implemented: false,
23
+ description: 'Queue processing preset (BullMQ-style app wiring).',
24
+ docFragments: ['00_title', '10_overview', '20_scope', '90_status_planned'],
25
+ },
26
+ };
27
+
28
+ export function listModulePresets() {
29
+ return Object.values(MODULE_PRESETS);
30
+ }
31
+
32
+ export function getModulePreset(moduleId) {
33
+ return MODULE_PRESETS[moduleId] ?? null;
34
+ }
35
+
36
+ export function ensureModuleExists(moduleId) {
37
+ const preset = getModulePreset(moduleId);
38
+ if (!preset) {
39
+ const available = listModulePresets()
40
+ .map((item) => item.id)
41
+ .join(', ');
42
+ throw new Error(`Unknown module "${moduleId}". Available modules: ${available}`);
43
+ }
44
+ return preset;
45
+ }