create-win-project 1.3.0 → 1.4.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 (232) hide show
  1. package/README.md +19 -28
  2. package/{scripts/compatibility-matrix.mjs → checks/check-compatibility.js} +24 -6
  3. package/{scripts/verify-generated.mjs → checks/check-generated-project.js} +2 -2
  4. package/{scripts/validate-content.mjs → checks/check-library.js} +5 -5
  5. package/checks/classify-changes.js +39 -0
  6. package/docs/ARCHITECTURE.md +83 -40
  7. package/docs/CONTENT_MODEL.md +3 -3
  8. package/docs/CONTRIBUTING.md +154 -0
  9. package/docs/DEPENDENCY_MAINTENANCE.md +1 -1
  10. package/index.js +2 -429
  11. package/{playbooks → library}/INDEX.md +4 -4
  12. package/{playbooks/capabilities/ci/github-actions.manifest.json → library/development-tools/ci/definition.json} +1 -0
  13. package/{playbooks/devops/makefile.manifest.json → library/development-tools/devops/makefile/definition.json} +1 -0
  14. package/{playbooks/devops/pr-template.manifest.json → library/development-tools/devops/pr-template/definition.json} +1 -0
  15. package/{playbooks/platform/mobile.manifest.json → library/platforms/mobile/definition.json} +1 -0
  16. package/{playbooks/platform/web.manifest.json → library/platforms/web/definition.json} +1 -0
  17. package/{compatibility/profiles.json → library/tested-versions.json} +4 -0
  18. package/{playbooks/universal/accessibility.manifest.json → library/universal/accessibility/definition.json} +1 -1
  19. package/{playbooks/universal/coding-rules.manifest.json → library/universal/coding-rules/definition.json} +1 -0
  20. package/{playbooks/universal/error-handling.manifest.json → library/universal/error-handling/definition.json} +1 -0
  21. package/{playbooks/universal/git-conventions.manifest.json → library/universal/git-conventions/definition.json} +1 -0
  22. package/{playbooks/universal/observability.manifest.json → library/universal/observability/definition.json} +1 -0
  23. package/{playbooks/universal/security.manifest.json → library/universal/security/definition.json} +1 -0
  24. package/{playbooks/universal/typescript.manifest.json → library/universal/typescript/definition.json} +1 -0
  25. package/package.json +12 -11
  26. package/public/logo.svg +19 -0
  27. package/src/cli/arguments.js +52 -0
  28. package/src/cli/display.js +220 -0
  29. package/src/cli/main.js +185 -0
  30. package/src/cli/navigation.js +156 -0
  31. package/src/cli/questions.js +187 -0
  32. package/src/cli/system-check.js +138 -0
  33. package/src/engine/create-project.js +10 -0
  34. package/src/engine/install-dependencies.js +13 -0
  35. package/{lib/catalog.js → src/engine/load-library.js} +5 -6
  36. package/{lib/playbooks.js → src/engine/project-guidance.js} +25 -3
  37. package/{lib/template.js → src/engine/render-templates.js} +8 -0
  38. package/src/engine/write-files.js +34 -0
  39. package/{lib/stacks/registry.js → src/stacks/available-stacks.js} +23 -1
  40. package/src/stacks/backends/README.md +5 -0
  41. package/{lib/stacks → src/stacks/backends}/laravel/architecture.js +1 -0
  42. package/{lib/stacks → src/stacks/backends}/laravel/auth/oidc.js +1 -0
  43. package/{lib/stacks → src/stacks/backends}/laravel/auth/public.js +1 -0
  44. package/{lib/stacks → src/stacks/backends}/laravel/auth/sanctum.js +1 -0
  45. package/{lib/stacks → src/stacks/backends}/laravel/auth/session.js +1 -0
  46. package/src/stacks/backends/laravel/ci.js +3 -0
  47. package/{lib/stacks → src/stacks/backends}/laravel/composer.js +1 -1
  48. package/{lib/stacks/laravel/generate.js → src/stacks/backends/laravel/create-files.js} +1 -0
  49. package/src/stacks/backends/laravel/docker.js +14 -0
  50. package/src/stacks/backends/laravel/environment.js +5 -0
  51. package/{lib/stacks → src/stacks/backends}/laravel/index.js +11 -3
  52. package/{lib/stacks → src/stacks/backends}/laravel/ui/blade.js +1 -0
  53. package/{lib/stacks → src/stacks/backends}/laravel/ui/index.js +1 -0
  54. package/{lib/stacks → src/stacks/backends}/laravel/ui/inertia-react.js +4 -2
  55. package/{lib/stacks → src/stacks/backends}/laravel/ui/livewire.js +1 -0
  56. package/{lib/stacks → src/stacks/backends}/laravel/ui/shared.js +1 -0
  57. package/src/stacks/backends/none/index.js +23 -0
  58. package/src/stacks/backends/postgres/ci.js +3 -0
  59. package/src/stacks/backends/postgres/create-files.js +21 -0
  60. package/src/stacks/backends/postgres/docker.js +3 -0
  61. package/src/stacks/backends/postgres/environment.js +3 -0
  62. package/src/stacks/backends/postgres/index.js +30 -0
  63. package/src/stacks/backends/springboot/ci.js +3 -0
  64. package/src/stacks/backends/springboot/create-files.js +91 -0
  65. package/src/stacks/backends/springboot/docker.js +3 -0
  66. package/src/stacks/backends/springboot/environment.js +3 -0
  67. package/src/stacks/backends/springboot/index.js +31 -0
  68. package/src/stacks/backends/supabase/ci.js +3 -0
  69. package/src/stacks/backends/supabase/create-files.js +52 -0
  70. package/src/stacks/backends/supabase/docker.js +3 -0
  71. package/src/stacks/backends/supabase/environment.js +3 -0
  72. package/src/stacks/backends/supabase/index.js +31 -0
  73. package/src/stacks/backends/supabase/native.js +14 -0
  74. package/src/stacks/compose-files.js +241 -0
  75. package/{lib/generator.js → src/stacks/create-project.js} +78 -50
  76. package/src/stacks/frontends/README.md +4 -0
  77. package/src/stacks/frontends/nextjs/ci.js +3 -0
  78. package/src/stacks/frontends/nextjs/create-files.js +46 -0
  79. package/src/stacks/frontends/nextjs/dependencies.js +5 -0
  80. package/src/stacks/frontends/nextjs/docker.js +3 -0
  81. package/src/stacks/frontends/nextjs/environment.js +7 -0
  82. package/src/stacks/frontends/nextjs/index.js +35 -0
  83. package/src/stacks/frontends/react-native/ci.js +3 -0
  84. package/src/stacks/frontends/react-native/create-files.js +25 -0
  85. package/src/stacks/frontends/react-native/dependencies.js +5 -0
  86. package/src/stacks/frontends/react-native/docker.js +5 -0
  87. package/src/stacks/frontends/react-native/environment.js +7 -0
  88. package/src/stacks/frontends/react-native/index.js +30 -0
  89. package/src/stacks/frontends/react-vite/ci.js +3 -0
  90. package/src/stacks/frontends/react-vite/create-files.js +27 -0
  91. package/src/stacks/frontends/react-vite/dependencies.js +5 -0
  92. package/src/stacks/frontends/react-vite/docker.js +3 -0
  93. package/src/stacks/frontends/react-vite/environment.js +8 -0
  94. package/src/stacks/frontends/react-vite/index.js +32 -0
  95. package/{lib → src}/stacks/shared/contributions.js +1 -1
  96. package/src/stacks/shared/environment.js +30 -0
  97. package/src/stacks/shared/javascript-package.js +111 -0
  98. package/src/stacks/shared/testing-files.js +16 -0
  99. package/templates/docker/compose/postgres.yml +1 -1
  100. package/templates/docker/compose/springboot.yml +3 -3
  101. package/templates/docker/compose/supabase.yml +1 -1
  102. package/templates/docker/compose-prod/springboot.yml +1 -1
  103. package/lib/banner.js +0 -45
  104. package/lib/constants.js +0 -3
  105. package/lib/doctor.js +0 -45
  106. package/lib/interview.js +0 -77
  107. package/lib/laravel-scaffold.js +0 -3
  108. package/lib/scaffold.js +0 -467
  109. package/lib/stacks/index.js +0 -8
  110. /package/{playbooks/capabilities → library/development-tools}/ci/github-actions.md +0 -0
  111. /package/{playbooks/devops → library/development-tools/devops/makefile}/makefile.md +0 -0
  112. /package/{playbooks/devops → library/development-tools/devops/pr-template}/pr-template.md +0 -0
  113. /package/{playbooks/capabilities/docker/docker.manifest.json → library/development-tools/docker/definition.json} +0 -0
  114. /package/{playbooks/capabilities → library/development-tools}/docker/overview.md +0 -0
  115. /package/{playbooks/capabilities → library/features}/auth/oidc-resource-server.md +0 -0
  116. /package/{playbooks/capabilities → library/features}/auth/spring-session.md +0 -0
  117. /package/{playbooks/capabilities/flyway/flyway.manifest.json → library/features/flyway/definition.json} +0 -0
  118. /package/{playbooks/capabilities → library/features}/flyway/environments.md +0 -0
  119. /package/{playbooks/capabilities → library/features}/flyway/migrations.md +0 -0
  120. /package/{playbooks/capabilities → library/features}/flyway/testing.md +0 -0
  121. /package/{playbooks/capabilities → library/features}/laravel/database.md +0 -0
  122. /package/{playbooks/capabilities → library/features}/laravel/migrations.md +0 -0
  123. /package/{playbooks/capabilities → library/features}/laravel/observability.md +0 -0
  124. /package/{playbooks/capabilities → library/features}/laravel/oidc-resource-server.md +0 -0
  125. /package/{playbooks/capabilities → library/features}/laravel/queues.md +0 -0
  126. /package/{playbooks/capabilities → library/features}/laravel/sanctum-spa.md +0 -0
  127. /package/{playbooks/capabilities → library/features}/laravel/scheduler.md +0 -0
  128. /package/{playbooks/capabilities → library/features}/laravel/session-auth.md +0 -0
  129. /package/{playbooks/capabilities → library/features}/laravel/storage-uploads.md +0 -0
  130. /package/{playbooks/capabilities → library/features}/postgresql/architecture.md +0 -0
  131. /package/{playbooks/capabilities/postgresql/postgresql.manifest.json → library/features/postgresql/definition.json} +0 -0
  132. /package/{playbooks/capabilities → library/features}/postgresql/migrations.md +0 -0
  133. /package/{playbooks/capabilities → library/features}/postgresql/schema-design.md +0 -0
  134. /package/{playbooks/capabilities → library/features}/postgresql/security.md +0 -0
  135. /package/{playbooks/capabilities → library/features}/postgresql/testing.md +0 -0
  136. /package/{playbooks/capabilities → library/features}/prisma/architecture.md +0 -0
  137. /package/{playbooks/capabilities/prisma/prisma.manifest.json → library/features/prisma/definition.json} +0 -0
  138. /package/{playbooks/capabilities → library/features}/prisma/migrations.md +0 -0
  139. /package/{playbooks/capabilities → library/features}/prisma/runtime.md +0 -0
  140. /package/{playbooks/capabilities → library/features}/prisma/schema.md +0 -0
  141. /package/{playbooks/capabilities → library/features}/prisma/testing.md +0 -0
  142. /package/{playbooks/capabilities → library/features}/supabase/architecture.md +0 -0
  143. /package/{playbooks/capabilities → library/features}/supabase/authentication.md +0 -0
  144. /package/{playbooks/capabilities/supabase/supabase.manifest.json → library/features/supabase/definition.json} +0 -0
  145. /package/{playbooks/capabilities → library/features}/supabase/expo.md +0 -0
  146. /package/{playbooks/capabilities → library/features}/supabase/migrations.md +0 -0
  147. /package/{playbooks/capabilities → library/features}/supabase/nextjs.md +0 -0
  148. /package/{playbooks/capabilities → library/features}/supabase/rls.md +0 -0
  149. /package/{playbooks/capabilities → library/features}/supabase/testing.md +0 -0
  150. /package/{playbooks/capabilities → library/features}/supabase/vite.md +0 -0
  151. /package/{playbooks → library/optional-features}/concerns/axios.md +0 -0
  152. /package/{playbooks → library/optional-features}/concerns/next-safe-action.md +0 -0
  153. /package/{playbooks → library/optional-features}/concerns/next-themes.md +0 -0
  154. /package/{playbooks → library/optional-features}/concerns/nuqs.md +0 -0
  155. /package/{playbooks → library/optional-features}/concerns/t3-env.md +0 -0
  156. /package/{playbooks → library/optional-features}/concerns/tanstack-query.md +0 -0
  157. /package/{playbooks → library/optional-features}/concerns/zod.md +0 -0
  158. /package/{playbooks → library/optional-features}/concerns/zustand.md +0 -0
  159. /package/{playbooks/styling → library/optional-features/styling/css-modules}/css-modules-extensions.md +0 -0
  160. /package/{playbooks/styling/css-modules.manifest.json → library/optional-features/styling/css-modules/definition.json} +0 -0
  161. /package/{playbooks/styling/native-styles.manifest.json → library/optional-features/styling/native-styles/definition.json} +0 -0
  162. /package/{playbooks/styling → library/optional-features/styling/native-styles}/native-styles.md +0 -0
  163. /package/{playbooks/styling/tailwind.manifest.json → library/optional-features/styling/tailwind/definition.json} +0 -0
  164. /package/{playbooks/styling → library/optional-features/styling/tailwind}/tailwind-extensions.md +0 -0
  165. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/architecture.md +0 -0
  166. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/runtime.md +0 -0
  167. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/security.md +0 -0
  168. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/structure.md +0 -0
  169. /package/{playbooks/platform → library/platforms}/laravel-ui/blade/testing.md +0 -0
  170. /package/{playbooks/stack/laravel-ui.manifest.json → library/platforms/laravel-ui/definition.json} +0 -0
  171. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/architecture.md +0 -0
  172. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/runtime.md +0 -0
  173. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/security.md +0 -0
  174. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/structure.md +0 -0
  175. /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/testing.md +0 -0
  176. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/architecture.md +0 -0
  177. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/runtime.md +0 -0
  178. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/security.md +0 -0
  179. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/structure.md +0 -0
  180. /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/testing.md +0 -0
  181. /package/{playbooks/platform → library/platforms/mobile}/mobile.md +0 -0
  182. /package/{playbooks/platform → library/platforms/web}/web.md +0 -0
  183. /package/{playbooks/stack → library/stacks}/expo/architecture.md +0 -0
  184. /package/{playbooks/stack/react-native.manifest.json → library/stacks/expo/definition.json} +0 -0
  185. /package/{playbooks/stack → library/stacks}/expo/runtime.md +0 -0
  186. /package/{playbooks/stack → library/stacks}/expo/security.md +0 -0
  187. /package/{playbooks/stack → library/stacks}/expo/structure.md +0 -0
  188. /package/{playbooks/stack → library/stacks}/expo/testing.md +0 -0
  189. /package/{playbooks/stack → library/stacks}/laravel/architecture.md +0 -0
  190. /package/{playbooks/stack/laravel.manifest.json → library/stacks/laravel/definition.json} +0 -0
  191. /package/{playbooks/stack → library/stacks}/laravel/runtime.md +0 -0
  192. /package/{playbooks/stack → library/stacks}/laravel/security.md +0 -0
  193. /package/{playbooks/stack → library/stacks}/laravel/structure.md +0 -0
  194. /package/{playbooks/stack → library/stacks}/laravel/testing.md +0 -0
  195. /package/{playbooks/stack → library/stacks}/nextjs/architecture.md +0 -0
  196. /package/{playbooks/stack/nextjs.manifest.json → library/stacks/nextjs/definition.json} +0 -0
  197. /package/{playbooks/stack → library/stacks}/nextjs/runtime.md +0 -0
  198. /package/{playbooks/stack → library/stacks}/nextjs/security.md +0 -0
  199. /package/{playbooks/stack → library/stacks}/nextjs/structure.md +0 -0
  200. /package/{playbooks/stack → library/stacks}/nextjs/testing.md +0 -0
  201. /package/{playbooks/stack/no-frontend.manifest.json → library/stacks/no-frontend/definition.json} +0 -0
  202. /package/{playbooks/stack/none.manifest.json → library/stacks/none/definition.json} +0 -0
  203. /package/{playbooks/stack → library/stacks}/react-vite/architecture.md +0 -0
  204. /package/{playbooks/stack/react-vite.manifest.json → library/stacks/react-vite/definition.json} +0 -0
  205. /package/{playbooks/stack → library/stacks}/react-vite/runtime.md +0 -0
  206. /package/{playbooks/stack → library/stacks}/react-vite/security.md +0 -0
  207. /package/{playbooks/stack → library/stacks}/react-vite/structure.md +0 -0
  208. /package/{playbooks/stack → library/stacks}/react-vite/testing.md +0 -0
  209. /package/{playbooks/stack → library/stacks}/springboot/architecture.md +0 -0
  210. /package/{playbooks/stack/springboot.manifest.json → library/stacks/springboot/definition.json} +0 -0
  211. /package/{playbooks/stack → library/stacks}/springboot/runtime.md +0 -0
  212. /package/{playbooks/stack → library/stacks}/springboot/security.md +0 -0
  213. /package/{playbooks/stack → library/stacks}/springboot/structure.md +0 -0
  214. /package/{playbooks/stack → library/stacks}/springboot/testing.md +0 -0
  215. /package/{playbooks/universal → library/universal/accessibility}/accessibility.md +0 -0
  216. /package/{playbooks/universal → library/universal/coding-rules}/coding-rules.md +0 -0
  217. /package/{playbooks/universal → library/universal/error-handling}/error-handling.md +0 -0
  218. /package/{playbooks/universal → library/universal/git-conventions}/git-conventions.md +0 -0
  219. /package/{playbooks/universal → library/universal/observability}/observability.md +0 -0
  220. /package/{playbooks/universal → library/universal/security}/security.md +0 -0
  221. /package/{playbooks/universal → library/universal/typescript}/typescript.md +0 -0
  222. /package/{lib/files.js → src/engine/project-files.js} +0 -0
  223. /package/{lib → src/engine}/project-location.js +0 -0
  224. /package/{lib/application-shapes.js → src/engine/project-shapes.js} +0 -0
  225. /package/{lib/compatibility.js → src/engine/tested-versions.js} +0 -0
  226. /package/{lib → src}/stacks/context.js +0 -0
  227. /package/{lib/stacks/contract.js → src/stacks/rules.js} +0 -0
  228. /package/{ci → templates/ci}/expo.yml +0 -0
  229. /package/{ci → templates/ci}/laravel.yml +0 -0
  230. /package/{ci → templates/ci}/nextjs.yml +0 -0
  231. /package/{ci → templates/ci}/springboot.yml +0 -0
  232. /package/{ci → templates/ci}/vite.yml +0 -0
@@ -0,0 +1,241 @@
1
+ import path from 'node:path'
2
+ import { packageVersion } from './shared/javascript-package.js'
3
+ import { stackRegistry } from './available-stacks.js'
4
+ import { collectContributions } from './shared/contributions.js'
5
+ import { buildSharedTestFiles, buildSupabaseWebFiles } from './backends/supabase/create-files.js'
6
+ import { augmentSupabaseNativeFiles } from './backends/supabase/native.js'
7
+ import { addPostgresScripts, buildPostgresFiles } from './backends/postgres/create-files.js'
8
+ import { buildEnvironmentFiles as nextjsEnvironment } from './frontends/nextjs/environment.js'
9
+ import { buildEnvironmentFiles as reactEnvironment } from './frontends/react-vite/environment.js'
10
+ import { buildEnvironmentFiles as nativeEnvironment } from './frontends/react-native/environment.js'
11
+ import { renderEnvironment } from './shared/environment.js'
12
+
13
+ function json(value) {
14
+ return `${JSON.stringify(value, null, 2)}\n`
15
+ }
16
+
17
+ function html(value) {
18
+ return String(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
19
+ }
20
+
21
+ function envFiles(answers, stack) {
22
+ if (stack.frontendKey === 'no-frontend') {
23
+ return { '.env.example': renderEnvironment(stack.env, answers) }
24
+ }
25
+ if (stack.frontendKey === 'react') return reactEnvironment(answers, stack)
26
+ if (stack.isMobile) return nativeEnvironment(answers, stack)
27
+ if (stack.frontendKey === 'nextjs') return nextjsEnvironment(answers, stack)
28
+ return { '.env.example': renderEnvironment(stack.env, answers) }
29
+ }
30
+
31
+ function projectReadme(answers, stack) {
32
+ if (stack.frontendKey === 'no-frontend') {
33
+ return `# ${answers.projectName}\n\n> ${answers.projectDescription}\n\nGenerated backend-only ${stack.backendLabel} application.\n\n## Start\n\n\`\`\`bash\ncp .env.example .env\ncd backend\n./mvnw spring-boot:run # use mvnw.cmd on Windows\n\`\`\`\n\n## Validate\n\n\`\`\`bash\ncd backend\n./mvnw --batch-mode test\n./mvnw --batch-mode package -DskipTests\n\`\`\`\n`
34
+ }
35
+ const root = stack.frontendKey === 'react' ? 'frontend/' : ''
36
+ const quality = stack.isMobile
37
+ ? `npm run typecheck\n${answers.testing === 'none' ? '' : 'npm test -- --runInBand\n'}npm run build -- --platform web`
38
+ : `npm run lint\nnpm run typecheck\nnpm run test --if-present\nnpm run build`
39
+ const backend = stack.backendKey === 'springboot'
40
+ ? `\nThe backend lives in \`backend/\`:\n\n\`\`\`bash\ncd backend\nmvn spring-boot:run\n# verify: curl http://localhost:8080/api/health\n\`\`\`\n`
41
+ : ''
42
+ return `# ${answers.projectName}\n\n> ${answers.projectDescription}\n\nGenerated with create-win-project. This repository includes a runnable application, tests, CI guidance, and task-routed agent playbooks.\n\n## Start\n\n\`\`\`bash\n${root ? `cd ${root}\n` : ''}cp .env.example .env.local 2>/dev/null || cp .env.example .env\nnpm install\nnpm run dev\n\`\`\`\n${backend}\n## Validate\n\n\`\`\`bash\n${root ? `cd ${root}\n` : ''}${quality}\n\`\`\`\n\nCommit the generated lockfile before enabling CI; CI intentionally uses \`npm ci\`.\n\n## Agent-assisted work\n\n1. Put product goals and boundaries in \`CONTEXT.md\`.\n2. Read \`AGENTS.md\` for commands and authority boundaries.\n3. Use \`RULES.md\` to open only the relevant playbook section.\n4. Treat tests and application behavior as the source of truth when prose drifts.\n\n## Important files\n\n- \`AGENTS.md\`: small always-on operating contract.\n- \`RULES.md\`: concern-to-playbook router.\n- \`CONTEXT.md\`: project-specific intent and decisions.\n- \`docs/\`: architecture, API, setup, and deployment documentation.\n`
43
+ }
44
+
45
+ function testingPackage(stack, level) {
46
+ const scripts = { ...stack.scripts, start: stack.frontendKey === 'nextjs' ? 'next start' : undefined }
47
+ const devDeps = { ...stack.devDeps }
48
+
49
+ if (level === 'none') delete scripts.test
50
+ if (level !== 'none') {
51
+ if (stack.isMobile) {
52
+ Object.assign(devDeps, {
53
+ jest: packageVersion(stack.profile, 'jest', 'react-native'),
54
+ '@types/jest': packageVersion(stack.profile, '@types/jest', 'react-native'),
55
+ 'jest-expo': packageVersion(stack.profile, 'jest-expo', 'react-native'),
56
+ '@testing-library/react-native': packageVersion(stack.profile, '@testing-library/react-native', 'react-native'),
57
+ })
58
+ } else {
59
+ Object.assign(devDeps, {
60
+ vitest: packageVersion(stack.profile, 'vitest', stack.frontendKey),
61
+ jsdom: packageVersion(stack.profile, 'jsdom', stack.frontendKey),
62
+ '@testing-library/react': packageVersion(stack.profile, '@testing-library/react', stack.frontendKey),
63
+ '@testing-library/jest-dom': packageVersion(stack.profile, '@testing-library/jest-dom', stack.frontendKey),
64
+ })
65
+ }
66
+ }
67
+ if (level === 'full' && !stack.isMobile) {
68
+ scripts['test:e2e'] = 'playwright test'
69
+ devDeps['@playwright/test'] = packageVersion(stack.profile, '@playwright/test', stack.frontendKey)
70
+ }
71
+
72
+ scripts.typecheck = 'tsc --noEmit'
73
+ scripts.format = 'prettier --write .'
74
+ scripts['format:check'] = 'prettier --check .'
75
+ if (!stack.isMobile) scripts.lint = 'eslint .'
76
+ if (stack.architecture === 'large') scripts['check:boundaries'] = 'node scripts/check-boundaries.mjs'
77
+ if (stack.backendKey === 'supabase') {
78
+ const workdir = stack.frontendKey === 'react' ? ' --workdir ..' : ''
79
+ scripts['supabase:start'] = `supabase${workdir} start`
80
+ scripts['supabase:stop'] = `supabase${workdir} stop`
81
+ scripts['supabase:status'] = `supabase${workdir} status`
82
+ scripts['supabase:reset'] = `supabase${workdir} db reset`
83
+ scripts['supabase:test'] = `supabase${workdir} test db`
84
+ const typePath = stack.frontendKey === 'react' ? 'src/types/database.types.ts' : stack.isMobile ? 'types/database.types.ts' : 'src/types/database.types.ts'
85
+ scripts['supabase:types'] = `supabase${workdir} gen types typescript --local > ${typePath}`
86
+ }
87
+ const boundaryCheck = stack.architecture === 'large' ? ' && npm run check:boundaries' : ''
88
+ scripts.check = stack.isMobile
89
+ ? `npm run format:check && npm run typecheck${level === 'none' ? '' : ' && npm test -- --runInBand'}${boundaryCheck}`
90
+ : `npm run format:check && npm run lint && npm run typecheck${level === 'none' ? '' : ' && npm test'}${boundaryCheck}`
91
+ Object.keys(scripts).forEach((key) => scripts[key] === undefined && delete scripts[key])
92
+ return { scripts, devDeps }
93
+ }
94
+
95
+ function packageFile(answers, stack) {
96
+ const { scripts, devDeps } = testingPackage(stack, answers.testing || 'basic')
97
+ if (stack.backendKey === 'postgres') {
98
+ addPostgresScripts(scripts)
99
+ }
100
+ if (stack.styleId === 'tailwind') {
101
+ devDeps.tailwindcss = packageVersion(stack.profile, 'tailwindcss', stack.frontendKey)
102
+ if (stack.frontendKey === 'nextjs') devDeps['@tailwindcss/postcss'] = packageVersion(stack.profile, '@tailwindcss/postcss', 'nextjs')
103
+ if (stack.frontendKey === 'react') devDeps['@tailwindcss/vite'] = packageVersion(stack.profile, '@tailwindcss/vite', 'react')
104
+ }
105
+ const dependencies = { ...stack.deps }
106
+ if (stack.frontendKey !== 'nextjs') delete dependencies['@supabase/ssr']
107
+ if (stack.isMobile && stack.authentication === 'supabase') {
108
+ dependencies['expo-secure-store'] = packageVersion(stack.profile, 'expo-secure-store', 'react-native')
109
+ }
110
+ const packageJson = {
111
+ name: answers.projectName,
112
+ version: '0.1.0',
113
+ private: true,
114
+ type: 'module',
115
+ packageManager: `npm@${stack.profile.runtimes.npmMinimum}`,
116
+ engines: {
117
+ node: `>=${stack.profile.runtimes.nodeMinimum}`,
118
+ npm: `>=${stack.profile.runtimes.npmMinimum}`,
119
+ },
120
+ main: stack.isMobile ? 'expo-router/entry' : undefined,
121
+ scripts,
122
+ dependencies,
123
+ devDependencies: devDeps,
124
+ }
125
+ if (!packageJson.main) delete packageJson.main
126
+ return json(packageJson)
127
+ }
128
+
129
+ function boundaryScript(sourceRoot) {
130
+ return `/* eslint-disable no-undef -- node script runs outside linted frontend bundle */
131
+ import fs from 'node:fs'\nimport path from 'node:path'\n\nconst root = path.resolve(${JSON.stringify(sourceRoot)}, 'features')\nconst violations = []\nif (fs.existsSync(root)) {\n for (const feature of fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)) {\n const featureRoot = path.join(root, feature)\n const visit = (directory) => {\n for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {\n const file = path.join(directory, entry.name)\n if (entry.isDirectory()) visit(file)\n else if (/\\.[cm]?[jt]sx?$/.test(entry.name)) {\n const content = fs.readFileSync(file, 'utf8')\n for (const match of content.matchAll(/from\\s+['\"]@\\/features\\/([^/'\"]+)(\\/[^'\"]+)?['\"]/g)) {\n if (match[1] !== feature && match[2]) violations.push(\`${'${file}'} deep-imports feature ${'${match[1]}'}; import its public API instead\`)\n }\n }\n }\n }\n visit(featureRoot)\n }\n}\nif (violations.length) { console.error(violations.join('\\n')); process.exit(1) }\nconsole.log('Feature boundaries are valid.')\n`
132
+ }
133
+
134
+ function statusFeatureFiles(root, stack) {
135
+ if (stack.architecture === 'small') return {}
136
+ const prefix = root ? `${root}/` : ''
137
+ const files = {
138
+ [`${prefix}src/features/status/types.ts`]: "export interface StarterStatus { heading: string; profile: 'medium' | 'large' }\n",
139
+ [`${prefix}src/features/status/services/getStarterStatus.ts`]: `import type { StarterStatus } from '../types'\n\nexport function getStarterStatus(): StarterStatus {\n return { heading: 'Your starter is running', profile: '${stack.architecture}' }\n}\n`,
140
+ [`${prefix}src/features/status/components/StarterStatus.tsx`]: "import type { StarterStatus as Status } from '../types'\n\nexport function StarterStatus({ status }: { status: Status }) {\n return <><h1>{status.heading}</h1><p>Architecture: {status.profile}</p></>\n}\n",
141
+ }
142
+ if (stack.backendKey === 'springboot') {
143
+ const baseUrl = stack.frontendKey === 'nextjs' ? 'process.env.NEXT_PUBLIC_API_URL' : 'import.meta.env.VITE_API_URL'
144
+ files[`${prefix}src/features/status/api/getBackendStatus.ts`] = `export interface BackendStatus { status: string }\n\nexport async function getBackendStatus(signal?: AbortSignal): Promise<BackendStatus> {\n const response = await fetch(\`${'${' + baseUrl + '}'}/api/health\`, { signal })\n if (!response.ok) throw new Error(\`Backend health request failed (${'${response.status}'})\`)\n return response.json() as Promise<BackendStatus>\n}\n`
145
+ }
146
+ if (stack.architecture === 'large') {
147
+ files[`${prefix}src/features/status/index.ts`] = "export { StarterStatus } from './components/StarterStatus'\nexport { getStarterStatus } from './services/getStarterStatus'\nexport type { StarterStatus as StarterStatusModel } from './types'\n"
148
+ files[`${prefix}scripts/check-boundaries.mjs`] = boundaryScript(prefix ? 'src' : 'src')
149
+ }
150
+ return files
151
+ }
152
+
153
+ function nativeStatusFeatureFiles(stack) {
154
+ if (stack.architecture === 'small') return {}
155
+ const files = {
156
+ 'features/status/types.ts': "export interface StarterStatus { heading: string; profile: 'medium' | 'large' }\n",
157
+ 'features/status/services/getStarterStatus.ts': `import type { StarterStatus } from '../types'\n\nexport function getStarterStatus(): StarterStatus {\n return { heading: 'Your starter is running', profile: '${stack.architecture}' }\n}\n`,
158
+ 'features/status/components/StarterStatus.tsx': "import { Text } from 'react-native'\nimport type { StarterStatus as Status } from '../types'\n\nexport function StarterStatus({ status }: { status: Status }) {\n return <><Text accessibilityRole=\"header\">{status.heading}</Text><Text>Architecture: {status.profile}</Text></>\n}\n",
159
+ }
160
+ if (stack.backendKey === 'springboot') {
161
+ files['features/status/api/getBackendStatus.ts'] = "export interface BackendStatus { status: string }\n\nexport async function getBackendStatus(signal?: AbortSignal): Promise<BackendStatus> {\n const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/api/health`, { signal })\n if (!response.ok) throw new Error(`Backend health request failed (${response.status})`)\n return response.json() as Promise<BackendStatus>\n}\n"
162
+ }
163
+ if (stack.architecture === 'large') {
164
+ files['features/status/index.ts'] = "export { StarterStatus } from './components/StarterStatus'\nexport { getStarterStatus } from './services/getStarterStatus'\nexport type { StarterStatus as StarterStatusModel } from './types'\n"
165
+ files['scripts/check-boundaries.mjs'] = boundaryScript('.')
166
+ }
167
+ return files
168
+ }
169
+
170
+ function supabaseWebFiles(isNext, withAuth = false) {
171
+ return buildSupabaseWebFiles(isNext, withAuth)
172
+ }
173
+
174
+ const testFiles = buildSharedTestFiles
175
+
176
+ function prismaFiles() {
177
+ return buildPostgresFiles()
178
+ }
179
+
180
+ function augmentViteFiles(files, stack) {
181
+ const root = 'frontend'
182
+ if (stack.backendKey === 'supabase') {
183
+ for (const [name, value] of Object.entries(supabaseWebFiles(false, stack.authentication === 'supabase'))) files[`${root}/${name}`] = value
184
+ if (stack.authentication === 'supabase') {
185
+ files[`${root}/src/features/auth/services/auth.ts`] = `import { supabase } from '@/lib/supabase'\n\nexport async function signIn(email: string, password: string) {\n const { error } = await supabase.auth.signInWithPassword({ email, password })\n if (error) throw new Error('Unable to sign in with those credentials.')\n}\n\nexport async function signOut() {\n const { error } = await supabase.auth.signOut()\n if (error) throw new Error('Unable to sign out.')\n}\n`
186
+ files[`${root}/src/features/auth/components/AuthPanel.tsx`] = `import { useState, type FormEvent } from 'react'\nimport { signIn } from '../services/auth'\n\nexport function AuthPanel() {\n const [error, setError] = useState('')\n const [pending, setPending] = useState(false)\n async function submit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setPending(true)\n setError('')\n const data = new FormData(event.currentTarget)\n try { await signIn(String(data.get('email') || ''), String(data.get('password') || '')) }\n catch (reason) { setError(reason instanceof Error ? reason.message : 'Unable to sign in.') }\n finally { setPending(false) }\n }\n return <section aria-labelledby="login-heading"><h2 id="login-heading">Sign in</h2>{error ? <p role="alert">{error}</p> : null}<form onSubmit={submit}><label>Email <input name="email" type="email" autoComplete="email" required /></label><label>Password <input name="password" type="password" autoComplete="current-password" required /></label><button disabled={pending} type="submit">{pending ? 'Signing in…' : 'Sign in'}</button></form></section>\n}\n`
187
+ files[`${root}/src/App.tsx`] = `import { AuthPanel } from '@/features/auth/components/AuthPanel'\n${files[`${root}/src/App.tsx`].replace('<p>Read <code>AGENTS.md</code> before your first agent-assisted change.</p>', '<p>Read <code>AGENTS.md</code> before your first agent-assisted change.</p><AuthPanel />')}`
188
+ }
189
+ }
190
+ return files
191
+ }
192
+
193
+ function nativeFiles(answers, stack) {
194
+ const files = frontendFiles(answers, stack)
195
+ augmentSupabaseNativeFiles(files, answers, stack)
196
+ return files
197
+ }
198
+
199
+ function frontendFiles(answers, stack) {
200
+ if (stack.frontendKey === 'no-frontend' || stack.frontendKey === 'laravel-ui') return {}
201
+ const adapter = stackRegistry.require(stack.frontendKey)
202
+ return Object.fromEntries(collectContributions([adapter], 'files', {
203
+ answers,
204
+ stack,
205
+ shared: { json, packageFile, testFiles, statusFeatureFiles, nativeStatusFeatureFiles, supabaseWebFiles, prismaFiles },
206
+ }))
207
+ }
208
+
209
+ function springFiles(answers, vars, stack) {
210
+ const adapter = stackRegistry.require(stack.backendKey)
211
+ return Object.fromEntries(collectContributions([adapter], 'files', { answers, stack, vars }))
212
+ }
213
+
214
+ export function buildRunnableFiles(answers, stack, vars) {
215
+ let files = stack.isMobile ? nativeFiles(answers, stack) : frontendFiles(answers, stack)
216
+ if (stack.frontendKey === 'react') files = augmentViteFiles(files, stack)
217
+ Object.assign(files, envFiles(answers, stack))
218
+ files['create-win-project.profile.json'] = json({
219
+ schemaVersion: 3,
220
+ applicationShape: stack.applicationShape,
221
+ compatibilityProfile: {
222
+ id: stack.profile.id,
223
+ status: stack.profile.status,
224
+ supportedUntil: stack.profile.supportedUntil,
225
+ },
226
+ architectureProfile: stack.architecture,
227
+ authentication: {
228
+ intent: stack.authenticationIntent,
229
+ model: stack.authentication,
230
+ audience: stack.authAudience,
231
+ },
232
+ stack: stack.key,
233
+ runtimes: stack.profile.runtimes,
234
+ })
235
+ files['README.md'] = projectReadme(answers, stack)
236
+ if (stack.backendKey === 'springboot') {
237
+ files['README.md'] = files['README.md'].replace('mvn spring-boot:run', './mvnw spring-boot:run # use mvnw.cmd on Windows')
238
+ }
239
+ if (stackRegistry.get(stack.backendKey)) Object.assign(files, springFiles(answers, vars, stack))
240
+ return files
241
+ }
@@ -1,39 +1,32 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
- import { randomUUID } from 'node:crypto'
4
- import { loadCatalog, resolveStack } from './catalog.js'
5
- import { loadCompatibility } from './compatibility.js'
6
- import { buildRulesIndex, copySelectedPlaybooks } from './playbooks.js'
7
- import { buildVars, render, readTemplate } from './template.js'
8
- import { buildRunnableFiles } from './scaffold.js'
9
- import { buildLaravelFiles } from './laravel-scaffold.js'
3
+ import { loadCatalog, resolveStack } from '../engine/load-library.js'
4
+ import { loadCompatibility } from '../engine/tested-versions.js'
5
+ import { buildRulesIndex, copySelectedPlaybooks } from '../engine/project-guidance.js'
6
+ import { buildVars, render, readTemplate } from '../engine/render-templates.js'
7
+ import { buildRunnableFiles } from './compose-files.js'
10
8
  import {
11
9
  contextMd, progressMd, docPlaceholder,
12
10
  editorconfig, prettierrc, prTemplate,
13
- } from './files.js'
14
-
15
- function laravelCompose(answers, stack, vars) {
16
- const laravelDir = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '.' : './backend'
17
- const frontend = stack.frontendKey === 'react'
18
- ? ` frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile.dev\n ports:\n - "5173:5173"\n volumes:\n - ./frontend:/app\n - frontend-node-modules:/app/node_modules\n environment:\n VITE_API_URL: http://localhost:8000\n depends_on:\n - backend\n\n`
19
- : stack.frontendKey === 'nextjs'
20
- ? ` frontend:\n build:\n context: .\n dockerfile: Dockerfile.dev\n ports:\n - "3000:3000"\n volumes:\n - .:/app\n - /app/backend\n - frontend-node-modules:/app/node_modules\n environment:\n NEXT_PUBLIC_API_URL: http://localhost:8000\n depends_on:\n - backend\n\n`
21
- : ''
22
- const frontendVolume = frontend ? ' frontend-node-modules:\n' : ''
23
- return `services:\n${frontend} backend:\n build:\n context: ${laravelDir}\n dockerfile: Dockerfile.dev\n ports:\n - "8000:8000"\n volumes:\n - ${laravelDir}:/app\n - laravel-vendor:/app/vendor\n environment:\n APP_ENV: local\n APP_DEBUG: "true"\n APP_KEY: \${APP_KEY:-}\n DB_CONNECTION: pgsql\n DB_HOST: db\n DB_PORT: 5432\n DB_DATABASE: \${POSTGRES_DB}\n DB_USERNAME: \${POSTGRES_USER}\n DB_PASSWORD: \${POSTGRES_PASSWORD}\n depends_on:\n db:\n condition: service_healthy\n\n db:\n image: ${vars.POSTGRES_IMAGE}\n ports:\n - "5432:5432"\n environment:\n POSTGRES_DB: \${POSTGRES_DB}\n POSTGRES_USER: \${POSTGRES_USER}\n POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}\n volumes:\n - postgres-data:/var/lib/postgresql/data\n healthcheck:\n test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER} -d \${POSTGRES_DB}"]\n interval: 5s\n timeout: 5s\n retries: 10\n\nvolumes:\n${frontendVolume} laravel-vendor:\n postgres-data:\n`
24
- }
11
+ } from '../engine/project-files.js'
12
+ import {
13
+ projectDestinations,
14
+ writeFile as write,
15
+ writeProjectAtomically,
16
+ } from '../engine/write-files.js'
17
+ import { writeRenderedFile as writeTemplate } from '../engine/render-templates.js'
18
+ import { laravelCompose } from './backends/laravel/docker.js'
25
19
 
26
20
  /**
27
21
  * Main entry point — generates the full project
28
22
  */
29
- export async function generateProject(answers, cliRoot) {
23
+ export async function scaffoldProject(answers, cliRoot) {
30
24
  validateAnswers(answers)
31
- const finalDest = path.join(process.cwd(), answers.projectName)
32
- const dest = path.join(process.cwd(), `.${answers.projectName}.tmp-${randomUUID()}`)
33
- const playbooksDir = path.join(cliRoot, 'playbooks')
34
- const ciDir = path.join(cliRoot, 'ci')
25
+ const { finalDestination, stagingDestination } = await projectDestinations(process.cwd(), answers.projectName)
26
+ const playbooksDir = path.join(cliRoot, 'library')
27
+ const ciDir = path.join(cliRoot, 'templates', 'ci')
35
28
  const { profile } = await loadCompatibility(
36
- path.join(cliRoot, 'compatibility/profiles.json'),
29
+ path.join(cliRoot, 'library/tested-versions.json'),
37
30
  answers.compatibilityProfile,
38
31
  )
39
32
  const catalog = await loadCatalog(playbooksDir, profile)
@@ -50,15 +43,12 @@ export async function generateProject(answers, cliRoot) {
50
43
 
51
44
  // 1. Refuse to merge into an existing project. Generation must never silently
52
45
  // overwrite user work.
53
- if (await fs.pathExists(finalDest)) {
54
- throw new Error(`Destination already exists: ${finalDest}`)
55
- }
56
-
57
- try {
46
+ await writeProjectAtomically({
47
+ finalDestination,
48
+ stagingDestination,
49
+ generate: async (dest) => {
58
50
  // 2. Staging folder. It is moved into place only after every generation
59
51
  // step succeeds, so failures never leave a half-written project.
60
- await fs.ensureDir(dest)
61
-
62
52
  // 3. Root documentation and repository files. Directories are created only
63
53
  // when a real generated file needs them; empty architecture theatre is not
64
54
  // part of the scaffold contract.
@@ -82,11 +72,8 @@ export async function generateProject(answers, cliRoot) {
82
72
  // 8. RULES.md
83
73
  const rulesContent = await buildRulesIndex(stack, catalog, playbooksDir)
84
74
  await write(dest, 'RULES.md', rulesContent)
85
- await fs.move(dest, finalDest)
86
- } catch (error) {
87
- await fs.remove(dest)
88
- throw error
89
- }
75
+ },
76
+ })
90
77
  }
91
78
 
92
79
  function validateAnswers(answers) {
@@ -135,6 +122,11 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
135
122
  }
136
123
  await write(dest, '.editorconfig', editorconfig())
137
124
  await write(dest, '.prettierrc', prettierrc())
125
+ if (stack.backendKey === 'springboot') await write(dest, 'backend/.java-version', `${stack.profile.runtimes.java}\n`)
126
+ if (stack.backendKey === 'laravel') {
127
+ const laravelRoot = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
128
+ await write(dest, `${laravelRoot}.php-version`, `${stack.profile.runtimes.php}\n`)
129
+ }
138
130
 
139
131
  // Makefile — template-driven (web only)
140
132
  if (answers.makefile && !stack.isMobile) {
@@ -206,6 +198,40 @@ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
206
198
 
207
199
  // ─── Doc placeholders ─────────────────────────────────────────────────────────
208
200
 
201
+ function toolchainGuide(answers, stack) {
202
+ const javascriptRoot = stack.frontendKey === 'react' ? 'frontend/' : ''
203
+ const hasJavaScript = stack.frontendKey !== 'no-frontend' &&
204
+ !(stack.frontendKey === 'laravel-ui' && stack.laravelUi !== 'inertia-react')
205
+ const rows = []
206
+ if (hasJavaScript) {
207
+ rows.push(`| Node.js | ${stack.profile.runtimes.nodeMinimum}+ (tested ${stack.profile.runtimes.node}) | JavaScript application | \`${javascriptRoot}.node-version\`, \`${javascriptRoot}package.json\` |`)
208
+ rows.push(`| npm | ${stack.profile.runtimes.npmMinimum}+ | Local package scripts | \`${javascriptRoot}package.json#packageManager\`, \`${javascriptRoot}.npmrc\` |`)
209
+ }
210
+ if (stack.backendKey === 'springboot') {
211
+ rows.push(`| Java | ${stack.profile.runtimes.java} | Host-run backend | \`backend/.java-version\`, \`backend/pom.xml\` |`)
212
+ rows.push(`| Maven | ${stack.profile.runtimes.maven} | Optional globally; prefer the generated wrapper | \`backend/mvnw\` or \`backend/mvnw.cmd\` |`)
213
+ }
214
+ if (stack.backendKey === 'laravel') {
215
+ const root = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
216
+ rows.push(`| PHP | ${stack.profile.runtimes.php} | Host-run Laravel backend | \`${root}.php-version\` |`)
217
+ rows.push(`| Composer | ${stack.profile.runtimes.composer} | Host-run Laravel backend | \`${root}composer.json\` |`)
218
+ }
219
+ if (answers.docker || ['supabase', 'postgres'].includes(stack.backendKey)) {
220
+ rows.push('| Docker with Compose | Current supported release | Generated containers and local managed services | `docker-compose.yml` when selected |')
221
+ }
222
+ return `# Toolchain Requirements\n\nInstall only the tools required by the chosen local workflow. ESLint, Prettier, Prisma, TypeScript, and framework CLIs are project dependencies; run them through npm scripts or \`npx\`, never as global installations.\n\n| Tool | Version | When needed | Version source |\n|---|---|---|---|\n${rows.join('\n')}\n\nDependency retries run inside the generated package directory, so npm reads this project's own \`package.json\`, lockfile, engines, and \`.npmrc\`. Different projects can retain different tested dependency versions without global conflicts.\n\n## Host ports\n\nBefore starting containers, check whether the default ports are already in use (for example, \`ss -ltn\` on Linux). Override conflicts when launching Compose with \`FRONTEND_HOST_PORT\`, \`BACKEND_HOST_PORT\`, or \`POSTGRES_HOST_PORT\`; container ports and service-to-service addresses remain unchanged.\n`
223
+ }
224
+
225
+ function developmentEnvironmentGuide(answers, stack) {
226
+ const mobile = stack.isMobile
227
+ ? '\n## Mobile boundary\n\nRun Expo, the iOS Simulator or Android Emulator, and physical-device tooling on the host. Containers may run a selected backend and database, but they do not replace the local device/emulator workflow.\n'
228
+ : ''
229
+ const docker = answers.docker
230
+ ? `\n## Optional Docker workflow\n\nDocker Engine 27 or newer with Docker Compose v2.30 or newer is recommended. Allocate at least 4 GB of memory and 10 GB of free disk space for images, caches, and databases. Check occupied ports before startup with \`ss -ltn\` (Linux) or your platform's equivalent.\n\n\`\`\`bash\ndocker compose config\ndocker compose build\ndocker compose up -d\n\`\`\`\n\nOverride host collisions with \`FRONTEND_HOST_PORT\`, \`BACKEND_HOST_PORT\`, and \`POSTGRES_HOST_PORT\`. Backend runtimes and databases stay inside containers, reducing the host tools you need.\n`
231
+ : '\n## Adding Docker later\n\nDocker files were not selected. Re-run the generator for a fresh project with Docker enabled if you want isolated backend runtimes and databases; the normal host workflow remains fully supported.\n'
232
+ return `# Development Environments\n\n## Default local workflow\n\nRun the generator directly on the host, then use the commands in \`setup.md\`. Docker is optional and is never required to run create-win-project itself. Local files and package-manager metadata remain the source of truth.\n${docker}${mobile}\n## Dev Containers\n\nA generic Dev Container is intentionally not generated: JavaScript, Java, PHP, and mobile stacks need different host/device boundaries. VS Code and Codespaces users can open the generated repository normally and add a stack-specific Dev Container later without changing the supported local or Compose workflows.\n`
233
+ }
234
+
209
235
  async function generateDocs(dest, answers, stack) {
210
236
  const docs = [
211
237
  ['docs/api/overview.md', 'API Overview', 'Base URL, authentication method, and response format.'],
@@ -230,15 +256,27 @@ async function generateDocs(dest, answers, stack) {
230
256
 
231
257
  const setupPath = path.join(dest, 'docs/guides/setup.md')
232
258
  let setupGuide = (await fs.readFile(setupPath, 'utf8'))
233
- .replace('Node.js 20 or newer', `Node.js ${stack.profile.runtimes.node}`)
259
+ .replace('Node.js 20 or newer', `Node.js ${stack.profile.runtimes.nodeMinimum} or newer with npm ${stack.profile.runtimes.npmMinimum} or newer (tested on Node.js ${stack.profile.runtimes.node})`)
234
260
  .replace('Java 21 and Maven 3.9, or Docker', `Java ${stack.profile.runtimes.java}; Maven ${stack.profile.runtimes.maven} or Docker is used through the generated launcher`)
235
261
  .replace('PostgreSQL 16, or Docker', `PostgreSQL ${stack.profile.runtimes.postgres}, or Docker`)
236
262
  .replace('mvn spring-boot:run', './mvnw spring-boot:run # use mvnw.cmd on Windows')
263
+ if (stack.backendKey === 'springboot' && !answers.docker) {
264
+ setupGuide = setupGuide.replace('docker compose up -d db', '# Start PostgreSQL on the host, then configure DATABASE_URL')
265
+ }
237
266
  if (stack.backendKey === 'laravel') {
238
267
  const laravelDir = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
239
- setupGuide = `# Local Setup Guide\n\n## Docker-first setup\n\nOnly Docker with Compose is required. Make is optional; every Make target has a direct Docker equivalent.\n\n\`\`\`bash\ncp ${laravelDir}.env.example ${laravelDir}.env\ndocker compose build\ndocker compose up -d\ndocker compose exec backend php artisan key:generate\ndocker compose exec backend php artisan migrate\n\`\`\`\n\nLater runs use \`docker compose up -d\`; rebuilding is explicit with \`docker compose build\`. If Make is available, \`make setup\` performs the first-run sequence and \`make run\` starts existing images without rebuilding.\n\n## Validate\n\n\`\`\`bash\ndocker compose exec backend composer check\n\`\`\`\n\nWhen dependencies are installed outside Docker, commit \`composer.lock\`${stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react' ? ' and `package-lock.json`' : ''}.\n`
268
+ const frontendSetup = stack.frontendKey === 'react'
269
+ ? '\nIn another terminal:\n\n```bash\ncd frontend\nnpm install\nnpm run dev\n```\n'
270
+ : stack.frontendKey === 'nextjs'
271
+ ? '\nIn another terminal from the repository root:\n\n```bash\nnpm install\nnpm run dev\n```\n'
272
+ : stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react'
273
+ ? '\nIn another terminal from the repository root:\n\n```bash\nnpm install\nnpm run dev\n```\n'
274
+ : ''
275
+ setupGuide = `# Local Setup Guide\n\n## Default local setup\n\nUse PHP ${stack.profile.runtimes.php}, Composer ${stack.profile.runtimes.composer}, and PostgreSQL ${stack.profile.runtimes.postgres}.\n\n\`\`\`bash\ncd ${laravelDir || '.'}\ncomposer install\ncp .env.example .env\nphp artisan key:generate\nphp artisan migrate\nphp artisan serve\n\`\`\`\n${frontendSetup}${answers.docker ? `\n## Optional Docker setup\n\nFrom the repository root:\n\n\`\`\`bash\ndocker compose build\ndocker compose up -d\ndocker compose exec backend php artisan key:generate\ndocker compose exec backend php artisan migrate\n\`\`\`\n\nLater runs use \`docker compose up -d\`; rebuilding remains explicit.\n` : ''}\n## Validate\n\n\`\`\`bash\n${laravelDir ? `cd ${laravelDir}\n` : ''}composer check\n\`\`\`\n\nCommit \`composer.lock\`${stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react' ? ' and `package-lock.json`' : ''}.\n`
240
276
  }
241
277
  await fs.writeFile(setupPath, setupGuide, 'utf8')
278
+ await write(dest, 'docs/guides/toolchain.md', toolchainGuide(answers, stack))
279
+ await write(dest, 'docs/guides/development-environments.md', developmentEnvironmentGuide(answers, stack))
242
280
 
243
281
  const envLocation = stack.frontendKey === 'react' ? '`frontend/.env` for client values' : stack.isMobile ? '`.env`' : '`.env.local`'
244
282
  await write(dest, 'docs/guides/env-variables.md', `# Environment Variables\n\nCopy the generated example before starting. Client environment location: ${envLocation}.\n\n| Variable | Visibility | Required | Purpose |\n|---|---|---:|---|\n${stack.env.map((name) => `| \`${name}\` | ${name.startsWith(stack.envPrefix) ? 'client/public' : 'server only'} | yes | ${environmentPurpose(name)} |`).join('\n')}\n\nValues with \`${stack.envPrefix}\` are bundled into client code and must never contain secrets. Keep real environment files out of version control.\n`)
@@ -344,7 +382,7 @@ async function generateCI(dest, stack, ciDir, answers, vars) {
344
382
  // ─── Runnable application files ─────────────────────────────────────────────────
345
383
 
346
384
  async function generateRunnableFiles(dest, answers, stack, vars) {
347
- const files = { ...buildRunnableFiles(answers, stack, vars), ...buildLaravelFiles(answers, stack, vars) }
385
+ const files = buildRunnableFiles(answers, stack, vars)
348
386
  for (const [filePath, content] of Object.entries(files)) await write(dest, filePath, content)
349
387
  if (stack.backendKey === 'springboot') await fs.chmod(path.join(dest, 'backend/mvnw'), 0o755)
350
388
  if (stack.backendKey === 'laravel') {
@@ -354,13 +392,3 @@ async function generateRunnableFiles(dest, answers, stack, vars) {
354
392
  }
355
393
 
356
394
  // ─── Helpers ──────────────────────────────────────────────────────────────────
357
-
358
- async function write(dest, filePath, content) {
359
- const fullPath = path.join(dest, filePath)
360
- await fs.ensureDir(path.dirname(fullPath))
361
- await fs.writeFile(fullPath, content, 'utf-8')
362
- }
363
-
364
- async function writeTemplate(dest, filePath, content, vars) {
365
- await write(dest, filePath, render(content, vars))
366
- }
@@ -0,0 +1,4 @@
1
+ # Frontend stacks
2
+
3
+ Frontend implementations move here one focused phase at a time. Until then,
4
+ the compatibility exports keep existing `lib` imports stable.
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return [{ template: 'nextjs', path: '.github/workflows/ci-frontend.yml' }]
3
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Builds the files owned by the Next.js frontend. Shared package, testing,
3
+ * feature, and backend integration builders are injected by the scaffold
4
+ * composer so this stack does not depend on unrelated implementations.
5
+ */
6
+ export function buildNextjsFiles(answers, stack, shared) {
7
+ const files = {}
8
+ files['package.json'] = packageFile(answers, stack)
9
+ files['.node-version'] = `${stack.profile.runtimes.node}\n`
10
+ files['.npmrc'] = 'engine-strict=true\n'
11
+ files['tsconfig.json'] = json({
12
+ compilerOptions: {
13
+ target: 'ES2017', lib: ['dom', 'dom.iterable', 'esnext'], allowJs: false,
14
+ skipLibCheck: true, strict: true, noEmit: true, esModuleInterop: true,
15
+ module: 'esnext', moduleResolution: 'bundler', resolveJsonModule: true,
16
+ isolatedModules: true, jsx: 'react-jsx', incremental: true,
17
+ plugins: [{ name: 'next' }], paths: { '@/*': ['./src/*'] },
18
+ },
19
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts', '.next/dev/types/**/*.ts'],
20
+ exclude: ['node_modules'],
21
+ })
22
+ files['next-env.d.ts'] = "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n"
23
+ files['eslint.config.mjs'] = "import { defineConfig, globalIgnores } from 'eslint/config'\nimport nextVitals from 'eslint-config-next/core-web-vitals'\nimport nextTs from 'eslint-config-next/typescript'\n\nexport default defineConfig([\n ...nextVitals,\n ...nextTs,\n globalIgnores(['.next/**', 'out/**', 'next-env.d.ts']),\n])\n"
24
+ files['next.config.ts'] = `import type { NextConfig } from 'next'\n\nconst securityHeaders = [\n { key: 'X-Content-Type-Options', value: 'nosniff' },\n { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },\n { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },\n { key: 'X-Frame-Options', value: 'DENY' },\n]\n\nconst nextConfig: NextConfig = {\n reactStrictMode: true,\n output: 'standalone',\n async headers() { return [{ source: '/(.*)', headers: securityHeaders }] },\n}\n\nexport default nextConfig\n`
25
+ files['postcss.config.mjs'] = stack.styleId === 'tailwind'
26
+ ? "const config = { plugins: { '@tailwindcss/postcss': {} } }\nexport default config\n"
27
+ : "const config = { plugins: {} }\nexport default config\n"
28
+ files['src/app/globals.css'] = `${stack.styleId === 'tailwind' ? '@import "tailwindcss";\n\n' : ''}:root { color-scheme: light dark; font-family: system-ui, sans-serif; }\n* { box-sizing: border-box; }\nbody { margin: 0; min-height: 100vh; }\nmain { max-width: 48rem; margin: 0 auto; padding: 4rem 1.5rem; }\na { color: inherit; }\n`
29
+ files['src/app/layout.tsx'] = `import type { Metadata } from 'next'\nimport type { ReactNode } from 'react'\nimport './globals.css'\n\nexport const metadata: Metadata = { title: ${JSON.stringify(answers.projectName)}, description: ${JSON.stringify(answers.projectDescription)} }\n\nexport default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {\n return <html lang="en"><body>{children}</body></html>\n}\n`
30
+ files['src/app/page.tsx'] = stack.architecture === 'small'
31
+ ? `export default function HomePage() {\n return (\n <main>\n <p>create-win-project</p>\n <h1>Your starter is running</h1>\n <p>{${JSON.stringify(answers.projectDescription)}}</p>\n <p>Read <code>AGENTS.md</code> before your first agent-assisted change.</p>\n </main>\n )\n}\n`
32
+ : `${stack.architecture === 'large'
33
+ ? "import { getStarterStatus, StarterStatus } from '@/features/status'"
34
+ : "import { StarterStatus } from '@/features/status/components/StarterStatus'\nimport { getStarterStatus } from '@/features/status/services/getStarterStatus'"}\n\nexport default function HomePage() {\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`
35
+ files['src/app/api/health/route.ts'] = "export function GET() {\n return Response.json({ status: 'ok' })\n}\n"
36
+ files['src/app/page.test.tsx'] = `import { expect, test } from 'vitest'\nimport { render, screen } from '@testing-library/react'\nimport HomePage from './page'\n\ntest('renders the starter heading', () => {\n render(<HomePage />)\n expect(screen.getByRole('heading', { name: 'Your starter is running' })).toBeInTheDocument()\n})\n`
37
+ if ((answers.testing || 'basic') === 'none') delete files['src/app/page.test.tsx']
38
+ addTestingFiles(files, '', stack, answers.testing || 'basic')
39
+ Object.assign(files, shared.statusFeatureFiles('', stack))
40
+ if (stack.backendKey === 'supabase') Object.assign(files, shared.supabaseWebFiles(true, stack.authentication === 'supabase'))
41
+ if (stack.backendKey === 'postgres') Object.assign(files, shared.prismaFiles())
42
+ return files
43
+ }
44
+ import { json } from '../../shared/javascript-package.js'
45
+ import { addTestingFiles } from '../../shared/testing-files.js'
46
+ import { packageFile } from './dependencies.js'
@@ -0,0 +1,5 @@
1
+ import { buildJavaScriptPackage } from '../../shared/javascript-package.js'
2
+
3
+ export function packageFile(answers, stack) {
4
+ return buildJavaScriptPackage(answers, stack, 'nextjs')
5
+ }
@@ -0,0 +1,3 @@
1
+ export function dockerContributions() {
2
+ return [{ template: 'nextjs', developmentPath: 'Dockerfile.dev', productionPath: 'Dockerfile' }]
3
+ }
@@ -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 = stack.backendKey === 'supabase' ? stack.env : [...publicNames, ...serverNames]
6
+ return { '.env.example': renderEnvironment(names, answers) }
7
+ }
@@ -0,0 +1,35 @@
1
+ import { defineStackAdapter } from '../../rules.js'
2
+ import { ciContributions } from './ci.js'
3
+ import { dockerContributions } from './docker.js'
4
+ import { buildNextjsFiles } from './create-files.js'
5
+
6
+ const verificationCases = Object.freeze([
7
+ Object.freeze({ backend: 'none', styling: 'tailwind', architecture: 'small', authentication: 'public' }),
8
+ Object.freeze({ backend: 'postgres', styling: 'tailwind', architecture: 'medium', authentication: 'undecided' }),
9
+ Object.freeze({ backend: 'supabase', styling: 'tailwind', architecture: 'large', authentication: 'supabase' }),
10
+ Object.freeze({ backend: 'springboot', styling: 'css-modules', architecture: 'medium', authentication: 'session' }),
11
+ Object.freeze({ backend: 'laravel', styling: 'tailwind', architecture: 'medium', authentication: 'sanctum-spa' }),
12
+ ])
13
+
14
+ export const nextjsAdapter = defineStackAdapter({
15
+ id: 'nextjs',
16
+ kind: 'frontend',
17
+ label: 'Next.js',
18
+ compatibleWith: {
19
+ backend: ['none', 'postgres', 'supabase', 'springboot', 'laravel'],
20
+ },
21
+ capabilities: {
22
+ applicationShapes: ['fullstack', 'separate'],
23
+ architectureProfiles: ['small', 'medium', 'large'],
24
+ authenticationModels: ['public', 'undecided', 'supabase', 'session', 'oidc', 'sanctum-spa', 'laravel-oidc'],
25
+ runtime: 'node',
26
+ },
27
+ contributes: {
28
+ files: ({ answers, stack, shared }) => Object.entries(buildNextjsFiles(answers, stack, shared)),
29
+ environment: ({ backend }) => backend.id === 'none' ? [] : ['API_URL'],
30
+ install: () => [{ cwd: '.', command: 'npm', args: ['install'] }],
31
+ docker: dockerContributions,
32
+ ci: ciContributions,
33
+ verification: () => verificationCases,
34
+ },
35
+ })
@@ -0,0 +1,3 @@
1
+ export function ciContributions() {
2
+ return [{ template: 'expo', path: '.github/workflows/ci-frontend.yml' }]
3
+ }
@@ -0,0 +1,25 @@
1
+ import { json } from '../../shared/javascript-package.js'
2
+ import { addTestingFiles } from '../../shared/testing-files.js'
3
+ import { packageFile } from './dependencies.js'
4
+
5
+ export function buildReactNativeFiles(answers, stack, shared) {
6
+ const files = {
7
+ 'package.json': packageFile(answers, stack),
8
+ '.node-version': `${stack.profile.runtimes.node}\n`,
9
+ '.npmrc': 'engine-strict=true\n',
10
+ 'app.json': json({ expo: { name: answers.projectName, slug: answers.projectName, version: '1.0.0', orientation: 'portrait', scheme: answers.projectName, userInterfaceStyle: 'automatic', plugins: stack.authentication === 'supabase' ? ['expo-router', 'expo-secure-store'] : ['expo-router'], experiments: { typedRoutes: true } } }),
11
+ 'tsconfig.json': json({ extends: 'expo/tsconfig.base', compilerOptions: { strict: true, types: ['jest'], paths: { '@/*': ['./*'] } }, include: ['**/*.ts', '**/*.tsx', '.expo/types/**/*.ts', 'expo-env.d.ts'] }),
12
+ 'expo-env.d.ts': "/// <reference types=\"expo/types\" />\n",
13
+ 'app/_layout.tsx': stack.authentication === 'supabase'
14
+ ? `import { Stack } from 'expo-router'\nimport { useEffect } from 'react'\nimport { bindSupabaseAuthLifecycle } from '@/lib/supabase-lifecycle'\n\nexport default function RootLayout() {\n useEffect(() => bindSupabaseAuthLifecycle(), [])\n return <Stack screenOptions={{ headerTitle: '${answers.projectName}' }} />\n}\n`
15
+ : `import { Stack } from 'expo-router'\n\nexport default function RootLayout() { return <Stack screenOptions={{ headerTitle: '${answers.projectName}' }} /> }\n`,
16
+ 'app/index.tsx': stack.architecture === 'small'
17
+ ? `import { StyleSheet, Text, View } from 'react-native'\nimport { SafeAreaView } from 'react-native-safe-area-context'\n\nexport default function HomeScreen() {\n return <SafeAreaView style={styles.safe}><View style={styles.container}><Text>create-win-project</Text><Text accessibilityRole="header">Your starter is running</Text><Text>{${JSON.stringify(answers.projectDescription)}}</Text></View></SafeAreaView>\n}\nconst styles = StyleSheet.create({ safe: { flex: 1 }, container: { flex: 1, justifyContent: 'center', padding: 24, gap: 12 } })\n`
18
+ : `import { StyleSheet, Text, View } from 'react-native'\nimport { SafeAreaView } from 'react-native-safe-area-context'\n${stack.architecture === 'large'
19
+ ? "import { getStarterStatus, StarterStatus } from '@/features/status'"
20
+ : "import { StarterStatus } from '@/features/status/components/StarterStatus'\nimport { getStarterStatus } from '@/features/status/services/getStarterStatus'"}\n\nexport default function HomeScreen() {\n const status = getStarterStatus()\n return <SafeAreaView style={styles.safe}><View style={styles.container}><Text>create-win-project</Text><StarterStatus status={status} /><Text>{${JSON.stringify(answers.projectDescription)}}</Text></View></SafeAreaView>\n}\nconst styles = StyleSheet.create({ safe: { flex: 1 }, container: { flex: 1, justifyContent: 'center', padding: 24, gap: 12 } })\n`,
21
+ }
22
+ addTestingFiles(files, '', stack, answers.testing || 'basic')
23
+ Object.assign(files, shared.nativeStatusFeatureFiles(stack))
24
+ return files
25
+ }
@@ -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-native')
5
+ }
@@ -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
+ }