crates/flotilla-commands/src/commands/agent.rs
Line | Count | Source |
1 | | use std::path::PathBuf; |
2 | | |
3 | | use clap::{Parser, Subcommand}; |
4 | | use flotilla_protocol::{Command, CommandAction}; |
5 | | |
6 | | use crate::{ |
7 | | resolved::{HostResolution, RepoContext}, |
8 | | Resolved, |
9 | | }; |
10 | | |
11 | | #[derive(Debug, Clone, PartialEq, Eq, Parser)] |
12 | | #[command(about = "Cloud agents")] |
13 | | pub struct AgentNoun { |
14 | | /// Agent/session ID |
15 | | pub subject: String, |
16 | | |
17 | | #[command(subcommand)] |
18 | | pub verb: AgentVerb, |
19 | | } |
20 | | |
21 | | #[derive(Debug, Clone, PartialEq, Eq, Subcommand)] |
22 | | pub enum AgentVerb { |
23 | | /// Connect to a remote agent session |
24 | | Teleport { |
25 | | #[arg(long)] |
26 | | branch: Option<String>, |
27 | | #[arg(long)] |
28 | | checkout: Option<PathBuf>, |
29 | | }, |
30 | | /// Archive an agent session |
31 | | Archive, |
32 | | } |
33 | | |
34 | | impl AgentNoun { |
35 | 4 | pub fn resolve(self) -> Result<Resolved, String> { |
36 | 4 | match self.verb { |
37 | 3 | AgentVerb::Teleport { branch, checkout } => Ok(Resolved::NeedsContext { |
38 | 3 | command: Command { |
39 | 3 | host: None, |
40 | 3 | provisioning_target: None, |
41 | 3 | context_repo: None, |
42 | 3 | action: CommandAction::TeleportSession { session_id: self.subject, branch, checkout_key: checkout }, |
43 | 3 | }, |
44 | 3 | repo: RepoContext::Inferred, |
45 | 3 | host: HostResolution::Local, |
46 | 3 | }), |
47 | 1 | AgentVerb::Archive => Ok(Resolved::NeedsContext { |
48 | 1 | command: Command { |
49 | 1 | host: None, |
50 | 1 | provisioning_target: None, |
51 | 1 | context_repo: None, |
52 | 1 | action: CommandAction::ArchiveSession { session_id: self.subject }, |
53 | 1 | }, |
54 | 1 | repo: RepoContext::Inferred, |
55 | 1 | host: HostResolution::ProviderHost, |
56 | 1 | }), |
57 | | } |
58 | 4 | } |
59 | | } |
60 | | |
61 | | impl std::fmt::Display for AgentNoun { |
62 | 6 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
63 | 6 | write!(f, "agent {}", self.subject)?0 ; |
64 | 6 | match &self.verb { |
65 | 4 | AgentVerb::Teleport { branch, checkout } => { |
66 | 4 | write!(f, " teleport")?0 ; |
67 | 4 | if let Some(b3 ) = branch { |
68 | 3 | write!(f, " --branch {b}")?0 ; |
69 | 1 | } |
70 | 4 | if let Some(c1 ) = checkout { |
71 | 1 | write!(f, " --checkout {}", c.display())?0 ; |
72 | 3 | } |
73 | | } |
74 | 2 | AgentVerb::Archive => write!(f, " archive")?0 , |
75 | | } |
76 | 6 | Ok(()) |
77 | 6 | } |
78 | | } |
79 | | |
80 | | #[cfg(test)] |
81 | | mod tests { |
82 | | use std::path::PathBuf; |
83 | | |
84 | | use clap::Parser; |
85 | | use flotilla_protocol::{Command, CommandAction}; |
86 | | |
87 | | use super::AgentNoun; |
88 | | use crate::{ |
89 | | resolved::{HostResolution, RepoContext}, |
90 | | test_utils::assert_round_trip, |
91 | | Resolved, |
92 | | }; |
93 | | |
94 | 4 | fn parse(args: &[&str]) -> AgentNoun { |
95 | 4 | AgentNoun::try_parse_from(args).expect("should parse") |
96 | 4 | } |
97 | | |
98 | | #[test] |
99 | 1 | fn agent_teleport_no_flags() { |
100 | 1 | let resolved = parse(&["agent", "claude-1", "teleport"]).resolve().unwrap(); |
101 | 1 | assert_eq!(resolved, Resolved::NeedsContext { |
102 | 1 | command: Command { |
103 | 1 | host: None, |
104 | 1 | provisioning_target: None, |
105 | 1 | context_repo: None, |
106 | 1 | action: CommandAction::TeleportSession { session_id: "claude-1".into(), branch: None, checkout_key: None }, |
107 | 1 | }, |
108 | 1 | repo: RepoContext::Inferred, |
109 | 1 | host: HostResolution::Local, |
110 | 1 | }); |
111 | 1 | } |
112 | | |
113 | | #[test] |
114 | 1 | fn agent_teleport_with_branch() { |
115 | 1 | let resolved = parse(&["agent", "claude-1", "teleport", "--branch", "feat"]).resolve().unwrap(); |
116 | 1 | assert_eq!(resolved, Resolved::NeedsContext { |
117 | 1 | command: Command { |
118 | 1 | host: None, |
119 | 1 | provisioning_target: None, |
120 | 1 | context_repo: None, |
121 | 1 | action: CommandAction::TeleportSession { session_id: "claude-1".into(), branch: Some("feat".into()), checkout_key: None }, |
122 | 1 | }, |
123 | 1 | repo: RepoContext::Inferred, |
124 | 1 | host: HostResolution::Local, |
125 | 1 | }); |
126 | 1 | } |
127 | | |
128 | | #[test] |
129 | 1 | fn agent_teleport_with_branch_and_checkout() { |
130 | 1 | let resolved = parse(&["agent", "claude-1", "teleport", "--branch", "feat", "--checkout", "/tmp/wt"]).resolve().unwrap(); |
131 | 1 | assert_eq!(resolved, Resolved::NeedsContext { |
132 | 1 | command: Command { |
133 | 1 | host: None, |
134 | 1 | provisioning_target: None, |
135 | 1 | context_repo: None, |
136 | 1 | action: CommandAction::TeleportSession { |
137 | 1 | session_id: "claude-1".into(), |
138 | 1 | branch: Some("feat".into()), |
139 | 1 | checkout_key: Some(PathBuf::from("/tmp/wt")), |
140 | 1 | }, |
141 | 1 | }, |
142 | 1 | repo: RepoContext::Inferred, |
143 | 1 | host: HostResolution::Local, |
144 | 1 | }); |
145 | 1 | } |
146 | | |
147 | | #[test] |
148 | 1 | fn agent_archive() { |
149 | 1 | let resolved = parse(&["agent", "claude-1", "archive"]).resolve().unwrap(); |
150 | 1 | assert_eq!(resolved, Resolved::NeedsContext { |
151 | 1 | command: Command { |
152 | 1 | host: None, |
153 | 1 | provisioning_target: None, |
154 | 1 | context_repo: None, |
155 | 1 | action: CommandAction::ArchiveSession { session_id: "claude-1".into() } |
156 | 1 | }, |
157 | 1 | repo: RepoContext::Inferred, |
158 | 1 | host: HostResolution::ProviderHost, |
159 | 1 | }); |
160 | 1 | } |
161 | | |
162 | | #[test] |
163 | 1 | fn round_trip_teleport() { |
164 | 1 | assert_round_trip::<AgentNoun>(&["agent", "claude-1", "teleport"]); |
165 | 1 | } |
166 | | |
167 | | #[test] |
168 | 1 | fn round_trip_teleport_with_branch() { |
169 | 1 | assert_round_trip::<AgentNoun>(&["agent", "claude-1", "teleport", "--branch", "feat"]); |
170 | 1 | } |
171 | | |
172 | | #[test] |
173 | 1 | fn round_trip_archive() { |
174 | 1 | assert_round_trip::<AgentNoun>(&["agent", "claude-1", "archive"]); |
175 | 1 | } |
176 | | } |