cli-mastermind 0.7.0 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/lib/cli/mastermind.rb +50 -15
- data/lib/cli/mastermind/configuration.rb +7 -26
- data/lib/cli/mastermind/errors.rb +6 -0
- data/lib/cli/mastermind/plan.rb +1 -0
- data/lib/cli/mastermind/version.rb +2 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd49841f87237f2a7223cd748c61772244232e84cdf46112241e291e73f1a311
|
|
4
|
+
data.tar.gz: '0197b2452f75c1c1c7ac50d84621a9dfef1635d04781c500fb4ca8516f2b65e5'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3f195e375e769c105e335c29ab33102db1b2ad76b963f6f463f9c77e0d25a6b79bced05eb44cc86eccaa8d32701236d10b73854310955f8f50742cbeb76a7ac
|
|
7
|
+
data.tar.gz: a5e649e5427b3bea7e52e6cb1a12fa3e563d5e54fbf405623c2d2cdb58147c89dab6043d8fabb3351bf70c13174b0c4c5827383cf75fea0270665847c4a2a3a0
|
data/README.md
CHANGED
|
@@ -26,11 +26,11 @@ See [Writing Masterplans][writing-masterplans] for more on their structure and
|
|
|
26
26
|
semantics.
|
|
27
27
|
|
|
28
28
|
Mastermind makes up for the lack of flexibility in its configuration with full
|
|
29
|
-
flexibility in its
|
|
29
|
+
flexibility in its plan files. Which brings us to...
|
|
30
30
|
|
|
31
31
|
## Extensibility
|
|
32
32
|
|
|
33
|
-
Mastermind is designed from the outset to provide a means of extending its
|
|
33
|
+
Mastermind is designed from the outset to provide a means of extending its plan file
|
|
34
34
|
formats through custom `Loader`s. In fact, Mastermind's own `PlanfileLoader` is
|
|
35
35
|
the first of such loaders. You can specify your own file extensions and provide
|
|
36
36
|
your own loaders as needed.
|
|
@@ -41,14 +41,14 @@ about in the details!
|
|
|
41
41
|
|
|
42
42
|
Obviously, it'd be a bit difficult to write your plan files in an entirely separate
|
|
43
43
|
language, but there's nothing stopping you from delegating actions to another
|
|
44
|
-
executable or even writing some C code to call into something else
|
|
44
|
+
executable or even writing some C code to call into something else altogether.
|
|
45
45
|
|
|
46
|
-
If you are writing your plans in Ruby, Mastermind provides `CLI::Mastermind::Plan
|
|
46
|
+
If you are writing your plans in Ruby, Mastermind provides `CLI::Mastermind::Plan`
|
|
47
47
|
which you can include in your plans to provide the basic `Plan` interface.
|
|
48
48
|
|
|
49
49
|
## Minimal Dependencies
|
|
50
50
|
|
|
51
|
-
Mastermind only has one dependency, Shopify's
|
|
51
|
+
Mastermind only has one dependency, Shopify's excellent [cli-ui project][cli-ui].
|
|
52
52
|
Mastermind doesn't require that you load it in your Gemfile or add anything to
|
|
53
53
|
your project's configuration files.
|
|
54
54
|
|
data/lib/cli/mastermind.rb
CHANGED
|
@@ -18,7 +18,7 @@ module CLI
|
|
|
18
18
|
class << self
|
|
19
19
|
# Expose the configuration loaded during +execute+.
|
|
20
20
|
def configuration
|
|
21
|
-
@config
|
|
21
|
+
@config ||= spinner('Loading configuration') { Configuration.new @base_path }
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Allows utilities wrapping Mastermind to specify that only plans under a
|
|
@@ -41,15 +41,11 @@ module CLI
|
|
|
41
41
|
enable_ui if @arguments.display_ui?
|
|
42
42
|
|
|
43
43
|
frame('Mastermind') do
|
|
44
|
-
@config = spinner('Loading configuration') { Configuration.new @base_path }
|
|
45
|
-
|
|
46
44
|
if @arguments.dump_config?
|
|
47
45
|
do_print_configuration
|
|
48
46
|
exit 0
|
|
49
47
|
end
|
|
50
48
|
|
|
51
|
-
@plans = spinner('Loading plans') { @config.load_plans }
|
|
52
|
-
|
|
53
49
|
if @arguments.display_plans?
|
|
54
50
|
do_filtered_plan_display
|
|
55
51
|
exit 0
|
|
@@ -67,22 +63,61 @@ module CLI
|
|
|
67
63
|
end
|
|
68
64
|
end
|
|
69
65
|
|
|
66
|
+
# Look up a specific plan by its name
|
|
67
|
+
#
|
|
68
|
+
# Because plans also implement this method in a compatible way, there are
|
|
69
|
+
# three ways this method could be used:
|
|
70
|
+
#
|
|
71
|
+
# 1. List of arguments
|
|
72
|
+
# * Mastermind['name', 'of', 'plans']
|
|
73
|
+
#
|
|
74
|
+
# 2. Space separated string
|
|
75
|
+
# * Mastermind['name of plans']
|
|
76
|
+
#
|
|
77
|
+
# 3. Hash-like access
|
|
78
|
+
# * Mastermind['name']['of']['plans']
|
|
79
|
+
#
|
|
80
|
+
# All will provide the same plan.
|
|
81
|
+
#
|
|
82
|
+
# ---
|
|
83
|
+
#
|
|
84
|
+
# GOTCHA: Be careful if your plan name includes a space!
|
|
85
|
+
#
|
|
86
|
+
# While it's entirely valid to have a plan name that inlcudes a space, you
|
|
87
|
+
# should avoid them if you plan to look up your plan using this method.
|
|
88
|
+
#
|
|
89
|
+
def [](*plan_stack)
|
|
90
|
+
# Allow for a single space-separated string
|
|
91
|
+
if plan_stack.size == 1 and plan_stack.first.is_a?(String)
|
|
92
|
+
plan_stack = plan_stack.first.split(' ')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
plan_stack.compact.reduce(plans) do |plan, plan_name|
|
|
96
|
+
plan[plan_name]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
70
100
|
private
|
|
71
101
|
|
|
102
|
+
def plans
|
|
103
|
+
@plans ||= spinner('Loading plans') { Loader.load_all configuration.plan_files }
|
|
104
|
+
end
|
|
105
|
+
|
|
72
106
|
def do_print_configuration
|
|
73
107
|
frame('Configuration') do
|
|
74
108
|
fade_code = CLI::UI::Color.new(90, '').code
|
|
75
109
|
puts stylize("{{?}} #{fade_code}Values starting with {{*}} #{fade_code}were lazy loaded.#{CLI::UI::Color::RESET.code}")
|
|
76
110
|
print "\n"
|
|
77
|
-
|
|
78
|
-
|
|
111
|
+
|
|
112
|
+
configuration.instance_variables.each do |attribute|
|
|
113
|
+
value = configuration.instance_variable_get(attribute)
|
|
79
114
|
|
|
80
115
|
name = attribute.to_s.sub(/^@/, '')
|
|
81
116
|
|
|
82
117
|
if value.respond_to? :call
|
|
83
118
|
if @arguments.resolve_callable_attributes?
|
|
84
119
|
value = begin
|
|
85
|
-
|
|
120
|
+
configuration.send(name)
|
|
86
121
|
rescue => e
|
|
87
122
|
"UNABLE TO LOAD: #{e.message}"
|
|
88
123
|
end
|
|
@@ -105,7 +140,7 @@ module CLI
|
|
|
105
140
|
def do_filtered_plan_display
|
|
106
141
|
filter_plans @arguments.pattern
|
|
107
142
|
|
|
108
|
-
unless
|
|
143
|
+
unless plans.empty?
|
|
109
144
|
frame('Plans') do
|
|
110
145
|
puts build_display_string
|
|
111
146
|
end
|
|
@@ -114,7 +149,7 @@ module CLI
|
|
|
114
149
|
end
|
|
115
150
|
end
|
|
116
151
|
|
|
117
|
-
def build_display_string(plans
|
|
152
|
+
def build_display_string(plans=self.plans, prefix='')
|
|
118
153
|
fade_code = CLI::UI::Color.new(90, '').code
|
|
119
154
|
reset = CLI::UI::Color::RESET.code
|
|
120
155
|
|
|
@@ -147,7 +182,7 @@ module CLI
|
|
|
147
182
|
display_string.gsub(/\n{3,}/, "\n\n")
|
|
148
183
|
end
|
|
149
184
|
|
|
150
|
-
def filter_plans(pattern, plans
|
|
185
|
+
def filter_plans(pattern, plans=self.plans)
|
|
151
186
|
plans.keep_if do |name, plan|
|
|
152
187
|
# Don't display plans without a description or children
|
|
153
188
|
next false unless plan.has_children? or plan.description
|
|
@@ -161,13 +196,13 @@ module CLI
|
|
|
161
196
|
end
|
|
162
197
|
|
|
163
198
|
def process_plan_names
|
|
164
|
-
@arguments.do_command_expansion!(
|
|
199
|
+
@arguments.do_command_expansion!(configuration)
|
|
165
200
|
|
|
166
201
|
@arguments.insert_base_plan!(@base_plan) unless @base_plan.nil?
|
|
167
202
|
|
|
168
203
|
@plan_stack = []
|
|
169
204
|
|
|
170
|
-
@selected_plan =
|
|
205
|
+
@selected_plan = plans if @arguments.has_additional_plan_names?
|
|
171
206
|
|
|
172
207
|
while @arguments.has_additional_plan_names?
|
|
173
208
|
plan_name = @arguments.get_next_plan_name
|
|
@@ -184,7 +219,7 @@ module CLI
|
|
|
184
219
|
end
|
|
185
220
|
|
|
186
221
|
def do_interactive_plan_selection
|
|
187
|
-
options = (@selected_plan&.children ||
|
|
222
|
+
options = (@selected_plan&.children || plans).map { |k,v| [titleize(k.to_s), v] }.to_h
|
|
188
223
|
|
|
189
224
|
@selected_plan = select("Select a plan under #{@plan_stack.join('/')}", options: options)
|
|
190
225
|
@plan_stack << titleize(@selected_plan.name)
|
|
@@ -195,7 +230,7 @@ module CLI
|
|
|
195
230
|
end
|
|
196
231
|
|
|
197
232
|
def user_is_sure?
|
|
198
|
-
!@arguments.ask? or
|
|
233
|
+
!@arguments.ask? or !configuration.ask? or confirm("Execute plan #{@plan_stack.join('/')}?")
|
|
199
234
|
end
|
|
200
235
|
|
|
201
236
|
def execute_plan!
|
|
@@ -24,7 +24,7 @@ module CLI
|
|
|
24
24
|
# Path to the top-level masterplan
|
|
25
25
|
MASTER_PLAN = File.join(Dir.home, PLANFILE)
|
|
26
26
|
|
|
27
|
-
attr_reader :
|
|
27
|
+
attr_reader :plan_files
|
|
28
28
|
|
|
29
29
|
# Adds an arbitrary attribute given by +attribute+ to the configuration class
|
|
30
30
|
def self.add_attribute(attribute)
|
|
@@ -73,31 +73,6 @@ module CLI
|
|
|
73
73
|
@plan_files.merge(allowed_plans)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
# Loads all plan files added using +add_plans+
|
|
77
|
-
# @see Plan.load
|
|
78
|
-
def load_plans
|
|
79
|
-
@plans = Loader.load_all(@plan_files)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def execute_plan(*plan_stack, arguments: nil)
|
|
83
|
-
if plan_stack.size == 1
|
|
84
|
-
case plan_stack.first
|
|
85
|
-
when Array
|
|
86
|
-
plan_stack = plan_stack.first
|
|
87
|
-
when String
|
|
88
|
-
plan_stack = plan_stack.first.split(' ')
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
plan = @plans
|
|
93
|
-
|
|
94
|
-
plan_stack.each do |plan_name|
|
|
95
|
-
plan = plan[plan_name]
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
plan.call(arguments)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
76
|
# Loads a masterplan using the DSL, if it exists and hasn't been loaded already
|
|
102
77
|
def load_masterplan filename
|
|
103
78
|
if File.exists? filename and !@loaded_masterplans.include? filename
|
|
@@ -126,6 +101,12 @@ module CLI
|
|
|
126
101
|
|
|
127
102
|
private
|
|
128
103
|
|
|
104
|
+
def method_missing(symbol, *args)
|
|
105
|
+
super
|
|
106
|
+
rescue NoMethodError
|
|
107
|
+
raise MissingConfigurationError, symbol
|
|
108
|
+
end
|
|
109
|
+
|
|
129
110
|
# Walks up the file tree looking for masterplans.
|
|
130
111
|
def lookup_and_load_masterplans
|
|
131
112
|
load_masterplan File.join(Dir.pwd, PLANFILE)
|
|
@@ -17,5 +17,11 @@ module CLI
|
|
|
17
17
|
super "#{message}: #{directory} does not exist or is not a directory"
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
class MissingConfigurationError < Error
|
|
22
|
+
def initialize(attribute)
|
|
23
|
+
super "#{attribute} has not been defined. Call `configure :#{attribute}[, value]` in a `#{Configuration::PLANFILE}` to set it."
|
|
24
|
+
end
|
|
25
|
+
end
|
|
20
26
|
end
|
|
21
27
|
end
|
data/lib/cli/mastermind/plan.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cli-mastermind
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Hall
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2019-
|
|
11
|
+
date: 2019-10-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cli-ui
|
|
@@ -103,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
103
|
- !ruby/object:Gem::Version
|
|
104
104
|
version: '0'
|
|
105
105
|
requirements: []
|
|
106
|
-
rubygems_version: 3.0.
|
|
106
|
+
rubygems_version: 3.0.4
|
|
107
107
|
signing_key:
|
|
108
108
|
specification_version: 4
|
|
109
109
|
summary: Mastermind is a framework for constructing command line toolboxes.
|