Commit 51514c9
authored
feat(studio-bridge): persistent sessions and Linux/Wine support (#669)
* chore(ci): add TypeScript lint/test scripts and dedicated typescript workflow
* chore(format): apply prettier to pre-existing TypeScript files for CI baseline
* chore: update pnpm lockfile and devcontainer for studio-bridge work
* feat(cli-output-helpers): add Reporter framework primitives and ResultReporter for single-result output
* refactor(auth): consolidate auth into nevermore-cli-helpers with cookie/ and open-cloud/ split
* feat(studio-bridge): add v2 protocol, transport, and persistent bridge host
* feat(studio-bridge): rewrite plugin template with action handlers, screenshot capture, PNG encoder
* feat(studio-bridge): add CLI commands (sessions, exec, run, query, screenshot, logs, plugin) with declarative grouping
* feat(studio-bridge): add persistent plugin manager
* refactor(template-helpers): add Linux fallback for rojo --plugin and prefix verbose log lines
* feat(studio-bridge): add Linux/Wine support and Docker image for headless E2E
* refactor(studio-bridge): drop v1 protocol path
* fix(studio-bridge): address review feedback (security, bugs, cleanup)
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.
* fix(studio-bridge): correlate plugin responses via PendingRequestMap
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.
* feat(studio-bridge): validate HostEnvelope action via zod schemas
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.
* test(studio-bridge): cover waitForSessionsToSettleAsync settle path
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.
* chore(studio-bridge): drop unused CommandRegistry.discoverAsync
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.
* refactor(studio-bridge,cli-output-helpers): simplify single-result CLI 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.
* chore(studio-bridge): emit apt manifest as Docker build artifact
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.
* refactor(studio-bridge): use EncodingService:Base64Encode for screenshot
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.
* chore(vscode): enable file nesting for init.lua and sibling files
* docs(studio-bridge): trim programmatic API and protocol sections from README
* chore(ci): skip studio-linux-ci e2e when ROBLOSECURITY cookie is stale1 parent 754fea6 commit 51514c9
258 files changed
Lines changed: 34306 additions & 2357 deletions
File tree
- .devcontainer
- .github/workflows
- .vscode
- docs/testing
- tools
- cli-output-helpers/src
- reporting
- github
- state
- nevermore-cli-helpers
- src
- auth
- cookie
- open-cloud
- nevermore-cli/src
- commands
- batch-command
- deploy-command
- init-command
- test-command
- tools-command
- utils
- batch
- build
- job-context
- linting/parsers
- sourcemap
- testing
- parsers
- reporting
- runner
- nevermore-template-helpers/src
- build
- scaffolding
- studio-bridge
- docker
- src
- bridge
- internal
- __tests__
- cli
- adapters
- args
- commands
- terminal
- commands
- console
- exec
- logs
- explorer/query
- framework
- linux
- inject-credentials
- setup
- status
- plugin
- install
- uninstall
- process
- close
- info
- launch
- list
- run
- serve
- viewport/screenshot
- docker
- linux
- plugin
- process
- server
- test
- e2e
- helpers
- templates
- studio-bridge-plugin-test/test
- studio-bridge-plugin/src
- Vendor
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
0 commit comments