gorails 0.1.1 → 0.1.4

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -1
  3. data/Gemfile.lock +1 -6
  4. data/README.md +41 -12
  5. data/bin/update-deps +95 -0
  6. data/exe/gorails +2 -1
  7. data/gorails.gemspec +0 -2
  8. data/lib/gorails/commands/railsbytes.rb +45 -4
  9. data/lib/gorails/commands/version.rb +15 -0
  10. data/lib/gorails/commands.rb +2 -5
  11. data/lib/gorails/version.rb +1 -1
  12. data/lib/gorails.rb +11 -20
  13. data/vendor/deps/cli-kit/REVISION +1 -0
  14. data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
  15. data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
  16. data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
  17. data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
  18. data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
  19. data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
  20. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
  21. data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
  22. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
  23. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
  24. data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
  25. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
  26. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
  27. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
  28. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
  29. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
  30. data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
  31. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
  32. data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
  33. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
  34. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
  35. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
  36. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
  37. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
  38. data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
  39. data/vendor/deps/cli-ui/REVISION +1 -0
  40. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
  41. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
  42. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
  43. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
  44. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
  45. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
  46. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
  47. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
  48. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
  49. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
  50. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
  51. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
  52. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
  53. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
  54. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
  55. data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
  56. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
  57. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
  58. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
  59. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
  60. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
  61. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
  62. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
  63. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
  64. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
  65. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
  66. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
  67. data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
  68. metadata +59 -30
@@ -0,0 +1,253 @@
1
+ require 'cli/kit'
2
+
3
+ module CLI
4
+ module Kit
5
+ module Support
6
+ module TestHelper
7
+ def setup
8
+ super
9
+ CLI::Kit::System.reset!
10
+ end
11
+
12
+ def assert_all_commands_run(should_raise: true)
13
+ errors = CLI::Kit::System.error_message
14
+ CLI::Kit::System.reset!
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?
18
+ errors
19
+ end
20
+
21
+ def teardown
22
+ super
23
+ assert_all_commands_run
24
+ end
25
+
26
+ module FakeConfig
27
+ require 'tmpdir'
28
+ require 'fileutils'
29
+
30
+ def setup
31
+ super
32
+ @tmpdir = Dir.mktmpdir
33
+ @prev_xdg = ENV['XDG_CONFIG_HOME']
34
+ ENV['XDG_CONFIG_HOME'] = @tmpdir
35
+ end
36
+
37
+ def teardown
38
+ FileUtils.rm_rf(@tmpdir)
39
+ ENV['XDG_CONFIG_HOME'] = @prev_xdg
40
+ super
41
+ end
42
+ end
43
+
44
+ class FakeSuccess
45
+ def initialize(success)
46
+ @success = success
47
+ end
48
+
49
+ def success?
50
+ @success
51
+ end
52
+ end
53
+
54
+ module ::CLI
55
+ module Kit
56
+ module System
57
+ class << self
58
+ alias_method :original_system, :system
59
+ def system(cmd, *a, sudo: false, env: {}, **kwargs)
60
+ a.unshift(cmd)
61
+ expected_command = expected_command(a, sudo: sudo, env: env)
62
+
63
+ # In the case of an unexpected command, expected_command will be nil
64
+ return FakeSuccess.new(false) if expected_command.nil?
65
+
66
+ # Otherwise handle the command
67
+ if expected_command[:allow]
68
+ T.unsafe(self).original_system(*a, sudo: sudo, env: env, **kwargs)
69
+ else
70
+ FakeSuccess.new(expected_command[:success])
71
+ end
72
+ end
73
+
74
+ alias_method :original_capture2, :capture2
75
+ def capture2(cmd, *a, sudo: false, env: {}, **kwargs)
76
+ a.unshift(cmd)
77
+ expected_command = expected_command(a, sudo: sudo, env: env)
78
+
79
+ # In the case of an unexpected command, expected_command will be nil
80
+ return [nil, FakeSuccess.new(false)] if expected_command.nil?
81
+
82
+ # Otherwise handle the command
83
+ if expected_command[:allow]
84
+ T.unsafe(self).original_capture2(*a, sudo: sudo, env: env, **kwargs)
85
+ else
86
+ [
87
+ expected_command[:stdout],
88
+ FakeSuccess.new(expected_command[:success]),
89
+ ]
90
+ end
91
+ end
92
+
93
+ alias_method :original_capture2e, :capture2e
94
+ def capture2e(cmd, *a, sudo: false, env: {}, **kwargs)
95
+ a.unshift(cmd)
96
+ expected_command = expected_command(a, sudo: sudo, env: env)
97
+
98
+ # In the case of an unexpected command, expected_command will be nil
99
+ return [nil, FakeSuccess.new(false)] if expected_command.nil?
100
+
101
+ # Otherwise handle the command
102
+ if expected_command[:allow]
103
+ T.unsafe(self).original_capture2e(*a, sudo: sudo, env: env, **kwargs)
104
+ else
105
+ [
106
+ expected_command[:stdout],
107
+ FakeSuccess.new(expected_command[:success]),
108
+ ]
109
+ end
110
+ end
111
+
112
+ alias_method :original_capture3, :capture3
113
+ def capture3(cmd, *a, sudo: false, env: {}, **kwargs)
114
+ a.unshift(cmd)
115
+ expected_command = expected_command(a, sudo: sudo, env: env)
116
+
117
+ # In the case of an unexpected command, expected_command will be nil
118
+ return [nil, nil, FakeSuccess.new(false)] if expected_command.nil?
119
+
120
+ # Otherwise handle the command
121
+ if expected_command[:allow]
122
+ T.unsafe(self).original_capture3(*a, sudo: sudo, env: env, **kwargs)
123
+ else
124
+ [
125
+ expected_command[:stdout],
126
+ expected_command[:stderr],
127
+ FakeSuccess.new(expected_command[:success]),
128
+ ]
129
+ end
130
+ end
131
+
132
+ # Sets up an expectation for a command and stubs out the call (unless allow is true)
133
+ #
134
+ # #### Parameters
135
+ # `*a` : the command, represented as a splat
136
+ # `stdout` : stdout to stub the command with (defaults to empty string)
137
+ # `stderr` : stderr to stub the command with (defaults to empty string)
138
+ # `allow` : allow determines if the command will be actually run, or stubbed. Defaults to nil (stub)
139
+ # `success` : success status to stub the command with (Defaults to nil)
140
+ # `sudo` : expectation of sudo being set or not (defaults to false)
141
+ # `env` : expectation of env being set or not (defaults to {})
142
+ #
143
+ # Note: Must set allow or success
144
+ #
145
+ def fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {})
146
+ raise ArgumentError, 'success or allow must be set' if success.nil? && allow.nil?
147
+
148
+ @delegate_open3 ||= {}
149
+ @delegate_open3[a.join(' ')] = {
150
+ expected: {
151
+ sudo: sudo,
152
+ env: env,
153
+ },
154
+ actual: {
155
+ sudo: nil,
156
+ env: nil,
157
+ },
158
+ stdout: stdout,
159
+ stderr: stderr,
160
+ allow: allow,
161
+ success: success,
162
+ run: false,
163
+ }
164
+ end
165
+
166
+ # Resets the faked commands
167
+ #
168
+ def reset!
169
+ @delegate_open3 = {}
170
+ end
171
+
172
+ # Returns the errors associated to a test run
173
+ #
174
+ # #### Returns
175
+ # `errors` (String) a string representing errors found on this run, nil if none
176
+ def error_message
177
+ errors = {
178
+ unexpected: [],
179
+ not_run: [],
180
+ other: {},
181
+ }
182
+
183
+ @delegate_open3.each do |cmd, opts|
184
+ if opts[:unexpected]
185
+ errors[:unexpected] << cmd
186
+ elsif opts[:run]
187
+ error = []
188
+
189
+ if opts[:expected][:sudo] != opts[:actual][:sudo]
190
+ error << "- sudo was supposed to be #{opts[:expected][:sudo]} but was #{opts[:actual][:sudo]}"
191
+ end
192
+
193
+ if opts[:expected][:env] != opts[:actual][:env]
194
+ error << "- env was supposed to be #{opts[:expected][:env]} but was #{opts[:actual][:env]}"
195
+ end
196
+
197
+ errors[:other][cmd] = error.join("\n") unless error.empty?
198
+ else
199
+ errors[:not_run] << cmd
200
+ end
201
+ end
202
+
203
+ final_error = []
204
+
205
+ unless errors[:unexpected].empty?
206
+ final_error << CLI::UI.fmt(<<~EOF)
207
+ {{bold:Unexpected command invocations:}}
208
+ {{command:#{errors[:unexpected].join("\n")}}}
209
+ EOF
210
+ end
211
+
212
+ unless errors[:not_run].empty?
213
+ final_error << CLI::UI.fmt(<<~EOF)
214
+ {{bold:Expected commands were not run:}}
215
+ {{command:#{errors[:not_run].join("\n")}}}
216
+ EOF
217
+ end
218
+
219
+ unless errors[:other].empty?
220
+ final_error << CLI::UI.fmt(<<~EOF)
221
+ {{bold:Commands were not run as expected:}}
222
+ #{errors[:other].map { |cmd, msg| "{{command:#{cmd}}}\n#{msg}" }.join("\n\n")}
223
+ EOF
224
+ end
225
+
226
+ return nil if final_error.empty?
227
+
228
+ "\n" + final_error.join("\n") # Initial new line for formatting reasons
229
+ end
230
+
231
+ private
232
+
233
+ def expected_command(a, sudo: raise, env: raise)
234
+ expected_cmd = @delegate_open3[a.join(' ')]
235
+
236
+ if expected_cmd.nil?
237
+ @delegate_open3[a.join(' ')] = { unexpected: true }
238
+ return nil
239
+ end
240
+
241
+ expected_cmd[:run] = true
242
+ expected_cmd[:actual][:sudo] = sudo
243
+ expected_cmd[:actual][:env] = env
244
+ expected_cmd
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,10 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+
4
+ module CLI
5
+ module Kit
6
+ module Support
7
+ autoload :TestHelper, 'cli/kit/support/test_helper'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,350 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+ require 'open3'
4
+ require 'English'
5
+
6
+ module CLI
7
+ module Kit
8
+ module System
9
+ SUDO_PROMPT = CLI::UI.fmt('{{info:(sudo)}} Password: ')
10
+ class << self
11
+ extend T::Sig
12
+
13
+ # Ask for sudo access with a message explaning the need for it
14
+ # Will make subsequent commands capable of running with sudo for a period of time
15
+ #
16
+ # #### Parameters
17
+ # - `msg`: A message telling the user why sudo is needed
18
+ #
19
+ # #### Usage
20
+ # `ctx.sudo_reason("We need to do a thing")`
21
+ #
22
+ sig { params(msg: String).void }
23
+ def sudo_reason(msg)
24
+ # See if sudo has a cached password
25
+ %x(env SUDO_ASKPASS=/usr/bin/false sudo -A true > /dev/null 2>&1)
26
+ return if $CHILD_STATUS.success?
27
+
28
+ CLI::UI.with_frame_color(:blue) do
29
+ puts(CLI::UI.fmt("{{i}} #{msg}"))
30
+ end
31
+ end
32
+
33
+ # Execute a command in the user's environment
34
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
35
+ # Captures the results of the command without output to the console
36
+ #
37
+ # #### Parameters
38
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
39
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
40
+ # - `env`: process environment with which to execute this command
41
+ # - `**kwargs`: additional arguments to pass to Open3.capture2
42
+ #
43
+ # #### Returns
44
+ # - `output`: output (STDOUT) of the command execution
45
+ # - `status`: boolean success status of the command execution
46
+ #
47
+ # #### Usage
48
+ # `out, stat = CLI::Kit::System.capture2('ls', 'a_folder')`
49
+ #
50
+ sig do
51
+ params(
52
+ cmd: String,
53
+ args: String,
54
+ sudo: T.any(T::Boolean, String),
55
+ env: T::Hash[String, T.nilable(String)],
56
+ kwargs: T.untyped
57
+ )
58
+ .returns([String, Process::Status])
59
+ end
60
+ def capture2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
61
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2)
62
+ end
63
+
64
+ # Execute a command in the user's environment
65
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
66
+ # Captures the results of the command without output to the console
67
+ #
68
+ # #### Parameters
69
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
70
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
71
+ # - `env`: process environment with which to execute this command
72
+ # - `**kwargs`: additional arguments to pass to Open3.capture2e
73
+ #
74
+ # #### Returns
75
+ # - `output`: output (STDOUT merged with STDERR) of the command execution
76
+ # - `status`: boolean success status of the command execution
77
+ #
78
+ # #### Usage
79
+ # `out_and_err, stat = CLI::Kit::System.capture2e('ls', 'a_folder')`
80
+ #
81
+ sig do
82
+ params(
83
+ cmd: String,
84
+ args: String,
85
+ sudo: T.any(T::Boolean, String),
86
+ env: T::Hash[String, T.nilable(String)],
87
+ kwargs: T.untyped
88
+ )
89
+ .returns([String, Process::Status])
90
+ end
91
+ def capture2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
92
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2e)
93
+ end
94
+
95
+ # Execute a command in the user's environment
96
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
97
+ # Captures the results of the command without output to the console
98
+ #
99
+ # #### Parameters
100
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
101
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
102
+ # - `env`: process environment with which to execute this command
103
+ # - `**kwargs`: additional arguments to pass to Open3.capture3
104
+ #
105
+ # #### Returns
106
+ # - `output`: STDOUT of the command execution
107
+ # - `error`: STDERR of the command execution
108
+ # - `status`: boolean success status of the command execution
109
+ #
110
+ # #### Usage
111
+ # `out, err, stat = CLI::Kit::System.capture3('ls', 'a_folder')`
112
+ #
113
+ sig do
114
+ params(
115
+ cmd: String,
116
+ args: String,
117
+ sudo: T.any(T::Boolean, String),
118
+ env: T::Hash[String, T.nilable(String)],
119
+ kwargs: T.untyped
120
+ )
121
+ .returns([String, String, Process::Status])
122
+ end
123
+ def capture3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
124
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture3)
125
+ end
126
+
127
+ sig do
128
+ params(
129
+ cmd: String,
130
+ args: String,
131
+ sudo: T.any(T::Boolean, String),
132
+ env: T::Hash[String, T.nilable(String)],
133
+ kwargs: T.untyped,
134
+ block: T.nilable(
135
+ T.proc.params(stdin: IO, stdout: IO, wait_thr: Process::Waiter)
136
+ .returns([IO, IO, Process::Waiter])
137
+ )
138
+ )
139
+ .returns([IO, IO, Process::Waiter])
140
+ end
141
+ def popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
142
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2, &block)
143
+ end
144
+
145
+ sig do
146
+ params(
147
+ cmd: String,
148
+ args: String,
149
+ sudo: T.any(T::Boolean, String),
150
+ env: T::Hash[String, T.nilable(String)],
151
+ kwargs: T.untyped,
152
+ block: T.nilable(
153
+ T.proc.params(stdin: IO, stdout: IO, wait_thr: Process::Waiter)
154
+ .returns([IO, IO, Process::Waiter])
155
+ )
156
+ )
157
+ .returns([IO, IO, Process::Waiter])
158
+ end
159
+ def popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
160
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2e, &block)
161
+ end
162
+
163
+ sig do
164
+ params(
165
+ cmd: String,
166
+ args: String,
167
+ sudo: T.any(T::Boolean, String),
168
+ env: T::Hash[String, T.nilable(String)],
169
+ kwargs: T.untyped,
170
+ block: T.nilable(
171
+ T.proc.params(stdin: IO, stdout: IO, stderr: IO, wait_thr: Process::Waiter)
172
+ .returns([IO, IO, IO, Process::Waiter])
173
+ )
174
+ )
175
+ .returns([IO, IO, IO, Process::Waiter])
176
+ end
177
+ def popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
178
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen3, &block)
179
+ end
180
+
181
+ # Execute a command in the user's environment
182
+ # Outputs result of the command without capturing it
183
+ #
184
+ # #### Parameters
185
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
186
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
187
+ # - `env`: process environment with which to execute this command
188
+ # - `**kwargs`: additional keyword arguments to pass to Process.spawn
189
+ #
190
+ # #### Returns
191
+ # - `status`: The `Process:Status` result for the command execution
192
+ #
193
+ # #### Usage
194
+ # `stat = CLI::Kit::System.system('ls', 'a_folder')`
195
+ #
196
+ sig do
197
+ params(
198
+ cmd: String,
199
+ args: String,
200
+ sudo: T.any(T::Boolean, String),
201
+ env: T::Hash[String, T.nilable(String)],
202
+ kwargs: T.untyped,
203
+ block: T.nilable(T.proc.params(out: String, err: String).void)
204
+ )
205
+ .returns(Process::Status)
206
+ end
207
+ def system(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
208
+ cmd, args = apply_sudo(cmd, args, sudo)
209
+
210
+ out_r, out_w = IO.pipe
211
+ err_r, err_w = IO.pipe
212
+ in_stream = STDIN.closed? ? :close : STDIN
213
+ cmd, args = resolve_path(cmd, args, env)
214
+ pid = T.unsafe(Process).spawn(env, cmd, *args, 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
215
+ out_w.close
216
+ err_w.close
217
+
218
+ handlers = if block_given?
219
+ {
220
+ out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') },
221
+ err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) },
222
+ }
223
+ else
224
+ {
225
+ out_r => ->(data) { STDOUT.write(data) },
226
+ err_r => ->(data) { STDOUT.write(data) },
227
+ }
228
+ end
229
+
230
+ previous_trailing = Hash.new('')
231
+ loop do
232
+ ios = [err_r, out_r].reject(&:closed?)
233
+ break if ios.empty?
234
+
235
+ readers, = IO.select(ios)
236
+ (readers || []).each do |io|
237
+ data, trailing = split_partial_characters(io.readpartial(4096))
238
+ handlers[io].call(previous_trailing[io] + data)
239
+ previous_trailing[io] = trailing
240
+ rescue IOError
241
+ io.close
242
+ end
243
+ end
244
+
245
+ Process.wait(pid)
246
+ $CHILD_STATUS
247
+ end
248
+
249
+ # Split off trailing partial UTF-8 Characters. UTF-8 Multibyte characters start with a 11xxxxxx byte that tells
250
+ # how many following bytes are part of this character, followed by some number of 10xxxxxx bytes. This simple
251
+ # algorithm will split off a whole trailing multi-byte character.
252
+ sig { params(data: String).returns([String, String]) }
253
+ def split_partial_characters(data)
254
+ last_byte = T.must(data.getbyte(-1))
255
+ return [data, ''] if (last_byte & 0b1000_0000).zero?
256
+
257
+ # UTF-8 is up to 6 characters per rune, so we could never want to trim more than that, and we want to avoid
258
+ # allocating an array for the whole of data with bytes
259
+ min_bound = -[6, data.bytesize].min
260
+ final_bytes = T.must(data.byteslice(min_bound..-1)).bytes
261
+ partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 }
262
+ # Bail out for non UTF-8
263
+ return [data, ''] unless partial_character_sub_index
264
+
265
+ partial_character_index = min_bound + partial_character_sub_index
266
+
267
+ [T.must(data.byteslice(0...partial_character_index)), T.must(data.byteslice(partial_character_index..-1))]
268
+ end
269
+
270
+ sig { returns(Symbol) }
271
+ def os
272
+ return :mac if /darwin/.match(RUBY_PLATFORM)
273
+ return :linux if /linux/.match(RUBY_PLATFORM)
274
+ return :windows if /mingw32/.match(RUBY_PLATFORM)
275
+
276
+ raise "Could not determine OS from platform #{RUBY_PLATFORM}"
277
+ end
278
+
279
+ private
280
+
281
+ sig do
282
+ params(cmd: String, args: T::Array[String], sudo: T.any(T::Boolean, String))
283
+ .returns([String, T::Array[String]])
284
+ end
285
+ def apply_sudo(cmd, args, sudo)
286
+ return [cmd, args] unless sudo
287
+
288
+ sudo_reason(sudo) if sudo.is_a?(String)
289
+ ['sudo', args.unshift('-S', '-p', SUDO_PROMPT, '--', cmd)]
290
+ end
291
+
292
+ sig do
293
+ params(
294
+ cmd: String,
295
+ args: T::Array[String],
296
+ kwargs: T::Hash[Symbol, T.untyped],
297
+ sudo: T.any(T::Boolean, String),
298
+ env: T::Hash[String, T.nilable(String)],
299
+ method: Symbol,
300
+ block: T.untyped
301
+ ).returns(T.untyped)
302
+ end
303
+ def delegate_open3(cmd, args, kwargs, sudo: raise, env: raise, method: raise, &block)
304
+ cmd, args = apply_sudo(cmd, args, sudo)
305
+ cmd, args = resolve_path(cmd, args, env)
306
+ T.unsafe(Open3).send(method, env, cmd, *args, **kwargs, &block)
307
+ rescue Errno::EINTR
308
+ raise(Errno::EINTR, "command interrupted: #{cmd} #{args.join(" ")}")
309
+ end
310
+
311
+ # Ruby resolves the program to execute using its own PATH, but we want it to
312
+ # use the provided one, so we ensure ruby chooses to spawn a shell, which will
313
+ # parse our command and properly spawn our target using the provided environment.
314
+ #
315
+ # This is important because dev clobbers its own environment such that ruby
316
+ # means /usr/bin/ruby, but we want it to select the ruby targeted by the active
317
+ # project.
318
+ #
319
+ # See https://github.com/Shopify/dev/pull/625 for more details.
320
+ sig do
321
+ params(cmd: String, args: T::Array[String], env: T::Hash[String, T.nilable(String)])
322
+ .returns([String, T::Array[String]])
323
+ end
324
+ def resolve_path(cmd, args, env)
325
+ # If only one argument was provided, make sure it's interpreted by a shell.
326
+ if args.empty?
327
+ prefix = os == :windows ? 'break && ' : 'true ; '
328
+ return [prefix + cmd, []]
329
+ end
330
+ return [cmd, args] if cmd.include?('/')
331
+
332
+ [which(cmd, env) || cmd, args]
333
+ end
334
+
335
+ sig { params(cmd: T.untyped, env: T.untyped).returns(T.untyped) }
336
+ def which(cmd, env)
337
+ exts = os == :windows ? env.fetch('PATHEXT').split(';') : ['']
338
+ env.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |path|
339
+ exts.each do |ext|
340
+ exe = File.join(path, "#{cmd}#{ext}")
341
+ return exe if File.executable?(exe) && !File.directory?(exe)
342
+ end
343
+ end
344
+
345
+ nil
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end