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.
Files changed (183) hide show
  1. package/README.md +57 -10
  2. package/bin/chainproof.js +126 -0
  3. package/bin/devforge.js +1 -1
  4. package/package.json +25 -7
  5. package/src/chainproof-bridge.js +330 -0
  6. package/src/ci-mode.js +85 -0
  7. package/src/claude-configurator.js +171 -78
  8. package/src/cli.js +30 -7
  9. package/src/composer.js +242 -214
  10. package/src/doctor-checks-chainproof.js +106 -0
  11. package/src/doctor-checks.js +39 -20
  12. package/src/doctor-prompts.js +9 -9
  13. package/src/doctor.js +37 -4
  14. package/src/guided.js +3 -3
  15. package/src/index.js +31 -10
  16. package/src/init-mode.js +76 -12
  17. package/src/menu.js +178 -0
  18. package/src/prompts.js +5 -12
  19. package/src/recommender.js +163 -30
  20. package/src/scanner.js +57 -2
  21. package/src/uat-generator.js +204 -189
  22. package/src/update-check.js +9 -4
  23. package/src/update.js +57 -13
  24. package/src/utils.js +162 -5
  25. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  29. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  34. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  35. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  36. package/templates/backend/express/Dockerfile.template +18 -0
  37. package/templates/backend/express/package.json.template +33 -0
  38. package/templates/backend/express/src/index.ts.template +34 -0
  39. package/templates/backend/express/src/routes/health.ts.template +27 -0
  40. package/templates/backend/express/tsconfig.json +17 -0
  41. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  42. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  44. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  45. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  46. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  47. package/templates/backend/hono/Dockerfile.template +18 -0
  48. package/templates/backend/hono/package.json.template +31 -0
  49. package/templates/backend/hono/src/index.ts.template +32 -0
  50. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  51. package/templates/backend/hono/tsconfig.json +18 -0
  52. package/templates/base/.gitignore.template +3 -0
  53. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
  54. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  55. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  56. package/templates/chainproof/base/.mcp.json +9 -0
  57. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  58. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  59. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  60. package/templates/claude-code/agents/architect.md +25 -11
  61. package/templates/claude-code/agents/build-error-resolver.md +19 -5
  62. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  63. package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
  64. package/templates/claude-code/agents/database-reviewer.md +15 -1
  65. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  66. package/templates/claude-code/agents/doc-updater.md +19 -5
  67. package/templates/claude-code/agents/docs-lookup.md +19 -5
  68. package/templates/claude-code/agents/e2e-runner.md +26 -12
  69. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  70. package/templates/claude-code/agents/frontend-builder.md +188 -0
  71. package/templates/claude-code/agents/harness-optimizer.md +36 -1
  72. package/templates/claude-code/agents/loop-operator.md +27 -13
  73. package/templates/claude-code/agents/planner.md +21 -7
  74. package/templates/claude-code/agents/product-strategist.md +24 -10
  75. package/templates/claude-code/agents/production-readiness.md +14 -0
  76. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  77. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  78. package/templates/claude-code/agents/security-reviewer.md +14 -0
  79. package/templates/claude-code/agents/spec-validator.md +15 -1
  80. package/templates/claude-code/agents/tdd-guide.md +21 -7
  81. package/templates/claude-code/agents/uat-validator.md +14 -0
  82. package/templates/claude-code/claude-md/base.md +14 -7
  83. package/templates/claude-code/claude-md/fastapi.md +8 -8
  84. package/templates/claude-code/claude-md/fullstack.md +6 -6
  85. package/templates/claude-code/claude-md/hono.md +18 -0
  86. package/templates/claude-code/claude-md/nextjs.md +5 -5
  87. package/templates/claude-code/claude-md/remix.md +18 -0
  88. package/templates/claude-code/commands/audit-security.md +14 -0
  89. package/templates/claude-code/commands/audit-spec.md +14 -0
  90. package/templates/claude-code/commands/audit-wiring.md +14 -0
  91. package/templates/claude-code/commands/build-fix.md +28 -0
  92. package/templates/claude-code/commands/build-ui.md +59 -0
  93. package/templates/claude-code/commands/code-review.md +53 -31
  94. package/templates/claude-code/commands/fix-loop.md +211 -0
  95. package/templates/claude-code/commands/full-audit.md +36 -8
  96. package/templates/claude-code/commands/generate-prd.md +1 -1
  97. package/templates/claude-code/commands/generate-sdd.md +74 -0
  98. package/templates/claude-code/commands/generate-uat.md +107 -35
  99. package/templates/claude-code/commands/help.md +68 -0
  100. package/templates/claude-code/commands/live-uat.md +268 -0
  101. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  102. package/templates/claude-code/commands/plan.md +3 -3
  103. package/templates/claude-code/commands/pre-pr.md +57 -19
  104. package/templates/claude-code/commands/product-strategist.md +21 -0
  105. package/templates/claude-code/commands/resume-session.md +10 -10
  106. package/templates/claude-code/commands/run-uat.md +59 -2
  107. package/templates/claude-code/commands/save-session.md +10 -10
  108. package/templates/claude-code/commands/simplify.md +36 -0
  109. package/templates/claude-code/commands/tdd.md +17 -18
  110. package/templates/claude-code/commands/verify-all.md +24 -0
  111. package/templates/claude-code/commands/verify-intent.md +55 -0
  112. package/templates/claude-code/commands/workflows.md +52 -40
  113. package/templates/claude-code/hooks/polyglot.json +10 -1
  114. package/templates/claude-code/hooks/python.json +10 -1
  115. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
  116. package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
  117. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
  118. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  119. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  120. package/templates/claude-code/hooks/typescript.json +10 -1
  121. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  122. package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
  123. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  124. package/templates/claude-code/skills/playwright/SKILL.md +5 -5
  125. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  126. package/templates/claude-code/skills/security-web/SKILL.md +1 -1
  127. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  128. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  129. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  130. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  131. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  132. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  133. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  134. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  135. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  136. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  137. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  138. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  139. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  140. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  141. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  142. package/templates/frontend/nextjs/package.json.template +3 -1
  143. package/templates/frontend/react/index.html.template +12 -0
  144. package/templates/frontend/react/package.json.template +34 -0
  145. package/templates/frontend/react/src/App.tsx.template +10 -0
  146. package/templates/frontend/react/src/index.css +1 -0
  147. package/templates/frontend/react/src/main.tsx +10 -0
  148. package/templates/frontend/react/tsconfig.json +17 -0
  149. package/templates/frontend/react/vite.config.ts.template +15 -0
  150. package/templates/frontend/react/vitest.config.ts +9 -0
  151. package/templates/frontend/remix/app/root.tsx.template +31 -0
  152. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  153. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  154. package/templates/frontend/remix/app/tailwind.css +1 -0
  155. package/templates/frontend/remix/package.json.template +39 -0
  156. package/templates/frontend/remix/tsconfig.json +18 -0
  157. package/templates/frontend/remix/vite.config.ts.template +7 -0
  158. package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
  159. package/templates/infra/k8s/k8s/deployment.yml.template +70 -0
  160. package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
  161. package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
  162. package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
  163. package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
  164. package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
  165. package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
  166. package/templates/infra/k8s/k8s/service.yml.template +15 -0
  167. package/templates/testing/load/k6/README.md.template +48 -0
  168. package/templates/testing/load/k6/load-test.js.template +57 -0
  169. package/docs/00-README.md +0 -310
  170. package/docs/01-universal-prompt-library.md +0 -1049
  171. package/docs/02-claude-code-mastery-playbook.md +0 -283
  172. package/docs/03-multi-agent-verification.md +0 -565
  173. package/docs/04-errata-and-verification-checklist.md +0 -284
  174. package/docs/05-universal-scaffolder-vision.md +0 -452
  175. package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
  176. package/docs/errata.md +0 -58
  177. package/docs/multi-agent-verification.md +0 -66
  178. package/docs/playbook.md +0 -95
  179. package/docs/prompt-library.md +0 -160
  180. package/docs/uat/UAT_CHECKLIST.csv +0 -9
  181. package/docs/uat/UAT_TEMPLATE.md +0 -163
  182. package/templates/claude-code/commands/done.md +0 -19
  183. /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} 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
-
25
- export function buildVariables(stackConfig) {
26
- const vars = {
27
- PROJECT_NAME: stackConfig.projectName,
28
- PROJECT_NAME_PASCAL: toPascalCase(stackConfig.projectName),
29
- PROJECT_NAME_SNAKE: toSnakeCase(stackConfig.projectName),
30
- };
31
-
32
- if (stackConfig.frontend) {
33
- vars.FRONTEND_FRAMEWORK = stackConfig.frontend.framework;
34
- vars.FRONTEND_LANGUAGE = stackConfig.frontend.language;
35
- vars.FRONTEND_STYLING = stackConfig.frontend.styling;
36
- vars.FRONTEND_UI = stackConfig.frontend.ui || '';
37
- }
38
-
39
- if (stackConfig.backend) {
40
- vars.BACKEND_FRAMEWORK = stackConfig.backend.framework;
41
- vars.BACKEND_LANGUAGE = stackConfig.backend.language;
42
- vars.BACKEND_ORM = stackConfig.backend.orm;
43
- }
44
-
45
- if (stackConfig.database) {
46
- vars.DATABASE_TYPE = stackConfig.database.type;
47
- vars.DATABASE_ORM = stackConfig.database.orm;
48
- }
49
-
50
- vars.AUTH_TYPE = stackConfig.auth || 'none';
51
- vars.DEPLOYMENT = stackConfig.deployment || 'docker';
52
-
53
- // Commands vary by stack
54
- if (stackConfig.stackId === 'nextjs-fullstack') {
55
- vars.LINT_COMMAND = 'npx eslint .';
56
- vars.TYPE_CHECK_COMMAND = 'npx tsc --noEmit';
57
- vars.TEST_COMMAND = 'npx vitest run';
58
- vars.BUILD_COMMAND = 'npm run build';
59
- vars.DEV_COMMAND = 'npm run dev';
60
- vars.STACK_DESCRIPTION = 'Next.js full-stack application with TypeScript, Tailwind CSS, Prisma, and PostgreSQL';
61
- vars.EXTRA_IGNORES = '';
62
- } else if (stackConfig.stackId === 'fastapi-backend') {
63
- vars.LINT_COMMAND = 'ruff check .';
64
- vars.TYPE_CHECK_COMMAND = 'pyright';
65
- vars.TEST_COMMAND = 'pytest';
66
- vars.BUILD_COMMAND = 'docker build -t app .';
67
- vars.DEV_COMMAND = 'uvicorn app.main:app --reload';
68
- vars.STACK_DESCRIPTION = 'FastAPI backend service with SQLAlchemy, PostgreSQL, and Alembic';
69
- vars.EXTRA_IGNORES = '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/';
70
- } else if (stackConfig.stackId === 'polyglot-fullstack') {
71
- vars.LINT_COMMAND = 'cd frontend && npx eslint . && cd ../backend && ruff check .';
72
- vars.TYPE_CHECK_COMMAND = 'cd frontend && npx tsc --noEmit';
73
- vars.TEST_COMMAND = 'cd frontend && npx vitest run && cd ../backend && pytest';
74
- vars.BUILD_COMMAND = 'docker compose build';
75
- vars.DEV_COMMAND = 'docker compose up';
76
- vars.STACK_DESCRIPTION = 'Full-stack application with Next.js frontend and FastAPI backend';
77
- vars.EXTRA_IGNORES = '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/';
78
- }
79
-
80
- // Setup commands for README
81
- vars.SETUP_COMMANDS = buildSetupCommands(stackConfig);
82
- vars.AVAILABLE_SCRIPTS = buildAvailableScripts(stackConfig);
83
-
84
- return vars;
85
- }
86
-
87
- function buildSetupCommands(config) {
88
- if (config.stackId === 'nextjs-fullstack') {
89
- return `npm install
90
- cp .env.example .env
91
- npx prisma db push
92
- npm run dev`;
93
- }
94
- if (config.stackId === 'fastapi-backend') {
95
- return `cd backend
96
- python -m venv venv
97
- source venv/bin/activate
98
- pip install -r requirements.txt
99
- cp .env.example .env
100
- uvicorn app.main:app --reload`;
101
- }
102
- if (config.stackId === 'polyglot-fullstack') {
103
- return `docker compose up -d postgres
104
- # Frontend
105
- cd frontend && npm install && npm run dev
106
- # Backend
107
- cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload`;
108
- }
109
- return '';
110
- }
111
-
112
- function buildAvailableScripts(config) {
113
- if (config.stackId === 'nextjs-fullstack') {
114
- return `- \`npm run dev\` Start development server
115
- - \`npm run build\` Production build
116
- - \`npm run lint\` Run ESLint
117
- - \`npx prisma studio\` — Database GUI
118
- - \`npx vitest\` — Run unit tests
119
- - \`npx playwright test\` — Run E2E tests`;
120
- }
121
- if (config.stackId === 'fastapi-backend') {
122
- return `- \`uvicorn app.main:app --reload\` — Start dev server
123
- - \`pytest\` — Run tests
124
- - \`ruff check .\` — Run linter
125
- - \`alembic upgrade head\` — Run migrations`;
126
- }
127
- if (config.stackId === 'polyglot-fullstack') {
128
- return `- \`docker compose up\` Start all services
129
- - \`docker compose up -d postgres\` — Start database only
130
- - Frontend: \`cd frontend && npm run dev\`
131
- - Backend: \`cd backend && uvicorn app.main:app --reload\``;
132
- }
133
- return '';
134
- }
135
-
136
- export function processTemplateDir(templateDir, outputDir, variables, prefix) {
137
- const entries = walkDir(templateDir);
138
-
139
- for (const filePath of entries) {
140
- const relativePath = path.relative(templateDir, filePath);
141
- let outputRelative = prefix ? path.join(prefix, relativePath) : relativePath;
142
-
143
- if (outputRelative.endsWith('.template')) {
144
- // Process template: replace vars, strip .template extension
145
- const content = readTemplate(filePath);
146
- const processed = replaceVars(content, variables);
147
- const outputPath = path.join(outputDir, outputRelative.replace(/\.template$/, ''));
148
- ensureDir(path.dirname(outputPath));
149
- fs.writeFileSync(outputPath, processed, 'utf-8');
150
- } else if (path.basename(filePath) === '.gitkeep') {
151
- // Create the directory but don't copy .gitkeep itself
152
- ensureDir(path.join(outputDir, path.dirname(outputRelative)));
153
- } else {
154
- // Binary copy
155
- const outputPath = path.join(outputDir, outputRelative);
156
- ensureDir(path.dirname(outputPath));
157
- fs.copyFileSync(filePath, outputPath);
158
- }
159
- }
160
- }
161
-
162
- function walkDir(dir) {
163
- const results = [];
164
- const entries = fs.readdirSync(dir, { withFileTypes: true });
165
- for (const entry of entries) {
166
- const fullPath = path.join(dir, entry.name);
167
- if (entry.isDirectory()) {
168
- results.push(...walkDir(fullPath));
169
- } else {
170
- results.push(fullPath);
171
- }
172
- }
173
- return results;
174
- }
175
-
176
- function injectAuthDependencies(outputDir, stackConfig) {
177
- if (!stackConfig.auth || stackConfig.auth === 'none') return;
178
-
179
- // Next.js projects with nextauth
180
- if (stackConfig.auth === 'nextauth' || stackConfig.auth === 'both') {
181
- const pkgPaths = [
182
- path.join(outputDir, 'package.json'),
183
- path.join(outputDir, 'frontend', 'package.json'),
184
- ];
185
- for (const pkgPath of pkgPaths) {
186
- if (!fs.existsSync(pkgPath)) continue;
187
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
188
- pkg.dependencies = pkg.dependencies || {};
189
- pkg.dependencies['next-auth'] = '^5.0.0-beta.25';
190
- pkg.dependencies['@auth/prisma-adapter'] = '^2.7.4';
191
- pkg.dependencies['bcryptjs'] = '^2.4.3';
192
- pkg.devDependencies = pkg.devDependencies || {};
193
- pkg.devDependencies['@types/bcryptjs'] = '^2.4.6';
194
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
195
- }
196
- }
197
-
198
- // FastAPI projects with jwt-custom
199
- if (stackConfig.auth === 'jwt-custom' || stackConfig.auth === 'both') {
200
- const reqPaths = [
201
- path.join(outputDir, 'requirements.txt'),
202
- path.join(outputDir, 'backend', 'requirements.txt'),
203
- ];
204
- for (const reqPath of reqPaths) {
205
- if (!fs.existsSync(reqPath)) continue;
206
- const content = fs.readFileSync(reqPath, 'utf-8');
207
- const authDeps = ['python-jose[cryptography]', 'passlib[bcrypt]', 'python-multipart'];
208
- const additions = authDeps.filter(dep => !content.includes(dep.split('[')[0]));
209
- if (additions.length > 0) {
210
- fs.writeFileSync(reqPath, content.trimEnd() + '\n' + additions.join('\n') + '\n', 'utf-8');
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
+ }