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
151 changes: 2 additions & 149 deletions exe/rdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,153 +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.
Use `query --schema` to describe the queryable schema (labels,
relationships, properties) without indexing the workspace.
console Open an interactive session with a populated graph for the current workspace
mcp [PATH] Run the MCP server for AI assistants (workspace defaults to the current dir)
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. Each branch
# performs its action and reports whether it handled the invocation, so we exit exactly once here
# rather than from inside every branch.
handled =
case ARGV.first
when "--version", "version"
require "rubydex/version"
puts "v#{Rubydex::VERSION}"
true
when nil, "-h", "--help", "help"
puts USAGE
true
else
false
end

exit if handled

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
graph.load_config
with_timer(progress_io, "Indexing workspace...") { graph.index_workspace }
with_timer(progress_io, "Resolving graph...") { graph.resolve }
graph
end

require "rubydex"

# Resolve the command into an operation on a populated graph. Each command parses its own options
# and does any graph-independent work here; a command that needs no graph (like `query --schema`)
# handles itself and exits. Whatever falls through returns a lambda that runs against the graph.
operation =
case command
when "query"
schema = false
format = "table"
OptionParser.new do |parser|
parser.banner = "Usage: rdx query <CYPHER> [options]"
parser.on("--schema", "Describe the queryable schema instead of running a query") { schema = true }
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!

query = ARGV.shift

if schema
warn("warning: ignoring query argument because `--schema` was given") if query
# The schema is static, so describe it without building a graph, then exit.
print(Rubydex::Query.schema(format))
exit
end

abort_with_usage("`query` requires a Cypher query argument (or pass `--schema`)") if query.nil? || query.empty?

# Parse the query up front so a malformed query fails fast, before the expensive indexing below.
parsed = begin
Rubydex::Query.parse(query)
rescue ArgumentError => e
abort(e.message)
end

lambda do |graph|
print(parsed.render(graph, format))
rescue ArgumentError => e
abort(e.message)
end
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!

lambda do |graph|
require "irb"
IRB.setup(nil)
IRB.conf[:IRB_NAME] = "rubydex"
IRB::Irb.new(IRB::WorkSpace.new(binding)).run(IRB.conf)
rescue LoadError
abort("Interactive mode requires `irb` to be in the bundle")
end
when "mcp"
parser = OptionParser.new do |p|
p.banner = "Usage: rdx mcp [PATH]"
p.on("-h", "--help", "Show this help") do
puts p
exit
end
end
begin
parser.parse!
rescue OptionParser::ParseError => e
abort_with_usage(e.message)
end

path = ARGV.shift || Dir.pwd
abort_with_usage("unexpected argument: #{ARGV.first}") unless ARGV.empty?

# The MCP server manages its own graph lifecycle for the given workspace, so it runs here
# instead of falling through to the shared graph build below.
require "rubydex/mcp_server"
Rubydex::MCPServer.run(path)
exit
else
abort_with_usage("unknown command: #{command}")
end

# Everything that reaches here operates on a populated graph. Progress goes to stderr so stdout
# carries only the command's output (e.g. for piping a query's JSON).
graph = build_graph($stderr)
operation.call(graph)
Rubydex::CLI.start
Loading
Loading