diff --git a/p/src/cli.rs b/p/src/cli.rs index c439e23..fa84ddc 100644 --- a/p/src/cli.rs +++ b/p/src/cli.rs @@ -26,7 +26,7 @@ pub enum Command { /// List all jobs across all workers Ls, - /// Re-attach to the console of a running job + /// Re-attach to the tmux session of a running job Attach { /// Job ID or unambiguous prefix job_id: String, @@ -66,7 +66,23 @@ pub enum Command { force: bool, }, - /// Register a remote worker + /// Manage registered workers + Worker { + #[command(subcommand)] + command: WorkerCommand, + }, +} + +#[derive(Subcommand)] +pub enum WorkerCommand { + /// List registered workers + Ls { + /// Also probe reachability over SSH (slow) + #[arg(short, long)] + check: bool, + }, + + /// Register a new worker Register { /// SSH connection string: user@host, user@host:port, or an SSH config alias connection: String, @@ -75,16 +91,15 @@ pub enum Command { name: Option, }, - /// List registered workers - Workers { - /// Check reachability of each worker over SSH - #[arg(short, long)] - check: bool, + /// Unregister a worker + Rm { + /// Worker name + name: String, }, /// Set the default worker Default { /// Worker name - worker: String, + name: String, }, } diff --git a/p/src/commands/ls.rs b/p/src/commands/ls.rs index 3a9aa7c..3bf1ebb 100644 --- a/p/src/commands/ls.rs +++ b/p/src/commands/ls.rs @@ -1,5 +1,142 @@ use anyhow::Result; +use std::collections::HashMap; + +use crate::{ + config, db, + job::{Job, JobStatus}, + ssh, +}; pub fn execute() -> Result<()> { - anyhow::bail!("not yet implemented") + let cfg = config::load()?; + let mut jobs = db::list()?; + + if jobs.is_empty() { + println!("No jobs yet. Run 'p -- ' to start one."); + return Ok(()); + } + + // ── Reconcile running jobs ──────────────────────────────────────────────── + // + // Group running jobs by worker, then make one SSH call per worker to + // check all of them at once. Update and persist any that have finished. + + let mut by_worker: HashMap> = HashMap::new(); + for (i, job) in jobs.iter().enumerate() { + if job.status == JobStatus::Running { + by_worker.entry(job.worker.clone()).or_default().push(i); + } + } + + for (worker_name, indices) in &by_worker { + let Some(worker_cfg) = cfg.get_worker(worker_name) else { + // Worker was deregistered; mark jobs unknown. + for &i in indices { + jobs[i].status = JobStatus::Unknown; + } + continue; + }; + + let ids: Vec<&str> = indices.iter().map(|&i| jobs[i].id.as_str()).collect(); + + match ssh::poll_jobs(worker_cfg, &ids) { + Ok(results) => { + let now = chrono::Utc::now().timestamp(); + for &i in indices { + let id = jobs[i].id.clone(); + if let Some(maybe_ec) = results.get(&id) { + if let Some(ec) = maybe_ec { + jobs[i].status = if *ec == 0 { + JobStatus::Done + } else { + JobStatus::Failed + }; + jobs[i].exit_code = Some(*ec); + jobs[i].finished_at = Some(now); + db::save(&jobs[i])?; + } + // None means still running — no update needed. + } + } + } + Err(_) => { + // Worker unreachable; show unknown rather than stale "running". + for &i in indices { + jobs[i].status = JobStatus::Unknown; + } + } + } + } + + // ── Display ─────────────────────────────────────────────────────────────── + + print_table(&jobs); + Ok(()) +} + +fn print_table(jobs: &[Job]) { + // Compute column widths from content, with a minimum equal to the header. + let id_w = 8; // always show 8-char short IDs + let worker_w = jobs + .iter() + .map(|j| j.worker.len()) + .max() + .unwrap_or(0) + .max(6); // "WORKER" + let cwd_w = jobs + .iter() + .map(|j| j.cwd_display().len().min(24)) + .max() + .unwrap_or(0) + .max(3); // "CWD" + let cmd_w = jobs + .iter() + .map(|j| j.command_display(24).len()) + .max() + .unwrap_or(0) + .max(7); // "COMMAND" + let status_w = jobs + .iter() + .map(|j| j.status_display().len()) + .max() + .unwrap_or(0) + .max(6); // "STATUS" + + // Header + println!( + "{: String { + if s.len() > max { + format!("{}…", &s[..max.saturating_sub(1)]) + } else { + s.to_string() + } } diff --git a/p/src/commands/mod.rs b/p/src/commands/mod.rs index c5c0a6c..a25b727 100644 --- a/p/src/commands/mod.rs +++ b/p/src/commands/mod.rs @@ -2,9 +2,7 @@ pub mod attach; pub mod logs; pub mod ls; pub mod pull; -pub mod register; pub mod rm; pub mod run; -pub mod set_default; pub mod stop; -pub mod workers; +pub mod worker; diff --git a/p/src/commands/workers.rs b/p/src/commands/worker/ls.rs similarity index 100% rename from p/src/commands/workers.rs rename to p/src/commands/worker/ls.rs diff --git a/p/src/commands/worker/mod.rs b/p/src/commands/worker/mod.rs new file mode 100644 index 0000000..612c7f3 --- /dev/null +++ b/p/src/commands/worker/mod.rs @@ -0,0 +1,4 @@ +pub mod ls; +pub mod register; +pub mod rm; +pub mod set_default; diff --git a/p/src/commands/register.rs b/p/src/commands/worker/register.rs similarity index 100% rename from p/src/commands/register.rs rename to p/src/commands/worker/register.rs diff --git a/p/src/commands/worker/rm.rs b/p/src/commands/worker/rm.rs new file mode 100644 index 0000000..3d9d568 --- /dev/null +++ b/p/src/commands/worker/rm.rs @@ -0,0 +1,6 @@ +use anyhow::Result; + +pub fn execute(name: &str) -> Result<()> { + let _ = name; + anyhow::bail!("not yet implemented") +} diff --git a/p/src/commands/set_default.rs b/p/src/commands/worker/set_default.rs similarity index 100% rename from p/src/commands/set_default.rs rename to p/src/commands/worker/set_default.rs diff --git a/p/src/main.rs b/p/src/main.rs index 0b74711..7fef693 100644 --- a/p/src/main.rs +++ b/p/src/main.rs @@ -63,10 +63,13 @@ fn main() -> Result<()> { local_dest, } => commands::pull::execute(&job_id, &remote_path, local_dest.as_deref()), cli::Command::Rm { job_id, force } => commands::rm::execute(&job_id, force), - cli::Command::Register { connection, name } => { - commands::register::execute(&connection, name.as_deref()) - } - cli::Command::Workers { check } => commands::workers::execute(check), - cli::Command::Default { worker } => commands::set_default::execute(&worker), + cli::Command::Worker { command } => match command { + cli::WorkerCommand::Ls { check } => commands::worker::ls::execute(check), + cli::WorkerCommand::Register { connection, name } => { + commands::worker::register::execute(&connection, name.as_deref()) + } + cli::WorkerCommand::Rm { name } => commands::worker::rm::execute(&name), + cli::WorkerCommand::Default { name } => commands::worker::set_default::execute(&name), + }, } }