-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommands.rb
More file actions
66 lines (59 loc) · 2.15 KB
/
commands.rb
File metadata and controls
66 lines (59 loc) · 2.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# frozen_string_literal: true
require "open3"
require "json"
module Importmap
module Update
# Abstracts execution of external commands (gh, git, bin/importmap) so
# the rest of the codebase doesn't shell out directly. This is the seam
# tests hook into — production code runs commands for real, tests inject
# a FixtureRunner that replays pre-recorded (argv → stdout, exit) tuples.
#
# The interface deliberately mirrors what Open3.capture3 returns:
#
# runner.run("gh", "pr", "list", "--state", "open")
# # => Result(stdout: "...", stderr: "...", success: true, exit: 0)
#
# Commands are passed as an argv array, not a shell string. That's both
# safer (no shell-injection surprises from package names) and easier to
# match against fixture keys.
module Commands
Result = Struct.new(:stdout, :stderr, :exit_code, keyword_init: true) do
def success?
exit_code == 0
end
end
class CommandError < StandardError
attr_reader :argv, :result
def initialize(argv, result)
@argv = argv
@result = result
super("`#{argv.join(" ")}` exited #{result.exit_code}: #{result.stderr.strip}")
end
end
# Production runner: actually executes the command.
class ShellRunner
# @param cwd [String, nil] working directory (defaults to current)
# @param env [Hash, nil] additional environment variables
def initialize(cwd: nil, env: nil)
@cwd = cwd
@env = env || {}
end
def run(*argv)
opts = {}
opts[:chdir] = @cwd if @cwd
Bundler.with_unbundled_env do
stdout, stderr, status = Open3.capture3(@env, *argv, opts)
Result.new(stdout: stdout, stderr: stderr, exit_code: status.exitstatus)
end
end
# Raises on non-zero exit. Use when you have no recovery strategy
# and just want to surface the error to the caller.
def run!(*argv)
result = run(*argv)
raise CommandError.new(argv, result) unless result.success?
result
end
end
end
end
end