cli-mastermind 0.0.1
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 +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: []
|