From b27e92b6f74188a840d997006017c1e0f82b1207 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 20 May 2026 17:39:46 +0200 Subject: [PATCH] feat: ls only lists running jobs, prune removes all running jobs --- SPEC.md | 14 ++++++-- p/src/cli.rs | 18 ++++++++-- p/src/commands/ls.rs | 13 +++++-- p/src/commands/mod.rs | 1 + p/src/commands/prune.rs | 79 +++++++++++++++++++++++++++++++++++++++++ p/src/main.rs | 3 +- 6 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 p/src/commands/prune.rs diff --git a/SPEC.md b/SPEC.md index 9171141..a7a747f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -66,8 +66,10 @@ Useful for commands that need no local files (e.g. `p -n -- htop`). ``` p ls ``` -List jobs across all workers. Shows: ID (short), worker, original CWD, -command, status, duration. Style inspired by `docker ps` / `lxc list`. +List **running jobs** across all workers. Pass `-a` / `--all` to also show +completed jobs (done, failed, stopped). +Shows: ID (short), worker, original CWD, command, status, duration. +Style inspired by `docker ps` / `lxc list`. ``` p attach @@ -100,6 +102,14 @@ p rm Remove a job record and its remote work directory. Refuses to remove a running job without `--force`. +``` +p prune +``` +Remove all finished job records (status: done, failed, stopped) and their +remote work directories. Jobs with status `running` or `unknown` are left +untouched. Pass `--force` to also include `unknown` jobs. +Pass `--dry-run` to preview what would be removed without deleting anything. + ### Worker management ``` diff --git a/p/src/cli.rs b/p/src/cli.rs index fa84ddc..9172ea8 100644 --- a/p/src/cli.rs +++ b/p/src/cli.rs @@ -23,8 +23,12 @@ pub struct Cli { #[derive(Subcommand)] pub enum Command { - /// List all jobs across all workers - Ls, + /// List running jobs (pass --all to include finished jobs) + Ls { + /// Show all jobs, not just running ones + #[arg(short, long)] + all: bool, + }, /// Re-attach to the tmux session of a running job Attach { @@ -66,6 +70,16 @@ pub enum Command { force: bool, }, + /// Remove all finished job records and their remote work directories + Prune { + /// Also remove jobs with unknown status (worker was unreachable) + #[arg(short, long)] + force: bool, + /// Preview what would be removed without deleting anything + #[arg(short = 'n', long)] + dry_run: bool, + }, + /// Manage registered workers Worker { #[command(subcommand)] diff --git a/p/src/commands/ls.rs b/p/src/commands/ls.rs index a851287..d2f99f0 100644 --- a/p/src/commands/ls.rs +++ b/p/src/commands/ls.rs @@ -7,12 +7,21 @@ use crate::{ ssh, }; -pub fn execute() -> Result<()> { +pub fn execute(all: bool) -> Result<()> { let cfg = config::load()?; let mut jobs = db::list()?; + // By default only show running jobs; --all includes finished ones. + if !all { + jobs.retain(|j| j.status == JobStatus::Running || j.status == JobStatus::Unknown); + } + if jobs.is_empty() { - println!("No jobs yet. Run 'p -- ' to start one."); + if all { + println!("No jobs yet. Run 'p -- ' to start one."); + } else { + println!("No running jobs. Use 'p ls --all' to see finished jobs."); + } return Ok(()); } diff --git a/p/src/commands/mod.rs b/p/src/commands/mod.rs index a25b727..09f498a 100644 --- a/p/src/commands/mod.rs +++ b/p/src/commands/mod.rs @@ -1,6 +1,7 @@ pub mod attach; pub mod logs; pub mod ls; +pub mod prune; pub mod pull; pub mod rm; pub mod run; diff --git a/p/src/commands/prune.rs b/p/src/commands/prune.rs new file mode 100644 index 0000000..11fbc3d --- /dev/null +++ b/p/src/commands/prune.rs @@ -0,0 +1,79 @@ +use anyhow::Result; + +use crate::{config, db, job::JobStatus, ssh}; + +pub fn execute(force: bool, dry_run: bool) -> Result<()> { + let cfg = config::load()?; + let jobs = db::list()?; + + let to_prune: Vec<_> = jobs + .into_iter() + .filter(|j| match j.status { + JobStatus::Done | JobStatus::Failed | JobStatus::Stopped => true, + JobStatus::Unknown => force, + JobStatus::Running => false, + }) + .collect(); + + if to_prune.is_empty() { + println!("Nothing to prune."); + if !force { + println!("(use --force to also include jobs with unknown status)"); + } + return Ok(()); + } + + if dry_run { + println!("Would remove {} job(s):", to_prune.len()); + } else { + println!("Removing {} job(s)...", to_prune.len()); + } + + let mut removed = 0; + let mut errors = 0; + + for job in &to_prune { + let remote_rm = format!( + "rm -rf {}/.p/jobs/{id} {}/.p/workdirs/{id}", + // use the worker's home via SSH expansion + "~", + "~", + id = job.id + ); + + println!( + " {} {} ({})", + job.short_id(), + job.status_display(), + job.command_display(40) + ); + + if dry_run { + continue; + } + + // Best-effort remote cleanup. + if let Some(worker) = cfg.get_worker(&job.worker) { + if let Err(e) = ssh::run_capture(worker, &remote_rm) { + eprintln!(" warning: could not remove remote files: {}", e); + errors += 1; + } + } + + db::delete(&job.id)?; + removed += 1; + } + + if !dry_run { + if errors > 0 { + eprintln!( + "Removed {} local record(s); {} remote cleanup(s) failed.", + removed, errors + ); + } else { + println!("Done. Removed {} job(s).", removed); + } + } + + Ok(()) +} diff --git a/p/src/main.rs b/p/src/main.rs index 7fef693..7c3bd9f 100644 --- a/p/src/main.rs +++ b/p/src/main.rs @@ -53,7 +53,7 @@ fn main() -> Result<()> { // All other subcommands go through clap. let cli = cli::Cli::parse(); match cli.command { - cli::Command::Ls => commands::ls::execute(), + cli::Command::Ls { all } => commands::ls::execute(all), cli::Command::Attach { job_id } => commands::attach::execute(&job_id), cli::Command::Logs { job_id, follow } => commands::logs::execute(&job_id, follow), cli::Command::Stop { job_id } => commands::stop::execute(&job_id), @@ -63,6 +63,7 @@ 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::Prune { force, dry_run } => commands::prune::execute(force, dry_run), cli::Command::Worker { command } => match command { cli::WorkerCommand::Ls { check } => commands::worker::ls::execute(check), cli::WorkerCommand::Register { connection, name } => {