From 7434c6aaea9669a4196576d9f52cfc29c299ca96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Wed, 1 Apr 2026 23:29:53 +0000 Subject: [PATCH 1/9] chore: improve dx, WIP --- src/auth.rs | 5 +- src/setup/mod.rs | 461 +++++++++++++++++++++++++++-------------------- 2 files changed, 271 insertions(+), 195 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 384fd98..55f09fb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -825,7 +825,7 @@ async fn login_interactive_api_key(base: &mut BaseArgs) -> Result { Ok(profile_name) } -async fn login_interactive_oauth(base: &mut BaseArgs) -> Result { +pub(crate) async fn login_interactive_oauth(base: &mut BaseArgs) -> Result { let api_url = base .api_url .clone() @@ -863,7 +863,9 @@ async fn login_interactive_oauth(base: &mut BaseArgs) -> Result { let _ = open::that(&authorize_url); eprintln!("Complete authorization in your browser."); + eprintln!(); eprintln!("{}", dialoguer::console::style(&authorize_url).dim()); + eprintln!(); let callback = collect_oauth_callback(listener, is_ssh_session()).await?; if let Some(error) = callback.error { @@ -1717,6 +1719,7 @@ async fn collect_oauth_callback( let pasted = Input::::new() .with_prompt("Callback URL/query/JSON (press Enter to wait for automatic callback)") .allow_empty(true) + .report(false) .interact_text() .context("failed to read callback URL")?; if pasted.trim().is_empty() { diff --git a/src/setup/mod.rs b/src/setup/mod.rs index 37f4ed9..60a07fe 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -69,6 +69,22 @@ pub struct SetupArgs { #[arg(long, conflicts_with_all = ["skills", "mcp"])] no_mcp_skill: bool, + /// Do not set up coding-agent skills + #[arg(long)] + no_skill: bool, + + /// Do not set up MCP server + #[arg(long)] + no_mcp: bool, + + /// Do not run instrumentation agent (overrides default) + #[arg(long, conflicts_with = "instrument")] + no_instrument: bool, + + /// Run the agent in background (non-interactive) mode instead of TUI + #[arg(long)] + background: bool, + #[command(flatten)] agents: AgentsSetupArgs, } @@ -388,9 +404,13 @@ pub async fn run_setup_top(base: BaseArgs, args: SetupArgs) -> Result<()> { yolo: args.agents.yolo, skills: args.skills, mcp: args.mcp, + no_skill: args.no_skill, + no_mcp: args.no_mcp, local: args.agents.local, global: args.agents.global, instrument: args.instrument, + no_instrument: args.no_instrument, + background: args.background, agents: args.agents.agents, no_mcp_skill: args.no_mcp_skill, workflows: args.agents.workflows, @@ -409,9 +429,13 @@ struct WizardFlags { yolo: bool, skills: bool, mcp: bool, + no_skill: bool, + no_mcp: bool, local: bool, global: bool, instrument: bool, + no_instrument: bool, + background: bool, agents: Vec, no_mcp_skill: bool, workflows: Vec, @@ -422,15 +446,21 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> yolo, skills: flag_skills, mcp: flag_mcp, + no_skill: flag_no_skill, + no_mcp: flag_no_mcp, local: flag_local, - global: flag_global, - instrument: flag_instrument, + global: _flag_global, + instrument: _flag_instrument, + no_instrument: flag_no_instrument, + background: flag_background, agents: flag_agents, no_mcp_skill: flag_no_mcp_skill, workflows: flag_workflows, } = flags; let mut had_failures = false; let quiet = base.quiet; + let home = home_dir().ok_or_else(|| anyhow!("failed to resolve HOME/USERPROFILE"))?; + let git_root = find_git_root(); // ── Step 1: Auth ── if !quiet { @@ -448,115 +478,60 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if !quiet { print_wizard_step(2, "Project"); } - let project = select_project_with_skip(&client, project_flag.as_deref(), quiet).await?; - if !quiet { - if let Some(ref project) = project { - if find_git_root().is_some() && maybe_init(&org, project)? { - eprintln!( - " {} Linked to {}/{}", - style("✓").green(), - org, - project.name - ); - } - } else { - eprintln!(" {}", style("Skipped").dim()); + let project = auto_select_project(&client, project_flag.as_deref()).await?; + if let Some(ref project) = project { + if git_root.is_some() { + let _ = maybe_init(&org, project); } - } else if let Some(ref project) = project { - if find_git_root().is_some() { - let _ = maybe_init(&org, project)?; + if !quiet { + eprintln!( + " {} Project: {}/{}", + style("✓").green(), + org, + project.name + ); } + } else if !quiet { + eprintln!( + " {} {}", + style("—").dim(), + style("Using existing project(s)").dim() + ); } // ── Step 3: Agent tools (skills + MCP) ── if !quiet { print_wizard_step(3, "Agents"); } - let mut multiselect_hint_shown = false; - let (wants_skills, wants_mcp) = if flag_no_mcp_skill { - if !quiet { - eprintln!( - "{} What would you like to set up? · {}", - style("✔").green(), - style("(none)").dim() - ); - } - (false, false) - } else if flag_skills || flag_mcp { - if !quiet { - let chosen: Vec<&str> = [("Skills", flag_skills), ("MCP", flag_mcp)] - .iter() - .filter(|(_, v)| *v) - .map(|(s, _)| *s) - .collect(); - let chosen_styled: Vec = chosen - .iter() - .map(|s| style(s).green().to_string()) - .collect(); - eprintln!( - "{} What would you like to set up? · {}", - style("✔").green(), - chosen_styled.join(", ") - ); - } - (flag_skills, flag_mcp) + // Default: install both skills and MCP with global scope. + // --no-skill / --no-mcp / --no-mcp-skill opt out. + let _ = flag_skills; // kept as flag but superseded by default-on behavior + let _ = flag_mcp; // kept as flag but superseded by default-on behavior + let wants_skills = !flag_no_mcp_skill && !flag_no_skill; + let wants_mcp = !flag_no_mcp_skill && !flag_no_mcp; + + // Scope: default global; --local overrides. + let scope = if flag_local { + InstallScope::Local } else { - if !quiet { - eprintln!( - " {}", - style("(Un)select option with Space, confirm selection with Enter.").dim() - ); - multiselect_hint_shown = true; - } - let choices = ["Skills", "MCP"]; - let defaults = [true, true]; - let selected = MultiSelect::with_theme(&ColorfulTheme::default()) - .with_prompt("What would you like to set up?") - .items(&choices) - .defaults(&defaults) - .interact()?; - (selected.contains(&0), selected.contains(&1)) + InstallScope::Global }; - let setup_context = if wants_skills || wants_mcp { - let scope = if flag_local { - if !quiet { - eprintln!( - "{} Select install scope · {}", - style("✔").green(), - style("local (current git repo)").green() - ); - } - InstallScope::Local - } else if flag_global { - if !quiet { - eprintln!( - "{} Select install scope · {}", - style("✔").green(), - style("global (user-wide)").green() - ); - } - InstallScope::Global + let local_root = resolve_local_root_for_scope(scope)?; + let detected = detect_agents(local_root.as_deref(), &home); + let agents = resolve_selected_agents(&flag_agents, &detected); + + if !quiet { + let scope_label = if flag_local { + "local (current git repo)" } else { - prompt_scope_selection("Select install scope")? - .ok_or_else(|| anyhow!("setup cancelled"))? + "global (user-wide)" }; - let home = home_dir().ok_or_else(|| anyhow!("failed to resolve HOME/USERPROFILE"))?; - let local_root = resolve_local_root_for_scope(scope)?; - let detected = detect_agents(local_root.as_deref(), &home); - let agents = resolve_selected_agents(&flag_agents, &detected); - if !quiet && !flag_agents.is_empty() { - let agent_names: Vec = agents - .iter() - .map(|a| style(a.as_str()).green().to_string()) - .collect(); - eprintln!( - "{} Select agents to configure · {}", - style("✔").green(), - agent_names.join(", ") - ); - } - Some((scope, agents, home)) + eprintln!(" {} Scope: {}", style("✓").green(), scope_label); + } + + let setup_context = if wants_skills || wants_mcp { + Some((scope, agents.clone())) } else { None }; @@ -565,7 +540,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if !quiet { eprintln!(" {}", style("Skills:").bold()); } - if let Some((scope, ref agents, _)) = setup_context { + if let Some((scope, ref agents)) = setup_context { let agent_args: Vec = agents.iter().map(|a| map_agent_to_agent_arg(*a)).collect(); let args = AgentsSetupArgs { @@ -595,9 +570,9 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if !quiet { eprintln!(" {}", style("MCP:").bold()); } - if let Some((scope, ref agents, ref home)) = setup_context { + if let Some((scope, ref agents)) = setup_context { let local_root = resolve_local_root_for_scope(scope)?; - let outcome = execute_mcp_install(scope, local_root.as_deref(), home, agents); + let outcome = execute_mcp_install(scope, local_root.as_deref(), &home, agents); for r in &outcome.results { if !quiet { print_wizard_agent_result(r); @@ -620,47 +595,71 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if !quiet { print_wizard_step(4, "Instrument"); } - if find_git_root().is_some() { - let instrument = if flag_instrument { - if !quiet { - eprintln!( - "Run instrumentation agent to set up tracing in this repo? {}", - style("yes").green() - ); - } - true - } else { - Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt("Run instrumentation agent to set up tracing in this repo?") - .default(false) - .interact()? - }; - if instrument { - let instrument_agent = match flag_agents.as_slice() { - [single] => match single { - AgentArg::Claude => Some(InstrumentAgentArg::Claude), - AgentArg::Codex => Some(InstrumentAgentArg::Codex), - AgentArg::Cursor => Some(InstrumentAgentArg::Cursor), - AgentArg::Opencode => Some(InstrumentAgentArg::Opencode), - AgentArg::All => None, - }, - _ => None, + if git_root.is_some() { + // Default: instrument. --no-instrument opts out. + let do_instrument = !flag_no_instrument; + if do_instrument { + // Determine agent: explicit flag > single detected > ask user + let instrument_agent = + determine_wizard_instrument_agent(&flag_agents, git_root.as_deref(), &home); + + let instrument_agent = if instrument_agent.is_none() && ui::is_interactive() { + // Multiple or zero detected agents — ask the user + let detected2 = detect_agents(git_root.as_deref(), &home); + let detected_set: BTreeSet = detected2.iter().map(|s| s.agent).collect(); + let candidates: Vec = if detected_set.is_empty() { + ALL_AGENTS.to_vec() + } else { + detected_set.into_iter().collect() + }; + let default = pick_agent_mode_target(&candidates).unwrap_or(Agent::Claude); + let selected = prompt_instrument_agent(default)?; + Some(match selected { + Agent::Claude => InstrumentAgentArg::Claude, + Agent::Codex => InstrumentAgentArg::Codex, + Agent::Cursor => InstrumentAgentArg::Cursor, + Agent::Opencode => InstrumentAgentArg::Opencode, + }) + } else { + instrument_agent }; + + // Auto-detect languages from the git root (no prompt) + let detected_languages = git_root + .as_deref() + .map(detect_languages_from_dir) + .unwrap_or_default(); + + // Default workflows: instrument + observe. --workflow flags override. + let wizard_workflows = if flag_workflows.is_empty() { + vec![WorkflowArg::Instrument, WorkflowArg::Observe] + } else { + let mut selected = resolve_workflow_selection(&flag_workflows); + if !selected.contains(&WorkflowArg::Instrument) { + selected.push(WorkflowArg::Instrument); + selected.sort(); + selected.dedup(); + } + selected + }; + run_instrument_setup( base, InstrumentSetupArgs { agent: instrument_agent, agent_cmd: None, - workflows: flag_workflows, - yes: false, + workflows: wizard_workflows, + // yes=true suppresses remaining prompts; all decisions are made above. + yes: true, refresh_docs: false, workers: crate::sync::default_workers(), quiet: false, - languages: Vec::new(), - interactive: false, + languages: detected_languages, + // TUI by default; --background switches to non-interactive. + interactive: !flag_background, yolo, }, - !multiselect_hint_shown, + false, ) .await?; } else if !quiet { @@ -681,78 +680,50 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> } async fn ensure_auth(base: &mut BaseArgs) -> Result { - if base.api_key.is_some() { + // 1. --api-key flag (or BRAINTRUST_API_KEY env) without --prefer-profile: use directly. + if base.api_key.is_some() && !base.prefer_profile { return auth::login(base).await; } + // 2. --prefer-profile or no api key: try stored profiles first. let profiles = auth::list_profiles()?; - match profiles.len() { - 0 => { - eprintln!("No auth profiles found. Let's set one up.\n"); - auth::login_interactive(base).await?; - auth::login(base).await - } - 1 => { - let p = &profiles[0]; - base.profile = Some(p.name.clone()); - auth::login(base).await - } - _ => { - let name = auth::select_profile_interactive(None)? - .ok_or_else(|| anyhow!("no profile selected"))?; - base.profile = Some(name); - auth::login(base).await - } - } -} + if !profiles.is_empty() { + let profile_name = if let Some(ref p) = base.profile { + p.clone() + } else if profiles.len() == 1 { + profiles[0].name.clone() + } else { + auth::select_profile_interactive(None)?.ok_or_else(|| anyhow!("no profile selected"))? + }; + base.profile = Some(profile_name.clone()); -async fn select_project_with_skip( - client: &ApiClient, - project_name: Option<&str>, - quiet: bool, -) -> Result> { - if let Some(name) = project_name { - let project = with_spinner( - "Loading project...", - crate::projects::api::get_project_by_name(client, name), - ) - .await?; - match project { - Some(p) => { - if !quiet { - eprintln!("{} Select project · {}", style("✔").green(), p.name); - } - return Ok(Some(p)); + // Try to login; if credentials are missing/inaccessible, re-auth via OAuth. + match auth::login(base).await { + Ok(ctx) => return Ok(ctx), + Err(err) if is_missing_credential_error(&err) => { + eprintln!( + " Profile '{}' credentials inaccessible ({}). Re-authenticating via OAuth...", + profile_name, err + ); + base.profile = None; + auth::login_interactive_oauth(base).await?; + return auth::login(base).await; } - None => bail!( - "project '{}' not found in org '{}'", - name, - client.org_name() - ), + Err(err) => return Err(err), } } - let mut projects = with_spinner( - "Loading projects...", - crate::projects::api::list_projects(client), - ) - .await?; - - if projects.is_empty() { - bail!("no projects found in org '{}'", client.org_name()); - } - - projects.sort_by(|a, b| a.name.cmp(&b.name)); - let mut labels: Vec = projects.iter().map(|p| p.name.clone()).collect(); - labels.push("Skip (not recommended)".to_string()); - - let selection = ui::fuzzy_select("Select project", &labels, 0)?; + // 3. No profiles: start OAuth flow. + eprintln!("No auth profiles found. Starting OAuth login.\n"); + auth::login_interactive_oauth(base).await?; + auth::login(base).await +} - if selection == labels.len() - 1 { - Ok(None) - } else { - Ok(Some(projects[selection].clone())) - } +fn is_missing_credential_error(err: &anyhow::Error) -> bool { + let msg = err.to_string(); + msg.contains("no keychain credential found") + || msg.contains("oauth refresh token missing") + || msg.contains("no login credentials found") } /// Returns `true` if config was written or already matched, `false` if user declined. @@ -792,6 +763,108 @@ fn maybe_init(org: &str, project: &crate::projects::api::Project) -> Result, +) -> Result> { + if let Some(name) = project_name { + let project = with_spinner( + "Loading project...", + crate::projects::api::get_project_by_name(client, name), + ) + .await?; + return match project { + Some(p) => Ok(Some(p)), + None => bail!( + "project '{}' not found in org '{}'", + name, + client.org_name() + ), + }; + } + + let projects = with_spinner( + "Loading projects...", + crate::projects::api::list_projects(client), + ) + .await?; + + // 2+ real projects: leave the user's setup untouched. + if projects.len() >= 2 { + return Ok(None); + } + + // Exactly one project: only proceed if it is the placeholder "My Project". + if projects.len() == 1 && projects[0].name != "My Project" { + return Ok(None); + } + + // 0 projects, or the sole project is the default placeholder: create a real one. + let username = get_whoami_username(); + let new_name = format!("{username}-test"); + let project = with_spinner( + &format!("Creating project '{new_name}'..."), + crate::projects::api::create_project(client, &new_name), + ) + .await?; + Ok(Some(project)) +} + +fn get_whoami_username() -> String { + std::process::Command::new("whoami") + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "user".to_string()) +} + +/// Determine which agent to use for instrumentation in the wizard. +/// +/// Returns `Some(agent)` when exactly one agent is unambiguously determined: +/// - A single `--agent` flag (not `all`) +/// - Exactly one agent detected on the system +/// +/// Returns `None` when the choice is ambiguous (0 or multiple detected agents, +/// no explicit `--agent` flag), letting the caller prompt the user. +fn determine_wizard_instrument_agent( + flag_agents: &[AgentArg], + local_root: Option<&Path>, + home: &Path, +) -> Option { + if flag_agents.len() == 1 { + return match flag_agents[0] { + AgentArg::Claude => Some(InstrumentAgentArg::Claude), + AgentArg::Codex => Some(InstrumentAgentArg::Codex), + AgentArg::Cursor => Some(InstrumentAgentArg::Cursor), + AgentArg::Opencode => Some(InstrumentAgentArg::Opencode), + AgentArg::All => None, + }; + } + + // Auto-detect: only use unambiguous single detection. + let detected = detect_agents(local_root, home); + let detected_agents: BTreeSet = detected.iter().map(|s| s.agent).collect(); + if detected_agents.len() == 1 { + return Some(match detected_agents.into_iter().next().unwrap() { + Agent::Claude => InstrumentAgentArg::Claude, + Agent::Codex => InstrumentAgentArg::Codex, + Agent::Cursor => InstrumentAgentArg::Cursor, + Agent::Opencode => InstrumentAgentArg::Opencode, + }); + } + + None +} + async fn run_setup(base: BaseArgs, args: AgentsSetupArgs) -> Result<()> { let outcome = execute_skills_setup(&base, &args, false).await?; if base.json { @@ -982,7 +1055,7 @@ async fn run_instrument_setup( if args.agent.is_none() && ui::is_interactive() && !args.yes { selected = prompt_instrument_agent(selected)?; - } else if args.agent.is_some() && !base.quiet { + } else if args.agent.is_some() && !base.quiet && !args.yes { eprintln!( "{} Select agent to instrument this repo · {}", style("✔").green(), From af7d7ff94d6be68195804b2a63ba3c0c37e7540b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Thu, 2 Apr 2026 12:25:37 +0000 Subject: [PATCH 2/9] gitting stuff, reformulate this commit message --- src/args.rs | 6 +- src/auth.rs | 2 +- src/main.rs | 4 +- src/setup/mod.rs | 390 +++++++++++++++++++++++++---------------------- src/switch.rs | 2 +- src/traces.rs | 2 +- 6 files changed, 213 insertions(+), 193 deletions(-) diff --git a/src/args.rs b/src/args.rs index 3748e8b..c5da1af 100644 --- a/src/args.rs +++ b/src/args.rs @@ -10,9 +10,9 @@ pub struct BaseArgs { #[arg(long, global = true)] pub json: bool, - /// Suppress non-essential output - #[arg(long, short = 'q', env = "BRAINTRUST_QUIET", global = true, value_parser = clap::builder::BoolishValueParser::new(), default_value_t = false)] - pub quiet: bool, + /// Show additional output + #[arg(long, short = 'v', env = "BRAINTRUST_VERBOSE", global = true, value_parser = clap::builder::BoolishValueParser::new(), default_value_t = false)] + pub verbose: bool, /// Disable ANSI color output #[arg(long, env = "BRAINTRUST_NO_COLOR", global = true, value_parser = clap::builder::BoolishValueParser::new(), default_value_t = false)] diff --git a/src/auth.rs b/src/auth.rs index 55f09fb..27e0d74 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -2583,7 +2583,7 @@ mod tests { fn make_base() -> BaseArgs { BaseArgs { json: false, - quiet: false, + verbose: false, no_color: false, profile: None, project: None, diff --git a/src/main.rs b/src/main.rs index db7760b..a10bfaf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,7 +80,7 @@ Flags --profile Use a saved login profile [env: BRAINTRUST_PROFILE] -o, --org Override active org [env: BRAINTRUST_ORG_NAME] -p, --project Override active project [env: BRAINTRUST_DEFAULT_PROJECT] - -q, --quiet Suppress non-essential output + -v, --verbose Show additional output --json Output as JSON --no-color Disable ANSI color output --no-input Disable all interactive prompts @@ -249,7 +249,7 @@ fn configure_output(base: &BaseArgs) { ui::set_animations_enabled(false); } - if base.quiet { + if !base.verbose { ui::set_quiet(true); ui::set_animations_enabled(false); } diff --git a/src/setup/mod.rs b/src/setup/mod.rs index 60a07fe..a905187 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -53,36 +53,36 @@ pub struct SetupArgs { #[command(subcommand)] command: Option, - /// Set up coding-agent skills (skips interactive selection in wizard) + /// Set up coding-agent skills [default] #[arg(long)] skills: bool, - /// Set up MCP server (skips interactive selection in wizard) + /// Do not set up coding-agent skills #[arg(long)] - mcp: bool, + no_skills: bool, - /// Run instrumentation agent (skips interactive prompt in wizard) - #[arg(long)] - instrument: bool, - - /// Skip skills and MCP setup (skips interactive selection in wizard) - #[arg(long, conflicts_with_all = ["skills", "mcp"])] - no_mcp_skill: bool, - - /// Do not set up coding-agent skills + /// Set up MCP server [default] #[arg(long)] - no_skill: bool, + mcp: bool, /// Do not set up MCP server #[arg(long)] no_mcp: bool, - /// Do not run instrumentation agent (overrides default) + /// Run instrumentation agent [default] + #[arg(long)] + instrument: bool, + + /// Do not run instrumentation agent #[arg(long, conflicts_with = "instrument")] no_instrument: bool, - /// Run the agent in background (non-interactive) mode instead of TUI - #[arg(long)] + /// Run the agent in interactive TUI mode [default] + #[arg(long, conflicts_with = "background")] + tui: bool, + + /// Run the agent in background (non-interactive) mode + #[arg(long, conflicts_with = "tui")] background: bool, #[command(flatten)] @@ -103,9 +103,9 @@ enum SetupSubcommand { #[derive(Debug, Clone, Args)] struct AgentsSetupArgs { - /// Agent(s) to configure (repeatable) - #[arg(long = "agent", value_enum)] - agents: Vec, + /// Agent to configure + #[arg(long, value_enum)] + agent: Option, /// Configure the current git repo root #[arg(long, conflicts_with = "global")] @@ -135,17 +135,16 @@ struct AgentsSetupArgs { #[arg(long, default_value_t = crate::sync::default_workers())] workers: usize, - /// Grant the agent full permissions and run it in the background without prompting. - /// Equivalent to choosing "Background" with all tool restrictions lifted. + /// Grant the agent full permissions (bypass permission prompts) #[arg(long)] yolo: bool, } #[derive(Debug, Clone, Args)] struct AgentsMcpSetupArgs { - /// Agent(s) to configure MCP for (repeatable) - #[arg(long = "agent", value_enum)] - agents: Vec, + /// Agent to configure MCP for + #[arg(long, value_enum)] + agent: Option, /// Configure MCP in the current git repo root #[arg(long, conflicts_with = "global")] @@ -186,10 +185,6 @@ struct InstrumentSetupArgs { #[arg(long, default_value_t = crate::sync::default_workers())] workers: usize, - /// Suppress streaming agent output; show a spinner and print results at the end - #[arg(long, short = 'q')] - quiet: bool, - /// Language(s) to instrument (repeatable; case-insensitive). /// When provided, the agent skips language auto-detection and instruments /// the specified language(s) directly. @@ -197,14 +192,16 @@ struct InstrumentSetupArgs { #[arg(long = "language", value_enum, ignore_case = true)] languages: Vec, - /// Run the agent in interactive mode: inherits the terminal so the user can - /// approve/deny tool uses directly. Conflicts with --quiet and --yolo. - #[arg(long, short = 'i', conflicts_with_all = ["quiet", "yolo"])] - interactive: bool, + /// Run the agent in interactive TUI mode [default] + #[arg(long, short = 'i', conflicts_with = "background")] + tui: bool, - /// Grant the agent full permissions and run it in the background without prompting. - /// Skips the run-mode selection question. Conflicts with --interactive. - #[arg(long, conflicts_with = "interactive")] + /// Run the agent in background (non-interactive) mode + #[arg(long, conflicts_with = "tui")] + background: bool, + + /// Grant the agent full permissions (bypass permission prompts) + #[arg(long)] yolo: bool, } @@ -226,7 +223,6 @@ enum AgentArg { Codex, Cursor, Opencode, - All, } #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)] @@ -404,15 +400,15 @@ pub async fn run_setup_top(base: BaseArgs, args: SetupArgs) -> Result<()> { yolo: args.agents.yolo, skills: args.skills, mcp: args.mcp, - no_skill: args.no_skill, + no_skills: args.no_skills, no_mcp: args.no_mcp, local: args.agents.local, global: args.agents.global, instrument: args.instrument, no_instrument: args.no_instrument, + tui: args.tui, background: args.background, - agents: args.agents.agents, - no_mcp_skill: args.no_mcp_skill, + agent: args.agents.agent, workflows: args.agents.workflows, }; run_setup_wizard(base, wizard_flags).await @@ -429,15 +425,15 @@ struct WizardFlags { yolo: bool, skills: bool, mcp: bool, - no_skill: bool, + no_skills: bool, no_mcp: bool, local: bool, global: bool, instrument: bool, no_instrument: bool, + tui: bool, background: bool, - agents: Vec, - no_mcp_skill: bool, + agent: Option, workflows: Vec, } @@ -446,36 +442,36 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> yolo, skills: flag_skills, mcp: flag_mcp, - no_skill: flag_no_skill, + no_skills: flag_no_skills, no_mcp: flag_no_mcp, local: flag_local, global: _flag_global, instrument: _flag_instrument, no_instrument: flag_no_instrument, + tui: flag_tui, background: flag_background, - agents: flag_agents, - no_mcp_skill: flag_no_mcp_skill, + agent: flag_agent, workflows: flag_workflows, } = flags; let mut had_failures = false; - let quiet = base.quiet; + let verbose = base.verbose; let home = home_dir().ok_or_else(|| anyhow!("failed to resolve HOME/USERPROFILE"))?; let git_root = find_git_root(); // ── Step 1: Auth ── - if !quiet { + if verbose { print_wizard_step(1, "Auth"); } let project_flag = base.project.clone(); let login_ctx = ensure_auth(&mut base).await?; let client = ApiClient::new(&login_ctx)?; let org = client.org_name().to_string(); - if !quiet { + if verbose { eprintln!(" {} Using org '{}'", style("✓").green(), org); } // ── Step 2: Project ── - if !quiet { + if verbose { print_wizard_step(2, "Project"); } let project = auto_select_project(&client, project_flag.as_deref()).await?; @@ -483,7 +479,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if git_root.is_some() { let _ = maybe_init(&org, project); } - if !quiet { + if verbose { eprintln!( " {} Project: {}/{}", style("✓").green(), @@ -491,7 +487,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> project.name ); } - } else if !quiet { + } else if verbose { eprintln!( " {} {}", style("—").dim(), @@ -500,15 +496,15 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> } // ── Step 3: Agent tools (skills + MCP) ── - if !quiet { + if verbose { print_wizard_step(3, "Agents"); } // Default: install both skills and MCP with global scope. // --no-skill / --no-mcp / --no-mcp-skill opt out. let _ = flag_skills; // kept as flag but superseded by default-on behavior let _ = flag_mcp; // kept as flag but superseded by default-on behavior - let wants_skills = !flag_no_mcp_skill && !flag_no_skill; - let wants_mcp = !flag_no_mcp_skill && !flag_no_mcp; + let wants_skills = !flag_no_skills; + let wants_mcp = !flag_no_mcp; // Scope: default global; --local overrides. let scope = if flag_local { @@ -519,9 +515,9 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> let local_root = resolve_local_root_for_scope(scope)?; let detected = detect_agents(local_root.as_deref(), &home); - let agents = resolve_selected_agents(&flag_agents, &detected); + let agents = resolve_selected_agents(flag_agent, &detected); - if !quiet { + if verbose { let scope_label = if flag_local { "local (current git repo)" } else { @@ -537,14 +533,12 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> }; if wants_skills { - if !quiet { + if verbose { eprintln!(" {}", style("Skills:").bold()); } if let Some((scope, ref agents)) = setup_context { - let agent_args: Vec = - agents.iter().map(|a| map_agent_to_agent_arg(*a)).collect(); let args = AgentsSetupArgs { - agents: agent_args, + agent: flag_agent, local: matches!(scope, InstallScope::Local), global: matches!(scope, InstallScope::Global), workflows: Vec::new(), @@ -554,9 +548,9 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> workers: crate::sync::default_workers(), yolo: false, }; - let outcome = execute_skills_setup(&base, &args, true).await?; + let outcome = execute_skills_setup(&base, &args).await?; for r in &outcome.results { - if !quiet { + if verbose { print_wizard_agent_result(r); } if matches!(r.status, InstallStatus::Failed) { @@ -567,14 +561,14 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> } if wants_mcp { - if !quiet { + if verbose { eprintln!(" {}", style("MCP:").bold()); } if let Some((scope, ref agents)) = setup_context { let local_root = resolve_local_root_for_scope(scope)?; let outcome = execute_mcp_install(scope, local_root.as_deref(), &home, agents); for r in &outcome.results { - if !quiet { + if verbose { print_wizard_agent_result(r); } if matches!(r.status, InstallStatus::Failed) { @@ -587,12 +581,12 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> } } - if !wants_skills && !wants_mcp && !quiet { + if !wants_skills && !wants_mcp && verbose { eprintln!(" {}", style("Skipped").dim()); } // ── Step 4: Instrument ── - if !quiet { + if verbose { print_wizard_step(4, "Instrument"); } if git_root.is_some() { @@ -601,7 +595,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if do_instrument { // Determine agent: explicit flag > single detected > ask user let instrument_agent = - determine_wizard_instrument_agent(&flag_agents, git_root.as_deref(), &home); + determine_wizard_instrument_agent(flag_agent, git_root.as_deref(), &home); let instrument_agent = if instrument_agent.is_none() && ui::is_interactive() { // Multiple or zero detected agents — ask the user @@ -653,24 +647,23 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> yes: true, refresh_docs: false, workers: crate::sync::default_workers(), - quiet: false, languages: detected_languages, - // TUI by default; --background switches to non-interactive. - interactive: !flag_background, + tui: flag_tui || !flag_background, + background: flag_background, yolo, }, false, ) .await?; - } else if !quiet { + } else if verbose { eprintln!(" {}", style("Skipped").dim()); } - } else if !quiet { + } else if verbose { eprintln!(" {}", style("Skipped").dim()); } // ── Done ── - if !quiet { + if verbose { print_wizard_done(had_failures); } if had_failures { @@ -701,10 +694,12 @@ async fn ensure_auth(base: &mut BaseArgs) -> Result { match auth::login(base).await { Ok(ctx) => return Ok(ctx), Err(err) if is_missing_credential_error(&err) => { - eprintln!( - " Profile '{}' credentials inaccessible ({}). Re-authenticating via OAuth...", - profile_name, err - ); + if base.verbose { + eprintln!( + " Profile '{}' credentials inaccessible ({}). Re-authenticating via OAuth...", + profile_name, err + ); + } base.profile = None; auth::login_interactive_oauth(base).await?; return auth::login(base).await; @@ -714,7 +709,9 @@ async fn ensure_auth(base: &mut BaseArgs) -> Result { } // 3. No profiles: start OAuth flow. - eprintln!("No auth profiles found. Starting OAuth login.\n"); + if base.verbose { + eprintln!("No auth profiles found. Starting OAuth login.\n"); + } auth::login_interactive_oauth(base).await?; auth::login(base).await } @@ -830,24 +827,23 @@ fn get_whoami_username() -> String { /// Determine which agent to use for instrumentation in the wizard. /// /// Returns `Some(agent)` when exactly one agent is unambiguously determined: -/// - A single `--agent` flag (not `all`) +/// - A single `--agent` flag /// - Exactly one agent detected on the system /// /// Returns `None` when the choice is ambiguous (0 or multiple detected agents, /// no explicit `--agent` flag), letting the caller prompt the user. fn determine_wizard_instrument_agent( - flag_agents: &[AgentArg], + flag_agent: Option, local_root: Option<&Path>, home: &Path, ) -> Option { - if flag_agents.len() == 1 { - return match flag_agents[0] { - AgentArg::Claude => Some(InstrumentAgentArg::Claude), - AgentArg::Codex => Some(InstrumentAgentArg::Codex), - AgentArg::Cursor => Some(InstrumentAgentArg::Cursor), - AgentArg::Opencode => Some(InstrumentAgentArg::Opencode), - AgentArg::All => None, - }; + if let Some(arg) = flag_agent { + return Some(match arg { + AgentArg::Claude => InstrumentAgentArg::Claude, + AgentArg::Codex => InstrumentAgentArg::Codex, + AgentArg::Cursor => InstrumentAgentArg::Cursor, + AgentArg::Opencode => InstrumentAgentArg::Opencode, + }); } // Auto-detect: only use unambiguous single detection. @@ -866,7 +862,7 @@ fn determine_wizard_instrument_agent( } async fn run_setup(base: BaseArgs, args: AgentsSetupArgs) -> Result<()> { - let outcome = execute_skills_setup(&base, &args, false).await?; + let outcome = execute_skills_setup(&base, &args).await?; if base.json { let report = SetupJsonReport { scope: outcome.scope.as_str().to_string(), @@ -901,7 +897,6 @@ async fn run_setup(base: BaseArgs, args: AgentsSetupArgs) -> Result<()> { async fn execute_skills_setup( base: &BaseArgs, args: &AgentsSetupArgs, - quiet: bool, ) -> Result { let home = home_dir().ok_or_else(|| anyhow!("failed to resolve HOME/USERPROFILE"))?; let selection = resolve_setup_selection(args, &home)?; @@ -913,7 +908,7 @@ async fn execute_skills_setup( let mut warnings = Vec::new(); let mut notes = Vec::new(); let mut results = Vec::new(); - let show_progress = !base.json && !quiet && !base.quiet; + let show_progress = !base.json && base.verbose; if show_progress { println!("Configuring coding agents for Braintrust"); @@ -1049,13 +1044,13 @@ async fn run_instrument_setup( let mut selected = if let Some(agent_arg) = args.agent { map_instrument_agent_arg(agent_arg) } else { - pick_agent_mode_target(&resolve_selected_agents(&[], &detected)) + pick_agent_mode_target(&resolve_selected_agents(None, &detected)) .ok_or_else(|| anyhow!("no detected agents available for instrumentation"))? }; if args.agent.is_none() && ui::is_interactive() && !args.yes { selected = prompt_instrument_agent(selected)?; - } else if args.agent.is_some() && !base.quiet && !args.yes { + } else if args.agent.is_some() && base.verbose && !args.yes { eprintln!( "{} Select agent to instrument this repo · {}", style("✔").green(), @@ -1063,7 +1058,7 @@ async fn run_instrument_setup( ); } - let mut hint_pending = print_hint && !base.quiet; + let mut hint_pending = print_hint && base.verbose; let selected_workflows = resolve_instrument_workflow_selection(&args, &mut hint_pending)?; let selected_languages: Vec = if !args.languages.is_empty() { @@ -1112,7 +1107,7 @@ async fn run_instrument_setup( .await?; } else { let setup_args = AgentsSetupArgs { - agents: vec![map_agent_to_agent_arg(selected)], + agent: Some(map_agent_to_agent_arg(selected)), local: true, global: false, workflows: selected_workflows.clone(), @@ -1122,7 +1117,7 @@ async fn run_instrument_setup( workers: args.workers, yolo: false, }; - let outcome = execute_skills_setup(&base, &setup_args, false).await?; + let outcome = execute_skills_setup(&base, &setup_args).await?; detected = outcome.detected_agents; results.extend(outcome.results); warnings.extend(outcome.warnings); @@ -1133,16 +1128,13 @@ async fn run_instrument_setup( } // Determine run mode: interactive TUI vs background (autonomous). - // --yolo: background, full bypassPermissions (no restrictions) - // --interactive: interactive TUI - // --yes or non-interactive terminal: background, restricted to language package managers + // --tui: interactive TUI (inherits terminal) + // --background / --yes / non-interactive terminal: background (autonomous) // Otherwise: ask the user. - let (run_interactive, bypass_permissions) = if args.interactive { - (true, false) - } else if args.yolo { - (false, true) - } else if args.yes || !ui::is_interactive() { - (false, false) + let run_interactive = if args.tui { + true + } else if args.background || args.yes || !ui::is_interactive() { + false } else { let pkg_mgrs = package_manager_cmds_for_languages(&selected_languages).join(", "); let background_label = format!( @@ -1162,9 +1154,10 @@ async fn run_instrument_setup( let Some(index) = selection else { bail!("instrument setup cancelled by user"); }; - let interactive = index == 1; - (interactive, false) + index == 1 }; + // --yolo grants full permissions regardless of TUI/background mode. + let bypass_permissions = args.yolo; let docs_output_dir = root.join(".bt").join("skills").join("docs"); sdk_install_docs::write_sdk_install_docs(&docs_output_dir)?; @@ -1205,8 +1198,8 @@ async fn run_instrument_setup( eprintln!(); } - let show_output = !base.json && !args.quiet; - let status = if args.quiet && !base.json { + let show_output = !base.json && base.verbose; + let status = if !base.verbose && !base.json { with_spinner( "Running agent instrumentation…", run_agent_invocation(&root, &invocation, false), @@ -1609,15 +1602,31 @@ fn resolve_instrument_invocation( } let invocation = match agent { - Agent::Codex => InstrumentInvocation::Program { - program: "codex".to_string(), - args: vec!["exec".to_string(), "-".to_string()], - stdin_file: Some(task_path.to_path_buf()), - prompt_file_arg: None, - initial_prompt: None, - stream_json: false, - interactive, - }, + Agent::Codex => { + if interactive { + // TUI mode: `codex ""` opens the interactive TUI with the task pre-loaded. + InstrumentInvocation::Program { + program: "codex".to_string(), + args: vec![], + stdin_file: None, + prompt_file_arg: Some(task_path.to_path_buf()), + initial_prompt: None, + stream_json: false, + interactive: true, + } + } else { + // Background mode: `codex exec -` reads the task from stdin. + InstrumentInvocation::Program { + program: "codex".to_string(), + args: vec!["exec".to_string(), "-".to_string()], + stdin_file: Some(task_path.to_path_buf()), + prompt_file_arg: None, + initial_prompt: None, + stream_json: false, + interactive: false, + } + } + } Agent::Claude => { if interactive { // In interactive mode the full task goes into --append-system-prompt so @@ -1677,30 +1686,61 @@ fn resolve_instrument_invocation( } } } - Agent::Opencode => InstrumentInvocation::Program { - program: "opencode".to_string(), - args: vec!["run".to_string()], - stdin_file: None, - prompt_file_arg: Some(task_path.to_path_buf()), - initial_prompt: None, - stream_json: false, - interactive, - }, - Agent::Cursor => InstrumentInvocation::Program { - program: "cursor-agent".to_string(), - args: vec![ - "-p".to_string(), - "-f".to_string(), - "--output-format".to_string(), - "stream-json".to_string(), - "--stream-partial-output".to_string(), - ], - stdin_file: None, - prompt_file_arg: Some(task_path.to_path_buf()), - initial_prompt: None, - stream_json: true, - interactive, - }, + Agent::Opencode => { + if interactive { + // TUI mode: `opencode` opens the interactive TUI. + InstrumentInvocation::Program { + program: "opencode".to_string(), + args: vec![], + stdin_file: None, + prompt_file_arg: None, + initial_prompt: None, + stream_json: false, + interactive: true, + } + } else { + // Background mode: `opencode run ""` runs non-interactively. + InstrumentInvocation::Program { + program: "opencode".to_string(), + args: vec!["run".to_string()], + stdin_file: None, + prompt_file_arg: Some(task_path.to_path_buf()), + initial_prompt: None, + stream_json: false, + interactive: false, + } + } + } + Agent::Cursor => { + if interactive { + // TUI mode: `cursor-agent ""` opens the interactive TUI with task pre-loaded. + InstrumentInvocation::Program { + program: "cursor-agent".to_string(), + args: vec![], + stdin_file: None, + prompt_file_arg: Some(task_path.to_path_buf()), + initial_prompt: None, + stream_json: false, + interactive: true, + } + } else { + // Background mode: `-p` enables non-interactive print mode. + InstrumentInvocation::Program { + program: "cursor-agent".to_string(), + args: vec![ + "-p".to_string(), + "--output-format".to_string(), + "stream-json".to_string(), + "--stream-partial-output".to_string(), + ], + stdin_file: None, + prompt_file_arg: Some(task_path.to_path_buf()), + initial_prompt: None, + stream_json: true, + interactive: false, + } + } + } }; Ok(invocation) } @@ -1960,7 +2000,7 @@ fn resolve_setup_selection(args: &AgentsSetupArgs, home: &Path) -> Result Result { prompted_agents = Some(selected); @@ -2033,7 +2073,7 @@ fn resolve_setup_selection(args: &AgentsSetupArgs, home: &Path) -> Result value, - None => resolve_selected_agents(&args.agents, &detected), + None => resolve_selected_agents(args.agent, &detected), }; if selected_agents.is_empty() { bail!("no agents selected for installation"); @@ -2072,7 +2112,7 @@ fn resolve_mcp_selection(args: &AgentsMcpSetupArgs, home: &Path) -> Result Result { prompted_agents = Some(selected); @@ -2127,7 +2167,7 @@ fn resolve_mcp_selection(args: &AgentsMcpSetupArgs, home: &Path) -> Result value, - None => resolve_selected_agents(&args.agents, &detected), + None => resolve_selected_agents(args.agent, &detected), }; if selected_agents.is_empty() { bail!("no agents selected for MCP setup"); @@ -2427,8 +2467,11 @@ fn resolve_scope_from_flags( }) } -fn resolve_selected_agents(requested: &[AgentArg], detected: &[DetectionSignal]) -> Vec { - if requested.is_empty() { +fn resolve_selected_agents( + requested: Option, + detected: &[DetectionSignal], +) -> Vec { + let Some(arg) = requested else { let mut inferred = BTreeSet::new(); for signal in detected { inferred.insert(signal.agent); @@ -2437,26 +2480,13 @@ fn resolve_selected_agents(requested: &[AgentArg], detected: &[DetectionSignal]) return ALL_AGENTS.to_vec(); } return inferred.into_iter().collect(); - } - - if requested.contains(&AgentArg::All) { - return ALL_AGENTS.to_vec(); - } - - let mut out = BTreeSet::new(); - for value in requested { - let mapped = match value { - AgentArg::Claude => Some(Agent::Claude), - AgentArg::Codex => Some(Agent::Codex), - AgentArg::Cursor => Some(Agent::Cursor), - AgentArg::Opencode => Some(Agent::Opencode), - AgentArg::All => None, - }; - if let Some(agent) = mapped { - out.insert(agent); - } - } - out.into_iter().collect() + }; + vec![match arg { + AgentArg::Claude => Agent::Claude, + AgentArg::Codex => Agent::Codex, + AgentArg::Cursor => Agent::Cursor, + AgentArg::Opencode => Agent::Opencode, + }] } fn map_instrument_agent_arg(agent: InstrumentAgentArg) -> Agent { @@ -3209,23 +3239,13 @@ mod tests { use super::*; use std::time::{SystemTime, UNIX_EPOCH}; - #[test] - fn all_agent_arg_expands_to_all_agents() { - let detected = vec![]; - let resolved = resolve_selected_agents(&[AgentArg::All], &detected); - assert_eq!( - resolved, - vec![Agent::Claude, Agent::Codex, Agent::Cursor, Agent::Opencode] - ); - } - #[test] fn detection_drives_default_selection() { let detected = vec![DetectionSignal { agent: Agent::Codex, reason: "hint".to_string(), }]; - let resolved = resolve_selected_agents(&[], &detected); + let resolved = resolve_selected_agents(None, &detected); assert_eq!(resolved, vec![Agent::Codex]); } @@ -3361,9 +3381,9 @@ mod tests { yes: true, refresh_docs: false, workers: crate::sync::default_workers(), - quiet: false, languages: Vec::new(), - interactive: false, + tui: false, + background: false, yolo: false, }; @@ -3384,9 +3404,9 @@ mod tests { yes: true, refresh_docs: false, workers: crate::sync::default_workers(), - quiet: false, languages: Vec::new(), - interactive: false, + tui: false, + background: false, yolo: false, }; diff --git a/src/switch.rs b/src/switch.rs index cc9bf3a..e022910 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -245,7 +245,7 @@ mod tests { fn base_args(org: Option<&str>, project: Option<&str>) -> BaseArgs { BaseArgs { json: false, - quiet: false, + verbose: false, no_color: false, profile: None, org_name: org.map(String::from), diff --git a/src/traces.rs b/src/traces.rs index 201b7f9..02523b5 100644 --- a/src/traces.rs +++ b/src/traces.rs @@ -6058,7 +6058,7 @@ mod tests { fn base_args() -> BaseArgs { BaseArgs { json: false, - quiet: false, + verbose: false, no_color: false, profile: None, org_name: None, From c4359fca627a92664777aea7d915b0955bbba6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Thu, 2 Apr 2026 17:54:09 +0000 Subject: [PATCH 3/9] remove unused code --- src/auth.rs | 11 ----------- src/setup/mod.rs | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 27e0d74..e376d50 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -779,17 +779,6 @@ async fn run_login_oauth(base: &BaseArgs, args: AuthLoginArgs) -> Result<()> { Ok(()) } -pub async fn login_interactive(base: &mut BaseArgs) -> Result { - let methods = ["OAuth (browser)", "API key"]; - let selected = ui::fuzzy_select("Select login method", &methods, 0)?; - - if selected == 0 { - login_interactive_oauth(base).await - } else { - login_interactive_api_key(base).await - } -} - async fn login_interactive_api_key(base: &mut BaseArgs) -> Result { let api_key = prompt_api_key()?; diff --git a/src/setup/mod.rs b/src/setup/mod.rs index a905187..3d3a9f4 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -536,7 +536,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if verbose { eprintln!(" {}", style("Skills:").bold()); } - if let Some((scope, ref agents)) = setup_context { + if let Some((scope, _)) = setup_context { let args = AgentsSetupArgs { agent: flag_agent, local: matches!(scope, InstallScope::Local), From 0f87ec0de867348c1ce67dbcc2709adc053ef417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Thu, 2 Apr 2026 17:57:13 +0000 Subject: [PATCH 4/9] fix CI --- src/auth.rs | 1 + src/functions/push.rs | 2 +- src/setup/mod.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index e376d50..da4f30c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -779,6 +779,7 @@ async fn run_login_oauth(base: &BaseArgs, args: AuthLoginArgs) -> Result<()> { Ok(()) } +#[allow(dead_code)] async fn login_interactive_api_key(base: &mut BaseArgs) -> Result { let api_key = prompt_api_key()?; diff --git a/src/functions/push.rs b/src/functions/push.rs index 12862a3..d1edb4b 100644 --- a/src/functions/push.rs +++ b/src/functions/push.rs @@ -3359,7 +3359,7 @@ mod tests { fn test_base_args() -> BaseArgs { BaseArgs { json: false, - quiet: false, + verbose: false, no_color: false, profile: None, org_name: None, diff --git a/src/setup/mod.rs b/src/setup/mod.rs index 3d3a9f4..ead88cc 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -3347,7 +3347,7 @@ mod tests { #[test] fn resolve_setup_selection_honors_no_fetch_docs() { let args = AgentsSetupArgs { - agents: vec![AgentArg::Codex], + agent: Some(AgentArg::Codex), local: false, global: true, workflows: vec![WorkflowArg::Evaluate], From 243a81d55e64c974b9b40e7938f1ddf9166fa427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Thu, 2 Apr 2026 19:10:01 +0000 Subject: [PATCH 5/9] fix bugs ; claude, codex, cursor work, opencode doesn't --- src/args.rs | 4 ++-- src/main.rs | 7 ++----- src/setup/mod.rs | 13 +++++++++++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/args.rs b/src/args.rs index c5da1af..37bd1f1 100644 --- a/src/args.rs +++ b/src/args.rs @@ -10,8 +10,8 @@ pub struct BaseArgs { #[arg(long, global = true)] pub json: bool, - /// Show additional output - #[arg(long, short = 'v', env = "BRAINTRUST_VERBOSE", global = true, value_parser = clap::builder::BoolishValueParser::new(), default_value_t = false)] + /// Verbose mode — set at runtime by subcommands that support it + #[arg(skip)] pub verbose: bool, /// Disable ANSI color output diff --git a/src/main.rs b/src/main.rs index a10bfaf..3365319 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,7 +80,6 @@ Flags --profile Use a saved login profile [env: BRAINTRUST_PROFILE] -o, --org Override active org [env: BRAINTRUST_ORG_NAME] -p, --project Override active project [env: BRAINTRUST_DEFAULT_PROJECT] - -v, --verbose Show additional output --json Output as JSON --no-color Disable ANSI color output --no-input Disable all interactive prompts @@ -249,10 +248,8 @@ fn configure_output(base: &BaseArgs) { ui::set_animations_enabled(false); } - if !base.verbose { - ui::set_quiet(true); - ui::set_animations_enabled(false); - } + ui::set_quiet(true); + ui::set_animations_enabled(false); if base.no_input { ui::set_no_input(true); diff --git a/src/setup/mod.rs b/src/setup/mod.rs index ead88cc..650cab2 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -85,6 +85,10 @@ pub struct SetupArgs { #[arg(long, conflicts_with = "tui")] background: bool, + /// Show additional setup output + #[arg(long, short = 'v')] + verbose: bool, + #[command(flatten)] agents: AgentsSetupArgs, } @@ -386,7 +390,12 @@ struct SkillsAliasResult { path: PathBuf, } -pub async fn run_setup_top(base: BaseArgs, args: SetupArgs) -> Result<()> { +pub async fn run_setup_top(mut base: BaseArgs, args: SetupArgs) -> Result<()> { + if args.verbose { + base.verbose = true; + crate::ui::set_quiet(false); + crate::ui::set_animations_enabled(true); + } match args.command { Some(SetupSubcommand::Skills(setup)) => run_setup(base, setup).await, Some(SetupSubcommand::Instrument(instrument)) => { @@ -542,7 +551,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> local: matches!(scope, InstallScope::Local), global: matches!(scope, InstallScope::Global), workflows: Vec::new(), - yes: false, + yes: true, no_fetch_docs: true, refresh_docs: false, workers: crate::sync::default_workers(), From 2967c9c85891747bf2b06c37d81616cce32190b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Thu, 2 Apr 2026 20:37:16 +0000 Subject: [PATCH 6/9] fix: yoloy flag --- src/setup/mod.rs | 108 +++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/src/setup/mod.rs b/src/setup/mod.rs index 650cab2..d335a34 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -1612,11 +1612,15 @@ fn resolve_instrument_invocation( let invocation = match agent { Agent::Codex => { + let mut codex_args = vec![]; + if bypass_permissions { + codex_args.push("--dangerously-bypass-approvals-and-sandbox".to_string()); + } if interactive { // TUI mode: `codex ""` opens the interactive TUI with the task pre-loaded. InstrumentInvocation::Program { program: "codex".to_string(), - args: vec![], + args: codex_args, stdin_file: None, prompt_file_arg: Some(task_path.to_path_buf()), initial_prompt: None, @@ -1625,9 +1629,10 @@ fn resolve_instrument_invocation( } } else { // Background mode: `codex exec -` reads the task from stdin. + codex_args.extend(["exec".to_string(), "-".to_string()]); InstrumentInvocation::Program { program: "codex".to_string(), - args: vec!["exec".to_string(), "-".to_string()], + args: codex_args, stdin_file: Some(task_path.to_path_buf()), prompt_file_arg: None, initial_prompt: None, @@ -1638,28 +1643,26 @@ fn resolve_instrument_invocation( } Agent::Claude => { if interactive { - // In interactive mode the full task goes into --append-system-prompt so - // Claude already knows what to do. A short initial user message is passed - // as the positional arg so Claude immediately starts working — the user only - // needs to press Enter once on a short, clear prompt rather than a wall of - // raw task markdown. - let task_content = std::fs::read_to_string(task_path) - .with_context(|| format!("failed to read task file {}", task_path.display()))?; + // TUI mode: pass the full task as a positional arg so Claude loads it + // as the initial user message and starts working immediately. + // --permission-mode acceptEdits overrides any "plan" defaultMode the + // user may have set globally, which would otherwise cause an immediate + // ExitPlanMode tool call with invalid parameters. InstrumentInvocation::Program { program: "claude".to_string(), args: vec![ - "--append-system-prompt".to_string(), - task_content, - "--disallowedTools".to_string(), - "EnterPlanMode".to_string(), + "--permission-mode".to_string(), + if bypass_permissions { + "bypassPermissions".to_string() + } else { + "acceptEdits".to_string() + }, "--name".to_string(), "Braintrust: Instrument".to_string(), ], stdin_file: None, - prompt_file_arg: None, - initial_prompt: Some( - "Please begin the Braintrust instrumentation task.".to_string(), - ), + prompt_file_arg: Some(task_path.to_path_buf()), + initial_prompt: None, stream_json: false, interactive: true, } @@ -1696,36 +1699,30 @@ fn resolve_instrument_invocation( } } Agent::Opencode => { - if interactive { - // TUI mode: `opencode` opens the interactive TUI. - InstrumentInvocation::Program { - program: "opencode".to_string(), - args: vec![], - stdin_file: None, - prompt_file_arg: None, - initial_prompt: None, - stream_json: false, - interactive: true, - } - } else { - // Background mode: `opencode run ""` runs non-interactively. - InstrumentInvocation::Program { - program: "opencode".to_string(), - args: vec!["run".to_string()], - stdin_file: None, - prompt_file_arg: Some(task_path.to_path_buf()), - initial_prompt: None, - stream_json: false, - interactive: false, - } + // `opencode` TUI does not accept an initial message (its positional arg is a + // project path). `opencode run [message..]` is the only way to deliver a + // prompt, so we use it for both interactive and background modes. In + // interactive mode we inherit all streams so the user can watch and steer. + InstrumentInvocation::Program { + program: "opencode".to_string(), + args: vec!["run".to_string()], + stdin_file: None, + prompt_file_arg: Some(task_path.to_path_buf()), + initial_prompt: None, + stream_json: false, + interactive, } } Agent::Cursor => { + let mut cursor_args = vec![]; + if bypass_permissions { + cursor_args.push("--yolo".to_string()); + } if interactive { // TUI mode: `cursor-agent ""` opens the interactive TUI with task pre-loaded. InstrumentInvocation::Program { program: "cursor-agent".to_string(), - args: vec![], + args: cursor_args, stdin_file: None, prompt_file_arg: Some(task_path.to_path_buf()), initial_prompt: None, @@ -1734,14 +1731,15 @@ fn resolve_instrument_invocation( } } else { // Background mode: `-p` enables non-interactive print mode. + cursor_args.extend([ + "-p".to_string(), + "--output-format".to_string(), + "stream-json".to_string(), + "--stream-partial-output".to_string(), + ]); InstrumentInvocation::Program { program: "cursor-agent".to_string(), - args: vec![ - "-p".to_string(), - "--output-format".to_string(), - "stream-json".to_string(), - "--stream-partial-output".to_string(), - ], + args: cursor_args, stdin_file: None, prompt_file_arg: Some(task_path.to_path_buf()), initial_prompt: None, @@ -3586,29 +3584,22 @@ mod tests { args, stdin_file, prompt_file_arg, - initial_prompt, stream_json, interactive, + .. } => { assert_eq!(program, "claude"); assert!( !args.contains(&"-p".to_string()), "interactive mode must not pass -p" ); - assert!( - args.contains(&"--append-system-prompt".to_string()), - "task should be in system prompt" - ); - assert!(args.contains(&"--disallowedTools".to_string())); + assert!(args.contains(&"--permission-mode".to_string())); assert!(args.contains(&"--name".to_string())); assert_eq!(stdin_file, None); assert_eq!( - prompt_file_arg, None, - "task is in system prompt, not prompt_file_arg" - ); - assert!( - initial_prompt.is_some(), - "short initial message must be set to trigger Claude" + prompt_file_arg, + Some(task_path), + "task passed as positional arg" ); assert!(!stream_json); assert!(interactive); @@ -3664,7 +3655,6 @@ mod tests { args, vec![ "-p".to_string(), - "-f".to_string(), "--output-format".to_string(), "stream-json".to_string(), "--stream-partial-output".to_string(), From 36c04fe7e1f3452a9116a89e12737c547381e0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Fri, 3 Apr 2026 00:06:39 +0000 Subject: [PATCH 7/9] chore: make behavior more consistent and logical --- src/args.rs | 4 --- src/auth.rs | 1 - src/functions/push.rs | 1 - src/main.rs | 4 --- src/setup/mod.rs | 64 ++++++++++++++++++++++++++----------------- src/switch.rs | 1 - src/traces.rs | 1 - src/ui/mod.rs | 7 +---- 8 files changed, 40 insertions(+), 43 deletions(-) diff --git a/src/args.rs b/src/args.rs index 37bd1f1..ce39887 100644 --- a/src/args.rs +++ b/src/args.rs @@ -44,10 +44,6 @@ pub struct BaseArgs { #[arg(long, global = true)] pub prefer_profile: bool, - /// Disable all interactive prompts - #[arg(long, global = true)] - pub no_input: bool, - /// Override API URL (or via BRAINTRUST_API_URL) #[arg( long, diff --git a/src/auth.rs b/src/auth.rs index da4f30c..7968c29 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -2580,7 +2580,6 @@ mod tests { org_name: None, api_key: None, prefer_profile: false, - no_input: false, api_url: None, app_url: None, env_file: None, diff --git a/src/functions/push.rs b/src/functions/push.rs index d1edb4b..0a14c2c 100644 --- a/src/functions/push.rs +++ b/src/functions/push.rs @@ -3366,7 +3366,6 @@ mod tests { project: None, api_key: None, prefer_profile: false, - no_input: false, api_url: None, app_url: None, env_file: None, diff --git a/src/main.rs b/src/main.rs index 3365319..3d9690c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -251,10 +251,6 @@ fn configure_output(base: &BaseArgs) { ui::set_quiet(true); ui::set_animations_enabled(false); - if base.no_input { - ui::set_no_input(true); - } - if disable_color { dialoguer::console::set_colors_enabled(false); dialoguer::console::set_colors_enabled_stderr(false); diff --git a/src/setup/mod.rs b/src/setup/mod.rs index d335a34..0af592e 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -54,19 +54,19 @@ pub struct SetupArgs { command: Option, /// Set up coding-agent skills [default] - #[arg(long)] + #[arg(long, conflicts_with = "no_skills")] skills: bool, /// Do not set up coding-agent skills - #[arg(long)] + #[arg(long, conflicts_with = "skills")] no_skills: bool, /// Set up MCP server [default] - #[arg(long)] + #[arg(long, conflicts_with = "no_mcp")] mcp: bool, /// Do not set up MCP server - #[arg(long)] + #[arg(long, conflicts_with = "mcp")] no_mcp: bool, /// Run instrumentation agent [default] @@ -85,6 +85,12 @@ pub struct SetupArgs { #[arg(long, conflicts_with = "tui")] background: bool, + /// Language(s) to instrument (repeatable; case-insensitive). + /// When provided, the agent skips language auto-detection and instruments + /// the specified language(s) directly. + #[arg(long = "language", value_enum, ignore_case = true)] + languages: Vec, + /// Show additional setup output #[arg(long, short = 'v')] verbose: bool, @@ -115,16 +121,15 @@ struct AgentsSetupArgs { #[arg(long, conflicts_with = "global")] local: bool, - /// Configure user-wide state + /// Configure user-wide state [default] #[arg(long)] global: bool, - /// Workflow docs to prefetch (repeatable) + /// Workflow docs to prefetch (repeatable) [default: all] #[arg(long = "workflow", value_enum)] workflows: Vec, - /// Skip confirmation prompts and use defaults - #[arg(long, short = 'y')] + #[arg(skip)] yes: bool, /// Do not auto-fetch workflow docs during setup @@ -154,12 +159,11 @@ struct AgentsMcpSetupArgs { #[arg(long, conflicts_with = "global")] local: bool, - /// Configure MCP in user-wide state + /// Configure MCP in user-wide state [default] #[arg(long)] global: bool, - /// Skip confirmation prompts and use defaults - #[arg(long, short = 'y')] + #[arg(skip)] yes: bool, } @@ -173,12 +177,11 @@ struct InstrumentSetupArgs { #[arg(long)] agent_cmd: Option, - /// Workflow docs to prefetch alongside instrument (repeatable; always includes instrument) + /// Workflow docs to prefetch alongside instrument (repeatable; always includes instrument) [default: all] #[arg(long = "workflow", value_enum)] workflows: Vec, - /// Skip confirmation prompts and use defaults - #[arg(long, short = 'y')] + #[arg(skip)] yes: bool, /// Refresh prefetched docs by clearing existing output before download @@ -197,7 +200,7 @@ struct InstrumentSetupArgs { languages: Vec, /// Run the agent in interactive TUI mode [default] - #[arg(long, short = 'i', conflicts_with = "background")] + #[arg(long, conflicts_with = "background")] tui: bool, /// Run the agent in background (non-interactive) mode @@ -215,7 +218,7 @@ struct AgentsDoctorArgs { #[arg(long, conflicts_with = "global")] local: bool, - /// Diagnose user-wide setup + /// Diagnose user-wide setup [default] #[arg(long)] global: bool, } @@ -419,6 +422,7 @@ pub async fn run_setup_top(mut base: BaseArgs, args: SetupArgs) -> Result<()> { background: args.background, agent: args.agents.agent, workflows: args.agents.workflows, + languages: args.languages, }; run_setup_wizard(base, wizard_flags).await } else { @@ -444,6 +448,7 @@ struct WizardFlags { background: bool, agent: Option, workflows: Vec, + languages: Vec, } async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> { @@ -461,6 +466,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> background: flag_background, agent: flag_agent, workflows: flag_workflows, + languages: flag_languages, } = flags; let mut had_failures = false; let verbose = base.verbose; @@ -627,11 +633,21 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> instrument_agent }; - // Auto-detect languages from the git root (no prompt) - let detected_languages = git_root - .as_deref() - .map(detect_languages_from_dir) - .unwrap_or_default(); + // Resolve languages: explicit flag > auto-detect from git root > prompt. + let detected_languages = if !flag_languages.is_empty() { + flag_languages.clone() + } else { + let auto = git_root + .as_deref() + .map(detect_languages_from_dir) + .unwrap_or_default(); + if !auto.is_empty() || !ui::is_interactive() { + auto + } else { + // Could not auto-detect and we have a TTY — ask the user. + prompt_instrument_language_selection(&[])?.unwrap_or_default() + } + }; // Default workflows: instrument + observe. --workflow flags override. let wizard_workflows = if flag_workflows.is_empty() { @@ -1028,10 +1044,8 @@ impl LanguageArg { } fn should_prompt_setup_action(base: &BaseArgs, args: &AgentsSetupArgs) -> bool { - if base.json || !ui::is_interactive() { - return false; - } - !args.yes + !base.json + && ui::is_interactive() && !args.no_fetch_docs && !args.refresh_docs && args.workers == crate::sync::default_workers() diff --git a/src/switch.rs b/src/switch.rs index e022910..2defe95 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -252,7 +252,6 @@ mod tests { project: project.map(String::from), api_key: None, prefer_profile: false, - no_input: false, api_url: None, app_url: None, env_file: None, diff --git a/src/traces.rs b/src/traces.rs index 02523b5..9bbd717 100644 --- a/src/traces.rs +++ b/src/traces.rs @@ -6065,7 +6065,6 @@ mod tests { project: None, api_key: None, prefer_profile: false, - no_input: false, api_url: None, app_url: None, env_file: None, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 07e999b..6a36782 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,14 +8,9 @@ mod spinner; mod status; mod table; -static NO_INPUT: AtomicBool = AtomicBool::new(false); static QUIET: AtomicBool = AtomicBool::new(false); static ANIMATIONS_ENABLED: AtomicBool = AtomicBool::new(true); -pub fn set_no_input(val: bool) { - NO_INPUT.store(val, Ordering::Relaxed); -} - pub fn set_quiet(val: bool) { QUIET.store(val, Ordering::Relaxed); } @@ -33,7 +28,7 @@ pub fn animations_enabled() -> bool { } pub fn is_interactive() -> bool { - std::io::stdin().is_terminal() && !NO_INPUT.load(Ordering::Relaxed) + std::io::stdin().is_terminal() } pub use pager::print_with_pager; From b3c46e70deead10e31e4b67f104ec6f616cc6f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Fri, 3 Apr 2026 17:31:22 +0000 Subject: [PATCH 8/9] interactive flag to use the interactive wizard bug fix --- src/setup/mod.rs | 309 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 261 insertions(+), 48 deletions(-) diff --git a/src/setup/mod.rs b/src/setup/mod.rs index 0af592e..7772f2b 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -16,7 +16,7 @@ use crate::auth; use crate::auth::LoginContext; use crate::config; use crate::http::ApiClient; -use crate::ui::{self, with_spinner}; +use crate::ui::{self, print_command_status, with_spinner, CommandStatus}; mod agent_stream; mod docs; @@ -91,6 +91,11 @@ pub struct SetupArgs { #[arg(long = "language", value_enum, ignore_case = true)] languages: Vec, + /// Run the interactive setup wizard, prompting for every choice not already + /// specified as a flag. + #[arg(long, short = 'i')] + interactive: bool, + /// Show additional setup output #[arg(long, short = 'v')] verbose: bool, @@ -404,11 +409,14 @@ pub async fn run_setup_top(mut base: BaseArgs, args: SetupArgs) -> Result<()> { Some(SetupSubcommand::Instrument(instrument)) => { run_instrument_setup(base, instrument, false).await } - Some(SetupSubcommand::Mcp(mcp)) => run_mcp_setup(base, mcp), + Some(SetupSubcommand::Mcp(mcp)) => run_mcp_setup(base, mcp).await, Some(SetupSubcommand::Doctor(doctor)) => run_doctor(base, doctor), None => { - if should_prompt_setup_action(&base, &args.agents) { + let run_wizard = (args.interactive && ui::is_interactive()) + || should_prompt_setup_action(&base, &args.agents); + if run_wizard { let wizard_flags = WizardFlags { + interactive: args.interactive, yolo: args.agents.yolo, skills: args.skills, mcp: args.mcp, @@ -435,6 +443,7 @@ pub async fn run_setup_top(mut base: BaseArgs, args: SetupArgs) -> Result<()> { pub use docs::run_docs_top; struct WizardFlags { + interactive: bool, yolo: bool, skills: bool, mcp: bool, @@ -453,14 +462,15 @@ struct WizardFlags { async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> { let WizardFlags { - yolo, + interactive, + yolo: flag_yolo, skills: flag_skills, mcp: flag_mcp, no_skills: flag_no_skills, no_mcp: flag_no_mcp, local: flag_local, - global: _flag_global, - instrument: _flag_instrument, + global: flag_global, + instrument: flag_instrument, no_instrument: flag_no_instrument, tui: flag_tui, background: flag_background, @@ -479,6 +489,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> } let project_flag = base.project.clone(); let login_ctx = ensure_auth(&mut base).await?; + let mcp_api_key: String = login_ctx.login.api_key.clone(); let client = ApiClient::new(&login_ctx)?; let org = client.org_name().to_string(); if verbose { @@ -514,29 +525,50 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if verbose { print_wizard_step(3, "Agents"); } - // Default: install both skills and MCP with global scope. - // --no-skill / --no-mcp / --no-mcp-skill opt out. - let _ = flag_skills; // kept as flag but superseded by default-on behavior - let _ = flag_mcp; // kept as flag but superseded by default-on behavior - let wants_skills = !flag_no_skills; - let wants_mcp = !flag_no_mcp; - // Scope: default global; --local overrides. + // Scope: --local / --global flag > interactive prompt > default global. let scope = if flag_local { InstallScope::Local + } else if flag_global { + InstallScope::Global + } else if interactive { + prompt_scope_selection("Install scope")?.unwrap_or(InstallScope::Global) } else { InstallScope::Global }; + // Skills: --no-skills opts out; --skills opts in; otherwise prompt or default on. + let wants_skills = if flag_no_skills { + false + } else if flag_skills || !interactive { + true + } else { + Confirm::new() + .with_prompt("Set up coding-agent skills?") + .default(true) + .interact()? + }; + + // MCP: same pattern. + let wants_mcp = if flag_no_mcp { + false + } else if flag_mcp || !interactive { + true + } else { + Confirm::new() + .with_prompt("Set up MCP server?") + .default(true) + .interact()? + }; + let local_root = resolve_local_root_for_scope(scope)?; let detected = detect_agents(local_root.as_deref(), &home); let agents = resolve_selected_agents(flag_agent, &detected); if verbose { - let scope_label = if flag_local { - "local (current git repo)" - } else { - "global (user-wide)" + let scope_label = match scope { + InstallScope::Local => "local (current git repo)", + InstallScope::Global => "global (user-wide)", }; eprintln!(" {} Scope: {}", style("✓").green(), scope_label); } @@ -564,6 +596,7 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> yolo: false, }; let outcome = execute_skills_setup(&base, &args).await?; + let mut any_installed = false; for r in &outcome.results { if verbose { print_wizard_agent_result(r); @@ -571,6 +604,24 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if matches!(r.status, InstallStatus::Failed) { had_failures = true; } + if matches!(r.status, InstallStatus::Installed) { + any_installed = true; + } + } + if !verbose && !outcome.results.is_empty() { + let label = if any_installed { + "installed" + } else { + "already configured" + }; + print_command_status( + if any_installed { + CommandStatus::Success + } else { + CommandStatus::Warning + }, + &format!("Skills: {label} (use /braintrust in Claude Code)"), + ); } } } @@ -581,7 +632,14 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> } if let Some((scope, ref agents)) = setup_context { let local_root = resolve_local_root_for_scope(scope)?; - let outcome = execute_mcp_install(scope, local_root.as_deref(), &home, agents); + let outcome = execute_mcp_install( + scope, + local_root.as_deref(), + &home, + agents, + Some(mcp_api_key.as_str()), + ); + let mut any_installed = false; for r in &outcome.results { if verbose { print_wizard_agent_result(r); @@ -589,10 +647,28 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> if matches!(r.status, InstallStatus::Failed) { had_failures = true; } + if matches!(r.status, InstallStatus::Installed) { + any_installed = true; + } } if outcome.installed_count == 0 && !agents.is_empty() { had_failures = true; } + if !verbose { + let label = if any_installed { + "configured" + } else { + "already configured" + }; + print_command_status( + if any_installed { + CommandStatus::Success + } else { + CommandStatus::Warning + }, + &format!("MCP: {label}"), + ); + } } } @@ -605,8 +681,17 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> print_wizard_step(4, "Instrument"); } if git_root.is_some() { - // Default: instrument. --no-instrument opts out. - let do_instrument = !flag_no_instrument; + // --no-instrument opts out; --instrument opts in; otherwise prompt or default on. + let do_instrument = if flag_no_instrument { + false + } else if flag_instrument || !interactive { + true + } else { + Confirm::new() + .with_prompt("Run instrumentation agent?") + .default(true) + .interact()? + }; if do_instrument { // Determine agent: explicit flag > single detected > ask user let instrument_agent = @@ -649,10 +734,8 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> } }; - // Default workflows: instrument + observe. --workflow flags override. - let wizard_workflows = if flag_workflows.is_empty() { - vec![WorkflowArg::Instrument, WorkflowArg::Observe] - } else { + // Workflows: explicit flag > interactive prompt > default instrument+observe. + let wizard_workflows = if !flag_workflows.is_empty() { let mut selected = resolve_workflow_selection(&flag_workflows); if !selected.contains(&WorkflowArg::Instrument) { selected.push(WorkflowArg::Instrument); @@ -660,6 +743,38 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> selected.dedup(); } selected + } else if interactive { + prompt_instrument_workflow_selection()?.unwrap_or_default() + } else { + vec![WorkflowArg::Instrument, WorkflowArg::Observe] + }; + + // Run mode: --tui / --background flag > interactive prompt > default TUI. + let run_tui = if flag_tui { + true + } else if flag_background { + false + } else if interactive { + let idx = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("How do you want to run the agent?") + .items(&["Interactive (TUI)", "Background"]) + .default(0) + .interact()?; + idx == 0 + } else { + true + }; + + // Yolo: explicit flag > interactive prompt > default false. + let effective_yolo = if flag_yolo { + true + } else if interactive { + Confirm::new() + .with_prompt("Grant agent full permissions? (bypass permission prompts)") + .default(false) + .interact()? + } else { + false }; run_instrument_setup( @@ -673,9 +788,9 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> refresh_docs: false, workers: crate::sync::default_workers(), languages: detected_languages, - tui: flag_tui || !flag_background, - background: flag_background, - yolo, + tui: run_tui, + background: !run_tui, + yolo: effective_yolo, }, false, ) @@ -1107,13 +1222,19 @@ async fn run_instrument_setup( let mut notes = Vec::new(); let mut results = Vec::new(); let skill_path = skill_config_path(selected, InstallScope::Local, Some(&root), &home)?; + let global_skill_path = skill_config_path(selected, InstallScope::Global, None, &home)?; - if skill_path.exists() { + if skill_path.exists() || global_skill_path.exists() { + let existing_path = if skill_path.exists() { + skill_path.clone() + } else { + global_skill_path + }; results.push(AgentInstallResult { agent: selected, status: InstallStatus::Skipped, message: "already configured".to_string(), - paths: vec![skill_path.display().to_string()], + paths: vec![existing_path.display().to_string()], }); notes.push("Skipped skills setup (already configured).".to_string()); prefetch_workflow_docs( @@ -1215,8 +1336,10 @@ async fn run_instrument_setup( if run_interactive { eprintln!(); - eprintln!("Claude Code is opening in interactive mode."); - eprintln!("The instrumentation task is pre-loaded. Press Enter to begin."); + eprintln!( + "{} is opening in interactive mode.", + selected.display_name() + ); eprintln!("Task file: {}", task_path.display()); eprintln!(); } @@ -1659,18 +1782,25 @@ fn resolve_instrument_invocation( if interactive { // TUI mode: pass the full task as a positional arg so Claude loads it // as the initial user message and starts working immediately. - // --permission-mode acceptEdits overrides any "plan" defaultMode the - // user may have set globally, which would otherwise cause an immediate - // ExitPlanMode tool call with invalid parameters. + // --permission-mode acceptEdits overrides the permission mode, but the + // user's global "defaultMode": "plan" in ~/.claude/settings.json can still + // cause Claude to start in plan mode and try to call ExitPlanMode (which + // produces "Invalid tool parameters"). We override defaultMode explicitly + // via --settings and also disallow ExitPlanMode to be safe. + let permission_mode = if bypass_permissions { + "bypassPermissions" + } else { + "acceptEdits" + }; InstrumentInvocation::Program { program: "claude".to_string(), args: vec![ "--permission-mode".to_string(), - if bypass_permissions { - "bypassPermissions".to_string() - } else { - "acceptEdits".to_string() - }, + permission_mode.to_string(), + "--settings".to_string(), + format!(r#"{{"defaultMode": "{permission_mode}"}}"#), + "--disallowedTools".to_string(), + "ExitPlanMode,EnterPlanMode".to_string(), "--name".to_string(), "Braintrust: Instrument".to_string(), ], @@ -1921,12 +2051,13 @@ fn execute_mcp_install( local_root: Option<&Path>, home: &Path, agents: &[Agent], + api_key: Option<&str>, ) -> McpSetupOutcome { let mut warnings = Vec::new(); let mut results = Vec::new(); for agent in agents.iter().copied() { - let result = install_mcp_for_agent(agent, scope, local_root, home); + let result = install_mcp_for_agent(agent, scope, local_root, home, api_key); match result { Ok(r) => { if matches!(r.status, InstallStatus::Skipped) @@ -1959,15 +2090,25 @@ fn execute_mcp_install( } } -fn run_mcp_setup(base: BaseArgs, args: AgentsMcpSetupArgs) -> Result<()> { +async fn run_mcp_setup(base: BaseArgs, args: AgentsMcpSetupArgs) -> Result<()> { let home = home_dir().ok_or_else(|| anyhow!("failed to resolve HOME/USERPROFILE"))?; + let api_key = crate::auth::resolve_auth(&base) + .await + .ok() + .and_then(|a| a.api_key); let selection = resolve_mcp_selection(&args, &home)?; let scope = selection.scope; let local_root = selection.local_root; let detected = selection.detected; let selected_agents = selection.selected_agents; - let outcome = execute_mcp_install(scope, local_root.as_deref(), &home, &selected_agents); + let outcome = execute_mcp_install( + scope, + local_root.as_deref(), + &home, + &selected_agents, + api_key.as_deref(), + ); if base.json { let report = SetupJsonReport { @@ -2841,7 +2982,15 @@ fn install_mcp_for_agent( scope: InstallScope, local_root: Option<&Path>, home: &Path, + api_key: Option<&str>, ) -> Result { + // For Claude with global scope, write to ~/.claude.json top-level mcpServers via + // `claude mcp add -s user`. This is the only path Claude Code reliably reads for + // user-wide MCP — it does not expand ${VAR} placeholders in .mcp.json headers. + if matches!(agent, Agent::Claude) && matches!(scope, InstallScope::Global) { + return install_mcp_for_claude_user(home, api_key); + } + let path = match agent { Agent::Cursor => { if matches!(scope, InstallScope::Global) { @@ -2865,7 +3014,7 @@ fn install_mcp_for_agent( } }; - merge_mcp_config(&path)?; + merge_mcp_config(&path, api_key)?; Ok(AgentInstallResult { agent, @@ -2875,7 +3024,70 @@ fn install_mcp_for_agent( }) } -fn merge_mcp_config(path: &Path) -> Result<()> { +/// Install the Braintrust MCP server into Claude Code's user-wide config (~/.claude.json). +/// Falls back to ~/.mcp.json if the `claude` CLI is not available. +fn install_mcp_for_claude_user(home: &Path, api_key: Option<&str>) -> Result { + let Some(key) = api_key else { + // No API key available — fall back to ~/.mcp.json with env-var placeholder and + // tell the user they need BRAINTRUST_API_KEY set for Claude Code to authenticate. + let path = home.join(".mcp.json"); + merge_mcp_config(&path, None)?; + return Ok(AgentInstallResult { + agent: Agent::Claude, + status: InstallStatus::Installed, + message: "installed MCP config (set BRAINTRUST_API_KEY env var for Claude Code)" + .to_string(), + paths: vec![path.display().to_string()], + }); + }; + + let header = format!("Authorization: Bearer {key}"); + let output = std::process::Command::new("claude") + .args([ + "mcp", + "add", + "--transport", + "http", + "braintrust", + "https://api.braintrust.dev/mcp", + "-H", + &header, + "-s", + "user", + ]) + .output(); + + match output { + Ok(out) if out.status.success() => Ok(AgentInstallResult { + agent: Agent::Claude, + status: InstallStatus::Installed, + message: "installed MCP config".to_string(), + paths: vec![home.join(".claude.json").display().to_string()], + }), + Ok(out) => { + let stderr = String::from_utf8_lossy(&out.stderr); + Err(anyhow!("claude mcp add failed: {stderr}")) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + // `claude` not in PATH — fall back to ~/.mcp.json + let path = home.join(".mcp.json"); + merge_mcp_config(&path, Some(key))?; + Ok(AgentInstallResult { + agent: Agent::Claude, + status: InstallStatus::Installed, + message: "installed MCP config".to_string(), + paths: vec![path.display().to_string()], + }) + } + Err(e) => Err(e.into()), + } +} + +fn merge_mcp_config(path: &Path, api_key: Option<&str>) -> Result<()> { + let auth_value = match api_key { + Some(key) => format!("Bearer {key}"), + None => "Bearer ${BRAINTRUST_API_KEY}".to_string(), + }; let mut root = load_json_object_or_default(path)?; let servers_value = root .entry("mcpServers".to_string()) @@ -2893,7 +3105,7 @@ fn merge_mcp_config(path: &Path) -> Result<()> { "type": "http", "url": "https://api.braintrust.dev/mcp", "headers": { - "Authorization": "Bearer ${BRAINTRUST_API_KEY}" + "Authorization": auth_value } }), ); @@ -3286,7 +3498,7 @@ mod tests { ) .expect("seed mcp"); - merge_mcp_config(&path).expect("merge mcp"); + merge_mcp_config(&path, None).expect("merge mcp"); let parsed: Value = serde_json::from_str(&fs::read_to_string(&path).expect("read mcp")).expect("json"); @@ -3762,8 +3974,9 @@ mod tests { let home = root.join("home"); fs::create_dir_all(&home).expect("create temp home"); - let result = install_mcp_for_agent(Agent::Codex, InstallScope::Local, Some(&root), &home) - .expect("install local mcp"); + let result = + install_mcp_for_agent(Agent::Codex, InstallScope::Local, Some(&root), &home, None) + .expect("install local mcp"); assert!(matches!(result.status, InstallStatus::Installed)); let mcp_path = root.join(".mcp.json"); From 5122ca677d252bdc531b510d060604066ff17f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Halber?= Date: Fri, 3 Apr 2026 18:58:25 +0000 Subject: [PATCH 9/9] fix: no-instrument flag didn't work and crashed --- src/auth.rs | 4 +- src/setup/mod.rs | 151 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 113 insertions(+), 42 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 7968c29..bf1cce4 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -405,11 +405,11 @@ fn maybe_warn_api_key_override(base: &BaseArgs) { if let Some(profile_name) = ignored_profile { eprintln!( - "Warning: using --api-key/BRAINTRUST_API_KEY credentials; selected profile '{profile_name}' is ignored for this command. Use --prefer-profile or unset BRAINTRUST_API_KEY.", + "Info: using --api-key/BRAINTRUST_API_KEY credentials; selected profile '{profile_name}' is ignored for this command. Use --prefer-profile or unset BRAINTRUST_API_KEY to use a profile with OAuth login.", ); } else { eprintln!( - "Warning: using --api-key/BRAINTRUST_API_KEY credentials for this command. Use --prefer-profile or unset BRAINTRUST_API_KEY." + "Info: using --api-key/BRAINTRUST_API_KEY credentials for this command. Use --prefer-profile or unset BRAINTRUST_API_KEY to use a registered profile." ); } } diff --git a/src/setup/mod.rs b/src/setup/mod.rs index 7772f2b..29f1a7d 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -73,16 +73,22 @@ pub struct SetupArgs { #[arg(long)] instrument: bool, - /// Do not run instrumentation agent - #[arg(long, conflicts_with = "instrument")] + /// Do not run instrumentation agent (skills and MCP are still configured) + #[arg( + long, + conflicts_with = "instrument", + conflicts_with = "tui", + conflicts_with = "background", + conflicts_with = "yolo" + )] no_instrument: bool, /// Run the agent in interactive TUI mode [default] - #[arg(long, conflicts_with = "background")] + #[arg(long, conflicts_with = "background", conflicts_with = "no_instrument")] tui: bool, /// Run the agent in background (non-interactive) mode - #[arg(long, conflicts_with = "tui")] + #[arg(long, conflicts_with = "tui", conflicts_with = "no_instrument")] background: bool, /// Language(s) to instrument (repeatable; case-insensitive). @@ -150,7 +156,7 @@ struct AgentsSetupArgs { workers: usize, /// Grant the agent full permissions (bypass permission prompts) - #[arg(long)] + #[arg(long, conflicts_with = "no_instrument")] yolo: bool, } @@ -215,6 +221,10 @@ struct InstrumentSetupArgs { /// Grant the agent full permissions (bypass permission prompts) #[arg(long)] yolo: bool, + + /// Set up skills and write the task file but do not launch the agent. + #[arg(skip)] + skip_launch: bool, } #[derive(Debug, Clone, Args)] @@ -651,9 +661,6 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> any_installed = true; } } - if outcome.installed_count == 0 && !agents.is_empty() { - had_failures = true; - } if !verbose { let label = if any_installed { "configured" @@ -681,8 +688,10 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> print_wizard_step(4, "Instrument"); } if git_root.is_some() { - // --no-instrument opts out; --instrument opts in; otherwise prompt or default on. - let do_instrument = if flag_no_instrument { + // Whether to launch the agent at the end of this step. + // --no-instrument / interactive "no": set up skills/docs but skip the launch. + // --instrument / non-interactive: always launch. + let launch_agent = if flag_no_instrument { false } else if flag_instrument || !interactive { true @@ -692,7 +701,10 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> .default(true) .interact()? }; - if do_instrument { + + // Skills, docs, and the task file are always set up when a git root exists. + // Only the agent launch is conditional. + { // Determine agent: explicit flag > single detected > ask user let instrument_agent = determine_wizard_instrument_agent(flag_agent, git_root.as_deref(), &home); @@ -749,32 +761,35 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> vec![WorkflowArg::Instrument, WorkflowArg::Observe] }; - // Run mode: --tui / --background flag > interactive prompt > default TUI. - let run_tui = if flag_tui { - true - } else if flag_background { - false - } else if interactive { - let idx = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("How do you want to run the agent?") - .items(&["Interactive (TUI)", "Background"]) - .default(0) - .interact()?; - idx == 0 - } else { - true - }; - - // Yolo: explicit flag > interactive prompt > default false. - let effective_yolo = if flag_yolo { - true - } else if interactive { - Confirm::new() - .with_prompt("Grant agent full permissions? (bypass permission prompts)") - .default(false) - .interact()? + // Run mode and yolo are only meaningful when actually launching. + let (run_tui, effective_yolo) = if launch_agent { + let tui = if flag_tui { + true + } else if flag_background { + false + } else if interactive { + let idx = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("How do you want to run the agent?") + .items(&["Interactive (TUI)", "Background"]) + .default(0) + .interact()?; + idx == 0 + } else { + true + }; + let yolo = if flag_yolo { + true + } else if interactive { + Confirm::new() + .with_prompt("Grant agent full permissions? (bypass permission prompts)") + .default(false) + .interact()? + } else { + false + }; + (tui, yolo) } else { - false + (false, false) }; run_instrument_setup( @@ -791,12 +806,11 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()> tui: run_tui, background: !run_tui, yolo: effective_yolo, + skip_launch: !launch_agent, }, false, ) .await?; - } else if verbose { - eprintln!(" {}", style("Skipped").dim()); } } else if verbose { eprintln!(" {}", style("Skipped").dim()); @@ -1274,8 +1288,11 @@ async fn run_instrument_setup( // Determine run mode: interactive TUI vs background (autonomous). // --tui: interactive TUI (inherits terminal) // --background / --yes / non-interactive terminal: background (autonomous) + // skip_launch: not launching at all — default to non-interactive for task rendering // Otherwise: ask the user. - let run_interactive = if args.tui { + let run_interactive = if args.skip_launch { + false + } else if args.tui { true } else if args.background || args.yes || !ui::is_interactive() { false @@ -1325,6 +1342,37 @@ async fn run_instrument_setup( task_path.display() )); + // --no-instrument (skip_launch): skills configured, task file written — done. + if args.skip_launch { + if base.json { + let report = SetupJsonReport { + scope: InstallScope::Local.as_str().to_string(), + selected_agents: vec![selected], + detected_agents: detected, + results: results.clone(), + warnings, + notes, + }; + println!( + "{}", + serde_json::to_string_pretty(&report) + .context("failed to serialize setup report")? + ); + } else { + eprintln!(); + for result in &results { + print_wizard_agent_result(result); + } + eprintln!( + " {} Task file: {}", + style("✓").green(), + task_path.display() + ); + print_wizard_done(false); + } + return Ok(()); + } + let invocation = resolve_instrument_invocation( selected, args.agent_cmd.as_deref(), @@ -3027,6 +3075,8 @@ fn install_mcp_for_agent( /// Install the Braintrust MCP server into Claude Code's user-wide config (~/.claude.json). /// Falls back to ~/.mcp.json if the `claude` CLI is not available. fn install_mcp_for_claude_user(home: &Path, api_key: Option<&str>) -> Result { + let claude_json_path = home.join(".claude.json"); + let Some(key) = api_key else { // No API key available — fall back to ~/.mcp.json with env-var placeholder and // tell the user they need BRAINTRUST_API_KEY set for Claude Code to authenticate. @@ -3041,6 +3091,25 @@ fn install_mcp_for_claude_user(home: &Path, api_key: Option<&str>) -> Result) -> Result { let stderr = String::from_utf8_lossy(&out.stderr); @@ -3618,6 +3687,7 @@ mod tests { tui: false, background: false, yolo: false, + skip_launch: false, }; let selected = resolve_instrument_workflow_selection(&args, &mut false) @@ -3641,6 +3711,7 @@ mod tests { tui: false, background: false, yolo: false, + skip_launch: false, }; let selected = resolve_instrument_workflow_selection(&args, &mut false)