@tanstack/cli 0.60.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 (66) hide show
  1. package/dist/cli.js +266 -11
  2. package/dist/command-line.js +103 -8
  3. package/dist/discovery.js +144 -0
  4. package/dist/options.js +35 -2
  5. package/dist/types/command-line.d.ts +7 -0
  6. package/dist/types/{mcp/types.d.ts → discovery.d.ts} +23 -75
  7. package/dist/types/types.d.ts +1 -2
  8. package/dist/types/ui-prompts.d.ts +5 -0
  9. package/dist/ui-prompts.js +26 -0
  10. package/package.json +6 -5
  11. package/skills/CHANGELOG.md +18 -0
  12. package/skills/add-addons-existing-app/SKILL.md +113 -0
  13. package/skills/choose-ecosystem-integrations/SKILL.md +140 -0
  14. package/skills/choose-ecosystem-integrations/references/authentication-providers.md +19 -0
  15. package/skills/choose-ecosystem-integrations/references/data-layer-providers.md +20 -0
  16. package/skills/choose-ecosystem-integrations/references/deployment-targets.md +19 -0
  17. package/skills/create-app-scaffold/SKILL.md +132 -0
  18. package/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md +34 -0
  19. package/skills/create-app-scaffold/references/deployment-providers.md +19 -0
  20. package/skills/create-app-scaffold/references/framework-adapters.md +17 -0
  21. package/skills/create-app-scaffold/references/toolchains.md +17 -0
  22. package/skills/maintain-custom-addons-dev-watch/SKILL.md +118 -0
  23. package/skills/query-docs-library-metadata/SKILL.md +85 -0
  24. package/skills/query-docs-library-metadata/references/discovery-command-output-schemas.md +70 -0
  25. package/CHANGELOG.md +0 -787
  26. package/dist/mcp/api.js +0 -31
  27. package/dist/mcp/tools.js +0 -250
  28. package/dist/mcp/types.js +0 -37
  29. package/dist/mcp.js +0 -181
  30. package/dist/types/mcp/api.d.ts +0 -4
  31. package/dist/types/mcp/tools.d.ts +0 -2
  32. package/dist/types/mcp.d.ts +0 -5
  33. package/playwright-report/index.html +0 -85
  34. package/playwright.config.ts +0 -21
  35. package/src/bin.ts +0 -15
  36. package/src/cli.ts +0 -767
  37. package/src/command-line.ts +0 -473
  38. package/src/dev-watch.ts +0 -564
  39. package/src/file-syncer.ts +0 -263
  40. package/src/index.ts +0 -21
  41. package/src/mcp/api.ts +0 -42
  42. package/src/mcp/tools.ts +0 -323
  43. package/src/mcp/types.ts +0 -46
  44. package/src/mcp.ts +0 -263
  45. package/src/options.ts +0 -234
  46. package/src/types.ts +0 -28
  47. package/src/ui-environment.ts +0 -74
  48. package/src/ui-prompts.ts +0 -355
  49. package/src/utils.ts +0 -30
  50. package/test-results/.last-run.json +0 -4
  51. package/tests/command-line.test.ts +0 -622
  52. package/tests/index.test.ts +0 -9
  53. package/tests/mcp.test.ts +0 -225
  54. package/tests/options.test.ts +0 -216
  55. package/tests/setupVitest.ts +0 -6
  56. package/tests/ui-environment.test.ts +0 -97
  57. package/tests/ui-prompts.test.ts +0 -205
  58. package/tests-e2e/addons-smoke.spec.ts +0 -31
  59. package/tests-e2e/create-smoke.spec.ts +0 -39
  60. package/tests-e2e/helpers.ts +0 -526
  61. package/tests-e2e/matrix-opportunistic.spec.ts +0 -142
  62. package/tests-e2e/router-only-smoke.spec.ts +0 -68
  63. package/tests-e2e/solid-smoke.spec.ts +0 -25
  64. package/tests-e2e/templates-smoke.spec.ts +0 -52
  65. package/tsconfig.json +0 -17
  66. package/vitest.config.js +0 -8
package/src/dev-watch.ts DELETED
@@ -1,564 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { spawn } from 'node:child_process'
4
-
5
- import chokidar from 'chokidar'
6
- import chalk from 'chalk'
7
- import { temporaryDirectory } from 'tempy'
8
- import {
9
- createApp,
10
- finalizeAddOns,
11
- getFrameworkById,
12
- registerFramework,
13
- scanAddOnDirectories,
14
- scanProjectDirectory,
15
- } from '@tanstack/create'
16
- import { FileSyncer } from './file-syncer.js'
17
- import { createUIEnvironment } from './ui-environment.js'
18
- import type {
19
- Environment,
20
- Framework,
21
- FrameworkDefinition,
22
- Options,
23
- } from '@tanstack/create'
24
- import type { FSWatcher } from 'chokidar'
25
-
26
- export interface DevWatchOptions {
27
- watchPath: string
28
- targetDir: string
29
- framework: Framework
30
- cliOptions: Options
31
- packageManager: string
32
- runDevCommand?: boolean
33
- environment: Environment
34
- frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
35
- }
36
-
37
- interface ChangeEvent {
38
- type: 'add' | 'change' | 'unlink'
39
- path: string
40
- relativePath: string
41
- timestamp: number
42
- }
43
-
44
- class DebounceQueue {
45
- private timer: NodeJS.Timeout | null = null
46
- private changes: Set<string> = new Set()
47
- private callback: (changes: Set<string>) => void
48
-
49
- constructor(
50
- callback: (changes: Set<string>) => void,
51
- private delay: number = 1000,
52
- ) {
53
- this.callback = callback
54
- }
55
-
56
- add(path: string): void {
57
- this.changes.add(path)
58
-
59
- if (this.timer) {
60
- clearTimeout(this.timer)
61
- }
62
-
63
- this.timer = setTimeout(() => {
64
- const currentChanges = new Set(this.changes)
65
- this.callback(currentChanges)
66
- this.changes.clear()
67
- }, this.delay)
68
- }
69
-
70
- size(): number {
71
- return this.changes.size
72
- }
73
-
74
- clear(): void {
75
- if (this.timer) {
76
- clearTimeout(this.timer)
77
- this.timer = null
78
- }
79
- this.changes.clear()
80
- }
81
- }
82
-
83
- export class DevWatchManager {
84
- private watcher: FSWatcher | null = null
85
- private debounceQueue: DebounceQueue
86
- private syncer: FileSyncer
87
- private tempDir: string | null = null
88
- private isBuilding = false
89
- private buildCount = 0
90
- private appDevProcess: ReturnType<typeof spawn> | null = null
91
- private lastSyncedSourceFiles: Set<string> | null = null
92
-
93
- constructor(private options: DevWatchOptions) {
94
- this.syncer = new FileSyncer()
95
- this.debounceQueue = new DebounceQueue((changes) => this.rebuild(changes))
96
- }
97
-
98
- async start(): Promise<void> {
99
- // Validate watch path
100
- if (!fs.existsSync(this.options.watchPath)) {
101
- throw new Error(`Watch path does not exist: ${this.options.watchPath}`)
102
- }
103
-
104
- // Validate target directory exists (should have been created by createApp)
105
- if (!fs.existsSync(this.options.targetDir)) {
106
- throw new Error(
107
- `Target directory does not exist: ${this.options.targetDir}`,
108
- )
109
- }
110
-
111
- if (this.options.cliOptions.install === false) {
112
- throw new Error('Cannot use the --no-install flag when using --dev-watch')
113
- }
114
-
115
- // Log startup with tree style
116
- console.log()
117
- console.log(chalk.bold('dev-watch'))
118
- this.log.tree('', `watching: ${chalk.cyan(this.options.watchPath)}`)
119
- this.log.tree('', `target: ${chalk.cyan(this.options.targetDir)}`)
120
- if (this.options.runDevCommand) {
121
- this.log.tree('', `app dev server: ${chalk.cyan('enabled')}`)
122
- }
123
- this.log.tree('', 'ready', true)
124
-
125
- // Setup signal handlers
126
- process.on('SIGINT', () => this.cleanup())
127
- process.on('SIGTERM', () => this.cleanup())
128
-
129
- // Start watching
130
- this.startWatcher()
131
-
132
- if (this.options.runDevCommand) {
133
- this.startAppDevServer()
134
- }
135
- }
136
-
137
- async stop(): Promise<void> {
138
- console.log()
139
- this.log.info('Stopping dev watch mode...')
140
-
141
- if (this.watcher) {
142
- await this.watcher.close()
143
- this.watcher = null
144
- }
145
-
146
- this.debounceQueue.clear()
147
- this.cleanup()
148
- }
149
-
150
- private startWatcher(): void {
151
- const watcherConfig = {
152
- ignored: [
153
- '**/node_modules/**',
154
- '**/.git/**',
155
- '**/dist/**',
156
- '**/build/**',
157
- '**/.DS_Store',
158
- '**/*.log',
159
- this.tempDir!,
160
- ],
161
- persistent: true,
162
- ignoreInitial: true,
163
- awaitWriteFinish: {
164
- stabilityThreshold: 100,
165
- pollInterval: 100,
166
- },
167
- }
168
-
169
- this.watcher = chokidar.watch(this.options.watchPath, watcherConfig)
170
-
171
- this.watcher.on('add', (filePath) => this.handleChange('add', filePath))
172
- this.watcher.on('change', (filePath) =>
173
- this.handleChange('change', filePath),
174
- )
175
- this.watcher.on('unlink', (filePath) =>
176
- this.handleChange('unlink', filePath),
177
- )
178
- this.watcher.on('error', (error) =>
179
- this.log.error(`Watcher error: ${error.message}`),
180
- )
181
-
182
- this.watcher.on('ready', () => {
183
- // Already shown in startup, no need to repeat
184
- })
185
- }
186
-
187
- private handleChange(_type: ChangeEvent['type'], filePath: string): void {
188
- const relativePath = path.relative(this.options.watchPath, filePath)
189
- // Log change only once for the first file in debounce queue
190
- if (this.debounceQueue.size() === 0) {
191
- this.log.section('change detected')
192
- this.log.subsection(`└─ ${relativePath}`)
193
- } else {
194
- this.log.subsection(`└─ ${relativePath}`)
195
- }
196
- this.debounceQueue.add(filePath)
197
- }
198
-
199
- private async rebuild(changes: Set<string>): Promise<void> {
200
- if (this.isBuilding) {
201
- this.log.warning('Build already in progress, skipping...')
202
- return
203
- }
204
-
205
- this.isBuilding = true
206
- this.buildCount++
207
- const buildId = this.buildCount
208
-
209
- try {
210
- this.log.section(`build #${buildId}`)
211
- const startTime = Date.now()
212
-
213
- let refreshedFramework: FrameworkDefinition | null | undefined =
214
- this.createFrameworkDefinitionFromWatchPath()
215
-
216
- if (!refreshedFramework && this.options.frameworkDefinitionInitializers) {
217
- const refreshedFrameworks =
218
- this.options.frameworkDefinitionInitializers.map(
219
- (frameworkInitalizer) => frameworkInitalizer(),
220
- )
221
-
222
- refreshedFramework = refreshedFrameworks.find(
223
- (f) => f.id === this.options.framework.id,
224
- )
225
- }
226
-
227
- if (!refreshedFramework) {
228
- throw new Error(
229
- 'Could not refresh framework from watch path or framework initializers',
230
- )
231
- }
232
-
233
- // Update the chosen addons to use the latest code
234
- const chosenAddonIds = this.options.cliOptions.chosenAddOns.map(
235
- (m) => m.id,
236
- )
237
- // Create temp directory for this build using tempy
238
- this.tempDir = temporaryDirectory()
239
-
240
- // Register the scanned framework
241
- registerFramework({
242
- ...refreshedFramework,
243
- id: `${refreshedFramework.id}-updated`,
244
- })
245
-
246
- // Get the registered framework
247
- const registeredFramework = getFrameworkById(
248
- `${refreshedFramework.id}-updated`,
249
- )
250
- if (!registeredFramework) {
251
- throw new Error(
252
- `Failed to register framework: ${this.options.framework.id}`,
253
- )
254
- }
255
-
256
- const updatedChosenAddons = await finalizeAddOns(
257
- registeredFramework,
258
- this.options.cliOptions.mode,
259
- chosenAddonIds,
260
- )
261
-
262
- // Check if package metadata was modified
263
- const packageMetadataChanged = Array.from(changes).some((filePath) => {
264
- const normalized = filePath.replace(/\\/g, '/')
265
- return /(^|\/)package\.json(\.ejs)?$/.test(normalized)
266
- })
267
-
268
- const updatedOptions: Options = {
269
- ...this.options.cliOptions,
270
- chosenAddOns: updatedChosenAddons,
271
- framework: registeredFramework,
272
- targetDir: this.tempDir,
273
- git: false,
274
- install: packageMetadataChanged,
275
- }
276
-
277
- // Show package installation indicator if needed
278
- if (packageMetadataChanged) {
279
- this.log.tree(' ', `${chalk.yellow('⟳')} installing packages...`)
280
- }
281
-
282
- // Create app in temp directory with silent environment
283
- const silentEnvironment = createUIEnvironment(
284
- this.options.environment.appName,
285
- true,
286
- )
287
- await createApp(silentEnvironment, updatedOptions)
288
-
289
- // Sync files to target directory
290
- const syncResult = await this.syncer.sync(this.tempDir, this.options.targetDir, {
291
- deleteRemoved: this.lastSyncedSourceFiles !== null,
292
- previousSourceFiles: this.lastSyncedSourceFiles ?? undefined,
293
- })
294
- this.lastSyncedSourceFiles = new Set(syncResult.sourceFiles)
295
-
296
- // Clean up temp directory after sync is complete
297
- try {
298
- await fs.promises.rm(this.tempDir, { recursive: true, force: true })
299
- } catch (cleanupError) {
300
- this.log.warning(
301
- `Failed to clean up temp directory: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`,
302
- )
303
- }
304
-
305
- const elapsed = Date.now() - startTime
306
-
307
- // Build tree-style summary
308
- this.log.tree(' ', `duration: ${chalk.cyan(elapsed + 'ms')}`)
309
-
310
- if (packageMetadataChanged) {
311
- this.log.tree(' ', `packages: ${chalk.green('✓ installed')}`)
312
- }
313
-
314
- // Always show the last item in tree without checking for files to show
315
- const noMoreTreeItems =
316
- syncResult.updated.length === 0 &&
317
- syncResult.created.length === 0 &&
318
- syncResult.deleted.length === 0 &&
319
- syncResult.errors.length === 0
320
-
321
- if (syncResult.updated.length > 0) {
322
- this.log.tree(
323
- ' ',
324
- `updated: ${chalk.green(syncResult.updated.length + ' file' + (syncResult.updated.length > 1 ? 's' : ''))}`,
325
- syncResult.created.length === 0 &&
326
- syncResult.deleted.length === 0 &&
327
- syncResult.errors.length === 0,
328
- )
329
- }
330
- if (syncResult.created.length > 0) {
331
- this.log.tree(
332
- ' ',
333
- `created: ${chalk.green(syncResult.created.length + ' file' + (syncResult.created.length > 1 ? 's' : ''))}`,
334
- syncResult.deleted.length === 0 && syncResult.errors.length === 0,
335
- )
336
- }
337
- if (syncResult.deleted.length > 0) {
338
- this.log.tree(
339
- ' ',
340
- `deleted: ${chalk.green(syncResult.deleted.length + ' file' + (syncResult.deleted.length > 1 ? 's' : ''))}`,
341
- syncResult.errors.length === 0,
342
- )
343
- }
344
- if (syncResult.errors.length > 0) {
345
- this.log.tree(
346
- ' ',
347
- `failed: ${chalk.red(syncResult.errors.length + ' file' + (syncResult.errors.length > 1 ? 's' : ''))}`,
348
- true,
349
- )
350
- }
351
-
352
- // If nothing changed, show that
353
- if (noMoreTreeItems) {
354
- this.log.tree(' ', `no changes`, true)
355
- }
356
-
357
- // Always show changed files with diffs
358
- if (syncResult.updated.length > 0) {
359
- syncResult.updated.forEach((update, index) => {
360
- const isLastFile =
361
- index === syncResult.updated.length - 1 &&
362
- syncResult.created.length === 0
363
-
364
- // For files with diffs, always use ├─
365
- const fileIsLast = isLastFile && !update.diff
366
- this.log.treeItem(' ', update.path, fileIsLast)
367
-
368
- // Always show diff if available
369
- if (update.diff) {
370
- const diffLines = update.diff.split('\n')
371
- const relevantLines = diffLines
372
- .slice(4)
373
- .filter(
374
- (line) =>
375
- line.startsWith('+') ||
376
- line.startsWith('-') ||
377
- line.startsWith('@'),
378
- )
379
-
380
- if (relevantLines.length > 0) {
381
- // Always use │ to continue the tree line through the diff
382
- const prefix = ' │ '
383
- relevantLines.forEach((line) => {
384
- if (line.startsWith('+') && !line.startsWith('+++')) {
385
- console.log(chalk.gray(prefix) + ' ' + chalk.green(line))
386
- } else if (line.startsWith('-') && !line.startsWith('---')) {
387
- console.log(chalk.gray(prefix) + ' ' + chalk.red(line))
388
- } else if (line.startsWith('@')) {
389
- console.log(chalk.gray(prefix) + ' ' + chalk.cyan(line))
390
- }
391
- })
392
- }
393
- }
394
- })
395
- }
396
-
397
- // Show created files
398
- if (syncResult.created.length > 0) {
399
- syncResult.created.forEach((file, index) => {
400
- const isLast =
401
- index === syncResult.created.length - 1 && syncResult.deleted.length === 0
402
- this.log.treeItem(' ', `${chalk.green('+')} ${file}`, isLast)
403
- })
404
- }
405
-
406
- if (syncResult.deleted.length > 0) {
407
- syncResult.deleted.forEach((file, index) => {
408
- const isLast = index === syncResult.deleted.length - 1
409
- this.log.treeItem(' ', `${chalk.red('-')} ${file}`, isLast)
410
- })
411
- }
412
-
413
- // Always show errors
414
- if (syncResult.errors.length > 0) {
415
- console.log() // Add spacing
416
- syncResult.errors.forEach((err, index) => {
417
- this.log.tree(
418
- ' ',
419
- `${chalk.red('error:')} ${err}`,
420
- index === syncResult.errors.length - 1,
421
- )
422
- })
423
- }
424
- } catch (error) {
425
- this.log.error(
426
- `Build #${buildId} failed: ${error instanceof Error ? error.message : String(error)}`,
427
- )
428
- } finally {
429
- this.isBuilding = false
430
- }
431
- }
432
-
433
- private createFrameworkDefinitionFromWatchPath(): FrameworkDefinition | null {
434
- const frameworkRoot = this.options.watchPath
435
- const projectDirectory = path.join(frameworkRoot, 'project')
436
- const baseDirectory = path.join(projectDirectory, 'base')
437
-
438
- if (!fs.existsSync(projectDirectory) || !fs.existsSync(baseDirectory)) {
439
- return null
440
- }
441
-
442
- const addOnDirectoryCandidates = [
443
- path.join(frameworkRoot, 'add-ons'),
444
- path.join(frameworkRoot, 'toolchains'),
445
- path.join(frameworkRoot, 'examples'),
446
- path.join(frameworkRoot, 'hosts'),
447
- ]
448
-
449
- const addOnDirectories = addOnDirectoryCandidates.filter((dir) =>
450
- fs.existsSync(dir),
451
- )
452
-
453
- const addOns =
454
- addOnDirectories.length > 0 ? scanAddOnDirectories(addOnDirectories) : []
455
- const { files, basePackageJSON, optionalPackages } = scanProjectDirectory(
456
- projectDirectory,
457
- baseDirectory,
458
- )
459
-
460
- return {
461
- id: this.options.framework.id,
462
- name: this.options.framework.name,
463
- description: this.options.framework.description,
464
- version: this.options.framework.version,
465
- base: files,
466
- addOns,
467
- basePackageJSON,
468
- optionalPackages,
469
- supportedModes: this.options.framework.supportedModes,
470
- }
471
- }
472
-
473
- private cleanup(): void {
474
- console.log()
475
- console.log('Cleaning up...')
476
-
477
- // Clean up temp directory
478
- if (this.tempDir && fs.existsSync(this.tempDir)) {
479
- try {
480
- fs.rmSync(this.tempDir, { recursive: true, force: true })
481
- } catch (error) {
482
- this.log.error(
483
- `Failed to clean up temp directory: ${error instanceof Error ? error.message : String(error)}`,
484
- )
485
- }
486
- }
487
-
488
- if (this.appDevProcess && !this.appDevProcess.killed) {
489
- this.appDevProcess.kill('SIGTERM')
490
- this.appDevProcess = null
491
- }
492
-
493
- process.exit(0)
494
- }
495
-
496
- private startAppDevServer(): void {
497
- if (this.appDevProcess) {
498
- return
499
- }
500
-
501
- const { command, args } = this.getDevCommandForPackageManager(
502
- this.options.packageManager,
503
- )
504
-
505
- this.log.section('app dev server')
506
- this.log.tree(' ', `starting: ${chalk.cyan([command, ...args].join(' '))}`)
507
-
508
- this.appDevProcess = spawn(command, args, {
509
- cwd: this.options.targetDir,
510
- stdio: 'inherit',
511
- shell: process.platform === 'win32',
512
- env: process.env,
513
- })
514
-
515
- this.appDevProcess.on('exit', (code, signal) => {
516
- if (signal) {
517
- this.log.warning(`app dev server exited via signal ${signal}`)
518
- } else if (code && code !== 0) {
519
- this.log.warning(`app dev server exited with code ${code}`)
520
- }
521
- this.appDevProcess = null
522
- })
523
-
524
- this.appDevProcess.on('error', (error) => {
525
- this.log.error(`Failed to start app dev server: ${error.message}`)
526
- this.appDevProcess = null
527
- })
528
- }
529
-
530
- private getDevCommandForPackageManager(packageManager: string): {
531
- command: string
532
- args: Array<string>
533
- } {
534
- switch (packageManager) {
535
- case 'npm':
536
- return { command: 'npm', args: ['run', 'dev'] }
537
- case 'yarn':
538
- return { command: 'yarn', args: ['dev'] }
539
- case 'bun':
540
- return { command: 'bun', args: ['run', 'dev'] }
541
- case 'deno':
542
- return { command: 'deno', args: ['task', 'dev'] }
543
- default:
544
- return { command: 'pnpm', args: ['dev'] }
545
- }
546
- }
547
-
548
- private log = {
549
- tree: (prefix: string, msg: string, isLast = false) => {
550
- const connector = isLast ? '└─' : '├─'
551
- console.log(chalk.gray(prefix + connector) + ' ' + msg)
552
- },
553
- treeItem: (prefix: string, msg: string, isLast = false) => {
554
- const connector = isLast ? '└─' : '├─'
555
- console.log(chalk.gray(prefix + ' ' + connector) + ' ' + msg)
556
- },
557
- info: (msg: string) => console.log(msg),
558
- error: (msg: string) => console.error(chalk.red('✗') + ' ' + msg),
559
- success: (msg: string) => console.log(chalk.green('✓') + ' ' + msg),
560
- warning: (msg: string) => console.log(chalk.yellow('⚠') + ' ' + msg),
561
- section: (title: string) => console.log('\n' + chalk.bold('▸ ' + title)),
562
- subsection: (msg: string) => console.log(' ' + msg),
563
- }
564
- }