create-win-project 1.3.0 → 2.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 (289) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +58 -160
  3. package/checks/check-compatibility.js +95 -0
  4. package/{scripts/verify-generated.mjs → checks/check-generated-project.js} +28 -4
  5. package/{scripts/validate-content.mjs → checks/check-library.js} +17 -7
  6. package/checks/check-package.js +15 -0
  7. package/checks/classify-changes.js +29 -0
  8. package/checks/run-compatibility-shard.js +21 -0
  9. package/docs/README.md +20 -0
  10. package/docs/capabilities.md +13 -0
  11. package/docs/compatibility.md +13 -0
  12. package/docs/generated-project.md +12 -0
  13. package/docs/getting-started.md +26 -0
  14. package/docs/maintainers/architecture.md +161 -0
  15. package/docs/maintainers/ci-strategy.md +46 -0
  16. package/docs/{CONTENT_MODEL.md → maintainers/content-model.md} +3 -3
  17. package/docs/maintainers/contributing.md +154 -0
  18. package/docs/{DEPENDENCY_MAINTENANCE.md → maintainers/dependencies.md} +1 -1
  19. package/docs/migration-v2.md +13 -0
  20. package/docs/production-contract.md +24 -0
  21. package/index.js +2 -429
  22. package/{playbooks → library}/INDEX.md +5 -5
  23. package/library/compatibility-impact.json +14 -0
  24. package/{playbooks/capabilities/ci/github-actions.manifest.json → library/development-tools/ci/definition.json} +1 -0
  25. package/library/development-tools/devops/makefile/commands.md +11 -0
  26. package/library/development-tools/devops/makefile/definition.json +11 -0
  27. package/library/development-tools/devops/makefile/docker.md +9 -0
  28. package/library/development-tools/devops/makefile/validation.md +5 -0
  29. package/{playbooks/devops/pr-template.manifest.json → library/development-tools/devops/pr-template/definition.json} +1 -0
  30. package/library/features/sqlalchemy-alembic.md +17 -0
  31. package/library/optional-features/concerns/zod/errors.md +5 -0
  32. package/library/optional-features/concerns/zod/testing.md +5 -0
  33. package/library/optional-features/concerns/zod/validation.md +21 -0
  34. package/library/optional-features/styling/css-modules/definition.json +12 -0
  35. package/library/optional-features/styling/css-modules/responsive.md +5 -0
  36. package/library/optional-features/styling/css-modules/theme.md +7 -0
  37. package/{playbooks/platform/mobile.manifest.json → library/platforms/mobile/definition.json} +1 -0
  38. package/{playbooks/platform/web.manifest.json → library/platforms/web/definition.json} +1 -0
  39. package/{playbooks/stack/react-native.manifest.json → library/stacks/expo/definition.json} +2 -2
  40. package/library/stacks/fastapi/architecture.md +40 -0
  41. package/library/stacks/fastapi/definition.json +39 -0
  42. package/library/stacks/fastapi/runtime.md +25 -0
  43. package/library/stacks/fastapi/security.md +26 -0
  44. package/library/stacks/fastapi/structure.md +27 -0
  45. package/library/stacks/fastapi/testing.md +23 -0
  46. package/{playbooks/stack/nextjs.manifest.json → library/stacks/nextjs/definition.json} +2 -2
  47. package/{playbooks/stack/no-frontend.manifest.json → library/stacks/no-frontend/definition.json} +1 -1
  48. package/{playbooks/stack/react-vite.manifest.json → library/stacks/react-vite/definition.json} +2 -2
  49. package/{compatibility/profiles.json → library/tested-versions.json} +46 -2
  50. package/{playbooks/universal/accessibility.manifest.json → library/universal/accessibility/definition.json} +1 -1
  51. package/library/universal/coding-rules/definition.json +13 -0
  52. package/library/universal/coding-rules/hygiene.md +9 -0
  53. package/library/universal/coding-rules/naming.md +17 -0
  54. package/{playbooks/universal/error-handling.manifest.json → library/universal/error-handling/definition.json} +1 -0
  55. package/library/universal/git-conventions/branches.md +5 -0
  56. package/library/universal/git-conventions/commits.md +7 -0
  57. package/library/universal/git-conventions/definition.json +13 -0
  58. package/library/universal/git-conventions/workflow.md +5 -0
  59. package/{playbooks/universal/observability.manifest.json → library/universal/observability/definition.json} +1 -0
  60. package/{playbooks/universal/security.manifest.json → library/universal/security/definition.json} +1 -0
  61. package/library/universal/typescript/boundaries.md +13 -0
  62. package/library/universal/typescript/definition.json +12 -0
  63. package/library/universal/typescript/errors.md +5 -0
  64. package/library/universal/typescript/patterns.md +7 -0
  65. package/package.json +14 -15
  66. package/public/logo.svg +19 -0
  67. package/src/cli/arguments.js +63 -0
  68. package/src/cli/display.js +220 -0
  69. package/src/cli/main.js +203 -0
  70. package/src/cli/navigation.js +156 -0
  71. package/src/cli/questions.js +188 -0
  72. package/src/cli/system-check.js +158 -0
  73. package/src/engine/create-project.js +10 -0
  74. package/src/engine/install-dependencies.js +13 -0
  75. package/{lib/catalog.js → src/engine/load-library.js} +8 -8
  76. package/{lib/files.js → src/engine/project-files.js} +10 -1
  77. package/{lib/playbooks.js → src/engine/project-guidance.js} +26 -3
  78. package/{lib/application-shapes.js → src/engine/project-shapes.js} +4 -4
  79. package/{lib/template.js → src/engine/render-templates.js} +11 -0
  80. package/{lib/compatibility.js → src/engine/tested-versions.js} +21 -2
  81. package/src/engine/upgrade-report.js +20 -0
  82. package/src/engine/write-files.js +34 -0
  83. package/{lib/stacks/registry.js → src/stacks/available-stacks.js} +25 -1
  84. package/src/stacks/backends/README.md +5 -0
  85. package/src/stacks/backends/fastapi/ci.js +3 -0
  86. package/src/stacks/backends/fastapi/create-files.js +874 -0
  87. package/src/stacks/backends/fastapi/docker.js +75 -0
  88. package/src/stacks/backends/fastapi/environment.js +3 -0
  89. package/src/stacks/backends/fastapi/index.js +32 -0
  90. package/{lib/stacks → src/stacks/backends}/laravel/architecture.js +1 -0
  91. package/{lib/stacks → src/stacks/backends}/laravel/auth/oidc.js +1 -0
  92. package/{lib/stacks → src/stacks/backends}/laravel/auth/public.js +1 -0
  93. package/{lib/stacks → src/stacks/backends}/laravel/auth/sanctum.js +1 -0
  94. package/{lib/stacks → src/stacks/backends}/laravel/auth/session.js +1 -0
  95. package/src/stacks/backends/laravel/ci.js +3 -0
  96. package/{lib/stacks → src/stacks/backends}/laravel/composer.js +1 -1
  97. package/{lib/stacks/laravel/generate.js → src/stacks/backends/laravel/create-files.js} +1 -0
  98. package/src/stacks/backends/laravel/docker.js +14 -0
  99. package/src/stacks/backends/laravel/environment.js +5 -0
  100. package/{lib/stacks → src/stacks/backends}/laravel/index.js +11 -3
  101. package/{lib/stacks → src/stacks/backends}/laravel/ui/blade.js +1 -0
  102. package/{lib/stacks → src/stacks/backends}/laravel/ui/index.js +1 -0
  103. package/{lib/stacks → src/stacks/backends}/laravel/ui/inertia-react.js +4 -2
  104. package/{lib/stacks → src/stacks/backends}/laravel/ui/livewire.js +1 -0
  105. package/{lib/stacks → src/stacks/backends}/laravel/ui/shared.js +1 -0
  106. package/src/stacks/backends/none/index.js +23 -0
  107. package/src/stacks/backends/postgres/ci.js +3 -0
  108. package/src/stacks/backends/postgres/create-files.js +21 -0
  109. package/src/stacks/backends/postgres/docker.js +3 -0
  110. package/src/stacks/backends/postgres/environment.js +3 -0
  111. package/src/stacks/backends/postgres/index.js +30 -0
  112. package/src/stacks/backends/springboot/ci.js +3 -0
  113. package/src/stacks/backends/springboot/create-files.js +91 -0
  114. package/src/stacks/backends/springboot/docker.js +3 -0
  115. package/src/stacks/backends/springboot/environment.js +3 -0
  116. package/src/stacks/backends/springboot/index.js +31 -0
  117. package/src/stacks/backends/supabase/ci.js +3 -0
  118. package/src/stacks/backends/supabase/create-files.js +52 -0
  119. package/src/stacks/backends/supabase/docker.js +3 -0
  120. package/src/stacks/backends/supabase/environment.js +3 -0
  121. package/src/stacks/backends/supabase/index.js +31 -0
  122. package/src/stacks/backends/supabase/native.js +14 -0
  123. package/src/stacks/compose-files.js +259 -0
  124. package/{lib/generator.js → src/stacks/create-project.js} +206 -57
  125. package/src/stacks/frontends/README.md +4 -0
  126. package/src/stacks/frontends/nextjs/ci.js +3 -0
  127. package/src/stacks/frontends/nextjs/create-files.js +46 -0
  128. package/src/stacks/frontends/nextjs/dependencies.js +5 -0
  129. package/src/stacks/frontends/nextjs/docker.js +3 -0
  130. package/src/stacks/frontends/nextjs/environment.js +7 -0
  131. package/src/stacks/frontends/nextjs/index.js +35 -0
  132. package/src/stacks/frontends/react-native/ci.js +3 -0
  133. package/src/stacks/frontends/react-native/create-files.js +28 -0
  134. package/src/stacks/frontends/react-native/dependencies.js +5 -0
  135. package/src/stacks/frontends/react-native/docker.js +5 -0
  136. package/src/stacks/frontends/react-native/environment.js +7 -0
  137. package/src/stacks/frontends/react-native/index.js +30 -0
  138. package/src/stacks/frontends/react-vite/ci.js +3 -0
  139. package/src/stacks/frontends/react-vite/create-files.js +27 -0
  140. package/src/stacks/frontends/react-vite/dependencies.js +5 -0
  141. package/src/stacks/frontends/react-vite/docker.js +3 -0
  142. package/src/stacks/frontends/react-vite/environment.js +8 -0
  143. package/src/stacks/frontends/react-vite/index.js +32 -0
  144. package/src/stacks/shared/capability-packs.js +31 -0
  145. package/{lib → src}/stacks/shared/contributions.js +1 -1
  146. package/src/stacks/shared/environment.js +38 -0
  147. package/src/stacks/shared/javascript-package.js +117 -0
  148. package/src/stacks/shared/testing-files.js +16 -0
  149. package/templates/ci/fastapi.yml +62 -0
  150. package/templates/docker/compose/postgres.yml +1 -1
  151. package/templates/docker/compose/springboot.yml +3 -3
  152. package/templates/docker/compose/supabase.yml +1 -1
  153. package/templates/docker/compose-prod/fastapi.yml +52 -0
  154. package/templates/docker/compose-prod/springboot.yml +20 -2
  155. package/templates/docker/dockerfile/fastapi.dev.dockerfile +9 -0
  156. package/templates/docker/dockerfile/fastapi.prod.dockerfile +11 -0
  157. package/templates/docker/dockerfile/nextjs.prod.dockerfile +1 -0
  158. package/templates/docker/dockerfile/springboot.prod.dockerfile +4 -1
  159. package/templates/docker/dockerfile/vite.prod.dockerfile +2 -1
  160. package/templates/makefile/fastapi.mk +97 -0
  161. package/docs/ARCHITECTURE.md +0 -118
  162. package/lib/banner.js +0 -45
  163. package/lib/constants.js +0 -3
  164. package/lib/doctor.js +0 -45
  165. package/lib/interview.js +0 -77
  166. package/lib/laravel-scaffold.js +0 -3
  167. package/lib/scaffold.js +0 -467
  168. package/lib/stacks/index.js +0 -8
  169. package/playbooks/concerns/zod.md +0 -174
  170. package/playbooks/devops/makefile.manifest.json +0 -10
  171. package/playbooks/devops/makefile.md +0 -556
  172. package/playbooks/styling/css-modules-extensions.md +0 -267
  173. package/playbooks/styling/css-modules.manifest.json +0 -11
  174. package/playbooks/universal/coding-rules.manifest.json +0 -12
  175. package/playbooks/universal/coding-rules.md +0 -281
  176. package/playbooks/universal/git-conventions.manifest.json +0 -10
  177. package/playbooks/universal/git-conventions.md +0 -186
  178. package/playbooks/universal/typescript.manifest.json +0 -11
  179. package/playbooks/universal/typescript.md +0 -272
  180. package/scripts/compatibility-matrix.mjs +0 -48
  181. /package/{playbooks/capabilities → library/development-tools}/ci/github-actions.md +0 -0
  182. /package/{playbooks/devops → library/development-tools/devops/pr-template}/pr-template.md +0 -0
  183. /package/{playbooks/capabilities/docker/docker.manifest.json → library/development-tools/docker/definition.json} +0 -0
  184. /package/{playbooks/capabilities → library/development-tools}/docker/overview.md +0 -0
  185. /package/{playbooks/capabilities → library/features}/auth/oidc-resource-server.md +0 -0
  186. /package/{playbooks/capabilities → library/features}/auth/spring-session.md +0 -0
  187. /package/{playbooks/capabilities/flyway/flyway.manifest.json → library/features/flyway/definition.json} +0 -0
  188. /package/{playbooks/capabilities → library/features}/flyway/environments.md +0 -0
  189. /package/{playbooks/capabilities → library/features}/flyway/migrations.md +0 -0
  190. /package/{playbooks/capabilities → library/features}/flyway/testing.md +0 -0
  191. /package/{playbooks/capabilities → library/features}/laravel/database.md +0 -0
  192. /package/{playbooks/capabilities → library/features}/laravel/migrations.md +0 -0
  193. /package/{playbooks/capabilities → library/features}/laravel/observability.md +0 -0
  194. /package/{playbooks/capabilities → library/features}/laravel/oidc-resource-server.md +0 -0
  195. /package/{playbooks/capabilities → library/features}/laravel/queues.md +0 -0
  196. /package/{playbooks/capabilities → library/features}/laravel/sanctum-spa.md +0 -0
  197. /package/{playbooks/capabilities → library/features}/laravel/scheduler.md +0 -0
  198. /package/{playbooks/capabilities → library/features}/laravel/session-auth.md +0 -0
  199. /package/{playbooks/capabilities → library/features}/laravel/storage-uploads.md +0 -0
  200. /package/{playbooks/capabilities → library/features}/postgresql/architecture.md +0 -0
  201. /package/{playbooks/capabilities/postgresql/postgresql.manifest.json → library/features/postgresql/definition.json} +0 -0
  202. /package/{playbooks/capabilities → library/features}/postgresql/migrations.md +0 -0
  203. /package/{playbooks/capabilities → library/features}/postgresql/schema-design.md +0 -0
  204. /package/{playbooks/capabilities → library/features}/postgresql/security.md +0 -0
  205. /package/{playbooks/capabilities → library/features}/postgresql/testing.md +0 -0
  206. /package/{playbooks/capabilities → library/features}/prisma/architecture.md +0 -0
  207. /package/{playbooks/capabilities/prisma/prisma.manifest.json → library/features/prisma/definition.json} +0 -0
  208. /package/{playbooks/capabilities → library/features}/prisma/migrations.md +0 -0
  209. /package/{playbooks/capabilities → library/features}/prisma/runtime.md +0 -0
  210. /package/{playbooks/capabilities → library/features}/prisma/schema.md +0 -0
  211. /package/{playbooks/capabilities → library/features}/prisma/testing.md +0 -0
  212. /package/{playbooks/capabilities → library/features}/supabase/architecture.md +0 -0
  213. /package/{playbooks/capabilities → library/features}/supabase/authentication.md +0 -0
  214. /package/{playbooks/capabilities/supabase/supabase.manifest.json → library/features/supabase/definition.json} +0 -0
  215. /package/{playbooks/capabilities → library/features}/supabase/expo.md +0 -0
  216. /package/{playbooks/capabilities → library/features}/supabase/migrations.md +0 -0
  217. /package/{playbooks/capabilities → library/features}/supabase/nextjs.md +0 -0
  218. /package/{playbooks/capabilities → library/features}/supabase/rls.md +0 -0
  219. /package/{playbooks/capabilities → library/features}/supabase/testing.md +0 -0
  220. /package/{playbooks/capabilities → library/features}/supabase/vite.md +0 -0
  221. /package/{playbooks → library/optional-features}/concerns/axios.md +0 -0
  222. /package/{playbooks → library/optional-features}/concerns/next-safe-action.md +0 -0
  223. /package/{playbooks → library/optional-features}/concerns/next-themes.md +0 -0
  224. /package/{playbooks → library/optional-features}/concerns/nuqs.md +0 -0
  225. /package/{playbooks → library/optional-features}/concerns/t3-env.md +0 -0
  226. /package/{playbooks → library/optional-features}/concerns/tanstack-query.md +0 -0
  227. /package/{playbooks → library/optional-features}/concerns/zustand.md +0 -0
  228. /package/{playbooks/styling/native-styles.manifest.json → library/optional-features/styling/native-styles/definition.json} +0 -0
  229. /package/{playbooks/styling → library/optional-features/styling/native-styles}/native-styles.md +0 -0
  230. /package/{playbooks/styling/tailwind.manifest.json → library/optional-features/styling/tailwind/definition.json} +0 -0
  231. /package/{playbooks/styling → library/optional-features/styling/tailwind}/tailwind-extensions.md +0 -0
  232. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/architecture.md +0 -0
  233. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/runtime.md +0 -0
  234. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/security.md +0 -0
  235. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/structure.md +0 -0
  236. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/testing.md +0 -0
  237. /package/{playbooks/stack/laravel-ui.manifest.json → library/platforms/laravel-ui/definition.json} +0 -0
  238. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/architecture.md +0 -0
  239. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/runtime.md +0 -0
  240. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/security.md +0 -0
  241. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/structure.md +0 -0
  242. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/testing.md +0 -0
  243. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/architecture.md +0 -0
  244. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/runtime.md +0 -0
  245. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/security.md +0 -0
  246. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/structure.md +0 -0
  247. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/testing.md +0 -0
  248. /package/{playbooks/platform → library/platforms/mobile}/mobile.md +0 -0
  249. /package/{playbooks/platform → library/platforms/web}/web.md +0 -0
  250. /package/{playbooks/stack → library/stacks}/expo/architecture.md +0 -0
  251. /package/{playbooks/stack → library/stacks}/expo/runtime.md +0 -0
  252. /package/{playbooks/stack → library/stacks}/expo/security.md +0 -0
  253. /package/{playbooks/stack → library/stacks}/expo/structure.md +0 -0
  254. /package/{playbooks/stack → library/stacks}/expo/testing.md +0 -0
  255. /package/{playbooks/stack → library/stacks}/laravel/architecture.md +0 -0
  256. /package/{playbooks/stack/laravel.manifest.json → library/stacks/laravel/definition.json} +0 -0
  257. /package/{playbooks/stack → library/stacks}/laravel/runtime.md +0 -0
  258. /package/{playbooks/stack → library/stacks}/laravel/security.md +0 -0
  259. /package/{playbooks/stack → library/stacks}/laravel/structure.md +0 -0
  260. /package/{playbooks/stack → library/stacks}/laravel/testing.md +0 -0
  261. /package/{playbooks/stack → library/stacks}/nextjs/architecture.md +0 -0
  262. /package/{playbooks/stack → library/stacks}/nextjs/runtime.md +0 -0
  263. /package/{playbooks/stack → library/stacks}/nextjs/security.md +0 -0
  264. /package/{playbooks/stack → library/stacks}/nextjs/structure.md +0 -0
  265. /package/{playbooks/stack → library/stacks}/nextjs/testing.md +0 -0
  266. /package/{playbooks/stack/none.manifest.json → library/stacks/none/definition.json} +0 -0
  267. /package/{playbooks/stack → library/stacks}/react-vite/architecture.md +0 -0
  268. /package/{playbooks/stack → library/stacks}/react-vite/runtime.md +0 -0
  269. /package/{playbooks/stack → library/stacks}/react-vite/security.md +0 -0
  270. /package/{playbooks/stack → library/stacks}/react-vite/structure.md +0 -0
  271. /package/{playbooks/stack → library/stacks}/react-vite/testing.md +0 -0
  272. /package/{playbooks/stack → library/stacks}/springboot/architecture.md +0 -0
  273. /package/{playbooks/stack/springboot.manifest.json → library/stacks/springboot/definition.json} +0 -0
  274. /package/{playbooks/stack → library/stacks}/springboot/runtime.md +0 -0
  275. /package/{playbooks/stack → library/stacks}/springboot/security.md +0 -0
  276. /package/{playbooks/stack → library/stacks}/springboot/structure.md +0 -0
  277. /package/{playbooks/stack → library/stacks}/springboot/testing.md +0 -0
  278. /package/{playbooks/universal → library/universal/accessibility}/accessibility.md +0 -0
  279. /package/{playbooks/universal → library/universal/error-handling}/error-handling.md +0 -0
  280. /package/{playbooks/universal → library/universal/observability}/observability.md +0 -0
  281. /package/{playbooks/universal → library/universal/security}/security.md +0 -0
  282. /package/{lib → src/engine}/project-location.js +0 -0
  283. /package/{lib → src}/stacks/context.js +0 -0
  284. /package/{lib/stacks/contract.js → src/stacks/rules.js} +0 -0
  285. /package/{ci → templates/ci}/expo.yml +0 -0
  286. /package/{ci → templates/ci}/laravel.yml +0 -0
  287. /package/{ci → templates/ci}/nextjs.yml +0 -0
  288. /package/{ci → templates/ci}/springboot.yml +0 -0
  289. /package/{ci → templates/ci}/vite.yml +0 -0
@@ -0,0 +1,5 @@
1
+ // Expo runs on the host with a simulator or physical device. The mobile
2
+ // frontend intentionally contributes no container image.
3
+ export function dockerContributions() {
4
+ return []
5
+ }
@@ -0,0 +1,7 @@
1
+ import { partitionEnvironment, renderEnvironment } from '../../shared/environment.js'
2
+
3
+ export function buildEnvironmentFiles(answers, stack) {
4
+ const { publicNames, serverNames } = partitionEnvironment(stack)
5
+ const names = ['springboot', 'fastapi'].includes(stack.backendKey) ? [...publicNames, ...serverNames] : publicNames
6
+ return { '.env.example': renderEnvironment(names, answers) }
7
+ }
@@ -0,0 +1,30 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { ciContributions } from './ci.js'
3
+ import { dockerContributions } from './docker.js'
4
+ import { buildReactNativeFiles } from './create-files.js'
5
+
6
+ export const reactNativeAdapter = defineStackAdapter({
7
+ id: 'react-native',
8
+ kind: 'frontend',
9
+ label: 'React Native (Expo)',
10
+ compatibleWith: { backend: ['none', 'supabase', 'springboot', 'laravel', 'fastapi'] },
11
+ capabilities: {
12
+ applicationShapes: ['mobile'],
13
+ architectureProfiles: ['small', 'medium', 'large'],
14
+ authenticationModels: ['public', 'undecided', 'supabase', 'oidc', 'laravel-oidc'],
15
+ runtime: 'node',
16
+ },
17
+ contributes: {
18
+ files: ({ answers, stack, shared }) => Object.entries(buildReactNativeFiles(answers, stack, shared)),
19
+ environment: ({ backend }) => backend.id === 'none' ? [] : ['API_URL'],
20
+ install: () => [{ cwd: '.', command: 'npm', args: ['install'] }],
21
+ docker: dockerContributions,
22
+ ci: ciContributions,
23
+ verification: () => [
24
+ { backend: 'none', architecture: 'small', authentication: 'public' },
25
+ { backend: 'supabase', architecture: 'medium', authentication: 'supabase' },
26
+ { backend: 'springboot', architecture: 'large', authentication: 'oidc' },
27
+ { backend: 'laravel', architecture: 'medium', authentication: 'laravel-oidc' },
28
+ ],
29
+ },
30
+ })
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return [{ template: 'vite', path: '.github/workflows/ci-frontend.yml' }]
3
+ }
@@ -0,0 +1,27 @@
1
+ import { html, json } from '../../shared/javascript-package.js'
2
+ import { addTestingFiles } from '../../shared/testing-files.js'
3
+ import { packageFile } from './dependencies.js'
4
+
5
+ export function buildReactViteFiles(answers, stack, shared) {
6
+ const root = 'frontend'
7
+ const files = {}
8
+ files[`${root}/package.json`] = packageFile(answers, stack)
9
+ files[`${root}/.node-version`] = `${stack.profile.runtimes.node}\n`
10
+ files[`${root}/.npmrc`] = 'engine-strict=true\n'
11
+ files[`${root}/tsconfig.json`] = json({ compilerOptions: { target: 'ES2022', useDefineForClassFields: true, lib: ['ES2022', 'DOM', 'DOM.Iterable'], allowJs: false, skipLibCheck: true, esModuleInterop: true, allowSyntheticDefaultImports: true, strict: true, forceConsistentCasingInFileNames: true, module: 'ESNext', moduleResolution: 'Bundler', resolveJsonModule: true, isolatedModules: true, noEmit: true, jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } }, include: ['src', 'vite.config.ts', 'vitest.config.ts'] })
12
+ files[`${root}/index.html`] = `<!doctype html>\n<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="description" content="${html(answers.projectDescription)}" /><title>${html(answers.projectName)}</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>\n`
13
+ files[`${root}/vite.config.ts`] = `import { fileURLToPath, URL } from 'node:url'\nimport { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n${stack.styleId === 'tailwind' ? "import tailwindcss from '@tailwindcss/vite'\n" : ''}\nexport default defineConfig({\n plugins: [react()${stack.styleId === 'tailwind' ? ', tailwindcss()' : ''}],\n resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },\n})\n`
14
+ files[`${root}/eslint.config.js`] = `import js from '@eslint/js'\nimport tseslint from 'typescript-eslint'\n\nexport default tseslint.config(\n { ignores: ['dist', 'coverage', 'playwright-report'] },\n js.configs.recommended,\n ...tseslint.configs.recommended,\n { files: ['**/*.{ts,tsx}'], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },\n)\n`
15
+ files[`${root}/src/vite-env.d.ts`] = "/// <reference types=\"vite/client\" />\n"
16
+ files[`${root}/src/main.tsx`] = `import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { App } from './App'\nimport './styles.css'\n\ncreateRoot(document.getElementById('root')!).render(<StrictMode><App /></StrictMode>)\n`
17
+ files[`${root}/src/App.tsx`] = stack.architecture === 'small'
18
+ ? `export function App() {\n return <main><p>create-win-project</p><h1>Your starter is running</h1><p>{${JSON.stringify(answers.projectDescription)}}</p><p>Read <code>AGENTS.md</code> before your first agent-assisted change.</p></main>\n}\n`
19
+ : `${stack.architecture === 'large'
20
+ ? "import { getStarterStatus, StarterStatus } from '@/features/status'"
21
+ : "import { StarterStatus } from '@/features/status/components/StarterStatus'\nimport { getStarterStatus } from '@/features/status/services/getStarterStatus'"}\n\nexport function App() {\n const status = getStarterStatus()\n return <main><p>create-win-project</p><StarterStatus status={status} /><p>{${JSON.stringify(answers.projectDescription)}}</p><p>Read <code>AGENTS.md</code> before your first agent-assisted change.</p></main>\n}\n`
22
+ files[`${root}/src/styles.css`] = `${stack.styleId === 'tailwind' ? '@import "tailwindcss";\n' : ''}:root { color-scheme: light dark; font-family: system-ui, sans-serif; }\n* { box-sizing: border-box; }\nbody { margin: 0; }\nmain { max-width: 48rem; margin: 0 auto; padding: 4rem 1.5rem; }\n`
23
+ if ((answers.testing || 'basic') !== 'none') files[`${root}/src/App.test.tsx`] = `import { expect, test } from 'vitest'\nimport { render, screen } from '@testing-library/react'\nimport { App } from './App'\n\ntest('renders the starter heading', () => {\n render(<App />)\n expect(screen.getByRole('heading', { name: 'Your starter is running' })).toBeInTheDocument()\n})\n`
24
+ addTestingFiles(files, root, stack, answers.testing || 'basic')
25
+ Object.assign(files, shared.statusFeatureFiles(root, stack))
26
+ return files
27
+ }
@@ -0,0 +1,5 @@
1
+ import { buildJavaScriptPackage } from '../../shared/javascript-package.js'
2
+
3
+ export function packageFile(answers, stack) {
4
+ return buildJavaScriptPackage(answers, stack, 'react')
5
+ }
@@ -0,0 +1,3 @@
1
+ export function dockerContributions() {
2
+ return [{ template: 'vite', root: 'frontend', developmentPath: 'Dockerfile.dev', productionPath: 'Dockerfile' }]
3
+ }
@@ -0,0 +1,8 @@
1
+ import { partitionEnvironment, renderEnvironment } from '../../shared/environment.js'
2
+
3
+ export function buildEnvironmentFiles(answers, stack) {
4
+ const { publicNames, serverNames } = partitionEnvironment(stack)
5
+ const files = { 'frontend/.env.example': renderEnvironment(publicNames, answers) }
6
+ if (['springboot', 'fastapi'].includes(stack.backendKey)) files['.env.example'] = renderEnvironment(serverNames, answers)
7
+ return files
8
+ }
@@ -0,0 +1,32 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { ciContributions } from './ci.js'
3
+ import { dockerContributions } from './docker.js'
4
+ import { buildReactViteFiles } from './create-files.js'
5
+
6
+ const verificationCases = Object.freeze([
7
+ Object.freeze({ backend: 'none', styling: 'css-modules', architecture: 'small', authentication: 'public' }),
8
+ Object.freeze({ backend: 'supabase', styling: 'tailwind', architecture: 'medium', authentication: 'supabase' }),
9
+ Object.freeze({ backend: 'springboot', styling: 'css-modules', architecture: 'large', authentication: 'session' }),
10
+ Object.freeze({ backend: 'laravel', styling: 'tailwind', architecture: 'medium', authentication: 'sanctum-spa' }),
11
+ ])
12
+
13
+ export const reactViteAdapter = defineStackAdapter({
14
+ id: 'react',
15
+ kind: 'frontend',
16
+ label: 'React + Vite',
17
+ compatibleWith: { backend: ['none', 'supabase', 'springboot', 'laravel', 'fastapi'] },
18
+ capabilities: {
19
+ applicationShapes: ['separate', 'frontend'],
20
+ architectureProfiles: ['small', 'medium', 'large'],
21
+ authenticationModels: ['public', 'undecided', 'supabase', 'session', 'oidc', 'sanctum-spa', 'laravel-oidc'],
22
+ runtime: 'node',
23
+ },
24
+ contributes: {
25
+ files: ({ answers, stack, shared }) => Object.entries(buildReactViteFiles(answers, stack, shared)),
26
+ environment: ({ backend }) => backend.id === 'none' ? [] : ['API_URL'],
27
+ install: () => [{ cwd: 'frontend', command: 'npm', args: ['install'] }],
28
+ docker: dockerContributions,
29
+ ci: ciContributions,
30
+ verification: () => verificationCases,
31
+ },
32
+ })
@@ -0,0 +1,31 @@
1
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`
2
+
3
+ export function capabilityPackFiles(answers, stack) {
4
+ const files = {}
5
+ if (answers.uploads === 'object-storage') {
6
+ files['config/capabilities/uploads.json'] = json({
7
+ visibility: 'private', authorizeEveryOperation: true, maximumBytes: 10_485_760,
8
+ acceptedTypes: [], generatedObjectNames: true, verifySignature: true,
9
+ quarantineBeforeUse: true, malwareScanRequired: true, abandonedUploadCleanupHours: 24,
10
+ })
11
+ files['docs/capabilities/uploads.md'] = '# Private uploads\n\nAuthorize initiation, completion, download, and deletion. Validate declared type and file signature, generate object names, enforce size limits, quarantine until scanning succeeds, and remove abandoned or rejected objects. Tests must cover unauthorized access, oversize and signature rejection, quarantine, scan failure, and cleanup.\n'
12
+ }
13
+ if (answers.backgroundJobs === 'queue') {
14
+ files['config/capabilities/queue.json'] = json({
15
+ adapter: stack.backendKey === 'laravel' ? 'laravel-queue' : 'spring-task',
16
+ attempts: 5, timeoutSeconds: 30, exponentialBackoff: true,
17
+ idempotencyRequired: true, failedJobStore: true, correlationIds: true,
18
+ })
19
+ files['docs/capabilities/queue.md'] = '# Durable background jobs\n\nJobs require stable idempotency keys, bounded exponential retry with jitter, explicit timeouts, a failed-job store, correlation IDs, metrics, and an operator replay procedure. Tests cover duplicate delivery, timeout, retry exhaustion, and safe replay.\n'
20
+ }
21
+ if (answers.offline && answers.offline !== 'none') {
22
+ files['config/capabilities/offline.json'] = json({
23
+ mode: answers.offline, owner: 'mobile-client', containsSensitiveData: false,
24
+ keyNamespace: `${answers.projectName}:v1`, ttlSeconds: 3600,
25
+ invalidation: answers.offline === 'sync' ? 'server-version-and-user-signout' : 'ttl-and-user-signout',
26
+ conflictPolicy: answers.offline === 'sync' ? 'server-authoritative-with-explicit-user-resolution' : 'not-applicable',
27
+ })
28
+ files['docs/capabilities/offline.md'] = `# Offline ${answers.offline}\n\nThe mobile client owns this store. Namespace keys by user and schema version, purge on sign-out, exclude secrets, enforce TTL, expose offline/stale/synchronizing/error states, and test reconnect behavior. ${answers.offline === 'sync' ? 'Synchronize through idempotent server operations and surface conflicts; never silently overwrite divergent user data.' : 'This is a measured read cache, not a source of truth.'}\n`
29
+ }
30
+ return files
31
+ }
@@ -1,4 +1,4 @@
1
- import { CONTRIBUTION_HOOKS, PROMPT_SLOTS } from '../contract.js'
1
+ import { CONTRIBUTION_HOOKS, PROMPT_SLOTS } from '../rules.js'
2
2
 
3
3
  export function collectContributions(adapters, hook, context) {
4
4
  if (!CONTRIBUTION_HOOKS.includes(hook)) throw new Error(`Unknown contribution hook: ${hook}`)
@@ -0,0 +1,38 @@
1
+ export const environmentHeader = '# Copy to .env or .env.local. Never commit real credentials.\n\n'
2
+
3
+ export function environmentLine(name, answers) {
4
+ const isFastApi = answers.backend === 'fastapi'
5
+ const apiPort = isFastApi ? '8000' : '8080'
6
+ const defaults = {
7
+ NEXT_PUBLIC_API_URL: `http://localhost:${apiPort}`,
8
+ VITE_API_URL: `http://localhost:${apiPort}`,
9
+ EXPO_PUBLIC_API_URL: `http://localhost:${apiPort}`,
10
+ DATABASE_URL: isFastApi
11
+ ? `postgresql+asyncpg://localhost:5432/${answers.projectName.replaceAll('-', '_')}`
12
+ : `jdbc:postgresql://localhost:5432/${answers.projectName.replaceAll('-', '_')}`,
13
+ POSTGRES_USER: 'postgres',
14
+ POSTGRES_PASSWORD: 'change-me',
15
+ POSTGRES_DB: answers.projectName.replaceAll('-', '_'),
16
+ SPRING_PROFILES_ACTIVE: 'dev',
17
+ SPRING_SECURITY_USER_NAME: 'developer',
18
+ SPRING_SECURITY_USER_PASSWORD: 'change-me-before-production',
19
+ OIDC_ISSUER_URI: 'http://localhost:9090/realms/app',
20
+ OIDC_ISSUER: '',
21
+ OIDC_AUDIENCE: 'api',
22
+ OIDC_ALGORITHMS: 'RS256',
23
+ OIDC_JWKS_URL: '',
24
+ CORS_ALLOWED_ORIGINS: 'http://localhost:3000',
25
+ }
26
+ return `${name}=${defaults[name] || ''}`
27
+ }
28
+
29
+ export function renderEnvironment(names, answers) {
30
+ return `${environmentHeader}${names.map((name) => environmentLine(name, answers)).join('\n')}\n`
31
+ }
32
+
33
+ export function partitionEnvironment(stack) {
34
+ return {
35
+ publicNames: stack.env.filter((name) => name.startsWith(stack.envPrefix)),
36
+ serverNames: stack.env.filter((name) => !name.startsWith(stack.envPrefix)),
37
+ }
38
+ }
@@ -0,0 +1,117 @@
1
+ export function packageVersion(profile, name, capability) {
2
+ const configured = profile.packages[name]
3
+ const resolved = typeof configured === 'string'
4
+ ? configured
5
+ : configured?.overrides?.[capability] || configured?.default
6
+ if (!resolved) throw new Error(`${capability} requires ${name} in compatibility profile ${profile.id}`)
7
+ return resolved
8
+ }
9
+
10
+ export function composerPackageVersion(profile, name, owner = name) {
11
+ const resolved = profile.composerPackages?.[name]
12
+ if (!resolved) throw new Error(`${owner} requires ${name} in compatibility profile ${profile.id}`)
13
+ return resolved
14
+ }
15
+
16
+ export function pythonPackageVersion(profile, name, owner = name) {
17
+ const resolved = profile.pythonPackages?.[name]
18
+ if (!resolved) throw new Error(`${owner} requires ${name} in compatibility profile ${profile.id}`)
19
+ return resolved
20
+ }
21
+
22
+ export function json(value) {
23
+ return `${JSON.stringify(value, null, 2)}\n`
24
+ }
25
+
26
+ export function html(value) {
27
+ return String(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
28
+ }
29
+
30
+ export function buildJavaScriptPackage(answers, stack, owner) {
31
+ const level = answers.testing || 'basic'
32
+ const scripts = { ...stack.scripts, start: owner === 'nextjs' ? 'next start' : undefined }
33
+ const devDependencies = { ...stack.devDeps }
34
+
35
+ if (level === 'none') delete scripts.test
36
+ if (level !== 'none') {
37
+ if (owner === 'react-native') {
38
+ Object.assign(devDependencies, {
39
+ jest: packageVersion(stack.profile, 'jest', owner),
40
+ '@types/jest': packageVersion(stack.profile, '@types/jest', owner),
41
+ 'jest-expo': packageVersion(stack.profile, 'jest-expo', owner),
42
+ '@testing-library/react-native': packageVersion(stack.profile, '@testing-library/react-native', owner),
43
+ })
44
+ } else {
45
+ Object.assign(devDependencies, {
46
+ vitest: packageVersion(stack.profile, 'vitest', owner),
47
+ jsdom: packageVersion(stack.profile, 'jsdom', owner),
48
+ '@testing-library/react': packageVersion(stack.profile, '@testing-library/react', owner),
49
+ '@testing-library/jest-dom': packageVersion(stack.profile, '@testing-library/jest-dom', owner),
50
+ })
51
+ }
52
+ }
53
+ if (level === 'full' && owner !== 'react-native') {
54
+ scripts['test:e2e'] = 'playwright test'
55
+ devDependencies['@playwright/test'] = packageVersion(stack.profile, '@playwright/test', owner)
56
+ }
57
+
58
+ scripts.typecheck = 'tsc --noEmit'
59
+ scripts.format = 'prettier --write .'
60
+ scripts['format:check'] = 'prettier --check .'
61
+ if (owner !== 'react-native') scripts.lint = 'eslint .'
62
+ if (stack.architecture === 'large') scripts['check:boundaries'] = 'node scripts/check-boundaries.mjs'
63
+ if (stack.backendKey === 'supabase') {
64
+ const workdir = owner === 'react' ? ' --workdir ..' : ''
65
+ scripts['supabase:start'] = `supabase${workdir} start`
66
+ scripts['supabase:stop'] = `supabase${workdir} stop`
67
+ scripts['supabase:status'] = `supabase${workdir} status`
68
+ scripts['supabase:reset'] = `supabase${workdir} db reset`
69
+ scripts['supabase:test'] = `supabase${workdir} test db`
70
+ const typePath = owner === 'react-native' ? 'types/database.types.ts' : 'src/types/database.types.ts'
71
+ scripts['supabase:types'] = `supabase${workdir} gen types typescript --local > ${typePath}`
72
+ }
73
+ if (stack.backendKey === 'postgres') {
74
+ scripts['prisma:generate'] = 'prisma generate'
75
+ scripts['db:migrate'] = 'prisma migrate dev'
76
+ scripts['db:deploy'] = 'prisma migrate deploy'
77
+ scripts['db:reset'] = 'prisma migrate reset --force'
78
+ scripts['db:studio'] = 'prisma studio'
79
+ scripts.dev = 'prisma generate && next dev'
80
+ scripts.build = 'prisma generate && next build'
81
+ scripts.typecheck = 'prisma generate && tsc --noEmit'
82
+ }
83
+ const boundary = stack.architecture === 'large' ? ' && npm run check:boundaries' : ''
84
+ scripts.check = owner === 'react-native'
85
+ ? `npm run format:check && npm run typecheck${level === 'none' ? '' : ' && npm test -- --runInBand'}${boundary}`
86
+ : `npm run format:check && npm run lint && npm run typecheck${level === 'none' ? '' : ' && npm test'}${boundary}`
87
+ Object.keys(scripts).forEach((key) => scripts[key] === undefined && delete scripts[key])
88
+
89
+ if (stack.styleId === 'tailwind') {
90
+ devDependencies.tailwindcss = packageVersion(stack.profile, 'tailwindcss', owner)
91
+ if (owner === 'nextjs') devDependencies['@tailwindcss/postcss'] = packageVersion(stack.profile, '@tailwindcss/postcss', owner)
92
+ if (owner === 'react') devDependencies['@tailwindcss/vite'] = packageVersion(stack.profile, '@tailwindcss/vite', owner)
93
+ }
94
+ const dependencies = { ...stack.deps }
95
+ if (owner !== 'nextjs') delete dependencies['@supabase/ssr']
96
+ if (owner === 'react-native' && stack.authentication === 'supabase') {
97
+ dependencies['expo-secure-store'] = packageVersion(stack.profile, 'expo-secure-store', owner)
98
+ }
99
+
100
+ const packageJson = {
101
+ name: answers.projectName,
102
+ version: '0.1.0',
103
+ private: true,
104
+ type: 'module',
105
+ packageManager: `npm@${stack.profile.runtimes.npmMinimum}`,
106
+ engines: {
107
+ node: `>=${stack.profile.runtimes.nodeMinimum}`,
108
+ npm: `>=${stack.profile.runtimes.npmMinimum}`,
109
+ },
110
+ main: owner === 'react-native' ? 'expo-router/entry' : undefined,
111
+ scripts,
112
+ dependencies,
113
+ devDependencies,
114
+ }
115
+ if (!packageJson.main) delete packageJson.main
116
+ return json(packageJson)
117
+ }
@@ -0,0 +1,16 @@
1
+ import path from 'node:path'
2
+
3
+ export function addTestingFiles(files, root, stack, level) {
4
+ if (level === 'none') return
5
+ if (stack.isMobile) {
6
+ files[path.join(root, 'jest.config.js')] = "export default { preset: 'jest-expo' }\n"
7
+ files[path.join(root, 'app/index.test.tsx')] = `import { render } from '@testing-library/react-native'\nimport HomeScreen from './index'\n\ntest('renders the starter heading', async () => {\n const view = await render(<HomeScreen />)\n expect(view.getByText('Your starter is running')).toBeTruthy()\n})\n`
8
+ return
9
+ }
10
+ files[path.join(root, 'vitest.config.ts')] = `import { fileURLToPath, URL } from 'node:url'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },\n test: { environment: 'jsdom', setupFiles: './src/test/setup.ts', exclude: ['e2e/**', 'node_modules/**'] },\n})\n`
11
+ files[path.join(root, 'src/test/setup.ts')] = "import '@testing-library/jest-dom/vitest'\n"
12
+ if (level === 'full') {
13
+ files[path.join(root, 'playwright.config.ts')] = `import { defineConfig, devices } from '@playwright/test'\n\nexport default defineConfig({\n testDir: './e2e',\n use: { baseURL: 'http://127.0.0.1:${stack.frontendPort}' },\n webServer: { command: 'npm run dev -- ${stack.frontendKey === 'nextjs' ? '--hostname' : '--host'} 127.0.0.1 --port ${stack.frontendPort}', url: 'http://127.0.0.1:${stack.frontendPort}', reuseExistingServer: true, timeout: 120_000 },\n projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],\n})\n`
14
+ files[path.join(root, 'e2e/home.spec.ts')] = `import { expect, test } from '@playwright/test'\n\ntest('loads the starter', async ({ page }) => {\n await page.goto('/')\n await expect(page.getByRole('heading', { name: 'Your starter is running' })).toBeVisible()\n})\n`
15
+ }
16
+ }
@@ -0,0 +1,62 @@
1
+ name: CI — Backend
2
+
3
+ on:
4
+ push:
5
+ branches: [dev, main]
6
+ paths:
7
+ - backend/**
8
+ pull_request:
9
+ branches: [dev, main]
10
+ paths:
11
+ - backend/**
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ ci-backend:
18
+ name: Test, Build
19
+ runs-on: ubuntu-latest
20
+ defaults:
21
+ run:
22
+ working-directory: backend
23
+ services:
24
+ postgres:
25
+ image: {{POSTGRES_IMAGE}}
26
+ env:
27
+ POSTGRES_DB: app_test
28
+ POSTGRES_USER: postgres
29
+ POSTGRES_PASSWORD: postgres
30
+ ports:
31
+ - 5432:5432
32
+ options: >-
33
+ --health-cmd "pg_isready -U postgres -d app_test"
34
+ --health-interval 5s
35
+ --health-timeout 5s
36
+ --health-retries 10
37
+ env:
38
+ DATABASE_URL: postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/app_test
39
+ steps:
40
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
41
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
42
+ with:
43
+ python-version: "{{PYTHON_VERSION}}"
44
+ - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v6
45
+ with:
46
+ version: "{{UV_VERSION}}"
47
+ - name: Install dependencies
48
+ run: uv sync --frozen
49
+ - name: Apply migrations
50
+ run: uv run alembic upgrade head
51
+ - name: Check migrations are current
52
+ run: uv run alembic check
53
+ - name: Run Ruff
54
+ run: uv run ruff check .
55
+ - name: Check formatting
56
+ run: uv run ruff format --check .
57
+ - name: Run mypy
58
+ run: uv run mypy .
59
+ - name: Run tests
60
+ run: uv run pytest
61
+ - name: Build image
62
+ run: docker build -f Dockerfile .
@@ -5,7 +5,7 @@ services:
5
5
  container_name: {{PROJECT_NAME}}-db
6
6
  image: {{POSTGRES_IMAGE}}
7
7
  ports:
8
- - "5432:5432"
8
+ - "${POSTGRES_HOST_PORT:-5432}:5432"
9
9
  environment:
10
10
  - POSTGRES_USER=${POSTGRES_USER}
11
11
  - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
@@ -7,7 +7,7 @@ services:
7
7
  context: {{FRONTEND_DIR}}
8
8
  dockerfile: {{FRONTEND_DOCKERFILE}}
9
9
  ports:
10
- - "{{FRONTEND_PORT}}:{{FRONTEND_PORT}}"
10
+ - "${FRONTEND_HOST_PORT:-{{FRONTEND_PORT}}}:{{FRONTEND_PORT}}"
11
11
  volumes:
12
12
  - ./{{FRONTEND_DIR}}:/app
13
13
  - /app/node_modules
@@ -24,7 +24,7 @@ services:
24
24
  context: ./backend
25
25
  dockerfile: Dockerfile.dev
26
26
  ports:
27
- - "8080:8080"
27
+ - "${BACKEND_HOST_PORT:-8080}:8080"
28
28
  volumes:
29
29
  - ./backend:/app
30
30
  - maven-cache:/root/.m2
@@ -43,7 +43,7 @@ services:
43
43
  container_name: {{PROJECT_NAME}}-db
44
44
  image: {{POSTGRES_IMAGE}}
45
45
  ports:
46
- - "5432:5432"
46
+ - "${POSTGRES_HOST_PORT:-5432}:5432"
47
47
  environment:
48
48
  - POSTGRES_USER=${POSTGRES_USER}
49
49
  - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
@@ -6,7 +6,7 @@ services:
6
6
  context: {{FRONTEND_DIR}}
7
7
  dockerfile: {{FRONTEND_DOCKERFILE}}
8
8
  ports:
9
- - "{{FRONTEND_PORT}}:{{FRONTEND_PORT}}"
9
+ - "${FRONTEND_HOST_PORT:-{{FRONTEND_PORT}}}:{{FRONTEND_PORT}}"
10
10
  volumes:
11
11
  - ./{{FRONTEND_DIR}}:/app
12
12
  - /app/node_modules
@@ -0,0 +1,52 @@
1
+ # docker-compose.prod.yml — {{PROJECT_NAME}} ({{STACK}}) — Prod
2
+ services:
3
+
4
+ backend:
5
+ container_name: {{PROJECT_NAME}}-backend
6
+ build:
7
+ context: ./backend
8
+ dockerfile: Dockerfile
9
+ ports:
10
+ - "${BACKEND_HOST_PORT:-8000}:8000"
11
+ environment:
12
+ - DATABASE_URL=${DATABASE_URL}
13
+ - OIDC_ISSUER=${OIDC_ISSUER}
14
+ - OIDC_AUDIENCE=${OIDC_AUDIENCE}
15
+ - OIDC_ALGORITHMS=${OIDC_ALGORITHMS}
16
+ - OIDC_JWKS_URL=${OIDC_JWKS_URL}
17
+ - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS}
18
+ depends_on:
19
+ db:
20
+ condition: service_healthy
21
+ networks:
22
+ - app-network
23
+ healthcheck:
24
+ test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
25
+ interval: 30s
26
+ timeout: 5s
27
+ retries: 3
28
+
29
+ db:
30
+ container_name: {{PROJECT_NAME}}-db
31
+ image: {{POSTGRES_IMAGE}}
32
+ environment:
33
+ - POSTGRES_USER=${POSTGRES_USER}
34
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
35
+ - POSTGRES_DB=${POSTGRES_DB}
36
+ volumes:
37
+ - postgres-data:/var/lib/postgresql/data
38
+ networks:
39
+ - app-network
40
+ healthcheck:
41
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
42
+ interval: 10s
43
+ timeout: 5s
44
+ retries: 5
45
+
46
+ volumes:
47
+ postgres-data:
48
+ name: {{PROJECT_NAME}}-postgres-data
49
+
50
+ networks:
51
+ app-network:
52
+ name: {{PROJECT_NAME}}-network
@@ -16,6 +16,11 @@ services:
16
16
  networks:
17
17
  - app-network
18
18
  restart: always
19
+ read_only: true
20
+ tmpfs: [/tmp]
21
+ deploy:
22
+ resources:
23
+ limits: { cpus: "1.0", memory: 512M }
19
24
 
20
25
  backend:
21
26
  container_name: {{PROJECT_NAME}}-backend-prod
@@ -24,7 +29,7 @@ services:
24
29
  dockerfile: Dockerfile
25
30
  target: production
26
31
  ports:
27
- - "8080:8080"
32
+ - "${BACKEND_HOST_PORT:-8080}:8080"
28
33
  environment:
29
34
  - SPRING_PROFILES_ACTIVE=prod
30
35
  - DATABASE_URL=jdbc:postgresql://db:5432/${POSTGRES_DB}
@@ -35,7 +40,17 @@ services:
35
40
  condition: service_healthy
36
41
  networks:
37
42
  - app-network
43
+ - data-network
38
44
  restart: always
45
+ stop_grace_period: 30s
46
+ healthcheck:
47
+ test: ["CMD", "wget", "-qO-", "http://localhost:8080/api/health"]
48
+ interval: 10s
49
+ timeout: 3s
50
+ retries: 5
51
+ deploy:
52
+ resources:
53
+ limits: { cpus: "1.0", memory: 768M }
39
54
 
40
55
  db:
41
56
  container_name: {{PROJECT_NAME}}-db-prod
@@ -47,7 +62,7 @@ services:
47
62
  volumes:
48
63
  - postgres-data-prod:/var/lib/postgresql/data
49
64
  networks:
50
- - app-network
65
+ - data-network
51
66
  restart: always
52
67
  healthcheck:
53
68
  test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
@@ -62,3 +77,6 @@ volumes:
62
77
  networks:
63
78
  app-network:
64
79
  name: {{PROJECT_NAME}}-network-prod
80
+ data-network:
81
+ name: {{PROJECT_NAME}}-data-network-prod
82
+ internal: true
@@ -0,0 +1,9 @@
1
+ FROM {{PYTHON_IMAGE}}
2
+ WORKDIR /app
3
+ COPY --from=ghcr.io/astral-sh/uv:{{UV_VERSION}} /uv /usr/local/bin/uv
4
+ COPY pyproject.toml uv.lock .python-version README.md ./
5
+ RUN uv sync --frozen --no-install-project
6
+ COPY . .
7
+ RUN uv sync --frozen
8
+ EXPOSE 8000
9
+ CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
@@ -0,0 +1,11 @@
1
+ FROM {{PYTHON_IMAGE}}
2
+ WORKDIR /app
3
+ COPY --from=ghcr.io/astral-sh/uv:{{UV_VERSION}} /uv /usr/local/bin/uv
4
+ COPY pyproject.toml uv.lock .python-version README.md ./
5
+ RUN uv sync --frozen --no-install-project
6
+ COPY . .
7
+ RUN uv sync --frozen
8
+ RUN useradd --create-home --no-log-init appuser && chown -R appuser:appuser /app
9
+ EXPOSE 8000
10
+ USER appuser
11
+ CMD ["/app/.venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -11,6 +11,7 @@ RUN npm run build
11
11
  FROM {{NODE_IMAGE}} AS production
12
12
  WORKDIR /app
13
13
  ENV NODE_ENV=production
14
+ USER node
14
15
  COPY --from=builder /app/.next/standalone ./
15
16
  COPY --from=builder /app/.next/static ./.next/static
16
17
  COPY --from=builder /app/public ./public
@@ -5,6 +5,9 @@ RUN mvn --batch-mode package -DskipTests
5
5
 
6
6
  FROM {{JAVA_IMAGE}} AS production
7
7
  WORKDIR /app
8
+ RUN addgroup -S app && adduser -S -G app app
8
9
  COPY --from=builder /app/target/*.jar app.jar
10
+ RUN chown app:app app.jar
11
+ USER app
9
12
  EXPOSE 8080
10
- CMD ["java", "-jar", "app.jar"]
13
+ CMD ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
@@ -8,5 +8,6 @@ RUN npm run build
8
8
  FROM {{NGINX_IMAGE}} AS production
9
9
  COPY --from=builder /app/dist /usr/share/nginx/html
10
10
  COPY nginx.conf /etc/nginx/conf.d/default.conf
11
- EXPOSE 80
11
+ USER nginx
12
+ EXPOSE 8080
12
13
  CMD ["nginx", "-g", "daemon off;"]