Module: Space::Core::CLI::Help
- Defined in:
- lib/space_core/cli/help.rb
Overview
Colourful replacement for dry-cli's plain Usage listing — the "global
help" shown by space, architect, and every bare namespace (space repo,
worktree, ...). Per-command help still flows through dry-cli's Banner,
whose content we like; only the listing is reskinned.
We reopen Dry::CLI::Usage.call (below) to delegate here, so BOTH the
intercepted top-level help and dry-cli's own bare-namespace path get the
same treatment from one place. Colour follows CLI.help_pastel, set once per
invocation from the output stream's tty-ness and --color, so piped and test
output stay plain. The src binary never loads space_core, so its own plain
Usage is untouched.
Constant Summary collapse
- TAGLINES =
One tagline per binary — the root header is served for both
spaceandarchitectfrom here, so we pick by $PROGRAM_NAME. The default covers the test runner and any other invocation where the binary can't be identified. { "space" => "date-prefixed workspaces; repos provisioned on fibers at copy-on-write speed", "architect" => "the Architect Loop: structured judgment plus a fleet of headless AI builders" }.freeze
- DEFAULT_TAGLINE =
TAGLINES.fetch("space")
Class Method Summary collapse
- .arguments(command) ⇒ Object
- .banner(node) ⇒ Object
- .call(result, pastel: CLI.help_pastel) ⇒ Object
- .current_space_project ⇒ Object
- .description(node) ⇒ Object
- .footer(result, pastel) ⇒ Object
-
.grouped_listing(result) ⇒ Object
Group non-hidden children into an ordered list of [header_or_nil, rows].
-
.header(result, pastel) ⇒ Object
The richer header only makes sense at the true root (
space/architect), not on every sub-namespace listing. - .label(result, name, node) ⇒ Object
-
.loop_status_block(result, pastel) ⇒ Object
Opportunistic, TOTAL loop-status embed: only the architect binary's ROOT listing gets it.
- .phase_of(node) ⇒ Object
-
.program_prefix(result) ⇒ Object
"space" at the root, "space repo" inside a namespace.
- .row(result, name, node) ⇒ Object
-
.tagline ⇒ Object
The tagline for the binary in hand:
spaceandarchitecteach get their own, keyed off $PROGRAM_NAME (the same signal program_prefix reads). -
.trailing_group_label ⇒ Object
Header for the trailing group of children that declare no phase.
- .trailing_group_label=(label) ⇒ Object
Class Method Details
.arguments(command) ⇒ Object
162 163 164 165 166 167 168 |
# File 'lib/space_core/cli/help.rb', line 162 def arguments(command) return "" unless command.respond_to?(:required_arguments) names = command.required_arguments.map { |arg| arg.name.to_s.upcase } names += command.optional_arguments.map { |arg| "[#{arg.name.to_s.upcase}]" } names.empty? ? "" : " #{names.join(" ")}" end |
.banner(node) ⇒ Object
152 153 154 155 156 157 158 159 160 |
# File 'lib/space_core/cli/help.rb', line 152 def (node) if node.command && node.leaf? && node.children? " [ARGUMENT|SUBCOMMAND]" elsif node.leaf? arguments(node.command) else " [SUBCOMMAND]" end end |
.call(result, pastel: CLI.help_pastel) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/space_core/cli/help.rb', line 41 def call(result, pastel: CLI.help_pastel) groups = grouped_listing(result) width = groups.flat_map { |_h, rows| rows.map { |label, _| label.length } }.max || 0 body = groups.flat_map do |group_header, rows| lines = rows.map do |label, description| painted = pastel.cyan(label.ljust(width)) description ? " #{painted} #{pastel.dim("# #{description}")}" : " #{painted}" end group_header ? ["", pastel.bold(group_header), *lines] : lines end [header(result, pastel), pastel.bold("Commands:"), *body, (result, pastel), loop_status_block(result, pastel)].compact.join("\n") end |
.current_space_project ⇒ Object
141 142 143 144 145 146 |
# File 'lib/space_core/cli/help.rb', line 141 def current_space_project store = Space::Core::SpaceStore.new(config: Space::Core::Config.load, state: Space::Core::State.load) store.current.value_or(nil)&.data&.[]("project") rescue StandardError nil end |
.description(node) ⇒ Object
170 171 172 173 174 |
# File 'lib/space_core/cli/help.rb', line 170 def description(node) return unless node.leaf? && node.command.respond_to?(:description) node.command.description end |
.footer(result, pastel) ⇒ Object
72 73 74 |
# File 'lib/space_core/cli/help.rb', line 72 def (result, pastel) "\n#{pastel.dim("Run `#{program_prefix(result)} <command> --help` for details on a command.")}" end |
.grouped_listing(result) ⇒ Object
Group non-hidden children into an ordered list of [header_or_nil, rows].
When no child declares a phase (e.g. the space binary), returns a single
unlabelled group sorted by name — byte-identical to the pre-phase listing.
Otherwise groups declared commands under their phase label (groups, and
members within a group, ordered by the declared order), with undeclared
children (namespaces) trailing in the default group.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/space_core/cli/help.rb', line 92 def grouped_listing(result) decorated = result.children.filter_map do |name, node| [name, node, phase_of(node)] unless node.hidden end if decorated.all? { |_name, _node, phase| phase.nil? } rows = decorated.sort_by { |name, _node, _phase| name } .map { |name, node, _phase| row(result, name, node) } return [[nil, rows]] end phased, unphased = decorated.partition { |_name, _node, phase| phase } groups = phased.group_by { |_name, _node, phase| phase.last } listing = groups.sort_by { |_label, members| members.map { |_n, _node, phase| phase.first }.min } .map do |label, members| rows = members.sort_by { |_n, _node, phase| phase.first } .map { |name, node, _phase| row(result, name, node) } [label, rows] end listing << [trailing_group_label, unphased.map { |name, node, _phase| row(result, name, node) }] \ unless unphased.empty? listing end |
.header(result, pastel) ⇒ Object
The richer header only makes sense at the true root (space / architect),
not on every sub-namespace listing.
59 60 61 62 63 64 |
# File 'lib/space_core/cli/help.rb', line 59 def header(result, pastel) return unless result.names.empty? "#{pastel.bold.cyan("space-architect")} #{pastel.dim(Space::Core::VERSION)} " \ "#{pastel.dim("— #{tagline}")}\n" end |
.label(result, name, node) ⇒ Object
148 149 150 |
# File 'lib/space_core/cli/help.rb', line 148 def label(result, name, node) "#{program_prefix(result)} #{name}#{(node)}" end |
.loop_status_block(result, pastel) ⇒ Object
Opportunistic, TOTAL loop-status embed: only the architect binary's ROOT
listing gets it. Resolves the current space best-effort and renders the
shared block from that space's own space.yaml project data (no
space_architect dependency). ANY failure — no space, store failure,
malformed yaml, no project block — omits the block; help always renders.
129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/space_core/cli/help.rb', line 129 def loop_status_block(result, pastel) return unless result.names.empty? && File.basename($PROGRAM_NAME) == "architect" project = current_space_project lines = project && LoopStatus.lines(project) return if lines.nil? || lines.empty? ["", pastel.bold("Loop:"), *lines.map { |l| " #{pastel.dim(l)}" }].join("\n") rescue StandardError nil end |
.phase_of(node) ⇒ Object
120 121 122 |
# File 'lib/space_core/cli/help.rb', line 120 def phase_of(node) node.command.respond_to?(:phase) ? node.command.phase : nil end |
.program_prefix(result) ⇒ Object
"space" at the root, "space repo" inside a namespace. The space/architect
binaries inject their name into ARGV, so $PROGRAM_NAME and the leading
namespace segment can collide ("space space ..."); drop the duplicate.
79 80 81 82 83 84 |
# File 'lib/space_core/cli/help.rb', line 79 def program_prefix(result) prog = File.basename($PROGRAM_NAME) names = result.names.dup names.shift if names.first == prog [prog, *names].join(" ") end |
.row(result, name, node) ⇒ Object
116 117 118 |
# File 'lib/space_core/cli/help.rb', line 116 def row(result, name, node) [label(result, name, node), description(node)] end |
.tagline ⇒ Object
The tagline for the binary in hand: space and architect each get their
own, keyed off $PROGRAM_NAME (the same signal program_prefix reads).
68 69 70 |
# File 'lib/space_core/cli/help.rb', line 68 def tagline TAGLINES.fetch(File.basename($PROGRAM_NAME), DEFAULT_TAGLINE) end |
.trailing_group_label ⇒ Object
Header for the trailing group of children that declare no phase. Left nil
for the space binary (whose commands are all phase-less → one ungrouped
listing); the architect binary sets it so its namespaces list under a
"Groups" header. Keeps phase vocabulary out of this generic renderer.
35 |
# File 'lib/space_core/cli/help.rb', line 35 def self.trailing_group_label = @trailing_group_label |
.trailing_group_label=(label) ⇒ Object
37 38 39 |
# File 'lib/space_core/cli/help.rb', line 37 def self.trailing_group_label=(label) @trailing_group_label = label end |