feat: -d/--detach flag for non-interactive use
All checks were successful
CI / Check, test, lint (push) Successful in 27s
All checks were successful
CI / Check, test, lint (push) Successful in 27s
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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 || {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user