1use crate::diag::{
2 AutomationToolSpec, DIAG_SCHEMA_VERSION, EXIT_OK, EXIT_USAGE, OutputFormat, ProbeMode,
3 ProbeModeArg, ReadinessSection, automation_tools, current_platform, doctor, emit_json,
4 resolve_probe_mode,
5};
6use crate::provider::registry::ProviderRegistry;
7use agent_runtime_core::schema::CapabilitiesRequest;
8use clap::Args;
9use serde::Serialize;
10
11#[derive(Debug, Args)]
12pub struct CapabilitiesArgs {
13 #[arg(long)]
15 pub provider: Option<String>,
16
17 #[arg(long)]
19 pub include_experimental: bool,
20
21 #[arg(long)]
23 pub timeout_ms: Option<u64>,
24
25 #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
27 pub format: OutputFormat,
28
29 #[arg(long, value_enum, default_value_t = ProbeModeArg::Auto)]
31 pub probe_mode: ProbeModeArg,
32}
33
34#[derive(Debug, Serialize)]
35struct CapabilitiesReport {
36 schema_version: &'static str,
37 command: &'static str,
38 probe_mode: ProbeMode,
39 readiness: ReadinessSection,
40 providers: Vec<ProviderCapabilities>,
41 automation_tools: Vec<AutomationToolCapabilities>,
42}
43
44#[derive(Debug, Serialize)]
45struct ProviderCapabilities {
46 id: String,
47 contract_version: String,
48 capabilities: Vec<CapabilityEntry>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 error: Option<ProviderCapabilitiesError>,
51}
52
53#[derive(Debug, Serialize)]
54struct CapabilityEntry {
55 name: String,
56 available: bool,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 description: Option<String>,
59}
60
61#[derive(Debug, Serialize)]
62struct ProviderCapabilitiesError {
63 category: String,
64 code: String,
65 message: String,
66}
67
68#[derive(Debug, Serialize)]
69struct AutomationToolCapabilities {
70 id: String,
71 command: String,
72 capabilities: Vec<String>,
73 supported_platforms: Vec<String>,
74 supports_current_platform: bool,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 test_mode_env: Option<String>,
77}
78
79pub fn run(args: CapabilitiesArgs) -> i32 {
80 let probe_mode = resolve_probe_mode(args.probe_mode);
81 let readiness =
82 match doctor::collect_readiness(args.provider.as_deref(), args.timeout_ms, probe_mode) {
83 Ok(readiness) => readiness,
84 Err(error) => {
85 eprintln!("agentctl diag capabilities: {error}");
86 return EXIT_USAGE;
87 }
88 };
89
90 let providers =
91 match collect_provider_capabilities(args.provider.as_deref(), args.include_experimental) {
92 Ok(providers) => providers,
93 Err(error) => {
94 eprintln!("agentctl diag capabilities: {error}");
95 return EXIT_USAGE;
96 }
97 };
98
99 let automation_tools = collect_automation_capabilities();
100 let report = CapabilitiesReport {
101 schema_version: DIAG_SCHEMA_VERSION,
102 command: "capabilities",
103 probe_mode,
104 readiness,
105 providers,
106 automation_tools,
107 };
108
109 match args.format {
110 OutputFormat::Json => emit_json(&report),
111 OutputFormat::Text => emit_text(&report),
112 }
113}
114
115fn collect_provider_capabilities(
116 provider_filter: Option<&str>,
117 include_experimental: bool,
118) -> Result<Vec<ProviderCapabilities>, String> {
119 let registry = ProviderRegistry::with_builtins();
120 let provider_ids = doctor::resolve_provider_ids(®istry, provider_filter)?;
121 let mut providers = Vec::with_capacity(provider_ids.len());
122
123 for provider_id in provider_ids {
124 let Some(adapter) = registry.get(provider_id.as_str()) else {
125 continue;
126 };
127 let metadata = adapter.metadata();
128 match adapter.capabilities(CapabilitiesRequest {
129 include_experimental,
130 }) {
131 Ok(response) => {
132 let capabilities = response
133 .capabilities
134 .into_iter()
135 .map(|capability| CapabilityEntry {
136 name: capability.name,
137 available: capability.available,
138 description: capability.description,
139 })
140 .collect::<Vec<_>>();
141 providers.push(ProviderCapabilities {
142 id: provider_id,
143 contract_version: metadata.contract_version.as_str().to_string(),
144 capabilities,
145 error: None,
146 });
147 }
148 Err(error) => {
149 let category = serde_json::to_value(error.category)
150 .ok()
151 .and_then(|value| value.as_str().map(ToOwned::to_owned))
152 .unwrap_or_else(|| "unknown".to_string());
153 providers.push(ProviderCapabilities {
154 id: provider_id,
155 contract_version: metadata.contract_version.as_str().to_string(),
156 capabilities: Vec::new(),
157 error: Some(ProviderCapabilitiesError {
158 category,
159 code: error.code,
160 message: error.message,
161 }),
162 });
163 }
164 }
165 }
166
167 Ok(providers)
168}
169
170fn collect_automation_capabilities() -> Vec<AutomationToolCapabilities> {
171 automation_tools()
172 .iter()
173 .map(|spec| AutomationToolCapabilities {
174 id: spec.id.to_string(),
175 command: spec.command.to_string(),
176 capabilities: spec
177 .capabilities
178 .iter()
179 .map(|capability| capability.to_string())
180 .collect(),
181 supported_platforms: spec
182 .supported_platforms
183 .iter()
184 .map(|platform| platform.to_string())
185 .collect(),
186 supports_current_platform: supports_current_platform(spec),
187 test_mode_env: spec.test_mode_env.map(ToOwned::to_owned),
188 })
189 .collect()
190}
191
192fn supports_current_platform(spec: &AutomationToolSpec) -> bool {
193 if spec.supported_platforms.is_empty() {
194 return true;
195 }
196
197 spec.supported_platforms.contains(¤t_platform())
198}
199
200fn emit_text(report: &CapabilitiesReport) -> i32 {
201 println!("schema_version: {}", report.schema_version);
202 println!("command: {}", report.command);
203 println!("probe_mode: {}", report.probe_mode.as_str());
204 println!(
205 "overall_status: {}",
206 report.readiness.overall_status.as_str()
207 );
208 println!(
209 "summary: total={} ready={} degraded={} not_ready={} unknown={}",
210 report.readiness.summary.total_checks,
211 report.readiness.summary.ready,
212 report.readiness.summary.degraded,
213 report.readiness.summary.not_ready,
214 report.readiness.summary.unknown
215 );
216 println!("providers:");
217 for provider in &report.providers {
218 println!("- {} ({})", provider.id, provider.contract_version);
219 if let Some(error) = provider.error.as_ref() {
220 println!(
221 " error: {} [{}:{}]",
222 error.message, error.category, error.code
223 );
224 continue;
225 }
226 for capability in &provider.capabilities {
227 if let Some(description) = capability.description.as_deref() {
228 println!(
229 " - {} [{}] {}",
230 capability.name,
231 if capability.available {
232 "available"
233 } else {
234 "unavailable"
235 },
236 description
237 );
238 } else {
239 println!(
240 " - {} [{}]",
241 capability.name,
242 if capability.available {
243 "available"
244 } else {
245 "unavailable"
246 }
247 );
248 }
249 }
250 }
251 println!("automation_tools:");
252 for tool in &report.automation_tools {
253 println!(
254 "- {} ({}) [{}]",
255 tool.id,
256 tool.command,
257 if tool.supports_current_platform {
258 "supported"
259 } else {
260 "unsupported"
261 }
262 );
263 println!(" capabilities: {}", tool.capabilities.join(", "));
264 if let Some(test_mode_env) = tool.test_mode_env.as_deref() {
265 println!(" test_mode_env: {test_mode_env}");
266 }
267 }
268
269 EXIT_OK
270}