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
@@ -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