@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.
@@ -0,0 +1,140 @@
1
+ import { cancel, confirm, isCancel, multiselect, select, text, } from '@clack/prompts';
2
+ import { CODE_ROUTER, DEFAULT_PACKAGE_MANAGER, FILE_ROUTER, SUPPORTED_PACKAGE_MANAGERS, getAllAddOns, } from '@tanstack/cta-engine';
3
+ export async function getProjectName() {
4
+ const value = await text({
5
+ message: 'What would you like to name your project?',
6
+ defaultValue: 'my-app',
7
+ validate(value) {
8
+ if (!value) {
9
+ return 'Please enter a name';
10
+ }
11
+ },
12
+ });
13
+ if (isCancel(value)) {
14
+ cancel('Operation cancelled.');
15
+ process.exit(0);
16
+ }
17
+ return value;
18
+ }
19
+ export async function selectRouterType() {
20
+ const routerType = await select({
21
+ message: 'Select the router type:',
22
+ options: [
23
+ {
24
+ value: FILE_ROUTER,
25
+ label: 'File Router - File-based routing structure',
26
+ },
27
+ {
28
+ value: CODE_ROUTER,
29
+ label: 'Code Router - Traditional code-based routing',
30
+ },
31
+ ],
32
+ initialValue: FILE_ROUTER,
33
+ });
34
+ if (isCancel(routerType)) {
35
+ cancel('Operation cancelled.');
36
+ process.exit(0);
37
+ }
38
+ return routerType;
39
+ }
40
+ export async function selectTypescript() {
41
+ const typescriptEnable = await confirm({
42
+ message: 'Would you like to use TypeScript?',
43
+ initialValue: true,
44
+ });
45
+ if (isCancel(typescriptEnable)) {
46
+ cancel('Operation cancelled.');
47
+ process.exit(0);
48
+ }
49
+ return typescriptEnable;
50
+ }
51
+ export async function selectTailwind() {
52
+ const tailwind = await confirm({
53
+ message: 'Would you like to use Tailwind CSS?',
54
+ initialValue: true,
55
+ });
56
+ if (isCancel(tailwind)) {
57
+ cancel('Operation cancelled.');
58
+ process.exit(0);
59
+ }
60
+ return tailwind;
61
+ }
62
+ export async function selectPackageManager() {
63
+ const packageManager = await select({
64
+ message: 'Select package manager:',
65
+ options: SUPPORTED_PACKAGE_MANAGERS.map((pm) => ({
66
+ value: pm,
67
+ label: pm,
68
+ })),
69
+ initialValue: DEFAULT_PACKAGE_MANAGER,
70
+ });
71
+ if (isCancel(packageManager)) {
72
+ cancel('Operation cancelled.');
73
+ process.exit(0);
74
+ }
75
+ return packageManager;
76
+ }
77
+ export async function selectAddOns(framework, mode, type, message, forcedAddOns = []) {
78
+ const allAddOns = await getAllAddOns(framework, mode);
79
+ const addOns = allAddOns.filter((addOn) => addOn.type === type);
80
+ if (addOns.length === 0) {
81
+ return [];
82
+ }
83
+ const value = await multiselect({
84
+ message,
85
+ options: addOns
86
+ .filter((addOn) => !forcedAddOns.includes(addOn.id))
87
+ .map((addOn) => ({
88
+ value: addOn.id,
89
+ label: addOn.name,
90
+ hint: addOn.description,
91
+ })),
92
+ required: false,
93
+ });
94
+ if (isCancel(value)) {
95
+ cancel('Operation cancelled.');
96
+ process.exit(0);
97
+ }
98
+ return value;
99
+ }
100
+ export async function selectGit() {
101
+ const git = await confirm({
102
+ message: 'Would you like to initialize a new git repository?',
103
+ initialValue: true,
104
+ });
105
+ if (isCancel(git)) {
106
+ cancel('Operation cancelled.');
107
+ process.exit(0);
108
+ }
109
+ return git;
110
+ }
111
+ export async function selectToolchain(framework, toolchain) {
112
+ const toolchains = new Set();
113
+ for (const addOn of framework.getAddOns()) {
114
+ if (addOn.type === 'toolchain') {
115
+ toolchains.add(addOn);
116
+ if (toolchain && addOn.id === toolchain) {
117
+ return toolchain;
118
+ }
119
+ }
120
+ }
121
+ const tc = await select({
122
+ message: 'Select toolchain',
123
+ options: [
124
+ {
125
+ value: undefined,
126
+ label: 'None',
127
+ },
128
+ ...Array.from(toolchains).map((tc) => ({
129
+ value: tc.id,
130
+ label: tc.name,
131
+ })),
132
+ ],
133
+ initialValue: undefined,
134
+ });
135
+ if (isCancel(tc)) {
136
+ cancel('Operation cancelled.');
137
+ process.exit(0);
138
+ }
139
+ return tc;
140
+ }
package/dist/utils.js ADDED
@@ -0,0 +1,7 @@
1
+ import { CODE_ROUTER, FILE_ROUTER } from '@tanstack/cta-engine';
2
+ export function convertTemplateToMode(template) {
3
+ if (template === 'typescript' || template === 'javascript') {
4
+ return CODE_ROUTER;
5
+ }
6
+ return FILE_ROUTER;
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-cli",
3
- "version": "0.10.0-alpha.18",
3
+ "version": "0.10.0-alpha.20",
4
4
  "description": "Tanstack Application Builder CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,19 +24,23 @@
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
26
  "@clack/prompts": "^0.10.0",
27
+ "@modelcontextprotocol/sdk": "^1.6.0",
27
28
  "chalk": "^5.4.1",
28
29
  "commander": "^13.1.0",
29
- "@tanstack/cta-engine": "0.10.0-alpha.18",
30
- "@tanstack/cta-mcp": "0.10.0-alpha.18",
31
- "@tanstack/cta-ui": "0.10.0-alpha.18",
32
- "@tanstack/cta-custom-add-on": "0.10.0-alpha.18"
30
+ "express": "^4.21.2",
31
+ "zod": "^3.24.2",
32
+ "@tanstack/cta-engine": "0.10.0-alpha.20",
33
+ "@tanstack/cta-ui": "0.10.0-alpha.20"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@tanstack/config": "^0.16.2",
37
+ "@types/express": "^5.0.1",
36
38
  "@types/node": "^22.13.4",
39
+ "@vitest/coverage-v8": "3.1.1",
37
40
  "eslint": "^9.20.0",
38
41
  "typescript": "^5.6.3",
39
- "vitest": "^3.0.8"
42
+ "vitest": "^3.1.1",
43
+ "vitest-fetch-mock": "^0.4.5"
40
44
  },
41
45
  "scripts": {}
42
46
  }
package/src/cli.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolve } from 'node:path'
1
2
  import { Command, InvalidArgumentError } from 'commander'
2
3
  import { intro, log } from '@clack/prompts'
3
4
  import chalk from 'chalk'
@@ -5,43 +6,50 @@ import chalk from 'chalk'
5
6
  import {
6
7
  SUPPORTED_PACKAGE_MANAGERS,
7
8
  addToApp,
9
+ compileAddOn,
10
+ compileStarter,
8
11
  createApp,
12
+ createSerializedOptions,
9
13
  getAllAddOns,
10
14
  getFrameworkById,
11
15
  getFrameworkByName,
12
16
  getFrameworks,
17
+ initAddOn,
18
+ initStarter,
13
19
  } from '@tanstack/cta-engine'
14
- import { initAddOn } from '@tanstack/cta-custom-add-on'
15
-
16
- import { runMCPServer } from '@tanstack/cta-mcp'
17
20
 
18
21
  import { launchUI } from '@tanstack/cta-ui'
19
22
 
20
- import { normalizeOptions, promptForOptions } from './options.js'
23
+ import { runMCPServer } from './mcp.js'
24
+
25
+ import { promptForOptions } from './options.js'
26
+ import { normalizeOptions } from './command-line.js'
21
27
 
22
28
  import { createUIEnvironment } from './ui-environment.js'
29
+ import { convertTemplateToMode } from './utils.js'
23
30
 
24
- import type {
25
- Mode,
26
- PackageManager,
27
- TemplateOptions,
28
- } from '@tanstack/cta-engine'
31
+ import type { Mode, Options, PackageManager } from '@tanstack/cta-engine'
29
32
 
30
- import type { CliOptions } from './types.js'
33
+ import type { CliOptions, TemplateOptions } from './types.js'
31
34
 
32
35
  async function listAddOns(
33
36
  options: CliOptions,
34
37
  {
35
38
  forcedMode,
36
- forcedAddOns = [],
39
+ forcedAddOns,
40
+ defaultTemplate,
37
41
  }: {
38
- forcedMode?: TemplateOptions
39
- forcedAddOns?: Array<string>
42
+ forcedMode?: Mode
43
+ forcedAddOns: Array<string>
44
+ defaultTemplate?: TemplateOptions
40
45
  },
41
46
  ) {
42
47
  const addOns = await getAllAddOns(
43
48
  getFrameworkById(options.framework || 'react-cra')!,
44
- forcedMode || options.template || 'typescript',
49
+ forcedMode ||
50
+ convertTemplateToMode(
51
+ options.template || defaultTemplate || 'javascript',
52
+ ),
45
53
  )
46
54
  for (const addOn of addOns.filter((a) => !forcedAddOns.includes(a.id))) {
47
55
  console.log(`${chalk.bold(addOn.id)}: ${addOn.description}`)
@@ -52,14 +60,16 @@ export function cli({
52
60
  name,
53
61
  appName,
54
62
  forcedMode,
55
- forcedAddOns,
63
+ forcedAddOns = [],
64
+ defaultTemplate = 'javascript',
56
65
  }: {
57
66
  name: string
58
67
  appName: string
59
68
  forcedMode?: Mode
60
69
  forcedAddOns?: Array<string>
70
+ defaultTemplate?: TemplateOptions
61
71
  }) {
62
- const environment = createUIEnvironment()
72
+ const environment = createUIEnvironment(appName, false)
63
73
 
64
74
  const program = new Command()
65
75
 
@@ -78,37 +88,61 @@ export function cli({
78
88
 
79
89
  program
80
90
  .command('add')
81
- .argument('add-on', 'Name of the add-on (or add-ons separated by commas)')
82
- .action(async (addOn: string) => {
83
- await addToApp(
84
- addOn.split(',').map((addon) => addon.trim()),
85
- {
86
- silent: false,
87
- },
88
- environment,
89
- )
91
+ .argument(
92
+ '[add-on...]',
93
+ 'Name of the add-ons (or add-ons separated by spaces or commas)',
94
+ )
95
+ .option('--forced', 'Force the add-on to be added', false)
96
+ .option('--ui', 'Add with the UI')
97
+ .action(async (addOns: Array<string>) => {
98
+ const parsedAddOns: Array<string> = []
99
+ for (const addOn of addOns) {
100
+ if (addOn.includes(',') || addOn.includes(' ')) {
101
+ parsedAddOns.push(
102
+ ...addOn.split(/[\s,]+/).map((addon) => addon.trim()),
103
+ )
104
+ } else {
105
+ parsedAddOns.push(addOn.trim())
106
+ }
107
+ }
108
+ if (program.opts().ui) {
109
+ launchUI({
110
+ mode: 'add',
111
+ addOns: parsedAddOns,
112
+ })
113
+ } else {
114
+ await addToApp(environment, parsedAddOns, process.cwd(), {
115
+ forced: program.opts().forced,
116
+ })
117
+ }
90
118
  })
91
119
 
92
120
  const addOnCommand = program.command('add-on')
93
-
94
121
  addOnCommand
95
- .command('update')
96
- .description('Create or update an add-on from the current project')
122
+ .command('init')
123
+ .description('Initialize an add-on from the current project')
97
124
  .action(async () => {
98
- await initAddOn('add-on', environment)
125
+ await initAddOn(environment)
99
126
  })
100
127
  addOnCommand
101
- .command('ui')
102
- .description('Show the add-on developer UI')
128
+ .command('compile')
129
+ .description('Update add-on from the current project')
103
130
  .action(async () => {
104
- launchUI()
131
+ await compileAddOn(environment)
105
132
  })
106
133
 
107
- program
108
- .command('update-starter')
109
- .description('Create or update a project starter from the current project')
134
+ const starterCommand = program.command('starter')
135
+ starterCommand
136
+ .command('init')
137
+ .description('Initialize a project starter from the current project')
138
+ .action(async () => {
139
+ await initStarter(environment)
140
+ })
141
+ starterCommand
142
+ .command('compile')
143
+ .description('Compile the starter JSON file for the current project')
110
144
  .action(async () => {
111
- await initAddOn('starter', environment)
145
+ await compileStarter(environment)
112
146
  })
113
147
 
114
148
  program.argument('[project-name]', 'name of the project')
@@ -179,6 +213,7 @@ export function cli({
179
213
  return value
180
214
  },
181
215
  )
216
+ .option('--interactive', 'interactive mode', false)
182
217
  .option('--tailwind', 'add Tailwind CSS', false)
183
218
  .option<Array<string> | boolean>(
184
219
  '--add-ons [...add-ons]',
@@ -199,16 +234,18 @@ export function cli({
199
234
  )
200
235
  .option('--mcp', 'run the MCP server', false)
201
236
  .option('--mcp-sse', 'run the MCP server in SSE mode', false)
237
+ .option('--ui', 'Add with the UI')
202
238
 
203
239
  program.action(async (projectName: string, options: CliOptions) => {
204
240
  if (options.listAddOns) {
205
241
  await listAddOns(options, {
206
- forcedMode: forcedMode as TemplateOptions,
242
+ forcedMode,
207
243
  forcedAddOns,
244
+ defaultTemplate,
208
245
  })
209
246
  } else if (options.mcp || options.mcpSse) {
210
247
  await runMCPServer(!!options.mcpSse, {
211
- forcedMode: forcedMode as TemplateOptions,
248
+ forcedMode,
212
249
  forcedAddOns,
213
250
  appName,
214
251
  })
@@ -227,26 +264,54 @@ export function cli({
227
264
  cliOptions.template = forcedMode as TemplateOptions
228
265
  }
229
266
 
230
- let finalOptions = await normalizeOptions(
231
- cliOptions,
232
- forcedMode,
233
- forcedAddOns,
234
- )
267
+ let finalOptions: Options | undefined
268
+ if (cliOptions.interactive) {
269
+ cliOptions.addOns = true
270
+ } else {
271
+ finalOptions = await normalizeOptions(
272
+ cliOptions,
273
+ forcedMode,
274
+ forcedAddOns,
275
+ )
276
+ }
277
+
278
+ if (options.ui) {
279
+ const defaultOptions: Options = {
280
+ framework: getFrameworkById(cliOptions.framework || 'react-cra')!,
281
+ mode: 'file-router',
282
+ chosenAddOns: [],
283
+ packageManager: 'pnpm',
284
+ projectName: projectName || 'my-app',
285
+ targetDir: resolve(process.cwd(), projectName || 'my-app'),
286
+ typescript: true,
287
+ tailwind: true,
288
+ git: true,
289
+ }
290
+ launchUI({
291
+ mode: 'setup',
292
+ options: createSerializedOptions(finalOptions || defaultOptions),
293
+ })
294
+ return
295
+ }
296
+
235
297
  if (finalOptions) {
236
298
  intro(`Creating a new ${appName} app in ${projectName}...`)
237
299
  } else {
238
300
  intro(`Let's configure your ${appName} application`)
239
301
  finalOptions = await promptForOptions(cliOptions, {
240
- forcedMode: forcedMode as TemplateOptions,
302
+ forcedMode,
241
303
  forcedAddOns,
242
304
  })
243
305
  }
244
- await createApp(finalOptions, {
245
- environment: createUIEnvironment(),
246
- cwd: options.targetDir || undefined,
247
- name,
248
- appName,
249
- })
306
+
307
+ if (!finalOptions) {
308
+ throw new Error('No options were provided')
309
+ }
310
+
311
+ finalOptions.targetDir =
312
+ options.targetDir || resolve(process.cwd(), finalOptions.projectName)
313
+
314
+ await createApp(environment, finalOptions)
250
315
  } catch (error) {
251
316
  log.error(
252
317
  error instanceof Error ? error.message : 'An unknown error occurred',
@@ -0,0 +1,111 @@
1
+ import { resolve } from 'node:path'
2
+
3
+ import {
4
+ CODE_ROUTER,
5
+ DEFAULT_PACKAGE_MANAGER,
6
+ FILE_ROUTER,
7
+ finalizeAddOns,
8
+ getFrameworkById,
9
+ getPackageManager,
10
+ loadStarter,
11
+ } from '@tanstack/cta-engine'
12
+
13
+ import type { Mode, Options } from '@tanstack/cta-engine'
14
+
15
+ import type { CliOptions } from './types.js'
16
+
17
+ export async function normalizeOptions(
18
+ cliOptions: CliOptions,
19
+ forcedMode?: Mode,
20
+ forcedAddOns?: Array<string>,
21
+ ): Promise<Options | undefined> {
22
+ const projectName = (cliOptions.projectName ?? '').trim()
23
+ if (!projectName) {
24
+ return undefined
25
+ }
26
+
27
+ let typescript =
28
+ cliOptions.template === 'typescript' ||
29
+ cliOptions.template === 'file-router' ||
30
+ cliOptions.framework === 'solid'
31
+
32
+ let tailwind = !!cliOptions.tailwind
33
+ if (cliOptions.framework === 'solid') {
34
+ tailwind = true
35
+ }
36
+
37
+ let mode: typeof FILE_ROUTER | typeof CODE_ROUTER =
38
+ forcedMode || cliOptions.template === 'file-router'
39
+ ? FILE_ROUTER
40
+ : CODE_ROUTER
41
+
42
+ const starter = cliOptions.starter
43
+ ? await loadStarter(cliOptions.starter)
44
+ : undefined
45
+
46
+ if (starter) {
47
+ tailwind = starter.tailwind
48
+ typescript = starter.typescript
49
+ cliOptions.framework = starter.framework
50
+ mode = starter.mode as Mode
51
+ }
52
+
53
+ const framework = getFrameworkById(cliOptions.framework || 'react-cra')!
54
+
55
+ async function selectAddOns() {
56
+ // Edge case for Windows Powershell
57
+ if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
58
+ const parseSeparatedArgs = cliOptions.addOns[0].split(' ')
59
+ if (parseSeparatedArgs.length > 1) {
60
+ cliOptions.addOns = parseSeparatedArgs
61
+ }
62
+ }
63
+
64
+ if (
65
+ Array.isArray(cliOptions.addOns) ||
66
+ starter?.dependsOn ||
67
+ forcedAddOns ||
68
+ cliOptions.toolchain
69
+ ) {
70
+ const selectedAddOns = new Set<string>([
71
+ ...(starter?.dependsOn || []),
72
+ ...(forcedAddOns || []),
73
+ ])
74
+ if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
75
+ for (const a of cliOptions.addOns) {
76
+ selectedAddOns.add(a)
77
+ }
78
+ }
79
+ if (cliOptions.toolchain) {
80
+ selectedAddOns.add(cliOptions.toolchain)
81
+ }
82
+
83
+ return await finalizeAddOns(framework, mode, Array.from(selectedAddOns))
84
+ }
85
+
86
+ return []
87
+ }
88
+
89
+ const chosenAddOns = await selectAddOns()
90
+
91
+ if (chosenAddOns.length) {
92
+ tailwind = true
93
+ typescript = true
94
+ }
95
+
96
+ return {
97
+ projectName: projectName,
98
+ targetDir: resolve(process.cwd(), projectName),
99
+ framework,
100
+ mode,
101
+ typescript,
102
+ tailwind,
103
+ packageManager:
104
+ cliOptions.packageManager ||
105
+ getPackageManager() ||
106
+ DEFAULT_PACKAGE_MANAGER,
107
+ git: !!cliOptions.git,
108
+ chosenAddOns,
109
+ starter: starter,
110
+ }
111
+ }