spoom 1.1.11 → 1.1.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,12 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'open3'
5
- require 'json'
4
+ require "open3"
5
+ require "json"
6
6
 
7
- require_relative 'lsp/base'
8
- require_relative 'lsp/structures'
9
- require_relative 'lsp/errors'
7
+ require_relative "lsp/base"
8
+ require_relative "lsp/structures"
9
+ require_relative "lsp/errors"
10
10
 
11
11
  module Spoom
12
12
  module LSP
@@ -58,10 +58,10 @@ module Spoom
58
58
  json = JSON.parse(raw_string)
59
59
 
60
60
  # Handle error in the LSP protocol
61
- raise ResponseError.from_json(json['error']) if json['error']
61
+ raise ResponseError.from_json(json["error"]) if json["error"]
62
62
 
63
63
  # Handle typechecking errors
64
- raise Error::Diagnostics.from_json(json['params']) if json['method'] == "textDocument/publishDiagnostics"
64
+ raise Error::Diagnostics.from_json(json["params"]) if json["method"] == "textDocument/publishDiagnostics"
65
65
 
66
66
  json
67
67
  end
@@ -71,16 +71,17 @@ module Spoom
71
71
  sig { params(workspace_path: String).void }
72
72
  def open(workspace_path)
73
73
  raise Error::AlreadyOpen, "Error: CLI already opened" if @open
74
+
74
75
  send(Request.new(
75
76
  next_id,
76
- 'initialize',
77
+ "initialize",
77
78
  {
78
- 'rootPath' => workspace_path,
79
- 'rootUri' => "file://#{workspace_path}",
80
- 'capabilities' => {},
79
+ "rootPath" => workspace_path,
80
+ "rootUri" => "file://#{workspace_path}",
81
+ "capabilities" => {},
81
82
  },
82
83
  ))
83
- send(Notification.new('initialized', {}))
84
+ send(Notification.new("initialized", {}))
84
85
  @open = true
85
86
  end
86
87
 
@@ -88,133 +89,140 @@ module Spoom
88
89
  def hover(uri, line, column)
89
90
  json = send(Request.new(
90
91
  next_id,
91
- 'textDocument/hover',
92
+ "textDocument/hover",
92
93
  {
93
- 'textDocument' => {
94
- 'uri' => uri,
94
+ "textDocument" => {
95
+ "uri" => uri,
95
96
  },
96
- 'position' => {
97
- 'line' => line,
98
- 'character' => column,
97
+ "position" => {
98
+ "line" => line,
99
+ "character" => column,
99
100
  },
100
- }
101
+ },
101
102
  ))
102
103
 
103
- return nil unless json && json['result']
104
- Hover.from_json(json['result'])
104
+ return nil unless json && json["result"]
105
+
106
+ Hover.from_json(json["result"])
105
107
  end
106
108
 
107
109
  sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[SignatureHelp]) }
108
110
  def signatures(uri, line, column)
109
111
  json = send(Request.new(
110
112
  next_id,
111
- 'textDocument/signatureHelp',
113
+ "textDocument/signatureHelp",
112
114
  {
113
- 'textDocument' => {
114
- 'uri' => uri,
115
+ "textDocument" => {
116
+ "uri" => uri,
115
117
  },
116
- 'position' => {
117
- 'line' => line,
118
- 'character' => column,
118
+ "position" => {
119
+ "line" => line,
120
+ "character" => column,
119
121
  },
120
- }
122
+ },
121
123
  ))
122
124
 
123
- return [] unless json && json['result'] && json['result']['signatures']
124
- json['result']['signatures'].map { |loc| SignatureHelp.from_json(loc) }
125
+ return [] unless json && json["result"] && json["result"]["signatures"]
126
+
127
+ json["result"]["signatures"].map { |loc| SignatureHelp.from_json(loc) }
125
128
  end
126
129
 
127
130
  sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[Location]) }
128
131
  def definitions(uri, line, column)
129
132
  json = send(Request.new(
130
133
  next_id,
131
- 'textDocument/definition',
134
+ "textDocument/definition",
132
135
  {
133
- 'textDocument' => {
134
- 'uri' => uri,
136
+ "textDocument" => {
137
+ "uri" => uri,
135
138
  },
136
- 'position' => {
137
- 'line' => line,
138
- 'character' => column,
139
+ "position" => {
140
+ "line" => line,
141
+ "character" => column,
139
142
  },
140
- }
143
+ },
141
144
  ))
142
145
 
143
- return [] unless json && json['result']
144
- json['result'].map { |loc| Location.from_json(loc) }
146
+ return [] unless json && json["result"]
147
+
148
+ json["result"].map { |loc| Location.from_json(loc) }
145
149
  end
146
150
 
147
151
  sig { params(uri: String, line: Integer, column: Integer).returns(T::Array[Location]) }
148
152
  def type_definitions(uri, line, column)
149
153
  json = send(Request.new(
150
154
  next_id,
151
- 'textDocument/typeDefinition',
155
+ "textDocument/typeDefinition",
152
156
  {
153
- 'textDocument' => {
154
- 'uri' => uri,
157
+ "textDocument" => {
158
+ "uri" => uri,
155
159
  },
156
- 'position' => {
157
- 'line' => line,
158
- 'character' => column,
160
+ "position" => {
161
+ "line" => line,
162
+ "character" => column,
159
163
  },
160
- }
164
+ },
161
165
  ))
162
166
 
163
- return [] unless json && json['result']
164
- json['result'].map { |loc| Location.from_json(loc) }
167
+ return [] unless json && json["result"]
168
+
169
+ json["result"].map { |loc| Location.from_json(loc) }
165
170
  end
166
171
 
167
172
  sig { params(uri: String, line: Integer, column: Integer, include_decl: T::Boolean).returns(T::Array[Location]) }
168
173
  def references(uri, line, column, include_decl = true)
169
174
  json = send(Request.new(
170
175
  next_id,
171
- 'textDocument/references',
176
+ "textDocument/references",
172
177
  {
173
- 'textDocument' => {
174
- 'uri' => uri,
178
+ "textDocument" => {
179
+ "uri" => uri,
175
180
  },
176
- 'position' => {
177
- 'line' => line,
178
- 'character' => column,
181
+ "position" => {
182
+ "line" => line,
183
+ "character" => column,
179
184
  },
180
- 'context' => {
181
- 'includeDeclaration' => include_decl,
185
+ "context" => {
186
+ "includeDeclaration" => include_decl,
182
187
  },
183
- }
188
+ },
184
189
  ))
185
190
 
186
- return [] unless json && json['result']
187
- json['result'].map { |loc| Location.from_json(loc) }
191
+ return [] unless json && json["result"]
192
+
193
+ json["result"].map { |loc| Location.from_json(loc) }
188
194
  end
189
195
 
190
196
  sig { params(query: String).returns(T::Array[DocumentSymbol]) }
191
197
  def symbols(query)
192
198
  json = send(Request.new(
193
199
  next_id,
194
- 'workspace/symbol',
200
+ "workspace/symbol",
195
201
  {
196
- 'query' => query,
197
- }
202
+ "query" => query,
203
+ },
198
204
  ))
199
205
 
200
- return [] unless json && json['result']
201
- json['result'].map { |loc| DocumentSymbol.from_json(loc) }
206
+ return [] unless json && json["result"]
207
+
208
+ json["result"].map { |loc| DocumentSymbol.from_json(loc) }
202
209
  end
203
210
 
204
211
  sig { params(uri: String).returns(T::Array[DocumentSymbol]) }
205
212
  def document_symbols(uri)
206
213
  json = send(Request.new(
207
214
  next_id,
208
- 'textDocument/documentSymbol',
215
+ "textDocument/documentSymbol",
209
216
  {
210
- 'textDocument' => {
211
- 'uri' => uri,
217
+ "textDocument" => {
218
+ "uri" => uri,
212
219
  },
213
- }
220
+ },
214
221
  ))
215
222
 
216
- return [] unless json && json['result']
217
- json['result'].map { |loc| DocumentSymbol.from_json(loc) }
223
+ return [] unless json && json["result"]
224
+
225
+ json["result"].map { |loc| DocumentSymbol.from_json(loc) }
218
226
  end
219
227
 
220
228
  sig { void }
@@ -6,26 +6,28 @@ require_relative "sigils"
6
6
  module Spoom
7
7
  module Sorbet
8
8
  module MetricsParser
9
- extend T::Sig
10
-
11
9
  DEFAULT_PREFIX = "ruby_typer.unknown.."
12
10
 
13
- sig { params(path: String, prefix: String).returns(T::Hash[String, Integer]) }
14
- def self.parse_file(path, prefix = DEFAULT_PREFIX)
15
- parse_string(File.read(path), prefix)
16
- end
11
+ class << self
12
+ extend T::Sig
17
13
 
18
- sig { params(string: String, prefix: String).returns(T::Hash[String, Integer]) }
19
- def self.parse_string(string, prefix = DEFAULT_PREFIX)
20
- parse_hash(JSON.parse(string), prefix)
21
- end
14
+ sig { params(path: String, prefix: String).returns(T::Hash[String, Integer]) }
15
+ def parse_file(path, prefix = DEFAULT_PREFIX)
16
+ parse_string(File.read(path), prefix)
17
+ end
18
+
19
+ sig { params(string: String, prefix: String).returns(T::Hash[String, Integer]) }
20
+ def parse_string(string, prefix = DEFAULT_PREFIX)
21
+ parse_hash(JSON.parse(string), prefix)
22
+ end
22
23
 
23
- sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(T::Hash[String, Integer]) }
24
- def self.parse_hash(obj, prefix = DEFAULT_PREFIX)
25
- obj["metrics"].each_with_object(Hash.new(0)) do |metric, metrics|
26
- name = metric["name"]
27
- name = name.sub(prefix, '')
28
- metrics[name] = metric["value"] || 0
24
+ sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(T::Hash[String, Integer]) }
25
+ def parse_hash(obj, prefix = DEFAULT_PREFIX)
26
+ obj["metrics"].each_with_object(Hash.new(0)) do |metric, metrics|
27
+ name = metric["name"]
28
+ name = name.sub(prefix, "")
29
+ metrics[name] = metric["value"] || 0
30
+ end
29
31
  end
30
32
  end
31
33
  end
@@ -27,70 +27,75 @@ module Spoom
27
27
 
28
28
  SIGIL_REGEXP = T.let(/^#[\ t]*typed[\ t]*:[ \t]*(\w*)[ \t]*/.freeze, Regexp)
29
29
 
30
- # returns the full sigil comment string for the passed strictness
31
- sig { params(strictness: String).returns(String) }
32
- def self.sigil_string(strictness)
33
- "# typed: #{strictness}"
34
- end
30
+ class << self
31
+ extend T::Sig
35
32
 
36
- # returns true if the passed string is a valid strictness (else false)
37
- sig { params(strictness: String).returns(T::Boolean) }
38
- def self.valid_strictness?(strictness)
39
- VALID_STRICTNESS.include?(strictness.strip)
40
- end
33
+ # returns the full sigil comment string for the passed strictness
34
+ sig { params(strictness: String).returns(String) }
35
+ def sigil_string(strictness)
36
+ "# typed: #{strictness}"
37
+ end
41
38
 
42
- # returns the strictness of a sigil in the passed file content string (nil if no sigil)
43
- sig { params(content: String).returns(T.nilable(String)) }
44
- def self.strictness_in_content(content)
45
- SIGIL_REGEXP.match(content)&.[](1)
46
- end
39
+ # returns true if the passed string is a valid strictness (else false)
40
+ sig { params(strictness: String).returns(T::Boolean) }
41
+ def valid_strictness?(strictness)
42
+ VALID_STRICTNESS.include?(strictness.strip)
43
+ end
47
44
 
48
- # returns a string which is the passed content but with the sigil updated to a new strictness
49
- sig { params(content: String, new_strictness: String).returns(String) }
50
- def self.update_sigil(content, new_strictness)
51
- content.sub(SIGIL_REGEXP, sigil_string(new_strictness))
52
- end
45
+ # returns the strictness of a sigil in the passed file content string (nil if no sigil)
46
+ sig { params(content: String).returns(T.nilable(String)) }
47
+ def strictness_in_content(content)
48
+ SIGIL_REGEXP.match(content)&.[](1)
49
+ end
53
50
 
54
- # returns a string containing the strictness of a sigil in a file at the passed path
55
- # * returns nil if no sigil
56
- sig { params(path: T.any(String, Pathname)).returns(T.nilable(String)) }
57
- def self.file_strictness(path)
58
- return nil unless File.file?(path)
59
- content = File.read(path, encoding: Encoding::ASCII_8BIT)
60
- strictness_in_content(content)
61
- end
51
+ # returns a string which is the passed content but with the sigil updated to a new strictness
52
+ sig { params(content: String, new_strictness: String).returns(String) }
53
+ def update_sigil(content, new_strictness)
54
+ content.sub(SIGIL_REGEXP, sigil_string(new_strictness))
55
+ end
62
56
 
63
- # changes the sigil in the file at the passed path to the specified new strictness
64
- sig { params(path: T.any(String, Pathname), new_strictness: String).returns(T::Boolean) }
65
- def self.change_sigil_in_file(path, new_strictness)
66
- content = File.read(path, encoding: Encoding::ASCII_8BIT)
67
- new_content = update_sigil(content, new_strictness)
57
+ # returns a string containing the strictness of a sigil in a file at the passed path
58
+ # * returns nil if no sigil
59
+ sig { params(path: T.any(String, Pathname)).returns(T.nilable(String)) }
60
+ def file_strictness(path)
61
+ return nil unless File.file?(path)
68
62
 
69
- File.write(path, new_content, encoding: Encoding::ASCII_8BIT)
63
+ content = File.read(path, encoding: Encoding::ASCII_8BIT)
64
+ strictness_in_content(content)
65
+ end
70
66
 
71
- strictness_in_content(new_content) == new_strictness
72
- end
67
+ # changes the sigil in the file at the passed path to the specified new strictness
68
+ sig { params(path: T.any(String, Pathname), new_strictness: String).returns(T::Boolean) }
69
+ def change_sigil_in_file(path, new_strictness)
70
+ content = File.read(path, encoding: Encoding::ASCII_8BIT)
71
+ new_content = update_sigil(content, new_strictness)
73
72
 
74
- # changes the sigil to have a new strictness in a list of files
75
- sig { params(path_list: T::Array[String], new_strictness: String).returns(T::Array[String]) }
76
- def self.change_sigil_in_files(path_list, new_strictness)
77
- path_list.filter do |path|
78
- change_sigil_in_file(path, new_strictness)
73
+ File.write(path, new_content, encoding: Encoding::ASCII_8BIT)
74
+
75
+ strictness_in_content(new_content) == new_strictness
79
76
  end
80
- end
81
77
 
82
- # finds all files in the specified directory with the passed strictness
83
- sig do
84
- params(
85
- directory: T.any(String, Pathname),
86
- strictness: String,
87
- extension: String
88
- ).returns(T::Array[String])
89
- end
90
- def self.files_with_sigil_strictness(directory, strictness, extension: ".rb")
91
- paths = Dir.glob("#{File.expand_path(directory)}/**/*#{extension}").sort.uniq
92
- paths.filter do |path|
93
- file_strictness(path) == strictness
78
+ # changes the sigil to have a new strictness in a list of files
79
+ sig { params(path_list: T::Array[String], new_strictness: String).returns(T::Array[String]) }
80
+ def change_sigil_in_files(path_list, new_strictness)
81
+ path_list.filter do |path|
82
+ change_sigil_in_file(path, new_strictness)
83
+ end
84
+ end
85
+
86
+ # finds all files in the specified directory with the passed strictness
87
+ sig do
88
+ params(
89
+ directory: T.any(String, Pathname),
90
+ strictness: String,
91
+ extension: String,
92
+ ).returns(T::Array[String])
93
+ end
94
+ def files_with_sigil_strictness(directory, strictness, extension: ".rb")
95
+ paths = Dir.glob("#{File.expand_path(directory)}/**/*#{extension}").sort.uniq
96
+ paths.filter do |path|
97
+ file_strictness(path) == strictness
98
+ end
94
99
  end
95
100
  end
96
101
  end
data/lib/spoom/sorbet.rb CHANGED
@@ -25,10 +25,10 @@ module Spoom
25
25
  arg: String,
26
26
  path: String,
27
27
  capture_err: T::Boolean,
28
- sorbet_bin: T.nilable(String)
28
+ sorbet_bin: T.nilable(String),
29
29
  ).returns(ExecResult)
30
30
  end
31
- def srb(*arg, path: '.', capture_err: false, sorbet_bin: nil)
31
+ def srb(*arg, path: ".", capture_err: false, sorbet_bin: nil)
32
32
  if sorbet_bin
33
33
  arg.prepend(sorbet_bin)
34
34
  else
@@ -42,20 +42,20 @@ module Spoom
42
42
  arg: String,
43
43
  path: String,
44
44
  capture_err: T::Boolean,
45
- sorbet_bin: T.nilable(String)
45
+ sorbet_bin: T.nilable(String),
46
46
  ).returns(ExecResult)
47
47
  end
48
- def srb_tc(*arg, path: '.', capture_err: false, sorbet_bin: nil)
48
+ def srb_tc(*arg, path: ".", capture_err: false, sorbet_bin: nil)
49
49
  arg.prepend("tc") unless sorbet_bin
50
50
  srb(*T.unsafe(arg), path: path, capture_err: capture_err, sorbet_bin: sorbet_bin)
51
51
  end
52
52
 
53
53
  # List all files typechecked by Sorbet from its `config`
54
54
  sig { params(config: Config, path: String).returns(T::Array[String]) }
55
- def srb_files(config, path: '.')
55
+ def srb_files(config, path: ".")
56
56
  regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
57
- exts = config.allowed_extensions.empty? ? ['.rb', '.rbi'] : config.allowed_extensions
58
- Dir.glob((Pathname.new(path) / "**/*{#{exts.join(',')}}").to_s).reject do |f|
57
+ exts = config.allowed_extensions.empty? ? [".rb", ".rbi"] : config.allowed_extensions
58
+ Dir.glob((Pathname.new(path) / "**/*{#{exts.join(",")}}").to_s).reject do |f|
59
59
  regs.any? { |re| re.match?(f) }
60
60
  end.sort
61
61
  end
@@ -65,19 +65,20 @@ module Spoom
65
65
  arg: String,
66
66
  path: String,
67
67
  capture_err: T::Boolean,
68
- sorbet_bin: T.nilable(String)
68
+ sorbet_bin: T.nilable(String),
69
69
  ).returns(T.nilable(String))
70
70
  end
71
- def srb_version(*arg, path: '.', capture_err: false, sorbet_bin: nil)
71
+ def srb_version(*arg, path: ".", capture_err: false, sorbet_bin: nil)
72
72
  result = T.let(T.unsafe(self).srb_tc(
73
73
  "--no-config",
74
74
  "--version",
75
75
  *arg,
76
76
  path: path,
77
77
  capture_err: capture_err,
78
- sorbet_bin: sorbet_bin
78
+ sorbet_bin: sorbet_bin,
79
79
  ), ExecResult)
80
80
  return nil unless result.status
81
+
81
82
  result.out.split(" ")[2]
82
83
  end
83
84
 
@@ -86,10 +87,10 @@ module Spoom
86
87
  arg: String,
87
88
  path: String,
88
89
  capture_err: T::Boolean,
89
- sorbet_bin: T.nilable(String)
90
+ sorbet_bin: T.nilable(String),
90
91
  ).returns(T.nilable(T::Hash[String, Integer]))
91
92
  end
92
- def srb_metrics(*arg, path: '.', capture_err: false, sorbet_bin: nil)
93
+ def srb_metrics(*arg, path: ".", capture_err: false, sorbet_bin: nil)
93
94
  metrics_file = "metrics.tmp"
94
95
  metrics_path = "#{path}/#{metrics_file}"
95
96
  T.unsafe(self).srb_tc(
@@ -98,7 +99,7 @@ module Spoom
98
99
  *arg,
99
100
  path: path,
100
101
  capture_err: capture_err,
101
- sorbet_bin: sorbet_bin
102
+ sorbet_bin: sorbet_bin,
102
103
  )
103
104
  if File.exist?(metrics_path)
104
105
  metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
@@ -112,11 +113,13 @@ module Spoom
112
113
  #
113
114
  # Returns `nil` if `gem` cannot be found in the Gemfile.
114
115
  sig { params(gem: String, path: String).returns(T.nilable(String)) }
115
- def version_from_gemfile_lock(gem: 'sorbet', path: '.')
116
+ def version_from_gemfile_lock(gem: "sorbet", path: ".")
116
117
  gemfile_path = "#{path}/Gemfile.lock"
117
118
  return nil unless File.exist?(gemfile_path)
119
+
118
120
  content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
119
121
  return nil unless content
122
+
120
123
  content[1]
121
124
  end
122
125
  end
@@ -15,7 +15,7 @@ module Spoom
15
15
  end
16
16
 
17
17
  # Return one commit for each month between `from` and `to`
18
- sig { returns(T::Array[String]) }
18
+ sig { returns(T::Array[Git::Commit]) }
19
19
  def ticks
20
20
  commits_for_dates(months)
21
21
  end
@@ -34,20 +34,21 @@ module Spoom
34
34
  end
35
35
 
36
36
  # Return one commit for each date in `dates`
37
- sig { params(dates: T::Array[Time]).returns(T::Array[String]) }
37
+ sig { params(dates: T::Array[Time]).returns(T::Array[Git::Commit]) }
38
38
  def commits_for_dates(dates)
39
39
  dates.map do |t|
40
40
  result = Spoom::Git.log(
41
41
  "--since='#{t}'",
42
42
  "--until='#{t.to_date.next_month}'",
43
- "--format='format:%h'",
43
+ "--format='format:%h %at'",
44
44
  "--author-date-order",
45
45
  "-1",
46
46
  path: @path,
47
47
  )
48
48
  next if result.out.empty?
49
- result.out
50
- end.compact.uniq
49
+
50
+ Git.parse_commit(result.out.strip)
51
+ end.compact.uniq(&:sha)
51
52
  end
52
53
  end
53
54
  end
data/lib/spoom/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.1.11"
5
+ VERSION = "1.1.13"
6
6
  end
data/lib/spoom.rb CHANGED
@@ -12,41 +12,59 @@ module Spoom
12
12
  class Error < StandardError; end
13
13
 
14
14
  class ExecResult < T::Struct
15
+ extend T::Sig
16
+
15
17
  const :out, String
16
18
  const :err, String
17
19
  const :status, T::Boolean
18
20
  const :exit_code, Integer
19
- end
20
21
 
21
- sig do
22
- params(
23
- cmd: String,
24
- arg: String,
25
- path: String,
26
- capture_err: T::Boolean
27
- ).returns(ExecResult)
22
+ sig { returns(String) }
23
+ def to_s
24
+ <<~STR
25
+ ########## STDOUT ##########
26
+ #{out.empty? ? "<empty>" : out}
27
+ ########## STDERR ##########
28
+ #{err.empty? ? "<empty>" : err}
29
+ ########## STATUS: #{status} ##########
30
+ STR
31
+ end
28
32
  end
29
- def self.exec(cmd, *arg, path: '.', capture_err: false)
30
- if capture_err
31
- stdout, stderr, status = T.unsafe(Open3).capture3([cmd, *arg].join(" "), chdir: path)
32
- ExecResult.new(
33
- out: stdout,
34
- err: stderr,
35
- status: status.success?,
36
- exit_code: status.exitstatus
37
- )
38
- else
39
- stdout, status = T.unsafe(Open3).capture2([cmd, *arg].join(" "), chdir: path)
40
- ExecResult.new(
41
- out: stdout,
42
- err: "",
43
- status: status.success?,
44
- exit_code: status.exitstatus
45
- )
33
+
34
+ class << self
35
+ extend T::Sig
36
+
37
+ sig do
38
+ params(
39
+ cmd: String,
40
+ arg: String,
41
+ path: String,
42
+ capture_err: T::Boolean,
43
+ ).returns(ExecResult)
44
+ end
45
+ def exec(cmd, *arg, path: ".", capture_err: false)
46
+ if capture_err
47
+ stdout, stderr, status = T.unsafe(Open3).capture3([cmd, *arg].join(" "), chdir: path)
48
+ ExecResult.new(
49
+ out: stdout,
50
+ err: stderr,
51
+ status: status.success?,
52
+ exit_code: status.exitstatus,
53
+ )
54
+ else
55
+ stdout, status = T.unsafe(Open3).capture2([cmd, *arg].join(" "), chdir: path)
56
+ ExecResult.new(
57
+ out: stdout,
58
+ err: "",
59
+ status: status.success?,
60
+ exit_code: status.exitstatus,
61
+ )
62
+ end
46
63
  end
47
64
  end
48
65
  end
49
66
 
67
+ require "spoom/context"
50
68
  require "spoom/colors"
51
69
  require "spoom/sorbet"
52
70
  require "spoom/cli"