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
@@ -0,0 +1,156 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ module T
5
+ class << self
6
+ def absurd(value); end
7
+ def all(type_a, type_b, *types); end
8
+ def any(type_a, type_b, *types); end
9
+ def attached_class; end
10
+ def class_of(klass); end
11
+ def enum(values); end
12
+ def nilable(type); end
13
+ def noreturn; end
14
+ def self_type; end
15
+ def type_alias(type = nil, &_blk); end
16
+ def type_parameter(name); end
17
+ def untyped; end
18
+
19
+ def assert_type!(value, _type, _checked: true)
20
+ value
21
+ end
22
+
23
+ def cast(value, _type, _checked: true)
24
+ value
25
+ end
26
+
27
+ def let(value, _type, _checked: true)
28
+ value
29
+ end
30
+
31
+ def must(arg, _msg = nil)
32
+ arg
33
+ end
34
+
35
+ def proc
36
+ T::Proc.new
37
+ end
38
+
39
+ def reveal_type(value)
40
+ value
41
+ end
42
+
43
+ def unsafe(value)
44
+ value
45
+ end
46
+ end
47
+
48
+ module Sig
49
+ def sig(arg0 = nil, &blk); end
50
+ end
51
+
52
+ module Helpers
53
+ def abstract!; end
54
+ def interface!; end
55
+ def final!; end
56
+ def sealed!; end
57
+ def mixes_in_class_methods(mod); end
58
+ end
59
+
60
+ module Generic
61
+ include(T::Helpers)
62
+
63
+ def type_parameters(*params); end
64
+ def type_member(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
65
+ def type_template(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
66
+
67
+ def [](*types)
68
+ self
69
+ end
70
+ end
71
+
72
+ module Array
73
+ class << self
74
+ def [](type); end
75
+ end
76
+ end
77
+
78
+ Boolean = Object.new.freeze
79
+
80
+ module Configuration
81
+ class << self
82
+ def call_validation_error_handler(signature, opts); end
83
+ def call_validation_error_handler=(value); end
84
+ def default_checked_level=(default_checked_level); end
85
+ def enable_checking_for_sigs_marked_checked_tests; end
86
+ def enable_final_checks_on_hooks; end
87
+ def enable_legacy_t_enum_migration_mode; end
88
+ def reset_final_checks_on_hooks; end
89
+ def hard_assert_handler(str, extra); end
90
+ def hard_assert_handler=(value); end
91
+ def inline_type_error_handler(error); end
92
+ def inline_type_error_handler=(value); end
93
+ def log_info_handler(str, extra); end
94
+ def log_info_handler=(value); end
95
+ def scalar_types; end
96
+ def scalar_types=(values); end
97
+ # rubocop:disable Naming/InclusiveLanguage
98
+ def sealed_violation_whitelist; end
99
+ def sealed_violation_whitelist=(sealed_violation_whitelist); end
100
+ # rubocop:enable Naming/InclusiveLanguage
101
+ def sig_builder_error_handler=(value); end
102
+ def sig_validation_error_handler(error, opts); end
103
+ def sig_validation_error_handler=(value); end
104
+ def soft_assert_handler(str, extra); end
105
+ def soft_assert_handler=(value); end
106
+ end
107
+ end
108
+
109
+ module Enumerable
110
+ class << self
111
+ def [](type); end
112
+ end
113
+ end
114
+
115
+ module Enumerator
116
+ class << self
117
+ def [](type); end
118
+ end
119
+ end
120
+
121
+ module Hash
122
+ class << self
123
+ def [](keys, values); end
124
+ end
125
+ end
126
+
127
+ class Proc
128
+ def bind(*_)
129
+ self
130
+ end
131
+
132
+ def params(*_param)
133
+ self
134
+ end
135
+
136
+ def void
137
+ self
138
+ end
139
+
140
+ def returns(_type)
141
+ self
142
+ end
143
+ end
144
+
145
+ module Range
146
+ class << self
147
+ def [](type); end
148
+ end
149
+ end
150
+
151
+ module Set
152
+ class << self
153
+ def [](type); end
154
+ end
155
+ end
156
+ end
@@ -1,3 +1,5 @@
1
+ require 'cli/kit'
2
+
1
3
  module CLI
2
4
  module Kit
3
5
  module Support
@@ -10,7 +12,9 @@ module CLI
10
12
  def assert_all_commands_run(should_raise: true)
11
13
  errors = CLI::Kit::System.error_message
12
14
  CLI::Kit::System.reset!
13
- assert(false, errors) if should_raise && !errors.nil?
15
+ # this is in minitest, but sorbet doesn't know that. probably we
16
+ # could structure this better.
17
+ T.unsafe(self).assert(false, errors) if should_raise && !errors.nil?
14
18
  errors
15
19
  end
16
20
 
@@ -52,30 +56,32 @@ module CLI
52
56
  module System
53
57
  class << self
54
58
  alias_method :original_system, :system
55
- def system(*a, sudo: false, env: {}, **kwargs)
56
- expected_command = expected_command(*a, sudo: sudo, env: env)
59
+ def system(cmd, *a, sudo: false, env: {}, stdin: nil, **kwargs)
60
+ a.unshift(cmd)
61
+ expected_command = expected_command(a, sudo: sudo, env: env)
57
62
 
58
63
  # In the case of an unexpected command, expected_command will be nil
59
64
  return FakeSuccess.new(false) if expected_command.nil?
60
65
 
61
66
  # Otherwise handle the command
62
67
  if expected_command[:allow]
63
- original_system(*a, sudo: sudo, env: env, **kwargs)
68
+ T.unsafe(self).original_system(*a, sudo: sudo, env: env, **kwargs)
64
69
  else
65
70
  FakeSuccess.new(expected_command[:success])
66
71
  end
67
72
  end
68
73
 
69
74
  alias_method :original_capture2, :capture2
70
- def capture2(*a, sudo: false, env: {}, **kwargs)
71
- expected_command = expected_command(*a, sudo: sudo, env: env)
75
+ def capture2(cmd, *a, sudo: false, env: {}, **kwargs)
76
+ a.unshift(cmd)
77
+ expected_command = expected_command(a, sudo: sudo, env: env)
72
78
 
73
79
  # In the case of an unexpected command, expected_command will be nil
74
80
  return [nil, FakeSuccess.new(false)] if expected_command.nil?
75
81
 
76
82
  # Otherwise handle the command
77
83
  if expected_command[:allow]
78
- original_capture2(*a, sudo: sudo, env: env, **kwargs)
84
+ T.unsafe(self).original_capture2(*a, sudo: sudo, env: env, **kwargs)
79
85
  else
80
86
  [
81
87
  expected_command[:stdout],
@@ -85,15 +91,16 @@ module CLI
85
91
  end
86
92
 
87
93
  alias_method :original_capture2e, :capture2e
88
- def capture2e(*a, sudo: false, env: {}, **kwargs)
89
- expected_command = expected_command(*a, sudo: sudo, env: env)
94
+ def capture2e(cmd, *a, sudo: false, env: {}, **kwargs)
95
+ a.unshift(cmd)
96
+ expected_command = expected_command(a, sudo: sudo, env: env)
90
97
 
91
98
  # In the case of an unexpected command, expected_command will be nil
92
99
  return [nil, FakeSuccess.new(false)] if expected_command.nil?
93
100
 
94
101
  # Otherwise handle the command
95
102
  if expected_command[:allow]
96
- original_capture2ecapture2e(*a, sudo: sudo, env: env, **kwargs)
103
+ T.unsafe(self).original_capture2e(*a, sudo: sudo, env: env, **kwargs)
97
104
  else
98
105
  [
99
106
  expected_command[:stdout],
@@ -103,15 +110,16 @@ module CLI
103
110
  end
104
111
 
105
112
  alias_method :original_capture3, :capture3
106
- def capture3(*a, sudo: false, env: {}, **kwargs)
107
- expected_command = expected_command(*a, sudo: sudo, env: env)
113
+ def capture3(cmd, *a, sudo: false, env: {}, **kwargs)
114
+ a.unshift(cmd)
115
+ expected_command = expected_command(a, sudo: sudo, env: env)
108
116
 
109
117
  # In the case of an unexpected command, expected_command will be nil
110
118
  return [nil, nil, FakeSuccess.new(false)] if expected_command.nil?
111
119
 
112
120
  # Otherwise handle the command
113
121
  if expected_command[:allow]
114
- original_capture3(*a, sudo: sudo, env: env, **kwargs)
122
+ T.unsafe(self).original_capture3(*a, sudo: sudo, env: env, **kwargs)
115
123
  else
116
124
  [
117
125
  expected_command[:stdout],
@@ -216,12 +224,13 @@ module CLI
216
224
  end
217
225
 
218
226
  return nil if final_error.empty?
227
+
219
228
  "\n" + final_error.join("\n") # Initial new line for formatting reasons
220
229
  end
221
230
 
222
231
  private
223
232
 
224
- def expected_command(*a, sudo: raise, env: raise)
233
+ def expected_command(a, sudo: raise, env: raise)
225
234
  expected_cmd = @delegate_open3[a.join(' ')]
226
235
 
227
236
  if expected_cmd.nil?
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  require 'cli/kit'
2
4
 
3
5
  module CLI
@@ -1,5 +1,6 @@
1
- require 'cli/kit'
1
+ # typed: true
2
2
 
3
+ require 'cli/kit'
3
4
  require 'open3'
4
5
  require 'English'
5
6
 
@@ -8,6 +9,8 @@ module CLI
8
9
  module System
9
10
  SUDO_PROMPT = CLI::UI.fmt('{{info:(sudo)}} Password: ')
10
11
  class << self
12
+ extend T::Sig
13
+
11
14
  # Ask for sudo access with a message explaning the need for it
12
15
  # Will make subsequent commands capable of running with sudo for a period of time
13
16
  #
@@ -17,10 +20,12 @@ module CLI
17
20
  # #### Usage
18
21
  # `ctx.sudo_reason("We need to do a thing")`
19
22
  #
23
+ sig { params(msg: String).void }
20
24
  def sudo_reason(msg)
21
25
  # See if sudo has a cached password
22
- %x(env SUDO_ASKPASS=/usr/bin/false sudo -A true)
26
+ %x(env SUDO_ASKPASS=/usr/bin/false sudo -A true > /dev/null 2>&1)
23
27
  return if $CHILD_STATUS.success?
28
+
24
29
  CLI::UI.with_frame_color(:blue) do
25
30
  puts(CLI::UI.fmt("{{i}} #{msg}"))
26
31
  end
@@ -43,8 +48,18 @@ module CLI
43
48
  # #### Usage
44
49
  # `out, stat = CLI::Kit::System.capture2('ls', 'a_folder')`
45
50
  #
46
- def capture2(*a, sudo: false, env: ENV, **kwargs)
47
- delegate_open3(*a, sudo: sudo, env: env, method: :capture2, **kwargs)
51
+ sig do
52
+ params(
53
+ cmd: String,
54
+ args: String,
55
+ sudo: T.any(T::Boolean, String),
56
+ env: T::Hash[String, T.nilable(String)],
57
+ kwargs: T.untyped,
58
+ )
59
+ .returns([String, Process::Status])
60
+ end
61
+ def capture2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
62
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2)
48
63
  end
49
64
 
50
65
  # Execute a command in the user's environment
@@ -64,8 +79,18 @@ module CLI
64
79
  # #### Usage
65
80
  # `out_and_err, stat = CLI::Kit::System.capture2e('ls', 'a_folder')`
66
81
  #
67
- def capture2e(*a, sudo: false, env: ENV, **kwargs)
68
- delegate_open3(*a, sudo: sudo, env: env, method: :capture2e, **kwargs)
82
+ sig do
83
+ params(
84
+ cmd: String,
85
+ args: String,
86
+ sudo: T.any(T::Boolean, String),
87
+ env: T::Hash[String, T.nilable(String)],
88
+ kwargs: T.untyped,
89
+ )
90
+ .returns([String, Process::Status])
91
+ end
92
+ def capture2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
93
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2e)
69
94
  end
70
95
 
71
96
  # Execute a command in the user's environment
@@ -86,20 +111,72 @@ module CLI
86
111
  # #### Usage
87
112
  # `out, err, stat = CLI::Kit::System.capture3('ls', 'a_folder')`
88
113
  #
89
- def capture3(*a, sudo: false, env: ENV, **kwargs)
90
- delegate_open3(*a, sudo: sudo, env: env, method: :capture3, **kwargs)
114
+ sig do
115
+ params(
116
+ cmd: String,
117
+ args: String,
118
+ sudo: T.any(T::Boolean, String),
119
+ env: T::Hash[String, T.nilable(String)],
120
+ kwargs: T.untyped,
121
+ )
122
+ .returns([String, String, Process::Status])
123
+ end
124
+ def capture3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
125
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture3)
91
126
  end
92
127
 
93
- def popen2(*a, sudo: false, env: ENV, **kwargs, &block)
94
- delegate_open3(*a, sudo: sudo, env: env, method: :popen2, **kwargs, &block)
128
+ sig do
129
+ params(
130
+ cmd: String,
131
+ args: String,
132
+ sudo: T.any(T::Boolean, String),
133
+ env: T::Hash[String, T.nilable(String)],
134
+ kwargs: T.untyped,
135
+ block: T.nilable(
136
+ T.proc.params(stdin: IO, stdout: IO, wait_thr: Process::Waiter)
137
+ .returns([IO, IO, Process::Waiter]),
138
+ ),
139
+ )
140
+ .returns([IO, IO, Process::Waiter])
141
+ end
142
+ def popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
143
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2, &block)
95
144
  end
96
145
 
97
- def popen2e(*a, sudo: false, env: ENV, **kwargs, &block)
98
- delegate_open3(*a, sudo: sudo, env: env, method: :popen2e, **kwargs, &block)
146
+ sig do
147
+ params(
148
+ cmd: String,
149
+ args: String,
150
+ sudo: T.any(T::Boolean, String),
151
+ env: T::Hash[String, T.nilable(String)],
152
+ kwargs: T.untyped,
153
+ block: T.nilable(
154
+ T.proc.params(stdin: IO, stdout: IO, wait_thr: Process::Waiter)
155
+ .returns([IO, IO, Process::Waiter]),
156
+ ),
157
+ )
158
+ .returns([IO, IO, Process::Waiter])
159
+ end
160
+ def popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
161
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2e, &block)
99
162
  end
100
163
 
101
- def popen3(*a, sudo: false, env: ENV, **kwargs, &block)
102
- delegate_open3(*a, sudo: sudo, env: env, method: :popen3, **kwargs, &block)
164
+ sig do
165
+ params(
166
+ cmd: String,
167
+ args: String,
168
+ sudo: T.any(T::Boolean, String),
169
+ env: T::Hash[String, T.nilable(String)],
170
+ kwargs: T.untyped,
171
+ block: T.nilable(
172
+ T.proc.params(stdin: IO, stdout: IO, stderr: IO, wait_thr: Process::Waiter)
173
+ .returns([IO, IO, IO, Process::Waiter]),
174
+ ),
175
+ )
176
+ .returns([IO, IO, IO, Process::Waiter])
177
+ end
178
+ def popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
179
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen3, &block)
103
180
  end
104
181
 
105
182
  # Execute a command in the user's environment
@@ -117,13 +194,32 @@ module CLI
117
194
  # #### Usage
118
195
  # `stat = CLI::Kit::System.system('ls', 'a_folder')`
119
196
  #
120
- def system(*a, sudo: false, env: ENV, **kwargs)
121
- a = apply_sudo(*a, sudo)
197
+ sig do
198
+ params(
199
+ cmd: String,
200
+ args: String,
201
+ sudo: T.any(T::Boolean, String),
202
+ env: T::Hash[String, T.nilable(String)],
203
+ stdin: T.nilable(T.any(IO, String, Integer, Symbol)),
204
+ kwargs: T.untyped,
205
+ block: T.nilable(T.proc.params(out: String, err: String).void),
206
+ )
207
+ .returns(Process::Status)
208
+ end
209
+ def system(cmd, *args, sudo: false, env: ENV.to_h, stdin: nil, **kwargs, &block)
210
+ cmd, args = apply_sudo(cmd, args, sudo)
122
211
 
123
212
  out_r, out_w = IO.pipe
124
213
  err_r, err_w = IO.pipe
125
- in_stream = STDIN.closed? ? :close : STDIN
126
- pid = Process.spawn(env, *resolve_path(a, env), 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
214
+ in_stream = if stdin
215
+ stdin
216
+ elsif STDIN.closed?
217
+ :close
218
+ else
219
+ STDIN
220
+ end
221
+ cmd, args = resolve_path(cmd, args, env)
222
+ pid = T.unsafe(Process).spawn(env, cmd, *args, 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
127
223
  out_w.close
128
224
  err_w.close
129
225
 
@@ -145,7 +241,7 @@ module CLI
145
241
  break if ios.empty?
146
242
 
147
243
  readers, = IO.select(ios)
148
- readers.each do |io|
244
+ (readers || []).each do |io|
149
245
  data, trailing = split_partial_characters(io.readpartial(4096))
150
246
  handlers[io].call(previous_trailing[io] + data)
151
247
  previous_trailing[io] = trailing
@@ -161,22 +257,43 @@ module CLI
161
257
  # Split off trailing partial UTF-8 Characters. UTF-8 Multibyte characters start with a 11xxxxxx byte that tells
162
258
  # how many following bytes are part of this character, followed by some number of 10xxxxxx bytes. This simple
163
259
  # algorithm will split off a whole trailing multi-byte character.
260
+ sig { params(data: String).returns([String, String]) }
164
261
  def split_partial_characters(data)
165
- last_byte = data.getbyte(-1)
262
+ last_byte = T.must(data.getbyte(-1))
166
263
  return [data, ''] if (last_byte & 0b1000_0000).zero?
167
264
 
168
- # UTF-8 is up to 6 characters per rune, so we could never want to trim more than that, and we want to avoid
265
+ # UTF-8 is up to 4 characters per rune, so we could never want to trim more than that, and we want to avoid
169
266
  # allocating an array for the whole of data with bytes
170
- min_bound = -[6, data.bytesize].min
171
- final_bytes = data.byteslice(min_bound..-1).bytes
267
+ min_bound = -[4, data.bytesize].min
268
+ final_bytes = T.must(data.byteslice(min_bound..-1)).bytes
172
269
  partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 }
270
+
173
271
  # Bail out for non UTF-8
174
272
  return [data, ''] unless partial_character_sub_index
273
+
274
+ start_byte = final_bytes[partial_character_sub_index]
275
+ full_size = if start_byte & 0b1111_1000 == 0b1111_0000
276
+ 4
277
+ elsif start_byte & 0b1111_0000 == 0b1110_0000
278
+ 3
279
+ elsif start_byte & 0b1110_0000 == 0b110_00000
280
+ 2
281
+ else
282
+ nil # Not a valid UTF-8 character
283
+ end
284
+ return [data, ''] if full_size.nil? # Bail out for non UTF-8
285
+
286
+ if final_bytes.size - partial_character_sub_index == full_size
287
+ # We have a full UTF-8 character, so we can just return the data
288
+ return [data, '']
289
+ end
290
+
175
291
  partial_character_index = min_bound + partial_character_sub_index
176
292
 
177
- [data.byteslice(0...partial_character_index), data.byteslice(partial_character_index..-1)]
293
+ [T.must(data.byteslice(0...partial_character_index)), T.must(data.byteslice(partial_character_index..-1))]
178
294
  end
179
295
 
296
+ sig { returns(Symbol) }
180
297
  def os
181
298
  return :mac if /darwin/.match(RUBY_PLATFORM)
182
299
  return :linux if /linux/.match(RUBY_PLATFORM)
@@ -185,19 +302,49 @@ module CLI
185
302
  raise "Could not determine OS from platform #{RUBY_PLATFORM}"
186
303
  end
187
304
 
305
+ sig { params(cmd: String, env: T::Hash[String, T.nilable(String)]).returns(T.nilable(String)) }
306
+ def which(cmd, env)
307
+ exts = os == :windows ? (env['PATHEXT'] || 'exe').split(';') : ['']
308
+ (env['PATH'] || '').split(File::PATH_SEPARATOR).each do |path|
309
+ exts.each do |ext|
310
+ exe = File.join(path, "#{cmd}#{ext}")
311
+ return exe if File.executable?(exe) && !File.directory?(exe)
312
+ end
313
+ end
314
+
315
+ nil
316
+ end
317
+
188
318
  private
189
319
 
190
- def apply_sudo(*a, sudo)
191
- a.unshift('sudo', '-S', '-p', SUDO_PROMPT, '--') if sudo
320
+ sig do
321
+ params(cmd: String, args: T::Array[String], sudo: T.any(T::Boolean, String))
322
+ .returns([String, T::Array[String]])
323
+ end
324
+ def apply_sudo(cmd, args, sudo)
325
+ return [cmd, args] unless sudo
326
+
192
327
  sudo_reason(sudo) if sudo.is_a?(String)
193
- a
328
+ ['sudo', args.unshift('-E', '-S', '-p', SUDO_PROMPT, '--', cmd)]
194
329
  end
195
330
 
196
- def delegate_open3(*a, sudo: raise, env: raise, method: raise, **kwargs, &block)
197
- a = apply_sudo(*a, sudo)
198
- Open3.send(method, env, *resolve_path(a, env), **kwargs, &block)
331
+ sig do
332
+ params(
333
+ cmd: String,
334
+ args: T::Array[String],
335
+ kwargs: T::Hash[Symbol, T.untyped],
336
+ sudo: T.any(T::Boolean, String),
337
+ env: T::Hash[String, T.nilable(String)],
338
+ method: Symbol,
339
+ block: T.untyped,
340
+ ).returns(T.untyped)
341
+ end
342
+ def delegate_open3(cmd, args, kwargs, sudo: raise, env: raise, method: raise, &block)
343
+ cmd, args = apply_sudo(cmd, args, sudo)
344
+ cmd, args = resolve_path(cmd, args, env)
345
+ T.unsafe(Open3).send(method, env, cmd, *args, **kwargs, &block)
199
346
  rescue Errno::EINTR
200
- raise(Errno::EINTR, "command interrupted: #{a.join(" ")}")
347
+ raise(Errno::EINTR, "command interrupted: #{cmd} #{args.join(" ")}")
201
348
  end
202
349
 
203
350
  # Ruby resolves the program to execute using its own PATH, but we want it to
@@ -209,32 +356,19 @@ module CLI
209
356
  # project.
210
357
  #
211
358
  # See https://github.com/Shopify/dev/pull/625 for more details.
212
- def resolve_path(a, env)
213
- # If only one argument was provided, make sure it's interpreted by a shell.
214
- if a.size == 1
215
- if os == :windows
216
- return ['break && ' + a[0]]
217
- else
218
- return ['true ; ' + a[0]]
219
- end
220
- end
221
- return a if a.first.include?('/')
222
-
223
- item = which(a.first, env)
224
- a[0] = item if item
225
- a
359
+ sig do
360
+ params(cmd: String, args: T::Array[String], env: T::Hash[String, T.nilable(String)])
361
+ .returns([String, T::Array[String]])
226
362
  end
227
-
228
- def which(cmd, env)
229
- exts = os == :windows ? env.fetch('PATHEXT').split(';') : ['']
230
- env.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |path|
231
- exts.each do |ext|
232
- exe = File.join(path, "#{cmd}#{ext}")
233
- return exe if File.executable?(exe) && !File.directory?(exe)
234
- end
363
+ def resolve_path(cmd, args, env)
364
+ # If only one argument was provided, make sure it's interpreted by a shell.
365
+ if args.empty?
366
+ prefix = os == :windows ? 'break && ' : 'true ; '
367
+ return [prefix + cmd, []]
235
368
  end
369
+ return [cmd, args] if cmd.include?('/')
236
370
 
237
- nil
371
+ [which(cmd, env) || cmd, args]
238
372
  end
239
373
  end
240
374
  end