@tanstack/cli 0.59.4 → 0.59.6

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,31 @@
1
1
  # @tanstack/cli
2
2
 
3
+ ## 0.59.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Improve CLI compatibility and scaffold behavior for legacy router-first workflows. ([`2949819`](https://github.com/TanStack/cli/commit/2949819058b4d4b1760be683ef29bfd459ddb28b))
8
+
9
+ - Add safer target directory handling by warning before creating into non-empty folders.
10
+ - Support explicit git initialization control via `--git` and `--no-git`.
11
+ - Restore router-only compatibility mode with file-based routing templates (without Start-dependent add-ons/deployments/starters), while still allowing toolchains.
12
+ - Default `create-tsrouter-app` to router-only compatibility mode.
13
+ - Remove stale `count.txt` ignore entries from base templates.
14
+
15
+ Also expands starter documentation with clearer creation, maintenance, UI usage, and banner guidance.
16
+
17
+ - Updated dependencies [[`164522e`](https://github.com/TanStack/cli/commit/164522e444188e83710fc599304132de8cb379e6), [`2949819`](https://github.com/TanStack/cli/commit/2949819058b4d4b1760be683ef29bfd459ddb28b)]:
18
+ - @tanstack/create@0.61.4
19
+ - @tanstack/create-ui@0.59.6
20
+
21
+ ## 0.59.5
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [[`cc5857c`](https://github.com/TanStack/cli/commit/cc5857c5c212132852f37878e039071c5a9b1ac5)]:
26
+ - @tanstack/create@0.61.3
27
+ - @tanstack/create-ui@0.59.5
28
+
3
29
  ## 0.59.4
4
30
 
5
31
  ### Patch Changes
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { Command, InvalidArgumentError } from 'commander';
4
- import { intro, log } from '@clack/prompts';
4
+ import { cancel, confirm, intro, isCancel, log } from '@clack/prompts';
5
5
  import chalk from 'chalk';
6
6
  import semver from 'semver';
7
7
  import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, createSerializedOptions, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
@@ -15,9 +15,31 @@ import { DevWatchManager } from './dev-watch.js';
15
15
  const packageJsonPath = new URL('../package.json', import.meta.url);
16
16
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
17
17
  const VERSION = packageJson.version;
18
- export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, }) {
18
+ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, defaultRouterOnly = false, }) {
19
19
  const environment = createUIEnvironment(appName, false);
20
20
  const program = new Command();
21
+ async function confirmTargetDirectorySafety(targetDir, forced) {
22
+ if (forced) {
23
+ return;
24
+ }
25
+ if (!fs.existsSync(targetDir)) {
26
+ return;
27
+ }
28
+ if (!fs.statSync(targetDir).isDirectory()) {
29
+ throw new Error(`Target path exists and is not a directory: ${targetDir}`);
30
+ }
31
+ if (fs.readdirSync(targetDir).length === 0) {
32
+ return;
33
+ }
34
+ const shouldContinue = await confirm({
35
+ message: `Target directory "${targetDir}" already exists and is not empty. Continue anyway?`,
36
+ initialValue: false,
37
+ });
38
+ if (isCancel(shouldContinue) || !shouldContinue) {
39
+ cancel('Operation cancelled.');
40
+ process.exit(0);
41
+ }
42
+ }
21
43
  const availableFrameworks = getFrameworks().map((f) => f.name);
22
44
  const toolchains = new Set();
23
45
  for (const framework of getFrameworks()) {
@@ -158,6 +180,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
158
180
  console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
159
181
  }
160
182
  const silentEnvironment = createUIEnvironment(appName, true);
183
+ await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force);
161
184
  await createApp(silentEnvironment, normalizedOpts);
162
185
  console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
163
186
  // Now start the dev watch mode
@@ -178,6 +201,14 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
178
201
  projectName,
179
202
  ...options,
180
203
  };
204
+ if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
205
+ cliOptions.routerOnly = true;
206
+ }
207
+ if (cliOptions.routerOnly !== true &&
208
+ cliOptions.template &&
209
+ cliOptions.template.toLowerCase() !== 'file-router') {
210
+ cliOptions.routerOnly = true;
211
+ }
181
212
  cliOptions.framework = getFrameworkByName(options.framework || defaultFramework || 'React').id;
182
213
  let finalOptions;
183
214
  if (cliOptions.interactive || cliOptions.addOns === true) {
@@ -217,6 +248,9 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
217
248
  if (!finalOptions) {
218
249
  throw new Error('No options were provided');
219
250
  }
251
+ ;
252
+ finalOptions.routerOnly =
253
+ !!cliOptions.routerOnly;
220
254
  // Determine target directory:
221
255
  // 1. Use --target-dir if provided
222
256
  // 2. Use targetDir from normalizeOptions if set (handles "." case)
@@ -234,6 +268,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
234
268
  else {
235
269
  finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName);
236
270
  }
271
+ await confirmTargetDirectorySafety(finalOptions.targetDir, options.force);
237
272
  await createApp(environment, finalOptions);
238
273
  }
239
274
  catch (error) {
@@ -262,7 +297,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
262
297
  return value;
263
298
  })
264
299
  .option('--dev-watch <path>', 'Watch a framework directory for changes and auto-rebuild')
265
- .option('--router-only', 'Deprecated: compatibility flag from create-tsrouter-app')
300
+ .option('--router-only', 'Use router-only compatibility mode (file-based routing without TanStack Start)')
266
301
  .option('--template <type>', 'Deprecated: compatibility flag from create-tsrouter-app')
267
302
  .option('--tailwind', 'Deprecated: compatibility flag; Tailwind is always enabled')
268
303
  .option('--no-tailwind', 'Deprecated: compatibility flag; Tailwind opt-out is ignored');
@@ -295,6 +330,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
295
330
  })
296
331
  .option('--list-add-ons', 'list all available add-ons', false)
297
332
  .option('--addon-details <addon-id>', 'show detailed information about a specific add-on')
333
+ .option('--git', 'create a git repository')
298
334
  .option('--no-git', 'do not create a git repository')
299
335
  .option('--target-dir <path>', 'the target directory for the application root')
300
336
  .option('--ui', 'Launch the UI for project creation')
@@ -10,7 +10,16 @@ const SUPPORTED_LEGACY_TEMPLATES = new Set([
10
10
  export function validateLegacyCreateFlags(cliOptions) {
11
11
  const warnings = [];
12
12
  if (cliOptions.routerOnly) {
13
- warnings.push('The --router-only flag is deprecated and ignored. `tanstack create` already creates router-based apps.');
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.');
14
+ }
15
+ if (cliOptions.routerOnly && cliOptions.addOns) {
16
+ warnings.push('Ignoring --add-ons in router-only compatibility mode.');
17
+ }
18
+ if (cliOptions.routerOnly && cliOptions.deployment) {
19
+ warnings.push('Ignoring --deployment in router-only compatibility mode.');
20
+ }
21
+ if (cliOptions.routerOnly && cliOptions.starter) {
22
+ warnings.push('Ignoring --starter in router-only compatibility mode.');
14
23
  }
15
24
  if (cliOptions.tailwind === true) {
16
25
  warnings.push('The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.');
@@ -34,7 +43,7 @@ export function validateLegacyCreateFlags(cliOptions) {
34
43
  error: `Invalid --template value: ${cliOptions.template}. Supported values are: file-router, typescript, tsx.`,
35
44
  };
36
45
  }
37
- warnings.push('The --template flag is deprecated. TypeScript/TSX is the default and only supported template.');
46
+ warnings.push('The --template flag is deprecated and mapped for compatibility.');
38
47
  return { warnings };
39
48
  }
40
49
  export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
@@ -60,7 +69,12 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
60
69
  }
61
70
  // Mode is always file-router (TanStack Start)
62
71
  let mode = 'file-router';
63
- const starter = cliOptions.starter
72
+ let routerOnly = !!cliOptions.routerOnly;
73
+ const template = cliOptions.template?.toLowerCase().trim();
74
+ if (template && template !== 'file-router') {
75
+ routerOnly = true;
76
+ }
77
+ const starter = !routerOnly && cliOptions.starter
64
78
  ? await loadStarter(cliOptions.starter)
65
79
  : undefined;
66
80
  // TypeScript and Tailwind are always enabled with TanStack Start
@@ -85,10 +99,10 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
85
99
  cliOptions.toolchain ||
86
100
  cliOptions.deployment) {
87
101
  const selectedAddOns = new Set([
88
- ...(starter?.dependsOn || []),
89
- ...(forcedAddOns || []),
102
+ ...(routerOnly ? [] : (starter?.dependsOn || [])),
103
+ ...(routerOnly ? [] : (forcedAddOns || [])),
90
104
  ]);
91
- if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
105
+ if (!routerOnly && cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
92
106
  for (const a of cliOptions.addOns) {
93
107
  if (a.toLowerCase() === 'start') {
94
108
  continue;
@@ -99,10 +113,10 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
99
113
  if (cliOptions.toolchain) {
100
114
  selectedAddOns.add(cliOptions.toolchain);
101
115
  }
102
- if (cliOptions.deployment) {
116
+ if (!routerOnly && cliOptions.deployment) {
103
117
  selectedAddOns.add(cliOptions.deployment);
104
118
  }
105
- if (!cliOptions.deployment && opts?.forcedDeployment) {
119
+ if (!routerOnly && !cliOptions.deployment && opts?.forcedDeployment) {
106
120
  selectedAddOns.add(opts.forcedDeployment);
107
121
  }
108
122
  return await finalizeAddOns(framework, mode, Array.from(selectedAddOns));
@@ -131,7 +145,7 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
131
145
  packageManager: cliOptions.packageManager ||
132
146
  getPackageManager() ||
133
147
  DEFAULT_PACKAGE_MANAGER,
134
- git: !!cliOptions.git,
148
+ git: cliOptions.git ?? true,
135
149
  install: cliOptions.install,
136
150
  chosenAddOns,
137
151
  addOnOptions: {
package/dist/options.js CHANGED
@@ -1,5 +1,4 @@
1
- import fs from 'node:fs';
2
- import { cancel, confirm, intro, isCancel } from '@clack/prompts';
1
+ import { intro } from '@clack/prompts';
3
2
  import { finalizeAddOns, getFrameworkById, getPackageManager, populateAddOnOptionsDefaults, readConfigFile, } from '@tanstack/create';
4
3
  import { getProjectName, promptForAddOnOptions, selectAddOns, selectDeployment, selectGit, selectPackageManager, selectToolchain, } from './ui-prompts.js';
5
4
  import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
@@ -24,21 +23,10 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
24
23
  else {
25
24
  options.projectName = await getProjectName();
26
25
  }
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
- }
40
26
  // Mode is always file-router (TanStack Start)
41
27
  options.mode = 'file-router';
28
+ const template = cliOptions.template?.toLowerCase().trim();
29
+ const routerOnly = !!cliOptions.routerOnly || (template ? template !== 'file-router' : false);
42
30
  // TypeScript is always enabled with file-router
43
31
  options.typescript = true;
44
32
  // Package manager selection
@@ -54,7 +42,9 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
54
42
  const toolchain = await selectToolchain(options.framework, cliOptions.toolchain);
55
43
  // Deployment selection
56
44
  const deployment = showDeploymentOptions
57
- ? await selectDeployment(options.framework, cliOptions.deployment)
45
+ ? routerOnly
46
+ ? undefined
47
+ : await selectDeployment(options.framework, cliOptions.deployment)
58
48
  : undefined;
59
49
  // Add-ons selection
60
50
  const addOns = new Set();
@@ -64,10 +54,12 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
64
54
  if (deployment) {
65
55
  addOns.add(deployment);
66
56
  }
67
- for (const addOn of forcedAddOns) {
68
- addOns.add(addOn);
57
+ if (!routerOnly) {
58
+ for (const addOn of forcedAddOns) {
59
+ addOns.add(addOn);
60
+ }
69
61
  }
70
- if (Array.isArray(cliOptions.addOns)) {
62
+ if (!routerOnly && Array.isArray(cliOptions.addOns)) {
71
63
  for (const addOn of cliOptions.addOns) {
72
64
  if (addOn.toLowerCase() === 'start') {
73
65
  continue;
@@ -75,7 +67,7 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
75
67
  addOns.add(addOn);
76
68
  }
77
69
  }
78
- else {
70
+ else if (!routerOnly) {
79
71
  for (const addOn of await selectAddOns(options.framework, options.mode, 'add-on', 'What add-ons would you like for your project?', forcedAddOns)) {
80
72
  addOns.add(addOn);
81
73
  }
@@ -98,7 +90,7 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
98
90
  // Merge user options with defaults
99
91
  options.addOnOptions = { ...defaultOptions, ...userOptions };
100
92
  }
101
- options.git = cliOptions.git || (await selectGit());
93
+ options.git = cliOptions.git ?? (await selectGit());
102
94
  if (cliOptions.install === false) {
103
95
  options.install = false;
104
96
  }
@@ -1,5 +1,5 @@
1
1
  import type { FrameworkDefinition } from '@tanstack/create';
2
- export declare function cli({ name, appName, forcedAddOns, forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions, legacyAutoCreate, }: {
2
+ export declare function cli({ name, appName, forcedAddOns, forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions, legacyAutoCreate, defaultRouterOnly, }: {
3
3
  name: string;
4
4
  appName: string;
5
5
  forcedAddOns?: Array<string>;
@@ -9,4 +9,5 @@ export declare function cli({ name, appName, forcedAddOns, forcedDeployment, def
9
9
  frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>;
10
10
  showDeploymentOptions?: boolean;
11
11
  legacyAutoCreate?: boolean;
12
+ defaultRouterOnly?: boolean;
12
13
  }): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cli",
3
- "version": "0.59.4",
3
+ "version": "0.59.6",
4
4
  "description": "TanStack CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,8 +38,8 @@
38
38
  "tempy": "^3.1.0",
39
39
  "validate-npm-package-name": "^7.0.0",
40
40
  "zod": "^3.24.2",
41
- "@tanstack/create": "0.61.2",
42
- "@tanstack/create-ui": "0.59.4"
41
+ "@tanstack/create": "0.61.4",
42
+ "@tanstack/create-ui": "0.59.6"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@tanstack/config": "^0.16.2",
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import { resolve } from 'node:path'
3
3
  import { Command, InvalidArgumentError } from 'commander'
4
- import { intro, log } from '@clack/prompts'
4
+ import { cancel, confirm, intro, isCancel, log } from '@clack/prompts'
5
5
  import chalk from 'chalk'
6
6
  import semver from 'semver'
7
7
 
@@ -55,6 +55,7 @@ export function cli({
55
55
  frameworkDefinitionInitializers,
56
56
  showDeploymentOptions = false,
57
57
  legacyAutoCreate = false,
58
+ defaultRouterOnly = false,
58
59
  }: {
59
60
  name: string
60
61
  appName: string
@@ -65,11 +66,43 @@ export function cli({
65
66
  frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
66
67
  showDeploymentOptions?: boolean
67
68
  legacyAutoCreate?: boolean
69
+ defaultRouterOnly?: boolean
68
70
  }) {
69
71
  const environment = createUIEnvironment(appName, false)
70
72
 
71
73
  const program = new Command()
72
74
 
75
+ async function confirmTargetDirectorySafety(
76
+ targetDir: string,
77
+ forced?: boolean,
78
+ ) {
79
+ if (forced) {
80
+ return
81
+ }
82
+
83
+ if (!fs.existsSync(targetDir)) {
84
+ return
85
+ }
86
+
87
+ if (!fs.statSync(targetDir).isDirectory()) {
88
+ throw new Error(`Target path exists and is not a directory: ${targetDir}`)
89
+ }
90
+
91
+ if (fs.readdirSync(targetDir).length === 0) {
92
+ return
93
+ }
94
+
95
+ const shouldContinue = await confirm({
96
+ message: `Target directory "${targetDir}" already exists and is not empty. Continue anyway?`,
97
+ initialValue: false,
98
+ })
99
+
100
+ if (isCancel(shouldContinue) || !shouldContinue) {
101
+ cancel('Operation cancelled.')
102
+ process.exit(0)
103
+ }
104
+ }
105
+
73
106
  const availableFrameworks = getFrameworks().map((f) => f.name)
74
107
 
75
108
  const toolchains = new Set<string>()
@@ -251,6 +284,7 @@ export function cli({
251
284
  console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...')
252
285
  }
253
286
  const silentEnvironment = createUIEnvironment(appName, true)
287
+ await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force)
254
288
  await createApp(silentEnvironment, normalizedOpts)
255
289
  console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
256
290
 
@@ -275,6 +309,18 @@ export function cli({
275
309
  ...options,
276
310
  } as CliOptions
277
311
 
312
+ if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
313
+ cliOptions.routerOnly = true
314
+ }
315
+
316
+ if (
317
+ cliOptions.routerOnly !== true &&
318
+ cliOptions.template &&
319
+ cliOptions.template.toLowerCase() !== 'file-router'
320
+ ) {
321
+ cliOptions.routerOnly = true
322
+ }
323
+
278
324
  cliOptions.framework = getFrameworkByName(
279
325
  options.framework || defaultFramework || 'React',
280
326
  )!.id
@@ -327,6 +373,9 @@ export function cli({
327
373
  throw new Error('No options were provided')
328
374
  }
329
375
 
376
+ ;(finalOptions as Options & { routerOnly?: boolean }).routerOnly =
377
+ !!cliOptions.routerOnly
378
+
330
379
  // Determine target directory:
331
380
  // 1. Use --target-dir if provided
332
381
  // 2. Use targetDir from normalizeOptions if set (handles "." case)
@@ -342,6 +391,7 @@ export function cli({
342
391
  finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName)
343
392
  }
344
393
 
394
+ await confirmTargetDirectorySafety(finalOptions.targetDir, options.force)
345
395
  await createApp(environment, finalOptions)
346
396
  } catch (error) {
347
397
  log.error(
@@ -402,7 +452,7 @@ export function cli({
402
452
  )
403
453
  .option(
404
454
  '--router-only',
405
- 'Deprecated: compatibility flag from create-tsrouter-app',
455
+ 'Use router-only compatibility mode (file-based routing without TanStack Start)',
406
456
  )
407
457
  .option(
408
458
  '--template <type>',
@@ -471,6 +521,7 @@ export function cli({
471
521
  '--addon-details <addon-id>',
472
522
  'show detailed information about a specific add-on',
473
523
  )
524
+ .option('--git', 'create a git repository')
474
525
  .option('--no-git', 'do not create a git repository')
475
526
  .option(
476
527
  '--target-dir <path>',
@@ -33,10 +33,26 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
33
33
 
34
34
  if (cliOptions.routerOnly) {
35
35
  warnings.push(
36
- 'The --router-only flag is deprecated and ignored. `tanstack create` already creates router-based apps.',
36
+ '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.',
37
37
  )
38
38
  }
39
39
 
40
+ if (cliOptions.routerOnly && cliOptions.addOns) {
41
+ warnings.push(
42
+ 'Ignoring --add-ons in router-only compatibility mode.',
43
+ )
44
+ }
45
+
46
+ if (cliOptions.routerOnly && cliOptions.deployment) {
47
+ warnings.push(
48
+ 'Ignoring --deployment in router-only compatibility mode.',
49
+ )
50
+ }
51
+
52
+ if (cliOptions.routerOnly && cliOptions.starter) {
53
+ warnings.push('Ignoring --starter in router-only compatibility mode.')
54
+ }
55
+
40
56
  if (cliOptions.tailwind === true) {
41
57
  warnings.push(
42
58
  'The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.',
@@ -70,9 +86,7 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
70
86
  }
71
87
  }
72
88
 
73
- warnings.push(
74
- 'The --template flag is deprecated. TypeScript/TSX is the default and only supported template.',
75
- )
89
+ warnings.push('The --template flag is deprecated and mapped for compatibility.')
76
90
 
77
91
  return { warnings }
78
92
  }
@@ -110,8 +124,14 @@ export async function normalizeOptions(
110
124
 
111
125
  // Mode is always file-router (TanStack Start)
112
126
  let mode = 'file-router'
127
+ let routerOnly = !!cliOptions.routerOnly
128
+
129
+ const template = cliOptions.template?.toLowerCase().trim()
130
+ if (template && template !== 'file-router') {
131
+ routerOnly = true
132
+ }
113
133
 
114
- const starter = cliOptions.starter
134
+ const starter = !routerOnly && cliOptions.starter
115
135
  ? await loadStarter(cliOptions.starter)
116
136
  : undefined
117
137
 
@@ -143,10 +163,10 @@ export async function normalizeOptions(
143
163
  cliOptions.deployment
144
164
  ) {
145
165
  const selectedAddOns = new Set<string>([
146
- ...(starter?.dependsOn || []),
147
- ...(forcedAddOns || []),
166
+ ...(routerOnly ? [] : (starter?.dependsOn || [])),
167
+ ...(routerOnly ? [] : (forcedAddOns || [])),
148
168
  ])
149
- if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
169
+ if (!routerOnly && cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
150
170
  for (const a of cliOptions.addOns) {
151
171
  if (a.toLowerCase() === 'start') {
152
172
  continue
@@ -157,11 +177,11 @@ export async function normalizeOptions(
157
177
  if (cliOptions.toolchain) {
158
178
  selectedAddOns.add(cliOptions.toolchain)
159
179
  }
160
- if (cliOptions.deployment) {
180
+ if (!routerOnly && cliOptions.deployment) {
161
181
  selectedAddOns.add(cliOptions.deployment)
162
182
  }
163
183
 
164
- if (!cliOptions.deployment && opts?.forcedDeployment) {
184
+ if (!routerOnly && !cliOptions.deployment && opts?.forcedDeployment) {
165
185
  selectedAddOns.add(opts.forcedDeployment)
166
186
  }
167
187
 
@@ -195,7 +215,7 @@ export async function normalizeOptions(
195
215
  cliOptions.packageManager ||
196
216
  getPackageManager() ||
197
217
  DEFAULT_PACKAGE_MANAGER,
198
- git: !!cliOptions.git,
218
+ git: cliOptions.git ?? true,
199
219
  install: cliOptions.install,
200
220
  chosenAddOns,
201
221
  addOnOptions: {
package/src/options.ts CHANGED
@@ -1,5 +1,4 @@
1
- import fs from 'node:fs'
2
- import { cancel, confirm, intro, isCancel } from '@clack/prompts'
1
+ import { intro } from '@clack/prompts'
3
2
 
4
3
  import {
5
4
  finalizeAddOns,
@@ -59,25 +58,11 @@ export async function promptForCreateOptions(
59
58
  options.projectName = await getProjectName()
60
59
  }
61
60
 
62
- // Check if target directory is empty
63
- if (
64
- !cliOptions.force &&
65
- fs.existsSync(options.projectName) &&
66
- fs.readdirSync(options.projectName).length > 0
67
- ) {
68
- const shouldContinue = await confirm({
69
- message: `Target directory ${options.projectName} is not empty. Do you want to continue?`,
70
- initialValue: true,
71
- })
72
-
73
- if (isCancel(shouldContinue) || !shouldContinue) {
74
- cancel('Operation cancelled.')
75
- process.exit(0)
76
- }
77
- }
78
-
79
61
  // Mode is always file-router (TanStack Start)
80
62
  options.mode = 'file-router'
63
+ const template = cliOptions.template?.toLowerCase().trim()
64
+ const routerOnly =
65
+ !!cliOptions.routerOnly || (template ? template !== 'file-router' : false)
81
66
 
82
67
  // TypeScript is always enabled with file-router
83
68
  options.typescript = true
@@ -99,7 +84,9 @@ export async function promptForCreateOptions(
99
84
 
100
85
  // Deployment selection
101
86
  const deployment = showDeploymentOptions
102
- ? await selectDeployment(options.framework, cliOptions.deployment)
87
+ ? routerOnly
88
+ ? undefined
89
+ : await selectDeployment(options.framework, cliOptions.deployment)
103
90
  : undefined
104
91
 
105
92
  // Add-ons selection
@@ -112,18 +99,20 @@ export async function promptForCreateOptions(
112
99
  addOns.add(deployment)
113
100
  }
114
101
 
115
- for (const addOn of forcedAddOns) {
116
- addOns.add(addOn)
102
+ if (!routerOnly) {
103
+ for (const addOn of forcedAddOns) {
104
+ addOns.add(addOn)
105
+ }
117
106
  }
118
107
 
119
- if (Array.isArray(cliOptions.addOns)) {
108
+ if (!routerOnly && Array.isArray(cliOptions.addOns)) {
120
109
  for (const addOn of cliOptions.addOns) {
121
110
  if (addOn.toLowerCase() === 'start') {
122
111
  continue
123
112
  }
124
113
  addOns.add(addOn)
125
114
  }
126
- } else {
115
+ } else if (!routerOnly) {
127
116
  for (const addOn of await selectAddOns(
128
117
  options.framework,
129
118
  options.mode,
@@ -168,7 +157,7 @@ export async function promptForCreateOptions(
168
157
  options.addOnOptions = { ...defaultOptions, ...userOptions }
169
158
  }
170
159
 
171
- options.git = cliOptions.git || (await selectGit())
160
+ options.git = cliOptions.git ?? (await selectGit())
172
161
  if (cliOptions.install === false) {
173
162
  options.install = false
174
163
  }
@@ -91,6 +91,23 @@ describe('normalizeOptions', () => {
91
91
  expect(solidOptions?.tailwind).toBe(true)
92
92
  })
93
93
 
94
+ it('defaults git initialization to enabled', async () => {
95
+ const options = await normalizeOptions({
96
+ projectName: 'test',
97
+ })
98
+
99
+ expect(options?.git).toBe(true)
100
+ })
101
+
102
+ it('respects explicit --no-git option', async () => {
103
+ const options = await normalizeOptions({
104
+ projectName: 'test',
105
+ git: false,
106
+ })
107
+
108
+ expect(options?.git).toBe(false)
109
+ })
110
+
94
111
  it('should handle a starter url', async () => {
95
112
  __testRegisterFramework({
96
113
  id: 'solid',
@@ -274,6 +291,56 @@ describe('normalizeOptions', () => {
274
291
  expect(options?.typescript).toBe(true)
275
292
  })
276
293
 
294
+ it('should keep file-router mode in router-only compatibility mode', async () => {
295
+ const options = await normalizeOptions({
296
+ projectName: 'test',
297
+ routerOnly: true,
298
+ })
299
+
300
+ expect(options?.mode).toBe('file-router')
301
+ })
302
+
303
+ it('should ignore add-ons and deployment in router-only mode but keep toolchain', async () => {
304
+ __testRegisterFramework({
305
+ id: 'react-cra',
306
+ name: 'react',
307
+ getAddOns: () => [
308
+ {
309
+ id: 'form',
310
+ name: 'Form',
311
+ modes: ['file-router'],
312
+ },
313
+ {
314
+ id: 'nitro',
315
+ name: 'nitro',
316
+ modes: ['file-router'],
317
+ type: 'deployment',
318
+ },
319
+ {
320
+ id: 'biome',
321
+ name: 'Biome',
322
+ modes: ['file-router'],
323
+ type: 'toolchain',
324
+ },
325
+ ],
326
+ })
327
+
328
+ const options = await normalizeOptions(
329
+ {
330
+ projectName: 'test',
331
+ framework: 'react-cra',
332
+ routerOnly: true,
333
+ addOns: ['form'],
334
+ deployment: 'nitro',
335
+ toolchain: 'biome',
336
+ },
337
+ ['form'],
338
+ { forcedDeployment: 'nitro' },
339
+ )
340
+
341
+ expect(options?.chosenAddOns.map((a) => a.id)).toEqual(['biome'])
342
+ })
343
+
277
344
  it('should handle the funky Windows edge case with CLI parsing', async () => {
278
345
  __testRegisterFramework({
279
346
  id: 'react-cra',