open-in-editor-bridge 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 894d087228ad0690acd8e32f1f2284240055e9b934965fd181556adc7aba116b
4
- data.tar.gz: f5fd6f10dc3110132077b781224d9c1e67eb46d9c6c83d85f0f083cca89b1391
3
+ metadata.gz: 7d25bb9c59762f68ee9b164865d324147c717b4b1d5c13bb3163dc57b2c65763
4
+ data.tar.gz: 85c973a0426c36ece6f5daec5b767380bc4621d0c39e28ccb01e572279c58db0
5
5
  SHA512:
6
- metadata.gz: 00a3e25e014adfdff6d33fa389e4eb11d75ba364fb89536d83b83128154854322dc367938544d34ccad2ffab7a5487ad3f7a3d4f135c35ec5694ebb15f2c315b
7
- data.tar.gz: 1b6a96b2710f51240f152028b49d0b9ac573dc844d7bb6c8177586faaf3c4a59cd18cf6c09aba920fafd688e22350be1403d6ddf102a606a6cb81af2bdb47260
6
+ metadata.gz: d35441d1493bfda18b675cacf05e94baa1c2872543d4464492bc8271b4032213b8aa8826e9b567334aa19cc6cdf7f7c1fbfed1bccda78e60a3bcfb66b09d9d1c
7
+ data.tar.gz: 7d8a2e2aaf8641d415ea9f8cd6a4d78f67735506bcb38a3095d9dca4b1123635661995e2358767d496be9ef13c3d2d81412ca777cb1d815f393c1854c633cd83
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-10-09
4
+
5
+ ### Added
6
+
7
+ - `OpenInEditorBridge.compose` for library-owned Docker/Vite integration: generated service overrides mount a bundled Vite plugin and checkout identity, without session environment/query plumbing in applications.
8
+ - Development-only Vite plugin that routes editor requests to the mounted checkout, replacing duplicate caller selectors while preserving encoded file paths and line/column.
9
+ - Minimal adoption instructions and an ELCC migration path for removing its bespoke session transport.
10
+
11
+ ### Security and Lifecycle
12
+
13
+ - Docker startup still requires explicit network-exposure opt-in. Only non-secret identity/target data and plugin code are mounted read-only; lifecycle credentials stay on the host.
14
+ - Missing/malformed Vite identity fails startup safely; only the editor endpoint is proxied.
15
+ - Foreground cleanup and idempotent detached registration reuse the existing lease protocol. Failed startup releases only a new registration, and failed shutdown retains the active registration.
16
+ - Unrelated Compose commands and production Vite builds do not require editor setup.
17
+
3
18
  ## 0.2.0 — 2026-10-09
4
19
 
5
20
  ### Added
data/README.md CHANGED
@@ -5,95 +5,104 @@ A Ruby gem that runs a shared local HTTP bridge for opening container files in t
5
5
  ## Install
6
6
 
7
7
  ```ruby
8
- gem "open-in-editor-bridge", "~> 0.2"
8
+ gem "open-in-editor-bridge", "~> 0.3"
9
9
  ```
10
10
 
11
11
  ```sh
12
12
  gem install open-in-editor-bridge
13
13
  ```
14
14
 
15
- ## Foreground Ruby API
15
+ ## Docker and Vite Integration
16
16
 
17
- Wrap commands whose lifetime matches the application, such as foreground Compose:
17
+ Use the host-side Compose adapter and the gem's mounted Vite plugin. The library owns checkout identity transport, the host-gateway entry, and the editor proxy; **do not export a session variable or write a request rewrite**.
18
+
19
+ Replace the development Compose startup/shutdown call:
18
20
 
19
21
  ```ruby
20
22
  require "open_in_editor_bridge"
21
23
 
22
- OpenInEditorBridge.with_running do |session_id|
23
- ENV["OPEN_IN_EDITOR_SESSION_ID"] = session_id
24
- system("docker", "compose", "up", exception: true)
25
- end
24
+ # Explicit network-exposure opt-in; use only on a trusted development network.
25
+ ENV["OPEN_IN_EDITOR_BRIDGE_BIND_ADDRESS"] = "0.0.0.0"
26
+ ENV["OPEN_IN_EDITOR_COMMAND"] = "code"
27
+
28
+ OpenInEditorBridge.compose("up", service: "web")
29
+ # Detached startup: OpenInEditorBridge.compose("up", "-d", service: "web")
30
+ # Shutdown: OpenInEditorBridge.compose("down", service: "web")
26
31
  ```
27
32
 
28
- The block returns its normal result. Its checkout lease is released in `ensure`, including when the block raises; cleanup does not replace the original exception. Overlapping wrappers have independent leases, even in the same checkout. Finishing one wrapper never releases another wrapper or an existing detached registration.
33
+ Add the plugin to your existing Vite configuration for Docker development:
29
34
 
30
- `OpenInEditorBridge.session_id` returns the stable ID for the configured checkout without starting the bridge. `OpenInEditorBridge.new(env: ..., project_root: ...)` supports explicit configuration for library consumers.
35
+ ```js
36
+ import { defineConfig } from "vite";
31
37
 
32
- ## Detached Compose
38
+ export default defineConfig(async ({ command, mode }) => {
39
+ const plugins = [];
40
+ if (command === "serve" && mode === "development") {
41
+ const integrationPath = "/open-in-editor-bridge/vite.mjs";
42
+ const { default: openInEditorBridge } = await import(integrationPath);
43
+ plugins.push(openInEditorBridge());
44
+ }
33
45
 
34
- `up -d` returns before the application stops. Use persistent registration, **not** `with_running` around detached startup:
46
+ return { plugins, server: { host: "0.0.0.0" } };
47
+ });
48
+ ```
35
49
 
36
- ```ruby
37
- require "open_in_editor_bridge"
50
+ Keep your application's existing plugins and other Vite settings. The variable-based dynamic import is intentional: production builds and test-mode configuration can load without Docker's development mounts. This path supports Docker-hosted Vite development; native-host Vite and custom development modes need their own explicit opt-in condition. The plugin requires a valid mounted manifest when enabled and fails startup if it is missing or malformed. Vite 6 is exercised by release QA.
38
51
 
39
- OpenInEditorBridge.call("--ensure-running")
40
- ENV["OPEN_IN_EDITOR_SESSION_ID"] = OpenInEditorBridge.session_id
41
- system("docker", "compose", "up", "-d", exception: true)
42
- ```
52
+ ### Application Configuration That Remains
43
53
 
44
- Release that checkout's persistent registration when taking it down:
54
+ - Select the Vite service (`service: "web"` by default); retain its image, command, source mounts, ports, and normal environment.
55
+ - Select Compose files/project options when necessary, for example `compose_options: ["-f", "docker-compose.development.yaml", "-p", "my-checkout"]`. Pass subcommand options after `"up"` / `"down"`, not in `compose_options`.
56
+ - Select checkout/container/host roots and the editor through the existing bridge configuration below. `project_root:` defaults to the current checkout; `env:` supplies bridge configuration and child-process environment overrides.
57
+ - Explicitly select a Docker-reachable bind address. The adapter rejects loopback startup and never changes the default binding itself.
45
58
 
46
- ```ruby
47
- OpenInEditorBridge.with_running(ensure_running: false) do
48
- system("docker", "compose", "down", exception: true)
49
- end
50
- ```
59
+ When no explicit `-f` is supplied, the adapter respects `COMPOSE_FILE` / `COMPOSE_PATH_SEPARATOR`, or discovers a standard Compose file and its default override from the project directory upward. `--project-directory` is supported. Compose runs in `project_root:` and inherits normal process input/output. Failures raise; successful commands return `true`.
51
60
 
52
- `ensure_running: false` does not register a new lease and releases only the detached registration after the block. You can alternatively call `OpenInEditorBridge.call("--shutdown")` directly. Repeated `--ensure-running` calls are idempotent for a checkout; one `--shutdown` releases its detached registration. Foreground leases and registrations for other checkouts remain active. The listener exits when the last lease is released.
61
+ Only `up` adds editor setup. Foreground `up` owns an independent lease and releases it even on failure. `up -d` / `up --detach` use the existing idempotent detached registration. Failed detached startup releases a newly acquired registration but preserves one that was already active. Successful `down` releases that checkout's detached registration; failed `down` keeps it. Other commands (`config`, `build`, `exec`, `run`, `logs`, and so on) execute ordinary Compose without editor setup or requiring an editor. `stop` does not release detached registration; use `down` or `--shutdown` when done. If a new container needs the editor mounts, create it through `up`.
53
62
 
54
- ## CLI
63
+ The generated override mounts just two read-only files at `/open-in-editor-bridge`: the packaged `vite.mjs` and a checkout-specific `session.json` containing identity and target URL. It never mounts broker state, the whole private runtime directory, or lifecycle control credentials. Temporary Compose overrides are removed after each invocation; non-secret manifests remain in the private host runtime directory for existing detached containers.
55
64
 
56
- Run lifecycle commands from the host checkout (or set `OPEN_IN_EDITOR_PROJECT_ROOT` explicitly):
65
+ All checkouts share the standard host port `3333`, bind address, and private runtime directory. Each may use the same container directory `/usr/src/web`: the plugin selects its own host checkout from its mounted identity. It replaces all caller-supplied `session` selectors, including duplicates and encoded selector names, without rewriting other query bytes. Only `/__open-in-editor` is proxied; health and lifecycle endpoints are not. Existing exact editor proxy entries are replaced, and editor routing takes precedence over broader application proxy entries.
57
66
 
58
- ```sh
59
- export OPEN_IN_EDITOR_COMMAND='code'
60
- # Explicit opt-in for Docker bridge-network access; read the security section.
61
- export OPEN_IN_EDITOR_BRIDGE_BIND_ADDRESS=0.0.0.0
62
- export OPEN_IN_EDITOR_SESSION_ID="$(open-in-editor-bridge --session-id)"
63
- open-in-editor-bridge --ensure-running
64
- docker compose up -d
67
+ ### Adopting in ELCC
65
68
 
66
- # Later, with the same port, bind address, runtime directory, and project root:
67
- docker compose down
68
- open-in-editor-bridge --shutdown
69
- ```
69
+ After installing 0.3.0, ELCC can replace the editor lifecycle/Compose `up` and `down` path in `bin/dev` with `OpenInEditorBridge.compose`, passing its existing development/gateway `-f` options, project name, host user/group variables, and gateway hostname. Keep gateway lifecycle ownership and pipe-input handling for unrelated commands unchanged.
70
70
 
71
- `--serve` runs the broker in the foreground and registers the current checkout. It requires a free port. Prefer the managed lifecycle above for sharing: manually interrupting a foreground broker stops all its sessions.
71
+ Remove the `OPEN_IN_EDITOR_SESSION_ID` export from `dynamic_environment_variables`, its web-service pass-through in `docker-compose.development.yaml`, and the handwritten `/__open-in-editor` target/rewrite in `web/vite.config.js`. Add the mounted plugin to the existing Vue/Vuetify/gateway plugin list under the development-only condition above. The adapter supplies the host-gateway entry, so an editor-only `extra_hosts` entry is no longer necessary. Keep the explicit trusted-network bind opt-in and existing root/editor settings. No consumer files are changed automatically.
72
72
 
73
- ## Docker and Checkout Routing
73
+ ## Low-Level Ruby API
74
74
 
75
- All worktrees using one listener must agree on port, bind address, and runtime directory. Each checkout registers its own host-root mapping and editor command. Simultaneous startup is serialized by a shared per-port lock, not by checkout-local pidfiles.
75
+ For non-Compose applications or custom integrations:
76
76
 
77
- On Linux, make the host accessible from Compose and pass the checkout ID into the web service:
77
+ ```ruby
78
+ require "open_in_editor_bridge"
78
79
 
79
- ```yaml
80
- services:
81
- web:
82
- extra_hosts:
83
- - "host.docker.internal:host-gateway"
84
- environment:
85
- OPEN_IN_EDITOR_SESSION_ID: "${OPEN_IN_EDITOR_SESSION_ID:?Set the host checkout session ID}"
80
+ OpenInEditorBridge.with_running do |session_id|
81
+ # A custom integration may use session_id to select this checkout.
82
+ system("./bin/app", exception: true)
83
+ end
86
84
  ```
87
85
 
88
- The container's dev-server proxy must forward editor requests to `http://host.docker.internal:3333` and add its `OPEN_IN_EDITOR_SESSION_ID` as the `session` query parameter. For example:
86
+ The block returns its normal result. Its checkout lease is released in `ensure`, including when the block raises; cleanup does not replace the original exception. Overlapping wrappers have independent leases, even in the same checkout. Finishing one wrapper never releases another wrapper or an existing detached registration.
87
+
88
+ `OpenInEditorBridge.session_id` returns the stable ID without starting the bridge. `OpenInEditorBridge.new(env: ..., project_root: ...)` supports explicit configuration. `call("--ensure-running")` registers a detached checkout; `call("--shutdown")` releases it. Repeated registration is idempotent. `with_running(ensure_running: false)` runs its block without acquiring a lease and releases detached registration afterward. Do not wrap detached startup in a foreground lease.
89
+
90
+ ## CLI and Session Protocol
91
+
92
+ Run lifecycle commands from the host checkout (or set `OPEN_IN_EDITOR_PROJECT_ROOT`):
89
93
 
90
- ```text
91
- GET /__open-in-editor?session=<checkout-id>&file=%2Fusr%2Fsrc%2Fweb%2Fapp.rb%3A12%3A4
94
+ ```sh
95
+ open-in-editor-bridge --session-id
96
+ open-in-editor-bridge --ensure-running
97
+ # Later, with the same configuration:
98
+ open-in-editor-bridge --shutdown
92
99
  ```
93
100
 
94
- `GET /health` reports the broker PID, protocol, bind address, and active checkout IDs. `GET /__open-in-editor` spawns the registered editor with `--goto <translated-target>`, preserving optional `:line:column`. Container paths equal to or below `/usr/src/web` map to that checkout's host `web` directory. Other paths pass through unchanged. Query encoding is decoded exactly once, preserving literal percent sequences in filenames.
101
+ The CLI is the low-level lifecycle interface, not a substitute for the Docker/Vite adapter. `--serve` runs a foreground broker and registers the current checkout; it requires a free port. Manually interrupting it stops all sessions. Prefer the managed API for sharing.
102
+
103
+ `GET /health` reports PID, protocol, bind address, and active checkout IDs. `GET /__open-in-editor?file=<encoded-target>&session=<checkout-id>` spawns that checkout's editor with `--goto <translated-target>`, preserving optional `:line:column`. Paths equal to or below `/usr/src/web` map to the host `web` directory; other paths pass through unchanged. Query encoding is decoded exactly once, preserving literal percent sequences in filenames.
95
104
 
96
- With exactly one checkout, existing links without `session` still work. With multiple checkouts, missing `session` returns HTTP 400 rather than guessing a checkout; unknown IDs return HTTP 404. Registering the same checkout with different roots or editor configuration while it is active is rejected; stop its existing leases before changing configuration.
105
+ Custom integrations may use the same session protocol. With exactly one checkout, links without `session` work. With multiple checkouts, missing identity returns HTTP 400 rather than guessing; unknown IDs return HTTP 404. Conflicting roots/editor settings for an active checkout are rejected. Foreground and detached leases are independent; the last released lease shuts down the listener.
97
106
 
98
107
  ## Configuration
99
108
 
@@ -122,6 +131,12 @@ Version 0.2 replaces checkout-local PID/log files with shared runtime state. Sto
122
131
 
123
132
  ## Development and Release
124
133
 
134
+ Commit the root `Gemfile.lock` to keep development and release-test dependencies reproducible. Regenerate it with Bundler after changing `Gemfile`, and commit both files together when both change. This development lockfile is not included in the gem package and does not constrain applications installing this gem; those applications resolve the gemspec's dependencies using their own lockfiles.
135
+
136
+ Keep generated `*.gem` packages, `.ruby-lsp/` editor state, and local `pkg/` / `tmp/` artifacts out of version control.
137
+
138
+ Development checks require Node.js 18+ for the bundled Vite plugin's dependency-free tests; the Ruby library itself remains dependency-free. `rake test` runs both Ruby and Node checks.
139
+
125
140
  ```sh
126
141
  bundle install
127
142
  bundle exec rake test
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+
5
+ class OpenInEditorBridge
6
+ class Docker
7
+ CONTAINER_DIRECTORY = "/open-in-editor-bridge"
8
+ COMPOSE_FILES = %w[compose.yaml compose.yml docker-compose.yaml docker-compose.yml].freeze
9
+ OVERRIDE_FILES = %w[compose.override.yaml compose.override.yml docker-compose.override.yaml docker-compose.override.yml].freeze
10
+
11
+ def initialize(env:, project_root:, service:, compose_options:)
12
+ @env = env.to_h
13
+ @project_root = File.expand_path(project_root || @env.fetch("OPEN_IN_EDITOR_PROJECT_ROOT", Dir.pwd))
14
+ @service = service
15
+ @compose_options = compose_options
16
+ end
17
+
18
+ def call(*args)
19
+ return up(args) if args.first == "up"
20
+
21
+ result = run(args)
22
+ bridge.call("--shutdown") if args.first == "down"
23
+ result
24
+ end
25
+
26
+ private
27
+
28
+ def configuration
29
+ @configuration ||= Configuration.new(env: @env, project_root: @project_root)
30
+ end
31
+
32
+ def bridge
33
+ @bridge ||= OpenInEditorBridge.new(env: @env, project_root: @project_root)
34
+ end
35
+
36
+ def up(args)
37
+ if %w[127.0.0.1 ::1 localhost].include?(configuration.bind_address)
38
+ raise ArgumentError, "Docker editor access requires explicit OPEN_IN_EDITOR_BRIDGE_BIND_ADDRESS (for example 0.0.0.0 on a trusted network)."
39
+ end
40
+ raise ArgumentError, "Select a Compose service for the Vite integration" if @service.to_s.empty?
41
+
42
+ # Resolve Compose inputs before acquiring a lease; invalid setup must not leave a broker behind.
43
+ files = compose_file_options
44
+ with_override do |override|
45
+ options = [*files, "-f", override]
46
+ detached?(args) ? detached_up(args, options) : foreground_up(args, options)
47
+ end
48
+ end
49
+
50
+ def foreground_up(args, options)
51
+ lease = SecureRandom.hex(16)
52
+ bridge.start_session(lease)
53
+ begin
54
+ run(args, options)
55
+ ensure
56
+ release_preserving_error(lease)
57
+ end
58
+ end
59
+
60
+ def detached_up(args, options)
61
+ registration = bridge.start_session("detached")
62
+ run(args, options)
63
+ rescue StandardError
64
+ release_preserving_error("detached") if registration == :started
65
+ raise
66
+ end
67
+
68
+ def release_preserving_error(lease)
69
+ original_error = $!
70
+ bridge.stop_session(lease)
71
+ rescue StandardError
72
+ raise unless original_error
73
+ end
74
+
75
+ def detached?(args)
76
+ flags = args.select { |arg| %w[-d --detach --detach=true --detach=false -d=true -d=false].include?(arg) }
77
+ !flags.empty? && !%w[--detach=false -d=false].include?(flags.last)
78
+ end
79
+
80
+ def run(args, extra_options = [])
81
+ system(@env, "docker", "compose", *@compose_options, *extra_options, *args,
82
+ chdir: @project_root, exception: true)
83
+ end
84
+
85
+ def with_override
86
+ configuration.ensure_runtime_directory
87
+ manifest = write_manifest
88
+ mounts = [
89
+ file_mount(File.join(__dir__, "vite.mjs"), "vite.mjs"),
90
+ file_mount(manifest, "session.json"),
91
+ ]
92
+ override = { "services" => { @service => {
93
+ "extra_hosts" => { "host.docker.internal" => "host-gateway" },
94
+ "volumes" => mounts,
95
+ } } }
96
+ Tempfile.create(["compose-", ".json"], configuration.runtime_directory) do |file|
97
+ file.write(JSON.generate(override))
98
+ file.flush
99
+ yield file.path
100
+ end
101
+ end
102
+
103
+ def file_mount(source, name)
104
+ { "type" => "bind", "source" => File.expand_path(source),
105
+ "target" => "#{CONTAINER_DIRECTORY}/#{name}", "read_only" => true,
106
+ "bind" => { "create_host_path" => false } }
107
+ end
108
+
109
+ def write_manifest
110
+ path = File.join(configuration.runtime_directory, "docker-#{configuration.port}-#{configuration.session_id}.json")
111
+ payload = { "session" => configuration.session_id, "target" => "http://host.docker.internal:#{configuration.port}" }
112
+ # Atomic replacement keeps simultaneous starts from exposing a partial identity file.
113
+ Tempfile.create(["session-", ".json"], configuration.runtime_directory) do |file|
114
+ file.write(JSON.generate(payload))
115
+ file.flush
116
+ file.chmod(0o644)
117
+ File.rename(file.path, path)
118
+ end
119
+ path
120
+ end
121
+
122
+ def compose_file_options
123
+ return [] if @compose_options.any? { |arg| arg == "-f" || arg == "--file" || arg.start_with?("--file=") || arg.match?(/\A-f.+/) }
124
+
125
+ if @env["COMPOSE_FILE"] && !@env["COMPOSE_FILE"].empty?
126
+ separator = @env.fetch("COMPOSE_PATH_SEPARATOR", File::PATH_SEPARATOR)
127
+ return @env.fetch("COMPOSE_FILE").split(separator).flat_map { |path| ["-f", File.expand_path(path, @project_root)] }
128
+ end
129
+
130
+ directory = discovery_directory
131
+ loop do
132
+ base = COMPOSE_FILES.find { |name| File.file?(File.join(directory, name)) }
133
+ if base
134
+ override = OVERRIDE_FILES.find { |name| File.file?(File.join(directory, name)) }
135
+ return [base, override].compact.flat_map { |name| ["-f", File.join(directory, name)] }
136
+ end
137
+ parent = File.dirname(directory)
138
+ raise ArgumentError, "No Compose file found; select it with compose_options: [\"-f\", path]" if parent == directory
139
+ directory = parent
140
+ end
141
+ end
142
+
143
+ def discovery_directory
144
+ @compose_options.each_with_index do |option, index|
145
+ return File.expand_path(@compose_options.fetch(index + 1), @project_root) if option == "--project-directory"
146
+ return File.expand_path(option.delete_prefix("--project-directory="), @project_root) if option.start_with?("--project-directory=")
147
+ end
148
+ @project_root
149
+ end
150
+ end
151
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OpenInEditorBridge
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const sessionManifestPath = new URL("./session.json", import.meta.url);
4
+ const editorRouteKey = "^/__open-in-editor(?:\\?|$)";
5
+
6
+ function readSession() {
7
+ try {
8
+ const manifest = JSON.parse(readFileSync(sessionManifestPath, "utf8"));
9
+ if (
10
+ !manifest ||
11
+ typeof manifest.session !== "string" ||
12
+ !/^[a-f0-9]{64}$/.test(manifest.session) ||
13
+ typeof manifest.target !== "string"
14
+ ) {
15
+ throw new Error("Invalid checkout identity");
16
+ }
17
+
18
+ const target = new URL(manifest.target);
19
+ if (
20
+ target.protocol !== "http:" ||
21
+ !target.hostname ||
22
+ target.username ||
23
+ target.password ||
24
+ target.pathname !== "/" ||
25
+ target.search ||
26
+ target.hash
27
+ ) {
28
+ throw new Error("Invalid bridge target");
29
+ }
30
+
31
+ return { session: manifest.session, target: target.origin };
32
+ } catch (cause) {
33
+ throw new Error("OpenInEditorBridge requires a valid Docker session manifest; start Vite through OpenInEditorBridge.compose.", { cause });
34
+ }
35
+ }
36
+
37
+ function withSession(requestUrl, session) {
38
+ const queryStart = requestUrl.indexOf("?");
39
+ if (queryStart === -1) return `${requestUrl}?session=${encodeURIComponent(session)}`;
40
+
41
+ const path = requestUrl.slice(0, queryStart);
42
+ const query = requestUrl.slice(queryStart + 1);
43
+ const retained = query.split("&").filter((part) => {
44
+ const separator = part.indexOf("=");
45
+ const rawName = separator === -1 ? part : part.slice(0, separator);
46
+ try {
47
+ return decodeURIComponent(rawName.replace(/\+/g, " ")) !== "session";
48
+ } catch {
49
+ return true;
50
+ }
51
+ });
52
+ retained.push(`session=${encodeURIComponent(session)}`);
53
+ return `${path}?${retained.join("&")}`;
54
+ }
55
+
56
+ export default function openInEditorBridge() {
57
+ const identity = readSession();
58
+
59
+ return {
60
+ name: "open-in-editor-bridge",
61
+ enforce: "post",
62
+ config(config) {
63
+ const existing = { ...config.server?.proxy };
64
+ delete existing["/__open-in-editor"];
65
+ delete existing["^/__open-in-editor$"];
66
+ delete existing[editorRouteKey];
67
+
68
+ config.server ??= {};
69
+ config.server.proxy = {
70
+ [editorRouteKey]: {
71
+ target: identity.target,
72
+ changeOrigin: true,
73
+ rewrite: (requestUrl) => withSession(requestUrl, identity.session),
74
+ },
75
+ ...existing,
76
+ };
77
+ },
78
+ };
79
+ }
@@ -14,6 +14,10 @@ class OpenInEditorBridge
14
14
  new.session_id
15
15
  end
16
16
 
17
+ def self.compose(*args, service: "web", compose_options: [], env: ENV, project_root: nil)
18
+ Docker.new(env: env, project_root: project_root, service: service, compose_options: compose_options).call(*args)
19
+ end
20
+
17
21
  def self.with_running(ensure_running: true)
18
22
  bridge = new
19
23
  lease = SecureRandom.hex(16) if ensure_running
@@ -69,5 +73,6 @@ require_relative "open_in_editor_bridge/configuration"
69
73
  require_relative "open_in_editor_bridge/authentication"
70
74
  require_relative "open_in_editor_bridge/server"
71
75
  require_relative "open_in_editor_bridge/client"
76
+ require_relative "open_in_editor_bridge/docker"
72
77
 
73
78
  OpenInEditorBridge.call(*ARGV) if $PROGRAM_NAME == __FILE__
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open-in-editor-bridge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marlen Brunner
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-09 00:00:00.000000000 Z
11
+ date: 2026-10-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Ruby API and CLI for serving local open-in-editor requests during development.
14
14
  email:
@@ -26,8 +26,10 @@ files:
26
26
  - lib/open_in_editor_bridge/authentication.rb
27
27
  - lib/open_in_editor_bridge/client.rb
28
28
  - lib/open_in_editor_bridge/configuration.rb
29
+ - lib/open_in_editor_bridge/docker.rb
29
30
  - lib/open_in_editor_bridge/server.rb
30
31
  - lib/open_in_editor_bridge/version.rb
32
+ - lib/open_in_editor_bridge/vite.mjs
31
33
  homepage: https://github.com/klondikemarlen/open-in-editor-bridge
32
34
  licenses:
33
35
  - MIT