use std::path::{Path, PathBuf};
use anyhow::Result;
use clap::Args;
use colored::Colorize;
use dialoguer::Confirm;
use super::init::{self, compose::ComposeRunner, InitArgs};
use super::update::{persist_image_tag, refresh_compose, resolve_image_tag};
#[derive(Args)]
pub struct UpArgs {
#[arg(long, default_value = "~/.aegis")]
pub dir: String,
#[arg(long, default_value = "127.0.0.1")]
pub host: String,
#[arg(long, default_value = "8088")]
pub port: u16,
#[arg(long)]
pub yes: bool,
#[arg(long)]
pub tag: Option<String>,
#[arg(long)]
pub profile: Option<String>,
}
pub async fn run(args: UpArgs) -> Result<()> {
let dir = expand_tilde(Path::new(&args.dir));
let config_file_path = dir.join("aegis-config.yaml");
if !dir.join("docker-compose.yml").exists() {
println!();
println!(
" {} No AEGIS stack found in {} — running {} first...",
"ℹ".cyan(),
dir.display(),
"aegis init".bold()
);
println!();
let advanced_override = if args.yes {
Some(false)
} else {
Some(
Confirm::new()
.with_prompt("Run advanced configuration walkthrough?")
.default(false)
.interact()?,
)
};
let initial_tag = resolve_image_tag(&config_file_path, args.tag.as_deref());
init::run(InitArgs {
yes: args.yes,
manual: false,
dir: args.dir.clone(),
host: args.host.clone(),
port: args.port,
tag: initial_tag,
advanced_override,
})
.await?;
if args.profile.is_some() {
println!(
" {} `--profile` is ignored during first-time setup (`aegis init` controls initial profile selection).",
"ℹ".cyan()
);
}
return Ok(());
}
println!();
println!("{}", "Starting AEGIS stack...".bold());
let image_tag = resolve_image_tag(&config_file_path, args.tag.as_deref());
refresh_compose(&dir, &image_tag, false).await?;
persist_image_tag(&config_file_path, &image_tag, false)?;
let runner = ComposeRunner::new(dir.clone());
runner.up_with_profile(args.profile.as_deref()).await?;
println!();
println!("{}", " ✓ AEGIS is up!".green().bold());
println!("\n {} http://{}:{}", "API".bold(), args.host, args.port);
println!(
"\n Run {} to view real-time logs.",
format!(
"docker compose -f {}/docker-compose.yml logs --follow",
dir.display()
)
.cyan()
);
Ok(())
}
fn expand_tilde(path: &Path) -> PathBuf {
let s = path.to_string_lossy();
if s == "~" || s.starts_with("~/") {
if let Some(home) = dirs_next::home_dir() {
if s == "~" {
return home;
} else {
let rest = &s[2..];
return home.join(rest);
}
}
}
path.to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
use aegis_orchestrator_core::domain::node_config::NodeConfigManifest;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_dir() -> PathBuf {
let mut dir = std::env::temp_dir();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before unix epoch")
.as_nanos();
dir.push(format!("aegis-up-test-{}-{nanos}", std::process::id()));
fs::create_dir_all(&dir).expect("create temp dir");
dir
}
#[test]
fn existing_stack_prefers_persisted_image_tag_when_override_missing() {
let dir = temp_dir();
let config_path = dir.join("aegis-config.yaml");
let mut manifest = NodeConfigManifest::default();
manifest.spec.image_tag = Some("latest".to_string());
manifest
.to_yaml_file(&config_path)
.expect("write config with latest tag");
let resolved = resolve_image_tag(&config_path, None);
assert_eq!(resolved, "latest");
let _ = fs::remove_dir_all(dir);
}
}