speccoder_runtime/
copy.rs1use std::io::Write as _;
8use std::process::{Command, Stdio};
9
10use speccoder_core::CommandResult;
11
12#[must_use]
14pub fn block(result: &CommandResult, run: Option<&str>) -> String {
15 let mut out = match result.code() {
16 Some(code) => format!(
17 "{}[{}]: {}\n",
18 code.kind.prefix(),
19 code.name,
20 result.message()
21 ),
22 None => format!("{}\n", result.message()),
23 };
24 for f in result.fix() {
25 out.push_str(&format!("fix: {}\n", f.trim_start_matches("→ ")));
26 }
27 if let Some(code) = result.code() {
28 out.push_str(&format!("docs: {}\n", code.docs()));
29 }
30 out.push_str(&format!(
31 "speccoder {} · {} {}",
32 env!("CARGO_PKG_VERSION"),
33 std::env::consts::OS,
34 std::env::consts::ARCH
35 ));
36 if let Some(run) = run.map(str::to_owned).or_else(|| run_of(result)) {
37 out.push_str(&format!(" · run {run}"));
38 }
39 out.push('\n');
40 crate::doctor::redact(&out)
41}
42
43fn run_of(result: &CommandResult) -> Option<String> {
45 let v = serde_json::to_value(result).ok()?;
46 v["data"]["run_id"]
47 .as_str()
48 .or_else(|| v["data"]["run"].as_str())
49 .filter(|r| !r.is_empty())
50 .map(str::to_owned)
51}
52
53const CLIPBOARDS: &[(&str, &[&str])] = &[
55 ("pbcopy", &[]),
56 ("wl-copy", &[]),
57 ("xclip", &["-selection", "clipboard"]),
58];
59
60pub fn to_clipboard(text: &str) -> Result<&'static str, String> {
66 for (program, args) in CLIPBOARDS {
67 let Ok(mut child) = Command::new(program)
68 .args(*args)
69 .stdin(Stdio::piped())
70 .stdout(Stdio::null())
71 .stderr(Stdio::null())
72 .spawn()
73 else {
74 continue;
75 };
76 let wrote = child
77 .stdin
78 .take()
79 .is_some_and(|mut stdin| stdin.write_all(text.as_bytes()).is_ok());
80 if wrote && child.wait().is_ok_and(|s| s.success()) {
81 return Ok(program);
82 }
83 }
84 Err("no clipboard program took it (pbcopy, wl-copy or xclip)".to_owned())
85}
86
87#[cfg(test)]
88mod tests {
89 use speccoder_core::CommandStatus;
90
91 use super::*;
92
93 #[test]
94 fn the_block_names_the_code_its_fixes_and_where_it_happened() {
95 let r = CommandResult::new("run", CommandStatus::ConfigError, "no API key")
96 .with_code("KEY-MISSING")
97 .with_fix(vec![
98 "export ANTHROPIC_API_KEY=…".to_owned(),
99 "→ or set it in Settings".to_owned(),
100 ]);
101 let b = block(&r, Some("r-42"));
102 let lines: Vec<&str> = b.lines().collect();
103 assert_eq!(lines[0], "error[KEY-MISSING]: no API key");
104 assert_eq!(lines[1], "fix: export ANTHROPIC_API_KEY=…");
105 assert_eq!(lines[2], "fix: or set it in Settings");
106 assert!(lines[3].starts_with("docs: https://"), "{b}");
107 assert!(
108 lines[4].starts_with(&format!("speccoder {}", env!("CARGO_PKG_VERSION")))
109 && lines[4].ends_with(" · run r-42"),
110 "{b}"
111 );
112 }
113
114 #[test]
115 fn a_masked_value_stays_masked() {
116 crate::secrets::register("COPY_TEST_TOKEN", "copy-block-Zq9 value");
117 let message = crate::secrets::mask("the provider said: copy-block-Zq9 value is wrong");
118 let r = CommandResult::new("run", CommandStatus::RunFailed, &message)
119 .with_fix(vec!["sk-ant-api03-abcdefghijklmnopq".to_owned()]);
120 let b = block(&r, None);
121 assert!(!b.contains("copy-block-Zq9"), "{b}");
122 assert!(b.contains("[redacted:COPY_TEST_TOKEN]"), "{b}");
123 assert!(!b.contains("abcdefghijklmnopq"), "{b}");
124 }
125}