create-lumen 2.0.0-alpha.1 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +38 -12
  2. package/bin/cli.js +3 -1
  3. package/package.json +3 -3
  4. package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +1 -1
  5. package/schema/fixtures/v2/valid/react-vite-type-based.json +1 -1
  6. package/schema/lumen.config.v2.json +0 -1
  7. package/src/cli-args.js +169 -7
  8. package/src/configure.js +12 -4
  9. package/src/css.js +14 -30
  10. package/src/dependencies.js +36 -20
  11. package/src/engine/capabilities.js +195 -4
  12. package/src/engine/composition.js +736 -0
  13. package/src/engine/index.js +1 -0
  14. package/src/engine/providers/base-provider.js +4 -4
  15. package/src/injector.js +294 -446
  16. package/src/main.js +38 -16
  17. package/src/manifest/emit.js +1 -1
  18. package/src/manifest/schema.js +1 -1
  19. package/src/prompts.js +581 -238
  20. package/src/readme.js +23 -2
  21. package/src/scaffold.js +17 -0
  22. package/templates/architectures/hybrid/main.jsx +10 -0
  23. package/templates/architectures/hybrid/main.tsx +10 -0
  24. package/templates/architectures/hybrid/src/App.jsx +20 -0
  25. package/templates/architectures/hybrid/src/App.tsx +20 -0
  26. package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +31 -0
  27. package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +36 -0
  28. package/templates/architectures/hybrid/src/shared/components/ui/index.js +1 -0
  29. package/templates/architectures/hybrid/src/shared/components/ui/index.ts +1 -0
  30. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +28 -0
  31. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +28 -0
  32. package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
  33. package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
  34. package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
  35. package/templates/architectures/hybrid/src/shared/types/index.js +20 -0
  36. package/templates/architectures/hybrid/src/shared/types/index.ts +16 -0
  37. package/templates/architectures/hybrid/src/shared/utils/cn.jsx +3 -0
  38. package/templates/architectures/hybrid/src/shared/utils/cn.ts +5 -0
  39. package/templates/architectures/none/main.jsx +10 -0
  40. package/templates/architectures/none/main.tsx +10 -0
  41. package/templates/architectures/none/src/App.jsx +15 -0
  42. package/templates/architectures/none/src/App.tsx +15 -0
  43. package/templates/architectures/none/src/styles/.gitkeep +1 -0
  44. package/templates/architectures/none/src/types/index.js +20 -0
  45. package/templates/architectures/none/src/types/index.ts +16 -0
  46. package/templates/bases/next/app-router/js/.provenance.json +19 -0
  47. package/templates/bases/next/app-router/js/README.md +36 -0
  48. package/templates/bases/next/app-router/js/jsconfig.json +7 -0
  49. package/templates/bases/next/app-router/js/next.config.mjs +6 -0
  50. package/templates/bases/next/app-router/js/package.json +15 -0
  51. package/templates/bases/next/app-router/js/public/file.svg +1 -0
  52. package/templates/bases/next/app-router/js/public/globe.svg +1 -0
  53. package/templates/bases/next/app-router/js/public/next.svg +1 -0
  54. package/templates/bases/next/app-router/js/public/vercel.svg +1 -0
  55. package/templates/bases/next/app-router/js/public/window.svg +1 -0
  56. package/templates/bases/next/app-router/js/src/app/api/hello/route.js +5 -0
  57. package/templates/bases/next/app-router/js/src/app/blog/[slug]/page.js +33 -0
  58. package/templates/bases/next/app-router/js/src/app/favicon.ico +0 -0
  59. package/templates/bases/next/app-router/js/src/app/globals.css +49 -0
  60. package/templates/bases/next/app-router/js/src/app/layout.js +25 -0
  61. package/templates/bases/next/app-router/js/src/app/loading.js +15 -0
  62. package/templates/bases/next/app-router/js/src/app/not-found.js +29 -0
  63. package/templates/bases/next/app-router/js/src/app/page.js +69 -0
  64. package/templates/bases/next/app-router/js/src/app/page.module.css +150 -0
  65. package/templates/bases/next/app-router/ts/.provenance.json +19 -0
  66. package/templates/bases/next/app-router/ts/README.md +36 -0
  67. package/templates/bases/next/app-router/ts/next.config.ts +7 -0
  68. package/templates/bases/next/app-router/ts/package.json +21 -0
  69. package/templates/bases/next/app-router/ts/public/file.svg +1 -0
  70. package/templates/bases/next/app-router/ts/public/globe.svg +1 -0
  71. package/templates/bases/next/app-router/ts/public/next.svg +1 -0
  72. package/templates/bases/next/app-router/ts/public/vercel.svg +1 -0
  73. package/templates/bases/next/app-router/ts/public/window.svg +1 -0
  74. package/templates/bases/next/app-router/ts/src/app/api/hello/route.ts +5 -0
  75. package/templates/bases/next/app-router/ts/src/app/blog/[slug]/page.tsx +38 -0
  76. package/templates/bases/next/app-router/ts/src/app/favicon.ico +0 -0
  77. package/templates/bases/next/app-router/ts/src/app/globals.css +49 -0
  78. package/templates/bases/next/app-router/ts/src/app/layout.tsx +30 -0
  79. package/templates/bases/next/app-router/ts/src/app/loading.tsx +15 -0
  80. package/templates/bases/next/app-router/ts/src/app/not-found.tsx +29 -0
  81. package/templates/bases/next/app-router/ts/src/app/page.module.css +150 -0
  82. package/templates/bases/next/app-router/ts/src/app/page.tsx +69 -0
  83. package/templates/bases/next/app-router/ts/tsconfig.json +34 -0
  84. package/templates/bases/next/pages-router/js/.provenance.json +19 -0
  85. package/templates/bases/next/pages-router/js/README.md +40 -0
  86. package/templates/bases/next/pages-router/js/jsconfig.json +7 -0
  87. package/templates/bases/next/pages-router/js/next.config.mjs +7 -0
  88. package/templates/bases/next/pages-router/js/package.json +15 -0
  89. package/templates/bases/next/pages-router/js/public/favicon.ico +0 -0
  90. package/templates/bases/next/pages-router/js/public/file.svg +1 -0
  91. package/templates/bases/next/pages-router/js/public/globe.svg +1 -0
  92. package/templates/bases/next/pages-router/js/public/next.svg +1 -0
  93. package/templates/bases/next/pages-router/js/public/vercel.svg +1 -0
  94. package/templates/bases/next/pages-router/js/public/window.svg +1 -0
  95. package/templates/bases/next/pages-router/js/src/pages/404.js +35 -0
  96. package/templates/bases/next/pages-router/js/src/pages/500.js +35 -0
  97. package/templates/bases/next/pages-router/js/src/pages/_app.js +20 -0
  98. package/templates/bases/next/pages-router/js/src/pages/_document.js +15 -0
  99. package/templates/bases/next/pages-router/js/src/pages/api/hello.js +5 -0
  100. package/templates/bases/next/pages-router/js/src/pages/blog/[slug].js +41 -0
  101. package/templates/bases/next/pages-router/js/src/pages/index.js +77 -0
  102. package/templates/bases/next/pages-router/js/src/styles/Home.module.css +149 -0
  103. package/templates/bases/next/pages-router/js/src/styles/globals.css +42 -0
  104. package/templates/bases/next/pages-router/ts/.provenance.json +19 -0
  105. package/templates/bases/next/pages-router/ts/README.md +40 -0
  106. package/templates/bases/next/pages-router/ts/next.config.ts +8 -0
  107. package/templates/bases/next/pages-router/ts/package.json +21 -0
  108. package/templates/bases/next/pages-router/ts/public/favicon.ico +0 -0
  109. package/templates/bases/next/pages-router/ts/public/file.svg +1 -0
  110. package/templates/bases/next/pages-router/ts/public/globe.svg +1 -0
  111. package/templates/bases/next/pages-router/ts/public/next.svg +1 -0
  112. package/templates/bases/next/pages-router/ts/public/vercel.svg +1 -0
  113. package/templates/bases/next/pages-router/ts/public/window.svg +1 -0
  114. package/templates/bases/next/pages-router/ts/src/pages/404.tsx +35 -0
  115. package/templates/bases/next/pages-router/ts/src/pages/500.tsx +35 -0
  116. package/templates/bases/next/pages-router/ts/src/pages/_app.tsx +21 -0
  117. package/templates/bases/next/pages-router/ts/src/pages/_document.tsx +15 -0
  118. package/templates/bases/next/pages-router/ts/src/pages/api/hello.ts +13 -0
  119. package/templates/bases/next/pages-router/ts/src/pages/blog/[slug].tsx +46 -0
  120. package/templates/bases/next/pages-router/ts/src/pages/index.tsx +77 -0
  121. package/templates/bases/next/pages-router/ts/src/styles/Home.module.css +149 -0
  122. package/templates/bases/next/pages-router/ts/src/styles/globals.css +42 -0
  123. package/templates/bases/next/pages-router/ts/tsconfig.json +29 -0
  124. package/templates/conditional/linting/biome/biome.json +31 -0
  125. package/templates/conditional/next/config/cloudflare/next.config.mjs +6 -0
  126. package/templates/conditional/next/config/cloudflare/next.config.ts +7 -0
  127. package/templates/conditional/next/config/node/next.config.mjs +6 -0
  128. package/templates/conditional/next/config/node/next.config.ts +7 -0
  129. package/templates/conditional/next/config/static/next.config.mjs +6 -0
  130. package/templates/conditional/next/config/static/next.config.ts +7 -0
  131. package/templates/conditional/next/config/turbopack/next.config.mjs +7 -0
  132. package/templates/conditional/next/config/turbopack/next.config.ts +8 -0
  133. package/templates/conditional/next/config/vercel/next.config.mjs +6 -0
  134. package/templates/conditional/next/config/vercel/next.config.ts +7 -0
  135. package/templates/conditional/next/config/webpack/next.config.mjs +9 -0
  136. package/templates/conditional/next/config/webpack/next.config.ts +10 -0
  137. package/templates/conditional/next/font/BUILD_SIZE_NOTE.md +17 -0
  138. package/templates/conditional/next/font/app-router/src/app/layout.js +25 -0
  139. package/templates/conditional/next/font/app-router/src/app/layout.tsx +30 -0
  140. package/templates/conditional/next/font/pages-router/src/pages/_app.js +20 -0
  141. package/templates/conditional/next/font/pages-router/src/pages/_app.tsx +21 -0
  142. package/templates/conditional/next/font/pages-router/src/pages/_document.js +18 -0
  143. package/templates/conditional/next/font/pages-router/src/pages/_document.tsx +18 -0
  144. package/templates/conditional/next/font/src/app/layout.js +25 -0
  145. package/templates/conditional/next/font/src/app/layout.tsx +30 -0
  146. package/templates/conditional/next/font/src/lib/fonts.js +11 -0
  147. package/templates/conditional/next/font/src/lib/fonts.ts +11 -0
  148. package/templates/conditional/next/font/src/pages/_app.js +20 -0
  149. package/templates/conditional/next/font/src/pages/_app.tsx +21 -0
  150. package/templates/conditional/next/font/src/pages/_document.js +18 -0
  151. package/templates/conditional/next/font/src/pages/_document.tsx +18 -0
  152. package/templates/conditional/next/image/BUILD_SIZE_NOTE.md +15 -0
  153. package/templates/conditional/next/image/app-router/src/components/ImageExample.jsx +34 -0
  154. package/templates/conditional/next/image/app-router/src/components/ImageExample.tsx +44 -0
  155. package/templates/conditional/next/image/pages-router/src/components/ImageExample.jsx +34 -0
  156. package/templates/conditional/next/image/pages-router/src/components/ImageExample.tsx +44 -0
  157. package/templates/conditional/next/image/src/components/ImageExample.jsx +44 -0
  158. package/templates/conditional/next/image/src/components/ImageExample.tsx +54 -0
  159. package/templates/conditional/next/image/src/components/image/index.js +1 -0
  160. package/templates/conditional/next/image/src/components/image/index.ts +2 -0
  161. package/templates/conditional/next/server-components/README.md +13 -0
  162. package/templates/conditional/next/server-components/eslint.config.mjs +47 -0
  163. package/templates/conditional/next/server-components/rules/no-server-imports-in-client.js +89 -0
  164. package/templates/conditional/next/server-components/rules/no-server-imports-in-client.mjs +88 -0
  165. package/templates/conditional/next/server-components/src/components/boundaries/ClientCounter.jsx +48 -0
  166. package/templates/conditional/next/server-components/src/components/boundaries/ClientCounter.tsx +53 -0
  167. package/templates/conditional/next/server-components/src/components/boundaries/ServerCard.jsx +41 -0
  168. package/templates/conditional/next/server-components/src/components/boundaries/ServerCard.tsx +46 -0
  169. package/templates/conditional/next/server-components/src/components/boundaries/index.js +2 -0
  170. package/templates/conditional/next/server-components/src/components/boundaries/index.ts +4 -0
  171. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +11 -0
  172. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +11 -0
  173. package/templates/conditional/testing/jest/none/src/__tests__/App.test.jsx +11 -0
  174. package/templates/conditional/testing/jest/none/src/__tests__/App.test.tsx +11 -0
  175. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +10 -0
  176. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +10 -0
  177. package/templates/conditional/testing/vitest/none/src/__tests__/App.test.jsx +10 -0
  178. package/templates/conditional/testing/vitest/none/src/__tests__/App.test.tsx +10 -0
  179. package/templates/css/tailwind/src/globals.css +10 -0
  180. package/templates/css/tailwind/src/themes.css +86 -4
  181. package/templates/ui/shadcn/lib/utils.js +6 -0
  182. package/templates/ui/shadcn/lib/utils.ts +6 -0
  183. package/templates/ui/shadcn/primitives/Button.jsx +48 -0
  184. package/templates/ui/shadcn/primitives/Button.tsx +59 -0
  185. package/templates/ui/shadcn/primitives/Card.jsx +55 -0
  186. package/templates/ui/shadcn/primitives/Card.tsx +73 -0
  187. package/templates/ui/shadcn/primitives/index.js +9 -0
  188. package/templates/ui/shadcn/primitives/index.ts +9 -0
  189. package/templates/css/bootstrap/src/themes.css +0 -14
  190. /package/templates/{css/bootstrap/src/globals.css → architectures/hybrid/src/features/.gitkeep} +0 -0
package/src/prompts.js CHANGED
@@ -1,26 +1,38 @@
1
- import { confirm, isCancel, note, select } from "@clack/prompts";
1
+ import { isCancel, note, select } from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { configExists, loadConfig, saveConfig } from "@/config-cache.js";
4
+ import {
5
+ isPromptVisible,
6
+ filterCompatibleChoices,
7
+ getDefaultResponses,
8
+ sanitizeResponsesForFramework,
9
+ normalizeFramework,
10
+ } from "./engine/capabilities.js";
11
+
12
+ export const BACK = Symbol("back");
13
+ export const CANCEL = Symbol("cancel");
4
14
 
5
15
  export function onCancel() {
6
16
  console.log(chalk.gray("\n\nOperation cancelled.\n"));
7
17
  process.exit(1);
8
18
  }
9
19
 
10
- function formatConfig(responses) {
20
+ export function formatConfig(responses) {
11
21
  const lang = responses.language === "ts" ? "TypeScript" : "JavaScript";
12
22
  const arch =
13
23
  responses.architecture === "feature-based"
14
24
  ? "Feature-based"
15
25
  : responses.architecture === "type-based" || responses.architecture === "component-based"
16
26
  ? "Type-based"
27
+ : responses.architecture === "hybrid"
28
+ ? "Hybrid (shared + features)"
29
+ : responses.architecture === "none"
30
+ ? "None (flat src)"
17
31
  : responses.architecture;
18
32
  const css =
19
33
  responses.cssFramework === "none"
20
34
  ? "None"
21
- : responses.cssFramework === "tailwind"
22
- ? "Tailwind CSS"
23
- : "Bootstrap";
35
+ : "Tailwind CSS";
24
36
  const testing =
25
37
  responses.testing === "none"
26
38
  ? "None"
@@ -35,274 +47,605 @@ function formatConfig(responses) {
35
47
  : "Zustand";
36
48
 
37
49
  const docsLang = responses.docsLanguage === "es" ? "ES" : responses.docsLanguage === "en" ? "EN" : "—";
38
- return [
50
+ const fwLabel =
51
+ responses.frameworkName === "next" && responses.frameworkVariant === "pages-router"
52
+ ? "Next.js (Pages Router)"
53
+ : responses.frameworkName === "next"
54
+ ? "Next.js (App Router)"
55
+ : "React + Vite";
56
+ const lines = [
57
+ `• ${chalk.bold("Framework:")} ${chalk.green(fwLabel)}`,
39
58
  `• ${chalk.bold("Architecture:")} ${chalk.green(arch)}`,
40
59
  `• ${chalk.bold("Language:")} ${chalk.green(lang)}`,
41
60
  `• ${chalk.bold("CSS:")} ${chalk.yellow(css)}`,
61
+ `• ${chalk.bold("UI Kit:")} ${responses.uiKit === "shadcn" ? chalk.cyan("shadcn/ui") : chalk.gray("None")}`,
42
62
  `• ${chalk.bold("Testing:")} ${chalk.magenta(testing)}`,
43
- `• ${chalk.bold("Router:")} ${responses.router ? chalk.green("Yes") : chalk.red("No")}`,
63
+ ];
64
+
65
+ if (responses.router !== undefined) {
66
+ lines.push(`• ${chalk.bold("Router:")} ${responses.router ? chalk.green("Yes") : chalk.red("No")}`);
67
+ }
68
+ if (responses.bundler) {
69
+ lines.push(`• ${chalk.bold("Bundler:")} ${chalk.cyan(responses.bundler)}`);
70
+ }
71
+ if (responses.adapter && responses.adapter !== "none") {
72
+ lines.push(`• ${chalk.bold("Adapter:")} ${chalk.cyan(responses.adapter)}`);
73
+ }
74
+ if (responses.reactCompiler !== undefined) {
75
+ lines.push(`• ${chalk.bold("React Compiler:")} ${responses.reactCompiler ? chalk.green("Yes") : chalk.red("No")}`);
76
+ }
77
+ lines.push(
44
78
  `• ${chalk.bold("State:")} ${chalk.blue(state)}`,
45
79
  `• ${chalk.bold("Icons:")} ${chalk.blue(responses.iconLibrary === "none" ? "None" : responses.iconLibrary === "lucide" ? "Lucide" : "Huge")}`,
46
80
  `• ${chalk.bold("API Client:")} ${responses.apiClient === "axios" ? chalk.green("Axios") : responses.apiClient === "fetch" ? chalk.yellow("Fetch") : chalk.red("None")}`,
47
- `• ${chalk.bold("Linter:")} ${responses.linter === "oxlint" ? chalk.green("Oxlint") : responses.linter === "eslint" ? chalk.yellow("ESLint") : chalk.red("None")}`,
81
+ `• ${chalk.bold("Linter:")} ${responses.linter === "oxlint" ? chalk.green("Oxlint") : responses.linter === "biome" ? chalk.cyan("Biome") : responses.linter === "eslint" ? chalk.yellow("ESLint") : chalk.red("None")}`,
48
82
  `• ${chalk.bold("Formatter:")} ${responses.formatter === "oxfmt" ? chalk.green("Oxfmt") : responses.formatter === "prettier" ? chalk.yellow("Prettier") : chalk.red("None")}`,
49
83
  `• ${chalk.bold("Docs:")} ${chalk.cyan(docsLang)}`,
84
+ `• ${chalk.bold("Agent Docs:")} ${responses.agentDocs ? chalk.green("Yes") : chalk.gray("No")}`,
50
85
  `• ${chalk.bold("Git:")} ${responses.gitInit ? chalk.green("Yes") : chalk.red("No")}`,
51
- `• ${chalk.bold("README:")} ${responses.readme ? chalk.green("Yes") : chalk.red("No")}`,
52
- ].join("\n");
86
+ `• ${chalk.bold("README:")} ${responses.readme ? chalk.green("Yes") : chalk.red("No")}`
87
+ );
88
+ return lines.join("\n");
53
89
  }
54
90
 
55
- export async function getUserInputs(projectName, { quickSetup = false } = {}) {
56
- let oldConfig;
57
- let useOldConfig;
58
91
 
59
- // Non-interactive quick setup: apply defaults without prompting.
60
- if (quickSetup) {
61
- const responses = {
62
- projectName,
63
- architecture: "feature-based",
64
- language: "ts",
65
- cssFramework: "tailwind",
66
- testing: "vitest",
67
- router: true,
68
- stateManagement: "none",
69
- iconLibrary: "none",
70
- apiClient: "none",
71
- linter: "eslint",
72
- formatter: "prettier",
73
- docsLanguage: "en",
74
- gitInit: true,
75
- readme: true,
76
- };
77
- await saveConfig(responses);
78
- return responses;
79
- }
92
+ /**
93
+ * Generic step-based runner with history stack and back navigation.
94
+ */
95
+ export async function runPromptFlow({
96
+ steps,
97
+ initialAnswers = {},
98
+ selectFn = select,
99
+ noteFn = note,
100
+ onCancelFn = onCancel,
101
+ } = {}) {
102
+ const answers = { ...initialAnswers };
103
+ const history = [];
104
+ let currentIndex = 0;
80
105
 
81
- if (await configExists()) {
82
- oldConfig = await loadConfig();
83
- oldConfig.projectName = projectName;
106
+ while (currentIndex < steps.length) {
107
+ const step = steps[currentIndex];
84
108
 
85
- note(
86
- `Previous setup found:\n\n${formatConfig(oldConfig)}\n`,
87
- "Previous Configuration"
88
- );
89
-
90
- const useExisting = await select({
91
- message: "How would you like to proceed?",
92
- options: [
93
- { value: true, label: "Continue with previous setup" },
94
- { value: false, label: "Start fresh (new setup)" },
95
- ],
96
- initialValue: true,
97
- });
98
-
99
- if (isCancel(useExisting)) onCancel();
100
- useOldConfig = useExisting;
101
-
102
- if (useOldConfig) {
103
- if (oldConfig.apiClient === undefined) {
104
- oldConfig.apiClient = oldConfig.axios ? "axios" : "none";
105
- }
106
- if (oldConfig.formatter === undefined) {
107
- oldConfig.formatter = "none";
108
- }
109
- if (oldConfig.docsLanguage === undefined) {
110
- oldConfig.docsLanguage = "en";
111
- }
112
- return { ...oldConfig, projectName };
109
+ // Check skip condition
110
+ if (step.skip?.(answers)) {
111
+ step.onSkip?.(answers);
112
+ currentIndex++;
113
+ continue;
113
114
  }
114
- }
115
115
 
116
- // Quick Setup
117
- const quickSetupChoice = await confirm({
118
- message:
119
- "Quick Setup? (TypeScript + Tailwind + Feature-based + Router + ESLint + Vitest)",
120
- initialValue: true,
121
- });
122
- if (isCancel(quickSetupChoice)) onCancel();
116
+ const canGoBack = history.length > 0;
117
+ let res;
123
118
 
124
- if (quickSetupChoice) {
125
- const responses = {
126
- projectName,
127
- architecture: "feature-based",
128
- language: "ts",
129
- cssFramework: "tailwind",
130
- testing: "vitest",
131
- router: true,
132
- stateManagement: "none",
133
- iconLibrary: "none",
134
- apiClient: "none",
135
- linter: "eslint",
136
- formatter: "prettier",
137
- docsLanguage: "en",
138
- gitInit: true,
139
- readme: true,
140
- };
141
- await saveConfig(responses);
142
- return responses;
143
- }
119
+ if (step.render) {
120
+ res = await step.render(answers, canGoBack, { selectFn, noteFn });
121
+ } else {
122
+ const stepOptions =
123
+ typeof step.getOptions === "function" ? step.getOptions(answers) : step.options;
124
+ const options = [
125
+ ...stepOptions,
126
+ ...(canGoBack
127
+ ? [{ label: chalk.dim("← Back"), value: BACK, hint: "Return to previous question" }]
128
+ : []),
129
+ ];
144
130
 
145
- // Custom setup
146
- const architecture = await select({
147
- message: "Which project architecture do you want?",
148
- options: [
149
- {
150
- label: "Feature-based",
151
- value: "feature-based",
152
- hint: "Scales to large apps — code grouped by business domain",
153
- },
154
- {
155
- label: "Type-based",
156
- value: "type-based",
157
- hint: "Small apps / component libraries — code grouped by UI type",
158
- },
159
- ],
160
- initialValue: "feature-based",
161
- });
162
- if (isCancel(architecture)) onCancel();
131
+ const prevValue = answers[step.id];
132
+ const hasPrevValue =
133
+ prevValue !== undefined && stepOptions.some((o) => o.value === prevValue);
134
+ const defaultInitial =
135
+ typeof step.defaultInitial === "function"
136
+ ? step.defaultInitial(answers)
137
+ : step.defaultInitial;
138
+ const initialValue = hasPrevValue ? prevValue : defaultInitial;
163
139
 
164
- const language = await select({
165
- message: "Which language do you want to use?",
166
- options: [
167
- { label: "TypeScript", value: "ts" },
168
- { label: "JavaScript", value: "js" },
169
- ],
170
- initialValue: "ts",
171
- });
172
- if (isCancel(language)) onCancel();
140
+ res = await selectFn({
141
+ message: step.message,
142
+ options,
143
+ initialValue,
144
+ });
145
+ }
173
146
 
174
- const cssFramework = await select({
175
- message: "Which CSS framework do you want to use?",
176
- options: [
177
- { label: "Tailwind CSS", value: "tailwind" },
178
- { label: "Bootstrap", value: "bootstrap" },
179
- { label: "None", value: "none" },
180
- ],
181
- initialValue: "tailwind",
182
- });
183
- if (isCancel(cssFramework)) onCancel();
147
+ if (isCancel(res) || res === CANCEL) {
148
+ onCancelFn();
149
+ return null;
150
+ }
184
151
 
185
- const testing = await select({
186
- message: "Which testing framework do you want to set up?",
187
- options: [
188
- { label: "Vitest", value: "vitest" },
189
- { label: "Jest", value: "jest" },
190
- { label: "None", value: "none" },
191
- ],
192
- initialValue: "vitest",
193
- });
194
- if (isCancel(testing)) onCancel();
152
+ if (res === BACK) {
153
+ if (history.length > 0) {
154
+ currentIndex = history.pop();
155
+ }
156
+ continue;
157
+ }
195
158
 
196
- const router = await confirm({
197
- message: "Would you like to install React Router?",
198
- initialValue: true,
199
- });
200
- if (isCancel(router)) onCancel();
159
+ // Step produced a normal answer
160
+ if (step.onAnswer) {
161
+ const nextIndex = step.onAnswer(res, answers, { history, currentIndex, steps });
162
+ if (typeof nextIndex === "number") {
163
+ history.push(currentIndex);
164
+ currentIndex = nextIndex;
165
+ continue;
166
+ }
167
+ } else if (step.id) {
168
+ answers[step.id] = res;
169
+ }
201
170
 
202
- const stateManagement = await select({
203
- message: "Which state management library do you want to use?",
204
- options: [
205
- { label: "None", value: "none" },
206
- { label: "Redux Toolkit", value: "redux" },
207
- { label: "Zustand", value: "zustand" },
208
- ],
209
- initialValue: "none",
210
- });
211
- if (isCancel(stateManagement)) onCancel();
171
+ history.push(currentIndex);
172
+ currentIndex++;
173
+ }
212
174
 
213
- const iconLibrary = await select({
214
- message: "Which icon library would you like to use?",
215
- options: [
216
- { label: "None", value: "none" },
217
- { label: "Lucide Icons", value: "lucide" },
218
- { label: "Huge Icons", value: "huge" },
219
- ],
220
- initialValue: "none",
221
- });
222
- if (isCancel(iconLibrary)) onCancel();
175
+ return answers;
176
+ }
223
177
 
224
- const apiClient = await select({
225
- message: "Which API client do you want to use?",
226
- options: [
227
- { label: "None", value: "none" },
228
- { label: "Axios", value: "axios" },
229
- { label: "Fetch (native)", value: "fetch" },
230
- ],
231
- initialValue: "none",
232
- });
233
- if (isCancel(apiClient)) onCancel();
178
+ export async function getUserInputs(
179
+ projectName,
180
+ {
181
+ quickSetup = false,
182
+ framework: rawFramework,
183
+ initialArch = null,
184
+ selectFn = select,
185
+ noteFn = note,
186
+ saveConfigFn = saveConfig,
187
+ configExistsFn = configExists,
188
+ loadConfigFn = loadConfig,
189
+ } = {}
190
+ ) {
191
+ let framework = normalizeFramework(rawFramework);
192
+ // When no framework flag is given, ask interactively as the first step so all
193
+ // downstream prompts gate on the user's selection instead of the default.
194
+ const frameworkLocked =
195
+ rawFramework !== undefined && rawFramework !== null && String(rawFramework).trim() !== "";
196
+ // Variant is locked only when the flag names it explicitly (next:app-router).
197
+ // A bare `-f next` still asks App vs Pages interactively.
198
+ const variantLocked = frameworkLocked && String(rawFramework).includes(":");
199
+ // Non-interactive quick setup: capability-aware defaults without prompting.
200
+ if (quickSetup) {
201
+ const responses = getDefaultResponses(framework, { projectName });
202
+ if (initialArch) responses.architecture = initialArch;
203
+ await saveConfigFn(sanitizeResponsesForFramework(framework, responses));
204
+ return sanitizeResponsesForFramework(framework, responses);
205
+ }
234
206
 
235
- const gitInit = await confirm({
236
- message: "Would you like to initialize a Git repository?",
237
- initialValue: false,
238
- });
239
- if (isCancel(gitInit)) onCancel();
207
+ let oldConfig = null;
208
+ const hasPrevious = await configExistsFn();
209
+ if (hasPrevious) {
210
+ oldConfig = await loadConfigFn();
211
+ oldConfig.projectName = projectName;
212
+ // Sanitize cached setup against current framework capabilities
213
+ oldConfig = sanitizeResponsesForFramework(framework, oldConfig);
214
+ }
240
215
 
241
- const linter = await select({
242
- message: "Which linter do you want to use?",
243
- options: [
244
- { label: "ESLint", value: "eslint", hint: "Industry standard JavaScript linter (Recommended)" },
245
- { label: "Oxlint", value: "oxlint", hint: "Fast Rust-based linter" },
246
- { label: "None", value: "none" },
247
- ],
248
- initialValue: "eslint",
249
- });
250
- if (isCancel(linter)) onCancel();
216
+ const steps = [];
251
217
 
252
- let formatter = "none";
253
- if (linter !== "none") {
254
- const formatterOptions =
255
- linter === "eslint"
256
- ? [
257
- { label: "None", value: "none" },
258
- { label: "Prettier", value: "prettier" },
259
- ]
260
- : [
261
- { label: "None", value: "none" },
262
- { label: "Oxfmt (Recommended)", value: "oxfmt" },
263
- { label: "Prettier", value: "prettier" },
264
- ];
265
- formatter = await select({
266
- message: "Which formatter do you want to use?",
267
- options: formatterOptions,
268
- initialValue: linter === "eslint" ? "prettier" : "oxfmt",
218
+ if (hasPrevious) {
219
+ steps.push({
220
+ id: "useExisting",
221
+ render: async (_ans, _canGoBack, { selectFn: sFn, noteFn: nFn }) => {
222
+ nFn(
223
+ `Previous setup found:\n\n${formatConfig(oldConfig)}\n`,
224
+ "Previous Configuration"
225
+ );
226
+ return await sFn({
227
+ message: "How would you like to proceed?",
228
+ options: [
229
+ { value: true, label: "Continue with previous setup" },
230
+ { value: false, label: "Start fresh (new setup)" },
231
+ ],
232
+ initialValue: true,
233
+ });
234
+ },
235
+ onAnswer: (val, ans) => {
236
+ ans.useExisting = val;
237
+ if (val === true) {
238
+ if (oldConfig.apiClient === undefined) {
239
+ oldConfig.apiClient = oldConfig.axios ? "axios" : "none";
240
+ }
241
+ if (oldConfig.formatter === undefined) {
242
+ oldConfig.formatter = "none";
243
+ }
244
+ if (oldConfig.docsLanguage === undefined) {
245
+ oldConfig.docsLanguage = "en";
246
+ }
247
+ return steps.length; // exit immediately
248
+ }
249
+ return null;
250
+ },
269
251
  });
270
- if (isCancel(formatter)) onCancel();
271
252
  }
272
253
 
273
- const docsLanguage = await select({
274
- message: "Which docs language do you want?",
275
- options: [
276
- { label: "English", value: "en" },
277
- { label: "Español", value: "es" },
278
- ],
279
- initialValue: "en",
280
- });
281
- if (isCancel(docsLanguage)) onCancel();
254
+ steps.push(
255
+ {
256
+ id: "framework",
257
+ skip: () => frameworkLocked,
258
+ onSkip: (ans) => {
259
+ ans.frameworkName = framework.name;
260
+ ans.frameworkVariant = framework.variant;
261
+ },
262
+ message: "Which framework do you want to scaffold?",
263
+ getOptions: () => [
264
+ {
265
+ label: "React + Vite",
266
+ value: "react:vite",
267
+ hint: "Client-side SPA with React Router",
268
+ },
269
+ {
270
+ label: "Next.js",
271
+ value: "next",
272
+ hint: "Filesystem routing, server components, route handlers",
273
+ },
274
+ ],
275
+ defaultInitial: "react:vite",
276
+ onAnswer: (val, ans, { steps: allSteps }) => {
277
+ const [name, ...rest] = String(val).split(":");
278
+ framework = normalizeFramework({ name, variant: rest.join(":") });
279
+ ans.frameworkName = framework.name;
280
+ ans.frameworkVariant = framework.variant;
281
+ const qs = allSteps.find((s) => s.id === "quickSetup");
282
+ if (qs) {
283
+ qs.message = isPromptVisible(framework, "router")
284
+ ? "Quick Setup? (TypeScript + Tailwind + Feature-based + Router + ESLint + Vitest)"
285
+ : "Quick Setup? (TypeScript + Tailwind + Feature-based + ESLint + Vitest)";
286
+ }
287
+ return null;
288
+ },
289
+ },
290
+ {
291
+ id: "nextRouter",
292
+ skip: () => framework.name !== "next" || variantLocked,
293
+ onSkip: (ans) => {
294
+ ans.frameworkVariant = framework.variant;
295
+ },
296
+ message: "Which Next.js router do you want to use?",
297
+ getOptions: () => [
298
+ {
299
+ label: "App Router",
300
+ value: "app-router",
301
+ hint: "Recommended: layouts, server components, route handlers",
302
+ },
303
+ {
304
+ label: "Pages Router",
305
+ value: "pages-router",
306
+ hint: "Classic pages directory with API routes",
307
+ },
308
+ ],
309
+ defaultInitial: "app-router",
310
+ onAnswer: (val, ans) => {
311
+ framework = normalizeFramework({ name: "next", variant: String(val) });
312
+ ans.frameworkName = framework.name;
313
+ ans.frameworkVariant = framework.variant;
314
+ return null;
315
+ },
316
+ },
317
+ {
318
+ id: "quickSetup",
319
+ message: isPromptVisible(framework, "router")
320
+ ? "Quick Setup? (TypeScript + Tailwind + Feature-based + Router + ESLint + Vitest)"
321
+ : "Quick Setup? (TypeScript + Tailwind + Feature-based + ESLint + Vitest)",
322
+ getOptions: () => [
323
+ { label: "Yes, use recommended defaults", value: true },
324
+ { label: "No, custom setup", value: false },
325
+ ],
326
+ defaultInitial: true,
327
+ onAnswer: (val, ans, { steps: allSteps }) => {
328
+ ans.quickSetup = val;
329
+ if (val === true) {
330
+ const defaults = getDefaultResponses(framework, { projectName });
331
+ Object.assign(ans, defaults, { projectName, quickSetup: true });
332
+ if (initialArch) ans.architecture = initialArch;
333
+ // Jump to finalConfirm
334
+ return allSteps.length - 1;
335
+ }
336
+ return null;
337
+ },
338
+ },
339
+ {
340
+ id: "architecture",
341
+ message: "Which project architecture do you want?",
342
+ getOptions: () =>
343
+ filterCompatibleChoices(framework, "architecture", [
344
+ {
345
+ label: "Feature-based",
346
+ value: "feature-based",
347
+ hint: "Scales to large apps — code grouped by business domain",
348
+ },
349
+ {
350
+ label: "Type-based",
351
+ value: "type-based",
352
+ hint: "Small apps / component libraries — code grouped by UI type",
353
+ },
354
+ {
355
+ label: "Hybrid",
356
+ value: "hybrid",
357
+ hint: "Next.js: features + shared code",
358
+ },
359
+ {
360
+ label: "None",
361
+ value: "none",
362
+ hint: "Minimal flat src/ scaffold — no imposed structure",
363
+ },
364
+ ]),
365
+ defaultInitial: initialArch || "feature-based",
366
+ },
367
+ {
368
+ id: "language",
369
+ message: "Which language do you want to use?",
370
+ getOptions: () =>
371
+ filterCompatibleChoices(framework, "language", [
372
+ { label: "TypeScript", value: "ts" },
373
+ { label: "JavaScript", value: "js" },
374
+ ]),
375
+ defaultInitial: "ts",
376
+ },
377
+ {
378
+ id: "cssFramework",
379
+ message: "Which CSS framework do you want to use?",
380
+ getOptions: () =>
381
+ filterCompatibleChoices(framework, "styling", [
382
+ { label: "Tailwind CSS", value: "tailwind" },
383
+ { label: "None", value: "none" },
384
+ ]),
385
+ defaultInitial: "tailwind",
386
+ },
387
+ {
388
+ id: "uiKit",
389
+ skip: (ans) => ans.cssFramework !== "tailwind",
390
+ onSkip: (ans) => {
391
+ ans.uiKit = "none";
392
+ },
393
+ message: "Which UI kit do you want to use?",
394
+ getOptions: () =>
395
+ filterCompatibleChoices(framework, "uiKit", [
396
+ { label: "None", value: "none" },
397
+ {
398
+ label: "shadcn/ui",
399
+ value: "shadcn",
400
+ hint: "Requires Tailwind CSS — primitives land in paths.ui",
401
+ },
402
+ ]),
403
+ defaultInitial: "none",
404
+ },
405
+ {
406
+ id: "testing",
407
+ message: "Which testing framework do you want to set up?",
408
+ getOptions: () => [
409
+ { label: "Vitest", value: "vitest" },
410
+ { label: "Jest", value: "jest" },
411
+ { label: "None", value: "none" },
412
+ ],
413
+ defaultInitial: "vitest",
414
+ },
415
+ {
416
+ id: "router",
417
+ skip: () => !isPromptVisible(framework, "router"),
418
+ onSkip: (ans) => {
419
+ ans.router = false;
420
+ },
421
+ message: "Would you like to install React Router?",
422
+ getOptions: () => [
423
+ { label: "Yes", value: true },
424
+ { label: "No", value: false },
425
+ ],
426
+ defaultInitial: true,
427
+ },
428
+ {
429
+ id: "bundler",
430
+ skip: () => !isPromptVisible(framework, "bundler"),
431
+ onSkip: (ans) => {
432
+ ans.bundler = undefined;
433
+ },
434
+ message: "Which bundler do you want to use?",
435
+ getOptions: () =>
436
+ filterCompatibleChoices(framework, "bundler", [
437
+ { label: "Turbopack", value: "turbopack" },
438
+ { label: "Webpack", value: "webpack" },
439
+ ]),
440
+ defaultInitial: "turbopack",
441
+ },
442
+ {
443
+ id: "adapter",
444
+ skip: () => !isPromptVisible(framework, "adapter"),
445
+ onSkip: (ans) => {
446
+ ans.adapter = "none";
447
+ },
448
+ message: "Which deployment adapter do you want to use?",
449
+ getOptions: () =>
450
+ filterCompatibleChoices(framework, "adapter", [
451
+ { label: "None (default)", value: "none" },
452
+ { label: "Node", value: "node" },
453
+ { label: "Vercel", value: "vercel" },
454
+ { label: "Cloudflare", value: "cloudflare" },
455
+ { label: "Static", value: "static" },
456
+ ]),
457
+ defaultInitial: "none",
458
+ },
459
+ {
460
+ id: "reactCompiler",
461
+ skip: () => !isPromptVisible(framework, "reactCompiler"),
462
+ onSkip: (ans) => {
463
+ ans.reactCompiler = false;
464
+ },
465
+ message: "Would you like to enable the React Compiler (experimental)?",
466
+ getOptions: () => [
467
+ { label: "No (default)", value: false },
468
+ { label: "Yes", value: true },
469
+ ],
470
+ defaultInitial: false,
471
+ },
472
+ {
473
+ id: "stateManagement",
474
+ message: "Which state management library do you want to use?",
475
+ getOptions: () => [
476
+ { label: "None", value: "none" },
477
+ { label: "Redux Toolkit", value: "redux" },
478
+ { label: "Zustand", value: "zustand" },
479
+ ],
480
+ defaultInitial: "none",
481
+ },
482
+ {
483
+ id: "iconLibrary",
484
+ message: "Which icon library would you like to use?",
485
+ getOptions: () => [
486
+ { label: "None", value: "none" },
487
+ { label: "Lucide Icons", value: "lucide" },
488
+ { label: "Huge Icons", value: "huge" },
489
+ ],
490
+ defaultInitial: "none",
491
+ },
492
+ {
493
+ id: "apiClient",
494
+ message: "Which API client do you want to use?",
495
+ getOptions: () => [
496
+ { label: "None", value: "none" },
497
+ { label: "Axios", value: "axios" },
498
+ { label: "Fetch (native)", value: "fetch" },
499
+ ],
500
+ defaultInitial: "none",
501
+ },
502
+ {
503
+ id: "gitInit",
504
+ message: "Would you like to initialize a Git repository?",
505
+ getOptions: () => [
506
+ { label: "Yes", value: true },
507
+ { label: "No", value: false },
508
+ ],
509
+ defaultInitial: false,
510
+ },
511
+ {
512
+ id: "linter",
513
+ message: "Which linter do you want to use?",
514
+ getOptions: () =>
515
+ filterCompatibleChoices(framework, "linter", [
516
+ {
517
+ label: "ESLint",
518
+ value: "eslint",
519
+ hint: "Industry standard JavaScript linter (Recommended)",
520
+ },
521
+ { label: "Oxlint", value: "oxlint", hint: "Fast Rust-based linter" },
522
+ { label: "Biome", value: "biome", hint: "Toolchain for web projects" },
523
+ { label: "None", value: "none" },
524
+ ]),
525
+ defaultInitial: "eslint",
526
+ },
527
+ {
528
+ id: "formatter",
529
+ skip: (ans) => ans.linter === "none",
530
+ onSkip: (ans) => {
531
+ ans.formatter = "none";
532
+ },
533
+ message: "Which formatter do you want to use?",
534
+ getOptions: (ans) =>
535
+ filterCompatibleChoices(
536
+ framework,
537
+ "formatter",
538
+ ans.linter === "eslint"
539
+ ? [
540
+ { label: "None", value: "none" },
541
+ { label: "Prettier", value: "prettier" },
542
+ ]
543
+ : [
544
+ { label: "None", value: "none" },
545
+ { label: "Oxfmt (Recommended)", value: "oxfmt" },
546
+ { label: "Prettier", value: "prettier" },
547
+ ]
548
+ ),
549
+ defaultInitial: (ans) => {
550
+ const compatible = filterCompatibleChoices(
551
+ framework,
552
+ "formatter",
553
+ ans.linter === "eslint"
554
+ ? [{ value: "none" }, { value: "prettier" }]
555
+ : [{ value: "none" }, { value: "oxfmt" }, { value: "prettier" }]
556
+ ).map((o) => o.value);
557
+ const preferred = ans.linter === "eslint" ? "prettier" : "oxfmt";
558
+ return compatible.includes(preferred) ? preferred : compatible[0];
559
+ },
560
+ },
561
+ {
562
+ id: "agentDocs",
563
+ message: "Generate .lumen/ agent context for AI assistants?",
564
+ getOptions: () => [
565
+ { label: "No (default)", value: false },
566
+ { label: "Yes", value: true, hint: "Emits .lumen/ with project, architecture and conventions" },
567
+ ],
568
+ defaultInitial: false,
569
+ },
570
+ {
571
+ id: "docsLanguage",
572
+ message: "Which docs language do you want?",
573
+ getOptions: () => [
574
+ { label: "English", value: "en" },
575
+ { label: "Español", value: "es" },
576
+ ],
577
+ defaultInitial: "en",
578
+ },
579
+ {
580
+ id: "readme",
581
+ message: "Would you like to generate a README.md and LICENSE?",
582
+ getOptions: () => [
583
+ { label: "Yes", value: true },
584
+ { label: "No", value: false },
585
+ ],
586
+ defaultInitial: true,
587
+ },
588
+ {
589
+ id: "finalConfirm",
590
+ render: async (ans, canGoBack, { selectFn: sFn, noteFn: nFn }) => {
591
+ nFn(`\n${formatConfig(ans)}\n`, "Project Configuration Summary");
592
+ return await sFn({
593
+ message: "Proceed with project generation?",
594
+ options: [
595
+ { label: "Confirm and scaffold project", value: true },
596
+ ...(canGoBack
597
+ ? [{ label: chalk.dim("← Back to edit options"), value: BACK }]
598
+ : []),
599
+ ],
600
+ initialValue: true,
601
+ });
602
+ },
603
+ }
604
+ );
282
605
 
283
- const readme = await confirm({
284
- message: "Would you like to generate a README.md and LICENSE?",
285
- initialValue: true,
606
+ const responses = await runPromptFlow({
607
+ steps,
608
+ initialAnswers: { projectName },
609
+ selectFn,
610
+ noteFn,
611
+ onCancelFn: onCancel,
286
612
  });
287
- if (isCancel(readme)) onCancel();
288
613
 
289
- const responses = {
614
+ if (responses.useExisting) {
615
+ return { ...oldConfig, projectName };
616
+ }
617
+
618
+ const {
619
+ useExisting: _useExisting,
620
+ quickSetup: _quickSetup,
621
+ bundler,
622
+ adapter,
623
+ reactCompiler,
624
+ agentDocs,
625
+ ...picked
626
+ } = responses;
627
+ // Final safety net: BACK-navigation can reselect the framework after other
628
+ // answers were collected, so re-validate everything before persisting.
629
+ const finalResponses = sanitizeResponsesForFramework(framework, {
290
630
  projectName,
291
- architecture,
292
- language,
293
- cssFramework,
294
- testing,
295
- router,
296
- stateManagement,
297
- iconLibrary,
298
- apiClient,
299
- linter,
300
- formatter,
301
- docsLanguage,
302
- gitInit,
303
- readme,
304
- };
631
+ frameworkName: framework.name,
632
+ frameworkVariant: framework.variant,
633
+ ...picked,
634
+ });
635
+ if (bundler !== undefined) {
636
+ finalResponses.bundler = bundler;
637
+ }
638
+ if (adapter !== undefined && adapter !== "none") {
639
+ finalResponses.adapter = adapter;
640
+ }
641
+ if (reactCompiler !== undefined) {
642
+ finalResponses.reactCompiler = reactCompiler;
643
+ }
644
+ if (agentDocs !== undefined) {
645
+ finalResponses.agentDocs = agentDocs;
646
+ }
305
647
 
306
- await saveConfig(responses);
307
- return responses;
648
+ // No config cache write until final confirm!
649
+ await saveConfigFn(finalResponses);
650
+ return finalResponses;
308
651
  }