use std::path::Path;
use std::sync::Arc;
use tracing::debug;
use crate::session_manager::ManagedTmuxDriver;
use super::RuntimeAdapter;
use super::RuntimeError;
fn spawn_command(claude_bin: &str) -> String {
format!(
"env -u ANTHROPIC_API_KEY {} {} {}",
claude_bin,
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 resolve_claude() -> Option<String> {
trusty_common::bin_resolve::resolve_binary("claude")
.and_then(|p| p.to_str().map(str::to_owned))
}
}
impl RuntimeAdapter for ClaudeCodeAdapter {
fn spawn(&self, tmux_name: &str, cwd: &Path, task: &str) -> Result<(), RuntimeError> {
let claude_bin = Self::resolve_claude().ok_or_else(|| {
RuntimeError::BinaryNotFound(
"claude binary not found on PATH or in well-known dirs \
(e.g. ~/.local/bin) — install Claude Code first"
.into(),
)
})?;
debug!(
session = %tmux_name,
cwd = %cwd.display(),
task = %task,
claude = %claude_bin,
"spawning claude-code in tmux pane"
);
self.tmux
.send_line(tmux_name, &spawn_command(&claude_bin))
.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("claude");
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("claude");
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 spawn_command_uses_resolved_binary() {
let cmd = spawn_command("/Users/me/.local/bin/claude");
assert!(
cmd.contains("env -u ANTHROPIC_API_KEY /Users/me/.local/bin/claude "),
"spawn command must invoke the resolved absolute claude path: {cmd}"
);
}
#[test]
fn claude_code_adapter_binary_check_returns_option() {
if let Some(p) = ClaudeCodeAdapter::resolve_claude() {
assert!(!p.is_empty(), "resolved claude path must be non-empty");
}
}
#[test]
fn spawn_sends_env_scrub_when_binary_available() {
let Some(claude_bin) = ClaudeCodeAdapter::resolve_claude() else {
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(&claude_bin));
}
}