forgedev 1.3.0 → 1.4.0
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/claude-configurator.js +114 -58
- package/src/composer.js +242 -339
- package/src/init-mode.js +14 -3
- package/src/recommender.js +85 -76
- package/src/update.js +56 -12
- package/src/utils.js +123 -25
- package/templates/base/.gitignore.template +3 -0
- 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
|
@@ -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) {
|