forgedev 1.4.0 → 1.4.1

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/chainproof-bridge.js +9 -2
  3. package/src/ci-mode.js +3 -4
  4. package/src/composer.js +228 -242
  5. package/src/doctor-checks-chainproof.js +14 -11
  6. package/src/doctor-checks.js +3 -19
  7. package/src/index.js +6 -34
  8. package/src/recommender.js +319 -319
  9. package/src/uat-generator.js +105 -93
  10. package/src/utils.js +245 -214
  11. package/templates/auth/jwt-custom/backend/app/api/auth.py.template +39 -45
  12. package/templates/auth/jwt-custom/backend/app/core/security.py.template +44 -37
  13. package/templates/backend/express/package.json.template +35 -33
  14. package/templates/backend/express/src/lib/prisma.ts.template +32 -0
  15. package/templates/backend/express/src/routes/health.ts.template +35 -27
  16. package/templates/backend/fastapi/backend/app/main.py.template +67 -60
  17. package/templates/backend/fastapi/backend/requirements.txt.template +18 -16
  18. package/templates/backend/hono/package.json.template +33 -31
  19. package/templates/backend/hono/src/lib/prisma.ts.template +32 -0
  20. package/templates/backend/hono/src/routes/health.ts.template +38 -27
  21. package/templates/base/.gitignore.template +32 -32
  22. package/templates/claude-code/commands/workflows.md +52 -52
  23. package/templates/database/prisma-postgres/prisma/schema.prisma.template +19 -18
  24. package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +39 -40
  25. package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +38 -36
  26. package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +50 -48
  27. package/templates/frontend/nextjs/package.json.template +45 -43
  28. package/templates/frontend/remix/package.json.template +41 -39
  29. package/templates/infra/docker/.dockerignore.template +16 -0
  30. package/templates/infra/docker-compose/docker-compose.yml.template +22 -19
  31. package/templates/infra/github-actions/.github/workflows/ci.yml.template +61 -52
  32. package/templates/infra/k8s/k8s/deployment.yml.template +70 -70
  33. package/templates/infra/k8s/k8s/hpa.yml.template +24 -24
  34. package/templates/infra/k8s/k8s/ingress.yml.template +26 -26
  35. package/templates/infra/k8s/k8s/kustomization.yml.template +13 -13
  36. package/templates/infra/k8s/k8s/namespace.yml.template +4 -4
  37. package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -41
  38. package/templates/infra/k8s/k8s/secrets.yml.template +10 -10
  39. package/templates/infra/k8s/k8s/service.yml.template +15 -15
  40. package/templates/testing/load/k6/README.md.template +48 -48
  41. package/templates/testing/load/k6/load-test.js.template +57 -57
@@ -1,319 +1,319 @@
1
- import chalk from 'chalk';
2
- import { getStackMetadata } from './utils.js';
3
-
4
- export const SUPPORTED_STACKS = Object.keys(
5
- // Import the metadata keys to keep SUPPORTED_STACKS in sync automatically.
6
- // getStackMetadata returns null for unknown keys, so we call it to get the map.
7
- (() => {
8
- const stacks = {};
9
- for (const id of ['nextjs-fullstack', 'fastapi-backend', 'polyglot-fullstack', 'react-express', 'remix-fullstack', 'hono-api']) {
10
- if (getStackMetadata(id)) stacks[id] = true;
11
- }
12
- return stacks;
13
- })()
14
- );
15
-
16
- // Modules included in every stack
17
- const UNIVERSAL_MODULES = [
18
- { path: 'testing/load', prefix: '' },
19
- { path: 'infra/docker-compose', prefix: '' },
20
- { path: 'infra/github-actions', prefix: '' },
21
- { path: 'infra/k8s', prefix: '' },
22
- ];
23
-
24
- export function recommend(serviceType, refinements) {
25
- const config = {
26
- projectName: null, // set by caller
27
- serviceType,
28
- frontend: null,
29
- backend: null,
30
- database: null,
31
- auth: null,
32
- testing: { unit: null, e2e: null, backend: null },
33
- ai: refinements.ai || false,
34
- realtime: refinements.realtime || false,
35
- fileUploads: refinements.fileUploads || false,
36
- deployment: refinements.deployment || 'docker',
37
- claudeCode: refinements.claudeCode !== false,
38
- templateModules: [],
39
- stackId: null,
40
- };
41
-
42
- const lang = refinements.language || 'typescript';
43
-
44
- switch (serviceType) {
45
- case 'web_app':
46
- if (refinements.framework === 'remix') {
47
- return buildRemixFullstack(config, refinements);
48
- }
49
- return buildNextjsFullstack(config, refinements);
50
-
51
- case 'full_stack':
52
- if (lang === 'python') {
53
- return buildPolyglotFullstack(config, refinements);
54
- }
55
- if (refinements.backend === 'express') {
56
- return buildReactExpress(config, refinements);
57
- }
58
- return buildNextjsFullstack(config, refinements);
59
-
60
- case 'api_service':
61
- if (lang === 'python') {
62
- return buildFastapiBackend(config, refinements);
63
- }
64
- return buildHonoApi(config, refinements);
65
-
66
- case 'ai_service':
67
- if (lang === 'python') {
68
- return buildPolyglotFullstack(config, { ...refinements, ai: true });
69
- }
70
- return buildPolyglotFullstack(config, { ...refinements, ai: true, language: 'python' });
71
-
72
- case 'mobile':
73
- case 'cli_tool':
74
- case 'desktop':
75
- case 'extension':
76
- case 'microservice':
77
- case 'describe':
78
- return unsupported(serviceType);
79
-
80
- default:
81
- return unsupported(serviceType);
82
- }
83
- }
84
-
85
- // ─── Shared tail logic for all builders ─────────────────────────────
86
- // Appends chainproof, auth, and AI modules so each builder only defines
87
- // its stack-specific modules.
88
-
89
- function finalizeConfig(config, refinements, { authModules = [], chainproofModules = ['chainproof/base'], aiModules = [] } = {}) {
90
- // Auth
91
- if (refinements.auth && authModules.length > 0) {
92
- for (const mod of authModules) {
93
- config.templateModules.push(typeof mod === 'string' ? { path: mod, prefix: '' } : mod);
94
- }
95
- }
96
-
97
- // ChainProof (always)
98
- for (const mod of chainproofModules) {
99
- config.templateModules.push(typeof mod === 'string' ? { path: mod, prefix: '' } : mod);
100
- }
101
-
102
- // AI guardrails
103
- if (refinements.ai && aiModules.length > 0) {
104
- for (const mod of aiModules) {
105
- config.templateModules.push(typeof mod === 'string' ? { path: mod, prefix: '' } : mod);
106
- }
107
- }
108
-
109
- return config;
110
- }
111
-
112
- // ─── Stack builders ─────────────────────────────────────────────────
113
-
114
- function buildNextjsFullstack(config, refinements) {
115
- config.stackId = 'nextjs-fullstack';
116
- config.frontend = { framework: 'nextjs', language: 'typescript', styling: 'tailwind', ui: 'shadcn' };
117
- config.backend = { framework: 'nextjs', language: 'typescript', orm: 'prisma' };
118
- config.database = { type: 'postgresql', orm: 'prisma' };
119
- config.testing = { unit: 'vitest', e2e: 'playwright', backend: null };
120
-
121
- if (refinements.auth) {
122
- config.auth = 'nextauth';
123
- }
124
-
125
- config.templateModules = [
126
- { path: 'base', prefix: '' },
127
- { path: 'frontend/nextjs', prefix: '' },
128
- { path: 'docs-portal/nextjs', prefix: '' },
129
- { path: 'database/prisma-postgres', prefix: '' },
130
- { path: 'testing/vitest', prefix: '' },
131
- { path: 'testing/playwright', prefix: '' },
132
- ...UNIVERSAL_MODULES,
133
- ];
134
-
135
- return finalizeConfig(config, refinements, {
136
- authModules: ['auth/nextauth'],
137
- chainproofModules: ['chainproof/base', 'chainproof/nextjs'],
138
- aiModules: ['ai/guardrails-ts'],
139
- });
140
- }
141
-
142
- function buildFastapiBackend(config, refinements) {
143
- config.stackId = 'fastapi-backend';
144
- config.frontend = null;
145
- config.backend = { framework: 'fastapi', language: 'python', orm: 'sqlalchemy' };
146
- config.database = { type: 'postgresql', orm: 'sqlalchemy' };
147
- config.testing = { unit: null, e2e: null, backend: 'pytest' };
148
-
149
- if (refinements.auth) {
150
- config.auth = 'jwt-custom';
151
- }
152
-
153
- config.templateModules = [
154
- { path: 'base', prefix: '' },
155
- { path: 'backend/fastapi', prefix: '' },
156
- { path: 'docs-portal/fastapi', prefix: '' },
157
- { path: 'database/sqlalchemy-postgres', prefix: '' },
158
- { path: 'testing/pytest', prefix: '' },
159
- ...UNIVERSAL_MODULES,
160
- ];
161
-
162
- return finalizeConfig(config, refinements, {
163
- authModules: ['auth/jwt-custom'],
164
- chainproofModules: ['chainproof/base', 'chainproof/fastapi'],
165
- aiModules: ['ai/guardrails-py'],
166
- });
167
- }
168
-
169
- function buildPolyglotFullstack(config, refinements) {
170
- config.stackId = 'polyglot-fullstack';
171
- config.frontend = { framework: 'nextjs', language: 'typescript', styling: 'tailwind', ui: 'shadcn' };
172
- config.backend = { framework: 'fastapi', language: 'python', orm: 'sqlalchemy' };
173
- config.database = { type: 'postgresql', orm: 'both' };
174
- config.testing = { unit: 'vitest', e2e: 'playwright', backend: 'pytest' };
175
- config.ai = refinements.ai || false;
176
-
177
- if (refinements.auth) {
178
- config.auth = 'both';
179
- }
180
-
181
- config.templateModules = [
182
- { path: 'base', prefix: '' },
183
- { path: 'frontend/nextjs', prefix: 'frontend' },
184
- { path: 'docs-portal/nextjs', prefix: 'frontend' },
185
- { path: 'backend/fastapi', prefix: '' },
186
- { path: 'database/prisma-postgres', prefix: 'frontend' },
187
- { path: 'database/sqlalchemy-postgres', prefix: '' },
188
- { path: 'testing/vitest', prefix: 'frontend' },
189
- { path: 'testing/playwright', prefix: '' },
190
- { path: 'testing/pytest', prefix: '' },
191
- ...UNIVERSAL_MODULES,
192
- ];
193
-
194
- return finalizeConfig(config, refinements, {
195
- authModules: [{ path: 'auth/nextauth', prefix: 'frontend' }, 'auth/jwt-custom'],
196
- chainproofModules: ['chainproof/base', 'chainproof/polyglot'],
197
- aiModules: [{ path: 'ai/guardrails-ts', prefix: 'frontend' }, 'ai/guardrails-py'],
198
- });
199
- }
200
-
201
- function buildReactExpress(config, refinements) {
202
- config.stackId = 'react-express';
203
- config.frontend = { framework: 'react', language: 'typescript', styling: 'tailwind', ui: null };
204
- config.backend = { framework: 'express', language: 'typescript', orm: 'prisma' };
205
- config.database = { type: 'postgresql', orm: 'prisma' };
206
- config.testing = { unit: 'vitest', e2e: null, backend: 'vitest' };
207
-
208
- if (refinements.auth) {
209
- config.auth = 'jwt-custom';
210
- }
211
-
212
- config.templateModules = [
213
- { path: 'base', prefix: '' },
214
- { path: 'frontend/react', prefix: 'frontend' },
215
- { path: 'backend/express', prefix: 'backend' },
216
- { path: 'database/prisma-postgres', prefix: 'backend' },
217
- { path: 'testing/vitest', prefix: 'frontend' },
218
- { path: 'testing/vitest', prefix: 'backend' },
219
- ...UNIVERSAL_MODULES,
220
- ];
221
-
222
- return finalizeConfig(config, refinements, {
223
- chainproofModules: ['chainproof/base'],
224
- aiModules: [{ path: 'ai/guardrails-ts', prefix: 'backend' }],
225
- });
226
- }
227
-
228
- function buildRemixFullstack(config, refinements) {
229
- config.stackId = 'remix-fullstack';
230
- config.frontend = { framework: 'remix', language: 'typescript', styling: 'tailwind', ui: null };
231
- config.backend = { framework: 'remix', language: 'typescript', orm: 'prisma' };
232
- config.database = { type: 'postgresql', orm: 'prisma' };
233
- config.testing = { unit: 'vitest', e2e: null, backend: null };
234
-
235
- if (refinements.auth) {
236
- config.auth = 'remix-auth';
237
- }
238
-
239
- config.templateModules = [
240
- { path: 'base', prefix: '' },
241
- { path: 'frontend/remix', prefix: '' },
242
- { path: 'database/prisma-postgres', prefix: '' },
243
- { path: 'testing/vitest', prefix: '' },
244
- ...UNIVERSAL_MODULES,
245
- ];
246
-
247
- // No auth template module — jwt-custom is Python-only, nextauth is Next.js-only.
248
- // Remix auth scaffolding is planned for a future release.
249
- return finalizeConfig(config, refinements, {
250
- chainproofModules: ['chainproof/base'],
251
- aiModules: ['ai/guardrails-ts'],
252
- });
253
- }
254
-
255
- function buildHonoApi(config, refinements) {
256
- config.stackId = 'hono-api';
257
- config.frontend = null;
258
- config.backend = { framework: 'hono', language: 'typescript', orm: 'prisma' };
259
- config.database = { type: 'postgresql', orm: 'prisma' };
260
- config.testing = { unit: 'vitest', e2e: null, backend: 'vitest' };
261
-
262
- if (refinements.auth) {
263
- config.auth = 'jwt-custom';
264
- }
265
-
266
- config.templateModules = [
267
- { path: 'base', prefix: '' },
268
- { path: 'backend/hono', prefix: '' },
269
- { path: 'database/prisma-postgres', prefix: '' },
270
- { path: 'testing/vitest', prefix: '' },
271
- ...UNIVERSAL_MODULES,
272
- ];
273
-
274
- return finalizeConfig(config, refinements, {
275
- chainproofModules: ['chainproof/base'],
276
- aiModules: ['ai/guardrails-ts'],
277
- });
278
- }
279
-
280
- function unsupported(serviceType) {
281
- return {
282
- supported: false,
283
- message: `"${serviceType}" is not yet supported in V1. Supported: web_app, full_stack, api_service, ai_service`,
284
- };
285
- }
286
-
287
- export function formatStackSummary(config) {
288
- if (config.supported === false) {
289
- return chalk.yellow(config.message);
290
- }
291
-
292
- const lines = [];
293
-
294
- if (config.frontend) {
295
- const uiPart = config.frontend.ui ? ` + ${config.frontend.ui}` : '';
296
- lines.push(` ${chalk.bold('Frontend:')} ${config.frontend.framework} + ${config.frontend.language} + ${config.frontend.styling}${uiPart}`);
297
- }
298
- if (config.backend) {
299
- lines.push(` ${chalk.bold('Backend:')} ${config.backend.framework} + ${config.backend.language} + ${config.backend.orm}`);
300
- }
301
- if (config.database) {
302
- lines.push(` ${chalk.bold('Database:')} ${config.database.type} (${config.database.orm})`);
303
- }
304
- if (config.auth) {
305
- lines.push(` ${chalk.bold('Auth:')} ${config.auth}`);
306
- }
307
-
308
- const testParts = [config.testing.unit, config.testing.e2e, config.testing.backend].filter(Boolean);
309
- if (testParts.length) {
310
- lines.push(` ${chalk.bold('Testing:')} ${testParts.join(' + ')}`);
311
- }
312
-
313
- lines.push(` ${chalk.bold('Deploy:')} ${config.deployment}`);
314
-
315
- if (config.ai) lines.push(` ${chalk.bold('AI:')} enabled`);
316
- if (config.claudeCode) lines.push(` ${chalk.bold('Claude:')} CLAUDE.md + hooks + skills + agents + commands`);
317
-
318
- return lines.join('\n');
319
- }
1
+ import chalk from 'chalk';
2
+ import { getStackMetadata } from './utils.js';
3
+
4
+ export const SUPPORTED_STACKS = Object.keys(
5
+ // Import the metadata keys to keep SUPPORTED_STACKS in sync automatically.
6
+ // getStackMetadata returns null for unknown keys, so we call it to get the map.
7
+ (() => {
8
+ const stacks = {};
9
+ for (const id of ['nextjs-fullstack', 'fastapi-backend', 'polyglot-fullstack', 'react-express', 'remix-fullstack', 'hono-api']) {
10
+ if (getStackMetadata(id)) stacks[id] = true;
11
+ }
12
+ return stacks;
13
+ })()
14
+ );
15
+
16
+ // Modules included in every stack
17
+ const UNIVERSAL_MODULES = [
18
+ { path: 'testing/load', prefix: '' },
19
+ { path: 'infra/docker-compose', prefix: '' },
20
+ { path: 'infra/github-actions', prefix: '' },
21
+ { path: 'infra/k8s', prefix: '' },
22
+ ];
23
+
24
+ export function recommend(serviceType, refinements) {
25
+ const config = {
26
+ projectName: null, // set by caller
27
+ serviceType,
28
+ frontend: null,
29
+ backend: null,
30
+ database: null,
31
+ auth: null,
32
+ testing: { unit: null, e2e: null, backend: null },
33
+ ai: refinements.ai || false,
34
+ realtime: refinements.realtime || false,
35
+ fileUploads: refinements.fileUploads || false,
36
+ deployment: refinements.deployment || 'docker',
37
+ claudeCode: refinements.claudeCode !== false,
38
+ templateModules: [],
39
+ stackId: null,
40
+ };
41
+
42
+ const lang = refinements.language || 'typescript';
43
+
44
+ switch (serviceType) {
45
+ case 'web_app':
46
+ if (refinements.framework === 'remix') {
47
+ return buildRemixFullstack(config, refinements);
48
+ }
49
+ return buildNextjsFullstack(config, refinements);
50
+
51
+ case 'full_stack':
52
+ if (lang === 'python') {
53
+ return buildPolyglotFullstack(config, refinements);
54
+ }
55
+ if (refinements.backend === 'express') {
56
+ return buildReactExpress(config, refinements);
57
+ }
58
+ return buildNextjsFullstack(config, refinements);
59
+
60
+ case 'api_service':
61
+ if (lang === 'python') {
62
+ return buildFastapiBackend(config, refinements);
63
+ }
64
+ return buildHonoApi(config, refinements);
65
+
66
+ case 'ai_service':
67
+ if (lang === 'python') {
68
+ return buildPolyglotFullstack(config, { ...refinements, ai: true });
69
+ }
70
+ return buildPolyglotFullstack(config, { ...refinements, ai: true, language: 'python' });
71
+
72
+ case 'mobile':
73
+ case 'cli_tool':
74
+ case 'desktop':
75
+ case 'extension':
76
+ case 'microservice':
77
+ case 'describe':
78
+ return unsupported(serviceType);
79
+
80
+ default:
81
+ return unsupported(serviceType);
82
+ }
83
+ }
84
+
85
+ // ─── Shared tail logic for all builders ─────────────────────────────
86
+ // Appends chainproof, auth, and AI modules so each builder only defines
87
+ // its stack-specific modules.
88
+
89
+ function finalizeConfig(config, refinements, { authModules = [], chainproofModules = ['chainproof/base'], aiModules = [] } = {}) {
90
+ // Auth
91
+ if (refinements.auth && authModules.length > 0) {
92
+ for (const mod of authModules) {
93
+ config.templateModules.push(typeof mod === 'string' ? { path: mod, prefix: '' } : mod);
94
+ }
95
+ }
96
+
97
+ // ChainProof (always)
98
+ for (const mod of chainproofModules) {
99
+ config.templateModules.push(typeof mod === 'string' ? { path: mod, prefix: '' } : mod);
100
+ }
101
+
102
+ // AI guardrails
103
+ if (refinements.ai && aiModules.length > 0) {
104
+ for (const mod of aiModules) {
105
+ config.templateModules.push(typeof mod === 'string' ? { path: mod, prefix: '' } : mod);
106
+ }
107
+ }
108
+
109
+ return config;
110
+ }
111
+
112
+ // ─── Stack builders ─────────────────────────────────────────────────
113
+
114
+ function buildNextjsFullstack(config, refinements) {
115
+ config.stackId = 'nextjs-fullstack';
116
+ config.frontend = { framework: 'nextjs', language: 'typescript', styling: 'tailwind', ui: 'shadcn' };
117
+ config.backend = { framework: 'nextjs', language: 'typescript', orm: 'prisma' };
118
+ config.database = { type: 'postgresql', orm: 'prisma' };
119
+ config.testing = { unit: 'vitest', e2e: 'playwright', backend: null };
120
+
121
+ if (refinements.auth) {
122
+ config.auth = 'nextauth';
123
+ }
124
+
125
+ config.templateModules = [
126
+ { path: 'base', prefix: '' },
127
+ { path: 'frontend/nextjs', prefix: '' },
128
+ { path: 'docs-portal/nextjs', prefix: '' },
129
+ { path: 'database/prisma-postgres', prefix: '' },
130
+ { path: 'testing/vitest', prefix: '' },
131
+ { path: 'testing/playwright', prefix: '' },
132
+ ...UNIVERSAL_MODULES,
133
+ ];
134
+
135
+ return finalizeConfig(config, refinements, {
136
+ authModules: ['auth/nextauth'],
137
+ chainproofModules: ['chainproof/base', 'chainproof/nextjs'],
138
+ aiModules: ['ai/guardrails-ts'],
139
+ });
140
+ }
141
+
142
+ function buildFastapiBackend(config, refinements) {
143
+ config.stackId = 'fastapi-backend';
144
+ config.frontend = null;
145
+ config.backend = { framework: 'fastapi', language: 'python', orm: 'sqlalchemy' };
146
+ config.database = { type: 'postgresql', orm: 'sqlalchemy' };
147
+ config.testing = { unit: null, e2e: null, backend: 'pytest' };
148
+
149
+ if (refinements.auth) {
150
+ config.auth = 'jwt-custom';
151
+ }
152
+
153
+ config.templateModules = [
154
+ { path: 'base', prefix: '' },
155
+ { path: 'backend/fastapi', prefix: '' },
156
+ { path: 'docs-portal/fastapi', prefix: '' },
157
+ { path: 'database/sqlalchemy-postgres', prefix: '' },
158
+ { path: 'testing/pytest', prefix: '' },
159
+ ...UNIVERSAL_MODULES,
160
+ ];
161
+
162
+ return finalizeConfig(config, refinements, {
163
+ authModules: ['auth/jwt-custom'],
164
+ chainproofModules: ['chainproof/base', 'chainproof/fastapi'],
165
+ aiModules: ['ai/guardrails-py'],
166
+ });
167
+ }
168
+
169
+ function buildPolyglotFullstack(config, refinements) {
170
+ config.stackId = 'polyglot-fullstack';
171
+ config.frontend = { framework: 'nextjs', language: 'typescript', styling: 'tailwind', ui: 'shadcn' };
172
+ config.backend = { framework: 'fastapi', language: 'python', orm: 'sqlalchemy' };
173
+ config.database = { type: 'postgresql', orm: 'both' };
174
+ config.testing = { unit: 'vitest', e2e: 'playwright', backend: 'pytest' };
175
+ config.ai = refinements.ai || false;
176
+
177
+ if (refinements.auth) {
178
+ config.auth = 'both';
179
+ }
180
+
181
+ config.templateModules = [
182
+ { path: 'base', prefix: '' },
183
+ { path: 'frontend/nextjs', prefix: 'frontend' },
184
+ { path: 'docs-portal/nextjs', prefix: 'frontend' },
185
+ { path: 'backend/fastapi', prefix: '' },
186
+ { path: 'database/prisma-postgres', prefix: 'frontend' },
187
+ { path: 'database/sqlalchemy-postgres', prefix: '' },
188
+ { path: 'testing/vitest', prefix: 'frontend' },
189
+ { path: 'testing/playwright', prefix: '' },
190
+ { path: 'testing/pytest', prefix: '' },
191
+ ...UNIVERSAL_MODULES,
192
+ ];
193
+
194
+ return finalizeConfig(config, refinements, {
195
+ authModules: [{ path: 'auth/nextauth', prefix: 'frontend' }, 'auth/jwt-custom'],
196
+ chainproofModules: ['chainproof/base', 'chainproof/polyglot'],
197
+ aiModules: [{ path: 'ai/guardrails-ts', prefix: 'frontend' }, 'ai/guardrails-py'],
198
+ });
199
+ }
200
+
201
+ function buildReactExpress(config, refinements) {
202
+ config.stackId = 'react-express';
203
+ config.frontend = { framework: 'react', language: 'typescript', styling: 'tailwind', ui: null };
204
+ config.backend = { framework: 'express', language: 'typescript', orm: 'prisma' };
205
+ config.database = { type: 'postgresql', orm: 'prisma' };
206
+ config.testing = { unit: 'vitest', e2e: null, backend: 'vitest' };
207
+
208
+ if (refinements.auth) {
209
+ config.auth = 'jwt-custom';
210
+ }
211
+
212
+ config.templateModules = [
213
+ { path: 'base', prefix: '' },
214
+ { path: 'frontend/react', prefix: 'frontend' },
215
+ { path: 'backend/express', prefix: 'backend' },
216
+ { path: 'database/prisma-postgres', prefix: 'backend' },
217
+ { path: 'testing/vitest', prefix: 'frontend' },
218
+ { path: 'testing/vitest', prefix: 'backend' },
219
+ ...UNIVERSAL_MODULES,
220
+ ];
221
+
222
+ return finalizeConfig(config, refinements, {
223
+ chainproofModules: ['chainproof/base'],
224
+ aiModules: [{ path: 'ai/guardrails-ts', prefix: 'backend' }],
225
+ });
226
+ }
227
+
228
+ function buildRemixFullstack(config, refinements) {
229
+ config.stackId = 'remix-fullstack';
230
+ config.frontend = { framework: 'remix', language: 'typescript', styling: 'tailwind', ui: null };
231
+ config.backend = { framework: 'remix', language: 'typescript', orm: 'prisma' };
232
+ config.database = { type: 'postgresql', orm: 'prisma' };
233
+ config.testing = { unit: 'vitest', e2e: null, backend: null };
234
+
235
+ if (refinements.auth) {
236
+ config.auth = 'remix-auth';
237
+ }
238
+
239
+ config.templateModules = [
240
+ { path: 'base', prefix: '' },
241
+ { path: 'frontend/remix', prefix: '' },
242
+ { path: 'database/prisma-postgres', prefix: '' },
243
+ { path: 'testing/vitest', prefix: '' },
244
+ ...UNIVERSAL_MODULES,
245
+ ];
246
+
247
+ // No auth template module — jwt-custom is Python-only, nextauth is Next.js-only.
248
+ // Remix auth scaffolding is planned for a future release.
249
+ return finalizeConfig(config, refinements, {
250
+ chainproofModules: ['chainproof/base'],
251
+ aiModules: ['ai/guardrails-ts'],
252
+ });
253
+ }
254
+
255
+ function buildHonoApi(config, refinements) {
256
+ config.stackId = 'hono-api';
257
+ config.frontend = null;
258
+ config.backend = { framework: 'hono', language: 'typescript', orm: 'prisma' };
259
+ config.database = { type: 'postgresql', orm: 'prisma' };
260
+ config.testing = { unit: 'vitest', e2e: null, backend: 'vitest' };
261
+
262
+ if (refinements.auth) {
263
+ config.auth = 'jwt-custom';
264
+ }
265
+
266
+ config.templateModules = [
267
+ { path: 'base', prefix: '' },
268
+ { path: 'backend/hono', prefix: '' },
269
+ { path: 'database/prisma-postgres', prefix: '' },
270
+ { path: 'testing/vitest', prefix: '' },
271
+ ...UNIVERSAL_MODULES,
272
+ ];
273
+
274
+ return finalizeConfig(config, refinements, {
275
+ chainproofModules: ['chainproof/base'],
276
+ aiModules: ['ai/guardrails-ts'],
277
+ });
278
+ }
279
+
280
+ function unsupported(serviceType) {
281
+ return {
282
+ supported: false,
283
+ message: `"${serviceType}" is not yet supported in V1. Supported: web_app, full_stack, api_service, ai_service`,
284
+ };
285
+ }
286
+
287
+ export function formatStackSummary(config) {
288
+ if (config.supported === false) {
289
+ return chalk.yellow(config.message);
290
+ }
291
+
292
+ const lines = [];
293
+
294
+ if (config.frontend) {
295
+ const uiPart = config.frontend.ui ? ` + ${config.frontend.ui}` : '';
296
+ lines.push(` ${chalk.bold('Frontend:')} ${config.frontend.framework} + ${config.frontend.language} + ${config.frontend.styling}${uiPart}`);
297
+ }
298
+ if (config.backend) {
299
+ lines.push(` ${chalk.bold('Backend:')} ${config.backend.framework} + ${config.backend.language} + ${config.backend.orm}`);
300
+ }
301
+ if (config.database) {
302
+ lines.push(` ${chalk.bold('Database:')} ${config.database.type} (${config.database.orm})`);
303
+ }
304
+ if (config.auth) {
305
+ lines.push(` ${chalk.bold('Auth:')} ${config.auth}`);
306
+ }
307
+
308
+ const testParts = [config.testing.unit, config.testing.e2e, config.testing.backend].filter(Boolean);
309
+ if (testParts.length) {
310
+ lines.push(` ${chalk.bold('Testing:')} ${testParts.join(' + ')}`);
311
+ }
312
+
313
+ lines.push(` ${chalk.bold('Deploy:')} ${config.deployment}`);
314
+
315
+ if (config.ai) lines.push(` ${chalk.bold('AI:')} enabled`);
316
+ if (config.claudeCode) lines.push(` ${chalk.bold('Claude:')} CLAUDE.md + hooks + skills + agents + commands`);
317
+
318
+ return lines.join('\n');
319
+ }