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 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 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;