@tanstack/cta-cli 0.10.0-alpha.19 → 0.10.0-alpha.21

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.19",
3
+ "version": "0.10.0-alpha.21",
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.19",
30
- "@tanstack/cta-custom-add-on": "0.10.0-alpha.19",
31
- "@tanstack/cta-mcp": "0.10.0-alpha.19",
32
- "@tanstack/cta-ui": "0.10.0-alpha.19"
30
+ "express": "^4.21.2",
31
+ "zod": "^3.24.2",
32
+ "@tanstack/cta-ui": "0.10.0-alpha.21",
33
+ "@tanstack/cta-engine": "0.10.0-alpha.21"
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,44 +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
- Options,
27
- PackageManager,
28
- TemplateOptions,
29
- } from '@tanstack/cta-engine'
31
+ import type { Mode, Options, PackageManager } from '@tanstack/cta-engine'
30
32
 
31
- import type { CliOptions } from './types.js'
33
+ import type { CliOptions, TemplateOptions } from './types.js'
32
34
 
33
35
  async function listAddOns(
34
36
  options: CliOptions,
35
37
  {
36
38
  forcedMode,
37
- forcedAddOns = [],
39
+ forcedAddOns,
40
+ defaultTemplate,
38
41
  }: {
39
- forcedMode?: TemplateOptions
40
- forcedAddOns?: Array<string>
42
+ forcedMode?: Mode
43
+ forcedAddOns: Array<string>
44
+ defaultTemplate?: TemplateOptions
41
45
  },
42
46
  ) {
43
47
  const addOns = await getAllAddOns(
44
48
  getFrameworkById(options.framework || 'react-cra')!,
45
- forcedMode || options.template || 'typescript',
49
+ forcedMode ||
50
+ convertTemplateToMode(
51
+ options.template || defaultTemplate || 'javascript',
52
+ ),
46
53
  )
47
54
  for (const addOn of addOns.filter((a) => !forcedAddOns.includes(a.id))) {
48
55
  console.log(`${chalk.bold(addOn.id)}: ${addOn.description}`)
@@ -53,14 +60,16 @@ export function cli({
53
60
  name,
54
61
  appName,
55
62
  forcedMode,
56
- forcedAddOns,
63
+ forcedAddOns = [],
64
+ defaultTemplate = 'javascript',
57
65
  }: {
58
66
  name: string
59
67
  appName: string
60
68
  forcedMode?: Mode
61
69
  forcedAddOns?: Array<string>
70
+ defaultTemplate?: TemplateOptions
62
71
  }) {
63
- const environment = createUIEnvironment()
72
+ const environment = createUIEnvironment(appName, false)
64
73
 
65
74
  const program = new Command()
66
75
 
@@ -79,37 +88,61 @@ export function cli({
79
88
 
80
89
  program
81
90
  .command('add')
82
- .argument('add-on', 'Name of the add-on (or add-ons separated by commas)')
83
- .action(async (addOn: string) => {
84
- await addToApp(
85
- addOn.split(',').map((addon) => addon.trim()),
86
- {
87
- silent: false,
88
- },
89
- environment,
90
- )
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
+ }
91
118
  })
92
119
 
93
120
  const addOnCommand = program.command('add-on')
94
-
95
121
  addOnCommand
96
- .command('update')
97
- .description('Create or update an add-on from the current project')
122
+ .command('init')
123
+ .description('Initialize an add-on from the current project')
98
124
  .action(async () => {
99
- await initAddOn('add-on', environment)
125
+ await initAddOn(environment)
100
126
  })
101
127
  addOnCommand
102
- .command('ui')
103
- .description('Show the add-on developer UI')
128
+ .command('compile')
129
+ .description('Update add-on from the current project')
104
130
  .action(async () => {
105
- launchUI()
131
+ await compileAddOn(environment)
106
132
  })
107
133
 
108
- program
109
- .command('update-starter')
110
- .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')
111
144
  .action(async () => {
112
- await initAddOn('starter', environment)
145
+ await compileStarter(environment)
113
146
  })
114
147
 
115
148
  program.argument('[project-name]', 'name of the project')
@@ -201,16 +234,18 @@ export function cli({
201
234
  )
202
235
  .option('--mcp', 'run the MCP server', false)
203
236
  .option('--mcp-sse', 'run the MCP server in SSE mode', false)
237
+ .option('--ui', 'Add with the UI')
204
238
 
205
239
  program.action(async (projectName: string, options: CliOptions) => {
206
240
  if (options.listAddOns) {
207
241
  await listAddOns(options, {
208
- forcedMode: forcedMode as TemplateOptions,
242
+ forcedMode,
209
243
  forcedAddOns,
244
+ defaultTemplate,
210
245
  })
211
246
  } else if (options.mcp || options.mcpSse) {
212
247
  await runMCPServer(!!options.mcpSse, {
213
- forcedMode: forcedMode as TemplateOptions,
248
+ forcedMode,
214
249
  forcedAddOns,
215
250
  appName,
216
251
  })
@@ -240,22 +275,43 @@ export function cli({
240
275
  )
241
276
  }
242
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
+
243
297
  if (finalOptions) {
244
298
  intro(`Creating a new ${appName} app in ${projectName}...`)
245
299
  } else {
246
300
  intro(`Let's configure your ${appName} application`)
247
301
  finalOptions = await promptForOptions(cliOptions, {
248
- forcedMode: forcedMode as TemplateOptions,
302
+ forcedMode,
249
303
  forcedAddOns,
250
304
  })
251
305
  }
252
306
 
253
- await createApp(finalOptions!, {
254
- environment: createUIEnvironment(),
255
- cwd: options.targetDir || undefined,
256
- name,
257
- appName,
258
- })
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)
259
315
  } catch (error) {
260
316
  log.error(
261
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
+ }