diff --git a/p/src/cli.rs b/p/src/cli.rs index 9172ea8..bf09503 100644 --- a/p/src/cli.rs +++ b/p/src/cli.rs @@ -14,7 +14,10 @@ Run on a specific worker: p -- Skip directory sync: - p -n -- " + p -n -- + +Detach immediately and print the job ID (for scripting / AI agents): + p -d -- " )] pub struct Cli { #[command(subcommand)] diff --git a/p/src/commands/run.rs b/p/src/commands/run.rs index cf584a1..7614c19 100644 --- a/p/src/commands/run.rs +++ b/p/src/commands/run.rs @@ -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, no_sync: bool) -> Result<()> { +pub fn execute( + worker_name: Option<&str>, + cmd: Vec, + 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, 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, 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 || { diff --git a/p/src/main.rs b/p/src/main.rs index 7c3bd9f..a35618c 100644 --- a/p/src/main.rs +++ b/p/src/main.rs @@ -20,19 +20,21 @@ fn main() -> Result<()> { if cmd.is_empty() { eprintln!("error: no command specified after --"); - eprintln!("usage: p [-n] [] -- "); + eprintln!("usage: p [-n] [-d] [] -- "); std::process::exit(1); } let mut no_sync = false; + let mut detach = false; let mut worker: Option = 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] [] -- ", + "error: unknown flag '{}'\nusage: p [-n] [-d] [] -- ", 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. diff --git a/p/src/sync.rs b/p/src/sync.rs index 61c9b60..873bc3b 100644 --- a/p/src/sync.rs +++ b/p/src/sync.rs @@ -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));