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