← Commit history

sync latest source from canonical working tree (rewired Send-to-KiCad)

John Lauer ·0b9372f513 ·3mo ago ·parent 747b3ef
1 file changed +103−9
src/deliver.rs+103−9
@@ -9,7 +9,16 @@ use std::path::Path; use std::process::Command;  fn ad(verb: &str, payload: Value) -> Result<Value, String> {-    let out = Command::new("adom-desktop")+    let mut cmd = Command::new("adom-desktop");+    // When the relay has >1 client (e.g. laptop + a cloud VM), a bare verb is+    // ambiguous. Honor ADOM_DESKTOP_TARGET to pin the host; single-client setups+    // need nothing.+    if let Ok(t) = std::env::var("ADOM_DESKTOP_TARGET") {+        if !t.is_empty() {+            cmd.args(["--target", &t]);+        }+    }+    let out = cmd         .args([verb, &payload.to_string()])         .output()         .map_err(|e| format!("adom-desktop unreachable: {e}"))?;@@ -64,16 +73,29 @@ pub fn deliver(mpn: &str, dir: &Path, target: &str) -> Value {     }      if target == "kicad" {-        let mut steps = Vec::new();-        if sym.is_file() {-            steps.push(ad("send_files", json!({ "filePaths": [sym.to_string_lossy()], "targetApp": "kicad", "destinationFolder": "symbols" })));-            steps.push(ad("kicad_install_library", json!({ "libraryPath": sym.to_string_lossy(), "libraryName": mpn, "libraryType": "symbol" })));+        // Real bridge verbs (verified), not the old send_files theater: the symbol+        // installs into the Adom library AND opens on the Symbol Editor canvas; the+        // footprint installs into the Adom footprint library (deterministic).+        let mut results: Vec<(&str, Result<Value, String>)> = Vec::new();+        if let Ok(content) = std::fs::read(&sym) {+            results.push(("symbol", ad("kicad_install_symbol", json!({+                "fileName": format!("{mpn}.kicad_sym"),+                "fileContent": b64(&content),+                "symbolName": mpn,+                "openEditor": true,+                "timeoutSeconds": 60,+            }))));         }-        if md.is_file() {-            steps.push(ad("send_files", json!({ "filePaths": [md.to_string_lossy()], "targetApp": "kicad", "destinationFolder": "footprints" })));-            steps.push(ad("kicad_install_library", json!({ "libraryPath": md.to_string_lossy(), "libraryName": mpn, "libraryType": "footprint" })));+        if let Ok(content) = std::fs::read(&md) {+            results.push(("footprint", ad("kicad_install_footprint", json!({+                "fileName": format!("{mpn}.kicad_mod"),+                "fileContent": b64(&content),+                "footprintName": mpn,+                "openEditor": false,+                "timeoutSeconds": 60,+            }))));         }-        return summarize("KiCad", "symbol + footprint", &steps);+        return summarize_kicad_chip(mpn, dir, results);     }      let (tool, app, folder) = match target {@@ -124,3 +146,75 @@ fn summarize(tool: &str, what: &str, results: &[Result<Value, String>]) -> Value     }     json!({ "ok": true, "target": tool, "message": format!("Delivered {what} to {tool}.") }) }++/// Standard base64 (no external crate) — for sending file content through the bridge.+fn b64(data: &[u8]) -> String {+    const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";+    let mut out = String::with_capacity((data.len() + 2) / 3 * 4);+    for chunk in data.chunks(3) {+        let b0 = chunk[0];+        let b1 = *chunk.get(1).unwrap_or(&0);+        let b2 = *chunk.get(2).unwrap_or(&0);+        out.push(T[(b0 >> 2) as usize] as char);+        out.push(T[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);+        out.push(if chunk.len() > 1 { T[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char } else { '=' });+        out.push(if chunk.len() > 2 { T[(b2 & 0x3f) as usize] as char } else { '=' });+    }+    out+}++/// True if a single bridge response confirms real success (the bridge's `success`+/// flag, or its confirmation text — handles AD wrapping the result in `output`).+fn bridge_ok(r: &Result<Value, String>) -> bool {+    match r {+        Err(_) => false,+        Ok(v) => {+            let s = v.to_string().to_lowercase();+            v.get("success").and_then(|b| b.as_bool()) == Some(true)+                || s.contains("loaded in the symbol editor")+                || (s.contains("added to") && s.contains("library"))+                || (s.contains("installed") && s.contains("library"))+        }+    }+}++/// A descriptive paste-into-Claude prompt — always offered so the user can finish+/// the delivery in their own AI thread if anything is off.+fn claude_prompt_chip(mpn: &str, dir: &Path) -> String {+    format!(+        "I fetched the KiCad CAD for chip \"{mpn}\" (files in `{}`: {mpn}.kicad_sym and {mpn}.kicad_mod). Please \+         install them into my KiCad \"Adom\" symbol + footprint libraries and open the symbol in the KiCad Symbol \+         Editor so I can see it — use the adom-desktop KiCad bridge (`kicad_install_symbol` with openEditor:true, \+         and `kicad_install_footprint`), then confirm they actually landed.",+        dir.display()+    )+}++/// Summarize the chip's KiCad delivery across symbol + footprint. Verifies REAL+/// per-artifact success and always includes a paste-into-Claude prompt fallback.+fn summarize_kicad_chip(mpn: &str, dir: &Path, results: Vec<(&str, Result<Value, String>)>) -> Value {+    let prompt = claude_prompt_chip(mpn, dir);+    // Offline / unreachable short-circuit (clear, actionable).+    for (_what, r) in &results {+        let offline = match r {+            Err(e) => e.contains("unreachable"),+            Ok(v) => looks_offline(v),+        };+        if offline {+            return json!({ "ok": false, "target": "KiCad", "offline": true, "claudePrompt": prompt,+                "message": "Adom Desktop isn't connected. Open it on your laptop and retry — or paste the prompt into Claude." });+        }+    }+    let delivered: Vec<&str> = results.iter().filter(|(_, r)| bridge_ok(r)).map(|(w, _)| *w).collect();+    let failed: Vec<&str> = results.iter().filter(|(_, r)| !bridge_ok(r)).map(|(w, _)| *w).collect();+    if failed.is_empty() && !delivered.is_empty() {+        json!({ "ok": true, "target": "KiCad", "claudePrompt": prompt, "delivered": delivered,+            "message": format!("Delivered {} for {mpn} to KiCad (symbol opened in the Symbol Editor).", delivered.join(" + ")) })+    } else {+        json!({ "ok": false, "target": "KiCad", "claudePrompt": prompt,+            "delivered": delivered, "failed": failed,+            "message": format!("KiCad delivery didn't fully confirm (ok: {}; unconfirmed: {}). Paste the prompt into Claude to finish it.",+                if delivered.is_empty() { "none".into() } else { delivered.join("+") },+                if failed.is_empty() { "none".into() } else { failed.join("+") }) })+    }+}