cli-kit 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,64 +1,110 @@
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
110
  resolve_global_command(name) || \
@@ -66,24 +112,30 @@ module CLI
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 nil 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
128
  return nil 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: strong
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,30 @@ 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).params(signal: String, handler: Method,
60
+ block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T))
61
+ end
62
+ def twrap(signal, handler, &block)
49
63
  return yield unless Signal.list.key?(signal)
50
64
 
51
65
  begin
@@ -63,6 +77,7 @@ module CLI
63
77
  end
64
78
  end
65
79
 
80
+ sig { params(_sig: T.untyped).void }
66
81
  def quit_handler(_sig)
67
82
  z = caller
68
83
  CLI::UI.raw do
@@ -72,6 +87,7 @@ module CLI
72
87
  exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
73
88
  end
74
89
 
90
+ sig { params(_sig: T.untyped).void }
75
91
  def info_handler(_sig)
76
92
  z = caller
77
93
  CLI::UI.raw do