rho_coding_agent/app/
bootstrap.rs1use 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;