← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.10.10\ No newline at end of file+1.10.11\ No newline at end of file
addin/AdomBridge/commands/open_electronics_file.py+63−10
@@ -19,6 +19,62 @@ import os import time  ++_SIBLING_EXTS = ("sch", "brd", "lbr", "step", "stp", "f3d", "edru", "dru", "flbr", "fsch", "fbrd", "ulp")+++def _stage_root():+    """The first space-free, writable stage root (a stage under a path with a space is useless)."""+    import os+    cands = [os.path.join(os.path.expanduser("~"), ".adom", "fusion-bridge", "stage")]+    for env in ("LOCALAPPDATA", "TEMP"):+        v = os.environ.get(env, "")+        if v:+            cands.append(os.path.join(v, "adom-fusion-stage"))+    cands.append("C:/adom-fusion-stage")+    for c in cands:+        c = c.replace("\\", "/")+        if " " in c:+            continue+        try:+            os.makedirs(c, exist_ok=True)+            return c+        except Exception:+            continue+    return None+++def _stage_for_fusion(file_path):+    """Copy the file (and its same-stem siblings) under a space-free folder; return the copy's path."""+    import os, shutil, hashlib+    src = file_path.replace("\\", "/")+    if not os.path.isfile(src):+        return None+    root = _stage_root()+    if not root:+        return None+    parent = os.path.dirname(src)+    bucket = hashlib.sha1(parent.lower().encode("utf-8")).hexdigest()[:8]+    d = os.path.join(root, bucket).replace("\\", "/")+    os.makedirs(d, exist_ok=True)+    stem = os.path.splitext(os.path.basename(src))[0].lower()+    for name in os.listdir(parent):+        q = os.path.join(parent, name)+        if not os.path.isfile(q):+            continue+        s, ext = os.path.splitext(name)+        if q.replace("\\", "/") == src or (s.lower() == stem and ext.lstrip(".").lower() in _SIBLING_EXTS):+            dst = os.path.join(d, name)+            try:+                if not (os.path.exists(dst) and os.path.getsize(dst) == os.path.getsize(q) and os.path.getmtime(dst) >= os.path.getmtime(q)):+                    shutil.copy2(q, dst)+            except Exception:+                if q.replace("\\", "/") == src:+                    raise+    out = os.path.join(d, os.path.basename(src)).replace("\\", "/")+    return out if os.path.isfile(out) else None++ def _open_local_document(app, file_path: str):     """Run Document.newDesignFromLocal for a path that MAY CONTAIN SPACES. @@ -32,8 +88,10 @@ def _open_local_document(app, file_path: str):      Two-step, so it works whichever way this Fusion build's parser behaves:       1. QUOTE the path.-      2. If that fails and the path has spaces, retry with the Windows 8.3 SHORT path-         (GetShortPathNameW) - same file, no spaces, no copying.+      2. If that fails and the path has spaces, retry with a STAGED copy of the file (plus its+         same-stem siblings) under a space-free folder. Never the 8.3 short path: Fusion titles+         the document after the path it is given, and "0603WA~1" broke every name check+         (bridge 1.10.11, issue #83).      Returns (result, how) and raises the LAST exception if every attempt fails.     """@@ -42,14 +100,9 @@ def _open_local_document(app, file_path: str):     attempts.append((quoted, "quoted"))     if " " in file_path:         try:-            import ctypes-            from ctypes import wintypes-            _gsp = ctypes.windll.kernel32.GetShortPathNameW-            _gsp.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]-            _gsp.restype = wintypes.DWORD-            buf = ctypes.create_unicode_buffer(600)-            if _gsp(file_path, buf, 600) and buf.value and " " not in buf.value:-                attempts.append((buf.value, "shortpath"))+            staged = _stage_for_fusion(file_path)+            if staged and " " not in staged:+                attempts.append((staged, "staged"))         except Exception:             pass     def _doc_name():
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.10.10",+  "version": "1.10.11",   "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.10"+version = "1.10.11" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.10"+version = "1.10.11" 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.10"+DASH_VERSION="1.10.11" 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.10",+  "version": "1.10.11",   "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.10",+  "version": "1.10.11",   "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.10"+version = "1.10.11" edition = "2021" license = "MIT" 
rust/PORTING.md+8
@@ -605,3 +605,11 @@ port learned:   the post-envelope (polled for 1.2 s after any window-raising verb) and by the sentinel, and   carried in the routing reply with a checkpoint hint; the document snapshot behind it is a   3 s undiagnosed hop so a hung main thread costs seconds, not the relay's whole minute.++## Never an 8.3 path (2026-09-10)++Fusion's text commands cannot take a path with a space and reject the quoted form, and the old+answer (GetShortPathNameW) made Fusion title documents "0603WA~1", which silently broke every+name check. `fusionpath::for_fusion` stages the file and its siblings into a space-free folder+instead; `output_for_fusion` + `finish_output` do the same for files Fusion writes. If a new+verb hands Fusion a path, go through those, never through `short_path`.
rust/bridge/src/addin.rs+4−2
@@ -200,7 +200,9 @@ pub struct Ctx<'a> {     pub host: &'a dyn fusion_core::HostProcess,     /// `HostPaths::fusion_executable()` at call time (None = not installed)     pub fusion_exe: Option<std::path::PathBuf>,-    /// `HostPaths::short_path` (the 8.3 form Fusion's text commands need)+    /// `HostPaths::short_path`. Unused since 1.10.11: paths with spaces are STAGED into a+    /// space-free folder (`fusionpath`) instead of shortened, because Fusion titles a document+    /// after the path it is given and the 8.3 alias ("0603WA~1") broke every name check.     pub short_path: fn(&str) -> String,     pub state_dir: std::path::PathBuf,     pub caller: fusion_core::ab::Caller,@@ -211,7 +213,7 @@ pub struct Ctx<'a> {     pub dialogs: &'a dyn Fn(Value) -> Value, } -impl<'a> Ctx<'a> { pub fn host_paths_short(&self, p: &str) -> String { (self.short_path)(p) } }+impl<'a> Ctx<'a> { #[deprecated(note = "stage with crate::fusionpath::for_fusion; never hand Fusion an 8.3 alias")] #[allow(dead_code)] pub fn host_paths_short(&self, p: &str) -> String { (self.short_path)(p) } }  /// The whole pass-through for one verb. pub fn pass_through(cmd: &str, args: &Value, ctx: &Ctx) -> Value {
rust/bridge/src/fusionpath.rsadded+117
@@ -0,0 +1,117 @@+//! Paths Fusion's text commands can take.+//!+//! `executeTextCommand` is EAGLE's space-delimited command line and its quoted form fails, so a+//! file that lives under a path with a space (the bridge cache under "Adom Bridge", a profile+//! like "John Lauer", "Program Files") is STAGED: copied, with its same-stem siblings, into a+//! space-free folder, and Fusion is handed the copy. Never the DOS 8.3 alias: Fusion titles the+//! document after the path it was given, and "0603WA~1" broke every name check (issue #83).+use std::path::{Path, PathBuf};++/// Files that travel with the one being opened (a .sch wants its .brd beside it, a mint wants+/// the .step next to the .lbr).+pub const SIBLING_EXTS: &[&str] = &["sch", "brd", "lbr", "step", "stp", "f3d", "edru", "dru", "flbr", "fsch", "fbrd", "ulp"];++fn has_space(p: &Path) -> bool { p.to_string_lossy().contains(' ') }++/// The first space-free, writable stage root: the bridge state dir, then LOCALAPPDATA, then the+/// drive root. A stage under a path with a space would defeat the whole point.+pub fn stage_root(state_dir: &Path) -> Result<PathBuf, String> {+    let mut candidates: Vec<PathBuf> = vec![state_dir.join("stage")];+    if let Ok(la) = std::env::var("LOCALAPPDATA") { if !la.trim().is_empty() { candidates.push(PathBuf::from(la).join("adom-fusion-stage")); } }+    if let Ok(t) = std::env::var("TEMP") { if !t.trim().is_empty() { candidates.push(PathBuf::from(t).join("adom-fusion-stage")); } }+    candidates.push(PathBuf::from(if cfg!(windows) { "C:/adom-fusion-stage" } else { "/tmp/adom-fusion-stage" }));+    for c in candidates {+        if has_space(&c) { continue; }+        if std::fs::create_dir_all(&c).is_ok() { return Ok(c); }+    }+    Err("no space-free folder is writable for staging a file Fusion can open (state dir, LOCALAPPDATA, TEMP and the drive root all failed)".into())+}++fn bucket(parent: &Path) -> String {+    use std::hash::{Hash, Hasher};+    let mut h = std::collections::hash_map::DefaultHasher::new();+    parent.to_string_lossy().to_lowercase().replace('\\', "/").hash(&mut h);+    format!("{:08x}", h.finish() as u32)+}++fn fwd(p: &Path) -> String { p.to_string_lossy().replace('\\', "/") }++/// The path to hand Fusion for an INPUT file: the file itself when its path has no space,+/// otherwise a staged copy (siblings included) under a space-free folder. The document Fusion+/// opens keeps its real name either way.+pub fn for_fusion(state_dir: &Path, path: &str) -> Result<String, String> {+    let p = PathBuf::from(path.replace('\\', "/"));+    if !has_space(&p) { return Ok(fwd(&p)); }+    if !p.is_file() { return Err(format!("{} does not exist, so it cannot be staged for Fusion", fwd(&p))); }+    let parent = p.parent().map(Path::to_path_buf).unwrap_or_default();+    let dir = stage_root(state_dir)?.join(bucket(&parent));+    std::fs::create_dir_all(&dir).map_err(|e| format!("cannot create stage folder {}: {e}", fwd(&dir)))?;+    let stem = p.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();+    let mut copied = Vec::new();+    if let Ok(rd) = std::fs::read_dir(&parent) {+        for e in rd.flatten() {+            let q = e.path();+            if !q.is_file() { continue; }+            let same_stem = q.file_stem().map(|s| s.to_string_lossy().eq_ignore_ascii_case(&stem)).unwrap_or(false);+            let ext_ok = q.extension().map(|x| SIBLING_EXTS.contains(&x.to_string_lossy().to_lowercase().as_str())).unwrap_or(false);+            if q == p || (same_stem && ext_ok) {+                let dst = dir.join(q.file_name().unwrap());+                let fresh = match (std::fs::metadata(&q), std::fs::metadata(&dst)) { (Ok(a), Ok(b)) => a.len() == b.len() && a.modified().ok() <= b.modified().ok(), _ => false };+                if !fresh { std::fs::copy(&q, &dst).map_err(|e| format!("cannot stage {} for Fusion: {e}", fwd(&q)))?; }+                copied.push(dst);+            }+        }+    }+    let staged = dir.join(p.file_name().unwrap());+    if !staged.is_file() { return Err(format!("staging {} produced no file", fwd(&p))); }+    Ok(fwd(&staged))+}++/// For an OUTPUT path Fusion will write: hand Fusion a space-free target when the wanted one+/// has a space, and return the staged path to move afterwards with `finish_output`.+pub fn output_for_fusion(state_dir: &Path, out: &str) -> Result<(String, Option<String>), String> {+    let p = PathBuf::from(out.replace('\\', "/"));+    if !has_space(&p) { return Ok((fwd(&p), None)); }+    let dir = stage_root(state_dir)?.join("out");+    std::fs::create_dir_all(&dir).map_err(|e| format!("cannot create stage folder {}: {e}", fwd(&dir)))?;+    let staged = dir.join(p.file_name().ok_or("output path has no file name")?);+    let _ = std::fs::remove_file(&staged);+    Ok((fwd(&staged), Some(fwd(&staged))))+}++/// Move a staged output to where the caller asked for it.+pub fn finish_output(staged: &str, out: &str) -> Result<(), String> {+    if let Some(parent) = Path::new(out).parent() { let _ = std::fs::create_dir_all(parent); }+    std::fs::copy(staged, out).map_err(|e| format!("cannot move the staged output to {out}: {e}"))?;+    let _ = std::fs::remove_file(staged);+    Ok(())+}++#[cfg(test)]+mod tests {+    use super::*;+    #[test]+    fn spaced_inputs_are_staged_with_siblings_and_named_the_same() {+        let root = std::env::temp_dir().join(format!("fusionpath-{}", std::process::id()));+        let src = root.join("Adom Bridge").join("samples"); std::fs::create_dir_all(&src).unwrap();+        std::fs::write(src.join("0603WAF3300T5E.lbr"), b"<eagle/>").unwrap();+        std::fs::write(src.join("0603WAF3300T5E.step"), b"ISO-10303").unwrap();+        std::fs::write(src.join("other.step"), b"x").unwrap();+        let state = root.join("state");+        let got = for_fusion(&state, &src.join("0603WAF3300T5E.lbr").to_string_lossy()).unwrap();+        assert!(!got.contains(' '), "{got}");+        assert!(got.ends_with("/0603WAF3300T5E.lbr"), "{got}");+        let dir = Path::new(&got).parent().unwrap();+        assert!(dir.join("0603WAF3300T5E.step").is_file(), "sibling staged");+        assert!(!dir.join("other.step").exists(), "unrelated file not staged");+        let plain = root.join("plain"); std::fs::create_dir_all(&plain).unwrap(); std::fs::write(plain.join("a.brd"), b"y").unwrap();+        assert_eq!(for_fusion(&state, &plain.join("a.brd").to_string_lossy()).unwrap(), plain.join("a.brd").to_string_lossy().replace('\\', "/"));+        assert!(for_fusion(&state, &src.join("missing.lbr").to_string_lossy()).is_err());+        let (o, staged) = output_for_fusion(&state, &root.join("out dir").join("lib.flbr").to_string_lossy()).unwrap();+        assert!(!o.contains(' ') && staged.is_some());+        std::fs::write(&o, b"flbr").unwrap();+        finish_output(&o, &root.join("out dir").join("lib.flbr").to_string_lossy()).unwrap();+        assert!(root.join("out dir").join("lib.flbr").is_file());+        let _ = std::fs::remove_dir_all(&root);+    }+}
rust/bridge/src/main.rs+1
@@ -39,6 +39,7 @@ mod addin_install; mod janitor; mod electronics; mod routing;+mod fusionpath; mod tour; mod palette; use fusion_core::ab::Caller;
rust/bridge/src/orchestrations.rs+5−2
@@ -119,7 +119,8 @@ pub fn open_lbr(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) ->     let verify = args["verify"].as_bool().unwrap_or(false);     crate::chains::note_bridge_doc_pub(&ctx.state_dir, &file_path);     let mut results: Vec<String> = Vec::new();-    let open_result = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", ctx.host_paths_short(&file_path))}));+    let fusion_path = match crate::fusionpath::for_fusion(&ctx.state_dir, &file_path) { Ok(p) => p, Err(e) => return json!({"success": false, "errorCode": "stage_failed", "error": e}) };+    let open_result = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", fusion_path)}));     if open_result["success"].as_bool() != Some(true) {         let expected = stem.to_lowercase();         let mut opened_anyway = false;@@ -756,8 +757,10 @@ pub fn save_lbr(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) ->     if out.is_empty() { return json!({"success": false, "error": "No outputPath specified"}); }     if !out.to_lowercase().ends_with(".flbr") { out.push_str(".flbr"); }     if let Some(parent) = std::path::Path::new(&out).parent() { if !parent.as_os_str().is_empty() && !parent.exists() { if let Err(e) = std::fs::create_dir_all(parent) { return json!({"success": false, "error": format!("Cannot create directory {}: {e}", parent.to_string_lossy())}); } } }-    let r = pass("execute_text_command", json!({"command": format!("Document.CopyToDesktop {}", ctx.host_paths_short(&out))}));+    let (target, staged) = match crate::fusionpath::output_for_fusion(&ctx.state_dir, &out) { Ok(t) => t, Err(e) => return json!({"success": false, "errorCode": "stage_failed", "error": e}) };+    let r = pass("execute_text_command", json!({"command": format!("Document.CopyToDesktop {}", target)}));     if r["success"].as_bool() != Some(true) { return json!({"success": false, "error": format!("Document.CopyToDesktop failed: {}", r["error"].as_str().unwrap_or("unknown")), "data": {"outputPath": out}}); }+    if let Some(s) = staged { if let Err(e) = crate::fusionpath::finish_output(&s, &out) { return json!({"success": false, "errorCode": "stage_failed", "error": e, "data": {"outputPath": out, "stagedAt": s}}); } }     let mut result = match std::fs::metadata(&out) { Ok(m) => json!({"success": true, "output": format!("Saved library to {} ({} bytes)", out.rsplit('/').next().unwrap_or(&out), m.len()), "data": {"outputPath": out, "fileSize": m.len()}}), Err(_) => json!({"success": true, "output": "Document.CopyToDesktop executed (file may still be writing)", "data": {"outputPath": out}}) };     let (shots, _) = crate::lifecycle::post_open_screenshot(ctx, 1.0);     result["data"]["postOpenScreenshot"] = shots;
rust/bridge/src/testproject.rs+4−2
@@ -117,7 +117,8 @@ pub fn test_project_activate(ctx: &Ctx, kind: &str, pass: &dyn Fn(&str, Value) -     let Some(tp) = crate::small_verbs::get_pref(&ctx.state_dir, "testProject") else { return false };     let Some(path) = tp[kind].as_str().filter(|p| !p.is_empty()) else { return false };     let base = tp["docBase"].as_str().unwrap_or("").to_lowercase();-    let _ = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", ctx.host_paths_short(path))}));+    let fp = crate::fusionpath::for_fusion(&ctx.state_dir, path).unwrap_or_else(|_| path.replace('\\', "/"));+    let _ = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", fp)}));     for _ in 0..6 {         std::thread::sleep(std::time::Duration::from_secs(2));         if !base.is_empty() && pass("get_app_state", json!({}))["data"]["activeDocument"].as_str().unwrap_or("").to_lowercase().contains(&base) { return true; }@@ -150,7 +151,8 @@ pub fn make_test_project(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) ->     let _ = std::fs::write(&sch_path, &sch); let _ = std::fs::write(&brd_path, &brd);     let mut steps: Vec<String> = Vec::new();     for (label, fpath) in [("board", &brd_path), ("schematic", &sch_path)] {-        let r = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", ctx.host_paths_short(fpath))}));+        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 {             let expected = fpath.rsplit('/').next().unwrap_or("").rsplit_once('.').map(|(s, _)| s.to_lowercase()).unwrap_or_default();
tools/check-naming.sh

  
tools/check-portable.sh

  
tools/promote.sh

  
tools/release.sh