cli-kit 4.0.0 → 5.0.1

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +3 -0
  3. data/.github/workflows/cla.yml +22 -0
  4. data/.github/workflows/ruby.yml +16 -2
  5. data/.gitignore +2 -0
  6. data/.rubocop.sorbet.yml +47 -0
  7. data/.rubocop.yml +32 -1
  8. data/.ruby-version +1 -0
  9. data/Gemfile +10 -1
  10. data/Gemfile.lock +102 -29
  11. data/README.md +46 -3
  12. data/Rakefile +1 -0
  13. data/bin/onchange +30 -0
  14. data/bin/tapioca +28 -0
  15. data/bin/testunit +1 -0
  16. data/cli-kit.gemspec +9 -4
  17. data/dev.yml +38 -3
  18. data/examples/minimal/example.rb +11 -6
  19. data/examples/single-file/example.rb +25 -35
  20. data/gen/lib/gen/commands/help.rb +8 -10
  21. data/gen/lib/gen/commands/new.rb +23 -9
  22. data/gen/lib/gen/commands.rb +21 -9
  23. data/gen/lib/gen/entry_point.rb +12 -3
  24. data/gen/lib/gen/generator.rb +32 -11
  25. data/gen/lib/gen/help.rb +63 -0
  26. data/gen/lib/gen.rb +18 -23
  27. data/gen/template/bin/update-deps +2 -2
  28. data/gen/template/dev-gems.yml +1 -1
  29. data/gen/template/dev-vendor.yml +1 -1
  30. data/gen/template/lib/__app__/commands.rb +1 -4
  31. data/gen/template/lib/__app__.rb +8 -17
  32. data/gen/template/test/example_test.rb +1 -1
  33. data/lib/cli/kit/args/definition.rb +344 -0
  34. data/lib/cli/kit/args/evaluation.rb +234 -0
  35. data/lib/cli/kit/args/parser/node.rb +132 -0
  36. data/lib/cli/kit/args/parser.rb +129 -0
  37. data/lib/cli/kit/args/tokenizer.rb +133 -0
  38. data/lib/cli/kit/args.rb +16 -0
  39. data/lib/cli/kit/base_command.rb +17 -32
  40. data/lib/cli/kit/command_help.rb +271 -0
  41. data/lib/cli/kit/command_registry.rb +72 -20
  42. data/lib/cli/kit/config.rb +25 -22
  43. data/lib/cli/kit/core_ext.rb +30 -0
  44. data/lib/cli/kit/error_handler.rb +131 -70
  45. data/lib/cli/kit/executor.rb +20 -3
  46. data/lib/cli/kit/ini.rb +31 -38
  47. data/lib/cli/kit/levenshtein.rb +12 -4
  48. data/lib/cli/kit/logger.rb +16 -2
  49. data/lib/cli/kit/opts.rb +301 -0
  50. data/lib/cli/kit/parse_args.rb +55 -0
  51. data/lib/cli/kit/resolver.rb +8 -0
  52. data/lib/cli/kit/sorbet_runtime_stub.rb +154 -0
  53. data/lib/cli/kit/support/test_helper.rb +27 -16
  54. data/lib/cli/kit/support.rb +2 -0
  55. data/lib/cli/kit/system.rb +194 -57
  56. data/lib/cli/kit/util.rb +48 -103
  57. data/lib/cli/kit/version.rb +3 -1
  58. data/lib/cli/kit.rb +104 -7
  59. metadata +30 -14
  60. data/.github/probots.yml +0 -2
  61. data/lib/cli/kit/autocall.rb +0 -21
  62. data/lib/cli/kit/ruby_backports/enumerable.rb +0 -6
data/lib/cli/kit/ini.rb CHANGED
@@ -1,3 +1,7 @@
1
+ # typed: true
2
+
3
+ require 'cli/kit'
4
+
1
5
  module CLI
2
6
  module Kit
3
7
  # INI is a language similar to JSON or YAML, but simplied
@@ -13,87 +17,76 @@ module CLI
13
17
  # See the ini_test.rb file for more examples
14
18
  #
15
19
  class Ini
20
+ extend T::Sig
21
+
22
+ sig { returns(T::Hash[String, T::Hash[String, String]]) }
16
23
  attr_accessor :ini
17
24
 
18
- def initialize(path = nil, config: nil, default_section: nil, convert_types: true)
25
+ sig do
26
+ params(path: T.nilable(String), config: T.nilable(String), default_section: String).void
27
+ end
28
+ def initialize(path = nil, config: nil, default_section: '[global]')
19
29
  @config = if path && File.exist?(path)
20
30
  File.readlines(path)
21
31
  elsif config
22
32
  config.lines
23
33
  end
24
34
  @ini = {}
25
- @current_key = nil
26
- @default_section = default_section
27
- @convert_types = convert_types
35
+ @current_key = default_section
28
36
  end
29
37
 
38
+ sig { returns(T::Hash[String, T::Hash[String, String]]) }
30
39
  def parse
31
40
  return @ini if @config.nil?
32
41
 
33
42
  @config.each do |l|
34
43
  l.strip!
35
44
 
36
- # If section, then set current key, this will nest the setting
37
45
  if section_designator?(l)
38
46
  @current_key = l
39
-
40
- # A new line will reset the current key
41
- elsif l.strip.empty?
42
- @current_key = nil
43
-
44
- # Otherwise set the values
45
47
  else
46
48
  k, v = l.split('=', 2).map(&:strip)
47
- set_val(k, v)
49
+ set_val(k, v) if k && v
48
50
  end
49
51
  end
52
+
50
53
  @ini
51
54
  end
52
55
 
56
+ sig { returns(String) }
53
57
  def git_format
54
- to_ini(@ini, git_format: true).flatten.join("\n")
58
+ to_ini(git_format: true)
55
59
  end
56
60
 
61
+ sig { returns(String) }
57
62
  def to_s
58
- to_ini(@ini).flatten.join("\n")
63
+ to_ini
59
64
  end
60
65
 
61
66
  private
62
67
 
63
- def to_ini(h, git_format: false)
68
+ sig { params(git_format: T::Boolean).returns(String) }
69
+ def to_ini(git_format: false)
64
70
  optional_tab = git_format ? "\t" : ''
65
71
  str = []
66
- h.each do |k, v|
67
- if section_designator?(k)
68
- str << '' unless str.empty? || git_format
69
- str << k
70
- str << to_ini(v, git_format: git_format)
71
- else
72
+ @ini.each do |section_designator, section|
73
+ str << '' unless str.empty? || git_format
74
+ str << section_designator
75
+ section.each do |k, v|
72
76
  str << "#{optional_tab}#{k} = #{v}"
73
77
  end
74
78
  end
75
- str
79
+ str.join("\n")
76
80
  end
77
81
 
82
+ sig { params(key: String, val: String).void }
78
83
  def set_val(key, val)
79
- return if key.nil? && val.nil?
80
-
81
- current_key = @current_key || @default_section
82
- if current_key
83
- @ini[current_key] ||= {}
84
- @ini[current_key][key] = typed_val(val)
85
- else
86
- @ini[key] = typed_val(val)
87
- end
88
- end
89
-
90
- def typed_val(val)
91
- return val.to_s unless @convert_types
92
- return val.to_i if val =~ /^-?[0-9]+$/
93
- return val.to_f if val =~ /^-?[0-9]+\.[0-9]*$/
94
- val.to_s
84
+ current_key = @current_key
85
+ @ini[current_key] ||= {}
86
+ @ini[current_key][key] = val
95
87
  end
96
88
 
89
+ sig { params(k: String).returns(T::Boolean) }
97
90
  def section_designator?(k)
98
91
  k.start_with?('[') && k.end_with?(']')
99
92
  end
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  # Copyright (c) 2014-2016 Yuki Nishijima
2
4
 
3
5
  # MIT License
@@ -21,13 +23,18 @@
21
23
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
24
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
25
 
26
+ require 'cli/kit'
27
+
24
28
  module CLI
25
29
  module Kit
26
30
  module Levenshtein
31
+ extend T::Sig
32
+
27
33
  # This code is based directly on the Text gem implementation
28
34
  # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.
29
35
  #
30
36
  # Returns a value representing the "cost" of transforming str1 into str2
37
+ sig { params(str1: String, str2: String).returns(Integer) }
31
38
  def distance(str1, str2)
32
39
  n = str1.length
33
40
  m = str2.length
@@ -35,7 +42,7 @@ module CLI
35
42
  return n if m.zero?
36
43
 
37
44
  d = (0..m).to_a
38
- x = nil
45
+ x = 0
39
46
 
40
47
  # to avoid duplicating an enumerable object, create it outside of the loop
41
48
  str2_codepoints = str2.codepoints
@@ -45,9 +52,9 @@ module CLI
45
52
  while j < m
46
53
  cost = char1 == str2_codepoints[j] ? 0 : 1
47
54
  x = min3(
48
- d[j + 1] + 1, # insertion
49
- i + 1, # deletion
50
- d[j] + cost # substitution
55
+ T.must(d[j + 1]) + 1, # insertion
56
+ i + 1, # deletion
57
+ T.must(d[j]) + cost, # substitution
51
58
  )
52
59
  d[j] = i
53
60
  i = x
@@ -67,6 +74,7 @@ module CLI
67
74
  # faster than `[a, b, c].min` and puts less GC pressure.
68
75
  # See https://github.com/yuki24/did_you_mean/pull/1 for a performance
69
76
  # benchmark.
77
+ sig { params(a: Integer, b: Integer, c: Integer).returns(Integer) }
70
78
  def min3(a, b, c)
71
79
  if a < b && a < c
72
80
  a
@@ -1,15 +1,21 @@
1
+ # typed: true
2
+
3
+ require 'cli/kit'
1
4
  require 'logger'
2
5
  require 'fileutils'
3
6
 
4
7
  module CLI
5
8
  module Kit
6
9
  class Logger
10
+ extend T::Sig
11
+
7
12
  MAX_LOG_SIZE = 5 * 1024 * 1000 # 5MB
8
13
  MAX_NUM_LOGS = 10
9
14
 
10
15
  # Constructor for CLI::Kit::Logger
11
16
  #
12
17
  # @param debug_log_file [String] path to the file where debug logs should be stored
18
+ sig { params(debug_log_file: String, env_debug_name: String).void }
13
19
  def initialize(debug_log_file:, env_debug_name: 'DEBUG')
14
20
  FileUtils.mkpath(File.dirname(debug_log_file))
15
21
  @debug_logger = ::Logger.new(debug_log_file, MAX_NUM_LOGS, MAX_LOG_SIZE)
@@ -21,6 +27,7 @@ module CLI
21
27
  #
22
28
  # @param msg [String] the message to log
23
29
  # @param debug [Boolean] determines if the debug logger will receive the log (default true)
30
+ sig { params(msg: String, debug: T::Boolean).void }
24
31
  def info(msg, debug: true)
25
32
  $stdout.puts CLI::UI.fmt(msg)
26
33
  @debug_logger.info(format_debug(msg)) if debug
@@ -31,6 +38,7 @@ module CLI
31
38
  #
32
39
  # @param msg [String] the message to log
33
40
  # @param debug [Boolean] determines if the debug logger will receive the log (default true)
41
+ sig { params(msg: String, debug: T::Boolean).void }
34
42
  def warn(msg, debug: true)
35
43
  $stdout.puts CLI::UI.fmt("{{yellow:#{msg}}}")
36
44
  @debug_logger.warn(format_debug(msg)) if debug
@@ -41,6 +49,7 @@ module CLI
41
49
  #
42
50
  # @param msg [String] the message to log
43
51
  # @param debug [Boolean] determines if the debug logger will receive the log (default true)
52
+ sig { params(msg: String, debug: T::Boolean).void }
44
53
  def error(msg, debug: true)
45
54
  $stderr.puts CLI::UI.fmt("{{red:#{msg}}}")
46
55
  @debug_logger.error(format_debug(msg)) if debug
@@ -51,6 +60,7 @@ module CLI
51
60
  #
52
61
  # @param msg [String] the message to log
53
62
  # @param debug [Boolean] determines if the debug logger will receive the log (default true)
63
+ sig { params(msg: String, debug: T::Boolean).void }
54
64
  def fatal(msg, debug: true)
55
65
  $stderr.puts CLI::UI.fmt("{{red:{{bold:Fatal:}} #{msg}}}")
56
66
  @debug_logger.fatal(format_debug(msg)) if debug
@@ -60,6 +70,7 @@ module CLI
60
70
  # Logs to the debug file, taking into account CLI::UI::StdoutRouter.current_id
61
71
  #
62
72
  # @param msg [String] the message to log
73
+ sig { params(msg: String).void }
63
74
  def debug(msg)
64
75
  $stdout.puts CLI::UI.fmt(msg) if debug?
65
76
  @debug_logger.debug(format_debug(msg))
@@ -67,15 +78,18 @@ module CLI
67
78
 
68
79
  private
69
80
 
81
+ sig { params(msg: String).returns(String) }
70
82
  def format_debug(msg)
71
83
  msg = CLI::UI.fmt(msg)
72
84
  return msg unless CLI::UI::StdoutRouter.current_id
73
- "[#{CLI::UI::StdoutRouter.current_id[:id]}] #{msg}"
85
+
86
+ "[#{CLI::UI::StdoutRouter.current_id&.fetch(:id, nil)}] #{msg}"
74
87
  end
75
88
 
89
+ sig { returns(T::Boolean) }
76
90
  def debug?
77
91
  val = ENV[@env_debug_name]
78
- val && val != '0' && val != ''
92
+ !!val && val != '0' && val != ''
79
93
  end
80
94
  end
81
95
  end
@@ -0,0 +1,301 @@
1
+ # typed: true
2
+
3
+ require 'cli/kit'
4
+
5
+ module CLI
6
+ module Kit
7
+ class Opts
8
+ extend T::Sig
9
+
10
+ module Mixin
11
+ extend T::Sig
12
+ include Kernel
13
+
14
+ module MixinClassMethods
15
+ extend T::Sig
16
+
17
+ sig { params(included_module: Module).void }
18
+ def include(included_module)
19
+ super
20
+ return unless included_module.is_a?(MixinClassMethods)
21
+
22
+ included_module.tracked_methods.each { |m| track_method(m) }
23
+ end
24
+
25
+ # No signature - Sorbet uses method_added internally, so can't verify it
26
+ def method_added(method_name) # rubocop:disable Sorbet/EnforceSignatures
27
+ super
28
+ track_method(method_name)
29
+ end
30
+
31
+ sig { params(method_name: Symbol).void }
32
+ def track_method(method_name)
33
+ @tracked_methods ||= []
34
+ @tracked_methods << method_name unless @tracked_methods.include?(method_name)
35
+ end
36
+
37
+ sig { returns(T::Array[Symbol]) }
38
+ def tracked_methods
39
+ @tracked_methods || []
40
+ end
41
+ end
42
+
43
+ class << self
44
+ extend T::Sig
45
+
46
+ sig { params(klass: Module).void }
47
+ def included(klass)
48
+ klass.extend(MixinClassMethods)
49
+ end
50
+ end
51
+
52
+ sig do
53
+ params(
54
+ name: Symbol,
55
+ short: T.nilable(String),
56
+ long: T.nilable(String),
57
+ desc: T.nilable(String),
58
+ default: T.any(NilClass, String, T.proc.returns(String)),
59
+ ).returns(T.nilable(String))
60
+ end
61
+ def option(name: infer_name, short: nil, long: nil, desc: nil, default: nil)
62
+ unless default.nil?
63
+ raise(ArgumentError, 'declare options with non-nil defaults using `option!` instead of `option`')
64
+ end
65
+
66
+ case @obj
67
+ when Args::Definition
68
+ @obj.add_option(
69
+ name, short: short, long: long, desc: desc, default: default
70
+ )
71
+ '(result unavailable)'
72
+ when Args::Evaluation
73
+ @obj.opt.send(name)
74
+ end
75
+ end
76
+
77
+ sig do
78
+ params(
79
+ name: Symbol,
80
+ short: T.nilable(String),
81
+ long: T.nilable(String),
82
+ desc: T.nilable(String),
83
+ default: T.any(NilClass, String, T.proc.returns(String)),
84
+ ).returns(String)
85
+ end
86
+ def option!(name: infer_name, short: nil, long: nil, desc: nil, default: nil)
87
+ case @obj
88
+ when Args::Definition
89
+ @obj.add_option(
90
+ name, short: short, long: long, desc: desc, default: default
91
+ )
92
+ '(result unavailable)'
93
+ when Args::Evaluation
94
+ @obj.opt.send(name)
95
+ end
96
+ end
97
+
98
+ sig do
99
+ params(
100
+ name: Symbol,
101
+ short: T.nilable(String),
102
+ long: T.nilable(String),
103
+ desc: T.nilable(String),
104
+ default: T.any(T::Array[String], T.proc.returns(T::Array[String])),
105
+ ).returns(T::Array[String])
106
+ end
107
+ def multi_option(name: infer_name, short: nil, long: nil, desc: nil, default: [])
108
+ case @obj
109
+ when Args::Definition
110
+ @obj.add_option(
111
+ name, short: short, long: long, desc: desc, default: default, multi: true
112
+ )
113
+ ['(result unavailable)']
114
+ when Args::Evaluation
115
+ @obj.opt.send(name)
116
+ end
117
+ end
118
+
119
+ sig do
120
+ params(
121
+ name: Symbol,
122
+ short: T.nilable(String),
123
+ long: T.nilable(String),
124
+ desc: T.nilable(String),
125
+ ).returns(T::Boolean)
126
+ end
127
+ def flag(name: infer_name, short: nil, long: nil, desc: nil)
128
+ case @obj
129
+ when Args::Definition
130
+ @obj.add_flag(name, short: short, long: long, desc: desc)
131
+ false
132
+ when Args::Evaluation
133
+ @obj.flag.send(name)
134
+ end
135
+ end
136
+
137
+ sig { params(name: Symbol, desc: T.nilable(String)).returns(String) }
138
+ def position!(name: infer_name, desc: nil)
139
+ case @obj
140
+ when Args::Definition
141
+ @obj.add_position(name, desc: desc, required: true, multi: false)
142
+ '(result unavailable)'
143
+ when Args::Evaluation
144
+ @obj.position.send(name)
145
+ end
146
+ end
147
+
148
+ sig do
149
+ params(
150
+ name: Symbol,
151
+ desc: T.nilable(String),
152
+ default: T.any(NilClass, String, T.proc.returns(String)),
153
+ skip: T.any(
154
+ NilClass,
155
+ T.proc.returns(T::Boolean),
156
+ T.proc.params(arg0: String).returns(T::Boolean),
157
+ ),
158
+ ).returns(T.nilable(String))
159
+ end
160
+ def position(name: infer_name, desc: nil, default: nil, skip: nil)
161
+ case @obj
162
+ when Args::Definition
163
+ @obj.add_position(name, desc: desc, required: false, multi: false, default: default, skip: skip)
164
+ '(result unavailable)'
165
+ when Args::Evaluation
166
+ @obj.position.send(name)
167
+ end
168
+ end
169
+
170
+ sig { params(name: Symbol, desc: T.nilable(String)).returns(T::Array[String]) }
171
+ def rest(name: infer_name, desc: nil)
172
+ case @obj
173
+ when Args::Definition
174
+ @obj.add_position(name, desc: desc, required: false, multi: true)
175
+ ['(result unavailable)']
176
+ when Args::Evaluation
177
+ @obj.position.send(name)
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ sig { returns(Symbol) }
184
+ def infer_name
185
+ to_skip = 1
186
+ Kernel.caller_locations.each do |loc|
187
+ next if loc.path =~ /sorbet-runtime/
188
+
189
+ if to_skip > 0
190
+ to_skip -= 1
191
+ next
192
+ end
193
+ return(T.must(loc.label&.to_sym))
194
+ end
195
+ raise(ArgumentError, 'could not infer name')
196
+ end
197
+ end
198
+ include(Mixin)
199
+
200
+ DEFAULT_OPTIONS = [:helpflag]
201
+
202
+ sig { returns(T::Boolean) }
203
+ def helpflag
204
+ flag(name: :help, short: '-h', long: '--help', desc: 'Show this help message')
205
+ end
206
+
207
+ sig { returns(T::Array[String]) }
208
+ def unparsed
209
+ obj = assert_result!
210
+ obj.unparsed
211
+ end
212
+
213
+ sig do
214
+ params(
215
+ block: T.nilable(
216
+ T.proc.params(arg0: Symbol, arg1: T.nilable(String)).void,
217
+ ),
218
+ ).returns(T.untyped)
219
+ end
220
+ def each_option(&block)
221
+ return(enum_for(:each_option)) unless block_given?
222
+
223
+ obj = assert_result!
224
+ obj.defn.options.each do |opt|
225
+ name = opt.name
226
+ value = obj.opt.send(name)
227
+ yield(name, value)
228
+ end
229
+ end
230
+
231
+ sig do
232
+ params(
233
+ block: T.nilable(
234
+ T.proc.params(arg0: Symbol, arg1: T::Boolean).void,
235
+ ),
236
+ ).returns(T.untyped)
237
+ end
238
+ def each_flag(&block)
239
+ return(enum_for(:each_flag)) unless block_given?
240
+
241
+ obj = assert_result!
242
+ obj.defn.flags.each do |flag|
243
+ name = flag.name
244
+ value = obj.flag.send(name)
245
+ yield(name, value)
246
+ end
247
+ end
248
+
249
+ sig { params(name: String).returns(T.nilable(T.any(String, T::Boolean))) }
250
+ def [](name)
251
+ obj = assert_result!
252
+ if obj.opt.respond_to?(name)
253
+ obj.opt.send(name)
254
+ elsif obj.flag.respond_to?(name)
255
+ obj.flag.send(name)
256
+ end
257
+ end
258
+
259
+ sig { params(name: String).returns(T.nilable(String)) }
260
+ def lookup_option(name)
261
+ obj = assert_result!
262
+ obj.opt.send(name)
263
+ rescue NoMethodError
264
+ # TODO: should we raise a KeyError?
265
+ nil
266
+ end
267
+
268
+ sig { params(name: String).returns(T::Boolean) }
269
+ def lookup_flag(name)
270
+ obj = assert_result!
271
+ obj.flag.send(name)
272
+ rescue NoMethodError
273
+ false
274
+ end
275
+
276
+ sig { returns(Args::Evaluation) }
277
+ def assert_result!
278
+ raise(NotImplementedError, 'not implemented') if @obj.is_a?(Args::Definition)
279
+
280
+ @obj
281
+ end
282
+
283
+ sig { params(defn: Args::Definition).void }
284
+ def define!(defn)
285
+ @obj = defn
286
+ T.cast(self.class, Mixin::MixinClassMethods).tracked_methods.each do |m|
287
+ send(m)
288
+ end
289
+ DEFAULT_OPTIONS.each do |m|
290
+ send(m)
291
+ end
292
+ end
293
+
294
+ sig { params(ev: Args::Evaluation).void }
295
+ def evaluate!(ev)
296
+ @obj = ev
297
+ ev.resolve_positions!
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,55 @@
1
+ # typed: true
2
+
3
+ module CLI
4
+ module Kit
5
+ module ParseArgs
6
+ # because sorbet type-checking takes the pedantic route that module doesn't include Kernel, therefore
7
+ # this is necessary (even tho it's ~probably fine~)
8
+ include Kernel
9
+ extend T::Sig
10
+
11
+ # T.untyped is used in two places. The interpretation of dynamic values from the provided `opts`
12
+ # and the resulting args[:opts] is pretty broad. There seems to be minimal value in expressing a
13
+ # tighter subset of T.untyped.
14
+
15
+ sig { params(args: String, opts_defn: T::Hash[Symbol, T::Array[T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
16
+ def parse_args(args, opts_defn)
17
+ start_opts, parser_config = opts_defn.reduce([{}, []]) do |(ini, pcfg), (n, cfg)|
18
+ (vals, desc, short, klass) = cfg
19
+ (init_val, def_val) = Array(vals)
20
+
21
+ [
22
+ init_val.nil? ? ini : ini.merge(n => init_val),
23
+ pcfg + [[n, short, desc, def_val, klass]],
24
+ ]
25
+ end
26
+
27
+ require('optparse')
28
+
29
+ acc_opts = {}
30
+ prsr = OptionParser.new do |opt_p|
31
+ parser_config.each do |(n, short, desc, def_val, klass)|
32
+ (_, mark) = short.split(' ')
33
+ long = "--#{n.to_s.tr("_", "-")}" + (mark.nil? ? '' : " #{mark}")
34
+ opt_args = klass.nil? ? [short, long, desc] : [short, long, klass, desc]
35
+
36
+ T.unsafe(opt_p).on(*opt_args) do |v|
37
+ acc_opts[n] = if acc_opts.key?(n)
38
+ Array(acc_opts[n]) + Array(v || def_val)
39
+ else
40
+ v || def_val
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ arg_v = args.strip.split(/\s+/).map(&:strip)
47
+ sub = prsr.parse(arg_v)
48
+
49
+ { opts: start_opts.merge(acc_opts) }.tap do |a|
50
+ a[:sub] = sub if sub
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,13 +1,19 @@
1
+ # typed: true
2
+
1
3
  require 'cli/kit'
2
4
 
3
5
  module CLI
4
6
  module Kit
5
7
  class Resolver
8
+ extend T::Sig
9
+
10
+ sig { params(tool_name: String, command_registry: CLI::Kit::CommandRegistry).void }
6
11
  def initialize(tool_name:, command_registry:)
7
12
  @tool_name = tool_name
8
13
  @command_registry = command_registry
9
14
  end
10
15
 
16
+ sig { params(args: T::Array[String]).returns([T.class_of(CLI::Kit::BaseCommand), String, T::Array[String]]) }
11
17
  def call(args)
12
18
  args = args.dup
13
19
  command_name = args.shift
@@ -24,6 +30,7 @@ module CLI
24
30
 
25
31
  private
26
32
 
33
+ sig { params(name: T.nilable(String)).void }
27
34
  def command_not_found(name)
28
35
  CLI::UI::Frame.open('Command not found', color: :red, timing: false) do
29
36
  $stderr.puts(CLI::UI.fmt("{{command:#{@tool_name} #{name}}} was not found"))
@@ -52,6 +59,7 @@ module CLI
52
59
  end
53
60
  end
54
61
 
62
+ sig { returns(T::Array[String]) }
55
63
  def commands_and_aliases
56
64
  @command_registry.command_names + @command_registry.aliases.keys
57
65
  end