msnav 0.2.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 +21 -0
- data/README.md +187 -0
- data/exe/msnav +11 -0
- data/lib/msnav/cli.rb +388 -0
- data/lib/msnav/config.rb +79 -0
- data/lib/msnav/ctl.rb +203 -0
- data/lib/msnav/datadir.rb +121 -0
- data/lib/msnav/errors.rb +11 -0
- data/lib/msnav/graph.rb +151 -0
- data/lib/msnav/navigator.rb +326 -0
- data/lib/msnav/server.rb +341 -0
- data/lib/msnav/store.rb +97 -0
- data/lib/msnav/version.rb +5 -0
- data/lib/msnav/windows.rb +221 -0
- data/lib/msnav.rb +23 -0
- metadata +101 -0
data/lib/msnav/store.rb
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "sqlite3"
|
|
5
|
+
|
|
6
|
+
module Msnav
|
|
7
|
+
# Access to the shared coderag SQLite index.
|
|
8
|
+
#
|
|
9
|
+
# coderag (Python) owns the schema and the writes that matter (graph, facts,
|
|
10
|
+
# chunks); msnav writes only the two v4 message-bus tables (`windows`,
|
|
11
|
+
# `window_commands`) that every daemon on the hub shares. One connection
|
|
12
|
+
# guarded by a Mutex: navigation itself runs on the in-memory graph, so the
|
|
13
|
+
# DB sees only window-registry traffic, generation polls, and graph reloads
|
|
14
|
+
# — serializing those is simpler than per-thread connections and equally
|
|
15
|
+
# correct across WEBrick's request threads.
|
|
16
|
+
class Store
|
|
17
|
+
SCHEMA_VERSION = 4 # the version msnav is written against
|
|
18
|
+
|
|
19
|
+
attr_reader :path
|
|
20
|
+
|
|
21
|
+
def initialize(path)
|
|
22
|
+
@path = Pathname.new(path.to_s)
|
|
23
|
+
unless @path.file?
|
|
24
|
+
raise IndexStaleError,
|
|
25
|
+
"no index at #{@path} — build it on the host with `coderag index` " \
|
|
26
|
+
"and mount the coderag data dir into this container"
|
|
27
|
+
end
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@conn = connect
|
|
30
|
+
check_version
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# All DB access funnels through here. Reentrant per call site by design:
|
|
34
|
+
# never call with_conn inside with_conn.
|
|
35
|
+
def with_conn
|
|
36
|
+
@mutex.synchronize { yield @conn }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def close
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@conn.close unless @conn.closed?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_meta(key)
|
|
46
|
+
with_conn do |db|
|
|
47
|
+
row = db.get_first_row("SELECT value FROM meta WHERE key = ?", [key])
|
|
48
|
+
row && row[0]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The shared DB's index generation — bumped by whichever coderag daemon
|
|
53
|
+
# (or host `coderag index`) last completed a build. 0 when unset.
|
|
54
|
+
def index_generation
|
|
55
|
+
(get_meta("index_generation") || 0).to_i
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Canonical [name, dirname, root] service rows as indexed on the host.
|
|
59
|
+
def services
|
|
60
|
+
with_conn do |db|
|
|
61
|
+
db.execute("SELECT name, dirname, root FROM services ORDER BY name")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def connect
|
|
68
|
+
db = SQLite3::Database.new(@path.to_s)
|
|
69
|
+
db.busy_timeout = 5000
|
|
70
|
+
db.execute("PRAGMA foreign_keys=ON")
|
|
71
|
+
db
|
|
72
|
+
rescue SQLite3::Exception => e
|
|
73
|
+
raise IndexStaleError, "#{@path} could not be opened (#{e.message})"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def check_version
|
|
77
|
+
row = begin
|
|
78
|
+
@conn.get_first_row("SELECT value FROM meta WHERE key='schema_version'")
|
|
79
|
+
rescue SQLite3::Exception => e
|
|
80
|
+
raise IndexStaleError,
|
|
81
|
+
"#{@path} is not a coderag database (#{e.message}) — " \
|
|
82
|
+
"delete it and run `coderag index` on the host"
|
|
83
|
+
end
|
|
84
|
+
version = row ? row[0].to_i : 0
|
|
85
|
+
if version < SCHEMA_VERSION
|
|
86
|
+
raise IndexStaleError,
|
|
87
|
+
"#{@path} is schema v#{version}, msnav needs v#{SCHEMA_VERSION} — " \
|
|
88
|
+
"run `coderag index` on the host to migrate"
|
|
89
|
+
end
|
|
90
|
+
return if version == SCHEMA_VERSION
|
|
91
|
+
# a newer coderag may have added tables msnav doesn't know; the v4
|
|
92
|
+
# tables it reads are append-only by convention, so proceed loudly
|
|
93
|
+
warn "msnav: #{@path} is schema v#{version} (msnav is written against " \
|
|
94
|
+
"v#{SCHEMA_VERSION}) — continuing, but upgrade msnav if navigation misbehaves"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Msnav
|
|
6
|
+
# DB-backed registry of live editor windows for bridge-mode open routing —
|
|
7
|
+
# the Ruby side of coderag/daemon/windows.py, byte-compatible on the shared
|
|
8
|
+
# `windows` / `window_commands` tables so msnav daemons in Ruby containers
|
|
9
|
+
# and coderag daemons elsewhere route opens to each other's windows.
|
|
10
|
+
#
|
|
11
|
+
# Each window registers and then short-polls for commands. When /api/open
|
|
12
|
+
# targets a file another live window owns, the command is enqueued in the
|
|
13
|
+
# DB (the message bus between containers) and that window's poll delivers
|
|
14
|
+
# it. A window counts as live only while it keeps polling.
|
|
15
|
+
class WindowRegistry
|
|
16
|
+
ALIVE_SECONDS = 6.0 # a registration is live only if it polled this recently
|
|
17
|
+
PURGE_SECONDS = 3600.0 # rows this stale are garbage-collected on registration
|
|
18
|
+
# A parked open-command (no owning window yet — a new window is being
|
|
19
|
+
# opened for it) waits this long for that window's extension to register.
|
|
20
|
+
# Generous: a first-time "Reopen in Container" builds the image first.
|
|
21
|
+
PARK_SECONDS = 600.0
|
|
22
|
+
# window_id marker for parked commands.
|
|
23
|
+
PARKED = ""
|
|
24
|
+
|
|
25
|
+
COLS = "window_id, authority, roots, path_mappings, host_roots, seq, last_poll"
|
|
26
|
+
|
|
27
|
+
WindowReg = Struct.new(:window_id, :authority, :roots, :path_mappings,
|
|
28
|
+
:host_roots, :seq, :last_poll, keyword_init: true) do
|
|
29
|
+
def alive?(now)
|
|
30
|
+
(now - last_poll) <= ALIVE_SECONDS
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(store)
|
|
35
|
+
@store = store
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def count
|
|
39
|
+
@store.with_conn { |db| db.get_first_value("SELECT COUNT(*) FROM windows") }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Record (or refresh) a window. A reload re-registers with a fresh
|
|
43
|
+
# window_id but identical roots — stale entries with the same mapped
|
|
44
|
+
# (daemon-space) roots are dropped so only the live window remains.
|
|
45
|
+
def register(window_id, authority, roots, path_mappings)
|
|
46
|
+
roots = Array(roots)
|
|
47
|
+
pmaps = Array(path_mappings)
|
|
48
|
+
host_roots = roots.map { |r| root_to_host(pmaps, r) }.uniq.sort
|
|
49
|
+
hr_json = JSON.generate(host_roots)
|
|
50
|
+
now = Time.now.to_f
|
|
51
|
+
reg = nil
|
|
52
|
+
@store.with_conn do |db|
|
|
53
|
+
db.transaction(:immediate) do
|
|
54
|
+
db.execute(
|
|
55
|
+
"DELETE FROM window_commands WHERE window_id IN " \
|
|
56
|
+
"(SELECT window_id FROM windows WHERE last_poll < ?)",
|
|
57
|
+
[now - PURGE_SECONDS])
|
|
58
|
+
db.execute("DELETE FROM windows WHERE last_poll < ?",
|
|
59
|
+
[now - PURGE_SECONDS])
|
|
60
|
+
# expired parked commands: the window they waited for never came
|
|
61
|
+
db.execute(
|
|
62
|
+
"DELETE FROM window_commands WHERE window_id = ? AND created_at < ?",
|
|
63
|
+
[PARKED, now - PARK_SECONDS])
|
|
64
|
+
db.execute(
|
|
65
|
+
"DELETE FROM windows WHERE host_roots = ? AND window_id != ?",
|
|
66
|
+
[hr_json, window_id])
|
|
67
|
+
seq = db.get_first_value(
|
|
68
|
+
"SELECT COALESCE(MAX(seq), 0) + 1 FROM windows")
|
|
69
|
+
db.execute(
|
|
70
|
+
"INSERT INTO windows (window_id, authority, roots, " \
|
|
71
|
+
"path_mappings, host_roots, seq, last_poll) " \
|
|
72
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?) " \
|
|
73
|
+
"ON CONFLICT(window_id) DO UPDATE SET authority = excluded.authority, " \
|
|
74
|
+
"roots = excluded.roots, path_mappings = excluded.path_mappings, " \
|
|
75
|
+
"host_roots = excluded.host_roots, seq = excluded.seq, " \
|
|
76
|
+
"last_poll = excluded.last_poll",
|
|
77
|
+
[window_id, authority || "", JSON.generate(roots),
|
|
78
|
+
JSON.generate(pmaps), hr_json, seq, now])
|
|
79
|
+
# hand parked commands to the window that was opened FOR them: the
|
|
80
|
+
# first registration whose roots cover a parked path claims it
|
|
81
|
+
db.execute(
|
|
82
|
+
"SELECT id, payload FROM window_commands " \
|
|
83
|
+
"WHERE window_id = ? ORDER BY id", [PARKED]).each do |cmd_id, payload|
|
|
84
|
+
target = JSON.parse(payload)["path"] || ""
|
|
85
|
+
next unless host_roots.any? { |hr| under?(hr, target) }
|
|
86
|
+
db.execute("UPDATE window_commands SET window_id = ? WHERE id = ?",
|
|
87
|
+
[window_id, cmd_id])
|
|
88
|
+
end
|
|
89
|
+
reg = WindowReg.new(window_id: window_id, authority: authority || "",
|
|
90
|
+
roots: roots, path_mappings: pmaps,
|
|
91
|
+
host_roots: host_roots, seq: seq, last_poll: now)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
reg
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Mark a window as still-alive on poll; nil if it isn't registered.
|
|
98
|
+
def touch(window_id)
|
|
99
|
+
@store.with_conn do |db|
|
|
100
|
+
db.execute("UPDATE windows SET last_poll = ? WHERE window_id = ?",
|
|
101
|
+
[Time.now.to_f, window_id])
|
|
102
|
+
return nil if db.changes.zero?
|
|
103
|
+
row = db.get_first_row(
|
|
104
|
+
"SELECT #{COLS} FROM windows WHERE window_id = ?", [window_id])
|
|
105
|
+
row && row_to_reg(row)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Return and clear the window's pending commands — including ones
|
|
110
|
+
# enqueued by a different daemon sharing this DB.
|
|
111
|
+
def drain(window_id)
|
|
112
|
+
rows = nil
|
|
113
|
+
@store.with_conn do |db|
|
|
114
|
+
db.transaction(:immediate) do
|
|
115
|
+
rows = db.execute(
|
|
116
|
+
"SELECT id, payload FROM window_commands " \
|
|
117
|
+
"WHERE window_id = ? ORDER BY id", [window_id])
|
|
118
|
+
unless rows.empty?
|
|
119
|
+
db.execute(
|
|
120
|
+
"DELETE FROM window_commands WHERE id <= ? AND window_id = ?",
|
|
121
|
+
[rows[-1][0], window_id])
|
|
122
|
+
end
|
|
123
|
+
db.execute("UPDATE windows SET last_poll = ? WHERE window_id = ?",
|
|
124
|
+
[Time.now.to_f, window_id])
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
rows.map { |_id, payload| JSON.parse(payload) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Queue an open-command on the live window that owns HOST_PATH; returns
|
|
131
|
+
# the matched registration, or nil when no live window claims it.
|
|
132
|
+
def enqueue_open(host_path, line)
|
|
133
|
+
now = Time.now.to_f
|
|
134
|
+
reg = nil
|
|
135
|
+
@store.with_conn do |db|
|
|
136
|
+
db.transaction(:immediate) do
|
|
137
|
+
rows = db.execute(
|
|
138
|
+
"SELECT #{COLS} FROM windows WHERE last_poll >= ? " \
|
|
139
|
+
"ORDER BY seq DESC", [now - ALIVE_SECONDS])
|
|
140
|
+
rows.each do |row|
|
|
141
|
+
candidate = row_to_reg(row)
|
|
142
|
+
if candidate.host_roots.any? { |hr| under?(hr, host_path) }
|
|
143
|
+
reg = candidate
|
|
144
|
+
break
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
unless reg.nil?
|
|
148
|
+
db.execute(
|
|
149
|
+
"INSERT INTO window_commands (window_id, payload, created_at) " \
|
|
150
|
+
"VALUES (?, ?, ?)",
|
|
151
|
+
[reg.window_id,
|
|
152
|
+
JSON.generate("type" => "open", "path" => host_path,
|
|
153
|
+
"line" => line), now])
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
reg
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Queue an open for a window that does not exist yet: the caller is
|
|
161
|
+
# opening a new editor window for this service, and its extension will
|
|
162
|
+
# claim the command when it registers.
|
|
163
|
+
def park_open(host_path, line)
|
|
164
|
+
now = Time.now.to_f
|
|
165
|
+
@store.with_conn do |db|
|
|
166
|
+
db.transaction(:immediate) do
|
|
167
|
+
# re-clicking must not stack duplicate opens for the same file
|
|
168
|
+
db.execute(
|
|
169
|
+
"SELECT id, payload FROM window_commands WHERE window_id = ?",
|
|
170
|
+
[PARKED]).each do |cmd_id, payload|
|
|
171
|
+
next unless JSON.parse(payload)["path"] == host_path
|
|
172
|
+
db.execute("DELETE FROM window_commands WHERE id = ?", [cmd_id])
|
|
173
|
+
end
|
|
174
|
+
db.execute(
|
|
175
|
+
"INSERT INTO window_commands (window_id, payload, created_at) " \
|
|
176
|
+
"VALUES (?, ?, ?)",
|
|
177
|
+
[PARKED, JSON.generate("type" => "open", "path" => host_path,
|
|
178
|
+
"line" => line), now])
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def all
|
|
185
|
+
rows = @store.with_conn do |db|
|
|
186
|
+
db.execute("SELECT #{COLS} FROM windows ORDER BY seq")
|
|
187
|
+
end
|
|
188
|
+
rows.map { |r| row_to_reg(r) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
# True if PATH is ROOT itself or a descendant of it.
|
|
194
|
+
def under?(root, path)
|
|
195
|
+
root = root.sub(%r{/+\z}, "")
|
|
196
|
+
root = "/" if root.empty?
|
|
197
|
+
path == root || path.start_with?(root + "/")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Map a window-space ROOT to daemon-space via {from: window, to: daemon}.
|
|
201
|
+
# In the per-container flow both sides see the hub at the same mount path,
|
|
202
|
+
# so the mappings are typically empty and this is the identity.
|
|
203
|
+
def root_to_host(path_mappings, root)
|
|
204
|
+
path_mappings.each do |m|
|
|
205
|
+
frm = m["from"]
|
|
206
|
+
to = m["to"]
|
|
207
|
+
next unless frm && !frm.empty? && root.start_with?(frm)
|
|
208
|
+
return to.to_s + root[frm.length..-1]
|
|
209
|
+
end
|
|
210
|
+
root
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def row_to_reg(row)
|
|
214
|
+
WindowReg.new(window_id: row[0], authority: row[1],
|
|
215
|
+
roots: JSON.parse(row[2]),
|
|
216
|
+
path_mappings: JSON.parse(row[3]),
|
|
217
|
+
host_roots: JSON.parse(row[4]),
|
|
218
|
+
seq: row[5], last_poll: row[6])
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
data/lib/msnav.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "msnav/version"
|
|
4
|
+
require "msnav/errors"
|
|
5
|
+
require "msnav/datadir"
|
|
6
|
+
require "msnav/config"
|
|
7
|
+
require "msnav/store"
|
|
8
|
+
require "msnav/graph"
|
|
9
|
+
require "msnav/navigator"
|
|
10
|
+
require "msnav/windows"
|
|
11
|
+
require "msnav/server"
|
|
12
|
+
require "msnav/ctl"
|
|
13
|
+
require "msnav/cli"
|
|
14
|
+
|
|
15
|
+
# msnav — the Ruby "navigation face" of a coderag index.
|
|
16
|
+
#
|
|
17
|
+
# coderag (Python) builds the index on the host: one SQLite file holding the
|
|
18
|
+
# cross-service graph. msnav reads that same file — typically through the
|
|
19
|
+
# devcontainer's single data-dir bind mount — and serves the daemon endpoints
|
|
20
|
+
# the coderag/msnav editor extension talks to. It never indexes; when a host
|
|
21
|
+
# reindex bumps `index_generation` in the shared DB, msnav hot-reloads.
|
|
22
|
+
module Msnav
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: msnav
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yaroslav Zahoruiko
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: sqlite3
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.3'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '1.3'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: webrick
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.6'
|
|
40
|
+
type: :runtime
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.6'
|
|
47
|
+
description: |
|
|
48
|
+
A pure-Ruby daemon (ruby-lsp style) serving the coderag navigation API —
|
|
49
|
+
cross-service go-to-definition, references, hover, and CodeLens targets —
|
|
50
|
+
from a coderag SQLite index. Built to run inside a Ruby devcontainer with
|
|
51
|
+
no Python: the host builds the index with coderag, the container mounts
|
|
52
|
+
the shared data dir and runs `msnav up`.
|
|
53
|
+
email:
|
|
54
|
+
- zahoruiko.yaroslav@gmail.com
|
|
55
|
+
executables:
|
|
56
|
+
- msnav
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- LICENSE
|
|
61
|
+
- README.md
|
|
62
|
+
- exe/msnav
|
|
63
|
+
- lib/msnav.rb
|
|
64
|
+
- lib/msnav/cli.rb
|
|
65
|
+
- lib/msnav/config.rb
|
|
66
|
+
- lib/msnav/ctl.rb
|
|
67
|
+
- lib/msnav/datadir.rb
|
|
68
|
+
- lib/msnav/errors.rb
|
|
69
|
+
- lib/msnav/graph.rb
|
|
70
|
+
- lib/msnav/navigator.rb
|
|
71
|
+
- lib/msnav/server.rb
|
|
72
|
+
- lib/msnav/store.rb
|
|
73
|
+
- lib/msnav/version.rb
|
|
74
|
+
- lib/msnav/windows.rb
|
|
75
|
+
homepage: https://github.com/YaroslavZahoruiko/msnav
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
homepage_uri: https://github.com/YaroslavZahoruiko/msnav
|
|
80
|
+
source_code_uri: https://github.com/YaroslavZahoruiko/msnav
|
|
81
|
+
bug_tracker_uri: https://github.com/YaroslavZahoruiko/msnav/issues
|
|
82
|
+
post_install_message:
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '2.6'
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 3.0.3.1
|
|
98
|
+
signing_key:
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: Cross-service Ruby navigation server over a coderag index
|
|
101
|
+
test_files: []
|