← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.10.21\ No newline at end of file+1.10.22\ No newline at end of file
bridge.json+4−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.10.21",+  "version": "1.10.22",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",@@ -82,7 +82,9 @@     "fusion_routing_drc",     "fusion_routing_state",     "fusion_plan_route",-    "fusion_schematic"+    "fusion_schematic",+    "fusion_make_showcase_project",+    "fusion_eagle_import_status"   ],   "dependencies": {},   "category": "CAD",
contracts/describe.json+51−2
@@ -825,6 +825,27 @@       "summary": "Metadata for the ACTIVE document: name, product type, save state, version, parent project.",       "timeoutSeconds": 30     },+    {+      "example": {+        "runId": "1a2b3c"+      },+      "input": {+        "runId": "required str (from fusion_new_electronics_from_eagle)"+      },+      "longRunning": false,+      "name": "fusion_eagle_import_status",+      "output": {+        "documentName": "str",+        "partsExpected": "int",+        "partsPlaced": "int",+        "result": "obj",+        "stage": "str",+        "success": "bool"+      },+      "statusVerb": null,+      "summary": "The run record of a background fusion_new_electronics_from_eagle import: stage (started | done | failed), documentName, partsExpected (elements in the .brd), partsPlaced (read back from the board), placed[], and the final result. Poll it every few seconds after the import returned its runId.",+      "timeoutSeconds": 20+    },     {       "example": {         "x1": 0,@@ -1746,6 +1767,34 @@       "summary": "Create a RENDERING component 3D package (FOOTPRINT + chip, aligned) for ONE part, fully programmatically with ZERO GUI dialogs. Opens the .lbr, runs Electron.Create3DPackage to load the package's footprint into a generator doc, imports the STEP onto it, orients it flat, saveAs an .f3d (skips the FINISH Save dialog + the two unbeatable 'Fusion360' CEF modals), uploads the f3d via APS, and returns an fs.file:vf wip_urn to hand-write into the library's <packages3d>. projectId/folderId default to the MAIN-project upload folder. Pass captureLabel to also get a BEFORE (footprint) + AFTER (chip placed) screenshot. PITFALL: a RAW STEP upload binds but renders NOTHING ('Thumbnail download failed') - this verb makes an f3d, which renders. Prefer fusion_build_library_3d for many parts.",       "timeoutSeconds": 200     },+    {+      "example": {+        "devicesets": [+          "VL53L5CX",+          "VL53L7CX",+          "VL53L8CX"+        ],+        "libraryPath": "C:/Users/me/Downloads/Adom_ToF.lbr"+      },+      "input": {+        "devicesets": "optional [str] (default: every deviceset in the library)",+        "libraryPath": "required str (.lbr on the Fusion machine; a component page's .lbr or a merged bundle library)",+        "name": "optional str document name (default <library>-showcase)"+      },+      "longRunning": true,+      "name": "fusion_make_showcase_project",+      "output": {+        "brdPath": "str",+        "documentName": "str",+        "parts": "[{ref, deviceset, package, symbol}]",+        "partsPlaced": "int (read back from the board)",+        "schPath": "str",+        "success": "bool"+      },+      "statusVerb": "fusion_get_app_state",+      "summary": "Put a library's parts on a NEW schematic and board to SHOW THEM OFF: every deviceset in the .lbr (or the ones in devicesets[]) placed in a row on a fresh sheet and a fresh board, no nets, with the library's bound 3D urns carried across so the 3D PCB shows the real bodies. Generated as EAGLE source on the box and opened dialog-free (Document.newDesignFromLocal), so nothing touches the user's screen. This is THE verb for 'make a board with these new parts on it'; fusion_make_test_project is the one-part circuit variant. After it: fusion_show_2d_board / fusion_show_3d_board {capture:true}, fusion_schematic {action:\"show\"}, fusion_save_to_cloud.",+      "timeoutSeconds": 300+    },     {       "example": {         "libraryPath": "C:/Temp/IN-S42ATR.lbr"@@ -1966,7 +2015,7 @@         "success": "bool"       },       "statusVerb": null,-      "summary": "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D. PREFLIGHT (1.10.20): refuses with eagle_files_missing before touching any dialog when either file is not on the Fusion machine; if the Open dialog does not accept the file the bridge cancels the dialog itself (open_dialog_stuck), so nothing is ever left on the user's screen. Prefer fusion_schematic {action:\"open\"}, which picks this route only when a .brd exists next to the .sch.",+      "summary": "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D. PREFLIGHT (1.10.20): refuses with eagle_files_missing before touching any dialog when either file is not on the Fusion machine; if the Open dialog does not accept the file the bridge cancels the dialog itself (open_dialog_stuck), so nothing is ever left on the user's screen. Prefer fusion_schematic {action:\"open\"}, which picks this route only when a .brd exists next to the .sch. ASYNC by contract (1.10.22): returns {started:true, runId, statusVerb} at once and runs the dialog-driven import on the exe; poll fusion_eagle_import_status {runId}. The record and the final reply carry documentName, partsExpected (elements in the .brd) and partsPlaced (read back from the board); success needs parts on the board (nothing_instantiated otherwise). Pass wait:true only from inside the exe.",       "timeoutSeconds": 200     },     {@@ -2498,7 +2547,7 @@         "via": "str: the verb that did the work"       },       "statusVerb": "fusion_get_app_state",-      "summary": "START HERE for a schematic. ONE verb, three actions. action:\"open\" lands in the schematic editor from whatever names the design: fileName (+projectName) or urn or query for a CLOUD design (opens it, then switches to the schematic view), schPath for a local EAGLE .sch (its sibling .brd is imported with it so parts instantiate, unless withBoard:false; a .sch alone opens dialog-free), or filePath for a Fusion-native .fsch. action:\"create\" writes a blank EAGLE schematic on the box and opens it as a NEW local electronics design with the schematic editor active, no dialog anywhere. action:\"show\" switches the already-open design to its schematic view. Every reply says which underlying verb it used (via). Remember: a schematic is a VIEW of an electronics design, not a file of its own, which is why there is no verb called open_a_schematic_file.",+      "summary": "START HERE for a schematic. ONE verb, three actions. action:\"open\" lands in the schematic editor from whatever names the design: fileName (+projectName) or urn or query for a CLOUD design (opens it, then switches to the schematic view), schPath for a local EAGLE .sch (its sibling .brd is imported with it so parts instantiate, unless withBoard:false; a .sch alone opens dialog-free), or filePath for a Fusion-native .fsch. action:\"create\" writes a blank EAGLE schematic on the box and opens it as a NEW local electronics design with the schematic editor active, no dialog anywhere. action:\"show\" switches the already-open design to its schematic view. Every reply says which underlying verb it used (via). Remember: a schematic is a VIEW of an electronics design, not a file of its own, which is why there is no verb called open_a_schematic_file. To put a library's parts on a NEW schematic and board for showing them off, use fusion_make_showcase_project instead.",       "timeoutSeconds": 240     },     {
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.10.21"+version = "1.10.22" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.21"+version = "1.10.22" edition = "2021"  [dependencies]
describe.py+8−2
@@ -43,7 +43,7 @@ _T = [     ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.",      {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}), -    ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D. PREFLIGHT (1.10.20): refuses with eagle_files_missing before touching any dialog when either file is not on the Fusion machine; if the Open dialog does not accept the file the bridge cancels the dialog itself (open_dialog_stuck), so nothing is ever left on the user's screen. Prefer fusion_schematic {action:\"open\"}, which picks this route only when a .brd exists next to the .sch.",+    ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D. PREFLIGHT (1.10.20): refuses with eagle_files_missing before touching any dialog when either file is not on the Fusion machine; if the Open dialog does not accept the file the bridge cancels the dialog itself (open_dialog_stuck), so nothing is ever left on the user's screen. Prefer fusion_schematic {action:\"open\"}, which picks this route only when a .brd exists next to the .sch. ASYNC by contract (1.10.22): returns {started:true, runId, statusVerb} at once and runs the dialog-driven import on the exe; poll fusion_eagle_import_status {runId}. The record and the final reply carry documentName, partsExpected (elements in the .brd) and partsPlaced (read back from the board); success needs parts on the board (nothing_instantiated otherwise). Pass wait:true only from inside the exe.",      {"schPath": "required str (Windows path to .sch)", "brdPath": "optional str (defaults to sibling .brd)"},      {"success": "bool", "imported": "list"}, 200, None, False,      {"schPath": "C:/Users/me/adom-lib/MyBoard.sch", "brdPath": "C:/Users/me/adom-lib/MyBoard.brd"}),@@ -251,6 +251,12 @@ _T = [     ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).", "name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)", "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch", "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}),     ("fusion_demo_progress", "Poll a running fusion_demo tour: {percent, elapsedSec, estimatedSec, step, stepLabel, done, failed, shots, shotCount, stepShot}. Poll ~1-2.5s and render a REAL progress bar. LIVE EVIDENCE (issue #30): each frame MAY carry stepShot {step, title, image} - a ~640px base64 capture the BRIDGE took at that step's completion, one per step, riding the first frame after it completes (render these during the run; the page should not guess-snap). shots[] streams too: mid-run it carries paths for every completed step; pass imagesAs:base64 + sinceShot:<count you have> to receive only NEW shots fully encoded. done:true = the full gallery (base64 by default). Finished tours are DISK-SNAPSHOTTED, so the final frame stays pollable even across a bridge respawn.", {"demoId": "required str (from fusion_demo mode:tour; omitted = the single running tour)", "imagesAs": "optional str paths|base64|both", "sinceShot": "optional int - shots you already received; response windows to the new ones"}, {"percent": "int", "step": "str", "stepLabel": "str", "done": "bool", "shots": "list", "shotCount": "int", "stepShot": "dict|absent"}, 15, None, False, {"demoId": "tour-abc123", "imagesAs": "base64", "sinceShot": 3}), +    ("fusion_make_showcase_project", "Put a library's parts on a NEW schematic and board to SHOW THEM OFF: every deviceset in the .lbr (or the ones in devicesets[]) placed in a row on a fresh sheet and a fresh board, no nets, with the library's bound 3D urns carried across so the 3D PCB shows the real bodies. Generated as EAGLE source on the box and opened dialog-free (Document.newDesignFromLocal), so nothing touches the user's screen. This is THE verb for 'make a board with these new parts on it'; fusion_make_test_project is the one-part circuit variant. After it: fusion_show_2d_board / fusion_show_3d_board {capture:true}, fusion_schematic {action:\"show\"}, fusion_save_to_cloud.",+     {"libraryPath": "required str (.lbr on the Fusion machine; a component page's .lbr or a merged bundle library)", "devicesets": "optional [str] (default: every deviceset in the library)", "name": "optional str document name (default <library>-showcase)"},+     {"success": "bool", "documentName": "str", "parts": "[{ref, deviceset, package, symbol}]", "partsPlaced": "int (read back from the board)", "schPath": "str", "brdPath": "str"}, 300, _STATUS, True, {"libraryPath": "C:/Users/me/Downloads/Adom_ToF.lbr", "devicesets": ["VL53L5CX", "VL53L7CX", "VL53L8CX"]}),+    ("fusion_eagle_import_status", "The run record of a background fusion_new_electronics_from_eagle import: stage (started | done | failed), documentName, partsExpected (elements in the .brd), partsPlaced (read back from the board), placed[], and the final result. Poll it every few seconds after the import returned its runId.",+     {"runId": "required str (from fusion_new_electronics_from_eagle)"},+     {"success": "bool", "stage": "str", "documentName": "str", "partsExpected": "int", "partsPlaced": "int", "result": "obj"}, 20, None, False, {"runId": "1a2b3c"}),     ("fusion_make_test_project", "Create a NEW electronics test project FROM SCRATCH around a library part - the Web Control demo's PROJECT button, and the answer to 'a customer only has a component page'. Generates a minimal test circuit server-side (the part + a generated 330R series resistor with VCC/GND stubs on a small board) as real EAGLE .sch/.brd, then imports the pair as a new electronics design. NO cloud, NO sign-in, no pre-existing content. After it, fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board all work on the result. Wiring uses the deviceset's own connects (first two pins), so 2-terminal parts get a real circuit and big ICs get a structurally-valid integration proof.", {"libraryPath": "required str (WINDOWS path .lbr; stage from container first)", "deviceset": "optional str (defaults to the library's first deviceset)", "device": "optional str: the DEVICE (package variant) to place, e.g. \"-HV\" (default: the deviceset's first device)"}, {"success": "bool", "schPath": "str", "brdPath": "str", "circuit": "str"}, 300, None, True, {"libraryPath": "C:/Temp/IN-S42ATR.lbr"}),     ("fusion_screenshot_fusion", "Background PrintWindow capture of Fusion's MAIN window (largest same-title window; works occluded/bottom-of-z-order, never raises anything) or a specific dialog by hwnd. Returns savedTo (PNG/WebP on the box; pull with pull_file). (Live since early versions but undescribed until 2026-08-15 - it was invisible to callers AND to the drift check.)", {"hwnd": "optional int (a dialog; default the main window)"}, {"success": "bool", "savedTo": "str", "sizeKB": "int"}, 60, None, False, {}),     ("fusion_bind_library_3d", "LOW-LEVEL primitive: binds ONE STEP onto one deviceset. Prefer fusion_create_library with parts[] (which handles one part or many through a single path); reach for this only when you are driving a single bind deliberately. The CORRECT library in ONE call (the Library button's real bar: the 3D chip renders in the Content Manager preview): mints the part's 3D as a cloud f3d (make_3d_package), injects the wip_urn binding into the .lbr ON DISK (packages3d + package3dinstances), and reopens the library. Needs a signed-in Fusion session - the one cloud-dependent step in the wiki-to-EE circle. Autodesk's thumbnail preview lags ~2-3 min behind a fresh mint. NOTE: distinct from the older fusion_build_library_3d (multi-part batch pipeline).", {"libraryPath": "required str (WINDOWS .lbr path)", "stepPath": "required str (WINDOWS .step path)", "deviceset": "optional str (defaults to the library's first)"}, {"success": "bool", "wip_urn": "str", "bound": "bool", "reopened": "bool"}, 480, None, True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),@@ -357,7 +363,7 @@ _T = [      {"filePath": "required str (WINDOWS path to the .brd, C:/...; must exist on the Fusion machine)"},      {"success": "bool", "filePath": "str", "fileType": "str", "activeWorkspace": "str", "activeDocument": "str", "nextSteps": "[str]", "postOpenCheck": "{required, steps}", "screenshots": "[{type,savedTo,sizeKB}] (sidecar shell) or postOpenScreenshot (exe shell)"},      60, None, True, {"filePath": "C:/Temp/TJA1050.brd"}),-    ("fusion_schematic", "START HERE for a schematic. ONE verb, three actions. action:\"open\" lands in the schematic editor from whatever names the design: fileName (+projectName) or urn or query for a CLOUD design (opens it, then switches to the schematic view), schPath for a local EAGLE .sch (its sibling .brd is imported with it so parts instantiate, unless withBoard:false; a .sch alone opens dialog-free), or filePath for a Fusion-native .fsch. action:\"create\" writes a blank EAGLE schematic on the box and opens it as a NEW local electronics design with the schematic editor active, no dialog anywhere. action:\"show\" switches the already-open design to its schematic view. Every reply says which underlying verb it used (via). Remember: a schematic is a VIEW of an electronics design, not a file of its own, which is why there is no verb called open_a_schematic_file.",+    ("fusion_schematic", "START HERE for a schematic. ONE verb, three actions. action:\"open\" lands in the schematic editor from whatever names the design: fileName (+projectName) or urn or query for a CLOUD design (opens it, then switches to the schematic view), schPath for a local EAGLE .sch (its sibling .brd is imported with it so parts instantiate, unless withBoard:false; a .sch alone opens dialog-free), or filePath for a Fusion-native .fsch. action:\"create\" writes a blank EAGLE schematic on the box and opens it as a NEW local electronics design with the schematic editor active, no dialog anywhere. action:\"show\" switches the already-open design to its schematic view. Every reply says which underlying verb it used (via). Remember: a schematic is a VIEW of an electronics design, not a file of its own, which is why there is no verb called open_a_schematic_file. To put a library's parts on a NEW schematic and board for showing them off, use fusion_make_showcase_project instead.",      {"action": "required str: open | create | show", "fileName": "cloud design name (open)", "projectName": "optional str (open, with fileName)", "urn": "cloud lineage urn (open)", "query": "find-and-open by name (open)", "schPath": "local EAGLE .sch on the Fusion machine (open)", "brdPath": "optional local .brd (open; defaults to the sibling)", "withBoard": "optional bool (open; false = schematic only, dialog-free)", "filePath": "local .fsch (open)", "name": "optional str (create)", "capture": "optional bool: inline screenshot proof after the switch"},      {"success": "bool", "via": "str: the verb that did the work", "documentName": "str (create)", "schPath": "str (create)"}, 240, _STATUS, True, {"action": "open", "fileName": "BQ25792 Assembly", "projectName": "Main"}),     ("fusion_open_schematic", "Open a LOCAL legacy EAGLE .sch in Fusion's Schematic Editor (dialog-free Document.newDesignFromLocal; a background open). NAV RULE: the schematic, 2D board and 3D board are VIEWS of ONE electronics design, not standalone files. For a team design open the DESIGN document (fusion_open_cloud_file / fusion_open_by_urn) and switch views with fusion_show_schematic; this verb is for a .sch that exists on the Fusion machine (push it there first). Drive the opened sheet with fusion_electron_run (WINDOW FIT, DISPLAY ALL). The bridge captures a post-open screenshot because Fusion can raise a blocking dialog the API cannot see.",
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.10.21"+DASH_VERSION="1.10.22" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.10.21",+  "version": "1.10.22",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+11−27
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.10.21",+  "version": "1.10.22",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -75,7 +79,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -104,28 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "agent_permissions": {-    "allow_bash": [],-    "allow": [-      "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",-      "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."-    ],-    "soft_deny": [-      "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",-      "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",-      "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",-      "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."-    ],-    "environment": [-      "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",-      "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."-    ]-  },-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.10.21"+version = "1.10.22" edition = "2021" license = "MIT" 
rust/bridge/src/main.rs+1−1
@@ -408,7 +408,7 @@ fn main() {                         r["shell"] = json!("rust"); r },                     "fusion_start" | "start" | "launch" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, "start", &caller); let mut r = launch::start(&ctx, &parsed["args"]); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, "start", t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     "fusion_open_lbr" | "open_lbr" if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, "open_lbr", &caller); let mut r = orchestrations::open_lbr(&ctx, &parsed["args"], &|v, a| addin::pass_through(v, &a, &ctx)); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, "open_lbr", t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },-                    v @ ("fusion_show_symbol" | "show_symbol" | "fusion_show_footprint" | "show_footprint" | "fusion_show_library" | "show_library" | "fusion_show_project" | "show_project" | "fusion_show_schematic" | "show_schematic" | "fusion_show_2d_board" | "show_2d_board" | "fusion_show_3d_board" | "show_3d_board" | "fusion_capture_library_views" | "capture_library_views" | "fusion_open_cloud_file" | "open_cloud_file" | "fusion_save_lbr" | "save_lbr" | "fusion_cleanup_cloud_files" | "cleanup_cloud_files" | "fusion_inspect_bodies" | "inspect_bodies" | "fusion_board_stackup" | "board_stackup" | "fusion_export_optimized_glb" | "export_optimized_glb" | "fusion_fetch_optimized_glb" | "fetch_optimized_glb" | "fusion_set_design_rules" | "set_design_rules" | "fusion_make_test_project" | "make_test_project" | "fusion_open_design" | "open_design" | "fusion_new_electronics_from_eagle" | "new_electronics_from_eagle" | "fusion_aps_open" | "aps_open" | "fusion_schematic" | "schematic") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, cmd, &caller); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "show_symbol" => orchestrations::show_symbol(&ctx, &parsed["args"], &pass), "show_footprint" => orchestrations::show_footprint(&ctx, &parsed["args"], &pass), "show_library" => orchestrations::show_library(&ctx, &parsed["args"], &pass), "show_project" => match orchestrations::show_project(&ctx, &parsed["args"], &pass) { Some(r) => r, None => side("show_project", parsed["args"].clone(), 240) }, "capture_library_views" => small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &parsed["args"], &pass), &caller, &pids), "open_cloud_file" => orchestrations::open_cloud_file(&ctx, &parsed["args"], &pass), "save_lbr" => small_verbs::merge_dialog_array(orchestrations::save_lbr(&ctx, &parsed["args"], &pass), &caller, &pids), "cleanup_cloud_files" => orchestrations::cleanup_cloud_files(&parsed["args"], &pass), "inspect_bodies" => orchestrations::inspect_bodies(&parsed["args"], &pass), "board_stackup" => services::board_stackup(&parsed["args"], &pass), "export_optimized_glb" => if parsed["args"]["async"].as_bool() == Some(true) { services::start_glb_export_async(&ctx, &parsed["args"]) } else { services::export_optimized_glb(&parsed["args"], &pass) }, "fetch_optimized_glb" => if parsed["args"]["exportId"].is_string() { services::glb_export_async_status(&parsed["args"]) } else { services::fetch_optimized_glb(&parsed["args"]) }, "set_design_rules" => services::set_design_rules(&ctx, &parsed["args"], &pass), "make_test_project" => small_verbs::merge_dialog_array(testproject::make_test_project(&ctx, &parsed["args"], &pass), &caller, &pids), "open_design" => orchestrations::open_design(&ctx, &parsed["args"], &pass), "new_electronics_from_eagle" => orchestrations::new_electronics_from_eagle(&ctx, &parsed["args"], &pass), "schematic" => orchestrations::schematic(&ctx, &parsed["args"], &pass), "aps_open" => orchestrations::aps_open(&ctx, &parsed["args"], &pass), other => orchestrations::open_with_screenshot(&ctx, other, &parsed["args"], &pass) }; let mut r = small_verbs::merge_dialog_array(r0, &caller, &pids); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },+                    v @ ("fusion_show_symbol" | "show_symbol" | "fusion_show_footprint" | "show_footprint" | "fusion_show_library" | "show_library" | "fusion_show_project" | "show_project" | "fusion_show_schematic" | "show_schematic" | "fusion_show_2d_board" | "show_2d_board" | "fusion_show_3d_board" | "show_3d_board" | "fusion_capture_library_views" | "capture_library_views" | "fusion_open_cloud_file" | "open_cloud_file" | "fusion_save_lbr" | "save_lbr" | "fusion_cleanup_cloud_files" | "cleanup_cloud_files" | "fusion_inspect_bodies" | "inspect_bodies" | "fusion_board_stackup" | "board_stackup" | "fusion_export_optimized_glb" | "export_optimized_glb" | "fusion_fetch_optimized_glb" | "fetch_optimized_glb" | "fusion_set_design_rules" | "set_design_rules" | "fusion_make_test_project" | "make_test_project" | "fusion_make_showcase_project" | "make_showcase_project" | "fusion_open_design" | "open_design" | "fusion_new_electronics_from_eagle" | "new_electronics_from_eagle" | "fusion_aps_open" | "aps_open" | "fusion_schematic" | "schematic" | "fusion_eagle_import_status" | "eagle_import_status") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, cmd, &caller); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "show_symbol" => orchestrations::show_symbol(&ctx, &parsed["args"], &pass), "show_footprint" => orchestrations::show_footprint(&ctx, &parsed["args"], &pass), "show_library" => orchestrations::show_library(&ctx, &parsed["args"], &pass), "show_project" => match orchestrations::show_project(&ctx, &parsed["args"], &pass) { Some(r) => r, None => side("show_project", parsed["args"].clone(), 240) }, "capture_library_views" => small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &parsed["args"], &pass), &caller, &pids), "open_cloud_file" => orchestrations::open_cloud_file(&ctx, &parsed["args"], &pass), "save_lbr" => small_verbs::merge_dialog_array(orchestrations::save_lbr(&ctx, &parsed["args"], &pass), &caller, &pids), "cleanup_cloud_files" => orchestrations::cleanup_cloud_files(&parsed["args"], &pass), "inspect_bodies" => orchestrations::inspect_bodies(&parsed["args"], &pass), "board_stackup" => services::board_stackup(&parsed["args"], &pass), "export_optimized_glb" => if parsed["args"]["async"].as_bool() == Some(true) { services::start_glb_export_async(&ctx, &parsed["args"]) } else { services::export_optimized_glb(&parsed["args"], &pass) }, "fetch_optimized_glb" => if parsed["args"]["exportId"].is_string() { services::glb_export_async_status(&parsed["args"]) } else { services::fetch_optimized_glb(&parsed["args"]) }, "set_design_rules" => services::set_design_rules(&ctx, &parsed["args"], &pass), "make_test_project" => small_verbs::merge_dialog_array(testproject::make_test_project(&ctx, &parsed["args"], &pass), &caller, &pids), "make_showcase_project" => small_verbs::merge_dialog_array(testproject::make_showcase_project(&ctx, &parsed["args"], &pass), &caller, &pids), "open_design" => orchestrations::open_design(&ctx, &parsed["args"], &pass), "new_electronics_from_eagle" => orchestrations::new_electronics_from_eagle(&ctx, &parsed["args"], &pass), "schematic" => orchestrations::schematic(&ctx, &parsed["args"], &pass), "eagle_import_status" => orchestrations::eagle_import_status(&ctx, &parsed["args"]), "aps_open" => orchestrations::aps_open(&ctx, &parsed["args"], &pass), other => orchestrations::open_with_screenshot(&ctx, other, &parsed["args"], &pass) }; let mut r = small_verbs::merge_dialog_array(r0, &caller, &pids); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     "fusion_state" | "state" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: h.bridge_state_dir(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let mut r = orchestrations::fusion_state(&ctx, &parsed["args"]); r["shell"] = json!("rust"); r },                     v @ ("fusion_make_3d_package" | "make_3d_package" | "fusion_build_library_3d" | "build_library_3d" | "fusion_bind_library_3d" | "bind_library_3d" | "fusion_prove_library" | "prove_library" | "fusion_attach_3d_package" | "attach_3d_package" | "fusion_show_3d_package" | "show_3d_package") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); *LONG_OP.lock().unwrap() = Some(cmd.to_string()); *LONG_OP_SINCE.lock().unwrap() = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "make_3d_package" => mint::make_3d_package(&ctx, &parsed["args"], &pass), "bind_library_3d" => mint::bind_library_3d(&ctx, &parsed["args"], &pass), "prove_library" => prove::prove_library(&ctx, &parsed["args"], &pass), "attach_3d_package" => mint::attach_3d_package(&ctx, &parsed["args"], &pass), "show_3d_package" => mint::show_3d_package(&ctx, &parsed["args"], &pass), _ => mint::build_library_3d(&ctx, &parsed["args"], &pass) }; *LONG_OP.lock().unwrap() = None; let mut r = small_verbs::apply_failure_dialogs(r0, &caller, &pids); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     v @ ("fusion_apply_fusion_update" | "apply_fusion_update" | "fusion_install_fusion" | "install_fusion" | "fusion_uninstall" | "uninstall" | "fusion_prefs_open" | "prefs_open" | "fusion_prefs_close" | "prefs_close" | "fusion_generate_package" | "generate_package" | "fusion_mcp_status" | "mcp_status" | "fusion_mcp_tools" | "mcp_tools" | "fusion_mcp_call" | "mcp_call" | "fusion_mcp_resources" | "mcp_resources" | "fusion_mcp_enable" | "mcp_enable" | "fusion_library_manager" | "library_manager" | "fusion_managed_library_create" | "managed_library_create" | "fusion_managed_library_link" | "managed_library_link" | "fusion_managed_library_pull" | "managed_library_pull" | "fusion_managed_library_push" | "managed_library_push" | "fusion_managed_library_unlink" | "managed_library_unlink" | "fusion_signin" | "signin" | "fusion_signin_2fa" | "signin_2fa" | "fusion_web_signin" | "web_signin" | "fusion_aps_signin" | "aps_signin" | "fusion_demo" | "demo" | "fusion_demo_progress" | "demo_progress" | "fusion_demo_control" | "demo_control" | "fusion_electronics_query" | "electronics_query" | "fusion_view" | "view" | "fusion_interactive" | "interactive" | "fusion_narrate" | "narrate") if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: h.bridge_state_dir(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let cmd = v.strip_prefix("fusion_").unwrap_or(v); let mut r = match cmd { "apply_fusion_update" => maintenance::apply_fusion_update(&ctx, &pass), "install_fusion" => maintenance::install_fusion(&ctx), "prefs_open" => maintenance::prefs_open(&ctx, &parsed["args"], &pass), "prefs_close" => maintenance::prefs_close(&ctx, &parsed["args"]), "generate_package" => epg::generate_package(&parsed["args"], VERSION, &pass),
rust/bridge/src/orchestrations.rs+63−3
@@ -954,7 +954,49 @@ pub fn schematic(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -     } } +/// Where an EAGLE import run keeps its state (`fusion_eagle_import_status {runId}`).+fn eagle_run_path(state_dir: &std::path::Path, run_id: &str) -> std::path::PathBuf { state_dir.join("eagle-imports").join(format!("{run_id}.json")) }+fn eagle_run_write(state_dir: &std::path::Path, run_id: &str, v: &Value) { let p = eagle_run_path(state_dir, run_id); let _ = std::fs::create_dir_all(p.parent().unwrap()); let _ = std::fs::write(&p, v.to_string()); }++/// `fusion_eagle_import_status`: the run record of a background EAGLE import.+pub fn eagle_import_status(ctx: &Ctx, args: &Value) -> Value {+    let Some(rid) = args["runId"].as_str().filter(|s| !s.is_empty()) else { return json!({"success": false, "errorCode": "run_id_required", "error": "runId is required (fusion_new_electronics_from_eagle returns it)."}) };+    match std::fs::read_to_string(eagle_run_path(&ctx.state_dir, rid)).ok().and_then(|t| serde_json::from_str::<Value>(&t).ok()) {+        Some(mut v) => { v["success"] = json!(true); v["runId"] = json!(rid); v }+        None => json!({"success": false, "errorCode": "unknown_run_id", "error": format!("no EAGLE import run {rid} on this box")}),+    }+}+ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    // Async by contract (issue #90): the dialog-driven import outlives ab's relay budget, so the+    // default returns a runId at once and the work runs on this exe's own thread; the caller+    // polls fusion_eagle_import_status while fusion_get_app_state keeps answering.+    if args["wait"].as_bool() != Some(true) {+        let sch0 = args["schPath"].as_str().or(args["filePath"].as_str()).unwrap_or("").replace('\\', "/");+        if sch0.is_empty() || !sch0.to_lowercase().ends_with(".sch") { return json!({"success": false, "errorCode": "sch_path_required", "error": "schPath (a .sch file on the Windows host) is required.", "_hint": "fusion_schematic {action:\"open\", schPath} is the front door; it takes this route only when a .brd sits next to the .sch."}); }+        let brd0 = args["brdPath"].as_str().map(|s| s.replace('\\', "/")).filter(|s| !s.is_empty()).unwrap_or_else(|| format!("{}.brd", &sch0[..sch0.len() - 4]));+        let missing: Vec<&str> = [(&sch0, "schPath"), (&brd0, "brdPath")].iter().filter(|(p, _)| !std::path::Path::new(p.as_str()).is_file()).map(|(_, k)| *k).collect();+        if !missing.is_empty() {+            return json!({"success": false, "errorCode": "eagle_files_missing", "schPath": sch0, "brdPath": brd0, "missing": missing,+                "error": format!("Not started: {} not found on the Fusion machine. This verb drives Fusion's Open dialogs, so it refuses before touching them.", missing.join(" and ")),+                "_hint": "Push both files to the box first (send_files, a space-free folder is best) and pass their absolute Windows paths. No dialog was opened. If you only have a .sch, fusion_schematic {action:\"open\", schPath, withBoard:false} opens it dialog-free (schematic only); if the .brd already carries placed parts, fusion_open_board {filePath} instantiates them with no dialog either."});+        }+        let run_id = format!("{:x}", (now_f() * 1000.0) as u64 ^ (std::process::id() as u64) << 20);+        let expected = std::fs::read_to_string(&brd0).map(|t| t.matches("<element ").count()).unwrap_or(0);+        eagle_run_write(&ctx.state_dir, &run_id, &json!({"stage": "started", "startedAt": now_f(), "schPath": sch0, "brdPath": brd0, "partsExpected": expected}));+        let url = crate::launch::SELF_URL.lock().unwrap().clone();+        let mut a2 = args.clone(); a2["wait"] = json!(true); a2["_runId"] = json!(run_id);+        let (sd, rid) = (ctx.state_dir.clone(), run_id.clone());+        std::thread::spawn(move || {+            let r = url.and_then(|u| ureq::post(&u).timeout(Duration::from_secs(900)).send_json(json!({"command": "fusion_new_electronics_from_eagle", "args": a2})).ok()).and_then(|r| r.into_json::<Value>().ok()).unwrap_or(json!({"success": false, "error": "the exe could not post the import to itself"}));+            let mut v = std::fs::read_to_string(eagle_run_path(&sd, &rid)).ok().and_then(|t| serde_json::from_str::<Value>(&t).ok()).unwrap_or(json!({}));+            v["stage"] = json!(if r["success"].as_bool() == Some(true) { "done" } else { "failed" }); v["finishedAt"] = json!(now_f()); v["result"] = r;+            eagle_run_write(&sd, &rid, &v);+        });+        return json!({"success": true, "started": true, "runId": run_id, "statusVerb": "fusion_eagle_import_status", "partsExpected": expected,+            "dialogsDriven": "Fusion's own Open dialogs (sch, then brd), driven in the background by accessible name; Autodesk offers no API for a sch+brd import",+            "_hint": format!("Import running in the background (it outlives the relay window). Poll fusion_eagle_import_status {{runId:\"{run_id}\"}} until stage is done or failed; fusion_get_app_state keeps answering meanwhile. The record reports documentName, partsExpected ({expected} elements in the .brd) and partsPlaced from the live board; success needs parts on the board.")});+    }     let sch = args["schPath"].as_str().or(args["filePath"].as_str()).unwrap_or("").replace('\\', "/");     if sch.is_empty() || !sch.to_lowercase().ends_with(".sch") { return json!({"success": false, "error": "schPath (a .sch file on the Windows host) is required.", "_hint": "Author the EAGLE .sch + paired .brd (same basename, same folder), stage both to Windows (send_files), then call with {schPath, brdPath}."}); }     let brd = args["brdPath"].as_str().map(|s| s.replace('\\', "/")).filter(|s| !s.is_empty()).unwrap_or_else(|| format!("{}.brd", &sch[..sch.len() - 4]));@@ -1001,14 +1043,32 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V         std::thread::sleep(Duration::from_secs(2));     }     let mut ready = false;+    let mut doc_name = String::new();     for _ in 0..40 {         std::thread::sleep(Duration::from_secs(3));         let st = pass("get_app_state", json!({}));         let d = &st["data"];-        if d["isElectronics"].as_bool() == Some(true) && ["pcb editor", "schematic editor", "board layout", "3d pcb"].contains(&d["activeWorkspace"].as_str().unwrap_or("").to_lowercase().as_str()) { ready = true; break; }+        if d["isElectronics"].as_bool() == Some(true) && ["pcb editor", "schematic editor", "board layout", "3d pcb"].contains(&d["activeWorkspace"].as_str().unwrap_or("").to_lowercase().as_str()) { ready = true; doc_name = d["activeDocument"].as_str().unwrap_or("").to_string(); break; }+    }+    // honest instantiation report (issue #90): what the .brd promised against what the board holds+    let expected = std::fs::read_to_string(&brd).map(|t| t.matches("<element ").count()).unwrap_or(0);+    let (mut placed, mut placed_names): (usize, Vec<Value>) = (0, vec![]);+    if ready {+        let bi = pass("board_info", json!({}));+        let d = if bi["data"].is_object() { bi["data"].clone() } else { bi.clone() };+        placed = d["componentCount"].as_u64().map(|n| n as usize).or_else(|| d["components"].as_array().map(|a| a.len())).unwrap_or(0);+        placed_names = d["components"].as_array().cloned().unwrap_or_default().iter().take(40).map(|c| c["name"].clone()).collect();+    }+    let instantiated = expected == 0 || placed > 0;+    if let Some(rid) = args["_runId"].as_str() {+        let mut v = std::fs::read_to_string(eagle_run_path(&ctx.state_dir, rid)).ok().and_then(|t| serde_json::from_str::<Value>(&t).ok()).unwrap_or(json!({}));+        v["documentName"] = json!(doc_name); v["partsExpected"] = json!(expected); v["partsPlaced"] = json!(placed); v["placed"] = json!(placed_names); v["imported"] = json!(picked);+        eagle_run_write(&ctx.state_dir, rid, &v);     }-    json!({"success": ready, "imported": picked, "staleDialogCancelled": stale_cancelled, "dialogsDriven": "Fusion's own Open dialogs (sch, then brd), driven in the background by accessible name; this is Autodesk's only route to a populated board from EAGLE source", "statusVerb": "fusion_board_info",-        "_hint": if ready { "EAGLE design imported via Fusion's own ImportSCHAndBRDCmd (both Open dialogs driven in the background). It is now an electronics design in the PCB editor. NEXT: fusion_show_2d_board, then RATSNEST + 'AUTO ;' (fusion_electron_run) to autoroute, fusion_show_3d_board for the 3D. NOTE: board_info can read 0 right after import (wrong-view query) even though the board is populated - verify by screenshotting the Fusion hwnd (read screenshots[]) or fusion_show_2d_board + WINDOW FIT. If 3D bodies are missing, the placed parts' package3d urns must resolve in THIS project - build the library's 3D into this project with fusion_build_library_3d (do NOT reuse another library's urns; they don't resolve cross-project)." } else { "Import fired + both Open dialogs driven, but the design did not confirm as electronics within the budget (a slow VM import can run longer). Poll fusion_get_app_state, and screenshot the Fusion hwnd reading screenshots[] for a blocking import dialog." }})+    let ready = ready && instantiated;+    json!({"success": ready, "imported": picked, "documentName": doc_name, "partsExpected": expected, "partsPlaced": placed, "placed": placed_names,+        "errorCode": if ready { Value::Null } else if picked.len() == 2 && expected > 0 && placed == 0 { json!("nothing_instantiated") } else { json!("import_not_ready") }, "staleDialogCancelled": stale_cancelled, "dialogsDriven": "Fusion's own Open dialogs (sch, then brd), driven in the background by accessible name; this is Autodesk's only route to a populated board from EAGLE source", "statusVerb": "fusion_board_info",+        "_hint": if !ready && placed == 0 && expected > 0 { "The import opened documents but placed NONE of the parts the .brd carries (Fusion names the new design Untitled until it is saved). The usual cause is a library the .brd references that Fusion cannot resolve, or a .sch/.brd pair that is not consistent; check the .brd's <libraries> block and fusion_check_dialogs. fusion_open_board {filePath} instantiates parts from a .brd whose elements embed their library, with no dialog." } else if ready { "EAGLE design imported via Fusion's own ImportSCHAndBRDCmd (both Open dialogs driven in the background). It is now an electronics design in the PCB editor. NEXT: fusion_show_2d_board, then RATSNEST + 'AUTO ;' (fusion_electron_run) to autoroute, fusion_show_3d_board for the 3D. NOTE: board_info can read 0 right after import (wrong-view query) even though the board is populated - verify by screenshotting the Fusion hwnd (read screenshots[]) or fusion_show_2d_board + WINDOW FIT. If 3D bodies are missing, the placed parts' package3d urns must resolve in THIS project - build the library's 3D into this project with fusion_build_library_3d (do NOT reuse another library's urns; they don't resolve cross-project)." } else { "Import fired + both Open dialogs driven, but the design did not confirm as electronics within the budget (a slow VM import can run longer). Poll fusion_get_app_state, and screenshot the Fusion hwnd reading screenshots[] for a blocking import dialog." }}) }  const READONLY_OPEN_NOTE: &str = "NOTE: a 'Read Only'/expired/trial Fusion still opens+views files — if it never opens, debug the open path (URN/picker/slow assembly), NOT the license. Only save/export are blocked in read-only.";
rust/bridge/src/testproject.rs+79
@@ -169,6 +169,85 @@ pub fn make_test_project(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) ->         "_hint": "Test project opened as TWO documents (board instantiated with placed parts, schematic as its drawn sheet) via the dialog-free Document.newDesignFromLocal path. The BOARD document is active: fusion_show_2d_board / fusion_show_3d_board work now; for the schematic view activate its document (it opened last... the SCHEMATIC document is actually active) - board legs may need the board doc re-activated via Document.newDesignFromLocal or open_board. Generated from NOTHING but the component's .lbr - no cloud, no sign-in."}) } ++/// `fusion_make_showcase_project`: every deviceset of a library (or the ones named) placed on a+/// fresh schematic and board, no nets, so a set of parts can be shown off (symbols on the+/// sheet, footprints on the board, the bound 3D bodies on the 3D PCB). Dialog-free: the pair is+/// generated as EAGLE source and opened through Document.newDesignFromLocal (John, 2026-09-20:+/// "create a new schematic and board to show off 3 new ToF sensors").+pub fn make_showcase_project(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    let lbr_path = args["libraryPath"].as_str().unwrap_or("").replace('\\', "/");+    if lbr_path.is_empty() { return json!({"success": false, "errorCode": "library_path_required", "error": "libraryPath (.lbr, Windows path on the Fusion machine) is required.", "_hint": "Stage the library on the box first (send_files); a component page's .lbr or the bundle's merged library both work."}); }+    let lbr_text = match std::fs::read(&lbr_path) { Ok(b) => String::from_utf8_lossy(&b).to_string(), Err(e) => return json!({"success": false, "errorCode": "library_unreadable", "error": format!("could not read {lbr_path}: {e}")}) };+    let all: Vec<String> = regex::Regex::new(r#"<deviceset name="([^"]+)""#).unwrap().captures_iter(&lbr_text).map(|c| c[1].to_string()).collect();+    let wanted: Vec<String> = match args["devicesets"].as_array() { Some(a) if !a.is_empty() => a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect(), _ => all.clone() };+    if wanted.is_empty() { return json!({"success": false, "errorCode": "no_devicesets", "error": "the library has no devicesets to place."}); }+    let missing: Vec<&String> = wanted.iter().filter(|w| !all.iter().any(|a| a.eq_ignore_ascii_case(w))).collect();+    if !missing.is_empty() { return json!({"success": false, "errorCode": "deviceset_not_in_library", "missing": missing, "available": all, "error": "some requested devicesets are not in the library."}); }+    let mut extracts = Vec::new();+    for ds in &wanted { match mtp_extract(&lbr_text, ds, None) { Ok(e) => extracts.push(e), Err(e) => return json!({"success": false, "errorCode": "deviceset_unusable", "deviceset": ds, "error": e}) } }+    let lib_name: String = regex::Regex::new(r"[^A-Za-z0-9_-]").unwrap().replace_all(lbr_path.rsplit('/').next().unwrap_or("").rsplit_once('.').map(|(s, _)| s).unwrap_or(""), "_").to_string();+    let name: String = args["name"].as_str().filter(|s| !s.is_empty()).map(|s| s.to_string()).unwrap_or_else(|| format!("{lib_name}-showcase")).chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').take(48).collect();+    // unique packages / symbols, plus each package's bound 3D urn when the library carries one+    let (mut pkgs, mut syms, mut p3d) = (Vec::<(String, String)>::new(), Vec::<(String, String)>::new(), Vec::<String>::new());+    for e in &extracts {+        if !pkgs.iter().any(|(n, _)| *n == e.pkg_name) {+            pkgs.push((e.pkg_name.clone(), e.package.clone()));+            let pk = regex::escape(&e.pkg_name);+            for c in regex::Regex::new(r#"(?s)<package3d [^>]*wip_urn="([^"]+)"[^>]*>(.*?)</package3d>"#).unwrap().captures_iter(&lbr_text) {+                if regex::Regex::new(&format!(r#"<packageinstance name="{pk}"/>"#)).unwrap().is_match(&c[2]) { p3d.push(p3d_block(&e.pkg_name, &c[1], &e.pkg_name)); break; }+            }+        }+        if !syms.iter().any(|(n, _)| *n == e.sym_name) { syms.push((e.sym_name.clone(), e.symbol.clone())); }+    }+    let packages: String = pkgs.iter().map(|(_, x)| x.as_str()).collect();+    let symbols: String = syms.iter().map(|(_, x)| x.as_str()).collect();+    let devicesets: String = extracts.iter().map(|e| e.deviceset.as_str()).collect();+    let p3ds: String = p3d.concat();+    let p3d_inner: String = p3ds.replace("<packages3d>", "").replace("</packages3d>", "");+    let packages3d = if p3d_inner.is_empty() { String::new() } else { format!("<packages3d>{p3d_inner}</packages3d>") };+    // layout: a row on the sheet (40 mm pitch) and a row on the board (each package's extent plus 6 mm)+    let mut parts = String::new(); let mut instances = String::new(); let mut elements = String::new();+    let (mut bx, margin) = (4.0f64, 4.0f64); let mut bh = 10.0f64;+    let mut refs: Vec<Value> = Vec::new();+    for (i, e) in extracts.iter().enumerate() {+        let pfx = { let p = e.prefix.trim(); if p.is_empty() { "U" } else { p } };+        let rname = format!("{pfx}{}", i + 1);+        parts.push_str(&format!(r#"<part name="{rname}" library="{lib_name}" deviceset="{}" device="{}"/>"#, wanted[i], e.device));+        instances.push_str(&format!(r#"<instance part="{rname}" gate="{}" x="{}" y="40"/>"#, e.gate_name, py_float(20.0 + 45.0 * i as f64)));+        let (x0, y0, x1, y1) = pkg_extent_mm(&e.package);+        let (pw, ph) = (x1 - x0, y1 - y0);+        let ex_ = round_to(bx - x0, 3);+        elements.push_str(&format!(r#"<element name="{rname}" library="{lib_name}" package="{}" value="{}" x="{}" y="{}"{}/>"#, e.pkg_name, wanted[i], py_float(ex_), py_float(round_to(margin + ph.max(6.0) / 2.0 - (y0 + y1) / 2.0 + 6.0, 3)), if p3d.iter().any(|b| b.contains(&format!("name=\"{}\"", e.pkg_name))) { let u = regex::Regex::new(&format!(r#"(?s)<package3d name="{}"[^>]*wip_urn="([^"]+)""#, regex::escape(&e.pkg_name))).unwrap().captures(&p3ds).map(|c| c[1].to_string()).unwrap_or_default(); if u.is_empty() { String::new() } else { format!(r#" package3d_urn="{u}""#) } } else { String::new() }));+        refs.push(json!({"ref": rname, "deviceset": wanted[i], "package": e.pkg_name, "symbol": e.sym_name}));+        bx = round_to(bx + pw.max(6.0) + 6.0, 2);+        bh = bh.max(ph + 2.0 * margin + 12.0);+    }+    let (bw, bh) = (round_to(bx + margin, 2), round_to(bh, 2));+    let sch = format!("{HEAD}{SCH_LAYERS}<schematic xreflabel=\"%F%N/%S.%C%R\" xrefpart=\"/%S.%C%R\"><libraries><library name=\"{lib_name}\"><packages>{packages}</packages>{packages3d}<symbols>{symbols}</symbols><devicesets>{devicesets}</devicesets></library></libraries><attributes/><variantdefs/><classes><class number=\"0\" name=\"default\" width=\"0\" drill=\"0\"/></classes><parts>{parts}</parts><sheets><sheet><plain/><instances>{instances}</instances><busses/><nets/></sheet></sheets></schematic></drawing></eagle>\n");+    let brd = format!("{HEAD}{}<board><plain>{}</plain><libraries><library name=\"{lib_name}\"><packages>{packages}</packages>{packages3d}</library></libraries><attributes/><variantdefs/><classes><class number=\"0\" name=\"default\" width=\"0\" drill=\"0\"/></classes><designrules name=\"default\"></designrules><autorouter></autorouter><elements>{elements}</elements><signals/></board></drawing></eagle>\n", extracts[0].layers, outline(bw, bh));+    let d = std::env::temp_dir().join("adom-fusion-wrap"); let _ = std::fs::create_dir_all(&d);+    let sch_path = d.join(format!("{name}.sch")).to_string_lossy().replace('\\', "/");+    let brd_path = d.join(format!("{name}.brd")).to_string_lossy().replace('\\', "/");+    if let Err(e) = std::fs::write(&sch_path, &sch).and_then(|_| std::fs::write(&brd_path, &brd)) { return json!({"success": false, "errorCode": "write_failed", "error": e.to_string()}); }+    crate::palette::say(&format!("Showcase: {} part(s) from {lib_name}", extracts.len()), "a schematic and a board from EAGLE source, opened dialog-free", None, crate::palette::eta_for_verb(&ctx.state_dir, "make_test_project"));+    let mut steps: Vec<String> = Vec::new();+    for (label, fpath) in [("board", &brd_path), ("schematic", &sch_path)] {+        let fp = crate::fusionpath::for_fusion(&ctx.state_dir, fpath).unwrap_or_else(|_| fpath.replace('\\', "/"));+        let r = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", fp)}));+        let mut okd = r["success"].as_bool() == Some(true);+        if !okd { for _ in 0..10 { std::thread::sleep(Duration::from_secs(3)); if pass("get_app_state", json!({}))["data"]["activeDocument"].as_str().unwrap_or("").to_lowercase().contains(&name.to_lowercase()) { okd = true; break; } } }+        steps.push(format!("{label} open: {}", if okd { "ok".to_string() } else { format!("FAILED: {}", r["error"].as_str().unwrap_or("None").chars().take(80).collect::<String>()) }));+        if !okd { return json!({"success": false, "errorCode": "open_failed", "schPath": sch_path, "brdPath": brd_path, "steps": steps, "error": format!("{label} open failed: {}", r["error"].as_str().unwrap_or("None").chars().take(150).collect::<String>())}); }+        std::thread::sleep(Duration::from_secs(2));+    }+    crate::chains::note_bridge_doc_pub(&ctx.state_dir, &name);+    let placed = pass("board_info", json!({}));+    let bd = if placed["data"].is_object() { placed["data"].clone() } else { placed.clone() };+    json!({"success": true, "documentName": name, "schPath": sch_path, "brdPath": brd_path, "parts": refs, "partsExpected": extracts.len(), "partsPlaced": bd["componentCount"], "boardMm": {"width": bw, "height": bh}, "steps": steps,+        "_hint": format!("Showcase opened as two documents named {name} (board with the placed parts, schematic with the symbols), dialog-free. NEXT: fusion_show_2d_board / fusion_show_3d_board {{capture:true}} to look (3D shows the bound bodies where the library carries package3d urns), fusion_schematic {{action:\"show\"}} for the sheet; fusion_save_to_cloud {{name}} to keep it. No nets were drawn: this is a display, not a circuit.")})+}+ #[cfg(test)] mod tests {     #[test]
rust/lbr/src/lib.rs+31
@@ -700,12 +700,21 @@ pub fn lint_library_geometry(xml: &str) -> Vec<Warning> {         for pkg in packages.children.iter().filter(|c| c.tag == "package") {             let pads: Vec<&Element> = pkg.children.iter().filter(|c| c.tag == "smd").chain(pkg.children.iter().filter(|c| c.tag == "pad")).collect();             let boxes: Vec<(String, (f64, f64, f64, f64))> = pads.iter().map(|p| (p.attr("name").unwrap_or("").to_string(), bbox(p))).collect();+            // pads that share a base name are one net by construction (a heatsink via under an+            // exposed pad is written as B4@2, B4@3 ... and connected to B4's pin): copper that+            // meets between them is the thermal connection, never a short (page issue #89)+            let base = |n: &str| n.split('@').next().unwrap_or(n).trim().to_ascii_uppercase();+            let mut thermal: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();             for i in 0..boxes.len() {                 for j in i + 1..boxes.len() {                     let (na, a) = &boxes[i]; let (nb, b) = &boxes[j];                     let gap_x = (a.0 - b.2).max(b.0 - a.2);                     let gap_y = (a.1 - b.3).max(b.1 - a.3);                     let gap = gap_x.max(gap_y);+                    if base(na) == base(nb) && !na.is_empty() {+                        if gap < 0.2 { *thermal.entry(base(na)).or_insert(0) += 1; }+                        continue;+                    }                     if gap < 0.0 {                         out.push(warn(&[("code", json!("pad_overlap")), ("package", json!(pkg.attr("name"))),                             ("detail", json!(format!("pads {} and {} overlap (copper intersects): a short on the board", py_repr(na), py_repr(nb))))]));@@ -715,6 +724,10 @@ pub fn lint_library_geometry(xml: &str) -> Vec<Warning> {                     }                 }             }+            for (b, n) in thermal {+                out.push(warn(&[("code", json!("heatsink_vias")), ("level", json!("info")), ("package", json!(pkg.attr("name"))),+                    ("detail", json!(format!("{n} same-net pad join(s) under {}: heatsink vias (B4@n) on the exposed pad's own net, the intended thermal connection", py_repr(&b))))]));+            }         }     }     // 3. generic names / descriptions@@ -869,3 +882,21 @@ fn finish(checks: Vec<Check>, xml: Option<&str>) -> Preflight {     }     res }++#[cfg(test)]+mod heatsink_tests {+    #[test]+    fn heatsink_vias_are_not_shorts() {+        let lbr = r#"<eagle version="9.6.2"><drawing><library><packages><package name="QFN-EP">+<smd name="B4" x="0" y="0" dx="3" dy="3" layer="1"/>+<pad name="B4@2" x="0.7" y="0.7" drill="0.3" diameter="0.6"/>+<pad name="B4@3" x="-0.7" y="0.7" drill="0.3" diameter="0.6"/>+<smd name="1" x="0" y="0.2" dx="1" dy="1" layer="1"/>+</package></packages></library></drawing></eagle>"#;+        let w = super::lint_library_geometry(lbr);+        let codes: Vec<String> = w.iter().map(|x| x["code"].as_str().unwrap_or("").to_string()).collect();+        let overlaps: Vec<&serde_json::Map<String, serde_json::Value>> = w.iter().filter(|x| x["code"] == "pad_overlap").collect();+        assert!(overlaps.iter().all(|o| o["detail"].as_str().unwrap_or("").contains("'1'")), "only the genuinely different pad '1' may be flagged: {codes:?}");+        assert!(w.iter().any(|x| x["code"] == "heatsink_vias" && x["level"] == "info"), "same-net joins are reported as info: {codes:?}");+    }+}
skills/fusion-electronics/SKILL.md+4−2
@@ -9,8 +9,10 @@ description: How to drive Fusion 360 Electronics (the EAGLE-based schematic + PC > cloud design and switches to its schematic; `{action:"open", schPath}` opens a local EAGLE .sch (with > its .brd when one sits beside it, so parts instantiate); `{action:"create", name}` writes a blank > schematic and opens it as a new local design with no dialog; `{action:"show"}` switches the open-> design to the schematic view. The reply's `via` names the verb it used. Everything below is the-> detail behind that front door.+> design to the schematic view. The reply's `via` names the verb it used. To put a LIBRARY'S PARTS on+> a new schematic and board (a demo board, "show these sensors"), `fusion_make_showcase_project+> {libraryPath, devicesets}` places them all, with their bound 3D bodies, dialog-free. Everything+> below is the detail behind that front door.  ## 1. ALWAYS open the PROJECT, never a child file 
tools/check-naming.sh

  
tools/check-portable.sh

  
tools/promote.sh

  
tools/release.sh