use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
mod commands;
mod config;
mod dag;
mod flow;
mod hooks;
mod registry;
mod targeting;
#[derive(Parser)]
#[command(name = "fleet")]
#[command(about = "Node lifecycle CLI for NixOS fleet management", long_about = None)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Deploy {
targets: Vec<String>,
#[arg(long)]
all: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
show_trace: bool,
},
Build {
targets: Vec<String>,
#[arg(long)]
all: bool,
#[arg(long)]
show_trace: bool,
},
Diff {
targets: Vec<String>,
#[arg(long)]
all: bool,
},
Exec {
targets: Vec<String>,
#[arg(long)]
all: bool,
#[arg(last = true, required = true)]
cmd: Vec<String>,
},
Status {
targets: Vec<String>,
#[arg(long)]
all: bool,
},
Rollback {
targets: Vec<String>,
#[arg(long)]
all: bool,
},
Reboot {
targets: Vec<String>,
#[arg(long)]
all: bool,
#[arg(short = 'y', long)]
yes: bool,
},
Rebuild {
#[arg(long)]
show_trace: bool,
},
Ssh {
node: String,
},
Info {
#[arg(long)]
json: bool,
},
Ping {
targets: Vec<String>,
#[arg(long)]
all: bool,
},
Flow {
#[command(subcommand)]
action: FlowAction,
},
}
#[derive(Subcommand)]
enum FlowAction {
List,
Run {
name: String,
targets: Vec<String>,
#[arg(long)]
all: bool,
#[arg(long)]
dry_run: bool,
},
}
fn load_config() -> config::FleetConfig {
let dir = std::env::current_dir()
.ok()
.and_then(|cwd| commands::rebuild::find_flake_root(&cwd).ok())
.or_else(|| std::env::var("FLEET_FLAKE_DIR").map(PathBuf::from).ok())
.unwrap_or_else(|| PathBuf::from("."));
config::FleetConfig::load(&dir).unwrap_or_default()
}
fn main() -> Result<()> {
let cli = Cli::parse();
let config = load_config();
match cli.command {
Commands::Deploy {
targets,
all,
dry_run,
show_trace,
} => {
let reg = registry::load_registry()?;
let resolved = targeting::resolve(®, &targets, all)?;
for (name, node) in &resolved.nodes {
hooks::run_pre(&config, "deploy", name, node)?;
}
commands::deploy::run(&resolved, dry_run, show_trace)?;
for (name, node) in &resolved.nodes {
hooks::run_post(&config, "deploy", name, node);
}
}
Commands::Build {
targets,
all,
show_trace,
} => {
let reg = registry::load_registry()?;
let resolved = targeting::resolve(®, &targets, all)?;
for (name, node) in &resolved.nodes {
hooks::run_pre(&config, "build", name, node)?;
}
commands::build::run(&resolved, show_trace)?;
for (name, node) in &resolved.nodes {
hooks::run_post(&config, "build", name, node);
}
}
Commands::Diff { targets, all } => {
let reg = registry::load_registry()?;
let resolved = targeting::resolve(®, &targets, all)?;
for (name, node) in &resolved.nodes {
hooks::run_pre(&config, "diff", name, node)?;
}
commands::diff::run(&resolved, &config)?;
for (name, node) in &resolved.nodes {
hooks::run_post(&config, "diff", name, node);
}
}
Commands::Exec { targets, all, cmd } => {
let reg = registry::load_registry()?;
let resolved = targeting::resolve(®, &targets, all)?;
for (name, node) in &resolved.nodes {
hooks::run_pre(&config, "exec", name, node)?;
}
commands::exec::run(&resolved, &cmd, &config)?;
for (name, node) in &resolved.nodes {
hooks::run_post(&config, "exec", name, node);
}
}
Commands::Status { targets, all } => {
let reg = registry::load_registry()?;
let all = all || targets.is_empty();
let resolved = targeting::resolve(®, &targets, all)?;
commands::status::run(&resolved, &config)?;
}
Commands::Rollback { targets, all } => {
let reg = registry::load_registry()?;
let resolved = targeting::resolve(®, &targets, all)?;
for (name, node) in &resolved.nodes {
hooks::run_pre(&config, "rollback", name, node)?;
}
commands::rollback::run(&resolved, &config)?;
for (name, node) in &resolved.nodes {
hooks::run_post(&config, "rollback", name, node);
}
}
Commands::Reboot { targets, all, yes } => {
let reg = registry::load_registry()?;
let resolved = targeting::resolve(®, &targets, all)?;
for (name, node) in &resolved.nodes {
hooks::run_pre(&config, "reboot", name, node)?;
}
commands::reboot::run(&resolved, yes, &config)?;
for (name, node) in &resolved.nodes {
hooks::run_post(&config, "reboot", name, node);
}
}
Commands::Rebuild { show_trace } => {
commands::rebuild::rebuild(show_trace)?;
}
Commands::Ssh { node } => {
let reg = registry::load_registry()?;
let resolved = targeting::resolve(®, &[node], false)?;
commands::ssh::run(&resolved, &config)?;
}
Commands::Info { json } => {
let reg = registry::load_registry()?;
commands::info::run(®, json)?;
}
Commands::Ping { targets, all } => {
let reg = registry::load_registry()?;
let all = all || targets.is_empty();
let resolved = targeting::resolve(®, &targets, all)?;
commands::ping::run(&resolved, &config)?;
}
Commands::Flow { action } => match action {
FlowAction::List => {
commands::flow::list(&config)?;
}
FlowAction::Run {
name,
targets,
all,
dry_run,
} => {
let reg = registry::load_registry()?;
commands::flow::run(&config, ®, &name, &targets, all, dry_run)?;
}
},
}
Ok(())
}