Module: Esp::Introspection

Defined in:
lib/esp/introspection.rb

Overview

Builds a structured snapshot of the CLI command tree and the core library modules. Two consumers:

  • ‘esp docs build` renders this into markdown under docs/reference/.

  • ‘esp docs introspect` emits it as JSON so AI tools, ESPresso, and the MCP server can discover capabilities at runtime.

The introspection is read-only and cheap — it just walks Thor’s metadata and reads each lib/esp/,mw/*.rb header comment block.

Constant Summary collapse

HIDDEN_COMMANDS =
%w[help tree].freeze

Class Method Summary collapse

Class Method Details

.command_treeObject



15
16
17
# File 'lib/esp/introspection.rb', line 15

def command_tree
  walk(Esp::CLI, [])
end

.module_docsObject

Modules documented in docs/reference/api/. Walks both the engine-agnostic shell (lib/esp/*.rb → Esp::Foo) and the active Mw plugin (lib/esp/mw/*.rb → Esp::Mw::Foo). Skips cli.rb (the command reference covers it) and version.rb (trivial). The cli/ and providers/ subdirs are intentionally not walked here — they are subordinate to their parent modules and don’t warrant their own docs pages.



52
53
54
55
56
# File 'lib/esp/introspection.rb', line 52

def module_docs
  shell = Dir.glob(File.join(Esp::ROOT, 'lib/esp/*.rb'))
  plugin = Dir.glob(File.join(Esp::ROOT, 'lib/esp/mw/*.rb'))
  entries_for(shell, namespace: 'Esp') + entries_for(plugin, namespace: 'Esp::Mw')
end

.parse_header_comment(path) ⇒ Object

Comment block immediately preceding the primary class/module declaration in ‘path`. Strips leading `# ` from each line.



60
61
62
63
64
65
66
# File 'lib/esp/introspection.rb', line 60

def parse_header_comment(path)
  lines = File.readlines(path, chomp: true)
  target_idx = primary_declaration_index(lines)
  return nil unless target_idx

  collect_comment_above(lines, target_idx)
end

.walk(thor_class, path) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/esp/introspection.rb', line 19

def walk(thor_class, path)
  reverse_map = build_reverse_map(thor_class)
  subcommand_names = Array(thor_class.subcommands)

  commands = thor_class.commands.filter_map do |name, cmd|
    next if HIDDEN_COMMANDS.include?(name) || subcommand_names.include?(name)

    display = reverse_map[name.to_s] || name.to_s
    command_entry(display, path, cmd, thor_class)
  end

  subcommands = subcommand_names.map do |sub_name|
    sub_class = thor_class.subcommand_classes[sub_name]
    sub_walk = walk(sub_class, path + [sub_name])
    {
      name: sub_name,
      path: path + [sub_name],
      description: thor_class.commands[sub_name]&.description,
      commands: sub_walk[:commands],
      subcommands: sub_walk[:subcommands]
    }
  end

  { commands: commands, subcommands: subcommands }
end