Skip to content

fix(cli): default to interactive mode and add missing prompts#435

Merged
tannerlinsley merged 1 commit intomainfrom
taren/fix-interactive-prompts
Apr 20, 2026
Merged

fix(cli): default to interactive mode and add missing prompts#435
tannerlinsley merged 1 commit intomainfrom
taren/fix-interactive-prompts

Conversation

@tannerlinsley
Copy link
Copy Markdown
Member

@tannerlinsley tannerlinsley commented Apr 20, 2026

Summary

Running `tanstack create my-app` without flags silently skipped prompts for framework, deployment, and install — it fell straight through to `normalizeOptions` and applied defaults, giving users no chance to configure things that should be interactive.

Problems

  1. Default interactive mode was never triggered. The old detection only entered interactive mode on `--interactive` or bare `--add-ons`. Everything else hit `normalizeOptions` which doesn't prompt for anything. Running `tanstack create my-app` silently picked React, no toolchain, no deployment, no add-ons.
  2. Deployment was never prompted. `showDeploymentOptions` defaulted to `false` on the root `@tanstack/cli`, so even if you did reach the interactive path, no deployment prompt appeared.
  3. Framework was never prompted. The CLI defaulted to React silently when a user had no `--framework` flag.
  4. Install was never prompted. The code only honored `--no-install`; there was no "install dependencies now?" prompt.

Fixes

  • Flip the interactive-mode default: interactive when stdin/stdout is a TTY and `CI` is unset. Opt out with `--yes` / `--non-interactive`, or by piping/running in CI (both auto-detected so existing scripts and e2e tests keep working).
  • Add a `selectFramework` prompt that shows up only when the CLI hosts multiple frameworks and no `--framework` flag was passed. Aliases with a fixed `defaultFramework` don't get the prompt.
  • Add a `selectInstall` prompt when `--no-install` is not passed.
  • Default `showDeploymentOptions` to `true`. Legacy aliases that set `forcedDeployment` (e.g. nitro) now pass that value through to the deployment prompt as the initial selection, so the old default is preserved but users can pick something else.
  • Preserve an explicit `--add-ons ` array in interactive mode instead of overwriting it with the "prompt me" sentinel.
  • Remove the `'React'` default from the commander `--framework` option so we can distinguish "flag passed" from "default applied."

Test plan

  • Unit tests: `pnpm --filter @tanstack/cli test` (84 passed)
  • E2E tests: `pnpm --filter @tanstack/cli test:e2e` (3 passed, including the CI-mode smoke that runs without `--yes`)
  • Manual: `tanstack create my-app` in a TTY prompts for framework, toolchain, deployment, examples, add-ons, add-on options, env vars, git, install
  • Manual: `tanstack create my-app --yes --no-install --no-git` skips all prompts, uses defaults
  • Manual: `CI=1 tanstack create my-app --framework react --package-manager pnpm --no-git` (matches the e2e test invocation) completes without hanging — auto-detected non-interactive

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed interactive mode to properly prompt for framework selection when multiple options are available
    • Corrected deployment adapter selection to respect forced deployment defaults
    • Fixed add-ons handling to preserve explicitly provided configurations
  • New Features

    • Deployment options now display by default in interactive mode
    • Added confirmation prompt for dependency installation

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".
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

The pull request updates the CLI's interactive mode behavior by enabling interactive prompts by default, adjusting framework resolution to avoid unconditional React fallback, detecting interactive terminals via TTY status, and adding new UI prompt functions for framework and install selection while refining deployment selection logic.

Changes

Cohort / File(s) Summary
Changeset Documentation
.changeset/fix-interactive-prompts.md
Documents behavioral fixes for interactive prompting: defaults to interactive mode, adds conditional prompts for framework/deployment/install selection, and preserves explicitly provided --add-ons arrays.
CLI Core Entry Point
packages/cli/src/cli.ts
Changes default showDeploymentOptions to true, refines framework resolution to avoid unconditional React fallback, detects interactive mode via TTY status and CI environment rather than --add-ons presence, and adjusts add-on defaulting logic for interactive mode.
Options Processing
packages/cli/src/options.ts
Adds forcedDeployment and defaultFrameworkId parameters to promptForCreateOptions, branches framework selection logic (auto-pick vs. prompt based on availability), updates deployment selection to prompt when enabled or fall back to forcedDeployment, and refines install selection logic.
UI Prompt Helpers
packages/cli/src/ui-prompts.ts
Introduces selectFramework() and selectInstall() exported functions, extends selectDeployment() signature to accept optional forcedDeployment parameter with precedence for initial value, and updates deployment UI to include explicit None choice and refined messaging.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as CLI Entry
    participant TTY as Terminal Detection
    participant Options as Options Processor
    participant UIPrompts as UI Prompts
    participant Result as Resolved Options

    User->>CLI: Run CLI command
    CLI->>TTY: Check stdin/stdout TTY & CI env
    TTY-->>CLI: Interactive terminal status
    CLI->>CLI: Determine wantsInteractiveMode
    
    alt Interactive Mode Enabled
        CLI->>Options: Call promptForCreateOptions<br/>(with defaultFrameworkId, forcedDeployment)
        
        alt Framework Not Provided
            Options->>UIPrompts: Multiple frameworks available?
            UIPrompts-->>Options: Yes → selectFramework()
            UIPrompts->>User: Prompt: Select framework
            User-->>UIPrompts: User selects framework
            UIPrompts-->>Options: Framework selected
        else Framework Not Provided & Single/Default
            Options->>Options: Auto-select default
        end
        
        alt Deployment Selection Enabled
            Options->>UIPrompts: selectDeployment(framework, deployment?, forcedDeployment)
            UIPrompts->>User: Prompt: Select deployment adapter
            User-->>UIPrompts: User selects deployment
            UIPrompts-->>Options: Deployment selected
        else Deployment Not Enabled
            Options->>Options: Use forcedDeployment as fallback
        end
        
        Options->>UIPrompts: selectInstall()
        UIPrompts->>User: Prompt: Install dependencies?
        User-->>UIPrompts: User confirms/declines
        UIPrompts-->>Options: Install flag
        
        Options-->>CLI: Complete options object
    else Non-Interactive Mode
        CLI->>Options: Skip prompts, use defaults
        Options-->>CLI: Options with defaults applied
    end
    
    CLI->>Result: Return final resolved options
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Interactive paths now bloom with care,
Framework choices dance through prompting air,
TTY whispers tell if we should ask,
Prompts and defaults work hand in task,
A CLI that listens—how fair and square!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: making interactive mode the default and adding missing prompts for framework, deployment, and installation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch taren/fix-interactive-prompts

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 673-675: The current logic is coercing/normalizing --add-ons in a
way that causes Array.isArray(cliOptions.addOns) to no longer reliably indicate
a user-supplied list, which suppresses promptForAddOnOptions() and
promptForEnvVars(); update the normalization so arrays are preserved: in cli.ts
keep cliOptions.addOns untouched if Array.isArray(cliOptions.addOns), only set
cliOptions.addOns = true when cliOptions.addOns === undefined (and if your
parser can produce an empty string for a bare flag, normalize that to true as
well), and remove any later code that converts arrays into booleans so
promptForAddOnOptions() and promptForEnvVars() (from
packages/cli/src/options.ts) still see arrays and run interactively when
appropriate.

In `@packages/cli/src/options.ts`:
- Around line 61-63: The code currently forces 'react' when defaultFrameworkId
is falsy and availableFrameworks.length <= 1; change the logic in the block that
sets options.framework so it uses the lone available framework's id if present:
use defaultFrameworkId if set, otherwise if availableFrameworks.length === 1 use
availableFrameworks[0].id, and only fall back to a hard-coded default (or
undefined) when no frameworks exist; update the assignment that calls
getFrameworkById(...) (the code that sets options.framework) to derive the id
from defaultFrameworkId || availableFrameworks[0]?.id instead of defaulting to
'react'.
- Line 257: The prompt never runs because cliOptions.install is always true when
only a negatable --no-install is defined; update the CLI option definition so
the flag is not default-true (e.g., define both --install and --no-install in
the Command declaration in the CLI module) so cliOptions.install will be
undefined when the user omits the flag and the existing assignment
options.install = cliOptions.install ?? (await selectInstall()) will correctly
invoke selectInstall(); alternatively, if you prefer not to change the CLI API,
change the detection logic around options.install to check a distinct presence
signal from Commander (the explicit option presence) rather than relying on the
boolean value so selectInstall() runs only when the flag was not provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8073aa5-1cfe-4e8e-bb40-e4ec9500e383

📥 Commits

Reviewing files that changed from the base of the PR and between 523c999 and c1be53f.

📒 Files selected for processing (4)
  • .changeset/fix-interactive-prompts.md
  • packages/cli/src/cli.ts
  • packages/cli/src/options.ts
  • packages/cli/src/ui-prompts.ts

Comment thread packages/cli/src/cli.ts
Comment on lines +673 to +675
if (cliOptions.addOns === undefined) {
cliOptions.addOns = true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserving --add-ons arrays here also suppresses later add-on config/env-var prompts.

After this change, Array.isArray(cliOptions.addOns) no longer means “non-interactive”. packages/cli/src/options.ts still uses that check to skip promptForAddOnOptions() and promptForEnvVars(), so an interactive run like tanstack create my-app --add-ons auth will silently take defaults and can miss required env vars.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/cli.ts` around lines 673 - 675, The current logic is
coercing/normalizing --add-ons in a way that causes
Array.isArray(cliOptions.addOns) to no longer reliably indicate a user-supplied
list, which suppresses promptForAddOnOptions() and promptForEnvVars(); update
the normalization so arrays are preserved: in cli.ts keep cliOptions.addOns
untouched if Array.isArray(cliOptions.addOns), only set cliOptions.addOns = true
when cliOptions.addOns === undefined (and if your parser can produce an empty
string for a bare flag, normalize that to true as well), and remove any later
code that converts arrays into booleans so promptForAddOnOptions() and
promptForEnvVars() (from packages/cli/src/options.ts) still see arrays and run
interactively when appropriate.

Comment on lines +61 to +63
if (defaultFrameworkId || availableFrameworks.length <= 1) {
options.framework = getFrameworkById(defaultFrameworkId || 'react')!
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Auto-pick the lone available framework instead of hard-coding react.

When there is exactly one available framework and defaultFrameworkId is unset, Line 62 still resolves react. A single-framework CLI variant backed by some other framework will pick the wrong framework or crash here.

Suggested fix
-    if (defaultFrameworkId || availableFrameworks.length <= 1) {
-      options.framework = getFrameworkById(defaultFrameworkId || 'react')!
-    } else {
+    if (defaultFrameworkId) {
+      options.framework = getFrameworkById(defaultFrameworkId)!
+    } else if (availableFrameworks.length === 1) {
+      options.framework = availableFrameworks[0]!
+    } else {
       options.framework = await selectFramework(
         availableFrameworks,
         defaultFrameworkId,
       )
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (defaultFrameworkId || availableFrameworks.length <= 1) {
options.framework = getFrameworkById(defaultFrameworkId || 'react')!
} else {
if (defaultFrameworkId) {
options.framework = getFrameworkById(defaultFrameworkId)!
} else if (availableFrameworks.length === 1) {
options.framework = availableFrameworks[0]!
} else {
options.framework = await selectFramework(
availableFrameworks,
defaultFrameworkId,
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/options.ts` around lines 61 - 63, The code currently forces
'react' when defaultFrameworkId is falsy and availableFrameworks.length <= 1;
change the logic in the block that sets options.framework so it uses the lone
available framework's id if present: use defaultFrameworkId if set, otherwise if
availableFrameworks.length === 1 use availableFrameworks[0].id, and only fall
back to a hard-coded default (or undefined) when no frameworks exist; update the
assignment that calls getFrameworkById(...) (the code that sets
options.framework) to derive the id from defaultFrameworkId ||
availableFrameworks[0]?.id instead of defaulting to 'react'.

if (cliOptions.install === false) {
options.install = false
}
options.install = cliOptions.install ?? (await selectInstall())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Commander.js, what value does an option defined only as --no-install have when the user does not pass the flag?

💡 Result:

In Commander.js, an option defined only as --no-install (a negatable boolean option defined alone) has the value true when the user does not pass the flag. This is the documented behavior for negatable boolean options: "You can define a boolean option long name with a leading no- to set the option value to false when used. Defined alone, this also makes the option true by default." The option name is install, so without --no-install the value is true (install), and with --no-install it is false (no install). This behavior has been consistent since at least v4 and remains in the latest documentation (v14+). If both --install and --no-install were defined, the default would be undefined unless explicitly set.

Citations:


🏁 Script executed:

#!/bin/bash
# Check the install option definition in cli.ts
echo "=== Checking cli.ts for install flag definition ==="
if [ -f "packages/cli/src/cli.ts" ]; then
  grep -n "install" packages/cli/src/cli.ts | head -20
else
  echo "File not found: packages/cli/src/cli.ts"
fi

Repository: TanStack/cli

Length of output: 580


🏁 Script executed:

#!/bin/bash
# Check options.ts around line 257
echo "=== Checking options.ts around line 257 ==="
if [ -f "packages/cli/src/options.ts" ]; then
  wc -l packages/cli/src/options.ts
  echo ""
  sed -n '250,265p' packages/cli/src/options.ts
else
  echo "File not found: packages/cli/src/options.ts"
fi

Repository: TanStack/cli

Length of output: 552


selectInstall() will never run with only --no-install defined.

The Commander.js option at packages/cli/src/cli.ts:784 defines only --no-install. According to Commander's documented behavior, a negatable option defined alone defaults to true when omitted. This means cliOptions.install is true when the user skips the flag, so line 257's nullish coalesce (true ?? (await selectInstall())) short-circuits and never calls the prompt.

You need either:

  1. Define both --install and --no-install (default becomes undefined), or
  2. Use a different signal to detect whether the flag was explicitly provided.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/options.ts` at line 257, The prompt never runs because
cliOptions.install is always true when only a negatable --no-install is defined;
update the CLI option definition so the flag is not default-true (e.g., define
both --install and --no-install in the Command declaration in the CLI module) so
cliOptions.install will be undefined when the user omits the flag and the
existing assignment options.install = cliOptions.install ?? (await
selectInstall()) will correctly invoke selectInstall(); alternatively, if you
prefer not to change the CLI API, change the detection logic around
options.install to check a distinct presence signal from Commander (the explicit
option presence) rather than relying on the boolean value so selectInstall()
runs only when the flag was not provided.

@tannerlinsley tannerlinsley merged commit 05456f2 into main Apr 20, 2026
4 checks passed
@tannerlinsley tannerlinsley deleted the taren/fix-interactive-prompts branch April 20, 2026 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant