@tanstack/cta-cli 0.46.2 → 0.48.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.
package/dist/options.js CHANGED
@@ -1,10 +1,12 @@
1
- import { intro } from '@clack/prompts';
1
+ import fs from 'node:fs';
2
+ import { cancel, confirm, intro, isCancel } from '@clack/prompts';
2
3
  import { finalizeAddOns, getFrameworkById, getPackageManager, populateAddOnOptionsDefaults, readConfigFile, } from '@tanstack/cta-engine';
3
- import { getProjectName, promptForAddOnOptions, selectAddOns, selectGit, selectDeployment, selectPackageManager, selectRouterType, selectTailwind, selectToolchain, selectTypescript, } from './ui-prompts.js';
4
+ import { getProjectName, promptForAddOnOptions, selectAddOns, selectDeployment, selectGit, selectPackageManager, selectRouterType, selectTailwind, selectToolchain, selectTypescript, } from './ui-prompts.js';
4
5
  import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
5
6
  export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], forcedMode, showDeploymentOptions = false, }) {
6
7
  const options = {};
7
8
  options.framework = getFrameworkById(cliOptions.framework || 'react-cra');
9
+ // Validate project name
8
10
  if (cliOptions.projectName) {
9
11
  // Handle "." as project name - use sanitized current directory name
10
12
  if (cliOptions.projectName === '.') {
@@ -22,6 +24,19 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], fo
22
24
  else {
23
25
  options.projectName = await getProjectName();
24
26
  }
27
+ // Check if target directory is empty
28
+ if (!cliOptions.force &&
29
+ fs.existsSync(options.projectName) &&
30
+ fs.readdirSync(options.projectName).length > 0) {
31
+ const shouldContinue = await confirm({
32
+ message: `Target directory ${options.projectName} is not empty. Do you want to continue?`,
33
+ initialValue: true,
34
+ });
35
+ if (isCancel(shouldContinue) || !shouldContinue) {
36
+ cancel('Operation cancelled.');
37
+ process.exit(0);
38
+ }
39
+ }
25
40
  // Router type selection
26
41
  if (forcedMode) {
27
42
  options.mode = forcedMode;
@@ -44,13 +59,6 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], fo
44
59
  if (!options.typescript && options.mode === 'code-router') {
45
60
  options.typescript = await selectTypescript();
46
61
  }
47
- // Tailwind selection
48
- if (!cliOptions.tailwind && options.framework.id === 'react-cra') {
49
- options.tailwind = await selectTailwind();
50
- }
51
- else {
52
- options.tailwind = true;
53
- }
54
62
  // Package manager selection
55
63
  if (cliOptions.packageManager) {
56
64
  options.packageManager = cliOptions.packageManager;
@@ -92,9 +100,27 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], fo
92
100
  }
93
101
  options.chosenAddOns = Array.from(await finalizeAddOns(options.framework, options.mode, Array.from(addOns)));
94
102
  if (options.chosenAddOns.length) {
95
- options.tailwind = true;
96
103
  options.typescript = true;
97
104
  }
105
+ // Tailwind selection
106
+ // Only treat add-ons as requiring tailwind if they explicitly have "tailwind": true
107
+ const addOnsRequireTailwind = options.chosenAddOns.some((addOn) => addOn.tailwind === true);
108
+ if (addOnsRequireTailwind) {
109
+ // If any add-on explicitly requires tailwind, enable it automatically
110
+ options.tailwind = true;
111
+ }
112
+ else if (cliOptions.tailwind !== undefined) {
113
+ // User explicitly provided a CLI flag, respect it
114
+ options.tailwind = !!cliOptions.tailwind;
115
+ }
116
+ else if (options.framework.id === 'react-cra') {
117
+ // Only show prompt for react-cra when no CLI flag and no add-ons require it
118
+ options.tailwind = await selectTailwind();
119
+ }
120
+ else {
121
+ // For other frameworks (like solid), default to true
122
+ options.tailwind = true;
123
+ }
98
124
  // Prompt for add-on options in interactive mode
99
125
  if (Array.isArray(cliOptions.addOns)) {
100
126
  // Non-interactive mode: use defaults
@@ -108,6 +134,9 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], fo
108
134
  options.addOnOptions = { ...defaultOptions, ...userOptions };
109
135
  }
110
136
  options.git = cliOptions.git || (await selectGit());
137
+ if (cliOptions.install === false) {
138
+ options.install = false;
139
+ }
111
140
  return options;
112
141
  }
113
142
  export async function promptForAddOns() {
@@ -1,5 +1,6 @@
1
1
  import type { TemplateOptions } from './types.js';
2
- export declare function cli({ name, appName, forcedMode, forcedAddOns, defaultTemplate, forcedDeployment, defaultFramework, craCompatible, webBase, showDeploymentOptions, }: {
2
+ import type { FrameworkDefinition } from '@tanstack/cta-engine';
3
+ export declare function cli({ name, appName, forcedMode, forcedAddOns, defaultTemplate, forcedDeployment, defaultFramework, craCompatible, webBase, frameworkDefinitionInitializers, showDeploymentOptions, }: {
3
4
  name: string;
4
5
  appName: string;
5
6
  forcedMode?: string;
@@ -9,5 +10,6 @@ export declare function cli({ name, appName, forcedMode, forcedAddOns, defaultTe
9
10
  defaultFramework?: string;
10
11
  craCompatible?: boolean;
11
12
  webBase?: string;
13
+ frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>;
12
14
  showDeploymentOptions?: boolean;
13
15
  }): void;
@@ -4,3 +4,7 @@ export declare function normalizeOptions(cliOptions: CliOptions, forcedMode?: st
4
4
  disableNameCheck?: boolean;
5
5
  forcedDeployment?: string;
6
6
  }): Promise<Options | undefined>;
7
+ export declare function validateDevWatchOptions(cliOptions: CliOptions): {
8
+ valid: boolean;
9
+ error?: string;
10
+ };
@@ -0,0 +1,27 @@
1
+ import type { Environment, Framework, FrameworkDefinition, Options } from '@tanstack/cta-engine';
2
+ export interface DevWatchOptions {
3
+ watchPath: string;
4
+ targetDir: string;
5
+ framework: Framework;
6
+ cliOptions: Options;
7
+ packageManager: string;
8
+ environment: Environment;
9
+ frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>;
10
+ }
11
+ export declare class DevWatchManager {
12
+ private options;
13
+ private watcher;
14
+ private debounceQueue;
15
+ private syncer;
16
+ private tempDir;
17
+ private isBuilding;
18
+ private buildCount;
19
+ constructor(options: DevWatchOptions);
20
+ start(): Promise<void>;
21
+ stop(): Promise<void>;
22
+ private startWatcher;
23
+ private handleChange;
24
+ private rebuild;
25
+ private cleanup;
26
+ private log;
27
+ }
@@ -0,0 +1,18 @@
1
+ export interface FileUpdate {
2
+ path: string;
3
+ diff?: string;
4
+ }
5
+ export interface SyncResult {
6
+ updated: Array<FileUpdate>;
7
+ skipped: Array<string>;
8
+ created: Array<string>;
9
+ errors: Array<string>;
10
+ }
11
+ export declare class FileSyncer {
12
+ sync(sourceDir: string, targetDir: string): Promise<SyncResult>;
13
+ private syncDirectory;
14
+ private shouldUpdateFile;
15
+ private calculateHash;
16
+ private shouldSkipDirectory;
17
+ private shouldSkipFile;
18
+ }
@@ -5,7 +5,7 @@ export interface CliOptions {
5
5
  framework?: string;
6
6
  tailwind?: boolean;
7
7
  packageManager?: PackageManager;
8
- toolchain?: string;
8
+ toolchain?: string | false;
9
9
  deployment?: string;
10
10
  projectName?: string;
11
11
  git?: boolean;
@@ -18,5 +18,8 @@ export interface CliOptions {
18
18
  targetDir?: string;
19
19
  interactive?: boolean;
20
20
  ui?: boolean;
21
+ devWatch?: string;
22
+ install?: boolean;
21
23
  addOnConfig?: string;
24
+ force?: boolean;
22
25
  }
@@ -7,6 +7,6 @@ export declare function selectTailwind(): Promise<boolean>;
7
7
  export declare function selectPackageManager(): Promise<PackageManager>;
8
8
  export declare function selectAddOns(framework: Framework, mode: string, type: string, message: string, forcedAddOns?: Array<string>, allowMultiple?: boolean): Promise<Array<string>>;
9
9
  export declare function selectGit(): Promise<boolean>;
10
- export declare function selectToolchain(framework: Framework, toolchain?: string): Promise<string | undefined>;
10
+ export declare function selectToolchain(framework: Framework, toolchain?: string | false): Promise<string | undefined>;
11
11
  export declare function promptForAddOnOptions(addOnIds: Array<string>, framework: Framework): Promise<Record<string, Record<string, any>>>;
12
12
  export declare function selectDeployment(framework: Framework, deployment?: string): Promise<string | undefined>;
@@ -147,6 +147,9 @@ export async function selectGit() {
147
147
  return git;
148
148
  }
149
149
  export async function selectToolchain(framework, toolchain) {
150
+ if (toolchain === false) {
151
+ return undefined;
152
+ }
150
153
  const toolchains = new Set();
151
154
  for (const addOn of framework.getAddOns()) {
152
155
  if (addOn.type === 'toolchain') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-cli",
3
- "version": "0.46.2",
3
+ "version": "0.48.0",
4
4
  "description": "Tanstack Application Builder CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,15 +27,20 @@
27
27
  "@clack/prompts": "^0.10.0",
28
28
  "@modelcontextprotocol/sdk": "^1.6.0",
29
29
  "chalk": "^5.4.1",
30
+ "chokidar": "^3.6.0",
30
31
  "commander": "^13.1.0",
32
+ "diff": "^7.0.0",
31
33
  "express": "^4.21.2",
32
34
  "semver": "^7.7.2",
35
+ "tempy": "^3.1.0",
33
36
  "validate-npm-package-name": "^7.0.0",
34
37
  "zod": "^3.24.2",
35
- "@tanstack/cta-engine": "0.46.2",
36
- "@tanstack/cta-ui": "0.46.2"
38
+ "@tanstack/cta-engine": "0.48.0",
39
+ "@tanstack/cta-ui": "0.48.0"
37
40
  },
38
41
  "devDependencies": {
42
+ "@tanstack/config": "^0.16.2",
43
+ "@types/diff": "^5.2.0",
39
44
  "@types/express": "^5.0.1",
40
45
  "@types/node": "^22.13.4",
41
46
  "@types/semver": "^7.7.0",
package/src/cli.ts CHANGED
@@ -24,13 +24,18 @@ import { launchUI } from '@tanstack/cta-ui'
24
24
  import { runMCPServer } from './mcp.js'
25
25
 
26
26
  import { promptForAddOns, promptForCreateOptions } from './options.js'
27
- import { normalizeOptions } from './command-line.js'
27
+ import { normalizeOptions, validateDevWatchOptions } from './command-line.js'
28
28
 
29
29
  import { createUIEnvironment } from './ui-environment.js'
30
30
  import { convertTemplateToMode } from './utils.js'
31
+ import { DevWatchManager } from './dev-watch.js'
31
32
 
32
33
  import type { CliOptions, TemplateOptions } from './types.js'
33
- import type { Options, PackageManager } from '@tanstack/cta-engine'
34
+ import type {
35
+ FrameworkDefinition,
36
+ Options,
37
+ PackageManager,
38
+ } from '@tanstack/cta-engine'
34
39
 
35
40
  // This CLI assumes that all of the registered frameworks have the same set of toolchains, deployments, modes, etc.
36
41
 
@@ -44,6 +49,7 @@ export function cli({
44
49
  defaultFramework,
45
50
  craCompatible = false,
46
51
  webBase,
52
+ frameworkDefinitionInitializers,
47
53
  showDeploymentOptions = false,
48
54
  }: {
49
55
  name: string
@@ -55,6 +61,7 @@ export function cli({
55
61
  defaultFramework?: string
56
62
  craCompatible?: boolean
57
63
  webBase?: string
64
+ frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
58
65
  showDeploymentOptions?: boolean
59
66
  }) {
60
67
  const environment = createUIEnvironment(appName, false)
@@ -293,6 +300,7 @@ Remove your node_modules directory and package lock file and re-install.`,
293
300
  'initialize this project from a starter URL',
294
301
  false,
295
302
  )
303
+ .option('--no-install', 'skip installing dependencies')
296
304
  .option<PackageManager>(
297
305
  `--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
298
306
  `Explicitly tell the CLI to use this package manager`,
@@ -307,6 +315,10 @@ Remove your node_modules directory and package lock file and re-install.`,
307
315
  return value as PackageManager
308
316
  },
309
317
  )
318
+ .option(
319
+ '--dev-watch <path>',
320
+ 'Watch a framework directory for changes and auto-rebuild',
321
+ )
310
322
 
311
323
  if (deployments.size > 0) {
312
324
  program.option<string>(
@@ -326,25 +338,28 @@ Remove your node_modules directory and package lock file and re-install.`,
326
338
  }
327
339
 
328
340
  if (toolchains.size > 0) {
329
- program.option<string>(
330
- `--toolchain <${Array.from(toolchains).join('|')}>`,
331
- `Explicitly tell the CLI to use this toolchain`,
332
- (value) => {
333
- if (!toolchains.has(value)) {
334
- throw new InvalidArgumentError(
335
- `Invalid toolchain: ${value}. The following are allowed: ${Array.from(
336
- toolchains,
337
- ).join(', ')}`,
338
- )
339
- }
340
- return value
341
- },
342
- )
341
+ program
342
+ .option<string>(
343
+ `--toolchain <${Array.from(toolchains).join('|')}>`,
344
+ `Explicitly tell the CLI to use this toolchain`,
345
+ (value) => {
346
+ if (!toolchains.has(value)) {
347
+ throw new InvalidArgumentError(
348
+ `Invalid toolchain: ${value}. The following are allowed: ${Array.from(
349
+ toolchains,
350
+ ).join(', ')}`,
351
+ )
352
+ }
353
+ return value
354
+ },
355
+ )
356
+ .option('--no-toolchain', 'skip toolchain selection')
343
357
  }
344
358
 
345
359
  program
346
360
  .option('--interactive', 'interactive mode', false)
347
- .option('--tailwind', 'add Tailwind CSS', false)
361
+ .option('--tailwind', 'add Tailwind CSS')
362
+ .option('--no-tailwind', 'skip Tailwind CSS')
348
363
  .option<Array<string> | boolean>(
349
364
  '--add-ons [...add-ons]',
350
365
  'pick from a list of available add-ons (comma separated list)',
@@ -373,6 +388,11 @@ Remove your node_modules directory and package lock file and re-install.`,
373
388
  '--add-on-config <config>',
374
389
  'JSON string with add-on configuration options',
375
390
  )
391
+ .option(
392
+ '-f, --force',
393
+ 'force project creation even if the target directory is not empty',
394
+ false,
395
+ )
376
396
 
377
397
  program.action(async (projectName: string, options: CliOptions) => {
378
398
  if (options.listAddOns) {
@@ -462,6 +482,73 @@ Remove your node_modules directory and package lock file and re-install.`,
462
482
  forcedAddOns,
463
483
  appName,
464
484
  })
485
+ } else if (options.devWatch) {
486
+ // Validate dev watch options
487
+ const validation = validateDevWatchOptions({ ...options, projectName })
488
+ if (!validation.valid) {
489
+ console.error(validation.error)
490
+ process.exit(1)
491
+ }
492
+
493
+ // Enter dev watch mode
494
+ if (!projectName && !options.targetDir) {
495
+ console.error(
496
+ 'Project name/target directory is required for dev watch mode',
497
+ )
498
+ process.exit(1)
499
+ }
500
+
501
+ if (!options.framework) {
502
+ console.error('Failed to detect framework')
503
+ process.exit(1)
504
+ }
505
+
506
+ const framework = getFrameworkByName(options.framework)
507
+ if (!framework) {
508
+ console.error('Failed to detect framework')
509
+ process.exit(1)
510
+ }
511
+
512
+ // First, create the app normally using the standard flow
513
+ const normalizedOpts = await normalizeOptions(
514
+ {
515
+ ...options,
516
+ projectName,
517
+ framework: framework.id,
518
+ },
519
+ defaultMode,
520
+ forcedAddOns,
521
+ )
522
+
523
+ if (!normalizedOpts) {
524
+ throw new Error('Failed to normalize options')
525
+ }
526
+
527
+ normalizedOpts.targetDir =
528
+ options.targetDir || resolve(process.cwd(), projectName)
529
+
530
+ // Create the initial app with minimal output for dev watch mode
531
+ console.log(chalk.bold('\ndev-watch'))
532
+ console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`)
533
+ if (normalizedOpts.install !== false) {
534
+ console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...')
535
+ }
536
+ const silentEnvironment = createUIEnvironment(appName, true)
537
+ await createApp(silentEnvironment, normalizedOpts)
538
+ console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
539
+
540
+ // Now start the dev watch mode
541
+ const manager = new DevWatchManager({
542
+ watchPath: options.devWatch,
543
+ targetDir: normalizedOpts.targetDir,
544
+ framework,
545
+ cliOptions: normalizedOpts,
546
+ packageManager: normalizedOpts.packageManager,
547
+ environment,
548
+ frameworkDefinitionInitializers,
549
+ })
550
+
551
+ await manager.start()
465
552
  } else {
466
553
  try {
467
554
  const cliOptions = {
@@ -1,4 +1,5 @@
1
1
  import { resolve } from 'node:path'
2
+ import fs from 'node:fs'
2
3
 
3
4
  import {
4
5
  DEFAULT_PACKAGE_MANAGER,
@@ -131,8 +132,25 @@ export async function normalizeOptions(
131
132
  const chosenAddOns = await selectAddOns()
132
133
 
133
134
  if (chosenAddOns.length) {
134
- tailwind = true
135
135
  typescript = true
136
+
137
+ // Check if any add-on explicitly requires tailwind
138
+ const addOnsRequireTailwind = chosenAddOns.some(
139
+ (addOn) => addOn.tailwind === true,
140
+ )
141
+
142
+ // Only set tailwind to true if:
143
+ // 1. An add-on explicitly requires it, OR
144
+ // 2. User explicitly set it via CLI
145
+ if (addOnsRequireTailwind) {
146
+ tailwind = true
147
+ } else if (cliOptions.tailwind === true) {
148
+ tailwind = true
149
+ } else if (cliOptions.tailwind === false) {
150
+ tailwind = false
151
+ }
152
+ // If cliOptions.tailwind is undefined and no add-ons require it,
153
+ // leave tailwind as is (will be prompted in interactive mode)
136
154
  }
137
155
 
138
156
  // Handle add-on configuration option
@@ -158,6 +176,7 @@ export async function normalizeOptions(
158
176
  getPackageManager() ||
159
177
  DEFAULT_PACKAGE_MANAGER,
160
178
  git: !!cliOptions.git,
179
+ install: cliOptions.install,
161
180
  chosenAddOns,
162
181
  addOnOptions: {
163
182
  ...populateAddOnOptionsDefaults(chosenAddOns),
@@ -166,3 +185,52 @@ export async function normalizeOptions(
166
185
  starter: starter,
167
186
  }
168
187
  }
188
+
189
+ export function validateDevWatchOptions(cliOptions: CliOptions): {
190
+ valid: boolean
191
+ error?: string
192
+ } {
193
+ if (!cliOptions.devWatch) {
194
+ return { valid: true }
195
+ }
196
+
197
+ // Validate watch path exists
198
+ const watchPath = resolve(process.cwd(), cliOptions.devWatch)
199
+ if (!fs.existsSync(watchPath)) {
200
+ return {
201
+ valid: false,
202
+ error: `Watch path does not exist: ${watchPath}`,
203
+ }
204
+ }
205
+
206
+ // Validate it's a directory
207
+ const stats = fs.statSync(watchPath)
208
+ if (!stats.isDirectory()) {
209
+ return {
210
+ valid: false,
211
+ error: `Watch path is not a directory: ${watchPath}`,
212
+ }
213
+ }
214
+
215
+ // Ensure target directory is specified
216
+ if (!cliOptions.projectName && !cliOptions.targetDir) {
217
+ return {
218
+ valid: false,
219
+ error: 'Project name or target directory is required for dev watch mode',
220
+ }
221
+ }
222
+
223
+ // Check for framework structure
224
+ const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'))
225
+ const hasAssets = fs.existsSync(resolve(watchPath, 'assets'))
226
+ const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'))
227
+
228
+ if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
229
+ return {
230
+ valid: false,
231
+ error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
232
+ }
233
+ }
234
+
235
+ return { valid: true }
236
+ }