create-win-project 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +54 -147
  3. package/checks/check-compatibility.js +37 -8
  4. package/checks/check-generated-project.js +27 -3
  5. package/checks/check-library.js +13 -3
  6. package/checks/check-package.js +15 -0
  7. package/checks/classify-changes.js +5 -15
  8. package/checks/run-compatibility-shard.js +21 -0
  9. package/docs/README.md +20 -0
  10. package/docs/capabilities.md +13 -0
  11. package/docs/compatibility.md +13 -0
  12. package/docs/generated-project.md +12 -0
  13. package/docs/getting-started.md +26 -0
  14. package/docs/{ARCHITECTURE.md → maintainers/architecture.md} +3 -3
  15. package/docs/maintainers/ci-strategy.md +46 -0
  16. package/docs/{CONTRIBUTING.md → maintainers/contributing.md} +7 -7
  17. package/docs/migration-v2.md +13 -0
  18. package/docs/production-contract.md +24 -0
  19. package/library/INDEX.md +1 -1
  20. package/library/compatibility-impact.json +14 -0
  21. package/library/development-tools/devops/makefile/commands.md +11 -0
  22. package/library/development-tools/devops/makefile/definition.json +2 -2
  23. package/library/development-tools/devops/makefile/docker.md +9 -0
  24. package/library/development-tools/devops/makefile/validation.md +5 -0
  25. package/library/features/sqlalchemy-alembic.md +17 -0
  26. package/library/optional-features/concerns/zod/errors.md +5 -0
  27. package/library/optional-features/concerns/zod/testing.md +5 -0
  28. package/library/optional-features/concerns/zod/validation.md +21 -0
  29. package/library/optional-features/styling/css-modules/definition.json +3 -2
  30. package/library/optional-features/styling/css-modules/responsive.md +5 -0
  31. package/library/optional-features/styling/css-modules/theme.md +7 -0
  32. package/library/stacks/expo/definition.json +2 -2
  33. package/library/stacks/fastapi/architecture.md +40 -0
  34. package/library/stacks/fastapi/definition.json +39 -0
  35. package/library/stacks/fastapi/runtime.md +25 -0
  36. package/library/stacks/fastapi/security.md +26 -0
  37. package/library/stacks/fastapi/structure.md +27 -0
  38. package/library/stacks/fastapi/testing.md +23 -0
  39. package/library/stacks/nextjs/definition.json +2 -2
  40. package/library/stacks/no-frontend/definition.json +1 -1
  41. package/library/stacks/react-vite/definition.json +2 -2
  42. package/library/tested-versions.json +42 -2
  43. package/library/universal/coding-rules/definition.json +3 -3
  44. package/library/universal/coding-rules/hygiene.md +9 -0
  45. package/library/universal/coding-rules/naming.md +17 -0
  46. package/library/universal/git-conventions/branches.md +5 -0
  47. package/library/universal/git-conventions/commits.md +7 -0
  48. package/library/universal/git-conventions/definition.json +4 -2
  49. package/library/universal/git-conventions/workflow.md +5 -0
  50. package/library/universal/typescript/boundaries.md +13 -0
  51. package/library/universal/typescript/definition.json +3 -3
  52. package/library/universal/typescript/errors.md +5 -0
  53. package/library/universal/typescript/patterns.md +7 -0
  54. package/package.json +4 -6
  55. package/src/cli/arguments.js +11 -0
  56. package/src/cli/main.js +18 -0
  57. package/src/cli/questions.js +16 -15
  58. package/src/cli/system-check.js +22 -2
  59. package/src/engine/load-library.js +3 -2
  60. package/src/engine/project-files.js +10 -1
  61. package/src/engine/project-guidance.js +2 -1
  62. package/src/engine/project-shapes.js +4 -4
  63. package/src/engine/render-templates.js +3 -0
  64. package/src/engine/tested-versions.js +21 -2
  65. package/src/engine/upgrade-report.js +20 -0
  66. package/src/stacks/available-stacks.js +2 -0
  67. package/src/stacks/backends/fastapi/ci.js +3 -0
  68. package/src/stacks/backends/fastapi/create-files.js +874 -0
  69. package/src/stacks/backends/fastapi/docker.js +75 -0
  70. package/src/stacks/backends/fastapi/environment.js +3 -0
  71. package/src/stacks/backends/fastapi/index.js +32 -0
  72. package/src/stacks/compose-files.js +19 -1
  73. package/src/stacks/create-project.js +128 -7
  74. package/src/stacks/frontends/nextjs/index.js +1 -1
  75. package/src/stacks/frontends/react-native/create-files.js +4 -1
  76. package/src/stacks/frontends/react-native/environment.js +1 -1
  77. package/src/stacks/frontends/react-native/index.js +1 -1
  78. package/src/stacks/frontends/react-vite/environment.js +1 -1
  79. package/src/stacks/frontends/react-vite/index.js +1 -1
  80. package/src/stacks/shared/capability-packs.js +31 -0
  81. package/src/stacks/shared/environment.js +12 -4
  82. package/src/stacks/shared/javascript-package.js +6 -0
  83. package/templates/ci/fastapi.yml +62 -0
  84. package/templates/docker/compose-prod/fastapi.yml +52 -0
  85. package/templates/docker/compose-prod/springboot.yml +19 -1
  86. package/templates/docker/dockerfile/fastapi.dev.dockerfile +9 -0
  87. package/templates/docker/dockerfile/fastapi.prod.dockerfile +11 -0
  88. package/templates/docker/dockerfile/nextjs.prod.dockerfile +1 -0
  89. package/templates/docker/dockerfile/springboot.prod.dockerfile +4 -1
  90. package/templates/docker/dockerfile/vite.prod.dockerfile +2 -1
  91. package/templates/makefile/fastapi.mk +97 -0
  92. package/library/development-tools/devops/makefile/makefile.md +0 -556
  93. package/library/optional-features/concerns/zod.md +0 -174
  94. package/library/optional-features/styling/css-modules/css-modules-extensions.md +0 -267
  95. package/library/universal/coding-rules/coding-rules.md +0 -281
  96. package/library/universal/git-conventions/git-conventions.md +0 -186
  97. package/library/universal/typescript/typescript.md +0 -272
  98. /package/docs/{CONTENT_MODEL.md → maintainers/content-model.md} +0 -0
  99. /package/docs/{DEPENDENCY_MAINTENANCE.md → maintainers/dependencies.md} +0 -0
@@ -15,7 +15,9 @@
15
15
  "maven": "3.9.16",
16
16
  "postgres": "16",
17
17
  "php": "8.5.10",
18
- "composer": "2.10.3"
18
+ "composer": "2.10.3",
19
+ "python": "3.14.7",
20
+ "uv": "0.12.10"
19
21
  },
20
22
  "images": {
21
23
  "node": {
@@ -45,8 +47,26 @@
45
47
  "composer": {
46
48
  "repository": "composer",
47
49
  "tag": "2.10.3"
50
+ },
51
+ "python": {
52
+ "repository": "python",
53
+ "tag": "3.14.7-slim"
48
54
  }
49
55
  },
56
+ "pythonPackages": {
57
+ "alembic": "1.19.2",
58
+ "asyncpg": "0.31.0",
59
+ "fastapi": "0.141.1",
60
+ "httpx": "0.28.1",
61
+ "mypy": "2.3.1",
62
+ "pydantic-settings": "2.15.0",
63
+ "pyjwt": "2.13.0",
64
+ "pytest": "9.1.1",
65
+ "pytest-asyncio": "1.4.0",
66
+ "ruff": "0.16.6",
67
+ "sqlalchemy": "2.0.52",
68
+ "uvicorn": "0.52.4"
69
+ },
50
70
  "composerPackages": {
51
71
  "auth0/login": "7.22.0",
52
72
  "fakerphp/faker": "1.24.1",
@@ -133,7 +153,9 @@
133
153
  "maven": "3.9.16",
134
154
  "postgres": "16",
135
155
  "php": "8.5.10",
136
- "composer": "2.10.3"
156
+ "composer": "2.10.3",
157
+ "python": "3.13.15",
158
+ "uv": "0.12.10"
137
159
  },
138
160
  "images": {
139
161
  "node": {
@@ -163,8 +185,26 @@
163
185
  "composer": {
164
186
  "repository": "composer",
165
187
  "tag": "2.10.3"
188
+ },
189
+ "python": {
190
+ "repository": "python",
191
+ "tag": "3.13.15-slim"
166
192
  }
167
193
  },
194
+ "pythonPackages": {
195
+ "alembic": "1.19.2",
196
+ "asyncpg": "0.31.0",
197
+ "fastapi": "0.141.1",
198
+ "httpx": "0.28.1",
199
+ "mypy": "2.3.1",
200
+ "pydantic-settings": "2.15.0",
201
+ "pyjwt": "2.13.0",
202
+ "pytest": "9.1.1",
203
+ "pytest-asyncio": "1.4.0",
204
+ "ruff": "0.16.6",
205
+ "sqlalchemy": "2.0.52",
206
+ "uvicorn": "0.52.4"
207
+ },
168
208
  "composerPackages": {
169
209
  "auth0/login": "7.22.0",
170
210
  "fakerphp/faker": "1.24.1",
@@ -2,12 +2,12 @@
2
2
  "id": "coding-rules",
3
3
  "kind": "universal",
4
4
  "label": "Coding Rules",
5
- "file": "universal/coding-rules.md",
5
+ "playbooks": ["universal/coding-rules/naming.md", "universal/coding-rules/hygiene.md"],
6
6
  "required": true,
7
7
  "folders": [],
8
8
  "devDeps": ["prettier"],
9
9
  "concerns": [
10
- { "id": "naming", "required": true, "sections": ["Naming", "Functions", "Imports", "Constants"] },
11
- { "id": "no-debug", "required": true, "sections": ["No Debug Code in Commits", "One Thing Per File"] }
10
+ { "id": "naming", "required": true, "playbook": "universal/coding-rules/naming.md", "sections": ["Naming", "Functions", "Imports", "Constants"] },
11
+ { "id": "no-debug", "required": true, "playbook": "universal/coding-rules/hygiene.md", "sections": ["No Debug Code in Commits", "One Thing Per File"] }
12
12
  ]
13
13
  }
@@ -0,0 +1,9 @@
1
+ # Code hygiene
2
+
3
+ ## No Debug Code in Commits
4
+
5
+ Remove temporary logging, breakpoints, fixtures, bypasses, and commented-out implementations. Retain structured operational logging only when it has defined level, fields, privacy handling, and ownership.
6
+
7
+ ## One Thing Per File
8
+
9
+ A file should have one clear reason to change. Keep tightly coupled helpers beside their owner, but split unrelated components, routes, services, or policies. Do not create one-line files merely to satisfy this rule.
@@ -0,0 +1,17 @@
1
+ # Naming and structure
2
+
3
+ ## Naming
4
+
5
+ Use names that describe domain intent. Avoid unexplained abbreviations, generic `data`/`utils` modules, and names that repeat their containing folder.
6
+
7
+ ## Functions
8
+
9
+ Keep functions focused, make side effects visible, validate at boundaries, and return early for invalid states.
10
+
11
+ ## Imports
12
+
13
+ Group platform, external, and local imports consistently. Respect architecture boundaries and remove unused imports.
14
+
15
+ ## Constants
16
+
17
+ Name shared constants by meaning, keep feature-local values near their owner, and do not disguise mutable state as a constant.
@@ -0,0 +1,5 @@
1
+ # Git branches
2
+
3
+ ## Branch Structure
4
+
5
+ Branch from the documented integration branch. Use short prefixes such as `feature/`, `fix/`, `docs/`, `refactor/`, or `chore/`, followed by a specific kebab-case purpose. Keep unrelated work on separate branches.
@@ -0,0 +1,7 @@
1
+ # Git commits
2
+
3
+ ## Commit Convention
4
+
5
+ Use `type(scope): imperative summary`. Each commit should represent one coherent, testable change and must not include secrets, generated noise, debug code, or unrelated formatting.
6
+
7
+ Explain motivation and migration impact in the body when the diff alone is insufficient.
@@ -2,10 +2,12 @@
2
2
  "id": "git-conventions",
3
3
  "kind": "universal",
4
4
  "label": "Git Conventions",
5
- "file": "universal/git-conventions.md",
5
+ "playbooks": ["universal/git-conventions/branches.md", "universal/git-conventions/commits.md", "universal/git-conventions/workflow.md"],
6
6
  "required": true,
7
7
  "folders": [],
8
8
  "concerns": [
9
- { "id": "git", "required": true, "sections": ["Branch Structure", "Commit Convention", "Daily Workflow"] }
9
+ { "id": "git-branches", "required": true, "playbook": "universal/git-conventions/branches.md", "sections": ["Branch Structure"] },
10
+ { "id": "git-commits", "required": true, "playbook": "universal/git-conventions/commits.md", "sections": ["Commit Convention"] },
11
+ { "id": "git-workflow", "required": true, "playbook": "universal/git-conventions/workflow.md", "sections": ["Daily Workflow"] }
10
12
  ]
11
13
  }
@@ -0,0 +1,5 @@
1
+ # Git workflow
2
+
3
+ ## Daily Workflow
4
+
5
+ Inspect status and upstream changes before editing. Preserve user work, stage deliberately, review the staged diff, and run risk-appropriate checks before committing. Do not push, merge, rewrite shared history, or create releases without authorization.
@@ -0,0 +1,13 @@
1
+ # TypeScript boundaries
2
+
3
+ ## Strict Mode — Always On
4
+
5
+ Keep `strict` enabled. Model inputs at trust boundaries as `unknown`, validate them, then pass typed values inward. Do not weaken compiler options for one failing file.
6
+
7
+ ## No any
8
+
9
+ Avoid `any`. Prefer `unknown`, generics, discriminated unions, or a narrow local type. If an external library forces an escape hatch, isolate and document it at the adapter boundary.
10
+
11
+ ## Type vs Interface
12
+
13
+ Use interfaces for extendable object contracts and types for unions, intersections, mapped types, and aliases. Consistency within a feature matters more than stylistic conversion.
@@ -2,11 +2,11 @@
2
2
  "id": "typescript",
3
3
  "kind": "universal",
4
4
  "label": "TypeScript",
5
- "file": "universal/typescript.md",
5
+ "playbooks": ["universal/typescript/boundaries.md", "universal/typescript/errors.md", "universal/typescript/patterns.md"],
6
6
  "required": true,
7
7
  "folders": [],
8
8
  "concerns": [
9
- { "id": "typescript-strict", "required": true, "sections": ["Strict Mode — Always On", "No any", "Type vs Interface"] },
10
- { "id": "validation", "required": false, "when": "Project validates external/runtime input", "sections": ["Zod for Runtime Validation"] }
9
+ { "id": "typescript-strict", "required": true, "playbook": "universal/typescript/boundaries.md", "sections": ["Strict Mode — Always On", "No any", "Type vs Interface"] },
10
+ { "id": "validation", "required": false, "when": "Project validates external/runtime input", "playbook": "universal/typescript/patterns.md", "sections": ["Zod for Runtime Validation"] }
11
11
  ]
12
12
  }
@@ -0,0 +1,5 @@
1
+ # TypeScript errors
2
+
3
+ ## Typed Errors
4
+
5
+ Represent expected failures with stable codes or discriminated results. Preserve causes for server logs, return safe messages at external boundaries, and exhaustively handle variants with `never` checks.
@@ -0,0 +1,7 @@
1
+ # TypeScript patterns
2
+
3
+ ## Zod for Runtime Validation
4
+
5
+ Static types do not validate runtime data. Parse environment variables, requests, storage, and third-party responses at entry points. Export the inferred type from the schema instead of maintaining a duplicate interface.
6
+
7
+ Prefer small pure functions, explicit return types on public APIs, `satisfies` for configuration, and immutable inputs where mutation is not part of the contract.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-win-project",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "Project scaffolding CLI by Win",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,10 +18,7 @@
18
18
  "templates",
19
19
  "checks",
20
20
  "public/logo.svg",
21
- "docs/ARCHITECTURE.md",
22
- "docs/CONTENT_MODEL.md",
23
- "docs/DEPENDENCY_MAINTENANCE.md",
24
- "docs/CONTRIBUTING.md",
21
+ "docs",
25
22
  "README.md",
26
23
  "LICENSE"
27
24
  ],
@@ -30,6 +27,7 @@
30
27
  "doctor": "node index.js doctor",
31
28
  "test": "vitest run",
32
29
  "validate:content": "node checks/check-library.js",
30
+ "package:check": "node checks/check-package.js",
33
31
  "matrix:smoke": "node checks/check-compatibility.js --scope=smoke",
34
32
  "matrix:full": "node checks/check-compatibility.js --scope=full",
35
33
  "verify:generated": "node checks/check-generated-project.js"
@@ -45,6 +43,6 @@
45
43
  "ora": "7.0.1"
46
44
  },
47
45
  "devDependencies": {
48
- "vitest": "2.1.9"
46
+ "vitest": "5.0.0"
49
47
  }
50
48
  }
@@ -6,6 +6,7 @@ function option(args, name) {
6
6
  }
7
7
 
8
8
  export function parseArguments(args) {
9
+ const upgradeReportIndex = args.indexOf('upgrade-report')
9
10
  const profile = option(args, 'profile')
10
11
  const shape = option(args, 'shape')
11
12
  const frontendValue = option(args, 'frontend')
@@ -16,6 +17,9 @@ export function parseArguments(args) {
16
17
  const authentication = option(args, 'authentication')
17
18
  const authAudience = option(args, 'auth-audience')
18
19
  const laravelUi = option(args, 'laravel-ui')
20
+ const uploads = option(args, 'uploads')
21
+ const backgroundJobs = option(args, 'background-jobs')
22
+ const offline = option(args, 'offline')
19
23
  const install = args.includes('--install')
20
24
  const noInstall = args.includes('--no-install')
21
25
 
@@ -34,6 +38,9 @@ export function parseArguments(args) {
34
38
  if (laravelUi && !laravelUis.some((ui) => ui.id === laravelUi)) {
35
39
  throw new Error('--laravel-ui must be blade, livewire, or inertia-react')
36
40
  }
41
+ if (uploads && !['none', 'object-storage'].includes(uploads)) throw new Error('--uploads must be none or object-storage')
42
+ if (backgroundJobs && !['none', 'queue'].includes(backgroundJobs)) throw new Error('--background-jobs must be none or queue')
43
+ if (offline && !['none', 'cache', 'sync'].includes(offline)) throw new Error('--offline must be none, cache, or sync')
37
44
  if (install && noInstall) throw new Error('Use either --install or --no-install, not both')
38
45
 
39
46
  return Object.freeze({
@@ -45,8 +52,12 @@ export function parseArguments(args) {
45
52
  authentication,
46
53
  authAudience,
47
54
  laravelUi,
55
+ uploads,
56
+ backgroundJobs,
57
+ offline,
48
58
  install,
49
59
  noInstall,
50
60
  doctor: args[0] === 'doctor' || args.includes('--doctor'),
61
+ upgradeReportPath: upgradeReportIndex >= 0 ? (args[upgradeReportIndex + 1] || '.') : undefined,
51
62
  })
52
63
  }
package/src/cli/main.js CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  startProjectSpinner,
27
27
  } from './display.js'
28
28
  import { parseArguments } from './arguments.js'
29
+ import { createUpgradeReport } from '../engine/upgrade-report.js'
29
30
  import {
30
31
  loadCatalog, resolveStack,
31
32
  } from '../engine/load-library.js'
@@ -50,8 +51,16 @@ const architectureArg = args.architecture
50
51
  const authenticationArg = args.authentication
51
52
  const authAudienceArg = args.authAudience
52
53
  const laravelUiArg = args.laravelUi
54
+ const uploadsArg = args.uploads
55
+ const backgroundJobsArg = args.backgroundJobs
56
+ const offlineArg = args.offline
53
57
  const wantsInstall = args.install
54
58
  const skipsInstall = args.noInstall
59
+ if (args.upgradeReportPath) {
60
+ const report = await createUpgradeReport(args.upgradeReportPath, path.join(projectRoot, 'library/tested-versions.json'))
61
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
62
+ return
63
+ }
55
64
  const { profile } = await loadCompatibility(
56
65
  path.join(projectRoot, 'library/tested-versions.json'),
57
66
  profileArg,
@@ -85,6 +94,11 @@ answers.architecture = architectureArg || answers.architecture || 'medium'
85
94
  answers.authentication = authenticationArg || answers.authentication || 'not-yet'
86
95
  answers.authAudience = authAudienceArg || answers.authAudience || (catalog.byId[answers.frontend]?.platform === 'mobile' ? 'multi-client' : 'website')
87
96
  answers.laravelUi = laravelUiArg || answers.laravelUi || (answers.frontend === 'laravel-ui' ? 'blade' : undefined)
97
+ answers.uploads = uploadsArg || answers.uploads || 'none'
98
+ answers.backgroundJobs = backgroundJobsArg || answers.backgroundJobs || 'none'
99
+ answers.offline = offlineArg || answers.offline || 'none'
100
+ answers.testing = answers.frontend === 'react-native' ? 'basic' : 'full'
101
+ answers.githubActions = true
88
102
  if (wantsInstall) answers.installDependencies = true
89
103
  if (skipsInstall) answers.installDependencies = false
90
104
 
@@ -157,6 +171,10 @@ try {
157
171
  const laravelRoot = ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? generatedProjectRoot : path.join(generatedProjectRoot, 'backend')
158
172
  steps.push({ command: process.platform === 'win32' ? 'composer.bat' : 'composer', args: ['install'], cwd: laravelRoot, retry: `cd ${path.relative(process.cwd(), laravelRoot)} && composer install` })
159
173
  }
174
+ if (stack.backendKey === 'fastapi') {
175
+ const apiRoot = stack.frontendKey === 'no-frontend' ? generatedProjectRoot : path.join(generatedProjectRoot, 'backend')
176
+ steps.push({ command: 'uv', args: ['sync'], cwd: apiRoot, retry: `cd ${path.relative(process.cwd(), apiRoot)} && uv sync` })
177
+ }
160
178
  const needsNpm = stack.frontendKey !== 'no-frontend' && (stack.frontendKey !== 'laravel-ui' || answers.laravelUi === 'inertia-react')
161
179
  if (needsNpm) {
162
180
  const npmRoot = stack.frontendKey === 'react' ? path.join(generatedProjectRoot, 'frontend') : generatedProjectRoot
@@ -106,12 +106,12 @@ export function buildQuestions({ args, catalog }) {
106
106
  },
107
107
  {
108
108
  type: 'list', name: 'authentication',
109
- message: (answers) => ['supabase', 'springboot', 'laravel'].includes(answers.backend)
109
+ message: (answers) => ['supabase', 'springboot', 'laravel', 'fastapi'].includes(answers.backend)
110
110
  ? 'User authentication'
111
111
  : `User authentication ${chalk.yellow(`(generation unavailable for ${answers.backend === 'none' ? 'a frontend-only project' : 'a PostgreSQL-only backend'})`)}`,
112
112
  choices: (answers) => {
113
113
  const choices = []
114
- if (['supabase', 'springboot', 'laravel'].includes(answers.backend)) {
114
+ if (['supabase', 'springboot', 'laravel', 'fastapi'].includes(answers.backend)) {
115
115
  choices.push(choice('Yes', 'yes'))
116
116
  }
117
117
  choices.push(
@@ -130,22 +130,23 @@ export function buildQuestions({ args, catalog }) {
130
130
  { name: 'Website and mobile — use a trusted identity provider for every client', value: 'multi-client' },
131
131
  ],
132
132
  default: 'website',
133
- when: (answers) => ['springboot', 'laravel'].includes(answers.backend) && answers.frontend !== 'laravel-ui' &&
133
+ when: (answers) => ['springboot', 'laravel', 'fastapi'].includes(answers.backend) && answers.frontend !== 'laravel-ui' &&
134
134
  (args.authentication || answers.authentication) === 'yes' && !args.authAudience,
135
135
  },
136
136
  {
137
- type: 'list', name: 'testing', message: 'Testing setup?',
138
- choices: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile'
139
- ? [
140
- { name: 'Basic (Jest + React Native Testing Library)', value: 'basic' },
141
- { name: 'None', value: 'none' },
142
- ]
143
- : [
144
- { name: 'Full (Vitest + React Testing Library + Playwright)', value: 'full' },
145
- { name: 'Basic (Vitest + React Testing Library)', value: 'basic' },
146
- { name: 'None', value: 'none' },
147
- ],
148
- default: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile' ? 'basic' : 'full',
137
+ type: 'list', name: 'uploads', message: 'User-provided file uploads',
138
+ choices: [{ name: 'None', value: 'none' }, { name: 'Private object storage', value: 'object-storage' }],
139
+ default: 'none', when: () => !args.uploads,
140
+ },
141
+ {
142
+ type: 'list', name: 'backgroundJobs', message: 'Background work',
143
+ choices: [{ name: 'None', value: 'none' }, { name: 'Durable queue', value: 'queue' }],
144
+ default: 'none', when: (answers) => catalog.byId[answers.frontend]?.platform !== 'mobile' && !args.backgroundJobs,
145
+ },
146
+ {
147
+ type: 'list', name: 'offline', message: 'Offline behavior',
148
+ choices: [{ name: 'None', value: 'none' }, { name: 'Local cache', value: 'cache' }, { name: 'Synchronization', value: 'sync' }],
149
+ default: 'none', when: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile' && !args.offline,
149
150
  },
150
151
  {
151
152
  type: 'confirm', name: 'docker', message: 'Add optional Docker development files', default: false,
@@ -30,6 +30,8 @@ export function detectSystemVersions({ run = spawnSync, nodeVersion = process.ve
30
30
  npm: probe(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['--version'], run),
31
31
  php: probe('php', ['--version'], run),
32
32
  composer: probe(process.platform === 'win32' ? 'composer.bat' : 'composer', ['--version'], run),
33
+ python: probe('python', ['--version'], run),
34
+ uv: probe('uv', ['--version'], run),
33
35
  }
34
36
  }
35
37
 
@@ -58,6 +60,14 @@ export function installationIssues(stack, profile, versions) {
58
60
  issues.push({ tool: 'Composer', found: versions.composer, required: profile.runtimes.composer })
59
61
  }
60
62
  }
63
+ if (stack.backendKey === 'fastapi') {
64
+ if (!versionAtLeast(versions.python, profile.runtimes.python)) {
65
+ issues.push({ tool: 'Python', found: versions.python, required: profile.runtimes.python })
66
+ }
67
+ if (!versionAtLeast(versions.uv, profile.runtimes.uv)) {
68
+ issues.push({ tool: 'uv', found: versions.uv, required: profile.runtimes.uv })
69
+ }
70
+ }
61
71
  return issues
62
72
  }
63
73
 
@@ -91,6 +101,12 @@ export function runtimeSetupInstructions(profile, issues = []) {
91
101
  'Verify: php --version && composer --version',
92
102
  )
93
103
  }
104
+ if (tools.has('Python') || tools.has('uv')) {
105
+ instructions.push(
106
+ `Install Python ${profile.runtimes.python} and uv ${profile.runtimes.uv} (https://docs.astral.sh/uv/), or use the generated Docker workflow.`,
107
+ 'Verify: python --version && uv --version',
108
+ )
109
+ }
94
110
  return instructions
95
111
  }
96
112
 
@@ -116,19 +132,23 @@ export function collectDiagnostics(profile) {
116
132
  const dockerVersion = probeDoctor('docker')
117
133
  const composeVersion = dockerVersion ? probeDoctor('docker', ['compose', 'version']) : null
118
134
  const javaVersion = probeDoctor('java', ['-version'])
135
+ const pythonVersion = probeDoctor('python')
136
+ const uvVersion = probeDoctor('uv')
119
137
  return [
120
138
  { name: 'Node.js', found: nodeVersion, expected: profile.runtimes.node, required: false },
121
139
  { name: 'npm', found: npmVersion, required: false },
122
140
  { name: 'Docker', found: dockerVersion, required: false },
123
141
  { name: 'Docker Compose', found: composeVersion, required: false },
124
142
  { name: 'Java', found: javaVersion, expected: profile.runtimes.java, required: false },
143
+ { name: 'Python', found: pythonVersion, expected: profile.runtimes.python, required: false },
144
+ { name: 'uv', found: uvVersion, expected: profile.runtimes.uv, required: false },
125
145
  ]
126
146
  }
127
147
 
128
- export function printDoctor(profile, output = console.log) {
148
+ export function printDoctor(profile, output = console.log, diagnostics = collectDiagnostics(profile)) {
129
149
  output(`create-win-project doctor (profile ${profile.id})`)
130
150
  output('Use either the Node/npm lane or the Docker/Compose lane; Java is only needed for host-run Spring projects.')
131
- for (const item of collectDiagnostics(profile)) {
151
+ for (const item of diagnostics) {
132
152
  const expectation = item.expected ? ` (tested: ${item.expected})` : ''
133
153
  output(`${item.found ? '✓' : '○'} ${item.name}: ${item.found || 'not found'}${expectation}`)
134
154
  }
@@ -190,7 +190,8 @@ export function resolveStack(answers, catalog) {
190
190
  if (fe.id === 'laravel-ui') authentication = 'laravel-session'
191
191
  else authentication = authAudience === 'website' && fe.platform === 'web' ? 'sanctum-spa' : 'laravel-oidc'
192
192
  }
193
- else throw new Error(`Authentication generation requires Supabase or Spring Boot for ${fe.label}`)
193
+ else if (be.id === 'fastapi') authentication = 'oidc'
194
+ else throw new Error(`Authentication generation requires Supabase, Spring Boot, Laravel, or FastAPI for ${fe.label}`)
194
195
  }
195
196
 
196
197
  // ── Collect selected manifests ───────────────────────────────────────────
@@ -277,7 +278,7 @@ export function resolveStack(answers, catalog) {
277
278
  // Apply env prefix: clientEnv vars get fe.envPrefix, others stay as-is
278
279
  const envPrefix = fe.envPrefix || ''
279
280
  if (authentication === 'session') rawEnv.push('SPRING_SECURITY_USER_NAME', 'SPRING_SECURITY_USER_PASSWORD')
280
- if (authentication === 'oidc') rawEnv.push('OIDC_ISSUER_URI', 'OIDC_AUDIENCE')
281
+ if (authentication === 'oidc' && be.id === 'springboot') rawEnv.push('OIDC_ISSUER_URI', 'OIDC_AUDIENCE')
281
282
  if (authentication === 'laravel-session') rawEnv.push('SESSION_DOMAIN')
282
283
  if (authentication === 'sanctum-spa') rawEnv.push('SESSION_DOMAIN', 'SANCTUM_STATEFUL_DOMAINS', 'CORS_ALLOWED_ORIGINS')
283
284
  if (authentication === 'laravel-oidc') rawEnv.push('AUTH0_DOMAIN', 'AUTH0_AUDIENCE')
@@ -3,7 +3,7 @@
3
3
  // Static helpers only — no stack identity checks. Stack-specific files are
4
4
  // now template-driven via templates/ and readTemplate().
5
5
 
6
- export function contextMd(vars, expectedConcerns) {
6
+ export function contextMd(vars, expectedConcerns, capabilities = {}) {
7
7
  const concernsBlock = (expectedConcerns && expectedConcerns.length)
8
8
  ? expectedConcerns.map((c) => `- ${c}`).join('\n')
9
9
  : '- (none selected — all optional concerns remain available)'
@@ -23,6 +23,15 @@ export function contextMd(vars, expectedConcerns) {
23
23
  ## Key Decisions
24
24
  <!-- Architecture and tech decisions made during the project -->
25
25
 
26
+ - Production baseline: generated default; deviations require approval.
27
+ - Uploads: ${capabilities.uploads || 'none'}
28
+ - Background jobs: ${capabilities.backgroundJobs || 'none'}
29
+ - Offline: ${capabilities.offline || 'none'}
30
+
31
+ ## Approved Deviations
32
+ <!-- Record date, approver, rationale, affected files, and recovery path. -->
33
+ - (none)
34
+
26
35
  ## Out of Scope
27
36
  <!-- What this project explicitly does NOT do -->
28
37
 
@@ -37,7 +37,7 @@ export async function resolvePlaybook(libraryDir, file) {
37
37
  const rest = restParts.join('/')
38
38
  const name = path.basename(file, '.md')
39
39
  const candidates = {
40
- universal: [path.join(libraryDir, 'universal', name, path.basename(file))],
40
+ universal: [path.join(libraryDir, 'universal', rest), path.join(libraryDir, 'universal', name, path.basename(file))],
41
41
  platform: [path.join(libraryDir, 'platforms', rest), path.join(libraryDir, 'platforms', name, path.basename(file))],
42
42
  stack: [path.join(libraryDir, 'stacks', rest)],
43
43
  capabilities: [
@@ -46,6 +46,7 @@ export async function resolvePlaybook(libraryDir, file) {
46
46
  ],
47
47
  concerns: [path.join(libraryDir, 'optional-features', 'concerns', rest)],
48
48
  styling: [
49
+ path.join(libraryDir, 'optional-features', 'styling', rest),
49
50
  path.join(libraryDir, 'optional-features', 'styling', name, path.basename(file)),
50
51
  path.join(libraryDir, 'optional-features', 'styling', name.replace(/-extensions$/, ''), path.basename(file)),
51
52
  ],
@@ -29,13 +29,13 @@ const VALID_COMBINATIONS = Object.freeze({
29
29
  ['laravel-ui', 'laravel'],
30
30
  ],
31
31
  separate: [
32
- ['nextjs', 'springboot'], ['nextjs', 'laravel'],
33
- ['react', 'springboot'], ['react', 'laravel'], ['react', 'supabase'],
32
+ ['nextjs', 'springboot'], ['nextjs', 'laravel'], ['nextjs', 'fastapi'],
33
+ ['react', 'springboot'], ['react', 'laravel'], ['react', 'supabase'], ['react', 'fastapi'],
34
34
  ],
35
- api: [['no-frontend', 'springboot'], ['no-frontend', 'laravel']],
35
+ api: [['no-frontend', 'springboot'], ['no-frontend', 'laravel'], ['no-frontend', 'fastapi']],
36
36
  mobile: [
37
37
  ['react-native', 'none'], ['react-native', 'supabase'],
38
- ['react-native', 'springboot'], ['react-native', 'laravel'],
38
+ ['react-native', 'springboot'], ['react-native', 'laravel'], ['react-native', 'fastapi'],
39
39
  ],
40
40
  frontend: [['react', 'none']],
41
41
  })
@@ -54,6 +54,8 @@ export function buildVars(answers, stack) {
54
54
  SPRING_MODULITH_VERSION: profile.springModulith,
55
55
  PHP_VERSION: profile.runtimes.php,
56
56
  COMPOSER_VERSION: profile.runtimes.composer,
57
+ PYTHON_VERSION: profile.runtimes.python,
58
+ UV_VERSION: profile.runtimes.uv,
57
59
  LARAVEL_VERSION: profile.composerPackages['laravel/framework'],
58
60
  LARAVEL_DIR: ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '.' : 'backend',
59
61
  TESTCONTAINERS_VERSION: profile.packages["testcontainers-junit-jupiter"] || profile.packages["testcontainers-postgresql"] || "1.19.8",
@@ -65,6 +67,7 @@ export function buildVars(answers, stack) {
65
67
  NGINX_IMAGE: `${profile.images.nginx.repository}:${profile.images.nginx.tag}`,
66
68
  PHP_IMAGE: `${profile.images.php.repository}:${profile.images.php.tag}`,
67
69
  COMPOSER_IMAGE: `${profile.images.composer.repository}:${profile.images.composer.tag}`,
70
+ PYTHON_IMAGE: `${profile.images.python.repository}:${profile.images.python.tag}`,
68
71
  COMPATIBILITY_PROFILE: profile.id,
69
72
  }
70
73
  }
@@ -20,6 +20,7 @@ export function validateCompatibility(catalog) {
20
20
  const statuses = new Map()
21
21
  let packageNames
22
22
  let composerPackageNames
23
+ let pythonPackageNames
23
24
  for (const [id, profile] of Object.entries(catalog.profiles)) {
24
25
  if (!/^\d{4}\.\d{2}$/.test(id)) throw new Error(`Invalid compatibility profile id: ${id}`)
25
26
  if (!['current', 'previous'].includes(profile.status)) throw new Error(`Invalid status for profile ${id}`)
@@ -30,10 +31,10 @@ export function validateCompatibility(catalog) {
30
31
  statuses.set(profile.status, id)
31
32
  if (!EXACT_VERSION.test(profile.springBoot || '')) throw new Error(`Profile ${id} needs an exact Spring Boot version`)
32
33
  if (!EXACT_VERSION.test(profile.springModulith || '')) throw new Error(`Profile ${id} needs an exact Spring Modulith version`)
33
- for (const key of ['node', 'java', 'maven', 'postgres', 'php', 'composer']) {
34
+ for (const key of ['node', 'java', 'maven', 'postgres', 'php', 'composer', 'python', 'uv']) {
34
35
  if (!profile.runtimes?.[key]) throw new Error(`Profile ${id} is missing runtime ${key}`)
35
36
  }
36
- for (const key of ['node', 'maven', 'java', 'postgres', 'nginx', 'php', 'composer']) {
37
+ for (const key of ['node', 'maven', 'java', 'postgres', 'nginx', 'php', 'composer', 'python']) {
37
38
  const image = profile.images?.[key]
38
39
  if (!image?.repository || !image?.tag || image.tag === 'latest') {
39
40
  throw new Error(`Profile ${id} has an invalid ${key} image`)
@@ -57,6 +58,9 @@ export function validateCompatibility(catalog) {
57
58
  if (profile.images.composer.repository !== 'composer' || profile.images.composer.tag !== profile.runtimes.composer) {
58
59
  throw new Error(`Profile ${id} Composer image must match runtime ${profile.runtimes.composer}`)
59
60
  }
61
+ if (profile.images.python.repository !== 'python' || !profile.images.python.tag.startsWith(`${profile.runtimes.python}-`)) {
62
+ throw new Error(`Profile ${id} Python image must match runtime ${profile.runtimes.python}`)
63
+ }
60
64
  const names = Object.keys(profile.packages || {}).sort()
61
65
  if (!names.length) throw new Error(`Profile ${id} has no package versions`)
62
66
  for (const [name, value] of Object.entries(profile.packages)) {
@@ -80,6 +84,15 @@ export function validateCompatibility(catalog) {
80
84
  throw new Error(`Profile ${id} does not own the same Composer package set as the current profile`)
81
85
  }
82
86
  composerPackageNames = composerNames
87
+ const pythonNames = Object.keys(profile.pythonPackages || {}).sort()
88
+ if (!pythonNames.length) throw new Error(`Profile ${id} has no Python package versions`)
89
+ for (const [name, version] of Object.entries(profile.pythonPackages)) {
90
+ if (!EXACT_VERSION.test(version || '')) throw new Error(`Profile ${id} Python package ${name} must use an exact version`)
91
+ }
92
+ if (pythonPackageNames && JSON.stringify(pythonNames) !== JSON.stringify(pythonPackageNames)) {
93
+ throw new Error(`Profile ${id} does not own the same Python package set as the current profile`)
94
+ }
95
+ pythonPackageNames = pythonNames
83
96
  }
84
97
  if (!statuses.has('current') || !statuses.has('previous')) {
85
98
  throw new Error('Exactly one current and one previous compatibility profile are required')
@@ -114,3 +127,9 @@ export function composerPackageVersion(profile, name, owner = name) {
114
127
  if (!version) throw new Error(`${owner} requires ${name}, missing from compatibility profile ${profile.id}`)
115
128
  return version
116
129
  }
130
+
131
+ export function pythonPackageVersion(profile, name, owner = name) {
132
+ const version = profile.pythonPackages?.[name]
133
+ if (!version) throw new Error(`${owner} requires ${name}, missing from compatibility profile ${profile.id}`)
134
+ return version
135
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'fs-extra'
2
+ import path from 'node:path'
3
+ import { loadCompatibility } from './tested-versions.js'
4
+
5
+ export async function createUpgradeReport(projectPath, compatibilityFile) {
6
+ const profilePath = path.join(path.resolve(projectPath), 'create-win-project.profile.json')
7
+ const existing = await fs.readJson(profilePath).catch(() => { throw new Error(`No create-win-project profile found at ${profilePath}`) })
8
+ const { profile: current } = await loadCompatibility(compatibilityFile)
9
+ const previousId = existing.compatibilityProfile?.id
10
+ const differences = []
11
+ if (existing.schemaVersion !== 2) differences.push({ area: 'migration', current: existing.schemaVersion, supported: 2 })
12
+ if (previousId !== current.id) differences.push({ area: 'compatibility-profile', current: previousId, supported: current.id })
13
+ for (const [runtime, version] of Object.entries(current.runtimes)) {
14
+ if (existing.runtimes?.[runtime] !== version) differences.push({ area: `runtime.${runtime}`, current: existing.runtimes?.[runtime], supported: version })
15
+ }
16
+ for (const contract of ['tests', 'continuousIntegration', 'securityRules', 'operationsDocumentation']) {
17
+ if (existing.productionBaseline?.[contract] !== true) differences.push({ area: `security-contract.${contract}`, current: existing.productionBaseline?.[contract], supported: true })
18
+ }
19
+ return { readOnly: true, project: path.resolve(projectPath), stack: existing.stack, profile: previousId, supportedProfile: current.id, differences }
20
+ }