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
@@ -1,89 +1,141 @@
1
+ # typed: true
2
+
1
3
  require 'cli/kit'
2
4
 
3
5
  module CLI
4
6
  module Kit
5
7
  class CommandRegistry
6
- attr_reader :commands, :aliases
8
+ extend T::Sig
7
9
 
8
- module NullContextualResolver
9
- def self.command_names
10
- []
11
- end
10
+ CommandOrProc = T.type_alias do
11
+ T.any(T.class_of(CLI::Kit::BaseCommand), T.proc.returns(T.class_of(CLI::Kit::BaseCommand)))
12
+ end
12
13
 
13
- def self.aliases
14
- {}
15
- end
14
+ sig { returns(T::Hash[String, CommandOrProc]) }
15
+ attr_reader :commands
16
16
 
17
- def self.command_class(_name)
18
- nil
17
+ sig { returns(T::Hash[String, String]) }
18
+ attr_reader :aliases
19
+
20
+ module ContextualResolver
21
+ extend T::Sig
22
+ extend T::Helpers
23
+ interface!
24
+
25
+ sig { abstract.returns(T::Array[String]) }
26
+ def command_names; end
27
+
28
+ sig { abstract.returns(T::Hash[String, String]) }
29
+ def aliases; end
30
+
31
+ sig { abstract.params(_name: String).returns(T.class_of(CLI::Kit::BaseCommand)) }
32
+ def command_class(_name); end
33
+ end
34
+
35
+ module NullContextualResolver
36
+ extend T::Sig
37
+ extend ContextualResolver
38
+
39
+ class << self
40
+ extend T::Sig
41
+
42
+ sig { override.returns(T::Array[String]) }
43
+ def command_names
44
+ []
45
+ end
46
+
47
+ sig { override.returns(T::Hash[String, String]) }
48
+ def aliases
49
+ {}
50
+ end
51
+
52
+ sig { override.params(_name: String).returns(T.class_of(CLI::Kit::BaseCommand)) }
53
+ def command_class(_name)
54
+ raise(CLI::Kit::Abort, 'Cannot be called on the NullContextualResolver since command_names is empty')
55
+ end
19
56
  end
20
57
  end
21
58
 
22
- def initialize(default:, contextual_resolver: nil)
59
+ sig { params(default: String, contextual_resolver: ContextualResolver).void }
60
+ def initialize(default:, contextual_resolver: NullContextualResolver)
23
61
  @commands = {}
24
62
  @aliases = {}
25
63
  @default = default
26
- @contextual_resolver = contextual_resolver || NullContextualResolver
64
+ @contextual_resolver = contextual_resolver
27
65
  end
28
66
 
67
+ sig { returns(T::Hash[String, T.class_of(CLI::Kit::BaseCommand)]) }
29
68
  def resolved_commands
30
69
  @commands.each_with_object({}) do |(k, v), a|
31
70
  a[k] = resolve_class(v)
32
71
  end
33
72
  end
34
73
 
74
+ sig { params(const: CommandOrProc, name: String).void }
35
75
  def add(const, name)
36
76
  commands[name] = const
37
77
  end
38
78
 
79
+ sig { params(name: T.nilable(String)).returns([T.nilable(T.class_of(CLI::Kit::BaseCommand)), String]) }
39
80
  def lookup_command(name)
40
81
  name = @default if name.to_s.empty?
41
- resolve_command(name)
82
+ resolve_command(T.must(name))
42
83
  end
43
84
 
85
+ sig { params(from: String, to: String).void }
44
86
  def add_alias(from, to)
45
87
  aliases[from] = to unless aliases[from]
46
88
  end
47
89
 
90
+ sig { returns(T::Array[String]) }
48
91
  def command_names
49
92
  @contextual_resolver.command_names + commands.keys
50
93
  end
51
94
 
95
+ sig { params(name: String).returns(T::Boolean) }
52
96
  def exist?(name)
53
97
  !resolve_command(name).first.nil?
54
98
  end
55
99
 
56
100
  private
57
101
 
102
+ sig { params(name: String).returns(String) }
58
103
  def resolve_alias(name)
59
104
  aliases[name] || @contextual_resolver.aliases.fetch(name, name)
60
105
  end
61
106
 
107
+ sig { params(name: String).returns([T.nilable(T.class_of(CLI::Kit::BaseCommand)), String]) }
62
108
  def resolve_command(name)
63
109
  name = resolve_alias(name)
64
- resolve_global_command(name) || \
65
- resolve_contextual_command(name) || \
110
+ resolve_global_command(name) ||
111
+ resolve_contextual_command(name) ||
66
112
  [nil, name]
67
113
  end
68
114
 
115
+ sig { params(name: String).returns(T.nilable([T.class_of(CLI::Kit::BaseCommand), String])) }
69
116
  def resolve_global_command(name)
70
117
  klass = resolve_class(commands.fetch(name, nil))
71
- return nil unless klass&.defined?
118
+ return unless klass
119
+
72
120
  [klass, name]
73
121
  rescue NameError
74
122
  nil
75
123
  end
76
124
 
125
+ sig { params(name: String).returns(T.nilable([T.class_of(CLI::Kit::BaseCommand), String])) }
77
126
  def resolve_contextual_command(name)
78
127
  found = @contextual_resolver.command_names.include?(name)
79
- return nil unless found
128
+ return unless found
129
+
80
130
  [@contextual_resolver.command_class(name), name]
81
131
  end
82
132
 
133
+ sig { params(class_or_proc: T.nilable(CommandOrProc)).returns(T.nilable(T.class_of(CLI::Kit::BaseCommand))) }
83
134
  def resolve_class(class_or_proc)
84
- if class_or_proc.is_a?(Class)
85
- class_or_proc
86
- elsif class_or_proc.respond_to?(:call)
135
+ case class_or_proc
136
+ when nil
137
+ nil
138
+ when Proc
87
139
  class_or_proc.call
88
140
  else
89
141
  class_or_proc
@@ -1,11 +1,16 @@
1
+ # typed: true
2
+
1
3
  require 'cli/kit'
2
4
  require 'fileutils'
3
5
 
4
6
  module CLI
5
7
  module Kit
6
8
  class Config
9
+ extend T::Sig
10
+
7
11
  XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
8
12
 
13
+ sig { params(tool_name: String).void }
9
14
  def initialize(tool_name:)
10
15
  @tool_name = tool_name
11
16
  end
@@ -23,18 +28,20 @@ module CLI
23
28
  # #### Example Usage
24
29
  # `config.get('name.of.config')`
25
30
  #
31
+ sig { params(section: String, name: String, default: T.nilable(String)).returns(T.nilable(String)) }
26
32
  def get(section, name, default: nil)
27
33
  all_configs.dig("[#{section}]", name) || default
28
34
  end
29
35
 
30
36
  # Coalesce and enforce the value of a config to a boolean
37
+ sig { params(section: String, name: String, default: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
31
38
  def get_bool(section, name, default: false)
32
- case get(section, name, default: default)
39
+ case get(section, name)
33
40
  when 'true'
34
41
  true
35
42
  when 'false'
36
43
  false
37
- when default
44
+ when nil
38
45
  default
39
46
  else
40
47
  raise CLI::Kit::Abort, "Invalid config: #{section}.#{name} is expected to be true or false"
@@ -51,9 +58,15 @@ module CLI
51
58
  # #### Example Usage
52
59
  # `config.set('section', 'name.of.config', 'value')`
53
60
  #
61
+ sig { params(section: String, name: String, value: T.nilable(T.any(String, T::Boolean))).void }
54
62
  def set(section, name, value)
55
63
  all_configs["[#{section}]"] ||= {}
56
- all_configs["[#{section}]"][name] = value.nil? ? nil : value.to_s
64
+ case value
65
+ when nil
66
+ T.must(all_configs["[#{section}]"]).delete(name)
67
+ else
68
+ T.must(all_configs["[#{section}]"])[name] = value.to_s
69
+ end
57
70
  write_config
58
71
  end
59
72
 
@@ -66,6 +79,7 @@ module CLI
66
79
  # #### Example Usage
67
80
  # `config.unset('section', 'name.of.config')`
68
81
  #
82
+ sig { params(section: String, name: String).void }
69
83
  def unset(section, name)
70
84
  set(section, name, nil)
71
85
  end
@@ -78,24 +92,12 @@ module CLI
78
92
  # #### Example Usage
79
93
  # `config.get_section('section')`
80
94
  #
95
+ sig { params(section: String).returns(T::Hash[String, String]) }
81
96
  def get_section(section)
82
97
  (all_configs["[#{section}]"] || {}).dup
83
98
  end
84
99
 
85
- # Returns a path from config in expanded form
86
- # e.g. shopify corresponds to ~/src/shopify, but is expanded to /Users/name/src/shopify
87
- #
88
- # #### Example Usage
89
- # `config.get_path('srcpath', 'shopify')`
90
- #
91
- # #### Returns
92
- # `path` : the expanded path to the corrsponding value
93
- #
94
- def get_path(section, name = nil)
95
- v = get(section, name)
96
- false == v ? v : File.expand_path(v)
97
- end
98
-
100
+ sig { returns(String) }
99
101
  def to_s
100
102
  ini.to_s
101
103
  end
@@ -105,6 +107,7 @@ module CLI
105
107
  # if ENV['XDG_CONFIG_HOME'] is not set, we default to ~/.config, e.g.:
106
108
  # ~/.config/tool/config
107
109
  #
110
+ sig { returns(String) }
108
111
  def file
109
112
  config_home = ENV.fetch(XDG_CONFIG_HOME, '~/.config')
110
113
  File.expand_path(File.join(@tool_name, 'config'), config_home)
@@ -112,20 +115,20 @@ module CLI
112
115
 
113
116
  private
114
117
 
118
+ sig { returns(T::Hash[String, T::Hash[String, String]]) }
115
119
  def all_configs
116
120
  ini.ini
117
121
  end
118
122
 
123
+ sig { returns(CLI::Kit::Ini) }
119
124
  def ini
120
- @ini ||= CLI::Kit::Ini
121
- .new(file, default_section: '[global]', convert_types: false)
122
- .tap(&:parse)
125
+ @ini ||= CLI::Kit::Ini.new(file).tap(&:parse)
123
126
  end
124
127
 
128
+ sig { void }
125
129
  def write_config
126
130
  all_configs.each do |section, sub_config|
127
- all_configs[section] = sub_config.reject { |_, value| value.nil? }
128
- all_configs.delete(section) if all_configs[section].empty?
131
+ all_configs.delete(section) if sub_config.empty?
129
132
  end
130
133
  FileUtils.mkdir_p(File.dirname(file))
131
134
  File.write(file, to_s)
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class Exception
5
+ extend(T::Sig)
6
+
7
+ # You'd think instance variables @bug and @silent would work here. They
8
+ # don't. I'm not sure why. If you, the reader, want to take some time to
9
+ # figure it out, go ahead and refactor to that.
10
+
11
+ sig { returns(T::Boolean) }
12
+ def bug?
13
+ true
14
+ end
15
+
16
+ sig { returns(T::Boolean) }
17
+ def silent?
18
+ false
19
+ end
20
+
21
+ sig { params(bug: T::Boolean).void }
22
+ def bug!(bug = true)
23
+ singleton_class.define_method(:bug?) { bug }
24
+ end
25
+
26
+ sig { params(silent: T::Boolean).void }
27
+ def silent!(silent = true)
28
+ singleton_class.define_method(:silent?) { silent }
29
+ end
30
+ end
@@ -1,121 +1,182 @@
1
+ # typed: true
2
+
1
3
  require 'cli/kit'
2
4
  require 'English'
3
5
 
4
6
  module CLI
5
7
  module Kit
6
8
  class ErrorHandler
7
- def initialize(log_file:, exception_reporter:, tool_name: nil)
9
+ extend T::Sig
10
+
11
+ ExceptionReporterOrProc = T.type_alias do
12
+ T.any(T.class_of(ExceptionReporter), T.proc.returns(T.class_of(ExceptionReporter)))
13
+ end
14
+
15
+ sig { params(override_exception_handler: T.proc.params(arg0: Exception).returns(Integer)).void }
16
+ attr_writer :override_exception_handler
17
+
18
+ sig do
19
+ params(
20
+ log_file: T.nilable(String),
21
+ exception_reporter: ExceptionReporterOrProc,
22
+ tool_name: T.nilable(String),
23
+ dev_mode: T::Boolean,
24
+ ).void
25
+ end
26
+ def initialize(log_file: nil, exception_reporter: NullExceptionReporter, tool_name: nil, dev_mode: false)
8
27
  @log_file = log_file
9
- @exception_reporter_or_proc = exception_reporter || NullExceptionReporter
28
+ @exception_reporter_or_proc = exception_reporter
10
29
  @tool_name = tool_name
30
+ @dev_mode = dev_mode
31
+ end
32
+
33
+ class ExceptionReporter
34
+ extend T::Sig
35
+ extend T::Helpers
36
+ abstract!
37
+
38
+ class << self
39
+ extend T::Sig
40
+
41
+ sig { abstract.params(exception: T.nilable(Exception), logs: T.nilable(String)).void }
42
+ def report(exception, logs = nil); end
43
+ end
11
44
  end
12
45
 
13
- module NullExceptionReporter
14
- def self.report(_exception, _logs)
15
- nil
46
+ class NullExceptionReporter < ExceptionReporter
47
+ extend T::Sig
48
+
49
+ class << self
50
+ extend T::Sig
51
+
52
+ sig { override.params(_exception: T.nilable(Exception), _logs: T.nilable(String)).void }
53
+ def report(_exception, _logs = nil)
54
+ nil
55
+ end
16
56
  end
17
57
  end
18
58
 
59
+ sig { params(block: T.proc.void).returns(Integer) }
19
60
  def call(&block)
20
- install!
21
- handle_abort(&block)
61
+ # @at_exit_exception is set if handle_abort decides to submit an error.
62
+ # $ERROR_INFO is set if we terminate because of a signal.
63
+ at_exit { report_exception(@at_exit_exception || $ERROR_INFO) }
64
+ triage_all_exceptions(&block)
22
65
  end
23
66
 
24
- def handle_exception(error)
67
+ sig { params(error: T.nilable(Exception)).void }
68
+ def report_exception(error)
25
69
  if (notify_with = exception_for_submission(error))
26
- logs = begin
27
- File.read(@log_file)
28
- rescue => e
29
- "(#{e.class}: #{e.message})"
70
+ logs = nil
71
+ if @log_file
72
+ logs = begin
73
+ File.read(@log_file)
74
+ rescue => e
75
+ "(#{e.class}: #{e.message})"
76
+ end
30
77
  end
31
78
  exception_reporter.report(notify_with, logs)
32
79
  end
33
80
  end
34
81
 
35
- # maybe we can get rid of this.
36
- attr_writer :exception
82
+ SIGNALS_THAT_ARENT_BUGS = [
83
+ 'SIGTERM', 'SIGHUP', 'SIGINT',
84
+ ].freeze
37
85
 
38
86
  private
39
87
 
88
+ # Run the program, handling any errors that occur.
89
+ #
90
+ # Errors are printed to stderr unless they're #silent?, and are reported
91
+ # to bugsnag (by setting @at_exit_exeption for our at_exit handler) if
92
+ # they're #bug?
93
+ #
94
+ # Returns an exit status for the program.
95
+ sig { params(block: T.proc.void).returns(Integer) }
96
+ def triage_all_exceptions(&block)
97
+ begin
98
+ block.call
99
+ CLI::Kit::EXIT_SUCCESS
100
+ rescue Interrupt => e # Ctrl-C
101
+ # transform message, prevent bugsnag
102
+ exc = e.exception('Interrupt')
103
+ CLI::Kit.raise(exc, bug: false)
104
+ rescue Errno::ENOSPC => e
105
+ # transform message, prevent bugsnag
106
+ message = if @tool_name
107
+ "Your disk is full - {{command:#{@tool_name}}} requires free space to operate"
108
+ else
109
+ 'Your disk is full - free space is required to operate'
110
+ end
111
+ exc = e.exception(message)
112
+ CLI::Kit.raise(exc, bug: false)
113
+ end
114
+ # If SystemExit was raised, e.g. `exit()`, then
115
+ # return whatever status is attached to the exception
116
+ # object. The special exit statuses have already been
117
+ # handled below.
118
+ rescue SystemExit => e
119
+ e.status
120
+ rescue Exception => e # rubocop:disable Lint/RescueException
121
+ @at_exit_exception = e if e.bug?
122
+
123
+ if (eh = @override_exception_handler)
124
+ return eh.call(e)
125
+ end
126
+
127
+ raise(e) if @dev_mode && e.bug?
128
+
129
+ stderr_puts(e.message) unless e.silent?
130
+ e.bug? ? CLI::Kit::EXIT_BUG : CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
131
+ end
132
+
133
+ sig { params(error: T.nilable(Exception)).returns(T.nilable(Exception)) }
40
134
  def exception_for_submission(error)
135
+ # happens on normal non-error termination
136
+ return(nil) if error.nil?
137
+
138
+ return(nil) unless error.bug?
139
+
41
140
  case error
42
- when nil # normal, non-error termination
43
- nil
44
- when Interrupt # ctrl-c
45
- nil
46
- when CLI::Kit::Abort, CLI::Kit::AbortSilent # Not a bug
47
- nil
48
141
  when SignalException
49
- skip = ['SIGTERM', 'SIGHUP', 'SIGINT']
50
- skip.include?(error.message) ? nil : error
142
+ SIGNALS_THAT_ARENT_BUGS.include?(error.message) ? nil : error
51
143
  when SystemExit # "exit N" called
52
144
  case error.status
53
145
  when CLI::Kit::EXIT_SUCCESS # submit nothing if it was `exit 0`
54
146
  nil
55
147
  when CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
56
- # if it was `exit 30`, translate the exit code to 1, and submit nothing.
57
- # 30 is used to signal normal failures that are not indicative of bugs.
58
- # However, users should see it presented as 1.
148
+ # if it was `exit 30`, translate the exit code to 1, and submit
149
+ # nothing. 30 is used to signal normal failures that are not
150
+ # indicative of bugs. However, users should see it presented as 1.
59
151
  exit(1)
60
152
  else
61
- # A weird termination status happened. `error.exception "message"` will maintain backtrace
62
- # but allow us to set a message
63
- error.exception("abnormal termination status: #{error.status}")
153
+ # don't treat this as an exception, simply reraise.
154
+ # this is indicative of `exit` being called with a
155
+ # non-zero number, and the requested exit status
156
+ # needs to be maintained.
157
+ exit(error.status)
64
158
  end
65
159
  else
66
160
  error
67
161
  end
68
162
  end
69
163
 
70
- def install!
71
- at_exit { handle_exception(@exception || $ERROR_INFO) }
72
- end
73
-
74
- def handle_abort
75
- yield
76
- CLI::Kit::EXIT_SUCCESS
77
- rescue CLI::Kit::GenericAbort => e
78
- is_bug = e.is_a?(CLI::Kit::Bug) || e.is_a?(CLI::Kit::BugSilent)
79
- is_silent = e.is_a?(CLI::Kit::AbortSilent) || e.is_a?(CLI::Kit::BugSilent)
80
-
81
- print_error_message(e) unless is_silent
82
- (@exception = e) if is_bug
83
-
84
- CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
85
- rescue Interrupt
86
- stderr_puts_message('Interrupt')
87
- CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
88
- rescue Errno::ENOSPC
89
- message = if @tool_name
90
- "Your disk is full - {{command:#{@tool_name}}} requires free space to operate"
91
- else
92
- 'Your disk is full - free space is required to operate'
93
- end
94
- stderr_puts_message(message)
95
- CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
96
- end
97
-
98
- def stderr_puts_message(message)
99
- $stderr.puts(format_error_message(message))
100
- rescue Errno::EPIPE
164
+ sig { params(message: String).void }
165
+ def stderr_puts(message)
166
+ $stderr.puts(CLI::UI.fmt("{{red:#{message}}}"))
167
+ rescue Errno::EPIPE, Errno::EIO
101
168
  nil
102
169
  end
103
170
 
171
+ sig { returns(T.class_of(ExceptionReporter)) }
104
172
  def exception_reporter
105
- if @exception_reporter_or_proc.respond_to?(:report)
106
- @exception_reporter_or_proc
107
- else
173
+ case @exception_reporter_or_proc
174
+ when Proc
108
175
  @exception_reporter_or_proc.call
176
+ else
177
+ @exception_reporter_or_proc
109
178
  end
110
179
  end
111
-
112
- def format_error_message(msg)
113
- CLI::UI.fmt("{{red:#{msg}}}")
114
- end
115
-
116
- def print_error_message(e)
117
- $stderr.puts(format_error_message(e.message))
118
- end
119
180
  end
120
181
  end
121
182
  end
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  require 'cli/kit'
2
4
  require 'English'
3
5
  require 'fileutils'
@@ -5,11 +7,15 @@ require 'fileutils'
5
7
  module CLI
6
8
  module Kit
7
9
  class Executor
10
+ extend T::Sig
11
+
12
+ sig { params(log_file: String).void }
8
13
  def initialize(log_file:)
9
14
  FileUtils.mkpath(File.dirname(log_file))
10
15
  @log_file = log_file
11
16
  end
12
17
 
18
+ sig { params(command: T.class_of(CLI::Kit::BaseCommand), command_name: String, args: T::Array[String]).void }
13
19
  def call(command, command_name, args)
14
20
  with_traps do
15
21
  with_logging do |id|
@@ -30,22 +36,31 @@ module CLI
30
36
 
31
37
  private
32
38
 
39
+ sig do
40
+ type_parameters(:T).params(block: T.proc.params(id: String).returns(T.type_parameter(:T)))
41
+ .returns(T.type_parameter(:T))
42
+ end
33
43
  def with_logging(&block)
34
- return yield unless @log_file
35
44
  CLI::UI.log_output_to(@log_file) do
36
- CLI::UI::StdoutRouter.with_id(on_streams: [CLI::UI::StdoutRouter.duplicate_output_to]) do |id|
45
+ CLI::UI::StdoutRouter.with_id(on_streams: [CLI::UI::StdoutRouter.duplicate_output_to].compact) do |id|
37
46
  block.call(id)
38
47
  end
39
48
  end
40
49
  end
41
50
 
51
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
42
52
  def with_traps(&block)
43
53
  twrap('QUIT', method(:quit_handler)) do
44
54
  twrap('INFO', method(:info_handler), &block)
45
55
  end
46
56
  end
47
57
 
48
- def twrap(signal, handler)
58
+ sig do
59
+ type_parameters(:T)
60
+ .params(signal: String, handler: Method, block: T.proc.returns(T.type_parameter(:T)))
61
+ .returns(T.type_parameter(:T))
62
+ end
63
+ def twrap(signal, handler, &block)
49
64
  return yield unless Signal.list.key?(signal)
50
65
 
51
66
  begin
@@ -63,6 +78,7 @@ module CLI
63
78
  end
64
79
  end
65
80
 
81
+ sig { params(_sig: T.untyped).void }
66
82
  def quit_handler(_sig)
67
83
  z = caller
68
84
  CLI::UI.raw do
@@ -72,6 +88,7 @@ module CLI
72
88
  exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
73
89
  end
74
90
 
91
+ sig { params(_sig: T.untyped).void }
75
92
  def info_handler(_sig)
76
93
  z = caller
77
94
  CLI::UI.raw do