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 #[test]
179 fn run_cli_from_routes_metadata_before_domain_runner() {
180 let _guard = env_lock();
181 set_tokens(None, None);
182 let env = env_from_detected();
183 let called = Cell::new(false);
184
185 let exit_code = run_cli_from::<TestCli, _, TestDoctor, _, _>(
186 &TOOL_SPEC,
187 &env,
188 ["tool", "meta", "doctor", "--json"],
189 &TestDoctor,
190 metadata_command,
191 |_cli| {
192 called.set(true);
193 Ok(9)
194 },
195 );
196
197 assert_eq!(exit_code, 0);
198 assert!(!called.get());
199 }
200
201 #[test]
202 fn run_cli_from_short_circuits_agent_help_output() {
203 let _guard = env_lock();
204 set_tokens(Some("shared-token"), Some("shared-token"));
205 let env = env_from_detected();
206 let called = Cell::new(false);
207
208 let exit_code = run_cli_from::<TestCli, _, TestDoctor, _, _>(
209 &TOOL_SPEC,
210 &env,
211 ["tool", "--agent-help"],
212 &TestDoctor,
213 metadata_command,
214 |_cli| {
215 called.set(true);
216 Ok(9)
217 },
218 );
219
220 assert_eq!(exit_code, 0);
221 assert!(!called.get());
222 set_tokens(None, None);
223 }
224
225 #[test]
226 fn run_cli_from_wraps_domain_errors_with_shared_fatal_handler() {
227 let _guard = env_lock();
228 set_tokens(None, None);
229 let env = env_from_detected();
230 let exit_code = run_cli_from::<TestCli, _, TestDoctor, _, _>(
231 &TOOL_SPEC,
232 &env,
233 ["tool", "query", "--fail"],
234 &TestDoctor,
235 metadata_command,
236 |_cli| {
237 Err(FatalCliError::new(
238 "query",
239 JsonOutput::Text,
240 "domain failure",
241 ))
242 },
243 );
244
245 assert_eq!(exit_code, 1);
246 }
247
248 #[test]
249 fn run_cli_no_doctor_from_routes_metadata_without_domain_runner() {
250 let _guard = env_lock();
251 set_tokens(None, None);
252 let env = env_from_detected();
253 let called = Cell::new(false);
254
255 let exit_code = run_cli_no_doctor_from::<TestCli, _, _, _>(
256 &TOOL_SPEC,
257 &env,
258 ["tool", "meta", "license"],
259 metadata_command,
260 |_cli| {
261 called.set(true);
262 Ok(9)
263 },
264 );
265
266 assert_eq!(exit_code, 0);
267 assert!(!called.get());
268 }
269}