cli-mastermind 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.masterplan +6 -0
- data/.rspec +3 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +46 -0
- data/LICENSE.txt +21 -0
- data/README.md +164 -0
- data/bin/console +12 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/cli-mastermind.gemspec +34 -0
- data/exe/mastermind +4 -0
- data/lib/cli/mastermind/arg_parse.rb +79 -0
- data/lib/cli/mastermind/configuration.rb +142 -0
- data/lib/cli/mastermind/errors.rb +12 -0
- data/lib/cli/mastermind/interface.rb +74 -0
- data/lib/cli/mastermind/loader/planfile_loader.rb +48 -0
- data/lib/cli/mastermind/loader.rb +34 -0
- data/lib/cli/mastermind/plan.rb +64 -0
- data/lib/cli/mastermind/version.rb +17 -0
- data/lib/cli/mastermind.rb +74 -0
- data/plans/gem.plan +39 -0
- data/plans/sample.plan +17 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a20b74d9d0e99c2d10a5f9c834be1ece3ffbecf8b5f978882c5fa165b7289316
|
4
|
+
data.tar.gz: af0406ca768e182e8243348a958adebac42f7640b6d497d2e990c0871d7aaee7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7bba8a6c1e62ebf4adcd69075982396f4121422866f20f387396442c9e6f3607d67b09bab11bad462a093e137416359ff8ab14b39ff32bcf3d95be26262241fc
|
7
|
+
data.tar.gz: 0e713288d9930e8d7b7a1a872d120264d9b2a6e51792a5d37ac5d1cc7d32a619085d9cc9ca235ecbae85fd18c68a617eeb1e08659ece7273beabef6abc285141
|
data/.gitignore
ADDED
data/.masterplan
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
#ruby=ruby-2.5.1
|
6
|
+
#ruby-gemset=cli-mastermind
|
7
|
+
|
8
|
+
# Specify your gem's dependencies in cli-mastermind.gemspec
|
9
|
+
gemspec
|
10
|
+
|
11
|
+
gem 'pry'
|
12
|
+
gem 'pry-byebug'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
cli-mastermind (0.0.1)
|
5
|
+
cli-ui (~> 1.2, >= 1.2.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
byebug (10.0.2)
|
11
|
+
cli-ui (1.2.1)
|
12
|
+
coderay (1.1.2)
|
13
|
+
diff-lcs (1.3)
|
14
|
+
method_source (0.9.2)
|
15
|
+
pry (0.12.2)
|
16
|
+
coderay (~> 1.1.0)
|
17
|
+
method_source (~> 0.9.0)
|
18
|
+
pry-byebug (3.6.0)
|
19
|
+
byebug (~> 10.0)
|
20
|
+
pry (~> 0.10)
|
21
|
+
rspec (3.8.0)
|
22
|
+
rspec-core (~> 3.8.0)
|
23
|
+
rspec-expectations (~> 3.8.0)
|
24
|
+
rspec-mocks (~> 3.8.0)
|
25
|
+
rspec-core (3.8.0)
|
26
|
+
rspec-support (~> 3.8.0)
|
27
|
+
rspec-expectations (3.8.2)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.8.0)
|
30
|
+
rspec-mocks (3.8.0)
|
31
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
32
|
+
rspec-support (~> 3.8.0)
|
33
|
+
rspec-support (3.8.0)
|
34
|
+
|
35
|
+
PLATFORMS
|
36
|
+
ruby
|
37
|
+
|
38
|
+
DEPENDENCIES
|
39
|
+
bundler (~> 1.16)
|
40
|
+
cli-mastermind!
|
41
|
+
pry
|
42
|
+
pry-byebug
|
43
|
+
rspec (~> 3.0)
|
44
|
+
|
45
|
+
BUNDLED WITH
|
46
|
+
1.16.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Chris Hall
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# Mastermind
|
2
|
+
|
3
|
+
Mastermind is a CLI toolkit. It's purpose is to help build, configure, and run
|
4
|
+
command line tools.
|
5
|
+
|
6
|
+
Mastermind is designed for flexibility, extensibility, and minimal dependencies.
|
7
|
+
|
8
|
+
## Flexibility
|
9
|
+
|
10
|
+
Mastermind is written in Ruby and, therefore, provides first-class citizenship
|
11
|
+
to that language and uses Ruby's syntax and semantics in its primary configuration
|
12
|
+
files.
|
13
|
+
|
14
|
+
Mastermind looks for and executes `.masterplan` files recursively up the file tree
|
15
|
+
until it reaches the root of your project, if defined, or your home directory,
|
16
|
+
if it's not. Additionally, it always looks for and attempts to load a `.masterplan`
|
17
|
+
file in your home directory, if one exists.
|
18
|
+
|
19
|
+
In this way, configuration for your tools can live where it makes the most sense
|
20
|
+
for it to live with as much or as little duplication as you deem necessary.
|
21
|
+
|
22
|
+
See [Writing Masterplans](#writing-masterplans) for more on their structure and semantics.
|
23
|
+
|
24
|
+
Mastermind makes up for the lack of language flexibility in its configuration
|
25
|
+
with full flexibility in its planfiles. Which brings us to...
|
26
|
+
|
27
|
+
## Extensibility
|
28
|
+
|
29
|
+
Mastermind is designed from the outset to provide means of extending its planfile
|
30
|
+
formats through custom `Loader`s. In fact, Mastermind's own `PlanfileLoader` is
|
31
|
+
the first of such loaders. You can specify your own file extensions and provide
|
32
|
+
your own loaders as needed.
|
33
|
+
|
34
|
+
As long as the objects returned by your loader quack like a `Plan`, Mastermind
|
35
|
+
won't fuss at them. After all, you can't take over the world if your busy mucking
|
36
|
+
about in the details!
|
37
|
+
|
38
|
+
## Minimal Dependencies
|
39
|
+
|
40
|
+
Mastermind only has one dependency, Shopify's excelent [cli-ui project][cli-ui].
|
41
|
+
It doesn't require that you load it in your Gemfile or do anything in particular.
|
42
|
+
All you need to be able to do is run its executable and Mastermind does the rest.
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
### The `mastermind` Executable
|
47
|
+
|
48
|
+
Mastermind's own help is pretty straightforward:
|
49
|
+
|
50
|
+
Usage: mastermind [--help, -h] [--plans[ PATTERN], --tasks[ PATTERN], -T [PATTERN], -P [PATTERN] [PLAN[, PLAN[, ...]]] -- [PLAN ARGUMENTS]
|
51
|
+
-h, --help Display this help
|
52
|
+
-P, -T, --plans [PATTERN], Display plans. Optional pattern is used to filter the returned plans.
|
53
|
+
--tasks
|
54
|
+
|
55
|
+
Any arguments specified after `--` are passed as-is down to the executed plan.
|
56
|
+
You can then process those arguments however you like or ignore them completely!
|
57
|
+
|
58
|
+
Unlike Rake and other, similar, tools that allow you to run multiple tasks in
|
59
|
+
parallel, Mastermind is designed to run only one task at a time. Specifying
|
60
|
+
multiple plans on the command line is how you walk down the tree to a specific
|
61
|
+
plan.
|
62
|
+
|
63
|
+
### Writing Masterplans
|
64
|
+
|
65
|
+
`.masterplan` files use a minimal DSL to configure Mastermind and load planfiles.
|
66
|
+
Some of these commands have sensible defaults designed to keep configuration to
|
67
|
+
a minimum.
|
68
|
+
|
69
|
+
#### Masterplan DSL
|
70
|
+
|
71
|
+
##### `project_root [directory]`
|
72
|
+
|
73
|
+
Specifies the root of your project. Must be specified to prevent Mastermind
|
74
|
+
from scanning more of your filesystem than it needs to. The easiest way to
|
75
|
+
do this is to just specify it in a `.masterplan` file in the actual root of
|
76
|
+
your project.
|
77
|
+
|
78
|
+
Aliased as `at_project_root`. The argument defaults to the directory of the
|
79
|
+
current `.masterplan`.
|
80
|
+
|
81
|
+
##### `plan_files [directory]`
|
82
|
+
|
83
|
+
Specifies that plan files exist in the given directory. Mastermind will search
|
84
|
+
this directory for any files that end in a supported extension and mark them
|
85
|
+
for loading. By default, Mastermind only supports files with a `.plan` extension.
|
86
|
+
|
87
|
+
Aliased as `has_plan_files`. The argument defaults to a `plans` directory in
|
88
|
+
the same directory as the current `.masterplan`.
|
89
|
+
|
90
|
+
##### `plan_file filename[, filename[, ...]]`
|
91
|
+
|
92
|
+
Instructs Mastermind to load the planfiles located at the given filenames.
|
93
|
+
|
94
|
+
##### `configure attribute [value] [&block]`
|
95
|
+
|
96
|
+
Used to set arbitrary configuration options. When a configuration option is
|
97
|
+
set in multiple `.masterplan` files, the "closest" one to your invocation wins.
|
98
|
+
In other words, since Mastermind reads `.masterplan` files starting in your
|
99
|
+
current directory and working it's way "up" the hierarchy, the first `.masterplan`
|
100
|
+
that specifies a configuration option "wins".
|
101
|
+
|
102
|
+
When provided a block, the value is computed the first time the option is called
|
103
|
+
for. The block runs in the context of the `Configuration` object built up by
|
104
|
+
all the loaded `.masterplan` files, so it has access to all previously set
|
105
|
+
configuration options.
|
106
|
+
|
107
|
+
The block is only executed once. After that, the value is cached so that it
|
108
|
+
doesn't need to be recomputed.
|
109
|
+
|
110
|
+
If both a block and a value are given, the block is ignored and only the value
|
111
|
+
is stored.
|
112
|
+
|
113
|
+
##### `see_also filename`
|
114
|
+
|
115
|
+
Instructs Mastermind to also load the configuration specified in `filename`.
|
116
|
+
This file does _not_ have to be named `.masterplan` but _does_ have to conform
|
117
|
+
to the syntax outlined here.
|
118
|
+
|
119
|
+
### Writing Planfiles
|
120
|
+
|
121
|
+
By default, planfiles use a very simple DSL that will feel familiar to anyone
|
122
|
+
that's ever used Rake. The biggest difference between Rake (and similar tools)
|
123
|
+
and Mastermind is that Mastermind has no support for dependent tasks or parallel
|
124
|
+
tasks. If your workflow requires either of those things, Mastermind is probably
|
125
|
+
not the tool you want to use. Or, rather, not the _only_ tool you want to use.
|
126
|
+
|
127
|
+
#### Planfile DSL
|
128
|
+
|
129
|
+
##### `plot name &block`
|
130
|
+
|
131
|
+
Creates a Plan that contains children with the given `name`. This is similar
|
132
|
+
to the `namespace` command in a Rakefile. The Plans created inside the block
|
133
|
+
are added as children of this Plan.
|
134
|
+
|
135
|
+
##### `description text`
|
136
|
+
|
137
|
+
Provides a description for the next Plan created. Plans created with `plot`
|
138
|
+
can also have descriptions.
|
139
|
+
|
140
|
+
##### `plan name &block`
|
141
|
+
|
142
|
+
Creates a Plan with the given name and sets the given block as its action.
|
143
|
+
This block is passed the arguments from the command line and is run as a Plan.
|
144
|
+
|
145
|
+
## Development
|
146
|
+
|
147
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can
|
148
|
+
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
149
|
+
|
150
|
+
Mastermind uses itself to run tests and build new versions. Run `bin/rspec` to
|
151
|
+
run tests. To install this gem onto your local machine, run `exe/mastermind gem install`
|
152
|
+
To release a new version, update the version number in `version.rb`, and then run
|
153
|
+
`exe/mastermind gem release`, which will create a git tag for the version, push
|
154
|
+
git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
155
|
+
|
156
|
+
## Contributing
|
157
|
+
|
158
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/chall8908/cli-mastermind.
|
159
|
+
|
160
|
+
## License
|
161
|
+
|
162
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
163
|
+
|
164
|
+
[cli-ui]: https://github.com/shopify/cli-ui
|
data/bin/console
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "cli/mastermind"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
require 'pry-byebug'
|
12
|
+
Pry.start
|
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "cli/mastermind/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "cli-mastermind"
|
7
|
+
spec.version = CLI::Mastermind.gem_version
|
8
|
+
spec.authors = ["Chris Hall"]
|
9
|
+
spec.email = ["chall8908@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Mastermind is a library for constructing command line toolboxes."
|
12
|
+
spec.description = <<-DESC
|
13
|
+
Take over the world from your command line!
|
14
|
+
With mastermind, you can quickly build and generate
|
15
|
+
command line toolkits without having to custom build
|
16
|
+
everything for every project.
|
17
|
+
DESC
|
18
|
+
spec.homepage = "https://github.com/chall8908/cli-mastermind"
|
19
|
+
spec.license = "MIT"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_runtime_dependency "cli-ui", "~> 1.2", ">= 1.2.1"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
34
|
+
end
|
data/exe/mastermind
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module CLI::Mastermind
|
4
|
+
class ArgParse
|
5
|
+
# When set, used to display available plans
|
6
|
+
attr_reader :pattern
|
7
|
+
|
8
|
+
# Used by mastermind to lookup plans
|
9
|
+
# attr_reader :mastermind_arguments
|
10
|
+
|
11
|
+
# Passed as-is into plans
|
12
|
+
attr_reader :plan_arguments
|
13
|
+
|
14
|
+
def initialize(arguments=ARGV)
|
15
|
+
@initial_arguments = arguments
|
16
|
+
@ask = true
|
17
|
+
@display_ui = true
|
18
|
+
|
19
|
+
parse_arguments
|
20
|
+
end
|
21
|
+
|
22
|
+
def display_plans?
|
23
|
+
!@pattern.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
def has_additional_plan_names?
|
27
|
+
@mastermind_arguments.any?
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_next_plan_name
|
31
|
+
@mastermind_arguments.shift
|
32
|
+
end
|
33
|
+
|
34
|
+
def display_ui?
|
35
|
+
@display_ui
|
36
|
+
end
|
37
|
+
|
38
|
+
def ask?
|
39
|
+
@ask
|
40
|
+
end
|
41
|
+
|
42
|
+
def parser
|
43
|
+
@parser ||= OptionParser.new do |opt|
|
44
|
+
opt.banner = 'Usage: mastermind [--help, -h] [--plans[ PATTERN], --tasks[ PATTERN], -T [PATTERN], -P [PATTERN] [PLAN[, PLAN[, ...]]] -- [PLAN ARGUMENTS]'
|
45
|
+
|
46
|
+
opt.on('--help', '-h', 'Display this help') do
|
47
|
+
puts opt
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
|
51
|
+
opt.on('-A', '--no-ask', "Don't ask before executing a plan") do
|
52
|
+
@ask = false
|
53
|
+
end
|
54
|
+
|
55
|
+
opt.on('-U', '--no-fancy-ui', "Don't display the fancy UI") do
|
56
|
+
@display_ui = false
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO: Finish plan display
|
60
|
+
# opt.on('--plans [PATTERN]', '--tasks [PATTERN]', '-P [PATTERN]', '-T [PATTERN]',
|
61
|
+
# [:text],
|
62
|
+
# 'Display plans. Optional pattern is used to filter the returned plans.') do |pattern|
|
63
|
+
# @pattern = RegExp.new(pattern || '*')
|
64
|
+
# end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def parse_arguments
|
71
|
+
@mastermind_arguments = @initial_arguments.take_while { |arg| arg != '--' }
|
72
|
+
@plan_arguments = @initial_arguments[(@mastermind_arguments.size + 1)..-1]
|
73
|
+
|
74
|
+
unless @mastermind_arguments.empty?
|
75
|
+
@mastermind_arguments = parser.parse *@mastermind_arguments
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module Mastermind
|
5
|
+
##
|
6
|
+
# Main configuration object. Walks up the file tree looking for masterplans
|
7
|
+
# and loading them into to build a the configuration used by the CLI.
|
8
|
+
#
|
9
|
+
# Masterplans are loaded such that configuration specified closest to the
|
10
|
+
# point of invocation override configuration from farther masterplans.
|
11
|
+
# This allows you to add folder specific configuration while still falling
|
12
|
+
# back to more and more general configuration options.
|
13
|
+
#
|
14
|
+
# A global masterplan located at $HOME/.masterplan (or equivalent) is loaded
|
15
|
+
# _last_. You can use this to specify plans you want accessible everywhere
|
16
|
+
# or global configuration that should apply everywhere (unless overridden by
|
17
|
+
# more specific masterplans).
|
18
|
+
class Configuration
|
19
|
+
# Filename of masterplan files
|
20
|
+
PLANFILE = '.masterplan'
|
21
|
+
|
22
|
+
# Path to the top-level masterplan
|
23
|
+
MASTER_PLAN = File.join(Dir.home, PLANFILE)
|
24
|
+
|
25
|
+
attr_reader :plans
|
26
|
+
|
27
|
+
# Adds an arbitrary attribute given by +attribute+ to the configuration class
|
28
|
+
def self.add_attribute(attribute)
|
29
|
+
return if self.method_defined? attribute
|
30
|
+
|
31
|
+
self.define_method "#{attribute}=" do |new_value=nil,&block|
|
32
|
+
self.instance_variable_set("@#{attribute}", new_value||block) if self.instance_variable_get("@#{attribute}").nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
self.define_method attribute do
|
36
|
+
value = self.instance_variable_get("@#{attribute}")
|
37
|
+
return value unless value.respond_to?(:call)
|
38
|
+
|
39
|
+
# Cache the value returned by the block so we're not doing potentially
|
40
|
+
# expensive operations mutliple times.
|
41
|
+
self.instance_variable_set("@#{attribute}", self.instance_eval(&value))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Specifies the directory that is the root of your project.
|
46
|
+
# This directory is where Mastermind will stop looking for more
|
47
|
+
# masterplans, so it's important that it be set.
|
48
|
+
add_attribute :project_root
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@loaded_masterplans = Set.new
|
52
|
+
@plan_files = Set.new
|
53
|
+
|
54
|
+
lookup_and_load_masterplans
|
55
|
+
load_masterplan MASTER_PLAN
|
56
|
+
end
|
57
|
+
|
58
|
+
# Adds a set of filenames for plans into the set of +@plan_files+
|
59
|
+
def add_plans(planfiles)
|
60
|
+
@plan_files.merge(planfiles)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Loads all plan files added using +add_plans+
|
64
|
+
# @see Plan.load
|
65
|
+
def load_plans
|
66
|
+
@plans ||= @plan_files.reduce({}) do |hash, file|
|
67
|
+
plans = Plan.load file
|
68
|
+
plans.each { |plan| hash[plan.name] = plan }
|
69
|
+
hash
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Loads a masterplan using the DSL, if it exists and hasn't been loaded already
|
74
|
+
def load_masterplan filename
|
75
|
+
if File.exists? filename and !@loaded_masterplans.include? filename
|
76
|
+
@loaded_masterplans << filename
|
77
|
+
DSL.new(self, filename)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Walks up the file tree looking for masterplans.
|
84
|
+
def lookup_and_load_masterplans
|
85
|
+
load_masterplan File.join(Dir.pwd, PLANFILE)
|
86
|
+
|
87
|
+
# Walk up the tree until we reach the project root, the home directory, or
|
88
|
+
# the root directory
|
89
|
+
unless [project_root, Dir.home, '/'].include? Dir.pwd
|
90
|
+
Dir.chdir('..') { lookup_and_load_masterplans }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class DSL
|
95
|
+
def initialize(config, filename)
|
96
|
+
@config = config
|
97
|
+
@filename = filename
|
98
|
+
instance_eval(File.read(filename), filename, 0) if File.exists? filename
|
99
|
+
end
|
100
|
+
|
101
|
+
# Specifies that another masterplan should also be loaded when loading
|
102
|
+
# this masterplan. NOTE: This _immediately_ loads the other masterplan.
|
103
|
+
def see_also(filename)
|
104
|
+
@config.load_masterplan(filename)
|
105
|
+
end
|
106
|
+
|
107
|
+
# With no arguments, specifies that the current directory containing this
|
108
|
+
# masterplan is at the root of your project. Otherwise, specifies the root
|
109
|
+
# of the project.
|
110
|
+
def project_root(root = File.dirname(@filename))
|
111
|
+
@config.project_root = root
|
112
|
+
end
|
113
|
+
alias_method :at_project_root, :project_root
|
114
|
+
|
115
|
+
# With no arguments, specifies that plans exist in a /plans/ directory
|
116
|
+
# under the directory the masterplan is in.
|
117
|
+
def plan_files(directory = File.join(File.dirname(@filename), 'plans'))
|
118
|
+
@config.add_plans(Dir.glob(File.join(directory, '**', "*{#{supported_extensions}}")))
|
119
|
+
end
|
120
|
+
alias_method :has_plan_files, :plan_files
|
121
|
+
|
122
|
+
# Specifies that a specific plan file exists at the given +filename+.
|
123
|
+
def plan_file(*files)
|
124
|
+
@config.add_plans(files)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Add arbitrary configuration attributes to the configuration object.
|
128
|
+
# Use this to add plan specific configuration options.
|
129
|
+
def configure(attribute, value=nil, &block)
|
130
|
+
Configuration.add_attribute(attribute)
|
131
|
+
@config.public_send "#{attribute}=", value, &block
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def supported_extensions
|
137
|
+
Loader.supported_extensions.join(',')
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module CLI::Mastermind::Interface
|
2
|
+
def enable_ui
|
3
|
+
CLI::UI::StdoutRouter.enable
|
4
|
+
end
|
5
|
+
|
6
|
+
def ui_enabled?
|
7
|
+
CLI::UI::StdoutRouter.enabled?
|
8
|
+
end
|
9
|
+
|
10
|
+
def spinner(title)
|
11
|
+
return yield unless ui_enabled?
|
12
|
+
|
13
|
+
yield_value = nil
|
14
|
+
|
15
|
+
group = CLI::UI::SpinGroup.new
|
16
|
+
group.add(title) do |spinner|
|
17
|
+
catch(:success) do
|
18
|
+
msg = catch(:fail) do
|
19
|
+
yield_value = yield spinner
|
20
|
+
throw :success
|
21
|
+
end
|
22
|
+
|
23
|
+
puts msg
|
24
|
+
CLI::UI::Spinner::TASK_FAILED
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
group.wait
|
29
|
+
|
30
|
+
yield_value
|
31
|
+
end
|
32
|
+
|
33
|
+
def frame(title)
|
34
|
+
return yield unless ui_enabled?
|
35
|
+
CLI::UI::Frame.open(title) { yield }
|
36
|
+
end
|
37
|
+
|
38
|
+
def ask(question, default: nil)
|
39
|
+
CLI::UI.ask(question, default: default)
|
40
|
+
end
|
41
|
+
|
42
|
+
def confirm(question)
|
43
|
+
CLI::UI.confirm(question)
|
44
|
+
end
|
45
|
+
|
46
|
+
def select(question, options:, default: options.first, **opts)
|
47
|
+
default_value = nil
|
48
|
+
options = case options
|
49
|
+
when Array
|
50
|
+
default_value = default
|
51
|
+
|
52
|
+
o = options - [default]
|
53
|
+
o.zip(o).to_h
|
54
|
+
when Hash
|
55
|
+
# Handle the "default" default. Otherwise, we expect the
|
56
|
+
# default to be a key in the options hash
|
57
|
+
default = default.first if default.is_a? Array
|
58
|
+
default_value = options[default]
|
59
|
+
options.dup.tap { |o| o.delete(default) }
|
60
|
+
end
|
61
|
+
|
62
|
+
CLI::UI::Prompt.ask(question, **opts) do |handler|
|
63
|
+
handler.option(titleize(default.to_s)) { default_value }
|
64
|
+
|
65
|
+
options.each do |(text, value)|
|
66
|
+
handler.option(titleize(text.to_s)) { value }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def titleize(string)
|
72
|
+
string.gsub(/[-_-]/, ' ').split(' ').map(&:capitalize).join(' ')
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CLI::Mastermind
|
2
|
+
class Loader
|
3
|
+
class PlanfileLoader < Loader
|
4
|
+
@loadable_extensions = %w[ .plan ].freeze
|
5
|
+
|
6
|
+
def self.load(filename)
|
7
|
+
DSL.new(filename).plans
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
class DSL
|
13
|
+
attr_reader :plans
|
14
|
+
|
15
|
+
def initialize(filename=nil, &block)
|
16
|
+
@plans = []
|
17
|
+
|
18
|
+
if block_given?
|
19
|
+
instance_eval(&block)
|
20
|
+
elsif File.exists? filename
|
21
|
+
instance_eval(File.read(filename), filename, 0)
|
22
|
+
else
|
23
|
+
raise 'Must provide valid path to a planfile or a block', Error
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def plot(name, &block)
|
28
|
+
plan = Plan.new name, @description
|
29
|
+
@description = nil
|
30
|
+
@plans << plan
|
31
|
+
plan.add_children DSL.new(&block).plans
|
32
|
+
end
|
33
|
+
alias_method :namespace, :plot
|
34
|
+
|
35
|
+
def description(text)
|
36
|
+
@description = text
|
37
|
+
end
|
38
|
+
alias_method :desc, :description
|
39
|
+
|
40
|
+
def plan(name, &block)
|
41
|
+
@plans << Plan.new(name, @description, &block)
|
42
|
+
@description = nil
|
43
|
+
end
|
44
|
+
alias_method :task, :plan
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CLI::Mastermind
|
2
|
+
class Loader
|
3
|
+
class << self
|
4
|
+
attr_reader :loadable_extensions
|
5
|
+
@@loaders = []
|
6
|
+
|
7
|
+
def inherited(subclass)
|
8
|
+
@@loaders << subclass
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_loader(extension)
|
12
|
+
loader = @@loaders.find { |l| l.can_load? extension }
|
13
|
+
|
14
|
+
raise UnsupportedFileTypeError.new(extension) unless loader
|
15
|
+
|
16
|
+
loader
|
17
|
+
end
|
18
|
+
|
19
|
+
def supported_extensions
|
20
|
+
@@loaders.flat_map { |l| l.loadable_extensions }
|
21
|
+
end
|
22
|
+
|
23
|
+
def can_load?(extension)
|
24
|
+
@loadable_extensions.include? extension
|
25
|
+
end
|
26
|
+
|
27
|
+
def load(filename)
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'cli/mastermind/loader/planfile_loader'
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module Mastermind
|
5
|
+
class Plan
|
6
|
+
extend Forwardable
|
7
|
+
include Interface
|
8
|
+
|
9
|
+
# The name of the plan. Used to specify the plan from the command line
|
10
|
+
# or from the interactive menu
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Displayed in the non-interactive list of available plans
|
14
|
+
attr_reader :description
|
15
|
+
|
16
|
+
# Used in the interactive plan selector to display child plans
|
17
|
+
attr_reader :children
|
18
|
+
|
19
|
+
# Loads a particular plan from the filesystem.
|
20
|
+
# @see Loader
|
21
|
+
def self.load(filename)
|
22
|
+
ext = File.extname(filename)
|
23
|
+
loader = Loader.find_loader(ext)
|
24
|
+
loader.load(filename)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(name, description=nil, &block)
|
28
|
+
@name = name.to_s.freeze
|
29
|
+
@description = description.freeze
|
30
|
+
@block = block
|
31
|
+
@children = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get the child plan with the specified +name+
|
35
|
+
def get_child(name)
|
36
|
+
@children[name]
|
37
|
+
end
|
38
|
+
alias_method :[], :get_child
|
39
|
+
alias_method :dig, :get_child
|
40
|
+
|
41
|
+
def add_children(plans)
|
42
|
+
raise 'Cannot add child plans to a plan with an action', InvalidPlanError unless @block.nil?
|
43
|
+
plans.each { |plan| @children[plan.name] = plan }
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_children?
|
47
|
+
@children.any?
|
48
|
+
end
|
49
|
+
|
50
|
+
def call(options=nil)
|
51
|
+
case @block.arity
|
52
|
+
when 1, -1 then instance_exec(options, &@block)
|
53
|
+
else instance_exec(&@block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Delegate configuration to the top-level configuration object
|
60
|
+
def_delegator :'CLI::Mastermind', :configuration
|
61
|
+
alias_method :config, :configuration
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module CLI
|
2
|
+
module Mastermind
|
3
|
+
|
4
|
+
def self.gem_version
|
5
|
+
Gem::Version.new VERSION::STRING
|
6
|
+
end
|
7
|
+
|
8
|
+
module VERSION
|
9
|
+
RELEASE = 0
|
10
|
+
MAJOR = 0
|
11
|
+
MINOR = 1
|
12
|
+
PATCH = nil
|
13
|
+
|
14
|
+
STRING = [RELEASE, MAJOR, MINOR, PATCH].compact.join('.').freeze
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'cli/ui'
|
2
|
+
require 'cli/mastermind/arg_parse'
|
3
|
+
require 'cli/mastermind/configuration'
|
4
|
+
require 'cli/mastermind/errors'
|
5
|
+
require 'cli/mastermind/interface'
|
6
|
+
require 'cli/mastermind/loader'
|
7
|
+
require 'cli/mastermind/plan'
|
8
|
+
require 'cli/mastermind/version'
|
9
|
+
|
10
|
+
module CLI
|
11
|
+
module Mastermind
|
12
|
+
extend Interface
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_reader :plans
|
16
|
+
|
17
|
+
def configuration
|
18
|
+
@config
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute(cli_args=ARGV)
|
22
|
+
@arguments = ArgParse.new(cli_args)
|
23
|
+
|
24
|
+
enable_ui if @arguments.display_ui?
|
25
|
+
|
26
|
+
frame('Mastermind') do
|
27
|
+
@config = spinner('Loading configuration') { Configuration.new }
|
28
|
+
@plans = spinner('Loading plans') { @config.load_plans }
|
29
|
+
@plan_stack = []
|
30
|
+
|
31
|
+
@selected_plan = nil
|
32
|
+
|
33
|
+
while @arguments.has_additional_plan_names?
|
34
|
+
plan_name = @arguments.get_next_plan_name
|
35
|
+
@selected_plan = (@selected_plan || @plans)[plan_name]
|
36
|
+
@plan_stack << titleize(plan_name)
|
37
|
+
|
38
|
+
if @selected_plan.nil?
|
39
|
+
puts "No plan found at #{@plan_stack.join('/')}"
|
40
|
+
puts @arguments.parser
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Prevent the prompt from exploading
|
46
|
+
if @selected_plan.nil? and @plans.count == 1
|
47
|
+
@selected_plan = @plans.values.first
|
48
|
+
@plan_stack << titleize(@selected_plan.name)
|
49
|
+
end
|
50
|
+
|
51
|
+
while @selected_plan.nil? or @selected_plan.has_children?
|
52
|
+
do_interactive_plan_selection
|
53
|
+
@plan_stack << titleize(@selected_plan.name)
|
54
|
+
end
|
55
|
+
|
56
|
+
if !@arguments.ask? or confirm("Execute plan #{@plan_stack.join('/')}?")
|
57
|
+
@selected_plan.call(@arguments.plan_arguments)
|
58
|
+
else
|
59
|
+
puts 'aborted!'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def do_interactive_plan_selection
|
67
|
+
options = @selected_plan&.children || @plans
|
68
|
+
|
69
|
+
@selected_plan = select("Select a plan under #{@plan_stack.join('/')}", options: options)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/plans/gem.plan
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#-*- ruby -*-
|
2
|
+
plot :gem do
|
3
|
+
description 'Builds the mastermind gem'
|
4
|
+
# NOTE: The Gem::Package interface doesn't appear to be completely stable.
|
5
|
+
# This may only work on version 2.7.7 of RubyGems, though other versions
|
6
|
+
# seem to support passing only a spec as I'm doing here.
|
7
|
+
plan :build do
|
8
|
+
Dir.mkdir 'pkg' unless Dir.exists? 'pkg'
|
9
|
+
|
10
|
+
require 'rubygems/package'
|
11
|
+
spec = config.gemspec
|
12
|
+
|
13
|
+
Gem::Package.build(spec)
|
14
|
+
|
15
|
+
File.write(File.join('pkg', spec.file_name), File.read(spec.file_name), mode: ?w)
|
16
|
+
File.delete(spec.file_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
description 'Install mastermind into the local system'
|
20
|
+
plan :install do
|
21
|
+
require 'rubygems/installer'
|
22
|
+
config.plans['gem']['build'].call
|
23
|
+
|
24
|
+
Gem::Installer.at(File.join('pkg', config.gemspec.file_name)).install
|
25
|
+
end
|
26
|
+
|
27
|
+
description 'Package and push a new release'
|
28
|
+
plan :release do
|
29
|
+
config.plans['gem']['build'].call
|
30
|
+
spec = config.gemspec
|
31
|
+
|
32
|
+
`git tag '#{spec.version}'`
|
33
|
+
`git push origin HEAD`
|
34
|
+
`git push --tags`
|
35
|
+
|
36
|
+
require 'rubygems/command_manager'
|
37
|
+
Gem::CommandManager.instance['push'].invoke File.join('pkg', spec.file_name)
|
38
|
+
end
|
39
|
+
end
|
data/plans/sample.plan
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#-*- ruby -*-
|
2
|
+
# aliased as `namespace` for a less fanciful, rake-like way of doing things
|
3
|
+
plot :top_level do
|
4
|
+
|
5
|
+
# aliased as `desc` if you prefer brevity
|
6
|
+
description 'This is the description of the next plan defined'
|
7
|
+
# aliased as `task`, for a less fanciful, rake-like way of doing things
|
8
|
+
plan :child_plan do |arguments|
|
9
|
+
puts 'EXECUTING'
|
10
|
+
puts arguments
|
11
|
+
end
|
12
|
+
|
13
|
+
description 'This is another plan under the top_level plan'
|
14
|
+
plan :second_child do
|
15
|
+
# more tasks
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cli-mastermind
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Hall
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-02-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: cli-ui
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.2.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.2'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.2.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bundler
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.16'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.16'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.0'
|
61
|
+
description: |2
|
62
|
+
Take over the world from your command line!
|
63
|
+
With mastermind, you can quickly build and generate
|
64
|
+
command line toolkits without having to custom build
|
65
|
+
everything for every project.
|
66
|
+
email:
|
67
|
+
- chall8908@gmail.com
|
68
|
+
executables:
|
69
|
+
- mastermind
|
70
|
+
extensions: []
|
71
|
+
extra_rdoc_files: []
|
72
|
+
files:
|
73
|
+
- ".gitignore"
|
74
|
+
- ".masterplan"
|
75
|
+
- ".rspec"
|
76
|
+
- Gemfile
|
77
|
+
- Gemfile.lock
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- bin/console
|
81
|
+
- bin/rspec
|
82
|
+
- bin/setup
|
83
|
+
- cli-mastermind.gemspec
|
84
|
+
- exe/mastermind
|
85
|
+
- lib/cli/mastermind.rb
|
86
|
+
- lib/cli/mastermind/arg_parse.rb
|
87
|
+
- lib/cli/mastermind/configuration.rb
|
88
|
+
- lib/cli/mastermind/errors.rb
|
89
|
+
- lib/cli/mastermind/interface.rb
|
90
|
+
- lib/cli/mastermind/loader.rb
|
91
|
+
- lib/cli/mastermind/loader/planfile_loader.rb
|
92
|
+
- lib/cli/mastermind/plan.rb
|
93
|
+
- lib/cli/mastermind/version.rb
|
94
|
+
- plans/gem.plan
|
95
|
+
- plans/sample.plan
|
96
|
+
homepage: https://github.com/chall8908/cli-mastermind
|
97
|
+
licenses:
|
98
|
+
- MIT
|
99
|
+
metadata: {}
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project:
|
116
|
+
rubygems_version: 2.7.7
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: Mastermind is a library for constructing command line toolboxes.
|
120
|
+
test_files: []
|