use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use tokio::process::Command;
use super::auth::{ClaudeAuthError, CLAUDE_PROGRAM};
use super::windows_shim_args::{bat_command_line, validate_powershell_args, WindowsShimArgError};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ClaudeInvocationKind {
Direct,
CmdScript,
PowerShellScript,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ClaudeExecutable {
program: PathBuf,
kind: ClaudeInvocationKind,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub(crate) enum ClaudeExecutableError {
#[error(transparent)]
WindowsShim(#[from] WindowsShimArgError),
}
impl ClaudeExecutable {
pub(crate) fn from_path(path: impl Into<PathBuf>) -> Self {
let program = path.into();
let kind = classify_program(&program);
Self { program, kind }
}
pub(crate) fn display(&self) -> String {
crate::paths::display(&self.program)
}
#[cfg(test)]
pub(crate) fn program(&self) -> &Path {
&self.program
}
#[cfg(test)]
pub(crate) fn kind(&self) -> ClaudeInvocationKind {
self.kind
}
pub(crate) fn plan<I, S>(&self, args: I) -> Result<ClaudeArgv, ClaudeExecutableError>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let args = collect_args(args);
match self.kind {
ClaudeInvocationKind::Direct => Ok(ClaudeArgv {
program: self.program.clone(),
args,
}),
ClaudeInvocationKind::CmdScript => {
let _ = bat_command_line(&self.program, &args)?;
Ok(ClaudeArgv {
program: self.program.clone(),
args,
})
}
ClaudeInvocationKind::PowerShellScript => {
validate_powershell_args(&args)?;
let mut argv = vec![
OsString::from("-NoProfile"),
OsString::from("-NonInteractive"),
OsString::from("-ExecutionPolicy"),
OsString::from("Bypass"),
OsString::from("-File"),
self.program.as_os_str().to_os_string(),
];
argv.extend(args);
Ok(ClaudeArgv {
program: PathBuf::from("powershell.exe"),
args: argv,
})
}
}
}
pub(crate) fn try_command<I, S>(&self, args: I) -> Result<Command, ClaudeExecutableError>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Ok(self.plan(args)?.command())
}
}
pub(crate) fn resolve() -> Result<ClaudeExecutable, ClaudeAuthError> {
resolve_named(CLAUDE_PROGRAM)
}
pub(crate) fn resolve_named(program: &str) -> Result<ClaudeExecutable, ClaudeAuthError> {
if program.contains('/') || program.contains('\\') {
let path = PathBuf::from(program);
if path.is_file() {
return Ok(ClaudeExecutable::from_path(path));
}
return Err(ClaudeAuthError::BinaryMissing);
}
if let Some(path) = crate::executable::find_on_path(program) {
return Ok(ClaudeExecutable::from_path(path));
}
#[cfg(windows)]
{
for candidate in [format!("{program}.cmd"), format!("{program}.ps1")] {
if let Some(path) = crate::executable::find_on_path(&candidate) {
return Ok(ClaudeExecutable::from_path(path));
}
}
}
Err(ClaudeAuthError::BinaryMissing)
}
fn classify_program(path: &Path) -> ClaudeInvocationKind {
let ext = path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_ascii_lowercase();
match ext.as_str() {
"cmd" | "bat" => ClaudeInvocationKind::CmdScript,
"ps1" => ClaudeInvocationKind::PowerShellScript,
_ => ClaudeInvocationKind::Direct,
}
}
fn collect_args<I, S>(args: I) -> Vec<OsString>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
args.into_iter()
.map(|arg| arg.as_ref().to_os_string())
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ClaudeArgv {
pub(crate) program: PathBuf,
pub(crate) args: Vec<OsString>,
}
impl ClaudeArgv {
fn command(&self) -> Command {
let mut command = Command::new(&self.program);
command.args(&self.args);
command
}
}
#[cfg(test)]
#[path = "executable_tests.rs"]
mod tests;