@tanstack/cli 0.59.8 → 0.60.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @tanstack/cli
2
2
 
3
+ ## 0.60.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`b54e202`](https://github.com/TanStack/cli/commit/b54e202ce56f2aa78a416634155bc22f0cb5cc46)]:
8
+ - @tanstack/create@0.62.1
9
+
10
+ ## 0.60.0
11
+
12
+ ### Minor Changes
13
+
14
+ - This release pulls together a large batch of improvements across the CLI and scaffolding engine since the last versioning pass. ([`154b25e`](https://github.com/TanStack/cli/commit/154b25eec9a13b9718c44cbed6cb3c8566f2fb11))
15
+
16
+ - Modernizes and refreshes the generated React/Solid template experience, including updated starter content and stronger defaults.
17
+ - Improves create flows with better option normalization, stronger guardrails around target directories, and clearer compatibility behavior in router-only mode.
18
+ - Expands scaffolding ergonomics with examples toggles, improved add-on/config handling, and reliability fixes across package-manager and cross-platform paths.
19
+ - Strengthens test and release confidence via e2e/release workflow hardening and broader smoke coverage.
20
+ - Streamlines product surface area by removing the local `create-ui` package and `--ui` command paths from the CLI; visual setup now lives at `https://tanstack.com/builder`.
21
+ - Cleans up docs and custom CLI examples to match the current terminal-first workflow and Builder guidance.
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [[`154b25e`](https://github.com/TanStack/cli/commit/154b25eec9a13b9718c44cbed6cb3c8566f2fb11)]:
26
+ - @tanstack/create@0.62.0
27
+
3
28
  ## 0.59.8
4
29
 
5
30
  ### Patch Changes
package/dist/bin.js CHANGED
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { cli } from './cli.js';
3
+ import { createReactFrameworkDefinition, createSolidFrameworkDefinition, } from '@tanstack/create';
3
4
  cli({
4
5
  name: 'tanstack',
5
6
  appName: 'TanStack',
7
+ frameworkDefinitionInitializers: [
8
+ createReactFrameworkDefinition,
9
+ createSolidFrameworkDefinition,
10
+ ],
6
11
  });
package/dist/cli.js CHANGED
@@ -4,8 +4,7 @@ import { Command, InvalidArgumentError } from 'commander';
4
4
  import { cancel, confirm, intro, isCancel, log } from '@clack/prompts';
5
5
  import chalk from 'chalk';
6
6
  import semver from 'semver';
7
- import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, devAddOn, compileStarter, createApp, createSerializedOptions, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
8
- import { launchUI } from '@tanstack/create-ui';
7
+ import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, devAddOn, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
9
8
  import { runMCPServer } from './mcp.js';
10
9
  import { promptForAddOns, promptForCreateOptions } from './options.js';
11
10
  import { normalizeOptions, validateDevWatchOptions, validateLegacyCreateFlags, } from './command-line.js';
@@ -15,7 +14,7 @@ import { DevWatchManager } from './dev-watch.js';
15
14
  const packageJsonPath = new URL('../package.json', import.meta.url);
16
15
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
17
16
  const VERSION = packageJson.version;
18
- export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, defaultRouterOnly = false, }) {
17
+ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, defaultRouterOnly = false, }) {
19
18
  const environment = createUIEnvironment(appName, false);
20
19
  const program = new Command();
21
20
  async function confirmTargetDirectorySafety(targetDir, forced) {
@@ -41,6 +40,73 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
41
40
  }
42
41
  }
43
42
  const availableFrameworks = getFrameworks().map((f) => f.name);
43
+ function resolveBuiltInDevWatchPath(frameworkId) {
44
+ const candidates = [
45
+ resolve(process.cwd(), 'packages/create/src/frameworks', frameworkId),
46
+ resolve(process.cwd(), '../create/src/frameworks', frameworkId),
47
+ ];
48
+ for (const candidate of candidates) {
49
+ if (fs.existsSync(candidate)) {
50
+ return candidate;
51
+ }
52
+ }
53
+ return candidates[0];
54
+ }
55
+ async function startDevWatchMode(projectName, options) {
56
+ // Validate dev watch options
57
+ const validation = validateDevWatchOptions({ ...options, projectName });
58
+ if (!validation.valid) {
59
+ console.error(validation.error);
60
+ process.exit(1);
61
+ }
62
+ // Enter dev watch mode
63
+ if (!projectName && !options.targetDir) {
64
+ console.error('Project name/target directory is required for dev watch mode');
65
+ process.exit(1);
66
+ }
67
+ if (!options.framework) {
68
+ console.error('Failed to detect framework');
69
+ process.exit(1);
70
+ }
71
+ const framework = getFrameworkByName(options.framework);
72
+ if (!framework) {
73
+ console.error('Failed to detect framework');
74
+ process.exit(1);
75
+ }
76
+ // First, create the app normally using the standard flow
77
+ const normalizedOpts = await normalizeOptions({
78
+ ...options,
79
+ projectName,
80
+ framework: framework.id,
81
+ }, forcedAddOns);
82
+ if (!normalizedOpts) {
83
+ throw new Error('Failed to normalize options');
84
+ }
85
+ normalizedOpts.targetDir =
86
+ options.targetDir || resolve(process.cwd(), projectName);
87
+ // Create the initial app with minimal output for dev watch mode
88
+ console.log(chalk.bold('\ndev-watch'));
89
+ console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`);
90
+ if (normalizedOpts.install !== false) {
91
+ console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
92
+ }
93
+ const silentEnvironment = createUIEnvironment(appName, true);
94
+ await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force);
95
+ await createApp(silentEnvironment, normalizedOpts);
96
+ console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
97
+ // Now start the dev watch mode
98
+ const manager = new DevWatchManager({
99
+ watchPath: options.devWatch,
100
+ targetDir: normalizedOpts.targetDir,
101
+ framework,
102
+ cliOptions: normalizedOpts,
103
+ packageManager: normalizedOpts.packageManager,
104
+ runDevCommand: options.runDev,
105
+ environment,
106
+ frameworkDefinitionInitializers,
107
+ });
108
+ await manager.start();
109
+ }
44
110
  const toolchains = new Set();
45
111
  for (const framework of getFrameworks()) {
46
112
  for (const addOn of framework.getAddOns()) {
@@ -142,58 +208,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
142
208
  return;
143
209
  }
144
210
  if (options.devWatch) {
145
- // Validate dev watch options
146
- const validation = validateDevWatchOptions({ ...options, projectName });
147
- if (!validation.valid) {
148
- console.error(validation.error);
149
- process.exit(1);
150
- }
151
- // Enter dev watch mode
152
- if (!projectName && !options.targetDir) {
153
- console.error('Project name/target directory is required for dev watch mode');
154
- process.exit(1);
155
- }
156
- if (!options.framework) {
157
- console.error('Failed to detect framework');
158
- process.exit(1);
159
- }
160
- const framework = getFrameworkByName(options.framework);
161
- if (!framework) {
162
- console.error('Failed to detect framework');
163
- process.exit(1);
164
- }
165
- // First, create the app normally using the standard flow
166
- const normalizedOpts = await normalizeOptions({
167
- ...options,
168
- projectName,
169
- framework: framework.id,
170
- }, forcedAddOns);
171
- if (!normalizedOpts) {
172
- throw new Error('Failed to normalize options');
173
- }
174
- normalizedOpts.targetDir =
175
- options.targetDir || resolve(process.cwd(), projectName);
176
- // Create the initial app with minimal output for dev watch mode
177
- console.log(chalk.bold('\ndev-watch'));
178
- console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`);
179
- if (normalizedOpts.install !== false) {
180
- console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
181
- }
182
- const silentEnvironment = createUIEnvironment(appName, true);
183
- await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force);
184
- await createApp(silentEnvironment, normalizedOpts);
185
- console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
186
- // Now start the dev watch mode
187
- const manager = new DevWatchManager({
188
- watchPath: options.devWatch,
189
- targetDir: normalizedOpts.targetDir,
190
- framework,
191
- cliOptions: normalizedOpts,
192
- packageManager: normalizedOpts.packageManager,
193
- environment,
194
- frameworkDefinitionInitializers,
195
- });
196
- await manager.start();
211
+ await startDevWatchMode(projectName, options);
197
212
  return;
198
213
  }
199
214
  try {
@@ -206,6 +221,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
206
221
  }
207
222
  if (cliOptions.routerOnly !== true &&
208
223
  cliOptions.template &&
224
+ ['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(cliOptions.template.toLowerCase()) &&
209
225
  cliOptions.template.toLowerCase() !== 'file-router') {
210
226
  cliOptions.routerOnly = true;
211
227
  }
@@ -217,24 +233,6 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
217
233
  else {
218
234
  finalOptions = await normalizeOptions(cliOptions, forcedAddOns, { forcedDeployment });
219
235
  }
220
- if (options.ui) {
221
- const optionsFromCLI = await normalizeOptions(cliOptions, forcedAddOns, { disableNameCheck: true, forcedDeployment });
222
- const uiOptions = {
223
- ...createSerializedOptions(optionsFromCLI),
224
- projectName: 'my-app',
225
- targetDir: resolve(process.cwd(), 'my-app'),
226
- };
227
- launchUI({
228
- mode: 'setup',
229
- options: uiOptions,
230
- forcedRouterMode: defaultMode,
231
- forcedAddOns,
232
- environmentFactory: () => createUIEnvironment(appName, false),
233
- webBase,
234
- showDeploymentOptions,
235
- });
236
- return;
237
- }
238
236
  if (finalOptions) {
239
237
  intro(`Creating a new ${appName} app in ${projectName}...`);
240
238
  }
@@ -281,6 +279,9 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
281
279
  cmd.argument('[project-name]', 'name of the project');
282
280
  if (!defaultFramework) {
283
281
  cmd.option('--framework <type>', `project framework (${availableFrameworks.join(', ')})`, (value) => {
282
+ if (value.toLowerCase() === 'react-cra') {
283
+ return 'react';
284
+ }
284
285
  if (!availableFrameworks.some((f) => f.toLowerCase() === value.toLowerCase())) {
285
286
  throw new InvalidArgumentError(`Invalid framework: ${value}. Only the following are allowed: ${availableFrameworks.join(', ')}`);
286
287
  }
@@ -288,7 +289,9 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
288
289
  }, defaultFramework || 'React');
289
290
  }
290
291
  cmd
291
- .option('--starter [url]', 'initialize this project from a starter URL', false)
292
+ .option('--starter [url-or-id]', 'DEPRECATED: use --template. Initializes from a template URL or built-in id', false)
293
+ .option('--template-id <id>', 'initialize using a built-in template id')
294
+ .option('--template [url-or-id]', 'initialize this project from a template URL or built-in template id')
292
295
  .option('--no-install', 'skip installing dependencies')
293
296
  .option(`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`, `Explicitly tell the CLI to use this package manager`, (value) => {
294
297
  if (!SUPPORTED_PACKAGE_MANAGERS.includes(value)) {
@@ -297,8 +300,8 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
297
300
  return value;
298
301
  })
299
302
  .option('--dev-watch <path>', 'Watch a framework directory for changes and auto-rebuild')
303
+ .option('--run-dev', 'Run the app dev server alongside dev-watch', false)
300
304
  .option('--router-only', 'Use router-only compatibility mode (file-based routing without TanStack Start)')
301
- .option('--template <type>', 'Deprecated: compatibility flag from create-tsrouter-app')
302
305
  .option('--tailwind', 'Deprecated: compatibility flag; Tailwind is always enabled')
303
306
  .option('--no-tailwind', 'Deprecated: compatibility flag; Tailwind opt-out is ignored')
304
307
  .option('--examples', 'include demo/example pages')
@@ -335,7 +338,6 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
335
338
  .option('--git', 'create a git repository')
336
339
  .option('--no-git', 'do not create a git repository')
337
340
  .option('--target-dir <path>', 'the target directory for the application root')
338
- .option('--ui', 'Launch the UI for project creation')
339
341
  .option('--add-on-config <config>', 'JSON string with add-on configuration options')
340
342
  .option('-f, --force', 'force project creation even if the target directory is not empty', false);
341
343
  return cmd;
@@ -347,6 +349,28 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
347
349
  .description(`Create a new TanStack Start application`);
348
350
  configureCreateCommand(createCommand);
349
351
  createCommand.action(handleCreate);
352
+ // === DEV SUBCOMMAND ===
353
+ const devCommand = program
354
+ .command('dev')
355
+ .description('Create a sandbox app and watch built-in framework templates/add-ons');
356
+ configureCreateCommand(devCommand);
357
+ devCommand.action(async (projectName, options) => {
358
+ const frameworkName = options.framework || defaultFramework || 'React';
359
+ const framework = getFrameworkByName(frameworkName);
360
+ if (!framework) {
361
+ console.error(`Unknown framework: ${frameworkName}`);
362
+ process.exit(1);
363
+ }
364
+ const watchPath = resolveBuiltInDevWatchPath(framework.id);
365
+ const devOptions = {
366
+ ...options,
367
+ framework: framework.name,
368
+ devWatch: watchPath,
369
+ runDev: true,
370
+ install: options.install ?? true,
371
+ };
372
+ await startDevWatchMode(projectName, devOptions);
373
+ });
350
374
  // === MCP SUBCOMMAND ===
351
375
  program
352
376
  .command('mcp')
@@ -424,7 +448,6 @@ Remove your node_modules directory and package lock file and re-install.`);
424
448
  .command('add')
425
449
  .argument('[add-on...]', 'Name of the add-ons (or add-ons separated by spaces or commas)')
426
450
  .option('--forced', 'Force the add-on to be added', false)
427
- .option('--ui', 'Add with the UI')
428
451
  .action(async (addOns, options) => {
429
452
  const parsedAddOns = [];
430
453
  for (const addOn of addOns) {
@@ -435,19 +458,7 @@ Remove your node_modules directory and package lock file and re-install.`);
435
458
  parsedAddOns.push(addOn.trim());
436
459
  }
437
460
  }
438
- if (options.ui) {
439
- launchUI({
440
- mode: 'add',
441
- addOns: parsedAddOns,
442
- projectPath: resolve(process.cwd()),
443
- forcedRouterMode: defaultMode,
444
- forcedAddOns,
445
- environmentFactory: () => createUIEnvironment(appName, false),
446
- webBase,
447
- showDeploymentOptions,
448
- });
449
- }
450
- else if (parsedAddOns.length < 1) {
461
+ if (parsedAddOns.length < 1) {
451
462
  const selectedAddOns = await promptForAddOns();
452
463
  if (selectedAddOns.length) {
453
464
  await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
@@ -481,17 +492,31 @@ Remove your node_modules directory and package lock file and re-install.`);
481
492
  .action(async () => {
482
493
  await devAddOn(environment);
483
494
  });
484
- // === STARTER SUBCOMMAND ===
495
+ // === TEMPLATE SUBCOMMAND ===
496
+ const templateCommand = program.command('template');
497
+ templateCommand
498
+ .command('init')
499
+ .description('Initialize a project template from the current project')
500
+ .action(async () => {
501
+ await initStarter(environment);
502
+ });
503
+ templateCommand
504
+ .command('compile')
505
+ .description('Compile the template JSON file for the current project')
506
+ .action(async () => {
507
+ await compileStarter(environment);
508
+ });
509
+ // Legacy alias for template command
485
510
  const starterCommand = program.command('starter');
486
511
  starterCommand
487
512
  .command('init')
488
- .description('Initialize a project starter from the current project')
513
+ .description('Deprecated alias: initialize a project template')
489
514
  .action(async () => {
490
515
  await initStarter(environment);
491
516
  });
492
517
  starterCommand
493
518
  .command('compile')
494
- .description('Compile the starter JSON file for the current project')
519
+ .description('Deprecated alias: compile the template JSON file')
495
520
  .action(async () => {
496
521
  await compileStarter(environment);
497
522
  });
@@ -1,16 +1,136 @@
1
1
  import { resolve } from 'node:path';
2
2
  import fs from 'node:fs';
3
- import { DEFAULT_PACKAGE_MANAGER, finalizeAddOns, getFrameworkById, getPackageManager, loadStarter, populateAddOnOptionsDefaults, } from '@tanstack/create';
3
+ import { DEFAULT_PACKAGE_MANAGER, finalizeAddOns, getFrameworkById, getPackageManager, getRawRegistry, loadStarter, populateAddOnOptionsDefaults, } from '@tanstack/create';
4
4
  import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
5
5
  const SUPPORTED_LEGACY_TEMPLATES = new Set([
6
6
  'file-router',
7
7
  'typescript',
8
8
  'tsx',
9
9
  ]);
10
+ const LEGACY_TEMPLATE_ALIASES = new Set(['javascript', 'js', 'jsx']);
11
+ function getLegacyTemplateValue(templateValue) {
12
+ if (!templateValue) {
13
+ return undefined;
14
+ }
15
+ const normalized = templateValue.toLowerCase().trim();
16
+ if (SUPPORTED_LEGACY_TEMPLATES.has(normalized) ||
17
+ LEGACY_TEMPLATE_ALIASES.has(normalized)) {
18
+ return normalized;
19
+ }
20
+ return undefined;
21
+ }
22
+ function slugifyStarterName(value) {
23
+ return value
24
+ .trim()
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, '-')
27
+ .replace(/^-+|-+$/g, '');
28
+ }
29
+ function isLikelyStarterUrlOrPath(value) {
30
+ return (/^https?:\/\//i.test(value) ||
31
+ /^file:\/\//i.test(value) ||
32
+ value.startsWith('./') ||
33
+ value.startsWith('../') ||
34
+ value.startsWith('/') ||
35
+ /^[a-zA-Z]:[\\/]/.test(value));
36
+ }
37
+ function getStarterIdsFromUrl(starterUrl) {
38
+ const ids = new Set();
39
+ try {
40
+ const pathname = new URL(starterUrl).pathname;
41
+ const parts = pathname.split('/').filter(Boolean);
42
+ const lastPart = parts.at(-1)?.toLowerCase();
43
+ if (lastPart) {
44
+ ids.add(lastPart.replace(/\.json$/i, ''));
45
+ }
46
+ if ((lastPart === 'starter.json' || lastPart === 'template.json') &&
47
+ parts.length >= 2) {
48
+ ids.add(parts.at(-2).toLowerCase());
49
+ }
50
+ }
51
+ catch {
52
+ // Ignore URL parse errors and rely on other id heuristics.
53
+ }
54
+ return ids;
55
+ }
56
+ function resolveMonorepoStarterById(starterId) {
57
+ const normalized = starterId.toLowerCase().trim();
58
+ const idVariants = Array.from(new Set([
59
+ normalized,
60
+ normalized.replace(/-template$/i, ''),
61
+ normalized.replace(/-starter$/i, ''),
62
+ ])).filter(Boolean);
63
+ const cwd = process.cwd();
64
+ const rootCandidates = [
65
+ cwd,
66
+ resolve(cwd, '..'),
67
+ resolve(cwd, '../..'),
68
+ resolve(cwd, '../../..'),
69
+ ];
70
+ for (const root of rootCandidates) {
71
+ for (const framework of ['react', 'solid']) {
72
+ for (const id of idVariants) {
73
+ const templatePath = resolve(root, 'examples', framework, id, 'template.json');
74
+ if (fs.existsSync(templatePath)) {
75
+ return templatePath;
76
+ }
77
+ const starterPath = resolve(root, 'examples', framework, id, 'starter.json');
78
+ if (fs.existsSync(starterPath)) {
79
+ return starterPath;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+ async function resolveStarterSpecifier(starterSpecifier) {
87
+ const normalized = starterSpecifier.trim();
88
+ if (!normalized || isLikelyStarterUrlOrPath(normalized)) {
89
+ return normalized;
90
+ }
91
+ const registry = await getRawRegistry();
92
+ if (registry && registry.starters?.length) {
93
+ const lookup = normalized.toLowerCase();
94
+ const match = registry.starters.find((starter) => {
95
+ const candidateIds = new Set();
96
+ candidateIds.add(starter.name.toLowerCase());
97
+ candidateIds.add(slugifyStarterName(starter.name));
98
+ for (const id of getStarterIdsFromUrl(starter.url)) {
99
+ candidateIds.add(id);
100
+ }
101
+ return candidateIds.has(lookup);
102
+ });
103
+ if (match) {
104
+ return match.url;
105
+ }
106
+ }
107
+ const monorepoStarterPath = resolveMonorepoStarterById(normalized);
108
+ if (monorepoStarterPath) {
109
+ return monorepoStarterPath;
110
+ }
111
+ if (!registry || !registry.starters?.length) {
112
+ throw new Error(`Could not resolve template id "${normalized}" because no template registry is configured. Pass a template URL (or set CTA_REGISTRY).`);
113
+ }
114
+ const availableIds = Array.from(new Set(registry.starters.flatMap((starter) => {
115
+ const ids = [slugifyStarterName(starter.name)];
116
+ ids.push(...Array.from(getStarterIdsFromUrl(starter.url)));
117
+ return ids;
118
+ })))
119
+ .filter(Boolean)
120
+ .sort();
121
+ throw new Error(`Unknown template id "${normalized}". Available built-in templates: ${availableIds.join(', ')}`);
122
+ }
10
123
  export function validateLegacyCreateFlags(cliOptions) {
11
124
  const warnings = [];
125
+ const legacyTemplate = getLegacyTemplateValue(cliOptions.template);
126
+ if (cliOptions.starter) {
127
+ warnings.push('The --starter flag is deprecated; prefer --template instead. Backward compatibility remains for now.');
128
+ }
129
+ if (cliOptions.starter && cliOptions.template && !legacyTemplate) {
130
+ warnings.push('Both --starter and --template were provided. --template takes precedence.');
131
+ }
12
132
  if (cliOptions.routerOnly) {
13
- warnings.push('The --router-only flag enables router-only compatibility mode. Start-dependent add-ons, deployment adapters, and starters are disabled; only the base template and optional toolchain are supported.');
133
+ warnings.push('The --router-only flag enables router-only compatibility mode. Start-dependent add-ons, deployment adapters, and templates are disabled; only the base template and optional toolchain are supported.');
14
134
  }
15
135
  if (cliOptions.routerOnly && cliOptions.addOns) {
16
136
  warnings.push('Ignoring --add-ons in router-only compatibility mode.');
@@ -19,7 +139,10 @@ export function validateLegacyCreateFlags(cliOptions) {
19
139
  warnings.push('Ignoring --deployment in router-only compatibility mode.');
20
140
  }
21
141
  if (cliOptions.routerOnly && cliOptions.starter) {
22
- warnings.push('Ignoring --starter in router-only compatibility mode.');
142
+ warnings.push('Ignoring --starter/--template in router-only compatibility mode.');
143
+ }
144
+ if (cliOptions.routerOnly && cliOptions.templateId) {
145
+ warnings.push('Ignoring --template-id in router-only compatibility mode.');
23
146
  }
24
147
  if (cliOptions.tailwind === true) {
25
148
  warnings.push('The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.');
@@ -27,10 +150,10 @@ export function validateLegacyCreateFlags(cliOptions) {
27
150
  if (cliOptions.tailwind === false) {
28
151
  warnings.push('The --no-tailwind flag is deprecated and ignored. Tailwind opt-out is intentionally unsupported to keep add-on permutations maintainable; remove Tailwind after scaffolding if needed.');
29
152
  }
30
- if (!cliOptions.template) {
153
+ if (!legacyTemplate) {
31
154
  return { warnings };
32
155
  }
33
- const template = cliOptions.template.toLowerCase().trim();
156
+ const template = legacyTemplate;
34
157
  if (template === 'javascript' || template === 'js' || template === 'jsx') {
35
158
  return {
36
159
  warnings,
@@ -70,12 +193,24 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
70
193
  // Mode is always file-router (TanStack Start)
71
194
  let mode = 'file-router';
72
195
  let routerOnly = !!cliOptions.routerOnly;
73
- const template = cliOptions.template?.toLowerCase().trim();
196
+ const legacyTemplate = getLegacyTemplateValue(cliOptions.template);
197
+ if (!cliOptions.starter) {
198
+ if (cliOptions.template && !legacyTemplate) {
199
+ cliOptions.starter = cliOptions.template;
200
+ }
201
+ else if (cliOptions.templateId) {
202
+ cliOptions.starter = cliOptions.templateId;
203
+ }
204
+ }
205
+ const template = legacyTemplate;
74
206
  if (template && template !== 'file-router') {
75
207
  routerOnly = true;
76
208
  }
209
+ if (!cliOptions.starter && cliOptions.templateId) {
210
+ cliOptions.starter = cliOptions.templateId;
211
+ }
77
212
  const starter = !routerOnly && cliOptions.starter
78
- ? await loadStarter(cliOptions.starter)
213
+ ? await loadStarter(await resolveStarterSpecifier(cliOptions.starter))
79
214
  : undefined;
80
215
  // TypeScript and Tailwind are always enabled with TanStack Start
81
216
  const typescript = true;
@@ -84,7 +219,7 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
84
219
  cliOptions.framework = starter.framework;
85
220
  mode = starter.mode;
86
221
  }
87
- const framework = getFrameworkById(cliOptions.framework || 'react-cra');
222
+ const framework = getFrameworkById(cliOptions.framework || 'react');
88
223
  async function selectAddOns() {
89
224
  // Edge case for Windows Powershell
90
225
  if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {