forgedev 1.2.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/README.md +57 -10
- package/bin/chainproof.js +126 -0
- package/bin/devforge.js +1 -1
- package/package.json +25 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +171 -78
- package/src/cli.js +30 -7
- package/src/composer.js +242 -214
- package/src/doctor-checks-chainproof.js +106 -0
- package/src/doctor-checks.js +39 -20
- package/src/doctor-prompts.js +9 -9
- package/src/doctor.js +37 -4
- package/src/guided.js +3 -3
- package/src/index.js +31 -10
- package/src/init-mode.js +76 -12
- package/src/menu.js +178 -0
- package/src/prompts.js +5 -12
- package/src/recommender.js +163 -30
- package/src/scanner.js +57 -2
- package/src/uat-generator.js +204 -189
- package/src/update-check.js +9 -4
- package/src/update.js +57 -13
- package/src/utils.js +162 -5
- package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
- package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
- package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
- package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
- package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
- package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
- package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
- package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
- package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
- package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
- package/templates/backend/express/Dockerfile.template +18 -0
- package/templates/backend/express/package.json.template +33 -0
- package/templates/backend/express/src/index.ts.template +34 -0
- package/templates/backend/express/src/routes/health.ts.template +27 -0
- package/templates/backend/express/tsconfig.json +17 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
- package/templates/backend/fastapi/backend/app/main.py.template +3 -1
- package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
- package/templates/backend/hono/Dockerfile.template +18 -0
- package/templates/backend/hono/package.json.template +31 -0
- package/templates/backend/hono/src/index.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +27 -0
- package/templates/backend/hono/tsconfig.json +18 -0
- package/templates/base/.gitignore.template +3 -0
- package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
- package/templates/chainproof/base/.chainproof/config.json.template +11 -0
- package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
- package/templates/chainproof/base/.mcp.json +9 -0
- package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
- package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
- package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
- package/templates/claude-code/agents/architect.md +25 -11
- package/templates/claude-code/agents/build-error-resolver.md +19 -5
- package/templates/claude-code/agents/chief-of-staff.md +42 -8
- package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
- package/templates/claude-code/agents/database-reviewer.md +15 -1
- package/templates/claude-code/agents/deep-reviewer.md +191 -0
- package/templates/claude-code/agents/doc-updater.md +19 -5
- package/templates/claude-code/agents/docs-lookup.md +19 -5
- package/templates/claude-code/agents/e2e-runner.md +26 -12
- package/templates/claude-code/agents/enforcement-gate.md +102 -0
- package/templates/claude-code/agents/frontend-builder.md +188 -0
- package/templates/claude-code/agents/harness-optimizer.md +36 -1
- package/templates/claude-code/agents/loop-operator.md +27 -13
- package/templates/claude-code/agents/planner.md +21 -7
- package/templates/claude-code/agents/product-strategist.md +24 -10
- package/templates/claude-code/agents/production-readiness.md +14 -0
- package/templates/claude-code/agents/prompt-auditor.md +115 -0
- package/templates/claude-code/agents/refactor-cleaner.md +22 -8
- package/templates/claude-code/agents/security-reviewer.md +14 -0
- package/templates/claude-code/agents/spec-validator.md +15 -1
- package/templates/claude-code/agents/tdd-guide.md +21 -7
- package/templates/claude-code/agents/uat-validator.md +14 -0
- package/templates/claude-code/claude-md/base.md +14 -7
- package/templates/claude-code/claude-md/fastapi.md +8 -8
- package/templates/claude-code/claude-md/fullstack.md +6 -6
- package/templates/claude-code/claude-md/hono.md +18 -0
- package/templates/claude-code/claude-md/nextjs.md +5 -5
- package/templates/claude-code/claude-md/remix.md +18 -0
- package/templates/claude-code/commands/audit-security.md +14 -0
- package/templates/claude-code/commands/audit-spec.md +14 -0
- package/templates/claude-code/commands/audit-wiring.md +14 -0
- package/templates/claude-code/commands/build-fix.md +28 -0
- package/templates/claude-code/commands/build-ui.md +59 -0
- package/templates/claude-code/commands/code-review.md +53 -31
- package/templates/claude-code/commands/fix-loop.md +211 -0
- package/templates/claude-code/commands/full-audit.md +36 -8
- package/templates/claude-code/commands/generate-prd.md +1 -1
- package/templates/claude-code/commands/generate-sdd.md +74 -0
- package/templates/claude-code/commands/generate-uat.md +107 -35
- package/templates/claude-code/commands/help.md +68 -0
- package/templates/claude-code/commands/live-uat.md +268 -0
- package/templates/claude-code/commands/optimize-claude-md.md +15 -1
- package/templates/claude-code/commands/plan.md +3 -3
- package/templates/claude-code/commands/pre-pr.md +57 -19
- package/templates/claude-code/commands/product-strategist.md +21 -0
- package/templates/claude-code/commands/resume-session.md +10 -10
- package/templates/claude-code/commands/run-uat.md +59 -2
- package/templates/claude-code/commands/save-session.md +10 -10
- package/templates/claude-code/commands/simplify.md +36 -0
- package/templates/claude-code/commands/tdd.md +17 -18
- package/templates/claude-code/commands/verify-all.md +24 -0
- package/templates/claude-code/commands/verify-intent.md +55 -0
- package/templates/claude-code/commands/workflows.md +52 -40
- package/templates/claude-code/hooks/polyglot.json +10 -1
- package/templates/claude-code/hooks/python.json +10 -1
- package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
- package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
- package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
- package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
- package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
- package/templates/claude-code/hooks/typescript.json +10 -1
- package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
- package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
- package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
- package/templates/claude-code/skills/playwright/SKILL.md +5 -5
- package/templates/claude-code/skills/security-api/SKILL.md +1 -1
- package/templates/claude-code/skills/security-web/SKILL.md +1 -1
- package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
- package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
- package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
- package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
- package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
- package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
- package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
- package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
- package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
- package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
- package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
- package/templates/frontend/nextjs/package.json.template +3 -1
- package/templates/frontend/react/index.html.template +12 -0
- package/templates/frontend/react/package.json.template +34 -0
- package/templates/frontend/react/src/App.tsx.template +10 -0
- package/templates/frontend/react/src/index.css +1 -0
- package/templates/frontend/react/src/main.tsx +10 -0
- package/templates/frontend/react/tsconfig.json +17 -0
- package/templates/frontend/react/vite.config.ts.template +15 -0
- package/templates/frontend/react/vitest.config.ts +9 -0
- package/templates/frontend/remix/app/root.tsx.template +31 -0
- package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
- package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
- package/templates/frontend/remix/app/tailwind.css +1 -0
- package/templates/frontend/remix/package.json.template +39 -0
- package/templates/frontend/remix/tsconfig.json +18 -0
- package/templates/frontend/remix/vite.config.ts.template +7 -0
- package/templates/infra/github-actions/.github/workflows/ci.yml.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/docs/00-README.md +0 -310
- package/docs/01-universal-prompt-library.md +0 -1049
- package/docs/02-claude-code-mastery-playbook.md +0 -283
- package/docs/03-multi-agent-verification.md +0 -565
- package/docs/04-errata-and-verification-checklist.md +0 -284
- package/docs/05-universal-scaffolder-vision.md +0 -452
- package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
- package/docs/errata.md +0 -58
- package/docs/multi-agent-verification.md +0 -66
- package/docs/playbook.md +0 -95
- package/docs/prompt-library.md +0 -160
- package/docs/uat/UAT_CHECKLIST.csv +0 -9
- package/docs/uat/UAT_TEMPLATE.md +0 -163
- package/templates/claude-code/commands/done.md +0 -19
- /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
package/src/composer.js
CHANGED
|
@@ -1,214 +1,242 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase, 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}
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
vars.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
vars.
|
|
60
|
-
vars.STACK_DESCRIPTION =
|
|
61
|
-
vars.EXTRA_IGNORES =
|
|
62
|
-
|
|
63
|
-
vars.
|
|
64
|
-
vars.
|
|
65
|
-
|
|
66
|
-
vars.
|
|
67
|
-
vars.
|
|
68
|
-
vars.
|
|
69
|
-
vars.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
path.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
function walkDir(dir, depth = 0) {
|
|
112
|
+
if (depth > 20) return []; // guard against deeply nested or circular structures
|
|
113
|
+
const results = [];
|
|
114
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (entry.isSymbolicLink()) continue; // skip symlinks to prevent infinite loops
|
|
117
|
+
const fullPath = path.join(dir, entry.name);
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
results.push(...walkDir(fullPath, depth + 1));
|
|
120
|
+
} else {
|
|
121
|
+
results.push(fullPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function injectAuthDependencies(outputDir, stackConfig) {
|
|
128
|
+
if (!stackConfig.auth || stackConfig.auth === 'none') return;
|
|
129
|
+
|
|
130
|
+
// Next.js projects with nextauth
|
|
131
|
+
if (stackConfig.auth === 'nextauth' || stackConfig.auth === 'both') {
|
|
132
|
+
const pkgPaths = [
|
|
133
|
+
path.join(outputDir, 'package.json'),
|
|
134
|
+
path.join(outputDir, 'frontend', 'package.json'),
|
|
135
|
+
];
|
|
136
|
+
for (const pkgPath of pkgPaths) {
|
|
137
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
138
|
+
let pkg;
|
|
139
|
+
try {
|
|
140
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
141
|
+
} catch {
|
|
142
|
+
log.warn(`Skipping auth dependency injection — could not parse ${pkgPath}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
146
|
+
pkg.dependencies['next-auth'] = '^5.0.0-beta.25';
|
|
147
|
+
pkg.dependencies['@auth/prisma-adapter'] = '^2.7.4';
|
|
148
|
+
pkg.dependencies['bcryptjs'] = '^2.4.3';
|
|
149
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
150
|
+
pkg.devDependencies['@types/bcryptjs'] = '^2.4.6';
|
|
151
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// FastAPI projects with jwt-custom
|
|
156
|
+
if (stackConfig.auth === 'jwt-custom' || stackConfig.auth === 'both') {
|
|
157
|
+
const reqPaths = [
|
|
158
|
+
path.join(outputDir, 'requirements.txt'),
|
|
159
|
+
path.join(outputDir, 'backend', 'requirements.txt'),
|
|
160
|
+
];
|
|
161
|
+
for (const reqPath of reqPaths) {
|
|
162
|
+
if (!fs.existsSync(reqPath)) continue;
|
|
163
|
+
const content = fs.readFileSync(reqPath, 'utf-8');
|
|
164
|
+
const authDeps = ['python-jose[cryptography]', 'passlib[bcrypt]', 'python-multipart'];
|
|
165
|
+
const additions = authDeps.filter(dep => !content.includes(dep.split('[')[0]));
|
|
166
|
+
if (additions.length > 0) {
|
|
167
|
+
fs.writeFileSync(reqPath, content.trimEnd() + '\n' + additions.join('\n') + '\n', 'utf-8');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function applyUserPlugins(outputDir, variables, stackConfig) {
|
|
174
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
175
|
+
if (!homeDir) return;
|
|
176
|
+
|
|
177
|
+
const pluginDir = path.join(homeDir, '.devforge');
|
|
178
|
+
if (!fs.existsSync(pluginDir)) return;
|
|
179
|
+
|
|
180
|
+
// User templates: ~/.devforge/templates/<stackId>/ → overlay onto output
|
|
181
|
+
const userTemplatesDir = path.join(pluginDir, 'templates', stackConfig.stackId);
|
|
182
|
+
if (fs.existsSync(userTemplatesDir)) {
|
|
183
|
+
processTemplateDir(userTemplatesDir, outputDir, variables, '');
|
|
184
|
+
log.dim(` Applied user templates from ~/.devforge/templates/${stackConfig.stackId}/`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Universal user templates: ~/.devforge/templates/universal/ → always applied
|
|
188
|
+
const universalDir = path.join(pluginDir, 'templates', 'universal');
|
|
189
|
+
if (fs.existsSync(universalDir)) {
|
|
190
|
+
processTemplateDir(universalDir, outputDir, variables, '');
|
|
191
|
+
log.dim(' Applied user templates from ~/.devforge/templates/universal/');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// User agents: ~/.devforge/agents/*.md → copy into .claude/agents/
|
|
195
|
+
const userAgentsDir = path.join(pluginDir, 'agents');
|
|
196
|
+
if (fs.existsSync(userAgentsDir)) {
|
|
197
|
+
const agents = fs.readdirSync(userAgentsDir).filter(f => f.endsWith('.md'));
|
|
198
|
+
for (const agent of agents) {
|
|
199
|
+
const src = path.join(userAgentsDir, agent);
|
|
200
|
+
const dest = path.join(outputDir, '.claude', 'agents', agent);
|
|
201
|
+
ensureDir(path.dirname(dest));
|
|
202
|
+
fs.copyFileSync(src, dest);
|
|
203
|
+
}
|
|
204
|
+
if (agents.length > 0) {
|
|
205
|
+
log.dim(` Applied ${agents.length} user agent(s) from ~/.devforge/agents/`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// User commands: ~/.devforge/commands/*.md → copy into .claude/commands/
|
|
210
|
+
const userCommandsDir = path.join(pluginDir, 'commands');
|
|
211
|
+
if (fs.existsSync(userCommandsDir)) {
|
|
212
|
+
const commands = fs.readdirSync(userCommandsDir).filter(f => f.endsWith('.md'));
|
|
213
|
+
for (const cmd of commands) {
|
|
214
|
+
const src = path.join(userCommandsDir, cmd);
|
|
215
|
+
const dest = path.join(outputDir, '.claude', 'commands', cmd);
|
|
216
|
+
ensureDir(path.dirname(dest));
|
|
217
|
+
fs.copyFileSync(src, dest);
|
|
218
|
+
}
|
|
219
|
+
if (commands.length > 0) {
|
|
220
|
+
log.dim(` Applied ${commands.length} user command(s) from ~/.devforge/commands/`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// User skills: ~/.devforge/skills/<name>/SKILL.md → copy into .claude/skills/
|
|
225
|
+
const userSkillsDir = path.join(pluginDir, 'skills');
|
|
226
|
+
if (fs.existsSync(userSkillsDir)) {
|
|
227
|
+
const skills = fs.readdirSync(userSkillsDir, { withFileTypes: true })
|
|
228
|
+
.filter(d => d.isDirectory())
|
|
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
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
export function checkChainproofExists(projectDir) {
|
|
6
|
+
const cpDir = path.join(projectDir, '.chainproof');
|
|
7
|
+
if (fs.existsSync(cpDir)) return [];
|
|
8
|
+
|
|
9
|
+
return [{
|
|
10
|
+
severity: 'info',
|
|
11
|
+
title: 'ChainProof trust chain not initialized',
|
|
12
|
+
impact: 'No provenance tracking for AI-generated code changes',
|
|
13
|
+
files: [],
|
|
14
|
+
autoFixable: true,
|
|
15
|
+
promptId: 'CHAINPROOF_MISSING',
|
|
16
|
+
effort: 'quick',
|
|
17
|
+
}];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function checkChainproofIntegrity(projectDir) {
|
|
21
|
+
const chainPath = path.join(projectDir, '.chainproof', 'chain.json');
|
|
22
|
+
if (!fs.existsSync(chainPath)) return [];
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const chain = JSON.parse(fs.readFileSync(chainPath, 'utf-8'));
|
|
26
|
+
let expectedHash = '0'.repeat(64);
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < chain.entries.length; i++) {
|
|
29
|
+
const entry = chain.entries[i];
|
|
30
|
+
if (entry.prevHash !== expectedHash) {
|
|
31
|
+
return [{
|
|
32
|
+
severity: 'critical',
|
|
33
|
+
title: `ChainProof hash chain broken at entry ${i}`,
|
|
34
|
+
impact: 'Trust chain integrity compromised. Provenance trail is unreliable',
|
|
35
|
+
files: ['.chainproof/chain.json'],
|
|
36
|
+
autoFixable: false,
|
|
37
|
+
promptId: 'CHAINPROOF_BROKEN',
|
|
38
|
+
effort: 'medium',
|
|
39
|
+
}];
|
|
40
|
+
}
|
|
41
|
+
const contentHash = createHash('sha256').update(entry.content, 'utf-8').digest('hex');
|
|
42
|
+
const computedChainHash = createHash('sha256').update(entry.prevHash + contentHash, 'utf-8').digest('hex');
|
|
43
|
+
if (entry.chainHash !== computedChainHash) {
|
|
44
|
+
return [{
|
|
45
|
+
severity: 'critical',
|
|
46
|
+
title: `ChainProof chainHash mismatch at entry ${i}`,
|
|
47
|
+
impact: 'Entry hash does not match computed value. Chain may have been tampered with',
|
|
48
|
+
files: ['.chainproof/chain.json'],
|
|
49
|
+
autoFixable: false,
|
|
50
|
+
promptId: 'CHAINPROOF_BROKEN',
|
|
51
|
+
effort: 'medium',
|
|
52
|
+
}];
|
|
53
|
+
}
|
|
54
|
+
expectedHash = computedChainHash;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Verify currentHash matches the last entry (same check as runtime verifier)
|
|
58
|
+
if (chain.entries.length > 0 && chain.currentHash !== expectedHash) {
|
|
59
|
+
return [{
|
|
60
|
+
severity: 'critical',
|
|
61
|
+
title: 'ChainProof currentHash is inconsistent with chain',
|
|
62
|
+
impact: 'The stored current hash does not match the last entry. Chain state is corrupted',
|
|
63
|
+
files: ['.chainproof/chain.json'],
|
|
64
|
+
autoFixable: false,
|
|
65
|
+
promptId: 'CHAINPROOF_BROKEN',
|
|
66
|
+
effort: 'medium',
|
|
67
|
+
}];
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Malformed chain.json
|
|
71
|
+
return [{
|
|
72
|
+
severity: 'critical',
|
|
73
|
+
title: 'ChainProof chain.json is malformed',
|
|
74
|
+
impact: 'Cannot verify trust chain integrity',
|
|
75
|
+
files: ['.chainproof/chain.json'],
|
|
76
|
+
autoFixable: false,
|
|
77
|
+
promptId: 'CHAINPROOF_MALFORMED',
|
|
78
|
+
effort: 'medium',
|
|
79
|
+
}];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function checkChainproofUnsigned(projectDir) {
|
|
86
|
+
const chainPath = path.join(projectDir, '.chainproof', 'chain.json');
|
|
87
|
+
if (!fs.existsSync(chainPath)) return [];
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const chain = JSON.parse(fs.readFileSync(chainPath, 'utf-8'));
|
|
91
|
+
const unsigned = chain.entries.filter(e => !e.signature);
|
|
92
|
+
if (unsigned.length === 0) return [];
|
|
93
|
+
|
|
94
|
+
return [{
|
|
95
|
+
severity: 'warning',
|
|
96
|
+
title: `${unsigned.length} unsigned entries in ChainProof trust chain`,
|
|
97
|
+
impact: 'Unsigned entries cannot be cryptographically verified',
|
|
98
|
+
files: ['.chainproof/chain.json'],
|
|
99
|
+
autoFixable: false,
|
|
100
|
+
promptId: 'CHAINPROOF_UNSIGNED',
|
|
101
|
+
effort: 'quick',
|
|
102
|
+
}];
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|