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 store = OsCredentialStore;
37    cli_config::refresh_model_cache(&cli, &store).await?;
38    if cli_config::apply_overrides(&mut config, &cli)? {
39        config_repository.save(&config)?;
40    }
41
42    validate_terminal_mode(&cli)?;
43    let automation_prompt = automation::prompt_for_command(&cli.command)?;
44    if automation_prompt.is_some()
45        && config.provider == "anthropic"
46        && cached_model_metadata(&config.provider, &config.model).is_none()
47    {
48        let _ =
49            crate::model::models_dev::fetch_model_metadata(&config.provider, &config.model).await;
50    }
51    let cwd = std::env::current_dir()?;
52    let diagnostics = RuntimeDiagnostics::new(&config);
53    let herdr = HerdrReporter::from_env();
54    if let Some(prompt) = automation_prompt {
55        return automation::run(
56            prompt,
57            automation::Startup {
58                config: &config,
59                cwd,
60                no_system_prompt: cli.no_system_prompt,
61                no_tools: cli.no_tools,
62                diagnostics,
63                herdr,
64            },
65        )
66        .await;
67    }
68
69    let pending_update_notice = config
70        .check_for_updates
71        .then(|| tokio::spawn(update::update_notice(env!("CARGO_PKG_VERSION"))));
72
73    let sdk_options = SdkBootstrapOptions::from_config(&config, &cwd)?;
74    let credentials = crate::auth::provider_credentials::ApplicationCredentialSource::new(
75        Arc::new(OsCredentialStore),
76    );
77    let provider_result =
78        crate::providers::build_sdk_provider_with_source(sdk_options.provider, &credentials);
79    let (missing_auth_error, missing_auth_model_error) = match provider_result {
80        Ok(_) => (None, None),
81        Err(error) if is_interactive_startup_unavailable_error(&error) => {
82            (Some(error.to_string()), Some(error))
83        }
84        Err(error) => return Err(error.into()),
85    };
86    let result = interactive::run(interactive::Startup {
87        cli: &cli,
88        config,
89        config_repository,
90        cwd,
91        missing_auth_error,
92        missing_auth_model_error,
93        pending_update_notice,
94        diagnostics,
95        herdr,
96    })
97    .await;
98    result
99}
100
101fn validate_terminal_mode(cli: &Cli) -> anyhow::Result<()> {
102    if cli.command.is_none() && (!io::stdin().is_terminal() || !io::stdout().is_terminal()) {
103        anyhow::bail!(
104            "rho's default mode is the interactive TUI; use `rho run` for non-interactive automation"
105        );
106    }
107    Ok(())
108}
109
110fn is_interactive_startup_unavailable_error(error: &ModelError) -> bool {
111    matches!(
112        error,
113        ModelError::MissingApiKey
114            | ModelError::MissingCodexAuth
115            | ModelError::MissingAnthropicApiKey
116            | ModelError::MissingGithubCopilotAuth
117            | ModelError::MissingXaiAuth
118            | ModelError::Credentials(_)
119            | ModelError::UnsupportedProvider(_)
120    )
121}
122
123#[cfg(test)]
124#[path = "bootstrap_tests.rs"]
125mod tests;