@next-friday/create-app 0.0.0-stage → 1.0.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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -2
  3. package/dist/main.mjs +414 -0
  4. package/package.json +84 -3
  5. package/templates/base/.agents/hooks/agent-shell-gate.sh +355 -0
  6. package/templates/base/.agents/hooks/agent-suppression-gate.sh +45 -0
  7. package/templates/base/.agents/rules/automation-owned-checks.md +5 -0
  8. package/templates/base/.agents/rules/code-comments.md +3 -0
  9. package/templates/base/.agents/rules/current-state-authority.md +17 -0
  10. package/templates/base/.agents/rules/demand-driven-configuration.md +7 -0
  11. package/templates/base/.agents/rules/dependency-security.md +5 -0
  12. package/templates/base/.agents/rules/deterministic-testing.md +15 -0
  13. package/templates/base/.agents/rules/diagnostics-and-error-handling.md +7 -0
  14. package/templates/base/.agents/rules/git-workflow.md +10 -0
  15. package/templates/base/.agents/rules/no-suppression.md +7 -0
  16. package/templates/base/.agents/rules/script-driven-changes.md +7 -0
  17. package/templates/base/.agents/rules/single-author-style.md +7 -0
  18. package/templates/base/.claude/rules/agents-guide.md +1 -0
  19. package/templates/base/.claude/rules/automation-owned-checks.md +1 -0
  20. package/templates/base/.claude/rules/code-comments.md +1 -0
  21. package/templates/base/.claude/rules/current-state-authority.md +1 -0
  22. package/templates/base/.claude/rules/demand-driven-configuration.md +1 -0
  23. package/templates/base/.claude/rules/dependency-security.md +12 -0
  24. package/templates/base/.claude/rules/diagnostics-and-error-handling.md +1 -0
  25. package/templates/base/.claude/rules/git-workflow.md +1 -0
  26. package/templates/base/.claude/rules/no-suppression.md +1 -0
  27. package/templates/base/.claude/rules/script-driven-changes.md +1 -0
  28. package/templates/base/.claude/rules/single-author-style.md +1 -0
  29. package/templates/base/.claude/settings.json +61 -0
  30. package/templates/base/.commitlintrc.json +3 -0
  31. package/templates/base/.cursor/BUGBOT.md +17 -0
  32. package/templates/base/.cursor/hooks/agent-constitution-gate.sh +32 -0
  33. package/templates/base/.cursor/hooks.json +26 -0
  34. package/templates/base/.cursor/rules/automation-owned-checks.mdc +10 -0
  35. package/templates/base/.cursor/rules/code-comments.mdc +8 -0
  36. package/templates/base/.cursor/rules/current-state-authority.mdc +22 -0
  37. package/templates/base/.cursor/rules/demand-driven-configuration.mdc +12 -0
  38. package/templates/base/.cursor/rules/dependency-security.mdc +11 -0
  39. package/templates/base/.cursor/rules/diagnostics-and-error-handling.mdc +12 -0
  40. package/templates/base/.cursor/rules/git-workflow.mdc +15 -0
  41. package/templates/base/.cursor/rules/no-suppression.mdc +12 -0
  42. package/templates/base/.cursor/rules/script-driven-changes.mdc +12 -0
  43. package/templates/base/.cursor/rules/single-author-style.mdc +12 -0
  44. package/templates/base/.editorconfig +13 -0
  45. package/templates/base/.gitattributes +1 -0
  46. package/templates/base/.github/CODEOWNERS +2 -0
  47. package/templates/base/.github/ISSUE_TEMPLATE/config.yml +1 -0
  48. package/templates/base/.github/dependabot.yml +77 -0
  49. package/templates/base/.github/file-filters.yml +19 -0
  50. package/templates/base/.github/labeler.yml +25 -0
  51. package/templates/base/.github/requirements/diff-cover.txt +213 -0
  52. package/templates/base/.github/workflows/deploy.yml +124 -0
  53. package/templates/base/.github/workflows/osv-scanner.yml +34 -0
  54. package/templates/base/.github/workflows/pr-triage.yml +29 -0
  55. package/templates/base/.github/workflows/secret-scan.yml +50 -0
  56. package/templates/base/.github/workflows/validate-pr-title.yml +48 -0
  57. package/templates/base/.github/workflows/zizmor.yml +62 -0
  58. package/templates/base/.github/zizmor.yml +4 -0
  59. package/templates/base/.gitleaks.toml +14 -0
  60. package/templates/base/.husky/commit-msg +3 -0
  61. package/templates/base/.husky/pre-commit +17 -0
  62. package/templates/base/.husky/pre-merge-commit +7 -0
  63. package/templates/base/.husky/pre-push +15 -0
  64. package/templates/base/.nvmrc +1 -0
  65. package/templates/base/SECURITY.md +15 -0
  66. package/templates/base/knip.json +3 -0
  67. package/templates/base/pnpm-workspace.yaml +18 -0
  68. package/templates/base/railway.json +9 -0
  69. package/templates/base/scripts/check-agent-gates.ts +252 -0
  70. package/templates/base/scripts/sync-agent-rules.ts +121 -0
  71. package/templates/nestjs/.agents/rules/typescript-first.md +7 -0
  72. package/templates/nestjs/.agents/rules.json +41 -0
  73. package/templates/nestjs/.claude/rules/deterministic-testing.md +20 -0
  74. package/templates/nestjs/.claude/rules/typescript-first.md +13 -0
  75. package/templates/nestjs/.cursor/rules/deterministic-testing.mdc +21 -0
  76. package/templates/nestjs/.cursor/rules/typescript-first.mdc +13 -0
  77. package/templates/nestjs/.dependency-cruiser.json +28 -0
  78. package/templates/nestjs/.dockerignore +8 -0
  79. package/templates/nestjs/.env.example +1 -0
  80. package/templates/nestjs/.github/ISSUE_TEMPLATE/bug.yml +66 -0
  81. package/templates/nestjs/.github/ISSUE_TEMPLATE/feature.yml +50 -0
  82. package/templates/nestjs/.github/pull_request_template.md +25 -0
  83. package/templates/nestjs/.github/workflows/ci.yml +181 -0
  84. package/templates/nestjs/.github/workflows/sast.yml +45 -0
  85. package/templates/nestjs/.lintstagedrc.json +10 -0
  86. package/templates/nestjs/.prettierignore +2 -0
  87. package/templates/nestjs/.prettierrc.json +1 -0
  88. package/templates/nestjs/AGENTS.md +8 -0
  89. package/templates/nestjs/CONTRIBUTING.md +57 -0
  90. package/templates/nestjs/Dockerfile +27 -0
  91. package/templates/nestjs/README.md +78 -0
  92. package/templates/nestjs/_gitignore +47 -0
  93. package/templates/nestjs/eslint.config.ts +7 -0
  94. package/templates/nestjs/package.json +83 -0
  95. package/templates/nestjs/src/app.controller.test.ts +23 -0
  96. package/templates/nestjs/src/app.controller.ts +13 -0
  97. package/templates/nestjs/src/app.module.ts +12 -0
  98. package/templates/nestjs/src/app.service.ts +10 -0
  99. package/templates/nestjs/src/health/health.controller.test.ts +17 -0
  100. package/templates/nestjs/src/health/health.controller.ts +14 -0
  101. package/templates/nestjs/src/health/health.module.ts +10 -0
  102. package/templates/nestjs/src/main.ts +25 -0
  103. package/templates/nestjs/src/port.test.ts +29 -0
  104. package/templates/nestjs/src/port.ts +20 -0
  105. package/templates/nestjs/test/app.e2e.test.ts +40 -0
  106. package/templates/nestjs/tsconfig.build.json +9 -0
  107. package/templates/nestjs/tsconfig.json +20 -0
  108. package/templates/nestjs/vitest.config.ts +13 -0
  109. package/templates/nestjs/vitest.e2e.config.ts +8 -0
  110. package/templates/nextjs/.agents/rules/typescript-first.md +7 -0
  111. package/templates/nextjs/.agents/rules.json +41 -0
  112. package/templates/nextjs/.claude/rules/deterministic-testing.md +21 -0
  113. package/templates/nextjs/.claude/rules/typescript-first.md +15 -0
  114. package/templates/nextjs/.cursor/rules/deterministic-testing.mdc +21 -0
  115. package/templates/nextjs/.cursor/rules/typescript-first.mdc +13 -0
  116. package/templates/nextjs/.dependency-cruiser.json +34 -0
  117. package/templates/nextjs/.dockerignore +10 -0
  118. package/templates/nextjs/.env.example +1 -0
  119. package/templates/nextjs/.github/ISSUE_TEMPLATE/bug.yml +66 -0
  120. package/templates/nextjs/.github/ISSUE_TEMPLATE/feature.yml +50 -0
  121. package/templates/nextjs/.github/pull_request_template.md +25 -0
  122. package/templates/nextjs/.github/workflows/ci.yml +187 -0
  123. package/templates/nextjs/.github/workflows/sast.yml +46 -0
  124. package/templates/nextjs/.lintstagedrc.json +10 -0
  125. package/templates/nextjs/.prettierignore +2 -0
  126. package/templates/nextjs/AGENTS.md +18 -0
  127. package/templates/nextjs/CONTRIBUTING.md +57 -0
  128. package/templates/nextjs/Dockerfile +23 -0
  129. package/templates/nextjs/README.md +74 -0
  130. package/templates/nextjs/_gitignore +51 -0
  131. package/templates/nextjs/app/favicon.ico +0 -0
  132. package/templates/nextjs/app/globals.css +26 -0
  133. package/templates/nextjs/app/health/route.test.ts +20 -0
  134. package/templates/nextjs/app/health/route.ts +23 -0
  135. package/templates/nextjs/app/layout.tsx +29 -0
  136. package/templates/nextjs/app/page.test.ts +11 -0
  137. package/templates/nextjs/app/page.tsx +8 -0
  138. package/templates/nextjs/eslint.config.ts +32 -0
  139. package/templates/nextjs/knip.json +4 -0
  140. package/templates/nextjs/next.config.ts +35 -0
  141. package/templates/nextjs/package.json +78 -0
  142. package/templates/nextjs/playwright.config.ts +27 -0
  143. package/templates/nextjs/postcss.config.mjs +7 -0
  144. package/templates/nextjs/prettier.config.ts +11 -0
  145. package/templates/nextjs/test/home.e2e.test.ts +11 -0
  146. package/templates/nextjs/tsconfig.json +33 -0
  147. package/templates/nextjs/vitest.config.ts +12 -0
  148. package/templates/nodejs/.agents/rules/typescript-first.md +7 -0
  149. package/templates/nodejs/.agents/rules.json +41 -0
  150. package/templates/nodejs/.claude/rules/deterministic-testing.md +20 -0
  151. package/templates/nodejs/.claude/rules/typescript-first.md +13 -0
  152. package/templates/nodejs/.cursor/rules/deterministic-testing.mdc +21 -0
  153. package/templates/nodejs/.cursor/rules/typescript-first.mdc +13 -0
  154. package/templates/nodejs/.dependency-cruiser.json +28 -0
  155. package/templates/nodejs/.dockerignore +8 -0
  156. package/templates/nodejs/.env.example +1 -0
  157. package/templates/nodejs/.github/ISSUE_TEMPLATE/bug.yml +66 -0
  158. package/templates/nodejs/.github/ISSUE_TEMPLATE/feature.yml +50 -0
  159. package/templates/nodejs/.github/pull_request_template.md +25 -0
  160. package/templates/nodejs/.github/workflows/ci.yml +179 -0
  161. package/templates/nodejs/.github/workflows/sast.yml +45 -0
  162. package/templates/nodejs/.lintstagedrc.json +10 -0
  163. package/templates/nodejs/.prettierignore +2 -0
  164. package/templates/nodejs/.prettierrc.json +1 -0
  165. package/templates/nodejs/AGENTS.md +8 -0
  166. package/templates/nodejs/CONTRIBUTING.md +56 -0
  167. package/templates/nodejs/Dockerfile +27 -0
  168. package/templates/nodejs/README.md +74 -0
  169. package/templates/nodejs/_gitignore +47 -0
  170. package/templates/nodejs/eslint.config.ts +7 -0
  171. package/templates/nodejs/package.json +66 -0
  172. package/templates/nodejs/src/main.ts +22 -0
  173. package/templates/nodejs/src/port.test.ts +30 -0
  174. package/templates/nodejs/src/port.ts +20 -0
  175. package/templates/nodejs/src/server.test.ts +60 -0
  176. package/templates/nodejs/src/server.ts +37 -0
  177. package/templates/nodejs/tsconfig.build.json +9 -0
  178. package/templates/nodejs/tsconfig.json +19 -0
  179. package/templates/nodejs/vitest.config.ts +12 -0
  180. package/templates/payload/.agents/rules/typescript-first.md +7 -0
  181. package/templates/payload/.agents/rules.json +41 -0
  182. package/templates/payload/.claude/rules/deterministic-testing.md +21 -0
  183. package/templates/payload/.claude/rules/typescript-first.md +15 -0
  184. package/templates/payload/.cursor/rules/deterministic-testing.mdc +21 -0
  185. package/templates/payload/.cursor/rules/typescript-first.mdc +13 -0
  186. package/templates/payload/.dependency-cruiser.json +34 -0
  187. package/templates/payload/.dockerignore +9 -0
  188. package/templates/payload/.editorconfig-checker.json +3 -0
  189. package/templates/payload/.env.example +4 -0
  190. package/templates/payload/.github/ISSUE_TEMPLATE/bug.yml +66 -0
  191. package/templates/payload/.github/ISSUE_TEMPLATE/feature.yml +50 -0
  192. package/templates/payload/.github/pull_request_template.md +25 -0
  193. package/templates/payload/.github/workflows/ci.yml +204 -0
  194. package/templates/payload/.github/workflows/sast.yml +46 -0
  195. package/templates/payload/.lintstagedrc.json +10 -0
  196. package/templates/payload/.prettierignore +5 -0
  197. package/templates/payload/.prettierrc.json +1 -0
  198. package/templates/payload/AGENTS.md +19 -0
  199. package/templates/payload/CONTRIBUTING.md +58 -0
  200. package/templates/payload/Dockerfile +26 -0
  201. package/templates/payload/README.md +98 -0
  202. package/templates/payload/_gitignore +52 -0
  203. package/templates/payload/eslint.config.ts +35 -0
  204. package/templates/payload/next.config.ts +43 -0
  205. package/templates/payload/package.json +86 -0
  206. package/templates/payload/playwright.config.ts +27 -0
  207. package/templates/payload/src/app/(frontend)/layout.tsx +18 -0
  208. package/templates/payload/src/app/(frontend)/page.test.ts +14 -0
  209. package/templates/payload/src/app/(frontend)/page.tsx +14 -0
  210. package/templates/payload/src/app/(frontend)/styles.css +15 -0
  211. package/templates/payload/src/app/(payload)/admin/[[...segments]]/not-found.tsx +24 -0
  212. package/templates/payload/src/app/(payload)/admin/[[...segments]]/page.tsx +24 -0
  213. package/templates/payload/src/app/(payload)/admin/importMap.ts +6 -0
  214. package/templates/payload/src/app/(payload)/api/[...slug]/route.ts +19 -0
  215. package/templates/payload/src/app/(payload)/api/graphql/route.ts +8 -0
  216. package/templates/payload/src/app/(payload)/api/graphql-playground/route.ts +7 -0
  217. package/templates/payload/src/app/(payload)/custom.scss +0 -0
  218. package/templates/payload/src/app/(payload)/layout.tsx +31 -0
  219. package/templates/payload/src/app/health/route.test.ts +20 -0
  220. package/templates/payload/src/app/health/route.ts +23 -0
  221. package/templates/payload/src/collections/media.test.ts +27 -0
  222. package/templates/payload/src/collections/media.ts +18 -0
  223. package/templates/payload/src/collections/users.test.ts +10 -0
  224. package/templates/payload/src/collections/users.ts +10 -0
  225. package/templates/payload/src/environment.test.ts +32 -0
  226. package/templates/payload/src/environment.ts +14 -0
  227. package/templates/payload/src/migrations/20261030_005055_initial.json +969 -0
  228. package/templates/payload/src/migrations/20261030_005055_initial.ts +134 -0
  229. package/templates/payload/src/migrations/index.ts +9 -0
  230. package/templates/payload/src/payload-types.ts +339 -0
  231. package/templates/payload/src/payload.config.ts +47 -0
  232. package/templates/payload/test/home.e2e.test.ts +16 -0
  233. package/templates/payload/tsconfig.json +34 -0
  234. package/templates/payload/vitest.config.ts +19 -0
@@ -0,0 +1,179 @@
1
+ name: Verify
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
6
+ push:
7
+ branches:
8
+ - main
9
+ schedule:
10
+ - cron: "0 3 * * 1"
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
17
+ cancel-in-progress: true
18
+
19
+ jobs:
20
+ changes:
21
+ name: Detect Changes
22
+ runs-on: ubuntu-latest
23
+ timeout-minutes: 5
24
+ permissions:
25
+ contents: read
26
+ pull-requests: read # Needed by paths-filter to inspect pull request changes.
27
+ outputs:
28
+ heavy: ${{ steps.filter.outputs.heavy }}
29
+ ci_policy: ${{ steps.filter.outputs.ci_policy }}
30
+ full_verification: ${{ steps.mode.outputs.full_verification }}
31
+ steps:
32
+ - name: Checkout repository
33
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
34
+ with:
35
+ persist-credentials: false
36
+ - name: Detect relevant changes
37
+ id: filter
38
+ if: github.event_name == 'pull_request'
39
+ uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
40
+ with:
41
+ predicate-quantifier: some-with-excludes
42
+ filters: .github/file-filters.yml
43
+ - name: Select verification mode
44
+ id: mode
45
+ shell: bash
46
+ env:
47
+ EVENT_NAME: ${{ github.event_name }}
48
+ DRAFT: ${{ github.event.pull_request.draft }}
49
+ HEAVY: ${{ steps.filter.outputs.heavy }}
50
+ CI_POLICY: ${{ steps.filter.outputs.ci_policy }}
51
+ run: |
52
+ full_verification=false
53
+ if [[ "$EVENT_NAME" == "push" || ( "$EVENT_NAME" == "pull_request" && "$DRAFT" != "true" && ( "$HEAVY" == "true" || "$CI_POLICY" == "true" ) ) ]]; then
54
+ full_verification=true
55
+ fi
56
+
57
+ echo "full_verification=$full_verification" >> "$GITHUB_OUTPUT"
58
+
59
+ checks:
60
+ name: Static Checks, Tests & Build
61
+ needs: changes
62
+ if: needs.changes.outputs.full_verification == 'true'
63
+ runs-on: ubuntu-latest
64
+ timeout-minutes: 20
65
+ steps:
66
+ - name: Checkout repository
67
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
68
+ with:
69
+ fetch-depth: 0
70
+ persist-credentials: false
71
+ - name: Set up Node.js and pnpm
72
+ uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0
73
+ with:
74
+ cache: true
75
+ require-lockfile: true
76
+ - name: Reject suppression directives
77
+ run: pnpm suppressions:check
78
+ - name: Check Cursor rules match .agents/rules
79
+ run: pnpm rules:check
80
+ - name: Check agent gates
81
+ run: pnpm gates:check
82
+ - name: Check lint
83
+ run: pnpm lint:check
84
+ - name: Check formatting
85
+ run: pnpm format:check
86
+ env:
87
+ GITHUB_TOKEN: ${{ github.token }} # editorconfig-checker downloads its binary from GitHub releases; the token avoids the anonymous rate limit.
88
+ - name: Check package.json ordering
89
+ run: pnpm sort-package-json:check
90
+ - name: Validate peer dependency contracts
91
+ run: pnpm peers:check
92
+ - name: Check types
93
+ run: pnpm typecheck
94
+ - name: Run Knip
95
+ run: pnpm knip
96
+ - name: Check dependency graph
97
+ run: pnpm depcruise
98
+ - name: Run tests with coverage
99
+ run: pnpm test:coverage
100
+ - name: Build
101
+ run: pnpm build
102
+ - name: Check patch coverage
103
+ if: github.event_name == 'pull_request'
104
+ env:
105
+ BASE_REF: ${{ github.base_ref }}
106
+ run: |
107
+ python3 -m venv "$RUNNER_TEMP/diff-cover"
108
+ "$RUNNER_TEMP/diff-cover/bin/pip" install --quiet --require-hashes --only-binary=:all: -r .github/requirements/diff-cover.txt
109
+ "$RUNNER_TEMP/diff-cover/bin/diff-cover" coverage/cobertura-coverage.xml --compare-branch "origin/$BASE_REF" --fail-under 95
110
+
111
+ image:
112
+ name: Container Image
113
+ needs: changes
114
+ if: needs.changes.outputs.full_verification == 'true' || github.event_name == 'schedule'
115
+ runs-on: ubuntu-latest
116
+ timeout-minutes: 15
117
+ env:
118
+ HADOLINT_VERSION: 2.15.1
119
+ HADOLINT_SHA256: c7187db94eeeeca956519a6af171adc31453941a1e777961f6e680f697c8c507
120
+ TRIVY_VERSION: 0.76.1
121
+ TRIVY_SHA256: c7cbe3137fb8a48a17574bede3dd6beb16742d1e9772929940fc90082c39d9c0
122
+ steps:
123
+ - name: Checkout repository
124
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
125
+ with:
126
+ persist-credentials: false
127
+ - name: Lint Dockerfile
128
+ run: |
129
+ curl --silent --show-error --fail --location --output hadolint \
130
+ "https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-linux-x86_64"
131
+ echo "${HADOLINT_SHA256} hadolint" | sha256sum --check --strict
132
+ chmod +x hadolint
133
+ ./hadolint Dockerfile
134
+ - name: Build image
135
+ run: docker build --tag app:ci .
136
+ - name: Smoke test image
137
+ id: smoke
138
+ run: |
139
+ docker run --detach --name app --env PORT=3000 --publish 3000:3000 app:ci
140
+ curl --silent --show-error --fail --retry 15 --retry-delay 1 --retry-all-errors --output /dev/null http://localhost:3000/health
141
+ - name: Show container logs
142
+ if: failure() && steps.smoke.outcome == 'failure'
143
+ run: docker logs app
144
+ - name: Scan image for vulnerabilities
145
+ run: |
146
+ archive="trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
147
+ curl --silent --show-error --fail --location --output "$archive" \
148
+ "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/${archive}"
149
+ echo "${TRIVY_SHA256} ${archive}" | sha256sum --check --strict
150
+ tar --extract --gzip --file "$archive" trivy
151
+ ./trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --no-progress app:ci
152
+
153
+ deploy-dev:
154
+ name: Deploy to dev
155
+ needs:
156
+ - checks
157
+ - image
158
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.DEPLOY_ENABLED == 'true'
159
+ permissions:
160
+ contents: read
161
+ actions: read # Needed by deploy.yml to find the CI run of the deployed commit.
162
+ uses: ./.github/workflows/deploy.yml
163
+ with:
164
+ environment: dev
165
+ commit_sha: ${{ github.sha }}
166
+
167
+ ci:
168
+ name: All Checks
169
+ if: always()
170
+ timeout-minutes: 5
171
+ needs:
172
+ - changes
173
+ - checks
174
+ - image
175
+ runs-on: ubuntu-latest
176
+ steps:
177
+ - name: Require all verification jobs
178
+ if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
179
+ run: exit 1
@@ -0,0 +1,45 @@
1
+ name: SAST
2
+
3
+ on:
4
+ pull_request: {}
5
+ push:
6
+ branches:
7
+ - main
8
+ paths-ignore:
9
+ - "**/*.md"
10
+ - ".agents/**"
11
+ - ".claude/**"
12
+ - ".cursor/**"
13
+ schedule:
14
+ - cron: "17 2 * * 1"
15
+
16
+ permissions: {}
17
+
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ semgrep:
24
+ name: Semgrep (OWASP)
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 15
27
+ permissions:
28
+ contents: read
29
+ container:
30
+ image: semgrep/semgrep:1.179.0@sha256:93963d9295a366f59e4850127b1550400ee7b388f04fe144e4a1f6325d96e01b
31
+ steps:
32
+ - name: Checkout repository
33
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
34
+ with:
35
+ persist-credentials: false
36
+ - name: Scan source
37
+ run: >-
38
+ semgrep scan
39
+ --config p/owasp-top-ten
40
+ --config p/typescript
41
+ --config p/nodejsscan
42
+ --severity ERROR --severity WARNING
43
+ --error --metrics=off
44
+ --timeout 60 --timeout-threshold 0
45
+ .
@@ -0,0 +1,10 @@
1
+ {
2
+ "*.{ts,tsx,js,jsx,mjs,cjs}": [
3
+ "pnpm exec eslint --fix --cache --max-warnings=0 --no-warn-ignored --no-inline-config",
4
+ "prettier --write --cache"
5
+ ],
6
+ "!(package).json": ["prettier --write --cache"],
7
+ "*.{md,yml,yaml}": ["prettier --write --cache"],
8
+ "package.json": ["sort-package-json", "prettier --write --cache"],
9
+ ".husky/{commit-msg,pre-commit,pre-merge-commit,pre-push}": ["prettier --write --cache"]
10
+ }
@@ -0,0 +1,2 @@
1
+ .husky/_
2
+ pnpm-lock.yaml
@@ -0,0 +1 @@
1
+ "@next-friday/prettier-config-friday"
@@ -0,0 +1,8 @@
1
+ # Agent guide
2
+
3
+ Node.js HTTP service on pnpm, without a framework. Source lives in `src/`.
4
+
5
+ - Follow the rules in `.agents/rules/`; `.agents/rules.json` gives each rule's scope. Claude Code and Cursor load them automatically.
6
+ - Rule files, agent hooks, and agent settings are company policy: propose a change to the user, who edits them.
7
+ - Put tests next to the code as `*.test.ts`.
8
+ - Read configuration from environment variables and fail when one is missing; document each variable in `.env.example`.
@@ -0,0 +1,56 @@
1
+ # Contributing
2
+
3
+ Use this guide to prepare changes to this repository.
4
+
5
+ ## Prerequisites
6
+
7
+ - Use the Node.js version declared by `.nvmrc` and `package.json#engines`.
8
+ - Change the Node.js version in `.nvmrc` and the `Dockerfile` together. Dependabot updates only the `Dockerfile`.
9
+ - Install pnpm once with `npm install --global pnpm`. It switches to the version declared by `package.json#packageManager` automatically.
10
+ - Use Claude Code v2.1.295 or later. It reads `AGENTS.md` directly and supports the `onFailure` field the agent hooks use.
11
+ - Install gitleaks 8.19 or later, which the `pre-commit` hook runs, and jq, which the agent hooks and the `pre-push` hook (`pnpm gates:check`) need: `brew install gitleaks jq` on macOS, or install them from each project's releases on Linux.
12
+
13
+ From the repository root:
14
+
15
+ ```sh
16
+ npm install --global pnpm
17
+ pnpm install
18
+ ```
19
+
20
+ ## Quality standards
21
+
22
+ - Fix lint, type, and test failures at their root cause.
23
+ - Put tests next to the code as `*.test.ts`.
24
+ - Treat `coverage/` as generated output.
25
+ - Do not commit `.env` files or credentials.
26
+
27
+ ## Verification
28
+
29
+ Run the narrowest check that covers the change while you work. Git hooks run these checks:
30
+
31
+ | Hook | Checks |
32
+ | :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | `pre-commit` | blocks commits on `main` except the first commit of a new repository, gitleaks on staged changes, `pnpm rules:check`, and lint-staged on staged files |
34
+ | `commit-msg` | commitlint |
35
+ | `pre-merge-commit` | blocks merge commits on `main` |
36
+ | `pre-push` | blocks pushes to `main` except the push that creates it, then `pnpm check`: the static checks and the unit tests related to your changes |
37
+
38
+ - **Scans:** CI also runs coverage, the build, the container image, and the secret, SAST, and OSV scans. Dependabot and OSV-Scanner report vulnerable dependencies.
39
+ - **Coverage:** CI requires 95% line coverage on changed lines in `src/`, except `main.ts`, which holds only wiring. Keep logic out of it, as `src/port.ts` and `src/server.ts` do.
40
+
41
+ Run `pnpm verify` for repository changes.
42
+
43
+ Agent rules live in `.agents/rules/` with their scope in `.agents/rules.json`. After you change either, run `pnpm rules:sync` to regenerate `.claude/rules/` and `.cursor/rules/`.
44
+
45
+ ## Pull requests
46
+
47
+ We use trunk-based development. `main` is the only long-lived branch, and every deploy promotes a commit from `main` (see the Deploy section in the README).
48
+
49
+ - Branch from `main` and merge back within a day or two. Do not create `develop` or release branches.
50
+ - Merge unfinished work behind a feature flag or keep it unreachable instead of holding a long-lived branch.
51
+ - Pull requests are squash-merged, so the pull request title becomes the commit on `main`.
52
+ - Write commit messages in the format that `@next-friday/commitlint-config-friday` defines. Pull request titles use `<type>(<scope>): <subject>`, which `.github/workflows/validate-pr-title.yml` checks.
53
+ - Keep each pull request focused on one cohesive change.
54
+ - Describe observable endpoint, request or response, or configuration effects.
55
+ - Report only checks that actually ran and include their results.
56
+ - Add or update tests only when they protect a distinct observable contract.
@@ -0,0 +1,27 @@
1
+ # Runs on any OCI platform (Railway, Fly.io, Cloud Run). The platform sets `PORT`.
2
+ FROM node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 AS base
3
+ WORKDIR /app
4
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
5
+ RUN npm install --global "pnpm@$(node --print 'require("./package.json").packageManager.split("+")[0].split("@")[1]')"
6
+
7
+ FROM base AS build
8
+ RUN pnpm install --frozen-lockfile --ignore-scripts
9
+ COPY tsconfig.json tsconfig.build.json ./
10
+ COPY src ./src
11
+ RUN pnpm build
12
+
13
+ FROM base AS prod-deps
14
+ RUN pnpm install --frozen-lockfile --ignore-scripts --prod
15
+
16
+ FROM node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53
17
+ ENV NODE_ENV=production
18
+ WORKDIR /app
19
+ # Apply Alpine security fixes released after the base image, and drop npm, corepack, and yarn: the app never runs them, and their bundled dependencies carry their own CVEs.
20
+ RUN apk upgrade --no-cache \
21
+ && rm -rf /usr/local/lib/node_modules /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack /usr/local/bin/yarn /usr/local/bin/yarnpkg /opt/yarn-*
22
+ COPY package.json ./
23
+ COPY --from=prod-deps /app/node_modules ./node_modules
24
+ COPY --from=build /app/dist ./dist
25
+ USER 1000
26
+ HEALTHCHECK --interval=30s --timeout=5s CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/health').then(response => process.exit(response.ok ? 0 : 1), () => process.exit(1))"]
27
+ CMD ["node", "dist/main.js"]
@@ -0,0 +1,74 @@
1
+ # next-friday-template-nodejs
2
+
3
+ Next Friday starter for Node.js HTTP services without a framework.
4
+
5
+ ## Getting started
6
+
7
+ ```sh
8
+ npm install --global pnpm
9
+ pnpm install
10
+ pnpm start:dev
11
+ ```
12
+
13
+ The server listens on the port set by `PORT` in `.env`, which the CLI created from `.env.example`.
14
+
15
+ ## After creating a project from this template
16
+
17
+ 1. Install gitleaks and jq (see [CONTRIBUTING.md](CONTRIBUTING.md)). Make the first commit on `main` and push it. The Git hooks allow only this first commit and push on `main`; work on branches after that.
18
+
19
+ ```sh
20
+ git add --all
21
+ git commit -m "chore(repo): start from template"
22
+ git push --set-upstream origin main
23
+ ```
24
+
25
+ 2. Add your team as the owner of every file in `.github/CODEOWNERS`, and replace the owner placeholder in `.github/zizmor.yml`.
26
+ 3. Replace the template's description and owner: `package.json#description`, `repository.url`, and `author`. Change `license` from `UNLICENSED` if the project is open source.
27
+ 4. In the repository settings:
28
+ - **Actions → General → Workflow permissions:** select **Read repository contents and packages permissions**. Every workflow requests the extra permissions it needs.
29
+ - **Advanced Security:** enable private vulnerability reporting, Dependabot alerts, Dependabot security updates, secret scanning, and push protection. `SECURITY.md` sends reporters to private vulnerability reporting.
30
+ - **Cursor Bugbot:** enable the repository at [Bugbot](https://cursor.com/automations/from-cursor/bugbot). Bugbot reviews pull requests against the rules linked from `.cursor/BUGBOT.md`.
31
+ - **Rules → Rulesets:** protect `main` with squash merges only, a linear history, no force pushes or deletion, and the required checks `All Checks` and `Validate pull request title`. Rulesets in a private repository need GitHub Pro, Team, or Enterprise; on GitHub Free, make the repository public or upgrade.
32
+ - Create the `needs triage` label, which the issue forms apply: `gh label create "needs triage"`.
33
+
34
+ ## Scripts
35
+
36
+ | Script | Purpose |
37
+ | :---------------- | :------------------------------------------------------------------------- |
38
+ | `pnpm start:dev` | Start in watch mode from `src/main.ts`; loads `.env` when present |
39
+ | `pnpm build` | Compile to `dist/` |
40
+ | `pnpm start` | Run `dist/main.js`; loads `.env` when present |
41
+ | `pnpm start:prod` | Run `dist/main.js`; the environment sets `PORT` |
42
+ | `pnpm test` | Run tests (`src/**/*.test.ts`) |
43
+ | `pnpm check` | Run the static checks and the tests for changed code (the `pre-push` hook) |
44
+ | `pnpm verify` | Run the static checks and all tests with coverage |
45
+
46
+ CI also runs the build, the container image scan, and the security scans.
47
+
48
+ ## Container
49
+
50
+ The `Dockerfile` builds a production image that runs as the `node` user. The platform must set `PORT`; Railway sets it automatically.
51
+
52
+ ```sh
53
+ docker build -t app .
54
+ docker run --env PORT=3000 --publish 3000:3000 app
55
+ ```
56
+
57
+ ## Deploy
58
+
59
+ `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
60
+
61
+ 1. In Railway, create a project with one service for this repository and the environments `dev`, `uat`, and `prod`. The workflow passes these names to `railway up --environment`. Create one project token per environment under **Project Settings → Tokens**.
62
+ 2. In GitHub, create the `dev`, `uat`, and `prod` environments under **Settings → Environments**, and add to each:
63
+ - the `RAILWAY_TOKEN` secret: that environment's project token;
64
+ - the `RAILWAY_SERVICE` variable: the service name;
65
+ - the `APP_URL` variable: the public `https://` URL.
66
+
67
+ Limit `prod` to the `main` branch and add required reviewers. Environment secrets and variables in a private repository need GitHub Pro, Team, or Enterprise; required reviewers in a private repository need GitHub Enterprise.
68
+
69
+ 3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
70
+ 4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
71
+
72
+ Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
73
+
74
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -0,0 +1,47 @@
1
+ node_modules
2
+ .pnpm-store
3
+
4
+ coverage
5
+ *.tsbuildinfo
6
+
7
+ .cache
8
+
9
+ *.pem
10
+
11
+ npm-debug.log*
12
+ pnpm-debug.log*
13
+ *.log
14
+
15
+ **/.env*
16
+ !.env.example
17
+
18
+ .eslintcache
19
+
20
+ .vscode/*
21
+ !.vscode/settings.json
22
+ !.vscode/extensions.json
23
+ /.idea
24
+
25
+ logs
26
+ .temp
27
+ .tmp
28
+
29
+ .DS_Store
30
+ .DS_Store?
31
+ ._*
32
+ .Spotlight-V100
33
+ .Trashes
34
+ ehthumbs.db
35
+ Thumbs.db
36
+ **/.DS_Store
37
+
38
+ *.key
39
+ *.p12
40
+ *.pfx
41
+ id_rsa*
42
+ id_ed25519*
43
+
44
+ /.claude/settings.local.json
45
+ /.claude/worktrees/
46
+
47
+ /dist
@@ -0,0 +1,7 @@
1
+ import {defineConfig, globalIgnores} from "eslint/config";
2
+ import friday from "@next-friday/eslint-config-friday";
3
+
4
+ export default defineConfig(
5
+ globalIgnores([".agents/**", ".claude/**", ".cursor/**", "coverage/**", "dist/**"]),
6
+ friday(),
7
+ );
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "next-friday-template-nodejs",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Next Friday Node.js template.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/next-friday/next-friday-template-nodejs.git"
9
+ },
10
+ "license": "UNLICENSED",
11
+ "author": "Next Friday",
12
+ "type": "module",
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.build.json",
15
+ "check": "pnpm verify:static && pnpm test:changed",
16
+ "clean": "rm -rf dist coverage *.tsbuildinfo .eslintcache node_modules/.cache",
17
+ "depcruise": "depcruise --config .dependency-cruiser.json src eslint.config.ts vitest.config.ts",
18
+ "format": "prettier --write --cache .",
19
+ "format:check": "prettier --check --cache . && editorconfig-checker",
20
+ "gates:check": "node scripts/check-agent-gates.ts",
21
+ "knip": "knip",
22
+ "lint": "eslint . --fix --cache --max-warnings=0 --no-inline-config",
23
+ "lint:check": "eslint . --cache --max-warnings=0 --no-inline-config",
24
+ "peers:check": "pnpm peers check",
25
+ "prepare": "husky",
26
+ "rules:check": "node scripts/sync-agent-rules.ts --check",
27
+ "rules:sync": "node scripts/sync-agent-rules.ts",
28
+ "sort-package-json": "sort-package-json package.json",
29
+ "sort-package-json:check": "sort-package-json --check package.json",
30
+ "start": "node --env-file-if-exists=.env dist/main.js",
31
+ "start:dev": "node --env-file-if-exists=.env --watch src/main.ts",
32
+ "start:prod": "node dist/main.js",
33
+ "suppressions:check": "bash .agents/hooks/agent-suppression-gate.sh --repo",
34
+ "test": "vitest run --passWithNoTests",
35
+ "test:changed": "if git rev-parse --quiet --verify origin/main > /dev/null && git diff --quiet \"$(git merge-base origin/main HEAD)\" -- pnpm-lock.yaml pnpm-workspace.yaml \"*package.json\" \"*tsconfig.json\" .husky; then vitest run --changed origin/main --passWithNoTests; else vitest run --passWithNoTests; fi",
36
+ "test:coverage": "vitest run --coverage --passWithNoTests",
37
+ "test:watch": "vitest",
38
+ "typecheck": "tsc --noEmit -p tsconfig.json",
39
+ "verify": "pnpm verify:static && pnpm test:coverage",
40
+ "verify:static": "pnpm suppressions:check && pnpm rules:check && pnpm gates:check && pnpm lint:check && pnpm format:check && pnpm sort-package-json:check && pnpm peers:check && pnpm typecheck && pnpm knip && pnpm depcruise"
41
+ },
42
+ "devDependencies": {
43
+ "@commitlint/cli": "21.2.4",
44
+ "@next-friday/commitlint-config-friday": "4.0.0",
45
+ "@next-friday/eslint-config-friday": "5.1.0",
46
+ "@next-friday/prettier-config-friday": "4.0.0",
47
+ "@types/node": "26.6.5",
48
+ "@vitest/coverage-v8": "5.0.5",
49
+ "dependency-cruiser": "18.7.0",
50
+ "editorconfig-checker": "7.0.0",
51
+ "eslint": "10.13.0",
52
+ "fast-check": "4.10.2",
53
+ "husky": "9.1.7",
54
+ "jiti": "2.7.0",
55
+ "knip": "6.43.0",
56
+ "lint-staged": "17.6.0",
57
+ "prettier": "3.9.10",
58
+ "sort-package-json": "4.1.0",
59
+ "typescript": "7.0.2",
60
+ "vitest": "5.0.5"
61
+ },
62
+ "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec",
63
+ "engines": {
64
+ "node": ">=24.15.0 <25"
65
+ }
66
+ }
@@ -0,0 +1,22 @@
1
+ import {createAppServer} from "./server.ts";
2
+ import {parsePort} from "./port.ts";
3
+
4
+ const shutdownTimeoutMs = 10_000;
5
+ const server = createAppServer();
6
+
7
+ server.listen(parsePort(process.env.PORT));
8
+
9
+ /**
10
+ * Stop accepting connections and let in-flight requests finish, closing any left after the timeout.
11
+ */
12
+ function shutdown(): void {
13
+ server.close();
14
+ server.closeIdleConnections();
15
+
16
+ setTimeout(() => {
17
+ server.closeAllConnections();
18
+ }, shutdownTimeoutMs).unref();
19
+ }
20
+
21
+ process.once("SIGTERM", shutdown);
22
+ process.once("SIGINT", shutdown);
@@ -0,0 +1,30 @@
1
+ import {assert, integer, property} from "fast-check";
2
+ import {describe, expect, it} from "vitest";
3
+
4
+ import {parsePort} from "./port.ts";
5
+
6
+ describe("parsePort", () => {
7
+ it("reads every port from 1 to 65535", () => {
8
+ assert(
9
+ property(
10
+ integer({
11
+ min: 1,
12
+ max: 65_535,
13
+ }),
14
+ port => {
15
+ expect(parsePort(String(port))).toBe(port);
16
+ },
17
+ ),
18
+ );
19
+ });
20
+
21
+ it.each([undefined, ""])("requires a value, rejecting %j", value => {
22
+ expect(() => parsePort(value)).toThrow("PORT environment variable is required");
23
+ });
24
+
25
+ it.each(["abc", "0", "65536", "3000.5", "-1", " 3000", "0x10"])("rejects %j", value => {
26
+ expect(() => parsePort(value)).toThrow(
27
+ `PORT must be an integer from 1 to 65535, got "${value}"`,
28
+ );
29
+ });
30
+ });
@@ -0,0 +1,20 @@
1
+ const maxPort = 65_535;
2
+
3
+ /**
4
+ * Read the TCP port the server listens on.
5
+ * @param value - The raw `PORT` environment variable.
6
+ * @returns The port as an integer from 1 to 65535.
7
+ */
8
+ export function parsePort(value: string | undefined): number {
9
+ if (value === undefined || value === "") {
10
+ throw new Error("PORT environment variable is required");
11
+ }
12
+
13
+ const port = Number(value);
14
+
15
+ if (!/^\d+$/.test(value) || port < 1 || port > maxPort) {
16
+ throw new Error(`PORT must be an integer from 1 to ${maxPort}, got "${value}"`);
17
+ }
18
+
19
+ return port;
20
+ }
@@ -0,0 +1,60 @@
1
+ import {describe, expect, it} from "vitest";
2
+ import {once} from "node:events";
3
+ import type {AddressInfo} from "node:net";
4
+
5
+ import {createAppServer} from "./server.ts";
6
+
7
+ /**
8
+ * Run a request against the server on a free port, then close it.
9
+ * @param pathname - Path to request.
10
+ * @param method - HTTP method.
11
+ * @returns The response.
12
+ */
13
+ async function request(pathname: string, method = "GET"): Promise<Response> {
14
+ const server = createAppServer();
15
+
16
+ server.listen(0, "127.0.0.1");
17
+ await once(server, "listening");
18
+
19
+ const {port} = server.address() as AddressInfo;
20
+
21
+ try {
22
+ return await fetch(`http://127.0.0.1:${port}${pathname}`, {
23
+ method,
24
+ });
25
+ } finally {
26
+ server.close();
27
+ await once(server, "close");
28
+ }
29
+ }
30
+
31
+ describe("createAppServer", () => {
32
+ it("reports ok on GET /health", async () => {
33
+ const response = await request("/health");
34
+
35
+ expect(response.status).toBe(200);
36
+ expect(response.headers.get("cache-control")).toBe("no-store");
37
+
38
+ await expect(response.json()).resolves.toStrictEqual({
39
+ status: "ok",
40
+ });
41
+ });
42
+
43
+ it("reports ok when /health has a query string", async () => {
44
+ const response = await request("/health?probe=1");
45
+
46
+ expect(response.status).toBe(200);
47
+ });
48
+
49
+ it("reports ok on HEAD /health", async () => {
50
+ const response = await request("/health", "HEAD");
51
+
52
+ expect(response.status).toBe(200);
53
+ });
54
+
55
+ it("returns 404 for other paths", async () => {
56
+ const response = await request("/missing");
57
+
58
+ expect(response.status).toBe(404);
59
+ });
60
+ });