use anyhow::{Context, Result};
use indicatif::{ProgressBar, ProgressStyle};
use octomind::agent::{deps, inputs, registry};
use octomind::config::{loading::merge_agent_toml, Config};
use std::time::Duration;
pub async fn resolve_config_and_role(
tag: Option<&str>,
config: &Config,
status_cb: Option<&dyn Fn(&str)>,
) -> Result<(Config, String)> {
let tag = tag.unwrap_or(config.default.as_str());
if tag.contains(':') {
if let Some(cb) = status_cb {
cb(&format!("Fetching agent: {tag}"));
}
let (raw_toml, tap_root) = registry::fetch_manifest(tag, &config.registry)
.await
.context(format!("Failed to fetch agent manifest for '{tag}'"))?;
let resolved_toml =
registry::resolve_capabilities(&raw_toml, &tap_root, &config.capabilities)
.context("Failed to resolve agent capabilities")?;
let resolved_toml = inputs::resolve_inputs(&resolved_toml).await?;
let resolved_toml = inputs::resolve_env_vars(&resolved_toml).await?;
deps::resolve_deps(&resolved_toml, &tap_root, status_cb).await?;
let tagged_toml = inject_role_name(&resolved_toml, tag)
.context("Failed to inject role name into agent manifest")?;
let merged = merge_agent_toml(config, &tagged_toml)
.context("Failed to merge agent manifest into config")?;
let base_names: std::collections::HashSet<&str> =
config.roles.iter().map(|r| r.name.as_str()).collect();
let role = merged
.roles
.iter()
.find(|r| !base_names.contains(r.name.as_str()))
.map(|r| r.name.clone())
.context(format!(
"Agent manifest for '{tag}' must define at least one new [[roles]] entry"
))?;
Ok((merged, role))
} else {
Ok((config.clone(), tag.to_string()))
}
}
pub async fn startup(
tag: Option<&str>,
config: &Config,
is_interactive: bool,
) -> Result<(Config, String)> {
if is_interactive {
let spinner = make_spinner();
let spinner_ref = &spinner;
let status_cb = |msg: &str| spinner_ref.set_message(msg.to_string());
let resolve_result = resolve_config_and_role(tag, config, Some(&status_cb)).await;
let (run_config, role) = match resolve_result {
Ok(v) => v,
Err(e) => {
spinner.finish_and_clear();
print!("\x1B[2K\r");
std::io::Write::flush(&mut std::io::stdout()).ok();
return Err(e);
}
};
if let Err(e) = mcp_init_with_spinner(&role, &run_config, &spinner).await {
spinner.finish_and_clear();
print!("\x1B[2K\r");
std::io::Write::flush(&mut std::io::stdout()).ok();
return Err(e);
}
spinner.finish_and_clear();
print!("\x1B[2K\r");
std::io::Write::flush(&mut std::io::stdout()).ok();
Ok((run_config, role))
} else {
let (run_config, role) = resolve_config_and_role(tag, config, None).await?;
octomind::mcp::initialize_mcp_for_role(&role, &run_config).await?;
Ok((run_config, role))
}
}
pub async fn startup_mcp_only(role: &str, config: &Config, is_interactive: bool) -> Result<()> {
if is_interactive {
let spinner = make_spinner();
let result = mcp_init_with_spinner(role, config, &spinner).await;
spinner.finish_and_clear();
print!("\x1B[2K\r");
std::io::Write::flush(&mut std::io::stdout()).ok();
result
} else {
octomind::mcp::initialize_mcp_for_role(role, config).await
}
}
fn make_spinner() -> ProgressBar {
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template(" {spinner:.cyan} {msg:.cyan}")
.unwrap()
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧"),
);
spinner.enable_steady_tick(Duration::from_millis(80));
spinner
}
async fn mcp_init_with_spinner(role: &str, config: &Config, spinner: &ProgressBar) -> Result<()> {
use octomind::mcp::McpInitProgress;
use std::sync::{Arc, Mutex};
let pending: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let total = Arc::new(Mutex::new(0usize));
let cb = |progress: McpInitProgress| match &progress {
McpInitProgress::Starting { servers } => {
*total.lock().unwrap() = servers.len();
if servers.is_empty() {
spinner.set_message("Starting MCP...".to_string());
} else {
*pending.lock().unwrap() = servers.clone();
spinner.set_message(format!(
"Starting MCP: {} [0/{}]",
servers.join(", "),
servers.len()
));
}
}
McpInitProgress::Completed { server, .. } => {
let mut pending_guard = pending.lock().unwrap();
pending_guard.retain(|s| s != server);
let done = *total.lock().unwrap() - pending_guard.len();
let total_count = *total.lock().unwrap();
if pending_guard.is_empty() {
spinner.set_message(format!("Starting MCP: done [{}/{}]", done, total_count));
} else {
spinner.set_message(format!(
"Starting MCP: {} [{}/{}]",
pending_guard.join(", "),
done,
total_count
));
}
}
};
octomind::mcp::initialize_mcp_for_role_with_callback(role, config, Some(&cb)).await
}
fn inject_role_name(toml_str: &str, tag: &str) -> Result<String> {
let role_name = tag;
let mut value: toml::Value =
toml::from_str(toml_str).context("Failed to parse agent manifest TOML")?;
if let Some(toml::Value::Array(roles)) = value.get_mut("roles") {
if let Some(toml::Value::Table(table)) = roles.first_mut() {
table.insert(
"name".to_string(),
toml::Value::String(role_name.to_string()),
);
}
}
toml::to_string(&value).context("Failed to re-serialize agent manifest TOML")
}