cli-kit 4.0.0 → 5.0.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/cla.yml +22 -0
  3. data/.github/workflows/ruby.yml +34 -2
  4. data/.gitignore +2 -0
  5. data/.rubocop.sorbet.yml +47 -0
  6. data/.rubocop.yml +16 -1
  7. data/Gemfile +10 -1
  8. data/Gemfile.lock +94 -18
  9. data/README.md +46 -3
  10. data/Rakefile +1 -0
  11. data/bin/onchange +30 -0
  12. data/bin/tapioca +29 -0
  13. data/bin/testunit +1 -0
  14. data/cli-kit.gemspec +2 -2
  15. data/dev.yml +35 -3
  16. data/examples/minimal/example.rb +3 -1
  17. data/examples/single-file/example.rb +25 -35
  18. data/gen/lib/gen/commands/help.rb +8 -10
  19. data/gen/lib/gen/commands/new.rb +23 -9
  20. data/gen/lib/gen/commands.rb +21 -9
  21. data/gen/lib/gen/entry_point.rb +12 -3
  22. data/gen/lib/gen/generator.rb +28 -7
  23. data/gen/lib/gen/help.rb +63 -0
  24. data/gen/lib/gen.rb +18 -23
  25. data/gen/template/bin/update-deps +2 -2
  26. data/gen/template/lib/__app__/commands.rb +1 -4
  27. data/gen/template/lib/__app__.rb +8 -17
  28. data/gen/template/test/example_test.rb +1 -1
  29. data/lib/cli/kit/args/definition.rb +344 -0
  30. data/lib/cli/kit/args/evaluation.rb +245 -0
  31. data/lib/cli/kit/args/parser/node.rb +132 -0
  32. data/lib/cli/kit/args/parser.rb +129 -0
  33. data/lib/cli/kit/args/tokenizer.rb +133 -0
  34. data/lib/cli/kit/args.rb +16 -0
  35. data/lib/cli/kit/base_command.rb +17 -32
  36. data/lib/cli/kit/command_help.rb +271 -0
  37. data/lib/cli/kit/command_registry.rb +69 -17
  38. data/lib/cli/kit/config.rb +25 -22
  39. data/lib/cli/kit/core_ext.rb +30 -0
  40. data/lib/cli/kit/error_handler.rb +131 -70
  41. data/lib/cli/kit/executor.rb +19 -3
  42. data/lib/cli/kit/ini.rb +31 -38
  43. data/lib/cli/kit/levenshtein.rb +12 -4
  44. data/lib/cli/kit/logger.rb +16 -2
  45. data/lib/cli/kit/opts.rb +301 -0
  46. data/lib/cli/kit/resolver.rb +8 -0
  47. data/lib/cli/kit/sorbet_runtime_stub.rb +156 -0
  48. data/lib/cli/kit/support/test_helper.rb +23 -14
  49. data/lib/cli/kit/support.rb +2 -0
  50. data/lib/cli/kit/system.rb +188 -54
  51. data/lib/cli/kit/util.rb +48 -103
  52. data/lib/cli/kit/version.rb +3 -1
  53. data/lib/cli/kit.rb +103 -7
  54. metadata +22 -10
  55. data/.github/probots.yml +0 -2
  56. data/lib/cli/kit/autocall.rb +0 -21
  57. data/lib/cli/kit/ruby_backports/enumerable.rb +0 -6
data/lib/cli/kit/util.rb CHANGED
@@ -1,81 +1,26 @@
1
+ # typed: true
2
+
3
+ require 'cli/kit'
4
+
1
5
  module CLI
2
6
  module Kit
3
7
  module Util
4
8
  class << self
5
- def snake_case(camel_case, seperator = '_')
6
- camel_case.to_s # MyCoolThing::MyAPIModule
7
- .gsub(/::/, '/') # MyCoolThing/MyAPIModule
8
- .gsub(/([A-Z]+)([A-Z][a-z])/, "\\1#{seperator}\\2") # MyCoolThing::MyAPI_Module
9
- .gsub(/([a-z\d])([A-Z])/, "\\1#{seperator}\\2") # My_Cool_Thing::My_API_Module
10
- .downcase # my_cool_thing/my_api_module
11
- end
12
-
13
- def dash_case(camel_case)
14
- snake_case(camel_case, '-')
15
- end
16
-
17
- # The following methods is taken from activesupport
18
- # All credit for this method goes to the original authors.
19
- # https://github.com/rails/rails/blob/d66e7835bea9505f7003e5038aa19b6ea95ceea1/activesupport/lib/active_support/core_ext/string/strip.rb
20
- #
21
- # Copyright (c) 2005-2018 David Heinemeier Hansson
22
- #
23
- # Permission is hereby granted, free of charge, to any person obtaining
24
- # a copy of this software and associated documentation files (the
25
- # "Software"), to deal in the Software without restriction, including
26
- # without limitation the rights to use, copy, modify, merge, publish,
27
- # distribute, sublicense, and/or sell copies of the Software, and to
28
- # permit persons to whom the Software is furnished to do so, subject to
29
- # the following conditions:
30
- #
31
- # The above copyright notice and this permission notice shall be
32
- # included in all copies or substantial portions of the Software.
33
- #
34
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
35
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
36
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
37
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
38
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
39
- # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
40
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41
- #
42
- # Strips indentation by removing the amount of leading whitespace in the least indented
43
- # non-empty line in the whole string
9
+ extend T::Sig
44
10
  #
45
- def strip_heredoc(str)
46
- str.gsub(/^#{str.scan(/^[ \t]*(?=\S)/).min}/, ''.freeze)
47
- end
48
-
49
- # Joins an array with commas and "and", using the Oxford comma.
50
- def english_join(array)
51
- return '' if array.nil?
52
- return array.join(' and ') if array.length < 3
53
-
54
- "#{array[0..-2].join(", ")}, and #{array[-1]}"
55
- end
56
-
57
- # Execute a block within the context of a variable enviroment
58
- #
59
- def with_environment(environment, value)
60
- return yield unless environment
61
-
62
- old_env = ENV[environment]
63
- begin
64
- ENV[environment] = value
65
- yield
66
- ensure
67
- old_env ? ENV[environment] = old_env : ENV.delete(environment)
68
- end
69
- end
70
-
71
11
  # Converts an integer representing bytes into a human readable format
72
12
  #
13
+ sig { params(bytes: Integer, precision: Integer, space: T::Boolean).returns(String) }
73
14
  def to_filesize(bytes, precision: 2, space: false)
74
15
  to_si_scale(bytes, 'B', precision: precision, space: space, factor: 1024)
75
16
  end
76
17
 
77
18
  # Converts a number to a human readable format on the SI scale
78
19
  #
20
+ sig do
21
+ params(number: Numeric, unit: String, factor: Integer, precision: Integer,
22
+ space: T::Boolean).returns(String)
23
+ end
79
24
  def to_si_scale(number, unit = '', factor: 1000, precision: 2, space: false)
80
25
  raise ArgumentError, 'factor should only be 1000 or 1024' unless [1000, 1024].include?(factor)
81
26
 
@@ -84,7 +29,7 @@ module CLI
84
29
  negative = number < 0
85
30
  number = number.abs.to_f
86
31
 
87
- if number == 0 || number.between?(1, factor)
32
+ if number == 0.0 || number.between?(1, factor)
88
33
  prefix = ''
89
34
  scale = 0
90
35
  else
@@ -92,22 +37,24 @@ module CLI
92
37
  if number < 1
93
38
  index = [-scale - 1, small_scale.length].min
94
39
  scale = -(index + 1)
95
- prefix = small_scale[index]
40
+ prefix = T.must(small_scale[index])
96
41
  else
97
42
  index = [scale - 1, big_scale.length].min
98
43
  scale = index + 1
99
- prefix = big_scale[index]
44
+ prefix = T.must(big_scale[index])
100
45
  end
101
46
  end
102
47
 
103
48
  divider = (factor**scale)
104
- fnum = (number / divider).round(precision)
49
+ fnum = (number / divider.to_f).round(precision)
105
50
 
106
51
  # Trim useless decimal
107
- fnum = fnum.to_i if (fnum.to_i.to_f * divider) == number
52
+ fnum = fnum.to_i if (fnum.to_i.to_f * divider.to_f) == number
108
53
 
109
54
  fnum = -fnum if negative
110
- prefix = ' ' + prefix if space
55
+ if space
56
+ prefix = ' ' + prefix
57
+ end
111
58
 
112
59
  "#{fnum}#{prefix}#{unit}"
113
60
  end
@@ -115,38 +62,18 @@ module CLI
115
62
  # Dir.chdir, when invoked in block form, complains when we call chdir
116
63
  # again recursively. There's no apparent good reason for this, so we
117
64
  # simply implement our own block form of Dir.chdir here.
118
- def with_dir(dir)
119
- prev = Dir.pwd
120
- Dir.chdir(dir)
121
- yield
122
- ensure
123
- Dir.chdir(prev)
65
+ sig do
66
+ type_parameters(:T).params(dir: String, block: T.proc.returns(T.type_parameter(:T)))
67
+ .returns(T.type_parameter(:T))
124
68
  end
125
-
126
- def with_tmp_dir
127
- require 'fileutils'
128
- dir = Dir.mktmpdir
129
- with_dir(dir) do
130
- yield(dir)
69
+ def with_dir(dir, &block)
70
+ prev = Dir.pwd
71
+ begin
72
+ Dir.chdir(dir)
73
+ yield
74
+ ensure
75
+ Dir.chdir(prev)
131
76
  end
132
- ensure
133
- FileUtils.remove_entry(dir)
134
- end
135
-
136
- # Standard way of checking for CI / Tests
137
- def testing?
138
- ci? || ENV['TEST']
139
- end
140
-
141
- # Set only in IntegrationTest#session; indicates that the process was
142
- # called by `session.execute` from an IntegrationTest subclass.
143
- def integration_test_session?
144
- ENV['INTEGRATION_TEST_SESSION']
145
- end
146
-
147
- # Standard way of checking for CI
148
- def ci?
149
- ENV['CI']
150
77
  end
151
78
 
152
79
  # Must call retry_after on the result in order to execute the block
@@ -158,25 +85,43 @@ module CLI
158
85
  # end.retry_after(ExpectedError) do
159
86
  # costly_prep()
160
87
  # end
88
+ sig do
89
+ type_parameters(:T).params(block_that_might_raise: T.proc.returns(T.type_parameter(:T)))
90
+ .returns(Retrier[T.type_parameter(:T)])
91
+ end
161
92
  def begin(&block_that_might_raise)
162
93
  Retrier.new(block_that_might_raise)
163
94
  end
164
95
  end
165
96
 
166
97
  class Retrier
98
+ extend T::Sig
99
+ extend T::Generic
100
+
101
+ BlockReturnType = type_member
102
+
103
+ sig { params(block_that_might_raise: T.proc.returns(BlockReturnType)).void }
167
104
  def initialize(block_that_might_raise)
168
105
  @block_that_might_raise = block_that_might_raise
169
106
  end
170
107
 
108
+ sig do
109
+ params(
110
+ exception: T.class_of(Exception),
111
+ retries: Integer,
112
+ before_retry: T.nilable(T.proc.params(e: Exception).void),
113
+ ).returns(BlockReturnType)
114
+ end
171
115
  def retry_after(exception = StandardError, retries: 1, &before_retry)
172
116
  @block_that_might_raise.call
173
117
  rescue exception => e
174
118
  raise if (retries -= 1) < 0
119
+
175
120
  if before_retry
176
121
  if before_retry.arity == 0
177
- yield
122
+ T.cast(before_retry, T.proc.void).call
178
123
  else
179
- yield e
124
+ T.cast(before_retry, T.proc.params(e: Exception).void).call(e)
180
125
  end
181
126
  end
182
127
  retry
@@ -1,5 +1,7 @@
1
+ # typed: true
2
+
1
3
  module CLI
2
4
  module Kit
3
- VERSION = '4.0.0'
5
+ VERSION = '5.0.0'
4
6
  end
5
7
  end
data/lib/cli/kit.rb CHANGED
@@ -1,17 +1,28 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui'
2
- require 'cli/kit/ruby_backports/enumerable'
4
+
5
+ unless defined?(T)
6
+ require('cli/kit/sorbet_runtime_stub')
7
+ end
8
+
9
+ require 'cli/kit/core_ext'
3
10
 
4
11
  module CLI
5
12
  module Kit
6
- autoload :Autocall, 'cli/kit/autocall'
13
+ extend T::Sig
14
+
15
+ autoload :Args, 'cli/kit/args'
7
16
  autoload :BaseCommand, 'cli/kit/base_command'
8
17
  autoload :CommandRegistry, 'cli/kit/command_registry'
18
+ autoload :CommandHelp, 'cli/kit/command_help'
9
19
  autoload :Config, 'cli/kit/config'
10
20
  autoload :ErrorHandler, 'cli/kit/error_handler'
11
21
  autoload :Executor, 'cli/kit/executor'
12
22
  autoload :Ini, 'cli/kit/ini'
13
23
  autoload :Levenshtein, 'cli/kit/levenshtein'
14
24
  autoload :Logger, 'cli/kit/logger'
25
+ autoload :Opts, 'cli/kit/opts'
15
26
  autoload :Resolver, 'cli/kit/resolver'
16
27
  autoload :Support, 'cli/kit/support'
17
28
  autoload :System, 'cli/kit/system'
@@ -28,7 +39,8 @@ module CLI
28
39
  # a bare `rescue => e`.
29
40
  #
30
41
  # * Abort prints its message in red and exits 1;
31
- # * Bug additionally submits the exception to Bugsnag;
42
+ # * Bug additionally submits the exception to the exception_reporter passed to
43
+ # `CLI::Kit::ErrorHandler.new`
32
44
  # * AbortSilent and BugSilent do the same as above, but do not print
33
45
  # messages before exiting.
34
46
  #
@@ -51,10 +63,94 @@ module CLI
51
63
  # 1. rescue Abort or Bug
52
64
  # 2. Print a contextualized error message
53
65
  # 3. Re-raise AbortSilent or BugSilent respectively.
66
+ #
67
+ # These aren't the only exceptions that can carry this 'bug' and 'silent'
68
+ # metadata, however:
69
+ #
70
+ # If you raise an exception with `CLI::Kit.raise(..., bug: x, silent: y)`,
71
+ # those last two (optional) keyword arguments will attach the metadata to
72
+ # whatever exception you raise. This is interpreted later in the
73
+ # ErrorHandler to decide how to print output and whether to submit the
74
+ # exception to bugsnag.
54
75
  GenericAbort = Class.new(Exception) # rubocop:disable Lint/InheritException
55
- Abort = Class.new(GenericAbort)
56
- Bug = Class.new(GenericAbort)
57
- BugSilent = Class.new(GenericAbort)
58
- AbortSilent = Class.new(GenericAbort)
76
+
77
+ class Abort < GenericAbort # bug:false; silent: false
78
+ extend(T::Sig)
79
+
80
+ sig { returns(T::Boolean) }
81
+ def bug?
82
+ false
83
+ end
84
+ end
85
+
86
+ class Bug < GenericAbort # bug:true; silent:false
87
+ end
88
+
89
+ class BugSilent < GenericAbort # bug:true; silent:true
90
+ extend(T::Sig)
91
+
92
+ sig { returns(T::Boolean) }
93
+ def silent?
94
+ true
95
+ end
96
+ end
97
+
98
+ class AbortSilent < GenericAbort # bug:false; silent:true
99
+ extend(T::Sig)
100
+
101
+ sig { returns(T::Boolean) }
102
+ def bug?
103
+ false
104
+ end
105
+
106
+ sig { returns(T::Boolean) }
107
+ def silent?
108
+ true
109
+ end
110
+ end
111
+
112
+ class << self
113
+ extend T::Sig
114
+
115
+ # Mirrors the API of Kernel#raise, but with the addition of a few new
116
+ # optional keyword arguments. `bug` and `silent` attach metadata to the
117
+ # exception being raised, which is interpreted later in the ErrorHandler to
118
+ # decide what to print and whether to submit to bugsnag.
119
+ #
120
+ # `depth` is used to trim leading elements of the backtrace. If you wrap
121
+ # this method in your own wrapper, you'll want to pass `depth: 2`, for
122
+ # example.
123
+ sig do
124
+ params(
125
+ exception: T.any(Class, String, Exception),
126
+ string: T.untyped,
127
+ array: T.nilable(T::Array[String]),
128
+ cause: T.nilable(Exception),
129
+ bug: T.nilable(T::Boolean),
130
+ silent: T.nilable(T::Boolean),
131
+ depth: Integer,
132
+ ).returns(T.noreturn)
133
+ end
134
+ def raise(
135
+ # default arguments
136
+ exception = T.unsafe(nil), string = T.unsafe(nil), array = T.unsafe(nil), cause: $ERROR_INFO,
137
+ # new arguments
138
+ bug: nil, silent: nil, depth: 1
139
+ )
140
+ if array
141
+ T.unsafe(Kernel).raise(exception, string, array, cause: cause)
142
+ elsif string
143
+ T.unsafe(Kernel).raise(exception, string, Kernel.caller(depth), cause: cause)
144
+ elsif exception.is_a?(String)
145
+ T.unsafe(Kernel).raise(RuntimeError, exception, Kernel.caller(depth), cause: cause)
146
+ else
147
+ T.unsafe(Kernel).raise(exception, exception.message, Kernel.caller(depth), cause: cause)
148
+ end
149
+ rescue Exception => e # rubocop:disable Lint/RescueException
150
+ e.bug!(bug) unless bug.nil?
151
+ e.silent!(silent) unless silent.nil?
152
+ Kernel.raise(e, cause: cause)
153
+ end
154
+ end
59
155
  end
60
156
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cli-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
@@ -10,22 +10,22 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2021-11-10 00:00:00.000000000 Z
13
+ date: 2022-11-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: cli-ui
17
17
  requirement: !ruby/object:Gem::Requirement
18
18
  requirements:
19
- - - ">="
19
+ - - "~>"
20
20
  - !ruby/object:Gem::Version
21
- version: 1.1.4
21
+ version: '2.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
- - - ">="
26
+ - - "~>"
27
27
  - !ruby/object:Gem::Version
28
- version: 1.1.4
28
+ version: '2.0'
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: bundler
31
31
  requirement: !ruby/object:Gem::Requirement
@@ -80,9 +80,10 @@ extra_rdoc_files: []
80
80
  files:
81
81
  - ".github/CODEOWNERS"
82
82
  - ".github/dependabot.yml"
83
- - ".github/probots.yml"
83
+ - ".github/workflows/cla.yml"
84
84
  - ".github/workflows/ruby.yml"
85
85
  - ".gitignore"
86
+ - ".rubocop.sorbet.yml"
86
87
  - ".rubocop.yml"
87
88
  - Gemfile
88
89
  - Gemfile.lock
@@ -91,6 +92,8 @@ files:
91
92
  - Rakefile
92
93
  - TODO.md
93
94
  - bin/console
95
+ - bin/onchange
96
+ - bin/tapioca
94
97
  - bin/test_gen
95
98
  - bin/testunit
96
99
  - cli-kit.gemspec
@@ -106,6 +109,7 @@ files:
106
109
  - gen/lib/gen/commands/new.rb
107
110
  - gen/lib/gen/entry_point.rb
108
111
  - gen/lib/gen/generator.rb
112
+ - gen/lib/gen/help.rb
109
113
  - gen/template/.gitignore
110
114
  - gen/template/Gemfile
111
115
  - gen/template/README.md
@@ -123,17 +127,25 @@ files:
123
127
  - gen/template/test/example_test.rb
124
128
  - gen/template/test/test_helper.rb
125
129
  - lib/cli/kit.rb
126
- - lib/cli/kit/autocall.rb
130
+ - lib/cli/kit/args.rb
131
+ - lib/cli/kit/args/definition.rb
132
+ - lib/cli/kit/args/evaluation.rb
133
+ - lib/cli/kit/args/parser.rb
134
+ - lib/cli/kit/args/parser/node.rb
135
+ - lib/cli/kit/args/tokenizer.rb
127
136
  - lib/cli/kit/base_command.rb
137
+ - lib/cli/kit/command_help.rb
128
138
  - lib/cli/kit/command_registry.rb
129
139
  - lib/cli/kit/config.rb
140
+ - lib/cli/kit/core_ext.rb
130
141
  - lib/cli/kit/error_handler.rb
131
142
  - lib/cli/kit/executor.rb
132
143
  - lib/cli/kit/ini.rb
133
144
  - lib/cli/kit/levenshtein.rb
134
145
  - lib/cli/kit/logger.rb
146
+ - lib/cli/kit/opts.rb
135
147
  - lib/cli/kit/resolver.rb
136
- - lib/cli/kit/ruby_backports/enumerable.rb
148
+ - lib/cli/kit/sorbet_runtime_stub.rb
137
149
  - lib/cli/kit/support.rb
138
150
  - lib/cli/kit/support/test_helper.rb
139
151
  - lib/cli/kit/system.rb
@@ -158,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
170
  - !ruby/object:Gem::Version
159
171
  version: '0'
160
172
  requirements: []
161
- rubygems_version: 3.2.20
173
+ rubygems_version: 3.2.33
162
174
  signing_key:
163
175
  specification_version: 4
164
176
  summary: Terminal UI framework extensions
data/.github/probots.yml DELETED
@@ -1,2 +0,0 @@
1
- enabled:
2
- - cla
@@ -1,21 +0,0 @@
1
- require 'cli/kit'
2
-
3
- module CLI
4
- module Kit
5
- module Autocall
6
- def autocall(const, &block)
7
- @autocalls ||= {}
8
- @autocalls[const] = block
9
- end
10
-
11
- def const_missing(const)
12
- block = begin
13
- @autocalls.fetch(const)
14
- rescue KeyError
15
- return super
16
- end
17
- const_set(const, block.call)
18
- end
19
- end
20
- end
21
- end
@@ -1,6 +0,0 @@
1
- module Enumerable
2
- def min_by(n = nil, &block)
3
- return sort_by(&block).first unless n
4
- sort_by(&block).first(n)
5
- end if instance_method(:min_by).arity == 0
6
- end