spoom 1.1.11 → 1.1.13

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