open_gemdocs 0.2.3 → 0.3.1
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
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +37 -0
- data/CHANGELOG.md +4 -0
- data/CLAUDE.md +136 -0
- data/README.md +60 -2
- data/checksums/open_gemdocs-0.2.4.gem.sha512 +1 -0
- data/checksums/open_gemdocs-0.3.1.gem.sha512 +1 -0
- data/exe/document-bundle +8 -0
- data/exe/open-gem-docs +11 -11
- data/exe/open-gem-docs-mcp +52 -0
- data/exe/open-gem-docs-mcp-stdio +76 -0
- data/exe/open-local-docs +3 -0
- data/lib/open_gemdocs/browser.rb +12 -14
- data/lib/open_gemdocs/mcp/handlers.rb +92 -0
- data/lib/open_gemdocs/mcp/server.rb +77 -0
- data/lib/open_gemdocs/mcp/tools.rb +471 -0
- data/lib/open_gemdocs/version.rb +1 -1
- data/lib/open_gemdocs/yard.rb +21 -7
- data/lib/open_gemdocs/yard_json_formatter.rb +225 -0
- data/lib/open_gemdocs.rb +11 -3
- data.tar.gz.sig +0 -0
- metadata +32 -2
- metadata.gz.sig +0 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webrick"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module OpenGemdocs
|
7
|
+
module MCP
|
8
|
+
class Server
|
9
|
+
attr_reader :port
|
10
|
+
|
11
|
+
def initialize(port: 6789)
|
12
|
+
@port = port
|
13
|
+
@handlers = Handlers.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
server = WEBrick::HTTPServer.new(
|
18
|
+
Port: @port,
|
19
|
+
Logger: WEBrick::Log.new($stderr, WEBrick::Log::INFO),
|
20
|
+
AccessLog: []
|
21
|
+
)
|
22
|
+
|
23
|
+
server.mount_proc "/" do |req, res|
|
24
|
+
handle_request(req, res)
|
25
|
+
end
|
26
|
+
|
27
|
+
trap("INT") do
|
28
|
+
puts "\nShutting down MCP server..."
|
29
|
+
OpenGemdocs::Yard.stop_server
|
30
|
+
server.shutdown
|
31
|
+
end
|
32
|
+
|
33
|
+
puts "MCP server started on port #{@port}"
|
34
|
+
puts "Press Ctrl+C to stop"
|
35
|
+
server.start
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def handle_request(req, res)
|
41
|
+
if req.request_method == "POST"
|
42
|
+
handle_post_request(req, res)
|
43
|
+
else
|
44
|
+
res.status = 405
|
45
|
+
res.body = "Method not allowed"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_post_request(req, res)
|
50
|
+
body = JSON.parse(req.body)
|
51
|
+
response = @handlers.handle(body)
|
52
|
+
send_json_response(res, response)
|
53
|
+
rescue JSON::ParserError => e
|
54
|
+
send_json_error(res, -32_700, "Parse error", e.message)
|
55
|
+
rescue StandardError => e
|
56
|
+
send_json_error(res, -32_603, "Internal error", e.message)
|
57
|
+
end
|
58
|
+
|
59
|
+
def send_json_response(res, response)
|
60
|
+
res.content_type = "application/json"
|
61
|
+
res.body = JSON.generate(response)
|
62
|
+
res.status = 200
|
63
|
+
end
|
64
|
+
|
65
|
+
def send_json_error(res, code, message, data)
|
66
|
+
send_json_response(res, {
|
67
|
+
jsonrpc: "2.0",
|
68
|
+
error: {
|
69
|
+
code: code,
|
70
|
+
message: message,
|
71
|
+
data: data
|
72
|
+
}
|
73
|
+
})
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,471 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module OpenGemdocs
|
8
|
+
module MCP
|
9
|
+
class Tools
|
10
|
+
def initialize
|
11
|
+
# Ensure we can access the Yard module
|
12
|
+
require_relative "../yard"
|
13
|
+
require_relative "../yard_json_formatter"
|
14
|
+
end
|
15
|
+
|
16
|
+
def list
|
17
|
+
[
|
18
|
+
{
|
19
|
+
"name" => "search_gems",
|
20
|
+
"description" => "Search for installed Ruby gems",
|
21
|
+
"inputSchema" => {
|
22
|
+
"type" => "object",
|
23
|
+
"properties" => {
|
24
|
+
"query" => {
|
25
|
+
"type" => "string",
|
26
|
+
"description" => "Search query for gem names (partial match supported)"
|
27
|
+
}
|
28
|
+
},
|
29
|
+
"required" => ["query"]
|
30
|
+
}
|
31
|
+
},
|
32
|
+
{
|
33
|
+
"name" => "get_gem_info",
|
34
|
+
"description" => "Get information about a specific gem including version and summary",
|
35
|
+
"inputSchema" => {
|
36
|
+
"type" => "object",
|
37
|
+
"properties" => {
|
38
|
+
"gem_name" => {
|
39
|
+
"type" => "string",
|
40
|
+
"description" => "Exact name of the gem"
|
41
|
+
}
|
42
|
+
},
|
43
|
+
"required" => ["gem_name"]
|
44
|
+
}
|
45
|
+
},
|
46
|
+
{
|
47
|
+
"name" => "start_yard_server",
|
48
|
+
"description" => "Start the Yard documentation server",
|
49
|
+
"inputSchema" => {
|
50
|
+
"type" => "object",
|
51
|
+
"properties" => {}
|
52
|
+
}
|
53
|
+
},
|
54
|
+
{
|
55
|
+
"name" => "stop_yard_server",
|
56
|
+
"description" => "Stop the Yard documentation server",
|
57
|
+
"inputSchema" => {
|
58
|
+
"type" => "object",
|
59
|
+
"properties" => {}
|
60
|
+
}
|
61
|
+
},
|
62
|
+
{
|
63
|
+
"name" => "get_yard_server_status",
|
64
|
+
"description" => "Check if the Yard documentation server is running",
|
65
|
+
"inputSchema" => {
|
66
|
+
"type" => "object",
|
67
|
+
"properties" => {}
|
68
|
+
}
|
69
|
+
},
|
70
|
+
{
|
71
|
+
"name" => "get_gem_documentation_url",
|
72
|
+
"description" => "Get the local documentation URL for a gem (Note: Use fetch_gem_docs instead to get actual documentation content)",
|
73
|
+
"inputSchema" => {
|
74
|
+
"type" => "object",
|
75
|
+
"properties" => {
|
76
|
+
"gem_name" => {
|
77
|
+
"type" => "string",
|
78
|
+
"description" => "Name of the gem"
|
79
|
+
},
|
80
|
+
"class_name" => {
|
81
|
+
"type" => "string",
|
82
|
+
"description" => "Optional: specific class or module name"
|
83
|
+
}
|
84
|
+
},
|
85
|
+
"required" => ["gem_name"]
|
86
|
+
}
|
87
|
+
},
|
88
|
+
{
|
89
|
+
"name" => "fetch_gem_docs",
|
90
|
+
"description" => "Fetch structured documentation content for a gem or specific class/module. Returns formatted documentation with methods, attributes, parameters, and examples. Use this instead of fetching URLs directly.",
|
91
|
+
"inputSchema" => {
|
92
|
+
"type" => "object",
|
93
|
+
"properties" => {
|
94
|
+
"gem_name" => {
|
95
|
+
"type" => "string",
|
96
|
+
"description" => "Name of the gem"
|
97
|
+
},
|
98
|
+
"path" => {
|
99
|
+
"type" => "string",
|
100
|
+
"description" => 'Optional: specific class or module path (e.g., "FactoryBot::Trait" or "ActiveRecord::Base")'
|
101
|
+
}
|
102
|
+
},
|
103
|
+
"required" => ["gem_name"]
|
104
|
+
}
|
105
|
+
}
|
106
|
+
]
|
107
|
+
end
|
108
|
+
|
109
|
+
def call(tool_name, arguments)
|
110
|
+
case tool_name
|
111
|
+
when "search_gems"
|
112
|
+
search_gems(arguments["query"])
|
113
|
+
when "get_gem_info"
|
114
|
+
get_gem_info(arguments["gem_name"])
|
115
|
+
when "start_yard_server"
|
116
|
+
start_yard_server
|
117
|
+
when "stop_yard_server"
|
118
|
+
stop_yard_server
|
119
|
+
when "get_yard_server_status"
|
120
|
+
get_yard_server_status
|
121
|
+
when "get_gem_documentation_url"
|
122
|
+
get_gem_documentation_url(arguments["gem_name"], arguments["class_name"])
|
123
|
+
when "fetch_gem_docs"
|
124
|
+
fetch_gem_docs(arguments["gem_name"], arguments["path"])
|
125
|
+
else
|
126
|
+
{
|
127
|
+
"content" => [{
|
128
|
+
"type" => "text",
|
129
|
+
"text" => "Unknown tool: #{tool_name}"
|
130
|
+
}],
|
131
|
+
"isError" => true
|
132
|
+
}
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def search_gems(query)
|
139
|
+
if query.nil? || query.empty?
|
140
|
+
return { "content" => [{ "type" => "text", "text" => "Query cannot be empty" }],
|
141
|
+
"isError" => true }
|
142
|
+
end
|
143
|
+
|
144
|
+
gems = `gem list --local`.lines
|
145
|
+
.map do |line|
|
146
|
+
parts = line.strip.match(/^([^\s]+)\s+\(([^)]+)\)/)
|
147
|
+
next unless parts
|
148
|
+
|
149
|
+
{ name: parts[1], versions: parts[2] }
|
150
|
+
end
|
151
|
+
.compact
|
152
|
+
.select { |gem| gem[:name].downcase.include?(query.downcase) }
|
153
|
+
|
154
|
+
if gems.empty?
|
155
|
+
{
|
156
|
+
"content" => [{
|
157
|
+
"type" => "text",
|
158
|
+
"text" => "No gems found matching '#{query}'"
|
159
|
+
}]
|
160
|
+
}
|
161
|
+
else
|
162
|
+
gem_list = gems.map { |g| "• #{g[:name]} (#{g[:versions]})" }.join("\n")
|
163
|
+
{
|
164
|
+
"content" => [{
|
165
|
+
"type" => "text",
|
166
|
+
"text" => "Found #{gems.count} gem(s):\n#{gem_list}"
|
167
|
+
}]
|
168
|
+
}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_gem_info(gem_name)
|
173
|
+
spec = Gem::Specification.find_by_name(gem_name)
|
174
|
+
|
175
|
+
info = []
|
176
|
+
info << "**#{spec.name}** v#{spec.version}"
|
177
|
+
info << ""
|
178
|
+
info << "**Summary:** #{spec.summary}" if spec.summary
|
179
|
+
info << "**Description:** #{spec.description}" if spec.description && spec.description != spec.summary
|
180
|
+
info << "**Homepage:** #{spec.homepage}" if spec.homepage
|
181
|
+
info << "**License:** #{spec.license || spec.licenses.join(", ")}" if spec.license || spec.licenses.any?
|
182
|
+
info << "**Authors:** #{spec.authors.join(", ")}" if spec.authors.any?
|
183
|
+
|
184
|
+
{
|
185
|
+
"content" => [{
|
186
|
+
"type" => "text",
|
187
|
+
"text" => info.join("\n")
|
188
|
+
}]
|
189
|
+
}
|
190
|
+
rescue Gem::LoadError => e
|
191
|
+
{
|
192
|
+
"content" => [{
|
193
|
+
"type" => "text",
|
194
|
+
"text" => "Gem '#{gem_name}' not found: #{e.message}"
|
195
|
+
}],
|
196
|
+
"isError" => true
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
def start_yard_server
|
201
|
+
if OpenGemdocs::Yard.server_running?
|
202
|
+
current_dir = Dir.pwd
|
203
|
+
yard_dir = OpenGemdocs::Yard.yard_server_directory
|
204
|
+
|
205
|
+
if yard_dir && yard_dir != current_dir
|
206
|
+
# Yard is running in a different directory
|
207
|
+
{
|
208
|
+
"content" => [{
|
209
|
+
"type" => "text",
|
210
|
+
"text" => "Yard server is running in a different directory:\n" \
|
211
|
+
"Running in: #{yard_dir}\n" \
|
212
|
+
"Current dir: #{current_dir}\n\n" \
|
213
|
+
"The server is serving gems from '#{yard_dir}'.\n" \
|
214
|
+
"To serve gems from the current directory, stop the server first with 'stop_yard_server' tool."
|
215
|
+
}]
|
216
|
+
}
|
217
|
+
else
|
218
|
+
{
|
219
|
+
"content" => [{
|
220
|
+
"type" => "text",
|
221
|
+
"text" => "Yard server is already running on port 8808"
|
222
|
+
}]
|
223
|
+
}
|
224
|
+
end
|
225
|
+
else
|
226
|
+
OpenGemdocs::Yard.start_yard_server
|
227
|
+
sleep 2 # Give server time to start
|
228
|
+
|
229
|
+
if OpenGemdocs::Yard.server_running?
|
230
|
+
{
|
231
|
+
"content" => [{
|
232
|
+
"type" => "text",
|
233
|
+
"text" => "Yard server started successfully on port 8808"
|
234
|
+
}]
|
235
|
+
}
|
236
|
+
else
|
237
|
+
{
|
238
|
+
"content" => [{
|
239
|
+
"type" => "text",
|
240
|
+
"text" => "Failed to start Yard server"
|
241
|
+
}],
|
242
|
+
"isError" => true
|
243
|
+
}
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def stop_yard_server
|
249
|
+
if OpenGemdocs::Yard.server_running?
|
250
|
+
OpenGemdocs::Yard.stop_server
|
251
|
+
{
|
252
|
+
"content" => [{
|
253
|
+
"type" => "text",
|
254
|
+
"text" => "Yard server stopped successfully"
|
255
|
+
}]
|
256
|
+
}
|
257
|
+
else
|
258
|
+
{
|
259
|
+
"content" => [{
|
260
|
+
"type" => "text",
|
261
|
+
"text" => "Yard server is not running"
|
262
|
+
}]
|
263
|
+
}
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def get_yard_server_status
|
268
|
+
if OpenGemdocs::Yard.server_running?
|
269
|
+
pids = OpenGemdocs::Yard.find_yard_pids
|
270
|
+
yard_dir = OpenGemdocs::Yard.yard_server_directory
|
271
|
+
current_dir = Dir.pwd
|
272
|
+
|
273
|
+
status = "Yard server is running (PID: #{pids.join(", ")}) on port 8808"
|
274
|
+
status += "\nServing from: #{yard_dir}" if yard_dir
|
275
|
+
|
276
|
+
if yard_dir && yard_dir != current_dir
|
277
|
+
status += "\nCurrent directory: #{current_dir}"
|
278
|
+
status += "\n\nNote: The server is serving gems from a different directory."
|
279
|
+
end
|
280
|
+
|
281
|
+
{
|
282
|
+
"content" => [{
|
283
|
+
"type" => "text",
|
284
|
+
"text" => status
|
285
|
+
}]
|
286
|
+
}
|
287
|
+
else
|
288
|
+
{
|
289
|
+
"content" => [{
|
290
|
+
"type" => "text",
|
291
|
+
"text" => "Yard server is not running"
|
292
|
+
}]
|
293
|
+
}
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def get_gem_documentation_url(gem_name, class_name = nil)
|
298
|
+
# Ensure server is running
|
299
|
+
unless OpenGemdocs::Yard.server_running?
|
300
|
+
OpenGemdocs::Yard.start_yard_server
|
301
|
+
sleep 2
|
302
|
+
end
|
303
|
+
|
304
|
+
url = "http://localhost:8808/docs/#{gem_name}"
|
305
|
+
url += "/#{class_name.gsub("::", "/")}" if class_name
|
306
|
+
|
307
|
+
path_hint = class_name || "the gem overview"
|
308
|
+
|
309
|
+
{
|
310
|
+
"content" => [{
|
311
|
+
"type" => "text",
|
312
|
+
"text" => "Documentation URL: #{url}\n\n" \
|
313
|
+
"**Tip:** Instead of fetching this URL directly, use the `fetch_gem_docs` tool with:\n" \
|
314
|
+
"- gem_name: \"#{gem_name}\"\n" \
|
315
|
+
"- path: \"#{class_name}\" (if you want specific class documentation)\n\n" \
|
316
|
+
"This will give you structured documentation for #{path_hint}."
|
317
|
+
}]
|
318
|
+
}
|
319
|
+
end
|
320
|
+
|
321
|
+
def fetch_gem_docs(gem_name, path = nil)
|
322
|
+
# Use the YardJsonFormatter to get structured documentation
|
323
|
+
result = OpenGemdocs::YardJsonFormatter.format_gem_docs(gem_name, path)
|
324
|
+
|
325
|
+
if result[:error]
|
326
|
+
{
|
327
|
+
"content" => [{
|
328
|
+
"type" => "text",
|
329
|
+
"text" => result[:error]
|
330
|
+
}],
|
331
|
+
"isError" => true
|
332
|
+
}
|
333
|
+
else
|
334
|
+
# Format the JSON result into readable text
|
335
|
+
formatted_text = format_json_docs(result, gem_name, path)
|
336
|
+
|
337
|
+
{
|
338
|
+
"content" => [{
|
339
|
+
"type" => "text",
|
340
|
+
"text" => formatted_text
|
341
|
+
}]
|
342
|
+
}
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def format_json_docs(data, gem_name, path)
|
347
|
+
lines = []
|
348
|
+
|
349
|
+
if path
|
350
|
+
# Specific object documentation
|
351
|
+
obj = data
|
352
|
+
lines << "# #{obj[:path]}"
|
353
|
+
lines << ""
|
354
|
+
lines << "**Type:** #{obj[:type]}"
|
355
|
+
lines << "**Namespace:** #{obj[:namespace]}" if obj[:namespace]
|
356
|
+
lines << ""
|
357
|
+
|
358
|
+
if obj[:docstring]
|
359
|
+
lines << "## Description"
|
360
|
+
lines << obj[:docstring]
|
361
|
+
lines << ""
|
362
|
+
end
|
363
|
+
|
364
|
+
if obj[:superclass]
|
365
|
+
lines << "**Inherits from:** #{obj[:superclass]}"
|
366
|
+
lines << ""
|
367
|
+
end
|
368
|
+
|
369
|
+
if obj[:includes] && obj[:includes].any?
|
370
|
+
lines << "**Includes:** #{obj[:includes].join(", ")}"
|
371
|
+
lines << ""
|
372
|
+
end
|
373
|
+
|
374
|
+
if obj[:parameters] && obj[:parameters].any?
|
375
|
+
lines << "## Parameters"
|
376
|
+
obj[:parameters].each do |param|
|
377
|
+
default = param[:default] ? " = #{param[:default]}" : ""
|
378
|
+
lines << "- `#{param[:name]}#{default}`"
|
379
|
+
end
|
380
|
+
lines << ""
|
381
|
+
end
|
382
|
+
|
383
|
+
if obj[:methods] && obj[:methods].any?
|
384
|
+
lines << "## Methods"
|
385
|
+
|
386
|
+
# Group methods by visibility
|
387
|
+
%w[public protected private].each do |visibility|
|
388
|
+
visible_methods = obj[:methods].select { |m| m[:visibility] == visibility }
|
389
|
+
next if visible_methods.empty?
|
390
|
+
|
391
|
+
lines << ""
|
392
|
+
lines << "### #{visibility.capitalize} Methods"
|
393
|
+
visible_methods.each do |method|
|
394
|
+
return_info = method[:return_type] ? " → #{method[:return_type].join(", ")}" : ""
|
395
|
+
lines << "- `#{method[:signature]}`#{return_info}"
|
396
|
+
lines << " #{method[:docstring]}" if method[:docstring]
|
397
|
+
end
|
398
|
+
end
|
399
|
+
lines << ""
|
400
|
+
end
|
401
|
+
|
402
|
+
if obj[:attributes] && obj[:attributes].any?
|
403
|
+
lines << "## Attributes"
|
404
|
+
obj[:attributes].each do |attr|
|
405
|
+
access = []
|
406
|
+
access << "read" if attr[:read]
|
407
|
+
access << "write" if attr[:write]
|
408
|
+
lines << "- `#{attr[:name]}` (#{access.join("/")})"
|
409
|
+
lines << " #{attr[:docstring]}" if attr[:docstring] && !attr[:docstring].empty?
|
410
|
+
end
|
411
|
+
lines << ""
|
412
|
+
end
|
413
|
+
|
414
|
+
if obj[:tags] && obj[:tags].any?
|
415
|
+
examples = obj[:tags].select { |t| t[:tag_name] == "example" }
|
416
|
+
if examples.any?
|
417
|
+
lines << "## Examples"
|
418
|
+
examples.each do |example|
|
419
|
+
lines << "```ruby"
|
420
|
+
lines << example[:text]
|
421
|
+
lines << "```"
|
422
|
+
end
|
423
|
+
lines << ""
|
424
|
+
end
|
425
|
+
end
|
426
|
+
else
|
427
|
+
# Gem overview documentation
|
428
|
+
lines << "# #{gem_name}"
|
429
|
+
|
430
|
+
if data[:summary]
|
431
|
+
lines << ""
|
432
|
+
lines << "**Version:** #{data[:summary][:version]}" if data[:summary][:version]
|
433
|
+
lines << "**Homepage:** #{data[:summary][:homepage]}" if data[:summary][:homepage]
|
434
|
+
lines << ""
|
435
|
+
lines << data[:summary][:description] if data[:summary][:description]
|
436
|
+
lines << ""
|
437
|
+
end
|
438
|
+
|
439
|
+
if data[:namespaces] && data[:namespaces].any?
|
440
|
+
lines << "## Top-level Namespaces"
|
441
|
+
data[:namespaces].each do |ns|
|
442
|
+
lines << "- `#{ns[:path]}` (#{ns[:type]})"
|
443
|
+
end
|
444
|
+
lines << ""
|
445
|
+
end
|
446
|
+
|
447
|
+
if data[:classes] && data[:classes].any?
|
448
|
+
lines << "## Classes"
|
449
|
+
data[:classes].each do |cls|
|
450
|
+
super_info = cls[:superclass] ? " < #{cls[:superclass]}" : ""
|
451
|
+
lines << "- `#{cls[:path]}#{super_info}` (#{cls[:methods_count]} methods)"
|
452
|
+
lines << " #{cls[:docstring][0..100]}..." if cls[:docstring]
|
453
|
+
end
|
454
|
+
lines << ""
|
455
|
+
end
|
456
|
+
|
457
|
+
if data[:modules] && data[:modules].any?
|
458
|
+
lines << "## Modules"
|
459
|
+
data[:modules].each do |mod|
|
460
|
+
lines << "- `#{mod[:path]}` (#{mod[:methods_count]} methods)"
|
461
|
+
lines << " #{mod[:docstring][0..100]}..." if mod[:docstring]
|
462
|
+
end
|
463
|
+
lines << ""
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
lines.join("\n")
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
data/lib/open_gemdocs/version.rb
CHANGED
data/lib/open_gemdocs/yard.rb
CHANGED
@@ -2,24 +2,24 @@
|
|
2
2
|
|
3
3
|
module OpenGemdocs
|
4
4
|
module Yard
|
5
|
-
|
6
|
-
|
5
|
+
module_function
|
6
|
+
|
7
|
+
SERVER_COMMAND = "yard server --daemon"
|
7
8
|
|
8
9
|
def browse_gem(gem_name)
|
9
10
|
if server_running?
|
10
11
|
puts "Yard server is already running. Opening browser..."
|
11
|
-
system("open http://localhost:8808/docs/#{gem_name}")
|
12
12
|
else
|
13
13
|
puts "Starting Yard server in the background..."
|
14
14
|
start_yard_server
|
15
15
|
sleep 2 # Give the server some time to start
|
16
|
-
system("open http://localhost:8808/docs/#{gem_name}")
|
17
16
|
end
|
17
|
+
system("open http://localhost:8808/docs/#{gem_name}")
|
18
18
|
puts " When you're done, remember to stop the server with `open-gem-docs --stop`"
|
19
19
|
end
|
20
20
|
|
21
21
|
def start_yard_server
|
22
|
-
if File.exist?(
|
22
|
+
if File.exist?("Gemfile.lock")
|
23
23
|
`#{SERVER_COMMAND} --gemfile`
|
24
24
|
else
|
25
25
|
`#{SERVER_COMMAND} --gems`
|
@@ -33,11 +33,25 @@ module OpenGemdocs
|
|
33
33
|
`lsof -i TCP:8808 | grep -E "^ruby.*:8808"`.strip.split("\n").map { |line| line.split(/\s+/)[1] }
|
34
34
|
end
|
35
35
|
|
36
|
+
def yard_server_directory
|
37
|
+
pids = find_yard_pids
|
38
|
+
return nil if pids.empty?
|
39
|
+
|
40
|
+
# Get the working directory of the first Yard process
|
41
|
+
pid = pids.first
|
42
|
+
cwd_line = `lsof -p #{pid} 2>/dev/null | grep cwd`.strip
|
43
|
+
return nil if cwd_line.empty?
|
44
|
+
|
45
|
+
# Extract the directory path from lsof output
|
46
|
+
parts = cwd_line.split(/\s+/)
|
47
|
+
parts.last if parts.length >= 9
|
48
|
+
end
|
49
|
+
|
36
50
|
def stop_server
|
37
51
|
yard_pids = find_yard_pids
|
38
52
|
if yard_pids.any?
|
39
|
-
puts "Stopping Yard server processes: #{yard_pids.join(
|
40
|
-
`kill #{yard_pids.join(
|
53
|
+
puts "Stopping Yard server processes: #{yard_pids.join(", ")}"
|
54
|
+
`kill #{yard_pids.join(" ")}`
|
41
55
|
puts "Yard server processes stopped."
|
42
56
|
else
|
43
57
|
puts "No Yard server processes found to stop"
|