use crate::error::{CliError, Result};
use std::path::Path;
pub(crate) fn run(
experiment_dir: Option<&Path>,
refresh_ms: u64,
compact: bool,
json: bool,
format: &str,
) -> Result<()> {
let output_format = if json { "json" } else { format };
let dir = match experiment_dir {
Some(d) => d.to_path_buf(),
None => {
return discover_active_runs(output_format);
}
};
if !dir.is_dir() {
return Err(CliError::ValidationFailed(format!(
"Experiment directory does not exist: {}",
dir.display()
)));
}
match output_format {
"json" => run_headless(&dir, refresh_ms, entrenar::monitor::tui::OutputFormat::Json),
"text" => run_headless(&dir, refresh_ms, entrenar::monitor::tui::OutputFormat::Text),
_ => run_tui(&dir, refresh_ms, compact),
}
}
fn run_tui(experiment_dir: &Path, refresh_ms: u64, compact: bool) -> Result<()> {
let config = entrenar::monitor::tui::TuiMonitorConfig {
refresh_ms,
compact,
exit_on_complete: true,
..Default::default()
};
let mut monitor = entrenar::monitor::tui::TuiMonitor::new(experiment_dir, config);
monitor
.run()
.map_err(|e| CliError::ValidationFailed(format!("Monitor error: {e}")))
}
fn run_headless(
experiment_dir: &Path,
refresh_ms: u64,
format: entrenar::monitor::tui::OutputFormat,
) -> Result<()> {
let monitor = entrenar::monitor::tui::HeadlessMonitor::new(format, refresh_ms);
monitor
.run(experiment_dir)
.map_err(|e| CliError::ValidationFailed(format!("Monitor error: {e}")))
}
fn discover_active_runs(format: &str) -> Result<()> {
let db_path = dirs::home_dir()
.map(|h| h.join(".entrenar").join("experiments.db"))
.ok_or_else(|| CliError::ValidationFailed("Could not determine home directory".into()))?;
if !db_path.exists() {
return Err(CliError::ValidationFailed(
"No global experiment registry found.\n\
Hint: Start a training run first, or specify a directory:\n\
\n\
apr monitor <DIR>\n\
apr train apply --config <yaml>"
.into(),
));
}
let store = entrenar::storage::SqliteBackend::open(db_path.to_string_lossy().as_ref())
.map_err(|e| {
CliError::ValidationFailed(format!("Failed to open experiment database: {e}"))
})?;
let active_runs = scan_active_runs(&store)?;
if active_runs.is_empty() {
print_no_active_runs(format);
return Ok(());
}
print_active_runs(&active_runs, format);
Ok(())
}
fn scan_active_runs(
store: &entrenar::storage::SqliteBackend,
) -> Result<Vec<(String, String, entrenar::storage::sqlite::Run)>> {
let experiments = store
.list_experiments()
.map_err(|e| CliError::ValidationFailed(format!("Failed to list experiments: {e}")))?;
let mut active = Vec::new();
for exp in &experiments {
let runs = match store.list_runs(&exp.id) {
Ok(r) => r,
Err(_) => continue,
};
for run in runs {
let dir = run_output_dir(store, &run.id);
if let Some(dir) = dir {
active.push((exp.name.clone(), dir, run));
}
}
}
Ok(active)
}
fn run_output_dir(store: &entrenar::storage::SqliteBackend, run_id: &str) -> Option<String> {
let params = store.get_params(run_id).ok()?;
let dir = match params.get("output_dir")? {
entrenar::storage::ParameterValue::String(d) => d.clone(),
_ => return None,
};
let state_path = std::path::PathBuf::from(&dir).join("training_state.json");
state_path.exists().then_some(dir)
}
fn print_no_active_runs(format: &str) {
if format == "json" {
println!("[]");
} else {
println!("No active training runs found.");
println!();
println!("Hint: Start a training run, then attach:");
println!(" apr train apply --config <yaml>");
println!(" apr monitor <checkpoint-dir>");
}
}
fn print_active_runs(
active_runs: &[(String, String, entrenar::storage::sqlite::Run)],
format: &str,
) {
if format == "json" {
let entries: Vec<serde_json::Value> = active_runs
.iter()
.map(|(name, dir, run)| {
serde_json::json!({
"experiment": name,
"directory": dir,
"run_id": run.id,
"status": format!("{:?}", run.status),
"start_time": run.start_time.to_rfc3339(),
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&entries).unwrap_or_else(|_| "[]".to_string())
);
} else {
println!("Active training runs:");
println!();
for (i, (name, dir, run)) in active_runs.iter().enumerate() {
println!(" [{}] {} — {}", i + 1, name, dir);
println!(
" Run: {} | Started: {}",
run.id,
run.start_time.format("%H:%M:%S")
);
}
println!();
println!("Attach to a run:");
if let Some((_, dir, _)) = active_runs.first() {
println!(" apr monitor {dir}");
}
}
}