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/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);
|
package/src/composer.js
CHANGED
|
@@ -1,242 +1,228 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase, getStackMetadata, log } from './utils.js';
|
|
4
|
-
|
|
5
|
-
const TEMPLATES_DIR = path.join(ROOT_DIR, 'templates');
|
|
6
|
-
|
|
7
|
-
export async function compose(outputDir, stackConfig) {
|
|
8
|
-
const variables = buildVariables(stackConfig);
|
|
9
|
-
|
|
10
|
-
ensureDir(outputDir);
|
|
11
|
-
|
|
12
|
-
for (const mod of stackConfig.templateModules) {
|
|
13
|
-
const templateDir = path.join(TEMPLATES_DIR, mod.path);
|
|
14
|
-
if (!fs.existsSync(templateDir)) {
|
|
15
|
-
log.warn(`Template module not found: ${mod.path}, skipping`);
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
processTemplateDir(templateDir, outputDir, variables, mod.prefix || '');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Inject auth dependencies into package.json if auth is enabled
|
|
22
|
-
injectAuthDependencies(outputDir, stackConfig);
|
|
23
|
-
|
|
24
|
-
// Apply user plugins from ~/.devforge/templates/ if they exist
|
|
25
|
-
applyUserPlugins(outputDir, variables, stackConfig);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function buildVariables(stackConfig) {
|
|
29
|
-
const vars = {
|
|
30
|
-
PROJECT_NAME: stackConfig.projectName,
|
|
31
|
-
PROJECT_NAME_PASCAL: toPascalCase(stackConfig.projectName),
|
|
32
|
-
PROJECT_NAME_SNAKE: toSnakeCase(stackConfig.projectName),
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
if (stackConfig.frontend) {
|
|
36
|
-
vars.FRONTEND_FRAMEWORK = stackConfig.frontend.framework;
|
|
37
|
-
vars.FRONTEND_LANGUAGE = stackConfig.frontend.language;
|
|
38
|
-
vars.FRONTEND_STYLING = stackConfig.frontend.styling;
|
|
39
|
-
vars.FRONTEND_UI = stackConfig.frontend.ui || '';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (stackConfig.backend) {
|
|
43
|
-
vars.BACKEND_FRAMEWORK = stackConfig.backend.framework;
|
|
44
|
-
vars.BACKEND_LANGUAGE = stackConfig.backend.language;
|
|
45
|
-
vars.BACKEND_ORM = stackConfig.backend.orm;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (stackConfig.database) {
|
|
49
|
-
vars.DATABASE_TYPE = stackConfig.database.type;
|
|
50
|
-
vars.DATABASE_ORM = stackConfig.database.orm;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
vars.AUTH_TYPE = stackConfig.auth || 'none';
|
|
54
|
-
vars.DEPLOYMENT = stackConfig.deployment || 'docker';
|
|
55
|
-
|
|
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 = '';
|
|
67
|
-
vars.EXTRA_IGNORES = '';
|
|
68
|
-
vars.APP_PORT = '3000';
|
|
69
|
-
vars.SETUP_COMMANDS = '';
|
|
70
|
-
vars.AVAILABLE_SCRIPTS = '';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
vars.IMAGE_TAG = '0.1.0';
|
|
74
|
-
|
|
75
|
-
return vars;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function processTemplateDir(templateDir, outputDir, variables, prefix) {
|
|
79
|
-
const entries = walkDir(templateDir);
|
|
80
|
-
|
|
81
|
-
for (const filePath of entries) {
|
|
82
|
-
const relativePath = path.relative(templateDir, filePath);
|
|
83
|
-
const outputRelative = prefix ? path.join(prefix, relativePath) : relativePath;
|
|
84
|
-
|
|
85
|
-
// Guard against path traversal (e.g. ../../etc/passwd in plugin templates)
|
|
86
|
-
const resolved = path.resolve(outputDir, outputRelative);
|
|
87
|
-
if (!resolved.startsWith(path.resolve(outputDir))) {
|
|
88
|
-
log.warn(`Skipping "${outputRelative}" (path traversal detected)`);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (outputRelative.endsWith('.template')) {
|
|
93
|
-
// Process template: replace vars, strip .template extension
|
|
94
|
-
const content = readTemplate(filePath);
|
|
95
|
-
const processed = replaceVars(content, variables);
|
|
96
|
-
const outputPath = path.join(outputDir, outputRelative.replace(/\.template$/, ''));
|
|
97
|
-
ensureDir(path.dirname(outputPath));
|
|
98
|
-
fs.writeFileSync(outputPath, processed, 'utf-8');
|
|
99
|
-
} else if (path.basename(filePath) === '.gitkeep') {
|
|
100
|
-
// Create the directory but don't copy .gitkeep itself
|
|
101
|
-
ensureDir(path.join(outputDir, path.dirname(outputRelative)));
|
|
102
|
-
} else {
|
|
103
|
-
// Binary copy
|
|
104
|
-
const outputPath = path.join(outputDir, outputRelative);
|
|
105
|
-
ensureDir(path.dirname(outputPath));
|
|
106
|
-
fs.copyFileSync(filePath, outputPath);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// User
|
|
181
|
-
const
|
|
182
|
-
if (fs.existsSync(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
.map(d => d.name);
|
|
230
|
-
for (const skill of skills) {
|
|
231
|
-
const skillFile = path.join(userSkillsDir, skill, 'SKILL.md');
|
|
232
|
-
if (fs.existsSync(skillFile)) {
|
|
233
|
-
const dest = path.join(outputDir, '.claude', 'skills', skill, 'SKILL.md');
|
|
234
|
-
ensureDir(path.dirname(dest));
|
|
235
|
-
fs.copyFileSync(skillFile, dest);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (skills.length > 0) {
|
|
239
|
-
log.dim(` Applied ${skills.length} user skill(s) from ~/.devforge/skills/`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase, getStackMetadata, walkDir, log } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const TEMPLATES_DIR = path.join(ROOT_DIR, 'templates');
|
|
6
|
+
|
|
7
|
+
export async function compose(outputDir, stackConfig) {
|
|
8
|
+
const variables = buildVariables(stackConfig);
|
|
9
|
+
|
|
10
|
+
ensureDir(outputDir);
|
|
11
|
+
|
|
12
|
+
for (const mod of stackConfig.templateModules) {
|
|
13
|
+
const templateDir = path.join(TEMPLATES_DIR, mod.path);
|
|
14
|
+
if (!fs.existsSync(templateDir)) {
|
|
15
|
+
log.warn(`Template module not found: ${mod.path}, skipping`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
processTemplateDir(templateDir, outputDir, variables, mod.prefix || '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Inject auth dependencies into package.json if auth is enabled
|
|
22
|
+
injectAuthDependencies(outputDir, stackConfig);
|
|
23
|
+
|
|
24
|
+
// Apply user plugins from ~/.devforge/templates/ if they exist
|
|
25
|
+
applyUserPlugins(outputDir, variables, stackConfig);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildVariables(stackConfig) {
|
|
29
|
+
const vars = {
|
|
30
|
+
PROJECT_NAME: stackConfig.projectName,
|
|
31
|
+
PROJECT_NAME_PASCAL: toPascalCase(stackConfig.projectName),
|
|
32
|
+
PROJECT_NAME_SNAKE: toSnakeCase(stackConfig.projectName),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (stackConfig.frontend) {
|
|
36
|
+
vars.FRONTEND_FRAMEWORK = stackConfig.frontend.framework;
|
|
37
|
+
vars.FRONTEND_LANGUAGE = stackConfig.frontend.language;
|
|
38
|
+
vars.FRONTEND_STYLING = stackConfig.frontend.styling;
|
|
39
|
+
vars.FRONTEND_UI = stackConfig.frontend.ui || '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (stackConfig.backend) {
|
|
43
|
+
vars.BACKEND_FRAMEWORK = stackConfig.backend.framework;
|
|
44
|
+
vars.BACKEND_LANGUAGE = stackConfig.backend.language;
|
|
45
|
+
vars.BACKEND_ORM = stackConfig.backend.orm;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (stackConfig.database) {
|
|
49
|
+
vars.DATABASE_TYPE = stackConfig.database.type;
|
|
50
|
+
vars.DATABASE_ORM = stackConfig.database.orm;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
vars.AUTH_TYPE = stackConfig.auth || 'none';
|
|
54
|
+
vars.DEPLOYMENT = stackConfig.deployment || 'docker';
|
|
55
|
+
|
|
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 = '';
|
|
67
|
+
vars.EXTRA_IGNORES = '';
|
|
68
|
+
vars.APP_PORT = '3000';
|
|
69
|
+
vars.SETUP_COMMANDS = '';
|
|
70
|
+
vars.AVAILABLE_SCRIPTS = '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
vars.IMAGE_TAG = '0.1.0';
|
|
74
|
+
|
|
75
|
+
return vars;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function processTemplateDir(templateDir, outputDir, variables, prefix) {
|
|
79
|
+
const entries = walkDir(templateDir);
|
|
80
|
+
|
|
81
|
+
for (const filePath of entries) {
|
|
82
|
+
const relativePath = path.relative(templateDir, filePath);
|
|
83
|
+
const outputRelative = prefix ? path.join(prefix, relativePath) : relativePath;
|
|
84
|
+
|
|
85
|
+
// Guard against path traversal (e.g. ../../etc/passwd in plugin templates)
|
|
86
|
+
const resolved = path.resolve(outputDir, outputRelative);
|
|
87
|
+
if (!resolved.startsWith(path.resolve(outputDir))) {
|
|
88
|
+
log.warn(`Skipping "${outputRelative}" (path traversal detected)`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (outputRelative.endsWith('.template')) {
|
|
93
|
+
// Process template: replace vars, strip .template extension
|
|
94
|
+
const content = readTemplate(filePath);
|
|
95
|
+
const processed = replaceVars(content, variables);
|
|
96
|
+
const outputPath = path.join(outputDir, outputRelative.replace(/\.template$/, ''));
|
|
97
|
+
ensureDir(path.dirname(outputPath));
|
|
98
|
+
fs.writeFileSync(outputPath, processed, 'utf-8');
|
|
99
|
+
} else if (path.basename(filePath) === '.gitkeep') {
|
|
100
|
+
// Create the directory but don't copy .gitkeep itself
|
|
101
|
+
ensureDir(path.join(outputDir, path.dirname(outputRelative)));
|
|
102
|
+
} else {
|
|
103
|
+
// Binary copy
|
|
104
|
+
const outputPath = path.join(outputDir, outputRelative);
|
|
105
|
+
ensureDir(path.dirname(outputPath));
|
|
106
|
+
fs.copyFileSync(filePath, outputPath);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// walkDir is imported from utils.js (shared with doctor-checks.js)
|
|
112
|
+
|
|
113
|
+
function injectAuthDependencies(outputDir, stackConfig) {
|
|
114
|
+
if (!stackConfig.auth || stackConfig.auth === 'none') return;
|
|
115
|
+
|
|
116
|
+
// Next.js projects with nextauth
|
|
117
|
+
if (stackConfig.auth === 'nextauth' || stackConfig.auth === 'both') {
|
|
118
|
+
const pkgPaths = [
|
|
119
|
+
path.join(outputDir, 'package.json'),
|
|
120
|
+
path.join(outputDir, 'frontend', 'package.json'),
|
|
121
|
+
];
|
|
122
|
+
for (const pkgPath of pkgPaths) {
|
|
123
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
124
|
+
let pkg;
|
|
125
|
+
try {
|
|
126
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
127
|
+
} catch {
|
|
128
|
+
log.warn(`Skipping auth dependency injection — could not parse ${pkgPath}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
132
|
+
pkg.dependencies['next-auth'] = '^5.0.0-beta.25';
|
|
133
|
+
pkg.dependencies['@auth/prisma-adapter'] = '^2.7.4';
|
|
134
|
+
pkg.dependencies['bcryptjs'] = '^2.4.3';
|
|
135
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
136
|
+
pkg.devDependencies['@types/bcryptjs'] = '^2.4.6';
|
|
137
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// FastAPI projects with jwt-custom
|
|
142
|
+
if (stackConfig.auth === 'jwt-custom' || stackConfig.auth === 'both') {
|
|
143
|
+
const reqPaths = [
|
|
144
|
+
path.join(outputDir, 'requirements.txt'),
|
|
145
|
+
path.join(outputDir, 'backend', 'requirements.txt'),
|
|
146
|
+
];
|
|
147
|
+
for (const reqPath of reqPaths) {
|
|
148
|
+
if (!fs.existsSync(reqPath)) continue;
|
|
149
|
+
const content = fs.readFileSync(reqPath, 'utf-8');
|
|
150
|
+
const authDeps = ['python-jose[cryptography]', 'passlib[bcrypt]', 'python-multipart'];
|
|
151
|
+
const additions = authDeps.filter(dep => !content.includes(dep.split('[')[0]));
|
|
152
|
+
if (additions.length > 0) {
|
|
153
|
+
fs.writeFileSync(reqPath, content.trimEnd() + '\n' + additions.join('\n') + '\n', 'utf-8');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function applyUserPlugins(outputDir, variables, stackConfig) {
|
|
160
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
161
|
+
if (!homeDir) return;
|
|
162
|
+
|
|
163
|
+
const pluginDir = path.join(homeDir, '.devforge');
|
|
164
|
+
if (!fs.existsSync(pluginDir)) return;
|
|
165
|
+
|
|
166
|
+
// User templates: ~/.devforge/templates/<stackId>/ → overlay onto output
|
|
167
|
+
const userTemplatesDir = path.join(pluginDir, 'templates', stackConfig.stackId);
|
|
168
|
+
if (fs.existsSync(userTemplatesDir)) {
|
|
169
|
+
processTemplateDir(userTemplatesDir, outputDir, variables, '');
|
|
170
|
+
log.dim(` Applied user templates from ~/.devforge/templates/${stackConfig.stackId}/`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Universal user templates: ~/.devforge/templates/universal/ → always applied
|
|
174
|
+
const universalDir = path.join(pluginDir, 'templates', 'universal');
|
|
175
|
+
if (fs.existsSync(universalDir)) {
|
|
176
|
+
processTemplateDir(universalDir, outputDir, variables, '');
|
|
177
|
+
log.dim(' Applied user templates from ~/.devforge/templates/universal/');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// User agents: ~/.devforge/agents/*.md → copy into .claude/agents/
|
|
181
|
+
const userAgentsDir = path.join(pluginDir, 'agents');
|
|
182
|
+
if (fs.existsSync(userAgentsDir)) {
|
|
183
|
+
const agents = fs.readdirSync(userAgentsDir).filter(f => f.endsWith('.md'));
|
|
184
|
+
for (const agent of agents) {
|
|
185
|
+
const src = path.join(userAgentsDir, agent);
|
|
186
|
+
const dest = path.join(outputDir, '.claude', 'agents', agent);
|
|
187
|
+
ensureDir(path.dirname(dest));
|
|
188
|
+
fs.copyFileSync(src, dest);
|
|
189
|
+
}
|
|
190
|
+
if (agents.length > 0) {
|
|
191
|
+
log.dim(` Applied ${agents.length} user agent(s) from ~/.devforge/agents/`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// User commands: ~/.devforge/commands/*.md → copy into .claude/commands/
|
|
196
|
+
const userCommandsDir = path.join(pluginDir, 'commands');
|
|
197
|
+
if (fs.existsSync(userCommandsDir)) {
|
|
198
|
+
const commands = fs.readdirSync(userCommandsDir).filter(f => f.endsWith('.md'));
|
|
199
|
+
for (const cmd of commands) {
|
|
200
|
+
const src = path.join(userCommandsDir, cmd);
|
|
201
|
+
const dest = path.join(outputDir, '.claude', 'commands', cmd);
|
|
202
|
+
ensureDir(path.dirname(dest));
|
|
203
|
+
fs.copyFileSync(src, dest);
|
|
204
|
+
}
|
|
205
|
+
if (commands.length > 0) {
|
|
206
|
+
log.dim(` Applied ${commands.length} user command(s) from ~/.devforge/commands/`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// User skills: ~/.devforge/skills/<name>/SKILL.md → copy into .claude/skills/
|
|
211
|
+
const userSkillsDir = path.join(pluginDir, 'skills');
|
|
212
|
+
if (fs.existsSync(userSkillsDir)) {
|
|
213
|
+
const skills = fs.readdirSync(userSkillsDir, { withFileTypes: true })
|
|
214
|
+
.filter(d => d.isDirectory())
|
|
215
|
+
.map(d => d.name);
|
|
216
|
+
for (const skill of skills) {
|
|
217
|
+
const skillFile = path.join(userSkillsDir, skill, 'SKILL.md');
|
|
218
|
+
if (fs.existsSync(skillFile)) {
|
|
219
|
+
const dest = path.join(outputDir, '.claude', 'skills', skill, 'SKILL.md');
|
|
220
|
+
ensureDir(path.dirname(dest));
|
|
221
|
+
fs.copyFileSync(skillFile, dest);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (skills.length > 0) {
|
|
225
|
+
log.dim(` Applied ${skills.length} user skill(s) from ~/.devforge/skills/`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -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) {
|