Skip to main content

atomr_agents_coding_cli_core/
vendor.rs

1//! The `CliVendor` integration seam.
2//!
3//! Each supported CLI (Claude Code, Codex, Antigravity, ...) lives in its
4//! own crate (`atomr-agents-coding-cli-vendor-<name>`) that implements
5//! this trait. The harness composes vendor adapters into a registry
6//! and dispatches based on `CliRequest::vendor`.
7
8use std::collections::BTreeMap;
9use std::ffi::OsString;
10use std::path::{Path, PathBuf};
11
12use async_trait::async_trait;
13use serde::{Deserialize, Serialize};
14
15use crate::error::{MapperError, ParseError};
16use crate::event::CodingCliEvent;
17use crate::projection::ConceptProjection;
18use crate::request::CliRequest;
19
20/// Stable identifier for a vendor adapter. Extensible — third-party
21/// adapters can use [`CliVendorKind::Other`].
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum CliVendorKind {
25    Claude,
26    Codex,
27    Antigravity,
28    Cursor,
29    Aider,
30    Other(String),
31}
32
33impl CliVendorKind {
34    pub fn as_str(&self) -> &str {
35        match self {
36            Self::Claude => "claude",
37            Self::Codex => "codex",
38            Self::Antigravity => "antigravity",
39            Self::Cursor => "cursor",
40            Self::Aider => "aider",
41            Self::Other(s) => s.as_str(),
42        }
43    }
44}
45
46impl std::fmt::Display for CliVendorKind {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.write_str(self.as_str())
49    }
50}
51
52/// A concrete process invocation produced by a vendor adapter.
53///
54/// Isolators consume this to spawn either a host `tokio::process` or
55/// an in-container exec — the spec is identical in both worlds.
56#[derive(Debug, Clone)]
57pub struct CliCommand {
58    pub program: PathBuf,
59    pub args: Vec<OsString>,
60    pub env: BTreeMap<String, String>,
61    /// Working directory inside whatever environment runs the command.
62    pub workdir: PathBuf,
63    /// If `true`, the isolator must allocate a PTY (interactive mode).
64    pub allocate_pty: bool,
65}
66
67impl CliCommand {
68    pub fn new(program: impl Into<PathBuf>, workdir: impl Into<PathBuf>) -> Self {
69        Self {
70            program: program.into(),
71            args: Vec::new(),
72            env: BTreeMap::new(),
73            workdir: workdir.into(),
74            allocate_pty: false,
75        }
76    }
77
78    pub fn arg(mut self, a: impl Into<OsString>) -> Self {
79        self.args.push(a.into());
80        self
81    }
82
83    pub fn arg_pair(mut self, flag: impl Into<OsString>, value: impl Into<OsString>) -> Self {
84        self.args.push(flag.into());
85        self.args.push(value.into());
86        self
87    }
88
89    pub fn envv(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
90        self.env.insert(k.into(), v.into());
91        self
92    }
93
94    pub fn with_pty(mut self) -> Self {
95        self.allocate_pty = true;
96        self
97    }
98}
99
100/// A stream parser owned by the harness for the lifetime of one run.
101///
102/// Adapters that emit NDJSON typically maintain no state and parse
103/// each line independently; adapters that emit multi-line frames can
104/// buffer in `self`. `flush` is called once at EOF.
105pub trait CliEventParser: Send {
106    fn parse_line(&mut self, line: &str) -> Result<Vec<CodingCliEvent>, ParseError>;
107    fn flush(&mut self) -> Result<Vec<CodingCliEvent>, ParseError>;
108}
109
110/// The integration seam each CLI adapter implements.
111#[async_trait]
112pub trait CliVendor: Send + Sync {
113    /// Stable identifier this adapter answers to.
114    fn kind(&self) -> CliVendorKind;
115
116    /// Human-friendly label for the UI (e.g. "Claude Code").
117    fn label(&self) -> &str;
118
119    /// Build the command line for a headless run.
120    fn build_headless_command(&self, req: &CliRequest, workdir: &Path) -> CliCommand;
121
122    /// Build the command line for an interactive run (TUI).
123    fn build_interactive_command(&self, req: &CliRequest, workdir: &Path) -> CliCommand;
124
125    /// Construct a fresh parser for one run's stream of NDJSON / lines.
126    fn new_parser(&self) -> Box<dyn CliEventParser>;
127
128    /// Write the vendor's on-disk config (e.g. `CLAUDE.md`,
129    /// `.mcp.json`, `AGENTS.md`) from the supplied projection.
130    /// Called *before* every run. Idempotent — overwrites prior files
131    /// the harness placed there.
132    async fn materialize_config(
133        &self,
134        projection: &ConceptProjection,
135        workdir: &Path,
136    ) -> Result<(), MapperError>;
137
138    /// Probe whether the CLI is actually installed and runnable in
139    /// the current isolator. The harness skips vendors that return
140    /// `false` from the `/api/cli/vendors` listing.
141    async fn is_available(&self) -> bool {
142        true
143    }
144}