Skip to content

Commit 05456f2

Browse files
fix(cli): default to interactive mode and add missing prompts (#435)
Running `tanstack create my-app` with no flags silently skipped prompts for framework, deployment, and install, falling through to normalizeOptions with defaults. Flip the default: interactive when stdin/stdout is a TTY and CI is unset, non-interactive when --yes/--non-interactive is passed or the environment is not interactive (pipe, CI, subprocess). - Add a framework prompt when the CLI hosts multiple frameworks and no --framework flag was passed. - Add an install prompt when --no-install is not passed. - Show the deployment prompt by default in the root @tanstack/cli (opt out with showDeploymentOptions: false). Legacy aliases that set forcedDeployment now use it as the prompt's initial value so the previous default is preserved. - Preserve an explicit --add-ons <ids> array instead of overwriting it with the interactive sentinel. - Remove the 'React' default from the --framework commander option so we can distinguish "flag passed" from "default applied".
1 parent e3de582 commit 05456f2

4 files changed

Lines changed: 126 additions & 29 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@tanstack/cli': patch
3+
---
4+
5+
Fix interactive mode not prompting for all options.
6+
7+
- Default to interactive mode. Previously, `tanstack create my-app` silently applied defaults for framework, deployment, and install. Opt out with `--yes` / `--non-interactive`.
8+
- Add framework selection prompt when the CLI supports multiple frameworks and no `--framework` flag is passed.
9+
- Add "install dependencies now?" prompt when `--no-install` is not passed.
10+
- Show deployment adapter prompt by default (previously required `showDeploymentOptions: true`).
11+
- Honor `forcedDeployment` as the default selection in the deployment prompt, so deprecated aliases keep a sensible default.
12+
- Preserve explicit `--add-ons` arrays instead of overwriting them with the interactive sentinel.

packages/cli/src/cli.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export function cli({
238238
forcedDeployment,
239239
defaultFramework,
240240
frameworkDefinitionInitializers,
241-
showDeploymentOptions = false,
241+
showDeploymentOptions = true,
242242
legacyAutoCreate = false,
243243
defaultRouterOnly = false,
244244
}: {
@@ -649,9 +649,11 @@ export function cli({
649649
cliOptions.routerOnly = true
650650
}
651651

652-
cliOptions.framework = getFrameworkByName(
653-
options.framework || defaultFramework || 'React',
654-
)!.id
652+
if (options.framework) {
653+
cliOptions.framework = getFrameworkByName(options.framework)!.id
654+
} else if (defaultFramework) {
655+
cliOptions.framework = getFrameworkByName(defaultFramework)!.id
656+
}
655657

656658
const nonInteractive = !!cliOptions.nonInteractive || !!cliOptions.yes
657659
if (cliOptions.interactive && nonInteractive) {
@@ -660,43 +662,52 @@ export function cli({
660662
)
661663
}
662664

663-
const addOnsFlagPassed = process.argv.includes('--add-ons')
665+
const hasInteractiveTerminal =
666+
!!process.stdin.isTTY && !!process.stdout.isTTY && !process.env.CI
664667
const wantsInteractiveMode =
665668
!nonInteractive &&
666-
(cliOptions.interactive ||
667-
(cliOptions.addOns === true && addOnsFlagPassed))
669+
(cliOptions.interactive || hasInteractiveTerminal)
668670

669671
let finalOptions: Options | undefined
670672
if (wantsInteractiveMode) {
671-
cliOptions.addOns = true
673+
if (cliOptions.addOns === undefined) {
674+
cliOptions.addOns = true
675+
}
672676
} else {
677+
if (!cliOptions.framework) {
678+
cliOptions.framework = getFrameworkByName(
679+
defaultFramework || 'React',
680+
)!.id
681+
}
673682
finalOptions = await normalizeOptions(
674683
cliOptions,
675684
forcedAddOns,
676685
{ forcedDeployment },
677686
)
678687
}
679688

680-
if (nonInteractive) {
681-
if (cliOptions.addOns === true) {
682-
throw new Error(
683-
'When using --non-interactive/--yes, pass explicit add-ons via --add-ons <ids>.',
684-
)
685-
}
689+
if (!wantsInteractiveMode && cliOptions.addOns === true) {
690+
throw new Error(
691+
'When running non-interactively, pass explicit add-ons via --add-ons <ids>.',
692+
)
686693
}
687694

688695
if (finalOptions) {
689696
intro(`Creating a new ${appName} app in ${projectName}...`)
690697
} else {
691-
if (nonInteractive) {
698+
if (!wantsInteractiveMode) {
692699
throw new Error(
693700
'Project name is required in non-interactive mode. Pass [project-name] or --target-dir.',
694701
)
695702
}
696703
intro(`Let's configure your ${appName} application`)
697704
finalOptions = await promptForCreateOptions(cliOptions, {
698705
forcedAddOns,
706+
forcedDeployment,
699707
showDeploymentOptions,
708+
defaultFrameworkId: defaultFramework
709+
? getFrameworkByName(defaultFramework)?.id
710+
: undefined,
700711
})
701712
}
702713

@@ -756,7 +767,6 @@ export function cli({
756767
}
757768
return value
758769
},
759-
defaultFramework || 'React',
760770
)
761771
}
762772

packages/cli/src/options.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { intro } from '@clack/prompts'
33
import {
44
finalizeAddOns,
55
getFrameworkById,
6+
getFrameworks,
67
getPackageManager,
78
loadStarter,
89
populateAddOnOptionsDefaults,
@@ -16,7 +17,9 @@ import {
1617
selectAddOns,
1718
selectDeployment,
1819
selectExamples,
20+
selectFramework,
1921
selectGit,
22+
selectInstall,
2023
selectPackageManager,
2124
selectTemplate,
2225
selectToolchain,
@@ -39,15 +42,31 @@ export async function promptForCreateOptions(
3942
cliOptions: CliOptions,
4043
{
4144
forcedAddOns = [],
42-
showDeploymentOptions = false,
45+
forcedDeployment,
46+
showDeploymentOptions = true,
47+
defaultFrameworkId,
4348
}: {
4449
forcedAddOns?: Array<string>
50+
forcedDeployment?: string
4551
showDeploymentOptions?: boolean
52+
defaultFrameworkId?: string
4653
},
4754
): Promise<Required<Options> | undefined> {
4855
const options = {} as Required<Options>
4956

50-
options.framework = getFrameworkById(cliOptions.framework || 'react')!
57+
if (cliOptions.framework) {
58+
options.framework = getFrameworkById(cliOptions.framework)!
59+
} else {
60+
const availableFrameworks = getFrameworks()
61+
if (defaultFrameworkId || availableFrameworks.length <= 1) {
62+
options.framework = getFrameworkById(defaultFrameworkId || 'react')!
63+
} else {
64+
options.framework = await selectFramework(
65+
availableFrameworks,
66+
defaultFrameworkId,
67+
)
68+
}
69+
}
5170

5271
// Validate project name
5372
if (cliOptions.projectName) {
@@ -130,11 +149,20 @@ export async function promptForCreateOptions(
130149
)
131150

132151
// Deployment selection
133-
const deployment = showDeploymentOptions
134-
? routerOnly
135-
? undefined
136-
: await selectDeployment(options.framework, cliOptions.deployment)
137-
: undefined
152+
let deployment: string | undefined
153+
if (routerOnly) {
154+
deployment = undefined
155+
} else if (cliOptions.deployment) {
156+
deployment = cliOptions.deployment
157+
} else if (showDeploymentOptions) {
158+
deployment = await selectDeployment(
159+
options.framework,
160+
cliOptions.deployment,
161+
forcedDeployment,
162+
)
163+
} else {
164+
deployment = forcedDeployment
165+
}
138166

139167
// Add-ons selection
140168
const addOns: Set<string> = new Set()
@@ -226,9 +254,7 @@ export async function promptForCreateOptions(
226254
envVarValues
227255

228256
options.git = cliOptions.git ?? (await selectGit())
229-
if (cliOptions.install === false) {
230-
options.install = false
231-
}
257+
options.install = cliOptions.install ?? (await selectInstall())
232258

233259
if (starter) {
234260
options.starter = starter

packages/cli/src/ui-prompts.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,47 @@ import type { AddOn, PackageManager } from '@tanstack/create'
2020

2121
import type { Framework } from '@tanstack/create/dist/types/types.js'
2222

23+
export async function selectFramework(
24+
frameworks: Array<Framework>,
25+
defaultFrameworkId?: string,
26+
): Promise<Framework> {
27+
const initialValue =
28+
(defaultFrameworkId &&
29+
frameworks.find(
30+
(f) => f.id.toLowerCase() === defaultFrameworkId.toLowerCase(),
31+
)?.id) ||
32+
frameworks[0]!.id
33+
34+
const selected = await select({
35+
message: 'Select framework:',
36+
options: frameworks.map((f) => ({ value: f.id, label: f.name })),
37+
initialValue,
38+
})
39+
40+
if (isCancel(selected)) {
41+
cancel('Operation cancelled.')
42+
process.exit(0)
43+
}
44+
45+
const framework = frameworks.find((f) => f.id === selected)
46+
if (!framework) {
47+
throw new Error(`Unknown framework: ${selected}`)
48+
}
49+
return framework
50+
}
51+
52+
export async function selectInstall(): Promise<boolean> {
53+
const install = await confirm({
54+
message: 'Would you like to install dependencies now?',
55+
initialValue: true,
56+
})
57+
if (isCancel(install)) {
58+
cancel('Operation cancelled.')
59+
process.exit(0)
60+
}
61+
return install
62+
}
63+
2364
export async function getProjectName(): Promise<string> {
2465
const value = await text({
2566
message: 'What would you like to name your project?',
@@ -350,6 +391,7 @@ export async function promptForEnvVars(
350391
export async function selectDeployment(
351392
framework: Framework,
352393
deployment?: string,
394+
forcedDeployment?: string,
353395
): Promise<string | undefined> {
354396
const deployments = new Set<AddOn>()
355397
let initialValue: string | undefined = undefined
@@ -361,21 +403,28 @@ export async function selectDeployment(
361403
if (deployment && addOn.id === deployment) {
362404
return deployment
363405
}
364-
if (addOn.default) {
406+
if (forcedDeployment && addOn.id === forcedDeployment) {
407+
initialValue = addOn.id
408+
} else if (!initialValue && addOn.default) {
365409
initialValue = addOn.id
366410
}
367411
}
368412
}
369413

414+
if (deployments.size === 0) {
415+
return undefined
416+
}
417+
370418
const dp = await select({
371-
message: 'Select deployment adapter',
419+
message: 'Select deployment adapter:',
372420
options: [
421+
{ value: undefined, label: 'None' },
373422
...Array.from(deployments).map((d) => ({
374423
value: d.id,
375424
label: d.name,
376425
})),
377426
],
378-
initialValue: initialValue,
427+
initialValue,
379428
})
380429

381430
if (isCancel(dp)) {

0 commit comments

Comments
 (0)