feat: -d/--detach flag for non-interactive use
All checks were successful
CI / Check, test, lint (push) Successful in 27s

This commit is contained in:
2026-05-21 20:57:48 +02:00
parent 113ea01147
commit 0fbd3f9952
4 changed files with 49 additions and 14 deletions

View File

@@ -14,7 +14,10 @@ Run on a specific worker:
p <worker> -- <command>
Skip directory sync:
p -n -- <command>"
p -n -- <command>
Detach immediately and print the job ID (for scripting / AI agents):
p -d -- <command>"
)]
pub struct Cli {
#[command(subcommand)]

View File

@@ -1,6 +1,7 @@
use anyhow::{Context, Result};
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use chrono::Utc;
use std::io::IsTerminal;
use uuid::Uuid;
use crate::{
@@ -9,10 +10,19 @@ use crate::{
ssh, sync,
};
pub fn execute(worker_name: Option<&str>, cmd: Vec<String>, no_sync: bool) -> Result<()> {
pub fn execute(
worker_name: Option<&str>,
cmd: Vec<String>,
no_sync: bool,
detach: bool,
) -> Result<()> {
let cfg = config::load()?;
let worker = cfg.resolve_worker(worker_name)?.clone();
// Auto-detach when stdout is not a terminal (pipe, script, AI agent, etc.).
// Explicit -d also sets detach=true before we get here.
let detach = detach || !std::io::stdout().is_terminal();
let id = Uuid::new_v4().to_string();
let short_id = &id[..8];
let session = format!("p-{}", short_id);
@@ -41,9 +51,13 @@ pub fn execute(worker_name: Option<&str>, cmd: Vec<String>, no_sync: bool) -> Re
// ── 2. Sync directory ─────────────────────────────────────────────────────
if no_sync {
eprintln!("Skipping sync (--no-sync).");
if !detach {
eprintln!("Skipping sync (--no-sync).");
}
} else {
eprintln!("Syncing to {}...", worker.name);
if !detach {
eprintln!("Syncing to {}...", worker.name);
}
sync::push_dir(&worker, &std::env::current_dir()?, &work_dir)
.context("directory sync failed")?;
}
@@ -84,13 +98,23 @@ pub fn execute(worker_name: Option<&str>, cmd: Vec<String>, no_sync: bool) -> Re
ssh::run_capture(&worker, &setup).context("failed to set up job on worker")?;
// ── 4. Attach to the tmux session ─────────────────────────────────────────
// run.sh keeps the session alive after the job finishes (via `read`),
// so there is no race between job completion and our attach call.
// ── 4. Attach or detach ───────────────────────────────────────────────────────
if detach {
// Non-interactive mode: print only the job UUID to stdout and exit.
// stderr is kept clean so the caller can capture just the ID:
// p logs $(p -d -- make)
println!("{}", id);
return Ok(());
}
// Interactive mode: attach to the tmux session.
// run.sh keeps the session alive after the job finishes (via `read`),
// so there is no race between job completion and our attach call.
//
// Ctrl+B D detaches cleanly mid-run; the job keeps going in the background.
// Any other key on the "press any key" screen triggers `tmux detach-client`
// from within run.sh — no [exited] or [detached] flash.
// Ctrl+B D detaches cleanly mid-run; the job keeps going in the background.
// Any other key on the "press any key" screen triggers `tmux detach-client`
// from within run.sh — no [exited] or [detached] flash.
let sid = short_id.to_string();
ctrlc::set_handler(move || {

View File

@@ -20,19 +20,21 @@ fn main() -> Result<()> {
if cmd.is_empty() {
eprintln!("error: no command specified after --");
eprintln!("usage: p [-n] [<worker>] -- <command>");
eprintln!("usage: p [-n] [-d] [<worker>] -- <command>");
std::process::exit(1);
}
let mut no_sync = false;
let mut detach = false;
let mut worker: Option<String> = None;
for arg in pre {
match arg.as_str() {
"-n" | "--no-sync" => no_sync = true,
"-d" | "--detach" => detach = true,
flag if flag.starts_with('-') => {
eprintln!(
"error: unknown flag '{}'\nusage: p [-n] [<worker>] -- <command>",
"error: unknown flag '{}'\nusage: p [-n] [-d] [<worker>] -- <command>",
flag
);
std::process::exit(1);
@@ -47,7 +49,7 @@ fn main() -> Result<()> {
}
}
return commands::run::execute(worker.as_deref(), cmd, no_sync);
return commands::run::execute(worker.as_deref(), cmd, no_sync, detach);
}
// All other subcommands go through clap.

View File

@@ -1,5 +1,6 @@
// Directory sync via rsync over SSH.
use anyhow::{Context, Result};
use std::io::IsTerminal;
use std::path::Path;
use std::process::Command;
@@ -13,7 +14,12 @@ pub fn push_dir(worker: &WorkerConfig, local_dir: &Path, remote_path: &str) -> R
let (user_host, port) = parse_connection(&worker.connection);
let mut cmd = Command::new("rsync");
cmd.args(["-az", "--info=progress2", "--filter=:- .gitignore"]);
// Only show progress when there is a human watching.
if std::io::stdout().is_terminal() {
cmd.args(["-az", "--info=progress2", "--filter=:- .gitignore"]);
} else {
cmd.args(["-az", "--filter=:- .gitignore"]);
}
if let Some(p) = port {
cmd.arg(format!("-e=ssh -p {}", p));