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,63 @@
1
+ import { APPLICATION_SHAPES } from '../engine/project-shapes.js'
2
+ import { laravelUis } from '../stacks/backends/laravel/ui/index.js'
3
+
4
+ function option(args, name) {
5
+ return args.find((argument) => argument.startsWith(`--${name}=`))?.split('=')[1]
6
+ }
7
+
8
+ export function parseArguments(args) {
9
+ const upgradeReportIndex = args.indexOf('upgrade-report')
10
+ const profile = option(args, 'profile')
11
+ const shape = option(args, 'shape')
12
+ const frontendValue = option(args, 'frontend')
13
+ const frontendAliases = { vite: 'react', expo: 'react-native', none: 'no-frontend' }
14
+ const frontend = frontendAliases[frontendValue] || frontendValue
15
+ const backend = option(args, 'backend')
16
+ const architecture = option(args, 'architecture')
17
+ const authentication = option(args, 'authentication')
18
+ const authAudience = option(args, 'auth-audience')
19
+ const laravelUi = option(args, 'laravel-ui')
20
+ const uploads = option(args, 'uploads')
21
+ const backgroundJobs = option(args, 'background-jobs')
22
+ const offline = option(args, 'offline')
23
+ const install = args.includes('--install')
24
+ const noInstall = args.includes('--no-install')
25
+
26
+ if (shape && !APPLICATION_SHAPES[shape]) {
27
+ throw new Error('--shape must be fullstack, separate, api, mobile, or frontend')
28
+ }
29
+ if (architecture && !['small', 'medium', 'large'].includes(architecture)) {
30
+ throw new Error('--architecture must be small, medium, or large')
31
+ }
32
+ if (authentication && !['yes', 'not-yet', 'none'].includes(authentication)) {
33
+ throw new Error('--authentication must be yes, not-yet, or none')
34
+ }
35
+ if (authAudience && !['website', 'multi-client'].includes(authAudience)) {
36
+ throw new Error('--auth-audience must be website or multi-client')
37
+ }
38
+ if (laravelUi && !laravelUis.some((ui) => ui.id === laravelUi)) {
39
+ throw new Error('--laravel-ui must be blade, livewire, or inertia-react')
40
+ }
41
+ if (uploads && !['none', 'object-storage'].includes(uploads)) throw new Error('--uploads must be none or object-storage')
42
+ if (backgroundJobs && !['none', 'queue'].includes(backgroundJobs)) throw new Error('--background-jobs must be none or queue')
43
+ if (offline && !['none', 'cache', 'sync'].includes(offline)) throw new Error('--offline must be none, cache, or sync')
44
+ if (install && noInstall) throw new Error('Use either --install or --no-install, not both')
45
+
46
+ return Object.freeze({
47
+ profile,
48
+ shape,
49
+ frontend,
50
+ backend,
51
+ architecture,
52
+ authentication,
53
+ authAudience,
54
+ laravelUi,
55
+ uploads,
56
+ backgroundJobs,
57
+ offline,
58
+ install,
59
+ noInstall,
60
+ doctor: args[0] === 'doctor' || args.includes('--doctor'),
61
+ upgradeReportPath: upgradeReportIndex >= 0 ? (args[upgradeReportIndex + 1] || '.') : undefined,
62
+ })
63
+ }
@@ -0,0 +1,220 @@
1
+ import chalk from 'chalk'
2
+ import ora from 'ora'
3
+ import { APPLICATION_SHAPES } from '../engine/project-shapes.js'
4
+ import path from 'node:path'
5
+
6
+ // ─── Banner (from lib/banner.js) ────────────────────────────────────────────
7
+ const W1N = [
8
+ ' ██╗ ██╗ ██╗ ███╗ ██╗',
9
+ ' ██║ ██║ ███║ ████╗ ██║',
10
+ ' ██║ █╗ ██║ ╚██║ ██╔██╗██║',
11
+ ' ██║███╗██║ ██║ ██║╚████║',
12
+ ' ╚███╔███╔╝ ██║ ██║ ╚███║',
13
+ ' ╚══╝╚══╝ ╚═╝ ╚═╝ ╚══╝',
14
+ ]
15
+ const PROJECT = [
16
+ ' █▀█ █▀█ █▀█ █ █▀▀ █▀▀ ▀█▀',
17
+ ' █▀▀ █▀█ █ █ █ █▀ █ █ ',
18
+ ' █ █ █ █▄█ █▄█ █▄▄ █▄▄ █ ',
19
+ ]
20
+ function plain() {
21
+ return [...W1N, '', ...PROJECT].join('\n')
22
+ }
23
+ function colored() {
24
+ const w1n = W1N.map((l) => chalk.bold.cyan(l)).join('\n')
25
+ const proj = PROJECT.map((l) => chalk.bold.white(l)).join('\n')
26
+ return `${w1n}\n${proj}`
27
+ }
28
+ export function w1nBanner() {
29
+ const noColor = process.env.NO_COLOR !== undefined || process.env.FORCE_COLOR === '0'
30
+ const isTTY = process.stdout.isTTY
31
+ const width = process.stdout.columns || 80
32
+ if (!isTTY || width < 80 || noColor) {
33
+ if (width < 60) return 'W1N PROJECT'
34
+ return plain()
35
+ }
36
+ return colored()
37
+ }
38
+ export const bannerRaw = plain()
39
+ export const W1N_ART = W1N.join('\n')
40
+ export const PROJECT_ART = PROJECT.join('\n')
41
+
42
+ // ─── Project location (from src/engine/project-location.js) ─────────────────
43
+ export function isInsideDirectory(candidate, parent) {
44
+ const relative = path.relative(path.resolve(parent), path.resolve(candidate))
45
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
46
+ }
47
+ export function projectLocationNotice({ cwd, cliRoot, projectName }) {
48
+ if (!isInsideDirectory(cwd, cliRoot)) return null
49
+ const generatedPath = path.join(path.resolve(cwd), projectName)
50
+ const suggestedPath = path.join(path.dirname(path.resolve(cliRoot)), projectName)
51
+ return {
52
+ generatedPath,
53
+ suggestedPath,
54
+ message: `This project was created inside the create-win-project repository. Move or cut the generated folder to your normal projects folder outside this repository before you start committing application work.`,
55
+ }
56
+ }
57
+
58
+ // ─── Display helpers ─────────────────────────────────────────────────────────
59
+ export function printBanner(profile, output = console.log) {
60
+ output('')
61
+ output(w1nBanner())
62
+ output('')
63
+ output(chalk.gray(' Production-ready project scaffolding'))
64
+ output(chalk.gray(` Compatibility profile: ${profile.id} (${profile.status})`))
65
+ output('')
66
+ }
67
+
68
+ export function printCancelled({ explainNoFiles = false } = {}, output = console.log) {
69
+ output(chalk.yellow(explainNoFiles ? '\n Cancelled. No files were created.\n' : '\n Cancelled.\n'))
70
+ }
71
+
72
+ export function startProjectSpinner() {
73
+ return ora('Scaffolding project...').start()
74
+ }
75
+
76
+ export function projectSucceeded(spinner) {
77
+ spinner.succeed(chalk.green('Project created!'))
78
+ }
79
+
80
+ export function startInstallSpinner() {
81
+ return ora('Installing exact dependencies and creating lockfiles...').start()
82
+ }
83
+
84
+ export function installSucceeded(spinner) {
85
+ spinner.succeed(chalk.green('Dependencies installed and lockfiles created.'))
86
+ }
87
+
88
+ export function installFailed(spinner) {
89
+ spinner.warn(chalk.yellow('Project created, but dependency installation did not finish.'))
90
+ }
91
+
92
+ export function generationFailed(spinner, error, report = console.error) {
93
+ spinner.fail(chalk.red('Failed to generate project'))
94
+ report(error)
95
+ report(chalk.yellow('Review the error above, correct the input or environment, then run create-win-project again.'))
96
+ }
97
+
98
+ export function printSummary({ answers, stack, catalog }, output = console.log) {
99
+ output('')
100
+ output(chalk.cyan.bold(' Review before creating files'))
101
+ output(chalk.gray(' ───────────────────────────'))
102
+ output(` ${chalk.cyan('Name:')} ${answers.projectName}`)
103
+ output(` ${chalk.cyan('Stack:')} ${stack.label}`)
104
+ output(` ${chalk.cyan('Shape:')} ${APPLICATION_SHAPES[stack.applicationShape].label}`)
105
+ output(` ${chalk.cyan('Platform:')} ${stack.platform}`)
106
+ if (stack.styleId) output(` ${chalk.cyan('Styling:')} ${catalog.byId[stack.styleId]?.label || stack.styleId}`)
107
+ if (answers.laravelUi) output(` ${chalk.cyan('Laravel UI:')} ${answers.laravelUi}`)
108
+ output(` ${chalk.cyan('Architecture:')} ${stack.architecture[0].toUpperCase()}${stack.architecture.slice(1)}`)
109
+ output(` ${chalk.cyan('Authentication:')} ${stack.authentication}`)
110
+ output(` ${chalk.cyan('Testing:')} ${answers.testing}`)
111
+ if (stack.platform !== 'mobile') {
112
+ output(` ${chalk.cyan('Docker:')} ${answers.docker ? 'yes' : 'no'}`)
113
+ output(` ${chalk.cyan('Makefile:')} ${answers.makefile ? 'yes' : 'no'}`)
114
+ }
115
+ output(` ${chalk.cyan('CI/CD:')} ${answers.githubActions ? 'yes' : 'no'}`)
116
+ output(` ${chalk.cyan('Install deps:')} ${answers.installDependencies ? 'yes' : 'no'}`)
117
+ if (answers.packageName) output(` ${chalk.cyan('Package:')} ${answers.packageName}`)
118
+ if (stack.constraints.length) {
119
+ output('')
120
+ output(chalk.gray(' Key constraints:'))
121
+ for (const rule of stack.constraints.slice(0, 3)) output(chalk.gray(` • ${rule}`))
122
+ }
123
+ output('')
124
+ }
125
+
126
+ export function printRuntimeMismatch(issues, output = console.log) {
127
+ output(chalk.yellow.bold('\n Runtime mismatch\n'))
128
+ for (const issue of issues) {
129
+ output(chalk.yellow(` ${issue.tool} ${issue.required} or newer is required.`))
130
+ output(chalk.gray(` Current terminal: ${issue.tool} ${issue.found || 'not found'}.`))
131
+ }
132
+ output(chalk.gray('\n Your global tools will not be changed.\n'))
133
+ }
134
+
135
+ export function printRuntimeInstructions(lines, output = console.log) {
136
+ output(chalk.cyan.bold('\n Runtime setup\n'))
137
+ for (const line of lines) output(chalk.gray(` ${line}`))
138
+ output(chalk.gray('\n Run create-win-project again after switching runtimes.\n'))
139
+ }
140
+
141
+ export function printRuntimeSkip(issues, output = console.log) {
142
+ output(chalk.yellow('\n Dependency installation will be skipped because this terminal does not meet the selected project requirements.'))
143
+ for (const issue of issues) output(chalk.gray(` ${issue.tool}: ${issue.found || 'not found'} (requires ${issue.required} or newer)`))
144
+ output(chalk.gray(' Project files will still be created. Use Mise/NVM, then run the retry command shown below.\n'))
145
+ }
146
+
147
+ export function printLocationNotice(notice, output = console.log) {
148
+ if (!notice) return
149
+ output('')
150
+ output(chalk.yellow.bold(' Project location note'))
151
+ output(chalk.yellow(` ${notice.message}`))
152
+ output(chalk.gray(` Created at: ${notice.generatedPath}`))
153
+ output(chalk.gray(` Suggested destination: ${notice.suggestedPath}`))
154
+ output(chalk.gray(` Linux/macOS: mv "${notice.generatedPath}" "${notice.suggestedPath}"`))
155
+ output(chalk.gray(' Windows: cut the generated folder in File Explorer and paste it into your projects folder.'))
156
+ }
157
+
158
+ function detectedLine(versions) {
159
+ return ` Detected Node.js ${versions.node || 'not found'}, npm ${versions.npm || 'not found'}, PHP ${versions.php || 'not found'}, and Composer ${versions.composer || 'not found'}.`
160
+ }
161
+
162
+ export function printInstallFailure(versions, retry, output = console.log) {
163
+ output(chalk.yellow(detectedLine(versions)))
164
+ output(chalk.yellow(` Retry with: ${retry}`))
165
+ }
166
+
167
+ export function printInstallSkipped(versions, steps, output = console.log) {
168
+ output(chalk.yellow(' Project files were created without installing dependencies.'))
169
+ output(chalk.gray(detectedLine(versions)))
170
+ for (const step of steps) output(chalk.gray(` Retry with: ${step.retry}`))
171
+ }
172
+
173
+ export function printNextSteps(answers, stack, output = console.log) {
174
+ output('')
175
+ output(chalk.bold(' Next steps:'))
176
+ output(chalk.gray(` cd ${answers.projectName}`))
177
+ if (stack.backendKey === 'laravel') {
178
+ const laravelDir = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '' : 'backend/'
179
+ output(chalk.gray(` cp ${laravelDir}.env.example ${laravelDir}.env`))
180
+ if (answers.makefile) {
181
+ output(chalk.gray(' make setup # first run only'))
182
+ output(chalk.gray(' make run # later runs; never rebuilds'))
183
+ } else {
184
+ output(chalk.gray(' docker compose build'))
185
+ output(chalk.gray(' docker compose up -d'))
186
+ }
187
+ } else if (stack.frontendKey === 'react') {
188
+ if (stack.backendKey === 'springboot') output(chalk.gray(' cp .env.example .env # Docker/backend values'))
189
+ output(chalk.gray(' cd frontend'))
190
+ output(chalk.gray(' cp .env.example .env'))
191
+ if (!answers.installDependencies) output(chalk.gray(' npm install'))
192
+ } else {
193
+ output(chalk.gray(` cp .env.example ${stack.isMobile ? '.env' : '.env.local'}`))
194
+ if (!answers.installDependencies) output(chalk.gray(' npm install'))
195
+ }
196
+ if (stack.backendKey !== 'laravel') {
197
+ if (stack.isMobile) output(chalk.gray(' npx expo start'))
198
+ else if (answers.makefile) {
199
+ if (stack.frontendKey === 'react') output(chalk.gray(' cd ..'))
200
+ output(chalk.gray(' make dev'))
201
+ } else if (stack.backendKey === 'supabase') {
202
+ output(chalk.gray(' npm run supabase:start'))
203
+ output(chalk.gray(' npm run dev'))
204
+ } else if (stack.frontendKey === 'react') output(chalk.gray(' npm run dev'))
205
+ else {
206
+ output(chalk.gray(' docker compose up -d db'))
207
+ output(chalk.gray(' npm run dev'))
208
+ }
209
+ }
210
+ output('')
211
+ output(chalk.cyan(' Read RULES.md before starting — it maps every playbook for this stack.'))
212
+ output('')
213
+ }
214
+
215
+ export function printSupportPrompt(output = console.log) {
216
+ output(chalk.gray(' ───────────────────────────'))
217
+ output(chalk.gray(' Enjoyed this starter? ⭐ Star https://github.com/itsw1n/create-win-project'))
218
+ output(chalk.gray(' Found a gap or idea? Open an issue: https://github.com/itsw1n/create-win-project/issues/new'))
219
+ output('')
220
+ }
@@ -0,0 +1,203 @@
1
+ import inquirer from 'inquirer'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { generateProject } from '../engine/create-project.js'
5
+ import { runDependencySteps } from '../engine/install-dependencies.js'
6
+ import { loadCompatibility } from '../engine/tested-versions.js'
7
+ import { buildQuestions, configurationDecisionChoices, lastEnabledIndex, promptWithBack } from './questions.js'
8
+ import {
9
+ printBanner,
10
+ generationFailed,
11
+ installFailed,
12
+ installSucceeded,
13
+ printInstallFailure,
14
+ printInstallSkipped,
15
+ printLocationNotice,
16
+ printNextSteps,
17
+ printSupportPrompt,
18
+ printCancelled,
19
+ printRuntimeInstructions,
20
+ printRuntimeMismatch,
21
+ printRuntimeSkip,
22
+ printSummary,
23
+ projectSucceeded,
24
+ projectLocationNotice,
25
+ startInstallSpinner,
26
+ startProjectSpinner,
27
+ } from './display.js'
28
+ import { parseArguments } from './arguments.js'
29
+ import { createUpgradeReport } from '../engine/upgrade-report.js'
30
+ import {
31
+ loadCatalog, resolveStack,
32
+ } from '../engine/load-library.js'
33
+ import {
34
+ decideInstallation,
35
+ detectSystemVersions,
36
+ installationIssues,
37
+ runtimeSetupInstructions,
38
+ printDoctor,
39
+ } from './system-check.js'
40
+
41
+ const cliDirectory = path.dirname(fileURLToPath(import.meta.url))
42
+ const projectRoot = path.resolve(cliDirectory, '../..')
43
+
44
+ export async function runCli() {
45
+ const args = parseArguments(process.argv.slice(2))
46
+ const profileArg = args.profile
47
+ const shapeArg = args.shape
48
+ const frontendArg = args.frontend
49
+ const backendArg = args.backend
50
+ const architectureArg = args.architecture
51
+ const authenticationArg = args.authentication
52
+ const authAudienceArg = args.authAudience
53
+ const laravelUiArg = args.laravelUi
54
+ const uploadsArg = args.uploads
55
+ const backgroundJobsArg = args.backgroundJobs
56
+ const offlineArg = args.offline
57
+ const wantsInstall = args.install
58
+ const skipsInstall = args.noInstall
59
+ if (args.upgradeReportPath) {
60
+ const report = await createUpgradeReport(args.upgradeReportPath, path.join(projectRoot, 'library/tested-versions.json'))
61
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
62
+ return
63
+ }
64
+ const { profile } = await loadCompatibility(
65
+ path.join(projectRoot, 'library/tested-versions.json'),
66
+ profileArg,
67
+ )
68
+ if (args.doctor) {
69
+ printDoctor(profile)
70
+ process.exit(0)
71
+ }
72
+ const catalog = await loadCatalog(path.join(projectRoot, 'library'), profile)
73
+
74
+ // ─── Banner ──────────────────────────────────────────────────────────────────
75
+
76
+ printBanner(profile)
77
+
78
+ // ─── Interview ───────────────────────────────────────────────────────────────
79
+
80
+ const questions = buildQuestions({ args, catalog })
81
+ let answers = {
82
+ applicationShape: shapeArg,
83
+ frontend: frontendArg,
84
+ backend: backendArg,
85
+ }
86
+ let stack
87
+ let resumeAt = 0
88
+ while (true) {
89
+ answers = await promptWithBack(inquirer, questions, answers, resumeAt)
90
+ resumeAt = 0
91
+ answers.compatibilityProfile = profile.id
92
+ answers.applicationShape = shapeArg || answers.applicationShape
93
+ answers.architecture = architectureArg || answers.architecture || 'medium'
94
+ answers.authentication = authenticationArg || answers.authentication || 'not-yet'
95
+ answers.authAudience = authAudienceArg || answers.authAudience || (catalog.byId[answers.frontend]?.platform === 'mobile' ? 'multi-client' : 'website')
96
+ answers.laravelUi = laravelUiArg || answers.laravelUi || (answers.frontend === 'laravel-ui' ? 'blade' : undefined)
97
+ answers.uploads = uploadsArg || answers.uploads || 'none'
98
+ answers.backgroundJobs = backgroundJobsArg || answers.backgroundJobs || 'none'
99
+ answers.offline = offlineArg || answers.offline || 'none'
100
+ answers.testing = answers.frontend === 'react-native' ? 'basic' : 'full'
101
+ answers.githubActions = true
102
+ if (wantsInstall) answers.installDependencies = true
103
+ if (skipsInstall) answers.installDependencies = false
104
+
105
+ stack = resolveStack({ ...answers, styling: answers.styling || catalog.byId[answers.frontend]?.stylingOptions?.[0] || 'tailwind' }, catalog)
106
+
107
+ // ─── Confirm ─────────────────────────────────────────────────────────────────
108
+
109
+ printSummary({ answers, stack, catalog })
110
+
111
+ const { decision } = await inquirer.prompt([{
112
+ type: 'list', name: 'decision', message: 'Ready?', choices: configurationDecisionChoices(),
113
+ }])
114
+
115
+ if (decision === 'back') {
116
+ resumeAt = lastEnabledIndex(questions, answers)
117
+ continue
118
+ }
119
+ if (decision === 'cancel') {
120
+ printCancelled()
121
+ process.exit(0)
122
+ }
123
+ break
124
+ }
125
+
126
+ const detectedVersions = detectSystemVersions()
127
+ const runtimeIssues = installationIssues(stack, profile, detectedVersions)
128
+ const installationDecision = await decideInstallation({
129
+ requested: answers.installDependencies,
130
+ issues: runtimeIssues,
131
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
132
+ choose: async () => {
133
+ printRuntimeMismatch(runtimeIssues)
134
+ const { runtimeDecision } = await inquirer.prompt([{
135
+ type: 'list',
136
+ name: 'runtimeDecision',
137
+ message: 'How should create-win-project continue?',
138
+ choices: [
139
+ { name: 'Create files and skip dependency installation', value: 'skip' },
140
+ { name: 'Show runtime setup instructions', value: 'instructions' },
141
+ { name: 'Cancel', value: 'cancel' },
142
+ ],
143
+ }])
144
+ return runtimeDecision
145
+ },
146
+ })
147
+
148
+ if (installationDecision.reason === 'cancelled') {
149
+ printCancelled({ explainNoFiles: true })
150
+ process.exit(0)
151
+ }
152
+ if (installationDecision.reason === 'show-instructions') {
153
+ printRuntimeInstructions(runtimeSetupInstructions(profile, runtimeIssues))
154
+ process.exit(0)
155
+ }
156
+ if (answers.installDependencies && !installationDecision.install) {
157
+ printRuntimeSkip(runtimeIssues)
158
+ }
159
+ answers.installDependencies = installationDecision.install
160
+
161
+ // ─── Generate ────────────────────────────────────────────────────────────────
162
+
163
+ const spinner = startProjectSpinner()
164
+
165
+ try {
166
+ await generateProject(answers, projectRoot)
167
+ projectSucceeded(spinner)
168
+ const generatedProjectRoot = path.join(process.cwd(), answers.projectName)
169
+ const steps = []
170
+ if (stack.backendKey === 'laravel') {
171
+ const laravelRoot = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? generatedProjectRoot : path.join(generatedProjectRoot, 'backend')
172
+ steps.push({ command: process.platform === 'win32' ? 'composer.bat' : 'composer', args: ['install'], cwd: laravelRoot, retry: `cd ${path.relative(process.cwd(), laravelRoot)} && composer install` })
173
+ }
174
+ if (stack.backendKey === 'fastapi') {
175
+ const apiRoot = stack.frontendKey === 'no-frontend' ? generatedProjectRoot : path.join(generatedProjectRoot, 'backend')
176
+ steps.push({ command: 'uv', args: ['sync'], cwd: apiRoot, retry: `cd ${path.relative(process.cwd(), apiRoot)} && uv sync` })
177
+ }
178
+ const needsNpm = stack.frontendKey !== 'no-frontend' && (stack.frontendKey !== 'laravel-ui' || answers.laravelUi === 'inertia-react')
179
+ if (needsNpm) {
180
+ const npmRoot = stack.frontendKey === 'react' ? path.join(generatedProjectRoot, 'frontend') : generatedProjectRoot
181
+ steps.push({ command: process.platform === 'win32' ? 'npm.cmd' : 'npm', args: ['install'], cwd: npmRoot, retry: `cd ${path.relative(process.cwd(), npmRoot)} && npm install` })
182
+ }
183
+ const locationNotice = projectLocationNotice({ cwd: process.cwd(), cliRoot: projectRoot, projectName: answers.projectName })
184
+ printLocationNotice(locationNotice)
185
+ if (answers.installDependencies) {
186
+ const installSpinner = startInstallSpinner()
187
+ const { failed } = runDependencySteps(steps)
188
+ if (failed) {
189
+ installFailed(installSpinner)
190
+ printInstallFailure(detectedVersions, failed.retry)
191
+ } else {
192
+ installSucceeded(installSpinner)
193
+ }
194
+ } else if (installationDecision.reason === 'runtime-mismatch') {
195
+ printInstallSkipped(detectedVersions, steps)
196
+ }
197
+ printNextSteps(answers, stack)
198
+ if (process.stdout.isTTY) printSupportPrompt()
199
+ } catch (err) {
200
+ generationFailed(spinner, err)
201
+ process.exit(1)
202
+ }
203
+ }
@@ -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
+ }