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
v0.9.10 source: upgrade.py handler + server.py wiring + docs
5 files changed
+226−4
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.9+0.9.10
SKILL.md+2@@ -43,6 +43,8 @@ If you pass a version that's not installed, the bridge returns a hint listing th | CLI form | Action | Purpose | Key args | |---|---|---|---| | `kicad_list_versions` | `list_versions` | Enumerate every installed KiCad version with paths and which one is the default | — |+| `kicad_check_for_updates` | `check_for_updates` | Read-only: installed KiCad vs latest STABLE (from downloads.kicad.org / KiCad GitLab tags); flags when the install predates the modern IPC/kipy automation API and should be upgraded | — |+| `kicad_upgrade` | `upgrade` | Download the **official** KiCad installer from downloads.kicad.org and silent-install it (`/S`) — **not winget** (winget lags the real release). May raise a UAC prompt to approve | optional `version`, `force` | | `kicad_open_schematic` | `open_schematic` | Open a `.kicad_sch` in the Schematic Editor | `filePath`, optional `kicadVersion` | | `kicad_open_board` | `open_board` | Open a `.kicad_pcb` in the PCB Editor | `filePath`, optional `kicadVersion` | | `kicad_open_symbol_editor` | `open_symbol_editor` | Launch Symbol Editor (optional: open specific library/symbol) | `libraryName`, `symbolName`, optional `kicadVersion` |
bridge.json+5−3@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.9",+ "version": "0.9.10", "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", "homepage": "https://github.com/adom-inc/adom-desktop", "author": "Adom Inc.",@@ -42,7 +42,9 @@ "kicad_screenshot_all", "kicad_send_key", "kicad_click",- "kicad_close"+ "kicad_close",+ "kicad_check_for_updates",+ "kicad_upgrade" ], "dependencies": { "python": ">=3.11",@@ -86,5 +88,5 @@ "Python", "PowerShell" ],- "releasedAt": "2026-06-25T16:16:19Z"+ "releasedAt": "2026-06-26T16:00:00Z" }
handlers/upgrade.pyadded+209@@ -0,0 +1,209 @@+"""KiCad version check + upgrade via the OFFICIAL downloads.kicad.org installer.++Why not winget: winget's KiCad package lags the real KiCad release (often by a+minor version or more), and it can't pin a specific build. The official+`downloads.kicad.org` NSIS installer is the canonical, signed artifact — so the+bridge fetches THAT and installs it silently (`/S`).++- `check_for_updates` — read-only: current installed version vs latest STABLE+ (resolved from KiCad's GitLab tags, baked fallback on a network blip), plus the+ "your KiCad predates the IPC/kipy API — upgrade" nudge.+- `upgrade` — downloads the official installer and runs it silently. Needs+ elevation for Program Files, so the user may see (and approve) a UAC prompt.+"""++import json+import re+import sys+import tempfile+import urllib.error+import urllib.request+from pathlib import Path++import kicad_detect+import proc++# Baked fallback if the GitLab tag query fails (network blip). Bump on new stable.+_FALLBACK_LATEST = "10.0.4"+_TAGS_URL = (+ "https://gitlab.com/api/v4/projects/kicad%2Fcode%2Fkicad/repository/tags?per_page=30"+)+_DL_URL = "https://downloads.kicad.org/kicad/windows/explore/stable/download/kicad-{v}-x86_64.exe"+_UA = "adom-desktop-kicad-bridge"+++def _is_stable(tag: str) -> bool:+ """True for a real stable tag like 10.0.4 / 9.0.9.1. Drops `-rcN` prereleases+ and KiCad's `X.99.*` nightly/development tags (minor component == 99)."""+ if not re.fullmatch(r"\d+(?:\.\d+)+", tag):+ return False+ parts = tag.split(".")+ return not (len(parts) >= 2 and parts[1] == "99")+++def latest_stable_version() -> tuple[str, str]:+ """Return (version, source) where source is 'gitlab' or 'fallback'."""+ try:+ req = urllib.request.Request(_TAGS_URL, headers={"User-Agent": _UA})+ with urllib.request.urlopen(req, timeout=10) as r:+ tags = [t["name"] for t in json.loads(r.read().decode())]+ stables = sorted(+ (t for t in tags if _is_stable(t)),+ key=kicad_detect._version_tuple,+ reverse=True,+ )+ if stables:+ return stables[0], "gitlab"+ except Exception:+ pass+ return _FALLBACK_LATEST, "fallback"+++def download_url(version: str) -> str:+ return _DL_URL.format(v=version)+++def handle_check_for_updates(kicad_info: dict, args: dict) -> dict:+ """Read-only: is a newer KiCad available, and does the current one have the IPC API?"""+ current = kicad_info.get("version") if kicad_info else None+ latest, src = latest_stable_version()+ cur_t = kicad_detect._version_tuple(current) if current else (0,)+ lat_t = kicad_detect._version_tuple(latest)+ upgrade_available = current is None or lat_t > cur_t+ ipc = bool(kicad_info.get("ipcApiAvailable")) if kicad_info else False++ if current is None:+ rec = (+ f"KiCad isn't installed. Run kicad_upgrade to install {latest} "+ f"from the official downloads.kicad.org installer."+ )+ elif upgrade_available and not ipc:+ rec = (+ f"Your KiCad {current} predates the modern IPC / kipy automation API — "+ f"upgrade to {latest} to unlock it (plus the latest fixes). Run kicad_upgrade."+ )+ elif upgrade_available:+ rec = f"KiCad {latest} is available (you're on {current}). Run kicad_upgrade to update."+ else:+ rec = f"KiCad {current} is up to date (latest stable is {latest})."++ return {+ "success": True,+ "currentVersion": current,+ "latestVersion": latest,+ "latestSource": src,+ "upgradeAvailable": upgrade_available,+ "ipcApiAvailable": ipc,+ "downloadUrl": download_url(latest),+ "recommendation": rec,+ "_hint": (+ "Run kicad_upgrade '{}' to install the latest (downloads the OFFICIAL "+ "downloads.kicad.org installer + silent /S install — NOT winget, which lags). "+ 'Pass {"version":"X.Y.Z"} to pin a specific build.'+ ),+ }+++def handle_upgrade(kicad_info: dict, args: dict) -> dict:+ """Download the official KiCad installer and run it silently (/S)."""+ if sys.platform != "win32":+ return {+ "success": False,+ "error": "kicad_upgrade is Windows-only (it downloads the .exe installer).",+ "errorCode": "unsupported_platform",+ }++ target = args.get("version") or latest_stable_version()[0]+ current = kicad_info.get("version") if kicad_info else None+ if (+ current+ and kicad_detect._version_tuple(current) >= kicad_detect._version_tuple(target)+ and not args.get("force")+ ):+ return {+ "success": True,+ "alreadyCurrent": True,+ "currentVersion": current,+ "targetVersion": target,+ "_hint": f'KiCad {current} >= {target}; nothing to do. Pass {{"force":true}} to reinstall.',+ }++ url = download_url(target)+ dest = Path(tempfile.gettempdir()) / f"kicad-{target}-x86_64.exe"++ # Download — urllib follows the downloads.kicad.org -> s3.cern.ch redirect.+ try:+ req = urllib.request.Request(url, headers={"User-Agent": _UA})+ total = 0+ with urllib.request.urlopen(req, timeout=120) as r, open(dest, "wb") as f:+ while True:+ chunk = r.read(1 << 20)+ if not chunk:+ break+ f.write(chunk)+ total += len(chunk)+ except (urllib.error.URLError, OSError) as e:+ return {+ "success": False,+ "error": f"download failed: {e}",+ "errorCode": "download_failed",+ "url": url,+ }++ # Sanity: a real KiCad installer is a >50 MB Windows PE (MZ header).+ if total < 50 * 1024 * 1024:+ return {+ "success": False,+ "error": f"download too small ({total} bytes) — not a full installer",+ "errorCode": "download_incomplete",+ "url": url,+ "path": str(dest),+ }+ with open(dest, "rb") as f:+ if f.read(2) != b"MZ":+ return {+ "success": False,+ "error": "downloaded file is not a Windows executable",+ "errorCode": "download_corrupt",+ "path": str(dest),+ }++ # Silent install (NSIS /S). Installing into Program Files needs elevation, so+ # the user may get a UAC prompt to approve. Block until the installer exits.+ try:+ result = proc.run([str(dest), "/S"], timeout=900)+ rc = result.returncode+ except Exception as e:+ return {+ "success": False,+ "error": f"installer launch failed: {e}",+ "errorCode": "install_failed",+ "path": str(dest),+ "_hint": "If a UAC prompt appeared, approve it; or run the downloaded installer manually.",+ }++ # Confirm by re-scanning installed versions.+ after = kicad_detect.detect_all_kicad_versions()+ installed = [v["version"] for v in after]+ now_has = any(+ kicad_detect._version_tuple(v) >= kicad_detect._version_tuple(target)+ for v in installed+ )+ return {+ "success": bool(now_has),+ "upgraded": bool(now_has),+ "targetVersion": target,+ "previousVersion": current,+ "installerExit": rc,+ "installedVersions": installed,+ "downloadUrl": url,+ "_hint": (+ f"KiCad {target} installed."+ if now_has+ else (+ f"Installer ran (exit {rc}) but {target} wasn't detected — it may have needed a "+ f"UAC approval, or the bridge needs a restart to re-scan. Installed: "+ f"{', '.join(installed) or 'none'}."+ )+ ),+ }
server.py+9@@ -54,6 +54,10 @@ from handlers.export import ( handle_export_gerber, handle_export_pdf, handle_export_svg, handle_export_step, handle_export_bom_csv, )+# v0.9.10+: check/upgrade KiCad via the OFFICIAL downloads.kicad.org installer+# (NOT winget, which lags the real release). check_for_updates is read-only;+# upgrade silent-installs the official signed NSIS build.+from handlers.upgrade import handle_check_for_updates, handle_upgrade from adom_library import ensure_adom_library import plugin_install @@ -227,6 +231,11 @@ COMMAND_HANDLERS = { "export_svg": handle_export_svg, "export_step": handle_export_step, "export_bom_csv": handle_export_bom_csv,+ # v0.9.10+: KiCad version check + upgrade via the official downloads.kicad.org+ # installer (read-only check; silent /S install). Exposed as kicad_check_for_updates+ # / kicad_upgrade through Adom Desktop's `kicad_` verb prefix.+ "check_for_updates": handle_check_for_updates,+ "upgrade": handle_upgrade, }