Skip to main content

rho_coding_agent/app/
bootstrap.rs

1use std::{
2    io::{self, IsTerminal},
3    sync::Arc,
4};
5
6use crate::{
7    cli::{Cli, Command},
8    credentials::OsCredentialStore,
9    diagnostics::RuntimeDiagnostics,
10    herdr::HerdrReporter,
11    model::{models_dev::cached_model_metadata, ModelError},
12    update,
13};
14
15use super::{
16    automation, cli_config, config_repository::ConfigRepository, interactive, login,
17    sdk_config::SdkBootstrapOptions,
18};
19
20pub async fn run(cli: Cli) -> anyhow::Result<()> {
21    cli_config::validate(&cli)?;
22    if matches!(cli.command, Some(Command::Update)) {
23        return update::run_update(env!("CARGO_PKG_VERSION")).await;
24    }
25    if let Some(Command::Login {
26        provider,
27        device_auth,
28    }) = &cli.command
29    {
30        return login::run(provider, *device_auth).await;
31    }
32
33    let config_path = cli.config.clone();
34    let config_repository = ConfigRepository::new(config_path.clone());
35    let mut config = config_repository.load()?;
36    let cwd = std::env::current_dir()?;
37    let automation_prompt = automation::prompt_for_command(&cli.command)?;
38    let (preset, output_file) = match &cli.command {
39        Some(Command::Run {
40            preset,
41            output_file,
42            ..
43        }) => (
44            preset
45                .as_deref()
46                .map(|name| crate::subagent::find(&cwd, name))
47                .transpose()?,
48            output_file.clone(),
49        ),
50        _ => (None, None),
51    };
52
53    let store = OsCredentialStore;
54    cli_config::refresh_model_cache(&cli, &store).await?;
55    if let Some(provider) = preset
56        .as_ref()
57        .and_then(|preset| preset.provider.as_deref())
58    {
59        cli_config::refresh_model_cache_for_provider(provider, &store).await?;
60    }
61    if cli_config::apply_overrides(&mut config, &cli)? {
62        config_repository.save(&config)?;
63    }
64    if let Some(preset) = &preset {
65        apply_preset_overrides(&mut config, preset)?;
66    }
67
68    validate_terminal_mode(&cli)?;
69    if automation_prompt.is_some()
70        && config.provider == "anthropic"
71        && cached_model_metadata(&config.provider, &config.model).is_none()
72    {
73        let _ =
74            crate::model::models_dev::fetch_model_metadata(&config.provider, &config.model).await;
75    }
76    if preset.is_some() {
77        cli_config::normalize_reasoning(&mut config);
78    }
79    let herdr = HerdrReporter::from_env();
80    if let Some(prompt) = automation_prompt {
81        let diagnostics = RuntimeDiagnostics::new(&config);
82        return automation::run(
83            prompt,
84            automation::Startup {
85                config: &config,
86                config_path: absolute_config_path(&config_repository)?,
87                cwd,
88                no_system_prompt: cli.no_system_prompt,
89                no_tools: cli.no_tools,
90                no_subagents: cli.no_subagents,
91                preset,
92                output_file,
93                diagnostics,
94                herdr,
95            },
96        )
97        .await;
98    }
99    let diagnostics = RuntimeDiagnostics::new(&config);
100
101    let pending_update_notice = config
102        .check_for_updates
103        .then(|| tokio::spawn(update::update_notice(env!("CARGO_PKG_VERSION"))));
104
105    let sdk_options = SdkBootstrapOptions::from_config(&config, &cwd)?;
106    let credentials = crate::auth::provider_credentials::ApplicationCredentialSource::new(
107        Arc::new(OsCredentialStore),
108    );
109    let provider_result =
110        crate::providers::build_sdk_provider_with_source(sdk_options.provider, &credentials);
111    let (missing_auth_error, missing_auth_model_error) = match provider_result {
112        Ok(_) => (None, None),
113        Err(error) if is_interactive_startup_unavailable_error(&error) => {
114            (Some(error.to_string()), Some(error))
115        }
116        Err(error) => return Err(error.into()),
117    };
118    let result = interactive::run(interactive::Startup {
119        cli: &cli,
120        config,
121        config_path: absolute_config_path(&config_repository)?,
122        config_repository,
123        cwd,
124        missing_auth_error,
125        missing_auth_model_error,
126        pending_update_notice,
127        diagnostics,
128        herdr,
129    })
130    .await;
131    result
132}
133
134fn apply_preset_overrides(
135    config: &mut crate::config::Config,
136    preset: &crate::subagent::Preset,
137) -> anyhow::Result<()> {
138    // Preset overrides apply to this run only; never persist them.
139    if let Some(provider) = &preset.provider {
140        cli_config::apply_provider_override(config, provider, preset.model.is_some())?;
141    }
142    if let Some(model) = &preset.model {
143        config.model = model.clone();
144    }
145    if let Some(reasoning) = preset.reasoning {
146        config.reasoning = reasoning;
147    }
148    Ok(())
149}
150
151fn absolute_config_path(repository: &ConfigRepository) -> anyhow::Result<std::path::PathBuf> {
152    let path = repository.configured_path()?;
153    if path.is_absolute() {
154        Ok(path)
155    } else {
156        Ok(std::env::current_dir()?.join(path))
157    }
158}
159
160fn validate_terminal_mode(cli: &Cli) -> anyhow::Result<()> {
161    if cli.command.is_none() && (!io::stdin().is_terminal() || !io::stdout().is_terminal()) {
162        anyhow::bail!(
163            "rho's default mode is the interactive TUI; use `rho run` for non-interactive automation"
164        );
165    }
166    Ok(())
167}
168
169fn is_interactive_startup_unavailable_error(error: &ModelError) -> bool {
170    matches!(
171        error,
172        ModelError::MissingApiKey
173            | ModelError::MissingCodexAuth
174            | ModelError::MissingAnthropicApiKey
175            | ModelError::MissingGithubCopilotAuth
176            | ModelError::MissingXaiAuth
177            | ModelError::Credentials(_)
178            | ModelError::UnsupportedProvider(_)
179    )
180}
181
182#[cfg(test)]
183#[path = "bootstrap_tests.rs"]
184mod tests;