Skip to content

Commit f642f76

Browse files
committed
fix(setup): scope tty fallback to setup prompts and honor no-workflow
1 parent b59f6f5 commit f642f76

3 files changed

Lines changed: 52 additions & 9 deletions

File tree

src/setup/mod.rs

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -610,11 +610,12 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()>
610610
}
611611
let choices = ["Skills", "MCP"];
612612
let defaults = [true, false];
613+
let term = ui::tty_term().ok_or_else(|| anyhow!("interactive mode requires TTY"))?;
613614
let selected = MultiSelect::with_theme(&ColorfulTheme::default())
614615
.with_prompt("What would you like to set up?")
615616
.items(&choices)
616617
.defaults(&defaults)
617-
.interact()?;
618+
.interact_on(&term)?;
618619
(selected.contains(&0), selected.contains(&1))
619620
};
620621

@@ -668,7 +669,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()>
668669
local: matches!(*scope, InstallScope::Local),
669670
global: matches!(*scope, InstallScope::Global),
670671
workflows: Vec::new(),
671-
no_workflow: false,
672+
no_workflow: flag_no_workflow,
672673
yes: true,
673674
no_fetch_docs: true,
674675
refresh_docs: false,
@@ -735,10 +736,11 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()>
735736
}
736737
true
737738
} else {
739+
let term = ui::tty_term().ok_or_else(|| anyhow!("interactive mode requires TTY"))?;
738740
Confirm::with_theme(&ColorfulTheme::default())
739741
.with_prompt("Run instrumentation agent to set up tracing in this repo?")
740742
.default(true)
741-
.interact()?
743+
.interact_on(&term)?
742744
};
743745
if instrument {
744746
let instrument_agent = setup_context
@@ -972,10 +974,11 @@ async fn select_project_for_setup(
972974

973975
if selection == labels.len() - 1 {
974976
let default_name = default_setup_project_name();
977+
let term = ui::tty_term().ok_or_else(|| anyhow!("interactive mode requires TTY"))?;
975978
let name: String = dialoguer::Input::with_theme(&ColorfulTheme::default())
976979
.with_prompt("Project name")
977980
.default(default_name)
978-
.interact_text()?;
981+
.interact_text_on(&term)?;
979982
let trimmed = name.trim();
980983
if trimmed.is_empty() {
981984
bail!("project name cannot be empty");
@@ -1052,7 +1055,9 @@ fn maybe_init(org: &str, project: &crate::projects::api::Project) -> Result<bool
10521055
let update = Confirm::new()
10531056
.with_prompt(format!("Update .bt/config.json to {org}/{}?", project.name))
10541057
.default(true)
1055-
.interact()?;
1058+
.interact_on(
1059+
&ui::tty_term().ok_or_else(|| anyhow!("interactive mode requires TTY"))?,
1060+
)?;
10561061
if !update {
10571062
return Ok(false);
10581063
}
@@ -2174,7 +2179,7 @@ fn resolve_setup_selection(args: &AgentsSetupArgs, home: &Path) -> Result<SetupS
21742179
if scope.is_none() {
21752180
steps.push(SetupWizardStep::Scope);
21762181
}
2177-
if !args.no_fetch_docs && args.workflows.is_empty() {
2182+
if !args.no_fetch_docs && !args.no_workflow && args.workflows.is_empty() {
21782183
steps.push(SetupWizardStep::Workflows);
21792184
}
21802185

@@ -2500,11 +2505,12 @@ fn resolve_local_root_for_scope(scope: InstallScope) -> Result<Option<PathBuf>>
25002505

25012506
fn prompt_scope_selection(prompt: &str) -> Result<Option<InstallScope>> {
25022507
let choices = ["local (current git repo)", "global (user-wide)"];
2508+
let term = ui::tty_term().ok_or_else(|| anyhow!("interactive mode requires TTY"))?;
25032509
let idx = Select::with_theme(&ColorfulTheme::default())
25042510
.with_prompt(prompt)
25052511
.items(&choices)
25062512
.default(1)
2507-
.interact_opt()?;
2513+
.interact_on_opt(&term)?;
25082514
Ok(idx.map(|i| {
25092515
if i == 0 {
25102516
InstallScope::Local
@@ -2538,13 +2544,14 @@ fn prompt_workflows_selection(defaults: &[WorkflowArg]) -> Result<Option<Vec<Wor
25382544
.map(|workflow| default_set.contains(workflow))
25392545
.collect::<Vec<_>>();
25402546

2547+
let term = ui::tty_term().ok_or_else(|| anyhow!("interactive mode requires TTY"))?;
25412548
let selected = MultiSelect::with_theme(&ColorfulTheme::default())
25422549
.with_prompt(
25432550
"Select the workflows you are interested in (will prefetch docs for them) (Esc: back)",
25442551
)
25452552
.items(&labels)
25462553
.defaults(&default_flags)
2547-
.interact_opt()?;
2554+
.interact_on_opt(&term)?;
25482555

25492556
Ok(selected.map(|indexes| {
25502557
indexes
@@ -2596,6 +2603,9 @@ fn resolve_default_agent_selection(
25962603

25972604
let path_agents = detected_agents_on_path(detected);
25982605
if allow_prompt {
2606+
if path_agents.len() == 1 {
2607+
return Ok(path_agents[0]);
2608+
}
25992609
let default = pick_agent_mode_target(&path_agents).unwrap_or(Agent::Codex);
26002610
return prompt_agent_selection(prompt, default)?
26012611
.ok_or_else(|| anyhow!("setup cancelled by user"));
@@ -3514,6 +3524,25 @@ mod tests {
35143524
assert!(selection.selected_workflows.is_empty());
35153525
}
35163526

3527+
#[test]
3528+
fn resolve_setup_selection_honors_no_workflow() {
3529+
let args = AgentsSetupArgs {
3530+
agent: Some(AgentArg::Codex),
3531+
local: false,
3532+
global: true,
3533+
workflows: vec![WorkflowArg::Evaluate],
3534+
no_workflow: true,
3535+
yes: true,
3536+
no_fetch_docs: false,
3537+
refresh_docs: false,
3538+
workers: crate::sync::default_workers(),
3539+
yolo: false,
3540+
};
3541+
let home = std::env::temp_dir();
3542+
let selection = resolve_setup_selection(&args, &home).expect("resolve setup selection");
3543+
assert!(selection.selected_workflows.is_empty());
3544+
}
3545+
35173546
#[test]
35183547
fn resolve_workflow_selection_resolves_explicit_values() {
35193548
let selected =
@@ -4083,4 +4112,17 @@ mod tests {
40834112
let resolved = resolve_unambiguous_instrument_agent(&[], &detected);
40844113
assert_eq!(resolved, Some(Agent::Codex));
40854114
}
4115+
4116+
#[test]
4117+
fn resolve_default_agent_selection_auto_selects_single_path_agent_even_when_prompt_allowed() {
4118+
let detected = vec![DetectionSignal {
4119+
agent: Agent::Codex,
4120+
on_path: true,
4121+
reason: "binary".to_string(),
4122+
}];
4123+
4124+
let resolved = resolve_default_agent_selection(None, &detected, "ignored", true)
4125+
.expect("resolve default agent selection");
4126+
assert_eq!(resolved, Agent::Codex);
4127+
}
40864128
}

src/ui/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub fn is_interactive() -> bool {
3838
}
3939

4040
pub use pager::print_with_pager;
41+
pub(crate) use select::tty_term;
4142
pub use select::{fuzzy_select, fuzzy_select_opt, select_project_interactive};
4243

4344
pub use spinner::{with_spinner, with_spinner_visible};

src/ui/select.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{http::ApiClient, projects::api, ui::with_spinner};
1313
/// is invoked from a shell script: `echo "bt setup" | sh`.
1414
///
1515
/// Returns `None` when no interactive terminal is available at all (headless CI).
16-
fn tty_term() -> Option<Term> {
16+
pub(crate) fn tty_term() -> Option<Term> {
1717
if std::io::stderr().is_terminal() {
1818
return Some(Term::stderr());
1919
}

0 commit comments

Comments
 (0)