use clap::Parser;
use heartbeat_rs::pty;
use std::path::PathBuf;
use std::process;
#[derive(Parser)]
#[command(name = "heartbeat-launch")]
#[command(about = "Launch a command inside a PTY. Designed to give Claude Code interactive mode.")]
#[command(
long_about = "Allocates a PTY via portable-pty (Unix PTY + Windows ConPTY), spawns the \
given command inside it, and forwards stdout to the current process. \
Polls for child exit with a configurable timeout and exits with the \
child's exit code.\n\n\
Everything after `--` is the command and its arguments. The consumer \
is responsible for inbox setup, settings.json, and stop hook wiring."
)]
struct Cli {
#[arg(long, default_value = ".")]
cwd: String,
#[arg(long, default_value = "3600")]
timeout: u64,
#[arg(long)]
exit_signal: Option<PathBuf>,
#[arg(trailing_var_arg = true, required = true)]
cmd: Vec<String>,
}
fn main() {
let cli = Cli::parse();
let cwd = PathBuf::from(&cli.cwd);
if !cwd.exists() {
eprintln!(
"heartbeat-launch: working directory does not exist: {}",
cwd.display()
);
process::exit(1);
}
let cwd = match cwd.canonicalize() {
Ok(p) => p,
Err(e) => {
eprintln!(
"heartbeat-launch: cannot resolve working directory {}: {e}",
cli.cwd
);
process::exit(1);
}
};
if cli.cmd.is_empty() {
eprintln!("heartbeat-launch: no command specified");
process::exit(1);
}
if cli.timeout > 0 {
eprintln!(
"heartbeat-launch: spawning {:?} in {} with {}s timeout",
cli.cmd,
cwd.display(),
cli.timeout
);
} else {
eprintln!(
"heartbeat-launch: spawning {:?} in {} (no timeout)",
cli.cmd,
cwd.display()
);
}
match pty::run(&cli.cmd, &cwd, cli.timeout, cli.exit_signal.as_deref()) {
Ok(result) => {
let code = result.exit_code.min(125) as i32;
process::exit(code);
}
Err(pty::PtyError::Timeout(secs)) => {
eprintln!("heartbeat-launch: timeout after {secs}s — child killed");
process::exit(124); }
Err(e) => {
eprintln!("heartbeat-launch: error: {e}");
process::exit(1);
}
}
}