Module: Wiq::Introspection

Defined in:
lib/wiq/introspection.rb

Overview

Walks Thor’s command registry and produces a stable JSON shape for agents to introspect the CLI surface in one call.

Schema (nested):

{
  "name": "wiq",
  "version": "0.1.0",
  "global_options": [ ... ],
  "top_level_commands": [ ... ],
  "groups": [ { "name": "...", "description": "...", "commands": [ ... ] } ]
}

Constant Summary collapse

INTERNAL_COMMANDS =

Thor auto-generates these on every Thor::Group subclass; they’re not useful for agents and would just bloat the dump.

%w[help tree].freeze

Class Method Summary collapse

Class Method Details

.dump_command(cmd, display_name:) ⇒ Object



52
53
54
55
56
57
58
59
60
# File 'lib/wiq/introspection.rb', line 52

def dump_command(cmd, display_name:)
  {
    "name" => display_name,
    "description" => cmd.description,
    "long_description" => cmd.long_description,
    "usage" => cmd.usage,
    "options" => dump_options(cmd.options)
  }
end

.dump_commands(commands, klass:) ⇒ Object



44
45
46
47
48
49
50
# File 'lib/wiq/introspection.rb', line 44

def dump_commands(commands, klass:)
  aliases = method_to_alias(klass)
  commands
    .reject { |k, _| INTERNAL_COMMANDS.include?(k) || subcommand_name?(klass, k) }
    .map { |k, cmd| dump_command(cmd, display_name: aliases[k] || k) }
    .sort_by { |row| row["name"] }
end

.dump_group(name, klass, root) ⇒ Object



36
37
38
39
40
41
42
# File 'lib/wiq/introspection.rb', line 36

def dump_group(name, klass, root)
  {
    "name" => name,
    "description" => root.commands[name]&.description,
    "commands" => dump_commands(klass.commands, klass: klass)
  }
end

.dump_options(options) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/wiq/introspection.rb', line 81

def dump_options(options)
  options.sort.map do |name, opt|
    row = {
      "name" => name.to_s,
      "type" => opt.type.to_s,
      "required" => opt.required?,
      "description" => opt.description
    }
    row["default"] = opt.default unless opt.default.nil?
    row["enum"] = opt.enum if opt.enum
    row
  end
end

.dump_tree(root: Wiq::CLI) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
# File 'lib/wiq/introspection.rb', line 24

def dump_tree(root: Wiq::CLI)
  {
    "name" => "wiq",
    "version" => Wiq::VERSION,
    "global_options" => dump_options(Wiq::Commands::Base.class_options),
    "top_level_commands" => dump_commands(root.commands, klass: root),
    "groups" => root.subcommand_classes.sort.map do |name, klass|
      dump_group(name, klass, root)
    end
  }
end

.method_to_alias(klass) ⇒ Object

Thor’s ‘map` declares user-facing command aliases. `map “run” => :run_report` means when a user types `wiq reports run`, Thor dispatches to method :run_report. Internally Thor stores the command under “run_report” — the method name — but agents need to see “run” (the name they type). Build a reverse map: method_name (String) => alias_name (String).

Flag-style aliases (–version, -v) are also registered via ‘map` for convenience but they’re not command names — skip them.



70
71
72
73
74
75
76
77
78
79
# File 'lib/wiq/introspection.rb', line 70

def method_to_alias(klass)
  return {} unless klass.respond_to?(:map)

  klass.map.each_with_object({}) do |(alias_name, method_name), h|
    alias_str = alias_name.to_s
    next if alias_str.start_with?("-")

    h[method_name.to_s] = alias_str
  end
end

.subcommand_name?(klass, command_name) ⇒ Boolean

When a class registers a ‘subcommand “foo”, FooClass`, Thor also adds a placeholder command named “foo” to the parent’s commands hash. Skip those when listing the parent’s own commands — they’re rendered as groups, not commands.

Returns:

  • (Boolean)


99
100
101
102
103
# File 'lib/wiq/introspection.rb', line 99

def subcommand_name?(klass, command_name)
  return false unless klass.respond_to?(:subcommand_classes)

  klass.subcommand_classes.key?(command_name)
end