create-win-project 1.3.0 → 1.4.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/README.md +19 -28
- package/{scripts/compatibility-matrix.mjs → checks/check-compatibility.js} +24 -6
- package/{scripts/verify-generated.mjs → checks/check-generated-project.js} +2 -2
- package/{scripts/validate-content.mjs → checks/check-library.js} +5 -5
- package/checks/classify-changes.js +39 -0
- package/docs/ARCHITECTURE.md +83 -40
- package/docs/CONTENT_MODEL.md +3 -3
- package/docs/CONTRIBUTING.md +154 -0
- package/docs/DEPENDENCY_MAINTENANCE.md +1 -1
- package/index.js +2 -429
- package/{playbooks → library}/INDEX.md +4 -4
- package/{playbooks/capabilities/ci/github-actions.manifest.json → library/development-tools/ci/definition.json} +1 -0
- package/{playbooks/devops/makefile.manifest.json → library/development-tools/devops/makefile/definition.json} +1 -0
- package/{playbooks/devops/pr-template.manifest.json → library/development-tools/devops/pr-template/definition.json} +1 -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/{compatibility/profiles.json → library/tested-versions.json} +4 -0
- package/{playbooks/universal/accessibility.manifest.json → library/universal/accessibility/definition.json} +1 -1
- package/{playbooks/universal/coding-rules.manifest.json → library/universal/coding-rules/definition.json} +1 -0
- package/{playbooks/universal/error-handling.manifest.json → library/universal/error-handling/definition.json} +1 -0
- package/{playbooks/universal/git-conventions.manifest.json → library/universal/git-conventions/definition.json} +1 -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/{playbooks/universal/typescript.manifest.json → library/universal/typescript/definition.json} +1 -0
- package/package.json +12 -11
- package/public/logo.svg +19 -0
- package/src/cli/arguments.js +52 -0
- package/src/cli/display.js +220 -0
- package/src/cli/main.js +185 -0
- package/src/cli/navigation.js +156 -0
- package/src/cli/questions.js +187 -0
- package/src/cli/system-check.js +138 -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} +5 -6
- package/{lib/playbooks.js → src/engine/project-guidance.js} +25 -3
- package/{lib/template.js → src/engine/render-templates.js} +8 -0
- package/src/engine/write-files.js +34 -0
- package/{lib/stacks/registry.js → src/stacks/available-stacks.js} +23 -1
- package/src/stacks/backends/README.md +5 -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 +241 -0
- package/{lib/generator.js → src/stacks/create-project.js} +78 -50
- 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 +25 -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/{lib → src}/stacks/shared/contributions.js +1 -1
- package/src/stacks/shared/environment.js +30 -0
- package/src/stacks/shared/javascript-package.js +111 -0
- package/src/stacks/shared/testing-files.js +16 -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/springboot.yml +1 -1
- 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/capabilities → library/development-tools}/ci/github-actions.md +0 -0
- /package/{playbooks/devops → library/development-tools/devops/makefile}/makefile.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/zod.md +0 -0
- /package/{playbooks → library/optional-features}/concerns/zustand.md +0 -0
- /package/{playbooks/styling → library/optional-features/styling/css-modules}/css-modules-extensions.md +0 -0
- /package/{playbooks/styling/css-modules.manifest.json → library/optional-features/styling/css-modules/definition.json} +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/react-native.manifest.json → library/stacks/expo/definition.json} +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/nextjs.manifest.json → library/stacks/nextjs/definition.json} +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/no-frontend.manifest.json → library/stacks/no-frontend/definition.json} +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/react-vite.manifest.json → library/stacks/react-vite/definition.json} +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/coding-rules}/coding-rules.md +0 -0
- /package/{playbooks/universal → library/universal/error-handling}/error-handling.md +0 -0
- /package/{playbooks/universal → library/universal/git-conventions}/git-conventions.md +0 -0
- /package/{playbooks/universal → library/universal/observability}/observability.md +0 -0
- /package/{playbooks/universal → library/universal/security}/security.md +0 -0
- /package/{playbooks/universal → library/universal/typescript}/typescript.md +0 -0
- /package/{lib/files.js → src/engine/project-files.js} +0 -0
- /package/{lib → src/engine}/project-location.js +0 -0
- /package/{lib/application-shapes.js → src/engine/project-shapes.js} +0 -0
- /package/{lib/compatibility.js → src/engine/tested-versions.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,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
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
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'].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'].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'].includes(answers.backend) && answers.frontend !== 'laravel-ui' &&
|
|
134
|
+
(args.authentication || answers.authentication) === 'yes' && !args.authAudience,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'list', name: 'testing', message: 'Testing setup?',
|
|
138
|
+
choices: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile'
|
|
139
|
+
? [
|
|
140
|
+
{ name: 'Basic (Jest + React Native Testing Library)', value: 'basic' },
|
|
141
|
+
{ name: 'None', value: 'none' },
|
|
142
|
+
]
|
|
143
|
+
: [
|
|
144
|
+
{ name: 'Full (Vitest + React Testing Library + Playwright)', value: 'full' },
|
|
145
|
+
{ name: 'Basic (Vitest + React Testing Library)', value: 'basic' },
|
|
146
|
+
{ name: 'None', value: 'none' },
|
|
147
|
+
],
|
|
148
|
+
default: (answers) => catalog.byId[answers.frontend]?.platform === 'mobile' ? 'basic' : 'full',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'confirm', name: 'docker', message: 'Add optional Docker development files', default: false,
|
|
152
|
+
when: (answers) => Boolean(catalog.byId[answers.backend]?.needsDocker) || catalog.byId[answers.frontend]?.platform !== 'mobile',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'confirm', name: 'makefile', message: 'Include Makefile?', default: true,
|
|
156
|
+
when: (answers) => catalog.byId[answers.frontend]?.platform !== 'mobile',
|
|
157
|
+
},
|
|
158
|
+
{ type: 'confirm', name: 'githubActions', message: 'Include GitHub Actions CI?', default: true },
|
|
159
|
+
{
|
|
160
|
+
type: 'confirm', name: 'installDependencies',
|
|
161
|
+
message: 'Install project dependencies and create the lockfile now?', default: true,
|
|
162
|
+
when: () => !args.install && !args.noInstall,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'input', name: 'packageName', message: 'Java package name? (e.g. com.yourname)', default: 'com.app',
|
|
166
|
+
when: (answers) => answers.backend === 'springboot',
|
|
167
|
+
validate: (value) => {
|
|
168
|
+
if (!value.trim()) return 'Package name is required'
|
|
169
|
+
if (!/^[a-z]+(\.[a-z]+)+$/.test(value)) return 'Use format: com.yourname'
|
|
170
|
+
return true
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'checkbox', name: 'expectedConcerns',
|
|
175
|
+
message: 'Planning notes for optional features (advisory only; no libraries or feature code are generated)',
|
|
176
|
+
choices: (answers) => {
|
|
177
|
+
const stack = resolveStack({
|
|
178
|
+
...answers,
|
|
179
|
+
styling: answers.styling || catalog.byId[answers.frontend]?.stylingOptions?.[0],
|
|
180
|
+
}, catalog)
|
|
181
|
+
const notes = [...new Set(stack.concerns.filter((concern) => !concern.required).map((concern) => concern.id))]
|
|
182
|
+
.map((id) => choice(CONCERN_LABELS[id] || id.replaceAll('-', ' '), id))
|
|
183
|
+
return [...notes, footerMarker('Write selected concerns to CONTEXT.md as planning notes.')]
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
]
|
|
187
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function needsNpmInstallation(stack) {
|
|
37
|
+
return stack.frontendKey !== 'no-frontend' &&
|
|
38
|
+
(stack.frontendKey !== 'laravel-ui' || stack.laravelUi === 'inertia-react')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function installationIssues(stack, profile, versions) {
|
|
42
|
+
const issues = []
|
|
43
|
+
if (needsNpmInstallation(stack)) {
|
|
44
|
+
const minimumNode = profile.runtimes.nodeMinimum
|
|
45
|
+
const minimumNpm = profile.runtimes.npmMinimum
|
|
46
|
+
if (!versionAtLeast(versions.node, minimumNode)) {
|
|
47
|
+
issues.push({ tool: 'Node.js', found: versions.node, required: minimumNode })
|
|
48
|
+
}
|
|
49
|
+
if (!versionAtLeast(versions.npm, minimumNpm)) {
|
|
50
|
+
issues.push({ tool: 'npm', found: versions.npm, required: minimumNpm })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (stack.backendKey === 'laravel') {
|
|
54
|
+
if (!versionAtLeast(versions.php, profile.runtimes.php)) {
|
|
55
|
+
issues.push({ tool: 'PHP', found: versions.php, required: profile.runtimes.php })
|
|
56
|
+
}
|
|
57
|
+
if (!versionAtLeast(versions.composer, profile.runtimes.composer)) {
|
|
58
|
+
issues.push({ tool: 'Composer', found: versions.composer, required: profile.runtimes.composer })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return issues
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function decideInstallation({ requested, issues, interactive, choose }) {
|
|
65
|
+
if (!requested) return { install: false, reason: 'not-requested' }
|
|
66
|
+
if (issues.length === 0) return { install: true, reason: 'supported' }
|
|
67
|
+
if (!interactive) return { install: false, reason: 'runtime-mismatch' }
|
|
68
|
+
|
|
69
|
+
while (true) {
|
|
70
|
+
const decision = await choose()
|
|
71
|
+
if (decision === 'skip') return { install: false, reason: 'runtime-mismatch' }
|
|
72
|
+
if (decision === 'cancel') return { install: false, reason: 'cancelled' }
|
|
73
|
+
if (decision !== 'instructions') throw new Error(`Unknown runtime decision: ${decision}`)
|
|
74
|
+
return { install: false, reason: 'show-instructions' }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function runtimeSetupInstructions(profile, issues = []) {
|
|
79
|
+
const tools = new Set(issues.map((issue) => issue.tool))
|
|
80
|
+
const instructions = []
|
|
81
|
+
if (tools.has('Node.js') || tools.has('npm')) {
|
|
82
|
+
instructions.push(
|
|
83
|
+
`Mise: mise use node@${profile.runtimes.node}`,
|
|
84
|
+
`NVM: nvm install ${profile.runtimes.node} && nvm use ${profile.runtimes.node}`,
|
|
85
|
+
'Verify: node --version && npm --version',
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
if (tools.has('PHP') || tools.has('Composer')) {
|
|
89
|
+
instructions.push(
|
|
90
|
+
`Install PHP ${profile.runtimes.php} and Composer ${profile.runtimes.composer}, or use the generated Docker workflow.`,
|
|
91
|
+
'Verify: php --version && composer --version',
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
return instructions
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Doctor (from lib/doctor.js) ───────────────────────────────────────────
|
|
98
|
+
function isOnPath(command) {
|
|
99
|
+
const suffixes = process.platform === 'win32' ? ['', '.cmd', '.exe', '.bat'] : ['']
|
|
100
|
+
return (process.env.PATH || '').split(path.delimiter).some((directory) =>
|
|
101
|
+
suffixes.some((suffix) => existsSync(path.join(directory, `${command}${suffix}`)))
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function probeDoctor(command, args = ['--version']) {
|
|
106
|
+
const result = spawnSync(command, args, { encoding: 'utf8', shell: false })
|
|
107
|
+
const version = `${result.stdout || result.stderr || ''}`.trim().split('\n')[0]
|
|
108
|
+
if (!version) return isOnPath(command) ? 'available (version unavailable)' : null
|
|
109
|
+
if (result.status !== 0) return null
|
|
110
|
+
return version
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function collectDiagnostics(profile) {
|
|
114
|
+
const nodeVersion = process.versions.node
|
|
115
|
+
const npmVersion = probeDoctor('npm')
|
|
116
|
+
const dockerVersion = probeDoctor('docker')
|
|
117
|
+
const composeVersion = dockerVersion ? probeDoctor('docker', ['compose', 'version']) : null
|
|
118
|
+
const javaVersion = probeDoctor('java', ['-version'])
|
|
119
|
+
return [
|
|
120
|
+
{ name: 'Node.js', found: nodeVersion, expected: profile.runtimes.node, required: false },
|
|
121
|
+
{ name: 'npm', found: npmVersion, required: false },
|
|
122
|
+
{ name: 'Docker', found: dockerVersion, required: false },
|
|
123
|
+
{ name: 'Docker Compose', found: composeVersion, required: false },
|
|
124
|
+
{ name: 'Java', found: javaVersion, expected: profile.runtimes.java, required: false },
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function printDoctor(profile, output = console.log) {
|
|
129
|
+
output(`create-win-project doctor (profile ${profile.id})`)
|
|
130
|
+
output('Use either the Node/npm lane or the Docker/Compose lane; Java is only needed for host-run Spring projects.')
|
|
131
|
+
for (const item of collectDiagnostics(profile)) {
|
|
132
|
+
const expectation = item.expected ? ` (tested: ${item.expected})` : ''
|
|
133
|
+
output(`${item.found ? '✓' : '○'} ${item.name}: ${item.found || 'not found'}${expectation}`)
|
|
134
|
+
}
|
|
135
|
+
output('')
|
|
136
|
+
output('Node lane: npm ci && npm start')
|
|
137
|
+
output('Docker lane: docker compose build && docker compose run --rm app')
|
|
138
|
+
}
|
|
@@ -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
|
}
|
|
@@ -32,8 +32,30 @@ 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', 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', name, path.basename(file)),
|
|
50
|
+
path.join(libraryDir, 'optional-features', 'styling', name.replace(/-extensions$/, ''), path.basename(file)),
|
|
51
|
+
],
|
|
52
|
+
devops: [path.join(libraryDir, 'development-tools', 'devops', name, path.basename(file))],
|
|
53
|
+
}[category] || []
|
|
54
|
+
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (await fs.pathExists(candidate)) return candidate
|
|
57
|
+
}
|
|
58
|
+
return candidates[0] || path.join(libraryDir, file)
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
/**
|
|
@@ -162,7 +184,7 @@ export async function buildRulesIndex(stack, catalog, playbooksDir) {
|
|
|
162
184
|
export async function copySelectedPlaybooks(playbooksDir, dest, stack) {
|
|
163
185
|
const allFiles = collectPlaybookFiles(stack)
|
|
164
186
|
for (const file of allFiles) {
|
|
165
|
-
const src =
|
|
187
|
+
const src = await resolvePlaybook(playbooksDir, file)
|
|
166
188
|
if (await fs.pathExists(src)) {
|
|
167
189
|
const dst = path.join(dest, 'playbooks', file)
|
|
168
190
|
await fs.ensureDir(path.dirname(dst))
|
|
@@ -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
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
export async function projectDestinations(cwd, projectName) {
|
|
6
|
+
const finalDestination = path.join(cwd, projectName)
|
|
7
|
+
if (await fs.pathExists(finalDestination)) {
|
|
8
|
+
throw new Error(`Destination already exists: ${finalDestination}`)
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
finalDestination,
|
|
12
|
+
stagingDestination: path.join(cwd, `.${projectName}.tmp-${randomUUID()}`),
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function writeProjectAtomically({ finalDestination, stagingDestination, generate }) {
|
|
17
|
+
try {
|
|
18
|
+
await fs.ensureDir(stagingDestination)
|
|
19
|
+
await generate(stagingDestination)
|
|
20
|
+
await fs.move(stagingDestination, finalDestination, { overwrite: false })
|
|
21
|
+
} catch (error) {
|
|
22
|
+
await fs.remove(stagingDestination)
|
|
23
|
+
throw error
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function writeFile(destination, filePath, content) {
|
|
28
|
+
const fullPath = path.join(destination, filePath)
|
|
29
|
+
await fs.ensureDir(path.dirname(fullPath))
|
|
30
|
+
await fs.writeFile(fullPath, content, 'utf8')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Generated documentation/file-content builders remain compatible during the migration.
|
|
34
|
+
export * from './project-files.js'
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import { isStackAdapter } from './
|
|
1
|
+
import { isStackAdapter } from './rules.js'
|
|
2
|
+
import { nextjsAdapter } from './frontends/nextjs/index.js'
|
|
3
|
+
import { reactViteAdapter } from './frontends/react-vite/index.js'
|
|
4
|
+
import { reactNativeAdapter } from './frontends/react-native/index.js'
|
|
5
|
+
import { springbootAdapter } from './backends/springboot/index.js'
|
|
6
|
+
import { supabaseAdapter } from './backends/supabase/index.js'
|
|
7
|
+
import { postgresAdapter } from './backends/postgres/index.js'
|
|
8
|
+
import { noBackendAdapter } from './backends/none/index.js'
|
|
9
|
+
import { laravelAdapter } from './backends/laravel/index.js'
|
|
2
10
|
|
|
3
11
|
export function createStackRegistry(initialAdapters = []) {
|
|
4
12
|
const adapters = new Map()
|
|
@@ -40,3 +48,17 @@ export function createStackRegistry(initialAdapters = []) {
|
|
|
40
48
|
return Object.freeze(registry)
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
// This is the only place stack adapters are assembled. Entries stay explicit
|
|
52
|
+
// so adding a source file never silently changes supported generator behavior.
|
|
53
|
+
export const stackRegistry = createStackRegistry([
|
|
54
|
+
nextjsAdapter,
|
|
55
|
+
reactViteAdapter,
|
|
56
|
+
reactNativeAdapter,
|
|
57
|
+
springbootAdapter,
|
|
58
|
+
supabaseAdapter,
|
|
59
|
+
postgresAdapter,
|
|
60
|
+
noBackendAdapter,
|
|
61
|
+
laravelAdapter,
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
export { stackRegistry as availableStacks }
|