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
v1.6.60: completeness = Fusion360.exe (NOT launcher/.ini) - fixes false installed:true mid-stream + rejecting a healthy install; reliable disk-state 'installing'; rich sign-in hints + new fusion-autodesk-signin skill (warm SSO, Identity Manager handoff, code-expiry, notify_owner escalation)
8 files changed
+268−60
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.59+1.6.60
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.59",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.59/adom-bridge-fusion-v1.6.59.zip",- "sha256": "a4269760b2aa726a06ec346e3dda78b65db5cd5a87e9055b34c4bbb82ad5140c",- "size": 349140,+ "version": "1.6.60",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.60/adom-bridge-fusion-v1.6.60.zip",+ "sha256": "03b4b9b136925eb216517e1051d0fc4fb378fdc5a32c0db7820657fe3d22599f",+ "size": 355794, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.59",+ "version": "1.6.60", "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/adom-desktop-fusion-bridge", "author": "Adom Inc.",
fusion_detect.py+107−43@@ -79,14 +79,61 @@ def detect_fusion() -> dict: } -def _find_fusion_launcher() -> Path | None:- """Find the newest FusionLauncher.exe across ALL webdeploy bases.+# ---------------------------------------------------------------------------------------------------+# INSTALL-COMPLETENESS DETECTION (root-caused live on a fresh Hyper-V VM, John 2026-07-14)+#+# WHAT A WEBDEPLOY INSTALL ACTUALLY LOOKS LIKE (verified on disk, do not assume):+# %LOCALAPPDATA%\Autodesk\webdeploy\production\ holds one hash dir PER content-build. A COMPLETE+# Fusion install is ~615 files (Fusion360.exe ~880 KB + hundreds of DLLs + Qt/ + Python/ + ...).+# BUT Autodesk ALSO drops a tiny 8-file launcher STUB dir next to it: icons + FusionLauncher.exe+# + a small FusionLauncher.exe.ini (~412 bytes). So at steady state there are TWO hash dirs.+#+# THE TRAP that broke the fresh VM (and the two earlier "fixes" that were WRONG):+# * Keying "installed" off FusionLauncher.exe EXISTING -> true mid-stream (launcher lands early) ->+# premature fusion_start -> "Error Launching Streamed Application ... .ini missing/incomplete".+# * Keying off FusionLauncher.exe.ini existing / its SIZE -> ALSO wrong: the COMPLETE app dir has+# NO .ini at all; only the throwaway stub carries a 412-byte .ini. A size gate would REJECT a+# perfectly good install and ACCEPT nothing. (Measured it: complete app dir = 615 files, no .ini.)+#+# THE RELIABLE SIGNAL: the real application binary Fusion360.exe. It only exists (full size) when the+# payload finished streaming. Gate "installed" on Fusion360.exe, NEVER on the launcher or its .ini.+# ---------------------------------------------------------------------------------------------------+_FUSION_APP_EXE = "Fusion360.exe" # the ACTUAL app binary (~880 KB); the completeness marker+_MIN_APP_EXE_BYTES = 200_000 # guards against a 0-byte placeholder the streamer writes early+++def _app_dir_complete(subdir: Path) -> bool:+ """A production hash dir is a COMPLETE Fusion install ONLY when it contains the real app binary+ Fusion360.exe at full size. This is process-independent and version-stable. Do NOT substitute+ FusionLauncher.exe (lands early, mid-stream) or FusionLauncher.exe.ini (absent from the complete+ app dir; only the launcher stub has one). See the block comment above for the on-disk anatomy."""+ exe = subdir / _FUSION_APP_EXE+ try:+ return exe.exists() and exe.stat().st_size >= _MIN_APP_EXE_BYTES+ except OSError:+ return False - Checks the per-user (%LOCALAPPDATA%) AND the system-wide (%ProgramFiles%,- from `--globalinstall`) locations. Prefers a COMPLETE install (one that also- has FusionLauncher.exe.ini) and, among those, the newest by mtime.- """- candidates = [] # (has_ini, mtime, launcher_path)++def _any_app_complete() -> bool:+ """True if ANY webdeploy hash dir holds a complete Fusion (Fusion360.exe). This is the single+ source of truth for 'Fusion is installed'."""+ for base in _WEBDEPLOY_BASES:+ try:+ if base.exists():+ for subdir in base.iterdir():+ if subdir.is_dir() and _app_dir_complete(subdir):+ return True+ except OSError:+ continue+ return False+++def _find_fusion_launcher() -> Path | None:+ """Return the FusionLauncher.exe to launch, but ONLY when a COMPLETE install exists (a hash dir+ with Fusion360.exe). Otherwise None -> detect_fusion() reports NOT installed, and readiness+ surfaces the streamer as installing. Prefers the launcher living beside the newest complete app;+ falls back to any FusionLauncher.exe on disk, then to Fusion360.exe itself."""+ complete_dirs = [] # (mtime, subdir) for base in _WEBDEPLOY_BASES: if not base.exists(): continue@@ -95,41 +142,63 @@ def _find_fusion_launcher() -> Path | None: except OSError: continue for subdir in subdirs:- launcher = subdir / "FusionLauncher.exe"- if not launcher.exists():+ if _app_dir_complete(subdir):+ try:+ mtime = subdir.stat().st_mtime+ except OSError:+ mtime = 0.0+ complete_dirs.append((mtime, subdir))++ if not complete_dirs:+ return None # nothing complete -> NOT installed+ complete_dirs.sort(key=lambda c: c[0], reverse=True) # newest complete install wins+ newest = complete_dirs[0][1]+ launcher = newest / "FusionLauncher.exe"+ if launcher.exists():+ return launcher+ # complete app dir lacks its own launcher (unusual) - use any FusionLauncher.exe, else the app exe+ for _mt, subdir in complete_dirs:+ cand = subdir / "FusionLauncher.exe"+ if cand.exists():+ return cand+ return newest / _FUSION_APP_EXE+++def _incomplete_webdeploy_present() -> bool:+ """Disk-state (process-INDEPENDENT) 'a fresh install is actively streaming' signal: some hash dir+ has FusionLauncher.exe but NO complete Fusion360.exe exists ANYWHERE yet. Why disk-state and not a+ tasklist check: the Autodesk streamer spawns short-lived per-chunk worker processes, so+ _installer_running() reads False most of a live multi-GB stream (flickered True only 3 of 25 polls+ during a real install, John 2026-07-14). Why gate on 'no complete app anywhere': once the app IS+ complete, the leftover launcher stub (FusionLauncher.exe, no Fusion360.exe) is NORMAL and must NOT+ read as 'still installing'. (An auto-UPDATE that streams a new build beside a complete old one is+ handled separately by fusion_update_in_progress(), not here.) Never raises."""+ if _any_app_complete():+ return False # a complete install exists -> not installing+ for base in _WEBDEPLOY_BASES:+ try:+ if not base.exists(): continue- has_ini = (subdir / "FusionLauncher.exe.ini").exists()- try:- mtime = subdir.stat().st_mtime- except OSError:- mtime = 0.0- candidates.append((has_ini, mtime, launcher))-- if not candidates:- return None- # Only a COMPLETE install (FusionLauncher.exe.ini present) counts as installed. A hash dir with- # FusionLauncher.exe but NO .ini is a half-streamed/corrupt install - returning it made- # detect_fusion() report installed:true mid-stream, so a fusion_start fired into it and hit- # "Error Launching Streamed Application ... .ini is missing or incomplete" (fresh Hyper-V VM,- # John, 2026-07-14). If nothing is complete, report NOT installed (readiness then surfaces the- # streamer as installing).- complete = [c for c in candidates if c[0]]- if complete:- complete.sort(key=lambda c: c[1], reverse=True) # newest complete install wins- return complete[0][2]- return None---_MIN_VALID_INI_BYTES = 512 # the truncated launcher .ini that broke the fresh VM was 412 bytes+ for subdir in base.iterdir():+ if subdir.is_dir() and (subdir / "FusionLauncher.exe").exists():+ return True # launcher present, no complete app -> streaming+ except OSError:+ continue+ return False def _clean_incomplete_webdeploy() -> list:- """Remove webdeploy production hash dirs that are INCOMPLETE: a FusionLauncher.exe present but its- FusionLauncher.exe.ini missing or truncated (< _MIN_VALID_INI_BYTES). These are the residue of an- interrupted stream that throws 'Error Launching Streamed Application'. Only ever called AFTER a- launch failure (we already know something is corrupt), so aggressive cleanup is safe. Returns the- dir names removed. Never raises."""+ """Remove the residue of an INTERRUPTED stream so the next fusion_install_fusion re-streams clean.+ Only ever called AFTER a launch failure (we already know something is corrupt).++ SAFETY GUARD: if a COMPLETE install (Fusion360.exe) exists anywhere, do NOTHING and return [] -+ the launcher-only stub dir beside a complete app is a LEGIT part of the install, not corruption,+ and nuking it would break a working Fusion. Only when NO complete app exists (a genuinely half-+ streamed state) do we remove the partial launcher-bearing dirs. Never raises. (John 2026-07-14:+ the old .ini-size test would have deleted the legit stub of a healthy install.)""" import shutil+ if _any_app_complete():+ return [] # healthy install present - never touch it removed = [] for base in _WEBDEPLOY_BASES: if not base.exists():@@ -141,12 +210,7 @@ def _clean_incomplete_webdeploy() -> list: for subdir in subdirs: if not (subdir / "FusionLauncher.exe").exists(): continue- ini = subdir / "FusionLauncher.exe.ini"- try:- ok = ini.exists() and ini.stat().st_size >= _MIN_VALID_INI_BYTES- except OSError:- ok = False- if not ok:+ if not _app_dir_complete(subdir): # partial: launcher but no real app binary try: shutil.rmtree(subdir, ignore_errors=True) removed.append(subdir.name)
server.py+28−11@@ -21,7 +21,7 @@ from pathlib import Path # Ensure the plugin root is on the path sys.path.insert(0, str(Path(__file__).parent)) -from fusion_detect import detect_fusion, ensure_fusion_running, wait_for_addin, _is_fusion_running, fusion_update_in_progress, find_licensing_dialog, family_windows+from fusion_detect import detect_fusion, ensure_fusion_running, wait_for_addin, _is_fusion_running, fusion_update_in_progress, find_licensing_dialog, family_windows, _incomplete_webdeploy_present from handlers.open_design import handle_open_design from handlers.close_fusion import handle_close_fusion, handle_fusion_stop, handle_fusion_kill from handlers.dismiss_recovery import dismiss_recovery_dialog@@ -1136,7 +1136,12 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict: # (found live on a fresh VM 2026-07-05: NO_WEBDEPLOY on disk, yet readiness said # ready). detect_fusion() is a fast filesystem scan, so pay it every call. raw_installed = bool(detect_fusion().get("installed"))- streaming = _installer_running()+ # `streaming` must be RELIABLE. _installer_running() is a tasklist check, but the Autodesk streamer+ # spawns short-lived per-chunk workers, so it reads False most of a live multi-GB stream (flickered+ # True only 3 of 25 polls during a real install, John 2026-07-14). OR in the disk-state signal - a+ # webdeploy hash dir with FusionLauncher.exe but a missing/truncated .ini - which is present for the+ # WHOLE stream. Now `installing` is trustworthy, not racy.+ streaming = _installer_running() or _incomplete_webdeploy_present() running = _is_fusion_running() if raw_installed else False # FRESH-INSTALL COMPLETENESS GUARD (John caught this live 2026-07-14 on a fresh Hyper-V VM): # the streamer writes FusionLauncher.exe BEFORE it finishes writing FusionLauncher.exe.ini, so@@ -1255,15 +1260,27 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict: "10-30 min). Meanwhile, wrap modeling/export verbs in a short retry/catch-loop to " "ride the up-windows rather than treating an intermittent 'not running' as fatal.") elif needs_signin:- hint = ("Fusion is RUNNING but stuck at the first-run Autodesk SIGN-IN (main UI blocked, "- "add-in cannot serve) - NOT ready. AUTOMATE the sign-in (see the fusion-onboarding "- "skill for the proven sequence: desktop_screenshot_window the Fusion hwnd and READ "- "its screenshots[] array - owned popups ride back as child shots; UIA-click 'Sign In', "- "'Continue with Google' (warm SSO), then isTrusted-click 'Open Product' + 'Always "- "allow'+'Open' on the protocol overlay), then poll until ready:true. NEVER enter the "- "password/2FA yourself; if the flow truly needs the user (password/2FA prompt), call "- "fusion_notify_owner {title, body} - it toasts their MAIN computer cross-AD. Last "- "resort only.")+ hint = ("Fusion is RUNNING but stuck at the first-run Autodesk SIGN-IN (main UI blocked, add-in "+ "cannot serve) - NOT ready. FULL PLAYBOOK: the `fusion-autodesk-signin` skill. Proven "+ "sequence + the pitfalls that cost hours on a fresh Hyper-V VM (John 2026-07-14): "+ "(1) BLANK white webview on first launch? fusion_stop then fusion_start - the sign-in "+ "renders 'Welcome to Fusion' + a 'Sign In' button only after a clean relaunch. "+ "(2) ASK the user FIRST (AskUserQuestion): do they have a warm Google/Apple/Microsoft "+ "session for the account LINKED to their Autodesk login? Offer to drive it. A truly "+ "fresh box has NO warm browser session, so 'Continue with Google' still hits a password "+ "wall. (3) Click Fusion 'Sign In' -> it opens the DEFAULT browser (Edge) to the OAuth. "+ "Edge windows DO exist even if PowerShell EnumWindows looks empty - that is CLIXML "+ "progress noise; set $ProgressPreference='SilentlyContinue', and just "+ "desktop_screenshot_window the Edge hwnd (PrintWindow works backgrounded). "+ "(4) In the browser: 'Continue with Google' -> type the user's EMAIL (not secret) -> if "+ "it asks for a PASSWORD/2FA, STOP and fusion_notify_owner {title, body} to toast their "+ "MAIN computer so THEY type it; NEVER enter password/2FA yourself. (5) Handoff back to "+ "Fusion is via the 'Autodesk Identity Manager' protocol - Edge shows a 'This site is "+ "trying to open...' overlay: tick 'Always allow' + click 'Open'. (6) SPEED MATTERS: the "+ "sign-in code EXPIRES in ~2 min; on a high-latency VM slow screenshot/click round-trips "+ "let it expire ('Sign-in request expired') and you loop. Minimize steps; if it expires, "+ "click Fusion 'Sign In' again for a FRESH code (the browser session stays warm). "+ "Then poll until ready:true.") elif ready and stale_info.get("addinStale"): hint = stale_info["_staleHint"] elif ready:
skills/fusion-autodesk-signin/SKILL.mdadded+100@@ -0,0 +1,100 @@+---+name: fusion-autodesk-signin+description: Drive the first-run Autodesk sign-in that blocks a freshly installed Fusion 360, from a headless/remote Adom desktop (pup/adom-desktop). Covers the whole browser OAuth handoff and every pitfall that cost hours on a fresh Hyper-V VM — blank sign-in webview, invisible Edge windows, warm Google/Apple/Microsoft SSO, the Autodesk Identity Manager protocol handoff, fast-expiring sign-in codes, and escalating the password/2FA step to the human without ever typing it. Trigger words — fusion signing in, welcome to fusion, sign in - autodesk, needsSignin, fusion stuck signing in, autodesk oauth, fusion login blocked, drive fusion sign in, fusion sign in on fresh box.+---++# Fusion 360 Autodesk sign-in — the drive-it playbook++A freshly installed Fusion launches to a first-run **Autodesk sign-in** that BLOCKS the main thread,+so the AdomBridge add-in never serves and `fusion_readiness` reports `ready:false, needsSignin:true`.+This skill gets you from that wall to `ready:true`. It is the durable record of a live drive on a+fresh Hyper-V VM (John, 2026-07-14); every step below is a thing that actually went wrong once.++**The one hard rule:** you NEVER type the user's Autodesk/Google password or 2FA. You may type their+**email address** (not a secret) and click every button. When a real secret is needed, you escalate+to the human with `fusion_notify_owner` and let THEM type it.++## Where you are (confirm first)+`fusion_readiness` → `installed:true, running:true, ready:false, needsSignin:true`. The Fusion window+title is "Signing in - Autodesk Fusion" or "Welcome to Fusion". Get its hwnd (Get-Process Fusion360+→ MainWindowHandle) and `desktop_screenshot_window {hwnd}` to see it. `desktop_screenshot_window` uses+PrintWindow, so it captures backgrounded/occluded windows and their owned child dialogs — use it, not+a screen grab.++## Step 0 — ASK the user before you drive (AskUserQuestion)+Sign-in needs a human decision, so ask up front — do NOT silently start clicking:+- **Do they have a warm browser session** (Google / Apple / Microsoft) for the account **linked to+ their Autodesk login?** If yes, "Continue with <provider>" clicks through with no password.+- **On a truly fresh box there is usually NO warm session** — the provider will demand a password.+ Confirm they're OK finishing the password/2FA step themselves (you'll toast them when it's time).+- Offer the choice: you drive the clicks and escalate only the secret step, OR they'd rather remote+ in and do it. Respect their answer.++## Step 1 — Blank white webview? RESTART Fusion.+The very first launch after install often renders the sign-in webview **blank white** (just a title+bar, no button). Do not fight it: `fusion_stop` then `fusion_start`. After a clean relaunch it renders+"Welcome to Fusion" + a real **Sign In** button. (This single restart unblocked the whole flow.)++## Step 2 — Click Fusion's "Sign In" (it opens the browser)+The sign-in webview exposes **0 accessibility controls** (it's Chromium), so click by IMAGE+coordinates: `desktop_screenshot_window {hwnd}` returns a `shotId`; then+`desktop_click {space:"image", shotId, x, y, hwnd}` — passing `hwnd` FOCUSES that window first, which+is essential (a click without it lands on whatever is foreground). Maximize the window first+(`desktop_set_window_state {hwnd, state:"maximize"}`) so the button is unambiguous. Fusion says "We+will take you to your web browser to sign in" and opens the **default browser** (Edge) to the OAuth.++## Step 3 — Find the Edge OAuth window (it IS there)+The Edge window can look invisible: `Get-Process msedge` shows background procs and your window+enumeration may print nothing. That "nothing" is almost always **PowerShell CLIXML progress noise+eating your output**, NOT a missing window. Fixes:+- Put `$ProgressPreference='SilentlyContinue'` at the TOP of the PS script.+- Enumerate `Chrome_WidgetWin` class windows (the OAuth tab is titled "Sign in - Autodesk"); a 2-tab+ window reads as "... and 1 more page".+- Then `desktop_screenshot_window {hwnd}` the Edge window — PrintWindow renders it regardless.+- The OAuth URL is handed to Edge over IPC, so it is NOT in any msedge command line — don't look there.++## Step 4 — Drive the OAuth (warm SSO path)+On the Autodesk sign-in page (`signin.autodesk.com/?flowId=...`): click **"Continue with Google"**+(or Apple/Microsoft, matching the linked account). On the provider page, click the email field and+`desktop_type` the user's **email** (e.g. [email protected] — an address, not a secret), click **Next**.+- If the provider recognizes a warm session → it proceeds with no password. +- If it shows **"Enter your password"** or a 2FA challenge → **STOP**. This is the human wall.++## Step 5 — Escalate the secret step (never type it)+`fusion_notify_owner {title:"Fusion sign-in needs you", body:"On <box> Fusion is at the Google+password step for <email>. Please enter your password/2FA in the open browser; I'll finish once+you're in."}`. It toasts the user's MAIN computer, fanning out cross-AD so it reaches them even though+this bridge runs on an unattended VM. Then WAIT and poll `fusion_readiness`; do not re-toast for a few+minutes. (The user may instead remote into the box and type it — either way you take over after.)++## Step 6 — The handoff back to Fusion (Autodesk Identity Manager)+After auth, the browser lands on `signin.autodesk.com/idmgr/callback#code=...` ("You're signed in").+The code is delivered to Fusion by the **Autodesk Identity Manager** protocol, so Edge pops a+**"This site is trying to open Autodesk Identity Manager"** overlay: tick **"Always allow"** and click+**"Open"**. (Always-allow means later sign-ins skip the prompt.) If instead you see a big+**"Open Product"** button, click it.++## Step 7 — SPEED: the code expires (~2 min)+The single biggest time-sink on a slow/remote box: each screenshot+click round-trip can take 20-30s,+and the Autodesk sign-in code **expires in about 2 minutes** ("Sign-in request expired"). If you dawdle+across the tabs, it dies and you loop forever. Mitigations:+- Do the fewest possible round-trips between clicking Fusion "Sign In" and the callback.+- Once the Autodesk/browser session is warm (after the first successful auth), a fresh Fusion+ "Sign In" click completes almost instantly — so if a code expires, just click Fusion "Sign In"+ AGAIN for a fresh code; you don't redo the whole login.+- If Edge tabs get stuck on a stale expired-callback URL, clear them (`taskkill /F /IM msedge.exe` —+ browser-specific and safe; Autodesk cookies persist so it stays warm) and re-click Fusion "Sign In".++## Step 8 — Confirm+Poll `fusion_readiness` until `ready:true` (needsSignin clears, the add-in responds). Now Fusion is+drivable: `fusion_open_lbr`, `fusion_build_library_3d`, exports, GPU/graphics checks, etc.++## Escalation ladder (in order)+1. Warm-SSO click-through (no secret) — try first.+2. Type the user's EMAIL only; let the provider's warm session finish it.+3. `fusion_notify_owner` for a genuine password/2FA/UAC/physical wall — the ONLY time a human is needed.+4. Ask the user to remote in — last resort if toasting doesn't land.++## Related+- `fusion-onboarding` — the install that precedes this (streams Fusion, then hands off here).+- `fusion-bridge-dev` — how the bridge detects installed/installing/ready (Fusion360.exe completeness).
skills/fusion-bridge-dev/SKILL.md+22@@ -296,3 +296,25 @@ skill gets read. In the bundled copies, rewrite image refs to **absolute wiki UR (`https://wiki.adom.inc/<org>/<slug>/files/<img>.png`) so screenshots still resolve. The repo-root copies stay for the wiki Files tab; **re-sync the bundled copies on every publish** (see PUBLISHING.md). Net: the installed skill carries the full content locally + always-current.++## Install-completeness detection (fusion_detect.py) — READ before touching install/readiness++Root-caused live on a fresh Hyper-V VM (John, 2026-07-14). The webdeploy layout is NOT obvious:++- `%LOCALAPPDATA%\Autodesk\webdeploy\production\<hash>\` — a COMPLETE install is ~615 files+ (**`Fusion360.exe`** ~880 KB + hundreds of DLLs + Qt/ Python/). Autodesk ALSO drops a tiny **8-file+ launcher STUB** dir next to it (icons + `FusionLauncher.exe` + a ~412-byte `FusionLauncher.exe.ini`).+ So a healthy install shows **two** hash dirs.+- **The completeness signal is `Fusion360.exe` (full size), NEVER the launcher or its `.ini`.** Two+ earlier "fixes" were WRONG: keying off `FusionLauncher.exe` existing → true mid-stream (it lands+ early) → premature `fusion_start` → *"Error Launching Streamed Application ... .ini missing or+ incomplete"*. Keying off `FusionLauncher.exe.ini` existing/size → also wrong: the **complete app dir+ has NO `.ini` at all**; only the throwaway stub has one, so a size gate would reject a healthy+ install and accept nothing.+- Helpers: `_app_dir_complete()` (Fusion360.exe ≥ 200 KB), `_any_app_complete()`, `_find_fusion_launcher()`+ (returns a launcher only when a complete app exists), `_incomplete_webdeploy_present()` (fresh-install+ "installing" signal = a launcher present but NO complete app anywhere; disk-state, because the+ streamer's tasklist presence is racy — flickered true only 3/25 polls during a real stream).+- `readiness`: gate `installed` on the app binary; `installing = _installer_running() OR+ _incomplete_webdeploy_present()`. **Poll to `installed:true`, not to `installing:false`** (the latter+ is racy). `_clean_incomplete_webdeploy()` no-ops when a complete app exists (never nuke the legit stub).
skills/fusion-onboarding/SKILL.md+5@@ -56,6 +56,11 @@ the sign-in page has **Create account** (free) — and Fusion offers a **free tr tier that's enough for viewing and a lot of work. **Don't auto-enter their password/2FA; do drive everything else.** +> **Use the dedicated `fusion-autodesk-signin` skill for the full drive** — it has the complete,+> battle-tested sequence and every pitfall (blank webview → restart Fusion, invisible-Edge / CLIXML+> noise, warm Google/Apple/MS SSO, the Identity Manager protocol overlay, fast-expiring codes, and+> `fusion_notify_owner` escalation). The summary below is the mechanism; that skill is the how-to.+ ### The mechanism (why naive clicking fails) Fusion's "Sign In" opens the **default browser** to an Autodesk OAuth page. When auth completes the browser lands on `signin.autodesk.com/idmgr/callback#code=…`, whose JS posts the code to Fusion's