forgedev 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -10
- package/bin/chainproof.js +126 -0
- package/bin/devforge.js +1 -1
- package/package.json +25 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +171 -78
- package/src/cli.js +30 -7
- package/src/composer.js +242 -214
- 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 +76 -12
- package/src/menu.js +178 -0
- package/src/prompts.js +5 -12
- package/src/recommender.js +163 -30
- package/src/scanner.js +57 -2
- package/src/uat-generator.js +204 -189
- package/src/update-check.js +9 -4
- package/src/update.js +57 -13
- package/src/utils.js +162 -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/.gitignore.template +3 -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/templates/infra/k8s/k8s/deployment.yml.template +70 -0
- package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
- package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
- package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
- package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
- package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
- package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
- package/templates/infra/k8s/k8s/service.yml.template +15 -0
- package/templates/testing/load/k6/README.md.template +48 -0
- package/templates/testing/load/k6/load-test.js.template +57 -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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface DocFile {
|
|
5
|
+
slug: string;
|
|
6
|
+
title: string;
|
|
7
|
+
content: string;
|
|
8
|
+
category: string;
|
|
9
|
+
categoryLabel: string;
|
|
10
|
+
lastModified: Date;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DocCategory {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
description: string;
|
|
17
|
+
icon: string;
|
|
18
|
+
docs: { slug: string; title: string }[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DOCS_ROOT = path.join(process.cwd(), 'docs');
|
|
22
|
+
|
|
23
|
+
const CATEGORY_META: Record<string, { label: string; description: string; icon: string; order: number }> = {
|
|
24
|
+
prd: { label: 'Product', description: 'Product requirements and roadmap', icon: '📋', order: 1 },
|
|
25
|
+
sdd: { label: 'Architecture', description: 'System design and technical architecture', icon: '🏗️', order: 2 },
|
|
26
|
+
uat: { label: 'Testing', description: 'Test plans, results, and acceptance criteria', icon: '✅', order: 3 },
|
|
27
|
+
sessions: { label: 'Session Logs', description: 'Testing session results and bug reports', icon: '📝', order: 4 },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function extractTitle(content: string, filename: string): string {
|
|
31
|
+
// Try to get title from first H1
|
|
32
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
33
|
+
if (match) return match[1];
|
|
34
|
+
|
|
35
|
+
// Fall back to filename
|
|
36
|
+
return filename
|
|
37
|
+
.replace(/\.md$/, '')
|
|
38
|
+
.replace(/[-_]/g, ' ')
|
|
39
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getCategories(): DocCategory[] {
|
|
43
|
+
if (!fs.existsSync(DOCS_ROOT)) return [];
|
|
44
|
+
|
|
45
|
+
const categories: DocCategory[] = [];
|
|
46
|
+
const entries = fs.readdirSync(DOCS_ROOT, { withFileTypes: true });
|
|
47
|
+
|
|
48
|
+
// Categorized docs (in subdirectories)
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.isDirectory()) continue;
|
|
51
|
+
|
|
52
|
+
const categoryId = entry.name;
|
|
53
|
+
const categoryDir = path.join(DOCS_ROOT, categoryId);
|
|
54
|
+
const meta = CATEGORY_META[categoryId] || {
|
|
55
|
+
label: categoryId.charAt(0).toUpperCase() + categoryId.slice(1),
|
|
56
|
+
description: '',
|
|
57
|
+
icon: '📄',
|
|
58
|
+
order: 99,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const files = fs.readdirSync(categoryDir).filter((f) => f.endsWith('.md'));
|
|
62
|
+
if (files.length === 0) continue;
|
|
63
|
+
|
|
64
|
+
const docs = files.map((f) => {
|
|
65
|
+
const raw = fs.readFileSync(path.join(categoryDir, f), 'utf-8');
|
|
66
|
+
return {
|
|
67
|
+
slug: f.replace(/\.md$/, ''),
|
|
68
|
+
title: extractTitle(raw, f),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
categories.push({ id: categoryId, ...meta, docs });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Top-level markdown files go into a "General" category
|
|
76
|
+
const topLevelFiles = entries.filter((e) => !e.isDirectory() && e.name.endsWith('.md'));
|
|
77
|
+
if (topLevelFiles.length > 0) {
|
|
78
|
+
const docs = topLevelFiles.map((f) => {
|
|
79
|
+
const raw = fs.readFileSync(path.join(DOCS_ROOT, f.name), 'utf-8');
|
|
80
|
+
return {
|
|
81
|
+
slug: f.name.replace(/\.md$/, ''),
|
|
82
|
+
title: extractTitle(raw, f.name),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
categories.push({
|
|
86
|
+
id: '_general',
|
|
87
|
+
label: 'General',
|
|
88
|
+
description: 'Project documentation',
|
|
89
|
+
icon: '📄',
|
|
90
|
+
docs,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Sort by configured order
|
|
95
|
+
return categories.sort((a, b) => {
|
|
96
|
+
const orderA = CATEGORY_META[a.id]?.order ?? 99;
|
|
97
|
+
const orderB = CATEGORY_META[b.id]?.order ?? 99;
|
|
98
|
+
return orderA - orderB;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getDoc(category: string, slug: string): DocFile | null {
|
|
103
|
+
const dirPath = category === '_general' ? DOCS_ROOT : path.join(DOCS_ROOT, category);
|
|
104
|
+
const filePath = path.join(dirPath, `${slug}.md`);
|
|
105
|
+
|
|
106
|
+
// Prevent path traversal — resolved path must stay within DOCS_ROOT
|
|
107
|
+
const resolved = path.resolve(filePath);
|
|
108
|
+
const docsRoot = path.resolve(DOCS_ROOT);
|
|
109
|
+
if (!resolved.startsWith(docsRoot + path.sep)) return null;
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(filePath)) return null;
|
|
112
|
+
|
|
113
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
114
|
+
const stat = fs.statSync(filePath);
|
|
115
|
+
const meta = CATEGORY_META[category] || { label: 'General', description: '', icon: '📄', order: 99 };
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
slug,
|
|
119
|
+
title: extractTitle(content, `${slug}.md`),
|
|
120
|
+
content,
|
|
121
|
+
category,
|
|
122
|
+
categoryLabel: meta.label,
|
|
123
|
+
lastModified: stat.mtime,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getAllDocs(): DocFile[] {
|
|
128
|
+
const categories = getCategories();
|
|
129
|
+
const docs: DocFile[] = [];
|
|
130
|
+
|
|
131
|
+
for (const cat of categories) {
|
|
132
|
+
for (const doc of cat.docs) {
|
|
133
|
+
const full = getDoc(cat.id, doc.slug);
|
|
134
|
+
if (full) docs.push(full);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return docs;
|
|
139
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{PROJECT_NAME_PASCAL}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"react": "^19.1.0",
|
|
17
|
+
"react-dom": "^19.1.0",
|
|
18
|
+
"react-router-dom": "^7.5.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.8.3",
|
|
22
|
+
"@types/react": "^19.1.0",
|
|
23
|
+
"@types/react-dom": "^19.1.0",
|
|
24
|
+
"@vitejs/plugin-react": "^4.4.0",
|
|
25
|
+
"vite": "^6.3.0",
|
|
26
|
+
"tailwindcss": "^4.1.3",
|
|
27
|
+
"@tailwindcss/vite": "^4.1.3",
|
|
28
|
+
"eslint": "^9.25.0",
|
|
29
|
+
"vitest": "^3.1.1",
|
|
30
|
+
"@testing-library/react": "^16.3.0",
|
|
31
|
+
"@testing-library/jest-dom": "^6.6.0",
|
|
32
|
+
"jsdom": "^26.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default function App() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
4
|
+
<div className="text-center">
|
|
5
|
+
<h1 className="text-4xl font-bold text-gray-900">{{PROJECT_NAME_PASCAL}}</h1>
|
|
6
|
+
<p className="mt-4 text-lg text-gray-600">{{STACK_DESCRIPTION}}</p>
|
|
7
|
+
</div>
|
|
8
|
+
</main>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react(), tailwindcss()],
|
|
7
|
+
server: {
|
|
8
|
+
proxy: {
|
|
9
|
+
'/api': {
|
|
10
|
+
target: 'http://localhost:3001',
|
|
11
|
+
changeOrigin: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Links,
|
|
3
|
+
Meta,
|
|
4
|
+
Outlet,
|
|
5
|
+
Scripts,
|
|
6
|
+
ScrollRestoration,
|
|
7
|
+
} from '@remix-run/react';
|
|
8
|
+
import type { LinksFunction } from '@remix-run/node';
|
|
9
|
+
import stylesheet from './tailwind.css?url';
|
|
10
|
+
|
|
11
|
+
export const links: LinksFunction = () => [
|
|
12
|
+
{ rel: 'stylesheet', href: stylesheet },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export default function App() {
|
|
16
|
+
return (
|
|
17
|
+
<html lang="en">
|
|
18
|
+
<head>
|
|
19
|
+
<meta charSet="utf-8" />
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
21
|
+
<Meta />
|
|
22
|
+
<Links />
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<Outlet />
|
|
26
|
+
<ScrollRestoration />
|
|
27
|
+
<Scripts />
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MetaFunction } from '@remix-run/node';
|
|
2
|
+
|
|
3
|
+
export const meta: MetaFunction = () => {
|
|
4
|
+
return [
|
|
5
|
+
{ title: '{{PROJECT_NAME_PASCAL}}' },
|
|
6
|
+
{ name: 'description', content: '{{STACK_DESCRIPTION}}' },
|
|
7
|
+
];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function Index() {
|
|
11
|
+
return (
|
|
12
|
+
<main className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
13
|
+
<div className="text-center">
|
|
14
|
+
<h1 className="text-4xl font-bold text-gray-900">{{PROJECT_NAME_PASCAL}}</h1>
|
|
15
|
+
<p className="mt-4 text-lg text-gray-600">{{STACK_DESCRIPTION}}</p>
|
|
16
|
+
</div>
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "remix vite:dev",
|
|
8
|
+
"build": "remix vite:build",
|
|
9
|
+
"start": "remix-serve ./build/server/index.js",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"db:push": "prisma db push",
|
|
15
|
+
"db:studio": "prisma studio",
|
|
16
|
+
"db:generate": "prisma generate"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@remix-run/node": "^2.16.0",
|
|
20
|
+
"@remix-run/react": "^2.16.0",
|
|
21
|
+
"@remix-run/serve": "^2.16.0",
|
|
22
|
+
"react": "^19.1.0",
|
|
23
|
+
"react-dom": "^19.1.0",
|
|
24
|
+
"isbot": "^5.1.0",
|
|
25
|
+
"@prisma/client": "^6.6.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@remix-run/dev": "^2.16.0",
|
|
29
|
+
"typescript": "^5.8.3",
|
|
30
|
+
"@types/react": "^19.1.0",
|
|
31
|
+
"@types/react-dom": "^19.1.0",
|
|
32
|
+
"vite": "^6.3.0",
|
|
33
|
+
"tailwindcss": "^4.1.3",
|
|
34
|
+
"@tailwindcss/vite": "^4.1.3",
|
|
35
|
+
"eslint": "^9.25.0",
|
|
36
|
+
"prisma": "^6.6.0",
|
|
37
|
+
"vitest": "^3.1.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"types": ["@remix-run/node", "vite/client"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["app", "env.d.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{PROJECT_NAME}}
|
|
5
|
+
labels:
|
|
6
|
+
app: {{PROJECT_NAME}}
|
|
7
|
+
spec:
|
|
8
|
+
replicas: 2
|
|
9
|
+
selector:
|
|
10
|
+
matchLabels:
|
|
11
|
+
app: {{PROJECT_NAME}}
|
|
12
|
+
template:
|
|
13
|
+
metadata:
|
|
14
|
+
labels:
|
|
15
|
+
app: {{PROJECT_NAME}}
|
|
16
|
+
spec:
|
|
17
|
+
automountServiceAccountToken: false
|
|
18
|
+
securityContext:
|
|
19
|
+
runAsNonRoot: true
|
|
20
|
+
runAsUser: 1000
|
|
21
|
+
fsGroup: 1000
|
|
22
|
+
containers:
|
|
23
|
+
- name: {{PROJECT_NAME}}
|
|
24
|
+
image: {{PROJECT_NAME}}:{{IMAGE_TAG}} # Update to match your CI-built image tag
|
|
25
|
+
imagePullPolicy: IfNotPresent
|
|
26
|
+
ports:
|
|
27
|
+
- containerPort: {{APP_PORT}}
|
|
28
|
+
env:
|
|
29
|
+
- name: PORT
|
|
30
|
+
value: "{{APP_PORT}}"
|
|
31
|
+
- name: DATABASE_URL
|
|
32
|
+
valueFrom:
|
|
33
|
+
secretKeyRef:
|
|
34
|
+
name: {{PROJECT_NAME}}-secrets
|
|
35
|
+
key: database-url
|
|
36
|
+
securityContext:
|
|
37
|
+
allowPrivilegeEscalation: false
|
|
38
|
+
readOnlyRootFilesystem: true
|
|
39
|
+
capabilities:
|
|
40
|
+
drop: ["ALL"]
|
|
41
|
+
volumeMounts:
|
|
42
|
+
- name: tmp
|
|
43
|
+
mountPath: /tmp
|
|
44
|
+
livenessProbe:
|
|
45
|
+
httpGet:
|
|
46
|
+
path: /health
|
|
47
|
+
port: {{APP_PORT}}
|
|
48
|
+
initialDelaySeconds: 10
|
|
49
|
+
periodSeconds: 15
|
|
50
|
+
timeoutSeconds: 5
|
|
51
|
+
failureThreshold: 3
|
|
52
|
+
readinessProbe:
|
|
53
|
+
httpGet:
|
|
54
|
+
path: /healthz
|
|
55
|
+
port: {{APP_PORT}}
|
|
56
|
+
initialDelaySeconds: 5
|
|
57
|
+
periodSeconds: 10
|
|
58
|
+
timeoutSeconds: 5
|
|
59
|
+
failureThreshold: 3
|
|
60
|
+
resources:
|
|
61
|
+
requests:
|
|
62
|
+
cpu: 100m
|
|
63
|
+
memory: 128Mi
|
|
64
|
+
limits:
|
|
65
|
+
cpu: 500m
|
|
66
|
+
memory: 512Mi
|
|
67
|
+
volumes:
|
|
68
|
+
- name: tmp
|
|
69
|
+
emptyDir: {}
|
|
70
|
+
terminationGracePeriodSeconds: 30
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
apiVersion: autoscaling/v2
|
|
2
|
+
kind: HorizontalPodAutoscaler
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{PROJECT_NAME}}
|
|
5
|
+
spec:
|
|
6
|
+
scaleTargetRef:
|
|
7
|
+
apiVersion: apps/v1
|
|
8
|
+
kind: Deployment
|
|
9
|
+
name: {{PROJECT_NAME}}
|
|
10
|
+
minReplicas: 2
|
|
11
|
+
maxReplicas: 10
|
|
12
|
+
metrics:
|
|
13
|
+
- type: Resource
|
|
14
|
+
resource:
|
|
15
|
+
name: cpu
|
|
16
|
+
target:
|
|
17
|
+
type: Utilization
|
|
18
|
+
averageUtilization: 70
|
|
19
|
+
- type: Resource
|
|
20
|
+
resource:
|
|
21
|
+
name: memory
|
|
22
|
+
target:
|
|
23
|
+
type: Utilization
|
|
24
|
+
averageUtilization: 80
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
apiVersion: networking.k8s.io/v1
|
|
2
|
+
kind: Ingress
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{PROJECT_NAME}}
|
|
5
|
+
annotations:
|
|
6
|
+
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
|
7
|
+
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
|
|
8
|
+
nginx.ingress.kubernetes.io/limit-rps: "10"
|
|
9
|
+
nginx.ingress.kubernetes.io/limit-connections: "5"
|
|
10
|
+
spec:
|
|
11
|
+
ingressClassName: nginx
|
|
12
|
+
rules:
|
|
13
|
+
- host: {{PROJECT_NAME}}.example.com
|
|
14
|
+
http:
|
|
15
|
+
paths:
|
|
16
|
+
- path: /
|
|
17
|
+
pathType: Prefix
|
|
18
|
+
backend:
|
|
19
|
+
service:
|
|
20
|
+
name: {{PROJECT_NAME}}
|
|
21
|
+
port:
|
|
22
|
+
number: 80
|
|
23
|
+
tls:
|
|
24
|
+
- hosts:
|
|
25
|
+
- {{PROJECT_NAME}}.example.com
|
|
26
|
+
secretName: {{PROJECT_NAME}}-tls
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
apiVersion: networking.k8s.io/v1
|
|
2
|
+
kind: NetworkPolicy
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{PROJECT_NAME}}
|
|
5
|
+
spec:
|
|
6
|
+
podSelector:
|
|
7
|
+
matchLabels:
|
|
8
|
+
app: {{PROJECT_NAME}}
|
|
9
|
+
policyTypes:
|
|
10
|
+
- Ingress
|
|
11
|
+
- Egress
|
|
12
|
+
ingress:
|
|
13
|
+
- from:
|
|
14
|
+
- namespaceSelector:
|
|
15
|
+
matchLabels:
|
|
16
|
+
kubernetes.io/metadata.name: ingress-nginx
|
|
17
|
+
ports:
|
|
18
|
+
- port: {{APP_PORT}}
|
|
19
|
+
egress:
|
|
20
|
+
# Database
|
|
21
|
+
- to:
|
|
22
|
+
- podSelector:
|
|
23
|
+
matchLabels:
|
|
24
|
+
app: postgres
|
|
25
|
+
ports:
|
|
26
|
+
- port: 5432
|
|
27
|
+
# DNS (UDP + TCP for large responses)
|
|
28
|
+
- to:
|
|
29
|
+
- namespaceSelector:
|
|
30
|
+
matchLabels:
|
|
31
|
+
kubernetes.io/metadata.name: kube-system
|
|
32
|
+
ports:
|
|
33
|
+
- port: 53
|
|
34
|
+
protocol: UDP
|
|
35
|
+
- port: 53
|
|
36
|
+
protocol: TCP
|
|
37
|
+
# Uncomment to allow outbound HTTPS (external APIs, OAuth, webhooks)
|
|
38
|
+
# - to: []
|
|
39
|
+
# ports:
|
|
40
|
+
# - port: 443
|
|
41
|
+
# protocol: TCP
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Secret
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{PROJECT_NAME}}-secrets
|
|
5
|
+
type: Opaque
|
|
6
|
+
stringData:
|
|
7
|
+
# IMPORTANT: Replace these placeholder values before deploying.
|
|
8
|
+
# In production, use sealed-secrets, external-secrets, or your cloud provider's secret manager.
|
|
9
|
+
# Do NOT commit real credentials to version control.
|
|
10
|
+
database-url: "postgresql://USER:CHANGE_ME_BEFORE_DEPLOY@postgres:5432/{{PROJECT_NAME_SNAKE}}"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Load Testing — {{PROJECT_NAME_PASCAL}}
|
|
2
|
+
|
|
3
|
+
Uses [k6](https://k6.io/) for load testing.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# macOS
|
|
9
|
+
brew install k6
|
|
10
|
+
|
|
11
|
+
# Windows (scoop)
|
|
12
|
+
scoop install k6
|
|
13
|
+
|
|
14
|
+
# Docker (no install)
|
|
15
|
+
docker run --rm -i grafana/k6 run - <load-test.js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Run
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Smoke test (quick, single request)
|
|
22
|
+
k6 run --vus 1 --iterations 1 k6/load-test.js
|
|
23
|
+
|
|
24
|
+
# Full load test against local server
|
|
25
|
+
k6 run k6/load-test.js
|
|
26
|
+
|
|
27
|
+
# Against staging
|
|
28
|
+
k6 run -e BASE_URL=https://staging.example.com k6/load-test.js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Thresholds
|
|
32
|
+
|
|
33
|
+
| Metric | Threshold | Meaning |
|
|
34
|
+
|--------|-----------|---------|
|
|
35
|
+
| `http_req_duration p(95)` | < 500ms | 95th percentile response time |
|
|
36
|
+
| `http_req_duration p(99)` | < 1.5s | 99th percentile response time |
|
|
37
|
+
| `http_req_failed` | < 1% | Error rate |
|
|
38
|
+
| `checks` | > 99% | Assertion pass rate |
|
|
39
|
+
|
|
40
|
+
## CI Integration
|
|
41
|
+
|
|
42
|
+
Add to your CI pipeline after deployment:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
- name: Load test
|
|
46
|
+
run: |
|
|
47
|
+
k6 run -e BASE_URL=${{ secrets.STAGING_URL }} k6/load-test.js
|
|
48
|
+
```
|