ace-support-cli 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Cli
6
+ module Help
7
+ module Concise
8
+ def self.call(command, name)
9
+ [
10
+ header_line(command, name),
11
+ usage_line(command, name),
12
+ options_block(command),
13
+ examples_block(command, name),
14
+ footer_line(name)
15
+ ].compact.join("\n\n")
16
+ end
17
+
18
+ def self.header_line(command, name)
19
+ summary = first_line(command.respond_to?(:description) ? command.description : nil)
20
+ summary ? "#{name} - #{summary}" : name.to_s
21
+ end
22
+
23
+ def self.usage_line(command, name)
24
+ args = Banner.arguments_synopsis(command)
25
+ opts = Banner.options(command).any? ? " [OPTIONS]" : ""
26
+ "Usage: #{name}#{args}#{opts}"
27
+ end
28
+
29
+ def self.options_block(command)
30
+ lines = Banner.options(command).map { |option| format_option(option) }
31
+ lines << " --help, -h Show this help"
32
+ "Options:\n#{lines.join("\n")}"
33
+ end
34
+
35
+ def self.examples_block(command, name)
36
+ items = Banner.examples(command)
37
+ return nil if items.empty?
38
+
39
+ rendered = items.first(3).map do |item|
40
+ cleaned = item.to_s.sub(/\A#{Regexp.escape(name)}\s*/, "")
41
+ " $ #{name} #{cleaned}".rstrip
42
+ end
43
+ "Examples:\n#{rendered.join("\n")}"
44
+ end
45
+
46
+ def self.footer_line(name)
47
+ "Run '#{name} --help' for full details."
48
+ end
49
+
50
+ def self.format_option(option)
51
+ name = Banner.option_name(option).sub("=VALUE1,VALUE2,..", " VALUES").sub("=VALUE", " VALUE")
52
+ aliases = Banner.option_aliases(option)
53
+ name = "#{name}, #{aliases.join(", ")}" if aliases.any?
54
+ " --#{name}"
55
+ end
56
+
57
+ def self.first_line(text)
58
+ return nil if text.nil?
59
+
60
+ text.to_s.strip.split("\n").first&.strip
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Cli
6
+ module Help
7
+ module HelpCommand
8
+ def self.build(program_name:, version:, commands:, examples: nil)
9
+ Class.new(Command) do
10
+ @program_name = program_name
11
+ @version = version
12
+ @commands = commands
13
+ @examples = examples
14
+
15
+ class << self
16
+ attr_reader :program_name, :version, :commands, :examples
17
+ end
18
+
19
+ desc "Show top-level help"
20
+ argument :args, type: :array, required: false
21
+
22
+ def call(**_params)
23
+ puts self.class.render
24
+ 0
25
+ end
26
+
27
+ def self.render
28
+ sections = []
29
+ sections << "#{program_name} #{version}".strip
30
+ sections << render_commands
31
+ rendered_examples = render_examples
32
+ sections << rendered_examples if rendered_examples
33
+ sections << render_options
34
+ sections.join("\n\n")
35
+ end
36
+
37
+ def self.render_commands
38
+ lines = normalized_commands.map do |name, description|
39
+ "#{" #{name}".ljust(16)}# #{description}"
40
+ end
41
+ "Commands:\n#{lines.join("\n")}"
42
+ end
43
+
44
+ def self.render_examples
45
+ return nil if examples.nil? || examples.empty?
46
+
47
+ "Examples:\n#{examples.map { |item| " #{item}" }.join("\n")}"
48
+ end
49
+
50
+ def self.render_options
51
+ <<~OPTIONS.chomp
52
+ Options:
53
+ --help, -h # Print this help
54
+ --version # Print version
55
+ OPTIONS
56
+ end
57
+
58
+ def self.normalized_commands
59
+ commands.is_a?(Hash) ? commands.to_a : commands
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Cli
6
+ module Help
7
+ module TwoTierHelp
8
+ def self.concise?(args)
9
+ values = Array(args)
10
+ values.include?("-h") && !values.include?("--help")
11
+ end
12
+
13
+ def self.render(command, name, args:)
14
+ if concise?(args)
15
+ Concise.call(command, name)
16
+ else
17
+ Banner.call(command, name)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Cli
6
+ module Help
7
+ class Usage
8
+ COLUMN_WIDTH = 34
9
+
10
+ def initialize(registry, program_name: nil)
11
+ @registry = registry
12
+ @program_name = program_name
13
+ end
14
+
15
+ def render
16
+ commands = visible_commands
17
+ groups = command_groups
18
+ output = if groups && !groups.empty?
19
+ format_grouped(commands, groups)
20
+ else
21
+ format_flat(commands, header: "COMMANDS")
22
+ end
23
+
24
+ examples = help_examples
25
+ output += "\n\n#{format_examples(examples)}" if examples && !examples.empty?
26
+ output
27
+ end
28
+
29
+ def render_concise
30
+ output = format_flat(visible_commands, header: "Commands:")
31
+ output + "\n\nRun '#{resolved_program_name} --help' for more info. Each command has its own --help."
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :registry, :program_name
37
+
38
+ def visible_commands
39
+ all_commands.reject { |_name, value| value[:hidden] }
40
+ end
41
+
42
+ def all_commands
43
+ commands =
44
+ if registry.respond_to?(:commands)
45
+ convert_commands_hash(registry.commands)
46
+ elsif registry.is_a?(Hash)
47
+ convert_commands_hash(registry)
48
+ elsif registry.respond_to?(:to_h)
49
+ convert_commands_hash(registry.to_h)
50
+ elsif registry.respond_to?(:const_defined?) && registry.const_defined?(:REGISTERED_COMMANDS)
51
+ convert_registered_commands(registry.const_get(:REGISTERED_COMMANDS))
52
+ else
53
+ []
54
+ end
55
+
56
+ commands.sort_by { |name, _| name }
57
+ end
58
+
59
+ def convert_commands_hash(hash)
60
+ hash.map do |name, command|
61
+ [name.to_s, {description: first_line(description(command)), hidden: hidden?(command)}]
62
+ end
63
+ end
64
+
65
+ def convert_registered_commands(commands)
66
+ Array(commands).map do |entry|
67
+ if entry.is_a?(Array)
68
+ [entry[0].to_s, {description: first_line(entry[1]), hidden: false}]
69
+ else
70
+ [entry.to_s, {description: nil, hidden: false}]
71
+ end
72
+ end
73
+ end
74
+
75
+ def format_flat(commands, header:)
76
+ lines = commands.map do |name, meta|
77
+ banner = " #{name}"
78
+ details = meta[:description] ? " # #{meta[:description]}" : nil
79
+ justify(banner, details)
80
+ end
81
+
82
+ ([header] + lines).join("\n")
83
+ end
84
+
85
+ def format_grouped(commands, groups)
86
+ index = commands.to_h
87
+ grouped_names = groups.values.flatten.map(&:to_s)
88
+ output = ["COMMANDS"]
89
+
90
+ groups.each do |group_name, names|
91
+ lines = names.filter_map do |name|
92
+ meta = index[name.to_s]
93
+ next unless meta
94
+
95
+ banner = " #{name}"
96
+ details = meta[:description] ? " # #{meta[:description]}" : nil
97
+ justify(banner, details, indent: 2)
98
+ end
99
+ next if lines.empty?
100
+
101
+ output << ""
102
+ output << " #{group_name}"
103
+ output.concat(lines)
104
+ end
105
+
106
+ ungrouped = commands.filter_map do |name, meta|
107
+ next if grouped_names.include?(name)
108
+
109
+ banner = " #{name}"
110
+ details = meta[:description] ? " # #{meta[:description]}" : nil
111
+ justify(banner, details)
112
+ end
113
+
114
+ unless ungrouped.empty?
115
+ output << ""
116
+ output.concat(ungrouped)
117
+ end
118
+ output.join("\n")
119
+ end
120
+
121
+ def justify(banner, details, indent: 0)
122
+ return "#{" " * indent}#{banner}" if details.nil?
123
+
124
+ base = banner.ljust(COLUMN_WIDTH)
125
+ "#{" " * indent}#{base}#{details}"
126
+ end
127
+
128
+ def command_groups
129
+ return nil unless registry.respond_to?(:const_defined?)
130
+ return nil unless registry.const_defined?(:COMMAND_GROUPS)
131
+
132
+ registry.const_get(:COMMAND_GROUPS)
133
+ end
134
+
135
+ def help_examples
136
+ return nil unless registry.respond_to?(:const_defined?)
137
+ return nil unless registry.const_defined?(:HELP_EXAMPLES)
138
+
139
+ registry.const_get(:HELP_EXAMPLES)
140
+ end
141
+
142
+ def format_examples(examples)
143
+ lines = examples.map do |entry|
144
+ if entry.is_a?(Array)
145
+ desc, cmd = entry
146
+ " $ #{cmd} # #{desc}"
147
+ else
148
+ " #{entry}"
149
+ end
150
+ end
151
+ "EXAMPLES\n#{lines.join("\n")}"
152
+ end
153
+
154
+ def resolved_program_name
155
+ return program_name unless program_name.nil? || program_name.empty?
156
+ return registry.const_get(:PROGRAM_NAME) if registry.respond_to?(:const_defined?) && registry.const_defined?(:PROGRAM_NAME)
157
+
158
+ $PROGRAM_NAME.split("/").last
159
+ end
160
+
161
+ def description(command)
162
+ command.respond_to?(:description) ? command.description : nil
163
+ end
164
+
165
+ def hidden?(command)
166
+ command.respond_to?(:hidden) && command.hidden
167
+ end
168
+
169
+ def first_line(text)
170
+ return nil if text.nil?
171
+
172
+ text.to_s.strip.split("\n").first&.strip
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Cli
6
+ module Help
7
+ module VersionCommand
8
+ def self.build(gem_name:, version:)
9
+ Class.new(Command) do
10
+ @gem_name = gem_name
11
+ @version = version
12
+
13
+ class << self
14
+ attr_reader :gem_name, :version
15
+ end
16
+
17
+ desc "Show version information"
18
+
19
+ def call(**_params)
20
+ puts "#{self.class.gem_name} #{self.class.version}"
21
+ 0
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.module(gem_name:, version:)
27
+ Module.new do
28
+ define_method(:show_version) do
29
+ puts "#{gem_name} #{version.call}"
30
+ 0
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Cli
6
+ module Models
7
+ class Argument
8
+ VALID_TYPES = %i[string integer float boolean array].freeze
9
+
10
+ attr_reader :name, :type, :required, :desc
11
+
12
+ def initialize(name:, type: :string, required: true, desc: "")
13
+ @name = name.to_sym
14
+ @type = type.to_sym
15
+ @required = required
16
+ @desc = desc
17
+ validate!
18
+ end
19
+
20
+ private
21
+
22
+ def validate!
23
+ return if VALID_TYPES.include?(type)
24
+
25
+ raise ArgumentError, "Invalid argument type: #{type.inspect}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Cli
6
+ module Models
7
+ class Option
8
+ VALID_TYPES = %i[string integer float boolean array hash].freeze
9
+
10
+ attr_reader :name, :type, :default, :desc, :aliases, :values, :required, :repeat
11
+
12
+ def initialize(name:, type: :string, default: nil, desc: "", aliases: [], values: nil, required: false, repeat: false)
13
+ @name = name.to_sym
14
+ @type = type.to_sym
15
+ @default = default
16
+ @desc = desc
17
+ @aliases = normalize_aliases(Array(aliases))
18
+ @values = values
19
+ @required = required
20
+ @repeat = repeat
21
+ validate!
22
+ end
23
+
24
+ def long_switch
25
+ "--#{name.to_s.tr("_", "-")}"
26
+ end
27
+
28
+ private
29
+
30
+ def validate!
31
+ return if VALID_TYPES.include?(type)
32
+
33
+ raise ArgumentError, "Invalid option type: #{type.inspect}"
34
+ end
35
+
36
+ def normalize_aliases(aliases)
37
+ aliases.map do |entry|
38
+ alias_name = entry.to_s
39
+ if alias_name.start_with?("-")
40
+ alias_name
41
+ elsif alias_name.length == 1
42
+ "-#{alias_name}"
43
+ else
44
+ "--#{alias_name.tr("_", "-")}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end