@tanstack/cta-cli 0.10.0-alpha.18 → 0.10.0-alpha.20

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/src/options.ts CHANGED
@@ -1,174 +1,26 @@
1
- import {
2
- cancel,
3
- confirm,
4
- isCancel,
5
- multiselect,
6
- select,
7
- text,
8
- } from '@clack/prompts'
9
-
10
1
  import {
11
2
  CODE_ROUTER,
12
- DEFAULT_PACKAGE_MANAGER,
13
3
  FILE_ROUTER,
14
- SUPPORTED_PACKAGE_MANAGERS,
15
4
  finalizeAddOns,
16
- getAllAddOns,
17
5
  getFrameworkById,
18
6
  getPackageManager,
19
- loadRemoteAddOn,
20
7
  } from '@tanstack/cta-engine'
21
8
 
22
- import type {
23
- AddOn,
24
- Mode,
25
- Options,
26
- Starter,
27
- TemplateOptions,
28
- Variable,
29
- } from '@tanstack/cta-engine'
9
+ import {
10
+ getProjectName,
11
+ selectAddOns,
12
+ selectGit,
13
+ selectPackageManager,
14
+ selectRouterType,
15
+ selectTailwind,
16
+ selectToolchain,
17
+ selectTypescript,
18
+ } from './ui-prompts.js'
19
+
20
+ import type { Mode, Options } from '@tanstack/cta-engine'
30
21
 
31
22
  import type { CliOptions } from './types.js'
32
23
 
33
- // If all CLI options are provided, use them directly
34
- export async function normalizeOptions(
35
- cliOptions: CliOptions,
36
- forcedMode?: Mode,
37
- forcedAddOns?: Array<string>,
38
- ): Promise<Options | undefined> {
39
- // in some cases, if you use windows/powershell, the argument for addons
40
- // if sepparated by comma is not really passed as an array, but as a string
41
- // with spaces, We need to normalize this edge case.
42
- if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
43
- const parseSeparatedArgs = cliOptions.addOns[0].split(' ')
44
- if (parseSeparatedArgs.length > 1) {
45
- cliOptions.addOns = parseSeparatedArgs
46
- }
47
- }
48
-
49
- if (cliOptions.projectName) {
50
- let typescript =
51
- cliOptions.template === 'typescript' ||
52
- cliOptions.template === 'file-router' ||
53
- cliOptions.framework === 'solid'
54
-
55
- let tailwind = !!cliOptions.tailwind
56
- if (cliOptions.framework === 'solid') {
57
- tailwind = true
58
- }
59
-
60
- let mode: typeof FILE_ROUTER | typeof CODE_ROUTER =
61
- cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER
62
-
63
- const starter = cliOptions.starter
64
- ? ((await loadRemoteAddOn(cliOptions.starter)) as Starter)
65
- : undefined
66
-
67
- if (starter) {
68
- tailwind = starter.tailwind
69
- typescript = starter.typescript
70
- cliOptions.framework = starter.framework
71
- mode = starter.mode
72
- }
73
-
74
- let addOns = false
75
- let chosenAddOns: Array<AddOn> = []
76
- if (
77
- Array.isArray(cliOptions.addOns) ||
78
- starter?.dependsOn ||
79
- forcedAddOns ||
80
- cliOptions.toolchain
81
- ) {
82
- addOns = true
83
- let finalAddOns = Array.from(
84
- new Set([...(starter?.dependsOn || []), ...(forcedAddOns || [])]),
85
- )
86
- if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
87
- finalAddOns = Array.from(
88
- new Set([
89
- ...(forcedAddOns || []),
90
- ...finalAddOns,
91
- ...cliOptions.addOns,
92
- ]),
93
- )
94
- }
95
- const framework = getFrameworkById(cliOptions.framework || 'react-cra')!
96
-
97
- if (cliOptions.toolchain) {
98
- finalAddOns.push(cliOptions.toolchain)
99
- }
100
-
101
- chosenAddOns = await finalizeAddOns(
102
- framework,
103
- forcedMode || cliOptions.template === 'file-router'
104
- ? FILE_ROUTER
105
- : CODE_ROUTER,
106
- finalAddOns,
107
- )
108
- tailwind = true
109
- typescript = true
110
- }
111
-
112
- return {
113
- // TODO: This is a bit to fix the default framework
114
- framework: getFrameworkById(cliOptions.framework || 'react-cra')!,
115
- projectName: cliOptions.projectName,
116
- typescript,
117
- tailwind,
118
- packageManager:
119
- cliOptions.packageManager ||
120
- getPackageManager() ||
121
- DEFAULT_PACKAGE_MANAGER,
122
- mode,
123
- git: !!cliOptions.git,
124
- addOns,
125
- chosenAddOns,
126
- variableValues: {},
127
- starter,
128
- }
129
- }
130
- }
131
-
132
- async function collectVariables(
133
- variables: Array<Variable>,
134
- ): Promise<Record<string, string | number | boolean>> {
135
- const responses: Record<string, string | number | boolean> = {}
136
- for (const variable of variables) {
137
- if (variable.type === 'string') {
138
- const response = await text({
139
- message: variable.description,
140
- initialValue: variable.default,
141
- })
142
- if (isCancel(response)) {
143
- cancel('Operation cancelled.')
144
- process.exit(0)
145
- }
146
- responses[variable.name] = response
147
- } else if (variable.type === 'number') {
148
- const response = await text({
149
- message: variable.description,
150
- initialValue: variable.default.toString(),
151
- })
152
- if (isCancel(response)) {
153
- cancel('Operation cancelled.')
154
- process.exit(0)
155
- }
156
- responses[variable.name] = Number(response)
157
- } else {
158
- const response = await confirm({
159
- message: variable.description,
160
- initialValue: variable.default === true,
161
- })
162
- if (isCancel(response)) {
163
- cancel('Operation cancelled.')
164
- process.exit(0)
165
- }
166
- responses[variable.name] = response
167
- }
168
- }
169
- return responses
170
- }
171
-
172
24
  export async function promptForOptions(
173
25
  cliOptions: CliOptions,
174
26
  {
@@ -176,277 +28,101 @@ export async function promptForOptions(
176
28
  forcedMode,
177
29
  }: {
178
30
  forcedAddOns?: Array<string>
179
- forcedMode?: TemplateOptions
31
+ forcedMode?: Mode
180
32
  },
181
- ): Promise<Required<Options>> {
33
+ ): Promise<Required<Options> | undefined> {
182
34
  const options = {} as Required<Options>
183
35
 
184
- const framework = getFrameworkById(cliOptions.framework || 'react-cra')!
185
- options.framework = framework
186
- // TODO: This is a bit of a hack to ensure that the framework is solid
187
- if (options.framework.id === 'solid') {
188
- options.typescript = true
189
- options.tailwind = true
190
- }
36
+ options.framework = getFrameworkById(cliOptions.framework || 'react-cra')!
191
37
 
192
- if (cliOptions.addOns) {
193
- options.typescript = true
194
- }
195
-
196
- if (!cliOptions.projectName) {
197
- const value = await text({
198
- message: 'What would you like to name your project?',
199
- defaultValue: 'my-app',
200
- validate(value) {
201
- if (!value) {
202
- return 'Please enter a name'
203
- }
204
- },
205
- })
206
- if (isCancel(value)) {
207
- cancel('Operation cancelled.')
208
- process.exit(0)
209
- }
210
- options.projectName = value
211
- }
38
+ options.projectName = cliOptions.projectName || (await getProjectName())
212
39
 
213
40
  // Router type selection
214
- if (!cliOptions.template && !forcedMode) {
215
- const routerType = await select({
216
- message: 'Select the router type:',
217
- options: [
218
- {
219
- value: FILE_ROUTER,
220
- label: 'File Router - File-based routing structure',
221
- },
222
- {
223
- value: CODE_ROUTER,
224
- label: 'Code Router - Traditional code-based routing',
225
- },
226
- ],
227
- initialValue: FILE_ROUTER,
228
- })
229
- if (isCancel(routerType)) {
230
- cancel('Operation cancelled.')
231
- process.exit(0)
232
- }
233
- options.mode = routerType as typeof CODE_ROUTER | typeof FILE_ROUTER
234
- } else if (forcedMode) {
235
- options.mode = forcedMode === 'file-router' ? FILE_ROUTER : CODE_ROUTER
236
- options.typescript = options.mode === FILE_ROUTER
237
- } else {
41
+ if (forcedMode) {
42
+ options.mode = forcedMode
43
+ } else if (cliOptions.template) {
238
44
  options.mode =
239
45
  cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER
240
- if (options.mode === FILE_ROUTER) {
241
- options.typescript = true
242
- }
46
+ } else {
47
+ options.mode = await selectRouterType()
243
48
  }
244
49
 
245
50
  // TypeScript selection (if using Code Router)
246
- if (!options.typescript) {
247
- if (options.mode === CODE_ROUTER) {
248
- const typescriptEnable = await confirm({
249
- message: 'Would you like to use TypeScript?',
250
- initialValue: true,
251
- })
252
- if (isCancel(typescriptEnable)) {
253
- cancel('Operation cancelled.')
254
- process.exit(0)
255
- }
256
- options.typescript = typescriptEnable
257
- } else {
258
- options.typescript = true
259
- }
51
+ options.typescript =
52
+ options.mode === FILE_ROUTER || options.framework.id === 'solid'
53
+ if (!options.typescript && options.mode === CODE_ROUTER) {
54
+ options.typescript = await selectTypescript()
260
55
  }
261
56
 
262
57
  // Tailwind selection
263
58
  if (!cliOptions.tailwind && options.framework.id === 'react-cra') {
264
- const tailwind = await confirm({
265
- message: 'Would you like to use Tailwind CSS?',
266
- initialValue: true,
267
- })
268
- if (isCancel(tailwind)) {
269
- cancel('Operation cancelled.')
270
- process.exit(0)
271
- }
272
- options.tailwind = tailwind
59
+ options.tailwind = await selectTailwind()
273
60
  } else {
274
- // TODO: This is a bit of a hack to ensure that the framework is solid
275
- options.tailwind = options.framework.id === 'solid' || !!cliOptions.tailwind
61
+ options.tailwind = true
276
62
  }
277
63
 
278
64
  // Package manager selection
279
- if (cliOptions.packageManager === undefined) {
280
- const detectedPackageManager = getPackageManager()
281
- if (!detectedPackageManager) {
282
- const pm = await select({
283
- message: 'Select package manager:',
284
- options: SUPPORTED_PACKAGE_MANAGERS.map((pm) => ({
285
- value: pm,
286
- label: pm,
287
- })),
288
- initialValue: DEFAULT_PACKAGE_MANAGER,
289
- })
290
- if (isCancel(pm)) {
291
- cancel('Operation cancelled.')
292
- process.exit(0)
293
- }
294
- options.packageManager = pm
295
- } else {
296
- options.packageManager = detectedPackageManager
297
- }
298
- } else {
65
+ if (cliOptions.packageManager) {
299
66
  options.packageManager = cliOptions.packageManager
67
+ } else {
68
+ const detectedPackageManager = await getPackageManager()
69
+ options.packageManager =
70
+ detectedPackageManager || (await selectPackageManager())
300
71
  }
301
72
 
302
73
  // Toolchain selection
303
- let toolchain: AddOn | undefined = undefined
304
- if (cliOptions.toolchain === undefined) {
305
- const toolchains = new Set<AddOn>()
306
- for (const addOn of framework.getAddOns()) {
307
- if (addOn.type === 'toolchain') {
308
- toolchains.add(addOn)
309
- }
310
- }
74
+ const toolchain = await selectToolchain(
75
+ options.framework,
76
+ cliOptions.toolchain,
77
+ )
311
78
 
312
- const tc = await select<AddOn | undefined>({
313
- message: 'Select toolchain',
314
- options: [
315
- {
316
- value: undefined,
317
- label: 'None',
318
- },
319
- ...Array.from(toolchains).map((tc) => ({
320
- value: tc,
321
- label: tc.name,
322
- })),
323
- ],
324
- initialValue: undefined,
325
- })
326
- if (isCancel(tc)) {
327
- cancel('Operation cancelled.')
328
- process.exit(0)
329
- }
330
- toolchain = tc
331
- } else {
332
- for (const addOn of framework.getAddOns()) {
333
- if (addOn.type === 'toolchain' && addOn.id === cliOptions.toolchain) {
334
- toolchain = addOn
335
- }
336
- }
79
+ // Add-ons selection
80
+ const addOns: Set<string> = new Set()
81
+
82
+ if (toolchain) {
83
+ addOns.add(toolchain)
84
+ }
85
+
86
+ for (const addOn of forcedAddOns) {
87
+ addOns.add(addOn)
337
88
  }
338
89
 
339
- options.chosenAddOns = toolchain ? [toolchain] : []
340
90
  if (Array.isArray(cliOptions.addOns)) {
341
- options.chosenAddOns = await finalizeAddOns(
91
+ for (const addOn of cliOptions.addOns) {
92
+ addOns.add(addOn)
93
+ }
94
+ } else {
95
+ for (const addOn of await selectAddOns(
342
96
  options.framework,
343
97
  options.mode,
344
- Array.from(
345
- new Set([...cliOptions.addOns, ...forcedAddOns, toolchain?.id]),
346
- ).filter(Boolean) as Array<string>,
347
- )
348
- options.tailwind = true
349
- } else if (cliOptions.addOns) {
350
- // Select any add-ons
351
- const allAddOns = await getAllAddOns(options.framework, options.mode)
352
- const addOns = allAddOns.filter((addOn) => addOn.type === 'add-on')
353
- let selectedAddOns: Array<string> = []
354
- if (options.typescript && addOns.length > 0) {
355
- const value = await multiselect({
356
- message: 'What add-ons would you like for your project:',
357
- options: addOns
358
- .filter((addOn) => !forcedAddOns.includes(addOn.id))
359
- .map((addOn) => ({
360
- value: addOn.id,
361
- label: addOn.name,
362
- hint: addOn.description,
363
- })),
364
- required: false,
365
- })
366
-
367
- if (isCancel(value)) {
368
- cancel('Operation cancelled.')
369
- process.exit(0)
370
- }
371
- selectedAddOns = value
98
+ 'add-on',
99
+ 'What add-ons would you like for your project?',
100
+ forcedAddOns,
101
+ )) {
102
+ addOns.add(addOn)
372
103
  }
373
104
 
374
- // Select any examples
375
- let selectedExamples: Array<string> = []
376
- const examples = allAddOns.filter((addOn) => addOn.type === 'example')
377
- if (options.typescript && examples.length > 0) {
378
- const value = await multiselect({
379
- message: 'Would you like any examples?',
380
- options: examples
381
- .filter((addOn) => !forcedAddOns.includes(addOn.id))
382
- .map((addOn) => ({
383
- value: addOn.id,
384
- label: addOn.name,
385
- hint: addOn.description,
386
- })),
387
- required: false,
388
- })
389
-
390
- if (isCancel(value)) {
391
- cancel('Operation cancelled.')
392
- process.exit(0)
393
- }
394
- selectedExamples = value
395
- }
396
-
397
- if (
398
- selectedAddOns.length > 0 ||
399
- selectedExamples.length > 0 ||
400
- forcedAddOns.length > 0 ||
401
- toolchain
402
- ) {
403
- options.chosenAddOns = await finalizeAddOns(
404
- options.framework,
405
- options.mode,
406
- Array.from(
407
- new Set([
408
- ...selectedAddOns,
409
- ...selectedExamples,
410
- ...forcedAddOns,
411
- toolchain?.id,
412
- ]),
413
- ).filter(Boolean) as Array<string>,
414
- )
415
- options.tailwind = true
416
- }
417
- } else if (forcedAddOns.length > 0) {
418
- options.chosenAddOns = await finalizeAddOns(
105
+ for (const addOn of await selectAddOns(
419
106
  options.framework,
420
107
  options.mode,
421
- Array.from(new Set([...forcedAddOns, toolchain?.id])).filter(
422
- Boolean,
423
- ) as Array<string>,
424
- )
425
- }
426
-
427
- // Collect variables
428
- const variables: Array<Variable> = []
429
- for (const addOn of options.chosenAddOns) {
430
- for (const variable of addOn.variables ?? []) {
431
- variables.push(variable)
108
+ 'example',
109
+ 'Would you like any examples?',
110
+ forcedAddOns,
111
+ )) {
112
+ addOns.add(addOn)
432
113
  }
433
114
  }
434
- options.variableValues = await collectVariables(variables)
435
115
 
436
- // Git selection
437
- if (cliOptions.git === undefined) {
438
- const git = await confirm({
439
- message: 'Would you like to initialize a new git repository?',
440
- initialValue: true,
441
- })
442
- if (isCancel(git)) {
443
- cancel('Operation cancelled.')
444
- process.exit(0)
445
- }
446
- options.git = git
447
- } else {
448
- options.git = !!cliOptions.git
116
+ options.chosenAddOns = Array.from(
117
+ await finalizeAddOns(options.framework, options.mode, Array.from(addOns)),
118
+ )
119
+
120
+ if (options.chosenAddOns.length) {
121
+ options.tailwind = true
122
+ options.typescript = true
449
123
  }
450
124
 
125
+ options.git = cliOptions.git || (await selectGit())
126
+
451
127
  return options
452
128
  }
package/src/types.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { PackageManager, TemplateOptions } from '@tanstack/cta-engine'
1
+ import type { PackageManager } from '@tanstack/cta-engine'
2
+
3
+ export type TemplateOptions = 'typescript' | 'javascript' | 'file-router'
2
4
 
3
5
  export interface CliOptions {
4
6
  template?: TemplateOptions
@@ -14,4 +16,6 @@ export interface CliOptions {
14
16
  mcpSse?: boolean
15
17
  starter?: string
16
18
  targetDir?: string
19
+ interactive?: boolean
20
+ ui?: boolean
17
21
  }
@@ -13,52 +13,60 @@ import { createDefaultEnvironment } from '@tanstack/cta-engine'
13
13
 
14
14
  import type { Environment } from '@tanstack/cta-engine'
15
15
 
16
- export function createUIEnvironment(): Environment {
16
+ export function createUIEnvironment(
17
+ appName: string,
18
+ silent: boolean,
19
+ ): Environment {
17
20
  const defaultEnvironment = createDefaultEnvironment()
18
21
 
19
- return {
22
+ let newEnvironment = {
20
23
  ...defaultEnvironment,
21
- intro: (message: string) => {
22
- intro(message)
23
- },
24
- outro: (message: string) => {
25
- outro(message)
26
- },
27
- info: (title?: string, message?: string) => {
28
- console.log('info', title, message)
29
- log.info(
30
- `${title ? chalk.red(title) : ''}${message ? chalk.green(message) : ''}`,
31
- )
32
- },
33
- error: (title?: string, message?: string) => {
34
- console.log('error', title, message)
35
- log.error(`${title ? `${title}: ` : ''}${message}`)
36
- },
37
- warn: (title?: string, message?: string) => {
38
- console.log('warn', title, message)
39
- log.warn(`${title ? `${title}: ` : ''}${message}`)
40
- },
41
- confirm: async (message: string) => {
42
- console.log('confirm', message)
43
- const shouldContinue = await confirm({
44
- message,
45
- })
46
- if (isCancel(shouldContinue)) {
47
- cancel('Operation cancelled.')
48
- process.exit(0)
49
- }
50
- return shouldContinue
51
- },
52
- spinner: () => {
53
- const s = spinner()
54
- return {
55
- start: (message: string) => {
56
- s.start(message)
57
- },
58
- stop: (message: string) => {
59
- s.stop(message)
60
- },
61
- }
62
- },
24
+ appName,
63
25
  }
26
+
27
+ if (!silent) {
28
+ newEnvironment = {
29
+ ...newEnvironment,
30
+ intro: (message: string) => {
31
+ intro(message)
32
+ },
33
+ outro: (message: string) => {
34
+ outro(message)
35
+ },
36
+ info: (title?: string, message?: string) => {
37
+ log.info(
38
+ `${title ? chalk.red(title) : ''}${message ? chalk.green(message) : ''}`,
39
+ )
40
+ },
41
+ error: (title?: string, message?: string) => {
42
+ log.error(`${title ? `${title}: ` : ''}${message}`)
43
+ },
44
+ warn: (title?: string, message?: string) => {
45
+ log.warn(`${title ? `${title}: ` : ''}${message}`)
46
+ },
47
+ confirm: async (message: string) => {
48
+ const shouldContinue = await confirm({
49
+ message,
50
+ })
51
+ if (isCancel(shouldContinue)) {
52
+ cancel('Operation cancelled.')
53
+ process.exit(0)
54
+ }
55
+ return shouldContinue
56
+ },
57
+ spinner: () => {
58
+ const s = spinner()
59
+ return {
60
+ start: (message: string) => {
61
+ s.start(message)
62
+ },
63
+ stop: (message: string) => {
64
+ s.stop(message)
65
+ },
66
+ }
67
+ },
68
+ }
69
+ }
70
+
71
+ return newEnvironment
64
72
  }