Skip to main content

path_cli/
harness.rs

1//! The harness layer over [`ArtifactType`]: [`Harness`] names the
2//! agent runtimes sessions can be shared from and resumed into, and
3//! `HarnessBundle` carries one provider manager per installed harness
4//! for commands that aggregate across them.
5
6use crate::artifact::ArtifactType;
7
8/// An installed agent harness — a runtime sessions can be shared from
9/// and resumed into. Deliberately parallel to [`ArtifactType`]: every
10/// harness maps onto an artifact type, but not every artifact type is
11/// a harness once non-session kinds (git, github) join `ArtifactType`
12/// — you can't resume into a git repo. Harness-facing surfaces
13/// (`share`/`resume` `--harness`) take this type so non-harness values
14/// stay unrepresentable.
15#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, clap::ValueEnum)]
16#[value(rename_all = "lower")]
17pub enum Harness {
18    Claude,
19    Gemini,
20    Codex,
21    Opencode,
22    Cursor,
23    Pi,
24    Copilot,
25}
26
27impl Harness {
28    /// Every harness, in presentation order.
29    pub(crate) const ALL: [Harness; 7] = [
30        Harness::Claude,
31        Harness::Gemini,
32        Harness::Codex,
33        Harness::Opencode,
34        Harness::Cursor,
35        Harness::Pi,
36        Harness::Copilot,
37    ];
38
39    /// The artifact type this harness's sessions ingest as.
40    pub(crate) fn artifact_type(&self) -> ArtifactType {
41        match self {
42            Harness::Claude => ArtifactType::Claude,
43            Harness::Gemini => ArtifactType::Gemini,
44            Harness::Codex => ArtifactType::Codex,
45            Harness::Opencode => ArtifactType::Opencode,
46            Harness::Cursor => ArtifactType::Cursor,
47            Harness::Pi => ArtifactType::Pi,
48            Harness::Copilot => ArtifactType::Copilot,
49        }
50    }
51
52    pub(crate) fn name(&self) -> &'static str {
53        self.artifact_type().name()
54    }
55
56    pub(crate) fn padded_name(&self) -> String {
57        self.artifact_type().padded_name()
58    }
59}
60
61impl ArtifactType {
62    /// The harness this artifact type's sessions come from — `None`
63    /// for artifact types that aren't agent harnesses (none yet).
64    pub(crate) fn harness(&self) -> Option<Harness> {
65        match self {
66            ArtifactType::Claude => Some(Harness::Claude),
67            ArtifactType::Gemini => Some(Harness::Gemini),
68            ArtifactType::Codex => Some(Harness::Codex),
69            ArtifactType::Opencode => Some(Harness::Opencode),
70            ArtifactType::Cursor => Some(Harness::Cursor),
71            ArtifactType::Pi => Some(Harness::Pi),
72            ArtifactType::Copilot => Some(Harness::Copilot),
73            ArtifactType::Git => None,
74        }
75    }
76}
77
78/// Bundle of provider managers used during aggregation. Production code
79/// builds this from real `$HOME` via `from_environment`; tests construct
80/// it directly with provider-specific resolvers.
81#[derive(Default)]
82pub(crate) struct HarnessBundle {
83    pub(crate) claude: Option<toolpath_claude::ClaudeConvo>,
84    pub(crate) gemini: Option<toolpath_gemini::GeminiConvo>,
85    pub(crate) codex: Option<toolpath_codex::CodexConvo>,
86    pub(crate) copilot: Option<toolpath_copilot::CopilotConvo>,
87    pub(crate) opencode: Option<toolpath_opencode::OpencodeConvo>,
88    pub(crate) cursor: Option<toolpath_cursor::CursorConvo>,
89    pub(crate) pi: Option<toolpath_pi::PiConvo>,
90}
91
92impl HarnessBundle {
93    /// Build the production bundle. Each provider is included
94    /// unconditionally (its `new()` doesn't fail on a missing home dir);
95    /// consumers skip the ones whose listing returns empty/NotFound.
96    pub(crate) fn from_environment() -> Self {
97        Self {
98            claude: Some(toolpath_claude::ClaudeConvo::new()),
99            gemini: Some(toolpath_gemini::GeminiConvo::new()),
100            codex: Some(toolpath_codex::CodexConvo::new()),
101            copilot: Some(toolpath_copilot::CopilotConvo::new()),
102            opencode: Some(toolpath_opencode::OpencodeConvo::new()),
103            cursor: Some(toolpath_cursor::CursorConvo::new()),
104            pi: Some(toolpath_pi::PiConvo::new()),
105        }
106    }
107}
108
109pub(crate) fn is_not_found_claude(err: &toolpath_claude::ConvoError) -> bool {
110    use toolpath_claude::ConvoError;
111    matches!(err, ConvoError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
112        || matches!(err, ConvoError::NoHomeDirectory)
113        || matches!(err, ConvoError::ClaudeDirectoryNotFound(_))
114}
115
116pub(crate) fn is_not_found_gemini(err: &toolpath_gemini::ConvoError) -> bool {
117    use toolpath_gemini::ConvoError;
118    matches!(err, ConvoError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
119        || matches!(err, ConvoError::NoHomeDirectory)
120        || matches!(err, ConvoError::GeminiDirectoryNotFound(_))
121}
122
123pub(crate) fn is_not_found_pi(err: &toolpath_pi::PiError) -> bool {
124    use toolpath_pi::PiError;
125    matches!(err, PiError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
126        || matches!(err, PiError::ProjectNotFound(_))
127}
128
129pub(crate) fn is_not_found_codex(err: &toolpath_codex::ConvoError) -> bool {
130    use toolpath_codex::ConvoError;
131    matches!(err, ConvoError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
132        || matches!(err, ConvoError::NoHomeDirectory)
133        || matches!(err, ConvoError::CodexDirectoryNotFound(_))
134}
135
136pub(crate) fn is_not_found_copilot(err: &toolpath_copilot::ConvoError) -> bool {
137    use toolpath_copilot::ConvoError;
138    matches!(err, ConvoError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
139        || matches!(err, ConvoError::NoHomeDirectory)
140        || matches!(err, ConvoError::CopilotDirectoryNotFound(_))
141}
142
143pub(crate) fn is_not_found_opencode(err: &toolpath_opencode::ConvoError) -> bool {
144    use toolpath_opencode::ConvoError;
145    matches!(err, ConvoError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
146        || matches!(err, ConvoError::NoHomeDirectory)
147        || matches!(err, ConvoError::OpencodeDirectoryNotFound(_))
148        || matches!(err, ConvoError::DatabaseNotFound(_))
149}
150
151pub(crate) fn is_not_found_cursor(err: &toolpath_cursor::CursorError) -> bool {
152    use toolpath_cursor::CursorError;
153    matches!(err, CursorError::Io(e) if e.kind() == std::io::ErrorKind::NotFound)
154        || matches!(err, CursorError::NoHomeDirectory)
155        || matches!(err, CursorError::CursorDataDirectoryNotFound(_))
156        || matches!(err, CursorError::DatabaseNotFound(_))
157}