Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 2 additions & 107 deletions exe/rdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,111 +3,6 @@

$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))

require "optparse"
require "rubydex/cli"

USAGE = <<~TEXT
Usage: rdx <command> [options]

Commands:
query <CYPHER> Run a Cypher query against the workspace graph and print the result
schema Describe the queryable Cypher schema (labels, relationships, properties)
console Open an interactive session with a populated graph for the current workspace
help Show this help message

Run `rdx <command> --help` for command-specific options.
TEXT

def abort_with_usage(message)
warn(message)
warn("")
warn(USAGE)
exit(1)
end

# Top-level --version / --help / bare invocation, handled before command dispatch.
case ARGV.first
when "--version", "version"
require "rubydex/version"
puts "v#{Rubydex::VERSION}"
exit
when nil, "-h", "--help", "help"
puts USAGE
exit
end

command = ARGV.shift

def with_timer(io, message)
io.print(message)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
yield
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
io.puts(" finished in #{duration.round(2)}ms")
end

# Builds the workspace graph, sending progress messages to `progress_io`.
def build_graph(progress_io)
graph = Rubydex::Graph.new
with_timer(progress_io, "Indexing workspace...") { graph.index_workspace }
with_timer(progress_io, "Resolving graph...") { graph.resolve }
graph
end

# Parses `--format`/`--help` for a command and returns the chosen format.
def parse_format(usage)
format = "table"
OptionParser.new do |parser|
parser.banner = usage
parser.on("--format FORMAT", ["table", "json"], "Output format (table or json)") { |value| format = value }
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
end.parse!
format
end

case command
when "query"
format = parse_format("Usage: rdx query <CYPHER> [options]")
query = ARGV.shift
abort_with_usage("`query` requires a Cypher query argument") if query.nil? || query.empty?

require "rubydex"
# Progress goes to stderr so stdout carries only the query result (e.g. for piping JSON).
graph = build_graph($stderr)
begin
print(graph.query(query, format))
rescue ArgumentError => e
abort(e.message)
end
when "schema"
format = parse_format("Usage: rdx schema [options]")

require "rubydex"
# The schema is static, so describe it without indexing the workspace.
print(Rubydex::Graph.cypher_schema(format))
when "console"
OptionParser.new do |parser|
parser.banner = "Usage: rdx console"
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
end.parse!

require "rubydex"
graph = build_graph($stdout)

begin
require "irb"
IRB.setup(nil)
IRB.conf[:IRB_NAME] = "rubydex"
workspace = IRB::WorkSpace.new(binding)
IRB::Irb.new(workspace).run(IRB.conf)
rescue LoadError
abort("Interactive mode requires `irb` to be in the bundle")
end
else
abort_with_usage("unknown command: #{command}")
end
Rubydex::CLI.start
226 changes: 226 additions & 0 deletions lib/rubydex/cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# frozen_string_literal: true

require "optparse"

module Rubydex
# Command-line entry point for the `rdx` executable. Parses the top-level command, dispatches to
# the matching subcommand handler and keeps `exe/rdx` itself a thin shim.
#
# Heavyweight requires (`rubydex`, `rubydex/server`) are deferred into the handlers so that paths
# which don't need the native extension (e.g. `--help`, or talking to a resident server) stay
# cheap to start.
class CLI
USAGE = <<~TEXT
Usage: rdx <command> [options]

Commands:
query <CYPHER> Run a Cypher query against the workspace graph and print the result
schema Describe the queryable Cypher schema (labels, relationships, properties)
console Open an interactive session with a populated graph for the current workspace
server <action> Manage the resident server (start, stop, restart, status)
help Show this help message

Run `rdx <command> --help` for command-specific options.
TEXT

SERVER_USAGE = <<~TEXT
Usage: rdx server <action> [options]

Actions:
start Start the server for this workspace
stop Stop the running server for this workspace
restart Restart the server for this workspace
status Print the status of the server for this workspace
TEXT

class << self
# Convenience entry point used by `exe/rdx`.
#: (?Array[String] argv) -> void
def start(argv = ARGV)
new(argv).run
end
end

#: (Array[String] argv) -> void
def initialize(argv = ARGV)
@argv = argv
end

#: -> void
def run
# Top-level --version / --help / bare invocation, handled before command dispatch.
case @argv.first
when "--version", "version"
require "rubydex/version"
puts "v#{Rubydex::VERSION}"
exit
when nil, "-h", "--help", "help"
puts USAGE
exit
end

dispatch(@argv.shift)
end

private

#: (String? command) -> void
def dispatch(command)
case command
when "query" then run_query
when "schema" then run_schema
when "console" then run_console
when "server" then run_server
else abort_with_usage("unknown command: #{command}")
end
end

#: -> void
def run_query
options = parse_query_options
query = @argv.shift
abort_with_usage("`query` requires a Cypher query argument") if query.nil? || query.empty?

require "rubydex/server"

# Server mode is opt-in via `--server` and falls back to inline execution when it's disabled or
# unsupported on this platform.
use_server = options[:server] && !Rubydex::Server.disabled? && Rubydex::Server.supported?

if use_server
cache = Rubydex::Server::Cache.new(workspace_path: Dir.pwd)
exit(Rubydex::Server::Client.query(cache, { query: query, query_format: options[:format] }))
else
require "rubydex"
# Progress goes to stderr so stdout carries only the query result (e.g. for piping JSON).
graph = build_graph($stderr)
begin
print(graph.query(query, options[:format]))
rescue ArgumentError => e
abort(e.message)
end
end
end

#: -> void
def run_schema
format = parse_format("Usage: rdx schema [options]")

require "rubydex"
# The schema is static, so describe it without indexing the workspace.
print(Rubydex::Graph.cypher_schema(format))
end

#: -> void
def run_console
OptionParser.new do |parser|
parser.banner = "Usage: rdx console"
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
end.parse!(@argv)

require "rubydex"
graph = build_graph($stdout)

begin
require "irb"
IRB.setup(nil)
IRB.conf[:IRB_NAME] = "rubydex"
workspace = IRB::WorkSpace.new(binding)
IRB::Irb.new(workspace).run(IRB.conf)
rescue LoadError
abort("Interactive mode requires `irb` to be in the bundle")
end
end

#: -> void
def run_server
action = @argv.shift
detach = true
OptionParser.new do |parser|
parser.banner = SERVER_USAGE
parser.on("--no-detach", "Run the server in the foreground (for debugging / containers)") { detach = false }
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
end.parse!(@argv)

require "rubydex/server"

unless Rubydex::Server.supported?
abort("rdx server mode is not supported on this platform (requires fork + UNIX sockets)")
end

cache = Rubydex::Server::Cache.new(workspace_path: Dir.pwd)
status = case action
when "start" then Rubydex::Server::Commands.start(cache, detach: detach)
when "stop" then Rubydex::Server::Commands.stop(cache)
when "restart" then Rubydex::Server::Commands.restart(cache, detach: detach)
when "status" then Rubydex::Server::Commands.status(cache)
else abort_with_usage("unknown server action: #{action.inspect}", SERVER_USAGE)
end

exit(status)
end

# Parses options for the `query` command: output format plus whether to use the resident server.
#: -> Hash[Symbol, untyped]
def parse_query_options
options = { format: "table", server: false }
OptionParser.new do |parser|
parser.banner = "Usage: rdx query <CYPHER> [options]"
parser.on("--format FORMAT", ["table", "json"], "Output format (table or json)") { |value| options[:format] = value }
parser.on("--[no-]server", "Use the resident server (opt-in; off by default)") { |value| options[:server] = value }
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
end.parse!(@argv)
options
end

# Parses `--format`/`--help` for a command and returns the chosen format.
#: (String usage) -> String
def parse_format(usage)
format = "table"
OptionParser.new do |parser|
parser.banner = usage
parser.on("--format FORMAT", ["table", "json"], "Output format (table or json)") { |value| format = value }
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
end.parse!(@argv)
format
end

# Builds the workspace graph, sending progress messages to `progress_io`.
#: (IO progress_io) -> Rubydex::Graph
def build_graph(progress_io)
graph = Rubydex::Graph.new
with_timer(progress_io, "Indexing workspace...") { graph.index_workspace }
with_timer(progress_io, "Resolving graph...") { graph.resolve }
graph
end

#: (IO io, String message) { -> void } -> void
def with_timer(io, message)
io.print(message)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
yield
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
io.puts(" finished in #{duration.round(2)}ms")
end

#: (String message, ?String usage) -> void
def abort_with_usage(message, usage = USAGE)
warn(message)
warn("")
warn(usage)
exit(1)
end
end
end
Loading
Loading