create-win-project 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +58 -160
- package/checks/check-compatibility.js +95 -0
- package/{scripts/verify-generated.mjs → checks/check-generated-project.js} +28 -4
- package/{scripts/validate-content.mjs → checks/check-library.js} +17 -7
- package/checks/check-package.js +15 -0
- package/checks/classify-changes.js +29 -0
- package/checks/run-compatibility-shard.js +21 -0
- package/docs/README.md +20 -0
- package/docs/capabilities.md +13 -0
- package/docs/compatibility.md +13 -0
- package/docs/generated-project.md +12 -0
- package/docs/getting-started.md +26 -0
- package/docs/maintainers/architecture.md +161 -0
- package/docs/maintainers/ci-strategy.md +46 -0
- package/docs/{CONTENT_MODEL.md → maintainers/content-model.md} +3 -3
- package/docs/maintainers/contributing.md +154 -0
- package/docs/{DEPENDENCY_MAINTENANCE.md → maintainers/dependencies.md} +1 -1
- package/docs/migration-v2.md +13 -0
- package/docs/production-contract.md +24 -0
- package/index.js +2 -429
- package/{playbooks → library}/INDEX.md +5 -5
- package/library/compatibility-impact.json +14 -0
- package/{playbooks/capabilities/ci/github-actions.manifest.json → library/development-tools/ci/definition.json} +1 -0
- package/library/development-tools/devops/makefile/commands.md +11 -0
- package/library/development-tools/devops/makefile/definition.json +11 -0
- package/library/development-tools/devops/makefile/docker.md +9 -0
- package/library/development-tools/devops/makefile/validation.md +5 -0
- package/{playbooks/devops/pr-template.manifest.json → library/development-tools/devops/pr-template/definition.json} +1 -0
- package/library/features/sqlalchemy-alembic.md +17 -0
- package/library/optional-features/concerns/zod/errors.md +5 -0
- package/library/optional-features/concerns/zod/testing.md +5 -0
- package/library/optional-features/concerns/zod/validation.md +21 -0
- package/library/optional-features/styling/css-modules/definition.json +12 -0
- package/library/optional-features/styling/css-modules/responsive.md +5 -0
- package/library/optional-features/styling/css-modules/theme.md +7 -0
- package/{playbooks/platform/mobile.manifest.json → library/platforms/mobile/definition.json} +1 -0
- package/{playbooks/platform/web.manifest.json → library/platforms/web/definition.json} +1 -0
- package/{playbooks/stack/react-native.manifest.json → library/stacks/expo/definition.json} +2 -2
- package/library/stacks/fastapi/architecture.md +40 -0
- package/library/stacks/fastapi/definition.json +39 -0
- package/library/stacks/fastapi/runtime.md +25 -0
- package/library/stacks/fastapi/security.md +26 -0
- package/library/stacks/fastapi/structure.md +27 -0
- package/library/stacks/fastapi/testing.md +23 -0
- package/{playbooks/stack/nextjs.manifest.json → library/stacks/nextjs/definition.json} +2 -2
- package/{playbooks/stack/no-frontend.manifest.json → library/stacks/no-frontend/definition.json} +1 -1
- package/{playbooks/stack/react-vite.manifest.json → library/stacks/react-vite/definition.json} +2 -2
- package/{compatibility/profiles.json → library/tested-versions.json} +46 -2
- package/{playbooks/universal/accessibility.manifest.json → library/universal/accessibility/definition.json} +1 -1
- package/library/universal/coding-rules/definition.json +13 -0
- package/library/universal/coding-rules/hygiene.md +9 -0
- package/library/universal/coding-rules/naming.md +17 -0
- package/{playbooks/universal/error-handling.manifest.json → library/universal/error-handling/definition.json} +1 -0
- package/library/universal/git-conventions/branches.md +5 -0
- package/library/universal/git-conventions/commits.md +7 -0
- package/library/universal/git-conventions/definition.json +13 -0
- package/library/universal/git-conventions/workflow.md +5 -0
- package/{playbooks/universal/observability.manifest.json → library/universal/observability/definition.json} +1 -0
- package/{playbooks/universal/security.manifest.json → library/universal/security/definition.json} +1 -0
- package/library/universal/typescript/boundaries.md +13 -0
- package/library/universal/typescript/definition.json +12 -0
- package/library/universal/typescript/errors.md +5 -0
- package/library/universal/typescript/patterns.md +7 -0
- package/package.json +14 -15
- package/public/logo.svg +19 -0
- package/src/cli/arguments.js +63 -0
- package/src/cli/display.js +220 -0
- package/src/cli/main.js +203 -0
- package/src/cli/navigation.js +156 -0
- package/src/cli/questions.js +188 -0
- package/src/cli/system-check.js +158 -0
- package/src/engine/create-project.js +10 -0
- package/src/engine/install-dependencies.js +13 -0
- package/{lib/catalog.js → src/engine/load-library.js} +8 -8
- package/{lib/files.js → src/engine/project-files.js} +10 -1
- package/{lib/playbooks.js → src/engine/project-guidance.js} +26 -3
- package/{lib/application-shapes.js → src/engine/project-shapes.js} +4 -4
- package/{lib/template.js → src/engine/render-templates.js} +11 -0
- package/{lib/compatibility.js → src/engine/tested-versions.js} +21 -2
- package/src/engine/upgrade-report.js +20 -0
- package/src/engine/write-files.js +34 -0
- package/{lib/stacks/registry.js → src/stacks/available-stacks.js} +25 -1
- package/src/stacks/backends/README.md +5 -0
- package/src/stacks/backends/fastapi/ci.js +3 -0
- package/src/stacks/backends/fastapi/create-files.js +874 -0
- package/src/stacks/backends/fastapi/docker.js +75 -0
- package/src/stacks/backends/fastapi/environment.js +3 -0
- package/src/stacks/backends/fastapi/index.js +32 -0
- package/{lib/stacks → src/stacks/backends}/laravel/architecture.js +1 -0
- package/{lib/stacks → src/stacks/backends}/laravel/auth/oidc.js +1 -0
- package/{lib/stacks → src/stacks/backends}/laravel/auth/public.js +1 -0
- package/{lib/stacks → src/stacks/backends}/laravel/auth/sanctum.js +1 -0
- package/{lib/stacks → src/stacks/backends}/laravel/auth/session.js +1 -0
- package/src/stacks/backends/laravel/ci.js +3 -0
- package/{lib/stacks → src/stacks/backends}/laravel/composer.js +1 -1
- package/{lib/stacks/laravel/generate.js → src/stacks/backends/laravel/create-files.js} +1 -0
- package/src/stacks/backends/laravel/docker.js +14 -0
- package/src/stacks/backends/laravel/environment.js +5 -0
- package/{lib/stacks → src/stacks/backends}/laravel/index.js +11 -3
- package/{lib/stacks → src/stacks/backends}/laravel/ui/blade.js +1 -0
- package/{lib/stacks → src/stacks/backends}/laravel/ui/index.js +1 -0
- package/{lib/stacks → src/stacks/backends}/laravel/ui/inertia-react.js +4 -2
- package/{lib/stacks → src/stacks/backends}/laravel/ui/livewire.js +1 -0
- package/{lib/stacks → src/stacks/backends}/laravel/ui/shared.js +1 -0
- package/src/stacks/backends/none/index.js +23 -0
- package/src/stacks/backends/postgres/ci.js +3 -0
- package/src/stacks/backends/postgres/create-files.js +21 -0
- package/src/stacks/backends/postgres/docker.js +3 -0
- package/src/stacks/backends/postgres/environment.js +3 -0
- package/src/stacks/backends/postgres/index.js +30 -0
- package/src/stacks/backends/springboot/ci.js +3 -0
- package/src/stacks/backends/springboot/create-files.js +91 -0
- package/src/stacks/backends/springboot/docker.js +3 -0
- package/src/stacks/backends/springboot/environment.js +3 -0
- package/src/stacks/backends/springboot/index.js +31 -0
- package/src/stacks/backends/supabase/ci.js +3 -0
- package/src/stacks/backends/supabase/create-files.js +52 -0
- package/src/stacks/backends/supabase/docker.js +3 -0
- package/src/stacks/backends/supabase/environment.js +3 -0
- package/src/stacks/backends/supabase/index.js +31 -0
- package/src/stacks/backends/supabase/native.js +14 -0
- package/src/stacks/compose-files.js +259 -0
- package/{lib/generator.js → src/stacks/create-project.js} +206 -57
- package/src/stacks/frontends/README.md +4 -0
- package/src/stacks/frontends/nextjs/ci.js +3 -0
- package/src/stacks/frontends/nextjs/create-files.js +46 -0
- package/src/stacks/frontends/nextjs/dependencies.js +5 -0
- package/src/stacks/frontends/nextjs/docker.js +3 -0
- package/src/stacks/frontends/nextjs/environment.js +7 -0
- package/src/stacks/frontends/nextjs/index.js +35 -0
- package/src/stacks/frontends/react-native/ci.js +3 -0
- package/src/stacks/frontends/react-native/create-files.js +28 -0
- package/src/stacks/frontends/react-native/dependencies.js +5 -0
- package/src/stacks/frontends/react-native/docker.js +5 -0
- package/src/stacks/frontends/react-native/environment.js +7 -0
- package/src/stacks/frontends/react-native/index.js +30 -0
- package/src/stacks/frontends/react-vite/ci.js +3 -0
- package/src/stacks/frontends/react-vite/create-files.js +27 -0
- package/src/stacks/frontends/react-vite/dependencies.js +5 -0
- package/src/stacks/frontends/react-vite/docker.js +3 -0
- package/src/stacks/frontends/react-vite/environment.js +8 -0
- package/src/stacks/frontends/react-vite/index.js +32 -0
- package/src/stacks/shared/capability-packs.js +31 -0
- package/{lib → src}/stacks/shared/contributions.js +1 -1
- package/src/stacks/shared/environment.js +38 -0
- package/src/stacks/shared/javascript-package.js +117 -0
- package/src/stacks/shared/testing-files.js +16 -0
- package/templates/ci/fastapi.yml +62 -0
- package/templates/docker/compose/postgres.yml +1 -1
- package/templates/docker/compose/springboot.yml +3 -3
- package/templates/docker/compose/supabase.yml +1 -1
- package/templates/docker/compose-prod/fastapi.yml +52 -0
- package/templates/docker/compose-prod/springboot.yml +20 -2
- package/templates/docker/dockerfile/fastapi.dev.dockerfile +9 -0
- package/templates/docker/dockerfile/fastapi.prod.dockerfile +11 -0
- package/templates/docker/dockerfile/nextjs.prod.dockerfile +1 -0
- package/templates/docker/dockerfile/springboot.prod.dockerfile +4 -1
- package/templates/docker/dockerfile/vite.prod.dockerfile +2 -1
- package/templates/makefile/fastapi.mk +97 -0
- package/docs/ARCHITECTURE.md +0 -118
- package/lib/banner.js +0 -45
- package/lib/constants.js +0 -3
- package/lib/doctor.js +0 -45
- package/lib/interview.js +0 -77
- package/lib/laravel-scaffold.js +0 -3
- package/lib/scaffold.js +0 -467
- package/lib/stacks/index.js +0 -8
- package/playbooks/concerns/zod.md +0 -174
- package/playbooks/devops/makefile.manifest.json +0 -10
- package/playbooks/devops/makefile.md +0 -556
- package/playbooks/styling/css-modules-extensions.md +0 -267
- package/playbooks/styling/css-modules.manifest.json +0 -11
- package/playbooks/universal/coding-rules.manifest.json +0 -12
- package/playbooks/universal/coding-rules.md +0 -281
- package/playbooks/universal/git-conventions.manifest.json +0 -10
- package/playbooks/universal/git-conventions.md +0 -186
- package/playbooks/universal/typescript.manifest.json +0 -11
- package/playbooks/universal/typescript.md +0 -272
- package/scripts/compatibility-matrix.mjs +0 -48
- /package/{playbooks/capabilities → library/development-tools}/ci/github-actions.md +0 -0
- /package/{playbooks/devops → library/development-tools/devops/pr-template}/pr-template.md +0 -0
- /package/{playbooks/capabilities/docker/docker.manifest.json → library/development-tools/docker/definition.json} +0 -0
- /package/{playbooks/capabilities → library/development-tools}/docker/overview.md +0 -0
- /package/{playbooks/capabilities → library/features}/auth/oidc-resource-server.md +0 -0
- /package/{playbooks/capabilities → library/features}/auth/spring-session.md +0 -0
- /package/{playbooks/capabilities/flyway/flyway.manifest.json → library/features/flyway/definition.json} +0 -0
- /package/{playbooks/capabilities → library/features}/flyway/environments.md +0 -0
- /package/{playbooks/capabilities → library/features}/flyway/migrations.md +0 -0
- /package/{playbooks/capabilities → library/features}/flyway/testing.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/database.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/migrations.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/observability.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/oidc-resource-server.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/queues.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/sanctum-spa.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/scheduler.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/session-auth.md +0 -0
- /package/{playbooks/capabilities → library/features}/laravel/storage-uploads.md +0 -0
- /package/{playbooks/capabilities → library/features}/postgresql/architecture.md +0 -0
- /package/{playbooks/capabilities/postgresql/postgresql.manifest.json → library/features/postgresql/definition.json} +0 -0
- /package/{playbooks/capabilities → library/features}/postgresql/migrations.md +0 -0
- /package/{playbooks/capabilities → library/features}/postgresql/schema-design.md +0 -0
- /package/{playbooks/capabilities → library/features}/postgresql/security.md +0 -0
- /package/{playbooks/capabilities → library/features}/postgresql/testing.md +0 -0
- /package/{playbooks/capabilities → library/features}/prisma/architecture.md +0 -0
- /package/{playbooks/capabilities/prisma/prisma.manifest.json → library/features/prisma/definition.json} +0 -0
- /package/{playbooks/capabilities → library/features}/prisma/migrations.md +0 -0
- /package/{playbooks/capabilities → library/features}/prisma/runtime.md +0 -0
- /package/{playbooks/capabilities → library/features}/prisma/schema.md +0 -0
- /package/{playbooks/capabilities → library/features}/prisma/testing.md +0 -0
- /package/{playbooks/capabilities → library/features}/supabase/architecture.md +0 -0
- /package/{playbooks/capabilities → library/features}/supabase/authentication.md +0 -0
- /package/{playbooks/capabilities/supabase/supabase.manifest.json → library/features/supabase/definition.json} +0 -0
- /package/{playbooks/capabilities → library/features}/supabase/expo.md +0 -0
- /package/{playbooks/capabilities → library/features}/supabase/migrations.md +0 -0
- /package/{playbooks/capabilities → library/features}/supabase/nextjs.md +0 -0
- /package/{playbooks/capabilities → library/features}/supabase/rls.md +0 -0
- /package/{playbooks/capabilities → library/features}/supabase/testing.md +0 -0
- /package/{playbooks/capabilities → library/features}/supabase/vite.md +0 -0
- /package/{playbooks → library/optional-features}/concerns/axios.md +0 -0
- /package/{playbooks → library/optional-features}/concerns/next-safe-action.md +0 -0
- /package/{playbooks → library/optional-features}/concerns/next-themes.md +0 -0
- /package/{playbooks → library/optional-features}/concerns/nuqs.md +0 -0
- /package/{playbooks → library/optional-features}/concerns/t3-env.md +0 -0
- /package/{playbooks → library/optional-features}/concerns/tanstack-query.md +0 -0
- /package/{playbooks → library/optional-features}/concerns/zustand.md +0 -0
- /package/{playbooks/styling/native-styles.manifest.json → library/optional-features/styling/native-styles/definition.json} +0 -0
- /package/{playbooks/styling → library/optional-features/styling/native-styles}/native-styles.md +0 -0
- /package/{playbooks/styling/tailwind.manifest.json → library/optional-features/styling/tailwind/definition.json} +0 -0
- /package/{playbooks/styling → library/optional-features/styling/tailwind}/tailwind-extensions.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/blade/architecture.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/blade/runtime.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/blade/security.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/blade/structure.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/blade/testing.md +0 -0
- /package/{playbooks/stack/laravel-ui.manifest.json → library/platforms/laravel-ui/definition.json} +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/architecture.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/runtime.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/security.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/structure.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/inertia-react/testing.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/architecture.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/runtime.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/security.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/structure.md +0 -0
- /package/{playbooks/platform → library/platforms}/laravel-ui/livewire/testing.md +0 -0
- /package/{playbooks/platform → library/platforms/mobile}/mobile.md +0 -0
- /package/{playbooks/platform → library/platforms/web}/web.md +0 -0
- /package/{playbooks/stack → library/stacks}/expo/architecture.md +0 -0
- /package/{playbooks/stack → library/stacks}/expo/runtime.md +0 -0
- /package/{playbooks/stack → library/stacks}/expo/security.md +0 -0
- /package/{playbooks/stack → library/stacks}/expo/structure.md +0 -0
- /package/{playbooks/stack → library/stacks}/expo/testing.md +0 -0
- /package/{playbooks/stack → library/stacks}/laravel/architecture.md +0 -0
- /package/{playbooks/stack/laravel.manifest.json → library/stacks/laravel/definition.json} +0 -0
- /package/{playbooks/stack → library/stacks}/laravel/runtime.md +0 -0
- /package/{playbooks/stack → library/stacks}/laravel/security.md +0 -0
- /package/{playbooks/stack → library/stacks}/laravel/structure.md +0 -0
- /package/{playbooks/stack → library/stacks}/laravel/testing.md +0 -0
- /package/{playbooks/stack → library/stacks}/nextjs/architecture.md +0 -0
- /package/{playbooks/stack → library/stacks}/nextjs/runtime.md +0 -0
- /package/{playbooks/stack → library/stacks}/nextjs/security.md +0 -0
- /package/{playbooks/stack → library/stacks}/nextjs/structure.md +0 -0
- /package/{playbooks/stack → library/stacks}/nextjs/testing.md +0 -0
- /package/{playbooks/stack/none.manifest.json → library/stacks/none/definition.json} +0 -0
- /package/{playbooks/stack → library/stacks}/react-vite/architecture.md +0 -0
- /package/{playbooks/stack → library/stacks}/react-vite/runtime.md +0 -0
- /package/{playbooks/stack → library/stacks}/react-vite/security.md +0 -0
- /package/{playbooks/stack → library/stacks}/react-vite/structure.md +0 -0
- /package/{playbooks/stack → library/stacks}/react-vite/testing.md +0 -0
- /package/{playbooks/stack → library/stacks}/springboot/architecture.md +0 -0
- /package/{playbooks/stack/springboot.manifest.json → library/stacks/springboot/definition.json} +0 -0
- /package/{playbooks/stack → library/stacks}/springboot/runtime.md +0 -0
- /package/{playbooks/stack → library/stacks}/springboot/security.md +0 -0
- /package/{playbooks/stack → library/stacks}/springboot/structure.md +0 -0
- /package/{playbooks/stack → library/stacks}/springboot/testing.md +0 -0
- /package/{playbooks/universal → library/universal/accessibility}/accessibility.md +0 -0
- /package/{playbooks/universal → library/universal/error-handling}/error-handling.md +0 -0
- /package/{playbooks/universal → library/universal/observability}/observability.md +0 -0
- /package/{playbooks/universal → library/universal/security}/security.md +0 -0
- /package/{lib → src/engine}/project-location.js +0 -0
- /package/{lib → src}/stacks/context.js +0 -0
- /package/{lib/stacks/contract.js → src/stacks/rules.js} +0 -0
- /package/{ci → templates/ci}/expo.yml +0 -0
- /package/{ci → templates/ci}/laravel.yml +0 -0
- /package/{ci → templates/ci}/nextjs.yml +0 -0
- /package/{ci → templates/ci}/springboot.yml +0 -0
- /package/{ci → templates/ci}/vite.yml +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export { configurationDecisionChoices, lastEnabledIndex, promptWithBack, wrapText } from './navigation.js'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import {
|
|
4
|
+
architectureChoicesFor,
|
|
5
|
+
resolveStack,
|
|
6
|
+
stylingChoicesFor,
|
|
7
|
+
} from '../engine/load-library.js'
|
|
8
|
+
import {
|
|
9
|
+
APPLICATION_SHAPES,
|
|
10
|
+
applicationShapeChoices,
|
|
11
|
+
backendChoicesForShape,
|
|
12
|
+
frontendChoicesForShape,
|
|
13
|
+
} from '../engine/project-shapes.js'
|
|
14
|
+
import { laravelUiPromptContribution } from '../stacks/backends/laravel/ui/index.js'
|
|
15
|
+
|
|
16
|
+
const CONCERN_LABELS = {
|
|
17
|
+
validation: 'Runtime validation', query: 'Server-state caching', state: 'Shared UI state',
|
|
18
|
+
env: 'Environment validation', 'url-state': 'URL state', 'safe-action': 'Type-safe server actions',
|
|
19
|
+
'dark-mode': 'Dark mode', 'http-client': 'HTTP client',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function choice(title, value, { hint, recommended } = {}) {
|
|
23
|
+
const suffix = [hint ? `(${hint})` : '', recommended ? chalk.green('Recommended') : '']
|
|
24
|
+
.filter(Boolean).join(' ')
|
|
25
|
+
return { name: suffix ? `${title} ${suffix}` : title, short: title, value }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function footerMarker(text) {
|
|
29
|
+
return { type: 'description-footer', text }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function exampleForShape(shape) {
|
|
33
|
+
const description = APPLICATION_SHAPES[shape]?.description
|
|
34
|
+
const match = description?.match(/Example:\s*(.+?)\.?$/)
|
|
35
|
+
return match ? match[1] : undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function describeExisting(entries, description, hintFor) {
|
|
39
|
+
return [
|
|
40
|
+
...entries.map((entry) => choice(
|
|
41
|
+
entry.short || entry.name.split('\n')[0].replace(' (Recommended)', ''),
|
|
42
|
+
entry.value,
|
|
43
|
+
{ hint: hintFor?.(entry.value), recommended: entry.name.includes('(Recommended)') },
|
|
44
|
+
)),
|
|
45
|
+
footerMarker(description),
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validationError(problem, recovery) {
|
|
50
|
+
return `${chalk.red(`Error: ${problem}`)} ${chalk.yellow(recovery)}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildQuestions({ args, catalog }) {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
type: 'list', name: 'applicationShape', message: 'Application shape',
|
|
57
|
+
choices: describeExisting(
|
|
58
|
+
applicationShapeChoices(),
|
|
59
|
+
'Choose the runtime layout that matches how this application will be deployed.',
|
|
60
|
+
(shape) => {
|
|
61
|
+
const example = exampleForShape(shape)
|
|
62
|
+
return example ? `e.g. ${example}` : undefined
|
|
63
|
+
},
|
|
64
|
+
), default: 'fullstack', when: () => !args.shape,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'input', name: 'projectName', message: 'Project name', default: 'my-project',
|
|
68
|
+
validate: (value) => {
|
|
69
|
+
if (!value.trim()) return validationError('Project name is required.', 'Enter a lowercase name such as my-project.')
|
|
70
|
+
if (!/^[a-z0-9-]+$/.test(value)) return validationError('The name contains unsupported characters.', 'Use lowercase letters, numbers, and hyphens only.')
|
|
71
|
+
return true
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{ type: 'input', name: 'projectDescription', message: 'One-line description', default: 'A new application' },
|
|
75
|
+
{
|
|
76
|
+
type: 'list', name: 'frontend', message: 'Application framework',
|
|
77
|
+
choices: (answers) => describeExisting(frontendChoicesForShape(args.shape || answers.applicationShape, catalog), 'The primary user-facing application framework.'),
|
|
78
|
+
when: () => !args.frontend,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'list', name: 'backend', message: 'Backend or data service',
|
|
82
|
+
choices: (answers) => describeExisting(backendChoicesForShape(args.shape || answers.applicationShape, answers.frontend, catalog), 'The server or managed data boundary used by this application.'),
|
|
83
|
+
when: () => !args.backend,
|
|
84
|
+
},
|
|
85
|
+
...laravelUiPromptContribution(args.laravelUi).questions,
|
|
86
|
+
{
|
|
87
|
+
type: 'list', name: 'styling', message: 'Styling approach',
|
|
88
|
+
choices: (answers) => describeExisting(stylingChoicesFor(catalog, answers.frontend), 'The default styling system for generated UI.'),
|
|
89
|
+
when: (answers) => stylingChoicesFor(catalog, answers.frontend).length > 1,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'list', name: 'architecture',
|
|
93
|
+
message: 'Architecture depth',
|
|
94
|
+
choices: (answers) => {
|
|
95
|
+
const supported = architectureChoicesFor(catalog, answers.frontend, answers.backend)
|
|
96
|
+
return [
|
|
97
|
+
...[
|
|
98
|
+
choice('Medium', 'medium', { recommended: true }),
|
|
99
|
+
choice('Small', 'small'),
|
|
100
|
+
choice('Large', 'large'),
|
|
101
|
+
].filter((choice) => supported.includes(choice.value)),
|
|
102
|
+
footerMarker('Clear feature boundaries for most long-term applications; fewer layers for prototypes; enforced boundaries for complex domains.'),
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
default: 'medium', when: () => !args.architecture,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'list', name: 'authentication',
|
|
109
|
+
message: (answers) => ['supabase', 'springboot', 'laravel', 'fastapi'].includes(answers.backend)
|
|
110
|
+
? 'User authentication'
|
|
111
|
+
: `User authentication ${chalk.yellow(`(generation unavailable for ${answers.backend === 'none' ? 'a frontend-only project' : 'a PostgreSQL-only backend'})`)}`,
|
|
112
|
+
choices: (answers) => {
|
|
113
|
+
const choices = []
|
|
114
|
+
if (['supabase', 'springboot', 'laravel', 'fastapi'].includes(answers.backend)) {
|
|
115
|
+
choices.push(choice('Yes', 'yes'))
|
|
116
|
+
}
|
|
117
|
+
choices.push(
|
|
118
|
+
choice('Not yet', 'not-yet', { recommended: true }),
|
|
119
|
+
choice('No', 'none'),
|
|
120
|
+
footerMarker('Generate working authentication for this stack, record guidance without pretending login exists, or stay intentionally public.'),
|
|
121
|
+
)
|
|
122
|
+
return choices
|
|
123
|
+
},
|
|
124
|
+
default: 'not-yet', when: () => !args.authentication,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'list', name: 'authAudience', message: 'Where will users access the application?',
|
|
128
|
+
choices: [
|
|
129
|
+
{ name: 'Website only — use a secure server-managed browser session', value: 'website' },
|
|
130
|
+
{ name: 'Website and mobile — use a trusted identity provider for every client', value: 'multi-client' },
|
|
131
|
+
],
|
|
132
|
+
default: 'website',
|
|
133
|
+
when: (answers) => ['springboot', 'laravel', 'fastapi'].includes(answers.backend) && answers.frontend !== 'laravel-ui' &&
|
|
134
|
+
(args.authentication || answers.authentication) === 'yes' && !args.authAudience,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
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,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'confirm', name: 'docker', message: 'Add optional Docker development files', default: false,
|
|
153
|
+
when: (answers) => Boolean(catalog.byId[answers.backend]?.needsDocker) || catalog.byId[answers.frontend]?.platform !== 'mobile',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'confirm', name: 'makefile', message: 'Include Makefile?', default: true,
|
|
157
|
+
when: (answers) => catalog.byId[answers.frontend]?.platform !== 'mobile',
|
|
158
|
+
},
|
|
159
|
+
{ type: 'confirm', name: 'githubActions', message: 'Include GitHub Actions CI?', default: true },
|
|
160
|
+
{
|
|
161
|
+
type: 'confirm', name: 'installDependencies',
|
|
162
|
+
message: 'Install project dependencies and create the lockfile now?', default: true,
|
|
163
|
+
when: () => !args.install && !args.noInstall,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
type: 'input', name: 'packageName', message: 'Java package name? (e.g. com.yourname)', default: 'com.app',
|
|
167
|
+
when: (answers) => answers.backend === 'springboot',
|
|
168
|
+
validate: (value) => {
|
|
169
|
+
if (!value.trim()) return 'Package name is required'
|
|
170
|
+
if (!/^[a-z]+(\.[a-z]+)+$/.test(value)) return 'Use format: com.yourname'
|
|
171
|
+
return true
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
type: 'checkbox', name: 'expectedConcerns',
|
|
176
|
+
message: 'Planning notes for optional features (advisory only; no libraries or feature code are generated)',
|
|
177
|
+
choices: (answers) => {
|
|
178
|
+
const stack = resolveStack({
|
|
179
|
+
...answers,
|
|
180
|
+
styling: answers.styling || catalog.byId[answers.frontend]?.stylingOptions?.[0],
|
|
181
|
+
}, catalog)
|
|
182
|
+
const notes = [...new Set(stack.concerns.filter((concern) => !concern.required).map((concern) => concern.id))]
|
|
183
|
+
.map((id) => choice(CONCERN_LABELS[id] || id.replaceAll('-', ' '), id))
|
|
184
|
+
return [...notes, footerMarker('Write selected concerns to CONTEXT.md as planning notes.')]
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
]
|
|
188
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
function numericVersion(value) {
|
|
6
|
+
const match = String(value || '').match(/\d+(?:\.\d+){0,2}/)
|
|
7
|
+
return match ? match[0].split('.').map(Number) : null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function versionAtLeast(found, required) {
|
|
11
|
+
const actual = numericVersion(found)
|
|
12
|
+
const minimum = numericVersion(required)
|
|
13
|
+
if (!actual || !minimum) return false
|
|
14
|
+
for (let index = 0; index < 3; index += 1) {
|
|
15
|
+
const difference = (actual[index] || 0) - (minimum[index] || 0)
|
|
16
|
+
if (difference !== 0) return difference > 0
|
|
17
|
+
}
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function probe(command, args = ['--version'], run = spawnSync) {
|
|
22
|
+
const result = run(command, args, { encoding: 'utf8', shell: false })
|
|
23
|
+
if (result.error || result.status !== 0) return null
|
|
24
|
+
return `${result.stdout || result.stderr || ''}`.trim().split('\n')[0] || null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function detectSystemVersions({ run = spawnSync, nodeVersion = process.versions.node } = {}) {
|
|
28
|
+
return {
|
|
29
|
+
node: nodeVersion,
|
|
30
|
+
npm: probe(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['--version'], run),
|
|
31
|
+
php: probe('php', ['--version'], run),
|
|
32
|
+
composer: probe(process.platform === 'win32' ? 'composer.bat' : 'composer', ['--version'], run),
|
|
33
|
+
python: probe('python', ['--version'], run),
|
|
34
|
+
uv: probe('uv', ['--version'], run),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function needsNpmInstallation(stack) {
|
|
39
|
+
return stack.frontendKey !== 'no-frontend' &&
|
|
40
|
+
(stack.frontendKey !== 'laravel-ui' || stack.laravelUi === 'inertia-react')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function installationIssues(stack, profile, versions) {
|
|
44
|
+
const issues = []
|
|
45
|
+
if (needsNpmInstallation(stack)) {
|
|
46
|
+
const minimumNode = profile.runtimes.nodeMinimum
|
|
47
|
+
const minimumNpm = profile.runtimes.npmMinimum
|
|
48
|
+
if (!versionAtLeast(versions.node, minimumNode)) {
|
|
49
|
+
issues.push({ tool: 'Node.js', found: versions.node, required: minimumNode })
|
|
50
|
+
}
|
|
51
|
+
if (!versionAtLeast(versions.npm, minimumNpm)) {
|
|
52
|
+
issues.push({ tool: 'npm', found: versions.npm, required: minimumNpm })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (stack.backendKey === 'laravel') {
|
|
56
|
+
if (!versionAtLeast(versions.php, profile.runtimes.php)) {
|
|
57
|
+
issues.push({ tool: 'PHP', found: versions.php, required: profile.runtimes.php })
|
|
58
|
+
}
|
|
59
|
+
if (!versionAtLeast(versions.composer, profile.runtimes.composer)) {
|
|
60
|
+
issues.push({ tool: 'Composer', found: versions.composer, required: profile.runtimes.composer })
|
|
61
|
+
}
|
|
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
|
+
}
|
|
71
|
+
return issues
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function decideInstallation({ requested, issues, interactive, choose }) {
|
|
75
|
+
if (!requested) return { install: false, reason: 'not-requested' }
|
|
76
|
+
if (issues.length === 0) return { install: true, reason: 'supported' }
|
|
77
|
+
if (!interactive) return { install: false, reason: 'runtime-mismatch' }
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
const decision = await choose()
|
|
81
|
+
if (decision === 'skip') return { install: false, reason: 'runtime-mismatch' }
|
|
82
|
+
if (decision === 'cancel') return { install: false, reason: 'cancelled' }
|
|
83
|
+
if (decision !== 'instructions') throw new Error(`Unknown runtime decision: ${decision}`)
|
|
84
|
+
return { install: false, reason: 'show-instructions' }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function runtimeSetupInstructions(profile, issues = []) {
|
|
89
|
+
const tools = new Set(issues.map((issue) => issue.tool))
|
|
90
|
+
const instructions = []
|
|
91
|
+
if (tools.has('Node.js') || tools.has('npm')) {
|
|
92
|
+
instructions.push(
|
|
93
|
+
`Mise: mise use node@${profile.runtimes.node}`,
|
|
94
|
+
`NVM: nvm install ${profile.runtimes.node} && nvm use ${profile.runtimes.node}`,
|
|
95
|
+
'Verify: node --version && npm --version',
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
if (tools.has('PHP') || tools.has('Composer')) {
|
|
99
|
+
instructions.push(
|
|
100
|
+
`Install PHP ${profile.runtimes.php} and Composer ${profile.runtimes.composer}, or use the generated Docker workflow.`,
|
|
101
|
+
'Verify: php --version && composer --version',
|
|
102
|
+
)
|
|
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
|
+
}
|
|
110
|
+
return instructions
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Doctor (from lib/doctor.js) ───────────────────────────────────────────
|
|
114
|
+
function isOnPath(command) {
|
|
115
|
+
const suffixes = process.platform === 'win32' ? ['', '.cmd', '.exe', '.bat'] : ['']
|
|
116
|
+
return (process.env.PATH || '').split(path.delimiter).some((directory) =>
|
|
117
|
+
suffixes.some((suffix) => existsSync(path.join(directory, `${command}${suffix}`)))
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function probeDoctor(command, args = ['--version']) {
|
|
122
|
+
const result = spawnSync(command, args, { encoding: 'utf8', shell: false })
|
|
123
|
+
const version = `${result.stdout || result.stderr || ''}`.trim().split('\n')[0]
|
|
124
|
+
if (!version) return isOnPath(command) ? 'available (version unavailable)' : null
|
|
125
|
+
if (result.status !== 0) return null
|
|
126
|
+
return version
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function collectDiagnostics(profile) {
|
|
130
|
+
const nodeVersion = process.versions.node
|
|
131
|
+
const npmVersion = probeDoctor('npm')
|
|
132
|
+
const dockerVersion = probeDoctor('docker')
|
|
133
|
+
const composeVersion = dockerVersion ? probeDoctor('docker', ['compose', 'version']) : null
|
|
134
|
+
const javaVersion = probeDoctor('java', ['-version'])
|
|
135
|
+
const pythonVersion = probeDoctor('python')
|
|
136
|
+
const uvVersion = probeDoctor('uv')
|
|
137
|
+
return [
|
|
138
|
+
{ name: 'Node.js', found: nodeVersion, expected: profile.runtimes.node, required: false },
|
|
139
|
+
{ name: 'npm', found: npmVersion, required: false },
|
|
140
|
+
{ name: 'Docker', found: dockerVersion, required: false },
|
|
141
|
+
{ name: 'Docker Compose', found: composeVersion, required: false },
|
|
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 },
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function printDoctor(profile, output = console.log, diagnostics = collectDiagnostics(profile)) {
|
|
149
|
+
output(`create-win-project doctor (profile ${profile.id})`)
|
|
150
|
+
output('Use either the Node/npm lane or the Docker/Compose lane; Java is only needed for host-run Spring projects.')
|
|
151
|
+
for (const item of diagnostics) {
|
|
152
|
+
const expectation = item.expected ? ` (tested: ${item.expected})` : ''
|
|
153
|
+
output(`${item.found ? '✓' : '○'} ${item.name}: ${item.found || 'not found'}${expectation}`)
|
|
154
|
+
}
|
|
155
|
+
output('')
|
|
156
|
+
output('Node lane: npm ci && npm start')
|
|
157
|
+
output('Docker lane: docker compose build && docker compose run --rm app')
|
|
158
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { scaffoldProject } from '../stacks/create-project.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs the generic project-creation pipeline. Stack-owned contributions are
|
|
5
|
+
* composed behind the shared stack boundary and are migrated into their
|
|
6
|
+
* individual adapters in the following phases.
|
|
7
|
+
*/
|
|
8
|
+
export async function generateProject(answers, cliRoot) {
|
|
9
|
+
return scaffoldProject(answers, cliRoot)
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export function runDependencySteps(steps, run = spawnSync) {
|
|
4
|
+
for (const step of steps) {
|
|
5
|
+
const result = run(step.command, step.args, {
|
|
6
|
+
cwd: step.cwd,
|
|
7
|
+
stdio: 'inherit',
|
|
8
|
+
shell: false,
|
|
9
|
+
})
|
|
10
|
+
if (result.error || result.status !== 0) return { ok: false, failed: step, result }
|
|
11
|
+
}
|
|
12
|
+
return { ok: true, failed: null }
|
|
13
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import { loadCompatibility, resolvePackages } from './
|
|
4
|
-
import { inferApplicationShape, validateApplicationShape } from './
|
|
3
|
+
import { loadCompatibility, resolvePackages } from './tested-versions.js'
|
|
4
|
+
import { inferApplicationShape, validateApplicationShape } from './project-shapes.js'
|
|
5
5
|
|
|
6
6
|
// ─── Loader ───────────────────────────────────────────────────────────────────
|
|
7
7
|
|
|
@@ -12,11 +12,10 @@ export async function loadCatalog(playbooksDir, selectedProfile) {
|
|
|
12
12
|
for (const e of entries) {
|
|
13
13
|
const full = path.join(dir, e.name)
|
|
14
14
|
if (e.isDirectory()) { await walk(full); continue }
|
|
15
|
-
if (e.name
|
|
15
|
+
if (e.name === 'definition.json') {
|
|
16
16
|
const m = await fs.readJson(full)
|
|
17
17
|
m._file = full
|
|
18
|
-
|
|
19
|
-
m._playbooks = m.playbooks || [m.file || defaultPlaybook]
|
|
18
|
+
m._playbooks = m.playbooks || (m.file ? [m.file] : [])
|
|
20
19
|
const firstPlaybook = m._playbooks[0]
|
|
21
20
|
m._playbook = typeof firstPlaybook === 'string' ? firstPlaybook : firstPlaybook?.file
|
|
22
21
|
manifests.push(m)
|
|
@@ -26,7 +25,7 @@ export async function loadCatalog(playbooksDir, selectedProfile) {
|
|
|
26
25
|
await walk(playbooksDir)
|
|
27
26
|
validateManifests(manifests)
|
|
28
27
|
const profile = selectedProfile || (await loadCompatibility(
|
|
29
|
-
path.join(
|
|
28
|
+
path.join(playbooksDir, 'tested-versions.json'),
|
|
30
29
|
)).profile
|
|
31
30
|
return { ...buildIndex(manifests), profile }
|
|
32
31
|
}
|
|
@@ -191,7 +190,8 @@ export function resolveStack(answers, catalog) {
|
|
|
191
190
|
if (fe.id === 'laravel-ui') authentication = 'laravel-session'
|
|
192
191
|
else authentication = authAudience === 'website' && fe.platform === 'web' ? 'sanctum-spa' : 'laravel-oidc'
|
|
193
192
|
}
|
|
194
|
-
else
|
|
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}`)
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
// ── Collect selected manifests ───────────────────────────────────────────
|
|
@@ -278,7 +278,7 @@ export function resolveStack(answers, catalog) {
|
|
|
278
278
|
// Apply env prefix: clientEnv vars get fe.envPrefix, others stay as-is
|
|
279
279
|
const envPrefix = fe.envPrefix || ''
|
|
280
280
|
if (authentication === 'session') rawEnv.push('SPRING_SECURITY_USER_NAME', 'SPRING_SECURITY_USER_PASSWORD')
|
|
281
|
-
if (authentication === 'oidc') rawEnv.push('OIDC_ISSUER_URI', 'OIDC_AUDIENCE')
|
|
281
|
+
if (authentication === 'oidc' && be.id === 'springboot') rawEnv.push('OIDC_ISSUER_URI', 'OIDC_AUDIENCE')
|
|
282
282
|
if (authentication === 'laravel-session') rawEnv.push('SESSION_DOMAIN')
|
|
283
283
|
if (authentication === 'sanctum-spa') rawEnv.push('SESSION_DOMAIN', 'SANCTUM_STATEFUL_DOMAINS', 'CORS_ALLOWED_ORIGINS')
|
|
284
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
|
|
|
@@ -32,8 +32,31 @@ export function extractSections(content) {
|
|
|
32
32
|
* Resolve a playbook relative path to an absolute path.
|
|
33
33
|
* Handles nested stack facets and concern playbooks transparently.
|
|
34
34
|
*/
|
|
35
|
-
export async function resolvePlaybook(
|
|
36
|
-
|
|
35
|
+
export async function resolvePlaybook(libraryDir, file) {
|
|
36
|
+
const [category, ...restParts] = file.split('/')
|
|
37
|
+
const rest = restParts.join('/')
|
|
38
|
+
const name = path.basename(file, '.md')
|
|
39
|
+
const candidates = {
|
|
40
|
+
universal: [path.join(libraryDir, 'universal', rest), path.join(libraryDir, 'universal', name, path.basename(file))],
|
|
41
|
+
platform: [path.join(libraryDir, 'platforms', rest), path.join(libraryDir, 'platforms', name, path.basename(file))],
|
|
42
|
+
stack: [path.join(libraryDir, 'stacks', rest)],
|
|
43
|
+
capabilities: [
|
|
44
|
+
path.join(libraryDir, 'features', rest),
|
|
45
|
+
path.join(libraryDir, 'development-tools', rest),
|
|
46
|
+
],
|
|
47
|
+
concerns: [path.join(libraryDir, 'optional-features', 'concerns', rest)],
|
|
48
|
+
styling: [
|
|
49
|
+
path.join(libraryDir, 'optional-features', 'styling', rest),
|
|
50
|
+
path.join(libraryDir, 'optional-features', 'styling', name, path.basename(file)),
|
|
51
|
+
path.join(libraryDir, 'optional-features', 'styling', name.replace(/-extensions$/, ''), path.basename(file)),
|
|
52
|
+
],
|
|
53
|
+
devops: [path.join(libraryDir, 'development-tools', 'devops', name, path.basename(file))],
|
|
54
|
+
}[category] || []
|
|
55
|
+
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
if (await fs.pathExists(candidate)) return candidate
|
|
58
|
+
}
|
|
59
|
+
return candidates[0] || path.join(libraryDir, file)
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
/**
|
|
@@ -162,7 +185,7 @@ export async function buildRulesIndex(stack, catalog, playbooksDir) {
|
|
|
162
185
|
export async function copySelectedPlaybooks(playbooksDir, dest, stack) {
|
|
163
186
|
const allFiles = collectPlaybookFiles(stack)
|
|
164
187
|
for (const file of allFiles) {
|
|
165
|
-
const src =
|
|
188
|
+
const src = await resolvePlaybook(playbooksDir, file)
|
|
166
189
|
if (await fs.pathExists(src)) {
|
|
167
190
|
const dst = path.join(dest, 'playbooks', file)
|
|
168
191
|
await fs.ensureDir(path.dirname(dst))
|
|
@@ -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
|
})
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
4
|
import path from 'path'
|
|
5
|
+
import { writeFile } from './write-files.js'
|
|
5
6
|
|
|
6
7
|
export function render(content, vars) {
|
|
7
8
|
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`)
|
|
@@ -14,6 +15,13 @@ export async function readTemplate(templatesDir, category, name, ext = '') {
|
|
|
14
15
|
return null
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export async function writeRenderedFile(destination, filePath, template, vars) {
|
|
19
|
+
const content = render(template, vars)
|
|
20
|
+
await writeFile(destination, filePath, content)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export * from './write-files.js'
|
|
24
|
+
|
|
17
25
|
export function buildVars(answers, stack) {
|
|
18
26
|
const packagePath = (answers.packageName ?? 'com.app').replace(/\./g, '/')
|
|
19
27
|
const profile = stack.profile
|
|
@@ -46,6 +54,8 @@ export function buildVars(answers, stack) {
|
|
|
46
54
|
SPRING_MODULITH_VERSION: profile.springModulith,
|
|
47
55
|
PHP_VERSION: profile.runtimes.php,
|
|
48
56
|
COMPOSER_VERSION: profile.runtimes.composer,
|
|
57
|
+
PYTHON_VERSION: profile.runtimes.python,
|
|
58
|
+
UV_VERSION: profile.runtimes.uv,
|
|
49
59
|
LARAVEL_VERSION: profile.composerPackages['laravel/framework'],
|
|
50
60
|
LARAVEL_DIR: ['laravel-ui', 'no-frontend'].includes(stack.frontendKey) ? '.' : 'backend',
|
|
51
61
|
TESTCONTAINERS_VERSION: profile.packages["testcontainers-junit-jupiter"] || profile.packages["testcontainers-postgresql"] || "1.19.8",
|
|
@@ -57,6 +67,7 @@ export function buildVars(answers, stack) {
|
|
|
57
67
|
NGINX_IMAGE: `${profile.images.nginx.repository}:${profile.images.nginx.tag}`,
|
|
58
68
|
PHP_IMAGE: `${profile.images.php.repository}:${profile.images.php.tag}`,
|
|
59
69
|
COMPOSER_IMAGE: `${profile.images.composer.repository}:${profile.images.composer.tag}`,
|
|
70
|
+
PYTHON_IMAGE: `${profile.images.python.repository}:${profile.images.python.tag}`,
|
|
60
71
|
COMPATIBILITY_PROFILE: profile.id,
|
|
61
72
|
}
|
|
62
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
|
+
}
|