1use std::ffi::OsString;
4
5use clap::Parser;
6use ready_set_sdk::{
7 CapabilityAction, CapabilityActionKind, CapabilityRelevance, CapabilityRunReport,
8 CapabilityVerb, ExitCode, OutputMode, RunStatus,
9};
10
11use crate::capabilities::{CapabilityRegistry, RegisteredCapability};
12use crate::env::EnvContract;
13use crate::lifecycle::{SetInvocation, invoke_set};
14
15#[derive(Debug, Clone, Parser)]
17#[command(name = "ready-set set", about, long_about = None, no_binary_name = true)]
18#[allow(clippy::struct_excessive_bools)]
19struct Options {
20 pub capability: Option<String>,
22
23 #[arg(long)]
25 pub force: bool,
26
27 #[arg(long)]
29 pub dry_run: bool,
30
31 #[arg(long)]
33 pub json: bool,
34
35 #[arg(long)]
37 pub quiet: bool,
38
39 #[arg(long)]
41 pub verbose: bool,
42
43 #[arg(long = "member")]
45 pub members: Vec<String>,
46
47 #[arg(long)]
49 pub no_discover: bool,
50}
51
52pub fn run(args: &[OsString], contract: &EnvContract) -> ExitCode {
54 let opts = match Options::try_parse_from(args) {
55 Ok(opts) => opts,
56 Err(err) => {
57 err.print().ok();
58 return ExitCode::UserError;
59 },
60 };
61 let cwd = match std::env::current_dir() {
62 Ok(cwd) => cwd,
63 Err(err) => {
64 eprintln!("ready-set set: cannot read current directory: {err}");
65 return ExitCode::SystemError;
66 },
67 };
68 let registry = match CapabilityRegistry::discover(&cwd) {
69 Ok(registry) => registry,
70 Err(err) => {
71 eprintln!("ready-set set: {err}");
72 return (&err).into();
73 },
74 };
75
76 let capabilities = match select_capabilities(®istry, opts.capability.as_deref()) {
77 Ok(capabilities) => capabilities,
78 Err(code) => return code,
79 };
80 if capabilities.is_empty() {
81 eprintln!("ready-set set: no required set-capable capabilities found");
82 return ExitCode::UserError;
83 }
84
85 let capture_json = matches!(contract.output, OutputMode::Json) || opts.json;
86 let provider_args = provider_args(&opts);
87 let mut reports = Vec::new();
88 let mut exit_code = ExitCode::Ok;
89
90 for capability in capabilities {
91 if capability.relevance == CapabilityRelevance::NotNeeded {
92 reports.push(noop_report(capability, "capability marked not needed"));
93 continue;
94 }
95
96 match invoke_set(
97 &capability.provider,
98 capability.id.as_str(),
99 &provider_args,
100 contract,
101 capture_json,
102 ) {
103 Ok(SetInvocation::Report(report)) => reports.push(report),
104 Ok(SetInvocation::Streamed { exit_code: code }) => {
105 if code != ExitCode::Ok {
106 exit_code = code;
107 }
108 },
109 Ok(SetInvocation::ProviderUnavailable { summary }) => {
110 eprintln!("ready-set set: {summary}");
111 return ExitCode::UserError;
112 },
113 Ok(SetInvocation::ProviderFailed {
114 exit_code: code,
115 summary,
116 }) => {
117 eprintln!("ready-set set: {summary}");
118 return code;
119 },
120 Err(err) => {
121 eprintln!("ready-set set: {err}");
122 return ExitCode::SystemError;
123 },
124 }
125 }
126
127 if capture_json {
128 match serde_json::to_string(&reports) {
129 Ok(json) => println!("{json}"),
130 Err(err) => {
131 eprintln!("ready-set set: failed to serialize JSON report: {err}");
132 return ExitCode::SystemError;
133 },
134 }
135 }
136
137 exit_code
138}
139
140fn select_capabilities<'a>(
141 registry: &'a CapabilityRegistry,
142 selected: Option<&str>,
143) -> Result<Vec<&'a RegisteredCapability>, ExitCode> {
144 if let Some(id) = selected {
145 let Some(capability) = registry
146 .capabilities()
147 .iter()
148 .find(|capability| capability.id.as_str() == id)
149 else {
150 eprintln!("ready-set set: unknown capability `{id}`");
151 return Err(ExitCode::UserError);
152 };
153 if !capability.verbs.contains(&CapabilityVerb::Set) {
154 eprintln!("ready-set set: capability `{id}` does not support set");
155 return Err(ExitCode::UserError);
156 }
157 return Ok(vec![capability]);
158 }
159
160 Ok(registry
161 .capabilities()
162 .iter()
163 .filter(|capability| {
164 capability.relevance == CapabilityRelevance::Required
165 && capability.verbs.contains(&CapabilityVerb::Set)
166 })
167 .collect())
168}
169
170fn provider_args(opts: &Options) -> Vec<OsString> {
171 let mut args = Vec::new();
172 if opts.dry_run {
173 args.push(OsString::from("--dry-run"));
174 }
175 if opts.force {
176 args.push(OsString::from("--force"));
177 }
178 if opts.no_discover {
179 args.push(OsString::from("--no-discover"));
180 }
181 for member in &opts.members {
182 args.push(OsString::from("--member"));
183 args.push(OsString::from(member));
184 }
185 args
186}
187
188fn noop_report(capability: &RegisteredCapability, summary: &str) -> CapabilityRunReport {
189 CapabilityRunReport {
190 id: capability.id.clone(),
191 verb: CapabilityVerb::Set,
192 status: RunStatus::Noop,
193 actions: vec![CapabilityAction {
194 kind: CapabilityActionKind::Skip,
195 summary: summary.into(),
196 path: None,
197 }],
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn provider_args_preserve_setup_flags() {
207 let opts = Options {
208 capability: Some("workspace".into()),
209 force: true,
210 dry_run: true,
211 json: false,
212 quiet: false,
213 verbose: false,
214 members: vec!["crates/foo".into()],
215 no_discover: true,
216 };
217
218 assert_eq!(
219 provider_args(&opts),
220 vec![
221 OsString::from("--dry-run"),
222 OsString::from("--force"),
223 OsString::from("--no-discover"),
224 OsString::from("--member"),
225 OsString::from("crates/foo"),
226 ]
227 );
228 }
229
230 #[test]
231 fn no_capability_selects_required_set_capabilities_only() {
232 let manifest = ready_set_sdk::manifest::Manifest {
233 description: "test".into(),
234 version: "0.1.0".parse().unwrap(),
235 stability: ready_set_sdk::describe::Stability::Stable,
236 min_dispatcher_version: "0.1.0".parse().unwrap(),
237 platforms: vec![ready_set_sdk::describe::Platform::current().unwrap()],
238 requires_cargo_workspace: false,
239 capabilities: vec![
240 ready_set_sdk::CapabilityDescriptor {
241 id: "required".into(),
242 title: "Required".into(),
243 provider: "provider".into(),
244 verbs: vec![CapabilityVerb::Ready, CapabilityVerb::Set],
245 default_relevance: CapabilityRelevance::Required,
246 },
247 ready_set_sdk::CapabilityDescriptor {
248 id: "optional".into(),
249 title: "Optional".into(),
250 provider: "provider".into(),
251 verbs: vec![CapabilityVerb::Ready, CapabilityVerb::Set],
252 default_relevance: CapabilityRelevance::Optional,
253 },
254 ],
255 };
256 let registry = CapabilityRegistry::from_parts(None, [manifest]);
257 let selected = select_capabilities(®istry, None).unwrap();
258
259 assert_eq!(selected.len(), 1);
260 assert_eq!(selected[0].id.as_str(), "required");
261 }
262
263 #[test]
264 fn explicit_capability_must_support_set() {
265 let manifest = ready_set_sdk::manifest::Manifest {
266 description: "test".into(),
267 version: "0.1.0".parse().unwrap(),
268 stability: ready_set_sdk::describe::Stability::Stable,
269 min_dispatcher_version: "0.1.0".parse().unwrap(),
270 platforms: vec![ready_set_sdk::describe::Platform::current().unwrap()],
271 requires_cargo_workspace: false,
272 capabilities: vec![ready_set_sdk::CapabilityDescriptor {
273 id: "ready-only".into(),
274 title: "Ready only".into(),
275 provider: "provider".into(),
276 verbs: vec![CapabilityVerb::Ready],
277 default_relevance: CapabilityRelevance::Required,
278 }],
279 };
280 let registry = CapabilityRegistry::from_parts(None, [manifest]);
281
282 assert!(select_capabilities(®istry, Some("ready-only")).is_err());
283 }
284}