@tanstack/cli 0.61.1 → 0.62.0

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 (46) hide show
  1. package/package.json +5 -1
  2. package/skills/CHANGELOG.md +18 -0
  3. package/skills/add-addons-existing-app/SKILL.md +113 -0
  4. package/skills/choose-ecosystem-integrations/SKILL.md +140 -0
  5. package/skills/choose-ecosystem-integrations/references/authentication-providers.md +19 -0
  6. package/skills/choose-ecosystem-integrations/references/data-layer-providers.md +20 -0
  7. package/skills/choose-ecosystem-integrations/references/deployment-targets.md +19 -0
  8. package/skills/create-app-scaffold/SKILL.md +132 -0
  9. package/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md +34 -0
  10. package/skills/create-app-scaffold/references/deployment-providers.md +19 -0
  11. package/skills/create-app-scaffold/references/framework-adapters.md +17 -0
  12. package/skills/create-app-scaffold/references/toolchains.md +17 -0
  13. package/skills/maintain-custom-addons-dev-watch/SKILL.md +118 -0
  14. package/skills/query-docs-library-metadata/SKILL.md +85 -0
  15. package/skills/query-docs-library-metadata/references/discovery-command-output-schemas.md +70 -0
  16. package/CHANGELOG.md +0 -815
  17. package/playwright-report/index.html +0 -85
  18. package/playwright.config.ts +0 -21
  19. package/src/bin.ts +0 -15
  20. package/src/cli.ts +0 -1099
  21. package/src/command-line.ts +0 -612
  22. package/src/dev-watch.ts +0 -564
  23. package/src/discovery.ts +0 -209
  24. package/src/file-syncer.ts +0 -263
  25. package/src/index.ts +0 -21
  26. package/src/options.ts +0 -280
  27. package/src/types.ts +0 -27
  28. package/src/ui-environment.ts +0 -74
  29. package/src/ui-prompts.ts +0 -387
  30. package/src/utils.ts +0 -30
  31. package/test-results/.last-run.json +0 -4
  32. package/tests/command-line.test.ts +0 -703
  33. package/tests/index.test.ts +0 -9
  34. package/tests/options.test.ts +0 -281
  35. package/tests/setupVitest.ts +0 -6
  36. package/tests/ui-environment.test.ts +0 -97
  37. package/tests/ui-prompts.test.ts +0 -233
  38. package/tests-e2e/addons-smoke.spec.ts +0 -31
  39. package/tests-e2e/create-smoke.spec.ts +0 -39
  40. package/tests-e2e/helpers.ts +0 -526
  41. package/tests-e2e/matrix-opportunistic.spec.ts +0 -142
  42. package/tests-e2e/router-only-smoke.spec.ts +0 -54
  43. package/tests-e2e/solid-smoke.spec.ts +0 -26
  44. package/tests-e2e/templates-smoke.spec.ts +0 -52
  45. package/tsconfig.json +0 -17
  46. package/vitest.config.js +0 -8
package/src/cli.ts DELETED
@@ -1,1099 +0,0 @@
1
- import fs from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import { Command, InvalidArgumentError } from 'commander'
4
- import { cancel, confirm, intro, isCancel, log } from '@clack/prompts'
5
- import chalk from 'chalk'
6
- import semver from 'semver'
7
-
8
- import {
9
- SUPPORTED_PACKAGE_MANAGERS,
10
- addToApp,
11
- compileAddOn,
12
- compileStarter,
13
- createApp,
14
- devAddOn,
15
- getAllAddOns,
16
- getFrameworkByName,
17
- getFrameworks,
18
- initAddOn,
19
- initStarter,
20
- } from '@tanstack/create'
21
- import {
22
- LIBRARY_GROUPS,
23
- fetchDocContent,
24
- fetchLibraries,
25
- fetchPartners,
26
- searchTanStackDocs,
27
- } from './discovery.js'
28
-
29
- import { promptForAddOns, promptForCreateOptions } from './options.js'
30
- import {
31
- normalizeOptions,
32
- validateDevWatchOptions,
33
- validateLegacyCreateFlags,
34
- } from './command-line.js'
35
-
36
- import { createUIEnvironment } from './ui-environment.js'
37
- import { DevWatchManager } from './dev-watch.js'
38
-
39
- import type { CliOptions } from './types.js'
40
- import type {
41
- FrameworkDefinition,
42
- Options,
43
- PackageManager,
44
- } from '@tanstack/create'
45
-
46
- // Read version from package.json
47
- const packageJsonPath = new URL('../package.json', import.meta.url)
48
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
49
- const VERSION = packageJson.version
50
-
51
- export function cli({
52
- name,
53
- appName,
54
- forcedAddOns = [],
55
- forcedDeployment,
56
- defaultFramework,
57
- frameworkDefinitionInitializers,
58
- showDeploymentOptions = false,
59
- legacyAutoCreate = false,
60
- defaultRouterOnly = false,
61
- }: {
62
- name: string
63
- appName: string
64
- forcedAddOns?: Array<string>
65
- forcedDeployment?: string
66
- defaultFramework?: string
67
- frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
68
- showDeploymentOptions?: boolean
69
- legacyAutoCreate?: boolean
70
- defaultRouterOnly?: boolean
71
- }) {
72
- const environment = createUIEnvironment(appName, false)
73
-
74
- const program = new Command()
75
-
76
- async function confirmTargetDirectorySafety(
77
- targetDir: string,
78
- forced?: boolean,
79
- ) {
80
- if (forced) {
81
- return
82
- }
83
-
84
- if (!fs.existsSync(targetDir)) {
85
- return
86
- }
87
-
88
- if (!fs.statSync(targetDir).isDirectory()) {
89
- throw new Error(`Target path exists and is not a directory: ${targetDir}`)
90
- }
91
-
92
- if (fs.readdirSync(targetDir).length === 0) {
93
- return
94
- }
95
-
96
- const shouldContinue = await confirm({
97
- message: `Target directory "${targetDir}" already exists and is not empty. Continue anyway?`,
98
- initialValue: false,
99
- })
100
-
101
- if (isCancel(shouldContinue) || !shouldContinue) {
102
- cancel('Operation cancelled.')
103
- process.exit(0)
104
- }
105
- }
106
-
107
- const availableFrameworks = getFrameworks().map((f) => f.name)
108
-
109
- function resolveBuiltInDevWatchPath(frameworkId: string): string {
110
- const candidates = [
111
- resolve(process.cwd(), 'packages/create/src/frameworks', frameworkId),
112
- resolve(process.cwd(), '../create/src/frameworks', frameworkId),
113
- ]
114
-
115
- for (const candidate of candidates) {
116
- if (fs.existsSync(candidate)) {
117
- return candidate
118
- }
119
- }
120
-
121
- return candidates[0]
122
- }
123
-
124
- async function startDevWatchMode(projectName: string, options: CliOptions) {
125
- // Validate dev watch options
126
- const validation = validateDevWatchOptions({ ...options, projectName })
127
- if (!validation.valid) {
128
- console.error(validation.error)
129
- process.exit(1)
130
- }
131
-
132
- // Enter dev watch mode
133
- if (!projectName && !options.targetDir) {
134
- console.error('Project name/target directory is required for dev watch mode')
135
- process.exit(1)
136
- }
137
-
138
- if (!options.framework) {
139
- console.error('Failed to detect framework')
140
- process.exit(1)
141
- }
142
-
143
- const framework = getFrameworkByName(options.framework)
144
- if (!framework) {
145
- console.error('Failed to detect framework')
146
- process.exit(1)
147
- }
148
-
149
- // First, create the app normally using the standard flow
150
- const normalizedOpts = await normalizeOptions(
151
- {
152
- ...options,
153
- projectName,
154
- framework: framework.id,
155
- },
156
- forcedAddOns,
157
- )
158
-
159
- if (!normalizedOpts) {
160
- throw new Error('Failed to normalize options')
161
- }
162
-
163
- normalizedOpts.targetDir =
164
- options.targetDir || resolve(process.cwd(), projectName)
165
-
166
- // Create the initial app with minimal output for dev watch mode
167
- console.log(chalk.bold('\ndev-watch'))
168
- console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`)
169
- if (normalizedOpts.install !== false) {
170
- console.log(
171
- chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...',
172
- )
173
- }
174
- const silentEnvironment = createUIEnvironment(appName, true)
175
- await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force)
176
- await createApp(silentEnvironment, normalizedOpts)
177
- console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
178
-
179
- // Now start the dev watch mode
180
- const manager = new DevWatchManager({
181
- watchPath: options.devWatch!,
182
- targetDir: normalizedOpts.targetDir,
183
- framework,
184
- cliOptions: normalizedOpts,
185
- packageManager: normalizedOpts.packageManager,
186
- runDevCommand: options.runDev,
187
- environment,
188
- frameworkDefinitionInitializers,
189
- })
190
-
191
- await manager.start()
192
- }
193
-
194
- const toolchains = new Set<string>()
195
- for (const framework of getFrameworks()) {
196
- for (const addOn of framework.getAddOns()) {
197
- if (addOn.type === 'toolchain') {
198
- toolchains.add(addOn.id)
199
- }
200
- }
201
- }
202
-
203
- const deployments = new Set<string>()
204
- for (const framework of getFrameworks()) {
205
- for (const addOn of framework.getAddOns()) {
206
- if (addOn.type === 'deployment') {
207
- deployments.add(addOn.id)
208
- }
209
- }
210
- }
211
-
212
- // Mode is always file-router (TanStack Start)
213
- const defaultMode = 'file-router'
214
- const categoryAliases: Record<string, string> = {
215
- db: 'database',
216
- postgres: 'database',
217
- sql: 'database',
218
- login: 'auth',
219
- authentication: 'auth',
220
- hosting: 'deployment',
221
- deploy: 'deployment',
222
- serverless: 'deployment',
223
- errors: 'monitoring',
224
- logging: 'monitoring',
225
- content: 'cms',
226
- 'api-keys': 'api',
227
- grid: 'data-grid',
228
- review: 'code-review',
229
- courses: 'learning',
230
- }
231
-
232
- function printJson(data: unknown) {
233
- console.log(JSON.stringify(data, null, 2))
234
- }
235
-
236
- function parsePositiveInteger(value: string) {
237
- const parsed = Number(value)
238
- if (!Number.isInteger(parsed) || parsed < 1) {
239
- throw new InvalidArgumentError('Value must be a positive integer')
240
- }
241
- return parsed
242
- }
243
-
244
- program
245
- .name(name)
246
- .description(`${appName} CLI`)
247
- .version(VERSION, '-v, --version', 'output the current version')
248
-
249
- // Helper to create the create command action handler
250
- async function handleCreate(projectName: string, options: CliOptions) {
251
- const legacyCreateFlags = validateLegacyCreateFlags(options)
252
- if (legacyCreateFlags.error) {
253
- log.error(legacyCreateFlags.error)
254
- process.exit(1)
255
- }
256
-
257
- for (const warning of legacyCreateFlags.warnings) {
258
- log.warn(warning)
259
- }
260
-
261
- if (options.listAddOns) {
262
- const addOns = await getAllAddOns(
263
- getFrameworkByName(options.framework || defaultFramework || 'React')!,
264
- defaultMode,
265
- )
266
- const visibleAddOns = addOns.filter((a) => !forcedAddOns.includes(a.id))
267
- if (options.json) {
268
- printJson(
269
- visibleAddOns.map((addOn) => ({
270
- id: addOn.id,
271
- name: addOn.name,
272
- description: addOn.description,
273
- type: addOn.type,
274
- category: addOn.category,
275
- phase: addOn.phase,
276
- modes: addOn.modes,
277
- link: addOn.link,
278
- warning: addOn.warning,
279
- exclusive: addOn.exclusive,
280
- dependsOn: addOn.dependsOn,
281
- options: addOn.options,
282
- })),
283
- )
284
- return
285
- }
286
-
287
- let hasConfigurableAddOns = false
288
- for (const addOn of visibleAddOns) {
289
- const hasOptions =
290
- addOn.options && Object.keys(addOn.options).length > 0
291
- const optionMarker = hasOptions ? '*' : ' '
292
- if (hasOptions) hasConfigurableAddOns = true
293
- console.log(
294
- `${optionMarker} ${chalk.bold(addOn.id)}: ${addOn.description}`,
295
- )
296
- }
297
- if (hasConfigurableAddOns) {
298
- console.log('\n* = has configuration options')
299
- }
300
- return
301
- }
302
-
303
- if (options.addonDetails) {
304
- const addOns = await getAllAddOns(
305
- getFrameworkByName(options.framework || defaultFramework || 'React')!,
306
- defaultMode,
307
- )
308
- const addOn =
309
- addOns.find((a) => a.id === options.addonDetails) ??
310
- addOns.find(
311
- (a) =>
312
- a.id.toLowerCase() === options.addonDetails!.toLowerCase(),
313
- )
314
- if (!addOn) {
315
- console.error(`Add-on '${options.addonDetails}' not found`)
316
- process.exit(1)
317
- }
318
-
319
- if (options.json) {
320
- const files = await addOn.getFiles()
321
- printJson({
322
- id: addOn.id,
323
- name: addOn.name,
324
- description: addOn.description,
325
- type: addOn.type,
326
- category: addOn.category,
327
- phase: addOn.phase,
328
- modes: addOn.modes,
329
- link: addOn.link,
330
- warning: addOn.warning,
331
- exclusive: addOn.exclusive,
332
- dependsOn: addOn.dependsOn,
333
- options: addOn.options,
334
- routes: addOn.routes,
335
- packageAdditions: addOn.packageAdditions,
336
- shadcnComponents: addOn.shadcnComponents,
337
- integrations: addOn.integrations,
338
- readme: addOn.readme,
339
- files,
340
- author: addOn.author,
341
- version: addOn.version,
342
- license: addOn.license,
343
- })
344
- return
345
- }
346
-
347
- console.log(
348
- `${chalk.bold.cyan('Add-on Details:')} ${chalk.bold(addOn.name)}`,
349
- )
350
- console.log(`${chalk.bold('ID:')} ${addOn.id}`)
351
- console.log(`${chalk.bold('Description:')} ${addOn.description}`)
352
- console.log(`${chalk.bold('Type:')} ${addOn.type}`)
353
- console.log(`${chalk.bold('Phase:')} ${addOn.phase}`)
354
- console.log(`${chalk.bold('Supported Modes:')} ${addOn.modes.join(', ')}`)
355
-
356
- if (addOn.link) {
357
- console.log(`${chalk.bold('Link:')} ${chalk.blue(addOn.link)}`)
358
- }
359
-
360
- if (addOn.dependsOn && addOn.dependsOn.length > 0) {
361
- console.log(
362
- `${chalk.bold('Dependencies:')} ${addOn.dependsOn.join(', ')}`,
363
- )
364
- }
365
-
366
- if (addOn.options && Object.keys(addOn.options).length > 0) {
367
- console.log(`\n${chalk.bold.yellow('Configuration Options:')}`)
368
- for (const [optionName, option] of Object.entries(addOn.options)) {
369
- if ('type' in option) {
370
- const opt = option as any
371
- console.log(` ${chalk.bold(optionName)}:`)
372
- console.log(` Label: ${opt.label}`)
373
- if (opt.description) {
374
- console.log(` Description: ${opt.description}`)
375
- }
376
- console.log(` Type: ${opt.type}`)
377
- console.log(` Default: ${opt.default}`)
378
- if (opt.type === 'select' && opt.options) {
379
- console.log(` Available values:`)
380
- for (const choice of opt.options) {
381
- console.log(` - ${choice.value}: ${choice.label}`)
382
- }
383
- }
384
- }
385
- }
386
- } else {
387
- console.log(`\n${chalk.gray('No configuration options available')}`)
388
- }
389
-
390
- if (addOn.routes && addOn.routes.length > 0) {
391
- console.log(`\n${chalk.bold.green('Routes:')}`)
392
- for (const route of addOn.routes) {
393
- console.log(` ${chalk.bold(route.url)} (${route.name})`)
394
- console.log(` File: ${route.path}`)
395
- }
396
- }
397
- return
398
- }
399
-
400
- if (options.devWatch) {
401
- await startDevWatchMode(projectName, options)
402
- return
403
- }
404
-
405
- try {
406
- const cliOptions = {
407
- projectName,
408
- ...options,
409
- } as CliOptions
410
-
411
- if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
412
- cliOptions.routerOnly = true
413
- }
414
-
415
- if (
416
- cliOptions.routerOnly !== true &&
417
- cliOptions.template &&
418
- ['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(
419
- cliOptions.template.toLowerCase(),
420
- ) &&
421
- cliOptions.template.toLowerCase() !== 'file-router'
422
- ) {
423
- cliOptions.routerOnly = true
424
- }
425
-
426
- cliOptions.framework = getFrameworkByName(
427
- options.framework || defaultFramework || 'React',
428
- )!.id
429
-
430
- let finalOptions: Options | undefined
431
- if (cliOptions.interactive || cliOptions.addOns === true) {
432
- cliOptions.addOns = true
433
- } else {
434
- finalOptions = await normalizeOptions(
435
- cliOptions,
436
- forcedAddOns,
437
- { forcedDeployment },
438
- )
439
- }
440
-
441
- if (finalOptions) {
442
- intro(`Creating a new ${appName} app in ${projectName}...`)
443
- } else {
444
- intro(`Let's configure your ${appName} application`)
445
- finalOptions = await promptForCreateOptions(cliOptions, {
446
- forcedAddOns,
447
- showDeploymentOptions,
448
- })
449
- }
450
-
451
- if (!finalOptions) {
452
- throw new Error('No options were provided')
453
- }
454
-
455
- ;(finalOptions as Options & { routerOnly?: boolean }).routerOnly =
456
- !!cliOptions.routerOnly
457
-
458
- // Determine target directory:
459
- // 1. Use --target-dir if provided
460
- // 2. Use targetDir from normalizeOptions if set (handles "." case)
461
- // 3. If original projectName was ".", use current directory
462
- // 4. Otherwise, use project name as subdirectory
463
- if (options.targetDir) {
464
- finalOptions.targetDir = options.targetDir
465
- } else if (finalOptions.targetDir) {
466
- // Keep the targetDir from normalizeOptions (handles "." case)
467
- } else if (projectName === '.') {
468
- finalOptions.targetDir = resolve(process.cwd())
469
- } else {
470
- finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName)
471
- }
472
-
473
- await confirmTargetDirectorySafety(finalOptions.targetDir, options.force)
474
- await createApp(environment, finalOptions)
475
- } catch (error) {
476
- log.error(
477
- error instanceof Error ? error.message : 'An unknown error occurred',
478
- )
479
- process.exit(1)
480
- }
481
- }
482
-
483
- // Helper to configure create command options
484
- function configureCreateCommand(cmd: Command) {
485
- cmd.argument('[project-name]', 'name of the project')
486
-
487
- if (!defaultFramework) {
488
- cmd.option<string>(
489
- '--framework <type>',
490
- `project framework (${availableFrameworks.join(', ')})`,
491
- (value) => {
492
- if (value.toLowerCase() === 'react-cra') {
493
- return 'react'
494
- }
495
-
496
- if (
497
- !availableFrameworks.some(
498
- (f) => f.toLowerCase() === value.toLowerCase(),
499
- )
500
- ) {
501
- throw new InvalidArgumentError(
502
- `Invalid framework: ${value}. Only the following are allowed: ${availableFrameworks.join(', ')}`,
503
- )
504
- }
505
- return value
506
- },
507
- defaultFramework || 'React',
508
- )
509
- }
510
-
511
- cmd
512
- .option(
513
- '--starter [url-or-id]',
514
- 'DEPRECATED: use --template. Initializes from a template URL or built-in id',
515
- false,
516
- )
517
- .option('--template-id <id>', 'initialize using a built-in template id')
518
- .option(
519
- '--template [url-or-id]',
520
- 'initialize this project from a template URL or built-in template id',
521
- )
522
- .option('--no-install', 'skip installing dependencies')
523
- .option<PackageManager>(
524
- `--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
525
- `Explicitly tell the CLI to use this package manager`,
526
- (value) => {
527
- if (!SUPPORTED_PACKAGE_MANAGERS.includes(value as PackageManager)) {
528
- throw new InvalidArgumentError(
529
- `Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(
530
- ', ',
531
- )}`,
532
- )
533
- }
534
- return value as PackageManager
535
- },
536
- )
537
- .option(
538
- '--dev-watch <path>',
539
- 'Watch a framework directory for changes and auto-rebuild',
540
- )
541
- .option('--run-dev', 'Run the app dev server alongside dev-watch', false)
542
- .option(
543
- '--router-only',
544
- 'Use router-only compatibility mode (file-based routing without TanStack Start)',
545
- )
546
- .option(
547
- '--tailwind',
548
- 'Deprecated: compatibility flag; Tailwind is always enabled',
549
- )
550
- .option(
551
- '--no-tailwind',
552
- 'Deprecated: compatibility flag; Tailwind opt-out is ignored',
553
- )
554
- .option('--examples', 'include demo/example pages')
555
- .option('--no-examples', 'exclude demo/example pages')
556
-
557
- if (deployments.size > 0) {
558
- cmd.option<string>(
559
- `--deployment <${Array.from(deployments).join('|')}>`,
560
- `Explicitly tell the CLI to use this deployment adapter`,
561
- (value) => {
562
- if (!deployments.has(value)) {
563
- throw new InvalidArgumentError(
564
- `Invalid adapter: ${value}. The following are allowed: ${Array.from(
565
- deployments,
566
- ).join(', ')}`,
567
- )
568
- }
569
- return value
570
- },
571
- )
572
- }
573
-
574
- if (toolchains.size > 0) {
575
- cmd
576
- .option<string>(
577
- `--toolchain <${Array.from(toolchains).join('|')}>`,
578
- `Explicitly tell the CLI to use this toolchain`,
579
- (value) => {
580
- if (!toolchains.has(value)) {
581
- throw new InvalidArgumentError(
582
- `Invalid toolchain: ${value}. The following are allowed: ${Array.from(
583
- toolchains,
584
- ).join(', ')}`,
585
- )
586
- }
587
- return value
588
- },
589
- )
590
- .option('--no-toolchain', 'skip toolchain selection')
591
- }
592
-
593
- cmd
594
- .option('--interactive', 'interactive mode', false)
595
- .option<Array<string> | boolean>(
596
- '--add-ons [...add-ons]',
597
- 'pick from a list of available add-ons (comma separated list)',
598
- (value: string) => {
599
- let addOns: Array<string> | boolean = !!value
600
- if (typeof value === 'string') {
601
- addOns = value.split(',').map((addon) => addon.trim())
602
- }
603
- return addOns
604
- },
605
- )
606
- .option('--list-add-ons', 'list all available add-ons', false)
607
- .option(
608
- '--addon-details <addon-id>',
609
- 'show detailed information about a specific add-on',
610
- )
611
- .option('--json', 'output JSON for automation', false)
612
- .option('--git', 'create a git repository')
613
- .option('--no-git', 'do not create a git repository')
614
- .option(
615
- '--target-dir <path>',
616
- 'the target directory for the application root',
617
- )
618
- .option(
619
- '--add-on-config <config>',
620
- 'JSON string with add-on configuration options',
621
- )
622
- .option(
623
- '-f, --force',
624
- 'force project creation even if the target directory is not empty',
625
- false,
626
- )
627
-
628
- return cmd
629
- }
630
-
631
- // === CREATE SUBCOMMAND ===
632
- // Creates a TanStack Start app (file-router mode).
633
- const createCommand = program
634
- .command('create')
635
- .description(`Create a new TanStack Start application`)
636
-
637
- configureCreateCommand(createCommand)
638
- createCommand.action(handleCreate)
639
-
640
- // === DEV SUBCOMMAND ===
641
- const devCommand = program
642
- .command('dev')
643
- .description(
644
- 'Create a sandbox app and watch built-in framework templates/add-ons',
645
- )
646
-
647
- configureCreateCommand(devCommand)
648
- devCommand.action(async (projectName: string, options: CliOptions) => {
649
- const frameworkName = options.framework || defaultFramework || 'React'
650
- const framework = getFrameworkByName(frameworkName)
651
- if (!framework) {
652
- console.error(`Unknown framework: ${frameworkName}`)
653
- process.exit(1)
654
- }
655
-
656
- const watchPath = resolveBuiltInDevWatchPath(framework.id)
657
- const devOptions: CliOptions = {
658
- ...options,
659
- framework: framework.name,
660
- devWatch: watchPath,
661
- runDev: true,
662
- install: options.install ?? true,
663
- }
664
-
665
- await startDevWatchMode(projectName, devOptions)
666
- })
667
-
668
- // === LIBRARIES SUBCOMMAND ===
669
- program
670
- .command('libraries')
671
- .description('List TanStack libraries')
672
- .option(
673
- '--group <group>',
674
- `filter by group (${LIBRARY_GROUPS.join(', ')})`,
675
- )
676
- .option('--json', 'output JSON for automation', false)
677
- .action(async (options: { group?: string; json: boolean }) => {
678
- try {
679
- const data = await fetchLibraries()
680
- let libraries = data.libraries
681
-
682
- if (
683
- options.group &&
684
- Object.prototype.hasOwnProperty.call(data.groups, options.group)
685
- ) {
686
- const groupIds = data.groups[options.group]
687
- libraries = libraries.filter((lib) => groupIds.includes(lib.id))
688
- }
689
-
690
- const groupName = options.group
691
- ? data.groupNames[options.group] || options.group
692
- : 'All Libraries'
693
-
694
- const payload = {
695
- group: groupName,
696
- count: libraries.length,
697
- libraries: libraries.map((lib) => ({
698
- id: lib.id,
699
- name: lib.name,
700
- tagline: lib.tagline,
701
- description: lib.description,
702
- frameworks: lib.frameworks,
703
- latestVersion: lib.latestVersion,
704
- docsUrl: lib.docsUrl,
705
- githubUrl: lib.githubUrl,
706
- })),
707
- }
708
-
709
- if (options.json) {
710
- printJson(payload)
711
- return
712
- }
713
-
714
- console.log(chalk.bold(groupName))
715
- for (const lib of payload.libraries) {
716
- console.log(
717
- `${chalk.bold(lib.id)} (${lib.latestVersion}) - ${lib.tagline}`,
718
- )
719
- }
720
- } catch (error) {
721
- log.error(error instanceof Error ? error.message : String(error))
722
- process.exit(1)
723
- }
724
- })
725
-
726
- // === DOC SUBCOMMAND ===
727
- program
728
- .command('doc')
729
- .description('Fetch a TanStack documentation page')
730
- .argument('<library>', 'library ID (eg. query, router, table)')
731
- .argument('<path>', 'documentation path (eg. framework/react/overview)')
732
- .option('--docs-version <version>', 'docs version (default: latest)', 'latest')
733
- .option('--json', 'output JSON for automation', false)
734
- .action(
735
- async (
736
- libraryId: string,
737
- path: string,
738
- options: { docsVersion: string; json: boolean },
739
- ) => {
740
- try {
741
- const data = await fetchLibraries()
742
- const library = data.libraries.find((l) => l.id === libraryId)
743
-
744
- if (!library) {
745
- throw new Error(
746
- `Library "${libraryId}" not found. Use \`tanstack libraries\` to see available libraries.`,
747
- )
748
- }
749
-
750
- if (
751
- options.docsVersion !== 'latest' &&
752
- !library.availableVersions.includes(options.docsVersion)
753
- ) {
754
- throw new Error(
755
- `Version "${options.docsVersion}" not found for ${library.name}. Available: ${library.availableVersions.join(', ')}`,
756
- )
757
- }
758
-
759
- const branch =
760
- options.docsVersion === 'latest' ||
761
- options.docsVersion === library.latestVersion
762
- ? library.latestBranch || 'main'
763
- : options.docsVersion
764
-
765
- const docsRoot = library.docsRoot || 'docs'
766
- const filePath = `${docsRoot}/${path}.md`
767
- const content = await fetchDocContent(library.repo, branch, filePath)
768
-
769
- if (!content) {
770
- throw new Error(
771
- `Document not found: ${library.name} / ${path} (version: ${options.docsVersion})`,
772
- )
773
- }
774
-
775
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
776
- let title = path.split('/').pop() || 'Untitled'
777
- let docContent = content
778
-
779
- if (frontmatterMatch && frontmatterMatch[1]) {
780
- const frontmatter = frontmatterMatch[1]
781
- const titleMatch = frontmatter.match(
782
- /title:\s*['"]?([^'"\n]+)['"]?/,
783
- )
784
- if (titleMatch && titleMatch[1]) {
785
- title = titleMatch[1]
786
- }
787
- docContent = content.slice(frontmatterMatch[0].length).trim()
788
- }
789
-
790
- const payload = {
791
- title,
792
- content: docContent,
793
- url: `https://tanstack.com/${libraryId}/${options.docsVersion}/docs/${path}`,
794
- library: library.name,
795
- version:
796
- options.docsVersion === 'latest'
797
- ? library.latestVersion
798
- : options.docsVersion,
799
- }
800
-
801
- if (options.json) {
802
- printJson(payload)
803
- return
804
- }
805
-
806
- console.log(chalk.bold(payload.title))
807
- console.log(chalk.blue(payload.url))
808
- console.log('')
809
- console.log(payload.content)
810
- } catch (error) {
811
- log.error(error instanceof Error ? error.message : String(error))
812
- process.exit(1)
813
- }
814
- },
815
- )
816
-
817
- // === SEARCH-DOCS SUBCOMMAND ===
818
- program
819
- .command('search-docs')
820
- .description('Search TanStack documentation')
821
- .argument('<query>', 'search query')
822
- .option('--library <id>', 'filter to specific library')
823
- .option('--framework <name>', 'filter to specific framework')
824
- .option('--limit <n>', 'max results (default: 10, max: 50)', parsePositiveInteger, 10)
825
- .option('--json', 'output JSON for automation', false)
826
- .action(
827
- async (
828
- query: string,
829
- options: {
830
- library?: string
831
- framework?: string
832
- limit: number
833
- json: boolean
834
- },
835
- ) => {
836
- try {
837
- const payload = await searchTanStackDocs({
838
- query,
839
- library: options.library,
840
- framework: options.framework,
841
- limit: options.limit,
842
- })
843
-
844
- if (options.json) {
845
- printJson(payload)
846
- return
847
- }
848
-
849
- for (const result of payload.results) {
850
- console.log(
851
- `${chalk.bold(result.title)} [${result.library}]\n${chalk.blue(result.url)}\n${result.snippet}\n`,
852
- )
853
- }
854
- } catch (error) {
855
- log.error(error instanceof Error ? error.message : String(error))
856
- process.exit(1)
857
- }
858
- },
859
- )
860
-
861
- // === ECOSYSTEM SUBCOMMAND ===
862
- program
863
- .command('ecosystem')
864
- .description('List TanStack ecosystem partners')
865
- .option('--category <category>', 'filter by category')
866
- .option('--library <id>', 'filter by TanStack library')
867
- .option('--json', 'output JSON for automation', false)
868
- .action(
869
- async (options: { category?: string; library?: string; json: boolean }) => {
870
- try {
871
- const data = await fetchPartners()
872
-
873
- let resolvedCategory: string | undefined
874
- if (options.category) {
875
- const normalized = options.category.toLowerCase().trim()
876
- resolvedCategory = categoryAliases[normalized] || normalized
877
- if (!data.categories.includes(resolvedCategory)) {
878
- resolvedCategory = undefined
879
- }
880
- }
881
-
882
- const library = options.library?.toLowerCase().trim()
883
- const partners = data.partners
884
- .filter((partner) =>
885
- resolvedCategory ? partner.category === resolvedCategory : true,
886
- )
887
- .filter((partner) =>
888
- library ? partner.libraries.some((l) => l === library) : true,
889
- )
890
- .map((partner) => ({
891
- id: partner.id,
892
- name: partner.name,
893
- tagline: partner.tagline,
894
- description: partner.description,
895
- category: partner.category,
896
- categoryLabel: partner.categoryLabel,
897
- url: partner.url,
898
- libraries: partner.libraries,
899
- }))
900
-
901
- const payload = {
902
- query: {
903
- category: options.category,
904
- categoryResolved: resolvedCategory,
905
- library: options.library,
906
- },
907
- count: partners.length,
908
- partners,
909
- }
910
-
911
- if (options.json) {
912
- printJson(payload)
913
- return
914
- }
915
-
916
- for (const partner of partners) {
917
- console.log(
918
- `${chalk.bold(partner.name)} [${partner.category}] - ${partner.description}\n${chalk.blue(partner.url)}`,
919
- )
920
- }
921
- } catch (error) {
922
- log.error(error instanceof Error ? error.message : String(error))
923
- process.exit(1)
924
- }
925
- },
926
- )
927
-
928
- // === PIN-VERSIONS SUBCOMMAND ===
929
- program
930
- .command('pin-versions')
931
- .description('Pin versions of the TanStack libraries')
932
- .action(async () => {
933
- if (!fs.existsSync('package.json')) {
934
- console.error('package.json not found')
935
- return
936
- }
937
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'))
938
-
939
- const packages: Record<string, string> = {
940
- '@tanstack/react-router': '',
941
- '@tanstack/router-generator': '',
942
- '@tanstack/react-router-devtools': '',
943
- '@tanstack/react-start': '',
944
- '@tanstack/react-start-config': '',
945
- '@tanstack/router-plugin': '',
946
- '@tanstack/react-start-client': '',
947
- '@tanstack/react-start-plugin': '1.115.0',
948
- '@tanstack/react-start-server': '',
949
- '@tanstack/start-server-core': '1.115.0',
950
- }
951
-
952
- function sortObject(obj: Record<string, string>): Record<string, string> {
953
- return Object.keys(obj)
954
- .sort()
955
- .reduce<Record<string, string>>((acc, key) => {
956
- acc[key] = obj[key]
957
- return acc
958
- }, {})
959
- }
960
-
961
- if (!packageJson.dependencies['@tanstack/react-start']) {
962
- console.error('@tanstack/react-start not found in dependencies')
963
- return
964
- }
965
- let changed = 0
966
- const startVersion = packageJson.dependencies[
967
- '@tanstack/react-start'
968
- ].replace(/^\^/, '')
969
- for (const pkg of Object.keys(packages)) {
970
- if (!packageJson.dependencies[pkg]) {
971
- packageJson.dependencies[pkg] = packages[pkg].length
972
- ? semver.maxSatisfying(
973
- [startVersion, packages[pkg]],
974
- `^${packages[pkg]}`,
975
- )!
976
- : startVersion
977
- changed++
978
- } else {
979
- if (packageJson.dependencies[pkg].startsWith('^')) {
980
- packageJson.dependencies[pkg] = packageJson.dependencies[
981
- pkg
982
- ].replace(/^\^/, '')
983
- changed++
984
- }
985
- }
986
- }
987
- packageJson.dependencies = sortObject(packageJson.dependencies)
988
- if (changed > 0) {
989
- fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2))
990
- console.log(
991
- `${changed} packages updated.
992
-
993
- Remove your node_modules directory and package lock file and re-install.`,
994
- )
995
- } else {
996
- console.log(
997
- 'No changes needed. The relevant TanStack packages are already pinned.',
998
- )
999
- }
1000
- })
1001
-
1002
- // === ADD SUBCOMMAND ===
1003
- program
1004
- .command('add')
1005
- .argument(
1006
- '[add-on...]',
1007
- 'Name of the add-ons (or add-ons separated by spaces or commas)',
1008
- )
1009
- .option('--forced', 'Force the add-on to be added', false)
1010
- .action(async (addOns: Array<string>, options: { forced: boolean }) => {
1011
- const parsedAddOns: Array<string> = []
1012
- for (const addOn of addOns) {
1013
- if (addOn.includes(',') || addOn.includes(' ')) {
1014
- parsedAddOns.push(
1015
- ...addOn.split(/[\s,]+/).map((addon) => addon.trim()),
1016
- )
1017
- } else {
1018
- parsedAddOns.push(addOn.trim())
1019
- }
1020
- }
1021
- if (parsedAddOns.length < 1) {
1022
- const selectedAddOns = await promptForAddOns()
1023
- if (selectedAddOns.length) {
1024
- await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
1025
- forced: options.forced,
1026
- })
1027
- }
1028
- } else {
1029
- await addToApp(environment, parsedAddOns, resolve(process.cwd()), {
1030
- forced: options.forced,
1031
- })
1032
- }
1033
- })
1034
-
1035
- // === ADD-ON SUBCOMMAND ===
1036
- const addOnCommand = program.command('add-on')
1037
- addOnCommand
1038
- .command('init')
1039
- .description('Initialize an add-on from the current project')
1040
- .action(async () => {
1041
- await initAddOn(environment)
1042
- })
1043
- addOnCommand
1044
- .command('compile')
1045
- .description('Update add-on from the current project')
1046
- .action(async () => {
1047
- await compileAddOn(environment)
1048
- })
1049
- addOnCommand
1050
- .command('dev')
1051
- .description(
1052
- 'Watch project files and continuously refresh .add-on and add-on.json',
1053
- )
1054
- .action(async () => {
1055
- await devAddOn(environment)
1056
- })
1057
-
1058
- // === TEMPLATE SUBCOMMAND ===
1059
- const templateCommand = program.command('template')
1060
- templateCommand
1061
- .command('init')
1062
- .description('Initialize a project template from the current project')
1063
- .action(async () => {
1064
- await initStarter(environment)
1065
- })
1066
- templateCommand
1067
- .command('compile')
1068
- .description('Compile the template JSON file for the current project')
1069
- .action(async () => {
1070
- await compileStarter(environment)
1071
- })
1072
-
1073
- // Legacy alias for template command
1074
- const starterCommand = program.command('starter')
1075
- starterCommand
1076
- .command('init')
1077
- .description('Deprecated alias: initialize a project template')
1078
- .action(async () => {
1079
- await initStarter(environment)
1080
- })
1081
- starterCommand
1082
- .command('compile')
1083
- .description('Deprecated alias: compile the template JSON file')
1084
- .action(async () => {
1085
- await compileStarter(environment)
1086
- })
1087
-
1088
- // === LEGACY AUTO-CREATE MODE ===
1089
- // For backward compatibility with cli-aliases (create-tsrouter-app, etc.)
1090
- // If legacyAutoCreate is true and no subcommand is provided, treat the first
1091
- // argument as a project name and auto-invoke create behavior
1092
- if (legacyAutoCreate) {
1093
- // Configure the main program with create options for legacy mode
1094
- configureCreateCommand(program)
1095
- program.action(handleCreate)
1096
- }
1097
-
1098
- program.parse()
1099
- }