psychic-runner 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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