open-in-editor-bridge 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/exe/open-in-editor-bridge +5 -0
- data/lib/open_in_editor_bridge/version.rb +5 -0
- data/lib/open_in_editor_bridge.rb +349 -0
- metadata +51 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b59090b703a55c7194b9898ffc6753cda0df2a3e3b1d1d311c1cd0dcd0fa57d9
|
|
4
|
+
data.tar.gz: d57211d78ebeb9048b963a853621ba575825115c45fdd2a651d75961b2062ee6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 86eccd454213ccc64074b85d2eb4a91ddfaec7bb879c0e7e7da0dc53a924ab9d926658d6ece2600ce699e2f5f46b97565d4bd17f1b9dd6358f15fc9bfca78224
|
|
7
|
+
data.tar.gz: 4be766b7b327ee39aa94f763979ab93b46a9a6c226774a80e3773c303f7f24d58870c6700e2685fc1399afcd8ee8424bfc47f976fffc99f25fb121f40ffb280b
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marlen Brunner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# OpenInEditorBridge
|
|
2
|
+
|
|
3
|
+
A small Ruby gem that runs a local HTTP bridge for opening files from a container in the host editor.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem "open-in-editor-bridge"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Ruby API
|
|
12
|
+
|
|
13
|
+
Wrap the local development command that needs the bridge:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "open_in_editor_bridge"
|
|
17
|
+
|
|
18
|
+
OpenInEditorBridge.with_running do
|
|
19
|
+
# Run the command that emits open-in-editor links.
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Use `ensure_running: false` when the wrapped command is responsible for stopping the bridge:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
OpenInEditorBridge.with_running(ensure_running: false) { run_down_command }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The bridge is stopped in the wrapper's `ensure` path. A block exception remains the raised exception.
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
open-in-editor-bridge --ensure-running
|
|
35
|
+
open-in-editor-bridge --shutdown
|
|
36
|
+
open-in-editor-bridge --serve
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The server provides `GET /health` and `GET /__open-in-editor?file=...`. Container paths beginning with `/usr/src/web` are mapped to the host `web` directory, preserving optional `:line:column` locations.
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
- `OPEN_IN_EDITOR_BRIDGE_PORT` (default `3333`)
|
|
44
|
+
- `OPEN_IN_EDITOR_PROJECT_ROOT` (default current directory)
|
|
45
|
+
- `OPEN_IN_EDITOR_CONTAINER_WEB_ROOT` (default `/usr/src/web`)
|
|
46
|
+
- `OPEN_IN_EDITOR_HOST_WEB_ROOT` (default `<project-root>/web`)
|
|
47
|
+
- `OPEN_IN_EDITOR_COMMAND` (or `EDITOR`)
|
|
48
|
+
|
|
49
|
+
The editor command is required and is parsed with `Shellwords`.
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
require "shellwords"
|
|
7
|
+
require "socket"
|
|
8
|
+
require "timeout"
|
|
9
|
+
require "uri"
|
|
10
|
+
require_relative "open_in_editor_bridge/version"
|
|
11
|
+
|
|
12
|
+
class OpenInEditorBridge
|
|
13
|
+
DEFAULT_PORT = 3333
|
|
14
|
+
HEALTH_PATH = "/health"
|
|
15
|
+
OPEN_IN_EDITOR_PATH = "/__open-in-editor"
|
|
16
|
+
RESPONSE_PHRASES = {
|
|
17
|
+
200 => "OK",
|
|
18
|
+
400 => "Bad Request",
|
|
19
|
+
404 => "Not Found",
|
|
20
|
+
405 => "Method Not Allowed",
|
|
21
|
+
500 => "Internal Server Error",
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
class StartupError < StandardError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.call(*args)
|
|
28
|
+
new.call(*args)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.with_running(ensure_running: true)
|
|
32
|
+
owns_server = !ensure_running
|
|
33
|
+
owns_server = call("--ensure-running") == :started if ensure_running
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
yield
|
|
37
|
+
ensure
|
|
38
|
+
if owns_server
|
|
39
|
+
block_error = $!
|
|
40
|
+
begin
|
|
41
|
+
call("--shutdown")
|
|
42
|
+
rescue StandardError => cleanup_error
|
|
43
|
+
raise cleanup_error unless block_error
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(env: ENV, project_root: nil)
|
|
50
|
+
@env = env
|
|
51
|
+
@project_root = project_root
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call(*args)
|
|
55
|
+
case args.first || "--serve"
|
|
56
|
+
when "--ensure-running"
|
|
57
|
+
ensure_running
|
|
58
|
+
when "--shutdown"
|
|
59
|
+
shutdown
|
|
60
|
+
when "--serve"
|
|
61
|
+
serve
|
|
62
|
+
else
|
|
63
|
+
serve
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def ensure_running
|
|
70
|
+
pid = running_pid(validate_health: true)
|
|
71
|
+
if pid
|
|
72
|
+
puts "Editor bridge already running on port #{config[:port]} (pid #{pid})."
|
|
73
|
+
return :reused
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
ensure_runtime_directory
|
|
77
|
+
pid = Process.spawn(
|
|
78
|
+
*server_process_command,
|
|
79
|
+
out: config[:log_file],
|
|
80
|
+
err: config[:log_file],
|
|
81
|
+
pgroup: true
|
|
82
|
+
)
|
|
83
|
+
Process.detach(pid)
|
|
84
|
+
|
|
85
|
+
wait_until_ready(pid)
|
|
86
|
+
write_pid_file(pid)
|
|
87
|
+
puts "Started editor bridge on port #{config[:port]}."
|
|
88
|
+
:started
|
|
89
|
+
rescue StandardError
|
|
90
|
+
if pid && process_running?(pid)
|
|
91
|
+
begin
|
|
92
|
+
Process.kill("TERM", -pid)
|
|
93
|
+
rescue Errno::ESRCH
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
delete_pid_file
|
|
97
|
+
raise
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def shutdown
|
|
101
|
+
pid = running_pid
|
|
102
|
+
return unless pid
|
|
103
|
+
|
|
104
|
+
Process.kill("TERM", -pid)
|
|
105
|
+
wait_until_stopped(pid)
|
|
106
|
+
delete_pid_file
|
|
107
|
+
puts "Stopped editor bridge."
|
|
108
|
+
rescue Errno::ESRCH
|
|
109
|
+
delete_pid_file
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def serve
|
|
113
|
+
abort("No editor configured. Set OPEN_IN_EDITOR_COMMAND or EDITOR environment variable.") if editor_command.empty?
|
|
114
|
+
|
|
115
|
+
ensure_runtime_directory
|
|
116
|
+
write_pid_file(Process.pid)
|
|
117
|
+
@server = TCPServer.new("127.0.0.1", config[:port])
|
|
118
|
+
@running = true
|
|
119
|
+
install_signal_handlers
|
|
120
|
+
|
|
121
|
+
while @running
|
|
122
|
+
begin
|
|
123
|
+
socket = @server.accept
|
|
124
|
+
handle_request(socket)
|
|
125
|
+
rescue IOError, Errno::EBADF
|
|
126
|
+
break
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
ensure
|
|
130
|
+
@server&.close
|
|
131
|
+
delete_pid_file
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def handle_request(socket)
|
|
135
|
+
request = read_request(socket)
|
|
136
|
+
return if request.nil?
|
|
137
|
+
|
|
138
|
+
method = request.fetch(:method)
|
|
139
|
+
consume_headers(socket)
|
|
140
|
+
return write_json_response(socket, status: 405, payload: { error: "Method not allowed" }) unless method == "GET"
|
|
141
|
+
|
|
142
|
+
respond_to_get_request(socket, request.fetch(:path), request.fetch(:params))
|
|
143
|
+
ensure
|
|
144
|
+
socket.close unless socket.closed?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def open_in_editor(socket, params)
|
|
148
|
+
file = params["file"]
|
|
149
|
+
if file.nil? || file.empty?
|
|
150
|
+
return write_json_response(
|
|
151
|
+
socket,
|
|
152
|
+
status: 400,
|
|
153
|
+
payload: { error: "Missing required query parameter: file" }
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
translated_target = translate_target(file)
|
|
158
|
+
pid = Process.spawn(*editor_command_args, "--goto", translated_target)
|
|
159
|
+
Process.detach(pid)
|
|
160
|
+
|
|
161
|
+
write_json_response(
|
|
162
|
+
socket,
|
|
163
|
+
status: 200,
|
|
164
|
+
payload: {
|
|
165
|
+
ok: true,
|
|
166
|
+
requestedFile: file,
|
|
167
|
+
translatedTarget: translated_target,
|
|
168
|
+
editorCommand: editor_command,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
rescue StandardError => error
|
|
172
|
+
write_json_response(
|
|
173
|
+
socket,
|
|
174
|
+
status: 500,
|
|
175
|
+
payload: {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: "Failed to open editor: #{error.message}",
|
|
178
|
+
requestedFile: file,
|
|
179
|
+
translatedTarget: translated_target,
|
|
180
|
+
editorCommand: editor_command,
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def write_json_response(socket, status: 200, payload:)
|
|
186
|
+
body = JSON.generate(payload)
|
|
187
|
+
socket.write("HTTP/1.1 #{status} #{RESPONSE_PHRASES.fetch(status)}\r\n")
|
|
188
|
+
socket.write("Content-Type: application/json\r\n")
|
|
189
|
+
socket.write("Access-Control-Allow-Origin: *\r\n")
|
|
190
|
+
socket.write("X-Open-In-Editor-Bridge: 1\r\n")
|
|
191
|
+
socket.write("Content-Length: #{body.bytesize}\r\n")
|
|
192
|
+
socket.write("Connection: close\r\n")
|
|
193
|
+
socket.write("\r\n")
|
|
194
|
+
socket.write(body)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def stop_server
|
|
198
|
+
@running = false
|
|
199
|
+
@server&.close
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def ensure_runtime_directory
|
|
203
|
+
FileUtils.mkdir_p(File.dirname(config[:pid_file]))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def write_pid_file(pid)
|
|
207
|
+
File.write(config[:pid_file], "#{pid}\n")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def delete_pid_file
|
|
211
|
+
File.delete(config[:pid_file]) if File.exist?(config[:pid_file])
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def server_process_command
|
|
215
|
+
[RbConfig.ruby, File.expand_path("../exe/open-in-editor-bridge", __dir__), "--serve"]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def editor_command
|
|
219
|
+
@env.fetch("OPEN_IN_EDITOR_COMMAND", @env.fetch("EDITOR", "")).to_s
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def editor_command_args
|
|
223
|
+
Shellwords.split(editor_command)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def install_signal_handlers
|
|
227
|
+
trap("INT") { stop_server }
|
|
228
|
+
trap("TERM") { stop_server }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def read_request(socket)
|
|
232
|
+
request_line = socket.gets
|
|
233
|
+
return nil if request_line.nil?
|
|
234
|
+
|
|
235
|
+
method, raw_target, = request_line.split(" ")
|
|
236
|
+
return nil if method.nil? || raw_target.nil?
|
|
237
|
+
|
|
238
|
+
path, query = raw_target.split("?", 2)
|
|
239
|
+
params = URI.decode_www_form(query.to_s).to_h
|
|
240
|
+
{ method: method, path: path, params: params }
|
|
241
|
+
rescue ArgumentError
|
|
242
|
+
write_json_response(socket, status: 400, payload: { error: "Bad request" })
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def consume_headers(socket)
|
|
247
|
+
loop do
|
|
248
|
+
line = socket.gets
|
|
249
|
+
break if line.nil? || line == "\r\n"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def respond_to_get_request(socket, path, params)
|
|
254
|
+
case path
|
|
255
|
+
when HEALTH_PATH
|
|
256
|
+
write_json_response(socket, payload: { ok: true })
|
|
257
|
+
when OPEN_IN_EDITOR_PATH
|
|
258
|
+
open_in_editor(socket, params)
|
|
259
|
+
else
|
|
260
|
+
write_json_response(socket, status: 404, payload: { error: "Not found" })
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def translate_target(target)
|
|
265
|
+
path, location = split_target(target)
|
|
266
|
+
translated_path = path.sub(/\A#{Regexp.escape(config[:container_web_root])}/, config[:host_web_root])
|
|
267
|
+
return translated_path if location.nil?
|
|
268
|
+
|
|
269
|
+
"#{translated_path}:#{location}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def split_target(target)
|
|
273
|
+
decoded_target = URI.decode_www_form_component(target)
|
|
274
|
+
match = decoded_target.match(/\A(.+?):(\d+)(?::(\d+))?\z/)
|
|
275
|
+
return [decoded_target, nil] if match.nil?
|
|
276
|
+
|
|
277
|
+
[match[1], [match[2], match[3]].compact.join(":")]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def running_pid(validate_health: false)
|
|
281
|
+
return nil unless File.exist?(config[:pid_file])
|
|
282
|
+
|
|
283
|
+
pid = Integer(File.read(config[:pid_file]).strip)
|
|
284
|
+
return pid if process_running?(pid) && (!validate_health || bridge_healthy?)
|
|
285
|
+
|
|
286
|
+
delete_pid_file
|
|
287
|
+
nil
|
|
288
|
+
rescue ArgumentError
|
|
289
|
+
delete_pid_file
|
|
290
|
+
nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def process_running?(pid)
|
|
294
|
+
Process.kill(0, pid)
|
|
295
|
+
true
|
|
296
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
297
|
+
false
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def wait_until_ready(pid)
|
|
301
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + config[:startup_timeout]
|
|
302
|
+
loop do
|
|
303
|
+
raise StartupError, "Editor bridge failed to start. See #{config[:log_file]}." unless process_running?(pid)
|
|
304
|
+
return if bridge_healthy?
|
|
305
|
+
|
|
306
|
+
raise StartupError, "Editor bridge did not become ready." if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
307
|
+
sleep 0.05
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def wait_until_stopped(pid)
|
|
312
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + config[:startup_timeout]
|
|
313
|
+
until !process_running?(pid) && !bridge_healthy?
|
|
314
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
315
|
+
|
|
316
|
+
sleep 0.05
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def bridge_healthy?
|
|
321
|
+
socket = TCPSocket.new("127.0.0.1", config[:port])
|
|
322
|
+
response = Timeout.timeout(0.25) do
|
|
323
|
+
socket.write("GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
|
|
324
|
+
socket.read
|
|
325
|
+
end
|
|
326
|
+
headers, body = response.split("\r\n\r\n", 2)
|
|
327
|
+
headers&.start_with?("HTTP/1.1 200 ") &&
|
|
328
|
+
headers.lines.any? { |line| line.casecmp("X-Open-In-Editor-Bridge: 1\r\n").zero? } &&
|
|
329
|
+
JSON.parse(body.to_s).fetch("ok") == true
|
|
330
|
+
rescue Errno::ECONNREFUSED, JSON::ParserError, KeyError, Timeout::Error
|
|
331
|
+
false
|
|
332
|
+
ensure
|
|
333
|
+
socket&.close unless socket&.closed?
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def config
|
|
337
|
+
root = @project_root || @env.fetch("OPEN_IN_EDITOR_PROJECT_ROOT", Dir.pwd)
|
|
338
|
+
{
|
|
339
|
+
port: Integer(@env.fetch("OPEN_IN_EDITOR_BRIDGE_PORT", DEFAULT_PORT)),
|
|
340
|
+
pid_file: File.join(root, "tmp", "open-in-editor-bridge.pid"),
|
|
341
|
+
log_file: File.join(root, "tmp", "open-in-editor-bridge.log"),
|
|
342
|
+
container_web_root: @env.fetch("OPEN_IN_EDITOR_CONTAINER_WEB_ROOT", "/usr/src/web"),
|
|
343
|
+
host_web_root: @env.fetch("OPEN_IN_EDITOR_HOST_WEB_ROOT", File.join(root, "web")),
|
|
344
|
+
startup_timeout: Float(@env.fetch("OPEN_IN_EDITOR_BRIDGE_STARTUP_TIMEOUT", "5")),
|
|
345
|
+
}
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
OpenInEditorBridge.call(*ARGV) if $PROGRAM_NAME == __FILE__
|
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: open-in-editor-bridge
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Marlen Brunner
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ruby API and CLI for serving local open-in-editor requests during development.
|
|
14
|
+
email:
|
|
15
|
+
- klondikemarlen@gmail.com
|
|
16
|
+
executables:
|
|
17
|
+
- open-in-editor-bridge
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- exe/open-in-editor-bridge
|
|
24
|
+
- lib/open_in_editor_bridge.rb
|
|
25
|
+
- lib/open_in_editor_bridge/version.rb
|
|
26
|
+
homepage: https://github.com/klondikemarlen/open-in-editor-bridge
|
|
27
|
+
licenses:
|
|
28
|
+
- MIT
|
|
29
|
+
metadata:
|
|
30
|
+
bug_tracker_uri: https://github.com/klondikemarlen/open-in-editor-bridge/issues
|
|
31
|
+
source_code_uri: https://github.com/klondikemarlen/open-in-editor-bridge
|
|
32
|
+
post_install_message:
|
|
33
|
+
rdoc_options: []
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.2'
|
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
requirements: []
|
|
47
|
+
rubygems_version: 3.5.22
|
|
48
|
+
signing_key:
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Local HTTP bridge for opening container paths in an editor
|
|
51
|
+
test_files: []
|