fix(cli): default to interactive mode and add missing prompts#435
fix(cli): default to interactive mode and add missing prompts#435tannerlinsley merged 1 commit intomainfrom
Conversation
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".
📝 WalkthroughWalkthroughThe 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
.changeset/fix-interactive-prompts.mdpackages/cli/src/cli.tspackages/cli/src/options.tspackages/cli/src/ui-prompts.ts
| if (cliOptions.addOns === undefined) { | ||
| cliOptions.addOns = true | ||
| } |
There was a problem hiding this comment.
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.
| if (defaultFrameworkId || availableFrameworks.length <= 1) { | ||
| options.framework = getFrameworkById(defaultFrameworkId || 'react')! | ||
| } else { |
There was a problem hiding this comment.
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.
| 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()) |
There was a problem hiding this comment.
🧩 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:
- 1: https://www.npmjs.com/package/commander
- 2: https://www.npmjs.com/package/commander?activeTab=versions
- 3: https://github.com/visionmedia/commander.js/
- 4: https://github.com/tj/commander.js/
- 5: https://github.com/tj/commander.js/blob/master/Readme.md
🏁 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"
fiRepository: 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"
fiRepository: 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:
- Define both
--installand--no-install(default becomesundefined), or - 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.
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
Fixes
Test plan
Summary by CodeRabbit
Release Notes
Bug Fixes
New Features