1use std::ffi::OsString;
4
5use ready_set_sdk::{
6 CapabilityRelevance, CapabilityReport, CapabilityState, CapabilityVerb, ExitCode, OutputMode,
7};
8
9use crate::capabilities::{
10 CapabilityRegistry, RegisteredCapability, render_human_matrix, render_json_matrix,
11};
12use crate::env::EnvContract;
13use crate::lifecycle::{ReadyInvocation, invoke_ready};
14
15pub fn run(args: &[OsString], contract: &EnvContract) -> ExitCode {
17 let selected = match parse_selected(args) {
18 Ok(selected) => selected,
19 Err(code) => return code,
20 };
21 let cwd = match std::env::current_dir() {
22 Ok(cwd) => cwd,
23 Err(err) => {
24 eprintln!("ready-set ready: cannot read current directory: {err}");
25 return ExitCode::SystemError;
26 },
27 };
28
29 let registry = match CapabilityRegistry::discover(&cwd) {
30 Ok(registry) => registry,
31 Err(err) => {
32 eprintln!("ready-set ready: {err}");
33 return (&err).into();
34 },
35 };
36
37 let capabilities = match select_capabilities(®istry, selected.as_deref()) {
38 Ok(capabilities) => capabilities,
39 Err(code) => return code,
40 };
41
42 let mut reports = Vec::new();
43 for capability in capabilities {
44 match build_report(capability, contract) {
45 Ok(report) => reports.push(report),
46 Err(err) => {
47 eprintln!("ready-set ready: {err}");
48 return ExitCode::SystemError;
49 },
50 }
51 }
52
53 match contract.output {
54 OutputMode::Json => match render_json_matrix(&reports) {
55 Ok(json) => println!("{json}"),
56 Err(err) => {
57 eprintln!("ready-set ready: failed to serialize JSON report: {err}");
58 return ExitCode::SystemError;
59 },
60 },
61 OutputMode::Human => print!("{}", render_human_matrix(&reports)),
62 }
63
64 if reports.iter().any(required_report_failed) {
65 ExitCode::UserError
66 } else {
67 ExitCode::Ok
68 }
69}
70
71fn select_capabilities<'a>(
72 registry: &'a CapabilityRegistry,
73 selected: Option<&str>,
74) -> Result<Vec<&'a RegisteredCapability>, ExitCode> {
75 if let Some(id) = selected {
76 let Some(capability) = registry
77 .capabilities()
78 .iter()
79 .find(|capability| capability.id.as_str() == id)
80 else {
81 eprintln!("ready-set ready: unknown capability `{id}`");
82 return Err(ExitCode::UserError);
83 };
84 if !capability.verbs.contains(&CapabilityVerb::Ready) {
85 eprintln!("ready-set ready: capability `{id}` does not support ready");
86 return Err(ExitCode::UserError);
87 }
88 return Ok(vec![capability]);
89 }
90
91 Ok(registry.capabilities().iter().collect())
92}
93
94fn parse_selected(args: &[OsString]) -> Result<Option<String>, ExitCode> {
95 match args {
96 [] => Ok(None),
97 [capability] => Ok(Some(capability.to_string_lossy().into_owned())),
98 _ => {
99 eprintln!("ready-set ready: expected at most one capability");
100 Err(ExitCode::UserError)
101 },
102 }
103}
104
105fn build_report(
106 capability: &RegisteredCapability,
107 contract: &EnvContract,
108) -> std::io::Result<CapabilityReport> {
109 if capability.relevance == CapabilityRelevance::NotNeeded {
110 return Ok(placeholder_report(
111 capability,
112 CapabilityState::NotNeeded,
113 "capability marked not needed",
114 ));
115 }
116 if !capability.verbs.contains(&CapabilityVerb::Ready) {
117 return Ok(placeholder_report(
118 capability,
119 unavailable_state(capability.relevance),
120 "capability does not support ready",
121 ));
122 }
123
124 match invoke_ready(&capability.provider, capability.id.as_str(), contract)? {
125 ReadyInvocation::Report(report) => Ok(apply_registry_metadata(capability, report)),
126 ReadyInvocation::ProviderUnavailable { summary }
127 | ReadyInvocation::ProviderFailed { summary } => Ok(placeholder_report(
128 capability,
129 unavailable_state(capability.relevance),
130 summary,
131 )),
132 }
133}
134
135fn apply_registry_metadata(
136 capability: &RegisteredCapability,
137 mut report: CapabilityReport,
138) -> CapabilityReport {
139 report.id = capability.id.clone();
140 capability.title.clone_into(&mut report.title);
141 report.provider = capability.provider.clone();
142 report.relevance = capability.relevance;
143 if capability.relevance == CapabilityRelevance::Optional
144 && !matches!(
145 report.state,
146 CapabilityState::Ready | CapabilityState::NotNeeded
147 )
148 {
149 report.state = CapabilityState::Optional;
150 }
151 report
152}
153
154fn placeholder_report(
155 capability: &RegisteredCapability,
156 state: CapabilityState,
157 summary: impl Into<String>,
158) -> CapabilityReport {
159 CapabilityReport {
160 id: capability.id.clone(),
161 title: capability.title.clone(),
162 provider: capability.provider.clone(),
163 state,
164 relevance: capability.relevance,
165 summary: summary.into(),
166 next_action: None,
167 }
168}
169
170const fn unavailable_state(relevance: CapabilityRelevance) -> CapabilityState {
171 match relevance {
172 CapabilityRelevance::Required => CapabilityState::Blocked,
173 CapabilityRelevance::Optional => CapabilityState::Optional,
174 CapabilityRelevance::NotNeeded => CapabilityState::NotNeeded,
175 }
176}
177
178fn required_report_failed(report: &CapabilityReport) -> bool {
179 report.relevance == CapabilityRelevance::Required
180 && !matches!(
181 report.state,
182 CapabilityState::Ready | CapabilityState::Optional | CapabilityState::NotNeeded
183 )
184}
185
186#[cfg(test)]
187mod tests {
188 use ready_set_sdk::{CapabilityId, CapabilityVerb, ProviderId};
189
190 use super::*;
191
192 fn capability(relevance: CapabilityRelevance) -> RegisteredCapability {
193 RegisteredCapability {
194 id: CapabilityId::from("formatting"),
195 title: "Formatting".into(),
196 provider: ProviderId::from("missing-provider"),
197 verbs: vec![CapabilityVerb::Ready],
198 relevance,
199 }
200 }
201
202 fn manifest(
203 capabilities: Vec<ready_set_sdk::CapabilityDescriptor>,
204 ) -> ready_set_sdk::manifest::Manifest {
205 ready_set_sdk::manifest::Manifest {
206 description: "test".into(),
207 version: "0.1.0".parse().unwrap(),
208 stability: ready_set_sdk::describe::Stability::Stable,
209 min_dispatcher_version: "0.1.0".parse().unwrap(),
210 platforms: vec![ready_set_sdk::describe::Platform::current().unwrap()],
211 requires_cargo_workspace: false,
212 capabilities,
213 }
214 }
215
216 fn descriptor(id: &str, verbs: Vec<CapabilityVerb>) -> ready_set_sdk::CapabilityDescriptor {
217 ready_set_sdk::CapabilityDescriptor {
218 id: id.into(),
219 title: id.into(),
220 provider: "provider".into(),
221 verbs,
222 default_relevance: CapabilityRelevance::Required,
223 }
224 }
225
226 #[test]
227 fn required_unavailable_provider_fails_readiness() {
228 let report = placeholder_report(
229 &capability(CapabilityRelevance::Required),
230 unavailable_state(CapabilityRelevance::Required),
231 "provider missing",
232 );
233
234 assert_eq!(report.state, CapabilityState::Blocked);
235 assert!(required_report_failed(&report));
236 }
237
238 #[test]
239 fn optional_unavailable_provider_does_not_fail_readiness() {
240 let report = placeholder_report(
241 &capability(CapabilityRelevance::Optional),
242 unavailable_state(CapabilityRelevance::Optional),
243 "provider missing",
244 );
245
246 assert_eq!(report.state, CapabilityState::Optional);
247 assert!(!required_report_failed(&report));
248 }
249
250 #[test]
251 fn not_needed_short_circuits_provider_lookup() {
252 let report = placeholder_report(
253 &capability(CapabilityRelevance::NotNeeded),
254 CapabilityState::NotNeeded,
255 "capability marked not needed",
256 );
257
258 assert_eq!(report.state, CapabilityState::NotNeeded);
259 assert!(!required_report_failed(&report));
260 }
261
262 #[test]
263 fn explicit_capability_must_support_ready() {
264 let registry = CapabilityRegistry::from_parts(
265 None,
266 [manifest(vec![descriptor(
267 "setup",
268 vec![CapabilityVerb::Set],
269 )])],
270 );
271
272 assert!(select_capabilities(®istry, Some("setup")).is_err());
273 }
274
275 #[test]
276 fn readyless_capability_reports_blocked_without_invocation() {
277 let capability = RegisteredCapability {
278 id: CapabilityId::from("setup"),
279 title: "Setup".into(),
280 provider: ProviderId::from("missing-provider"),
281 verbs: vec![CapabilityVerb::Set],
282 relevance: CapabilityRelevance::Required,
283 };
284
285 let contract = EnvContract {
286 dispatcher_version: semver::Version::new(0, 1, 0),
287 project_root: None,
288 config_path: None,
289 output: OutputMode::Human,
290 log: ready_set_sdk::context::LogLevel::Normal,
291 color: ready_set_sdk::context::ColorMode::Auto,
292 };
293 let report = build_report(&capability, &contract).unwrap();
294
295 assert_eq!(report.state, CapabilityState::Blocked);
296 assert_eq!(report.summary, "capability does not support ready");
297 }
298}