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.
- package/README.md +58 -10
- package/bin/chainproof.js +126 -0
- package/bin/devforge.js +2 -1
- package/package.json +33 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +87 -49
- package/src/cli.js +35 -12
- 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 +65 -6
- 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/plans/.gitkeep +0 -0
- package/templates/base/docs/uat/UAT_CHECKLIST.csv.template +2 -0
- package/templates/base/docs/uat/UAT_TEMPLATE.md.template +22 -0
- 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 +22 -7
- package/templates/claude-code/agents/chief-of-staff.md +42 -8
- package/templates/claude-code/agents/code-quality-reviewer.md +15 -1
- package/templates/claude-code/agents/database-reviewer.md +16 -2
- 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 +61 -0
- package/templates/claude-code/agents/loop-operator.md +27 -12
- package/templates/claude-code/agents/planner.md +21 -7
- package/templates/claude-code/agents/product-strategist.md +138 -0
- 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 +15 -0
- package/templates/claude-code/agents/spec-validator.md +45 -1
- package/templates/claude-code/agents/tdd-guide.md +21 -7
- package/templates/claude-code/agents/uat-validator.md +18 -0
- package/templates/claude-code/claude-md/base.md +15 -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 +54 -26
- package/templates/claude-code/commands/fix-loop.md +211 -0
- package/templates/claude-code/commands/full-audit.md +37 -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 -37
- 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 +20 -10
- package/templates/claude-code/hooks/scripts/autofix-python.mjs +4 -5
- package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +4 -4
- package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
- package/templates/claude-code/hooks/scripts/guard-protected-files.mjs +2 -2
- 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 +6 -6
- package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
- package/templates/claude-code/skills/playwright/SKILL.md +6 -5
- package/templates/claude-code/skills/security-api/SKILL.md +1 -1
- package/templates/claude-code/skills/security-web/SKILL.md +2 -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/__init__.py +0 -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 +52 -0
- package/templates/testing/pytest/backend/tests/__init__.py +0 -0
- package/templates/testing/pytest/backend/tests/conftest.py.template +11 -0
- package/templates/testing/pytest/backend/tests/test_health.py.template +10 -0
- package/templates/testing/vitest/vitest.config.ts.template +18 -0
- package/CLAUDE.md +0 -38
- 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
|
-
|
|
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();
|
|
@@ -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.
|
|
73
|
+
console.error('');
|
|
60
74
|
log.warn(`"${command}" already exists. Did you mean:`);
|
|
61
|
-
console.
|
|
62
|
-
console.
|
|
63
|
-
console.
|
|
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')}
|
|
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
|
|
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')}
|
|
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
|
+
}
|