create-lumen 2.0.0-alpha.0 → 2.0.0-alpha.2

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 (74) hide show
  1. package/README.md +35 -12
  2. package/bin/cli.js +1 -0
  3. package/package.json +3 -2
  4. package/schema/fixtures/v2/invalid/architecture-preset-type-mismatch.json +35 -0
  5. package/schema/fixtures/v2/invalid/harness-empty-commands.json +37 -0
  6. package/schema/fixtures/v2/invalid/harness-missing-command.json +41 -0
  7. package/schema/fixtures/v2/invalid/invalid-validation-mode.json +34 -0
  8. package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +1 -1
  9. package/schema/fixtures/v2/valid/harness-full.json +44 -0
  10. package/schema/fixtures/v2/valid/react-vite-type-based.json +1 -1
  11. package/schema/fixtures/v2/valid/validation-none.json +41 -0
  12. package/schema/fixtures/v2/valid/validation-relaxed.json +41 -0
  13. package/schema/lumen.config.v2.json +54 -2
  14. package/skills/create-lumen/SKILL.md +110 -0
  15. package/skills/create-lumen/references/manifest-v2.md +9 -0
  16. package/src/agent-context.js +114 -0
  17. package/src/cli-args.js +65 -3
  18. package/src/configure.js +4 -0
  19. package/src/css.js +14 -30
  20. package/src/dependencies.js +28 -18
  21. package/src/engine/capabilities.js +174 -4
  22. package/src/engine/composition.js +644 -0
  23. package/src/engine/index.js +1 -0
  24. package/src/injector.js +294 -446
  25. package/src/main.js +20 -7
  26. package/src/manifest/emit.js +49 -3
  27. package/src/manifest/schema.js +65 -7
  28. package/src/prompts.js +459 -247
  29. package/src/readme.js +23 -2
  30. package/src/scaffold.js +17 -0
  31. package/templates/architectures/hybrid/main.jsx +10 -0
  32. package/templates/architectures/hybrid/main.tsx +10 -0
  33. package/templates/architectures/hybrid/src/App.jsx +20 -0
  34. package/templates/architectures/hybrid/src/App.tsx +20 -0
  35. package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +31 -0
  36. package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +36 -0
  37. package/templates/architectures/hybrid/src/shared/components/ui/index.js +1 -0
  38. package/templates/architectures/hybrid/src/shared/components/ui/index.ts +1 -0
  39. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +28 -0
  40. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +28 -0
  41. package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
  42. package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
  43. package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
  44. package/templates/architectures/hybrid/src/shared/types/index.js +20 -0
  45. package/templates/architectures/hybrid/src/shared/types/index.ts +16 -0
  46. package/templates/architectures/hybrid/src/shared/utils/cn.jsx +3 -0
  47. package/templates/architectures/hybrid/src/shared/utils/cn.ts +5 -0
  48. package/templates/architectures/none/main.jsx +10 -0
  49. package/templates/architectures/none/main.tsx +10 -0
  50. package/templates/architectures/none/src/App.jsx +15 -0
  51. package/templates/architectures/none/src/App.tsx +15 -0
  52. package/templates/architectures/none/src/styles/.gitkeep +1 -0
  53. package/templates/architectures/none/src/types/index.js +20 -0
  54. package/templates/architectures/none/src/types/index.ts +16 -0
  55. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +11 -0
  56. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +11 -0
  57. package/templates/conditional/testing/jest/none/src/__tests__/App.test.jsx +11 -0
  58. package/templates/conditional/testing/jest/none/src/__tests__/App.test.tsx +11 -0
  59. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +10 -0
  60. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +10 -0
  61. package/templates/conditional/testing/vitest/none/src/__tests__/App.test.jsx +10 -0
  62. package/templates/conditional/testing/vitest/none/src/__tests__/App.test.tsx +10 -0
  63. package/templates/css/tailwind/src/globals.css +10 -0
  64. package/templates/css/tailwind/src/themes.css +86 -4
  65. package/templates/ui/shadcn/lib/utils.js +6 -0
  66. package/templates/ui/shadcn/lib/utils.ts +6 -0
  67. package/templates/ui/shadcn/primitives/Button.jsx +48 -0
  68. package/templates/ui/shadcn/primitives/Button.tsx +59 -0
  69. package/templates/ui/shadcn/primitives/Card.jsx +55 -0
  70. package/templates/ui/shadcn/primitives/Card.tsx +73 -0
  71. package/templates/ui/shadcn/primitives/index.js +9 -0
  72. package/templates/ui/shadcn/primitives/index.ts +9 -0
  73. package/templates/css/bootstrap/src/themes.css +0 -14
  74. /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"
@@ -34,275 +46,475 @@ function formatConfig(responses) {
34
46
  ? "Redux Toolkit"
35
47
  : "Zustand";
36
48
 
37
- const docsLang = responses.docsLanguage === "es" ? "ES" : responses.docsLanguage === "en" ? "EN" : "—";
38
- return [
39
- `• ${chalk.bold("Architecture:")} ${chalk.green(arch)}`,
40
- `• ${chalk.bold("Language:")} ${chalk.green(lang)}`,
41
- `• ${chalk.bold("CSS:")} ${chalk.yellow(css)}`,
42
- `• ${chalk.bold("Testing:")} ${chalk.magenta(testing)}`,
43
- `• ${chalk.bold("Router:")} ${responses.router ? chalk.green("Yes") : chalk.red("No")}`,
44
- `• ${chalk.bold("State:")} ${chalk.blue(state)}`,
45
- `• ${chalk.bold("Icons:")} ${chalk.blue(responses.iconLibrary === "none" ? "None" : responses.iconLibrary === "lucide" ? "Lucide" : "Huge")}`,
46
- `• ${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")}`,
48
- `• ${chalk.bold("Formatter:")} ${responses.formatter === "oxfmt" ? chalk.green("Oxfmt") : responses.formatter === "prettier" ? chalk.yellow("Prettier") : chalk.red("None")}`,
49
- `• ${chalk.bold("Docs:")} ${chalk.cyan(docsLang)}`,
50
- `• ${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");
53
- }
54
-
55
- export async function getUserInputs(projectName, { quickSetup = false } = {}) {
56
- let oldConfig;
57
- let useOldConfig;
49
+ const docsLang = responses.docsLanguage === "es" ? "ES" : responses.docsLanguage === "en" ? "EN" : "—";
50
+ const lines = [
51
+ `• ${chalk.bold("Architecture:")} ${chalk.green(arch)}`,
52
+ `• ${chalk.bold("Language:")} ${chalk.green(lang)}`,
53
+ `• ${chalk.bold("CSS:")} ${chalk.yellow(css)}`,
54
+ `• ${chalk.bold("Testing:")} ${chalk.magenta(testing)}`,
55
+ ];
58
56
 
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;
57
+ if (responses.router !== undefined) {
58
+ lines.push(`• ${chalk.bold("Router:")} ${responses.router ? chalk.green("Yes") : chalk.red("No")}`);
79
59
  }
60
+ if (responses.bundler) {
61
+ lines.push(`• ${chalk.bold("Bundler:")} ${chalk.cyan(responses.bundler)}`);
62
+ }
63
+ if (responses.adapter && responses.adapter !== "none") {
64
+ lines.push(`• ${chalk.bold("Adapter:")} ${chalk.cyan(responses.adapter)}`);
65
+ }
66
+ lines.push(
67
+ `• ${chalk.bold("State:")} ${chalk.blue(state)}`,
68
+ `• ${chalk.bold("Icons:")} ${chalk.blue(responses.iconLibrary === "none" ? "None" : responses.iconLibrary === "lucide" ? "Lucide" : "Huge")}`,
69
+ `• ${chalk.bold("API Client:")} ${responses.apiClient === "axios" ? chalk.green("Axios") : responses.apiClient === "fetch" ? chalk.yellow("Fetch") : chalk.red("None")}`,
70
+ `• ${chalk.bold("Linter:")} ${responses.linter === "oxlint" ? chalk.green("Oxlint") : responses.linter === "biome" ? chalk.cyan("Biome") : responses.linter === "eslint" ? chalk.yellow("ESLint") : chalk.red("None")}`,
71
+ `• ${chalk.bold("Formatter:")} ${responses.formatter === "oxfmt" ? chalk.green("Oxfmt") : responses.formatter === "prettier" ? chalk.yellow("Prettier") : chalk.red("None")}`,
72
+ `• ${chalk.bold("Docs:")} ${chalk.cyan(docsLang)}`,
73
+ `• ${chalk.bold("Git:")} ${responses.gitInit ? chalk.green("Yes") : chalk.red("No")}`,
74
+ `• ${chalk.bold("README:")} ${responses.readme ? chalk.green("Yes") : chalk.red("No")}`
75
+ );
76
+ return lines.join("\n");
77
+ }
80
78
 
81
- if (await configExists()) {
82
- oldConfig = await loadConfig();
83
- oldConfig.projectName = projectName;
84
-
85
- note(
86
- `Previous setup found:\n\n${formatConfig(oldConfig)}\n`,
87
- "Previous Configuration"
88
- );
89
79
 
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
- });
80
+ /**
81
+ * Generic step-based runner with history stack and back navigation.
82
+ */
83
+ export async function runPromptFlow({
84
+ steps,
85
+ initialAnswers = {},
86
+ selectFn = select,
87
+ noteFn = note,
88
+ onCancelFn = onCancel,
89
+ } = {}) {
90
+ const answers = { ...initialAnswers };
91
+ const history = [];
92
+ let currentIndex = 0;
98
93
 
99
- if (isCancel(useExisting)) onCancel();
100
- useOldConfig = useExisting;
94
+ while (currentIndex < steps.length) {
95
+ const step = steps[currentIndex];
101
96
 
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 };
97
+ // Check skip condition
98
+ if (step.skip?.(answers)) {
99
+ step.onSkip?.(answers);
100
+ currentIndex++;
101
+ continue;
113
102
  }
114
- }
115
103
 
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();
104
+ const canGoBack = history.length > 0;
105
+ let res;
123
106
 
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
- }
107
+ if (step.render) {
108
+ res = await step.render(answers, canGoBack, { selectFn, noteFn });
109
+ } else {
110
+ const stepOptions =
111
+ typeof step.getOptions === "function" ? step.getOptions(answers) : step.options;
112
+ const options = [
113
+ ...stepOptions,
114
+ ...(canGoBack
115
+ ? [{ label: chalk.dim(" Back"), value: BACK, hint: "Return to previous question" }]
116
+ : []),
117
+ ];
144
118
 
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();
119
+ const prevValue = answers[step.id];
120
+ const hasPrevValue =
121
+ prevValue !== undefined && stepOptions.some((o) => o.value === prevValue);
122
+ const defaultInitial =
123
+ typeof step.defaultInitial === "function"
124
+ ? step.defaultInitial(answers)
125
+ : step.defaultInitial;
126
+ const initialValue = hasPrevValue ? prevValue : defaultInitial;
163
127
 
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();
128
+ res = await selectFn({
129
+ message: step.message,
130
+ options,
131
+ initialValue,
132
+ });
133
+ }
173
134
 
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();
135
+ if (isCancel(res) || res === CANCEL) {
136
+ onCancelFn();
137
+ return null;
138
+ }
184
139
 
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();
140
+ if (res === BACK) {
141
+ if (history.length > 0) {
142
+ currentIndex = history.pop();
143
+ }
144
+ continue;
145
+ }
195
146
 
196
- const router = await confirm({
197
- message: "Would you like to install React Router?",
198
- initialValue: true,
199
- });
200
- if (isCancel(router)) onCancel();
147
+ // Step produced a normal answer
148
+ if (step.onAnswer) {
149
+ const nextIndex = step.onAnswer(res, answers, { history, currentIndex, steps });
150
+ if (typeof nextIndex === "number") {
151
+ history.push(currentIndex);
152
+ currentIndex = nextIndex;
153
+ continue;
154
+ }
155
+ } else if (step.id) {
156
+ answers[step.id] = res;
157
+ }
201
158
 
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();
159
+ history.push(currentIndex);
160
+ currentIndex++;
161
+ }
212
162
 
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();
163
+ return answers;
164
+ }
223
165
 
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();
166
+ export async function getUserInputs(
167
+ projectName,
168
+ {
169
+ quickSetup = false,
170
+ framework: rawFramework,
171
+ initialArch = null,
172
+ selectFn = select,
173
+ noteFn = note,
174
+ saveConfigFn = saveConfig,
175
+ configExistsFn = configExists,
176
+ loadConfigFn = loadConfig,
177
+ } = {}
178
+ ) {
179
+ const framework = normalizeFramework(rawFramework);
180
+ // Non-interactive quick setup: capability-aware defaults without prompting.
181
+ if (quickSetup) {
182
+ const responses = getDefaultResponses(framework, { projectName });
183
+ if (initialArch) responses.architecture = initialArch;
184
+ await saveConfigFn(responses);
185
+ return responses;
186
+ }
234
187
 
235
- const gitInit = await confirm({
236
- message: "Would you like to initialize a Git repository?",
237
- initialValue: false,
238
- });
239
- if (isCancel(gitInit)) onCancel();
188
+ let oldConfig = null;
189
+ const hasPrevious = await configExistsFn();
190
+ if (hasPrevious) {
191
+ oldConfig = await loadConfigFn();
192
+ oldConfig.projectName = projectName;
193
+ // Sanitize cached setup against current framework capabilities
194
+ oldConfig = sanitizeResponsesForFramework(framework, oldConfig);
195
+ }
240
196
 
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();
197
+ const steps = [];
251
198
 
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",
199
+ if (hasPrevious) {
200
+ steps.push({
201
+ id: "useExisting",
202
+ render: async (_ans, _canGoBack, { selectFn: sFn, noteFn: nFn }) => {
203
+ nFn(
204
+ `Previous setup found:\n\n${formatConfig(oldConfig)}\n`,
205
+ "Previous Configuration"
206
+ );
207
+ return await sFn({
208
+ message: "How would you like to proceed?",
209
+ options: [
210
+ { value: true, label: "Continue with previous setup" },
211
+ { value: false, label: "Start fresh (new setup)" },
212
+ ],
213
+ initialValue: true,
214
+ });
215
+ },
216
+ onAnswer: (val, ans) => {
217
+ ans.useExisting = val;
218
+ if (val === true) {
219
+ if (oldConfig.apiClient === undefined) {
220
+ oldConfig.apiClient = oldConfig.axios ? "axios" : "none";
221
+ }
222
+ if (oldConfig.formatter === undefined) {
223
+ oldConfig.formatter = "none";
224
+ }
225
+ if (oldConfig.docsLanguage === undefined) {
226
+ oldConfig.docsLanguage = "en";
227
+ }
228
+ return steps.length; // exit immediately
229
+ }
230
+ return null;
231
+ },
269
232
  });
270
- if (isCancel(formatter)) onCancel();
271
233
  }
272
234
 
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();
235
+ steps.push(
236
+ {
237
+ id: "quickSetup",
238
+ message: isPromptVisible(framework, "router")
239
+ ? "Quick Setup? (TypeScript + Tailwind + Feature-based + Router + ESLint + Vitest)"
240
+ : "Quick Setup? (TypeScript + Tailwind + Feature-based + ESLint + Vitest)",
241
+ getOptions: () => [
242
+ { label: "Yes, use recommended defaults", value: true },
243
+ { label: "No, custom setup", value: false },
244
+ ],
245
+ defaultInitial: true,
246
+ onAnswer: (val, ans, { steps: allSteps }) => {
247
+ ans.quickSetup = val;
248
+ if (val === true) {
249
+ const defaults = getDefaultResponses(framework, { projectName });
250
+ Object.assign(ans, defaults, { projectName, quickSetup: true });
251
+ if (initialArch) ans.architecture = initialArch;
252
+ // Jump to finalConfirm
253
+ return allSteps.length - 1;
254
+ }
255
+ return null;
256
+ },
257
+ },
258
+ {
259
+ id: "architecture",
260
+ message: "Which project architecture do you want?",
261
+ getOptions: () =>
262
+ filterCompatibleChoices(framework, "architecture", [
263
+ {
264
+ label: "Feature-based",
265
+ value: "feature-based",
266
+ hint: "Scales to large apps — code grouped by business domain",
267
+ },
268
+ {
269
+ label: "Type-based",
270
+ value: "type-based",
271
+ hint: "Small apps / component libraries — code grouped by UI type",
272
+ },
273
+ {
274
+ label: "Hybrid",
275
+ value: "hybrid",
276
+ hint: "Next.js: features + shared code",
277
+ },
278
+ {
279
+ label: "None",
280
+ value: "none",
281
+ hint: "Minimal flat src/ scaffold — no imposed structure",
282
+ },
283
+ ]),
284
+ defaultInitial: initialArch || "feature-based",
285
+ },
286
+ {
287
+ id: "language",
288
+ message: "Which language do you want to use?",
289
+ getOptions: () =>
290
+ filterCompatibleChoices(framework, "language", [
291
+ { label: "TypeScript", value: "ts" },
292
+ { label: "JavaScript", value: "js" },
293
+ ]),
294
+ defaultInitial: "ts",
295
+ },
296
+ {
297
+ id: "cssFramework",
298
+ message: "Which CSS framework do you want to use?",
299
+ getOptions: () =>
300
+ filterCompatibleChoices(framework, "styling", [
301
+ { label: "Tailwind CSS", value: "tailwind" },
302
+ { label: "None", value: "none" },
303
+ ]),
304
+ defaultInitial: "tailwind",
305
+ },
306
+ {
307
+ id: "testing",
308
+ message: "Which testing framework do you want to set up?",
309
+ getOptions: () => [
310
+ { label: "Vitest", value: "vitest" },
311
+ { label: "Jest", value: "jest" },
312
+ { label: "None", value: "none" },
313
+ ],
314
+ defaultInitial: "vitest",
315
+ },
316
+ {
317
+ id: "router",
318
+ skip: () => !isPromptVisible(framework, "router"),
319
+ onSkip: (ans) => {
320
+ ans.router = false;
321
+ },
322
+ message: "Would you like to install React Router?",
323
+ getOptions: () => [
324
+ { label: "Yes", value: true },
325
+ { label: "No", value: false },
326
+ ],
327
+ defaultInitial: true,
328
+ },
329
+ {
330
+ id: "bundler",
331
+ skip: () => !isPromptVisible(framework, "bundler"),
332
+ onSkip: (ans) => {
333
+ ans.bundler = undefined;
334
+ },
335
+ message: "Which bundler do you want to use?",
336
+ getOptions: () =>
337
+ filterCompatibleChoices(framework, "bundler", [
338
+ { label: "Turbopack", value: "turbopack" },
339
+ { label: "Webpack", value: "webpack" },
340
+ ]),
341
+ defaultInitial: "turbopack",
342
+ },
343
+ {
344
+ id: "adapter",
345
+ skip: () => !isPromptVisible(framework, "adapter"),
346
+ onSkip: (ans) => {
347
+ ans.adapter = "none";
348
+ },
349
+ message: "Which deployment adapter do you want to use?",
350
+ getOptions: () =>
351
+ filterCompatibleChoices(framework, "adapter", [
352
+ { label: "None (default)", value: "none" },
353
+ { label: "Node", value: "node" },
354
+ { label: "Vercel", value: "vercel" },
355
+ { label: "Cloudflare", value: "cloudflare" },
356
+ { label: "Static", value: "static" },
357
+ ]),
358
+ defaultInitial: "none",
359
+ },
360
+ {
361
+ id: "stateManagement",
362
+ message: "Which state management library do you want to use?",
363
+ getOptions: () => [
364
+ { label: "None", value: "none" },
365
+ { label: "Redux Toolkit", value: "redux" },
366
+ { label: "Zustand", value: "zustand" },
367
+ ],
368
+ defaultInitial: "none",
369
+ },
370
+ {
371
+ id: "iconLibrary",
372
+ message: "Which icon library would you like to use?",
373
+ getOptions: () => [
374
+ { label: "None", value: "none" },
375
+ { label: "Lucide Icons", value: "lucide" },
376
+ { label: "Huge Icons", value: "huge" },
377
+ ],
378
+ defaultInitial: "none",
379
+ },
380
+ {
381
+ id: "apiClient",
382
+ message: "Which API client do you want to use?",
383
+ getOptions: () => [
384
+ { label: "None", value: "none" },
385
+ { label: "Axios", value: "axios" },
386
+ { label: "Fetch (native)", value: "fetch" },
387
+ ],
388
+ defaultInitial: "none",
389
+ },
390
+ {
391
+ id: "gitInit",
392
+ message: "Would you like to initialize a Git repository?",
393
+ getOptions: () => [
394
+ { label: "Yes", value: true },
395
+ { label: "No", value: false },
396
+ ],
397
+ defaultInitial: false,
398
+ },
399
+ {
400
+ id: "linter",
401
+ message: "Which linter do you want to use?",
402
+ getOptions: () =>
403
+ filterCompatibleChoices(framework, "linter", [
404
+ {
405
+ label: "ESLint",
406
+ value: "eslint",
407
+ hint: "Industry standard JavaScript linter (Recommended)",
408
+ },
409
+ { label: "Oxlint", value: "oxlint", hint: "Fast Rust-based linter" },
410
+ { label: "Biome", value: "biome", hint: "Toolchain for web projects" },
411
+ { label: "None", value: "none" },
412
+ ]),
413
+ defaultInitial: "eslint",
414
+ },
415
+ {
416
+ id: "formatter",
417
+ skip: (ans) => ans.linter === "none",
418
+ onSkip: (ans) => {
419
+ ans.formatter = "none";
420
+ },
421
+ message: "Which formatter do you want to use?",
422
+ getOptions: (ans) =>
423
+ filterCompatibleChoices(
424
+ framework,
425
+ "formatter",
426
+ ans.linter === "eslint"
427
+ ? [
428
+ { label: "None", value: "none" },
429
+ { label: "Prettier", value: "prettier" },
430
+ ]
431
+ : [
432
+ { label: "None", value: "none" },
433
+ { label: "Oxfmt (Recommended)", value: "oxfmt" },
434
+ { label: "Prettier", value: "prettier" },
435
+ ]
436
+ ),
437
+ defaultInitial: (ans) => {
438
+ const compatible = filterCompatibleChoices(
439
+ framework,
440
+ "formatter",
441
+ ans.linter === "eslint"
442
+ ? [{ value: "none" }, { value: "prettier" }]
443
+ : [{ value: "none" }, { value: "oxfmt" }, { value: "prettier" }]
444
+ ).map((o) => o.value);
445
+ const preferred = ans.linter === "eslint" ? "prettier" : "oxfmt";
446
+ return compatible.includes(preferred) ? preferred : compatible[0];
447
+ },
448
+ },
449
+ {
450
+ id: "docsLanguage",
451
+ message: "Which docs language do you want?",
452
+ getOptions: () => [
453
+ { label: "English", value: "en" },
454
+ { label: "Español", value: "es" },
455
+ ],
456
+ defaultInitial: "en",
457
+ },
458
+ {
459
+ id: "readme",
460
+ message: "Would you like to generate a README.md and LICENSE?",
461
+ getOptions: () => [
462
+ { label: "Yes", value: true },
463
+ { label: "No", value: false },
464
+ ],
465
+ defaultInitial: true,
466
+ },
467
+ {
468
+ id: "finalConfirm",
469
+ render: async (ans, canGoBack, { selectFn: sFn, noteFn: nFn }) => {
470
+ nFn(`\n${formatConfig(ans)}\n`, "Project Configuration Summary");
471
+ return await sFn({
472
+ message: "Proceed with project generation?",
473
+ options: [
474
+ { label: "Confirm and scaffold project", value: true },
475
+ ...(canGoBack
476
+ ? [{ label: chalk.dim("← Back to edit options"), value: BACK }]
477
+ : []),
478
+ ],
479
+ initialValue: true,
480
+ });
481
+ },
482
+ }
483
+ );
282
484
 
283
- const readme = await confirm({
284
- message: "Would you like to generate a README.md and LICENSE?",
285
- initialValue: true,
485
+ const responses = await runPromptFlow({
486
+ steps,
487
+ initialAnswers: { projectName },
488
+ selectFn,
489
+ noteFn,
490
+ onCancelFn: onCancel,
286
491
  });
287
- if (isCancel(readme)) onCancel();
288
492
 
289
- const responses = {
493
+ if (responses.useExisting) {
494
+ return { ...oldConfig, projectName };
495
+ }
496
+
497
+ const {
498
+ useExisting: _useExisting,
499
+ quickSetup: _quickSetup,
500
+ bundler,
501
+ adapter,
502
+ ...picked
503
+ } = responses;
504
+ const finalResponses = {
290
505
  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,
506
+ frameworkName: framework.name,
507
+ frameworkVariant: framework.variant,
508
+ ...picked,
304
509
  };
510
+ if (bundler !== undefined) {
511
+ finalResponses.bundler = bundler;
512
+ }
513
+ if (adapter !== undefined && adapter !== "none") {
514
+ finalResponses.adapter = adapter;
515
+ }
305
516
 
306
- await saveConfig(responses);
307
- return responses;
517
+ // No config cache write until final confirm!
518
+ await saveConfigFn(finalResponses);
519
+ return finalResponses;
308
520
  }