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