cli-mastermind 1.2.5 → 1.3.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/lib/cli/mastermind.rb +97 -14
- data/lib/cli/mastermind/arg_parse.rb +80 -5
- data/lib/cli/mastermind/configuration.rb +88 -10
- data/lib/cli/mastermind/executable_plan.rb +6 -0
- data/lib/cli/mastermind/loader.rb +27 -1
- data/lib/cli/mastermind/loader/planfile_loader.rb +25 -0
- data/lib/cli/mastermind/parent_plan.rb +39 -1
- data/lib/cli/mastermind/plan.rb +19 -0
- data/lib/cli/mastermind/user_interface.rb +45 -14
- data/lib/cli/mastermind/version.rb +2 -2
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e6d02e18aa42a0c2c627dcdc76f2989b30a89c222bfd183a177a186f54c9953
|
4
|
+
data.tar.gz: 92541c8f669b777e89ba6c273b31f05ab3c51831653e4a2eacedd262d013ca10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fbea7750593009ed092d09055dd5ee120a1ea3ee442a9576adff043d0d7214bee30f825956c99645e0eac2df177d739d505fc4efbf42a9c8ce7105f158be19ca
|
7
|
+
data.tar.gz: cc50ce47da3cc8aee137b85ae0c7bb04ca8060759a6e2a22999316ef3ee14e2a98ab5a36a41e354304158fad38c3651f642d425b912b765c525b70d689d06a7f
|
data/lib/cli/mastermind.rb
CHANGED
@@ -13,50 +13,86 @@ require 'cli/mastermind/executable_plan'
|
|
13
13
|
require 'cli/mastermind/version'
|
14
14
|
|
15
15
|
module CLI
|
16
|
+
# The main Mastermind module handles initial setup, user interaction, and final plan execution
|
16
17
|
module Mastermind
|
17
18
|
extend UserInterface
|
18
19
|
|
19
20
|
class << self
|
20
|
-
#
|
21
|
+
# Lazy-load configuration object
|
22
|
+
#
|
23
|
+
# @return [Configuration] the loaded configuration
|
21
24
|
def configuration
|
22
|
-
@config ||= spinner('Loading configuration')
|
25
|
+
@config ||= spinner('Loading configuration') do
|
26
|
+
Configuration.new(@base_path).tap do |config|
|
27
|
+
|
28
|
+
# Load any autoloads
|
29
|
+
if @autoloads && @autoloads.any?
|
30
|
+
@autoloads.each { |masterplan| config.load_masterplan masterplan }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
23
34
|
end
|
24
35
|
|
25
36
|
# Allows utilities wrapping Mastermind to specify that only plans under a
|
26
37
|
# particular path should be loaded.
|
38
|
+
#
|
39
|
+
# @param base_path [String] the path to the planfiles that should be loaded
|
40
|
+
# @return [Void]
|
27
41
|
def base_path=(base_path)
|
28
42
|
@base_path = base_path
|
29
43
|
end
|
30
44
|
|
31
45
|
# Allows utilities wrapping Mastermind to specify a top level plan without
|
32
46
|
# having to monkey with the incomming arguments.
|
47
|
+
#
|
48
|
+
# @param base_plan [String] the top-level plan that should be automatically selected
|
49
|
+
# @return [Void]
|
33
50
|
def base_plan=(base_plan)
|
34
51
|
@base_plan = base_plan
|
35
52
|
end
|
36
53
|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
54
|
+
# Convenience method for ArgParse.add_option
|
55
|
+
#
|
56
|
+
# @see ArgParse.add_option
|
57
|
+
#
|
58
|
+
# @param args arguments passed directly to OptionParser#on
|
59
|
+
# @param block [Proc] block passed as the handler for the above arguments
|
60
|
+
# @return [Void]
|
61
|
+
def add_argument(*args, &block)
|
62
|
+
ArgParse.add_option(*args, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Allows utilities wrapping Mastermind to specify masterplans that should be
|
66
|
+
# automatically loaded. Masterplans loaded this way are loaded _after_ all
|
67
|
+
# others and so should only be used to set default values.
|
68
|
+
#
|
69
|
+
# Adding a new autoload after configuration has been initialized will
|
70
|
+
# immediately load the new masterplan.
|
71
|
+
#
|
72
|
+
# @param masterplan_path [String] the path to the masterplan to load
|
73
|
+
# @return [Void]
|
74
|
+
def autoload_masterplan(masterplan_path)
|
75
|
+
path = Pathname.new masterplan_path
|
76
|
+
raise Error, "`#{masterplan_path}` is not an absolute path" unless path.absolute?
|
77
|
+
raise Error, "`#{masterplan_path}` does not exist or is not a file" unless path.file?
|
44
78
|
@autoloads ||= []
|
45
|
-
@autoloads <<
|
79
|
+
@autoloads << masterplan_path
|
80
|
+
|
81
|
+
# Don't use configuration method here to avoid loading configuration early
|
82
|
+
@config.load_masterplan masterplan_path unless @config.nil?
|
46
83
|
end
|
47
84
|
|
48
85
|
# Process incoming options and take an appropriate action.
|
49
86
|
# This is normally called by the mastermind executable.
|
87
|
+
#
|
88
|
+
# @param cli_args [Array<String>] the arguments to pass into {ArgParse}
|
89
|
+
# @return [Void]
|
50
90
|
def execute(cli_args=ARGV)
|
51
91
|
@arguments = ArgParse.new(cli_args)
|
52
92
|
|
53
93
|
enable_ui if @arguments.display_ui?
|
54
94
|
|
55
95
|
frame('Mastermind') do
|
56
|
-
if @autoloads && @autoloads.any?
|
57
|
-
@autoloads.each { |masterplan| configuration.load_masterplan masterplan }
|
58
|
-
end
|
59
|
-
|
60
96
|
if @arguments.dump_config?
|
61
97
|
do_print_configuration
|
62
98
|
exit 0
|
@@ -102,6 +138,10 @@ module CLI
|
|
102
138
|
# While it's entirely valid to have a plan name that inlcudes a space, you
|
103
139
|
# should avoid them if you plan to look up your plan using this method.
|
104
140
|
#
|
141
|
+
# Plans with spaces in the name can be looked up using only the first form
|
142
|
+
# of this method.
|
143
|
+
#
|
144
|
+
# @param plan_stack [Array<String>] an array of plans to navigate to
|
105
145
|
def [](*plan_stack)
|
106
146
|
# Allow for a single space-separated string
|
107
147
|
if plan_stack.size == 1 and plan_stack.first.is_a?(String)
|
@@ -113,12 +153,18 @@ module CLI
|
|
113
153
|
end
|
114
154
|
end
|
115
155
|
|
156
|
+
# Lazy-load the plans to be used by Mastermind
|
157
|
+
#
|
158
|
+
# @return [ParentPlan] the top-level parent plan which holds all loaded plans
|
116
159
|
def plans
|
117
160
|
@plans ||= spinner('Loading plans') { Loader.load_all configuration.plan_files }
|
118
161
|
end
|
119
162
|
|
120
163
|
private
|
121
164
|
|
165
|
+
# Prints the configuration object built from the loaded masterplan files.
|
166
|
+
#
|
167
|
+
# @return [Void]
|
122
168
|
def do_print_configuration
|
123
169
|
frame('Configuration') do
|
124
170
|
fade_code = CLI::UI::Color.new(90, '').code
|
@@ -153,6 +199,9 @@ module CLI
|
|
153
199
|
end
|
154
200
|
end
|
155
201
|
|
202
|
+
# Filters and displays plans based on the pattern from the passed in arguements
|
203
|
+
#
|
204
|
+
# @return [Void]
|
156
205
|
def do_filtered_plan_display
|
157
206
|
filter_plans @arguments.pattern
|
158
207
|
|
@@ -165,6 +214,14 @@ module CLI
|
|
165
214
|
end
|
166
215
|
end
|
167
216
|
|
217
|
+
# Builds the string that describes a plan.
|
218
|
+
#
|
219
|
+
# Used for human-readable output of a plan's name, aliases, and description.
|
220
|
+
#
|
221
|
+
# @param plans [ParentPlan,Hash<name, Plan>] the plans to be displayed
|
222
|
+
# @param prefix [String] a prefix to print at the beginning of the output line
|
223
|
+
#
|
224
|
+
# @return [String] the display string for the given plans
|
168
225
|
def build_display_string(plans=self.plans, prefix='')
|
169
226
|
fade_code = CLI::UI::Color.new(90, '').code
|
170
227
|
reset = CLI::UI::Color::RESET.code
|
@@ -198,6 +255,14 @@ module CLI
|
|
198
255
|
display_string.gsub(/\n{3,}/, "\n\n")
|
199
256
|
end
|
200
257
|
|
258
|
+
# Removes plans whose names don't match the given pattern from the tree.
|
259
|
+
#
|
260
|
+
# Modifies +plans+ in place!
|
261
|
+
#
|
262
|
+
# @param pattern [Regexp] the pattern to match against
|
263
|
+
# @param plans [ParentPlan, Hash<name, Plan>] the plans to filter
|
264
|
+
#
|
265
|
+
# @return [Void]
|
201
266
|
def filter_plans(pattern, plans=self.plans)
|
202
267
|
plans.keep_if do |name, plan|
|
203
268
|
# Don't display plans without a description or children
|
@@ -211,6 +276,11 @@ module CLI
|
|
211
276
|
end
|
212
277
|
end
|
213
278
|
|
279
|
+
# Processes command line arguements to build the starting plan stack.
|
280
|
+
#
|
281
|
+
# @see ArgParse
|
282
|
+
#
|
283
|
+
# @return [Void]
|
214
284
|
def process_plan_names
|
215
285
|
@arguments.do_command_expansion!(configuration)
|
216
286
|
|
@@ -235,6 +305,11 @@ module CLI
|
|
235
305
|
end
|
236
306
|
end
|
237
307
|
|
308
|
+
# Asks the user to select a plan from the current list of plans.
|
309
|
+
#
|
310
|
+
# Repeated invokations of this command allow the user to traverse the plan tree.
|
311
|
+
#
|
312
|
+
# @return [Void]
|
238
313
|
def do_interactive_plan_selection
|
239
314
|
options = (@selected_plan || plans).map { |k,v| [titleize(k.to_s), v] }.to_h
|
240
315
|
|
@@ -242,14 +317,22 @@ module CLI
|
|
242
317
|
@plan_stack << titleize(@selected_plan.name)
|
243
318
|
end
|
244
319
|
|
320
|
+
# @return [Boolean] if the currently selected plan is executable
|
245
321
|
def executable_plan_selected?
|
246
322
|
not (@selected_plan.nil? or @selected_plan.has_children?)
|
247
323
|
end
|
248
324
|
|
325
|
+
# Interaction can be skipped via command line flags (see {ArgParse}) or configuration
|
326
|
+
# from a masterplan (see {Configuration})
|
327
|
+
#
|
328
|
+
# @return [Boolean] if the user is sure they want to execute the given plan
|
249
329
|
def user_is_sure?
|
250
330
|
!@arguments.ask? or !configuration.ask? or confirm("Execute plan #{@plan_stack.join('/')}?")
|
251
331
|
end
|
252
332
|
|
333
|
+
# Executes the selected plan
|
334
|
+
#
|
335
|
+
# @return [Void]
|
253
336
|
def execute_plan!
|
254
337
|
@selected_plan.call(@arguments.plan_arguments)
|
255
338
|
end
|
@@ -1,16 +1,35 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
|
3
3
|
module CLI::Mastermind
|
4
|
+
# Processes command line arguments and provides a more useful representation of
|
5
|
+
# the provided options.
|
4
6
|
class ArgParse
|
5
|
-
#
|
7
|
+
# @return [Regexp] the pattern to use when filtering plans for display
|
6
8
|
attr_reader :pattern
|
7
9
|
|
8
|
-
#
|
9
|
-
# attr_reader :mastermind_arguments
|
10
|
-
|
11
|
-
# Passed as-is into plans
|
10
|
+
# @return [Array<String>] additional command line arguements passed into the executed plan
|
12
11
|
attr_reader :plan_arguments
|
13
12
|
|
13
|
+
class << self
|
14
|
+
# @see ArgParse.add_option
|
15
|
+
# @return [Array] a set of extra options added to the argument parser
|
16
|
+
attr_reader :extra_options
|
17
|
+
|
18
|
+
# Adds arbitrary options to the argument parser.
|
19
|
+
#
|
20
|
+
# Mostly useful for tools wrapping mastermind to add options that all methods
|
21
|
+
# should have access to.
|
22
|
+
#
|
23
|
+
# @param args arguments passed directly to OptionParser#on
|
24
|
+
# @param block [Proc] block passed as the handler for the above arguments
|
25
|
+
# @return [Void]
|
26
|
+
def add_option(*args, &block)
|
27
|
+
@extra_options ||= []
|
28
|
+
@extra_options << [args, block]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param arguments [Array<String>] the arguements to parse
|
14
33
|
def initialize(arguments=ARGV)
|
15
34
|
@initial_arguments = arguments
|
16
35
|
@ask = true
|
@@ -21,6 +40,36 @@ module CLI::Mastermind
|
|
21
40
|
parse_arguments
|
22
41
|
end
|
23
42
|
|
43
|
+
# Uses configured user aliases to perform command expansion.
|
44
|
+
#
|
45
|
+
# For example, an alias defined in a masterplan like so:
|
46
|
+
#
|
47
|
+
# define_alias 'foo', 'foobar'
|
48
|
+
#
|
49
|
+
# when invoked like `mastermind foo` would operate as if the user had actually
|
50
|
+
# typed `mastermind foobar`.
|
51
|
+
#
|
52
|
+
# User aliases (defined in a masterplan) are much more powerful than planfile
|
53
|
+
# aliases (defined in a planfile). Unlike planfile aliases, user aliases
|
54
|
+
# can define entire "plan stacks" and are recursively expanded.
|
55
|
+
#
|
56
|
+
# For example, the following aliases:
|
57
|
+
#
|
58
|
+
# define_alias 'foo', 'foobar'
|
59
|
+
# define_alias 'bar', 'foo sub'
|
60
|
+
#
|
61
|
+
# invoked as `mastermind bar` would operate as if the user had actually typed
|
62
|
+
# `mastermind foobar sub`.
|
63
|
+
#
|
64
|
+
# Plan arguments can also be specified in a user alias. For example:
|
65
|
+
#
|
66
|
+
# define_alias '2-add-2', 'calculator add -- 2 2'
|
67
|
+
#
|
68
|
+
# would expand as expected with the extra arguements (`'2 2'`) being passed
|
69
|
+
# into the executed plan.
|
70
|
+
#
|
71
|
+
# @param config [Configuration] the configuration object to use when expanding user aliases
|
72
|
+
# @return [Void]
|
24
73
|
def do_command_expansion!(config)
|
25
74
|
@alias_arguments = []
|
26
75
|
|
@@ -35,39 +84,51 @@ module CLI::Mastermind
|
|
35
84
|
end
|
36
85
|
|
37
86
|
# Adds the given base plan to the beginning of the arguments array
|
87
|
+
#
|
88
|
+
# @param base_plan [String] the base plan to add to the beginning of the arguments
|
38
89
|
def insert_base_plan!(base_plan)
|
39
90
|
@mastermind_arguments.unshift base_plan
|
40
91
|
nil # prevent @mastermind_arguments from leaking
|
41
92
|
end
|
42
93
|
|
94
|
+
# @return [Boolean] if the user has requested plan display
|
43
95
|
def display_plans?
|
44
96
|
!@pattern.nil?
|
45
97
|
end
|
46
98
|
|
99
|
+
# @return [Boolean] if additional plan names exist in mastermind's arguments
|
47
100
|
def has_additional_plan_names?
|
48
101
|
@mastermind_arguments.any?
|
49
102
|
end
|
50
103
|
|
104
|
+
# Removes and returns the plan name at the beginning of the argument list.
|
105
|
+
#
|
106
|
+
# @return [String] the name of the next plan in the list of arguments
|
51
107
|
def get_next_plan_name
|
52
108
|
@mastermind_arguments.shift
|
53
109
|
end
|
54
110
|
|
111
|
+
# @return [Boolean] if the UI is displayed
|
55
112
|
def display_ui?
|
56
113
|
@display_ui
|
57
114
|
end
|
58
115
|
|
116
|
+
# @return [Boolean] if the user should be asked for confirmation prior to plan execution
|
59
117
|
def ask?
|
60
118
|
@ask
|
61
119
|
end
|
62
120
|
|
121
|
+
# @return [Boolean] if the user requested their configuration be displayed
|
63
122
|
def dump_config?
|
64
123
|
@show_config
|
65
124
|
end
|
66
125
|
|
126
|
+
# @return [Boolean] if callable attributes should be resolved prior to being displayed
|
67
127
|
def resolve_callable_attributes?
|
68
128
|
@call_blocks
|
69
129
|
end
|
70
130
|
|
131
|
+
# @return [OptionParser] the parser to process command line arguments with
|
71
132
|
def parser
|
72
133
|
@parser ||= OptionParser.new do |opt|
|
73
134
|
opt.banner = 'Usage: mastermind [--help, -h] [--plans[ PATTERN], --tasks[ PATTERN], -T [PATTERN], -P [PATTERN] [PLAN[, PLAN[, ...]]] -- [PLAN ARGUMENTS]'
|
@@ -93,11 +154,21 @@ module CLI::Mastermind
|
|
93
154
|
@call_blocks = @show_config
|
94
155
|
@show_config = true
|
95
156
|
end
|
157
|
+
|
158
|
+
self.class.extra_options.each do |(arguments, block)|
|
159
|
+
opt.on(*arguments, &block)
|
160
|
+
end
|
96
161
|
end
|
97
162
|
end
|
98
163
|
|
99
164
|
private
|
100
165
|
|
166
|
+
# Performs alias expansion using the provided configuration object
|
167
|
+
#
|
168
|
+
# @param config [Configuration] the configuration object used to perform expansion
|
169
|
+
# @param argument [String] the argument to be expanded
|
170
|
+
#
|
171
|
+
# @return [Array<String>, String] the expanded arguments
|
101
172
|
def expand_argument(config, argument)
|
102
173
|
dealiased = config.map_alias(argument)
|
103
174
|
|
@@ -121,6 +192,10 @@ module CLI::Mastermind
|
|
121
192
|
dealiased
|
122
193
|
end
|
123
194
|
|
195
|
+
# Splits the incoming arguments and processes those before the first `--`.
|
196
|
+
#
|
197
|
+
# Arguments after the first `--` on the command line are passed verbatim into
|
198
|
+
# the executed plan.
|
124
199
|
def parse_arguments
|
125
200
|
@mastermind_arguments = @initial_arguments.take_while { |arg| arg != '--' }
|
126
201
|
@plan_arguments = @initial_arguments[(@mastermind_arguments.size + 1)..-1] || []
|
@@ -3,20 +3,28 @@ require 'set'
|
|
3
3
|
module CLI
|
4
4
|
module Mastermind
|
5
5
|
##
|
6
|
-
# Main configuration object. Walks up the file tree looking for
|
7
|
-
# and loading them
|
6
|
+
# Main configuration object. Walks up the file tree looking for masterplan
|
7
|
+
# files and loading them to build a the configuration used by the CLI.
|
8
8
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
9
|
+
# These masterplan files are loaded starting from the current working directory
|
10
|
+
# and traversing up until a masterplan with a `at_project_root` directive or
|
11
|
+
# or the directory specified by a `project_root` directive is reached.
|
12
|
+
#
|
13
|
+
# Configuration options set with `configure` are latched once set to something
|
14
|
+
# non-nil. This, along with the aforementioned load order of masterplan files,
|
15
|
+
# means that masterplan files closest to the source of your invokation will
|
16
|
+
# "beat" other masterplan files.
|
13
17
|
#
|
14
18
|
# A global masterplan located at $HOME/.masterplan (or equivalent) is loaded
|
15
19
|
# _last_. You can use this to specify plans you want accessible everywhere
|
16
20
|
# or global configuration that should apply everywhere (unless overridden by
|
17
|
-
# more
|
21
|
+
# more proximal masterplans).
|
22
|
+
#
|
23
|
+
# Additionally, there is a directive (`see_other`) that allows for masterplan
|
24
|
+
# files outside of the lookup tree to be loaded.
|
18
25
|
#
|
19
|
-
#
|
26
|
+
# See {DSL} for a full list of the commands provided by Mastermind and a sample
|
27
|
+
# masterplan file.
|
20
28
|
class Configuration
|
21
29
|
# Filename of masterplan files
|
22
30
|
PLANFILE = '.masterplan'
|
@@ -24,9 +32,15 @@ module CLI
|
|
24
32
|
# Path to the top-level masterplan
|
25
33
|
MASTER_PLAN = File.join(Dir.home, PLANFILE)
|
26
34
|
|
35
|
+
# The set of planfiles to load
|
27
36
|
attr_reader :plan_files
|
28
37
|
|
29
38
|
# Adds an arbitrary attribute given by +attribute+ to the configuration class
|
39
|
+
#
|
40
|
+
# @param attribute [String,Symbol] the attribute to define
|
41
|
+
#
|
42
|
+
# @!macro [attach] add_attribute
|
43
|
+
# @!attribute [rw] $1
|
30
44
|
def self.add_attribute(attribute)
|
31
45
|
return if self.method_defined? attribute
|
32
46
|
|
@@ -49,6 +63,7 @@ module CLI
|
|
49
63
|
# masterplans, so it's important that it be set.
|
50
64
|
add_attribute :project_root
|
51
65
|
|
66
|
+
# @param base_path [String,nil] plans outside of the base path will be ignored
|
52
67
|
def initialize(base_path=nil)
|
53
68
|
@base_path = base_path
|
54
69
|
@loaded_masterplans = Set.new
|
@@ -62,7 +77,12 @@ module CLI
|
|
62
77
|
load_masterplan MASTER_PLAN
|
63
78
|
end
|
64
79
|
|
65
|
-
# Adds a set of filenames for plans into the set of +@plan_files
|
80
|
+
# Adds a set of filenames for plans into the set of +@plan_files+.
|
81
|
+
#
|
82
|
+
# Plans with paths outside the +@base_path+, if set, will be ignored.
|
83
|
+
#
|
84
|
+
# @param planfiles [Array<String>] new planfiles to add to the set of planfiles
|
85
|
+
# @return [Void]
|
66
86
|
def add_plans(planfiles)
|
67
87
|
allowed_plans = if @base_path.nil?
|
68
88
|
planfiles
|
@@ -74,6 +94,9 @@ module CLI
|
|
74
94
|
end
|
75
95
|
|
76
96
|
# Loads a masterplan using the DSL, if it exists and hasn't been loaded already
|
97
|
+
#
|
98
|
+
# @param filename [String] the path to the masterplan to load
|
99
|
+
# @return [Void]
|
77
100
|
def load_masterplan filename
|
78
101
|
if File.exists? filename and !@loaded_masterplans.include? filename
|
79
102
|
@loaded_masterplans << filename
|
@@ -81,26 +104,44 @@ module CLI
|
|
81
104
|
end
|
82
105
|
end
|
83
106
|
|
107
|
+
# Defines a user alias
|
108
|
+
#
|
109
|
+
# @param alias_from [String] the string to be replaced during expansion
|
110
|
+
# @param alias_to [String, Array<String>] the expanded argument
|
111
|
+
# @return [Void]
|
84
112
|
def define_alias(alias_from, alias_to)
|
85
113
|
arguments = alias_to.split(' ') if alias_to.is_a? String
|
86
114
|
|
87
115
|
@aliases[alias_from] = arguments unless @aliases.has_key? alias_from
|
88
116
|
end
|
89
117
|
|
118
|
+
# Maps an input string to an alias.
|
119
|
+
#
|
120
|
+
# @param input [String] the value to be replaced
|
121
|
+
# @return [String,Array<String>] the replacement alias or the input, if no replacement exists
|
90
122
|
def map_alias(input)
|
91
123
|
@aliases[input]
|
92
124
|
end
|
93
125
|
|
126
|
+
# @return [Boolean] the user's ask_for_confirmation setting
|
94
127
|
def ask?
|
95
128
|
@ask_for_confirmation
|
96
129
|
end
|
97
130
|
|
131
|
+
# Sets +@ask_for_confirmation+ to `false`.
|
132
|
+
#
|
133
|
+
# @return [false]
|
98
134
|
def skip_confirmation!
|
99
135
|
@ask_for_confirmation = false
|
100
136
|
end
|
101
137
|
|
102
138
|
private
|
103
139
|
|
140
|
+
# Override the default NoMethodError with a more useful MissingConfigurationError.
|
141
|
+
#
|
142
|
+
# Since the configuration object is used directly by plans for configuration information,
|
143
|
+
# accessing non-existant configuration can lead to unhelpful NoMethodErrors. This replaces
|
144
|
+
# those errors with more helpful errors.
|
104
145
|
def method_missing(symbol, *args)
|
105
146
|
super
|
106
147
|
rescue NoMethodError
|
@@ -108,6 +149,8 @@ module CLI
|
|
108
149
|
end
|
109
150
|
|
110
151
|
# Walks up the file tree looking for masterplans.
|
152
|
+
#
|
153
|
+
# @return [Void]
|
111
154
|
def lookup_and_load_masterplans
|
112
155
|
load_masterplan File.join(Dir.pwd, PLANFILE)
|
113
156
|
|
@@ -123,6 +166,8 @@ module CLI
|
|
123
166
|
# See the .masterplan file in the root of this repo for a full example of
|
124
167
|
# the available options.
|
125
168
|
class DSL
|
169
|
+
# @param config [Configuration] the configuration object used by the DSL
|
170
|
+
# @param filename [String] the path to the masterplan to be loaded
|
126
171
|
def initialize(config, filename)
|
127
172
|
@config = config
|
128
173
|
@filename = filename
|
@@ -131,12 +176,17 @@ module CLI
|
|
131
176
|
|
132
177
|
# Specifies that another masterplan should also be loaded when loading
|
133
178
|
# this masterplan. NOTE: This _immediately_ loads the other masterplan.
|
179
|
+
#
|
180
|
+
# @param filename [String] the path to the masterplan to be loaded
|
134
181
|
def see_also(filename)
|
135
182
|
@config.load_masterplan(File.expand_path(filename))
|
136
183
|
end
|
137
184
|
|
138
185
|
# Specifies the root of the project.
|
139
186
|
# +root+ must be a directory.
|
187
|
+
#
|
188
|
+
# @param root [String] the root directory of the project
|
189
|
+
# @raise [InvalidDirectoryError] if +root+ is not a directory
|
140
190
|
def project_root(root)
|
141
191
|
unless Dir.exist? root
|
142
192
|
raise InvalidDirectoryError.new('Invalid project root', root)
|
@@ -147,12 +197,17 @@ module CLI
|
|
147
197
|
|
148
198
|
# Syntactic sugar on top of `project_root` to specify that the current
|
149
199
|
# masterplan resides in the root of the project.
|
200
|
+
#
|
201
|
+
# @see project_root
|
150
202
|
def at_project_root
|
151
203
|
project_root File.dirname(@filename)
|
152
204
|
end
|
153
205
|
|
154
206
|
# Specify that plans exist in the given +directory+.
|
155
|
-
# Must be a valid directory
|
207
|
+
# Must be a valid directory.
|
208
|
+
#
|
209
|
+
# @param directory [String] path to a directory containing planfiles
|
210
|
+
# @raise [InvalidDirectoryError] if +directory+ is not a directory
|
156
211
|
def plan_files(directory)
|
157
212
|
unless Dir.exist? directory
|
158
213
|
raise InvalidDirectoryError.new('Invalid plan file directory', directory)
|
@@ -166,11 +221,15 @@ module CLI
|
|
166
221
|
|
167
222
|
# Syntactic sugar on top of `plan_files` to specify that plans exist in
|
168
223
|
# a +plans/+ directory in the current directory.
|
224
|
+
#
|
225
|
+
# @see plan_files
|
169
226
|
def has_plan_files
|
170
227
|
plan_files File.join(File.dirname(@filename), 'plans')
|
171
228
|
end
|
172
229
|
|
173
230
|
# Specifies that a specific plan file exists at the given +filename+.
|
231
|
+
#
|
232
|
+
# @param files [Array<String>] an array of planfile paths
|
174
233
|
def plan_file(*files)
|
175
234
|
files = files.map { |file| File.expand_path file }
|
176
235
|
|
@@ -179,6 +238,19 @@ module CLI
|
|
179
238
|
|
180
239
|
# Add arbitrary configuration attributes to the configuration object.
|
181
240
|
# Use this to add plan specific configuration options.
|
241
|
+
#
|
242
|
+
# @overload configure(attribute, value=nil, &block)
|
243
|
+
# @example configure(:foo, 'bar')
|
244
|
+
# @example configure(:foo) { 'bar' }
|
245
|
+
# @param attribute [String,Symbol] the attribute to define
|
246
|
+
# @param value [] the value to assign
|
247
|
+
# @param block [#call,nil] a callable that will return the value
|
248
|
+
#
|
249
|
+
# @overload configure(attribute)
|
250
|
+
# @example configure(foo: 'bar')
|
251
|
+
# @example configure('foo' => -> { 'bar' } # not recommended, but should work
|
252
|
+
# @param attribute [Hash] a single entry hash with the key as the attribute
|
253
|
+
# name and value as the corresponding value
|
182
254
|
def configure(attribute, value=nil, &block)
|
183
255
|
attribute, value = attribute.first if attribute.is_a? Hash
|
184
256
|
|
@@ -189,6 +261,9 @@ module CLI
|
|
189
261
|
|
190
262
|
# Define a user alias. User aliases are expanded as part of plan selection.
|
191
263
|
# @see ArgParse#do_command_expansion!
|
264
|
+
#
|
265
|
+
# @param name [String] the string to be replaced
|
266
|
+
# @param arguments [String,Array<String>] the replacement
|
192
267
|
def define_alias(name, arguments)
|
193
268
|
@config.define_alias(name, arguments)
|
194
269
|
end
|
@@ -201,6 +276,9 @@ module CLI
|
|
201
276
|
|
202
277
|
private
|
203
278
|
|
279
|
+
# Used during planfile loading with a Dir.glob to load only supported planfiles
|
280
|
+
#
|
281
|
+
# @return [String] a comma separated list of supported file extensions
|
204
282
|
def supported_extensions
|
205
283
|
Loader.supported_extensions.join(',')
|
206
284
|
end
|
@@ -1,8 +1,14 @@
|
|
1
1
|
module CLI
|
2
2
|
module Mastermind
|
3
|
+
# Executable Plan implementation. Used in Planfile Loader to generate executable
|
4
|
+
# plans from its DSL.
|
3
5
|
class ExecutablePlan
|
4
6
|
include Plan
|
5
7
|
|
8
|
+
# Implementation of {Plan#call} which calls the block this plan was created with
|
9
|
+
#
|
10
|
+
# @param (see Plan#call)
|
11
|
+
# @see Plan#call
|
6
12
|
def call(options=nil)
|
7
13
|
case @block.arity
|
8
14
|
when 1, -1 then instance_exec(options, &@block)
|
@@ -1,13 +1,26 @@
|
|
1
1
|
module CLI::Mastermind
|
2
|
+
# Loader handles loading planfiles (+not+ masterplans) and is used directly by
|
3
|
+
# Mastermind. Subclasses handle the actual parsing of plans, this class is
|
4
|
+
# primarily concerned with finding the correct loader to load a particular
|
5
|
+
# file.
|
6
|
+
#
|
7
|
+
# Loader subclasses are automatically added to the list of loaders. Thus, adding
|
8
|
+
# a new loader is as simple as subclassing and adding the appropriate methods.
|
2
9
|
class Loader
|
3
10
|
class << self
|
4
11
|
attr_reader :loadable_extensions
|
5
12
|
@@loaders = []
|
6
13
|
|
14
|
+
# Adds a newly subclasses loader into the set of loaders.
|
7
15
|
def inherited(subclass)
|
8
16
|
@@loaders << subclass
|
9
17
|
end
|
10
18
|
|
19
|
+
# Finds the correct loader for a given extension
|
20
|
+
#
|
21
|
+
# @param extension [String] the extensions to search with
|
22
|
+
# @raise [UnsupportedFileTypeError] if no compatible loader is found
|
23
|
+
# @return [Loader] the loader for the given +extension+.
|
11
24
|
def find_loader(extension)
|
12
25
|
loader = @@loaders.find { |l| l.can_load? extension }
|
13
26
|
|
@@ -16,19 +29,32 @@ module CLI::Mastermind
|
|
16
29
|
loader
|
17
30
|
end
|
18
31
|
|
32
|
+
# @return [Array<String>] all loadable extensions
|
19
33
|
def supported_extensions
|
20
34
|
@@loaders.flat_map { |l| l.loadable_extensions }
|
21
35
|
end
|
22
36
|
|
37
|
+
# @param extension [String] the extension to check
|
38
|
+
# @return [Boolean] if the +extension+ is loadable
|
23
39
|
def can_load?(extension)
|
24
40
|
@loadable_extensions.include? extension
|
25
41
|
end
|
26
42
|
|
43
|
+
# @abstract
|
44
|
+
# Used to load a given plan.
|
45
|
+
#
|
46
|
+
# @param filename [String] the path to the planfile to be loaded
|
27
47
|
def load(filename)
|
28
48
|
raise NotImplementedError
|
29
49
|
end
|
30
50
|
|
31
|
-
# Loads
|
51
|
+
# Loads plans from the filesystem.
|
52
|
+
#
|
53
|
+
# The returned ParentPlan is intended for use by Mastermind itself.
|
54
|
+
#
|
55
|
+
# @param files [Array<String>] the list of planfiles to load
|
56
|
+
# @return [ParentPlan] a ParentPlan containing all the loaded plans
|
57
|
+
# @private
|
32
58
|
def load_all(files)
|
33
59
|
temp_plan = ParentPlan.new('INTERNAL PLAN HOLDER')
|
34
60
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module CLI::Mastermind
|
2
2
|
class Loader
|
3
|
+
# Loader implementation to handle the default .plan format
|
4
|
+
# @private
|
3
5
|
class PlanfileLoader < Loader
|
4
6
|
@loadable_extensions = %w[ .plan ].freeze
|
5
7
|
|
@@ -15,8 +17,17 @@ module CLI::Mastermind
|
|
15
17
|
class DSL
|
16
18
|
extend Forwardable
|
17
19
|
|
20
|
+
# @return [Array<Plan>] the plans defined by the loaded file or block
|
18
21
|
attr_reader :plans
|
19
22
|
|
23
|
+
# Loads and evaluates a local file or a given block.
|
24
|
+
#
|
25
|
+
# If given both, the block takes priority.
|
26
|
+
#
|
27
|
+
# @example DSL.new('path/to/file.plan')
|
28
|
+
# @example DSL.new { ...methods go here... }
|
29
|
+
# @param filename [String,nil] the name of the file that contains this plan
|
30
|
+
# @param block [#call,nil] a block to evaluate
|
20
31
|
def initialize(filename=nil, &block)
|
21
32
|
@plans = []
|
22
33
|
@filename = filename
|
@@ -30,6 +41,10 @@ module CLI::Mastermind
|
|
30
41
|
end
|
31
42
|
end
|
32
43
|
|
44
|
+
# Describes a ParentPlan
|
45
|
+
#
|
46
|
+
# @param name [String] the name of the plan
|
47
|
+
# @param block [#call] passed to a new DSL object to define more plans
|
33
48
|
def plot(name, &block)
|
34
49
|
plan = ParentPlan.new name, @description, @filename
|
35
50
|
@description = nil
|
@@ -38,17 +53,27 @@ module CLI::Mastermind
|
|
38
53
|
end
|
39
54
|
alias_method :namespace, :plot
|
40
55
|
|
56
|
+
# @param text [String] the description of the next plan
|
41
57
|
def description(text)
|
42
58
|
@description = text
|
43
59
|
end
|
44
60
|
alias_method :desc, :description
|
45
61
|
|
62
|
+
# Defines an executable plan
|
63
|
+
#
|
64
|
+
# @param name [String] the name of the plan
|
65
|
+
# @param plan_class [Plan] the plan class
|
66
|
+
# @param block [#call] passed into the newly created plan
|
67
|
+
# @return [void]
|
46
68
|
def plan(name, plan_class = ExecutablePlan, &block)
|
47
69
|
@plans << plan_class.new(name, @description, @filename, &block)
|
48
70
|
@description = nil
|
49
71
|
end
|
50
72
|
alias_method :task, :plan
|
51
73
|
|
74
|
+
# Sets an alias on the previously created plan
|
75
|
+
#
|
76
|
+
# @param alias_to [String] the alias to add
|
52
77
|
def set_alias(alias_to)
|
53
78
|
@plans.last.add_alias(alias_to)
|
54
79
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
module CLI
|
2
2
|
module Mastermind
|
3
|
+
# Plan implementation designed to hold other plans forming the intermediate
|
4
|
+
# nodes on the tree of loaded plans.
|
5
|
+
# @private
|
3
6
|
class ParentPlan
|
4
7
|
extend Forwardable
|
5
8
|
include Plan
|
@@ -8,13 +11,21 @@ module CLI
|
|
8
11
|
# Used in the interactive plan selector to display child plans
|
9
12
|
attr_reader :children
|
10
13
|
|
14
|
+
# @param (see Plan)
|
15
|
+
# @see Plan
|
11
16
|
def initialize(name, description=nil, filename=nil, &block)
|
12
17
|
super
|
13
18
|
|
14
19
|
@children = {}
|
15
20
|
end
|
16
21
|
|
17
|
-
# Get the child plan with the specified +name
|
22
|
+
# Get the child plan with the specified +name+.
|
23
|
+
#
|
24
|
+
# This method also checks plan aliases, so the given +name+ can also be a
|
25
|
+
# plan's alias.
|
26
|
+
#
|
27
|
+
# @param name [String] the name of the child plan to retrieve
|
28
|
+
# @return [Plan,nil] the child plan, if it exists
|
18
29
|
def get_child(name)
|
19
30
|
return @children[name] if @children.has_key? name
|
20
31
|
@children.each_value.find { |child| child.aliases.include? name }
|
@@ -22,23 +33,50 @@ module CLI
|
|
22
33
|
alias_method :[], :get_child
|
23
34
|
alias_method :dig, :get_child
|
24
35
|
|
36
|
+
# For Enumerable support
|
25
37
|
def_delegators :@children, :each, :keep_if, :empty?
|
26
38
|
|
39
|
+
# Adds new children to this plan
|
40
|
+
#
|
41
|
+
# @param plans [Array<Plan>] the plans to add
|
42
|
+
# @return [Void]
|
27
43
|
def add_children(plans)
|
28
44
|
raise InvalidPlanError, 'Cannot add child plans to a plan with an action' unless @block.nil?
|
29
45
|
plans.each(&method(:incorporate_plan))
|
30
46
|
end
|
31
47
|
|
48
|
+
# @return [Boolean] if this plan has any children
|
32
49
|
def has_children?
|
33
50
|
@children.any?
|
34
51
|
end
|
35
52
|
|
36
53
|
private
|
37
54
|
|
55
|
+
# Adds a new plan to the children hash
|
56
|
+
#
|
57
|
+
# @param plan [Plan] the plan to add
|
58
|
+
# @see resolve_conflicts
|
38
59
|
def incorporate_plan(plan)
|
39
60
|
@children[plan.name] = resolve_conflicts(plan.name, plan)
|
40
61
|
end
|
41
62
|
|
63
|
+
# Resolves plan name collisions.
|
64
|
+
#
|
65
|
+
# If two child plans have the same name, how they're resolved depends on
|
66
|
+
# what kind of plan they are. The following situations are convered by
|
67
|
+
# this method:
|
68
|
+
#
|
69
|
+
# 1) Both plans have children.
|
70
|
+
# * In this case, the incoming plan's children are merged into the existing
|
71
|
+
# plan and the incoming plan is discarded.
|
72
|
+
#
|
73
|
+
# 2) One or both plans have no children.
|
74
|
+
# * In this case, it's assumed that the childless plans are executable.
|
75
|
+
# A warning is printed an the incoming plan replaces the existing plan.
|
76
|
+
#
|
77
|
+
# @param key [String] the key the incoming plan will be stored under
|
78
|
+
# @param plan [Plan] the incoming plan
|
79
|
+
# @return [Plan] the plan to store
|
42
80
|
def resolve_conflicts(key, plan)
|
43
81
|
# If this namespace isn't taken we're good
|
44
82
|
return plan unless @children.has_key?(key)
|
data/lib/cli/mastermind/plan.rb
CHANGED
@@ -33,23 +33,42 @@ module CLI
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
# @param name [String] the name of the plan
|
37
|
+
# @param description [String] the description of the plan
|
38
|
+
# @param filename [String] the name of the file which defined this plan
|
39
|
+
# @param block [#call,nil] a callable used by ExecutablePlan
|
36
40
|
def initialize(name, description=nil, filename=nil, &block)
|
37
41
|
@name = name.to_s.freeze
|
38
42
|
@description = description.freeze
|
39
43
|
@filename = filename
|
44
|
+
# TODO: Move this to ExecutablePlan?
|
40
45
|
@block = block
|
41
46
|
@aliases = Set.new
|
42
47
|
end
|
43
48
|
|
49
|
+
# If this plan has children.
|
50
|
+
#
|
51
|
+
# Implemented for compatibility with ParentPlan to make plan traversal easier.
|
52
|
+
#
|
53
|
+
# @return [false] Plans have no children by default
|
44
54
|
def has_children?
|
45
55
|
false
|
46
56
|
end
|
47
57
|
|
58
|
+
# Entrypoint called by Mastermind
|
59
|
+
#
|
60
|
+
# @abstract
|
61
|
+
# @param options [Array<String>,nil] options passed from the command line
|
62
|
+
# @raise [NotImplementedError]
|
48
63
|
def call(options=nil)
|
49
64
|
raise NotImplementedError
|
50
65
|
end
|
51
66
|
alias_method :execute, :call
|
52
67
|
|
68
|
+
# Defines a plan alias which allows this plan to be accessed using another
|
69
|
+
# string than its name.
|
70
|
+
#
|
71
|
+
# @param alias_to [String] the alias to accept
|
53
72
|
def add_alias(alias_to)
|
54
73
|
@aliases.add alias_to.to_s
|
55
74
|
end
|
@@ -6,14 +6,19 @@ module CLI::Mastermind::UserInterface
|
|
6
6
|
CLI::UI::StdoutRouter.enable
|
7
7
|
end
|
8
8
|
|
9
|
-
#
|
9
|
+
# @private
|
10
|
+
# @return [Boolean] if the StdoutRouter is enabled
|
10
11
|
def ui_enabled?
|
11
12
|
CLI::UI::StdoutRouter.enabled?
|
12
13
|
end
|
13
14
|
|
14
15
|
# Display a spinner with a +title+ while data is being loaded
|
15
|
-
#
|
16
|
+
#
|
16
17
|
# @see https://github.com/Shopify/cli-ui#spinner-groups
|
18
|
+
#
|
19
|
+
# @param title [String] the title to display
|
20
|
+
# @param block [#call] passed to the underlying spinner implementation.
|
21
|
+
# @return the result of calling the given +block+
|
17
22
|
def spinner(title, &block)
|
18
23
|
return yield unless ui_enabled?
|
19
24
|
|
@@ -30,6 +35,10 @@ module CLI::Mastermind::UserInterface
|
|
30
35
|
# The only difference between the two is that +AsyncSpinners+ provides a
|
31
36
|
# mechanism for exfiltrating results by using +await+ instead of the usual
|
32
37
|
# +add+.
|
38
|
+
#
|
39
|
+
# @see AsyncSpinners
|
40
|
+
#
|
41
|
+
# @yieldparam group [AsyncSpinners]
|
33
42
|
def concurrently
|
34
43
|
group = AsyncSpinners.new
|
35
44
|
|
@@ -42,6 +51,9 @@ module CLI::Mastermind::UserInterface
|
|
42
51
|
|
43
52
|
# Uses +CLI::UI.fmt+ to format a string
|
44
53
|
# @see https://github.com/Shopify/cli-ui#symbolglyph-formatting
|
54
|
+
#
|
55
|
+
# @param string [String] the string to format
|
56
|
+
# @return [String] the formatted string
|
45
57
|
def stylize(string)
|
46
58
|
CLI::UI.fmt string
|
47
59
|
end
|
@@ -55,12 +67,19 @@ module CLI::Mastermind::UserInterface
|
|
55
67
|
|
56
68
|
# Ask the user for some text.
|
57
69
|
# @see https://github.com/Shopify/cli-ui#free-form-text-prompts
|
70
|
+
#
|
71
|
+
# @param question [String] the question to ask the user
|
72
|
+
# @param default [String] the default answer
|
73
|
+
# @return [String] the user's answer
|
58
74
|
def ask(question, default: nil)
|
59
75
|
CLI::UI.ask(question, default: default)
|
60
76
|
end
|
61
77
|
|
62
78
|
# Ask the user a yes/no +question+
|
63
79
|
# @see https://github.com/Shopify/cli-ui#interactive-prompts
|
80
|
+
#
|
81
|
+
# @param question [String] the question to ask the user
|
82
|
+
# @return [Boolean] how the user answered
|
64
83
|
def confirm(question)
|
65
84
|
CLI::UI.confirm(question)
|
66
85
|
end
|
@@ -69,13 +88,11 @@ module CLI::Mastermind::UserInterface
|
|
69
88
|
# If less than 2 options would be displayed, the default value is automatically
|
70
89
|
# returned.
|
71
90
|
#
|
72
|
-
# @param
|
73
|
-
# @param
|
74
|
-
# @param
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
# Any other keyword arguments given are passed down into +CLI::UI::Prompt.ask+.
|
91
|
+
# @param question [String] The question to ask the user
|
92
|
+
# @param options [Array<String>,Hash] the options to display
|
93
|
+
# @param default [String] The default value for this question. Assumed to exist
|
94
|
+
# within the given options.
|
95
|
+
# @param opts [Hash] additional options passed into +CLI::UI::Prompt.ask+.
|
79
96
|
#
|
80
97
|
# @see https://github.com/Shopify/cli-ui#interactive-prompts
|
81
98
|
def select(question, options:, default: options.first, **opts)
|
@@ -111,14 +128,16 @@ module CLI::Mastermind::UserInterface
|
|
111
128
|
end
|
112
129
|
|
113
130
|
# Titleize the given +string+.
|
131
|
+
#
|
114
132
|
# Replaces any dashes (-) or underscores (_) in the +string+ with spaces and
|
115
133
|
# then capitalizes each word.
|
116
134
|
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
121
|
-
#
|
135
|
+
# @example titleize('foo') => 'Foo'
|
136
|
+
# @example titleize('foo bar') => 'Foo Bar'
|
137
|
+
# @example titleize('foo-bar') => 'Foo Bar'
|
138
|
+
# @example titleize('foo_bar') => 'Foo Bar'
|
139
|
+
#
|
140
|
+
# @param string [String] the string to titleize.
|
122
141
|
def titleize(string)
|
123
142
|
string.gsub(/[-_-]/, ' ').split(' ').map(&:capitalize).join(' ')
|
124
143
|
end
|
@@ -133,6 +152,11 @@ module CLI::Mastermind::UserInterface
|
|
133
152
|
# Optionally, a block may be passed to modify the output of the line prior to
|
134
153
|
# printing.
|
135
154
|
#
|
155
|
+
# @param command [Array<String>] the command to execute
|
156
|
+
# @param kwargs [Hash] additional arguments to be passed into +IO.popen+
|
157
|
+
#
|
158
|
+
# @yieldparam line [String] a line of output to be processed
|
159
|
+
#
|
136
160
|
# @see IO.popen
|
137
161
|
# @see Open3.popen
|
138
162
|
def capture_command_output(*command, **kwargs, &block)
|
@@ -141,6 +165,8 @@ module CLI::Mastermind::UserInterface
|
|
141
165
|
IO.popen(command.flatten, **kwargs) { |io| io.each_line { |line| print block.call(line) } }
|
142
166
|
end
|
143
167
|
|
168
|
+
# Implementation of CLI::UI::SpinGroup with that keeps track of the results from
|
169
|
+
# individual spinners.
|
144
170
|
class AsyncSpinners < CLI::UI::SpinGroup
|
145
171
|
attr_reader :results
|
146
172
|
|
@@ -149,6 +175,11 @@ module CLI::Mastermind::UserInterface
|
|
149
175
|
super
|
150
176
|
end
|
151
177
|
|
178
|
+
# Waits for a block to execute while displaying a spinner.
|
179
|
+
#
|
180
|
+
# @param title [String] the title to display
|
181
|
+
#
|
182
|
+
# @yieldparam spinner [CLI::UI::Spinner]
|
152
183
|
def await(title)
|
153
184
|
@results[title] = nil
|
154
185
|
|
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: 1.
|
4
|
+
version: 1.3.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:
|
11
|
+
date: 2020-03-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cli-ui
|
@@ -58,6 +58,20 @@ dependencies:
|
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '3.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: yard
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.9.24
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.9.24
|
61
75
|
description: |2
|
62
76
|
Take over the world from your command line!
|
63
77
|
With mastermind, you can quickly build and generate
|