forgedev 1.1.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +58 -10
  2. package/bin/chainproof.js +126 -0
  3. package/bin/devforge.js +2 -1
  4. package/package.json +33 -7
  5. package/src/chainproof-bridge.js +330 -0
  6. package/src/ci-mode.js +85 -0
  7. package/src/claude-configurator.js +87 -49
  8. package/src/cli.js +35 -12
  9. package/src/composer.js +159 -34
  10. package/src/doctor-checks-chainproof.js +106 -0
  11. package/src/doctor-checks.js +39 -20
  12. package/src/doctor-prompts.js +9 -9
  13. package/src/doctor.js +37 -4
  14. package/src/guided.js +3 -3
  15. package/src/index.js +31 -10
  16. package/src/init-mode.js +64 -11
  17. package/src/menu.js +178 -0
  18. package/src/prompts.js +5 -12
  19. package/src/recommender.js +134 -10
  20. package/src/scanner.js +57 -2
  21. package/src/uat-generator.js +204 -189
  22. package/src/update-check.js +9 -4
  23. package/src/update.js +1 -1
  24. package/src/utils.js +65 -6
  25. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  29. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  34. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  35. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  36. package/templates/backend/express/Dockerfile.template +18 -0
  37. package/templates/backend/express/package.json.template +33 -0
  38. package/templates/backend/express/src/index.ts.template +34 -0
  39. package/templates/backend/express/src/routes/health.ts.template +27 -0
  40. package/templates/backend/express/tsconfig.json +17 -0
  41. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  42. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  44. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  45. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  46. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  47. package/templates/backend/hono/Dockerfile.template +18 -0
  48. package/templates/backend/hono/package.json.template +31 -0
  49. package/templates/backend/hono/src/index.ts.template +32 -0
  50. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  51. package/templates/backend/hono/tsconfig.json +18 -0
  52. package/templates/base/docs/plans/.gitkeep +0 -0
  53. package/templates/base/docs/uat/UAT_CHECKLIST.csv.template +2 -0
  54. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +22 -0
  55. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  56. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  57. package/templates/chainproof/base/.mcp.json +9 -0
  58. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  59. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  60. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  61. package/templates/claude-code/agents/architect.md +25 -11
  62. package/templates/claude-code/agents/build-error-resolver.md +22 -7
  63. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  64. package/templates/claude-code/agents/code-quality-reviewer.md +15 -1
  65. package/templates/claude-code/agents/database-reviewer.md +16 -2
  66. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  67. package/templates/claude-code/agents/doc-updater.md +19 -5
  68. package/templates/claude-code/agents/docs-lookup.md +19 -5
  69. package/templates/claude-code/agents/e2e-runner.md +26 -12
  70. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  71. package/templates/claude-code/agents/frontend-builder.md +188 -0
  72. package/templates/claude-code/agents/harness-optimizer.md +61 -0
  73. package/templates/claude-code/agents/loop-operator.md +27 -12
  74. package/templates/claude-code/agents/planner.md +21 -7
  75. package/templates/claude-code/agents/product-strategist.md +138 -0
  76. package/templates/claude-code/agents/production-readiness.md +14 -0
  77. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  78. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  79. package/templates/claude-code/agents/security-reviewer.md +15 -0
  80. package/templates/claude-code/agents/spec-validator.md +45 -1
  81. package/templates/claude-code/agents/tdd-guide.md +21 -7
  82. package/templates/claude-code/agents/uat-validator.md +18 -0
  83. package/templates/claude-code/claude-md/base.md +15 -7
  84. package/templates/claude-code/claude-md/fastapi.md +8 -8
  85. package/templates/claude-code/claude-md/fullstack.md +6 -6
  86. package/templates/claude-code/claude-md/hono.md +18 -0
  87. package/templates/claude-code/claude-md/nextjs.md +5 -5
  88. package/templates/claude-code/claude-md/remix.md +18 -0
  89. package/templates/claude-code/commands/audit-security.md +14 -0
  90. package/templates/claude-code/commands/audit-spec.md +14 -0
  91. package/templates/claude-code/commands/audit-wiring.md +14 -0
  92. package/templates/claude-code/commands/build-fix.md +28 -0
  93. package/templates/claude-code/commands/build-ui.md +59 -0
  94. package/templates/claude-code/commands/code-review.md +54 -26
  95. package/templates/claude-code/commands/fix-loop.md +211 -0
  96. package/templates/claude-code/commands/full-audit.md +37 -8
  97. package/templates/claude-code/commands/generate-prd.md +1 -1
  98. package/templates/claude-code/commands/generate-sdd.md +74 -0
  99. package/templates/claude-code/commands/generate-uat.md +107 -35
  100. package/templates/claude-code/commands/help.md +68 -0
  101. package/templates/claude-code/commands/live-uat.md +268 -0
  102. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  103. package/templates/claude-code/commands/plan.md +3 -3
  104. package/templates/claude-code/commands/pre-pr.md +57 -19
  105. package/templates/claude-code/commands/product-strategist.md +21 -0
  106. package/templates/claude-code/commands/resume-session.md +10 -10
  107. package/templates/claude-code/commands/run-uat.md +59 -2
  108. package/templates/claude-code/commands/save-session.md +10 -10
  109. package/templates/claude-code/commands/simplify.md +36 -0
  110. package/templates/claude-code/commands/tdd.md +17 -18
  111. package/templates/claude-code/commands/verify-all.md +24 -0
  112. package/templates/claude-code/commands/verify-intent.md +55 -0
  113. package/templates/claude-code/commands/workflows.md +52 -37
  114. package/templates/claude-code/hooks/polyglot.json +10 -1
  115. package/templates/claude-code/hooks/python.json +10 -1
  116. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +20 -10
  117. package/templates/claude-code/hooks/scripts/autofix-python.mjs +4 -5
  118. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +4 -4
  119. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  120. package/templates/claude-code/hooks/scripts/guard-protected-files.mjs +2 -2
  121. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  122. package/templates/claude-code/hooks/typescript.json +10 -1
  123. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  124. package/templates/claude-code/skills/git-workflow/SKILL.md +6 -6
  125. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  126. package/templates/claude-code/skills/playwright/SKILL.md +6 -5
  127. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  128. package/templates/claude-code/skills/security-web/SKILL.md +2 -1
  129. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  130. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  131. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  132. package/templates/docs-portal/fastapi/backend/app/portal/__init__.py +0 -0
  133. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  134. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  135. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  136. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  137. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  138. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  139. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  140. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  141. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  142. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  143. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  144. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  145. package/templates/frontend/nextjs/package.json.template +3 -1
  146. package/templates/frontend/react/index.html.template +12 -0
  147. package/templates/frontend/react/package.json.template +34 -0
  148. package/templates/frontend/react/src/App.tsx.template +10 -0
  149. package/templates/frontend/react/src/index.css +1 -0
  150. package/templates/frontend/react/src/main.tsx +10 -0
  151. package/templates/frontend/react/tsconfig.json +17 -0
  152. package/templates/frontend/react/vite.config.ts.template +15 -0
  153. package/templates/frontend/react/vitest.config.ts +9 -0
  154. package/templates/frontend/remix/app/root.tsx.template +31 -0
  155. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  156. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  157. package/templates/frontend/remix/app/tailwind.css +1 -0
  158. package/templates/frontend/remix/package.json.template +39 -0
  159. package/templates/frontend/remix/tsconfig.json +18 -0
  160. package/templates/frontend/remix/vite.config.ts.template +7 -0
  161. package/templates/infra/github-actions/.github/workflows/ci.yml.template +52 -0
  162. package/templates/testing/pytest/backend/tests/__init__.py +0 -0
  163. package/templates/testing/pytest/backend/tests/conftest.py.template +11 -0
  164. package/templates/testing/pytest/backend/tests/test_health.py.template +10 -0
  165. package/templates/testing/vitest/vitest.config.ts.template +18 -0
  166. package/CLAUDE.md +0 -38
  167. package/templates/claude-code/commands/done.md +0 -19
@@ -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
+ }
@@ -21,7 +21,9 @@
21
21
  "react-dom": "^19.1.0",
22
22
  "@prisma/client": "^6.6.0",
23
23
  "clsx": "^2.1.1",
24
- "tailwind-merge": "^3.2.0"
24
+ "tailwind-merge": "^3.2.0",
25
+ "react-markdown": "^9.0.3",
26
+ "remark-gfm": "^4.0.0"
25
27
  },
26
28
  "devDependencies": {
27
29
  "typescript": "^5.8.3",
@@ -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,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -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,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ globals: true,
7
+ setupFiles: [],
8
+ },
9
+ });
@@ -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,10 @@
1
+ import { json } from '@remix-run/node';
2
+
3
+ export function loader() {
4
+ return json({
5
+ status: 'ok',
6
+ timestamp: new Date().toISOString(),
7
+ uptime: process.uptime(),
8
+ service: '{{PROJECT_NAME}}',
9
+ });
10
+ }
@@ -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,7 @@
1
+ import { vitePlugin as remix } from '@remix-run/dev';
2
+ import { defineConfig } from 'vite';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+
5
+ export default defineConfig({
6
+ plugins: [remix(), tailwindcss()],
7
+ });
@@ -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}}
@@ -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
+ });