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.
- package/README.md +57 -10
- package/bin/chainproof.js +126 -0
- package/package.json +25 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +86 -49
- package/src/cli.js +30 -7
- package/src/composer.js +159 -34
- package/src/doctor-checks-chainproof.js +106 -0
- package/src/doctor-checks.js +39 -20
- package/src/doctor-prompts.js +9 -9
- package/src/doctor.js +37 -4
- package/src/guided.js +3 -3
- package/src/index.js +31 -10
- package/src/init-mode.js +64 -11
- package/src/menu.js +178 -0
- package/src/prompts.js +5 -12
- package/src/recommender.js +134 -10
- package/src/scanner.js +57 -2
- package/src/uat-generator.js +204 -189
- package/src/update-check.js +9 -4
- package/src/update.js +1 -1
- package/src/utils.js +64 -5
- package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
- package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
- package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
- package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
- package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
- package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
- package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
- package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
- package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
- package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
- package/templates/backend/express/Dockerfile.template +18 -0
- package/templates/backend/express/package.json.template +33 -0
- package/templates/backend/express/src/index.ts.template +34 -0
- package/templates/backend/express/src/routes/health.ts.template +27 -0
- package/templates/backend/express/tsconfig.json +17 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
- package/templates/backend/fastapi/backend/app/main.py.template +3 -1
- package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
- package/templates/backend/hono/Dockerfile.template +18 -0
- package/templates/backend/hono/package.json.template +31 -0
- package/templates/backend/hono/src/index.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +27 -0
- package/templates/backend/hono/tsconfig.json +18 -0
- package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
- package/templates/chainproof/base/.chainproof/config.json.template +11 -0
- package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
- package/templates/chainproof/base/.mcp.json +9 -0
- package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
- package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
- package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
- package/templates/claude-code/agents/architect.md +25 -11
- package/templates/claude-code/agents/build-error-resolver.md +19 -5
- package/templates/claude-code/agents/chief-of-staff.md +42 -8
- package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
- package/templates/claude-code/agents/database-reviewer.md +15 -1
- package/templates/claude-code/agents/deep-reviewer.md +191 -0
- package/templates/claude-code/agents/doc-updater.md +19 -5
- package/templates/claude-code/agents/docs-lookup.md +19 -5
- package/templates/claude-code/agents/e2e-runner.md +26 -12
- package/templates/claude-code/agents/enforcement-gate.md +102 -0
- package/templates/claude-code/agents/frontend-builder.md +188 -0
- package/templates/claude-code/agents/harness-optimizer.md +36 -1
- package/templates/claude-code/agents/loop-operator.md +27 -13
- package/templates/claude-code/agents/planner.md +21 -7
- package/templates/claude-code/agents/product-strategist.md +24 -10
- package/templates/claude-code/agents/production-readiness.md +14 -0
- package/templates/claude-code/agents/prompt-auditor.md +115 -0
- package/templates/claude-code/agents/refactor-cleaner.md +22 -8
- package/templates/claude-code/agents/security-reviewer.md +14 -0
- package/templates/claude-code/agents/spec-validator.md +15 -1
- package/templates/claude-code/agents/tdd-guide.md +21 -7
- package/templates/claude-code/agents/uat-validator.md +14 -0
- package/templates/claude-code/claude-md/base.md +14 -7
- package/templates/claude-code/claude-md/fastapi.md +8 -8
- package/templates/claude-code/claude-md/fullstack.md +6 -6
- package/templates/claude-code/claude-md/hono.md +18 -0
- package/templates/claude-code/claude-md/nextjs.md +5 -5
- package/templates/claude-code/claude-md/remix.md +18 -0
- package/templates/claude-code/commands/audit-security.md +14 -0
- package/templates/claude-code/commands/audit-spec.md +14 -0
- package/templates/claude-code/commands/audit-wiring.md +14 -0
- package/templates/claude-code/commands/build-fix.md +28 -0
- package/templates/claude-code/commands/build-ui.md +59 -0
- package/templates/claude-code/commands/code-review.md +53 -31
- package/templates/claude-code/commands/fix-loop.md +211 -0
- package/templates/claude-code/commands/full-audit.md +36 -8
- package/templates/claude-code/commands/generate-prd.md +1 -1
- package/templates/claude-code/commands/generate-sdd.md +74 -0
- package/templates/claude-code/commands/generate-uat.md +107 -35
- package/templates/claude-code/commands/help.md +68 -0
- package/templates/claude-code/commands/live-uat.md +268 -0
- package/templates/claude-code/commands/optimize-claude-md.md +15 -1
- package/templates/claude-code/commands/plan.md +3 -3
- package/templates/claude-code/commands/pre-pr.md +57 -19
- package/templates/claude-code/commands/product-strategist.md +21 -0
- package/templates/claude-code/commands/resume-session.md +10 -10
- package/templates/claude-code/commands/run-uat.md +59 -2
- package/templates/claude-code/commands/save-session.md +10 -10
- package/templates/claude-code/commands/simplify.md +36 -0
- package/templates/claude-code/commands/tdd.md +17 -18
- package/templates/claude-code/commands/verify-all.md +24 -0
- package/templates/claude-code/commands/verify-intent.md +55 -0
- package/templates/claude-code/commands/workflows.md +52 -40
- package/templates/claude-code/hooks/polyglot.json +10 -1
- package/templates/claude-code/hooks/python.json +10 -1
- package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
- package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
- package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
- package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
- package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
- package/templates/claude-code/hooks/typescript.json +10 -1
- package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
- package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
- package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
- package/templates/claude-code/skills/playwright/SKILL.md +5 -5
- package/templates/claude-code/skills/security-api/SKILL.md +1 -1
- package/templates/claude-code/skills/security-web/SKILL.md +1 -1
- package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
- package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
- package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
- package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
- package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
- package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
- package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
- package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
- package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
- package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
- package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
- package/templates/frontend/nextjs/package.json.template +3 -1
- package/templates/frontend/react/index.html.template +12 -0
- package/templates/frontend/react/package.json.template +34 -0
- package/templates/frontend/react/src/App.tsx.template +10 -0
- package/templates/frontend/react/src/index.css +1 -0
- package/templates/frontend/react/src/main.tsx +10 -0
- package/templates/frontend/react/tsconfig.json +17 -0
- package/templates/frontend/react/vite.config.ts.template +15 -0
- package/templates/frontend/react/vitest.config.ts +9 -0
- package/templates/frontend/remix/app/root.tsx.template +31 -0
- package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
- package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
- package/templates/frontend/remix/app/tailwind.css +1 -0
- package/templates/frontend/remix/package.json.template +39 -0
- package/templates/frontend/remix/tsconfig.json +18 -0
- package/templates/frontend/remix/vite.config.ts.template +7 -0
- package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
- package/docs/00-README.md +0 -310
- package/docs/01-universal-prompt-library.md +0 -1049
- package/docs/02-claude-code-mastery-playbook.md +0 -283
- package/docs/03-multi-agent-verification.md +0 -565
- package/docs/04-errata-and-verification-checklist.md +0 -284
- package/docs/05-universal-scaffolder-vision.md +0 -452
- package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
- package/docs/errata.md +0 -58
- package/docs/multi-agent-verification.md +0 -66
- package/docs/playbook.md +0 -95
- package/docs/prompt-library.md +0 -160
- package/docs/uat/UAT_CHECKLIST.csv +0 -9
- package/docs/uat/UAT_TEMPLATE.md +0 -163
- package/templates/claude-code/commands/done.md +0 -19
- /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
package/src/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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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')}
|
|
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')}
|
|
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
|
|
102
|
-
${chalk.dim('• Developer mode
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
115
|
-
- \`npm run build
|
|
116
|
-
- \`npm run lint
|
|
117
|
-
- \`npx prisma studio
|
|
118
|
-
- \`npx vitest
|
|
119
|
-
- \`npx playwright test
|
|
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
|
|
123
|
-
- \`pytest
|
|
124
|
-
- \`ruff check
|
|
125
|
-
- \`alembic upgrade head
|
|
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
|
|
129
|
-
- \`docker compose up -d postgres
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/doctor-checks.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
446
|
+
function checkUnusedDependencies(projectDir, _scan) {
|
|
434
447
|
const pkgPath = path.join(projectDir, 'package.json');
|
|
435
448
|
if (!fs.existsSync(pkgPath)) return [];
|
|
436
449
|
|
|
437
|
-
|
|
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
|
|
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
|
|
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
|
+
|