create-rigel 0.1.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/LICENSE +21 -0
- package/README.md +156 -0
- package/cli.js +122 -0
- package/package.json +47 -0
- package/templates/express/.claude/CLAUDE.md +114 -0
- package/templates/express/.claude/agents/arch-validator.md +85 -0
- package/templates/express/.claude/agents/db-optimizer.md +72 -0
- package/templates/express/.claude/agents/doc-gardener.md +67 -0
- package/templates/express/.claude/agents/garbage-collector.md +94 -0
- package/templates/express/.claude/agents/gate-checker.md +176 -0
- package/templates/express/.claude/agents/reviewer.md +84 -0
- package/templates/express/.claude/agents/security-auditor.md +82 -0
- package/templates/express/.claude/hooks/post-write.sh +93 -0
- package/templates/express/.claude/rules/api.md +149 -0
- package/templates/express/.claude/rules/architecture.md +78 -0
- package/templates/express/.claude/rules/database.md +104 -0
- package/templates/express/.claude/rules/jobs.md +81 -0
- package/templates/express/.claude/rules/security.md +92 -0
- package/templates/express/.claude/rules/testing.md +97 -0
- package/templates/express/.claude/settings.json +35 -0
- package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
- package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
- package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
- package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
- package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
- package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
- package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
- package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
- package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
- package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
- package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
- package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
- package/templates/express/.dockerignore +43 -0
- package/templates/express/.env.example +42 -0
- package/templates/express/.github/CODEOWNERS +22 -0
- package/templates/express/.github/dependabot.yml +48 -0
- package/templates/express/.github/pull_request_template.md +58 -0
- package/templates/express/.github/workflows/ci.yml +278 -0
- package/templates/express/.github/workflows/load-test.yml +41 -0
- package/templates/express/.gitleaks.toml +18 -0
- package/templates/express/.lintstagedrc.json +3 -0
- package/templates/express/.nvmrc +1 -0
- package/templates/express/.prettierrc +8 -0
- package/templates/express/.sequelizerc +8 -0
- package/templates/express/AGENTS.md +94 -0
- package/templates/express/ARCHITECTURE.md +161 -0
- package/templates/express/Dockerfile +40 -0
- package/templates/express/Makefile +79 -0
- package/templates/express/QUICKSTART.md +131 -0
- package/templates/express/docker-compose.yml +114 -0
- package/templates/express/docs/PLANS.md +111 -0
- package/templates/express/docs/QUALITY_SCORE.md +66 -0
- package/templates/express/docs/design-docs/core-beliefs.md +89 -0
- package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
- package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
- package/templates/express/docs/design-docs/decisions/index.md +23 -0
- package/templates/express/docs/design-docs/index.md +11 -0
- package/templates/express/docs/design-docs/observability.md +194 -0
- package/templates/express/docs/design-docs/team-workflow.md +64 -0
- package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
- package/templates/express/docs/generated/.gitkeep +0 -0
- package/templates/express/docs/product-specs/ROADMAP.md +49 -0
- package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/express/docs/product-specs/index.md +21 -0
- package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/express/eslint.config.mjs +94 -0
- package/templates/express/gitignore +36 -0
- package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
- package/templates/express/jest.config.ts +61 -0
- package/templates/express/package.json +45 -0
- package/templates/express/scripts/openapi.export.ts +77 -0
- package/templates/express/tests/architecture/isolation.test.ts +51 -0
- package/templates/express/tests/architecture/layers.test.ts +83 -0
- package/templates/express/tests/integration/health.test.ts +40 -0
- package/templates/express/tests/integration/isolation.test.template.ts +88 -0
- package/templates/express/tests/load/smoke.js +46 -0
- package/templates/express/tests/load/soak.js +45 -0
- package/templates/express/tests/load/stress.js +44 -0
- package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
- package/templates/express/tsconfig.build.json +8 -0
- package/templates/express/tsconfig.json +20 -0
- package/templates/fastapi/.claude/CLAUDE.md +122 -0
- package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
- package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
- package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
- package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
- package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
- package/templates/fastapi/.claude/agents/reviewer.md +74 -0
- package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
- package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
- package/templates/fastapi/.claude/rules/api.md +119 -0
- package/templates/fastapi/.claude/rules/architecture.md +84 -0
- package/templates/fastapi/.claude/rules/database.md +153 -0
- package/templates/fastapi/.claude/rules/jobs.md +85 -0
- package/templates/fastapi/.claude/rules/security.md +102 -0
- package/templates/fastapi/.claude/rules/testing.md +207 -0
- package/templates/fastapi/.claude/settings.json +50 -0
- package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
- package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
- package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
- package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
- package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
- package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
- package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
- package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
- package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
- package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
- package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
- package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
- package/templates/fastapi/.dockerignore +30 -0
- package/templates/fastapi/.github/dependabot.yml +24 -0
- package/templates/fastapi/AGENTS.md +87 -0
- package/templates/fastapi/ARCHITECTURE.md +141 -0
- package/templates/fastapi/Makefile +42 -0
- package/templates/fastapi/README.md +179 -0
- package/templates/fastapi/docs/PLANS.md +26 -0
- package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
- package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
- package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
- package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
- package/templates/fastapi/docs/design-docs/index.md +9 -0
- package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/fastapi/docs/generated/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
- package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/index.md +18 -0
- package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/fastapi/gitignore +46 -0
- package/templates/fastapi/scripts/gate.sh +68 -0
- package/templates/nestjs/.claude/CLAUDE.md +121 -0
- package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
- package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
- package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
- package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
- package/templates/nestjs/.claude/agents/reviewer.md +68 -0
- package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
- package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
- package/templates/nestjs/.claude/rules/api.md +87 -0
- package/templates/nestjs/.claude/rules/architecture.md +104 -0
- package/templates/nestjs/.claude/rules/database.md +111 -0
- package/templates/nestjs/.claude/rules/security.md +110 -0
- package/templates/nestjs/.claude/rules/testing.md +145 -0
- package/templates/nestjs/.claude/settings.json +33 -0
- package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
- package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
- package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
- package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
- package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
- package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
- package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
- package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
- package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
- package/templates/nestjs/.env.example +11 -0
- package/templates/nestjs/AGENTS.md +88 -0
- package/templates/nestjs/ARCHITECTURE.md +192 -0
- package/templates/nestjs/docs/PLANS.md +22 -0
- package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
- package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
- package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nestjs/docs/design-docs/index.md +7 -0
- package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
- package/templates/nestjs/docs/generated/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/index.md +7 -0
- package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nestjs/gitignore +8 -0
- package/templates/nextjs/.claude/CLAUDE.md +139 -0
- package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
- package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
- package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
- package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
- package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
- package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
- package/templates/nextjs/.claude/agents/reviewer.md +79 -0
- package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
- package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
- package/templates/nextjs/.claude/rules/api-contract.md +166 -0
- package/templates/nextjs/.claude/rules/architecture.md +99 -0
- package/templates/nextjs/.claude/rules/components.md +122 -0
- package/templates/nextjs/.claude/rules/security.md +122 -0
- package/templates/nextjs/.claude/rules/testing.md +187 -0
- package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
- package/templates/nextjs/.claude/settings.json +33 -0
- package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
- package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
- package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
- package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
- package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
- package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
- package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
- package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
- package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
- package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
- package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
- package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
- package/templates/nextjs/.dockerignore +25 -0
- package/templates/nextjs/.gitattributes +10 -0
- package/templates/nextjs/.github/CODEOWNERS +24 -0
- package/templates/nextjs/.github/dependabot.yml +31 -0
- package/templates/nextjs/.github/pull_request_template.md +59 -0
- package/templates/nextjs/.github/workflows/ci.yml +143 -0
- package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
- package/templates/nextjs/.github/workflows/load-test.yml +54 -0
- package/templates/nextjs/.husky/pre-commit +1 -0
- package/templates/nextjs/.lintstagedrc.json +4 -0
- package/templates/nextjs/.mcp.json +8 -0
- package/templates/nextjs/.prettierignore +13 -0
- package/templates/nextjs/.prettierrc +9 -0
- package/templates/nextjs/AGENTS.md +88 -0
- package/templates/nextjs/ARCHITECTURE.md +165 -0
- package/templates/nextjs/Dockerfile +38 -0
- package/templates/nextjs/Makefile +50 -0
- package/templates/nextjs/QUICKSTART.md +128 -0
- package/templates/nextjs/docs/PLANS.md +23 -0
- package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
- package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
- package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nextjs/docs/design-docs/index.md +7 -0
- package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
- package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/nextjs/docs/generated/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
- package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/index.md +18 -0
- package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nextjs/eslint.config.mjs +150 -0
- package/templates/nextjs/gitignore +35 -0
- package/templates/nextjs/lighthouserc.js +37 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# .claude/scripts/infra-setup.sh
|
|
3
|
+
# Deterministic POST-SCAFFOLD setup: deps + shadcn + dirs + package.json scripts +
|
|
4
|
+
# gate-critical config/test/observability files + husky.
|
|
5
|
+
# Run via: bash .claude/scripts/infra-setup.sh
|
|
6
|
+
#
|
|
7
|
+
# PRECONDITION: the Next.js app is already scaffolded (Step 1 of the skill runs
|
|
8
|
+
# `create-next-app` via the park-and-restore sequence — it cannot live here because
|
|
9
|
+
# this script's own home (.claude/) is one of the files that gets parked aside).
|
|
10
|
+
# After this finishes, Claude authors the remaining harness glue (Step 5 of the skill).
|
|
11
|
+
#
|
|
12
|
+
# No version pins for libraries — everything resolves to latest LTS. Reproducibility
|
|
13
|
+
# is provided instead by the committed package-lock.json + `npm ci` in CI + Dependabot.
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
if [[ ! -f package.json ]]; then
|
|
18
|
+
echo "❌ No package.json — run the scaffold step first (Step 1 of /infra-setup:"
|
|
19
|
+
echo " park harness files → npx create-next-app@latest . → restore). Aborting."
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# write_if_absent <path> ← content on stdin. Never clobbers an existing file, so a
|
|
24
|
+
# re-run (or a user edit) is safe. Harness-owned files are created exactly once.
|
|
25
|
+
write_if_absent() {
|
|
26
|
+
local p="$1"
|
|
27
|
+
if [[ -e "$p" ]]; then
|
|
28
|
+
echo " • skip $p (exists)"
|
|
29
|
+
cat >/dev/null
|
|
30
|
+
return
|
|
31
|
+
fi
|
|
32
|
+
mkdir -p "$(dirname "$p")"
|
|
33
|
+
cat >"$p"
|
|
34
|
+
echo " • wrote $p"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
echo "▶ Step 1 — install dependencies (latest LTS, no pins)"
|
|
38
|
+
# Server state + API contract
|
|
39
|
+
npm install @tanstack/react-query @tanstack/react-query-devtools openapi-fetch
|
|
40
|
+
npm install -D openapi-typescript
|
|
41
|
+
# Client state
|
|
42
|
+
npm install zustand
|
|
43
|
+
# Forms + validation
|
|
44
|
+
npm install react-hook-form @hookform/resolvers zod
|
|
45
|
+
# Testing
|
|
46
|
+
npm install -D vitest @vitejs/plugin-react jsdom @vitest/coverage-v8
|
|
47
|
+
npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
|
|
48
|
+
npm install -D msw @playwright/test
|
|
49
|
+
# Performance
|
|
50
|
+
npm install -D @next/bundle-analyzer
|
|
51
|
+
# Linting (the committed eslint.config.mjs uses Next's native flat config — no FlatCompat)
|
|
52
|
+
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-import
|
|
53
|
+
# Formatting + pre-commit (config files are committed in the template)
|
|
54
|
+
npm install -D prettier prettier-plugin-tailwindcss eslint-config-prettier husky lint-staged
|
|
55
|
+
# Core Web Vitals budget (config/workflows committed in the template)
|
|
56
|
+
npm install -D @lhci/cli
|
|
57
|
+
|
|
58
|
+
echo "▶ Step 2 — shadcn/ui init + base components (non-interactive)"
|
|
59
|
+
npx shadcn@latest init -d
|
|
60
|
+
npx shadcn@latest add -y button input label form card dialog sonner badge skeleton
|
|
61
|
+
|
|
62
|
+
echo "▶ Step 3 — directory structure"
|
|
63
|
+
mkdir -p \
|
|
64
|
+
src/types src/lib src/hooks src/store src/features src/components/shared src/utils \
|
|
65
|
+
tests/unit/hooks tests/unit/features tests/unit/components tests/unit/utils \
|
|
66
|
+
tests/e2e tests/e2e/helpers tests/visual tests/architecture tests/load tests/mocks tests/utils \
|
|
67
|
+
docs/product-specs/draft docs/product-specs/ready \
|
|
68
|
+
docs/exec-plans/active docs/exec-plans/completed \
|
|
69
|
+
docs/design-docs/decisions docs/generated
|
|
70
|
+
|
|
71
|
+
echo "▶ Step 4 — package.json scripts (patched via node, no clobber of dev/build/start)"
|
|
72
|
+
node -e "
|
|
73
|
+
const fs = require('fs');
|
|
74
|
+
const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
75
|
+
// Defaults fill in if create-next-app's names ever change; existing scripts win over
|
|
76
|
+
// them; harness scripts win over everything (notably: lint = 'eslint .', not 'next lint').
|
|
77
|
+
const defaults = { dev: 'next dev', build: 'next build', start: 'next start' };
|
|
78
|
+
const harness = {
|
|
79
|
+
'api:sync': 'openapi-typescript openapi.json -o src/types/api.generated.ts',
|
|
80
|
+
'test': 'vitest',
|
|
81
|
+
'test:ui': 'vitest --ui',
|
|
82
|
+
'test:coverage': 'vitest run --coverage',
|
|
83
|
+
'test:e2e': 'playwright test',
|
|
84
|
+
'test:visual': 'playwright test tests/visual/',
|
|
85
|
+
'typecheck': 'tsc --noEmit',
|
|
86
|
+
'lint': 'eslint .',
|
|
87
|
+
'lint:fix': 'eslint . --fix',
|
|
88
|
+
'format': 'prettier --write .',
|
|
89
|
+
'format:check': 'prettier --check .',
|
|
90
|
+
'gate': 'npm run typecheck && npm run lint && npm run format:check && npm run test:coverage',
|
|
91
|
+
'analyze': 'ANALYZE=true next build',
|
|
92
|
+
'prepare': 'husky',
|
|
93
|
+
};
|
|
94
|
+
p.scripts = Object.assign(defaults, p.scripts, harness);
|
|
95
|
+
fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n');
|
|
96
|
+
console.log(' scripts:', Object.keys(p.scripts).join(', '));
|
|
97
|
+
"
|
|
98
|
+
|
|
99
|
+
echo "▶ Step 5 — gate-critical config files (deterministic, never improvised)"
|
|
100
|
+
|
|
101
|
+
# next.config.ts: OVERWRITE the trivial generated one with a known-good base. `output:
|
|
102
|
+
# 'standalone'` is REQUIRED by the Dockerfile (it copies .next/standalone) — making it
|
|
103
|
+
# deterministic here means the container build can't silently break. Security headers
|
|
104
|
+
# included. reactCompiler / typedRoutes are version-sensitive, so Claude adds them later
|
|
105
|
+
# (Step 6) after checking the installed Next version against the freshness rule.
|
|
106
|
+
cat >next.config.ts <<'EOF'
|
|
107
|
+
import type { NextConfig } from 'next'
|
|
108
|
+
|
|
109
|
+
const nextConfig: NextConfig = {
|
|
110
|
+
output: 'standalone', // minimal server bundle for the Docker runner stage
|
|
111
|
+
async headers() {
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
source: '/(.*)',
|
|
115
|
+
headers: [
|
|
116
|
+
{ key: 'X-Frame-Options', value: 'DENY' },
|
|
117
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
118
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
119
|
+
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default nextConfig
|
|
127
|
+
EOF
|
|
128
|
+
echo " • wrote next.config.ts (output: standalone + security headers)"
|
|
129
|
+
|
|
130
|
+
write_if_absent vitest.config.ts <<'EOF'
|
|
131
|
+
import { defineConfig, configDefaults } from 'vitest/config'
|
|
132
|
+
import react from '@vitejs/plugin-react'
|
|
133
|
+
import path from 'path'
|
|
134
|
+
|
|
135
|
+
export default defineConfig({
|
|
136
|
+
plugins: [react()],
|
|
137
|
+
test: {
|
|
138
|
+
environment: 'jsdom',
|
|
139
|
+
globals: true,
|
|
140
|
+
setupFiles: ['./tests/setup.ts'],
|
|
141
|
+
// Vitest runs unit + architecture tests ONLY. Playwright owns tests/e2e &
|
|
142
|
+
// tests/visual; k6 owns tests/load — exclude them or Vitest tries to run them
|
|
143
|
+
// and crashes on the Playwright/k6 globals.
|
|
144
|
+
include: ['tests/**/*.{test,spec}.{ts,tsx}', 'src/**/*.{test,spec}.{ts,tsx}'],
|
|
145
|
+
exclude: [...configDefaults.exclude, 'tests/e2e/**', 'tests/visual/**', 'tests/load/**'],
|
|
146
|
+
// env.ts validates NEXT_PUBLIC_* at import — provide them so api-client imports
|
|
147
|
+
// don't throw "Invalid environment variables" in jsdom.
|
|
148
|
+
env: { NEXT_PUBLIC_API_URL: 'http://localhost:8000' },
|
|
149
|
+
coverage: {
|
|
150
|
+
provider: 'v8',
|
|
151
|
+
// Scope coverage to the layers the rules actually govern (utils/hooks/features/
|
|
152
|
+
// components). lib/store/app/instrumentation/types are infra — not gated — so
|
|
153
|
+
// they don't drag the gate below threshold for being untested.
|
|
154
|
+
include: [
|
|
155
|
+
'src/utils/**/*.{ts,tsx}',
|
|
156
|
+
'src/hooks/**/*.{ts,tsx}',
|
|
157
|
+
'src/features/**/*.{ts,tsx}',
|
|
158
|
+
'src/components/**/*.{ts,tsx}',
|
|
159
|
+
],
|
|
160
|
+
exclude: ['src/components/ui/**', 'src/components/providers.tsx', 'src/**/*.d.ts'],
|
|
161
|
+
// Per-layer thresholds — these MATCH .claude/rules/testing.md and the PR
|
|
162
|
+
// template. The flat block is the global floor; the glob blocks raise
|
|
163
|
+
// specific layers. A breach fails `vitest run --coverage` (and therefore CI).
|
|
164
|
+
thresholds: {
|
|
165
|
+
lines: 70,
|
|
166
|
+
functions: 70,
|
|
167
|
+
branches: 70,
|
|
168
|
+
statements: 70,
|
|
169
|
+
'src/utils/**': { lines: 100, functions: 100, branches: 100, statements: 100 },
|
|
170
|
+
'src/hooks/**': { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
171
|
+
'src/features/**': { lines: 70, functions: 70, branches: 70, statements: 70 },
|
|
172
|
+
'src/components/**': { lines: 70, functions: 70, branches: 70, statements: 70 },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
resolve: { alias: { '@': path.resolve(__dirname, './src') } },
|
|
177
|
+
})
|
|
178
|
+
EOF
|
|
179
|
+
|
|
180
|
+
write_if_absent playwright.config.ts <<'EOF'
|
|
181
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
182
|
+
|
|
183
|
+
export default defineConfig({
|
|
184
|
+
testDir: './tests/e2e',
|
|
185
|
+
fullyParallel: true,
|
|
186
|
+
forbidOnly: !!process.env.CI,
|
|
187
|
+
retries: process.env.CI ? 2 : 0,
|
|
188
|
+
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
|
|
189
|
+
use: {
|
|
190
|
+
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000',
|
|
191
|
+
trace: 'on-first-retry',
|
|
192
|
+
},
|
|
193
|
+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
|
194
|
+
webServer: {
|
|
195
|
+
command: 'npm run dev',
|
|
196
|
+
url: 'http://localhost:3000',
|
|
197
|
+
reuseExistingServer: !process.env.CI,
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
EOF
|
|
201
|
+
|
|
202
|
+
write_if_absent tests/setup.ts <<'EOF'
|
|
203
|
+
import '@testing-library/jest-dom/vitest'
|
|
204
|
+
import { beforeAll, afterAll, afterEach } from 'vitest'
|
|
205
|
+
import { server } from './mocks/server'
|
|
206
|
+
|
|
207
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
|
|
208
|
+
afterEach(() => server.resetHandlers())
|
|
209
|
+
afterAll(() => server.close())
|
|
210
|
+
EOF
|
|
211
|
+
|
|
212
|
+
echo "▶ Step 6 — gate-critical test artifacts (the gate references these — ship them real)"
|
|
213
|
+
|
|
214
|
+
write_if_absent tests/architecture/layers.test.ts <<'EOF'
|
|
215
|
+
import { describe, it, expect } from 'vitest'
|
|
216
|
+
import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs'
|
|
217
|
+
import { join } from 'node:path'
|
|
218
|
+
|
|
219
|
+
// Layer import rules. A layer must NOT import from any of its "forbidden" layers.
|
|
220
|
+
// Aliases are matched as `@/<layer>` and bare `app/` imports. This is the structural
|
|
221
|
+
// backstop to the ESLint no-restricted-imports rules (defence in depth: ESLint runs
|
|
222
|
+
// per-file with config that can be disabled; this test reads the raw import graph).
|
|
223
|
+
const RULES: Record<string, string[]> = {
|
|
224
|
+
'src/types': ['@/lib', '@/hooks', '@/features', '@/store', '@/components'],
|
|
225
|
+
'src/lib': ['@/hooks', '@/features', '@/components'],
|
|
226
|
+
'src/store': ['@/hooks', '@/features', '@/components'],
|
|
227
|
+
'src/hooks': ['@/features', '@/components'],
|
|
228
|
+
'src/features': ['app/'],
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function walk(dir: string): string[] {
|
|
232
|
+
if (!existsSync(dir)) return []
|
|
233
|
+
return readdirSync(dir).flatMap((name) => {
|
|
234
|
+
const full = join(dir, name)
|
|
235
|
+
if (statSync(full).isDirectory()) return walk(full)
|
|
236
|
+
return /\.(ts|tsx)$/.test(full) && !full.endsWith('.d.ts') ? [full] : []
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const IMPORT_RE = /(?:import|from)\s+['"]([^'"]+)['"]/g
|
|
241
|
+
|
|
242
|
+
function importsOf(file: string): string[] {
|
|
243
|
+
const src = readFileSync(file, 'utf8')
|
|
244
|
+
const out: string[] = []
|
|
245
|
+
for (const m of src.matchAll(IMPORT_RE)) out.push(m[1])
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
describe('layer import rules', () => {
|
|
250
|
+
for (const [layer, forbidden] of Object.entries(RULES)) {
|
|
251
|
+
it(`${layer} does not import from ${forbidden.join(', ')}`, () => {
|
|
252
|
+
const violations: string[] = []
|
|
253
|
+
for (const file of walk(layer)) {
|
|
254
|
+
for (const imp of importsOf(file)) {
|
|
255
|
+
const hit = forbidden.some((f) => {
|
|
256
|
+
const base = f.endsWith('/') ? f.slice(0, -1) : f
|
|
257
|
+
return imp === base || imp.startsWith(base + '/')
|
|
258
|
+
})
|
|
259
|
+
if (hit) violations.push(`${file} → ${imp}`)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
expect(violations, violations.join('\n')).toHaveLength(0)
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('app pages contain no business logic', () => {
|
|
268
|
+
it('page.tsx files import features, not hooks directly', () => {
|
|
269
|
+
// Supports both app-router layouts (src/app with --src-dir, or top-level app/).
|
|
270
|
+
const pages = [...walk('src/app'), ...walk('app')].filter((f) => f.endsWith('page.tsx'))
|
|
271
|
+
const violations = pages.filter((f) =>
|
|
272
|
+
importsOf(f).some((imp) => imp.startsWith('@/hooks')),
|
|
273
|
+
)
|
|
274
|
+
expect(violations, violations.join('\n')).toHaveLength(0)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
EOF
|
|
278
|
+
|
|
279
|
+
write_if_absent tests/e2e/helpers/auth.ts <<'EOF'
|
|
280
|
+
import { type Page, expect } from '@playwright/test'
|
|
281
|
+
|
|
282
|
+
// Logs a user in through the real login form and waits for the post-login redirect.
|
|
283
|
+
// Adjust selectors / destination to match your app's auth flow. Used by the
|
|
284
|
+
// REQUIRED cross-user isolation test (tests/e2e/isolation.spec.ts).
|
|
285
|
+
export async function loginAs(page: Page, email: string, password = 'password123') {
|
|
286
|
+
await page.goto('/login')
|
|
287
|
+
await page.getByLabel('Email').fill(email)
|
|
288
|
+
await page.getByLabel('Password').fill(password)
|
|
289
|
+
await page.getByRole('button', { name: /sign in/i }).click()
|
|
290
|
+
await expect(page).not.toHaveURL(/\/login$/)
|
|
291
|
+
}
|
|
292
|
+
EOF
|
|
293
|
+
|
|
294
|
+
write_if_absent tests/e2e/isolation.spec.ts <<'EOF'
|
|
295
|
+
import { test, expect } from '@playwright/test'
|
|
296
|
+
import { loginAs } from './helpers/auth'
|
|
297
|
+
|
|
298
|
+
// The single most important security test: confirm User B cannot reach User A's
|
|
299
|
+
// resource, and that the app reveals 404 (not 403 — a 403 leaks that it exists).
|
|
300
|
+
// Add one of these per user-owned resource type. The backend enforces isolation via
|
|
301
|
+
// `userId in WHERE`; this proves the frontend surfaces it correctly.
|
|
302
|
+
//
|
|
303
|
+
// SKIPPED BY DEFAULT: a fresh scaffold has no /login flow or backend, so this would
|
|
304
|
+
// fail CI on day one. Change `test.skip` → `test` (and wire helpers/auth.ts) the moment
|
|
305
|
+
// you ship your first user-owned resource. The PR template makes that a DoD item.
|
|
306
|
+
test.skip('User B cannot access User A resource (404, not 403, not the data)', async ({ page }) => {
|
|
307
|
+
await loginAs(page, 'usera@example.com')
|
|
308
|
+
await page.getByRole('button', { name: 'New Application' }).click()
|
|
309
|
+
await page.getByLabel('Company').fill('Acme Corp')
|
|
310
|
+
await page.getByRole('button', { name: 'Create' }).click()
|
|
311
|
+
await expect(page).toHaveURL(/\/applications\/[\w-]+$/)
|
|
312
|
+
const url = page.url()
|
|
313
|
+
|
|
314
|
+
await loginAs(page, 'userb@example.com')
|
|
315
|
+
const res = await page.goto(url)
|
|
316
|
+
|
|
317
|
+
expect(res?.status()).toBe(404)
|
|
318
|
+
await expect(page.getByText('Acme Corp')).toHaveCount(0)
|
|
319
|
+
await expect(page.getByText(/not found/i)).toBeVisible()
|
|
320
|
+
})
|
|
321
|
+
EOF
|
|
322
|
+
|
|
323
|
+
# k6 load scripts — pure, app-independent; driven by .github/workflows/load-test.yml.
|
|
324
|
+
# All three share the same request shape and differ only in load profile.
|
|
325
|
+
write_if_absent tests/load/smoke.js <<'EOF'
|
|
326
|
+
import http from 'k6/http'
|
|
327
|
+
import { check, sleep } from 'k6'
|
|
328
|
+
|
|
329
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
|
|
330
|
+
const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
|
|
331
|
+
|
|
332
|
+
// Smoke: minimal load — proves the deploy is up and within budget at all.
|
|
333
|
+
export const options = {
|
|
334
|
+
vus: 1,
|
|
335
|
+
duration: '30s',
|
|
336
|
+
thresholds: {
|
|
337
|
+
http_req_failed: ['rate<0.01'],
|
|
338
|
+
http_req_duration: ['p(95)<800'],
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const params = AUTH_TOKEN ? { headers: { Authorization: `Bearer ${AUTH_TOKEN}` } } : {}
|
|
343
|
+
|
|
344
|
+
export default function () {
|
|
345
|
+
const res = http.get(`${BASE_URL}/`, params)
|
|
346
|
+
check(res, { 'status is 200': (r) => r.status === 200 })
|
|
347
|
+
sleep(1)
|
|
348
|
+
}
|
|
349
|
+
EOF
|
|
350
|
+
|
|
351
|
+
write_if_absent tests/load/stress.js <<'EOF'
|
|
352
|
+
import http from 'k6/http'
|
|
353
|
+
import { check, sleep } from 'k6'
|
|
354
|
+
|
|
355
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
|
|
356
|
+
const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
|
|
357
|
+
|
|
358
|
+
// Stress: ramp to find the breaking point. Looser failure budget than smoke.
|
|
359
|
+
export const options = {
|
|
360
|
+
stages: [
|
|
361
|
+
{ duration: '1m', target: 50 },
|
|
362
|
+
{ duration: '3m', target: 50 },
|
|
363
|
+
{ duration: '1m', target: 100 },
|
|
364
|
+
{ duration: '3m', target: 100 },
|
|
365
|
+
{ duration: '1m', target: 0 },
|
|
366
|
+
],
|
|
367
|
+
thresholds: {
|
|
368
|
+
http_req_failed: ['rate<0.05'],
|
|
369
|
+
http_req_duration: ['p(95)<1500'],
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const params = AUTH_TOKEN ? { headers: { Authorization: `Bearer ${AUTH_TOKEN}` } } : {}
|
|
374
|
+
|
|
375
|
+
export default function () {
|
|
376
|
+
const res = http.get(`${BASE_URL}/`, params)
|
|
377
|
+
check(res, { 'status is 200': (r) => r.status === 200 })
|
|
378
|
+
sleep(1)
|
|
379
|
+
}
|
|
380
|
+
EOF
|
|
381
|
+
|
|
382
|
+
write_if_absent tests/load/soak.js <<'EOF'
|
|
383
|
+
import http from 'k6/http'
|
|
384
|
+
import { check, sleep } from 'k6'
|
|
385
|
+
|
|
386
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
|
|
387
|
+
const AUTH_TOKEN = __ENV.AUTH_TOKEN || ''
|
|
388
|
+
|
|
389
|
+
// Soak: sustained moderate load for an hour — surfaces memory leaks / slow drift.
|
|
390
|
+
export const options = {
|
|
391
|
+
stages: [
|
|
392
|
+
{ duration: '2m', target: 30 },
|
|
393
|
+
{ duration: '56m', target: 30 },
|
|
394
|
+
{ duration: '2m', target: 0 },
|
|
395
|
+
],
|
|
396
|
+
thresholds: {
|
|
397
|
+
http_req_failed: ['rate<0.02'],
|
|
398
|
+
http_req_duration: ['p(95)<1000'],
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const params = AUTH_TOKEN ? { headers: { Authorization: `Bearer ${AUTH_TOKEN}` } } : {}
|
|
403
|
+
|
|
404
|
+
export default function () {
|
|
405
|
+
const res = http.get(`${BASE_URL}/`, params)
|
|
406
|
+
check(res, { 'status is 200': (r) => r.status === 200 })
|
|
407
|
+
sleep(1)
|
|
408
|
+
}
|
|
409
|
+
EOF
|
|
410
|
+
|
|
411
|
+
echo "▶ Step 7 — observability + contract glue (logger, api-error, error boundary, instrumentation)"
|
|
412
|
+
|
|
413
|
+
write_if_absent src/lib/api-error.ts <<'EOF'
|
|
414
|
+
// src/lib/api-error.ts
|
|
415
|
+
// Normalises backend errors into a typed ApiError so hooks NEVER discard the backend
|
|
416
|
+
// code/message for a hard-coded string. Reads the canonical error envelope
|
|
417
|
+
// { error: { code, message, details? } } and falls back for a bare FastAPI { detail }
|
|
418
|
+
// or an opaque error. See .claude/rules/api-contract.md (Canonical Wire Contract).
|
|
419
|
+
export class ApiError extends Error {
|
|
420
|
+
constructor(
|
|
421
|
+
public readonly code: string,
|
|
422
|
+
message: string,
|
|
423
|
+
public readonly details?: unknown,
|
|
424
|
+
) {
|
|
425
|
+
super(message)
|
|
426
|
+
this.name = 'ApiError'
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function toApiError(error: unknown, fallback = 'Request failed'): ApiError {
|
|
431
|
+
const e = error as
|
|
432
|
+
| { error?: { code?: string; message?: string; details?: unknown }; detail?: unknown }
|
|
433
|
+
| undefined
|
|
434
|
+
if (e?.error?.message) return new ApiError(e.error.code ?? 'unknown', e.error.message, e.error.details)
|
|
435
|
+
if (typeof e?.detail === 'string') return new ApiError('unknown', e.detail)
|
|
436
|
+
return new ApiError('unknown', fallback)
|
|
437
|
+
}
|
|
438
|
+
EOF
|
|
439
|
+
|
|
440
|
+
write_if_absent src/lib/logger.ts <<'EOF'
|
|
441
|
+
// src/lib/logger.ts
|
|
442
|
+
// Zero-dependency structured logger for the frontend. Emits single-line JSON so logs
|
|
443
|
+
// are greppable in the browser console AND parseable when captured by a runtime/edge
|
|
444
|
+
// collector. This is the ONLY sanctioned logging path — raw console.log is flagged by
|
|
445
|
+
// .claude/hooks/post-write.sh. Swap `emit` for a transport (Sentry, OTel logs) later.
|
|
446
|
+
|
|
447
|
+
type Level = 'debug' | 'info' | 'warn' | 'error'
|
|
448
|
+
type Fields = Record<string, unknown>
|
|
449
|
+
|
|
450
|
+
const ORDER: Record<Level, number> = { debug: 10, info: 20, warn: 30, error: 40 }
|
|
451
|
+
|
|
452
|
+
// NODE_ENV is a build-time constant Next inlines — the one process.env read allowed
|
|
453
|
+
// outside env.ts (the validated NEXT_PUBLIC boundary).
|
|
454
|
+
function threshold(): number {
|
|
455
|
+
return process.env.NODE_ENV === 'production' ? ORDER.info : ORDER.debug
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function emit(level: Level, msg: string, fields?: Fields) {
|
|
459
|
+
if (ORDER[level] < threshold()) return
|
|
460
|
+
const line = JSON.stringify({ level, msg, time: new Date().toISOString(), ...fields })
|
|
461
|
+
if (level === 'error') console.error(line)
|
|
462
|
+
else if (level === 'warn') console.warn(line)
|
|
463
|
+
else console.info(line)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export const logger = {
|
|
467
|
+
debug: (msg: string, fields?: Fields) => emit('debug', msg, fields),
|
|
468
|
+
info: (msg: string, fields?: Fields) => emit('info', msg, fields),
|
|
469
|
+
warn: (msg: string, fields?: Fields) => emit('warn', msg, fields),
|
|
470
|
+
error: (msg: string, fields?: Fields) => emit('error', msg, fields),
|
|
471
|
+
}
|
|
472
|
+
EOF
|
|
473
|
+
|
|
474
|
+
write_if_absent src/instrumentation.ts <<'EOF'
|
|
475
|
+
// src/instrumentation.ts
|
|
476
|
+
// Next.js instrumentation hook — runs once when the server process boots. This is the
|
|
477
|
+
// seam for wiring OpenTelemetry / a trace exporter later; for now it emits a structured
|
|
478
|
+
// boot log so deploys are observable.
|
|
479
|
+
// https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
|
|
480
|
+
export async function register() {
|
|
481
|
+
const { logger } = await import('@/lib/logger')
|
|
482
|
+
logger.info('app.boot', { runtime: process.env.NEXT_RUNTIME ?? 'nodejs' })
|
|
483
|
+
}
|
|
484
|
+
EOF
|
|
485
|
+
|
|
486
|
+
write_if_absent src/app/global-error.tsx <<'EOF'
|
|
487
|
+
'use client' // Client: error boundaries must be client components
|
|
488
|
+
|
|
489
|
+
// src/app/global-error.tsx
|
|
490
|
+
// Catches render errors that escape the route tree and reports them through the
|
|
491
|
+
// structured logger. Replace the logger.error call with your error-tracking SDK
|
|
492
|
+
// (Sentry, etc.) when one is wired. global-error replaces the root layout, so it
|
|
493
|
+
// must render its own <html>/<body>.
|
|
494
|
+
import { useEffect } from 'react'
|
|
495
|
+
import { logger } from '@/lib/logger'
|
|
496
|
+
|
|
497
|
+
export default function GlobalError({
|
|
498
|
+
error,
|
|
499
|
+
reset,
|
|
500
|
+
}: {
|
|
501
|
+
error: Error & { digest?: string }
|
|
502
|
+
reset: () => void
|
|
503
|
+
}) {
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
logger.error('unhandled.render_error', {
|
|
506
|
+
message: error.message,
|
|
507
|
+
digest: error.digest,
|
|
508
|
+
stack: error.stack,
|
|
509
|
+
})
|
|
510
|
+
}, [error])
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<html lang="en">
|
|
514
|
+
<body>
|
|
515
|
+
<div role="alert" style={{ padding: '2rem', fontFamily: 'system-ui' }}>
|
|
516
|
+
<h1>Something went wrong</h1>
|
|
517
|
+
<p>An unexpected error occurred. The team has been notified.</p>
|
|
518
|
+
<button onClick={() => reset()}>Try again</button>
|
|
519
|
+
</div>
|
|
520
|
+
</body>
|
|
521
|
+
</html>
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
EOF
|
|
525
|
+
|
|
526
|
+
write_if_absent .env.example <<'EOF'
|
|
527
|
+
# Public base URL of the backend API (validated in src/lib/env.ts).
|
|
528
|
+
NEXT_PUBLIC_API_URL=http://localhost:8000
|
|
529
|
+
EOF
|
|
530
|
+
|
|
531
|
+
echo "▶ Step 8 — activate husky (pre-commit hook is committed at .husky/pre-commit)"
|
|
532
|
+
npm run prepare
|
|
533
|
+
|
|
534
|
+
echo "▶ Step 8b — format the scaffold so the first gate run is clean"
|
|
535
|
+
# create-next-app emits double-quote/semicolon code; the harness Prettier config differs.
|
|
536
|
+
# Format now so `npm run format:check` (in the gate/CI) passes without a manual pass.
|
|
537
|
+
npm run format >/dev/null 2>&1 || echo " ⚠ prettier format skipped (non-fatal)"
|
|
538
|
+
|
|
539
|
+
echo "▶ Step 9 — commit the lockfile for reproducible installs"
|
|
540
|
+
# `npm install` above generated package-lock.json. Committing it (with `npm ci` in CI)
|
|
541
|
+
# is what makes scaffolds reproducible despite the no-pins policy. Dependabot then
|
|
542
|
+
# proposes reviewed bumps. (Actual commit happens in the skill's Commit step.)
|
|
543
|
+
if [[ ! -f package-lock.json ]]; then
|
|
544
|
+
echo " ⚠ no package-lock.json found — expected after npm install. Check npm version."
|
|
545
|
+
fi
|
|
546
|
+
|
|
547
|
+
echo "✅ Deterministic infra complete."
|
|
548
|
+
echo " Configs, gate tests, load tests, and observability glue are written."
|
|
549
|
+
echo " Next: Claude authors the remaining glue (Step 5 of the skill) — env.ts,"
|
|
550
|
+
echo " api-client.ts, utils, store, providers, mocks, and ADR-000."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"model": "claude-opus-4-8",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"allow": [
|
|
5
|
+
"Bash(npm *)",
|
|
6
|
+
"Bash(npx *)",
|
|
7
|
+
"Bash(node *)",
|
|
8
|
+
"Bash(git *)",
|
|
9
|
+
"Bash(bash *)",
|
|
10
|
+
"Bash(sh *)",
|
|
11
|
+
"Bash(cat *)",
|
|
12
|
+
"Bash(ls *)",
|
|
13
|
+
"Bash(grep *)",
|
|
14
|
+
"Bash(find *)",
|
|
15
|
+
"Bash(wc *)",
|
|
16
|
+
"Bash(k6 *)"
|
|
17
|
+
],
|
|
18
|
+
"deny": []
|
|
19
|
+
},
|
|
20
|
+
"hooks": {
|
|
21
|
+
"PostToolUse": [
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "bash .claude/hooks/post-write.sh"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|