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
0.9.252: kicad_install_library_bundle - one-verb Cloud Library bundle install (zip in, symbols+footprints+3D models installed, restart-needed report)
6 files changed
+196−7
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.251+0.9.252
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.251",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.251/adom-bridge-kicad-v0.9.251.zip",- "sha256": "f84e87f08c6e8881d93e50053f0416608cdb5292c5a58c89968d499e9f8ab805",- "size": 449044,+ "version": "0.9.252",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.252/adom-bridge-kicad-v0.9.252.zip",+ "sha256": "599b975ada07114872d9c9138d59a14e5b9f4356eb98cb53982af1b8437749d5",+ "size": 452444, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-26T20:34:19Z",+ "released_at": "2026-08-26T21:13:35Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
bridge.json+2−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.251",+ "version": "0.9.252", "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",@@ -36,6 +36,7 @@ "kicad_describe", "kicad_diagnostics", "kicad_install_library",+ "kicad_install_library_bundle", "kicad_install_symbol", "kicad_install_footprint", "kicad_install_plugin",
handlers/install_library_bundle.pyadded+181@@ -0,0 +1,181 @@+"""Handler for install_library_bundle: install an Adom wiki Cloud Library+bundle (one zip: <Lib>.kicad_sym + <Lib>.pretty/ + 3dmodels/) in one verb.++This is the no-AI lane: a wiki page stages the zip over the web-control+transport (write_file / write_file_chunk) and calls this verb; the whole+symbol + footprint + 3D-model install happens here, reusing the proven+install_library paths (including the 0.9.251 model-ref rewrite)."""++import re+import shutil+import tempfile+import zipfile+from pathlib import Path+import sys++sys.path.insert(0, str(Path(__file__).parent.parent))++from handlers.install_library import handle_install_library, _install_footprint++MAX_ZIP_BYTES = 256 * 1024 * 1024+MAX_UNPACKED_BYTES = 512 * 1024 * 1024++# Library names become file/dir names and lib-table entries; keep them tame.+_NAME_OK = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$")+++def _safe_extract(zf: zipfile.ZipFile, dest: Path) -> int:+ """Extract with a zip-slip guard and an unpacked-size cap. Returns the+ number of files extracted."""+ total = 0+ count = 0+ dest = dest.resolve()+ for info in zf.infolist():+ name = info.filename+ if not name or name.endswith("/"):+ continue+ # Reject absolute paths, drive letters, parent escapes, backslash tricks.+ norm = name.replace("\\", "/")+ if norm.startswith("/") or re.match(r"^[A-Za-z]:", norm) or ".." in norm.split("/"):+ raise ValueError(f"unsafe zip entry: {name}")+ total += info.file_size+ if total > MAX_UNPACKED_BYTES:+ raise ValueError("bundle unpacks past the size cap")+ target = (dest / norm).resolve()+ if not str(target).startswith(str(dest)):+ raise ValueError(f"unsafe zip entry: {name}")+ target.parent.mkdir(parents=True, exist_ok=True)+ with zf.open(info) as src, open(target, "wb") as out:+ shutil.copyfileobj(src, out)+ count += 1+ return count+++def _find_bundle_parts(root: Path):+ """Locate the bundle pieces regardless of a wrapping top-level folder:+ the first *.kicad_sym (shallowest wins), its sibling *.pretty dir, and a+ sibling 3dmodels dir."""+ syms = sorted(root.rglob("*.kicad_sym"), key=lambda p: (len(p.parts), str(p)))+ if not syms:+ return None, None, None+ sym = syms[0]+ base = sym.parent+ pretty = None+ for d in sorted(base.iterdir()):+ if d.is_dir() and d.suffix == ".pretty":+ pretty = d+ break+ models = base / "3dmodels"+ return sym, pretty, (models if models.is_dir() else None)+++def _count_symbols(sym_path: Path) -> int:+ """Top-level symbols only (sub-units are named NAME_<n>_<n>)."""+ try:+ text = sym_path.read_text(encoding="utf-8", errors="replace")+ except OSError:+ return 0+ names = re.findall(r'\(symbol\s+"([^"]+)"', text)+ return sum(1 for n in names if not re.search(r"_\d+_\d+$", n))+++def handle_install_library_bundle(kicad_info: dict, args: dict) -> dict:+ if not kicad_info.get("installed"):+ return {+ "success": False,+ "error": "KiCad not installed",+ "_hint": "Install KiCad first (kicad_upgrade can do it), then retry.",+ }++ zip_path = args.get("zipPath", "")+ if not zip_path:+ return {"success": False, "error": "No zipPath specified",+ "_hint": "Stage the bundle zip with write_file / write_file_chunk first, then pass its path."}+ source = Path(zip_path).expanduser()+ if not source.is_file():+ return {"success": False, "error": f"Bundle zip not found: {zip_path}",+ "_hint": "The path must exist on THIS machine. Stage it with write_file / write_file_chunk first."}+ if source.stat().st_size > MAX_ZIP_BYTES:+ return {"success": False, "error": "Bundle zip is larger than the 256MB cap"}++ lib_name = args.get("libraryName", "") or source.stem+ lib_name = lib_name.strip()+ if not _NAME_OK.match(lib_name):+ return {"success": False, "error": f"Library name not usable as a KiCad library id: {lib_name!r}",+ "_hint": "Letters, digits, space, dot, dash, underscore; up to 64 chars."}++ extract_dir = Path(tempfile.mkdtemp(prefix="adom-lib-bundle-"))+ try:+ try:+ with zipfile.ZipFile(str(source)) as zf:+ extracted = _safe_extract(zf, extract_dir)+ except (zipfile.BadZipFile, ValueError, OSError) as e:+ return {"success": False, "error": f"Could not unpack the bundle: {e}"}+ if not extracted:+ return {"success": False, "error": "The bundle zip is empty"}++ sym, pretty, models = _find_bundle_parts(extract_dir)+ if not sym:+ return {"success": False, "error": "No .kicad_sym found in the bundle",+ "_hint": "Expected an Adom wiki Cloud Library bundle: <Lib>.kicad_sym + <Lib>.pretty/ + 3dmodels/."}++ report = {"libraryName": lib_name, "symbols": _count_symbols(sym),+ "footprints": 0, "modelsInstalled": 0, "modelRefsRewritten": 0}++ sym_res = handle_install_library(kicad_info, {+ "libraryPath": str(sym), "libraryType": "symbol", "libraryName": lib_name,+ "description": "Adom wiki Cloud Library",+ })+ if not sym_res.get("success"):+ return {"success": False, "error": f"Symbol install failed: {sym_res.get('error')}",+ "step": "symbols", "detail": sym_res}++ # KiCad registers the file under its OWN name; a bundle whose inner file+ # is <Lib>.kicad_sym matches lib_name because the exporter names them+ # together. When they disagree, the installed copy keeps the file name+ # but the table entry uses lib_name (handle_install_library's contract).+ if pretty:+ fp_res = _install_footprint(kicad_info, {+ "libraryPath": str(pretty), "libraryName": lib_name,+ "modelsPath": str(models) if models else "",+ "description": "Adom wiki Cloud Library",+ })+ if not fp_res.get("success"):+ return {"success": False, "error": f"Footprint install failed: {fp_res.get('error')}",+ "step": "footprints", "detail": fp_res, "symbolsInstalled": True}+ report["footprints"] = len(list(pretty.glob("*.kicad_mod")))+ fp_data = fp_res.get("data") or {}+ report["modelsInstalled"] = fp_data.get("modelsInstalled", 0)+ report["modelRefsRewritten"] = fp_data.get("modelRefsRewritten", 0)++ try:+ from handlers import win_focus+ kicad_running = bool(win_focus._any_kicad_process_running())+ except Exception:+ kicad_running = False+ report["kicadRunning"] = kicad_running+ report["restartNeeded"] = kicad_running++ parts = [f"Library '{lib_name}' installed: {report['symbols']} symbols"]+ if report["footprints"]:+ parts.append(f"{report['footprints']} footprints")+ if report["modelsInstalled"]:+ parts.append(f"{report['modelsInstalled']} 3D models")+ summary = ", ".join(parts)+ if kicad_running:+ summary += ". KiCad is running and reads library tables at startup, so restart KiCad to see it."++ return {+ "success": True,+ "output": summary,+ "data": report,+ "_hint": (+ "The bundle is installed and registered in both lib tables. "+ + ("KiCad was RUNNING during the install: it reads sym-/fp-lib-table at startup only, "+ "so the user must restart KiCad before the library appears. Never close their KiCad for them."+ if kicad_running else+ "KiCad was not running; the library appears on next launch.")+ ),+ }+ finally:+ shutil.rmtree(str(extract_dir), ignore_errors=True)
server.py+6@@ -40,6 +40,7 @@ from handlers.close_windows import ( handle_close_3d_viewer, handle_close_kicad, handle_window_info, handle_dismiss_dialogs, ) from handlers.install_library import handle_install_library+from handlers.install_library_bundle import handle_install_library_bundle from handlers.install_symbol import handle_install_symbol from handlers.install_footprint import handle_install_footprint, handle_list_footprints from handlers.place_footprint import handle_place_footprint@@ -357,6 +358,10 @@ _VERB_CATALOG = { "hint": "Adds a library to the selected KiCad version's lib table so its parts are usable. Footprint installs take modelsPath (a directory of STEP/WRL files) or auto-detect a 3dmodels/ sibling of the source .pretty (the Adom wiki library-bundle layout): models copy into 3dmodels/<lib>.3dshapes and the COPIED footprints' matching (model ...) refs are rewritten to the installed absolute paths, so 3D previews resolve.", "related": ["kicad_install_symbol", "kicad_install_footprint", "kicad_list_versions"], "pitfalls": ["each KiCad version has its OWN lib tables — install under every version the user needs", "restart/reload KiCad to see a newly-added library", "model-ref rewriting touches only the INSTALLED copy, never the source, and only refs whose basename matches an installed model (stock ${KICAD*_3DMODEL_DIR} refs stay)"]},+ "install_library_bundle": {"summary": "Install an Adom wiki Cloud Library bundle zip (symbols + footprints + 3D models) in one call.", "long": False,+ "hint": "Pass zipPath (staged on THIS machine via write_file / write_file_chunk) and optionally libraryName (defaults to the zip stem). Unpacks the bundle (<Lib>.kicad_sym + <Lib>.pretty/ + 3dmodels/), installs symbols and footprints, carries the 3D models with ref rewriting, and registers both lib tables. Returns {symbols, footprints, modelsInstalled, kicadRunning, restartNeeded}.",+ "related": ["kicad_install_library", "kicad_lint_library", "kicad_open_symbol_editor"],+ "pitfalls": ["a RUNNING KiCad reads lib tables at startup only — when kicadRunning is true the user must restart KiCad themselves (never close it for them)", "zip entries with .. or absolute paths are refused"]}, "install_symbol": {"summary": "Install a .kicad_sym into the user's sym-lib-table.", "long": False, "hint": "Absolute path to a .kicad_sym; registers it in the sym-lib-table.", "related": ["kicad_open_symbol_editor", "kicad_install_library", "kicad_install_footprint"],@@ -730,6 +735,7 @@ COMMAND_HANDLERS = { "dismiss_dialogs": handle_dismiss_dialogs, "enable_software_opengl": handle_enable_software_opengl, "install_library": handle_install_library,+ "install_library_bundle": handle_install_library_bundle, "install_symbol": handle_install_symbol, "install_footprint": handle_install_footprint, "place_footprint": handle_place_footprint,
tools/release_files.json+1@@ -23,6 +23,7 @@ "handlers/fix_keyboard.py", "handlers/install_footprint.py", "handlers/install_library.py",+ "handlers/install_library_bundle.py", "handlers/install_symbol.py", "handlers/kicad_cli_lint.py", "handlers/kicad_ui.py",