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,188 @@
1
+ export { configurationDecisionChoices, lastEnabledIndex, promptWithBack, wrapText } from './navigation.js'
2
+ import chalk from 'chalk'
3
+ import {
4
+ architectureChoicesFor,
5
+ resolveStack,
6
+ stylingChoicesFor,
7
+ } from '../engine/load-library.js'
8
+ import {
9
+ APPLICATION_SHAPES,
10
+ applicationShapeChoices,
11
+ backendChoicesForShape,
12
+ frontendChoicesForShape,
13
+ } from '../engine/project-shapes.js'
14
+ import { laravelUiPromptContribution } from '../stacks/backends/laravel/ui/index.js'
15
+
16
+ const CONCERN_LABELS = {
17
+ validation: 'Runtime validation', query: 'Server-state caching', state: 'Shared UI state',
18
+ env: 'Environment validation', 'url-state': 'URL state', 'safe-action': 'Type-safe server actions',
19
+ 'dark-mode': 'Dark mode', 'http-client': 'HTTP client',
20
+ }
21
+
22
+ function choice(title, value, { hint, recommended } = {}) {
23
+ const suffix = [hint ? `(${hint})` : '', recommended ? chalk.green('Recommended') : '']
24
+ .filter(Boolean).join(' ')
25
+ return { name: suffix ? `${title} ${suffix}` : title, short: title, value }
26
+ }
27
+
28
+ function footerMarker(text) {
29
+ return { type: 'description-footer', text }
30
+ }
31
+
32
+ export function exampleForShape(shape) {
33
+ const description = APPLICATION_SHAPES[shape]?.description
34
+ const match = description?.match(/Example:\s*(.+?)\.?$/)
35
+ return match ? match[1] : undefined
36
+ }
37
+
38
+ function describeExisting(entries, description, hintFor) {
39
+ return [
40
+ ...entries.map((entry) => choice(
41
+ entry.short || entry.name.split('\n')[0].replace(' (Recommended)', ''),
42
+ entry.value,
43
+ { hint: hintFor?.(entry.value), recommended: entry.name.includes('(Recommended)') },
44
+ )),
45
+ footerMarker(description),
46
+ ]
47
+ }
48
+
49
+ function validationError(problem, recovery) {
50
+ return `${chalk.red(`Error: ${problem}`)} ${chalk.yellow(recovery)}`
51
+ }
52
+
53
+ export function buildQuestions({ args, catalog }) {
54
+ return [
55
+ {
56
+ type: 'list', name: 'applicationShape', message: 'Application shape',
57
+ choices: describeExisting(
58
+ applicationShapeChoices(),
59
+ 'Choose the runtime layout that matches how this application will be deployed.',
60
+ (shape) => {
61
+ const example = exampleForShape(shape)
62
+ return example ? `e.g. ${example}` : undefined
63
+ },
64
+ ), default: 'fullstack', when: () => !args.shape,
65
+ },
66
+ {
67
+ type: 'input', name: 'projectName', message: 'Project name', default: 'my-project',
68
+ validate: (value) => {
69
+ if (!value.trim()) return validationError('Project name is required.', 'Enter a lowercase name such as my-project.')
70
+ if (!/^[a-z0-9-]+$/.test(value)) return validationError('The name contains unsupported characters.', 'Use lowercase letters, numbers, and hyphens only.')
71
+ return true
72
+ },
73
+ },
74
+ { type: 'input', name: 'projectDescription', message: 'One-line description', default: 'A new application' },
75
+ {
76
+ type: 'list', name: 'frontend', message: 'Application framework',
77
+ choices: (answers) => describeExisting(frontendChoicesForShape(args.shape || answers.applicationShape, catalog), 'The primary user-facing application framework.'),
78
+ when: () => !args.frontend,
79
+ },
80
+ {
81
+ type: 'list', name: 'backend', message: 'Backend or data service',
82
+ choices: (answers) => describeExisting(backendChoicesForShape(args.shape || answers.applicationShape, answers.frontend, catalog), 'The server or managed data boundary used by this application.'),
83
+ when: () => !args.backend,
84
+ },
85
+ ...laravelUiPromptContribution(args.laravelUi).questions,
86
+ {
87
+ type: 'list', name: 'styling', message: 'Styling approach',
88
+ choices: (answers) => describeExisting(stylingChoicesFor(catalog, answers.frontend), 'The default styling system for generated UI.'),
89
+ when: (answers) => stylingChoicesFor(catalog, answers.frontend).length > 1,
90
+ },
91
+ {
92
+ type: 'list', name: 'architecture',
93
+ message: 'Architecture depth',
94
+ choices: (answers) => {
95
+ const supported = architectureChoicesFor(catalog, answers.frontend, answers.backend)
96
+ return [
97
+ ...[
98
+ choice('Medium', 'medium', { recommended: true }),
99
+ choice('Small', 'small'),
100
+ choice('Large', 'large'),
101
+ ].filter((choice) => supported.includes(choice.value)),
102
+ footerMarker('Clear feature boundaries for most long-term applications; fewer layers for prototypes; enforced boundaries for complex domains.'),
103
+ ]
104
+ },
105
+ default: 'medium', when: () => !args.architecture,
106
+ },
107
+ {
108
+ type: 'list', name: 'authentication',
109
+ message: (answers) => ['supabase', 'springboot', 'laravel', 'fastapi'].includes(answers.backend)
110
+ ? 'User authentication'
111
+ : `User authentication ${chalk.yellow(`(generation unavailable for ${answers.backend === 'none' ? 'a frontend-only project' : 'a PostgreSQL-only backend'})`)}`,
112
+ choices: (answers) => {
113
+ const choices = []
114
+ if (['supabase', 'springboot', 'laravel', 'fastapi'].includes(answers.backend)) {
115
+ choices.push(choice('Yes', 'yes'))
116
+ }
117
+ choices.push(
118
+ choice('Not yet', 'not-yet', { recommended: true }),
119
+ choice('No', 'none'),
120
+ footerMarker('Generate working authentication for this stack, record guidance without pretending login exists, or stay intentionally public.'),
121
+ )
122
+ return choices
123
+ },
124
+ default: 'not-yet', when: () => !args.authentication,
125
+ },
126
+ {
127
+ type: 'list', name: 'authAudience', message: 'Where will users access the application?',
128
+ choices: [
129
+ { name: 'Website only — use a secure server-managed browser session', value: 'website' },
130
+ { name: 'Website and mobile — use a trusted identity provider for every client', value: 'multi-client' },
131
+ ],
132
+ default: 'website',
133
+ when: (answers) => ['springboot', 'laravel', 'fastapi'].includes(answers.backend) && answers.frontend !== 'laravel-ui' &&
134
+ (args.authentication || answers.authentication) === 'yes' && !args.authAudience,
135
+ },
136
+ {
137
+ type: 'list', name: 'uploads', message: 'User-provided file uploads',
138
+ choices: [{ name: 'None', value: 'none' }, { name: 'Private object storage', value: 'object-storage' }],
139
+ default: 'none', when: () => !args.uploads,
140
+ },
141
+ {
142
+ type: 'list', name: 'backgroundJobs', message: 'Background work',
143
+ choices: [{ name: 'None', value: 'none' }, { name: 'Durable queue', value: 'queue' }],
144
+ default: 'none', when: (answers) => catalog.byId[answers.frontend]?.platform !== 'mobile' && !args.backgroundJobs,
145
+ },
146
+ {
147
+ type: 'list', name: 'offline', message: 'Offline behavior',
148
+ choices: [{ name: 'None', value: 'none' }, { name: 'Local cache', value: 'cache' }, { name: 'Synchronization', value: 'sync' }],
149
+ default: 'none', when: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile' && !args.offline,
150
+ },
151
+ {
152
+ type: 'confirm', name: 'docker', message: 'Add optional Docker development files', default: false,
153
+ when: (answers) => Boolean(catalog.byId[answers.backend]?.needsDocker) || catalog.byId[answers.frontend]?.platform !== 'mobile',
154
+ },
155
+ {
156
+ type: 'confirm', name: 'makefile', message: 'Include Makefile?', default: true,
157
+ when: (answers) => catalog.byId[answers.frontend]?.platform !== 'mobile',
158
+ },
159
+ { type: 'confirm', name: 'githubActions', message: 'Include GitHub Actions CI?', default: true },
160
+ {
161
+ type: 'confirm', name: 'installDependencies',
162
+ message: 'Install project dependencies and create the lockfile now?', default: true,
163
+ when: () => !args.install && !args.noInstall,
164
+ },
165
+ {
166
+ type: 'input', name: 'packageName', message: 'Java package name? (e.g. com.yourname)', default: 'com.app',
167
+ when: (answers) => answers.backend === 'springboot',
168
+ validate: (value) => {
169
+ if (!value.trim()) return 'Package name is required'
170
+ if (!/^[a-z]+(\.[a-z]+)+$/.test(value)) return 'Use format: com.yourname'
171
+ return true
172
+ },
173
+ },
174
+ {
175
+ type: 'checkbox', name: 'expectedConcerns',
176
+ message: 'Planning notes for optional features (advisory only; no libraries or feature code are generated)',
177
+ choices: (answers) => {
178
+ const stack = resolveStack({
179
+ ...answers,
180
+ styling: answers.styling || catalog.byId[answers.frontend]?.stylingOptions?.[0],
181
+ }, catalog)
182
+ const notes = [...new Set(stack.concerns.filter((concern) => !concern.required).map((concern) => concern.id))]
183
+ .map((id) => choice(CONCERN_LABELS[id] || id.replaceAll('-', ' '), id))
184
+ return [...notes, footerMarker('Write selected concerns to CONTEXT.md as planning notes.')]
185
+ },
186
+ },
187
+ ]
188
+ }
@@ -0,0 +1,158 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ function numericVersion(value) {
6
+ const match = String(value || '').match(/\d+(?:\.\d+){0,2}/)
7
+ return match ? match[0].split('.').map(Number) : null
8
+ }
9
+
10
+ export function versionAtLeast(found, required) {
11
+ const actual = numericVersion(found)
12
+ const minimum = numericVersion(required)
13
+ if (!actual || !minimum) return false
14
+ for (let index = 0; index < 3; index += 1) {
15
+ const difference = (actual[index] || 0) - (minimum[index] || 0)
16
+ if (difference !== 0) return difference > 0
17
+ }
18
+ return true
19
+ }
20
+
21
+ function probe(command, args = ['--version'], run = spawnSync) {
22
+ const result = run(command, args, { encoding: 'utf8', shell: false })
23
+ if (result.error || result.status !== 0) return null
24
+ return `${result.stdout || result.stderr || ''}`.trim().split('\n')[0] || null
25
+ }
26
+
27
+ export function detectSystemVersions({ run = spawnSync, nodeVersion = process.versions.node } = {}) {
28
+ return {
29
+ node: nodeVersion,
30
+ npm: probe(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['--version'], run),
31
+ php: probe('php', ['--version'], run),
32
+ composer: probe(process.platform === 'win32' ? 'composer.bat' : 'composer', ['--version'], run),
33
+ python: probe('python', ['--version'], run),
34
+ uv: probe('uv', ['--version'], run),
35
+ }
36
+ }
37
+
38
+ export function needsNpmInstallation(stack) {
39
+ return stack.frontendKey !== 'no-frontend' &&
40
+ (stack.frontendKey !== 'laravel-ui' || stack.laravelUi === 'inertia-react')
41
+ }
42
+
43
+ export function installationIssues(stack, profile, versions) {
44
+ const issues = []
45
+ if (needsNpmInstallation(stack)) {
46
+ const minimumNode = profile.runtimes.nodeMinimum
47
+ const minimumNpm = profile.runtimes.npmMinimum
48
+ if (!versionAtLeast(versions.node, minimumNode)) {
49
+ issues.push({ tool: 'Node.js', found: versions.node, required: minimumNode })
50
+ }
51
+ if (!versionAtLeast(versions.npm, minimumNpm)) {
52
+ issues.push({ tool: 'npm', found: versions.npm, required: minimumNpm })
53
+ }
54
+ }
55
+ if (stack.backendKey === 'laravel') {
56
+ if (!versionAtLeast(versions.php, profile.runtimes.php)) {
57
+ issues.push({ tool: 'PHP', found: versions.php, required: profile.runtimes.php })
58
+ }
59
+ if (!versionAtLeast(versions.composer, profile.runtimes.composer)) {
60
+ issues.push({ tool: 'Composer', found: versions.composer, required: profile.runtimes.composer })
61
+ }
62
+ }
63
+ if (stack.backendKey === 'fastapi') {
64
+ if (!versionAtLeast(versions.python, profile.runtimes.python)) {
65
+ issues.push({ tool: 'Python', found: versions.python, required: profile.runtimes.python })
66
+ }
67
+ if (!versionAtLeast(versions.uv, profile.runtimes.uv)) {
68
+ issues.push({ tool: 'uv', found: versions.uv, required: profile.runtimes.uv })
69
+ }
70
+ }
71
+ return issues
72
+ }
73
+
74
+ export async function decideInstallation({ requested, issues, interactive, choose }) {
75
+ if (!requested) return { install: false, reason: 'not-requested' }
76
+ if (issues.length === 0) return { install: true, reason: 'supported' }
77
+ if (!interactive) return { install: false, reason: 'runtime-mismatch' }
78
+
79
+ while (true) {
80
+ const decision = await choose()
81
+ if (decision === 'skip') return { install: false, reason: 'runtime-mismatch' }
82
+ if (decision === 'cancel') return { install: false, reason: 'cancelled' }
83
+ if (decision !== 'instructions') throw new Error(`Unknown runtime decision: ${decision}`)
84
+ return { install: false, reason: 'show-instructions' }
85
+ }
86
+ }
87
+
88
+ export function runtimeSetupInstructions(profile, issues = []) {
89
+ const tools = new Set(issues.map((issue) => issue.tool))
90
+ const instructions = []
91
+ if (tools.has('Node.js') || tools.has('npm')) {
92
+ instructions.push(
93
+ `Mise: mise use node@${profile.runtimes.node}`,
94
+ `NVM: nvm install ${profile.runtimes.node} && nvm use ${profile.runtimes.node}`,
95
+ 'Verify: node --version && npm --version',
96
+ )
97
+ }
98
+ if (tools.has('PHP') || tools.has('Composer')) {
99
+ instructions.push(
100
+ `Install PHP ${profile.runtimes.php} and Composer ${profile.runtimes.composer}, or use the generated Docker workflow.`,
101
+ 'Verify: php --version && composer --version',
102
+ )
103
+ }
104
+ if (tools.has('Python') || tools.has('uv')) {
105
+ instructions.push(
106
+ `Install Python ${profile.runtimes.python} and uv ${profile.runtimes.uv} (https://docs.astral.sh/uv/), or use the generated Docker workflow.`,
107
+ 'Verify: python --version && uv --version',
108
+ )
109
+ }
110
+ return instructions
111
+ }
112
+
113
+ // ─── Doctor (from lib/doctor.js) ───────────────────────────────────────────
114
+ function isOnPath(command) {
115
+ const suffixes = process.platform === 'win32' ? ['', '.cmd', '.exe', '.bat'] : ['']
116
+ return (process.env.PATH || '').split(path.delimiter).some((directory) =>
117
+ suffixes.some((suffix) => existsSync(path.join(directory, `${command}${suffix}`)))
118
+ )
119
+ }
120
+
121
+ function probeDoctor(command, args = ['--version']) {
122
+ const result = spawnSync(command, args, { encoding: 'utf8', shell: false })
123
+ const version = `${result.stdout || result.stderr || ''}`.trim().split('\n')[0]
124
+ if (!version) return isOnPath(command) ? 'available (version unavailable)' : null
125
+ if (result.status !== 0) return null
126
+ return version
127
+ }
128
+
129
+ export function collectDiagnostics(profile) {
130
+ const nodeVersion = process.versions.node
131
+ const npmVersion = probeDoctor('npm')
132
+ const dockerVersion = probeDoctor('docker')
133
+ const composeVersion = dockerVersion ? probeDoctor('docker', ['compose', 'version']) : null
134
+ const javaVersion = probeDoctor('java', ['-version'])
135
+ const pythonVersion = probeDoctor('python')
136
+ const uvVersion = probeDoctor('uv')
137
+ return [
138
+ { name: 'Node.js', found: nodeVersion, expected: profile.runtimes.node, required: false },
139
+ { name: 'npm', found: npmVersion, required: false },
140
+ { name: 'Docker', found: dockerVersion, required: false },
141
+ { name: 'Docker Compose', found: composeVersion, required: false },
142
+ { name: 'Java', found: javaVersion, expected: profile.runtimes.java, required: false },
143
+ { name: 'Python', found: pythonVersion, expected: profile.runtimes.python, required: false },
144
+ { name: 'uv', found: uvVersion, expected: profile.runtimes.uv, required: false },
145
+ ]
146
+ }
147
+
148
+ export function printDoctor(profile, output = console.log, diagnostics = collectDiagnostics(profile)) {
149
+ output(`create-win-project doctor (profile ${profile.id})`)
150
+ output('Use either the Node/npm lane or the Docker/Compose lane; Java is only needed for host-run Spring projects.')
151
+ for (const item of diagnostics) {
152
+ const expectation = item.expected ? ` (tested: ${item.expected})` : ''
153
+ output(`${item.found ? '✓' : '○'} ${item.name}: ${item.found || 'not found'}${expectation}`)
154
+ }
155
+ output('')
156
+ output('Node lane: npm ci && npm start')
157
+ output('Docker lane: docker compose build && docker compose run --rm app')
158
+ }
@@ -0,0 +1,10 @@
1
+ import { scaffoldProject } from '../stacks/create-project.js'
2
+
3
+ /**
4
+ * Runs the generic project-creation pipeline. Stack-owned contributions are
5
+ * composed behind the shared stack boundary and are migrated into their
6
+ * individual adapters in the following phases.
7
+ */
8
+ export async function generateProject(answers, cliRoot) {
9
+ return scaffoldProject(answers, cliRoot)
10
+ }
@@ -0,0 +1,13 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ export function runDependencySteps(steps, run = spawnSync) {
4
+ for (const step of steps) {
5
+ const result = run(step.command, step.args, {
6
+ cwd: step.cwd,
7
+ stdio: 'inherit',
8
+ shell: false,
9
+ })
10
+ if (result.error || result.status !== 0) return { ok: false, failed: step, result }
11
+ }
12
+ return { ok: true, failed: null }
13
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
- import { loadCompatibility, resolvePackages } from './compatibility.js'
4
- import { inferApplicationShape, validateApplicationShape } from './application-shapes.js'
3
+ import { loadCompatibility, resolvePackages } from './tested-versions.js'
4
+ import { inferApplicationShape, validateApplicationShape } from './project-shapes.js'
5
5
 
6
6
  // ─── Loader ───────────────────────────────────────────────────────────────────
7
7
 
@@ -12,11 +12,10 @@ export async function loadCatalog(playbooksDir, selectedProfile) {
12
12
  for (const e of entries) {
13
13
  const full = path.join(dir, e.name)
14
14
  if (e.isDirectory()) { await walk(full); continue }
15
- if (e.name.endsWith('.manifest.json')) {
15
+ if (e.name === 'definition.json') {
16
16
  const m = await fs.readJson(full)
17
17
  m._file = full
18
- const defaultPlaybook = path.relative(playbooksDir, full).replace(/\.manifest\.json$/, '.md')
19
- m._playbooks = m.playbooks || [m.file || defaultPlaybook]
18
+ m._playbooks = m.playbooks || (m.file ? [m.file] : [])
20
19
  const firstPlaybook = m._playbooks[0]
21
20
  m._playbook = typeof firstPlaybook === 'string' ? firstPlaybook : firstPlaybook?.file
22
21
  manifests.push(m)
@@ -26,7 +25,7 @@ export async function loadCatalog(playbooksDir, selectedProfile) {
26
25
  await walk(playbooksDir)
27
26
  validateManifests(manifests)
28
27
  const profile = selectedProfile || (await loadCompatibility(
29
- path.join(path.dirname(playbooksDir), 'compatibility/profiles.json'),
28
+ path.join(playbooksDir, 'tested-versions.json'),
30
29
  )).profile
31
30
  return { ...buildIndex(manifests), profile }
32
31
  }
@@ -191,7 +190,8 @@ export function resolveStack(answers, catalog) {
191
190
  if (fe.id === 'laravel-ui') authentication = 'laravel-session'
192
191
  else authentication = authAudience === 'website' && fe.platform === 'web' ? 'sanctum-spa' : 'laravel-oidc'
193
192
  }
194
- else throw new Error(`Authentication generation requires Supabase or Spring Boot for ${fe.label}`)
193
+ else if (be.id === 'fastapi') authentication = 'oidc'
194
+ else throw new Error(`Authentication generation requires Supabase, Spring Boot, Laravel, or FastAPI for ${fe.label}`)
195
195
  }
196
196
 
197
197
  // ── Collect selected manifests ───────────────────────────────────────────
@@ -278,7 +278,7 @@ export function resolveStack(answers, catalog) {
278
278
  // Apply env prefix: clientEnv vars get fe.envPrefix, others stay as-is
279
279
  const envPrefix = fe.envPrefix || ''
280
280
  if (authentication === 'session') rawEnv.push('SPRING_SECURITY_USER_NAME', 'SPRING_SECURITY_USER_PASSWORD')
281
- if (authentication === 'oidc') rawEnv.push('OIDC_ISSUER_URI', 'OIDC_AUDIENCE')
281
+ if (authentication === 'oidc' && be.id === 'springboot') rawEnv.push('OIDC_ISSUER_URI', 'OIDC_AUDIENCE')
282
282
  if (authentication === 'laravel-session') rawEnv.push('SESSION_DOMAIN')
283
283
  if (authentication === 'sanctum-spa') rawEnv.push('SESSION_DOMAIN', 'SANCTUM_STATEFUL_DOMAINS', 'CORS_ALLOWED_ORIGINS')
284
284
  if (authentication === 'laravel-oidc') rawEnv.push('AUTH0_DOMAIN', 'AUTH0_AUDIENCE')
@@ -3,7 +3,7 @@
3
3
  // Static helpers only — no stack identity checks. Stack-specific files are
4
4
  // now template-driven via templates/ and readTemplate().
5
5
 
6
- export function contextMd(vars, expectedConcerns) {
6
+ export function contextMd(vars, expectedConcerns, capabilities = {}) {
7
7
  const concernsBlock = (expectedConcerns && expectedConcerns.length)
8
8
  ? expectedConcerns.map((c) => `- ${c}`).join('\n')
9
9
  : '- (none selected — all optional concerns remain available)'
@@ -23,6 +23,15 @@ export function contextMd(vars, expectedConcerns) {
23
23
  ## Key Decisions
24
24
  <!-- Architecture and tech decisions made during the project -->
25
25
 
26
+ - Production baseline: generated default; deviations require approval.
27
+ - Uploads: ${capabilities.uploads || 'none'}
28
+ - Background jobs: ${capabilities.backgroundJobs || 'none'}
29
+ - Offline: ${capabilities.offline || 'none'}
30
+
31
+ ## Approved Deviations
32
+ <!-- Record date, approver, rationale, affected files, and recovery path. -->
33
+ - (none)
34
+
26
35
  ## Out of Scope
27
36
  <!-- What this project explicitly does NOT do -->
28
37
 
@@ -32,8 +32,31 @@ export function extractSections(content) {
32
32
  * Resolve a playbook relative path to an absolute path.
33
33
  * Handles nested stack facets and concern playbooks transparently.
34
34
  */
35
- export async function resolvePlaybook(playbooksDir, file) {
36
- return path.join(playbooksDir, file)
35
+ export async function resolvePlaybook(libraryDir, file) {
36
+ const [category, ...restParts] = file.split('/')
37
+ const rest = restParts.join('/')
38
+ const name = path.basename(file, '.md')
39
+ const candidates = {
40
+ universal: [path.join(libraryDir, 'universal', rest), path.join(libraryDir, 'universal', name, path.basename(file))],
41
+ platform: [path.join(libraryDir, 'platforms', rest), path.join(libraryDir, 'platforms', name, path.basename(file))],
42
+ stack: [path.join(libraryDir, 'stacks', rest)],
43
+ capabilities: [
44
+ path.join(libraryDir, 'features', rest),
45
+ path.join(libraryDir, 'development-tools', rest),
46
+ ],
47
+ concerns: [path.join(libraryDir, 'optional-features', 'concerns', rest)],
48
+ styling: [
49
+ path.join(libraryDir, 'optional-features', 'styling', rest),
50
+ path.join(libraryDir, 'optional-features', 'styling', name, path.basename(file)),
51
+ path.join(libraryDir, 'optional-features', 'styling', name.replace(/-extensions$/, ''), path.basename(file)),
52
+ ],
53
+ devops: [path.join(libraryDir, 'development-tools', 'devops', name, path.basename(file))],
54
+ }[category] || []
55
+
56
+ for (const candidate of candidates) {
57
+ if (await fs.pathExists(candidate)) return candidate
58
+ }
59
+ return candidates[0] || path.join(libraryDir, file)
37
60
  }
38
61
 
39
62
  /**
@@ -162,7 +185,7 @@ export async function buildRulesIndex(stack, catalog, playbooksDir) {
162
185
  export async function copySelectedPlaybooks(playbooksDir, dest, stack) {
163
186
  const allFiles = collectPlaybookFiles(stack)
164
187
  for (const file of allFiles) {
165
- const src = path.join(playbooksDir, file)
188
+ const src = await resolvePlaybook(playbooksDir, file)
166
189
  if (await fs.pathExists(src)) {
167
190
  const dst = path.join(dest, 'playbooks', file)
168
191
  await fs.ensureDir(path.dirname(dst))
@@ -29,13 +29,13 @@ const VALID_COMBINATIONS = Object.freeze({
29
29
  ['laravel-ui', 'laravel'],
30
30
  ],
31
31
  separate: [
32
- ['nextjs', 'springboot'], ['nextjs', 'laravel'],
33
- ['react', 'springboot'], ['react', 'laravel'], ['react', 'supabase'],
32
+ ['nextjs', 'springboot'], ['nextjs', 'laravel'], ['nextjs', 'fastapi'],
33
+ ['react', 'springboot'], ['react', 'laravel'], ['react', 'supabase'], ['react', 'fastapi'],
34
34
  ],
35
- api: [['no-frontend', 'springboot'], ['no-frontend', 'laravel']],
35
+ api: [['no-frontend', 'springboot'], ['no-frontend', 'laravel'], ['no-frontend', 'fastapi']],
36
36
  mobile: [
37
37
  ['react-native', 'none'], ['react-native', 'supabase'],
38
- ['react-native', 'springboot'], ['react-native', 'laravel'],
38
+ ['react-native', 'springboot'], ['react-native', 'laravel'], ['react-native', 'fastapi'],
39
39
  ],
40
40
  frontend: [['react', 'none']],
41
41
  })
@@ -2,6 +2,7 @@
2
2
 
3
3
  import fs from 'fs-extra'
4
4
  import path from 'path'
5
+ import { writeFile } from './write-files.js'
5
6
 
6
7
  export function render(content, vars) {
7
8
  return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`)
@@ -14,6 +15,13 @@ export async function readTemplate(templatesDir, category, name, ext = '') {
14
15
  return null
15
16
  }
16
17
 
18
+ export async function writeRenderedFile(destination, filePath, template, vars) {
19
+ const content = render(template, vars)
20
+ await writeFile(destination, filePath, content)
21
+ }
22
+
23
+ export * from './write-files.js'
24
+
17
25
  export function buildVars(answers, stack) {
18
26
  const packagePath = (answers.packageName ?? 'com.app').replace(/\./g, '/')
19
27
  const profile = stack.profile
@@ -46,6 +54,8 @@ export function buildVars(answers, stack) {
46
54
  SPRING_MODULITH_VERSION: profile.springModulith,
47
55
  PHP_VERSION: profile.runtimes.php,
48
56
  COMPOSER_VERSION: profile.runtimes.composer,
57
+ PYTHON_VERSION: profile.runtimes.python,
58
+ UV_VERSION: profile.runtimes.uv,
49
59
  LARAVEL_VERSION: profile.composerPackages['laravel/framework'],
50
60
  LARAVEL_DIR: ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '.' : 'backend',
51
61
  TESTCONTAINERS_VERSION: profile.packages["testcontainers-junit-jupiter"] || profile.packages["testcontainers-postgresql"] || "1.19.8",
@@ -57,6 +67,7 @@ export function buildVars(answers, stack) {
57
67
  NGINX_IMAGE: `${profile.images.nginx.repository}:${profile.images.nginx.tag}`,
58
68
  PHP_IMAGE: `${profile.images.php.repository}:${profile.images.php.tag}`,
59
69
  COMPOSER_IMAGE: `${profile.images.composer.repository}:${profile.images.composer.tag}`,
70
+ PYTHON_IMAGE: `${profile.images.python.repository}:${profile.images.python.tag}`,
60
71
  COMPATIBILITY_PROFILE: profile.id,
61
72
  }
62
73
  }
@@ -20,6 +20,7 @@ export function validateCompatibility(catalog) {
20
20
  const statuses = new Map()
21
21
  let packageNames
22
22
  let composerPackageNames
23
+ let pythonPackageNames
23
24
  for (const [id, profile] of Object.entries(catalog.profiles)) {
24
25
  if (!/^\d{4}\.\d{2}$/.test(id)) throw new Error(`Invalid compatibility profile id: ${id}`)
25
26
  if (!['current', 'previous'].includes(profile.status)) throw new Error(`Invalid status for profile ${id}`)
@@ -30,10 +31,10 @@ export function validateCompatibility(catalog) {
30
31
  statuses.set(profile.status, id)
31
32
  if (!EXACT_VERSION.test(profile.springBoot || '')) throw new Error(`Profile ${id} needs an exact Spring Boot version`)
32
33
  if (!EXACT_VERSION.test(profile.springModulith || '')) throw new Error(`Profile ${id} needs an exact Spring Modulith version`)
33
- for (const key of ['node', 'java', 'maven', 'postgres', 'php', 'composer']) {
34
+ for (const key of ['node', 'java', 'maven', 'postgres', 'php', 'composer', 'python', 'uv']) {
34
35
  if (!profile.runtimes?.[key]) throw new Error(`Profile ${id} is missing runtime ${key}`)
35
36
  }
36
- for (const key of ['node', 'maven', 'java', 'postgres', 'nginx', 'php', 'composer']) {
37
+ for (const key of ['node', 'maven', 'java', 'postgres', 'nginx', 'php', 'composer', 'python']) {
37
38
  const image = profile.images?.[key]
38
39
  if (!image?.repository || !image?.tag || image.tag === 'latest') {
39
40
  throw new Error(`Profile ${id} has an invalid ${key} image`)
@@ -57,6 +58,9 @@ export function validateCompatibility(catalog) {
57
58
  if (profile.images.composer.repository !== 'composer' || profile.images.composer.tag !== profile.runtimes.composer) {
58
59
  throw new Error(`Profile ${id} Composer image must match runtime ${profile.runtimes.composer}`)
59
60
  }
61
+ if (profile.images.python.repository !== 'python' || !profile.images.python.tag.startsWith(`${profile.runtimes.python}-`)) {
62
+ throw new Error(`Profile ${id} Python image must match runtime ${profile.runtimes.python}`)
63
+ }
60
64
  const names = Object.keys(profile.packages || {}).sort()
61
65
  if (!names.length) throw new Error(`Profile ${id} has no package versions`)
62
66
  for (const [name, value] of Object.entries(profile.packages)) {
@@ -80,6 +84,15 @@ export function validateCompatibility(catalog) {
80
84
  throw new Error(`Profile ${id} does not own the same Composer package set as the current profile`)
81
85
  }
82
86
  composerPackageNames = composerNames
87
+ const pythonNames = Object.keys(profile.pythonPackages || {}).sort()
88
+ if (!pythonNames.length) throw new Error(`Profile ${id} has no Python package versions`)
89
+ for (const [name, version] of Object.entries(profile.pythonPackages)) {
90
+ if (!EXACT_VERSION.test(version || '')) throw new Error(`Profile ${id} Python package ${name} must use an exact version`)
91
+ }
92
+ if (pythonPackageNames && JSON.stringify(pythonNames) !== JSON.stringify(pythonPackageNames)) {
93
+ throw new Error(`Profile ${id} does not own the same Python package set as the current profile`)
94
+ }
95
+ pythonPackageNames = pythonNames
83
96
  }
84
97
  if (!statuses.has('current') || !statuses.has('previous')) {
85
98
  throw new Error('Exactly one current and one previous compatibility profile are required')
@@ -114,3 +127,9 @@ export function composerPackageVersion(profile, name, owner = name) {
114
127
  if (!version) throw new Error(`${owner} requires ${name}, missing from compatibility profile ${profile.id}`)
115
128
  return version
116
129
  }
130
+
131
+ export function pythonPackageVersion(profile, name, owner = name) {
132
+ const version = profile.pythonPackages?.[name]
133
+ if (!version) throw new Error(`${owner} requires ${name}, missing from compatibility profile ${profile.id}`)
134
+ return version
135
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'fs-extra'
2
+ import path from 'node:path'
3
+ import { loadCompatibility } from './tested-versions.js'
4
+
5
+ export async function createUpgradeReport(projectPath, compatibilityFile) {
6
+ const profilePath = path.join(path.resolve(projectPath), 'create-win-project.profile.json')
7
+ const existing = await fs.readJson(profilePath).catch(() => { throw new Error(`No create-win-project profile found at ${profilePath}`) })
8
+ const { profile: current } = await loadCompatibility(compatibilityFile)
9
+ const previousId = existing.compatibilityProfile?.id
10
+ const differences = []
11
+ if (existing.schemaVersion !== 2) differences.push({ area: 'migration', current: existing.schemaVersion, supported: 2 })
12
+ if (previousId !== current.id) differences.push({ area: 'compatibility-profile', current: previousId, supported: current.id })
13
+ for (const [runtime, version] of Object.entries(current.runtimes)) {
14
+ if (existing.runtimes?.[runtime] !== version) differences.push({ area: `runtime.${runtime}`, current: existing.runtimes?.[runtime], supported: version })
15
+ }
16
+ for (const contract of ['tests', 'continuousIntegration', 'securityRules', 'operationsDocumentation']) {
17
+ if (existing.productionBaseline?.[contract] !== true) differences.push({ area: `security-contract.${contract}`, current: existing.productionBaseline?.[contract], supported: true })
18
+ }
19
+ return { readOnly: true, project: path.resolve(projectPath), stack: existing.stack, profile: previousId, supportedProfile: current.id, differences }
20
+ }