feat(studio-bridge): persistent sessions and Linux/Wine support#669
Open
feat(studio-bridge): persistent sessions and Linux/Wine support#669
Conversation
74b3063 to
1a5b8da
Compare
a45bded to
cf9cc1a
Compare
…tReporter for single-result output
…ie/ and open-cloud/ split
8b17a6a to
57d5f71
Compare
57d5f71 to
2643673
Compare
…reenshot capture, PNG encoder
…reenshot, logs, plugin) with declarative grouping
…prefix verbose log lines
2643673 to
0ec8953
Compare
Security - Use execFileSync with args arrays in FileResultReporter and linux-credential-writer instead of execSync + JSON.stringify quoting, which is not equivalent to shell quoting. - Pass ROBLOSECURITY through the docker process env (via -e ROBLOSECURITY with no value) instead of baking it into argv, where it would be visible to ps on the host. - Tighten ~/.nevermore/credentials.json to mode 0600 (and parent dir 0700). - Use err.message instead of template-stringifying raw err in validateApiKeyAsync. - Restore the terminal cursor in watch-renderer on render-callback errors and SIGINT/SIGTERM, instead of leaving the cursor hidden. Bugs - process run no longer drops the global --verbose flag; it reads the effective value via OutputHelper.isVerbose(). - exec/run resolve script file paths to absolute via path.resolve and switch from sync fs.readFileSync to async fs.readFile. - explorer query honors --depth even when --descendants is not passed. - process close (still a stub) returns success: false so the adapter exits non-zero. - StudioBridgePlugin sends state "Edit" on register (was "ready", which is not a valid StudioState) and advertises the dynamically-dispatched capabilities (execute, queryState, captureScreenshot, queryDataModel, queryLogs). - Persistent plugin install is now atomic: rojo builds into the temp build dir and the result is copyFile + rename'd into the plugins folder so Studio's polling watcher never observes a partial .rbxm. - Plugin uninstall uses unlink-with-ENOENT-handling instead of a pre-check, removing a TOCTOU window. - CompositeResultReporter teardown is failure-isolated via Promise.allSettled so a throwing reporter no longer skips siblings. - DiscoveryStateMachine drops the dead _currentPort rotation logic (the field was set on construction but never advanced, so the rotation was a no-op anyway given parallel scanPortsAsync). - png.luau asserts that dynamic-Huffman code 16 (repeat-previous) is not the first symbol; previously a malformed input would silently no-op into table.insert(nil) and spin the until-loop forever. Cleanup - Remove stale welcome / protocolVersion: 2 references left over from the v1 protocol drop in plugin-test templates and hand-off.test.ts; drop the matching "(v2)" suffix in describe and rename web-socket-protocol-v2.test.ts to web-socket-protocol.test.ts (the prior basic file becomes web-socket-protocol-basic.test.ts). - Rename linux-display-manager sleep to sleepAsync per repo convention. - Extract resolveScriptContentAsync (shared by exec and run) and colorizeState (shared by process list and process info). - Combine duplicate getPersistentPluginPath imports in uninstall.ts. CI - Guard the new TS jobs' .npmrc _authToken= writes on $NPM_TOKEN / $GITHUB_TOKEN being non-empty, so fork PRs don't append empty _authToken= lines. - Gate the studio-linux-ci "Diagnose Wine networking" step behind failure() instead of always() so it doesn't run on every successful PR.
sendToPluginAsync attached a fresh ws.on('message', ...) listener per
call and matched the first non-heartbeat reply (or any error) when no
requestId was supplied. With ≥2 requests in flight this could (a) cross
responses between callers, (b) let an unrelated error reply satisfy a
real request, and (c) leak listeners until the EventEmitter MaxListeners
warning fired.
Replace the per-call pattern with a per-connection PluginConnectionState
that owns a PendingRequestMap. A single dispatcher is installed in
_registerPlugin; it routes plugin responses by requestId. Every outgoing
request must carry a requestId — all real callers (BridgeSession,
BridgeClient) already do; ensureRequestId is a defensive fallback.
Pending requests are cancelled on plugin disconnect, replacement, host
stopAsync, and shutdownAsync (graceful + force-close paths).
New unit tests in bridge-host.test.ts cover:
- concurrent in-flight requests resolve to the correct caller even when
the plugin replies in reverse order
- an error reply with an unrelated requestId no longer satisfies a
pending request (it now times out, as it should)
- pending requests reject when the plugin disconnects
- the dispatcher is installed once and does not accumulate listeners
across many requests
All 669 TS tests + 158 plugin Lune tests pass.
Replace the unchecked `obj.action as ServerMessage` cast in decodeHostMessage with a zod discriminated union covering all nine ServerMessage variants. A buggy or hostile /client connection can no longer forward malformed actions to the plugin. Adds nine targeted tests for missing/unknown action types, wrong field types, and unknown error codes.
The settle path that gates cold-start commands had no direct test coverage — all bridge-connection tests used keepAlive: true, which short-circuits the settle in the constructor. Adds five targeted tests using the existing options parameter to inject small timeouts: no-plugin firstSessionTimeout exit, single-plugin settleMs quiet wait, settle timer reset on second plugin, maxMs cap on continuous streams, and immediate return for client role. Establishes a baseline before any settle-constant tuning.
cli.ts has always registered commands explicitly, and discoverAsync (plus DiscoverOptions and the _tryImportAsync helper) was never wired into production. Removes the convention-based loader, its barrel re-export, and its seven unit tests. Net −245 / +4 lines.
Quenty
commented
May 1, 2026
…I output
The single-result output path had grown a 3-mode dispatch system (text |
table | json) plus a studio-bridge-specific base64 extension, all routed
through a thin format-output.ts wrapper that re-exported upstream types.
On inspection: every command's text and table formatters were literally
identical fns — text/table was a phantom distinction nobody used; base64
was a stdout pipeline strictly inferior to --output file.png.
Collapses the per-command `formatResult: { text, table, json }` dict to
two optional callbacks:
cli.format(result) — human terminal output (also for --format=text)
cli.json(result) — overrides default formatJson, e.g. drop binary
Hoists reporter selection (Stdout/File/Watch dispatch) into a new
`buildResultReporter` factory in cli-output-helpers — the package that
owns the reporter classes — and inlines its construction in the adapter
handler, which is now mode-blind. Drops --format=base64 (binary file
output via -o is unaffected; binaryField + extractBinaryBuffer remain),
deletes the format-output.ts wrapper and its tests, and removes the
unused upstream output-mode module (OutputMode/resolveOutputMode no
longer have any callers). Updates tools/CLAUDE.md to match.
Net: −240 lines across the four formerly-separate refactor steps.
Apt deps (winehq-stable, nodejs, gh, ...) in the studio-linux Docker image aren't pinned to specific versions. That's a deliberate trade-off versus the brittleness of "this exact apt version is no longer in the repo," but it leaves drift across rebuilds invisible. Dumps a sorted TSV of all installed packages and versions to /image-manifest-apt.tsv at build time, then has the studio-linux-ci workflow extract and upload it as a 90-day artifact. When a future rebuild mysteriously breaks Studio, diffing the manifest against the last known-good build shows what actually changed.
Replaces the hand-rolled buffer-based base64 encoder (~60 lines, including a 64-entry ASCII LUT and a localized hot loop) with a call to Roblox's built-in EncodingService:Base64Encode. The native implementation is faster than any Luau loop and removes a chunk of vendored encoding logic we'd otherwise have to maintain. Resolves a code review note from Quenty.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds persistent sessions to studio-bridge: the plugin stays open in Studio with a live WebSocket to the bridge instead of the previous one-shot launch-execute-exit flow, enabling multiple concurrent Studio instances, host failover, and split-server mode for devcontainers. Ships new commands (sessions, exec, run, query, screenshot, logs, plugin install/uninstall) plus Linux/Wine and Docker support for headless E2E. Also consolidates auth into nevermore-cli-helpers, adds the cli-output-helpers package for structured output, and wires path-filtered TypeScript lint/test jobs into the linting and tests workflows.