create-win-project 1.3.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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +187 -0
  3. package/ci/expo.yml +28 -0
  4. package/ci/laravel.yml +49 -0
  5. package/ci/nextjs.yml +29 -0
  6. package/ci/springboot.yml +33 -0
  7. package/ci/vite.yml +36 -0
  8. package/compatibility/profiles.json +237 -0
  9. package/docs/ARCHITECTURE.md +118 -0
  10. package/docs/CONTENT_MODEL.md +36 -0
  11. package/docs/DEPENDENCY_MAINTENANCE.md +31 -0
  12. package/index.js +431 -0
  13. package/lib/application-shapes.js +95 -0
  14. package/lib/banner.js +45 -0
  15. package/lib/catalog.js +343 -0
  16. package/lib/compatibility.js +116 -0
  17. package/lib/constants.js +3 -0
  18. package/lib/doctor.js +45 -0
  19. package/lib/files.js +158 -0
  20. package/lib/generator.js +366 -0
  21. package/lib/interview.js +77 -0
  22. package/lib/laravel-scaffold.js +3 -0
  23. package/lib/playbooks.js +172 -0
  24. package/lib/project-location.js +18 -0
  25. package/lib/scaffold.js +467 -0
  26. package/lib/stacks/context.js +27 -0
  27. package/lib/stacks/contract.js +142 -0
  28. package/lib/stacks/index.js +8 -0
  29. package/lib/stacks/laravel/architecture.js +27 -0
  30. package/lib/stacks/laravel/auth/oidc.js +13 -0
  31. package/lib/stacks/laravel/auth/public.js +28 -0
  32. package/lib/stacks/laravel/auth/sanctum.js +14 -0
  33. package/lib/stacks/laravel/auth/session.js +17 -0
  34. package/lib/stacks/laravel/composer.js +50 -0
  35. package/lib/stacks/laravel/generate.js +362 -0
  36. package/lib/stacks/laravel/index.js +18 -0
  37. package/lib/stacks/laravel/ui/blade.js +15 -0
  38. package/lib/stacks/laravel/ui/index.js +29 -0
  39. package/lib/stacks/laravel/ui/inertia-react.js +35 -0
  40. package/lib/stacks/laravel/ui/livewire.js +39 -0
  41. package/lib/stacks/laravel/ui/shared.js +14 -0
  42. package/lib/stacks/registry.js +42 -0
  43. package/lib/stacks/shared/contributions.js +25 -0
  44. package/lib/template.js +62 -0
  45. package/package.json +49 -0
  46. package/playbooks/INDEX.md +58 -0
  47. package/playbooks/capabilities/auth/oidc-resource-server.md +15 -0
  48. package/playbooks/capabilities/auth/spring-session.md +15 -0
  49. package/playbooks/capabilities/ci/github-actions.manifest.json +10 -0
  50. package/playbooks/capabilities/ci/github-actions.md +61 -0
  51. package/playbooks/capabilities/docker/docker.manifest.json +11 -0
  52. package/playbooks/capabilities/docker/overview.md +55 -0
  53. package/playbooks/capabilities/flyway/environments.md +5 -0
  54. package/playbooks/capabilities/flyway/flyway.manifest.json +12 -0
  55. package/playbooks/capabilities/flyway/migrations.md +7 -0
  56. package/playbooks/capabilities/flyway/testing.md +5 -0
  57. package/playbooks/capabilities/laravel/database.md +8 -0
  58. package/playbooks/capabilities/laravel/migrations.md +15 -0
  59. package/playbooks/capabilities/laravel/observability.md +8 -0
  60. package/playbooks/capabilities/laravel/oidc-resource-server.md +10 -0
  61. package/playbooks/capabilities/laravel/queues.md +8 -0
  62. package/playbooks/capabilities/laravel/sanctum-spa.md +8 -0
  63. package/playbooks/capabilities/laravel/scheduler.md +7 -0
  64. package/playbooks/capabilities/laravel/session-auth.md +8 -0
  65. package/playbooks/capabilities/laravel/storage-uploads.md +7 -0
  66. package/playbooks/capabilities/postgresql/architecture.md +5 -0
  67. package/playbooks/capabilities/postgresql/migrations.md +5 -0
  68. package/playbooks/capabilities/postgresql/postgresql.manifest.json +15 -0
  69. package/playbooks/capabilities/postgresql/schema-design.md +7 -0
  70. package/playbooks/capabilities/postgresql/security.md +5 -0
  71. package/playbooks/capabilities/postgresql/testing.md +5 -0
  72. package/playbooks/capabilities/prisma/architecture.md +5 -0
  73. package/playbooks/capabilities/prisma/migrations.md +11 -0
  74. package/playbooks/capabilities/prisma/prisma.manifest.json +14 -0
  75. package/playbooks/capabilities/prisma/runtime.md +5 -0
  76. package/playbooks/capabilities/prisma/schema.md +5 -0
  77. package/playbooks/capabilities/prisma/testing.md +5 -0
  78. package/playbooks/capabilities/supabase/architecture.md +5 -0
  79. package/playbooks/capabilities/supabase/authentication.md +5 -0
  80. package/playbooks/capabilities/supabase/expo.md +16 -0
  81. package/playbooks/capabilities/supabase/migrations.md +7 -0
  82. package/playbooks/capabilities/supabase/nextjs.md +24 -0
  83. package/playbooks/capabilities/supabase/rls.md +7 -0
  84. package/playbooks/capabilities/supabase/supabase.manifest.json +32 -0
  85. package/playbooks/capabilities/supabase/testing.md +5 -0
  86. package/playbooks/capabilities/supabase/vite.md +14 -0
  87. package/playbooks/concerns/axios.md +123 -0
  88. package/playbooks/concerns/next-safe-action.md +66 -0
  89. package/playbooks/concerns/next-themes.md +87 -0
  90. package/playbooks/concerns/nuqs.md +59 -0
  91. package/playbooks/concerns/t3-env.md +48 -0
  92. package/playbooks/concerns/tanstack-query.md +145 -0
  93. package/playbooks/concerns/zod.md +174 -0
  94. package/playbooks/concerns/zustand.md +93 -0
  95. package/playbooks/devops/makefile.manifest.json +10 -0
  96. package/playbooks/devops/makefile.md +556 -0
  97. package/playbooks/devops/pr-template.manifest.json +10 -0
  98. package/playbooks/devops/pr-template.md +106 -0
  99. package/playbooks/platform/laravel-ui/blade/architecture.md +5 -0
  100. package/playbooks/platform/laravel-ui/blade/runtime.md +5 -0
  101. package/playbooks/platform/laravel-ui/blade/security.md +5 -0
  102. package/playbooks/platform/laravel-ui/blade/structure.md +5 -0
  103. package/playbooks/platform/laravel-ui/blade/testing.md +5 -0
  104. package/playbooks/platform/laravel-ui/inertia-react/architecture.md +5 -0
  105. package/playbooks/platform/laravel-ui/inertia-react/runtime.md +5 -0
  106. package/playbooks/platform/laravel-ui/inertia-react/security.md +5 -0
  107. package/playbooks/platform/laravel-ui/inertia-react/structure.md +5 -0
  108. package/playbooks/platform/laravel-ui/inertia-react/testing.md +5 -0
  109. package/playbooks/platform/laravel-ui/livewire/architecture.md +5 -0
  110. package/playbooks/platform/laravel-ui/livewire/runtime.md +5 -0
  111. package/playbooks/platform/laravel-ui/livewire/security.md +5 -0
  112. package/playbooks/platform/laravel-ui/livewire/structure.md +5 -0
  113. package/playbooks/platform/laravel-ui/livewire/testing.md +5 -0
  114. package/playbooks/platform/mobile.manifest.json +17 -0
  115. package/playbooks/platform/mobile.md +15 -0
  116. package/playbooks/platform/web.manifest.json +17 -0
  117. package/playbooks/platform/web.md +17 -0
  118. package/playbooks/stack/expo/architecture.md +27 -0
  119. package/playbooks/stack/expo/runtime.md +14 -0
  120. package/playbooks/stack/expo/security.md +18 -0
  121. package/playbooks/stack/expo/structure.md +26 -0
  122. package/playbooks/stack/expo/testing.md +13 -0
  123. package/playbooks/stack/laravel/architecture.md +34 -0
  124. package/playbooks/stack/laravel/runtime.md +27 -0
  125. package/playbooks/stack/laravel/security.md +23 -0
  126. package/playbooks/stack/laravel/structure.md +29 -0
  127. package/playbooks/stack/laravel/testing.md +22 -0
  128. package/playbooks/stack/laravel-ui.manifest.json +42 -0
  129. package/playbooks/stack/laravel.manifest.json +35 -0
  130. package/playbooks/stack/nextjs/architecture.md +62 -0
  131. package/playbooks/stack/nextjs/runtime.md +31 -0
  132. package/playbooks/stack/nextjs/security.md +32 -0
  133. package/playbooks/stack/nextjs/structure.md +50 -0
  134. package/playbooks/stack/nextjs/testing.md +20 -0
  135. package/playbooks/stack/nextjs.manifest.json +49 -0
  136. package/playbooks/stack/no-frontend.manifest.json +25 -0
  137. package/playbooks/stack/none.manifest.json +21 -0
  138. package/playbooks/stack/react-native.manifest.json +49 -0
  139. package/playbooks/stack/react-vite/architecture.md +33 -0
  140. package/playbooks/stack/react-vite/runtime.md +13 -0
  141. package/playbooks/stack/react-vite/security.md +16 -0
  142. package/playbooks/stack/react-vite/structure.md +23 -0
  143. package/playbooks/stack/react-vite/testing.md +12 -0
  144. package/playbooks/stack/react-vite.manifest.json +46 -0
  145. package/playbooks/stack/springboot/architecture.md +40 -0
  146. package/playbooks/stack/springboot/runtime.md +28 -0
  147. package/playbooks/stack/springboot/security.md +28 -0
  148. package/playbooks/stack/springboot/structure.md +30 -0
  149. package/playbooks/stack/springboot/testing.md +22 -0
  150. package/playbooks/stack/springboot.manifest.json +38 -0
  151. package/playbooks/styling/css-modules-extensions.md +267 -0
  152. package/playbooks/styling/css-modules.manifest.json +11 -0
  153. package/playbooks/styling/native-styles.manifest.json +9 -0
  154. package/playbooks/styling/native-styles.md +29 -0
  155. package/playbooks/styling/tailwind-extensions.md +182 -0
  156. package/playbooks/styling/tailwind.manifest.json +11 -0
  157. package/playbooks/universal/accessibility.manifest.json +11 -0
  158. package/playbooks/universal/accessibility.md +45 -0
  159. package/playbooks/universal/coding-rules.manifest.json +12 -0
  160. package/playbooks/universal/coding-rules.md +281 -0
  161. package/playbooks/universal/error-handling.manifest.json +10 -0
  162. package/playbooks/universal/error-handling.md +21 -0
  163. package/playbooks/universal/git-conventions.manifest.json +10 -0
  164. package/playbooks/universal/git-conventions.md +186 -0
  165. package/playbooks/universal/observability.manifest.json +10 -0
  166. package/playbooks/universal/observability.md +29 -0
  167. package/playbooks/universal/security.manifest.json +11 -0
  168. package/playbooks/universal/security.md +30 -0
  169. package/playbooks/universal/typescript.manifest.json +11 -0
  170. package/playbooks/universal/typescript.md +272 -0
  171. package/scripts/compatibility-matrix.mjs +48 -0
  172. package/scripts/validate-content.mjs +76 -0
  173. package/scripts/verify-generated.mjs +153 -0
  174. package/templates/agents/nextjs.md +47 -0
  175. package/templates/agents/react-native.md +48 -0
  176. package/templates/agents/react-vite.md +48 -0
  177. package/templates/docker/compose/postgres.yml +29 -0
  178. package/templates/docker/compose/springboot.yml +69 -0
  179. package/templates/docker/compose/supabase.yml +18 -0
  180. package/templates/docker/compose-prod/springboot.yml +64 -0
  181. package/templates/docker/dockerfile/laravel-inertia.dev.dockerfile +21 -0
  182. package/templates/docker/dockerfile/laravel.dev.dockerfile +12 -0
  183. package/templates/docker/dockerfile/laravel.prod.dockerfile +13 -0
  184. package/templates/docker/dockerfile/nextjs.dev.dockerfile +6 -0
  185. package/templates/docker/dockerfile/nextjs.prod.dockerfile +18 -0
  186. package/templates/docker/dockerfile/springboot.dev.dockerfile +5 -0
  187. package/templates/docker/dockerfile/springboot.prod.dockerfile +10 -0
  188. package/templates/docker/dockerfile/vite.dev.dockerfile +6 -0
  189. package/templates/docker/dockerfile/vite.prod.dockerfile +12 -0
  190. package/templates/gitignore/base.gitignore +32 -0
  191. package/templates/gitignore/nextjs.gitignore +40 -0
  192. package/templates/gitignore/react-native.gitignore +43 -0
  193. package/templates/gitignore/react-vite.gitignore +39 -0
  194. package/templates/makefile/frontend.mk +31 -0
  195. package/templates/makefile/laravel.mk +47 -0
  196. package/templates/makefile/postgres.mk +40 -0
  197. package/templates/makefile/springboot.mk +125 -0
  198. package/templates/makefile/supabase.mk +42 -0
package/lib/files.js ADDED
@@ -0,0 +1,158 @@
1
+ // ─── Generated File Contents ──────────────────────────────────────────────────
2
+ // Each function returns the content for a specific generated file.
3
+ // Static helpers only — no stack identity checks. Stack-specific files are
4
+ // now template-driven via templates/ and readTemplate().
5
+
6
+ export function contextMd(vars, expectedConcerns) {
7
+ const concernsBlock = (expectedConcerns && expectedConcerns.length)
8
+ ? expectedConcerns.map((c) => `- ${c}`).join('\n')
9
+ : '- (none selected — all optional concerns remain available)'
10
+ return `# CONTEXT.md
11
+
12
+ ## Project
13
+ **Name:** {{PROJECT_NAME}}
14
+ **Description:** {{PROJECT_DESCRIPTION}}
15
+ **Stack:** {{STACK}}
16
+ **Styling:** {{STYLE_MODE}}
17
+ **Compatibility profile:** {{COMPATIBILITY_PROFILE}}
18
+ **Year:** {{YEAR}}
19
+
20
+ ## Goals
21
+ <!-- What this project does and who it's for -->
22
+
23
+ ## Key Decisions
24
+ <!-- Architecture and tech decisions made during the project -->
25
+
26
+ ## Out of Scope
27
+ <!-- What this project explicitly does NOT do -->
28
+
29
+ ## Notes
30
+ <!-- Anything else agents or contributors should know -->
31
+
32
+ ## Expected Concerns (advisory)
33
+ ${concernsBlock}
34
+ `
35
+ }
36
+
37
+ export function progressMd() {
38
+ return `# PROGRESS.md
39
+
40
+ ## Status
41
+ 🟡 In Progress
42
+
43
+ ---
44
+
45
+ ## Completed
46
+ <!-- Move items here when done -->
47
+
48
+ ## In Progress
49
+ <!-- Current work -->
50
+
51
+ ## Up Next
52
+ <!-- Planned work -->
53
+
54
+ ## Blocked
55
+ <!-- Anything blocking progress -->
56
+
57
+ ## Decisions Made
58
+ <!-- Key decisions logged here -->
59
+ `
60
+ }
61
+
62
+ export function docPlaceholder(title, description) {
63
+ return `# ${title}
64
+
65
+ > ${description}
66
+
67
+ <!-- Add content here -->
68
+ `
69
+ }
70
+
71
+ export function editorconfig() {
72
+ return `root = true
73
+
74
+ [*]
75
+ end_of_line = lf
76
+ insert_final_newline = true
77
+ charset = utf-8
78
+ trim_trailing_whitespace = true
79
+
80
+ [*.{js,ts,jsx,tsx,json,css,md,yml,yaml}]
81
+ indent_style = space
82
+ indent_size = 2
83
+
84
+ [*.java]
85
+ indent_style = space
86
+ indent_size = 4
87
+
88
+ [Makefile]
89
+ indent_style = tab
90
+ `
91
+ }
92
+
93
+ export function prettierrc() {
94
+ return `{
95
+ "$schema": "https://json.schemastore.org/prettierrc",
96
+ "semi": false,
97
+ "singleQuote": true,
98
+ "jsxSingleQuote": false,
99
+ "trailingComma": "es5",
100
+ "printWidth": 100,
101
+ "tabWidth": 2,
102
+ "useTabs": false,
103
+ "bracketSpacing": true,
104
+ "bracketSameLine": false,
105
+ "arrowParens": "always",
106
+ "endOfLine": "lf"
107
+ }
108
+ `
109
+ }
110
+
111
+ // ─── PR Template ─────────────────────────────────────────────────────────────
112
+
113
+ export function prTemplate() {
114
+ return `## What does this PR do?
115
+
116
+ <!-- Describe the change clearly. What problem does it solve? -->
117
+
118
+ ---
119
+
120
+ ## Type of change
121
+ - [ ] \`feat\` — new feature
122
+ - [ ] \`fix\` — bug fix
123
+ - [ ] \`refactor\` — restructure without behavior change
124
+ - [ ] \`chore\` — deps, config, tooling
125
+ - [ ] \`docs\` — documentation only
126
+ - [ ] \`test\` — adding or updating tests
127
+
128
+ ## Scope
129
+ - [ ] \`frontend\`
130
+ - [ ] \`backend\`
131
+ - [ ] \`mobile\`
132
+ - [ ] \`ci\`
133
+ - [ ] \`docs\`
134
+ - [ ] \`deps\`
135
+
136
+ ---
137
+
138
+ ## How to test?
139
+ 1.
140
+ 2.
141
+ 3.
142
+
143
+ ---
144
+
145
+ ## Checklist
146
+ - [ ] Branched off \`dev\`
147
+ - [ ] Commits follow \`type(scope): description\`
148
+ - [ ] No \`console.log\` or debug code
149
+ - [ ] No hardcoded secrets
150
+ - [ ] Lint passes
151
+ - [ ] Tests pass
152
+ - [ ] Docs updated if endpoints or rules changed
153
+ - [ ] Compatibility matrix passes if a profile, dependency, runtime, or container changed
154
+ - [ ] Major upgrades include migration notes
155
+
156
+ Closes #
157
+ `
158
+ }
@@ -0,0 +1,366 @@
1
+ import fs from 'fs-extra'
2
+ import path from 'path'
3
+ import { randomUUID } from 'node:crypto'
4
+ import { loadCatalog, resolveStack } from './catalog.js'
5
+ import { loadCompatibility } from './compatibility.js'
6
+ import { buildRulesIndex, copySelectedPlaybooks } from './playbooks.js'
7
+ import { buildVars, render, readTemplate } from './template.js'
8
+ import { buildRunnableFiles } from './scaffold.js'
9
+ import { buildLaravelFiles } from './laravel-scaffold.js'
10
+ import {
11
+ contextMd, progressMd, docPlaceholder,
12
+ editorconfig, prettierrc, prTemplate,
13
+ } from './files.js'
14
+
15
+ function laravelCompose(answers, stack, vars) {
16
+ const laravelDir = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '.' : './backend'
17
+ const frontend = stack.frontendKey === 'react'
18
+ ? ` frontend:\n build:\n context: ./frontend\n dockerfile: Dockerfile.dev\n ports:\n - "5173:5173"\n volumes:\n - ./frontend:/app\n - frontend-node-modules:/app/node_modules\n environment:\n VITE_API_URL: http://localhost:8000\n depends_on:\n - backend\n\n`
19
+ : stack.frontendKey === 'nextjs'
20
+ ? ` frontend:\n build:\n context: .\n dockerfile: Dockerfile.dev\n ports:\n - "3000:3000"\n volumes:\n - .:/app\n - /app/backend\n - frontend-node-modules:/app/node_modules\n environment:\n NEXT_PUBLIC_API_URL: http://localhost:8000\n depends_on:\n - backend\n\n`
21
+ : ''
22
+ const frontendVolume = frontend ? ' frontend-node-modules:\n' : ''
23
+ return `services:\n${frontend} backend:\n build:\n context: ${laravelDir}\n dockerfile: Dockerfile.dev\n ports:\n - "8000:8000"\n volumes:\n - ${laravelDir}:/app\n - laravel-vendor:/app/vendor\n environment:\n APP_ENV: local\n APP_DEBUG: "true"\n APP_KEY: \${APP_KEY:-}\n DB_CONNECTION: pgsql\n DB_HOST: db\n DB_PORT: 5432\n DB_DATABASE: \${POSTGRES_DB}\n DB_USERNAME: \${POSTGRES_USER}\n DB_PASSWORD: \${POSTGRES_PASSWORD}\n depends_on:\n db:\n condition: service_healthy\n\n db:\n image: ${vars.POSTGRES_IMAGE}\n ports:\n - "5432:5432"\n environment:\n POSTGRES_DB: \${POSTGRES_DB}\n POSTGRES_USER: \${POSTGRES_USER}\n POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}\n volumes:\n - postgres-data:/var/lib/postgresql/data\n healthcheck:\n test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER} -d \${POSTGRES_DB}"]\n interval: 5s\n timeout: 5s\n retries: 10\n\nvolumes:\n${frontendVolume} laravel-vendor:\n postgres-data:\n`
24
+ }
25
+
26
+ /**
27
+ * Main entry point — generates the full project
28
+ */
29
+ export async function generateProject(answers, cliRoot) {
30
+ validateAnswers(answers)
31
+ const finalDest = path.join(process.cwd(), answers.projectName)
32
+ const dest = path.join(process.cwd(), `.${answers.projectName}.tmp-${randomUUID()}`)
33
+ const playbooksDir = path.join(cliRoot, 'playbooks')
34
+ const ciDir = path.join(cliRoot, 'ci')
35
+ const { profile } = await loadCompatibility(
36
+ path.join(cliRoot, 'compatibility/profiles.json'),
37
+ answers.compatibilityProfile,
38
+ )
39
+ const catalog = await loadCatalog(playbooksDir, profile)
40
+
41
+ // Ensure styling is resolved before passing to resolveStack
42
+ const resolvedAnswers = {
43
+ ...answers,
44
+ styling: answers.styling || catalog.byId[answers.frontend]?.stylingOptions?.[0] || 'tailwind',
45
+ }
46
+
47
+ const stack = resolveStack(resolvedAnswers, catalog)
48
+ const vars = buildVars(resolvedAnswers, stack)
49
+ const templatesDir = path.join(cliRoot, 'templates')
50
+
51
+ // 1. Refuse to merge into an existing project. Generation must never silently
52
+ // overwrite user work.
53
+ if (await fs.pathExists(finalDest)) {
54
+ throw new Error(`Destination already exists: ${finalDest}`)
55
+ }
56
+
57
+ try {
58
+ // 2. Staging folder. It is moved into place only after every generation
59
+ // step succeeds, so failures never leave a half-written project.
60
+ await fs.ensureDir(dest)
61
+
62
+ // 3. Root documentation and repository files. Directories are created only
63
+ // when a real generated file needs them; empty architecture theatre is not
64
+ // part of the scaffold contract.
65
+ await generateRootFiles(dest, resolvedAnswers, vars, stack, templatesDir)
66
+
67
+ // 4. Small, runnable framework foundations. These files are the executable
68
+ // contract that the playbooks describe.
69
+ await generateRunnableFiles(dest, resolvedAnswers, stack, vars)
70
+
71
+ // 5. Product documentation
72
+ await generateDocs(dest, resolvedAnswers, stack)
73
+
74
+ // 6. GitHub Actions CI (template-driven)
75
+ if (resolvedAnswers.githubActions) {
76
+ await generateCI(dest, stack, ciDir, resolvedAnswers, vars)
77
+ }
78
+
79
+ // 7. Copy selected playbooks (including concern files)
80
+ await copySelectedPlaybooks(playbooksDir, dest, stack)
81
+
82
+ // 8. RULES.md
83
+ const rulesContent = await buildRulesIndex(stack, catalog, playbooksDir)
84
+ await write(dest, 'RULES.md', rulesContent)
85
+ await fs.move(dest, finalDest)
86
+ } catch (error) {
87
+ await fs.remove(dest)
88
+ throw error
89
+ }
90
+ }
91
+
92
+ function validateAnswers(answers) {
93
+ if (!answers || typeof answers !== 'object') throw new Error('Project answers are required')
94
+ if (!/^[a-z0-9-]+$/.test(answers.projectName || '')) {
95
+ throw new Error('Project name must use lowercase letters, numbers, and hyphens only')
96
+ }
97
+ if (typeof answers.projectDescription !== 'string' || !answers.projectDescription.trim()) {
98
+ throw new Error('Project description is required')
99
+ }
100
+ if (answers.testing && !['none', 'basic', 'full'].includes(answers.testing)) {
101
+ throw new Error(`Unknown testing setup: ${answers.testing}`)
102
+ }
103
+ if (answers.architecture && !['small', 'medium', 'large'].includes(answers.architecture)) {
104
+ throw new Error(`Unknown architecture profile: ${answers.architecture}`)
105
+ }
106
+ if (answers.authentication && !['yes', 'not-yet', 'none'].includes(answers.authentication)) {
107
+ throw new Error(`Unknown authentication choice: ${answers.authentication}`)
108
+ }
109
+ if (answers.backend === 'springboot' && !/^[a-z]+(\.[a-z][a-z0-9]*)+$/.test(answers.packageName || '')) {
110
+ throw new Error('Java package name must look like com.example')
111
+ }
112
+ }
113
+
114
+ // ─── Root files ───────────────────────────────────────────────────────────────
115
+
116
+ async function generateRootFiles(dest, answers, vars, stack, templatesDir) {
117
+ await writeTemplate(dest, 'CONTEXT.md', contextMd(vars, answers.expectedConcerns), vars)
118
+ // AGENTS.md — template-driven
119
+ {
120
+ const tpl = await readTemplate(templatesDir, 'agents', stack.agentsTemplate, '.md')
121
+ if (tpl) await writeTemplate(dest, 'AGENTS.md', tpl, vars)
122
+ else await write(dest, 'AGENTS.md', `# AGENTS.md\nStack: ${stack.label}\n`)
123
+ }
124
+ await write(dest, 'PROGRESS.md', progressMd())
125
+ // README.md is generated with the runnable framework files. Keeping one
126
+ // owner prevents a generic template from drifting away from real commands.
127
+ // .gitignore — template-driven
128
+ {
129
+ const tpl = await readTemplate(templatesDir, 'gitignore', stack.gitignoreTemplate, '.gitignore')
130
+ if (tpl) await writeTemplate(dest, '.gitignore', tpl, vars)
131
+ else {
132
+ const baseTpl = await readTemplate(templatesDir, 'gitignore', 'base', '.gitignore')
133
+ if (baseTpl) await write(dest, '.gitignore', baseTpl)
134
+ }
135
+ }
136
+ await write(dest, '.editorconfig', editorconfig())
137
+ await write(dest, '.prettierrc', prettierrc())
138
+
139
+ // Makefile — template-driven (web only)
140
+ if (answers.makefile && !stack.isMobile) {
141
+ const tpl = await readTemplate(templatesDir, 'makefile', stack.makefileTemplate, '.mk')
142
+ if (tpl) await writeTemplate(dest, 'Makefile', tpl, vars)
143
+ }
144
+
145
+ // Docker — template-driven
146
+ // Mobile frontends run through Expo, but a separate Laravel backend still
147
+ // needs its backend and PostgreSQL services. laravelCompose intentionally
148
+ // omits a frontend service for React Native.
149
+ if (answers.docker && (!stack.isMobile || stack.backendKey === 'laravel')) {
150
+ // docker-compose.yml
151
+ let composeTpl = null
152
+ if (stack.backendKey === 'laravel') {
153
+ composeTpl = laravelCompose(answers, stack, vars)
154
+ } else if (stack.needsPackage) {
155
+ composeTpl = await readTemplate(templatesDir, 'docker/compose', 'springboot', '.yml')
156
+ } else if (stack.backendKey === 'supabase') {
157
+ composeTpl = await readTemplate(templatesDir, 'docker/compose', 'supabase', '.yml')
158
+ } else {
159
+ composeTpl = await readTemplate(templatesDir, 'docker/compose', 'postgres', '.yml')
160
+ }
161
+ if (composeTpl) await writeTemplate(dest, 'docker-compose.yml', composeTpl, vars)
162
+
163
+ if (stack.backendKey === 'laravel') {
164
+ const laravelRoot = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
165
+ const dockerfileName = stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react' ? 'laravel-inertia.dev' : 'laravel.dev'
166
+ const laravelDev = await readTemplate(templatesDir, 'docker/dockerfile', dockerfileName, '.dockerfile')
167
+ if (laravelDev) await writeTemplate(dest, `${laravelRoot}Dockerfile.dev`, laravelDev, vars)
168
+ const laravelProd = await readTemplate(templatesDir, 'docker/dockerfile', 'laravel.prod', '.dockerfile')
169
+ if (laravelProd) await writeTemplate(dest, `${laravelRoot}Dockerfile`, laravelProd, vars)
170
+ }
171
+
172
+ // docker-compose.prod.yml — only for springboot
173
+ if (stack.needsPackage) {
174
+ const prodTpl = await readTemplate(templatesDir, 'docker/compose-prod', 'springboot', '.yml')
175
+ if (prodTpl) await writeTemplate(dest, 'docker-compose.prod.yml', prodTpl, vars)
176
+
177
+ const beDev = await readTemplate(templatesDir, 'docker/dockerfile', 'springboot.dev', '.dockerfile')
178
+ if (beDev) await writeTemplate(dest, 'backend/Dockerfile.dev', beDev, vars)
179
+ const beProd = await readTemplate(templatesDir, 'docker/dockerfile', 'springboot.prod', '.dockerfile')
180
+ if (beProd) await writeTemplate(dest, 'backend/Dockerfile', beProd, vars)
181
+ }
182
+
183
+ // frontend dockerfiles — vite vs nextjs
184
+ if (stack.frontendKey === 'react') {
185
+ const viteDev = await readTemplate(templatesDir, 'docker/dockerfile', 'vite.dev', '.dockerfile')
186
+ if (viteDev) await writeTemplate(dest, 'frontend/Dockerfile.dev', viteDev, vars)
187
+ const viteProd = await readTemplate(templatesDir, 'docker/dockerfile', 'vite.prod', '.dockerfile')
188
+ if (viteProd) {
189
+ await writeTemplate(dest, 'frontend/Dockerfile', viteProd, vars)
190
+ const nginx = `server {\n listen 80;\n location / {\n root /usr/share/nginx/html;\n index index.html;\n try_files $uri $uri/ /index.html;\n }\n}\n`
191
+ await write(dest, 'frontend/nginx.conf', nginx)
192
+ }
193
+ } else if (stack.frontendKey === 'nextjs') {
194
+ const nextDev = await readTemplate(templatesDir, 'docker/dockerfile', 'nextjs.dev', '.dockerfile')
195
+ if (nextDev) await writeTemplate(dest, 'Dockerfile.dev', nextDev, vars)
196
+ const nextProd = await readTemplate(templatesDir, 'docker/dockerfile', 'nextjs.prod', '.dockerfile')
197
+ if (nextProd) await writeTemplate(dest, 'Dockerfile', nextProd, vars)
198
+ }
199
+ }
200
+
201
+ // PR template
202
+ if (answers.githubActions) {
203
+ await write(dest, '.github/PULL_REQUEST_TEMPLATE.md', prTemplate())
204
+ }
205
+ }
206
+
207
+ // ─── Doc placeholders ─────────────────────────────────────────────────────────
208
+
209
+ async function generateDocs(dest, answers, stack) {
210
+ const docs = [
211
+ ['docs/api/overview.md', 'API Overview', 'Base URL, authentication method, and response format.'],
212
+ ['docs/api/endpoints.md', 'API Endpoints', 'All endpoint documentation goes here.'],
213
+ ['docs/api/errors.md', 'Error Code Registry', 'All error codes with status, category, and description.'],
214
+ ['docs/architecture/overview.md', 'Architecture Overview', 'System design and how the pieces connect.'],
215
+ ['docs/architecture/database-schema.md', 'Database Schema', 'Tables, columns, relationships, and ERD.'],
216
+ ['docs/architecture/auth-flow.md', 'Auth Flow', 'Step-by-step authentication flow.'],
217
+ ['docs/guides/setup.md', 'Local Setup Guide', 'Prerequisites and step-by-step local setup.'],
218
+ ['docs/guides/deployment.md', 'Deployment Guide', 'How to deploy to production.'],
219
+ ['docs/guides/env-variables.md', 'Environment Variables', 'Every variable: name, required, description, example.'],
220
+ ]
221
+ for (const [filePath, title, description] of docs) {
222
+ await write(dest, filePath, docPlaceholder(title, description))
223
+ }
224
+
225
+ const frontendRoot = stack.frontendKey === 'react' ? 'frontend/' : ''
226
+ const validation = stack.isMobile
227
+ ? `npm run typecheck\n${answers.testing === 'none' ? '' : 'npm test -- --runInBand\n'}npm run build -- --platform web`
228
+ : 'npm run lint\nnpm run typecheck\nnpm run test --if-present\nnpm run build'
229
+ await write(dest, 'docs/guides/setup.md', `# Local Setup Guide\n\n## Prerequisites\n\n- Node.js 20 or newer\n- npm\n${stack.backendKey === 'springboot' ? '- Java 21 and Maven 3.9, or Docker\n- PostgreSQL 16, or Docker\n' : ''}${stack.backendKey === 'supabase' ? '- A running Docker-compatible runtime for the pinned local Supabase CLI\n' : ''}\n## Install and start\n\n\`\`\`bash\n${frontendRoot ? `cd ${frontendRoot}\n` : ''}cp .env.example ${stack.isMobile ? '.env' : '.env.local'}\nnpm install\nnpm run dev\n\`\`\`\n${stack.backendKey === 'springboot' ? `\nIn another terminal from the repository root:\n\n\`\`\`bash\n${stack.frontendKey === 'react' ? 'cp .env.example .env\n' : ''}docker compose up -d db\ncd backend\nmvn spring-boot:run\n\`\`\`\n` : ''}${stack.backendKey === 'supabase' ? `\nStart the local Supabase stack from the npm package directory:\n\n\`\`\`bash\n${frontendRoot ? `cd ${frontendRoot}\n` : ''}npm run supabase:start\n\`\`\`\n` : ''}\n## Validate\n\n\`\`\`bash\n${frontendRoot ? `cd ${frontendRoot}\n` : ''}${validation}\n\`\`\`\n\nCommit the generated \`package-lock.json\`; CI uses \`npm ci\`.\n`)
230
+
231
+ const setupPath = path.join(dest, 'docs/guides/setup.md')
232
+ let setupGuide = (await fs.readFile(setupPath, 'utf8'))
233
+ .replace('Node.js 20 or newer', `Node.js ${stack.profile.runtimes.node}`)
234
+ .replace('Java 21 and Maven 3.9, or Docker', `Java ${stack.profile.runtimes.java}; Maven ${stack.profile.runtimes.maven} or Docker is used through the generated launcher`)
235
+ .replace('PostgreSQL 16, or Docker', `PostgreSQL ${stack.profile.runtimes.postgres}, or Docker`)
236
+ .replace('mvn spring-boot:run', './mvnw spring-boot:run # use mvnw.cmd on Windows')
237
+ if (stack.backendKey === 'laravel') {
238
+ const laravelDir = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? '' : 'backend/'
239
+ setupGuide = `# Local Setup Guide\n\n## Docker-first setup\n\nOnly Docker with Compose is required. Make is optional; every Make target has a direct Docker equivalent.\n\n\`\`\`bash\ncp ${laravelDir}.env.example ${laravelDir}.env\ndocker compose build\ndocker compose up -d\ndocker compose exec backend php artisan key:generate\ndocker compose exec backend php artisan migrate\n\`\`\`\n\nLater runs use \`docker compose up -d\`; rebuilding is explicit with \`docker compose build\`. If Make is available, \`make setup\` performs the first-run sequence and \`make run\` starts existing images without rebuilding.\n\n## Validate\n\n\`\`\`bash\ndocker compose exec backend composer check\n\`\`\`\n\nWhen dependencies are installed outside Docker, commit \`composer.lock\`${stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react' ? ' and `package-lock.json`' : ''}.\n`
240
+ }
241
+ await fs.writeFile(setupPath, setupGuide, 'utf8')
242
+
243
+ const envLocation = stack.frontendKey === 'react' ? '`frontend/.env` for client values' : stack.isMobile ? '`.env`' : '`.env.local`'
244
+ await write(dest, 'docs/guides/env-variables.md', `# Environment Variables\n\nCopy the generated example before starting. Client environment location: ${envLocation}.\n\n| Variable | Visibility | Required | Purpose |\n|---|---|---:|---|\n${stack.env.map((name) => `| \`${name}\` | ${name.startsWith(stack.envPrefix) ? 'client/public' : 'server only'} | yes | ${environmentPurpose(name)} |`).join('\n')}\n\nValues with \`${stack.envPrefix}\` are bundled into client code and must never contain secrets. Keep real environment files out of version control.\n`)
245
+
246
+ await write(dest, 'docs/architecture/overview.md', `# Architecture Overview\n\n## Runtime shape\n\n- Frontend: ${stack.frontendLabel}\n- Backend/data: ${stack.backendLabel}\n- Platform: ${stack.platform}\n- Architecture profile: ${stack.architecture}\n- Authentication: ${stack.authentication}\n\nThe generated application is intentionally a small vertical slice. Add domain features only after recording product goals and boundaries in \`CONTEXT.md\`. Keep entry points thin, validate at trust boundaries, and enforce authorization beside protected data or side effects.\n\n## Verification boundary\n\nThe starter is considered healthy when its lint/typecheck/tests/build commands pass. Documentation explains those executable patterns; it does not override working code and tests.\n`)
247
+
248
+ await write(dest, 'docs/architecture/auth-flow.md', authDocumentation(stack))
249
+
250
+ const health = stack.backendKey === 'springboot'
251
+ ? 'GET http://localhost:8080/api/health'
252
+ : stack.backendKey === 'laravel'
253
+ ? 'GET http://localhost:8000/api/health'
254
+ : stack.frontendKey === 'nextjs' ? 'GET /api/health' : '(No custom HTTP API is generated for this client-only starter.)'
255
+ await write(dest, 'docs/api/overview.md', `# API Overview\n\n## Health\n\n\`${health}\`\n\nAuthentication and authorization behavior is documented in \`docs/architecture/auth-flow.md\`. Add endpoints to \`docs/api/endpoints.md\` in the same change that adds their implementation and tests.\n`)
256
+ }
257
+
258
+ function environmentPurpose(name) {
259
+ if (name.endsWith('SUPABASE_URL')) return 'Supabase project URL.'
260
+ if (name.endsWith('SUPABASE_PUBLISHABLE_KEY')) return 'Public Supabase key; RLS protects data.'
261
+ if (name.endsWith('API_URL')) return 'Base URL of the application API.'
262
+ if (name === 'DATABASE_URL') return 'Server-side PostgreSQL JDBC connection URL.'
263
+ if (name === 'POSTGRES_USER') return 'Local/deployed database user.'
264
+ if (name === 'POSTGRES_PASSWORD') return 'Database credential; replace the development example.'
265
+ if (name === 'POSTGRES_DB') return 'Database name.'
266
+ if (name === 'SPRING_PROFILES_ACTIVE') return 'Active Spring configuration profile.'
267
+ if (name === 'SPRING_SECURITY_USER_NAME') return 'Development-only generated Spring login name; replace with the product identity store.'
268
+ if (name === 'SPRING_SECURITY_USER_PASSWORD') return 'Development-only Spring login credential; never commit a real value.'
269
+ if (name === 'OIDC_ISSUER_URI') return 'Trusted OpenID Connect issuer used to validate access tokens.'
270
+ if (name === 'OIDC_AUDIENCE') return 'Required audience for tokens accepted by this API.'
271
+ if (name === 'SESSION_DOMAIN') return 'Exact domain scope for the secure session cookie.'
272
+ if (name === 'SANCTUM_STATEFUL_DOMAINS') return 'Comma-separated first-party browser hosts allowed to use Sanctum session authentication.'
273
+ if (name === 'CORS_ALLOWED_ORIGINS') return 'Exact browser origins allowed to make credentialed API requests.'
274
+ if (name === 'AUTH0_DOMAIN') return 'Trusted Auth0 tenant domain used for access-token signature and issuer validation.'
275
+ if (name === 'AUTH0_AUDIENCE') return 'Exact API audience required in accepted Auth0 access tokens.'
276
+ return 'Stack configuration.'
277
+ }
278
+
279
+ function authDocumentation(stack) {
280
+ if (stack.authentication === 'public') return `# Authentication Flow\n\nThis project intentionally has no user accounts. No login, session, or refresh mechanism is generated. Application endpoints are public; add authentication and resource authorization before introducing protected data. Do not rerun the generator over this project to add auth—change the existing project deliberately with its stack security playbook and tests.\n`
281
+ if (stack.authentication === 'undecided') return `# Authentication Flow\n\nAuthentication is intentionally undecided. No login or pretend role state is generated. ${stack.backendKey === 'springboot' ? 'Spring permits health endpoints and denies every other request.' : 'The client starts without a user session; any remote service remains responsible for access control.'}\n\nWhen auth is added, document login, storage, expiry, refresh ownership, logout, recovery, request-integrity boundaries, and resource authorization here. Do not rerun the generator over this project.\n`
282
+ if (stack.authentication === 'supabase' && stack.frontendKey === 'nextjs') return `# Authentication Flow\n\n## Generated Supabase SSR flow\n\n1. Browser code uses \`src/lib/supabase/client.ts\` with the publishable key.\n2. Server code uses the cookie-aware client in \`src/lib/supabase/server.ts\`.\n3. \`src/proxy.ts\` refreshes/verifies with \`getClaims()\` and synchronizes request and response cookies.\n4. The PKCE callback exchanges an authorization code and redirects only to the application root.\n5. Every server operation authorizes again near data access; RLS is the final row boundary.\n\nProxy is not authorization. Never authorize from \`getSession()\` alone. See \`playbooks/capabilities/supabase/nextjs.md\`.\n`
283
+ if (stack.authentication === 'supabase') return `# Authentication Flow\n\nSupabase Auth owns access-token and refresh-token rotation. The project uses only the publishable key and RLS remains the authorization boundary. ${stack.isMobile ? 'Expo stores the session in SecureStore and binds automatic refresh to application foreground/background lifecycle.' : 'The browser SDK owns persistence and refresh; do not build a second interceptor.'}\n\nRegister exact callback/deep-link URLs and test expiry, revocation, logout, recovery, and RLS allow/deny cases. See the selected Supabase capability playbook.\n`
284
+ if (stack.authentication === 'session') return `# Authentication Flow\n\nSpring Security owns a server-side browser session. The browser receives an HttpOnly session cookie; Spring rotates the session identifier after authentication, keeps CSRF protection enabled, and invalidates the session on logout. The generated default login is development scaffolding—replace it with the product identity store before production. Authorization remains server-side and includes resource ownership. There is no browser refresh token.\n`
285
+ if (stack.authentication === 'laravel-session') return `# Authentication Flow\n\nLaravel owns the website session. Login regenerates the session identifier, logout invalidates the session and rotates the CSRF token, and protected routes use server-side authorization. The browser receives an HttpOnly session cookie; there is no browser refresh token. Keep CSRF protection enabled for every cookie-authenticated mutation.\n`
286
+ if (stack.authentication === 'sanctum-spa') return `# Authentication Flow\n\nLaravel Sanctum uses Laravel's secure session cookie for this first-party browser application. The SPA first requests \`/sanctum/csrf-cookie\`, then sends credentialed login and API requests. Sanctum does not give this browser a custom bearer/refresh-token system. Configure exact stateful domains and CORS origins, and enforce resource authorization in Laravel.\n`
287
+ if (stack.authentication === 'laravel-oidc') return `# Authentication Flow\n\nAuth0 owns login, access/refresh-token issuance, rotation, revocation, and recovery. Clients use Authorization Code with PKCE. Laravel uses the pinned Auth0 resource-server adapter to accept access tokens only and validate signature, issuer, audience, and time claims. Refresh tokens never go to this API. Configure \`AUTH0_DOMAIN\` and \`AUTH0_AUDIENCE\`; resource ownership and permission checks remain application responsibilities.\n`
288
+ return `# Authentication Flow\n\nAn external OpenID Connect provider owns login, access/refresh token issuance, rotation, revocation, and recovery. Clients use Authorization Code with PKCE. Spring is an OAuth2 Resource Server: it accepts bearer access tokens and validates signature, issuer, audience, time, and authorities. Refresh tokens never go to the Spring resource API. Configure \`OIDC_ISSUER_URI\` and \`OIDC_AUDIENCE\`, then test invalid and authorized tokens.\n`
289
+ }
290
+
291
+ // ─── CI — template-driven ─────────────────────────────────────────────────────
292
+
293
+ async function generateCI(dest, stack, ciDir, answers, vars) {
294
+ // Frontend CI — read from ci/{ciTemplate}.yml
295
+ const feTpl = path.join(ciDir, `${stack.ciTemplate}.yml`)
296
+ if (await fs.pathExists(feTpl)) {
297
+ let content = await fs.readFile(feTpl, 'utf-8')
298
+ if (answers.testing === 'none') {
299
+ content = content.replace(/^\s*- run: npm (?:run )?test.*\n/gm, '')
300
+ }
301
+ if (stack.frontendKey === 'nextjs' && stack.backendKey !== 'supabase') {
302
+ content = content.replace(/\n\s+env:\n\s+NEXT_PUBLIC_SUPABASE_URL:.*\n\s+NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY:.*\n/, '\n')
303
+ }
304
+ if (stack.frontendKey === 'react' && stack.backendKey === 'supabase') {
305
+ content = content.replace(
306
+ ' VITE_API_URL: http://localhost:8080',
307
+ ' VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}\n VITE_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_KEY }}',
308
+ )
309
+ }
310
+ if (answers.testing === 'full' && !stack.isMobile) {
311
+ content += ' - name: Install Playwright browser\n run: npx playwright install --with-deps chromium\n'
312
+ content += ' - name: Run end-to-end tests\n run: npm run test:e2e\n'
313
+ if (stack.frontendKey === 'nextjs' && stack.backendKey === 'supabase') {
314
+ content += ' env:\n NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}\n NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY }}\n'
315
+ }
316
+ }
317
+ await write(dest, `.github/workflows/ci-frontend.yml`, render(content, vars))
318
+ }
319
+
320
+ // Backend CI — always springboot.yml if backend is Spring Boot
321
+ if (stack.needsPackage) {
322
+ const beTpl = path.join(ciDir, 'springboot.yml')
323
+ if (await fs.pathExists(beTpl)) {
324
+ let content = await fs.readFile(beTpl, 'utf-8')
325
+ if (answers.testing === 'none') {
326
+ content = content.replace(' - name: Run tests\n run: ./mvnw --batch-mode test\n', '')
327
+ }
328
+ await write(dest, `.github/workflows/ci-backend.yml`, render(content, vars))
329
+ }
330
+ }
331
+ if (stack.backendKey === 'laravel') {
332
+ const beTpl = path.join(ciDir, 'laravel.yml')
333
+ if (await fs.pathExists(beTpl)) {
334
+ let content = await fs.readFile(beTpl, 'utf-8')
335
+ if (answers.testing === 'none') content = content.replace(' - run: composer check\n', ' - run: composer format:check\n - run: composer analyse\n')
336
+ if (stack.frontendKey === 'laravel-ui' && stack.laravelUi === 'inertia-react') {
337
+ content += ` - uses: actions/setup-node@v4\n with:\n node-version: "${stack.profile.runtimes.node}"\n cache: npm\n cache-dependency-path: package-lock.json\n - run: npm ci\n - run: npm run build\n`
338
+ }
339
+ await write(dest, `.github/workflows/ci-backend.yml`, render(content, vars))
340
+ }
341
+ }
342
+ }
343
+
344
+ // ─── Runnable application files ─────────────────────────────────────────────────
345
+
346
+ async function generateRunnableFiles(dest, answers, stack, vars) {
347
+ const files = { ...buildRunnableFiles(answers, stack, vars), ...buildLaravelFiles(answers, stack, vars) }
348
+ for (const [filePath, content] of Object.entries(files)) await write(dest, filePath, content)
349
+ if (stack.backendKey === 'springboot') await fs.chmod(path.join(dest, 'backend/mvnw'), 0o755)
350
+ if (stack.backendKey === 'laravel') {
351
+ const artisan = stack.frontendKey === 'laravel-ui' || stack.frontendKey === 'no-frontend' ? 'artisan' : 'backend/artisan'
352
+ await fs.chmod(path.join(dest, artisan), 0o755)
353
+ }
354
+ }
355
+
356
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
357
+
358
+ async function write(dest, filePath, content) {
359
+ const fullPath = path.join(dest, filePath)
360
+ await fs.ensureDir(path.dirname(fullPath))
361
+ await fs.writeFile(fullPath, content, 'utf-8')
362
+ }
363
+
364
+ async function writeTemplate(dest, filePath, content, vars) {
365
+ await write(dest, filePath, render(content, vars))
366
+ }
@@ -0,0 +1,77 @@
1
+ const BACK = Symbol('back')
2
+
3
+ function enabled(question, answers) {
4
+ return typeof question.when === 'function' ? question.when(answers) : question.when !== false
5
+ }
6
+
7
+ async function resolved(value, answers) {
8
+ return typeof value === 'function' ? value(answers) : value
9
+ }
10
+
11
+ function clearFrom(questions, answers, index) {
12
+ for (let cursor = index; cursor < questions.length; cursor += 1) {
13
+ delete answers[questions[cursor].name]
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Runs Inquirer questions one at a time so users can safely revisit earlier answers.
19
+ * List/checkbox/confirm prompts expose a visible Back choice; text prompts accept `:back`.
20
+ */
21
+ export async function promptWithBack(inquirer, questions, initialAnswers = {}) {
22
+ const answers = { ...initialAnswers }
23
+ const history = []
24
+ let index = 0
25
+
26
+ while (index < questions.length) {
27
+ const source = questions[index]
28
+ if (!enabled(source, answers)) { index += 1; continue }
29
+
30
+ const canGoBack = history.length > 0
31
+ const question = { ...source, when: undefined }
32
+ if (answers[source.name] !== undefined && question.default === undefined) {
33
+ question.default = answers[source.name]
34
+ }
35
+ if (question.type === 'input' && canGoBack) {
36
+ question.message = `${question.message} (type :back to return)`
37
+ } else if (question.type === 'confirm') {
38
+ const defaultValue = await resolved(question.default, answers)
39
+ question.type = 'list'
40
+ question.choices = [
41
+ { name: 'Yes', value: true },
42
+ { name: 'No', value: false },
43
+ ...(canGoBack ? [{ name: '← Back', value: BACK }] : []),
44
+ ]
45
+ question.default = defaultValue ? 0 : 1
46
+ } else if (['list', 'checkbox'].includes(question.type) && canGoBack) {
47
+ const choices = await resolved(question.choices, answers)
48
+ question.choices = [...choices, new inquirer.Separator(), { name: '← Back', value: BACK }]
49
+ }
50
+
51
+ if (typeof question.default === 'function') question.default = await question.default(answers)
52
+ const result = await inquirer.prompt([question], answers)
53
+ const value = result[source.name]
54
+ const requestedBack = value === BACK || value === ':back' || (Array.isArray(value) && value.includes(BACK))
55
+ if (requestedBack) {
56
+ const target = history.pop()
57
+ if (target === undefined) continue
58
+ clearFrom(questions, answers, target)
59
+ index = target
60
+ continue
61
+ }
62
+
63
+ answers[source.name] = value
64
+ history.push(index)
65
+ index += 1
66
+ }
67
+
68
+ return answers
69
+ }
70
+
71
+ export function configurationDecisionChoices() {
72
+ return [
73
+ { name: 'Create project', value: 'create' },
74
+ { name: '← Back and edit', value: 'back' },
75
+ { name: 'Cancel', value: 'cancel' },
76
+ ]
77
+ }
@@ -0,0 +1,3 @@
1
+ // Compatibility export for programmatic consumers. Laravel generation now
2
+ // lives behind its stack adapter boundary.
3
+ export { buildLaravelFiles } from './stacks/laravel/generate.js'