use std::io::Write;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use crate::pipe::cli::cli_builder;
use crate::transport::SpawnOptions;
use crate::core::types::CliTool;
#[derive(Debug, Clone, Default)]
pub struct ClaudeOptions {
pub append_system_prompt: Option<String>,
pub resume_session_id: Option<String>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct PipeProcessOptions {
pub extra_args: Vec<String>,
pub claude: ClaudeOptions,
}
pub struct PipeProcess {
child: Child,
stdin: Option<std::process::ChildStdin>,
output_rx: Receiver<String>,
tool: CliTool,
}
impl PipeProcess {
pub fn new(
tool: CliTool,
working_dir: &std::path::Path,
initial_prompt: &str,
) -> Result<Self, std::io::Error> {
Self::new_with_options(
tool,
working_dir,
initial_prompt,
PipeProcessOptions::default(),
)
}
pub fn new_with_options(
tool: CliTool,
working_dir: &std::path::Path,
initial_prompt: &str,
options: PipeProcessOptions,
) -> Result<Self, std::io::Error> {
let spawn_opts = Self::pipe_options_to_spawn_opts(tool, initial_prompt, &options, working_dir);
let mut cmd = Self::build_command_with_options(tool, &spawn_opts);
cmd.current_dir(working_dir);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::null());
let mut child = cmd.spawn()?;
let stdin = child.stdin.take();
if let Some(mut s) = stdin {
if tool == CliTool::ClaudeCode {
s.write_all(initial_prompt.as_bytes())?;
s.flush()?;
}
drop(s); }
let stdin: Option<std::process::ChildStdin> = None;
let stdout = child
.stdout
.take()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "no stdout"))?;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
Self::reader_thread(stdout, tx);
});
Ok(Self {
child,
stdin,
output_rx: rx,
tool,
})
}
fn pipe_options_to_spawn_opts(
_tool: CliTool,
prompt: &str,
options: &PipeProcessOptions,
working_dir: &std::path::Path,
) -> SpawnOptions {
SpawnOptions {
working_dir: working_dir.to_path_buf(),
prompt: prompt.to_string(),
resume_session_id: options.claude.resume_session_id.clone(),
model: options.claude.model.clone(),
append_system_prompt: options.claude.append_system_prompt.clone(),
extra_args: options.extra_args.clone(),
env_vars: Vec::new(),
}
}
fn build_command_with_options(tool: CliTool, opts: &SpawnOptions) -> Command {
let builder = cli_builder(tool);
let inner_cmd = builder.build_command(opts);
if cfg!(windows) {
let shell_str = Self::argv_to_windows_shell_string(&inner_cmd);
let mut cmd = Command::new("cmd");
cmd.args(["/C", &shell_str]);
cmd
} else {
inner_cmd
}
}
fn argv_to_windows_shell_string(cmd: &Command) -> String {
let program = cmd.get_program().to_string_lossy();
let mut parts = vec![Self::win_quote(&program)];
for arg in cmd.get_args() {
let s = arg.to_string_lossy();
parts.push(Self::win_quote(&s));
}
parts.join(" ")
}
fn win_quote(s: &str) -> String {
let needs_quoting = s.is_empty()
|| s.contains(' ')
|| s.contains('"')
|| s.contains('&')
|| s.contains('|')
|| s.contains('<')
|| s.contains('>');
if needs_quoting {
format!("\"{}\"", s.replace('"', "\\\""))
} else {
s.to_string()
}
}
fn reader_thread(stdout: std::process::ChildStdout, tx: Sender<String>) {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(line) => {
if tx.send(format!("{}\n", line)).is_err() {
break;
}
}
Err(_) => break,
}
}
}
pub fn try_recv(&self) -> Option<String> {
self.output_rx.try_recv().ok()
}
pub fn write(&mut self, data: &str) -> Result<(), std::io::Error> {
if let Some(stdin) = &mut self.stdin {
stdin.write_all(data.as_bytes())?;
stdin.flush()?;
}
Ok(())
}
pub fn is_running(&mut self) -> bool {
self.child.try_wait().ok().flatten().is_none()
}
pub fn kill(&mut self) -> Result<(), std::io::Error> {
self.child.kill()
}
pub fn wait(&mut self) -> Result<Option<std::process::ExitStatus>, std::io::Error> {
self.child.try_wait()
}
pub fn tool(&self) -> CliTool {
self.tool
}
}