cli-kit 3.3.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +10 -0
  3. data/.github/workflows/cla.yml +22 -0
  4. data/.github/workflows/ruby.yml +64 -0
  5. data/.gitignore +2 -0
  6. data/.rubocop.sorbet.yml +47 -0
  7. data/.rubocop.yml +22 -13
  8. data/Gemfile +13 -3
  9. data/Gemfile.lock +110 -28
  10. data/README.md +46 -3
  11. data/Rakefile +28 -1
  12. data/bin/console +3 -3
  13. data/bin/onchange +30 -0
  14. data/bin/tapioca +29 -0
  15. data/bin/test_gen +4 -1
  16. data/bin/testunit +3 -2
  17. data/cli-kit.gemspec +7 -6
  18. data/dev.yml +35 -3
  19. data/examples/minimal/example.rb +5 -3
  20. data/examples/single-file/example.rb +25 -35
  21. data/gen/lib/gen/commands/help.rb +8 -10
  22. data/gen/lib/gen/commands/new.rb +23 -9
  23. data/gen/lib/gen/commands.rb +21 -9
  24. data/gen/lib/gen/entry_point.rb +12 -3
  25. data/gen/lib/gen/generator.rb +39 -18
  26. data/gen/lib/gen/help.rb +63 -0
  27. data/gen/lib/gen.rb +18 -23
  28. data/gen/template/bin/update-deps +2 -2
  29. data/gen/template/lib/__app__/commands.rb +1 -4
  30. data/gen/template/lib/__app__.rb +8 -17
  31. data/gen/template/test/example_test.rb +1 -1
  32. data/lib/cli/kit/args/definition.rb +344 -0
  33. data/lib/cli/kit/args/evaluation.rb +245 -0
  34. data/lib/cli/kit/args/parser/node.rb +132 -0
  35. data/lib/cli/kit/args/parser.rb +129 -0
  36. data/lib/cli/kit/args/tokenizer.rb +133 -0
  37. data/lib/cli/kit/args.rb +16 -0
  38. data/lib/cli/kit/base_command.rb +17 -32
  39. data/lib/cli/kit/command_help.rb +271 -0
  40. data/lib/cli/kit/command_registry.rb +69 -17
  41. data/lib/cli/kit/config.rb +30 -25
  42. data/lib/cli/kit/core_ext.rb +30 -0
  43. data/lib/cli/kit/error_handler.rb +134 -67
  44. data/lib/cli/kit/executor.rb +39 -20
  45. data/lib/cli/kit/ini.rb +32 -39
  46. data/lib/cli/kit/levenshtein.rb +12 -4
  47. data/lib/cli/kit/logger.rb +23 -3
  48. data/lib/cli/kit/opts.rb +301 -0
  49. data/lib/cli/kit/resolver.rb +10 -2
  50. data/lib/cli/kit/sorbet_runtime_stub.rb +156 -0
  51. data/lib/cli/kit/support/test_helper.rb +31 -22
  52. data/lib/cli/kit/support.rb +2 -0
  53. data/lib/cli/kit/system.rb +217 -48
  54. data/lib/cli/kit/util.rb +52 -107
  55. data/lib/cli/kit/version.rb +3 -1
  56. data/lib/cli/kit.rb +104 -8
  57. metadata +35 -22
  58. data/.github/probots.yml +0 -2
  59. data/.travis.yml +0 -14
  60. data/lib/cli/kit/autocall.rb +0 -21
  61. 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
@@ -18,22 +23,26 @@ module CLI
18
23
  # `name` : the name of the config value you are looking for
19
24
  #
20
25
  # #### Returns
21
- # `value` : the value of the config variable (false if none)
26
+ # `value` : the value of the config variable (nil if none)
22
27
  #
23
28
  # #### Example Usage
24
29
  # `config.get('name.of.config')`
25
30
  #
26
- def get(section, name, default: false)
31
+ sig { params(section: String, name: String, default: T.nilable(String)).returns(T.nilable(String)) }
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).to_s
33
- when "true"
39
+ case get(section, name)
40
+ when 'true'
34
41
  true
35
- when "false"
42
+ when 'false'
36
43
  false
44
+ when nil
45
+ default
37
46
  else
38
47
  raise CLI::Kit::Abort, "Invalid config: #{section}.#{name} is expected to be true or false"
39
48
  end
@@ -49,9 +58,15 @@ module CLI
49
58
  # #### Example Usage
50
59
  # `config.set('section', 'name.of.config', 'value')`
51
60
  #
61
+ sig { params(section: String, name: String, value: T.nilable(T.any(String, T::Boolean))).void }
52
62
  def set(section, name, value)
53
63
  all_configs["[#{section}]"] ||= {}
54
- 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
55
70
  write_config
56
71
  end
57
72
 
@@ -64,6 +79,7 @@ module CLI
64
79
  # #### Example Usage
65
80
  # `config.unset('section', 'name.of.config')`
66
81
  #
82
+ sig { params(section: String, name: String).void }
67
83
  def unset(section, name)
68
84
  set(section, name, nil)
69
85
  end
@@ -76,24 +92,12 @@ module CLI
76
92
  # #### Example Usage
77
93
  # `config.get_section('section')`
78
94
  #
95
+ sig { params(section: String).returns(T::Hash[String, String]) }
79
96
  def get_section(section)
80
97
  (all_configs["[#{section}]"] || {}).dup
81
98
  end
82
99
 
83
- # Returns a path from config in expanded form
84
- # e.g. shopify corresponds to ~/src/shopify, but is expanded to /Users/name/src/shopify
85
- #
86
- # #### Example Usage
87
- # `config.get_path('srcpath', 'shopify')`
88
- #
89
- # #### Returns
90
- # `path` : the expanded path to the corrsponding value
91
- #
92
- def get_path(section, name = nil)
93
- v = get(section, name)
94
- false == v ? v : File.expand_path(v)
95
- end
96
-
100
+ sig { returns(String) }
97
101
  def to_s
98
102
  ini.to_s
99
103
  end
@@ -103,6 +107,7 @@ module CLI
103
107
  # if ENV['XDG_CONFIG_HOME'] is not set, we default to ~/.config, e.g.:
104
108
  # ~/.config/tool/config
105
109
  #
110
+ sig { returns(String) }
106
111
  def file
107
112
  config_home = ENV.fetch(XDG_CONFIG_HOME, '~/.config')
108
113
  File.expand_path(File.join(@tool_name, 'config'), config_home)
@@ -110,20 +115,20 @@ module CLI
110
115
 
111
116
  private
112
117
 
118
+ sig { returns(T::Hash[String, T::Hash[String, String]]) }
113
119
  def all_configs
114
120
  ini.ini
115
121
  end
116
122
 
123
+ sig { returns(CLI::Kit::Ini) }
117
124
  def ini
118
- @ini ||= CLI::Kit::Ini
119
- .new(file, default_section: "[global]", convert_types: false)
120
- .tap(&:parse)
125
+ @ini ||= CLI::Kit::Ini.new(file).tap(&:parse)
121
126
  end
122
127
 
128
+ sig { void }
123
129
  def write_config
124
130
  all_configs.each do |section, sub_config|
125
- all_configs[section] = sub_config.reject { |_, value| value.nil? }
126
- all_configs.delete(section) if all_configs[section].empty?
131
+ all_configs.delete(section) if sub_config.empty?
127
132
  end
128
133
  FileUtils.mkdir_p(File.dirname(file))
129
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,115 +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
11
31
  end
12
32
 
13
- module NullExceptionReporter
14
- def self.report(_exception, _logs)
15
- nil
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
16
43
  end
17
44
  end
18
45
 
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
56
+ end
57
+ end
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)
25
- if notify_with = exception_for_submission(error)
26
- logs = begin
27
- File.read(@log_file)
28
- rescue => e
29
- "(#{e.class}: #{e.message})"
67
+ sig { params(error: T.nilable(Exception)).void }
68
+ def report_exception(error)
69
+ if (notify_with = exception_for_submission(error))
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 = %w(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.
59
- exit 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.
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(format_error_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(format_error_message(message))
95
- CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
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
168
+ nil
96
169
  end
97
170
 
171
+ sig { returns(T.class_of(ExceptionReporter)) }
98
172
  def exception_reporter
99
- if @exception_reporter_or_proc.respond_to?(:report)
100
- @exception_reporter_or_proc
101
- else
173
+ case @exception_reporter_or_proc
174
+ when Proc
102
175
  @exception_reporter_or_proc.call
176
+ else
177
+ @exception_reporter_or_proc
103
178
  end
104
179
  end
105
-
106
- def format_error_message(msg)
107
- CLI::UI.fmt("{{red:#{msg}}}")
108
- end
109
-
110
- def print_error_message(e)
111
- $stderr.puts(format_error_message(e.message))
112
- end
113
180
  end
114
181
  end
115
182
  end
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  require 'cli/kit'
2
4
  require 'English'
3
5
  require 'fileutils'
@@ -5,61 +7,77 @@ 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|
22
+ command.call(args, command_name)
23
+ rescue => e
16
24
  begin
17
- command.call(args, command_name)
18
- rescue => e
19
- begin
20
- $stderr.puts "This command ran with ID: #{id}"
21
- $stderr.puts "Please include this information in any issues/report along with relevant logs"
22
- rescue SystemCallError
23
- # Outputting to stderr is best-effort. Avoid raising another error when outputting debug info so that
24
- # we can detect and log the original error, which may even be the source of this error.
25
- nil
26
- end
27
- raise e
25
+ $stderr.puts "This command ran with ID: #{id}"
26
+ $stderr.puts 'Please include this information in any issues/report along with relevant logs'
27
+ rescue SystemCallError
28
+ # Outputting to stderr is best-effort. Avoid raising another error when outputting debug info so that
29
+ # we can detect and log the original error, which may even be the source of this error.
30
+ nil
28
31
  end
32
+ raise e
29
33
  end
30
34
  end
31
35
  end
32
36
 
33
37
  private
34
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
35
43
  def with_logging(&block)
36
- return yield unless @log_file
37
44
  CLI::UI.log_output_to(@log_file) do
38
- 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|
39
46
  block.call(id)
40
47
  end
41
48
  end
42
49
  end
43
50
 
44
- def with_traps
51
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
52
+ def with_traps(&block)
45
53
  twrap('QUIT', method(:quit_handler)) do
46
- twrap('INFO', method(:info_handler)) do
47
- yield
48
- end
54
+ twrap('INFO', method(:info_handler), &block)
49
55
  end
50
56
  end
51
57
 
52
- 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)
53
63
  return yield unless Signal.list.key?(signal)
54
64
 
55
65
  begin
56
- prev_handler = trap(signal, handler)
66
+ begin
67
+ prev_handler = trap(signal, handler)
68
+ installed = true
69
+ rescue ArgumentError
70
+ # If we couldn't install a signal handler because the signal is
71
+ # reserved, remember not to uninstall it later.
72
+ installed = false
73
+ end
57
74
  yield
58
75
  ensure
59
- trap(signal, prev_handler)
76
+ trap(signal, prev_handler) if installed
60
77
  end
61
78
  end
62
79
 
80
+ sig { params(_sig: T.untyped).void }
63
81
  def quit_handler(_sig)
64
82
  z = caller
65
83
  CLI::UI.raw do
@@ -69,6 +87,7 @@ module CLI
69
87
  exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
70
88
  end
71
89
 
90
+ sig { params(_sig: T.untyped).void }
72
91
  def info_handler(_sig)
73
92
  z = caller
74
93
  CLI::UI.raw do