use std::path::{Path, PathBuf};
use thiserror::Error;
use super::launch_spec::{LaunchSpec, LaunchSpecError};
const CLAUDE_PROGRAM: &str = "claude";
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CliLaunchError {
#[error("could not resolve this `tm` binary's own path to read the launch spec with (#8308)")]
NoWrapper,
#[error(transparent)]
Spec(#[from] LaunchSpecError),
#[error("tmux could not type the launch line: {0}")]
Send(std::io::Error),
}
#[derive(Debug, Clone, Copy)]
pub struct CliLaunch<'a> {
pub model: Option<&'a str>,
pub prompt_file: Option<&'a Path>,
pub config_dir: Option<&'a Path>,
pub oauth_token: Option<&'a str>,
pub mcp_env: &'a [(String, String)],
pub scoped_mcp: Option<&'a Path>,
pub alternate_screen: bool,
}
fn base(cwd: &Path, alternate_screen: bool) -> LaunchSpec {
LaunchSpec {
session_id: String::new(),
launch_id: String::new(),
cwd: cwd.to_path_buf(),
program: CLAUDE_PROGRAM.to_owned(),
args: Vec::new(),
env_unset: crate::core::claude_env_scrub::scrubbed_on_spawn()
.map(str::to_owned)
.collect(),
env_set: crate::core::alt_screen::configured_env(alternate_screen),
}
}
pub fn isolated_spec(cwd: &Path, launch: &CliLaunch<'_>) -> LaunchSpec {
let mut spec = base(cwd, launch.alternate_screen);
if let Some(dir) = launch.config_dir {
spec.env_set
.push(("CLAUDE_CONFIG_DIR".to_owned(), dir.display().to_string()));
}
if let Some(token) = launch.oauth_token {
spec.env_set.push((
crate::core::oauth_token::OAUTH_TOKEN_ENV_VAR.to_owned(),
token.to_owned(),
));
}
spec.env_set.extend(launch.mcp_env.iter().cloned());
if let Some(model) = launch.model {
spec.args.extend(["--model".to_owned(), model.to_owned()]);
}
push_prompt(&mut spec.args, launch.prompt_file);
spec.args
.extend(split(crate::core::model_inject::setting_sources_flag(
launch.config_dir,
)));
spec.args
.extend(crate::core::session_mcp_scope::mcp_config_argv(
launch.scoped_mcp,
));
spec.args
.extend(split(crate::core::model_inject::PERMISSION_MODE_FLAG));
spec
}
pub fn inplace_spec(cwd: &Path, prompt_file: &Path, alternate_screen: bool) -> LaunchSpec {
let mut spec = base(cwd, alternate_screen);
push_prompt(&mut spec.args, Some(prompt_file));
spec.args
.extend(split(crate::core::model_inject::PERMISSION_MODE_FLAG));
spec
}
pub fn client_spec(cwd: &Path, prompt_file: Option<&Path>, alternate_screen: bool) -> LaunchSpec {
let mut spec = base(cwd, alternate_screen);
push_prompt(&mut spec.args, prompt_file);
spec
}
fn push_prompt(args: &mut Vec<String>, prompt_file: Option<&Path>) {
if let Some(prompt) = prompt_file {
args.push("--append-system-prompt-file".to_owned());
args.push(prompt.display().to_string());
}
}
fn split(flag: &str) -> impl Iterator<Item = String> + '_ {
flag.split_whitespace().map(str::to_owned)
}
pub fn spec_dir(home: Option<&Path>) -> Option<PathBuf> {
match home {
Some(home) => Some(LaunchSpec::root_at(
&crate::core::paths::FrameworkPaths::under(home).crate_config_root(),
)),
None => LaunchSpec::root(),
}
}
pub fn pane_line(wrapper_bin: &str, spec_path: &Path) -> String {
let quote = crate::core::spawn_disclaim::pane::shell_single_quote;
format!(
"{} {} --launch-spec {}",
quote(wrapper_bin),
crate::core::spawn_disclaim::PANE_DISCLAIM_SUBCOMMAND,
quote(&spec_path.display().to_string()),
)
}
pub fn send_spec_launch(
session: &str,
spec: &LaunchSpec,
spec_dir: &Path,
) -> Result<(), CliLaunchError> {
let wrapper =
crate::core::spawn_disclaim::launch_wrapper_bin().ok_or(CliLaunchError::NoWrapper)?;
send_spec_launch_with(None, &wrapper, session, spec, spec_dir)
}
pub(crate) fn send_spec_launch_with(
tmux_bin: Option<&str>,
wrapper: &str,
session: &str,
spec: &LaunchSpec,
spec_dir: &Path,
) -> Result<(), CliLaunchError> {
let path = spec.write_in(spec_dir)?;
let result = LaunchSpec::verify(&path)
.map_err(CliLaunchError::from)
.and_then(|_| {
crate::core::tmux::send_launch_line(tmux_bin, session, &pane_line(wrapper, &path))
.map_err(CliLaunchError::Send)
});
if result.is_err() {
let _ = std::fs::remove_file(&path);
}
result
}
#[cfg(test)]
#[path = "cli_launch_tests.rs"]
mod tests;