1use crate::provider::registry::{ProviderRegistry, ResolveProviderError};
2use agent_runtime_core::schema::{HealthStatus, HealthcheckRequest};
3use clap::{Args, Subcommand, ValueEnum};
4use serde::Serialize;
5
6const EXIT_OK: i32 = 0;
7const EXIT_RUNTIME_ERROR: i32 = 1;
8const EXIT_USAGE: i32 = 64;
9
10#[derive(Debug, Args)]
11pub struct ProviderArgs {
12 #[command(subcommand)]
13 pub command: Option<ProviderSubcommand>,
14}
15
16#[derive(Debug, Subcommand)]
17pub enum ProviderSubcommand {
18 List(ProviderListArgs),
20 Healthcheck(ProviderHealthcheckArgs),
22}
23
24#[derive(Debug, Args)]
25pub struct ProviderListArgs {
26 #[arg(long)]
28 pub provider: Option<String>,
29
30 #[arg(long)]
32 pub timeout_ms: Option<u64>,
33
34 #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
36 pub format: OutputFormat,
37}
38
39#[derive(Debug, Args)]
40pub struct ProviderHealthcheckArgs {
41 #[arg(long)]
43 pub provider: Option<String>,
44
45 #[arg(long)]
47 pub timeout_ms: Option<u64>,
48
49 #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
51 pub format: OutputFormat,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
55pub enum OutputFormat {
56 #[default]
57 Text,
58 Json,
59}
60
61pub fn run(command: ProviderSubcommand) -> i32 {
62 match command {
63 ProviderSubcommand::List(args) => run_list(args),
64 ProviderSubcommand::Healthcheck(args) => run_healthcheck(args),
65 }
66}
67
68fn run_list(args: ProviderListArgs) -> i32 {
69 let registry = ProviderRegistry::with_builtins();
70 let selection = match registry.resolve_selection(args.provider.as_deref()) {
71 Ok(selection) => selection,
72 Err(error) => return report_selection_error(®istry, error),
73 };
74 let default_provider = registry.default_provider_id().map(ToOwned::to_owned);
75 let selected_provider = selection.provider_id;
76 let selected_source = selection.source.as_str().to_string();
77
78 let providers = registry
79 .iter()
80 .map(|(provider_id, adapter)| {
81 let metadata = adapter.metadata();
82 let health = adapter.healthcheck(HealthcheckRequest {
83 timeout_ms: args.timeout_ms,
84 });
85 let (status, summary) = match health {
86 Ok(response) => (response.status, response.summary),
87 Err(error) => (
88 HealthStatus::Unknown,
89 Some(format!("healthcheck failed: {}", error.message)),
90 ),
91 };
92
93 ProviderListItem {
94 id: provider_id.to_string(),
95 contract_version: metadata.contract_version.as_str().to_string(),
96 maturity: metadata.maturity.as_str().to_string(),
97 status: health_status_text(status).to_string(),
98 summary,
99 is_default: default_provider.as_deref() == Some(provider_id),
100 is_selected: selected_provider.as_str() == provider_id,
101 }
102 })
103 .collect::<Vec<_>>();
104
105 let output = ProviderListOutput {
106 default_provider,
107 selected_provider,
108 selected_source,
109 providers,
110 };
111
112 emit_list_output(&output, args.format)
113}
114
115fn run_healthcheck(args: ProviderHealthcheckArgs) -> i32 {
116 let registry = ProviderRegistry::with_builtins();
117 let selection = match registry.resolve_selection(args.provider.as_deref()) {
118 Ok(selection) => selection,
119 Err(error) => return report_selection_error(®istry, error),
120 };
121
122 let Some(adapter) = registry.get(selection.provider_id.as_str()) else {
123 eprintln!(
124 "agentctl provider: selected provider '{}' is not registered",
125 selection.provider_id
126 );
127 return EXIT_RUNTIME_ERROR;
128 };
129
130 let health = adapter.healthcheck(HealthcheckRequest {
131 timeout_ms: args.timeout_ms,
132 });
133 match health {
134 Ok(response) => {
135 let output = ProviderHealthcheckOutput {
136 provider: selection.provider_id,
137 selected_source: selection.source.as_str().to_string(),
138 status: health_status_text(response.status).to_string(),
139 summary: response.summary,
140 details: response.details,
141 };
142 emit_healthcheck_output(&output, args.format)
143 }
144 Err(error) => {
145 if args.format == OutputFormat::Json {
146 let output = ProviderHealthcheckFailureOutput {
147 provider: selection.provider_id,
148 selected_source: selection.source.as_str().to_string(),
149 error: ProviderCommandErrorOutput {
150 category: serde_json::to_value(error.category)
151 .ok()
152 .and_then(|value| value.as_str().map(ToOwned::to_owned))
153 .unwrap_or_else(|| "unknown".to_string()),
154 code: error.code,
155 message: error.message,
156 },
157 };
158 return emit_json(&output);
159 }
160
161 eprintln!(
162 "agentctl provider: healthcheck failed for '{}': {}",
163 selection.provider_id, error.message
164 );
165 EXIT_RUNTIME_ERROR
166 }
167 }
168}
169
170fn emit_list_output(output: &ProviderListOutput, format: OutputFormat) -> i32 {
171 match format {
172 OutputFormat::Json => emit_json(output),
173 OutputFormat::Text => {
174 println!(
175 "default_provider: {}",
176 output.default_provider.as_deref().unwrap_or("<none>")
177 );
178 println!(
179 "selected_provider: {} ({})",
180 output.selected_provider, output.selected_source
181 );
182 println!("providers:");
183 for provider in &output.providers {
184 let mut tags = Vec::new();
185 if provider.is_default {
186 tags.push("default");
187 }
188 if provider.is_selected {
189 tags.push("selected");
190 }
191
192 if tags.is_empty() {
193 println!(
194 "- {} [{}] (maturity: {})",
195 provider.id, provider.status, provider.maturity
196 );
197 } else {
198 println!(
199 "- {} [{}] (maturity: {}, {})",
200 provider.id,
201 provider.status,
202 provider.maturity,
203 tags.join(", ")
204 );
205 }
206 if let Some(summary) = provider.summary.as_deref() {
207 println!(" summary: {summary}");
208 }
209 }
210 EXIT_OK
211 }
212 }
213}
214
215fn emit_healthcheck_output(output: &ProviderHealthcheckOutput, format: OutputFormat) -> i32 {
216 match format {
217 OutputFormat::Json => emit_json(output),
218 OutputFormat::Text => {
219 println!("provider: {}", output.provider);
220 println!("selected_source: {}", output.selected_source);
221 println!("status: {}", output.status);
222 if let Some(summary) = output.summary.as_deref() {
223 println!("summary: {summary}");
224 }
225 EXIT_OK
226 }
227 }
228}
229
230fn emit_json<T: Serialize>(value: &T) -> i32 {
231 match serde_json::to_string_pretty(value) {
232 Ok(encoded) => {
233 println!("{encoded}");
234 EXIT_OK
235 }
236 Err(error) => {
237 eprintln!("agentctl provider: failed to render json output: {error}");
238 EXIT_RUNTIME_ERROR
239 }
240 }
241}
242
243fn report_selection_error(registry: &ProviderRegistry, error: ResolveProviderError) -> i32 {
244 eprintln!("agentctl provider: {error}");
245
246 let registered = registry
247 .iter()
248 .map(|(provider_id, _)| provider_id.to_string())
249 .collect::<Vec<_>>();
250 if !registered.is_empty() {
251 eprintln!(
252 "agentctl provider: available providers: {}",
253 registered.join(", ")
254 );
255 }
256
257 EXIT_USAGE
258}
259
260fn health_status_text(status: HealthStatus) -> &'static str {
261 match status {
262 HealthStatus::Healthy => "healthy",
263 HealthStatus::Degraded => "degraded",
264 HealthStatus::Unhealthy => "unhealthy",
265 HealthStatus::Unknown => "unknown",
266 }
267}
268
269#[derive(Debug, Serialize)]
270struct ProviderListOutput {
271 #[serde(skip_serializing_if = "Option::is_none")]
272 default_provider: Option<String>,
273 selected_provider: String,
274 selected_source: String,
275 providers: Vec<ProviderListItem>,
276}
277
278#[derive(Debug, Serialize)]
279struct ProviderListItem {
280 id: String,
281 contract_version: String,
282 maturity: String,
283 status: String,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 summary: Option<String>,
286 is_default: bool,
287 is_selected: bool,
288}
289
290#[derive(Debug, Serialize)]
291struct ProviderHealthcheckOutput {
292 provider: String,
293 selected_source: String,
294 status: String,
295 #[serde(skip_serializing_if = "Option::is_none")]
296 summary: Option<String>,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 details: Option<serde_json::Value>,
299}
300
301#[derive(Debug, Serialize)]
302struct ProviderHealthcheckFailureOutput {
303 provider: String,
304 selected_source: String,
305 error: ProviderCommandErrorOutput,
306}
307
308#[derive(Debug, Serialize)]
309struct ProviderCommandErrorOutput {
310 category: String,
311 code: String,
312 message: String,
313}