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.
- package/package.json +1 -1
- package/src/chainproof-bridge.js +9 -2
- package/src/ci-mode.js +3 -4
- package/src/composer.js +228 -242
- package/src/doctor-checks-chainproof.js +14 -11
- package/src/doctor-checks.js +3 -19
- package/src/index.js +6 -34
- package/src/recommender.js +319 -319
- package/src/uat-generator.js +105 -93
- package/src/utils.js +245 -214
- package/templates/auth/jwt-custom/backend/app/api/auth.py.template +39 -45
- package/templates/auth/jwt-custom/backend/app/core/security.py.template +44 -37
- package/templates/backend/express/package.json.template +35 -33
- package/templates/backend/express/src/lib/prisma.ts.template +32 -0
- package/templates/backend/express/src/routes/health.ts.template +35 -27
- package/templates/backend/fastapi/backend/app/main.py.template +67 -60
- package/templates/backend/fastapi/backend/requirements.txt.template +18 -16
- package/templates/backend/hono/package.json.template +33 -31
- package/templates/backend/hono/src/lib/prisma.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +38 -27
- package/templates/base/.gitignore.template +32 -32
- package/templates/claude-code/commands/workflows.md +52 -52
- package/templates/database/prisma-postgres/prisma/schema.prisma.template +19 -18
- package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +39 -40
- package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +38 -36
- package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +50 -48
- package/templates/frontend/nextjs/package.json.template +45 -43
- package/templates/frontend/remix/package.json.template +41 -39
- package/templates/infra/docker/.dockerignore.template +16 -0
- package/templates/infra/docker-compose/docker-compose.yml.template +22 -19
- package/templates/infra/github-actions/.github/workflows/ci.yml.template +61 -52
- package/templates/infra/k8s/k8s/deployment.yml.template +70 -70
- package/templates/infra/k8s/k8s/hpa.yml.template +24 -24
- package/templates/infra/k8s/k8s/ingress.yml.template +26 -26
- package/templates/infra/k8s/k8s/kustomization.yml.template +13 -13
- package/templates/infra/k8s/k8s/namespace.yml.template +4 -4
- package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -41
- package/templates/infra/k8s/k8s/secrets.yml.template +10 -10
- package/templates/infra/k8s/k8s/service.yml.template +15 -15
- package/templates/testing/load/k6/README.md.template +48 -48
- package/templates/testing/load/k6/load-test.js.template +57 -57
package/src/recommender.js
CHANGED
|
@@ -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
|
+
}
|