use std::path::Path;
use std::process::Command;
use std::sync::Arc;
use tracing::debug;
use crate::session_manager::ManagedTmuxDriver;
use super::RuntimeAdapter;
use super::RuntimeError;
fn spawn_command() -> String {
format!(
"env -u ANTHROPIC_API_KEY claude {} {}",
crate::core::model_inject::SETTING_SOURCES_FLAG,
crate::core::model_inject::PERMISSION_MODE_FLAG,
)
}
pub struct ClaudeCodeAdapter {
tmux: Arc<dyn ManagedTmuxDriver + Send + Sync>,
}
impl ClaudeCodeAdapter {
pub fn new(tmux: Arc<dyn ManagedTmuxDriver + Send + Sync>) -> Self {
Self { tmux }
}
fn claude_available() -> bool {
Command::new("which")
.arg("claude")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}
impl RuntimeAdapter for ClaudeCodeAdapter {
fn spawn(&self, tmux_name: &str, cwd: &Path, task: &str) -> Result<(), RuntimeError> {
if !Self::claude_available() {
return Err(RuntimeError::BinaryNotFound(
"claude binary not found on PATH — install Claude Code first".into(),
));
}
debug!(
session = %tmux_name,
cwd = %cwd.display(),
task = %task,
"spawning claude-code in tmux pane"
);
self.tmux
.send_line(tmux_name, &spawn_command())
.map_err(|e| RuntimeError::TmuxUnavailable(e.to_string()))
}
fn identify(&self) -> &str {
"claude-code"
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::FakeTmux;
use super::*;
#[test]
fn claude_code_adapter_identifies() {
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake);
assert_eq!(adapter.identify(), "claude-code");
}
#[test]
fn spawn_command_contains_env_scrub() {
let cmd = spawn_command();
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY"),
"spawn command must contain env scrub: {cmd}"
);
assert!(
cmd.contains(" claude "),
"spawn command must invoke claude: {cmd}"
);
}
#[test]
fn spawn_command_contains_isolation_flags() {
let cmd = spawn_command();
assert!(
cmd.contains("--setting-sources project,local"),
"spawn command must isolate settings: {cmd}"
);
assert!(
cmd.contains("--permission-mode acceptEdits"),
"spawn command must set unattended permission mode: {cmd}"
);
}
#[test]
fn claude_code_adapter_binary_check_returns_bool() {
let _ = ClaudeCodeAdapter::claude_available();
}
#[test]
fn spawn_sends_env_scrub_when_binary_available() {
if !ClaudeCodeAdapter::claude_available() {
return;
}
let fake = FakeTmux::new();
let adapter = ClaudeCodeAdapter::new(fake.clone());
adapter
.spawn("tmpm-test", Path::new("/tmp"), "some task")
.expect("spawn");
let sends = fake.sends.lock().unwrap();
assert_eq!(sends.len(), 1);
assert_eq!(sends[0].0, "tmpm-test");
assert_eq!(sends[0].1, spawn_command());
}
}