forgedev 1.2.0 → 1.3.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 (171) hide show
  1. package/README.md +57 -10
  2. package/bin/chainproof.js +126 -0
  3. package/package.json +25 -7
  4. package/src/chainproof-bridge.js +330 -0
  5. package/src/ci-mode.js +85 -0
  6. package/src/claude-configurator.js +86 -49
  7. package/src/cli.js +30 -7
  8. package/src/composer.js +159 -34
  9. package/src/doctor-checks-chainproof.js +106 -0
  10. package/src/doctor-checks.js +39 -20
  11. package/src/doctor-prompts.js +9 -9
  12. package/src/doctor.js +37 -4
  13. package/src/guided.js +3 -3
  14. package/src/index.js +31 -10
  15. package/src/init-mode.js +64 -11
  16. package/src/menu.js +178 -0
  17. package/src/prompts.js +5 -12
  18. package/src/recommender.js +134 -10
  19. package/src/scanner.js +57 -2
  20. package/src/uat-generator.js +204 -189
  21. package/src/update-check.js +9 -4
  22. package/src/update.js +1 -1
  23. package/src/utils.js +64 -5
  24. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  25. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  29. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  34. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  35. package/templates/backend/express/Dockerfile.template +18 -0
  36. package/templates/backend/express/package.json.template +33 -0
  37. package/templates/backend/express/src/index.ts.template +34 -0
  38. package/templates/backend/express/src/routes/health.ts.template +27 -0
  39. package/templates/backend/express/tsconfig.json +17 -0
  40. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  41. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  42. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  44. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  45. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  46. package/templates/backend/hono/Dockerfile.template +18 -0
  47. package/templates/backend/hono/package.json.template +31 -0
  48. package/templates/backend/hono/src/index.ts.template +32 -0
  49. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  50. package/templates/backend/hono/tsconfig.json +18 -0
  51. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
  52. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  53. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  54. package/templates/chainproof/base/.mcp.json +9 -0
  55. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  56. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  57. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  58. package/templates/claude-code/agents/architect.md +25 -11
  59. package/templates/claude-code/agents/build-error-resolver.md +19 -5
  60. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  61. package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
  62. package/templates/claude-code/agents/database-reviewer.md +15 -1
  63. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  64. package/templates/claude-code/agents/doc-updater.md +19 -5
  65. package/templates/claude-code/agents/docs-lookup.md +19 -5
  66. package/templates/claude-code/agents/e2e-runner.md +26 -12
  67. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  68. package/templates/claude-code/agents/frontend-builder.md +188 -0
  69. package/templates/claude-code/agents/harness-optimizer.md +36 -1
  70. package/templates/claude-code/agents/loop-operator.md +27 -13
  71. package/templates/claude-code/agents/planner.md +21 -7
  72. package/templates/claude-code/agents/product-strategist.md +24 -10
  73. package/templates/claude-code/agents/production-readiness.md +14 -0
  74. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  75. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  76. package/templates/claude-code/agents/security-reviewer.md +14 -0
  77. package/templates/claude-code/agents/spec-validator.md +15 -1
  78. package/templates/claude-code/agents/tdd-guide.md +21 -7
  79. package/templates/claude-code/agents/uat-validator.md +14 -0
  80. package/templates/claude-code/claude-md/base.md +14 -7
  81. package/templates/claude-code/claude-md/fastapi.md +8 -8
  82. package/templates/claude-code/claude-md/fullstack.md +6 -6
  83. package/templates/claude-code/claude-md/hono.md +18 -0
  84. package/templates/claude-code/claude-md/nextjs.md +5 -5
  85. package/templates/claude-code/claude-md/remix.md +18 -0
  86. package/templates/claude-code/commands/audit-security.md +14 -0
  87. package/templates/claude-code/commands/audit-spec.md +14 -0
  88. package/templates/claude-code/commands/audit-wiring.md +14 -0
  89. package/templates/claude-code/commands/build-fix.md +28 -0
  90. package/templates/claude-code/commands/build-ui.md +59 -0
  91. package/templates/claude-code/commands/code-review.md +53 -31
  92. package/templates/claude-code/commands/fix-loop.md +211 -0
  93. package/templates/claude-code/commands/full-audit.md +36 -8
  94. package/templates/claude-code/commands/generate-prd.md +1 -1
  95. package/templates/claude-code/commands/generate-sdd.md +74 -0
  96. package/templates/claude-code/commands/generate-uat.md +107 -35
  97. package/templates/claude-code/commands/help.md +68 -0
  98. package/templates/claude-code/commands/live-uat.md +268 -0
  99. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  100. package/templates/claude-code/commands/plan.md +3 -3
  101. package/templates/claude-code/commands/pre-pr.md +57 -19
  102. package/templates/claude-code/commands/product-strategist.md +21 -0
  103. package/templates/claude-code/commands/resume-session.md +10 -10
  104. package/templates/claude-code/commands/run-uat.md +59 -2
  105. package/templates/claude-code/commands/save-session.md +10 -10
  106. package/templates/claude-code/commands/simplify.md +36 -0
  107. package/templates/claude-code/commands/tdd.md +17 -18
  108. package/templates/claude-code/commands/verify-all.md +24 -0
  109. package/templates/claude-code/commands/verify-intent.md +55 -0
  110. package/templates/claude-code/commands/workflows.md +52 -40
  111. package/templates/claude-code/hooks/polyglot.json +10 -1
  112. package/templates/claude-code/hooks/python.json +10 -1
  113. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
  114. package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
  115. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
  116. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  117. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  118. package/templates/claude-code/hooks/typescript.json +10 -1
  119. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  120. package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
  121. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  122. package/templates/claude-code/skills/playwright/SKILL.md +5 -5
  123. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  124. package/templates/claude-code/skills/security-web/SKILL.md +1 -1
  125. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  126. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  127. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  128. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  129. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  130. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  131. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  132. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  133. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  134. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  135. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  136. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  137. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  138. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  139. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  140. package/templates/frontend/nextjs/package.json.template +3 -1
  141. package/templates/frontend/react/index.html.template +12 -0
  142. package/templates/frontend/react/package.json.template +34 -0
  143. package/templates/frontend/react/src/App.tsx.template +10 -0
  144. package/templates/frontend/react/src/index.css +1 -0
  145. package/templates/frontend/react/src/main.tsx +10 -0
  146. package/templates/frontend/react/tsconfig.json +17 -0
  147. package/templates/frontend/react/vite.config.ts.template +15 -0
  148. package/templates/frontend/react/vitest.config.ts +9 -0
  149. package/templates/frontend/remix/app/root.tsx.template +31 -0
  150. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  151. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  152. package/templates/frontend/remix/app/tailwind.css +1 -0
  153. package/templates/frontend/remix/package.json.template +39 -0
  154. package/templates/frontend/remix/tsconfig.json +18 -0
  155. package/templates/frontend/remix/vite.config.ts.template +7 -0
  156. package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
  157. package/docs/00-README.md +0 -310
  158. package/docs/01-universal-prompt-library.md +0 -1049
  159. package/docs/02-claude-code-mastery-playbook.md +0 -283
  160. package/docs/03-multi-agent-verification.md +0 -565
  161. package/docs/04-errata-and-verification-checklist.md +0 -284
  162. package/docs/05-universal-scaffolder-vision.md +0 -452
  163. package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
  164. package/docs/errata.md +0 -58
  165. package/docs/multi-agent-verification.md +0 -66
  166. package/docs/playbook.md +0 -95
  167. package/docs/prompt-library.md +0 -160
  168. package/docs/uat/UAT_CHECKLIST.csv +0 -9
  169. package/docs/uat/UAT_TEMPLATE.md +0 -163
  170. package/templates/claude-code/commands/done.md +0 -19
  171. /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
package/src/cli.js CHANGED
@@ -5,7 +5,13 @@ import { log } from './utils.js';
5
5
 
6
6
  export async function parseCommand(args) {
7
7
  if (args.includes('--version') || args.includes('-v')) {
8
- const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
8
+ let pkg;
9
+ try {
10
+ pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
11
+ } catch (err) {
12
+ console.error(`Could not read package.json: ${err.message}`);
13
+ process.exit(1);
14
+ }
9
15
  console.log(pkg.version);
10
16
  process.exit(0);
11
17
  }
@@ -18,8 +24,10 @@ export async function parseCommand(args) {
18
24
  const command = args[0];
19
25
 
20
26
  if (!command) {
21
- showUsage();
22
- process.exit(0);
27
+ const { showInteractiveMenu, handleMenuSelection } = await import('./menu.js');
28
+ const selected = await showInteractiveMenu();
29
+ await handleMenuSelection(selected);
30
+ return;
23
31
  }
24
32
 
25
33
  if (command === 'new') {
@@ -46,6 +54,12 @@ export async function parseCommand(args) {
46
54
  return;
47
55
  }
48
56
 
57
+ if (command === 'ci') {
58
+ const { runCI } = await import('./ci-mode.js');
59
+ await runCI(process.cwd());
60
+ return;
61
+ }
62
+
49
63
  if (command === 'update') {
50
64
  const { runUpdate } = await import('./update.js');
51
65
  await runUpdate();
@@ -73,12 +87,13 @@ export async function parseCommand(args) {
73
87
 
74
88
  function showUsage() {
75
89
  console.log(`
76
- ${chalk.bold.cyan('DevForge')}AI-first project scaffolding
90
+ ${chalk.bold.cyan('DevForge')} AI-first project scaffolding
77
91
 
78
92
  ${chalk.bold('Usage:')}
79
93
  devforge new <name> Create a new project
80
94
  devforge init Add dev guardrails to current project
81
95
  devforge doctor Diagnose and optimize current project
96
+ devforge ci Run health checks for CI/CD (non-interactive)
82
97
  devforge update Check for newer version
83
98
  devforge <name> Shorthand for ${chalk.dim('devforge new <name>')}
84
99
 
@@ -92,14 +107,14 @@ function showUsage() {
92
107
 
93
108
  function showHelp() {
94
109
  console.log(`
95
- ${chalk.bold.cyan('DevForge')}Universal, AI-first project scaffolding
110
+ ${chalk.bold.cyan('DevForge')} Universal, AI-first project scaffolding
96
111
 
97
112
  ${chalk.bold('Commands:')}
98
113
 
99
114
  ${chalk.bold('devforge new <name>')}
100
115
  Create a new project. Choose between:
101
- ${chalk.dim('• Guided mode describe what you want in plain English')}
102
- ${chalk.dim('• Developer mode pick your stack directly')}
116
+ ${chalk.dim('• Guided mode: describe what you want in plain English')}
117
+ ${chalk.dim('• Developer mode: pick your stack directly')}
103
118
 
104
119
  ${chalk.bold('devforge init')}
105
120
  Add Claude Code infrastructure to an existing project.
@@ -112,6 +127,11 @@ function showHelp() {
112
127
  flaky tests, dead code, duplicate code, and more.
113
128
  Generates Claude Code prompts to fix each issue.
114
129
 
130
+ ${chalk.bold('devforge ci')}
131
+ Run project health checks non-interactively for CI/CD.
132
+ Exits with code 1 if critical issues found.
133
+ Saves report to docs/ci-report.md if docs/ exists.
134
+
115
135
  ${chalk.bold('devforge update')}
116
136
  Check if a newer version of DevForge is available.
117
137
  Shows upgrade instructions if an update exists.
@@ -120,6 +140,9 @@ function showHelp() {
120
140
  - Next.js full-stack (TypeScript + Prisma + PostgreSQL)
121
141
  - FastAPI backend (Python + SQLAlchemy + PostgreSQL)
122
142
  - Polyglot full-stack (Next.js + FastAPI monorepo)
143
+ - React + Express (Vite + Express + Prisma + PostgreSQL)
144
+ - Remix full-stack (Vite + Tailwind + Prisma + PostgreSQL)
145
+ - Hono API (TypeScript + Prisma + PostgreSQL)
123
146
 
124
147
  ${chalk.bold('Examples:')}
125
148
  devforge new my-app
package/src/composer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase, log } from './utils.js';
3
+ import { ROOT_DIR, ensureDir, readTemplate, replaceVars, toPascalCase, toSnakeCase, getStackCommands, copyEnvCmd, log } from './utils.js';
4
4
 
5
5
  const TEMPLATES_DIR = path.join(ROOT_DIR, 'templates');
6
6
 
@@ -12,7 +12,7 @@ export async function compose(outputDir, stackConfig) {
12
12
  for (const mod of stackConfig.templateModules) {
13
13
  const templateDir = path.join(TEMPLATES_DIR, mod.path);
14
14
  if (!fs.existsSync(templateDir)) {
15
- log.warn(`Template module not found: ${mod.path} skipping`);
15
+ log.warn(`Template module not found: ${mod.path}, skipping`);
16
16
  continue;
17
17
  }
18
18
  processTemplateDir(templateDir, outputDir, variables, mod.prefix || '');
@@ -20,6 +20,9 @@ export async function compose(outputDir, stackConfig) {
20
20
 
21
21
  // Inject auth dependencies into package.json if auth is enabled
22
22
  injectAuthDependencies(outputDir, stackConfig);
23
+
24
+ // Apply user plugins from ~/.devforge/templates/ if they exist
25
+ applyUserPlugins(outputDir, variables, stackConfig);
23
26
  }
24
27
 
25
28
  export function buildVariables(stackConfig) {
@@ -50,31 +53,27 @@ export function buildVariables(stackConfig) {
50
53
  vars.AUTH_TYPE = stackConfig.auth || 'none';
51
54
  vars.DEPLOYMENT = stackConfig.deployment || 'docker';
52
55
 
53
- // Commands vary by stack
56
+ // Commands vary by stack (shared with claude-configurator)
57
+ Object.assign(vars, getStackCommands(stackConfig.stackId));
58
+
54
59
  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
60
  vars.STACK_DESCRIPTION = 'Next.js full-stack application with TypeScript, Tailwind CSS, Prisma, and PostgreSQL';
61
61
  vars.EXTRA_IGNORES = '';
62
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
63
  vars.STACK_DESCRIPTION = 'FastAPI backend service with SQLAlchemy, PostgreSQL, and Alembic';
69
64
  vars.EXTRA_IGNORES = '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/';
70
65
  } 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
66
  vars.STACK_DESCRIPTION = 'Full-stack application with Next.js frontend and FastAPI backend';
77
67
  vars.EXTRA_IGNORES = '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/';
68
+ } else if (stackConfig.stackId === 'react-express') {
69
+ vars.STACK_DESCRIPTION = 'Full-stack application with React (Vite) frontend and Express backend';
70
+ vars.EXTRA_IGNORES = '\ndist/';
71
+ } else if (stackConfig.stackId === 'remix-fullstack') {
72
+ vars.STACK_DESCRIPTION = 'Full-stack Remix application with Vite, Tailwind CSS, and PostgreSQL';
73
+ vars.EXTRA_IGNORES = '\nbuild/';
74
+ } else if (stackConfig.stackId === 'hono-api') {
75
+ vars.STACK_DESCRIPTION = 'Hono API service with TypeScript, Prisma, and PostgreSQL';
76
+ vars.EXTRA_IGNORES = '\ndist/';
78
77
  }
79
78
 
80
79
  // Setup commands for README
@@ -87,7 +86,7 @@ export function buildVariables(stackConfig) {
87
86
  function buildSetupCommands(config) {
88
87
  if (config.stackId === 'nextjs-fullstack') {
89
88
  return `npm install
90
- cp .env.example .env
89
+ ${copyEnvCmd()}
91
90
  npx prisma db push
92
91
  npm run dev`;
93
92
  }
@@ -96,7 +95,7 @@ npm run dev`;
96
95
  python -m venv venv
97
96
  source venv/bin/activate
98
97
  pip install -r requirements.txt
99
- cp .env.example .env
98
+ ${copyEnvCmd()}
100
99
  uvicorn app.main:app --reload`;
101
100
  }
102
101
  if (config.stackId === 'polyglot-fullstack') {
@@ -105,30 +104,72 @@ uvicorn app.main:app --reload`;
105
104
  cd frontend && npm install && npm run dev
106
105
  # Backend
107
106
  cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload`;
107
+ }
108
+ if (config.stackId === 'react-express') {
109
+ return `# Frontend
110
+ cd frontend && npm install && npm run dev
111
+ # Backend (in a separate terminal)
112
+ cd backend && npm install && ${copyEnvCmd()} && npx prisma db push && npm run dev`;
113
+ }
114
+ if (config.stackId === 'remix-fullstack') {
115
+ return `npm install
116
+ ${copyEnvCmd()}
117
+ npx prisma db push
118
+ npm run dev`;
119
+ }
120
+ if (config.stackId === 'hono-api') {
121
+ return `npm install
122
+ ${copyEnvCmd()}
123
+ npx prisma db push
124
+ npm run dev`;
108
125
  }
109
126
  return '';
110
127
  }
111
128
 
112
129
  function buildAvailableScripts(config) {
113
130
  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`;
131
+ return `- \`npm run dev\`: Start development server
132
+ - \`npm run build\`: Production build
133
+ - \`npm run lint\`: Run ESLint
134
+ - \`npx prisma studio\`: Database GUI
135
+ - \`npx vitest\`: Run unit tests
136
+ - \`npx playwright test\`: Run E2E tests`;
120
137
  }
121
138
  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`;
139
+ return `- \`uvicorn app.main:app --reload\`: Start dev server
140
+ - \`pytest\`: Run tests
141
+ - \`ruff check .\`: Run linter
142
+ - \`alembic upgrade head\`: Run migrations`;
126
143
  }
127
144
  if (config.stackId === 'polyglot-fullstack') {
128
- return `- \`docker compose up\` Start all services
129
- - \`docker compose up -d postgres\` Start database only
145
+ return `- \`docker compose up\`: Start all services
146
+ - \`docker compose up -d postgres\`: Start database only
130
147
  - Frontend: \`cd frontend && npm run dev\`
131
148
  - Backend: \`cd backend && uvicorn app.main:app --reload\``;
149
+ }
150
+ if (config.stackId === 'react-express') {
151
+ return `- Frontend: \`cd frontend && npm run dev\` (Vite dev server)
152
+ - Backend: \`cd backend && npm run dev\` (Express with tsx watch)
153
+ - \`cd backend && npm run build\`: Build backend for production
154
+ - \`cd frontend && npm run build\`: Build frontend for production
155
+ - \`cd backend && npx prisma studio\`: Database GUI
156
+ - \`cd frontend && npx vitest\`: Run frontend tests`;
157
+ }
158
+ if (config.stackId === 'remix-fullstack') {
159
+ return `- \`npm run dev\`: Start Remix dev server
160
+ - \`npm run build\`: Production build
161
+ - \`npm run start\`: Start production server
162
+ - \`npm run lint\`: Run ESLint
163
+ - \`npx prisma studio\`: Database GUI
164
+ - \`npx vitest\`: Run unit tests`;
165
+ }
166
+ if (config.stackId === 'hono-api') {
167
+ return `- \`npm run dev\`: Start Hono dev server (tsx watch)
168
+ - \`npm run build\`: Compile TypeScript
169
+ - \`npm run start\`: Start production server
170
+ - \`npm run lint\`: Run ESLint
171
+ - \`npx prisma studio\`: Database GUI
172
+ - \`npx vitest\`: Run unit tests`;
132
173
  }
133
174
  return '';
134
175
  }
@@ -138,7 +179,14 @@ export function processTemplateDir(templateDir, outputDir, variables, prefix) {
138
179
 
139
180
  for (const filePath of entries) {
140
181
  const relativePath = path.relative(templateDir, filePath);
141
- let outputRelative = prefix ? path.join(prefix, relativePath) : relativePath;
182
+ const outputRelative = prefix ? path.join(prefix, relativePath) : relativePath;
183
+
184
+ // Guard against path traversal (e.g. ../../etc/passwd in plugin templates)
185
+ const resolved = path.resolve(outputDir, outputRelative);
186
+ if (!resolved.startsWith(path.resolve(outputDir))) {
187
+ log.warn(`Skipping "${outputRelative}" (path traversal detected)`);
188
+ continue;
189
+ }
142
190
 
143
191
  if (outputRelative.endsWith('.template')) {
144
192
  // Process template: replace vars, strip .template extension
@@ -184,7 +232,13 @@ function injectAuthDependencies(outputDir, stackConfig) {
184
232
  ];
185
233
  for (const pkgPath of pkgPaths) {
186
234
  if (!fs.existsSync(pkgPath)) continue;
187
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
235
+ let pkg;
236
+ try {
237
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
238
+ } catch {
239
+ log.warn(`Skipping auth dependency injection — could not parse ${pkgPath}`);
240
+ continue;
241
+ }
188
242
  pkg.dependencies = pkg.dependencies || {};
189
243
  pkg.dependencies['next-auth'] = '^5.0.0-beta.25';
190
244
  pkg.dependencies['@auth/prisma-adapter'] = '^2.7.4';
@@ -212,3 +266,74 @@ function injectAuthDependencies(outputDir, stackConfig) {
212
266
  }
213
267
  }
214
268
  }
269
+
270
+ function applyUserPlugins(outputDir, variables, stackConfig) {
271
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
272
+ if (!homeDir) return;
273
+
274
+ const pluginDir = path.join(homeDir, '.devforge');
275
+ if (!fs.existsSync(pluginDir)) return;
276
+
277
+ // User templates: ~/.devforge/templates/<stackId>/ → overlay onto output
278
+ const userTemplatesDir = path.join(pluginDir, 'templates', stackConfig.stackId);
279
+ if (fs.existsSync(userTemplatesDir)) {
280
+ processTemplateDir(userTemplatesDir, outputDir, variables, '');
281
+ log.dim(` Applied user templates from ~/.devforge/templates/${stackConfig.stackId}/`);
282
+ }
283
+
284
+ // Universal user templates: ~/.devforge/templates/universal/ → always applied
285
+ const universalDir = path.join(pluginDir, 'templates', 'universal');
286
+ if (fs.existsSync(universalDir)) {
287
+ processTemplateDir(universalDir, outputDir, variables, '');
288
+ log.dim(' Applied user templates from ~/.devforge/templates/universal/');
289
+ }
290
+
291
+ // User agents: ~/.devforge/agents/*.md → copy into .claude/agents/
292
+ const userAgentsDir = path.join(pluginDir, 'agents');
293
+ if (fs.existsSync(userAgentsDir)) {
294
+ const agents = fs.readdirSync(userAgentsDir).filter(f => f.endsWith('.md'));
295
+ for (const agent of agents) {
296
+ const src = path.join(userAgentsDir, agent);
297
+ const dest = path.join(outputDir, '.claude', 'agents', agent);
298
+ ensureDir(path.dirname(dest));
299
+ fs.copyFileSync(src, dest);
300
+ }
301
+ if (agents.length > 0) {
302
+ log.dim(` Applied ${agents.length} user agent(s) from ~/.devforge/agents/`);
303
+ }
304
+ }
305
+
306
+ // User commands: ~/.devforge/commands/*.md → copy into .claude/commands/
307
+ const userCommandsDir = path.join(pluginDir, 'commands');
308
+ if (fs.existsSync(userCommandsDir)) {
309
+ const commands = fs.readdirSync(userCommandsDir).filter(f => f.endsWith('.md'));
310
+ for (const cmd of commands) {
311
+ const src = path.join(userCommandsDir, cmd);
312
+ const dest = path.join(outputDir, '.claude', 'commands', cmd);
313
+ ensureDir(path.dirname(dest));
314
+ fs.copyFileSync(src, dest);
315
+ }
316
+ if (commands.length > 0) {
317
+ log.dim(` Applied ${commands.length} user command(s) from ~/.devforge/commands/`);
318
+ }
319
+ }
320
+
321
+ // User skills: ~/.devforge/skills/<name>/SKILL.md → copy into .claude/skills/
322
+ const userSkillsDir = path.join(pluginDir, 'skills');
323
+ if (fs.existsSync(userSkillsDir)) {
324
+ const skills = fs.readdirSync(userSkillsDir, { withFileTypes: true })
325
+ .filter(d => d.isDirectory())
326
+ .map(d => d.name);
327
+ for (const skill of skills) {
328
+ const skillFile = path.join(userSkillsDir, skill, 'SKILL.md');
329
+ if (fs.existsSync(skillFile)) {
330
+ const dest = path.join(outputDir, '.claude', 'skills', skill, 'SKILL.md');
331
+ ensureDir(path.dirname(dest));
332
+ fs.copyFileSync(skillFile, dest);
333
+ }
334
+ }
335
+ if (skills.length > 0) {
336
+ log.dim(` Applied ${skills.length} user skill(s) from ~/.devforge/skills/`);
337
+ }
338
+ }
339
+ }
@@ -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
+ }
@@ -1,5 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import {
4
+ checkChainproofExists,
5
+ checkChainproofIntegrity,
6
+ checkChainproofUnsigned,
7
+ } from './doctor-checks-chainproof.js';
8
+
9
+ export { checkChainproofExists, checkChainproofIntegrity, checkChainproofUnsigned };
3
10
 
4
11
  export function runAllChecks(projectDir, scan) {
5
12
  const issues = [];
@@ -21,6 +28,9 @@ export function runAllChecks(projectDir, scan) {
21
28
  issues.push(...checkDuplicateCode(projectDir));
22
29
  issues.push(...checkDeadFeatures(projectDir, scan));
23
30
  issues.push(...checkAIPrompts(projectDir));
31
+ issues.push(...checkChainproofExists(projectDir));
32
+ issues.push(...checkChainproofIntegrity(projectDir));
33
+ issues.push(...checkChainproofUnsigned(projectDir));
24
34
 
25
35
  return issues.sort((a, b) => {
26
36
  const order = { critical: 0, warning: 1, info: 2 };
@@ -36,7 +46,7 @@ function checkClaudeMdLength(projectDir, scan) {
36
46
  return [{
37
47
  severity: lines > 500 ? 'critical' : 'warning',
38
48
  title: `CLAUDE.md is ${lines} lines (recommended limit: 150)`,
39
- impact: 'Instructions are being dropped Claude Code ignores rules randomly when context is too large',
49
+ impact: 'Instructions are being dropped. Claude Code ignores rules randomly when context is too large',
40
50
  files: ['CLAUDE.md'],
41
51
  autoFixable: false,
42
52
  promptId: 'CLAUDE_MD_TOO_LONG',
@@ -68,7 +78,7 @@ function checkUnauthEndpoints(projectDir, scan) {
68
78
  issues.push({
69
79
  severity: 'critical',
70
80
  title: `Unauthenticated endpoint: ${relPath}:${i + 1}`,
71
- impact: 'Security vulnerability data accessible without login',
81
+ impact: 'Security vulnerability, data accessible without login',
72
82
  files: [`${relPath}:${i + 1}`],
73
83
  autoFixable: false,
74
84
  promptId: 'UNAUTH_ENDPOINT',
@@ -86,11 +96,14 @@ function checkUnauthEndpoints(projectDir, scan) {
86
96
  const apiDir = path.join(projectDir, srcDir, 'app', 'api');
87
97
  if (!fs.existsSync(apiDir)) return issues;
88
98
 
89
- const routeFiles = findFiles(apiDir, '.ts').concat(findFiles(apiDir, '.tsx'));
99
+ const routeFiles = findFiles(apiDir, '.ts').concat(findFiles(apiDir, '.tsx'))
100
+ .concat(findFiles(apiDir, '.js')).concat(findFiles(apiDir, '.jsx'));
90
101
  for (const file of routeFiles) {
91
- if (path.basename(file) === 'route.ts' || path.basename(file) === 'route.tsx') {
102
+ const basename = path.basename(file);
103
+ if (basename === 'route.ts' || basename === 'route.tsx' ||
104
+ basename === 'route.js' || basename === 'route.jsx') {
92
105
  // Skip health endpoints
93
- if (file.includes('/health/')) continue;
106
+ if (file.includes(`${path.sep}health${path.sep}`)) continue;
94
107
 
95
108
  const content = fs.readFileSync(file, 'utf-8');
96
109
  if (!content.includes('getServerSession') && !content.includes('auth(') &&
@@ -118,7 +131,7 @@ function checkUnauthEndpoints(projectDir, scan) {
118
131
  return [{
119
132
  severity,
120
133
  title: `${issues.length} endpoints have no authentication`,
121
- impact: 'Security vulnerability data accessible without login',
134
+ impact: 'Security vulnerability, data accessible without login',
122
135
  files: allFiles,
123
136
  autoFixable: false,
124
137
  promptId: 'UNAUTH_ENDPOINT',
@@ -161,7 +174,7 @@ function checkFlakyTestPatterns(projectDir) {
161
174
  issues.push({
162
175
  severity: 'critical',
163
176
  title: `${flakyFiles.length} tests use waitForTimeout() or hardcoded delays`,
164
- impact: 'Tests fail randomly unreliable CI',
177
+ impact: 'Tests fail randomly, which means unreliable CI',
165
178
  files: flakyFiles,
166
179
  autoFixable: false,
167
180
  promptId: 'FLAKY_TESTS',
@@ -191,7 +204,7 @@ function checkCrossDomainImports(projectDir, scan) {
191
204
  issues.push({
192
205
  severity: 'critical',
193
206
  title: `Cross-domain import: ${path.relative(projectDir, file)} imports from ${backendDir}`,
194
- impact: 'Build will break or bundle server code into client security risk',
207
+ impact: 'Build will break or bundle server code into client. This is a security risk',
195
208
  files: [path.relative(projectDir, file)],
196
209
  autoFixable: false,
197
210
  promptId: 'CROSS_DOMAIN_IMPORT',
@@ -206,7 +219,7 @@ function checkCrossDomainImports(projectDir, scan) {
206
219
  return [{
207
220
  severity: 'critical',
208
221
  title: `${issues.length} cross-domain imports (frontend importing backend or vice versa)`,
209
- impact: 'Build will break or bundle server code into client security risk',
222
+ impact: 'Build will break or bundle server code into client. This is a security risk',
210
223
  files: allFiles,
211
224
  autoFixable: false,
212
225
  promptId: 'CROSS_DOMAIN_IMPORT',
@@ -252,7 +265,7 @@ function checkBareExcepts(projectDir) {
252
265
  }];
253
266
  }
254
267
 
255
- function checkMissingHealthEndpoint(projectDir, scan) {
268
+ function checkMissingHealthEndpoint(projectDir, _scan) {
256
269
  // Search for health endpoints by file path or content
257
270
  const patterns = ['/health', '/healthz', '/api/health'];
258
271
  const searchDirs = ['src', 'backend', 'app', 'frontend/src'];
@@ -286,7 +299,7 @@ function checkMissingHealthEndpoint(projectDir, scan) {
286
299
  }];
287
300
  }
288
301
 
289
- function checkMissingGracefulShutdown(projectDir, scan) {
302
+ function checkMissingGracefulShutdown(projectDir, _scan) {
290
303
  const searchDirs = ['src', 'backend', 'app', 'frontend/src'];
291
304
  const signals = ['SIGTERM', 'SIGINT', 'process.on', 'signal.signal', 'lifespan'];
292
305
 
@@ -318,7 +331,7 @@ function checkMissingUAT(scan) {
318
331
  return [{
319
332
  severity: 'warning',
320
333
  title: 'No UAT scenarios',
321
- impact: 'No formal acceptance criteria features may not work as intended',
334
+ impact: 'No formal acceptance criteria, so features may not work as intended',
322
335
  files: [],
323
336
  autoFixable: false,
324
337
  promptId: 'MISSING_UAT',
@@ -340,7 +353,7 @@ function checkScatteredAPICalls(projectDir, scan) {
340
353
  // Skip files that ARE the API client
341
354
  if (['api.ts', 'api.js', 'apiClient.ts', 'apiClient.js', 'fetcher.ts', 'fetcher.js'].includes(basename)) continue;
342
355
  // Skip non-component files
343
- if (file.includes('/lib/') || file.includes('/services/') || file.includes('/utils/')) continue;
356
+ if (file.includes(`${path.sep}lib${path.sep}`) || file.includes(`${path.sep}services${path.sep}`) || file.includes(`${path.sep}utils${path.sep}`)) continue;
344
357
 
345
358
  const content = fs.readFileSync(file, 'utf-8');
346
359
  if (/fetch\s*\(\s*['"`]\/api\//.test(content) || /fetch\s*\(\s*['"`]http/.test(content) ||
@@ -387,7 +400,7 @@ function checkLargeFiles(projectDir) {
387
400
 
388
401
  return [{
389
402
  severity: 'info',
390
- title: `${largeFiles.length} files over 500 lines candidates for splitting`,
403
+ title: `${largeFiles.length} files over 500 lines (candidates for splitting)`,
391
404
  impact: 'Large files are hard to navigate, review, and test',
392
405
  files: largeFiles.map(f => `${f.file} (${f.lines} lines)`),
393
406
  autoFixable: false,
@@ -406,7 +419,7 @@ function checkMissingScopedClaudeMd(projectDir, scan) {
406
419
  if (!fs.existsSync(path.join(projectDir, frontendDir, 'CLAUDE.md'))) {
407
420
  issues.push({
408
421
  severity: 'info',
409
- title: `No ${frontendDir}/CLAUDE.md frontend rules not scoped`,
422
+ title: `No ${frontendDir}/CLAUDE.md, frontend rules not scoped`,
410
423
  impact: 'Claude Code loads all rules even when working only on frontend',
411
424
  files: [],
412
425
  autoFixable: false,
@@ -418,7 +431,7 @@ function checkMissingScopedClaudeMd(projectDir, scan) {
418
431
  if (!fs.existsSync(path.join(projectDir, backendDir, 'CLAUDE.md'))) {
419
432
  issues.push({
420
433
  severity: 'info',
421
- title: `No ${backendDir}/CLAUDE.md backend rules not scoped`,
434
+ title: `No ${backendDir}/CLAUDE.md, backend rules not scoped`,
422
435
  impact: 'Claude Code loads all rules even when working only on backend',
423
436
  files: [],
424
437
  autoFixable: false,
@@ -430,11 +443,16 @@ function checkMissingScopedClaudeMd(projectDir, scan) {
430
443
  return issues;
431
444
  }
432
445
 
433
- function checkUnusedDependencies(projectDir, scan) {
446
+ function checkUnusedDependencies(projectDir, _scan) {
434
447
  const pkgPath = path.join(projectDir, 'package.json');
435
448
  if (!fs.existsSync(pkgPath)) return [];
436
449
 
437
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
450
+ let pkg;
451
+ try {
452
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
453
+ } catch {
454
+ return [];
455
+ }
438
456
  const deps = Object.keys(pkg.dependencies || {});
439
457
  if (deps.length === 0) return [];
440
458
 
@@ -526,7 +544,7 @@ function checkHardcodedValues(projectDir) {
526
544
  return [{
527
545
  severity: 'warning',
528
546
  title: `${hardcoded.length} hardcoded values that should be in environment variables`,
529
- impact: 'Cannot change config without code changes breaks deployment',
547
+ impact: 'Cannot change config without code changes, which breaks deployment',
530
548
  files: hardcoded,
531
549
  autoFixable: false,
532
550
  promptId: 'HARDCODED_VALUES',
@@ -709,7 +727,7 @@ function checkAIPrompts(projectDir) {
709
727
 
710
728
  return [{
711
729
  severity: 'info',
712
- title: `${issues.length} files have inline AI prompts consider a prompts file`,
730
+ title: `${issues.length} files have inline AI prompts. Consider a prompts file`,
713
731
  impact: 'Inline prompts are hard to version, test, and iterate on',
714
732
  files: issues,
715
733
  autoFixable: false,
@@ -741,3 +759,4 @@ function findFiles(dir, ext) {
741
759
 
742
760
  return results;
743
761
  }
762
+