forgedev 1.1.3 → 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 (167) hide show
  1. package/README.md +58 -10
  2. package/bin/chainproof.js +126 -0
  3. package/bin/devforge.js +2 -1
  4. package/package.json +33 -7
  5. package/src/chainproof-bridge.js +330 -0
  6. package/src/ci-mode.js +85 -0
  7. package/src/claude-configurator.js +87 -49
  8. package/src/cli.js +35 -12
  9. package/src/composer.js +159 -34
  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 +64 -11
  17. package/src/menu.js +178 -0
  18. package/src/prompts.js +5 -12
  19. package/src/recommender.js +134 -10
  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 +1 -1
  24. package/src/utils.js +65 -6
  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/docs/plans/.gitkeep +0 -0
  53. package/templates/base/docs/uat/UAT_CHECKLIST.csv.template +2 -0
  54. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +22 -0
  55. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  56. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  57. package/templates/chainproof/base/.mcp.json +9 -0
  58. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  59. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  60. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  61. package/templates/claude-code/agents/architect.md +25 -11
  62. package/templates/claude-code/agents/build-error-resolver.md +22 -7
  63. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  64. package/templates/claude-code/agents/code-quality-reviewer.md +15 -1
  65. package/templates/claude-code/agents/database-reviewer.md +16 -2
  66. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  67. package/templates/claude-code/agents/doc-updater.md +19 -5
  68. package/templates/claude-code/agents/docs-lookup.md +19 -5
  69. package/templates/claude-code/agents/e2e-runner.md +26 -12
  70. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  71. package/templates/claude-code/agents/frontend-builder.md +188 -0
  72. package/templates/claude-code/agents/harness-optimizer.md +61 -0
  73. package/templates/claude-code/agents/loop-operator.md +27 -12
  74. package/templates/claude-code/agents/planner.md +21 -7
  75. package/templates/claude-code/agents/product-strategist.md +138 -0
  76. package/templates/claude-code/agents/production-readiness.md +14 -0
  77. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  78. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  79. package/templates/claude-code/agents/security-reviewer.md +15 -0
  80. package/templates/claude-code/agents/spec-validator.md +45 -1
  81. package/templates/claude-code/agents/tdd-guide.md +21 -7
  82. package/templates/claude-code/agents/uat-validator.md +18 -0
  83. package/templates/claude-code/claude-md/base.md +15 -7
  84. package/templates/claude-code/claude-md/fastapi.md +8 -8
  85. package/templates/claude-code/claude-md/fullstack.md +6 -6
  86. package/templates/claude-code/claude-md/hono.md +18 -0
  87. package/templates/claude-code/claude-md/nextjs.md +5 -5
  88. package/templates/claude-code/claude-md/remix.md +18 -0
  89. package/templates/claude-code/commands/audit-security.md +14 -0
  90. package/templates/claude-code/commands/audit-spec.md +14 -0
  91. package/templates/claude-code/commands/audit-wiring.md +14 -0
  92. package/templates/claude-code/commands/build-fix.md +28 -0
  93. package/templates/claude-code/commands/build-ui.md +59 -0
  94. package/templates/claude-code/commands/code-review.md +54 -26
  95. package/templates/claude-code/commands/fix-loop.md +211 -0
  96. package/templates/claude-code/commands/full-audit.md +37 -8
  97. package/templates/claude-code/commands/generate-prd.md +1 -1
  98. package/templates/claude-code/commands/generate-sdd.md +74 -0
  99. package/templates/claude-code/commands/generate-uat.md +107 -35
  100. package/templates/claude-code/commands/help.md +68 -0
  101. package/templates/claude-code/commands/live-uat.md +268 -0
  102. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  103. package/templates/claude-code/commands/plan.md +3 -3
  104. package/templates/claude-code/commands/pre-pr.md +57 -19
  105. package/templates/claude-code/commands/product-strategist.md +21 -0
  106. package/templates/claude-code/commands/resume-session.md +10 -10
  107. package/templates/claude-code/commands/run-uat.md +59 -2
  108. package/templates/claude-code/commands/save-session.md +10 -10
  109. package/templates/claude-code/commands/simplify.md +36 -0
  110. package/templates/claude-code/commands/tdd.md +17 -18
  111. package/templates/claude-code/commands/verify-all.md +24 -0
  112. package/templates/claude-code/commands/verify-intent.md +55 -0
  113. package/templates/claude-code/commands/workflows.md +52 -37
  114. package/templates/claude-code/hooks/polyglot.json +10 -1
  115. package/templates/claude-code/hooks/python.json +10 -1
  116. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +20 -10
  117. package/templates/claude-code/hooks/scripts/autofix-python.mjs +4 -5
  118. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +4 -4
  119. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  120. package/templates/claude-code/hooks/scripts/guard-protected-files.mjs +2 -2
  121. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  122. package/templates/claude-code/hooks/typescript.json +10 -1
  123. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  124. package/templates/claude-code/skills/git-workflow/SKILL.md +6 -6
  125. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  126. package/templates/claude-code/skills/playwright/SKILL.md +6 -5
  127. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  128. package/templates/claude-code/skills/security-web/SKILL.md +2 -1
  129. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  130. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  131. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  132. package/templates/docs-portal/fastapi/backend/app/portal/__init__.py +0 -0
  133. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  134. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  135. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  136. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  137. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  138. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  139. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  140. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  141. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  142. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  143. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  144. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  145. package/templates/frontend/nextjs/package.json.template +3 -1
  146. package/templates/frontend/react/index.html.template +12 -0
  147. package/templates/frontend/react/package.json.template +34 -0
  148. package/templates/frontend/react/src/App.tsx.template +10 -0
  149. package/templates/frontend/react/src/index.css +1 -0
  150. package/templates/frontend/react/src/main.tsx +10 -0
  151. package/templates/frontend/react/tsconfig.json +17 -0
  152. package/templates/frontend/react/vite.config.ts.template +15 -0
  153. package/templates/frontend/react/vitest.config.ts +9 -0
  154. package/templates/frontend/remix/app/root.tsx.template +31 -0
  155. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  156. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  157. package/templates/frontend/remix/app/tailwind.css +1 -0
  158. package/templates/frontend/remix/package.json.template +39 -0
  159. package/templates/frontend/remix/tsconfig.json +18 -0
  160. package/templates/frontend/remix/vite.config.ts.template +7 -0
  161. package/templates/infra/github-actions/.github/workflows/ci.yml.template +52 -0
  162. package/templates/testing/pytest/backend/tests/__init__.py +0 -0
  163. package/templates/testing/pytest/backend/tests/conftest.py.template +11 -0
  164. package/templates/testing/pytest/backend/tests/test_health.py.template +10 -0
  165. package/templates/testing/vitest/vitest.config.ts.template +18 -0
  166. package/CLAUDE.md +0 -38
  167. package/templates/claude-code/commands/done.md +0 -19
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();
@@ -56,11 +70,11 @@ export async function parseCommand(args) {
56
70
  if (!command.startsWith('-')) {
57
71
  const targetDir = path.resolve(process.cwd(), command);
58
72
  if (fs.existsSync(targetDir)) {
59
- console.log('');
73
+ console.error('');
60
74
  log.warn(`"${command}" already exists. Did you mean:`);
61
- console.log(` ${chalk.bold('devforge init')} Add dev guardrails to current project`);
62
- console.log(` ${chalk.bold('devforge doctor')} Diagnose and optimize current project`);
63
- console.log('');
75
+ console.error(` ${chalk.bold('devforge init')} Add dev guardrails to current project`);
76
+ console.error(` ${chalk.bold('devforge doctor')} Diagnose and optimize current project`);
77
+ console.error('');
64
78
  process.exit(1);
65
79
  }
66
80
  const { runNew } = await import('./index.js');
@@ -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
 
@@ -86,20 +101,20 @@ function showUsage() {
86
101
  -h, --help Show this help message
87
102
  -v, --version Show version number
88
103
 
89
- Run ${chalk.cyan('devforge new --help')} for more details.
104
+ Run ${chalk.cyan('devforge --help')} for more details.
90
105
  `);
91
106
  }
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
+ }