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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { usePathname } from 'next/navigation';
|
|
6
|
+
|
|
7
|
+
interface NavCategory {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon: string;
|
|
11
|
+
docs: { slug: string; title: string }[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function MobilePortalNav({ categories }: { categories: NavCategory[] }) {
|
|
15
|
+
const pathname = usePathname();
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<button
|
|
21
|
+
onClick={() => setOpen(!open)}
|
|
22
|
+
className="p-2 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors"
|
|
23
|
+
aria-label="Toggle navigation menu"
|
|
24
|
+
>
|
|
25
|
+
{open ? (
|
|
26
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
27
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
28
|
+
</svg>
|
|
29
|
+
) : (
|
|
30
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
31
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
32
|
+
</svg>
|
|
33
|
+
)}
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
{open && (
|
|
37
|
+
<div className="absolute top-full left-0 right-0 bg-white border-b border-gray-200 shadow-lg z-50">
|
|
38
|
+
<div className="max-w-7xl mx-auto px-4 py-4 space-y-2">
|
|
39
|
+
<Link
|
|
40
|
+
href="/portal"
|
|
41
|
+
onClick={() => setOpen(false)}
|
|
42
|
+
className={`block px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
43
|
+
pathname === '/portal'
|
|
44
|
+
? 'bg-gray-100 text-gray-900'
|
|
45
|
+
: 'text-gray-600 hover:bg-gray-50'
|
|
46
|
+
}`}
|
|
47
|
+
>
|
|
48
|
+
Overview
|
|
49
|
+
</Link>
|
|
50
|
+
{categories.map((cat) => (
|
|
51
|
+
<div key={cat.id} className="space-y-1">
|
|
52
|
+
<Link
|
|
53
|
+
href={`/portal/${cat.id}`}
|
|
54
|
+
onClick={() => setOpen(false)}
|
|
55
|
+
className={`block px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
56
|
+
pathname.startsWith(`/portal/${cat.id}`)
|
|
57
|
+
? 'bg-gray-100 text-gray-900'
|
|
58
|
+
: 'text-gray-600 hover:bg-gray-50'
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
<span className="mr-2">{cat.icon}</span>
|
|
62
|
+
{cat.label}
|
|
63
|
+
</Link>
|
|
64
|
+
{cat.docs.map((doc) => (
|
|
65
|
+
<Link
|
|
66
|
+
key={doc.slug}
|
|
67
|
+
href={`/portal/${cat.id}/${doc.slug}`}
|
|
68
|
+
onClick={() => setOpen(false)}
|
|
69
|
+
className="block pl-8 pr-3 py-1.5 text-sm text-gray-500 hover:text-gray-900 hover:bg-gray-50 rounded-lg transition-colors"
|
|
70
|
+
>
|
|
71
|
+
{doc.title}
|
|
72
|
+
</Link>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { useState, useRef, useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
interface NavCategory {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon: string;
|
|
11
|
+
docs: { slug: string; title: string }[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PortalNav({ categories }: { categories: NavCategory[] }) {
|
|
15
|
+
const pathname = usePathname();
|
|
16
|
+
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
|
17
|
+
const navRef = useRef<HTMLElement>(null);
|
|
18
|
+
|
|
19
|
+
// Close dropdown when clicking outside
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
function handleClickOutside(e: MouseEvent) {
|
|
22
|
+
if (navRef.current && !navRef.current.contains(e.target as Node)) {
|
|
23
|
+
setOpenMenu(null);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
27
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<nav ref={navRef} className="flex items-center gap-1">
|
|
32
|
+
<Link
|
|
33
|
+
href="/portal"
|
|
34
|
+
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
35
|
+
pathname === '/portal'
|
|
36
|
+
? 'bg-gray-100 text-gray-900'
|
|
37
|
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
|
38
|
+
}`}
|
|
39
|
+
>
|
|
40
|
+
Overview
|
|
41
|
+
</Link>
|
|
42
|
+
{categories.map((cat) => (
|
|
43
|
+
<div
|
|
44
|
+
key={cat.id}
|
|
45
|
+
className="relative"
|
|
46
|
+
onMouseEnter={() => setOpenMenu(cat.id)}
|
|
47
|
+
onMouseLeave={() => setOpenMenu(null)}
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
aria-expanded={openMenu === cat.id}
|
|
51
|
+
aria-haspopup="true"
|
|
52
|
+
onClick={() => setOpenMenu(openMenu === cat.id ? null : cat.id)}
|
|
53
|
+
onKeyDown={(e) => {
|
|
54
|
+
if (e.key === 'Escape') setOpenMenu(null);
|
|
55
|
+
}}
|
|
56
|
+
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-1.5 ${
|
|
57
|
+
pathname.startsWith(`/portal/${cat.id}`)
|
|
58
|
+
? 'bg-gray-100 text-gray-900'
|
|
59
|
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
|
60
|
+
}`}
|
|
61
|
+
>
|
|
62
|
+
<span>{cat.icon}</span>
|
|
63
|
+
<span>{cat.label}</span>
|
|
64
|
+
<svg className="w-3.5 h-3.5 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
65
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
66
|
+
</svg>
|
|
67
|
+
</button>
|
|
68
|
+
{openMenu === cat.id && cat.docs.length > 0 && (
|
|
69
|
+
<div role="menu" className="absolute top-full left-0 mt-1 w-64 bg-white rounded-xl shadow-lg border border-gray-200 py-2 z-50">
|
|
70
|
+
{cat.docs.map((doc) => (
|
|
71
|
+
<Link
|
|
72
|
+
key={doc.slug}
|
|
73
|
+
role="menuitem"
|
|
74
|
+
href={`/portal/${cat.id}/${doc.slug}`}
|
|
75
|
+
className="block px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 transition-colors"
|
|
76
|
+
>
|
|
77
|
+
{doc.title}
|
|
78
|
+
</Link>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</nav>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -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,52 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint-and-test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
services:
|
|
14
|
+
postgres:
|
|
15
|
+
image: postgres:17-alpine
|
|
16
|
+
env:
|
|
17
|
+
POSTGRES_DB: {{PROJECT_NAME_SNAKE}}
|
|
18
|
+
POSTGRES_USER: postgres
|
|
19
|
+
POSTGRES_PASSWORD: postgres
|
|
20
|
+
ports:
|
|
21
|
+
- 5432:5432
|
|
22
|
+
options: >-
|
|
23
|
+
--health-cmd pg_isready
|
|
24
|
+
--health-interval 10s
|
|
25
|
+
--health-timeout 5s
|
|
26
|
+
--health-retries 5
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- name: Setup Node.js
|
|
32
|
+
uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version: '22'
|
|
35
|
+
cache: 'npm'
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: npm ci
|
|
39
|
+
|
|
40
|
+
- name: Lint
|
|
41
|
+
run: {{LINT_COMMAND}}
|
|
42
|
+
|
|
43
|
+
- name: Type check
|
|
44
|
+
run: {{TYPE_CHECK_COMMAND}}
|
|
45
|
+
|
|
46
|
+
- name: Build
|
|
47
|
+
run: {{BUILD_COMMAND}}
|
|
48
|
+
|
|
49
|
+
- name: Test
|
|
50
|
+
run: {{TEST_COMMAND}}
|
|
51
|
+
env:
|
|
52
|
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME_SNAKE}}
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from httpx import ASGITransport, AsyncClient
|
|
3
|
+
|
|
4
|
+
from app.main import app
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
async def client():
|
|
9
|
+
transport = ASGITransport(app=app)
|
|
10
|
+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
11
|
+
yield ac
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@pytest.mark.asyncio
|
|
5
|
+
async def test_health_endpoint(client):
|
|
6
|
+
response = await client.get("/health")
|
|
7
|
+
assert response.status_code == 200
|
|
8
|
+
data = response.json()
|
|
9
|
+
assert data["status"] == "ok"
|
|
10
|
+
assert data["service"] == "{{PROJECT_NAME}}"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['src/**/*.test.{ts,tsx}'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'json', 'html'],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
resolve: {
|
|
14
|
+
alias: {
|
|
15
|
+
'@': path.resolve(__dirname, './src'),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|