cobra_commander 1.0.1 → 1.2.0

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/cobra_commander.gemspec +15 -20
  3. data/docs/CHANGELOG.md +10 -0
  4. data/lib/cobra_commander/affected.rb +6 -41
  5. data/lib/cobra_commander/cli/filters.rb +1 -1
  6. data/lib/cobra_commander/cli/output/ascii_tree.rb +2 -2
  7. data/lib/cobra_commander/cli/output/change.rb +12 -52
  8. data/lib/cobra_commander/cli.rb +21 -30
  9. data/lib/cobra_commander/component.rb +29 -0
  10. data/lib/cobra_commander/executor/buffered_printer.rb +41 -0
  11. data/lib/cobra_commander/executor/command.rb +22 -45
  12. data/lib/cobra_commander/executor/isolated_pty.rb +20 -0
  13. data/lib/cobra_commander/executor/output_prompt.rb +59 -0
  14. data/lib/cobra_commander/executor/package_criteria.rb +0 -3
  15. data/lib/cobra_commander/executor/run_script.rb +25 -0
  16. data/lib/cobra_commander/executor/script.rb +16 -30
  17. data/lib/cobra_commander/executor/worker_pool.rb +104 -0
  18. data/lib/cobra_commander/executor.rb +33 -31
  19. data/lib/cobra_commander/git_changed.rb +2 -2
  20. data/lib/cobra_commander/package.rb +5 -1
  21. data/lib/cobra_commander/source.rb +13 -2
  22. data/lib/cobra_commander/version.rb +1 -1
  23. metadata +48 -89
  24. data/.gitignore +0 -16
  25. data/.rspec +0 -3
  26. data/.rubocop.yml +0 -8
  27. data/Gemfile +0 -12
  28. data/Guardfile +0 -14
  29. data/Rakefile +0 -10
  30. data/bin/console +0 -15
  31. data/bin/setup +0 -8
  32. data/doc/dependency_decisions.yml +0 -9
  33. data/lib/cobra_commander/executor/execution.rb +0 -52
  34. data/lib/cobra_commander/executor/interactive_printer.rb +0 -53
  35. data/lib/cobra_commander/executor/job.rb +0 -51
  36. data/lib/cobra_commander/executor/markdown_printer.rb +0 -21
  37. data/lib/cobra_commander/executor/spinners.rb +0 -40
  38. data/mkdocs.yml +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5f6e4d7e8d890c46aed5efff6219f669e79f64325f6f73c899cf6a0c5959088
4
- data.tar.gz: 3fe47e6df4f555345c640051ff6aebf25dea8a5907507e6be4ebcc18876897c1
3
+ metadata.gz: da1b8808311f316b8e200895575b653357a88b92f8a1d05d833ab8f0bd55b0d5
4
+ data.tar.gz: 442d8ae693c4bac9f3d888a33fc4b5e1d1a5904c681dba43144284651b42abfd
5
5
  SHA512:
6
- metadata.gz: 0add3576fb98e23aa8332939c2c19d7b4dd04cb82ab3d7cebb12191687fbe72e43efad27ddfa24c80d4393d63898f8f01915af1aa6a74a2331c00c1f57dc99a6
7
- data.tar.gz: '03942e8553ee6163ad3703ce4be836ec2d538ea908cbd32d7fcd274a01b316a37f77407e32758c8d57043ec3e5a0b30007b7398e1c1a0d21018e245d8c2c09d6'
6
+ metadata.gz: '07940522fad6ad8f3220e25f654a19a34a8b61497b0f68c33c368db9926fd8f32446e4423112f91630511c447469866398b152736db79fc726e27dd96809d0c4'
7
+ data.tar.gz: a1e5b9db402504b0114c7cad3a65a4ba22bce8ba003269abb03a824128df5d586c1c39fde4d9132da97d71143b2b3d65bb6966dba464a8068e5193fb159aa373
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path("lib", __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require "cobra_commander/version"
3
+ require_relative "lib/cobra_commander/version"
6
4
 
7
5
  Gem::Specification.new do |spec|
8
6
  spec.name = "cobra_commander"
@@ -25,33 +23,30 @@ Gem::Specification.new do |spec|
25
23
  DESCRIPTION
26
24
  spec.homepage = "http://tech.powerhrg.com/cobra_commander/"
27
25
  spec.license = "MIT"
26
+ spec.required_ruby_version = ">= 3.2.0"
28
27
 
29
28
  spec.metadata["rubygems_mfa_required"] = "true"
30
29
  spec.metadata["homepage_uri"] = spec.homepage
31
30
  spec.metadata["source_code_uri"] = "https://github.com/powerhome/cobra_commander"
32
31
  spec.metadata["changelog_uri"] = "https://github.com/powerhome/cobra_commander/blob/main/cobra_commander/docs/CHANGELOG.md"
33
32
 
34
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
35
- f.match(%r{^(test|spec|features)/})
36
- end
33
+ spec.files = Dir["{docs,exe,lib}/**/*"] + ["cobra_commander.gemspec"]
37
34
  spec.bindir = "exe"
38
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
39
- spec.require_paths = ["lib"]
35
+ spec.executables = %w[cobra]
36
+ spec.require_paths = %w[lib]
40
37
 
41
- spec.add_dependency "bundler"
42
- spec.add_dependency "concurrent-ruby", "~> 1.1"
38
+ spec.add_dependency "bundler", ">= 2.4.17"
43
39
  spec.add_dependency "thor", ["< 2.0", ">= 0.18.1"]
44
40
  spec.add_dependency "tty-command", "~> 0.10.0"
45
41
  spec.add_dependency "tty-prompt", "~> 0.23.1"
46
- spec.add_dependency "tty-spinner", "~> 0.9.3"
47
42
 
48
- spec.add_development_dependency "aruba", "~> 0.14.2"
49
- spec.add_development_dependency "bundler"
50
- spec.add_development_dependency "guard-rspec"
51
- spec.add_development_dependency "license_finder", ">= 7.0"
52
- spec.add_development_dependency "pry"
53
- spec.add_development_dependency "rake", ">= 12.3.3"
54
- spec.add_development_dependency "rspec", "~> 3.5"
55
- spec.add_development_dependency "rubocop", "1.30.1"
56
- spec.add_development_dependency "rubocop-powerhome", ">= 0.5.0"
43
+ spec.add_development_dependency "aruba", "0.14.14"
44
+ spec.add_development_dependency "guard-rspec", "4.7.3"
45
+ spec.add_development_dependency "license_finder", "7.1"
46
+ spec.add_development_dependency "ostruct", "0.6.3"
47
+ spec.add_development_dependency "pry", "0.14.2"
48
+ spec.add_development_dependency "rake", "13.0.6"
49
+ spec.add_development_dependency "rspec", "3.13.0"
50
+ spec.add_development_dependency "rubocop", "1.82.1"
51
+ spec.add_development_dependency "rubocop-powerhome", "0.6.1"
57
52
  end
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Change Log
2
2
 
3
+ ## Version 1.2.0 - 2026-06-09
4
+
5
+ * Add an `around_command` extension point so plugins can enhance the shell used to run `cmd`/`exec`. `Source#around_command` wraps execution (e.g. Bundler isolation) and yields a hash of environment variables for the command (base yields `{}`, scoped per-execution via TTY). Packages delegate to their source; `Component#around_command` nests each distinct source's wrapper and yields their merged env. `exec` applies it once per component (all packages contribute, later sources win); `cmd` applies each package's own. `IsolatedPTY` is now a plain PTY command — isolation comes from the sources.
6
+ * Raise the minimum supported Ruby version to 3.2 and expand CI to cover Ruby 4.0.
7
+
8
+ ## Version 1.1.0 - 2023-03-09
9
+
10
+ * New Executor by @xjunior in [#104](https://github.com/powerhome/cobra_commander/pull/104)
11
+ * Cleanup cobra changes by @xjunior in [#105](https://github.com/powerhome/cobra_commander/pull/105)
12
+
3
13
  ## Version 1.0.1 - 2023-01-05
4
14
 
5
15
  * Fix Umbrella#resolve unable to resolve a path relative to the project @xjunior [#103](https://github.com/powerhome/cobra_commander/pull/103)
@@ -1,25 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
-
5
3
  module CobraCommander
6
4
  # Calculates directly & transitively affected components
5
+ # Takes a list of changes, and resolves all components affected
6
+ #
7
7
  class Affected
8
+ include Enumerable
9
+
8
10
  def initialize(umbrella, changes)
9
11
  @umbrella = umbrella
10
12
  @changes = changes
11
13
  end
12
14
 
13
- def names
14
- @names ||= all.map(&:name)
15
- end
16
-
17
- def all
18
- @all ||= (directly | transitively).sort_by(&:name)
19
- end
20
-
21
- def scripts
22
- @scripts ||= paths.map { |path| path.join("test.sh") }
15
+ def each(&)
16
+ (directly | transitively).sort_by(&:name).each(&)
23
17
  end
24
18
 
25
19
  def directly
@@ -31,34 +25,5 @@ module CobraCommander
31
25
  @transitively ||= directly.flat_map(&:deep_dependents)
32
26
  .uniq.sort_by(&:name)
33
27
  end
34
-
35
- def to_json(*_args)
36
- {
37
- changed_files: @changes,
38
- directly_affected_components: directly.map { |c| affected_component(c) },
39
- transitively_affected_components: transitively.map { |c| affected_component(c) },
40
- test_scripts: scripts,
41
- component_names: names,
42
- languages: all_affected_packages,
43
- }.to_json
44
- end
45
-
46
- private
47
-
48
- def affected_component(component)
49
- {
50
- name: component.name,
51
- path: component.root_paths.map(&:to_s),
52
- type: component.packages.map(&:key).map(&:to_s),
53
- }
54
- end
55
-
56
- def paths
57
- @paths ||= all.map(&:root_paths).flatten
58
- end
59
-
60
- def all_affected_packages
61
- all.flat_map(&:packages).map(&:key).uniq
62
- end
63
28
  end
64
29
  end
@@ -29,7 +29,7 @@ module CobraCommander
29
29
 
30
30
  def affected_by_changes(origin_branch)
31
31
  changes = GitChanged.new(umbrella.path, origin_branch)
32
- Affected.new(umbrella, changes).all
32
+ Affected.new(umbrella, changes).to_a
33
33
  end
34
34
 
35
35
  def filter_component(component_name)
@@ -43,12 +43,12 @@ module CobraCommander
43
43
 
44
44
  def add_tee(io, outdents, dep)
45
45
  io.puts line(outdents, TEE, dep.name)
46
- list_dependencies(io, dep, (outdents + [BAR]))
46
+ list_dependencies(io, dep, outdents + [BAR])
47
47
  end
48
48
 
49
49
  def add_corner(io, outdents, dep)
50
50
  io.puts line(outdents, CORNER, dep.name)
51
- list_dependencies(io, dep, (outdents + [SPACE]))
51
+ list_dependencies(io, dep, outdents + [SPACE])
52
52
  end
53
53
 
54
54
  def line(outdents, sym, name)
@@ -6,27 +6,19 @@ require "cobra_commander/affected"
6
6
  module CobraCommander
7
7
  class CLI
8
8
  module Output
9
- # Calculates and prints affected components & files
10
9
  class Change
11
- InvalidSelectionError = Class.new(StandardError)
12
-
13
- def initialize(umbrella, oformat, branch, changes: nil)
14
- @format = oformat
10
+ def initialize(umbrella, branch, changes: nil)
15
11
  @branch = branch
16
12
  @umbrella = umbrella
17
13
  @changes = changes || GitChanged.new(umbrella.path, branch)
18
14
  end
19
15
 
20
16
  def run!
21
- assert_valid_result_choice
22
- if selected_format?("json")
23
- puts affected.to_json
24
- else
25
- show_full if selected_format?("full")
26
- tests_to_run
27
- end
28
- rescue GitChanged::InvalidSelectionError => e
29
- puts e.message
17
+ print_changes_since_last_commit
18
+ puts
19
+ print_directly_affected_components
20
+ puts
21
+ print_transitively_affected_components
30
22
  end
31
23
 
32
24
  private
@@ -35,55 +27,23 @@ module CobraCommander
35
27
  @affected ||= Affected.new(@umbrella, @changes)
36
28
  end
37
29
 
38
- def show_full
39
- changes_since_last_commit
40
- directly_affected_components
41
- transitively_affected_components
42
- end
43
-
44
- def assert_valid_result_choice
45
- return if %w[test full name json].include?(@format)
46
-
47
- raise InvalidSelectionError, "--results must be 'test', 'full', 'name' or 'json'"
48
- end
49
-
50
- def selected_format?(result)
51
- @format == result
52
- end
53
-
54
- def changes_since_last_commit
30
+ def print_changes_since_last_commit
55
31
  puts "<<< Changes since last commit on #{@branch} >>>"
56
32
  puts(*@changes) if @changes.any?
57
- puts blank_line
58
33
  end
59
34
 
60
- def directly_affected_components
35
+ def print_directly_affected_components
61
36
  puts "<<< Directly affected components >>>"
62
- affected.directly.each { |component| puts display(component) }
63
- puts blank_line
37
+ affected.directly.each { |component| display(component) }
64
38
  end
65
39
 
66
- def transitively_affected_components
40
+ def print_transitively_affected_components
67
41
  puts "<<< Transitively affected components >>>"
68
- affected.transitively.each { |component| puts display(component) }
69
- puts blank_line
70
- end
71
-
72
- def tests_to_run
73
- puts "<<< Test scripts to run >>>" if selected_format?("full")
74
- if selected_format?("name")
75
- affected.names.each { |component_name| puts component_name }
76
- else
77
- affected.scripts.each { |component_script| puts component_script }
78
- end
42
+ affected.transitively.each { |component| display(component) }
79
43
  end
80
44
 
81
45
  def display(component)
82
- "#{component.name} - #{component.packages.map(&:key).map(&:to_s).map(&:capitalize).join(' & ')}"
83
- end
84
-
85
- def blank_line
86
- ""
46
+ puts "#{component.name} - #{component.packages.map(&:key).map(&:to_s).map(&:capitalize).join(' & ')}"
87
47
  end
88
48
  end
89
49
  end
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "etc"
3
4
  require "thor"
4
5
  require "fileutils"
5
- require "concurrent-ruby"
6
-
7
6
  require "cobra_commander"
8
7
 
9
8
  module CobraCommander
10
9
  # Implements the tool's CLI
11
10
  class CLI < Thor
11
+ DEFAULT_CONCURRENCY = (Etc.nprocessors / 2.0).ceil
12
+
12
13
  require_relative "cli/filters"
13
14
  require_relative "cli/output/ascii_tree"
14
15
  require_relative "cli/output/change"
15
16
  require_relative "cli/output/dot_graph"
16
17
 
17
- DEFAULT_CONCURRENCY = (Concurrent.processor_count / 2.0).ceil
18
-
19
18
  class_option :app, default: Dir.pwd, aliases: "-a", type: :string
20
19
  Source.all.keys.each do |key|
21
20
  class_option key, default: false, type: :boolean, desc: "Consider only the #{key} dependency graph"
@@ -39,46 +38,42 @@ module CobraCommander
39
38
  "Defaults to all components."
40
39
  filter_options dependents: "Run the script on each dependent of a given component",
41
40
  dependencies: "Run the script on each dependency of a given component"
42
- method_option :concurrency, type: :numeric, default: DEFAULT_CONCURRENCY, aliases: "-c",
43
- desc: "Max number of jobs to run concurrently"
41
+ method_option :concurrency, type: :numeric, aliases: "-c", desc: "Max number of jobs to run concurrently",
42
+ default: DEFAULT_CONCURRENCY
44
43
  method_option :interactive, type: :boolean, default: true, aliases: "-i",
45
44
  desc: "Runs in interactive mode to allow the user to inspect the output of each " \
46
45
  "component"
47
46
  def exec(script_or_components, script = nil)
48
- jobs = CobraCommander::Executor::Script.for(
49
- components_filtered(script && script_or_components),
50
- script || script_or_components
47
+ CobraCommander::Executor.execute_and_handle_exit(
48
+ runner: ::CobraCommander::Executor::Script.new(script || script_or_components),
49
+ workers: options.concurrency,
50
+ interactive: options.interactive,
51
+ jobs: components_filtered(script && script_or_components)
51
52
  )
52
- output_mode = options.interactive && jobs.count > 1 ? :interactive : :markdown
53
- execution = CobraCommander::Executor.execute(jobs: jobs, workers: options.concurrency,
54
- output_mode: output_mode, output: $stdout, status_output: $stderr)
55
- exit 1 unless execution.success?
56
53
  end
57
54
 
58
55
  desc "cmd [components] <command>", "Executes the command in the context of a given component or set thereof. " \
59
56
  "Defaults to all components."
60
57
  filter_options dependents: "Run the command on each dependent of a given component",
61
58
  dependencies: "Run the command on each dependency of a given component"
62
- method_option :concurrency, type: :numeric, default: DEFAULT_CONCURRENCY, aliases: "-c",
63
- desc: "Max number of jobs to run concurrently"
59
+ method_option :concurrency, type: :numeric, aliases: "-c", desc: "Max number of jobs to run concurrently",
60
+ default: DEFAULT_CONCURRENCY
64
61
  method_option :interactive, type: :boolean, default: true, aliases: "-i",
65
62
  desc: "Runs in interactive mode to allow the user to inspect the output of each " \
66
63
  "component"
67
64
  def cmd(command_or_components, command = nil)
68
- jobs = CobraCommander::Executor::Command.for(
69
- components_filtered(command && command_or_components),
70
- command || command_or_components
65
+ CobraCommander::Executor.execute_and_handle_exit(
66
+ runner: ::CobraCommander::Executor::Command.new(command || command_or_components),
67
+ workers: options.concurrency,
68
+ interactive: options.interactive,
69
+ jobs: components_filtered(command && command_or_components).flat_map(&:packages)
71
70
  )
72
- output_mode = options.interactive && jobs.count > 1 ? :interactive : :markdown
73
- execution = CobraCommander::Executor.execute(jobs: jobs, workers: options.concurrency,
74
- output_mode: output_mode, output: $stdout, status_output: $stderr)
75
- exit 1 unless execution.success?
76
71
  end
77
72
 
78
73
  desc "tree [component]", "Prints the dependency tree of a given component or umbrella"
79
74
  def tree(component = nil)
80
75
  components = component ? [find_component(component)] : umbrella.components
81
- puts Output::AsciiTree.new(components).to_s
76
+ puts Output::AsciiTree.new(components)
82
77
  end
83
78
 
84
79
  desc "graph [component]", "Outputs a graph of a given component or umbrella"
@@ -97,22 +92,18 @@ module CobraCommander
97
92
  end
98
93
 
99
94
  desc "changes [--results=RESULTS] [--branch=BRANCH]", "Prints list of changed files"
100
- method_option :results, default: "test", aliases: "-r", desc: "Accepts test, full, name or json"
101
95
  method_option :branch, default: "master", aliases: "-b", desc: "Specified target to calculate against"
102
96
  def changes
103
- Output::Change.new(umbrella, options.results, options.branch).run!
97
+ Output::Change.new(umbrella, options.branch).run!
104
98
  end
105
99
 
106
100
  private
107
101
 
108
102
  def umbrella
109
- selector = Source.all.keys.reduce({}) do |sel, key|
103
+ selector = CobraCommander::Source.all.keys.reduce({}) do |sel, key|
110
104
  sel.merge(key => options.public_send(key))
111
105
  end
112
- @umbrella ||= CobraCommander::Umbrella.new(
113
- options.app,
114
- **selector
115
- )
106
+ @umbrella ||= CobraCommander::Umbrella.new(options.app, **selector)
116
107
  rescue ::CobraCommander::Source::Error => e
117
108
  error(e.message)
118
109
  exit(1)
@@ -12,6 +12,10 @@ module CobraCommander
12
12
  @packages = []
13
13
  end
14
14
 
15
+ def describe
16
+ "#{name} (#{packages.map(&:key).join(', ')})"
17
+ end
18
+
15
19
  def add_package(package)
16
20
  @packages << package
17
21
  @dependency_names |= package.dependencies
@@ -21,6 +25,18 @@ module CobraCommander
21
25
  @packages.map(&:path).uniq
22
26
  end
23
27
 
28
+ # Wraps the execution of a command on this component by nesting the
29
+ # around_command of each distinct source backing its packages, so every
30
+ # package gets a chance to enhance the surrounding process. The env vars
31
+ # each source yields are merged (later sources win) and the merged hash is
32
+ # yielded to the block.
33
+ #
34
+ # @yieldparam env [Hash{String => String}] merged env vars for the command
35
+ # @return the value returned by the block
36
+ def around_command(&)
37
+ nest_sources(@packages.map(&:source).uniq, {}, &)
38
+ end
39
+
24
40
  def inspect
25
41
  "#<CobraCommander::Component:#{object_id} #{name} dependencies=#{dependencies.map(&:name)} packages=#{packages}>"
26
42
  end
@@ -46,5 +62,18 @@ module CobraCommander
46
62
  def dependencies
47
63
  @dependencies ||= @dependency_names.sort.filter_map { |name| @umbrella.find(name) }
48
64
  end
65
+
66
+ private
67
+
68
+ # Nests each source's #around_command, accumulating the env each yields,
69
+ # and finally yields the merged env to the block.
70
+ def nest_sources(sources, env, &block)
71
+ return yield(env) if sources.empty?
72
+
73
+ head, *rest = sources
74
+ head.around_command do |source_env|
75
+ nest_sources(rest, env.merge(source_env), &block)
76
+ end
77
+ end
49
78
  end
50
79
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ module Executor
5
+ class BufferedPrinter < TTY::Command::Printers::Null
6
+ TIME_FORMAT = "Finished in %5.3fs"
7
+
8
+ def initialize(*)
9
+ super
10
+
11
+ @buffers = Hash.new { |h, k| h[k] = StringIO.new }
12
+ end
13
+
14
+ def write(cmd, message)
15
+ @buffers[cmd.uuid].write message
16
+ end
17
+
18
+ def print_command_out_data(cmd, *args)
19
+ write(cmd, args.join)
20
+ end
21
+
22
+ def print_command_err_data(cmd, *args)
23
+ write(cmd, args.join)
24
+ end
25
+
26
+ def print_command_start(cmd, *args)
27
+ message = "Running #{decorate(cmd.to_command, :yellow, :bold)} #{args.join(' ')}"
28
+ write(cmd, message)
29
+ end
30
+
31
+ def print_command_exit(cmd, status, runtime, *)
32
+ message = TIME_FORMAT % runtime
33
+ message << " with exit status #{status}" if status
34
+
35
+ output.puts @buffers.delete(cmd.uuid).string
36
+ output.puts decorate(message, status == 0 ? :green : :red)
37
+ output.puts
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,74 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./job"
4
- require_relative "./package_criteria"
5
-
6
3
  module CobraCommander
7
4
  module Executor
8
5
  class Command
9
- include ::CobraCommander::Executor::Job
10
6
  include ::CobraCommander::Executor::PackageCriteria
7
+ include ::CobraCommander::Executor::RunScript
11
8
 
12
9
  SKIP_UNEXISTING = "Command %s does not exist. Check your cobra.yml for existing commands in %s."
13
10
  SKIP_CRITERIA = "Package %s does not match criteria."
14
11
 
15
- # Builds the given commands in all packages of all components.
16
- #
17
- # @param components [Enumerable<CobraCommander::Component>] the target components
18
- # @param command [String] shell script to run from the directories of the component's packages
19
- # @return [Array<CobraCommander::Executor::Command>]
20
- def self.for(components, command)
21
- components.flat_map(&:packages).map do |package|
22
- new(package, command)
23
- end
12
+ def initialize(command_name)
13
+ @command_name = command_name
24
14
  end
25
15
 
26
16
  # Calls the commands sequentially, stopping ony if an :error happens.
27
17
  #
28
18
  # If one of the commands skips, the result will be :success.
29
19
  #
30
- # @param commands [Enumerable<CobraCommander::Component>] the target components
31
- # @return [Array<CobraCommander::Executor::Command>]
32
- # @see CobraCommander::Executor::Job
33
- def self.join(commands)
34
- commands.lazy.map(&:call).reduce do |(_, prev_output), (result, output)|
35
- new_output = [prev_output&.strip, output&.strip].join("\n")
36
- return [:error, new_output] if result == :error
37
-
38
- [:success, new_output]
20
+ # @param tty [CobraComander::Executor::IsolatedPTY] tty to execute shell scripts
21
+ # @param package [CobraComander::Package] target package to execute the named command
22
+ # @return [Array<Symbol, String>]
23
+ def call(tty, package)
24
+ package.around_command do |env|
25
+ run_command tty, package, @command_name, env
39
26
  end
40
27
  end
41
28
 
42
- def initialize(package, command)
43
- @package = package
44
- @command = command
45
- end
46
-
47
- def to_s
48
- "#{@package.name} (#{@package.key})"
49
- end
29
+ private
50
30
 
51
- # @see CobraCommander::Executor::Job
52
- def call
53
- command = @package.source.config&.dig("commands", @command)
54
- case command
55
- when Array then run_multiple(@package, command)
56
- when Hash then run_with_criteria(command)
57
- when nil then skip(format(SKIP_UNEXISTING, @command, @package.key))
58
- else run_script(command, @package.path)
31
+ def run_command(tty, package, command_name, env)
32
+ definition = package.source.config&.dig("commands", command_name)
33
+ case definition
34
+ when Array then run_multiple(tty, package, definition, env)
35
+ when Hash then run_with_criteria(tty, package, definition, env)
36
+ when nil then [:skip, format(SKIP_UNEXISTING, command_name, package.key)]
37
+ else run_script(tty, definition, package.path, env: env)
59
38
  end
60
39
  end
61
40
 
62
- private
63
-
64
- def run_with_criteria(command)
65
- return skip(format(SKIP_CRITERIA, @package.name)) unless match_criteria?(@package, command.fetch("if", {}))
41
+ def run_with_criteria(tty, package, command, env)
42
+ return [:skip, format(SKIP_CRITERIA, package.name)] unless match_criteria?(package, command.fetch("if", {}))
66
43
 
67
- run_script(command["run"], @package.path)
44
+ run_script(tty, command["run"], package.path, env: env)
68
45
  end
69
46
 
70
- def run_multiple(package, commands)
71
- Command.join(commands.map { |command| Command.new(package, command) })
47
+ def run_multiple(tty, package, commands, env)
48
+ run_many(commands) { run_command(tty, package, _1, env) }
72
49
  end
73
50
  end
74
51
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ module Executor
5
+ #
6
+ # A PTY-enabled TTY::Command used to execute component scripts.
7
+ #
8
+ # Per-execution environment variables are supplied to TTY::Command#run!
9
+ # (so they are scoped to each subprocess and never mutate the parent
10
+ # ENV), while environment isolation/enhancement such as Bundler's
11
+ # `with_unbundled_env` is contributed by the package sources through
12
+ # their #around_command (see CobraCommander::Ruby::Bundle).
13
+ #
14
+ class IsolatedPTY < ::TTY::Command
15
+ def initialize(**)
16
+ super(pty: true, **)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ module Executor
5
+ # Runs an interactive output printer
6
+ class OutputPrompt
7
+ def self.run(pool, output = $stdout)
8
+ new(pool.jobs).run(output)
9
+ end
10
+
11
+ pastel = Pastel.new
12
+ ICONS = {
13
+ nil => pastel.dim("⦻"),
14
+ success: pastel.green("✔"),
15
+ skip: pastel.yellow("↷"),
16
+ error: pastel.red("✖"),
17
+ }.freeze
18
+ CANCELLED = pastel.dim("(cancelled)")
19
+ BYE = pastel.bold("\n\n👋 Bye!")
20
+
21
+ def initialize(jobs)
22
+ @jobs = jobs
23
+ @prompt = TTY::Prompt.new(symbols: { cross: " " })
24
+ end
25
+
26
+ def options
27
+ @options ||= @jobs.map do |job|
28
+ {
29
+ name: format_name(job),
30
+ value: job,
31
+ disabled: !job.resolved? && CANCELLED,
32
+ }
33
+ end
34
+ end
35
+
36
+ def run(output)
37
+ return unless @jobs.any?(&:resolved?)
38
+
39
+ selected = nil
40
+ output.puts
41
+ loop do
42
+ selected = @prompt.select("Print output?", options, default: format_name(selected))
43
+ output.puts nil, selected.output, nil
44
+ rescue TTY::Reader::InputInterrupt
45
+ output.puts BYE
46
+ break
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def format_name(job)
53
+ return unless job
54
+
55
+ "#{ICONS[job.status]} #{job.name}"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./job"
4
- require_relative "./package_criteria"
5
-
6
3
  module CobraCommander
7
4
  module Executor
8
5
  module PackageCriteria