← Commit history

v0.9.76: kicad_launch (cold-start, #19) + kicad_uninstall (SDK cascade, #18) + open_editors teaching hint; SKILL.md CLI-name sweep

John Lauer ·48a72eeec0 ·2mo ago ·parent ab9340e
10 files changed +363−38
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.75+0.9.76
README.md+3−3
@@ -197,10 +197,10 @@ viewer. This is how every 3D shot above exists.  ## Verb reference -All 45 verbs (prefix `kicad_`). Call `kicad_describe` for the live catalog with per-verb hints,+All 47 verbs (prefix `kicad_`). Call `kicad_describe` for the live catalog with per-verb hints, related verbs, and pitfalls. -**Windows & UI** — `open_board`, `open_schematic`, `open_symbol_editor`, `open_footprint_editor`,+**Windows & UI** — `launch`, `open_board`, `open_schematic`, `open_symbol_editor`, `open_footprint_editor`, `open_3d_viewer`, `open_editors`, `close_symbol_editor`, `close_footprint_editor`, `close_3d_viewer`, `close`, `window_info`, `dismiss_dialogs`, `screenshot_all`, `send_key`, `click`, `fix_keyboard`, `demo` @@ -212,7 +212,7 @@ related verbs, and pitfalls. `export_molecule`  **Detect, install & meta** — `list_versions`, `readiness`, `describe`, `diagnostics`, `status`,-`enable_software_opengl`, `check_for_updates`, `upgrade`, `bridge_status`, `bridge_call`+`enable_software_opengl`, `check_for_updates`, `upgrade`, `uninstall`, `bridge_status`, `bridge_call`  --- 
SKILL.md+18−16
@@ -1,15 +1,15 @@ --- name: adom-desktop-kicad public: true-description: "Launch and drive KiCad on the user's laptop (KiCad 7/8/9/10 supported, multi-version side-by-side) from this container via the adom-desktop CLI. Open schematics/boards/symbols/footprints, run DRC, install sym-lib-table / fp-lib-table libraries, show the 3D viewer, capture KiCad window screenshots, send keyboard shortcuts and click coordinates. Trigger words: kicad, open schematic, open board, open pcb, open symbol, open footprint, KiCad symbol editor, KiCad footprint editor, KiCad 3D viewer, run DRC, install KiCad library, sym-lib-table, fp-lib-table, KiCad screenshot, kicad_send_key, kicad_click, kicad_window_info, kicad_screenshot_all, close KiCad, kicad versions, kicad_list_versions, KiCad bridge."+description: "Launch and drive KiCad on the user's laptop (KiCad 7/8/9/10 supported, multi-version side-by-side) from this container via the adom-bridge-cli CLI. Open schematics/boards/symbols/footprints, run DRC, install sym-lib-table / fp-lib-table libraries, show the 3D viewer, capture KiCad window screenshots, send keyboard shortcuts and click coordinates. Trigger words: kicad, open schematic, open board, open pcb, open symbol, open footprint, KiCad symbol editor, KiCad footprint editor, KiCad 3D viewer, run DRC, install KiCad library, sym-lib-table, fp-lib-table, KiCad screenshot, kicad_send_key, kicad_click, kicad_window_info, kicad_screenshot_all, close KiCad, kicad versions, kicad_list_versions, KiCad bridge." --- -# adom-desktop — KiCad bridge+# adom-bridge-cli — KiCad bridge  All commands dispatch to the KiCad Python bridge running alongside the desktop app. Invoke from this container via Bash:  ```bash-adom-desktop kicad_<action> '<json_args>'+adom-bridge-cli kicad_<action> '<json_args>' ```  The leading `kicad_` routes the call to the KiCad plugin; the `<action>` names below are the plugin-side command names.@@ -28,6 +28,8 @@ A fresh machine can be missing KiCad, or the reverse-bridge plugin auto-installs **Readiness before you drive:** - `kicad_readiness '{}'` — read-only, zero side effects: is KiCad installed/ready? (`{detected, ready, versionCount, defaultVersion}`.) There's **no prewarm** — KiCad is a host app you install on request via `kicad_upgrade`, not a downloaded runtime asset. - `kicad_describe '{}'` — the full machine-readable verb catalog (the completeness escape hatch; the per-call `_hint`s are what carry you in practice).+- `kicad_launch '{}'` — cold-start KiCad's project manager when nothing is running (idempotent, opens in the background). NOTE: `kicad_open_editors` only LISTS editors in a running KiCad — it never launches; use `kicad_launch` for "open KiCad".+- `kicad_uninstall '{}'` — remove every artifact the bridge created on this machine (plugin, Adom libraries, mesa fallback, caches). Part of Adom Bridge's uninstall cascade; `dryRun:true` previews. - For **"what EDA tools do I have"** across all bridges, use Bridge's `bridge_readiness` (see below).  ## Installing KiCad on a fresh machine (hard-won, v0.9.26+)@@ -45,7 +47,7 @@ Users can have multiple KiCad versions installed at the same time. The bridge au  ```bash # Discover what's installed (run this first if unsure)-adom-desktop kicad_list_versions '{}'+adom-bridge-cli kicad_list_versions '{}' # → {"versions":[{"version":"10.0", "default":true, ...}, {"version":"9.0", ...}], "default":"10.0", "count":2} ``` @@ -53,10 +55,10 @@ Every `kicad_open_*` (and `install_*`, `run_drc`) command accepts an optional **  ```bash # Open in KiCad 10 (newest — default if you omit kicadVersion)-adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb"}'+adom-bridge-cli kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb"}'  # Open in KiCad 9 explicitly (e.g. to check that an older project still works)-adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb","kicadVersion":"9.0"}'+adom-bridge-cli kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb","kicadVersion":"9.0"}' ```  If you pass a version that's not installed, the bridge returns a hint listing the installed versions. The success response includes `kicadVersionUsed` so you can confirm which install actually ran.@@ -67,8 +69,8 @@ If you pass a version that's not installed, the bridge returns a hint listing th  **KiCad is the user's HOST APP. Adom Bridge *detects* it — it won't silently auto-install it as a prewarm/dependency** (Bridge v1.9.47+). But if KiCad isn't found, **do NOT hand the user a manual download** — offer to install it for them and run `kicad_upgrade '{}'`. That verb installs the latest stable from the official downloads.kicad.org installer (silent `/S`, Windows only; may raise a UAC prompt) and **installs from scratch, not just upgrades**. The distinction: Bridge-core never auto-installs a host app, but a user-requested, agent-run install is exactly right. -- To answer **"what EDA tools / host apps do I have?"** across every bridge, call the Bridge-level verb **`adom-desktop bridge_readiness '{}'`** — Bridge aggregates each bridge's host-app detection (KiCad, Fusion 360, …) into one report. Start here for any "do I have X installed?" question.-- For **KiCad specifically**, `adom-desktop kicad_readiness '{}'` is a zero-side-effect read-only probe: `{hostApp, detected, ready, versionCount, defaultVersion, versions[]}`. It does not install the plugin or spawn subprocesses, and works even when KiCad is absent (`detected:false`). Use `kicad_list_versions` when you need the full per-version detail (paths, IPC API, lib tables).+- To answer **"what EDA tools / host apps do I have?"** across every bridge, call the Bridge-level verb **`adom-bridge-cli bridge_readiness '{}'`** — Bridge aggregates each bridge's host-app detection (KiCad, Fusion 360, …) into one report. Start here for any "do I have X installed?" question.+- For **KiCad specifically**, `adom-bridge-cli kicad_readiness '{}'` is a zero-side-effect read-only probe: `{hostApp, detected, ready, versionCount, defaultVersion, versions[]}`. It does not install the plugin or spawn subprocesses, and works even when KiCad is absent (`detected:false`). Use `kicad_list_versions` when you need the full per-version detail (paths, IPC API, lib tables).  ## Commands @@ -102,19 +104,19 @@ If you pass a version that's not installed, the bridge returns a hint listing th  ```bash # Open a schematic-adom-desktop kicad_open_schematic '{"path":"C:/designs/foo.kicad_sch"}'+adom-bridge-cli kicad_open_schematic '{"path":"C:/designs/foo.kicad_sch"}'  # List open KiCad windows-adom-desktop kicad_window_info '{}'+adom-bridge-cli kicad_window_info '{}'  # Screenshot every KiCad window-adom-desktop kicad_screenshot_all '{}'+adom-bridge-cli kicad_screenshot_all '{}'  # Run DRC-adom-desktop kicad_run_drc '{"board_path":"C:/designs/foo.kicad_pcb"}'+adom-bridge-cli kicad_run_drc '{"board_path":"C:/designs/foo.kicad_pcb"}'  # Install a library pair-adom-desktop kicad_install_library '{+adom-bridge-cli kicad_install_library '{   "name":"my-lib",   "sym_path":"C:/libs/my-lib.kicad_sym",   "fp_path":"C:/libs/my-lib.pretty"@@ -129,10 +131,10 @@ Every failing response includes a `_hint` field with a human-readable next step  Earlier versions of the KiCad bridge listened on hardcoded port `8772`. **As of v1.8.31, bridges use OS-assigned ephemeral ports** — every spawn gets a new free port. You never need to know it. -- The CLI (`adom-desktop kicad_*`) automatically routes through adom-desktop's direct API (default port 47200 as of v1.8.33, with auto-fallback to 47201-47209 if 47200 is taken). adom-desktop then forwards to the bridge's actual runtime port.+- The CLI (`adom-bridge-cli kicad_*`) automatically routes through adom-desktop's direct API (default port 47200 as of v1.8.33, with auto-fallback to 47201-47209 if 47200 is taken). adom-bridge-cli then forwards to the bridge's actual runtime port. - The bridge port is internal plumbing and can change every spawn. `bridge_list` reports it as `spawn.runtimePort` for debugging only — do NOT hardcode that anywhere.-- Why: this lets adom-desktop coexist with Hydrogen (which used to claim 8772 itself) and any other tool that grabs ports in the 8000 range. Side-by-side bridges from different apps no longer collide.-- The relay path (Docker → wss proxy → Windows GUI) is unchanged — same `adom-desktop kicad_*` verbs, same JSON shape.+- Why: this lets adom-bridge-cli coexist with Hydrogen (which used to claim 8772 itself) and any other tool that grabs ports in the 8000 range. Side-by-side bridges from different apps no longer collide.+- The relay path (Docker → wss proxy → Windows GUI) is unchanged — same `adom-bridge-cli kicad_*` verbs, same JSON shape.  If you ever find yourself wanting to talk directly to the KiCad bridge process: don't. Go through the verb dispatch. If the verb you need doesn't exist, request a feature in `adom-inc/adom-desktop`. 
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.75",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.75/adom-bridge-kicad-v0.9.75.zip",-  "sha256": "3a64441f363b2c8c504c7531288f41cbd61063ff6f4eb30428c3954947004722",-  "size": 367957,+  "version": "0.9.76",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.76/adom-bridge-kicad-v0.9.76.zip",+  "sha256": "cb2d1a1388017d5b6f5bf14c9720fc079af175c1480f7080e648c19efe2bd460",+  "size": 373323,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-09T10:29:21Z",+  "released_at": "2026-08-13T23:46:06Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.76.zipadded
⋯ 1 unchanged line ⋯
bridge.json+6−3
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.75",+  "version": "0.9.76",   "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.",@@ -62,7 +62,9 @@     "kicad_export_bom_csv",     "kicad_check_for_updates",     "kicad_upgrade",-    "kicad_demo"+    "kicad_demo",+    "kicad_launch",+    "kicad_uninstall"   ],   "statusVerb": "kicad_status",   "timeouts": {@@ -149,5 +151,6 @@     "Python",     "PowerShell"   ],-  "releasedAt": "2026-06-26T16:00:00Z"+  "releasedAt": "2026-06-26T16:00:00Z",+  "uninstall": "kicad_uninstall" }\ No newline at end of file
handlers/bridge_client.py+17−7
@@ -391,23 +391,33 @@ def handle_open_editors(kicad_info: dict, args: dict) -> dict:             out_editors.append(row)             by_kind.setdefault(row["kind"], []).append(row) +    if not out_editors and probed_plugins == 0:+        hint = (+            "0 editors and 0 live plugins: KiCad is almost certainly NOT RUNNING. This verb only "+            "INVENTORIES editors inside a running KiCad (despite its name) — it never launches "+            "anything. To cold-start KiCad use kicad_launch; kicad_open_board / "+            "kicad_open_symbol_editor also cold-launch when opening a specific target."+        )+    else:+        hint = (+            "Cross-process inventory: each KiCad GUI exe's plugin contributes its own "+            "wx top-level frames. If totalEditors=0 but the user has KiCad windows open, "+            "the plugins likely haven't deployed yet OR KiCad was running before "+            "v1.7.5 plugin install (those instances need a restart to load the bridge). "+            "Call kicad_bridge_status to confirm plugins are alive."+        )     return {         "success": True,         "editors": out_editors,         "byKind": by_kind,+        "kicadRunning": probed_plugins > 0 or bool(out_editors),         "summary": {             "totalEditors": len(out_editors),             "probedPlugins": probed_plugins,             "failedPlugins": failed_plugins,             "byKindCounts": {k: len(v) for k, v in by_kind.items()},         },-        "_hint": (-            "Cross-process inventory: each KiCad GUI exe's plugin contributes its own "-            "wx top-level frames. If totalEditors=0 but the user has KiCad windows open, "-            "the plugins likely haven't deployed yet OR KiCad was running before "-            "v1.7.5 plugin install (those instances need a restart to load the bridge). "-            "Call kicad_bridge_status to confirm plugins are alive."-        ),+        "_hint": hint,     }  
handlers/launch.pyadded+110
@@ -0,0 +1,110 @@+"""launch.py — cold-start KiCad (the project manager) from nothing.++Fills the gap found in issue #19: `kicad_open_editors` only INVENTORIES editors+inside already-running KiCad instances, and every other open_* verb launches a+specific editor. This is the verb whose one job is "KiCad is closed; open it."++Windows-first (Popen on kicad.exe with a no-activate startupinfo so the window+opens in the BACKGROUND per the bridge's focus rules); macOS falls back to+`open -a KiCad`.+"""+import os+import subprocess+import sys+import time+from pathlib import Path++IS_WINDOWS = sys.platform == "win32"+++def _kicad_windows() -> list:+    try:+        from handlers.kicad_ui import _find_all_kicad_windows+        return _find_all_kicad_windows()+    except Exception:+        return []+++def handle_launch(kicad_info: dict, args: dict) -> dict:+    """Cold-start KiCad's project manager (idempotent).++    Args (all optional):+      project:     path to a .kicad_pro to open with it+      waitSeconds: how long to wait for a KiCad window (default 15)+    """+    existing = _kicad_windows()+    if existing:+        return {+            "success": True,+            "alreadyRunning": True,+            "windows": [w.get("title") for w in existing][:10],+            "output": f"KiCad is already running ({len(existing)} window(s)) — nothing launched.",+            "_hint": ("Idempotent no-op: KiCad was already up. Use kicad_open_editors to inventory "+                      "editors, or the open_* verbs to open a specific one."),+        }++    project = (args.get("project") or "").strip()+    if project and not Path(project).exists():+        return {"success": False, "error": f"Project not found: {project}",+                "_hint": "Absolute Windows path required; %VAR% expansion is not performed."}++    if not IS_WINDOWS:+        try:+            cmd = ["open", "-a", "KiCad"] + ([project] if project else [])+            subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)+            return {"success": True, "launched": True,+                    "output": "KiCad launch requested via `open -a KiCad`."}+        except Exception as e:+            return {"success": False, "error": f"Failed to launch KiCad: {e}"}++    exe = kicad_info.get("kicad_exe")+    if not exe or not Path(exe).exists():+        return {+            "success": False,+            "error": "kicad.exe not found",+            "_hint": ("No KiCad installation detected. kicad_readiness reports install state; "+                      "kicad_upgrade installs KiCad silently (per-user, no UAC)."),+        }++    _si = None+    try:+        from handlers import win_focus+        _si = win_focus.background_startupinfo()+    except Exception:+        pass+    try:+        subprocess.Popen([exe] + ([project] if project else []),+                         stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,+                         startupinfo=_si)+    except Exception as e:+        return {"success": False, "error": f"Failed to launch KiCad: {e}"}++    wait_s = float(args.get("waitSeconds") or 15)+    deadline = time.monotonic() + max(3.0, wait_s)+    windows: list = []+    while time.monotonic() < deadline:+        windows = _kicad_windows()+        if windows:+            break+        time.sleep(0.5)++    if windows:+        return {+            "success": True,+            "launched": True,+            "windows": [w.get("title") for w in windows][:10],+            "output": f"KiCad launched — {len(windows)} window(s) up.",+            "_hint": ("Launched in the BACKGROUND (no focus steal). First run on a fresh install "+                      "may show a setup wizard — kicad_window_info self-heals benign dialogs; "+                      "kicad_screenshot_all shows every window if something looks stuck."),+            "_next": ["kicad_window_info", "kicad_open_editors", "kicad_open_board"],+        }+    return {+        "success": False,+        "error": f"kicad.exe started but no KiCad window appeared within {int(wait_s)}s",+        "_hint": ("Cold start on a slow box (or software-OpenGL) can exceed the wait — retry "+                  "kicad_window_info in a few seconds before treating this as a failure. "+                  "A modal (crash-recovery / first-run) can also hold the window back: "+                  "kicad_screenshot_all will show it."),+        "statusVerb": "kicad_window_info",+    }
handlers/uninstall.pyadded+190
@@ -0,0 +1,190 @@+"""uninstall.py — remove everything this bridge ever created on the machine.++Implements the Bridge SDK uninstall contract (kicad-bridge issue #18, ab 2.0.28):+Adom Bridge's uninstaller CASCADES — it calls each bridge's `<prefix>uninstall`+before removing itself — so this verb must leave nothing of ours behind.++Contract requirements honored here:+- IDEMPOTENT + NON-INTERACTIVE: safe when already clean; never prompts, never+  blocks; everything goes to the log/report, not a dialog.+- BEST-EFFORT PER ITEM: one locked file doesn't abort the rest; the report says+  exactly what was and wasn't removed.+- SCOPED TO OUR OWN ARTIFACTS: never touches KiCad itself, other bridges, or+  the user's own projects/data.++What this bridge creates (and this verb removes):+  1. Plugin payload  — adom_bridge.py + usercustomize.py (+ their __pycache__)+     in Documents/KiCad/<ver>/3rdparty/Python311/site-packages/+  2. Adom symbol lib — Documents/KiCad/<ver>/symbols/Adom.kicad_sym + its+     "Adom" row in %APPDATA%/kicad/<ver>/sym-lib-table+  3. Adom footprints — Documents/KiCad/<ver>/footprints/Adom.pretty/ + its+     "Adom" row in fp-lib-table+  4. Mesa fallback   — opengl32.dll / libgallium_wgl.dll / dxil.dll (+ *.local)+     that kicad_enable_software_opengl dropped into a KiCad bin/ (KiCad never+     ships these in bin, so presence == our deploy)+  5. Discovery files — %TEMP%/adom-kicad-bridge-*.json+  6. Screenshot dir  — C:/tmp/adom-desktop-screenshots+  7. Demo project    — Documents/adom-kicad-demo (generated by kicad_demo)++Deliberately NOT removed: the user's KiCad config beyond our two lib-table rows,+any user project, KiCad itself.+"""+import os+import shutil+import sys+import tempfile+from pathlib import Path++IS_WINDOWS = sys.platform == "win32"++_MESA_DLLS = ("opengl32.dll", "libgallium_wgl.dll", "dxil.dll")+++def _kicad_user_roots() -> list[Path]:+    """Documents/KiCad/<ver> dirs (all versions, not just the active one)."""+    up = os.environ.get("USERPROFILE")+    if not up:+        return []+    root = Path(up) / "Documents" / "KiCad"+    if not root.is_dir():+        return []+    return [d for d in root.iterdir() if d.is_dir()]+++def _kicad_config_roots() -> list[Path]:+    """%APPDATA%/kicad/<ver> dirs."""+    ap = os.environ.get("APPDATA")+    if not ap:+        return []+    root = Path(ap) / "kicad"+    if not root.is_dir():+        return []+    return [d for d in root.iterdir() if d.is_dir()]+++def _kicad_bin_dirs(kicad_info: dict) -> list[Path]:+    """Every KiCad bin/ we can find (all installs, both scopes)."""+    dirs: list[Path] = []+    bd = kicad_info.get("bin_dir")+    if bd:+        dirs.append(Path(bd))+    for env, sub in (("ProgramFiles", "KiCad"), ("LOCALAPPDATA", "Programs/KiCad")):+        base = os.environ.get(env)+        if not base:+            continue+        root = Path(base) / sub+        if root.is_dir():+            for ver in root.iterdir():+                b = ver / "bin"+                if b.is_dir():+                    dirs.append(b)+    seen, out = set(), []+    for d in dirs:+        k = str(d).lower()+        if k not in seen:+            seen.add(k)+            out.append(d)+    return out+++def handle_uninstall(kicad_info: dict, args: dict) -> dict:+    """Remove every artifact this bridge created. Args: dryRun (default false)."""+    dry = bool(args.get("dryRun"))+    removed: list[str] = []+    failed: list[dict] = []+    absent = 0++    def rm_file(p: Path, label: str):+        nonlocal absent+        try:+            if not p.exists():+                absent += 1+                return+            if not dry:+                p.unlink()+            removed.append(f"{label}: {p}")+        except Exception as e:+            failed.append({"item": f"{label}: {p}", "error": str(e)})++    def rm_tree(p: Path, label: str):+        nonlocal absent+        try:+            if not p.exists():+                absent += 1+                return+            if not dry:+                shutil.rmtree(p, ignore_errors=False)+            removed.append(f"{label}: {p}")+        except Exception as e:+            failed.append({"item": f"{label}: {p}", "error": str(e)})++    # 1) plugin payload (+ pycache), 2) Adom symbol lib file, 3) Adom.pretty+    for ver_dir in _kicad_user_roots():+        site = ver_dir / "3rdparty" / "Python311" / "site-packages"+        rm_file(site / "adom_bridge.py", "plugin payload")+        rm_file(site / "usercustomize.py", "plugin loader")+        pyc = site / "__pycache__"+        if pyc.is_dir():+            for f in pyc.glob("adom_bridge*.pyc"):+                rm_file(f, "plugin pycache")+            for f in pyc.glob("usercustomize*.pyc"):+                rm_file(f, "plugin pycache")+        rm_file(ver_dir / "symbols" / "Adom.kicad_sym", "Adom symbol library")+        rm_tree(ver_dir / "footprints" / "Adom.pretty", "Adom footprint library")++    # lib-table rows (sym + fp) in every config version+    try:+        from lib_table import LibTable+        for cfg in _kicad_config_roots():+            for table_name in ("sym-lib-table", "fp-lib-table"):+                tp = cfg / table_name+                if not tp.exists():+                    absent += 1+                    continue+                try:+                    table = LibTable.parse_file(str(tp))+                    if table.has_library("Adom"):+                        if not dry:+                            table.remove_library("Adom")+                            table.write_file(str(tp))+                        removed.append(f"'Adom' row in {tp}")+                    else:+                        absent += 1+                except Exception as e:+                    failed.append({"item": f"'Adom' row in {tp}", "error": str(e)})+    except Exception as e:+        failed.append({"item": "lib-table cleanup", "error": str(e)})++    # 4) Mesa software-OpenGL deploy+    for b in _kicad_bin_dirs(kicad_info):+        for dll in _MESA_DLLS:+            rm_file(b / dll, "mesa fallback")+            rm_file(b / (dll + ".local"), "mesa fallback marker")+        for loc in b.glob("*.exe.local"):+            rm_file(loc, "mesa dotlocal")++    # 5) discovery files+    tmp = Path(tempfile.gettempdir())+    for f in tmp.glob("adom-kicad-bridge-*.json"):+        rm_file(f, "plugin discovery file")++    # 6) screenshot cache, 7) demo project+    rm_tree(Path("C:/tmp/adom-desktop-screenshots"), "screenshot cache")+    up = os.environ.get("USERPROFILE")+    if up:+        rm_tree(Path(up) / "Documents" / "adom-kicad-demo", "demo project")++    ok = len(failed) == 0+    return {+        "success": ok,+        "dryRun": dry,+        "removed": removed,+        "failed": failed,+        "alreadyClean": len(removed) == 0 and ok,+        "summary": f"{'would remove' if dry else 'removed'} {len(removed)} item(s), "+                   f"{len(failed)} failed, {absent} already absent",+        "_hint": ("Bridge-created artifacts only — KiCad itself, user projects, and other bridges are "+                  "untouched. Idempotent: safe to re-run. A running KiCad may hold the plugin's "+                  ".pyc open; re-run after KiCad exits if anything failed. Bridge removes its own "+                  "bridges-cache during the cascade."),+    }
server.py+13−3
@@ -44,6 +44,8 @@ from handlers.run_drc import handle_run_drc from handlers.fix_keyboard import handle_fix_keyboard from handlers.kicad_ui import handle_screenshot_all, handle_send_key, handle_click from handlers.bridge_client import handle_bridge_status, handle_bridge_call, handle_open_editors+from handlers.launch import handle_launch+from handlers.uninstall import handle_uninstall from handlers.kicad_cli_lint import (     handle_run_erc, handle_lint_board, handle_lint_schematic,     handle_lint_library, handle_format_upgrade,@@ -312,8 +314,14 @@ _VERB_CATALOG = {         "hint": "Low-level IPC into a live pcbnew/eeschema; use bridge_status first to find a live instance.",         "related": ["kicad_bridge_status", "kicad_install_plugin"], "pitfalls": ["fails if no plugin instance is live — open a board first"]},     "open_editors": {"summary": "List currently open KiCad editor windows.", "long": False,-        "hint": "Read-only; which editors are open right now. Use to reuse a window instead of opening a duplicate.",-        "related": ["kicad_window_info", "kicad_screenshot_all", "kicad_close"], "pitfalls": []},+        "hint": "Read-only INVENTORY (despite the name): lists editors inside a running KiCad. It never launches anything — to cold-start KiCad use kicad_launch.",+        "related": ["kicad_launch", "kicad_window_info", "kicad_screenshot_all", "kicad_close"], "pitfalls": ["with KiCad closed it succeeds with 0 editors — that is not an error; kicad_launch is the verb that starts KiCad"]},+    "launch": {"summary": "Cold-start KiCad (project manager) when nothing is running.", "long": True,+        "hint": "Idempotent: no-op with a hint if KiCad is already up. Opens in the BACKGROUND (no focus steal). Optional project: path to a .kicad_pro.",+        "related": ["kicad_open_editors", "kicad_window_info", "kicad_readiness", "kicad_upgrade"], "pitfalls": ["first launch on a fresh install can show a setup wizard — kicad_window_info self-heals benign dialogs"]},+    "uninstall": {"summary": "Remove every artifact this bridge created on the machine (SDK uninstall cascade).", "long": False,+        "hint": "Idempotent, non-interactive, best-effort per item. Removes plugin payload, Adom libraries + lib-table rows, mesa fallback DLLs, discovery files, screenshot cache, demo project. Never touches KiCad itself or user projects. dryRun:true previews.",+        "related": ["kicad_bridge_status"], "pitfalls": ["a running KiCad can hold plugin .pyc files open — re-run after it exits if items fail"]},     "export_gerber": {"summary": "Export Gerbers from a board via kicad-cli.", "long": False,         "hint": "Absolute board path + output dir; writes Gerber + drill files (JLCPCB/InstaPCB ready).",         "related": ["kicad_export_step", "kicad_export_bom_csv", "kicad_run_drc"], "pitfalls": ["run kicad_run_drc first — don't ship Gerbers from a board with DRC violations"]},@@ -470,6 +478,8 @@ COMMAND_HANDLERS = {     "status": handle_bridge_status,     "bridge_call": handle_bridge_call,     "open_editors": handle_open_editors,+    "launch": handle_launch,+    "uninstall": handle_uninstall,     "install_plugin": plugin_install.handle_install_plugin,     # v1.7.12+: headless lint pre-checks via kicad-cli     "run_erc": handle_run_erc,@@ -654,7 +664,7 @@ def dispatch_command(command: str, args: dict) -> dict:     # Only verbs that actually SPAWN a window get the ~3s settle-poll (they can     # trigger an async dialog). Read-only reporters (open_editors, screenshot_all,     # window_info) don't spawn windows → one cheap scan, no needless 3s poll.-    _SPAWNS_WINDOW = {"open_board", "open_schematic", "open_symbol_editor",+    _SPAWNS_WINDOW = {"launch", "open_board", "open_schematic", "open_symbol_editor",                       "open_footprint_editor", "open_3d_viewer",                       "place_footprint", "install_symbol", "install_footprint",                       "install_plugin", "upgrade",