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