From c1be53f64b2ffec1bf466073f8c0782f77e8a8a7 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 20 Apr 2026 01:15:35 -0600 Subject: [PATCH] fix(cli): default to interactive mode and add missing prompts 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 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". --- .changeset/fix-interactive-prompts.md | 12 ++++++ packages/cli/src/cli.ts | 42 ++++++++++++-------- packages/cli/src/options.ts | 46 +++++++++++++++++----- packages/cli/src/ui-prompts.ts | 55 +++++++++++++++++++++++++-- 4 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 .changeset/fix-interactive-prompts.md diff --git a/.changeset/fix-interactive-prompts.md b/.changeset/fix-interactive-prompts.md new file mode 100644 index 00000000..2a3c5b2f --- /dev/null +++ b/.changeset/fix-interactive-prompts.md @@ -0,0 +1,12 @@ +--- +'@tanstack/cli': patch +--- + +Fix interactive mode not prompting for all options. + +- Default to interactive mode. Previously, `tanstack create my-app` silently applied defaults for framework, deployment, and install. Opt out with `--yes` / `--non-interactive`. +- Add framework selection prompt when the CLI supports multiple frameworks and no `--framework` flag is passed. +- Add "install dependencies now?" prompt when `--no-install` is not passed. +- Show deployment adapter prompt by default (previously required `showDeploymentOptions: true`). +- Honor `forcedDeployment` as the default selection in the deployment prompt, so deprecated aliases keep a sensible default. +- Preserve explicit `--add-ons` arrays instead of overwriting them with the interactive sentinel. diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 7c487f84..31815a13 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -238,7 +238,7 @@ export function cli({ forcedDeployment, defaultFramework, frameworkDefinitionInitializers, - showDeploymentOptions = false, + showDeploymentOptions = true, legacyAutoCreate = false, defaultRouterOnly = false, }: { @@ -649,9 +649,11 @@ export function cli({ cliOptions.routerOnly = true } - cliOptions.framework = getFrameworkByName( - options.framework || defaultFramework || 'React', - )!.id + if (options.framework) { + cliOptions.framework = getFrameworkByName(options.framework)!.id + } else if (defaultFramework) { + cliOptions.framework = getFrameworkByName(defaultFramework)!.id + } const nonInteractive = !!cliOptions.nonInteractive || !!cliOptions.yes if (cliOptions.interactive && nonInteractive) { @@ -660,16 +662,23 @@ export function cli({ ) } - const addOnsFlagPassed = process.argv.includes('--add-ons') + const hasInteractiveTerminal = + !!process.stdin.isTTY && !!process.stdout.isTTY && !process.env.CI const wantsInteractiveMode = !nonInteractive && - (cliOptions.interactive || - (cliOptions.addOns === true && addOnsFlagPassed)) + (cliOptions.interactive || hasInteractiveTerminal) let finalOptions: Options | undefined if (wantsInteractiveMode) { - cliOptions.addOns = true + if (cliOptions.addOns === undefined) { + cliOptions.addOns = true + } } else { + if (!cliOptions.framework) { + cliOptions.framework = getFrameworkByName( + defaultFramework || 'React', + )!.id + } finalOptions = await normalizeOptions( cliOptions, forcedAddOns, @@ -677,18 +686,16 @@ export function cli({ ) } - if (nonInteractive) { - if (cliOptions.addOns === true) { - throw new Error( - 'When using --non-interactive/--yes, pass explicit add-ons via --add-ons .', - ) - } + if (!wantsInteractiveMode && cliOptions.addOns === true) { + throw new Error( + 'When running non-interactively, pass explicit add-ons via --add-ons .', + ) } if (finalOptions) { intro(`Creating a new ${appName} app in ${projectName}...`) } else { - if (nonInteractive) { + if (!wantsInteractiveMode) { throw new Error( 'Project name is required in non-interactive mode. Pass [project-name] or --target-dir.', ) @@ -696,7 +703,11 @@ export function cli({ intro(`Let's configure your ${appName} application`) finalOptions = await promptForCreateOptions(cliOptions, { forcedAddOns, + forcedDeployment, showDeploymentOptions, + defaultFrameworkId: defaultFramework + ? getFrameworkByName(defaultFramework)?.id + : undefined, }) } @@ -756,7 +767,6 @@ export function cli({ } return value }, - defaultFramework || 'React', ) } diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index 15902ade..4b4d7d35 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -3,6 +3,7 @@ import { intro } from '@clack/prompts' import { finalizeAddOns, getFrameworkById, + getFrameworks, getPackageManager, loadStarter, populateAddOnOptionsDefaults, @@ -16,7 +17,9 @@ import { selectAddOns, selectDeployment, selectExamples, + selectFramework, selectGit, + selectInstall, selectPackageManager, selectTemplate, selectToolchain, @@ -39,15 +42,31 @@ export async function promptForCreateOptions( cliOptions: CliOptions, { forcedAddOns = [], - showDeploymentOptions = false, + forcedDeployment, + showDeploymentOptions = true, + defaultFrameworkId, }: { forcedAddOns?: Array + forcedDeployment?: string showDeploymentOptions?: boolean + defaultFrameworkId?: string }, ): Promise | undefined> { const options = {} as Required - options.framework = getFrameworkById(cliOptions.framework || 'react')! + if (cliOptions.framework) { + options.framework = getFrameworkById(cliOptions.framework)! + } else { + const availableFrameworks = getFrameworks() + if (defaultFrameworkId || availableFrameworks.length <= 1) { + options.framework = getFrameworkById(defaultFrameworkId || 'react')! + } else { + options.framework = await selectFramework( + availableFrameworks, + defaultFrameworkId, + ) + } + } // Validate project name if (cliOptions.projectName) { @@ -130,11 +149,20 @@ export async function promptForCreateOptions( ) // Deployment selection - const deployment = showDeploymentOptions - ? routerOnly - ? undefined - : await selectDeployment(options.framework, cliOptions.deployment) - : undefined + let deployment: string | undefined + if (routerOnly) { + deployment = undefined + } else if (cliOptions.deployment) { + deployment = cliOptions.deployment + } else if (showDeploymentOptions) { + deployment = await selectDeployment( + options.framework, + cliOptions.deployment, + forcedDeployment, + ) + } else { + deployment = forcedDeployment + } // Add-ons selection const addOns: Set = new Set() @@ -226,9 +254,7 @@ export async function promptForCreateOptions( envVarValues options.git = cliOptions.git ?? (await selectGit()) - if (cliOptions.install === false) { - options.install = false - } + options.install = cliOptions.install ?? (await selectInstall()) if (starter) { options.starter = starter diff --git a/packages/cli/src/ui-prompts.ts b/packages/cli/src/ui-prompts.ts index d438a955..b559aa4c 100644 --- a/packages/cli/src/ui-prompts.ts +++ b/packages/cli/src/ui-prompts.ts @@ -20,6 +20,47 @@ import type { AddOn, PackageManager } from '@tanstack/create' import type { Framework } from '@tanstack/create/dist/types/types.js' +export async function selectFramework( + frameworks: Array, + defaultFrameworkId?: string, +): Promise { + const initialValue = + (defaultFrameworkId && + frameworks.find( + (f) => f.id.toLowerCase() === defaultFrameworkId.toLowerCase(), + )?.id) || + frameworks[0]!.id + + const selected = await select({ + message: 'Select framework:', + options: frameworks.map((f) => ({ value: f.id, label: f.name })), + initialValue, + }) + + if (isCancel(selected)) { + cancel('Operation cancelled.') + process.exit(0) + } + + const framework = frameworks.find((f) => f.id === selected) + if (!framework) { + throw new Error(`Unknown framework: ${selected}`) + } + return framework +} + +export async function selectInstall(): Promise { + const install = await confirm({ + message: 'Would you like to install dependencies now?', + initialValue: true, + }) + if (isCancel(install)) { + cancel('Operation cancelled.') + process.exit(0) + } + return install +} + export async function getProjectName(): Promise { const value = await text({ message: 'What would you like to name your project?', @@ -350,6 +391,7 @@ export async function promptForEnvVars( export async function selectDeployment( framework: Framework, deployment?: string, + forcedDeployment?: string, ): Promise { const deployments = new Set() let initialValue: string | undefined = undefined @@ -361,21 +403,28 @@ export async function selectDeployment( if (deployment && addOn.id === deployment) { return deployment } - if (addOn.default) { + if (forcedDeployment && addOn.id === forcedDeployment) { + initialValue = addOn.id + } else if (!initialValue && addOn.default) { initialValue = addOn.id } } } + if (deployments.size === 0) { + return undefined + } + const dp = await select({ - message: 'Select deployment adapter', + message: 'Select deployment adapter:', options: [ + { value: undefined, label: 'None' }, ...Array.from(deployments).map((d) => ({ value: d.id, label: d.name, })), ], - initialValue: initialValue, + initialValue, }) if (isCancel(dp)) {