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,156 @@
1
+ import chalk from 'chalk'
2
+
3
+ const BACK = Symbol('back')
4
+
5
+ export function wrapText(text, width = Math.max(28, Math.min(72, (process.stdout.columns || 80) - 8))) {
6
+ const lines = []
7
+ for (const word of text.split(/\s+/)) {
8
+ const last = lines.at(-1)
9
+ if (!last || `${last} ${word}`.length > width) lines.push(word)
10
+ else lines[lines.length - 1] = `${last} ${word}`
11
+ }
12
+ return lines.join('\n ')
13
+ }
14
+
15
+ function enabled(question, answers) {
16
+ return typeof question.when === 'function' ? question.when(answers) : question.when !== false
17
+ }
18
+
19
+ async function resolved(value, answers) {
20
+ return typeof value === 'function' ? value(answers) : value
21
+ }
22
+
23
+ function clearFrom(questions, answers, index) {
24
+ for (let cursor = index; cursor < questions.length; cursor += 1) {
25
+ delete answers[questions[cursor].name]
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Runs Inquirer questions one at a time so users can safely revisit earlier answers.
31
+ * List/checkbox/confirm prompts expose a visible Back choice; text prompts accept `:back`.
32
+ */
33
+ export function lastEnabledIndex(questions, answers) {
34
+ let last = 0
35
+ questions.forEach((question, index) => {
36
+ if (enabled(question, answers)) last = index
37
+ })
38
+ return last
39
+ }
40
+
41
+ export async function promptWithBack(inquirer, questions, initialAnswers = {}, startIndex = 0) {
42
+ const answers = { ...initialAnswers }
43
+ // Seed history with previously asked prompts so Back navigation,
44
+ // step numbering, and clearFrom targets mirror a natural pass.
45
+ // Skipped (when:false) prompts are never seeded, exactly as a pass never pushes them.
46
+ const history = []
47
+ for (let seed = 0; seed < startIndex; seed += 1) {
48
+ if (enabled(questions[seed], answers)) history.push(seed)
49
+ }
50
+ let index = startIndex
51
+ let resumeHintShown = startIndex === 0
52
+
53
+ while (index < questions.length) {
54
+ const source = questions[index]
55
+ if (!enabled(source, answers)) { index += 1; continue }
56
+
57
+ const canGoBack = history.length > 0
58
+ // askAnswered: inquirer skips questions whose answer already exists in
59
+ // the passed answers object. We always want to ask (re-entry after
60
+ // Back-and-edit arrives with complete answers); defaults pre-fill
61
+ // previous values via question.default below.
62
+ const question = { ...source, when: undefined, askAnswered: true }
63
+ const title = await resolved(question.message, answers)
64
+ const controls = question.type === 'checkbox'
65
+ ? 'Arrow keys move • Space selects (Space on ← Back first) • Enter continues • Back returns'
66
+ : question.type === 'input'
67
+ ? 'Type an answer • Enter continues • :back returns'
68
+ : 'Arrow keys move • Enter selects • Back returns'
69
+ question.message = `${chalk.cyan.bold(`Step ${history.length + 1} of ${questions.length} - ${title}`)}\n${chalk.dim(controls)}\n`
70
+ if (!resumeHintShown) {
71
+ question.message = `${question.message}${chalk.dim('(editing previous answers — choose ← Back to revisit earlier steps, Enter keeps values and returns)\n')}`
72
+ resumeHintShown = true
73
+ }
74
+ if (answers[source.name] !== undefined && question.default === undefined) {
75
+ question.default = answers[source.name]
76
+ }
77
+ if (question.type === 'input' && canGoBack) {
78
+ question.message = `${question.message} (type :back to return)`
79
+ }
80
+ if (question.type === 'input') {
81
+ // Let the :back sentinel through validation so promptWithBack can
82
+ // handle it; inquirer validates before returning the value.
83
+ const originalValidate = question.validate
84
+ question.validate = (value, answersState) =>
85
+ value === ':back' ? true : (originalValidate?.(value, answersState) ?? true)
86
+ }
87
+ if (question.type === 'confirm') {
88
+ const defaultValue = await resolved(question.default, answers)
89
+ question.type = 'list'
90
+ question.choices = [
91
+ { name: 'Yes', value: true },
92
+ { name: 'No', value: false },
93
+ ...(canGoBack ? [{ name: '← Back', value: BACK }] : []),
94
+ ]
95
+ question.default = defaultValue ? 0 : 1
96
+ } else if (['list', 'checkbox'].includes(question.type)) {
97
+ const raw = await resolved(question.choices, answers)
98
+ const rest = raw.filter((choice) => choice?.type !== 'description-footer')
99
+ const footers = raw
100
+ .filter((choice) => choice?.type === 'description-footer')
101
+ .map((footer) => new inquirer.Separator(chalk.dim(wrapText(footer.text))))
102
+ question.choices = canGoBack
103
+ ? [...rest, ...footers, new inquirer.Separator(), { name: '← Back', value: BACK }]
104
+ : [...rest, ...footers]
105
+ }
106
+
107
+ if (typeof question.default === 'function') question.default = await question.default(answers)
108
+ const result = await inquirer.prompt([question], answers)
109
+ const value = result[source.name]
110
+ const requestedBack = value === BACK || value === ':back' || (Array.isArray(value) && value.includes(BACK))
111
+ if (requestedBack) {
112
+ const target = history.pop()
113
+ if (target === undefined) continue
114
+ clearFrom(questions, answers, target)
115
+ index = target
116
+ continue
117
+ }
118
+ if (question.type === 'checkbox' && startIndex > 0 && canGoBack) {
119
+ // Checkbox Enter only confirms checked items, so a highlighted-but-unchecked
120
+ // ← Back would silently complete the interview instead of going back.
121
+ // On re-entry runs, ask explicitly (Enter-selectable). Fresh passes keep
122
+ // the inline Back + Space hint with no extra prompt.
123
+ const follow = await inquirer.prompt([{
124
+ type: 'list',
125
+ name: '__continue',
126
+ message: 'Continue with these selections?',
127
+ choices: [
128
+ { name: 'Continue', value: 'continue' },
129
+ { name: '← Back', value: BACK },
130
+ ],
131
+ }], answers)
132
+ if (follow.__continue === BACK) {
133
+ const target = history.pop()
134
+ if (target !== undefined) {
135
+ clearFrom(questions, answers, target)
136
+ index = target
137
+ continue
138
+ }
139
+ }
140
+ }
141
+
142
+ answers[source.name] = value
143
+ history.push(index)
144
+ index += 1
145
+ }
146
+
147
+ return answers
148
+ }
149
+
150
+ export function configurationDecisionChoices() {
151
+ return [
152
+ { name: 'Create project\n Write the reviewed files and selected configuration.\n', value: 'create' },
153
+ { name: 'Back and edit\n Return to the interview without creating files.\n', value: 'back' },
154
+ { name: 'Cancel\n Exit without creating files.\n', value: 'cancel' },
155
+ ]
156
+ }
@@ -0,0 +1,187 @@
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'].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'].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'].includes(answers.backend) && answers.frontend !== 'laravel-ui' &&
134
+ (args.authentication || answers.authentication) === 'yes' && !args.authAudience,
135
+ },
136
+ {
137
+ type: 'list', name: 'testing', message: 'Testing setup?',
138
+ choices: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile'
139
+ ? [
140
+ { name: 'Basic (Jest + React Native Testing Library)', value: 'basic' },
141
+ { name: 'None', value: 'none' },
142
+ ]
143
+ : [
144
+ { name: 'Full (Vitest + React Testing Library + Playwright)', value: 'full' },
145
+ { name: 'Basic (Vitest + React Testing Library)', value: 'basic' },
146
+ { name: 'None', value: 'none' },
147
+ ],
148
+ default: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile' ? 'basic' : 'full',
149
+ },
150
+ {
151
+ type: 'confirm', name: 'docker', message: 'Add optional Docker development files', default: false,
152
+ when: (answers) => Boolean(catalog.byId[answers.backend]?.needsDocker) || catalog.byId[answers.frontend]?.platform !== 'mobile',
153
+ },
154
+ {
155
+ type: 'confirm', name: 'makefile', message: 'Include Makefile?', default: true,
156
+ when: (answers) => catalog.byId[answers.frontend]?.platform !== 'mobile',
157
+ },
158
+ { type: 'confirm', name: 'githubActions', message: 'Include GitHub Actions CI?', default: true },
159
+ {
160
+ type: 'confirm', name: 'installDependencies',
161
+ message: 'Install project dependencies and create the lockfile now?', default: true,
162
+ when: () => !args.install && !args.noInstall,
163
+ },
164
+ {
165
+ type: 'input', name: 'packageName', message: 'Java package name? (e.g. com.yourname)', default: 'com.app',
166
+ when: (answers) => answers.backend === 'springboot',
167
+ validate: (value) => {
168
+ if (!value.trim()) return 'Package name is required'
169
+ if (!/^[a-z]+(\.[a-z]+)+$/.test(value)) return 'Use format: com.yourname'
170
+ return true
171
+ },
172
+ },
173
+ {
174
+ type: 'checkbox', name: 'expectedConcerns',
175
+ message: 'Planning notes for optional features (advisory only; no libraries or feature code are generated)',
176
+ choices: (answers) => {
177
+ const stack = resolveStack({
178
+ ...answers,
179
+ styling: answers.styling || catalog.byId[answers.frontend]?.stylingOptions?.[0],
180
+ }, catalog)
181
+ const notes = [...new Set(stack.concerns.filter((concern) => !concern.required).map((concern) => concern.id))]
182
+ .map((id) => choice(CONCERN_LABELS[id] || id.replaceAll('-', ' '), id))
183
+ return [...notes, footerMarker('Write selected concerns to CONTEXT.md as planning notes.')]
184
+ },
185
+ },
186
+ ]
187
+ }
@@ -0,0 +1,138 @@
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
+ }
34
+ }
35
+
36
+ export function needsNpmInstallation(stack) {
37
+ return stack.frontendKey !== 'no-frontend' &&
38
+ (stack.frontendKey !== 'laravel-ui' || stack.laravelUi === 'inertia-react')
39
+ }
40
+
41
+ export function installationIssues(stack, profile, versions) {
42
+ const issues = []
43
+ if (needsNpmInstallation(stack)) {
44
+ const minimumNode = profile.runtimes.nodeMinimum
45
+ const minimumNpm = profile.runtimes.npmMinimum
46
+ if (!versionAtLeast(versions.node, minimumNode)) {
47
+ issues.push({ tool: 'Node.js', found: versions.node, required: minimumNode })
48
+ }
49
+ if (!versionAtLeast(versions.npm, minimumNpm)) {
50
+ issues.push({ tool: 'npm', found: versions.npm, required: minimumNpm })
51
+ }
52
+ }
53
+ if (stack.backendKey === 'laravel') {
54
+ if (!versionAtLeast(versions.php, profile.runtimes.php)) {
55
+ issues.push({ tool: 'PHP', found: versions.php, required: profile.runtimes.php })
56
+ }
57
+ if (!versionAtLeast(versions.composer, profile.runtimes.composer)) {
58
+ issues.push({ tool: 'Composer', found: versions.composer, required: profile.runtimes.composer })
59
+ }
60
+ }
61
+ return issues
62
+ }
63
+
64
+ export async function decideInstallation({ requested, issues, interactive, choose }) {
65
+ if (!requested) return { install: false, reason: 'not-requested' }
66
+ if (issues.length === 0) return { install: true, reason: 'supported' }
67
+ if (!interactive) return { install: false, reason: 'runtime-mismatch' }
68
+
69
+ while (true) {
70
+ const decision = await choose()
71
+ if (decision === 'skip') return { install: false, reason: 'runtime-mismatch' }
72
+ if (decision === 'cancel') return { install: false, reason: 'cancelled' }
73
+ if (decision !== 'instructions') throw new Error(`Unknown runtime decision: ${decision}`)
74
+ return { install: false, reason: 'show-instructions' }
75
+ }
76
+ }
77
+
78
+ export function runtimeSetupInstructions(profile, issues = []) {
79
+ const tools = new Set(issues.map((issue) => issue.tool))
80
+ const instructions = []
81
+ if (tools.has('Node.js') || tools.has('npm')) {
82
+ instructions.push(
83
+ `Mise: mise use node@${profile.runtimes.node}`,
84
+ `NVM: nvm install ${profile.runtimes.node} && nvm use ${profile.runtimes.node}`,
85
+ 'Verify: node --version && npm --version',
86
+ )
87
+ }
88
+ if (tools.has('PHP') || tools.has('Composer')) {
89
+ instructions.push(
90
+ `Install PHP ${profile.runtimes.php} and Composer ${profile.runtimes.composer}, or use the generated Docker workflow.`,
91
+ 'Verify: php --version && composer --version',
92
+ )
93
+ }
94
+ return instructions
95
+ }
96
+
97
+ // ─── Doctor (from lib/doctor.js) ───────────────────────────────────────────
98
+ function isOnPath(command) {
99
+ const suffixes = process.platform === 'win32' ? ['', '.cmd', '.exe', '.bat'] : ['']
100
+ return (process.env.PATH || '').split(path.delimiter).some((directory) =>
101
+ suffixes.some((suffix) => existsSync(path.join(directory, `${command}${suffix}`)))
102
+ )
103
+ }
104
+
105
+ function probeDoctor(command, args = ['--version']) {
106
+ const result = spawnSync(command, args, { encoding: 'utf8', shell: false })
107
+ const version = `${result.stdout || result.stderr || ''}`.trim().split('\n')[0]
108
+ if (!version) return isOnPath(command) ? 'available (version unavailable)' : null
109
+ if (result.status !== 0) return null
110
+ return version
111
+ }
112
+
113
+ export function collectDiagnostics(profile) {
114
+ const nodeVersion = process.versions.node
115
+ const npmVersion = probeDoctor('npm')
116
+ const dockerVersion = probeDoctor('docker')
117
+ const composeVersion = dockerVersion ? probeDoctor('docker', ['compose', 'version']) : null
118
+ const javaVersion = probeDoctor('java', ['-version'])
119
+ return [
120
+ { name: 'Node.js', found: nodeVersion, expected: profile.runtimes.node, required: false },
121
+ { name: 'npm', found: npmVersion, required: false },
122
+ { name: 'Docker', found: dockerVersion, required: false },
123
+ { name: 'Docker Compose', found: composeVersion, required: false },
124
+ { name: 'Java', found: javaVersion, expected: profile.runtimes.java, required: false },
125
+ ]
126
+ }
127
+
128
+ export function printDoctor(profile, output = console.log) {
129
+ output(`create-win-project doctor (profile ${profile.id})`)
130
+ output('Use either the Node/npm lane or the Docker/Compose lane; Java is only needed for host-run Spring projects.')
131
+ for (const item of collectDiagnostics(profile)) {
132
+ const expectation = item.expected ? ` (tested: ${item.expected})` : ''
133
+ output(`${item.found ? '✓' : '○'} ${item.name}: ${item.found || 'not found'}${expectation}`)
134
+ }
135
+ output('')
136
+ output('Node lane: npm ci && npm start')
137
+ output('Docker lane: docker compose build && docker compose run --rm app')
138
+ }
@@ -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
  }
@@ -32,8 +32,30 @@ 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', 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', name, path.basename(file)),
50
+ path.join(libraryDir, 'optional-features', 'styling', name.replace(/-extensions$/, ''), path.basename(file)),
51
+ ],
52
+ devops: [path.join(libraryDir, 'development-tools', 'devops', name, path.basename(file))],
53
+ }[category] || []
54
+
55
+ for (const candidate of candidates) {
56
+ if (await fs.pathExists(candidate)) return candidate
57
+ }
58
+ return candidates[0] || path.join(libraryDir, file)
37
59
  }
38
60
 
39
61
  /**
@@ -162,7 +184,7 @@ export async function buildRulesIndex(stack, catalog, playbooksDir) {
162
184
  export async function copySelectedPlaybooks(playbooksDir, dest, stack) {
163
185
  const allFiles = collectPlaybookFiles(stack)
164
186
  for (const file of allFiles) {
165
- const src = path.join(playbooksDir, file)
187
+ const src = await resolvePlaybook(playbooksDir, file)
166
188
  if (await fs.pathExists(src)) {
167
189
  const dst = path.join(dest, 'playbooks', file)
168
190
  await fs.ensureDir(path.dirname(dst))
@@ -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
@@ -0,0 +1,34 @@
1
+ import fs from 'fs-extra'
2
+ import path from 'node:path'
3
+ import { randomUUID } from 'node:crypto'
4
+
5
+ export async function projectDestinations(cwd, projectName) {
6
+ const finalDestination = path.join(cwd, projectName)
7
+ if (await fs.pathExists(finalDestination)) {
8
+ throw new Error(`Destination already exists: ${finalDestination}`)
9
+ }
10
+ return {
11
+ finalDestination,
12
+ stagingDestination: path.join(cwd, `.${projectName}.tmp-${randomUUID()}`),
13
+ }
14
+ }
15
+
16
+ export async function writeProjectAtomically({ finalDestination, stagingDestination, generate }) {
17
+ try {
18
+ await fs.ensureDir(stagingDestination)
19
+ await generate(stagingDestination)
20
+ await fs.move(stagingDestination, finalDestination, { overwrite: false })
21
+ } catch (error) {
22
+ await fs.remove(stagingDestination)
23
+ throw error
24
+ }
25
+ }
26
+
27
+ export async function writeFile(destination, filePath, content) {
28
+ const fullPath = path.join(destination, filePath)
29
+ await fs.ensureDir(path.dirname(fullPath))
30
+ await fs.writeFile(fullPath, content, 'utf8')
31
+ }
32
+
33
+ // Generated documentation/file-content builders remain compatible during the migration.
34
+ export * from './project-files.js'
@@ -1,4 +1,12 @@
1
- import { isStackAdapter } from './contract.js'
1
+ import { isStackAdapter } from './rules.js'
2
+ import { nextjsAdapter } from './frontends/nextjs/index.js'
3
+ import { reactViteAdapter } from './frontends/react-vite/index.js'
4
+ import { reactNativeAdapter } from './frontends/react-native/index.js'
5
+ import { springbootAdapter } from './backends/springboot/index.js'
6
+ import { supabaseAdapter } from './backends/supabase/index.js'
7
+ import { postgresAdapter } from './backends/postgres/index.js'
8
+ import { noBackendAdapter } from './backends/none/index.js'
9
+ import { laravelAdapter } from './backends/laravel/index.js'
2
10
 
3
11
  export function createStackRegistry(initialAdapters = []) {
4
12
  const adapters = new Map()
@@ -40,3 +48,17 @@ export function createStackRegistry(initialAdapters = []) {
40
48
  return Object.freeze(registry)
41
49
  }
42
50
 
51
+ // This is the only place stack adapters are assembled. Entries stay explicit
52
+ // so adding a source file never silently changes supported generator behavior.
53
+ export const stackRegistry = createStackRegistry([
54
+ nextjsAdapter,
55
+ reactViteAdapter,
56
+ reactNativeAdapter,
57
+ springbootAdapter,
58
+ supabaseAdapter,
59
+ postgresAdapter,
60
+ noBackendAdapter,
61
+ laravelAdapter,
62
+ ])
63
+
64
+ export { stackRegistry as availableStacks }