mod claude_code;
mod tcode;
#[cfg(test)]
pub(crate) mod test_helpers;
pub use claude_code::ClaudeCodeAdapter;
pub use tcode::TcodeAdapter;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::session_manager::ManagedTmuxDriver;
#[derive(Debug, Error)]
pub enum RuntimeError {
#[error("spawn failed: {0}")]
Spawn(String),
#[error("tmux unavailable: {0}")]
TmuxUnavailable(String),
#[error("binary not found: {0}")]
BinaryNotFound(String),
}
pub trait RuntimeAdapter: Send + Sync {
fn spawn(&self, tmux_name: &str, cwd: &Path, task: &str) -> Result<(), RuntimeError>;
fn identify(&self) -> &str;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum RuntimeKind {
#[value(name = "claude-code")]
ClaudeCode,
#[value(name = "tcode")]
Tcode,
}
impl Default for RuntimeKind {
fn default() -> Self {
RuntimeKind::ClaudeCode
}
}
impl RuntimeKind {
pub fn as_str(&self) -> &'static str {
match self {
RuntimeKind::ClaudeCode => "claude-code",
RuntimeKind::Tcode => "tcode",
}
}
}
impl FromStr for RuntimeKind {
type Err = RuntimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"claude-code" | "claude_code" | "claude" => Ok(RuntimeKind::ClaudeCode),
"tcode" | "trusty-code" => Ok(RuntimeKind::Tcode),
other => Err(RuntimeError::Spawn(format!(
"unknown runtime '{other}'; supported values: claude-code, tcode"
))),
}
}
}
pub fn build_adapter(
kind: RuntimeKind,
tmux: Arc<dyn ManagedTmuxDriver + Send + Sync>,
) -> Box<dyn RuntimeAdapter> {
match kind {
RuntimeKind::ClaudeCode => Box::new(ClaudeCodeAdapter::new(tmux)),
RuntimeKind::Tcode => Box::new(TcodeAdapter::new(tmux)),
}
}
#[cfg(test)]
mod tests {
use super::test_helpers::FakeTmux;
use super::*;
#[test]
fn runtime_kind_default_is_claude_code() {
assert_eq!(RuntimeKind::default(), RuntimeKind::ClaudeCode);
}
#[test]
fn runtime_kind_as_str_matches_identify() {
assert_eq!(RuntimeKind::ClaudeCode.as_str(), "claude-code");
assert_eq!(RuntimeKind::Tcode.as_str(), "tcode");
}
#[test]
fn runtime_kind_from_str_accepts_known() {
assert_eq!(
"claude-code".parse::<RuntimeKind>().unwrap(),
RuntimeKind::ClaudeCode
);
assert_eq!(
"CLAUDE".parse::<RuntimeKind>().unwrap(),
RuntimeKind::ClaudeCode
);
assert_eq!("tcode".parse::<RuntimeKind>().unwrap(), RuntimeKind::Tcode);
assert_eq!(
"trusty-code".parse::<RuntimeKind>().unwrap(),
RuntimeKind::Tcode
);
}
#[test]
fn runtime_kind_from_str_rejects_unknown() {
let err = "gpt".parse::<RuntimeKind>().unwrap_err();
assert!(err.to_string().contains("unknown runtime"));
}
#[test]
fn runtime_kind_serde_round_trip() {
for kind in [RuntimeKind::ClaudeCode, RuntimeKind::Tcode] {
let json = serde_json::to_string(&kind).unwrap();
let back: RuntimeKind = serde_json::from_str(&json).unwrap();
assert_eq!(kind, back);
}
assert_eq!(
serde_json::to_string(&RuntimeKind::ClaudeCode).unwrap(),
"\"claude-code\""
);
assert_eq!(
serde_json::to_string(&RuntimeKind::Tcode).unwrap(),
"\"tcode\""
);
}
#[test]
fn runtime_kind_value_enum_matches_wire() {
for kind in [RuntimeKind::ClaudeCode, RuntimeKind::Tcode] {
let value = kind.to_possible_value().expect("kind has a CLI value");
assert_eq!(value.get_name(), kind.as_str());
let parsed = kind.as_str().parse::<RuntimeKind>().expect("parses");
assert_eq!(parsed, kind);
}
let names: Vec<String> = RuntimeKind::value_variants()
.iter()
.map(|k| k.to_possible_value().unwrap().get_name().to_owned())
.collect();
assert_eq!(names, vec!["claude-code".to_owned(), "tcode".to_owned()]);
}
#[test]
fn build_adapter_returns_matching_identify() {
let tmux = FakeTmux::new();
let claude = build_adapter(RuntimeKind::ClaudeCode, tmux.clone());
assert_eq!(claude.identify(), "claude-code");
let tcode = build_adapter(RuntimeKind::Tcode, tmux);
assert_eq!(tcode.identify(), "tcode");
}
}