1use std::path::Path;
8
9use owo_colors::OwoColorize;
10use serde_json::Value;
11
12use crate::client::CellosClient;
13use crate::exit::{CtlError, CtlResult};
14use crate::model::Formation;
15
16pub async fn run(client: &CellosClient, path: &Path) -> CtlResult<()> {
17 let yaml = std::fs::read_to_string(path)
18 .map_err(|e| CtlError::usage(format!("read {}: {e}", path.display())))?;
19 let local: Value = serde_yaml::from_str(&yaml)?;
20
21 let local_name = extract_name(&local).ok_or_else(|| {
22 CtlError::validation(format!(
23 "{}: cannot find formation name (`name` or `metadata.name`)",
24 path.display()
25 ))
26 })?;
27
28 let remote: Formation = match client
29 .get_json(&format!("/v1/formations/{}", urlencode(&local_name)))
30 .await
31 {
32 Ok(f) => f,
33 Err(e) if matches!(e.kind, crate::exit::ErrorKind::Api) => {
34 println!("(formation/{local_name} not found on server — would create)");
36 for line in serde_yaml::to_string(&local).unwrap_or_default().lines() {
37 println!("{}", format!("+ {line}").green());
38 }
39 return Ok(());
40 }
41 Err(e) => return Err(e),
42 };
43
44 let local_pretty = serde_json::to_string_pretty(&local).unwrap_or_default();
45 let remote_pretty = serde_json::to_string_pretty(&remote).unwrap_or_default();
46 print_unified_diff(&remote_pretty, &local_pretty);
47 Ok(())
48}
49
50fn extract_name(v: &Value) -> Option<String> {
51 if let Some(n) = v.get("name").and_then(|x| x.as_str()) {
52 return Some(n.to_string());
53 }
54 v.get("metadata")
55 .and_then(|m| m.get("name"))
56 .and_then(|n| n.as_str())
57 .map(|s| s.to_string())
58}
59
60fn print_unified_diff(a: &str, b: &str) {
64 println!("--- server (current)");
65 println!("+++ local (proposed)");
66 println!(
67 "# (cellctl diff is a shallow line comparison; for fine-grained review use `diff -u`)"
68 );
69 let a_lines: Vec<&str> = a.lines().collect();
70 let b_lines: Vec<&str> = b.lines().collect();
71 let n = a_lines.len().max(b_lines.len());
72 for i in 0..n {
73 let al = a_lines.get(i).copied();
74 let bl = b_lines.get(i).copied();
75 match (al, bl) {
76 (Some(x), Some(y)) if x == y => println!(" {x}"),
77 (Some(x), Some(y)) => {
78 println!("{}", format!("- {x}").red());
79 println!("{}", format!("+ {y}").green());
80 }
81 (Some(x), None) => println!("{}", format!("- {x}").red()),
82 (None, Some(y)) => println!("{}", format!("+ {y}").green()),
83 (None, None) => {}
84 }
85 }
86}
87
88fn urlencode(s: &str) -> String {
89 url::form_urlencoded::byte_serialize(s.as_bytes()).collect()
90}