app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
← Commit history
1.0.52: 1.0.52: wiki #117: kicad_prewarm, declared as bridge.json prewarm.verb (requiresHostApp), which Adom Bridge 2.1.153 calls once after each install or update: detects KiCad (warming kicad-cli), seeds the first-run config, marks KiCad's setup wizard done, repairs the taskbar shortcuts; idempotent, config steps skipped while KiCad runs. Wiki #116: the modal-dialog hints Adom Bridge's CLI used to append now come from the bridge itself (_modalHint on the open and install verbs and on kicad_window_info, which says whether a dialog is up). And bridge.json now declares all 119 verbs; it had drifted to 69, and a unit test now fails the build if it drifts again.
5 files changed
+83−4
rust/Cargo.lock+3−3@@ -244,7 +244,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.51"+version = "1.0.52" dependencies = [ "kicad-core", "kicad-platform",@@ -255,7 +255,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.51"+version = "1.0.52" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -281,7 +281,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.51"+version = "1.0.52" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.51"+version = "1.0.52" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/catalog.rs+18@@ -204,3 +204,21 @@ pub fn describe(version: &str) -> Value { "_hint": "Native bridge: headless, schematic edit and live routing verbs are served here. Window, show and demo verbs arrive with phase 3; until then the Python bridge serves them.", }) }++#[cfg(test)]+mod manifest_tests {+ /// bridge.json is what Adom Bridge reads to know this bridge's verbs. It had drifted to 69 of+ /// 118 (wiki #116): every verb the catalog serves must be declared, and nothing else.+ #[test]+ fn bridge_json_declares_exactly_the_catalog() {+ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../bridge.json");+ let doc: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();+ let mut declared: Vec<String> = doc["verbs"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect();+ let mut served: Vec<String> = super::all().iter().map(|v| v.name.to_string()).collect();+ declared.sort(); declared.dedup(); served.sort(); served.dedup();+ let missing: Vec<&String> = served.iter().filter(|v| !declared.contains(v)).collect();+ let stale: Vec<&String> = declared.iter().filter(|v| !served.contains(v)).collect();+ assert!(missing.is_empty() && stale.is_empty(), "bridge.json verbs out of sync: missing {missing:?}, not served {stale:?}");+ assert_eq!(doc["prewarm"]["verb"], "kicad_prewarm");+ }+}
rust/crates/kicad-bridge/src/verbs.rs+21@@ -48,6 +48,7 @@ pub fn dispatch(state: &mut State, command_in: &str, args: &Value, _caller: &Cal let _driving = crate::taskbar::note_driving(command, verb.mechanism.as_str()); // Verb groups ported later live in their own files; each returns Some when it owns the verb. if let Some(mut out) = crate::groups::dispatch(state, command, args) {+ reply_hints(command, &mut out); prog.attach(&mut out); if let Some(o) = out.as_object_mut() { o.entry("mechanism").or_insert(json!(verb.mechanism.as_str()));@@ -70,6 +71,7 @@ pub fn dispatch(state: &mut State, command_in: &str, args: &Value, _caller: &Cal "kicad_uninstall" => { let info = state.kicad_info(); kicad_core::uninstall::handle(&info.installs, args) } _ => 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."), };+ reply_hints(command, &mut out); prog.attach(&mut out); if let Some(o) = out.as_object_mut() { o.entry("mechanism").or_insert(json!(verb.mechanism.as_str()));@@ -77,6 +79,25 @@ pub fn dispatch(state: &mut State, command_in: &str, args: &Value, _caller: &Cal out } +/// Hints Adom Bridge's CLI used to append to KiCad replies until ab 2.1.145 (wiki #116, adom/adom-bridge#212:+/// ab keeps nothing bridge-specific). They belong to the bridge, which knows it sweeps benign dialogs itself.+pub fn reply_hints(command: &str, out: &mut Value) {+ const OPENS: &[&str] = &["kicad_open_schematic", "kicad_open_board", "kicad_install_symbol", "kicad_install_footprint",+ "kicad_open_footprint_editor", "kicad_open_symbol_editor", "kicad_open_3d_viewer"];+ let ok = out.get("success").and_then(Value::as_bool).unwrap_or(true) && out.get("error").map(|e| e.as_str().unwrap_or("").is_empty()).unwrap_or(true);+ if OPENS.contains(&command) && ok {+ out["_modalHint"] = json!("KiCad can show a modal dialog (save prompt, file warning, format error) that blocks the next command. The bridge closes benign ones itself (see _autoDialogs); for anything else kicad_window_info reports hasModalDialogs, and kicad_screenshot_all captures every window including the dialog so you can read it and decide: kicad_dismiss_dialogs, retry, or close.");+ }+ if command == "kicad_window_info" {+ let modal = out.pointer("/data/hasModalDialogs").and_then(Value::as_bool).unwrap_or(false);+ out["_modalHint"] = json!(if modal {+ "hasModalDialogs is TRUE: something is blocking KiCad. kicad_screenshot_all captures every window including the dialog (or desktop_screenshot_window with the dialog's hwnd from modalDialogs). Read its text, then kicad_dismiss_dialogs, retry, or kicad_close {force:true}."+ } else {+ "No modal dialog is up. If a later verb stalls, call this again: hasModalDialogs turns true while a dialog blocks KiCad."+ });+ }+}+ fn is_sch(args: &Value) -> bool { arg_any(args, &["filePath", "schematicPath"]).map(|p| p.to_ascii_lowercase().ends_with(".kicad_sch")).unwrap_or(false) }
rust/crates/kicad-bridge/src/verbs_maint.rs+40@@ -34,6 +34,16 @@ pub static VERBS: &[Verb] = &[ related: &["kicad_readiness", "kicad_open_board", "kicad_launch"], pitfalls: &["Never stamp Adom identity on KiCad windows: Windows caches the taskbar icon per AUMID string and a bad stamp is permanent (skills/kicad-bridge-dev, THE WHITE-ICON MECHANISM).", "Needs a detected KiCad (the shortcuts are named by major.minor)."], },+ Verb {+ name: "kicad_prewarm",+ summary: "Get this machine ready for a fast first KiCad call: detect KiCad, seed its first-run config, mark its setup wizard done, repair its taskbar shortcuts. Idempotent.",+ mechanism: Mechanism::File, risk: "write", timeout_sec: 120,+ input: "{}",+ example: "kicad_prewarm {}",+ hint: "Adom Bridge calls this once per installed bridge version, in the background, about 20 s after the first boot that follows an install or update (bridge.json prewarm.verb, ab 2.1.153); bridge_prewarm {\"name\":\"kicad\",\"force\":true} runs it again. Everything it does is what the first real verb would otherwise pay for: kicad-cli is run once (detection), and the config steps only happen while no KiCad is running, because KiCad rewrites its own settings on exit. Never downloads KiCad: kicad_upgrade does that, and asks first.",+ related: &["kicad_readiness", "kicad_taskbar_repair", "kicad_upgrade"],+ pitfalls: &["With KiCad running the config steps are skipped (reported), not forced: run it again with KiCad closed.", "No KiCad installed answers ok with nothing done; bridge.json declares requiresHostApp so ab normally skips such a box."],+ }, Verb { name: "kicad_model_check", summary: "Read-only 3D model diagnostics for a footprint or a whole board: which (model ...) references exist, where KiCad looks, whether each file exists and holds geometry.",@@ -139,6 +149,7 @@ pub static VERBS: &[Verb] = &[ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> { let out = match command { "kicad_taskbar_repair" => taskbar_repair(&state.kicad_info().version),+ "kicad_prewarm" => prewarm(), "kicad_3d_viewer_settings" => { let ctx = Ctx::from_info(&state.kicad_info()); let sets = model_check::VIEWER_FLAGS.iter().any(|(k, _, _)| args.get(*k).map(|v| !v.is_null()).unwrap_or(false));@@ -191,6 +202,35 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> /// Wiki: the white taskbar icon. Also run once at boot from main.rs.+/// `kicad_prewarm` (wiki #117): everything a first real verb would otherwise wait for, once.+pub fn prewarm() -> Value {+ let started = std::time::Instant::now();+ let info = kicad_core::detect::detect(); // runs kicad-cli version: the CLI is warm afterwards+ let mut steps = vec![json!({"step": "detect", "ok": true, "installs": info.installs.iter().map(|i| json!({"version": i.version, "cli": i.cli_version})).collect::<Vec<_>>()})];+ if info.installs.is_empty() {+ return json!({"success": true, "ok": true, "warmed": false, "steps": steps, "seconds": started.elapsed().as_secs_f64(),+ "output": "No KiCad on this machine: nothing to prewarm.",+ "_hint": "kicad_upgrade installs KiCad (it asks the user first); prewarm again afterwards."});+ }+ let running = !native().processes(&["kicad.exe", "pcbnew.exe", "eeschema.exe"]).unwrap_or_default().is_empty();+ for inst in &info.installs {+ let Some(cfg) = native().config_dir(&inst.version) else { continue };+ if running {+ steps.push(json!({"step": "config", "version": inst.version, "ok": true, "skipped": "KiCad is running; it rewrites its own settings on exit"}));+ continue;+ }+ let seeded = kicad_core::kicad_common::seed_first_run(&cfg);+ let wizard = kicad_core::kicad_common::mark_first_run_done(&cfg);+ steps.push(json!({"step": "config", "version": inst.version, "ok": seeded.errors.is_empty() && wizard.is_ok(),+ "written": seeded.written, "merged": seeded.merged, "setupWizardMarkedDone": wizard.as_ref().ok(), "errors": seeded.errors, "wizardError": wizard.err()}));+ }+ let tb = taskbar_repair(&info.version);+ steps.push(json!({"step": "taskbar", "ok": tb["success"].as_bool().unwrap_or(false), "written": tb["written"], "already": tb["already"], "missing": tb["missing"]}));+ let ok = steps.iter().all(|s| s["ok"].as_bool().unwrap_or(false));+ json!({"success": ok, "ok": ok, "warmed": true, "steps": steps, "seconds": (started.elapsed().as_secs_f64() * 100.0).round() / 100.0,+ "output": format!("KiCad {} ready: CLI warm, first-run config seeded, setup wizard marked done, taskbar shortcuts checked{}.", info.version, if running { " (config skipped: KiCad is running)" } else { "" })})+}+ pub fn taskbar_repair(version: &str) -> Value { let mm: String = version.split('.').take(2).collect::<Vec<_>>().join("."); if mm.is_empty() || !mm.contains('.') {