app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
rust/src/verbs.rsdeleted−211@@ -1,211 +0,0 @@-//! Verb handlers for phase 1 (headless). Window, routing and demo verbs come in later phases.--use std::path::PathBuf;-use std::time::Duration;--use serde_json::{json, Value};--use crate::catalog;-use crate::cli;-use crate::detect::{self, KicadInfo};-use crate::Caller;--pub struct State {- info: Option<(std::time::Instant, KicadInfo)>,-}--impl State {- pub fn new() -> Self {- State { info: None }- }- /// Detection is cached for 60 s; a verb that needs fresh truth calls refresh().- pub fn kicad_info(&mut self) -> KicadInfo {- if let Some((t, ref i)) = self.info {- if t.elapsed() < Duration::from_secs(60) {- return i.clone();- }- }- self.refresh()- }- pub fn refresh(&mut self) -> KicadInfo {- let i = detect::detect();- self.info = Some((std::time::Instant::now(), i.clone()));- i- }-}--fn fail(code: &str, msg: impl Into<String>, hint: impl Into<String>) -> Value {- json!({"success": false, "errorCode": code, "error": msg.into(), "_hint": hint.into()})-}--fn arg_str<'a>(args: &'a Value, key: &str) -> Option<&'a str> {- args.get(key).and_then(Value::as_str).filter(|s| !s.trim().is_empty())-}--fn alternatives() -> Value {- json!([- {"backend": "service", "what": "The shared service-kicad container runs DRC, ERC, exports, renders and library lookups with no desktop KiCad.", "hint": "Load the service-kicad skill; pass backend:\"service\" to the headless verbs."},- {"backend": "container", "what": "Install kicad-cli into your own container.", "hint": "The service-kicad skill has the install; then the headless verbs run locally."},- {"backend": "target", "what": "Run KiCad on another box (a second laptop, a conference room machine, a Grok Computer session) and address it by name through Adom Bridge.", "hint": "adom-bridge targets lists the boxes; add --target <name> to every call."}- ])-}--pub fn dispatch(state: &mut State, command: &str, args: &Value, _caller: &Caller) -> Value {- let Some(verb) = catalog::find(command) else {- let names: Vec<&str> = catalog::VERBS.iter().map(|v| v.name).collect();- return fail("unknown_verb", format!("Unknown command: {command}"), format!("Valid commands: {}", names.join(", ")));- };- let mut out = match command {- "kicad_describe" => catalog::describe(crate::VERSION),- "kicad_status" => status(state),- "kicad_readiness" => readiness(state),- "kicad_list_versions" => list_versions(state),- "kicad_diagnostics" => diagnostics(state),- "kicad_run_drc" => run_check(state, args, true),- "kicad_run_erc" => run_check(state, args, false),- "kicad_uninstall" => uninstall(),- _ => fail("not_implemented", format!("{command} is catalogued but not implemented in this phase"), "Use the Python bridge for this verb until the phase that ports it lands."),- };- if let Some(o) = out.as_object_mut() {- o.entry("mechanism").or_insert(json!(verb.mechanism.as_str()));- }- out-}--fn status(state: &mut State) -> Value {- let info = state.kicad_info();- json!({- "success": true,- "bridgeVersion": crate::VERSION,- "runtime": "native",- "installed": info.installed,- "kicadVersion": info.version,- "install": info.primary(),- "_hint": if info.installed { "KiCad found. Headless verbs (run_drc, run_erc) are ready." } else { "KiCad not found here. kicad_readiness lists the alternatives." },- })-}--fn readiness(state: &mut State) -> Value {- let info = state.refresh();- let mut v = json!({- "success": true,- "installed": info.installed,- "ready": info.installed,- "kicadVersion": info.version,- "installs": info.installs,- "configDir": info.primary().and_then(|p| detect::config_dir(&p.version)).map(|p| p.to_string_lossy().replace('\\', "/")),- "runtime": {"kind": "native", "provisioned": false},- });- if !info.installed {- v["errorCode"] = json!("not_installed");- v["alternatives"] = alternatives();- v["_hint"] = json!("No KiCad on this box. Pick one of `alternatives`, or call kicad_upgrade to install KiCad silently (asks the user first).");- } else {- v["_hint"] = json!("Ready. Headless verbs need nothing else; window verbs are served by the Python bridge until phase 3.");- }- v-}--fn list_versions(state: &mut State) -> Value {- let info = state.refresh();- json!({"success": true, "count": info.installs.len(), "installs": info.installs, "primary": info.primary()})-}--fn diagnostics(state: &mut State) -> Value {- let info = state.kicad_info();- let keys = ["USERPROFILE", "APPDATA", "LOCALAPPDATA", "ProgramFiles", "ProgramFiles(x86)", "ADOM_BIND_HOST", "ADOM_DIRECT_API_URL", "HOME", "PATH"];- let env: serde_json::Map<String, Value> = keys- .iter()- .map(|k| (k.to_string(), json!(std::env::var(k).ok())))- .collect();- json!({- "success": true,- "bridgeVersion": crate::VERSION,- "runtime": "native",- "exe": std::env::current_exe().ok().map(|p| p.to_string_lossy().replace('\\', "/")),- "cwd": std::env::current_dir().ok().map(|p| p.to_string_lossy().replace('\\', "/")),- "os": std::env::consts::OS,- "arch": std::env::consts::ARCH,- "env": env,- "kicad": info,- "configDir": info.primary().and_then(|p| detect::config_dir(&p.version)).map(|p| p.to_string_lossy().replace('\\', "/")),- })-}--fn run_check(state: &mut State, args: &Value, drc: bool) -> Value {- let key = if drc { "boardPath" } else { "schematicPath" };- let Some(path) = arg_str(args, key) else {- return fail("missing_arg", format!("{key} is required"), format!("Pass the absolute path of the {} file.", if drc { ".kicad_pcb" } else { ".kicad_sch" }));- };- let path = PathBuf::from(path);- if !path.is_file() {- return fail("file_not_found", format!("{} does not exist", path.display()), "Check the path; the bridge runs on the desktop, so use the desktop's path, not the container's.");- }- let backend = arg_str(args, "backend").unwrap_or("local");- if backend != "local" {- return fail("backend_not_implemented", format!("backend {backend} lands with phase 1b"), "Use backend local for now, or run service-kicad from the container directly.");- }- let info = state.kicad_info();- let Some(inst) = info.primary() else {- let mut v = fail("not_installed", "KiCad is not installed on this box", "See alternatives.");- v["alternatives"] = alternatives();- return v;- };- let cli_path = PathBuf::from(&inst.kicad_cli);- let report = std::env::temp_dir().join(format!("adom-kicad-{}-{}.json", if drc { "drc" } else { "erc" }, std::process::id()));- let report_s = report.to_string_lossy().to_string();- let path_s = path.to_string_lossy().to_string();- let mut argv: Vec<&str> = if drc {- vec!["pcb", "drc", "--format", "json", "--severity-all", "--all-track-errors"]- } else {- vec!["sch", "erc", "--format", "json", "--severity-all"]- };- if drc && args.get("refillZones").and_then(Value::as_bool).unwrap_or(false) {- argv.push("--refill-zones");- }- argv.extend(["--output", report_s.as_str(), path_s.as_str()]);- let r = cli::run(&cli_path, &argv, Duration::from_secs(280));- let parsed = std::fs::read_to_string(&report).ok().and_then(|s| serde_json::from_str::<Value>(&s).ok());- let _ = std::fs::remove_file(&report);- match parsed {- Some(rep) => {- let summary = cli::summarize_report(&rep);- json!({- "success": true,- "check": if drc { "drc" } else { "erc" },- "path": path_s,- "summary": summary,- "cliExitCode": r.exit_code,- "cliElapsedMs": r.elapsed_ms,- "cliVersion": inst.cli_version,- "report": rep,- "_hint": if drc { "summary.clean is true only with zero errors and zero unconnected items; warnings are listed but do not fail the board." } else { "summary.clean means zero errors; ERC warnings are listed." },- })- }- None => {- let mut v = fail(- if r.timed_out { "cli_timeout" } else { "cli_failed" },- format!("kicad-cli exit {}: {}", r.exit_code, r.stderr.trim()),- "kicad-cli wrote no report. The stderr above usually names the reason (file version, missing library, bad path).",- );- v["stdout"] = json!(r.stdout);- v["cliElapsedMs"] = json!(r.elapsed_ms);- v- }- }-}--fn uninstall() -> Value {- let mut cleaned: Vec<String> = Vec::new();- for name in ["adom-kicad-drc", "adom-kicad-erc"] {- if let Ok(rd) = std::fs::read_dir(std::env::temp_dir()) {- for e in rd.flatten() {- let n = e.file_name().to_string_lossy().to_string();- if n.starts_with(name) && n.ends_with(".json") && std::fs::remove_file(e.path()).is_ok() {- cleaned.push(n);- }- }- }- }- json!({"success": true, "cleaned": cleaned, "_hint": "Only bridge scratch files are removed. KiCad, its settings and user libraries are untouched."})-}