app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
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.
← Commit history
Update 163 file(s)
19 files changed
+258−45
BRIDGE_VERSION+1−1bridge.json+4−2contracts/describe.json+51−2dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1describe.py+8−2install.sh+1−1package.json+1−1page.json+11−27rust/Cargo.toml+1−1rust/bridge/src/main.rs+1−1rust/bridge/src/orchestrations.rs+63−3rust/bridge/src/testproject.rs+79rust/lbr/src/lib.rs+31skills/fusion-electronics/SKILL.md+4−2tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.shBRIDGE_VERSION+1−1@@ -1 +1 @@-1.10.21+1.10.22
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]"- }-}+ ]+}
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.shtools/check-portable.shtools/promote.shtools/release.sh