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
@@ -0,0 +1,118 @@
1
+ # Architecture
2
+
3
+ `create-win-project` composes a validated project specification into two coordinated outputs:
4
+
5
+ 1. a small executable application that establishes the stack's real conventions;
6
+ 2. a task-routed documentation layer for humans and coding agents.
7
+
8
+ Executable behavior, tests, and framework configuration are the source of truth. Playbooks explain and extend that behavior; they must not contradict it.
9
+
10
+ ## Generation pipeline
11
+
12
+ ```text
13
+ CLI interview
14
+
15
+ validated answers
16
+
17
+ manifest catalog + compatibility resolver
18
+
19
+ resolved stack descriptor
20
+ ├── runnable framework files
21
+ ├── optional CI / Docker / Makefile files
22
+ ├── project context and documentation
23
+ └── selected playbooks + RULES.md router
24
+
25
+ generated-project contract tests
26
+ ```
27
+
28
+ ### CLI boundary
29
+
30
+ `index.js` owns interaction and presentation. It gathers project identity, frontend, optional backend, styling, one Small/Medium/Large architecture profile, login intent, client audience when relevant, testing depth, and optional operational tooling. Medium is shown first and is the default. It does not contain stack dependency tables.
31
+
32
+ `generateProject()` validates the same answers again because it is also an exported programmatic API. A caller cannot bypass destination-name, Java-package, or testing-profile validation by skipping the CLI.
33
+
34
+ ### Catalog and resolution
35
+
36
+ `lib/catalog.js` loads co-located `*.manifest.json` files and merges selected capabilities into one stack descriptor. `lib/compatibility.js` validates `compatibility/profiles.json` and resolves every package name to an exact version for the selected profile.
37
+
38
+ Stack-specific behavior crosses into core orchestration through the contract in `lib/stacks/contract.js`. Every frontend, backend, or data adapter has a stable identity, declares compatible adapters and supported application/authentication/architecture models, and is added to the explicit registry. Registration is deliberate; adapters are never discovered by scanning directories.
39
+
40
+ Adapters may contribute data in these areas:
41
+
42
+ - questions in a core-defined prompt slot;
43
+ - authentication models;
44
+ - generated file descriptions;
45
+ - environment declarations;
46
+ - install steps;
47
+ - Docker and CI fragments;
48
+ - verification cases.
49
+
50
+ Core code owns prompt order, compatibility-profile resolution, atomic writes, process execution, and final composition. An adapter receives a read-only stack context and returns contributions; it cannot write arbitrary paths or install global tools. Adapter definitions do not accept dependency tables or version fields. Exact versions remain exclusively owned by `compatibility/profiles.json`.
51
+
52
+ Manifests declare:
53
+
54
+ - identity and compatibility (`id`, `kind`, `appliesTo`);
55
+ - dependency names and scripts (never dependency versions);
56
+ - semantic environment names and which ones are client-visible;
57
+ - supported architecture profiles, conditional playbooks, and optional templates;
58
+ - constraints shown to the agent;
59
+ - concerns and their playbook sections.
60
+
61
+ Client environment variables are semantic in manifests (`API_URL`) and receive exactly one framework prefix during resolution (`NEXT_PUBLIC_API_URL`, `VITE_API_URL`, or `EXPO_PUBLIC_API_URL`). Public prefixes always mean the value is shipped to the client.
62
+
63
+ ### Runnable scaffold
64
+
65
+ `lib/scaffold.js` owns the minimum executable vertical slice:
66
+
67
+ - Next.js: route-oriented Small; familiar feature services/actions/queries and owned repositories or remote API clients in Medium; public feature APIs and enforced boundaries in Large.
68
+ - React + Vite: thin browser features in Small, feature modules in Medium, and public feature APIs plus boundary checks in Large.
69
+ - Expo: screens/data in Small, feature modules in Medium, and boundaries ready for offline sync, background work, and platform adapters in Large.
70
+ - Spring Boot: conventional package-by-feature in Small, explicit API/service/repository ownership in Medium, and a verified Spring Modulith modular monolith in Large.
71
+
72
+ Authentication is selected during generation. Supabase emits Supabase Auth clients and login examples; website-only Spring uses a secure server-managed session; multi-client Spring emits an OIDC Resource Server and delegates issuance, refresh, and revocation to the identity provider. `Not yet` is fail-closed for Spring and never emits pretend authentication. Existing projects are never told to rerun the generator to add auth.
73
+
74
+ This module intentionally generates a small working example. Domain-specific features are added after product context is known; the generator does not invent business entities.
75
+
76
+ ### Repository and operational files
77
+
78
+ `lib/generator.js` coordinates writes and refuses to merge into a non-empty destination. It adds documentation, selected playbooks, CI, Docker, Makefile, environment examples, and repository conventions around the runnable foundation.
79
+
80
+ The first `npm install` creates the lockfile. Generated CI uses `npm ci`, so the lockfile must be committed before CI is enabled. `create-win-project.profile.json` separately records the compatibility profile, architecture profile, and authentication intent/model/audience; after generation, that project owns its own upgrade lifecycle.
81
+
82
+ ### Compatibility profile lifecycle
83
+
84
+ Exactly one profile is `current` and one is `previous`. The current profile is the default. A profile owns exact npm, Spring Boot, runtime, and container versions; manifests and scaffold code may only request names or capabilities. Promotion copies the candidate into a new dated profile, marks the former current profile previous, and happens only after the generated-project matrix passes. Major changes also require migration notes. See `DEPENDENCY_MAINTENANCE.md`.
85
+
86
+ ## Documentation model
87
+
88
+ | File | Responsibility |
89
+ |------|----------------|
90
+ | `AGENTS.md` | Small always-on command, workflow, safety, and definition-of-done contract. |
91
+ | `CONTEXT.md` | Product goals, boundaries, decisions, and project-specific facts. |
92
+ | `RULES.md` | Generated concern-to-playbook section router. |
93
+ | `playbooks/` | Reusable standards, recipes, rationale, and stack guidance. |
94
+ | `docs/` | Documentation for the generated product, not generic framework teaching. |
95
+
96
+ Manifest section names are checked against Markdown headings. Numbered headings are normalized for matching, and generated contract tests reject unresolved `RULES.md` entries.
97
+
98
+ ## Testing strategy
99
+
100
+ The generator itself has three verification levels:
101
+
102
+ 1. unit tests for catalog composition and template rendering;
103
+ 2. generated-output contract tests for required files, environment naming, playbook routing, profile-specific boundaries, auth metadata/code, testing profiles, and overwrite safety;
104
+ 3. a current-and-previous matrix across every supported stack combination, all three architecture profiles, and every applicable authentication model. It installs and runs lint, typecheck, tests, builds, Expo compatibility checks/web export, Spring MVC/security/Modulith tests and Maven packaging, Compose validation, and current-profile container builds.
105
+
106
+ Canonical Markdown code examples should progressively move into extracted fixtures so examples compile against the versions they teach.
107
+
108
+ ## Extension workflow
109
+
110
+ When adding a stack or capability:
111
+
112
+ 1. Add its manifest, all five stack facets, and any platform/capability routes.
113
+ 2. Add the smallest runnable files needed in `lib/scaffold.js` or a focused scaffold module.
114
+ 3. Add every supported architecture/authentication combination to the generated-output matrix.
115
+ 4. Run install, lint/typecheck, tests, and production build for the new fixture.
116
+ 5. Update this architecture document if ownership or the generation pipeline changed.
117
+
118
+ Do not advertise a capability solely because a playbook mentions it. A generated capability must have executable configuration and contract coverage.
@@ -0,0 +1,36 @@
1
+ # Content Model
2
+
3
+ The project separates always-loaded instructions, task-routed standards, generated product documentation, and executable examples. Each fact should have one owner.
4
+
5
+ | Content | Owner | Include when |
6
+ |---|---|---|
7
+ | Agent commands, workflow, authority limits, definition of done | `templates/agents/*.md` → generated `AGENTS.md` | Every project; keep short |
8
+ | Product goal, scope, decisions, unknowns | generated `CONTEXT.md` | Every project; project-specific |
9
+ | Concern routing | manifests → generated `RULES.md` | Selected stack/capability only |
10
+ | Reusable engineering policy and rationale | `playbooks/**/*.md` | A task touches that concern |
11
+ | Setup, API, architecture, deployment for this product | generated `docs/` | Every project, then maintained with code |
12
+ | Framework configuration and canonical patterns | `lib/scaffold.js` output plus tests | The capability is selected |
13
+
14
+ ## Authoring rules
15
+
16
+ 1. Put a rule in the narrowest applicable playbook. Do not repeat it in `AGENTS.md`, `README.md`, and stack guides.
17
+ 2. Write normative language only for behavior the generator configures or tests. Label uninstalled libraries and alternative architectures as optional.
18
+ 3. Route concerns to exact Markdown headings from their manifest. Heading changes and manifest changes belong in the same commit.
19
+ 4. Prefer a compact rule, a reason, and one canonical example. Remove tutorial-length alternatives that compete with the default.
20
+ 5. Keep secrets, authentication, authorization, validation, accessibility, and failure behavior at explicit trust boundaries.
21
+ 6. Product docs describe the generated application. Playbooks teach reusable practices; they must not invent product endpoints or entities.
22
+ 7. Code and executable tests win when prose conflicts. Fix the prose in the same change.
23
+ 8. Stack guides always use the five-facet contract: `architecture.md`, `structure.md`, `runtime.md`, `security.md`, and `testing.md`.
24
+ 9. Universal guidance cannot claim browser, mobile, Spring, or provider-specific behavior. Route it through `platform/`, `stack/`, or `capabilities/`.
25
+
26
+ ## Review checklist
27
+
28
+ - Does this content have exactly one authoritative home?
29
+ - Is it selected only for stacks where it applies?
30
+ - Does every `RULES.md` section resolve?
31
+ - Are packages/configuration used by examples actually generated, or clearly marked optional?
32
+ - Can a new developer run the documented commands from the stated directory?
33
+ - Do auth examples cover storage, refresh/expiry, CSRF/CORS, authorization, revocation, and failure paths appropriate to that session model?
34
+ - Would moving a long example into a compile-tested fixture reduce drift?
35
+
36
+ When adding a stack, implement all three profiles, all five facets, a runnable vertical slice for each profile, relevant auth mappings, and matrix entries before advertising support.
@@ -0,0 +1,31 @@
1
+ # Dependency maintenance
2
+
3
+ `compatibility/profiles.json` is the only owner of versions emitted into generated applications. Stack and concern manifests declare package names and capabilities, never ranges or versions. Direct npm dependencies are generated as exact versions, and consumers commit the lockfile created by their first install.
4
+
5
+ ## Profile policy
6
+
7
+ - Keep exactly one `current` and one `previous` profile.
8
+ - Use a dated, immutable profile ID (`YYYY.MM`). Do not silently rewrite a released profile used by generated projects.
9
+ - Build a new candidate from the current profile. Promote it only when the complete generated-project workflow passes.
10
+ - Keep a support date and successor link on the previous profile.
11
+ - Require explicit review and migration notes for every major runtime, framework, or package upgrade.
12
+ - Treat Expo, React Native, its React override, and related Expo packages as one atomic set. `CI=1 npx expo install --check` is a release gate.
13
+
14
+ The two initial profiles intentionally bootstrap from the same known-good set. Future promotions preserve the old current profile unchanged as the meaningful previous fallback.
15
+
16
+ ## Automated proposals
17
+
18
+ Renovate scans only the current profile and opens weekly grouped proposals for web frameworks, Expo, Prisma, testing, TypeScript/lint, Spring Boot, and runtime/container versions. Security alerts are allowed immediately. A Renovate PR is a proposal, not proof of compatibility: CI must pass before merge.
19
+
20
+ When a runtime and its container image must move together, update both in the same PR. Catalog validation rejects mismatched Node, Java, Maven, or PostgreSQL ownership and rejects ranges, missing versions, duplicate dependency ownership, and unknown package requests.
21
+
22
+ ## Promotion checklist
23
+
24
+ 1. Create the new dated candidate profile; leave the retained previous profile unchanged.
25
+ 2. Review upstream release and migration notes, especially for majors.
26
+ 3. Run `npm test` and the compatibility workflow for every stack, architecture profile, and applicable authentication model.
27
+ 4. Confirm Expo install and auth-lifecycle checks, browser production builds and Playwright, Spring MVC/security/Modulith tests and Maven packages, Supabase RLS tests, Compose parsing, npm package contents, and container builds.
28
+ 5. Mark the candidate `current`, the former current `previous`, and remove profiles older than the supported previous profile.
29
+ 6. Summarize breaking changes and required migrations in the PR.
30
+
31
+ Generated projects do not automatically follow catalog updates. Their profile record explains their starting point; they manage later dependency upgrades independently.
package/index.js ADDED
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ import inquirer from 'inquirer'
3
+ import chalk from 'chalk'
4
+ import ora from 'ora'
5
+ import path from 'path'
6
+ import { fileURLToPath } from 'url'
7
+ import { spawnSync } from 'node:child_process'
8
+ import { generateProject } from './lib/generator.js'
9
+ import { loadCompatibility } from './lib/compatibility.js'
10
+ import { printDoctor } from './lib/doctor.js'
11
+ import { configurationDecisionChoices, promptWithBack } from './lib/interview.js'
12
+ import { projectLocationNotice } from './lib/project-location.js'
13
+ import { w1nBanner } from './lib/banner.js'
14
+ import {
15
+ loadCatalog, resolveStack,
16
+ stylingChoicesFor, architectureChoicesFor,
17
+ } from './lib/catalog.js'
18
+ import {
19
+ APPLICATION_SHAPES,
20
+ applicationShapeChoices,
21
+ backendChoicesForShape,
22
+ frontendChoicesForShape,
23
+ } from './lib/application-shapes.js'
24
+ import { laravelUiPromptContribution, laravelUis } from './lib/stacks/laravel/ui/index.js'
25
+
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
27
+ const cliArgs = process.argv.slice(2)
28
+ const profileArg = cliArgs.find((arg) => arg.startsWith('--profile='))?.split('=')[1]
29
+ const shapeArg = cliArgs.find((arg) => arg.startsWith('--shape='))?.split('=')[1]
30
+ const frontendValue = cliArgs.find((arg) => arg.startsWith('--frontend='))?.split('=')[1]
31
+ const backendArg = cliArgs.find((arg) => arg.startsWith('--backend='))?.split('=')[1]
32
+ const frontendAliases = { vite: 'react', expo: 'react-native', none: 'no-frontend' }
33
+ const frontendArg = frontendAliases[frontendValue] || frontendValue
34
+ const architectureArg = cliArgs.find((arg) => arg.startsWith('--architecture='))?.split('=')[1]
35
+ const authenticationArg = cliArgs.find((arg) => arg.startsWith('--authentication='))?.split('=')[1]
36
+ const authAudienceArg = cliArgs.find((arg) => arg.startsWith('--auth-audience='))?.split('=')[1]
37
+ const laravelUiArg = cliArgs.find((arg) => arg.startsWith('--laravel-ui='))?.split('=')[1]
38
+ if (shapeArg && !APPLICATION_SHAPES[shapeArg]) {
39
+ throw new Error('--shape must be fullstack, separate, api, mobile, or frontend')
40
+ }
41
+ if (architectureArg && !['small', 'medium', 'large'].includes(architectureArg)) {
42
+ throw new Error('--architecture must be small, medium, or large')
43
+ }
44
+ if (authenticationArg && !['yes', 'not-yet', 'none'].includes(authenticationArg)) {
45
+ throw new Error('--authentication must be yes, not-yet, or none')
46
+ }
47
+ if (authAudienceArg && !['website', 'multi-client'].includes(authAudienceArg)) {
48
+ throw new Error('--auth-audience must be website or multi-client')
49
+ }
50
+ if (laravelUiArg && !laravelUis.some((ui) => ui.id === laravelUiArg)) {
51
+ throw new Error('--laravel-ui must be blade, livewire, or inertia-react')
52
+ }
53
+ const wantsInstall = cliArgs.includes('--install')
54
+ const skipsInstall = cliArgs.includes('--no-install')
55
+ if (wantsInstall && skipsInstall) throw new Error('Use either --install or --no-install, not both')
56
+ const { profile } = await loadCompatibility(
57
+ path.join(__dirname, 'compatibility/profiles.json'),
58
+ profileArg,
59
+ )
60
+ if (cliArgs[0] === 'doctor' || cliArgs.includes('--doctor')) {
61
+ printDoctor(profile)
62
+ process.exit(0)
63
+ }
64
+ const catalog = await loadCatalog(path.join(__dirname, 'playbooks'), profile)
65
+
66
+ // ─── Banner ──────────────────────────────────────────────────────────────────
67
+
68
+ console.log('')
69
+ console.log(w1nBanner())
70
+ console.log('')
71
+ console.log(chalk.gray(' Production-ready project scaffolding'))
72
+ console.log(chalk.gray(` Compatibility profile: ${profile.id} (${profile.status})`))
73
+ console.log('')
74
+
75
+ // ─── Interview ───────────────────────────────────────────────────────────────
76
+
77
+ const questions = [
78
+ {
79
+ type: 'list',
80
+ name: 'applicationShape',
81
+ message: 'What kind of application are you building?',
82
+ choices: applicationShapeChoices(),
83
+ default: 'fullstack',
84
+ when: () => !shapeArg,
85
+ },
86
+ // ── Always-on ──────────────────────────────────────────────────────────
87
+ {
88
+ type: 'input',
89
+ name: 'projectName',
90
+ message: 'Project name?',
91
+ default: 'my-project',
92
+ validate: (v) => {
93
+ if (!v.trim()) return 'Project name is required'
94
+ if (!/^[a-z0-9-]+$/.test(v)) return 'Use lowercase letters, numbers, and hyphens only'
95
+ return true
96
+ },
97
+ },
98
+ {
99
+ type: 'input',
100
+ name: 'projectDescription',
101
+ message: 'One-line description?',
102
+ default: 'A new application',
103
+ },
104
+
105
+ // ── Stack selection (catalog-driven) ───────────────────────────────────
106
+ {
107
+ type: 'list',
108
+ name: 'frontend',
109
+ message: 'Which application framework or frontend?',
110
+ choices: (a) => frontendChoicesForShape(shapeArg || a.applicationShape, catalog),
111
+ when: () => !frontendArg,
112
+ },
113
+ {
114
+ type: 'list',
115
+ name: 'backend',
116
+ message: 'Which backend or data service?',
117
+ choices: (a) => backendChoicesForShape(shapeArg || a.applicationShape, a.frontend, catalog),
118
+ when: () => !backendArg,
119
+ },
120
+ ...laravelUiPromptContribution(laravelUiArg).questions,
121
+
122
+ // ── Styling: only shown when frontend has >1 option (catalog-driven) ───
123
+ {
124
+ type: 'list',
125
+ name: 'styling',
126
+ message: 'Styling approach?',
127
+ choices: (a) => stylingChoicesFor(catalog, a.frontend),
128
+ when: (a) => stylingChoicesFor(catalog, a.frontend).length > 1,
129
+ },
130
+
131
+ // ── One architecture profile, interpreted natively by every stack ──────
132
+ {
133
+ type: 'list',
134
+ name: 'architecture',
135
+ message: 'Architecture? (Medium is recommended for most long-term applications)',
136
+ choices: (a) => {
137
+ const supported = architectureChoicesFor(catalog, a.frontend, a.backend)
138
+ return [
139
+ { name: 'Medium (Recommended) — clear feature, service, and data boundaries', value: 'medium' },
140
+ { name: 'Small — fewer layers for prototypes and simple applications', value: 'small' },
141
+ { name: 'Large — enforced boundaries for complex domains and larger teams', value: 'large' },
142
+ ].filter((choice) => supported.includes(choice.value))
143
+ },
144
+ default: 'medium',
145
+ when: () => !architectureArg,
146
+ },
147
+
148
+ // ── Authentication intent, expressed without protocol jargon ───────────
149
+ {
150
+ type: 'list',
151
+ name: 'authentication',
152
+ message: 'Does your application need user login?',
153
+ choices: (a) => {
154
+ const choices = []
155
+ if (a.backend === 'supabase' || a.backend === 'springboot' || a.backend === 'laravel') {
156
+ choices.push({ name: 'Yes — generate authentication appropriate for this stack', value: 'yes' })
157
+ }
158
+ choices.push(
159
+ { name: 'Not yet (Recommended) — add guidance without pretending login exists', value: 'not-yet' },
160
+ { name: 'No — this application is intentionally public and has no user accounts', value: 'none' },
161
+ )
162
+ return choices
163
+ },
164
+ default: 'not-yet',
165
+ when: () => !authenticationArg,
166
+ },
167
+ {
168
+ type: 'list',
169
+ name: 'authAudience',
170
+ message: 'Where will users access the application?',
171
+ choices: [
172
+ { name: 'Website only — use a secure server-managed browser session', value: 'website' },
173
+ { name: 'Website and mobile — use a trusted identity provider for every client', value: 'multi-client' },
174
+ ],
175
+ default: 'website',
176
+ when: (a) => ['springboot', 'laravel'].includes(a.backend) && a.frontend !== 'laravel-ui' && (authenticationArg || a.authentication) === 'yes' && !authAudienceArg,
177
+ },
178
+
179
+ // ── Testing ────────────────────────────────────────────────────────────
180
+ {
181
+ type: 'list',
182
+ name: 'testing',
183
+ message: 'Testing setup?',
184
+ choices: (a) => {
185
+ const fe = catalog.byId[a.frontend]
186
+ if (fe?.platform === 'mobile') {
187
+ return [
188
+ { name: 'Basic (Jest + React Native Testing Library)', value: 'basic' },
189
+ { name: 'None', value: 'none' },
190
+ ]
191
+ }
192
+ return [
193
+ { name: 'Full (Vitest + React Testing Library + Playwright)', value: 'full' },
194
+ { name: 'Basic (Vitest + React Testing Library)', value: 'basic' },
195
+ { name: 'None', value: 'none' },
196
+ ]
197
+ },
198
+ default: (a) => {
199
+ const fe = catalog.byId[a.frontend]
200
+ return fe?.platform === 'mobile' ? 'basic' : 'full'
201
+ },
202
+ },
203
+
204
+ // ── DevOps extras ──────────────────────────────────────────────────────
205
+ {
206
+ type: 'confirm',
207
+ name: 'docker',
208
+ message: 'Include Docker?',
209
+ default: true,
210
+ when: (a) => {
211
+ const fe = catalog.byId[a.frontend]
212
+ const be = catalog.byId[a.backend]
213
+ return (be?.needsDocker ?? fe?.needsDocker ?? false)
214
+ },
215
+ },
216
+ {
217
+ type: 'confirm',
218
+ name: 'makefile',
219
+ message: 'Include Makefile?',
220
+ default: true,
221
+ when: (a) => {
222
+ // Makefile is useful for web stacks; less relevant for bare mobile
223
+ const fe = catalog.byId[a.frontend]
224
+ return fe?.platform !== 'mobile'
225
+ },
226
+ },
227
+ {
228
+ type: 'confirm',
229
+ name: 'githubActions',
230
+ message: 'Include GitHub Actions CI?',
231
+ default: true,
232
+ },
233
+ {
234
+ type: 'confirm',
235
+ name: 'installDependencies',
236
+ message: 'Install project dependencies and create the lockfile now?',
237
+ default: true,
238
+ when: () => !wantsInstall && !skipsInstall,
239
+ },
240
+
241
+ // ── Spring Boot specific ───────────────────────────────────────────────
242
+ {
243
+ type: 'input',
244
+ name: 'packageName',
245
+ message: 'Java package name? (e.g. com.yourname)',
246
+ default: 'com.app',
247
+ when: (a) => a.backend === 'springboot',
248
+ validate: (v) => {
249
+ if (!v.trim()) return 'Package name is required'
250
+ if (!/^[a-z]+(\.[a-z]+)+$/.test(v)) return 'Use format: com.yourname'
251
+ return true
252
+ },
253
+ },
254
+
255
+ // ── Optional concerns (filtered to current stack) ──────────────────────
256
+ {
257
+ type: 'checkbox',
258
+ name: 'expectedConcerns',
259
+ message: 'Expected optional concerns? (advisory only — all stay available)',
260
+ choices: (a) => {
261
+ const stack = resolveStack({ ...a, styling: a.styling || catalog.byId[a.frontend]?.stylingOptions?.[0] }, catalog)
262
+ const opts = new Set()
263
+ for (const c of stack.concerns) if (!c.required) opts.add(c.id)
264
+ return [...opts].map((id) => ({ name: id, value: id }))
265
+ },
266
+ },
267
+ ]
268
+ let answers = {
269
+ applicationShape: shapeArg,
270
+ frontend: frontendArg,
271
+ backend: backendArg,
272
+ }
273
+ let stack
274
+ while (true) {
275
+ answers = await promptWithBack(inquirer, questions, answers)
276
+ answers.compatibilityProfile = profile.id
277
+ answers.applicationShape = shapeArg || answers.applicationShape
278
+ answers.architecture = architectureArg || answers.architecture || 'medium'
279
+ answers.authentication = authenticationArg || answers.authentication || 'not-yet'
280
+ answers.authAudience = authAudienceArg || answers.authAudience || (catalog.byId[answers.frontend]?.platform === 'mobile' ? 'multi-client' : 'website')
281
+ answers.laravelUi = laravelUiArg || answers.laravelUi || (answers.frontend === 'laravel-ui' ? 'blade' : undefined)
282
+ if (wantsInstall) answers.installDependencies = true
283
+ if (skipsInstall) answers.installDependencies = false
284
+
285
+ // ── Auto-resolve docker for stacks that need it ───────────────────────────────
286
+ if (resolveStack({ ...answers, styling: answers.styling || catalog.byId[answers.frontend]?.stylingOptions?.[0] || 'tailwind' }, catalog).needsDocker) {
287
+ answers.docker = answers.docker ?? true
288
+ }
289
+
290
+ stack = resolveStack({ ...answers, styling: answers.styling || catalog.byId[answers.frontend]?.stylingOptions?.[0] || 'tailwind' }, catalog)
291
+
292
+ // ─── Confirm ─────────────────────────────────────────────────────────────────
293
+
294
+ console.log('')
295
+ console.log(chalk.bold(' Summary'))
296
+ console.log(chalk.gray(' ───────────────────────────'))
297
+ console.log(` ${chalk.cyan('Name:')} ${answers.projectName}`)
298
+ console.log(` ${chalk.cyan('Stack:')} ${stack.label}`)
299
+ console.log(` ${chalk.cyan('Shape:')} ${APPLICATION_SHAPES[stack.applicationShape].label}`)
300
+ console.log(` ${chalk.cyan('Platform:')} ${stack.platform}`)
301
+ if (stack.styleId) {
302
+ console.log(` ${chalk.cyan('Styling:')} ${catalog.byId[stack.styleId]?.label || stack.styleId}`)
303
+ }
304
+ if (answers.laravelUi) console.log(` ${chalk.cyan('Laravel UI:')} ${answers.laravelUi}`)
305
+ console.log(` ${chalk.cyan('Architecture:')} ${stack.architecture[0].toUpperCase()}${stack.architecture.slice(1)}`)
306
+ console.log(` ${chalk.cyan('Authentication:')} ${stack.authentication}`)
307
+ console.log(` ${chalk.cyan('Testing:')} ${answers.testing}`)
308
+ if (stack.platform !== 'mobile') {
309
+ console.log(` ${chalk.cyan('Docker:')} ${answers.docker ? 'yes' : 'no'}`)
310
+ console.log(` ${chalk.cyan('Makefile:')} ${answers.makefile ? 'yes' : 'no'}`)
311
+ }
312
+ console.log(` ${chalk.cyan('CI/CD:')} ${answers.githubActions ? 'yes' : 'no'}`)
313
+ console.log(` ${chalk.cyan('Install deps:')} ${answers.installDependencies ? 'yes' : 'no'}`)
314
+ if (answers.packageName) {
315
+ console.log(` ${chalk.cyan('Package:')} ${answers.packageName}`)
316
+ }
317
+ if (stack.constraints.length) {
318
+ console.log('')
319
+ console.log(chalk.gray(' Key constraints:'))
320
+ for (const rule of stack.constraints.slice(0, 3)) {
321
+ console.log(chalk.gray(` • ${rule}`))
322
+ }
323
+ }
324
+ console.log('')
325
+
326
+ const { decision } = await inquirer.prompt([{
327
+ type: 'list', name: 'decision', message: 'Ready?', choices: configurationDecisionChoices(),
328
+ }])
329
+
330
+ if (decision === 'back') {
331
+ continue
332
+ }
333
+ if (decision === 'cancel') {
334
+ console.log(chalk.yellow('\n Cancelled.\n'))
335
+ process.exit(0)
336
+ }
337
+ break
338
+ }
339
+
340
+ // ─── Generate ────────────────────────────────────────────────────────────────
341
+
342
+ console.log('')
343
+ const spinner = ora('Scaffolding project...').start()
344
+
345
+ try {
346
+ await generateProject(answers, __dirname)
347
+ spinner.succeed(chalk.green('Project created!'))
348
+ const locationNotice = projectLocationNotice({ cwd: process.cwd(), cliRoot: __dirname, projectName: answers.projectName })
349
+ if (locationNotice) {
350
+ console.log('')
351
+ console.log(chalk.yellow.bold(' Project location note'))
352
+ console.log(chalk.yellow(` ${locationNotice.message}`))
353
+ console.log(chalk.gray(` Created at: ${locationNotice.generatedPath}`))
354
+ console.log(chalk.gray(` Suggested destination: ${locationNotice.suggestedPath}`))
355
+ console.log(chalk.gray(` Linux/macOS: mv "${locationNotice.generatedPath}" "${locationNotice.suggestedPath}"`))
356
+ console.log(chalk.gray(` Windows: cut the generated folder in File Explorer and paste it into your projects folder.`))
357
+ }
358
+ if (answers.installDependencies) {
359
+ const projectRoot = path.join(process.cwd(), answers.projectName)
360
+ const steps = []
361
+ if (stack.backendKey === 'laravel') {
362
+ const laravelRoot = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? projectRoot : path.join(projectRoot, 'backend')
363
+ steps.push({ command: process.platform === 'win32' ? 'composer.bat' : 'composer', args: ['install'], cwd: laravelRoot, retry: `cd ${path.relative(process.cwd(), laravelRoot)} && composer install` })
364
+ }
365
+ const needsNpm = stack.frontendKey !== 'no-frontend' && (stack.frontendKey !== 'laravel-ui' || answers.laravelUi === 'inertia-react')
366
+ if (needsNpm) {
367
+ const npmRoot = stack.frontendKey === 'react' ? path.join(projectRoot, 'frontend') : projectRoot
368
+ steps.push({ command: process.platform === 'win32' ? 'npm.cmd' : 'npm', args: ['install'], cwd: npmRoot, retry: `cd ${path.relative(process.cwd(), npmRoot)} && npm install` })
369
+ }
370
+ const installSpinner = ora('Installing exact dependencies and creating lockfiles...').start()
371
+ const failed = steps.find((step) => spawnSync(step.command, step.args, { cwd: step.cwd, stdio: 'inherit', shell: false }).status !== 0)
372
+ if (failed) {
373
+ installSpinner.warn(chalk.yellow('Project created, but dependency installation did not finish.'))
374
+ console.log(chalk.yellow(` Retry with: ${failed.retry}`))
375
+ } else {
376
+ installSpinner.succeed(chalk.green('Dependencies installed and lockfiles created.'))
377
+ }
378
+ }
379
+ console.log('')
380
+ console.log(chalk.bold(` Next steps:`))
381
+ console.log(chalk.gray(` cd ${answers.projectName}`))
382
+
383
+ if (stack.backendKey === 'laravel') {
384
+ const laravelDir = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '' : 'backend/'
385
+ console.log(chalk.gray(` cp ${laravelDir}.env.example ${laravelDir}.env`))
386
+ if (answers.makefile) {
387
+ console.log(chalk.gray(` make setup # first run only`))
388
+ console.log(chalk.gray(` make run # later runs; never rebuilds`))
389
+ } else {
390
+ console.log(chalk.gray(` docker compose build`))
391
+ console.log(chalk.gray(` docker compose up -d`))
392
+ }
393
+ } else if (stack.frontendKey === 'react') {
394
+ if (stack.backendKey === 'springboot') {
395
+ console.log(chalk.gray(` cp .env.example .env # Docker/backend values`))
396
+ }
397
+ console.log(chalk.gray(` cd frontend`))
398
+ console.log(chalk.gray(` cp .env.example .env`))
399
+ if (!answers.installDependencies) console.log(chalk.gray(` npm install`))
400
+ } else {
401
+ console.log(chalk.gray(` cp .env.example ${stack.isMobile ? '.env' : '.env.local'}`))
402
+ if (!answers.installDependencies) console.log(chalk.gray(` npm install`))
403
+ }
404
+
405
+ if (stack.backendKey === 'laravel') {
406
+ // Laravel commands were printed above because its root differs by application shape.
407
+ } else if (stack.isMobile) {
408
+ console.log(chalk.gray(` npx expo start`))
409
+ } else if (answers.makefile) {
410
+ if (stack.frontendKey === 'react') {
411
+ console.log(chalk.gray(` cd ..`))
412
+ }
413
+ console.log(chalk.gray(` make dev`))
414
+ } else if (stack.backendKey === 'supabase') {
415
+ console.log(chalk.gray(` npm run supabase:start`))
416
+ console.log(chalk.gray(` npm run dev`))
417
+ } else if (stack.frontendKey === 'react') {
418
+ console.log(chalk.gray(` npm run dev`))
419
+ } else {
420
+ console.log(chalk.gray(` docker compose up -d db`))
421
+ console.log(chalk.gray(` npm run dev`))
422
+ }
423
+
424
+ console.log('')
425
+ console.log(chalk.cyan(` Read RULES.md before starting — it maps every playbook for this stack.`))
426
+ console.log('')
427
+ } catch (err) {
428
+ spinner.fail(chalk.red('Failed to generate project'))
429
+ console.error(err)
430
+ process.exit(1)
431
+ }