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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +68 -53
- data/lib/open_in_editor_bridge/docker.rb +151 -0
- data/lib/open_in_editor_bridge/version.rb +1 -1
- data/lib/open_in_editor_bridge/vite.mjs +79 -0
- data/lib/open_in_editor_bridge.rb +5 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d25bb9c59762f68ee9b164865d324147c717b4b1d5c13bb3163dc57b2c65763
|
|
4
|
+
data.tar.gz: 85c973a0426c36ece6f5daec5b767380bc4621d0c39e28ccb01e572279c58db0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
##
|
|
15
|
+
## Docker and Vite Integration
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
33
|
+
Add the plugin to your existing Vite configuration for Docker development:
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
```js
|
|
36
|
+
import { defineConfig } from "vite";
|
|
31
37
|
|
|
32
|
-
|
|
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
|
-
|
|
46
|
+
return { plugins, server: { host: "0.0.0.0" } };
|
|
47
|
+
});
|
|
48
|
+
```
|
|
35
49
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
73
|
+
## Low-Level Ruby API
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
For non-Compose applications or custom integrations:
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
```ruby
|
|
78
|
+
require "open_in_editor_bridge"
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
-
```
|
|
91
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
@@ -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.
|
|
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-
|
|
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
|