← 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)

Colby Knox ·5639ef27ef ·1mo ago ·parent 39e1177
6 files changed +196−7
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.251\ No newline at end of file+0.9.252\ No newline at end of file
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",