cli-mastermind 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.masterplan ADDED
@@ -0,0 +1,6 @@
1
+ #-*- ruby -*-
2
+ at_project_root
3
+
4
+ has_plan_files
5
+
6
+ configure(:gemspec) { Gem::Specification.load('cli-mastermind.gemspec') }
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'cli/mastermind'
3
+
4
+ CLI::Mastermind.execute(ARGV)
@@ -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,12 @@
1
+ module CLI
2
+ module Mastermind
3
+ class Error < StandardError
4
+ end
5
+
6
+ class UnsupportedFileTypeError < Error
7
+ def initialize(extension)
8
+ super "Unsupported file type: #{extension}"
9
+ end
10
+ end
11
+ end
12
+ 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: []