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.
kicad_uninstall_library: remove a library the bridge registered (inverse of kicad_install_library) #20
feature/uninstall-library → masterWhat
New verb kicad_uninstall_library: the inverse of kicad_install_library. Until now the bridge could register a library but never remove one, so test and scratch libraries pile up in the user's tables (the daily verb test itself registers AdomVerbTest on every run and never removes it).
Requested by John (2026-09-20) and referenced from #111.
Base sha: 14e12dec0247c5863abb202f5a6034a2bdd255ec (1.0.36, the head when pushed). branch compare: 5 files, 191 insertions, 1 deletion; every hunk is additive except the module doc line in verbs_install.rs that lists the group's verbs.
Contract
kicad_uninstall_library {libraryName, libraryType?: "symbol|footprint|both" (default both), deleteFiles?: false, kicadVersion?}
- Removes the named row from the selected version's
sym-lib-tableand/orfp-lib-tableusing the existingLibTable::remove_library+write_file. Repliesremoved[] {table, uri},filesDeleted[],filesKept[] {path, why},kicadRunning,tableReloadNeeded. deleteFiles:truedeletes the installed copy only when it resolves underDocuments/KiCad/<ver>/symbols|footprints(the folders the install verbs write to), plus3dmodels/<name>.3dshapesfor a footprint library. A library registered in place from anywhere else is unregistered and its files are left alone, named infilesKept.- Refusals, each a named errorCode with the next call in
Hint:library_name_required,bad_library_type,adom_library_protected(the sharedAdomlibrary),stock_library_protected(a${KICAD*_..._DIR}row: KiCad's own libraries, never the bridge's to remove),library_not_registered,lib_table_unreadable,lib_table_write_failed. - When KiCad is running the reply says so: the running instance holds its tables in memory, keeps showing the library, and can write the row back when it saves. Same root cause as #111.
Files
rust/crates/kicad-core/src/install.rs:uninstall_library+ 2 unit tests.rust/crates/kicad-bridge/src/verbs_install.rs: catalog entry + dispatch (crate::verbs_show::kicad_running()for the running flag).bridge.json: verb declared.SKILL.md: one table row.skills/kicad-bridge-test/run_verb_tests.py: coverage entry in phase 4, right after the installs; it removes theAdomVerbTestscratch library withdeleteFiles:true, so the daily run cleans up after itself.
Verified
cargo test -p kicad-core --no-default-features --features net: 224 passed, 0 failed (includes the 2 new tests: row removed, managed files deleted, in-place library's files kept,Adom/ stock / unknown / missing name / bad type all refused, stock row still present afterwards).cargo test -p kicad-bridge --no-default-features: 26 passed (catalog consistency).cargo check -p kicad-bridge --target x86_64-pc-windows-gnu --no-default-features: clean.run_verb_tests.pyparses.
NOT verified (needs your native acceptance)
- The full default-feature build and the release exe: this container has no
cmake, sonng-sys(theipcfeature) does not build here. Nothing in this change touchesipc. - The verb against a real KiCad on Windows: not run. In particular, please check on a test box (a) the phase order in
run_verb_tests.pyputsuninstall_libraryafterinstall_libraryas intended, (b)expand_kicad_uri+norm_pathclassify a${KIPRJMOD}-free absolute Windows uri underDocuments/KiCad/<ver>/footprintsas managed, and (c) whetherkicad_running()is the right signal fortableReloadNeeded. - I did not bump the version or touch
RELEASE_NOTES.md, per CONTRIBUTING.
Leftover this would clean
ConfRoomROG currently has an Adom_Check footprint library I registered for an adom-footprint check (KiCad 10.0); I will remove it with this verb once it ships.
Diff Skip to comments (1)
SKILL.md+1@@ -99,6 +99,7 @@ If you pass a version that's not installed, the bridge returns a hint listing th | `kicad_close` | `close` | Close KiCad (all editors). From 0.9.347 an editor with unsaved work is NOT killed: the reply says errorCode unsaved_changes with the dialog text. `discardChanges:true` presses Discard for the caller; `force:true` is the only kill | optional `discardChanges`, `force` | | `kicad_window_info` | `window_info` | Enumerate open KiCad windows (HWND, title, bounds, editor type) |: | | `kicad_install_library` | `install_library` | Register a `.kicad_sym` + `.pretty/` pair in sym-lib-table + fp-lib-table | `libraryPath`, `libraryType`, `libraryName`, optional `kicadVersion` |+| `kicad_uninstall_library` | `uninstall_library` | Remove a library row from sym-lib-table / fp-lib-table (the inverse of install). `deleteFiles:true` also deletes the installed copy, only under `Documents/KiCad/<ver>/`; refuses `Adom` and KiCad's stock libraries | `libraryName`, optional `libraryType` (symbol/footprint/both), `deleteFiles`, `kicadVersion` | | `kicad_install_symbol` | `install_symbol` | Add a single symbol to an existing library | `fileName`, `fileContent`, optional `kicadVersion` | | `kicad_install_footprint` | `install_footprint` | Add a single footprint to an existing library | `library`, `footprint_path`, optional `kicadVersion` | | `kicad_run_drc` | `run_drc` | Run DRC on the current board, return violations | `filePath`, optional `kicadVersion` |
bridge.json+1@@ -36,6 +36,7 @@ "kicad_describe", "kicad_diagnostics", "kicad_install_library",+ "kicad_uninstall_library", "kicad_install_library_bundle", "kicad_install_symbol", "kicad_install_footprint",
rust/crates/kicad-bridge/src/verbs_install.rs+20−1@@ -1,4 +1,4 @@-//! Verb group "install": kicad_install_library, kicad_install_symbol,+//! Verb group "install": kicad_install_library, kicad_uninstall_library, kicad_install_symbol, //! kicad_install_footprint, kicad_install_library_bundle, kicad_adom_library_heal, //! kicad_make_part_project, kicad_export_part. Same arg names and response shapes //! as the Python handlers (`handlers/install_library.py`, `handlers/install_symbol.py`,@@ -38,6 +38,21 @@ pub static VERBS: &[Verb] = &[ "a .kicad_sym KiCad's own parser refuses is not copied or registered (errorCode kicad_refuses_library, wiki #71); without kicad-cli there is no verdict and the install proceeds", ], },+ Verb {+ name: "kicad_uninstall_library",+ summary: "Remove a library from the user's sym-/fp-lib-table: the inverse of kicad_install_library. Optionally deletes the copy the install made.",+ mechanism: Mechanism::File, risk: "write", timeout_sec: 60,+ input: "{\"libraryName\": \"MyParts\", \"libraryType\"?: \"symbol|footprint|both\" (default both), \"deleteFiles\"?: false, \"kicadVersion\"?: \"10.0\"}",+ example: "kicad_uninstall_library {\"libraryName\":\"Adom_Check\",\"deleteFiles\":true}",+ hint: "Removes the named row from the selected KiCad version's lib table(s) and answers removed[] (table + uri). deleteFiles:true also deletes the installed copy, but ONLY under Documents/KiCad/<ver>/symbols|footprints (plus 3dmodels/<name>.3dshapes): a library registered in place from anywhere else is unregistered and its files are listed in filesKept. kicadRunning/tableReloadNeeded tell you when the running KiCad still holds the old table.",+ related: &["kicad_install_library", "kicad_show_library", "kicad_list_versions", "kicad_adom_library_heal"],+ pitfalls: &[+ "refuses the shared 'Adom' library (adom_library_protected) and KiCad's own stock libraries (stock_library_protected): it only removes libraries that were added to the user tables",+ "each KiCad version has its OWN lib tables: uninstall under every version it was installed for",+ "a RUNNING KiCad holds its tables in memory and can write the row back when it saves them: uninstall with KiCad closed, or tell the user to restart it",+ "library names are case-sensitive; library_not_registered means the name is not in that version's table",+ ],+ }, Verb { name: "kicad_install_symbol", summary: "Merge the symbol(s) of a .kicad_sym into the single global Adom library (Adom.kicad_sym), creating and registering it on first use.",@@ -135,6 +150,10 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> let info = state.kicad_info(); install::install_library(&lib_ctx(&info), info.installed, args) }+ "kicad_uninstall_library" => {+ let info = state.kicad_info();+ install::uninstall_library(&lib_ctx(&info), crate::verbs_show::kicad_running(), args)+ } "kicad_install_symbol" => { let info = state.kicad_info(); install::install_symbol(&lib_ctx(&info), info.installed, args)
rust/crates/kicad-core/src/install.rs+167@@ -2018,6 +2018,124 @@ pub fn export_part(ctx: &LibCtx, args: &Value) -> Value { }) } +// ---- uninstall_library ---------------------------------------------------------------------++/// The exact inverse of `install_library`: remove a library's row from the user's+/// sym-lib-table and/or fp-lib-table and, only when asked, delete the files the install copied.+///+/// Safety rules (each refusal is a named errorCode with the next call in the hint):+/// - the shared `Adom` library is never removed here (every Adom part lives in it);+/// - a row whose uri is a KiCad stock path (`${KICAD*_..._DIR}`) is never removed: that is+/// KiCad's own library set, not something the bridge installed;+/// - `deleteFiles` only ever deletes inside `Documents/KiCad/<ver>/{symbols,footprints,3dmodels}`,+/// the folders the install verbs write to. A library registered in place from anywhere else+/// is unregistered and its files are left alone (`filesKept` says why).+pub fn uninstall_library(ctx: &LibCtx, kicad_running: bool, args: &Value) -> Value {+ let name = s_arg(args, "libraryName");+ if name.is_empty() {+ return json!({"success": false, "errorCode": "library_name_required",+ "error": "libraryName is required",+ "Hint": "kicad_uninstall_library {\"libraryName\":\"MyParts\"}; the name is the row name in Preferences > Manage Symbol/Footprint Libraries (kicad_install_library returned it as data.name)."});+ }+ let which = s_arg(args, "libraryType").to_lowercase();+ if !matches!(which.as_str(), "" | "both" | "symbol" | "footprint") {+ return json!({"success": false, "errorCode": "bad_library_type",+ "error": format!("libraryType must be symbol, footprint or both (default), got '{which}'"),+ "Hint": "omit libraryType to remove the name from both tables"});+ }+ if name.eq_ignore_ascii_case("Adom") {+ return json!({"success": false, "errorCode": "adom_library_protected",+ "error": "the shared 'Adom' library is not removed by this verb: every part installed with kicad_install_symbol / kicad_install_footprint lives in it",+ "Hint": "remove one part with the Symbol/Footprint Editor, or repair the library with kicad_adom_library_heal"});+ }+ let delete_files = args.get("deleteFiles").and_then(Value::as_bool).unwrap_or(false);+ let user_dir = ctx.user_dir.clone();+ let mut removed: Vec<Value> = Vec::new();+ let mut files_deleted: Vec<String> = Vec::new();+ let mut files_kept: Vec<Value> = Vec::new();+ let mut stock: Vec<String> = Vec::new();++ let tables: Vec<(&str, Option<PathBuf>)> = vec![("symbol", ctx.sym_lib_table()), ("footprint", ctx.fp_lib_table())];+ for (kind, table_path) in tables {+ if !(which.is_empty() || which == "both" || which == kind) {+ continue;+ }+ let Some(table_path) = table_path else { continue };+ if !table_path.is_file() {+ continue;+ }+ let mut table = match LibTable::parse_file(&table_path) {+ Ok(t) => t,+ Err(e) => return json!({"success": false, "errorCode": "lib_table_unreadable",+ "error": format!("cannot parse {}: {e}", table_path.display()),+ "Hint": "kicad_adom_library_heal repairs a damaged lib table"}),+ };+ let Some(entry) = table.get(&name).cloned() else { continue };+ if entry.uri.contains("${KICAD") && entry.uri.contains("_DIR}") && !entry.uri.contains("3RD_PARTY") {+ stock.push(format!("{kind}: {}", entry.uri));+ continue;+ }+ table.remove_library(&name);+ if let Err(e) = table.write_file(&table_path) {+ return json!({"success": false, "errorCode": "lib_table_write_failed",+ "error": format!("cannot write {}: {e}", table_path.display()),+ "Hint": "close KiCad's Manage Libraries dialog if it is open, then retry"});+ }+ removed.push(json!({"table": if kind == "symbol" { "sym-lib-table" } else { "fp-lib-table" }, "uri": entry.uri}));++ let resolved = crate::libraries::expand_kicad_uri(&entry.uri, ctx).unwrap_or_else(|| entry.uri.clone());+ let target = PathBuf::from(&resolved);+ if !delete_files {+ continue;+ }+ let managed = user_dir.as_ref().map(|u| {+ let sub = u.join(if kind == "symbol" { "symbols" } else { "footprints" });+ crate::libraries::norm_path(&target).starts_with(&crate::libraries::norm_path(&sub))+ }).unwrap_or(false);+ if !managed {+ files_kept.push(json!({"path": resolved, "why": "outside Documents/KiCad/<ver>/symbols|footprints: registered in place, not a copy the bridge made"}));+ continue;+ }+ let gone = if target.is_dir() { std::fs::remove_dir_all(&target).is_ok() } else if target.is_file() { std::fs::remove_file(&target).is_ok() } else { false };+ if gone {+ files_deleted.push(resolved.clone());+ }+ if kind == "footprint" {+ if let Some(u) = &user_dir {+ let models = u.join("3dmodels").join(format!("{name}.3dshapes"));+ if models.is_dir() && std::fs::remove_dir_all(&models).is_ok() {+ files_deleted.push(models.to_string_lossy().replace('\\', "/"));+ }+ }+ }+ }++ if removed.is_empty() {+ if !stock.is_empty() {+ return json!({"success": false, "errorCode": "stock_library_protected", "libraryName": name, "stock": stock,+ "error": format!("'{name}' is one of KiCad's own stock libraries; this verb only removes libraries that were added to the user tables"),+ "Hint": "disable a stock library in Preferences > Manage Libraries instead (the Active checkbox)"});+ }+ return json!({"success": false, "errorCode": "library_not_registered", "libraryName": name,+ "error": format!("no library named '{name}' in the {} lib table(s) of KiCad {}", if which.is_empty() || which == "both" { "symbol or footprint" } else { which.as_str() }, ctx.version),+ "Hint": "names are case-sensitive and per KiCad version: kicad_list_versions, then pass kicadVersion; kicad_show_library lists what is registered"});+ }+ let mut out = json!({"success": true, "libraryName": name, "kicadVersion": ctx.version,+ "removed": removed, "filesDeleted": files_deleted, "filesKept": files_kept,+ "deleteFiles": delete_files, "kicadRunning": kicad_running, "tableReloadNeeded": kicad_running});+ if !stock.is_empty() {+ out["stockRowsLeft"] = json!(stock);+ }+ out["Hint"] = json!(if kicad_running {+ "Removed from the table on disk. KiCad is RUNNING and holds its lib tables in memory: the library stays listed (and usable) in this session until KiCad restarts or the user presses OK in Preferences > Manage Libraries. If KiCad then saves its tables it can write the row back: prefer uninstalling with KiCad closed."+ } else if delete_files {+ "Removed from the lib table(s) and the installed copy deleted. kicad_install_library puts it back."+ } else {+ "Removed from the lib table(s); the library files were left in place. Pass deleteFiles:true to also delete the copy under Documents/KiCad/<ver>/ (never files outside it)."+ });+ out+}+ // --------------------------------------------------------------------------- // Tests // ---------------------------------------------------------------------------@@ -2554,4 +2672,53 @@ mod tests { assert!(w.contains("(model \"C:/new.step\" (offset"), "{w}"); assert!(!w.contains("old.step")); }++ // ---- uninstall_library ----++ fn uninstall_ctx(root: &Path) -> LibCtx {+ let cfg = root.join("cfg"); let user = root.join("user");+ std::fs::create_dir_all(&cfg).unwrap();+ std::fs::create_dir_all(user.join("footprints").join("Mine.pretty")).unwrap();+ std::fs::create_dir_all(user.join("3dmodels").join("Mine.3dshapes")).unwrap();+ std::fs::write(user.join("footprints").join("Mine.pretty").join("a.kicad_mod"), "(footprint \"a\")").unwrap();+ let fp_uri = user.join("footprints").join("Mine.pretty").to_string_lossy().replace('\\', "/");+ std::fs::write(cfg.join("fp-lib-table"), format!("(fp_lib_table\n (version 7)\n (lib (name \"Mine\")(type \"KiCad\")(uri \"{fp_uri}\")(options \"\")(descr \"\"))\n (lib (name \"Elsewhere\")(type \"KiCad\")(uri \"{}/outside.pretty\")(options \"\")(descr \"\"))\n (lib (name \"Resistor_SMD\")(type \"KiCad\")(uri \"${{KICAD10_FOOTPRINT_DIR}}/Resistor_SMD.pretty\")(options \"\")(descr \"\"))\n)\n", root.to_string_lossy().replace('\\', "/"))).unwrap();+ std::fs::write(cfg.join("sym-lib-table"), "(sym_lib_table\n (version 7)\n)\n").unwrap();+ std::fs::create_dir_all(root.join("outside.pretty")).unwrap();+ LibCtx { version: "10.0".into(), base_dir: None, config_dir: Some(cfg), user_dir: Some(user), kicad_cli: None, env_overrides: BTreeMap::new() }+ }++ #[test]+ fn uninstall_removes_row_and_only_managed_files() {+ let root = unique_temp_dir("kb-uninstall-a");+ let ctx = uninstall_ctx(&root);+ let r = uninstall_library(&ctx, false, &json!({"libraryName": "Mine", "deleteFiles": true}));+ assert_eq!(r["success"], json!(true), "{r}");+ assert_eq!(r["removed"].as_array().unwrap().len(), 1);+ assert!(!ctx.user_dir.as_ref().unwrap().join("footprints").join("Mine.pretty").exists());+ assert!(!ctx.user_dir.as_ref().unwrap().join("3dmodels").join("Mine.3dshapes").exists());+ let table = LibTable::parse_file(&ctx.fp_lib_table().unwrap()).unwrap();+ assert!(!table.has_library("Mine") && table.has_library("Elsewhere") && table.has_library("Resistor_SMD"));++ // registered in place, outside the managed folders: unregistered, files kept+ let r = uninstall_library(&ctx, true, &json!({"libraryName": "Elsewhere", "deleteFiles": true}));+ assert_eq!(r["success"], json!(true), "{r}");+ assert!(root.join("outside.pretty").exists());+ assert_eq!(r["filesKept"].as_array().unwrap().len(), 1);+ assert_eq!(r["tableReloadNeeded"], json!(true));+ let _ = std::fs::remove_dir_all(&root);+ }++ #[test]+ fn uninstall_refuses_adom_stock_and_unknown() {+ let root = unique_temp_dir("kb-uninstall-b");+ let ctx = uninstall_ctx(&root);+ assert_eq!(uninstall_library(&ctx, false, &json!({"libraryName": "Adom"}))["errorCode"], json!("adom_library_protected"));+ assert_eq!(uninstall_library(&ctx, false, &json!({"libraryName": "Resistor_SMD"}))["errorCode"], json!("stock_library_protected"));+ assert_eq!(uninstall_library(&ctx, false, &json!({"libraryName": "Nope"}))["errorCode"], json!("library_not_registered"));+ assert_eq!(uninstall_library(&ctx, false, &json!({}))["errorCode"], json!("library_name_required"));+ assert_eq!(uninstall_library(&ctx, false, &json!({"libraryName": "Mine", "libraryType": "banana"}))["errorCode"], json!("bad_library_type"));+ assert!(LibTable::parse_file(&ctx.fp_lib_table().unwrap()).unwrap().has_library("Resistor_SMD"));+ let _ = std::fs::remove_dir_all(&root);+ } }
skills/kicad-bridge-test/run_verb_tests.py+2@@ -225,6 +225,8 @@ def matrix(w: dict) -> dict: "install_symbol": dict(phase=4, args={"__special": "install_symbol"}, timeout=160), "install_footprint": dict(phase=4, args={"__special": "install_footprint"}, timeout=160), "install_plugin": dict(phase=4, args={}, timeout=160, note="idempotent payload re-deploy"),+ "uninstall_library": dict(phase=4, args={"libraryName": "AdomVerbTest", "deleteFiles": True}, timeout=60,+ note="removes the scratch library install_library registered above, so daily runs stop piling it up"), "place_footprint": dict(phase=4, args={"__special": "place_footprint"}, timeout=220), "pcm_add_repository": dict(phase=4, args={"url": PCM_REPO_URL}, check="repo_added", timeout=90, note="a REAL add of the sample repository (docs/pcm.md) into kicad.json, backup kept; "
Comments
Log in to comment.