← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.10.22\ No newline at end of file+1.10.23\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.10.22",+  "version": "1.10.23",   "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.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.10.22"+version = "1.10.23" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.22"+version = "1.10.23" edition = "2021"  [dependencies]
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.22"+DASH_VERSION="1.10.23" 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.22",+  "version": "1.10.23",   "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.22",+  "version": "1.10.23",   "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.22"+version = "1.10.23" edition = "2021" license = "MIT" 
rust/bridge/src/orchestrations.rs+2
@@ -956,6 +956,8 @@ 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")) }+pub fn now_pub() -> f64 { now_f() }+pub fn eagle_run_write_pub(state_dir: &std::path::Path, run_id: &str, v: &Value) { eagle_run_write(state_dir, run_id, v) } 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.
rust/bridge/src/testproject.rs+47−29
@@ -177,6 +177,25 @@ pub fn make_test_project(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> /// "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('\\', "/");+    // Async by contract: two local opens plus fits outlive ab's relay budget, so the default+    // returns a runId at once and the work runs on this exe; poll fusion_eagle_import_status.+    if args["wait"].as_bool() != Some(true) && !lbr_path.is_empty() && std::path::Path::new(&lbr_path).is_file() {+        let run_id = format!("{:x}", ((crate::orchestrations::now_pub() * 1000.0) as u64) ^ ((std::process::id() as u64) << 20));+        crate::orchestrations::eagle_run_write_pub(&ctx.state_dir, &run_id, &json!({"stage": "started", "kind": "showcase", "startedAt": crate::orchestrations::now_pub(), "libraryPath": lbr_path}));+        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_make_showcase_project", "args": a2})).ok()).and_then(|r| r.into_json::<Value>().ok()).unwrap_or(json!({"success": false, "error": "the exe could not post the showcase to itself"}));+            let mut v = json!({"kind": "showcase", "libraryPath": r["libraryPath"]});+            v["stage"] = json!(if r["success"].as_bool() == Some(true) { "done" } else { "failed" }); v["finishedAt"] = json!(crate::orchestrations::now_pub());+            for k in ["documentName", "schematicDocument", "boardDocument", "partsExpected", "partsPlaced", "parts", "schPath", "brdPath", "boardMm", "steps", "error", "errorCode", "_hint"] { if !r[k].is_null() { v[k] = r[k].clone(); } }+            v["result"] = r;+            crate::orchestrations::eagle_run_write_pub(&sd, &rid, &v);+        });+        return json!({"success": true, "started": true, "runId": run_id, "statusVerb": "fusion_eagle_import_status",+            "_hint": format!("Showcase generating and opening in the background (it outlives the relay window). Poll fusion_eagle_import_status {{runId:\"{run_id}\"}} until stage is done: it carries boardDocument, schematicDocument, partsPlaced and the hints. Meanwhile fusion_get_app_state keeps answering.")});+    }     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();@@ -188,24 +207,17 @@ pub fn make_showcase_project(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value)     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>") };+    // The library goes in VERBATIM (whole <library> block, renamed), not re-assembled from+    // extracted pieces: a Fusion-exported library carries <atts>/<att_sets> and other structure+    // the piecewise rebuild lost, and Fusion then refused the whole schematic ("has errors and+    // cannot be opened") and instantiated nothing (ConfRoomROG, 2026-09-20). The board gets the+    // same block minus symbols, devicesets and attribute declarations.+    let lib_re = regex::Regex::new(r"(?s)<library(?:\s[^>]*)?>.*?</library>").unwrap();+    let Some(lib_block) = lib_re.find(&lbr_text).map(|m| m.as_str().to_string()) else { return json!({"success": false, "errorCode": "library_unreadable", "error": "no <library> block in the .lbr"}); };+    let lib_block = regex::Regex::new(r"^<library(?:\s[^>]*)?>").unwrap().replace(&lib_block, format!(r#"<library name="{lib_name}">"#).as_str()).to_string();+    let strip = |t: &str, tag: &str| regex::Regex::new(&format!(r"(?s)<{tag}>.*?</{tag}>")).unwrap().replace_all(t, "").to_string();+    let lib_block_brd = strip(&strip(&strip(&lib_block, "symbols"), "devicesets"), "atts");+    let p3ds: String = regex::Regex::new(r"(?s)<packages3d>.*?</packages3d>").unwrap().find(&lbr_text).map(|m| m.as_str().to_string()).unwrap_or_default();     // 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;@@ -218,34 +230,40 @@ pub fn make_showcase_project(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value)         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() }));+        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)), { let pk = regex::escape(&e.pkg_name); let u = regex::Regex::new(r#"(?s)<package3d [^>]*wip_urn="([^"]+)"[^>]*>(.*?)</package3d>"#).unwrap().captures_iter(&p3ds).find(|c| regex::Regex::new(&format!(r#"<packageinstance name="{pk}"/>"#)).unwrap().is_match(&c[2])).map(|c| c[1].to_string()).unwrap_or_default(); if u.is_empty() { String::new() } else { format!(r#" package3d_urn="{u}""#) } }));         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 sch = format!("{HEAD}{}<schematic xreflabel=\"%F%N/%S.%C%R\" xrefpart=\"/%S.%C%R\"><libraries>{lib_block}</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", extracts[0].layers);+    let brd = format!("{HEAD}{}<board><plain>{}</plain><libraries>{lib_block_brd}</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('\\', "/");+    // two documents with DISTINCT names: a local .sch and a local .brd open as separate designs,+    // so the caller must be able to activate the board one by name for the 2D/3D views+    let (sch_doc, brd_doc) = (format!("{name}-sch"), format!("{name}-brd"));+    let sch_path = d.join(format!("{sch_doc}.sch")).to_string_lossy().replace('\\', "/");+    let brd_path = d.join(format!("{brd_doc}.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)] {+    // schematic first, board last: the board document ends up ACTIVE, so show_2d_board /+    // show_3d_board work at once; each open is followed by WINDOW FIT so the capture shows the parts+    for (label, fpath, docn) in [("schematic", &sch_path, &sch_doc), ("board", &brd_path, &brd_doc)] {         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; } } }+        for _ in 0..10 { if pass("get_app_state", json!({}))["data"]["activeDocument"].as_str().unwrap_or("").to_lowercase().contains(&docn.to_lowercase()) { okd = true; break; } std::thread::sleep(Duration::from_secs(3)); }         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));+        let _ = pass("execute_text_command", json!({"command": "Electron.run WINDOW FIT;"}));+        std::thread::sleep(Duration::from_secs(1));     }-    crate::chains::note_bridge_doc_pub(&ctx.state_dir, &name);+    crate::chains::note_bridge_doc_pub(&ctx.state_dir, &sch_doc); crate::chains::note_bridge_doc_pub(&ctx.state_dir, &brd_doc);     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.")})+    json!({"success": true, "documentName": brd_doc, "boardDocument": brd_doc, "schematicDocument": sch_doc, "libraryPath": lbr_path, "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 local documents, dialog-free: {brd_doc} (the board with the placed parts, ACTIVE now) and {sch_doc} (the sheet with the symbols). NEXT: fusion_show_3d_board {{capture:true}} then fusion_view {{action:\"orbit\", degrees:45}} / {{action:\"iso\"}} to turn the board (the bound 3D bodies render where the library carries package3d urns); fusion_show_2d_board for copper; fusion_activate_document {{name:\"{sch_doc}\"}} then fusion_schematic {{action:\"show\", capture:true}} for the sheet; fusion_save_to_cloud {{name}} to keep either. No nets were drawn: this is a display, not a circuit.")}) }  #[cfg(test)]
tools/check-naming.sh

  
tools/check-portable.sh

  
tools/promote.sh

  
tools/release.sh