1use clap::{CommandFactory, FromArgMatches};
4
5use crate::{
6 AgentDispatch, DoctorChecks, FatalCliError, ProcessEnv, ToolSpec,
7 command::run_standard_command, parse_command_with_agent_surface_from,
8 run_standard_command_no_doctor, run_with_fatal_handler,
9};
10
11fn run_cli_with_parsed<T, D, M, F>(
12 spec: &'static ToolSpec,
13 env: &ProcessEnv,
14 parsed: AgentDispatch<T>,
15 doctor: Option<&D>,
16 metadata_command: M,
17 run: F,
18) -> i32
19where
20 T: CommandFactory,
21 D: DoctorChecks,
22 M: FnOnce(&T) -> Option<crate::StandardCommand>,
23 F: FnOnce(T) -> Result<i32, FatalCliError>,
24{
25 match parsed {
26 AgentDispatch::Cli(cli) => {
27 if let Some(command) = metadata_command(&cli) {
28 return run_standard_command::<T, D>(spec, env, &command, doctor);
29 }
30 run_with_fatal_handler(|| run(cli))
31 }
32 AgentDispatch::Printed(code) => code,
33 }
34}
35
36#[must_use]
41pub fn run_cli_from<T, I, D, M, F>(
42 spec: &'static ToolSpec,
43 env: &ProcessEnv,
44 argv: I,
45 doctor: &D,
46 metadata_command: M,
47 run: F,
48) -> i32
49where
50 T: CommandFactory + FromArgMatches,
51 I: IntoIterator,
52 I::Item: Into<std::ffi::OsString> + Clone,
53 D: DoctorChecks,
54 M: FnOnce(&T) -> Option<crate::StandardCommand>,
55 F: FnOnce(T) -> Result<i32, FatalCliError>,
56{
57 match parse_command_with_agent_surface_from::<T, I>(spec, &env.agent, argv) {
58 Ok(parsed) => run_cli_with_parsed(spec, env, parsed, Some(doctor), metadata_command, run),
59 Err(error) => error.exit(),
60 }
61}
62
63#[must_use]
68pub fn run_cli_no_doctor_from<T, I, M, F>(
69 spec: &'static ToolSpec,
70 env: &ProcessEnv,
71 argv: I,
72 metadata_command: M,
73 run: F,
74) -> i32
75where
76 T: CommandFactory + FromArgMatches,
77 I: IntoIterator,
78 I::Item: Into<std::ffi::OsString> + Clone,
79 M: FnOnce(&T) -> Option<crate::StandardCommand>,
80 F: FnOnce(T) -> Result<i32, FatalCliError>,
81{
82 match parse_command_with_agent_surface_from::<T, I>(spec, &env.agent, argv) {
83 Ok(AgentDispatch::Cli(cli)) => {
84 if let Some(command) = metadata_command(&cli) {
85 return run_standard_command_no_doctor::<T>(spec, env, &command);
86 }
87 run_with_fatal_handler(|| run(cli))
88 }
89 Ok(AgentDispatch::Printed(code)) => code,
90 Err(error) => error.exit(),
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use std::cell::Cell;
97
98 use clap::{Parser, Subcommand};
99
100 use super::*;
101 use crate::{
102 AGENT_TOKEN_ENV, AGENT_TOKEN_EXPECTED_ENV, AgentCapability, AgentSurfaceSpec,
103 CommandSelector, FatalCliError, FlagSelector, JsonOutput, LicenseType, MetaCommand,
104 RepoInfo, StandardCommand, map_standard_command, test_support::env_lock, workspace_tool,
105 };
106
107 const QUERY_COMMAND: CommandSelector = CommandSelector::new(&["query"]);
108 const QUERY_FAIL_FLAG: FlagSelector = FlagSelector::new(&["query"], "fail");
109 const QUERY_CAPABILITY: AgentCapability =
110 AgentCapability::minimal("query", &[QUERY_COMMAND], &[QUERY_FAIL_FLAG]);
111 const AGENT_SURFACE: AgentSurfaceSpec = AgentSurfaceSpec::new(&[QUERY_CAPABILITY]);
112 const TOOL_SPEC: ToolSpec =
113 workspace_tool("tool", "Tool", "1.2.3", LicenseType::MIT, true, true)
114 .with_agent_surface(&AGENT_SURFACE);
115
116 #[derive(Debug, Parser)]
117 #[command(name = "tool")]
118 struct TestCli {
119 #[command(subcommand)]
120 command: TestCommand,
121 }
122
123 #[derive(Debug, Subcommand)]
124 enum TestCommand {
125 Meta {
126 #[command(subcommand)]
127 command: MetaCommand,
128 },
129 Query {
130 #[arg(long)]
131 fail: bool,
132 },
133 }
134
135 struct TestDoctor;
136
137 impl DoctorChecks for TestDoctor {
138 fn repo_info() -> RepoInfo {
139 RepoInfo::new("tftio-stuff", "tool")
140 }
141
142 fn current_version() -> &'static str {
143 "1.2.3"
144 }
145 }
146
147 fn metadata_command(cli: &TestCli) -> Option<StandardCommand> {
148 match &cli.command {
149 TestCommand::Meta { command } => Some(map_standard_command(command, JsonOutput::Text)),
150 TestCommand::Query { .. } => None,
151 }
152 }
153
154 #[allow(unsafe_code)]
155 fn set_tokens(presented: Option<&str>, expected: Option<&str>) {
156 unsafe {
157 std::env::remove_var(AGENT_TOKEN_ENV);
158 std::env::remove_var(AGENT_TOKEN_EXPECTED_ENV);
159 if let Some(presented) = presented {
160 std::env::set_var(AGENT_TOKEN_ENV, presented);
161 }
162 if let Some(expected) = expected {
163 std::env::set_var(AGENT_TOKEN_EXPECTED_ENV, expected);
164 }
165 }
166 }
167
168 fn env_from_detected() -> ProcessEnv {
169 ProcessEnv {
170 agent: crate::AgentModeContext::from_tokens(
171 std::env::var(AGENT_TOKEN_ENV).ok(),
172 std::env::var(AGENT_TOKEN_EXPECTED_ENV).ok(),
173 ),
174 home: None,
175 }
176 }
177
178 fn recording_runner(
179 called: &Cell<bool>,
180 ) -> impl FnOnce(TestCli) -> Result<i32, FatalCliError> + '_ {
181 move |_cli| {
182 called.set(true);
183 Ok(9)
184 }
185 }
186
187 #[test]
188 fn run_cli_from_routes_metadata_before_domain_runner() {
189 let _guard = env_lock();
190 set_tokens(None, None);
191 let env = env_from_detected();
192 let called = Cell::new(false);
193
194 let exit_code = run_cli_from::<TestCli, _, TestDoctor, _, _>(
195 &TOOL_SPEC,
196 &env,
197 ["tool", "meta", "doctor", "--json"],
198 &TestDoctor,
199 metadata_command,
200 recording_runner(&called),
201 );
202
203 assert_eq!(exit_code, 0);
204 assert!(!called.get());
205 }
206
207 #[test]
208 fn run_cli_from_short_circuits_agent_help_output() {
209 let _guard = env_lock();
210 set_tokens(Some("shared-token"), Some("shared-token"));
211 let env = env_from_detected();
212 let called = Cell::new(false);
213
214 let exit_code = run_cli_from::<TestCli, _, TestDoctor, _, _>(
215 &TOOL_SPEC,
216 &env,
217 ["tool", "--agent-help"],
218 &TestDoctor,
219 metadata_command,
220 recording_runner(&called),
221 );
222
223 assert_eq!(exit_code, 0);
224 assert!(!called.get());
225 set_tokens(None, None);
226 }
227
228 #[test]
229 fn run_cli_from_wraps_domain_errors_with_shared_fatal_handler() {
230 let _guard = env_lock();
231 set_tokens(None, None);
232 let env = env_from_detected();
233 let exit_code = run_cli_from::<TestCli, _, TestDoctor, _, _>(
234 &TOOL_SPEC,
235 &env,
236 ["tool", "query", "--fail"],
237 &TestDoctor,
238 metadata_command,
239 |_cli| {
240 Err(FatalCliError::new(
241 "query",
242 JsonOutput::Text,
243 "domain failure",
244 ))
245 },
246 );
247
248 assert_eq!(exit_code, 1);
249 }
250
251 #[test]
252 fn run_cli_no_doctor_from_routes_metadata_without_domain_runner() {
253 let _guard = env_lock();
254 set_tokens(None, None);
255 let env = env_from_detected();
256 let called = Cell::new(false);
257
258 let exit_code = run_cli_no_doctor_from::<TestCli, _, _, _>(
259 &TOOL_SPEC,
260 &env,
261 ["tool", "meta", "license"],
262 metadata_command,
263 recording_runner(&called),
264 );
265
266 assert_eq!(exit_code, 0);
267 assert!(!called.get());
268 }
269
270 #[test]
271 fn run_cli_no_doctor_from_runs_domain_handler_for_non_metadata_command() {
272 let _guard = env_lock();
273 set_tokens(None, None);
274 let env = env_from_detected();
275 let called = Cell::new(false);
276
277 let exit_code = run_cli_no_doctor_from::<TestCli, _, _, _>(
278 &TOOL_SPEC,
279 &env,
280 ["tool", "query"],
281 metadata_command,
282 recording_runner(&called),
283 );
284
285 assert_eq!(exit_code, 9);
286 assert!(called.get());
287 }
288
289 #[test]
290 fn run_cli_no_doctor_from_short_circuits_agent_help_output() {
291 let _guard = env_lock();
292 set_tokens(Some("shared-token"), Some("shared-token"));
293 let env = env_from_detected();
294 let called = Cell::new(false);
295
296 let exit_code = run_cli_no_doctor_from::<TestCli, _, _, _>(
297 &TOOL_SPEC,
298 &env,
299 ["tool", "--agent-help"],
300 metadata_command,
301 recording_runner(&called),
302 );
303
304 assert_eq!(exit_code, 0);
305 assert!(!called.get());
306 set_tokens(None, None);
307 }
308}