psychic-runner 0.0.7 → 0.0.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 966decba16f73cabfd0e9c5ec9b06264da9cff30
4
- data.tar.gz: 124f2dda5812f6ef48ab04e626f0f1fc92a612e5
3
+ metadata.gz: a3af75e80b40bfaa12b7fdf0c0321d653ab56b86
4
+ data.tar.gz: ed65f470ce7e85d0f62b329c34ecd476818c9c3e
5
5
  SHA512:
6
- metadata.gz: 2677291d81e7f08bf56415062a19f2b4eee25fdb3e8a539c2d4b5b84e1e1e8ab65470a5c2b137e91f5c5c3b4a91e550739f93ed077dac6e28776f5d5c006d265
7
- data.tar.gz: 2528ceea531136d5b1e1125965fdb95e29a1074398a3f1fb32bc918ecf34066491c762f4ee16264195e35bda8fe144783898ff1d9a48deb6b21f77682f0db438
6
+ metadata.gz: 03565dc8cc94cca3682ce30a9e6e3967f7b682882591d5005d08f11c6722141bfad92a1642456611ce4ebe3ba0dec91d4131505a4f1b51c59a487778444b53fa
7
+ data.tar.gz: 48e7bd41635a48408e4b6a103a4e9688135e2ee75d82c7f81da9042f144b285bb1835aff15a549a6f89820d478f29df7766eade9c4647a84882a1e92acf3b8ba
data/Gemfile CHANGED
@@ -3,3 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in psychic-runner.gemspec
4
4
  gemspec
5
5
  gem 'pry'
6
+ gem 'rouge'
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'psychic/runner/cli'
3
3
 
4
- Psychic::CLI.start
4
+ Psychic::Runner::CLI.start
@@ -11,7 +11,14 @@ module Psychic
11
11
 
12
12
  def start(given_args = ARGV, config = {})
13
13
  if given_args && (split_pos = given_args.index('--'))
14
- @extra_args = given_args.slice(split_pos + 1, given_args.length)
14
+ @extra_args = given_args.slice(split_pos + 1, given_args.length).map do | arg |
15
+ # Restore quotes
16
+ next unless arg.match(/\=/)
17
+ lhs, rhs = arg.split('=')
18
+ lhs = "\"#{lhs}\"" if lhs.match(/\s/)
19
+ rhs = "\"#{rhs}\"" if rhs.match(/\s/)
20
+ [lhs, rhs].join('=')
21
+ end
15
22
  given_args = given_args.slice(0, split_pos)
16
23
  end
17
24
  super given_args, config
@@ -0,0 +1,84 @@
1
+ module Psychic
2
+ module OutputHelper
3
+ class StringShell < Thor::Base.shell
4
+ attr_reader :io
5
+
6
+ def initialize(*args)
7
+ @io = StringIO.new
8
+ super
9
+ end
10
+
11
+ alias_method :stdout, :io
12
+ alias_method :stderr, :io
13
+
14
+ def string
15
+ @io.string
16
+ end
17
+
18
+ def can_display_colors?
19
+ # Still capture colors if they can eventually be displayed.
20
+ $stdout.tty?
21
+ end
22
+ end
23
+
24
+ def cli
25
+ @cli ||= Thor::Base.shell.new
26
+ end
27
+
28
+ def build_string
29
+ old_cli = @cli
30
+ new_cli = @cli = StringShell.new
31
+ yield
32
+ @cli = old_cli
33
+ new_cli.string
34
+ end
35
+
36
+ def reformat(string)
37
+ return if string.nil? || string.empty?
38
+
39
+ indent do
40
+ string.gsub(/^/, indent)
41
+ end
42
+ end
43
+
44
+ def indent
45
+ @indent_level ||= 0
46
+ if block_given?
47
+ @indent_level += 2
48
+ result = yield
49
+ @indent_level -= 2
50
+ result
51
+ else
52
+ ' ' * @indent_level
53
+ end
54
+ end
55
+
56
+ def say(msg)
57
+ cli.say msg if msg
58
+ end
59
+
60
+ def status(status, msg = nil, color = :cyan, colwidth = 50)
61
+ msg = yield if block_given?
62
+ cli.say(indent) if indent.length > 0
63
+ status = cli.set_color("#{status}:", color, true)
64
+ # The built-in say_status is right-aligned, we want left-aligned
65
+ cli.say format("%-#{colwidth}s %s", status, msg).rstrip
66
+ end
67
+
68
+ # TODO: Reporters for different formats
69
+ def print_table(*args)
70
+ # @reporter.print_table(*args)
71
+ cli.print_table(*args)
72
+ end
73
+
74
+ def colorize(string, *args)
75
+ return string unless @reporter.respond_to? :set_color
76
+ # @reporter.set_color(string, *args)
77
+ cli.set_color(string, *args)
78
+ end
79
+
80
+ def color_pad(string)
81
+ string + colorize('', :white)
82
+ end
83
+ end
84
+ end
@@ -1,38 +1,110 @@
1
1
  require 'psychic/runner/version'
2
2
 
3
+ autoload :Thor, 'thor'
3
4
  autoload :YAML, 'yaml'
4
5
 
5
6
  module Psychic
6
7
  autoload :Util, 'psychic/util'
7
8
  autoload :Logger, 'psychic/logger'
8
9
  autoload :Shell, 'psychic/shell'
10
+ autoload :OutputHelper, 'psychic/output_helper'
9
11
  class Runner
12
+ autoload :MagicTaskFactory, 'psychic/runner/magic_task_factory'
10
13
  autoload :BaseRunner, 'psychic/runner/base_runner'
14
+ autoload :CodeSample, 'psychic/runner/code_sample'
15
+ autoload :SampleFinder, 'psychic/runner/sample_finder'
11
16
  autoload :SampleRunner, 'psychic/runner/sample_runner'
12
- autoload :HotRunner, 'psychic/runner/hot_runner'
13
- autoload :CompoundRunner, 'psychic/runner/compound_runner'
14
- autoload :ColdRunnerRegistry, 'psychic/runner/cold_runner_registry'
15
- class TaskNotImplementedError < NotImplementedError; end
16
- ColdRunnerRegistry.autoload_runners!
17
+ autoload :HotReadTaskFactory, 'psychic/runner/hot_read_task_factory'
18
+ autoload :TaskFactoryRegistry, 'psychic/runner/task_factory_registry'
19
+ class TaskNotImplementedError < NotImplementedError
20
+ def initialize(task_name)
21
+ super("#{self.class} cannot handle task #{task_name}")
22
+ end
23
+ end
24
+ TaskFactoryRegistry.autoload_task_factories!
17
25
 
18
26
  include BaseRunner
19
27
  include SampleRunner
20
- attr_reader :runners, :hot_runner, :cold_runners
28
+ attr_reader :runners, :hot_read_task_factory, :task_factories, :sample_factories
21
29
 
22
- def initialize(opts = { cwd: Dir.pwd })
30
+ def initialize(opts = { cwd: Dir.pwd }) # rubocop:disable Metrics/MethodLength
31
+ # TODO: Will reduce method length after further splitting Runner vs TaskFactory
23
32
  fail 'cwd is required' unless opts[:cwd]
24
- opts[:cwd] = Pathname(opts[:cwd]).to_s # must be a string on windows...
25
- super
26
- @hot_runner = HotRunner.new(opts)
27
- @cold_runners = ColdRunnerRegistry.active_runners(opts)
28
- @runners = [@hot_runner, @cold_runners].flatten
29
- @known_tasks = @runners.map(&:known_tasks).uniq
33
+ # must be a string on windows...
34
+ opts[:cwd] = Pathname(opts[:cwd]).to_s
35
+ @opts = opts
36
+ init_attr(:cwd) { Dir.pwd }
37
+ init_hints
38
+ init_attr(:logger) { new_logger }
39
+ init_attr(:env) { ENV.to_hash }
40
+ init_attrs :cli, :interactive, :parameter_mode, :restore_mode, :dry_run
41
+ @shell_opts = select_shell_opts
42
+ @parameters = load_parameters(opts[:parameters])
43
+ # super
44
+ @hot_read_task_factory = HotReadTaskFactory.new(opts)
45
+ @sample_finder = SampleFinder.new(opts[:cwd], @hot_read_task_factory.hints['samples'])
46
+ @task_factories = TaskFactoryRegistry.active_task_factories(opts)
47
+ @runners = [@hot_read_task_factory, @task_factories].flatten
48
+ @known_tasks = @runners.flat_map(&:known_tasks).uniq
49
+ end
50
+
51
+ def known_samples
52
+ @sample_finder.known_samples
30
53
  end
31
54
 
32
- def [](task_name)
33
- runner = runners.find { |r| r.command_for_task(task_name) }
55
+ def task_for(task_name)
56
+ runner = runners.find { |r| r.known_task?(task_name) }
34
57
  return nil unless runner
35
- runner[task_name]
58
+ runner.task_for(task_name)
59
+ end
60
+
61
+ private
62
+
63
+ def init_attr(var)
64
+ var_name = "@#{var}"
65
+ var_value = @opts[var]
66
+ var_value = yield if var_value.nil? && block_given?
67
+ instance_variable_set(var_name, var_value)
68
+ end
69
+
70
+ def init_attrs(*vars)
71
+ vars.each do | var |
72
+ init_attr var
73
+ end
74
+ end
75
+
76
+ def init_hints
77
+ @hints = Psychic::Util.stringified_hash(@opts[:hints] || load_hints || {})
78
+ if @hints['options']
79
+ @opts.merge! Psychic::Util.symbolized_hash(@hints['options'])
80
+ end
81
+ end
82
+
83
+ def select_shell_opts
84
+ # Make sure to delete any option that isn't a MixLib::ShellOut option
85
+ @opts.select { |key, _| Psychic::Shell::AVAILABLE_OPTIONS.include? key }
86
+ end
87
+
88
+ def load_hints
89
+ hints_file = Dir["#{@cwd}/psychic.{yaml,yml}"].first
90
+ YAML.load(File.read(hints_file)) unless hints_file.nil?
91
+ end
92
+
93
+ def load_parameters(parameters)
94
+ if parameters.nil? || parameters.is_a?(String)
95
+ load_parameters_file(parameters)
96
+ else
97
+ parameters
98
+ end
99
+ end
100
+
101
+ def load_parameters_file(file = nil)
102
+ if file.nil?
103
+ file ||= File.expand_path(DEFAULT_PARAMS_FILE, cwd)
104
+ return {} unless File.exist? file
105
+ end
106
+ parameters = Psychic::Util.replace_tokens(File.read(file), @env)
107
+ YAML.load(parameters)
36
108
  end
37
109
  end
38
110
  end
@@ -6,20 +6,41 @@ module Psychic
6
6
  include Psychic::Shell
7
7
  include Psychic::Logger
8
8
 
9
- attr_reader :known_tasks
10
- attr_reader :cwd
11
- attr_reader :env
12
- attr_reader :hints
9
+ attr_reader :known_tasks, :tasks, :cwd, :env, :hints
13
10
 
14
11
  module ClassMethods
15
- attr_accessor :magic_file_pattern
12
+ def register_task_factory
13
+ Psychic::Runner::TaskFactoryRegistry.register(self)
14
+ end
16
15
 
17
- def register_runner
18
- Psychic::Runner::ColdRunnerRegistry.register(self)
16
+ def magic_file_patterns
17
+ @magic_file_patterns ||= []
19
18
  end
20
19
 
21
20
  def magic_file(pattern) # rubocop:disable Style/TrivialAccessors
22
- @magic_file_pattern = pattern
21
+ magic_file_patterns << pattern
22
+ end
23
+
24
+ def magic_env_vars
25
+ @magic_env_vars ||= []
26
+ end
27
+
28
+ def magic_env_var(var)
29
+ magic_env_vars << var
30
+ end
31
+
32
+ def known_tasks
33
+ @known_tasks ||= []
34
+ end
35
+
36
+ def tasks
37
+ @tasks ||= {}
38
+ end
39
+
40
+ def task(name, &block)
41
+ name = name.to_s
42
+ tasks[name] = block
43
+ known_tasks << name
23
44
  end
24
45
  end
25
46
 
@@ -27,53 +48,54 @@ module Psychic
27
48
  base.extend(ClassMethods)
28
49
  end
29
50
 
30
- def initialize(opts = {}, _hints = {})
31
- @cwd = opts[:cwd] ||= Dir.pwd
32
- @hints = Psychic::Util.stringified_hash(opts[:hints] || load_hints || {})
33
- if @hints['options']
34
- opts.merge! Psychic::Util.symbolized_hash(@hints['options'])
35
- end
36
- @logger = opts[:logger] || new_logger
37
- @env = opts[:env] || ENV.to_hash
51
+ def initialize(opts = {})
52
+ @opts = opts
53
+ init_attr(:cwd) { Dir.pwd }
54
+ init_hints
55
+ init_attr(:known_tasks) { self.class.known_tasks }
56
+ init_attr(:tasks) { self.class.tasks }
57
+ init_attr(:logger) { new_logger }
58
+ init_attr(:env) { ENV.to_hash }
59
+ init_attrs :cli, :interactive, :parameter_mode, :restore_mode, :dry_run
60
+ @shell_opts = select_shell_opts
38
61
  @parameters = load_parameters(opts[:parameters])
39
- @cli, @interactive_mode, @parameter_mode, @restore_mode, @dry_run = opts.values_at(
40
- :cli, :interactive, :parameter_mode, :restore_mode, :dry_run)
41
- # Make sure to delete any option that isn't a MixLib::ShellOut option
42
- @shell_opts = opts.select { |key, _| Psychic::Shell::AVAILABLE_OPTIONS.include? key }
43
62
  end
44
63
 
45
- def respond_to_missing?(task, include_all = false)
46
- return true if known_tasks.include?(task.to_s)
47
- super
64
+ def known_task?(task_name)
65
+ known_tasks.include?(task_name.to_s)
48
66
  end
49
67
 
50
- def method_missing(task, *args, &block)
51
- execute_task(task, *args)
52
- rescue Psychic::Runner::TaskNotImplementedError
53
- super
68
+ def task_for(task_name)
69
+ tasks[task_name] if tasks.include? task_name
54
70
  end
55
71
 
56
- # Reserved words
57
-
58
72
  def execute(command, *args)
59
73
  full_cmd = [command, *args].join(' ')
60
74
  logger.info("Executing #{full_cmd}")
61
75
  shell.execute(full_cmd, @shell_opts) unless dry_run?
62
76
  end
63
77
 
64
- def command_for_task(task, *_args)
65
- task_name = task.to_s
66
- self[task_name]
78
+ def build_task(task_name, *_args)
79
+ task_name = task_name.to_s
80
+ task = task_for(task_name)
81
+ task = task.call if task.respond_to? :call
82
+ fail Psychic::Runner::TaskNotImplementedError, task_name if task.nil?
83
+ task
67
84
  end
68
85
 
69
- def execute_task(task, *args)
70
- command = command_for_task(task, *args)
71
- fail Psychic::Runner::TaskNotImplementedError if command.nil?
86
+ def execute_task(task_name, *args)
87
+ command = build_task(task_name, *args)
72
88
  execute(command, *args)
73
89
  end
74
90
 
75
91
  def active?
76
- self.class.magic_file_pattern ? false : Dir["#{@cwd}/#{self.class.magic_file_pattern}"]
92
+ self.class.magic_file_patterns.each do | pattern |
93
+ return true unless Dir["#{@cwd}/#{pattern}"].empty?
94
+ end
95
+ self.class.magic_env_vars.each do | var |
96
+ return true if ENV[var]
97
+ end
98
+ false
77
99
  end
78
100
 
79
101
  def dry_run?
@@ -82,6 +104,31 @@ module Psychic
82
104
 
83
105
  private
84
106
 
107
+ def init_attr(var)
108
+ var_name = "@#{var}"
109
+ var_value = @opts[var]
110
+ var_value = yield if var_value.nil? && block_given?
111
+ instance_variable_set(var_name, var_value)
112
+ end
113
+
114
+ def init_attrs(*vars)
115
+ vars.each do | var |
116
+ init_attr var
117
+ end
118
+ end
119
+
120
+ def init_hints
121
+ @hints = Psychic::Util.stringified_hash(@opts[:hints] || load_hints || {})
122
+ if @hints['options']
123
+ @opts.merge! Psychic::Util.symbolized_hash(@hints['options'])
124
+ end
125
+ end
126
+
127
+ def select_shell_opts
128
+ # Make sure to delete any option that isn't a MixLib::ShellOut option
129
+ @opts.select { |key, _| Psychic::Shell::AVAILABLE_OPTIONS.include? key }
130
+ end
131
+
85
132
  def load_hints
86
133
  hints_file = Dir["#{@cwd}/psychic.{yaml,yml}"].first
87
134
  YAML.load(File.read(hints_file)) unless hints_file.nil?
@@ -103,6 +150,12 @@ module Psychic
103
150
  parameters = Psychic::Util.replace_tokens(File.read(file), @env)
104
151
  YAML.load(parameters)
105
152
  end
153
+
154
+ # Blame Ruby's flatten and Array(...) behavior...
155
+ def to_ary
156
+ nil
157
+ end
158
+ alias_method :to_a, :to_ary
106
159
  end
107
160
  end
108
161
  end
@@ -1,10 +1,32 @@
1
+ require 'psychic/cli'
1
2
  require 'psychic/runner'
2
3
 
4
+ # rubocop:disable Metrics/LineLength
5
+
3
6
  module Psychic
4
- module Runner
5
- class CLI < Psychic::CLI
7
+ class Runner
8
+ class RunnerCLI < Psychic::CLI
9
+ no_commands do
10
+ def runner
11
+ @runner ||= setup_runner
12
+ end
13
+
14
+ def setup_runner
15
+ runner_opts = { cwd: Dir.pwd, cli: shell, parameters: options.parameters }
16
+ runner_opts.merge!(Util.symbolized_hash(options))
17
+ Psychic::Runner.new(runner_opts)
18
+ end
19
+ end
20
+ end
21
+
22
+ class CLI < RunnerCLI
6
23
  desc 'task <name>', 'Executes any task by name'
7
- def task(task_name)
24
+ method_option :list, aliases: '-l', desc: 'List known tasks'
25
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
26
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
27
+ def task(task_name = nil)
28
+ return list_tasks if options[:list]
29
+ abort 'You must specify a task name, run with -l for a list of known tasks' unless task_name
8
30
  result = runner.execute_task(task_name, *extra_args)
9
31
  result.error!
10
32
  say_status :success, task_name
@@ -15,33 +37,22 @@ module Psychic
15
37
 
16
38
  BUILT_IN_TASKS.each do |task_name|
17
39
  desc task_name, "Executes the #{task_name} task"
40
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
41
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
18
42
  define_method(task_name) do
19
43
  task(task_name)
20
44
  end
21
45
  end
22
46
 
23
47
  desc 'sample <name>', 'Executes a code sample'
24
- # rubocop:disable Metrics/LineLength
48
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
49
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
25
50
  method_option :interactive, desc: 'Prompt for parameters?', enum: %w(always missing), lazy_default: 'missing'
26
51
  method_option :parameters, desc: 'YAML file containing key/value parameters. Default: psychic-parameters.yaml'
27
52
  method_option :parameter_mode, desc: 'How should the parameters be passed?', enum: %w(tokens arguments env)
28
53
  method_option :dry_run, desc: 'Do not execute - just show what command would be run', lazy_default: true
29
- # rubocop:enable Metrics/LineLength
30
- def sample(*sample_names)
31
- sample_names.each do | sample_name |
32
- say_status :executing, sample_name
33
- begin
34
- run_sample sample_name
35
- rescue Errno::ENOENT
36
- say_status :failed, "No code sample found for #{sample_name}", :red
37
- # TODO: Fail on missing? Fail fast?
38
- end
39
- end
40
- end
41
-
42
- private
43
-
44
- def run_sample(sample_name)
54
+ def sample(sample_name = nil)
55
+ abort 'You must specify a sample name, run `psychic list samples` for a list of known samples' unless sample_name
45
56
  result = runner.run_sample(sample_name, *extra_args)
46
57
  if options.dry_run
47
58
  say_status :dry_run, sample_name
@@ -51,12 +62,56 @@ module Psychic
51
62
  end
52
63
  end
53
64
 
54
- def runner
55
- runner_opts = Util.symbolized_hash(options).merge(
56
- cwd: Dir.pwd, cli: shell, parameters: options.parameters
57
- )
58
- @runner ||= Psychic::Runner.new(runner_opts)
65
+ class List < RunnerCLI
66
+ desc 'samples', 'Lists known code samples'
67
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
68
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
69
+ def samples
70
+ samples = runner.known_samples.map do |sample|
71
+ [set_color(sample.name, :bold), sample.source_file]
72
+ end
73
+ print_table samples
74
+ end
75
+
76
+ desc 'tasks', 'List known tasks'
77
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
78
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
79
+ def tasks
80
+ runner.known_tasks.map do |task|
81
+ task_id = set_color(task, :bold)
82
+ if options[:verbose]
83
+ details = runner.task_for(task)
84
+ details = details.call if details.respond_to? :call
85
+ details = "\n#{details}".lines.join(' ') if details.lines.size > 1
86
+ say "#{task_id}: #{details}"
87
+ else
88
+ say task_id
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ class Show < RunnerCLI
95
+ desc 'sample <name>', 'Show detailed information about a code sample'
96
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
97
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
98
+ def sample(sample_name)
99
+ sample = runner.find_sample(sample_name)
100
+ say sample.to_s(options[:verbose])
101
+ end
102
+ end
103
+
104
+ desc 'list', 'List known tasks or code samples'
105
+ subcommand 'list', List
106
+ desc 'show', 'Show details about a task or code sample'
107
+ subcommand 'show', Show
108
+
109
+ no_commands do
110
+ def show_sample(_sample_name)
111
+ end
59
112
  end
60
113
  end
61
114
  end
62
115
  end
116
+
117
+ # rubocop:enable Metrics/LineLength