feat: ls only lists running jobs, prune removes all running jobs
All checks were successful
CI / Check, test, lint (push) Successful in 31s

This commit is contained in:
2026-05-20 17:39:46 +02:00
parent e9ab6c6a0f
commit b27e92b6f7
6 changed files with 121 additions and 7 deletions

14
SPEC.md
View File

@@ -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 <job-id>
@@ -100,6 +102,14 @@ p rm <job-id>
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
```

View File

@@ -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)]

View File

@@ -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 -- <command>' to start one.");
if all {
println!("No jobs yet. Run 'p -- <command>' to start one.");
} else {
println!("No running jobs. Use 'p ls --all' to see finished jobs.");
}
return Ok(());
}

View File

@@ -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;

79
p/src/commands/prune.rs Normal file
View File

@@ -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(())
}

View File

@@ -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 } => {