forgedev 1.3.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/bin/chainproof.js +1 -1
- package/bin/devforge.js +1 -1
- package/package.json +1 -1
- package/src/chainproof-bridge.js +9 -2
- package/src/ci-mode.js +3 -4
- package/src/claude-configurator.js +114 -58
- package/src/composer.js +17 -128
- package/src/doctor-checks-chainproof.js +14 -11
- package/src/doctor-checks.js +3 -19
- package/src/index.js +6 -34
- package/src/init-mode.js +14 -3
- package/src/recommender.js +319 -310
- package/src/uat-generator.js +105 -93
- package/src/update.js +56 -12
- package/src/utils.js +245 -116
- 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 -29
- 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 -0
- package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
- package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
- package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
- package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
- package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
- package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
- package/templates/infra/k8s/k8s/service.yml.template +15 -0
- package/templates/testing/load/k6/README.md.template +48 -0
- package/templates/testing/load/k6/load-test.js.template +57 -0
package/bin/chainproof.js
CHANGED
package/bin/devforge.js
CHANGED
package/package.json
CHANGED
package/src/chainproof-bridge.js
CHANGED
|
@@ -43,8 +43,15 @@ export function verifySignature(payload, signatureB64, publicKeyPem) {
|
|
|
43
43
|
publicKeyPem,
|
|
44
44
|
Buffer.from(signatureB64, 'base64')
|
|
45
45
|
);
|
|
46
|
-
} catch {
|
|
47
|
-
//
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// Verification should never throw — any failure means "not verified"
|
|
48
|
+
if (err?.code?.startsWith?.('ERR_OSSL') ||
|
|
49
|
+
err?.code?.startsWith?.('ERR_CRYPTO') ||
|
|
50
|
+
err instanceof TypeError) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
// Log unexpected errors for debugging but still return false
|
|
54
|
+
console.warn('Unexpected crypto error during signature verification:', err?.code || err?.message);
|
|
48
55
|
return false;
|
|
49
56
|
}
|
|
50
57
|
}
|
package/src/ci-mode.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { log } from './utils.js';
|
|
5
|
-
import { scanProject
|
|
5
|
+
import { scanProject } from './scanner.js';
|
|
6
6
|
import { runAllChecks } from './doctor-checks.js';
|
|
7
7
|
import { generateReport } from './doctor-prompts.js';
|
|
8
8
|
|
|
@@ -18,10 +18,9 @@ export async function runCI(projectDir) {
|
|
|
18
18
|
log.info(' DevForge CI Automated project health check');
|
|
19
19
|
console.log('');
|
|
20
20
|
|
|
21
|
-
// Scan project
|
|
21
|
+
// Scan project (scanProject already calls detectStack internally)
|
|
22
22
|
const scan = scanProject(resolvedDir);
|
|
23
|
-
|
|
24
|
-
log.dim(` Stack: ${stack || 'unknown'}`);
|
|
23
|
+
log.dim(` Stack: ${scan.stackId || 'unknown'}`);
|
|
25
24
|
|
|
26
25
|
// Run all checks
|
|
27
26
|
const issues = runAllChecks(resolvedDir, scan);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { ROOT_DIR, ensureDir, writeFile, readTemplate, replaceVars, getStackCommands } from './utils.js';
|
|
3
|
+
import { ROOT_DIR, ensureDir, writeFile, readTemplate, replaceVars, getStackCommands, toPascalCase } from './utils.js';
|
|
4
4
|
|
|
5
5
|
const CLAUDE_TEMPLATES_DIR = path.join(ROOT_DIR, 'templates', 'claude-code');
|
|
6
6
|
const DOCS_DIR = path.join(ROOT_DIR, 'docs');
|
|
@@ -13,10 +13,12 @@ export async function generateClaudeConfig(outputDir, stackConfig, options = {})
|
|
|
13
13
|
}
|
|
14
14
|
generateHooks(outputDir, stackConfig, { merge: options.mergeSettings });
|
|
15
15
|
generateSkills(outputDir, stackConfig);
|
|
16
|
-
generateAgents(outputDir, stackConfig, vars);
|
|
17
|
-
generateCommands(outputDir, stackConfig, vars);
|
|
16
|
+
const agents = generateAgents(outputDir, stackConfig, vars);
|
|
17
|
+
const commands = generateCommands(outputDir, stackConfig, vars);
|
|
18
18
|
copyPromptLibrary(outputDir);
|
|
19
19
|
stampVersion(outputDir);
|
|
20
|
+
|
|
21
|
+
return { agents, commands };
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function stampVersion(outputDir) {
|
|
@@ -30,59 +32,69 @@ function stampVersion(outputDir) {
|
|
|
30
32
|
function buildClaudeVars(config) {
|
|
31
33
|
const vars = {
|
|
32
34
|
PROJECT_NAME: config.projectName,
|
|
33
|
-
PROJECT_NAME_PASCAL: config.projectName
|
|
35
|
+
PROJECT_NAME_PASCAL: toPascalCase(config.projectName),
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
// Commands vary by stack (shared with composer)
|
|
37
39
|
Object.assign(vars, getStackCommands(config.stackId));
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const claudeVars = {
|
|
42
|
+
'nextjs-fullstack': {
|
|
43
|
+
STACK_SUMMARY: 'Next.js 15 (App Router) + TypeScript + Tailwind CSS + Prisma + PostgreSQL',
|
|
44
|
+
DIR_MAP: `- src/app/ - Next.js App Router pages and API routes
|
|
42
45
|
- src/lib/ - Shared utilities, database client, error handling
|
|
43
46
|
- src/components/ - React components
|
|
44
47
|
- prisma/ - Database schema and migrations
|
|
45
|
-
- e2e/ - Playwright E2E tests
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
- e2e/ - Playwright E2E tests`,
|
|
49
|
+
},
|
|
50
|
+
'fastapi-backend': {
|
|
51
|
+
STACK_SUMMARY: 'FastAPI + Python + SQLAlchemy 2.0 + PostgreSQL + Alembic',
|
|
52
|
+
DIR_MAP: `- backend/app/ - FastAPI application
|
|
49
53
|
- backend/app/api/ - API route handlers
|
|
50
54
|
- backend/app/core/ - Config, security, error handling
|
|
51
55
|
- backend/app/db/ - Database session and models
|
|
52
56
|
- backend/app/models/ - SQLAlchemy models
|
|
53
57
|
- backend/app/schemas/ - Pydantic schemas
|
|
54
58
|
- backend/tests/ - Pytest tests
|
|
55
|
-
- backend/alembic/ - Database migrations
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
- backend/alembic/ - Database migrations`,
|
|
60
|
+
},
|
|
61
|
+
'polyglot-fullstack': {
|
|
62
|
+
STACK_SUMMARY: 'Next.js 15 (frontend) + FastAPI (backend) + PostgreSQL',
|
|
63
|
+
DIR_MAP: `- frontend/ - Next.js 15 App Router application
|
|
59
64
|
- frontend/src/app/ - Pages and API routes
|
|
60
65
|
- frontend/src/lib/ - Shared utilities
|
|
61
66
|
- backend/ - FastAPI application
|
|
62
67
|
- backend/app/api/ - API route handlers
|
|
63
68
|
- backend/app/core/ - Config, security, error handling
|
|
64
69
|
- backend/app/db/ - Database session and models
|
|
65
|
-
- e2e/ - Playwright E2E tests
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
- e2e/ - Playwright E2E tests`,
|
|
71
|
+
},
|
|
72
|
+
'react-express': {
|
|
73
|
+
STACK_SUMMARY: 'React (Vite) + Express + TypeScript + Prisma + PostgreSQL',
|
|
74
|
+
DIR_MAP: `- frontend/ - React (Vite) SPA
|
|
69
75
|
- frontend/src/ - React components and pages
|
|
70
76
|
- backend/ - Express API server
|
|
71
77
|
- backend/src/ - Express routes and middleware
|
|
72
78
|
- backend/src/routes/ - API route handlers
|
|
73
|
-
- backend/prisma/ - Database schema and migrations
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
- backend/prisma/ - Database schema and migrations`,
|
|
80
|
+
},
|
|
81
|
+
'remix-fullstack': {
|
|
82
|
+
STACK_SUMMARY: 'Remix + Vite + TypeScript + Tailwind CSS + Prisma + PostgreSQL',
|
|
83
|
+
DIR_MAP: `- app/ - Remix application
|
|
77
84
|
- app/routes/ - File-based routes and API resource routes
|
|
78
85
|
- app/routes/api.health.ts - Health check endpoint
|
|
79
|
-
- prisma/ - Database schema and migrations
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
- prisma/ - Database schema and migrations`,
|
|
87
|
+
},
|
|
88
|
+
'hono-api': {
|
|
89
|
+
STACK_SUMMARY: 'Hono + TypeScript + Prisma + PostgreSQL',
|
|
90
|
+
DIR_MAP: `- src/ - Hono application
|
|
83
91
|
- src/routes/ - API route handlers
|
|
84
92
|
- src/routes/health.ts - Health check endpoints
|
|
85
|
-
- prisma/ - Database schema and migrations
|
|
93
|
+
- prisma/ - Database schema and migrations`,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
if (claudeVars[config.stackId]) {
|
|
97
|
+
Object.assign(vars, claudeVars[config.stackId]);
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
// Build skills list for CLAUDE.md reference
|
|
@@ -115,20 +127,18 @@ function generateClaudeMd(outputDir, config, vars) {
|
|
|
115
127
|
let content = readTemplate(basePath);
|
|
116
128
|
|
|
117
129
|
// Read stack-specific section
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'hono.md'));
|
|
131
|
-
}
|
|
130
|
+
const claudeMdTemplates = {
|
|
131
|
+
'nextjs-fullstack': 'nextjs.md',
|
|
132
|
+
'fastapi-backend': 'fastapi.md',
|
|
133
|
+
'polyglot-fullstack': 'fullstack.md',
|
|
134
|
+
'react-express': 'fullstack.md',
|
|
135
|
+
'remix-fullstack': 'remix.md',
|
|
136
|
+
'hono-api': 'hono.md',
|
|
137
|
+
};
|
|
138
|
+
const mdFile = claudeMdTemplates[config.stackId];
|
|
139
|
+
const stackSection = mdFile
|
|
140
|
+
? readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', mdFile))
|
|
141
|
+
: '';
|
|
132
142
|
|
|
133
143
|
content = content.replace('{{STACK_SPECIFIC_RULES}}', stackSection);
|
|
134
144
|
content = replaceVars(content, vars);
|
|
@@ -137,21 +147,19 @@ function generateClaudeMd(outputDir, config, vars) {
|
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
function generateHooks(outputDir, config, options = {}) {
|
|
140
|
-
|
|
150
|
+
const hookConfig = {
|
|
151
|
+
'nextjs-fullstack': { file: 'typescript.json', autofix: 'autofix-typescript.mjs' },
|
|
152
|
+
'fastapi-backend': { file: 'python.json', autofix: 'autofix-python.mjs' },
|
|
153
|
+
'polyglot-fullstack': { file: 'polyglot.json', autofix: 'autofix-polyglot.mjs' },
|
|
154
|
+
'react-express': { file: 'typescript.json', autofix: 'autofix-typescript.mjs' },
|
|
155
|
+
'remix-fullstack': { file: 'typescript.json', autofix: 'autofix-typescript.mjs' },
|
|
156
|
+
'hono-api': { file: 'typescript.json', autofix: 'autofix-typescript.mjs' },
|
|
157
|
+
};
|
|
158
|
+
const hook = hookConfig[config.stackId];
|
|
159
|
+
const hookFile = hook?.file;
|
|
141
160
|
const scriptFiles = ['guard-protected-files.mjs', 'code-hygiene.mjs', 'pre-commit-gate.mjs'];
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
hookFile = 'typescript.json';
|
|
145
|
-
scriptFiles.push('autofix-typescript.mjs');
|
|
146
|
-
} else if (config.stackId === 'fastapi-backend') {
|
|
147
|
-
hookFile = 'python.json';
|
|
148
|
-
scriptFiles.push('autofix-python.mjs');
|
|
149
|
-
} else if (config.stackId === 'polyglot-fullstack') {
|
|
150
|
-
hookFile = 'polyglot.json';
|
|
151
|
-
scriptFiles.push('autofix-polyglot.mjs');
|
|
152
|
-
} else if (config.stackId === 'react-express') {
|
|
153
|
-
hookFile = 'typescript.json';
|
|
154
|
-
scriptFiles.push('autofix-typescript.mjs');
|
|
161
|
+
if (hook?.autofix) {
|
|
162
|
+
scriptFiles.push(hook.autofix);
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
const settingsPath = path.join(outputDir, '.claude', 'settings.json');
|
|
@@ -160,7 +168,7 @@ function generateHooks(outputDir, config, options = {}) {
|
|
|
160
168
|
// Merge hooks into existing settings.json
|
|
161
169
|
const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
162
170
|
const incoming = JSON.parse(readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'hooks', hookFile)));
|
|
163
|
-
const merged =
|
|
171
|
+
const merged = mergeHooksIntoSettings(existing, incoming);
|
|
164
172
|
writeFile(settingsPath, JSON.stringify(merged, null, 2));
|
|
165
173
|
} else {
|
|
166
174
|
const hookPath = path.join(CLAUDE_TEMPLATES_DIR, 'hooks', hookFile);
|
|
@@ -179,7 +187,7 @@ function generateHooks(outputDir, config, options = {}) {
|
|
|
179
187
|
}
|
|
180
188
|
}
|
|
181
189
|
|
|
182
|
-
function
|
|
190
|
+
function mergeHooksIntoSettings(existing, incoming) {
|
|
183
191
|
const merged = { ...existing };
|
|
184
192
|
if (incoming.hooks) {
|
|
185
193
|
merged.hooks = merged.hooks || {};
|
|
@@ -264,9 +272,14 @@ function generateAgents(outputDir, config, vars) {
|
|
|
264
272
|
if (fs.existsSync(srcPath)) {
|
|
265
273
|
let content = readTemplate(srcPath);
|
|
266
274
|
content = replaceVars(content, vars);
|
|
275
|
+
content = stampManaged(content);
|
|
267
276
|
writeFile(path.join(outputDir, '.claude', 'agents', agent), content);
|
|
268
277
|
}
|
|
269
278
|
}
|
|
279
|
+
|
|
280
|
+
// Clean up orphaned agents from older versions
|
|
281
|
+
const removed = cleanOrphans(path.join(outputDir, '.claude', 'agents'), agents);
|
|
282
|
+
return { count: agents.length, removed };
|
|
270
283
|
}
|
|
271
284
|
|
|
272
285
|
function generateCommands(outputDir, config, vars) {
|
|
@@ -306,9 +319,52 @@ function generateCommands(outputDir, config, vars) {
|
|
|
306
319
|
if (fs.existsSync(srcPath)) {
|
|
307
320
|
let content = readTemplate(srcPath);
|
|
308
321
|
content = replaceVars(content, vars);
|
|
322
|
+
content = stampManaged(content);
|
|
309
323
|
writeFile(path.join(outputDir, '.claude', 'commands', cmd), content);
|
|
310
324
|
}
|
|
311
325
|
}
|
|
326
|
+
|
|
327
|
+
// Clean up orphaned commands from older versions
|
|
328
|
+
const removed = cleanOrphans(path.join(outputDir, '.claude', 'commands'), commands);
|
|
329
|
+
return { count: commands.length, removed };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const MANAGED_MARKER = '<!-- Generated by DevForge -->';
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Append the DevForge managed marker to generated file content.
|
|
336
|
+
*/
|
|
337
|
+
function stampManaged(content) {
|
|
338
|
+
return content.trimEnd() + '\n\n' + MANAGED_MARKER + '\n';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Remove DevForge-managed .md files that are no longer in the current manifest.
|
|
343
|
+
* Only deletes files stamped with the DevForge marker. User-created custom
|
|
344
|
+
* files are left untouched. Returns array of removed filenames.
|
|
345
|
+
*/
|
|
346
|
+
function cleanOrphans(dir, expectedFiles) {
|
|
347
|
+
const removed = [];
|
|
348
|
+
if (!fs.existsSync(dir)) return removed;
|
|
349
|
+
|
|
350
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
351
|
+
const existing = entries
|
|
352
|
+
.filter(e => e.isFile() && !e.isSymbolicLink() && e.name.endsWith('.md'))
|
|
353
|
+
.map(e => e.name);
|
|
354
|
+
const expectedSet = new Set(expectedFiles);
|
|
355
|
+
for (const file of existing) {
|
|
356
|
+
if (!expectedSet.has(file)) {
|
|
357
|
+
const filePath = path.join(dir, file);
|
|
358
|
+
const stat = fs.statSync(filePath);
|
|
359
|
+
if (stat.size > 1_000_000) continue; // skip unexpectedly large files
|
|
360
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
361
|
+
if (content.includes(MANAGED_MARKER)) {
|
|
362
|
+
fs.unlinkSync(filePath);
|
|
363
|
+
removed.push(file);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return removed;
|
|
312
368
|
}
|
|
313
369
|
|
|
314
370
|
function copyPromptLibrary(outputDir) {
|
package/src/composer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase,
|
|
3
|
+
import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase, getStackMetadata, walkDir, log } from './utils.js';
|
|
4
4
|
|
|
5
5
|
const TEMPLATES_DIR = path.join(ROOT_DIR, 'templates');
|
|
6
6
|
|
|
@@ -53,127 +53,28 @@ export function buildVariables(stackConfig) {
|
|
|
53
53
|
vars.AUTH_TYPE = stackConfig.auth || 'none';
|
|
54
54
|
vars.DEPLOYMENT = stackConfig.deployment || 'docker';
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
vars.STACK_DESCRIPTION =
|
|
56
|
+
// All per-stack values come from a single metadata source
|
|
57
|
+
const meta = getStackMetadata(stackConfig.stackId);
|
|
58
|
+
if (meta) {
|
|
59
|
+
Object.assign(vars, meta.commands);
|
|
60
|
+
vars.STACK_DESCRIPTION = meta.description;
|
|
61
|
+
vars.EXTRA_IGNORES = meta.extraIgnores;
|
|
62
|
+
vars.APP_PORT = meta.port;
|
|
63
|
+
vars.SETUP_COMMANDS = meta.setupCommands();
|
|
64
|
+
vars.AVAILABLE_SCRIPTS = meta.availableScripts;
|
|
65
|
+
} else {
|
|
66
|
+
vars.STACK_DESCRIPTION = '';
|
|
61
67
|
vars.EXTRA_IGNORES = '';
|
|
62
|
-
|
|
63
|
-
vars.
|
|
64
|
-
vars.
|
|
65
|
-
} else if (stackConfig.stackId === 'polyglot-fullstack') {
|
|
66
|
-
vars.STACK_DESCRIPTION = 'Full-stack application with Next.js frontend and FastAPI backend';
|
|
67
|
-
vars.EXTRA_IGNORES = '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/';
|
|
68
|
-
} else if (stackConfig.stackId === 'react-express') {
|
|
69
|
-
vars.STACK_DESCRIPTION = 'Full-stack application with React (Vite) frontend and Express backend';
|
|
70
|
-
vars.EXTRA_IGNORES = '\ndist/';
|
|
71
|
-
} else if (stackConfig.stackId === 'remix-fullstack') {
|
|
72
|
-
vars.STACK_DESCRIPTION = 'Full-stack Remix application with Vite, Tailwind CSS, and PostgreSQL';
|
|
73
|
-
vars.EXTRA_IGNORES = '\nbuild/';
|
|
74
|
-
} else if (stackConfig.stackId === 'hono-api') {
|
|
75
|
-
vars.STACK_DESCRIPTION = 'Hono API service with TypeScript, Prisma, and PostgreSQL';
|
|
76
|
-
vars.EXTRA_IGNORES = '\ndist/';
|
|
68
|
+
vars.APP_PORT = '3000';
|
|
69
|
+
vars.SETUP_COMMANDS = '';
|
|
70
|
+
vars.AVAILABLE_SCRIPTS = '';
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
vars.SETUP_COMMANDS = buildSetupCommands(stackConfig);
|
|
81
|
-
vars.AVAILABLE_SCRIPTS = buildAvailableScripts(stackConfig);
|
|
73
|
+
vars.IMAGE_TAG = '0.1.0';
|
|
82
74
|
|
|
83
75
|
return vars;
|
|
84
76
|
}
|
|
85
77
|
|
|
86
|
-
function buildSetupCommands(config) {
|
|
87
|
-
if (config.stackId === 'nextjs-fullstack') {
|
|
88
|
-
return `npm install
|
|
89
|
-
${copyEnvCmd()}
|
|
90
|
-
npx prisma db push
|
|
91
|
-
npm run dev`;
|
|
92
|
-
}
|
|
93
|
-
if (config.stackId === 'fastapi-backend') {
|
|
94
|
-
return `cd backend
|
|
95
|
-
python -m venv venv
|
|
96
|
-
source venv/bin/activate
|
|
97
|
-
pip install -r requirements.txt
|
|
98
|
-
${copyEnvCmd()}
|
|
99
|
-
uvicorn app.main:app --reload`;
|
|
100
|
-
}
|
|
101
|
-
if (config.stackId === 'polyglot-fullstack') {
|
|
102
|
-
return `docker compose up -d postgres
|
|
103
|
-
# Frontend
|
|
104
|
-
cd frontend && npm install && npm run dev
|
|
105
|
-
# Backend
|
|
106
|
-
cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload`;
|
|
107
|
-
}
|
|
108
|
-
if (config.stackId === 'react-express') {
|
|
109
|
-
return `# Frontend
|
|
110
|
-
cd frontend && npm install && npm run dev
|
|
111
|
-
# Backend (in a separate terminal)
|
|
112
|
-
cd backend && npm install && ${copyEnvCmd()} && npx prisma db push && npm run dev`;
|
|
113
|
-
}
|
|
114
|
-
if (config.stackId === 'remix-fullstack') {
|
|
115
|
-
return `npm install
|
|
116
|
-
${copyEnvCmd()}
|
|
117
|
-
npx prisma db push
|
|
118
|
-
npm run dev`;
|
|
119
|
-
}
|
|
120
|
-
if (config.stackId === 'hono-api') {
|
|
121
|
-
return `npm install
|
|
122
|
-
${copyEnvCmd()}
|
|
123
|
-
npx prisma db push
|
|
124
|
-
npm run dev`;
|
|
125
|
-
}
|
|
126
|
-
return '';
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function buildAvailableScripts(config) {
|
|
130
|
-
if (config.stackId === 'nextjs-fullstack') {
|
|
131
|
-
return `- \`npm run dev\`: Start development server
|
|
132
|
-
- \`npm run build\`: Production build
|
|
133
|
-
- \`npm run lint\`: Run ESLint
|
|
134
|
-
- \`npx prisma studio\`: Database GUI
|
|
135
|
-
- \`npx vitest\`: Run unit tests
|
|
136
|
-
- \`npx playwright test\`: Run E2E tests`;
|
|
137
|
-
}
|
|
138
|
-
if (config.stackId === 'fastapi-backend') {
|
|
139
|
-
return `- \`uvicorn app.main:app --reload\`: Start dev server
|
|
140
|
-
- \`pytest\`: Run tests
|
|
141
|
-
- \`ruff check .\`: Run linter
|
|
142
|
-
- \`alembic upgrade head\`: Run migrations`;
|
|
143
|
-
}
|
|
144
|
-
if (config.stackId === 'polyglot-fullstack') {
|
|
145
|
-
return `- \`docker compose up\`: Start all services
|
|
146
|
-
- \`docker compose up -d postgres\`: Start database only
|
|
147
|
-
- Frontend: \`cd frontend && npm run dev\`
|
|
148
|
-
- Backend: \`cd backend && uvicorn app.main:app --reload\``;
|
|
149
|
-
}
|
|
150
|
-
if (config.stackId === 'react-express') {
|
|
151
|
-
return `- Frontend: \`cd frontend && npm run dev\` (Vite dev server)
|
|
152
|
-
- Backend: \`cd backend && npm run dev\` (Express with tsx watch)
|
|
153
|
-
- \`cd backend && npm run build\`: Build backend for production
|
|
154
|
-
- \`cd frontend && npm run build\`: Build frontend for production
|
|
155
|
-
- \`cd backend && npx prisma studio\`: Database GUI
|
|
156
|
-
- \`cd frontend && npx vitest\`: Run frontend tests`;
|
|
157
|
-
}
|
|
158
|
-
if (config.stackId === 'remix-fullstack') {
|
|
159
|
-
return `- \`npm run dev\`: Start Remix dev server
|
|
160
|
-
- \`npm run build\`: Production build
|
|
161
|
-
- \`npm run start\`: Start production server
|
|
162
|
-
- \`npm run lint\`: Run ESLint
|
|
163
|
-
- \`npx prisma studio\`: Database GUI
|
|
164
|
-
- \`npx vitest\`: Run unit tests`;
|
|
165
|
-
}
|
|
166
|
-
if (config.stackId === 'hono-api') {
|
|
167
|
-
return `- \`npm run dev\`: Start Hono dev server (tsx watch)
|
|
168
|
-
- \`npm run build\`: Compile TypeScript
|
|
169
|
-
- \`npm run start\`: Start production server
|
|
170
|
-
- \`npm run lint\`: Run ESLint
|
|
171
|
-
- \`npx prisma studio\`: Database GUI
|
|
172
|
-
- \`npx vitest\`: Run unit tests`;
|
|
173
|
-
}
|
|
174
|
-
return '';
|
|
175
|
-
}
|
|
176
|
-
|
|
177
78
|
export function processTemplateDir(templateDir, outputDir, variables, prefix) {
|
|
178
79
|
const entries = walkDir(templateDir);
|
|
179
80
|
|
|
@@ -207,19 +108,7 @@ export function processTemplateDir(templateDir, outputDir, variables, prefix) {
|
|
|
207
108
|
}
|
|
208
109
|
}
|
|
209
110
|
|
|
210
|
-
|
|
211
|
-
const results = [];
|
|
212
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
213
|
-
for (const entry of entries) {
|
|
214
|
-
const fullPath = path.join(dir, entry.name);
|
|
215
|
-
if (entry.isDirectory()) {
|
|
216
|
-
results.push(...walkDir(fullPath));
|
|
217
|
-
} else {
|
|
218
|
-
results.push(fullPath);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return results;
|
|
222
|
-
}
|
|
111
|
+
// walkDir is imported from utils.js (shared with doctor-checks.js)
|
|
223
112
|
|
|
224
113
|
function injectAuthDependencies(outputDir, stackConfig) {
|
|
225
114
|
if (!stackConfig.auth || stackConfig.auth === 'none') return;
|
|
@@ -66,17 +66,20 @@ export function checkChainproofIntegrity(projectDir) {
|
|
|
66
66
|
effort: 'medium',
|
|
67
67
|
}];
|
|
68
68
|
}
|
|
69
|
-
} catch {
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// JSON parse errors → malformed file; anything else should propagate
|
|
71
|
+
if (err instanceof SyntaxError) {
|
|
72
|
+
return [{
|
|
73
|
+
severity: 'critical',
|
|
74
|
+
title: 'ChainProof chain.json is malformed',
|
|
75
|
+
impact: 'Cannot verify trust chain integrity',
|
|
76
|
+
files: ['.chainproof/chain.json'],
|
|
77
|
+
autoFixable: false,
|
|
78
|
+
promptId: 'CHAINPROOF_MALFORMED',
|
|
79
|
+
effort: 'medium',
|
|
80
|
+
}];
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
return [];
|
package/src/doctor-checks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { walkDir, DEFAULT_SKIP_DIRS } from './utils.js';
|
|
3
4
|
import {
|
|
4
5
|
checkChainproofExists,
|
|
5
6
|
checkChainproofIntegrity,
|
|
@@ -739,24 +740,7 @@ function checkAIPrompts(projectDir) {
|
|
|
739
740
|
// Helpers
|
|
740
741
|
|
|
741
742
|
function findFiles(dir, ext) {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
try {
|
|
746
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
747
|
-
for (const entry of entries) {
|
|
748
|
-
const fullPath = path.join(dir, entry.name);
|
|
749
|
-
if (entry.isDirectory()) {
|
|
750
|
-
if (['node_modules', '.next', '__pycache__', '.git', 'venv', '.venv', 'dist', 'build'].includes(entry.name)) continue;
|
|
751
|
-
results.push(...findFiles(fullPath, ext));
|
|
752
|
-
} else if (entry.name.endsWith(ext)) {
|
|
753
|
-
results.push(fullPath);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
} catch {
|
|
757
|
-
// Permission errors, etc.
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
return results;
|
|
743
|
+
if (!fs.existsSync(dir)) return [];
|
|
744
|
+
return walkDir(dir, { ext, skipDirs: DEFAULT_SKIP_DIRS });
|
|
761
745
|
}
|
|
762
746
|
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { log, toKebabCase,
|
|
5
|
+
import { log, toKebabCase, getStackMetadata } from './utils.js';
|
|
6
6
|
import { askServiceType, askRefinements, askNewMode, confirmStack } from './prompts.js';
|
|
7
7
|
import { recommend } from './recommender.js';
|
|
8
8
|
import { compose } from './composer.js';
|
|
@@ -123,39 +123,11 @@ export function printNextSteps(projectName, config, isGuided = false) {
|
|
|
123
123
|
console.log(chalk.bold(' Next steps:'));
|
|
124
124
|
console.log(` cd ${projectName}`);
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} else if (config.stackId === 'fastapi-backend') {
|
|
132
|
-
console.log(' cd backend');
|
|
133
|
-
console.log(' python -m venv venv');
|
|
134
|
-
console.log(' source venv/bin/activate # or venv\\Scripts\\activate on Windows');
|
|
135
|
-
console.log(' pip install -r requirements.txt');
|
|
136
|
-
console.log(` ${copyEnvCmd()}`);
|
|
137
|
-
console.log(' uvicorn app.main:app --reload');
|
|
138
|
-
} else if (config.stackId === 'polyglot-fullstack') {
|
|
139
|
-
console.log(' docker compose up -d postgres');
|
|
140
|
-
console.log(' # Frontend:');
|
|
141
|
-
console.log(' cd frontend && npm install && npm run dev');
|
|
142
|
-
console.log(' # Backend:');
|
|
143
|
-
console.log(' cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload');
|
|
144
|
-
} else if (config.stackId === 'react-express') {
|
|
145
|
-
console.log(' # Frontend:');
|
|
146
|
-
console.log(' cd frontend && npm install && npm run dev');
|
|
147
|
-
console.log(' # Backend (in a separate terminal):');
|
|
148
|
-
console.log(` cd backend && npm install && ${copyEnvCmd()} && npx prisma db push && npm run dev`);
|
|
149
|
-
} else if (config.stackId === 'remix-fullstack') {
|
|
150
|
-
console.log(' npm install');
|
|
151
|
-
console.log(` ${copyEnvCmd()}`);
|
|
152
|
-
console.log(' npx prisma db push');
|
|
153
|
-
console.log(' npm run dev');
|
|
154
|
-
} else if (config.stackId === 'hono-api') {
|
|
155
|
-
console.log(' npm install');
|
|
156
|
-
console.log(` ${copyEnvCmd()}`);
|
|
157
|
-
console.log(' npx prisma db push');
|
|
158
|
-
console.log(' npm run dev');
|
|
126
|
+
const meta = getStackMetadata(config.stackId);
|
|
127
|
+
if (meta) {
|
|
128
|
+
for (const line of meta.setupCommands().split('\n')) {
|
|
129
|
+
console.log(` ${line}`);
|
|
130
|
+
}
|
|
159
131
|
}
|
|
160
132
|
|
|
161
133
|
if (config.claudeCode) {
|