← Commit history

list_installed_libraries; uninstall_library not supported (unreleased)

Colby Knox ·7aa6a2345c ·13d ago ·parent c955cd4
7 files changed +78−2
bridge.json+2
@@ -185,6 +185,7 @@     "fusion_list_cloud_projects",     "fusion_list_commands",     "fusion_list_designators",+    "fusion_list_installed_libraries",     "fusion_list_nets",     "fusion_list_text_commands",     "fusion_live_feed",@@ -525,6 +526,7 @@       "project_snapshot": 300,       "project_create": 180,       "test_board": 180,+      "list_installed_libraries": 300,       "diagnostics": 30,       "instances": 10,       "progress": 10,
contracts/describe.json+21
@@ -3488,6 +3488,27 @@       "summary": "Every part designator on the live board, in natural order (C2 before C11).",       "timeoutSeconds": 60     },+    {+      "example": {+        "projectName": "Default Project"+      },+      "input": {+        "budgetSeconds": "default 150",+        "depth": "folder depth, default 3",+        "projectName": "one project to walk (default: every project)"+      },+      "longRunning": false,+      "name": "fusion_list_installed_libraries",+      "output": {+        "complete": "bool",+        "count": "int",+        "foldersWalked": "int",+        "libraries": "[{name, project, folder, version, id, modified}]"+      },+      "statusVerb": null,+      "summary": "The libraries Fusion offers to place from (the standard's list_installed_libraries): the hub's electronics libraries (.flbr files), which Library Manager's Hub Libraries tab shows and fusion_install_library_bundle saves to. Fusion keeps no local list and draws that tab without UI Automation, so this walks the hub's projects and folders through the signed-in session, breadth first to a depth and within a time budget; complete:false when cut short.",+      "timeoutSeconds": 300+    },     {       "example": {         "expectDocument": "fusion-routing"
describe.py+4
@@ -446,6 +446,10 @@ _T = [      {"reason": "a sentence of 10 characters or more, written for the person at the PC (required)"},      {"hwnd": "int", "title": "str", "windowEtiquette": "str"},      30, None, False, {"reason": "Showing you the routed board you asked to see"}),+    ('fusion_list_installed_libraries', "The libraries Fusion offers to place from (the standard's list_installed_libraries): the hub's electronics libraries (.flbr files), which Library Manager's Hub Libraries tab shows and fusion_install_library_bundle saves to. Fusion keeps no local list and draws that tab without UI Automation, so this walks the hub's projects and folders through the signed-in session, breadth first to a depth and within a time budget; complete:false when cut short.",+     {"projectName": "one project to walk (default: every project)", "depth": "folder depth, default 3", "budgetSeconds": "default 150"},+     {"count": "int", "libraries": "[{name, project, folder, version, id, modified}]", "foldersWalked": "int", "complete": "bool"},+     300, None, False, {"projectName": "Default Project"}),     ('fusion_test_board', "Footprints from a .lbr on ONE board (the standard's test_board): a NEW board (wMm x hMm outline, or one that fits the row) when pcbPath is new or omitted, APPENDED right of the current parts when pcbPath is an existing .brd, designators continuing (U4...). Written as EAGLE source, opened in Fusion as a local design dialog-free, and read back. assigns and stubs apply to the LAST part placed.",      {"libraryPath": "a .lbr on the Fusion machine (required)", "footprintName": "str | 'A;B;C' (required)", "pcbPath": "a .brd: existing = append, new = create (default %TEMP%/adom-test-boards/<name>.brd)", "wMm": "number (new board only)", "hMm": "number (new board only)", "assigns": "pad=NET;... (last part)", "stubs": "pad=x,y;... mm from the part's origin (last part): a 0.2 mm top track", "prefix": "designator prefix, default U", "layers": "2|4|6|8 (new board)", "open": "bool, default true"},      {"appended": "bool", "pcb": "path", "placed": "'U1;U2'", "count": "int (parts on the board)", "existingParts": "int", "padsOnComp": "int", "bodiesOnComp": "int", "saved": "bool (checked on disk)", "documentName": "str", "unknownPins": "[]"},
rust/bridge/src/appverbs.rs+47
@@ -82,6 +82,53 @@ pub fn foreground(args: &Value, state_dir: &Path, caller: &Caller) -> Value {            "error": if ok { Value::Null } else { json!(r.err().map(|e| e.to_string()).unwrap_or_else(|| "ab could not bring the window forward".into())) }}) } +/// `fusion_list_installed_libraries`: the libraries Fusion offers to place from are its hub's+/// electronics libraries (.flbr files: what Library Manager's Hub Libraries tab shows, and where+/// fusion_install_library_bundle saves). Fusion keeps no local list a script can read and draws that+/// tab without UI Automation, so this walks the hub's projects and folders (breadth first, to a depth,+/// within a time budget) through the signed-in session and lists the .flbr files.+pub fn list_installed_libraries(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    let started = std::time::Instant::now();+    let budget = std::time::Duration::from_secs(args["budgetSeconds"].as_u64().unwrap_or(150).clamp(10, 900));+    let depth = args["depth"].as_u64().unwrap_or(3) as usize;+    let projects: Vec<String> = match args["projectName"].as_str() {+        Some(p) => vec![p.to_string()],+        None => {+            let r = pass("list_cloud_projects", json!({}));+            if r["success"].as_bool() == Some(false) { return r; }+            let d = if r["data"].is_object() { &r["data"] } else { &r };+            d["hubs"].as_array().into_iter().flatten().flat_map(|h| h["projects"].as_array().cloned().unwrap_or_default()).filter_map(|p| p["name"].as_str().map(str::to_string)).collect()+        }+    };+    let (mut libs, mut walked, mut complete) = (Vec::new(), 0usize, true);+    'projects: for proj in &projects {+        let mut queue: std::collections::VecDeque<(String, usize)> = std::collections::VecDeque::from([(String::new(), 0)]);+        while let Some((folder, lvl)) = queue.pop_front() {+            if started.elapsed() > budget { complete = false; break 'projects; }+            let mut a = json!({"projectName": proj});+            if !folder.is_empty() { a["folderPath"] = json!(folder); }+            let r = pass("list_cloud_files", a);+            let d = if r["data"].is_object() { r["data"].clone() } else { r.clone() };+            walked += 1;+            if d["listTimedOut"] == true { complete = false; }+            for f in d["files"].as_array().into_iter().flatten().filter(|f| f["fileExtension"].as_str().map(|e| e.eq_ignore_ascii_case("flbr")).unwrap_or(false)) {+                libs.push(json!({"name": f["name"], "project": proj, "folder": folder, "version": f["versionNumber"], "id": f["id"], "modified": f["dateModified"]}));+            }+            if lvl < depth {+                // list_cloud_files names each subfolder as {name, ...} (a bare string from older add-ins)+                for sf in d["subfolders"].as_array().into_iter().flatten().filter_map(|x| x["name"].as_str().or(x.as_str())) {+                    queue.push_back((if folder.is_empty() { sf.to_string() } else { format!("{folder}/{sf}") }, lvl + 1));+                }+            }+        }+    }+    let n = libs.len();+    json!({"success": true, "mutated": false, "count": n, "libraries": libs, "projects": projects.len(), "foldersWalked": walked, "complete": complete,+           "elapsedSec": (started.elapsed().as_secs_f64() * 10.0).round() / 10.0,+           "_hint": if complete { "Fusion's hub libraries (.flbr): Library Manager's Hub Libraries tab shows these. fusion_install_library_bundle adds one; fusion_open_cloud_file opens one." }+                    else { "Cut short (budgetSeconds or a folder listing timed out): pass projectName to walk one project, or a larger budgetSeconds." }})+}+ #[cfg(test)] mod tests {     use super::*;
rust/bridge/src/main.rs+2−2
@@ -508,13 +508,13 @@ fn main() {                     "fusion_generate_body_engine" => { let mut r = body::generate(&parsed["args"]); r["bridgeVersion"] = json!(VERSION); r["shell"] = json!("rust"); r },                     "fusion_read_sheet_file" | "read_sheet_file" => { let mut r = sheet::read_sheet_file(&parsed["args"]); r["bridgeVersion"] = json!(VERSION); r["shell"] = json!("rust"); r },                     "fusion_netlist" | "netlist" if parsed["args"]["schPath"].is_string() => { let mut r = sheet::netlist_from_sch(&parsed["args"]); r["bridgeVersion"] = json!(VERSION); r["shell"] = json!("rust"); r },-                    v if routing::owns(v.strip_prefix("fusion_").unwrap_or(v)) || matches!(v.strip_prefix("fusion_").unwrap_or(v), "replicate_layout" | "silkscreen_prep") || sch::owns(v.strip_prefix("fusion_").unwrap_or(v)) || docs::owns(v.strip_prefix("fusion_").unwrap_or(v)) || checks::owns(v.strip_prefix("fusion_").unwrap_or(v)) => {+                    v if routing::owns(v.strip_prefix("fusion_").unwrap_or(v)) || matches!(v.strip_prefix("fusion_").unwrap_or(v), "replicate_layout" | "silkscreen_prep" | "list_installed_libraries") || sch::owns(v.strip_prefix("fusion_").unwrap_or(v)) || docs::owns(v.strip_prefix("fusion_").unwrap_or(v)) || checks::owns(v.strip_prefix("fusion_").unwrap_or(v)) => {                         let (sc, ca) = (sidecar.clone(), caller.clone());                         let side = |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 cmd=v.strip_prefix("fusion_").unwrap_or(v);-                        let mut r=match cmd { "bodies" => bodies::bodies(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "replicate_layout" => replicate::replicate_layout(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "silkscreen_prep" => checks::silkscreen_prep(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "simulate" => spice::simulate(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "jlcpcb_order" => order::jlcpcb_order(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "export_bom" => routing::export_bom(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "save_document" => savedoc::save_document(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "save_to_cloud" => savedoc::save_to_cloud(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if sch::owns(c) => sch::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if docs::owns(c) => docs::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if writers::owns(c) => writers::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if checks::owns(c) => checks::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), _ => routing::call(cmd,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)) };+                        let mut r=match cmd { "bodies" => bodies::bodies(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "replicate_layout" => replicate::replicate_layout(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "silkscreen_prep" => checks::silkscreen_prep(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "list_installed_libraries" => appverbs::list_installed_libraries(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "simulate" => spice::simulate(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "jlcpcb_order" => order::jlcpcb_order(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "export_bom" => routing::export_bom(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "save_document" => savedoc::save_document(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "save_to_cloud" => savedoc::save_to_cloud(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if sch::owns(c) => sch::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if docs::owns(c) => docs::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if writers::owns(c) => writers::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if checks::owns(c) => checks::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), _ => routing::call(cmd,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)) };                         if cmd=="netlist" { r=sheet::shape_board_netlist(r,&parsed["args"]); }                         r["bridgeVersion"]=json!(VERSION); r["shell"]=json!("rust"); r                     },
rust/bridge/src/unsupported.rs+1
@@ -29,6 +29,7 @@ pub const TABLE: &[(&str, &str)] = &[     ("plate_vias", "EAGLE pads are always plated: an unplated hole is a separate hole object"),     ("panel", "Altium's docked panel tabs; Fusion's editors show their own panels, and the bridge opens a surface (library, symbol, footprint, board, schematic, 3D) with fusion_show_* in the background"),     ("selection", "Fusion does not show the user's board selection to the bridge: a ULP's ingroup() reads no part even right after a GROUP (checked live on Fusion 2706, 2026-10-30). Name the parts instead; fusion_electron_select shows one to the user"),+    ("uninstall_library", "a library Fusion offers is a file in the Autodesk hub, so taking it out means deleting that file for everyone in the hub, which the standard's uninstall never does (it leaves files alone); a hub file the AI made itself is deleted with fusion_cleanup_cloud_files"),     ("component_classes", "an EAGLE board has net classes (fusion_net_classes, fusion_add_rule) but no component classes or rooms: group a channel by its designators (fusion_replicate_layout takes them as lists)"), ]; 
tests/run_verb_tests.py+1
@@ -468,6 +468,7 @@ t("fusion_instances", "daily", {}, {"ok": True, "fields": ["count", "instances"] t("fusion_progress", "daily", {}, {"ok": True, "fields": ["active", "verbTimes"]}, "the bridge's timing book") t("fusion_foreground", "manual", {"reason": "battery check of the foreground verb"}, {"ok": True}, "brings Fusion in front: manual only, never in an unattended battery") t("fusion_foreground#noreason", "negative", {}, {"pred": "must_refuse"}, "a foreground with no reason is refused")+t("fusion_list_installed_libraries", "daily", {"projectName": "Default Project", "depth": 2}, {"ok": True, "fields": ["libraries", "complete"]}, "the hub's .flbr libraries in one project") t("fusion_build_board", "daily", {"libraryPath": BOARDLIB, "pcbPath": BOARDDIR + "/build.brd", "parts": [{"ref": "R1", "footprint": "R0603", "x": 5, "y": 5}, {"ref": "Q1", "footprint": "SOT23", "x": 12, "y": 5}], "nets": {"GND": ["R1.2", "Q1.2"]}, "outline": {"rect": [0, 0, 18, 10], "cornerRadius": 1}, "open": False}, {"ok": True, "fields": ["pcbPath", "unknownPins"]}, "written offline (open:false keeps the battery from opening a design each run)") t("fusion_build_board#missing", "negative", {"libraryPath": BOARDLIB, "pcbPath": BOARDDIR + "/bad.brd", "parts": [{"ref": "U1", "footprint": "NO_SUCH_FP", "x": 0, "y": 0}], "open": False}, {"pred": "must_refuse"}, "a footprint missing from the library refuses the whole build") t("fusion_test_board", "daily", {"libraryPath": BOARDLIB, "footprintName": "R0603;SOT23", "pcbPath": BOARDDIR + "/test.brd", "open": False}, {"ok": True, "fields": ["pcb", "placed", "saved"]}, "test.brd is made on the first run and appended to after (designators continue); written offline")