use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::error::{MapperError, ParseError};
use crate::event::CodingCliEvent;
use crate::projection::ConceptProjection;
use crate::request::CliRequest;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CliVendorKind {
Claude,
Codex,
Antigravity,
Cursor,
Aider,
Other(String),
}
impl CliVendorKind {
pub fn as_str(&self) -> &str {
match self {
Self::Claude => "claude",
Self::Codex => "codex",
Self::Antigravity => "antigravity",
Self::Cursor => "cursor",
Self::Aider => "aider",
Self::Other(s) => s.as_str(),
}
}
}
impl std::fmt::Display for CliVendorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct CliCommand {
pub program: PathBuf,
pub args: Vec<OsString>,
pub env: BTreeMap<String, String>,
pub workdir: PathBuf,
pub allocate_pty: bool,
}
impl CliCommand {
pub fn new(program: impl Into<PathBuf>, workdir: impl Into<PathBuf>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
env: BTreeMap::new(),
workdir: workdir.into(),
allocate_pty: false,
}
}
pub fn arg(mut self, a: impl Into<OsString>) -> Self {
self.args.push(a.into());
self
}
pub fn arg_pair(mut self, flag: impl Into<OsString>, value: impl Into<OsString>) -> Self {
self.args.push(flag.into());
self.args.push(value.into());
self
}
pub fn envv(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
self.env.insert(k.into(), v.into());
self
}
pub fn with_pty(mut self) -> Self {
self.allocate_pty = true;
self
}
}
pub trait CliEventParser: Send {
fn parse_line(&mut self, line: &str) -> Result<Vec<CodingCliEvent>, ParseError>;
fn flush(&mut self) -> Result<Vec<CodingCliEvent>, ParseError>;
}
#[async_trait]
pub trait CliVendor: Send + Sync {
fn kind(&self) -> CliVendorKind;
fn label(&self) -> &str;
fn build_headless_command(&self, req: &CliRequest, workdir: &Path) -> CliCommand;
fn build_interactive_command(&self, req: &CliRequest, workdir: &Path) -> CliCommand;
fn new_parser(&self) -> Box<dyn CliEventParser>;
async fn materialize_config(
&self,
projection: &ConceptProjection,
workdir: &Path,
) -> Result<(), MapperError>;
async fn is_available(&self) -> bool {
true
}
}