Skip to content

Releases: denysosadchyi/figmosha2

Figmosha 2.3.0

Choose a tag to compare

@denysosadchyi denysosadchyi released this 06 Oct 14:13

Changed

  • The plugin bar names its file. Once connected it shows the Figma file's
    name instead of "connected", cut with an ellipsis if long.
  • The plugin bar counts open files. With two or more plugin windows
    connected, the check icon sits in a white pill with this window's position,
    e.g. 1/2 ✓. A single file shows just the icon. The bridge pushes a peers
    message ({index, total}) to every plugin when one connects or leaves; older
    plugin builds ignore it.
  • The plugin bar shows what the file is doing. The check spins while an
    exec runs on that file (held at least 400ms so quick ones still show), and a
    failed exec turns the bar red with a ! for 2s.
  • New plugin bar layout. A Figma file icon and the name on the left, the
    status icon or 1/2 ✓ pill on the right.

Fixed

  • A caller hanging up mid-run let the next caller into the file beside its
    still-running script.
    The hang-up path skipped the interlock when the
    reply future was done — but cancelling wait_for cancels that future, so
    it always was. Rare on Python 3.12, constant on 3.14: found by the final
    Linux run of the scenario fuzzer (34–39 of 300 scenarios failed).
  • The timeout interlock was per connection, not per document, so a caller
    routed through another tab of the same file got past it. It now covers the
    document, and /clear through any tab clears it.
  • A hang-up while the script was being sent could release the lock with
    the script already on its way; the send is now always completed and the
    run interlocked.
  • h.ck() could not stop a tight loop. It waited for an abort message
    from the bridge, but a loop that only awaits Figma APIs never lets the plugin
    read its messages. The exec's timeout now travels with the script and
    h.ck() checks the plugin's own clock: verified live, such a loop stops at
    its 1 s timeout instead of running on and holding the file.
  • A finished script could leave its file interlocked until /clear. When
    its reply landed right at the timeout — which h.ck() now makes the normal
    case — the bridge dropped the reply and then marked the run abandoned.
    Both orderings of that race are handled and tested.
  • A timed-out script's print() lifted the interlock while it was still
    running, letting the next caller write alongside it. Only its final result
    or error lifts it now.
  • A file could stay interlocked forever after a caller hung up. If a
    caller disconnected in the same moment its script's reply arrived, the
    bridge marked the finished script as still running and blocked the file
    until someone POSTed /clear. Found by the scenario fuzzer (seed 24).
  • start-bridge.sh failed on a stock Mac. It required tmux (not shipped
    with macOS) and ./venv/bin/python, and gave up after 1 s. It now falls back
    to a plain background process without tmux, to python3 without a venv,
    waits up to 10 s, checks aiohttp up front, and has --stop. The docs say
    python3 / ./venv/bin/python on macOS, which has no python command.
  • Every CLI call took ~2 s on Windows. The CLI talked to localhost, which
    Windows resolves to ::1 first; the bridge only listened on 127.0.0.1, so
    each request waited for the IPv6 attempt to fail. The bridge now also listens
    on ::1 (any client saying localhost is fast) and the CLI defaults to
    127.0.0.1. A CLI call went from 2157 ms to ~100 ms, an HTTP call to ~2 ms.
  • Big results were shipped twice. The plugin sent every result as both
    text and value, and serialized objects three times on the way. It now
    serializes once and sends the value only; the bridge derives the result
    text (compact JSON past 1 MB, where indenting alone cost ~120 ms). A 50k-item
    result: 641 → 255 ms; a 5 MB string: 495 → 250 ms. The HTTP API is unchanged.
  • print() sent one message per line through two hops; 20k lines took
    ~0.5 s. Lines are batched now (every 200 lines or 100 ms): 492 → 37 ms.
  • Two new "Untitled" files were treated as one document, so every write
    went to the newest and the other file was unreachable. The plugin identified a
    document by its page ids, and every new file starts with page 0:1. It now
    stores a random id in the file's plugin data once and reports that.
  • One file open in two tabs had two locks, one per connection, so two
    writers could get into the same document at once. The lock is now per
    document.
  • A file could stay locked until the bridge restarted. When a caller hung
    up while waiting in the queue, the bridge stopped waiting for the lock, but
    the lock request itself lived on, was granted later and never released —
    found by the new chaos test.
  • Malformed /exec and /clear bodies crashed the handler with a 500: a
    JSON array instead of an object, a non-string target, a timeout that isn't
    a number. They are now a 400 naming the field; timeouts must be in
    (0, 3600] seconds.
  • A caller that gave up while queued still ran later. The CLI's socket
    timeout ignored time spent waiting for a busy file; when it hung up, the
    bridge still ran the script once the file was free — a write nobody was
    waiting for, likely retried by the caller. The CLI now allows for the queue,
    and the bridge drops callers that left.
  • Descenders in the plugin bar's text were clipped at the bottom.
  • start-bridge.ps1 did not run in Windows PowerShell 5.1, the one Windows
    ships with. The file had em dashes and arrows but no BOM, so 5.1 read it as
    ANSI and failed with "The string is missing the terminator". It is now pure
    ASCII.
  • The bridge did not start from a folder with a space in its path
    (can't open file 'F:\\00'). PS 5.1's Start-Process doesn't quote
    -ArgumentList array elements; the arguments are now one pre-quoted string.
  • start-bridge.ps1 found the running bridge by grepping netstat for
    LISTENING, which is translated on non-English Windows, so -Stop and the
    already-running check silently did nothing there. It now asks
    Get-NetTCPConnection.
  • start-bridge.ps1 treats the Microsoft Store python.exe stub as "no
    Python" instead of launching it.
  • The bridge forces UTF-8 on its output, so a Cyrillic, CJK or emoji file name
    can't raise UnicodeEncodeError when the log is redirected on Windows.
  • On Windows every closed Figma tab left a ConnectionResetError [WinError 10054] traceback in bridge.err.log. It was asyncio noise after the
    disconnect was already handled, and the bridge now drops it.
  • CLAUDE.md told Claude to start the bridge with bash and tmux and described
    one maintainer's Mac. It now covers macOS / Linux / WSL and native Windows,
    with machine specifics left to CLAUDE.local.md.
  • h.ck() never fired. The bridge sent abort for a timed-out run, but
    the plugin UI dropped every message type except exec and ping, so the
    sandbox never heard about it. The UI now forwards abort.
  • figmosha doctor with several files connected and no -T told you to
    re-run the plugin; it now says to pick a file with -T, and to
    figmosha clear when the file is interlocked by a timed-out script.
  • --raw and status printed non-Latin text as П… escapes.
  • The 1/2 pill counted connections that had not identified themselves yet,
    so it could flash 1/3 while a tab reconnected.
  • start-bridge.ps1 exited 0 when the bridge failed to start and showed the
    stdout log, while the reason is on stderr. It now prints the tail of
    bridge.err.log, suggests installing the requirements when aiohttp is
    missing, and exits 1.
  • README's Windows venv line used &&, which Windows PowerShell 5.1 rejects;
    helper and subcommand counts were out of date.
  • The bridge test fixture reset globals that no longer exist instead of the
    multi-file registries. New tests cover the timeout interlock (504 → 409 →
    lifts when the orphan replies, abort sent) and the 1/2 counter.
  • tests/helpers.test.js crashed before running a single check: code.js
    now reads figma.root.name on load and the test's Figma stub had no root.

Added

  • Fuzz and hostile-input tests. tests/test_fuzz.py runs random
    scenarios (tabs opening and closing, agents writing, giving up, hanging up,
    failing) and checks after each that no document ran two scripts at once and
    nothing leaked; FIGMOSHA_FUZZ_SEEDS=300 for a long run. Plus junk from a
    misbehaving plugin, duplicate and late results, forged Host / Origin
    headers, and 3000 execs that must leave no residue. The state reset moved
    to tests/conftest.py, so it covers every test file.
  • Stress and chaos tests. tests/test_stress.py runs 50 concurrent
    writers through a read-modify-write race (no lost update allowed), 4 files x
    15 agents, a chaos mix of impatient, failing and hung-up callers, a plugin
    dying mid-queue, a reconnect storm over one document, junk input and 5 MB
    payloads. tests/live_stress.py runs the same race against your real files.
  • A queue per file for several agents. /exec waits its turn on the
    document's lock, and now says so: targets / /status show who is running on
    each file and who is waiting, callers name themselves with --agent /
    FIGMOSHA_AGENT / "agent", replies carry queued_ms, and
    --queue-timeout / "queue_timeout" caps the wait with a 503 file busy
    that guarantees nothing ran.
  • -T takes a document id, shown by targets and stored in the file so it
    survives reconnects, so two different files with the same name (two fresh
    "Untitled" files) can each be targeted. A connection id works too.
  • Releases, and an Update button when one is out. Figmosha now has a
    VERSION (in bridge.py, shown by /status and doctor) and is released as
    vX.Y.Z tags. Every 6 hours the bridge reads the release tags on GitHub; when
    a newer one exists the plugin bar turns blue with "New version X.Y.Z" and an
    Update b...
Read more

Figmosha 2.2.0

Choose a tag to compare

@denysosadchyi denysosadchyi released this 06 Oct 14:13

Added

  • Multiple Figma files at once. The bridge keeps one connection per open
    file running the plugin instead of one globally. The plugin reports its
    identity and /exec routes by a target (CLI -T / --target), resolved as
    exact name → fileKey → unambiguous substring. GET /targets and
    figmosha targets list what's connected. One file connected with no target
    behaves exactly as before, so existing scripts don't change.
  • Per-file exec lock. A file's plugin sandbox is a single-threaded async
    handler over one document and one undo stack, so two concurrent scripts
    interleave at every await and invalidate each other's findAll snapshots.
    /exec now takes a per-connection lock; callers on different files never wait
    on each other. Read-only scripts can pass parallel: true (--parallel) to
    bypass it and fan out.
  • Abandoned-run interlock. A script that outlives its timeout can't be
    killed, and the next caller used to mutate the file underneath it. The 504
    now carries a warning and the request id, further execs on that file return
    409 until the orphan replies, and POST /clear (figmosha clear -T <file>)
    lifts it. force: true pushes past.
  • Cooperative cancellation. h.ck() throws once the bridge has abandoned
    the run, so a chunked sweep stops instead of mutating under the next caller.
    h.aborted() is the non-throwing form. Older plugin builds ignore the signal.
  • The same document open in several tabs or windows. Each view registers its
    own connection and -T routes to the newest live one; /status flags the
    extra views with sameDocAs.

Fixed

  • One file open in two tabs looped forever. Any second hello for a known
    file name evicted the incumbent, so each side kicked the other out and the
    loser reconnected 2s later and kicked back. A live connection is now never
    evicted: hello replaces an older same-file registration only when its socket
    is already closed or it fails a 1s liveness ping.
  • Two different files sharing a name are still refused, and now for the
    right reason. The plugin sends a docSig — a hash of its page node ids, since
    figma.fileKey is null for a local dev plugin and figma.root.id is "0:0"
    everywhere — so the bridge distinguishes two views of one document from two
    documents, instead of treating both as ambiguous.
  • Connections registered unnamed, so -T could not find them. The plugin
    UI's identity handler wrote to a #server element that no longer exists; it
    threw before sendHello() ran.
  • The bridge log stayed empty. start-bridge.sh piped bridge.py into
    tee without -u, so Python block-buffered stdout and plugin
    connect/disconnect events were invisible exactly when they were needed.
  • In-flight requests routed to a dead connection fail fast instead of hanging,
    and a dead connection drops its abandoned interlock with it.

Removed

  • The single-slot 1008 rejection and its Slot busy UI state, with the 15s
    backoff. The bridge no longer turns away a second connection for a file, and a
    tab that becomes visible again reconnects immediately.

v2.1.0 — origin guard, selection awareness, self-diagnosis

Choose a tag to compare

@denysosadchyi denysosadchyi released this 18 Aug 12:47

The bridge now refuses requests that come from a web page — the one change here worth acting on today.

It executes arbitrary JS inside your open Figma file, so every tab in your browser was part of the threat model: a page could post to localhost:8787 as a text/plain "simple request", dodge the CORS preflight, and silently edit or delete your work. Requests carrying an Origin header are now rejected, and Host is pinned to the loopback names actually served, which closes DNS rebinding. curl and the CLI are unaffected — they never send Origin.

Also in this release

Knows what you selected. h.sel() and figmosha sel close the gap between "this frame here", which you point at with a mouse, and a node id. page and sel now work anywhere an id is taken — figmosha tree sel --layout.

Diagnoses itself. figmosha doctor walks bridge → plugin → round trip → which file is open, and names the fix at whichever link is broken.

Colours and frames without ceremony. h.hex("#1a2b3c") retires the hand-rolled /255. h.frame() applies auto-layout in the order Figma requires — an order that fails silently when you get it wrong, which is why it is now a helper rather than a third paragraph of documentation.

Runs on Windows properly. start-bridge.ps1 launches detached, with -Restart and -Stop. And the CLI no longer crashes on any layer name outside cp1252, which on a default Windows console meant most non-English names.

Reconnects when it should. A half-open socket used to lock a returning plugin out for ~20s. The bridge now pings the incumbent: silence for a second and the newcomer takes over, while a plugin that is genuinely alive keeps the slot — so two Figma windows no longer evict each other forever.

Tested without Figma. A fake plugin drives the real WebSocket, covering the guard, exec round trip, timeouts, disconnect cleanup and slot handover; the pure helpers run against a stubbed Figma.

Upgrading

Pull, then re-run the plugin in Figma — a running plugin keeps the code it started with, so the new helpers will not exist until you do. manifest.json is unchanged, so a re-import is not needed.

Full detail in CHANGELOG.md.