begin_cli 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/LICENSE.md +21 -0
- data/README.md +145 -0
- data/begin_cli.gemspec +46 -0
- data/exe/begin +14 -0
- data/lib/begin.rb +2 -0
- data/lib/begin/cli.rb +73 -0
- data/lib/begin/config.rb +99 -0
- data/lib/begin/input.rb +61 -0
- data/lib/begin/output.rb +33 -0
- data/lib/begin/path.rb +80 -0
- data/lib/begin/repository.rb +68 -0
- data/lib/begin/template.rb +215 -0
- data/lib/begin/version.rb +3 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e81ba548666c85ad86e52b27b0c559a683d1a2e9
|
4
|
+
data.tar.gz: 3b7d6cc55d5e16ac49d99e9b0025d54538040202
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 14951d1b81a107188e8f21bffc6b0f2733e815bd51da3b4332c67b76936c3ea687fec84f3e25c7e9ce5c03fd2629ae2056bf2fad64a9ed35233df0bf8ceb270b
|
7
|
+
data.tar.gz: 4b0d650e1613e63af6815ba55d850b2f6069f7dd456c4c9a68d2028e7fd7962e3c7afefb737c30411ad034fddfc2888c76a95135bcd256b03e369a324f27979d
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# Begin 1.0.0
|
2
|
+
|
3
|
+
* Initial version of Begin
|
4
|
+
|
5
|
+
* Support for both Git based and File-System based templates
|
6
|
+
|
7
|
+
* Support for Mustache tags in template file names, directory names, and file content
|
8
|
+
|
9
|
+
* Read the docs at: [https://jbrd.github.io/begin](https://jbrd.github.io/begin)
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at jbrd.git@outlook.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 James Bird
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# begin
|
2
|
+
|
3
|
+
`begin` is a terminal command for running logic-less project templates. Templates are just [git](https://git-scm.com)
|
4
|
+
repositories whose files and directories are copied to the working directory when run. Directory names, file names,
|
5
|
+
and file content can contain [Mustache](https://mustache.github.io/mustache.5.html) tags - the values of which are
|
6
|
+
prompted for in the terminal and substituted when the template is run.
|
7
|
+
|
8
|
+
<div align="center">
|
9
|
+
<img alt="Begin Terminal Example" src="docs/assets/terminal.png" />
|
10
|
+
</div>
|
11
|
+
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
To install on your system, obtain the Ruby gem with:
|
16
|
+
|
17
|
+
```bash
|
18
|
+
$ gem install begin_cli
|
19
|
+
```
|
20
|
+
|
21
|
+
Read the [Installation Guide](https://jbrd.github.io/begin/install.html) for more in-depth examples
|
22
|
+
for various operating systems.
|
23
|
+
|
24
|
+
|
25
|
+
## Overview
|
26
|
+
|
27
|
+
### Installing A Template
|
28
|
+
|
29
|
+
* Install a template with the ```begin install``` command, e.g:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
$ begin install path/to/template.git
|
33
|
+
```
|
34
|
+
|
35
|
+
* Once you have installed a template, you may run it...
|
36
|
+
|
37
|
+
|
38
|
+
### Running A Template
|
39
|
+
|
40
|
+
* Run a template with the ```begin new``` command, e.g:
|
41
|
+
|
42
|
+
```bash
|
43
|
+
$ begin new template
|
44
|
+
```
|
45
|
+
|
46
|
+
|
47
|
+
### Template Structure
|
48
|
+
|
49
|
+
* A template is just a [Git](https://git-scm.com) repository
|
50
|
+
|
51
|
+
* A template can therefore contain any number of files and directories, and can be easily shared with others
|
52
|
+
|
53
|
+
* A template name can optionally start with `begin-`. This prefix is ignored and stripped by the command automatically, e.g:
|
54
|
+
|
55
|
+
```bash
|
56
|
+
$ begin install path/to/begin-latex-document.git
|
57
|
+
$ begin new latex-document
|
58
|
+
```
|
59
|
+
|
60
|
+
* An example template can be found at (https://github.com/jbrd/begin-latex-document)[https://github.com/jbrd/begin-latex-document], e.g:
|
61
|
+
|
62
|
+
```bash
|
63
|
+
$ begin install https://github.com/jbrd/begin-latex-document.git
|
64
|
+
$ begin new latex-document
|
65
|
+
```
|
66
|
+
|
67
|
+
|
68
|
+
### Template Tags
|
69
|
+
|
70
|
+
* File names, directory names, and file content can contain [Mustache](https://mustache.github.io/mustache.5.html) tags
|
71
|
+
|
72
|
+
|
73
|
+
* Create a ```.begin.yml``` in your template repository to describe expected tags:
|
74
|
+
|
75
|
+
```yaml
|
76
|
+
tags: !!omap
|
77
|
+
title:
|
78
|
+
label: 'Title'
|
79
|
+
author:
|
80
|
+
label: 'Author'
|
81
|
+
sections:
|
82
|
+
label: 'Sections'
|
83
|
+
array: true
|
84
|
+
```
|
85
|
+
|
86
|
+
|
87
|
+
* The user will be prompted for expected tags upon running a template:
|
88
|
+
|
89
|
+
```
|
90
|
+
$ begin new latex-document
|
91
|
+
Title: My Amazing New Document
|
92
|
+
Author: John Smith
|
93
|
+
Sections (CTRL+D to stop): Introduction
|
94
|
+
Sections (CTRL+D to stop): Background
|
95
|
+
Sections (CTRL+D to stop): ^D
|
96
|
+
Running template 'latex-document'...
|
97
|
+
Template 'latex-document' successfully run
|
98
|
+
```
|
99
|
+
|
100
|
+
### Terminal Commands
|
101
|
+
|
102
|
+
* Run a template with ```begin new```
|
103
|
+
|
104
|
+
* List installed templates with ```begin list```
|
105
|
+
|
106
|
+
* Install a template with ```begin install```
|
107
|
+
|
108
|
+
* Uninstall a template with ```begin uninstall```
|
109
|
+
|
110
|
+
* Update templates with ```begin update```
|
111
|
+
|
112
|
+
* Get help with ```begin help```
|
113
|
+
|
114
|
+
* Print the command version with ```begin version```
|
115
|
+
|
116
|
+
|
117
|
+
## Example Templates
|
118
|
+
|
119
|
+
* An example template can be found at: [https://github.com/jbrd/begin-latex-document.git](https://github.com/jbrd/begin-latex-document.git)
|
120
|
+
|
121
|
+
|
122
|
+
## Development
|
123
|
+
|
124
|
+
* Ensure [Bundler](http://bundler.io/) is installed on your system
|
125
|
+
* After checking out the repo, run ```bundle install --path vendor/bundle``` to install dependencies
|
126
|
+
* Run ```bundle exec begin``` to use the gem in this directory, ignoring other installed copies of this gem
|
127
|
+
* Run ```bundle exec rake``` to run test specs and ensure the code conforms to style guidelines
|
128
|
+
* To package this gem from source, run ```bundle exec rake install```
|
129
|
+
* To release a new version, update the version number in `version.rb`, and then run ```bundle exec rake release```
|
130
|
+
|
131
|
+
|
132
|
+
## Contributing
|
133
|
+
|
134
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jbrd/begin. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
135
|
+
|
136
|
+
|
137
|
+
## Contributors
|
138
|
+
|
139
|
+
* James Bird (jbrd)
|
140
|
+
|
141
|
+
|
142
|
+
## License
|
143
|
+
|
144
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
145
|
+
|
data/begin_cli.gemspec
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'begin/version'
|
6
|
+
|
7
|
+
GEM_NAME = 'begin_cli'.freeze
|
8
|
+
|
9
|
+
SUMMARY = \
|
10
|
+
'A terminal command for running logic-less project templates.'.freeze
|
11
|
+
|
12
|
+
DESCRIPTION = \
|
13
|
+
'A terminal command for running logic-less project templates. ' \
|
14
|
+
'Templates are just git repositories whose files and directories ' \
|
15
|
+
'are copied to the working directory when run. Directory names, ' \
|
16
|
+
'file names, and file content can contain Mustache tags - the ' \
|
17
|
+
'values of which are prompted for in the terminal and substituted ' \
|
18
|
+
'when the template is run.'.freeze
|
19
|
+
|
20
|
+
Gem::Specification.new do |spec|
|
21
|
+
spec.name = GEM_NAME
|
22
|
+
spec.version = Begin::VERSION
|
23
|
+
spec.authors = ['James Bird']
|
24
|
+
spec.email = ['jbrd.git@outlook.com']
|
25
|
+
|
26
|
+
spec.summary = SUMMARY
|
27
|
+
spec.description = DESCRIPTION
|
28
|
+
|
29
|
+
spec.homepage = 'https://jbrd.github.io/begin'
|
30
|
+
spec.license = 'MIT'
|
31
|
+
|
32
|
+
spec.files = %w(begin_cli.gemspec) + Dir["*.md", "exe/*", "lib/**/*.rb"]
|
33
|
+
spec.bindir = 'exe'
|
34
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
35
|
+
spec.require_paths = ['lib']
|
36
|
+
|
37
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
38
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
39
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
40
|
+
spec.add_development_dependency 'rubocop', '~> 0.49'
|
41
|
+
|
42
|
+
spec.add_dependency 'colorize', '~> 0.8'
|
43
|
+
spec.add_dependency 'git', '~> 1.3'
|
44
|
+
spec.add_dependency 'mustache', '~> 1.0'
|
45
|
+
spec.add_dependency 'thor', '~> 0.20'
|
46
|
+
end
|
data/exe/begin
ADDED
data/lib/begin.rb
ADDED
data/lib/begin/cli.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'begin/input'
|
2
|
+
require 'begin/output'
|
3
|
+
require 'begin/repository'
|
4
|
+
require 'begin/version'
|
5
|
+
require 'thor'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module Begin
|
9
|
+
# The CLI interface for the application.
|
10
|
+
class CLI < Thor
|
11
|
+
desc 'new TEMPLATE', 'Begin a new project by running the named TEMPLATE'
|
12
|
+
option :yaml, desc: 'Do not prompt user for tag values. ' \
|
13
|
+
'Instead, take them from given YAML file.'
|
14
|
+
def new(template)
|
15
|
+
template_impl = repository.template template
|
16
|
+
if options[:yaml]
|
17
|
+
context = YAML.load_file(options[:yaml])
|
18
|
+
else
|
19
|
+
context = Input.prompt_user_for_tag_values(template_impl.config.tags)
|
20
|
+
end
|
21
|
+
Output.action "Running template '#{template}'"
|
22
|
+
template_impl.run Dir.getwd, context
|
23
|
+
Output.success "Template '#{template}' successfully run"
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'list', 'List installed templates'
|
27
|
+
def list
|
28
|
+
repository.each { |x| Output.info(x) }
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'install PATH', 'Installs a template given its PATH'
|
32
|
+
def install(path)
|
33
|
+
repo = repository
|
34
|
+
template_name = repo.template_name path
|
35
|
+
Output.action "Installing template '#{template_name}' from '#{path}'"
|
36
|
+
repo.install path, template_name
|
37
|
+
Output.success "Template '#{template_name}' successfully installed"
|
38
|
+
end
|
39
|
+
|
40
|
+
desc 'uninstall TEMPLATE', 'Uninstalls the named TEMPLATE'
|
41
|
+
def uninstall(template)
|
42
|
+
template_impl = repository.template template
|
43
|
+
Output.action "Uninstalling template #{template}"
|
44
|
+
template_impl.uninstall
|
45
|
+
Output.success "Template '#{template}' successfully uninstalled"
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'update [TEMPLATE]', 'Updates all templates or one specific TEMPLATE'
|
49
|
+
def update(template = nil)
|
50
|
+
if template
|
51
|
+
template_impl = repository.template template
|
52
|
+
Output.action "Updating template #{template}"
|
53
|
+
template_impl.update
|
54
|
+
else
|
55
|
+
repository.each do |x|
|
56
|
+
Output.action "Updating template #{x}"
|
57
|
+
repository.template(x).update
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
desc 'version', 'Prints the version of this command'
|
63
|
+
def version
|
64
|
+
Output.info VERSION
|
65
|
+
end
|
66
|
+
|
67
|
+
no_commands do
|
68
|
+
def repository
|
69
|
+
Repository.new
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/begin/config.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Begin
|
4
|
+
# The root-level template configuration structure. A class representation
|
5
|
+
# of the template config file (.begin.yml)
|
6
|
+
class Config
|
7
|
+
@tags = []
|
8
|
+
|
9
|
+
attr_reader :tags
|
10
|
+
|
11
|
+
def initialize(tags)
|
12
|
+
@tags = tags
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_file(path)
|
16
|
+
if path.exists?
|
17
|
+
config = YAML.load_file path
|
18
|
+
tags = HashTag.from_config_hash(config)
|
19
|
+
Config.new tags
|
20
|
+
else
|
21
|
+
{}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Represents an expected mustache tag, as defined in the template config.
|
27
|
+
# Every type of tag has a key name (as inserted into the mustache context),
|
28
|
+
# and a human-readable label (as presented to the user).
|
29
|
+
class Tag
|
30
|
+
@key = ''
|
31
|
+
@label = ''
|
32
|
+
@array = false
|
33
|
+
|
34
|
+
attr_reader :key
|
35
|
+
attr_reader :label
|
36
|
+
attr_reader :array
|
37
|
+
|
38
|
+
def initialize(key, label, array)
|
39
|
+
@key = key
|
40
|
+
@label = label
|
41
|
+
@array = array
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.from_config(key, value)
|
45
|
+
return HashTag.from_config(key, value) if value.include? 'tags'
|
46
|
+
ValueTag.from_config(key, value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Represents a tag with a single value. Value tags can have a default value
|
51
|
+
# assigned in the config. If the user chooses not to enter a value, the
|
52
|
+
# default value is substituted instead.
|
53
|
+
class ValueTag < Tag
|
54
|
+
@default = ''
|
55
|
+
|
56
|
+
attr_reader :default
|
57
|
+
|
58
|
+
def initialize(key, label, array, default)
|
59
|
+
super key, label, array
|
60
|
+
@default = default
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.from_config(key, value)
|
64
|
+
array = value.include?('array') ? value['array'] : false
|
65
|
+
label = value.include?('label') ? value['label'] : key
|
66
|
+
default = value.include?('default') ? value['default'] : ''
|
67
|
+
ValueTag.new key, label, array, default
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Represents a nested object hash tag. On encountering a hash tag, the user
|
72
|
+
# is prompted to enter a value for each member of the hash.
|
73
|
+
class HashTag < Tag
|
74
|
+
@children = []
|
75
|
+
|
76
|
+
attr_reader :children
|
77
|
+
|
78
|
+
def initialize(key, label, array, children)
|
79
|
+
super key, label, array
|
80
|
+
@children = children
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.from_config_hash(config)
|
84
|
+
return [] unless config.include?('tags') && config['tags'].is_a?(Hash)
|
85
|
+
config['tags'].each.map do |key, value|
|
86
|
+
raise "Invalid template. Expected value of '#{key}' to be a Hash" \
|
87
|
+
unless value.is_a? Hash
|
88
|
+
Tag.from_config key, value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.from_config(key, value)
|
93
|
+
array = value.include?('array') ? value['array'] : false
|
94
|
+
label = value.include?('label') ? value['label'] : key
|
95
|
+
children = value.include?('tags') ? from_config_hash(value) : []
|
96
|
+
HashTag.new key, label, array, children
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/begin/input.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
module Begin
|
2
|
+
# All console input is routed through this module
|
3
|
+
module Input
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def prompt(msg)
|
7
|
+
STDOUT.write msg
|
8
|
+
begin
|
9
|
+
value = STDIN.gets
|
10
|
+
raise EOFError if value.nil?
|
11
|
+
return value.chomp
|
12
|
+
rescue StandardError, Interrupt
|
13
|
+
Output.newline
|
14
|
+
raise
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def prompt_user_for_tag(tag, level = 0, in_array = false)
|
19
|
+
indent = ' ' * level
|
20
|
+
array_msg = in_array ? " (#{eof_shortcut} to stop)" : ''
|
21
|
+
case tag
|
22
|
+
when HashTag
|
23
|
+
Output.info "#{indent}#{tag.label}#{array_msg}:"
|
24
|
+
prompt_user_for_tag_values tag.children, level + 1
|
25
|
+
when ValueTag
|
26
|
+
prompt "#{indent}#{tag.label}#{array_msg}: "
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def prompt_user_for_array_tag(tag, level = 0)
|
31
|
+
result = []
|
32
|
+
loop do
|
33
|
+
begin
|
34
|
+
value = prompt_user_for_tag tag, level, true
|
35
|
+
result.push value
|
36
|
+
rescue EOFError
|
37
|
+
break
|
38
|
+
end
|
39
|
+
end
|
40
|
+
result
|
41
|
+
end
|
42
|
+
|
43
|
+
def prompt_user_for_tag_values(tags, level = 0)
|
44
|
+
context = {}
|
45
|
+
tags.each do |x|
|
46
|
+
context[x.key] = prompt_user_for_array_tag(x, level) if x.array
|
47
|
+
context[x.key] = prompt_user_for_tag(x, level) unless x.array
|
48
|
+
end
|
49
|
+
context
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the keyboard accelerator shortcut for the EOF signal,
|
53
|
+
# which is dependant on the host terminal
|
54
|
+
def eof_shortcut
|
55
|
+
if ENV.key? 'ComSpec'
|
56
|
+
return 'CTRL+Z' if ENV['ComSpec'].upcase.end_with? '\CMD.EXE'
|
57
|
+
end
|
58
|
+
'CTRL+D'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/begin/output.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module Begin
|
4
|
+
# All console output is routed through this module ensuring
|
5
|
+
# it is formatted consistently
|
6
|
+
module Output
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def error(value)
|
10
|
+
STDERR.puts "ERROR: #{value}".colorize :red
|
11
|
+
end
|
12
|
+
|
13
|
+
def warning(value)
|
14
|
+
STDOUT.puts "WARNING: #{value}".colorize :yellow
|
15
|
+
end
|
16
|
+
|
17
|
+
def info(value)
|
18
|
+
STDOUT.puts value
|
19
|
+
end
|
20
|
+
|
21
|
+
def action(value)
|
22
|
+
STDOUT.puts "#{value}..."
|
23
|
+
end
|
24
|
+
|
25
|
+
def success(value)
|
26
|
+
STDOUT.puts value.colorize :green
|
27
|
+
end
|
28
|
+
|
29
|
+
def newline
|
30
|
+
STDOUT.puts ''
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/begin/path.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Begin
|
2
|
+
# The canonical file path representation used throughout the application.
|
3
|
+
# Paths are immediately expanded into absolute file paths on construction
|
4
|
+
class Path
|
5
|
+
def initialize(path, parent_dir, help)
|
6
|
+
@path = File.expand_path path, parent_dir
|
7
|
+
@help = help
|
8
|
+
end
|
9
|
+
|
10
|
+
def eql?(other)
|
11
|
+
@path.eql?(other.to_str)
|
12
|
+
end
|
13
|
+
|
14
|
+
def hash
|
15
|
+
@path.hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
@path
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_str
|
23
|
+
@path
|
24
|
+
end
|
25
|
+
|
26
|
+
def ensure_exists
|
27
|
+
return if File.exist? @path
|
28
|
+
raise IOError, "#{@help} '#{@path}' does not exist"
|
29
|
+
end
|
30
|
+
|
31
|
+
def ensure_symlink_exists
|
32
|
+
ensure_exists
|
33
|
+
return if File.symlink? @path
|
34
|
+
raise IOError, "#{@help} '#{@path}' is not a symbolic link"
|
35
|
+
end
|
36
|
+
|
37
|
+
def ensure_dir_exists
|
38
|
+
ensure_exists
|
39
|
+
return if directory?
|
40
|
+
raise IOError, "#{@help} '#{@path}' is not a directory"
|
41
|
+
end
|
42
|
+
|
43
|
+
def dir_contents
|
44
|
+
escaped_path = @path.gsub(/[\\\{\}\[\]\*\?\.]/) { |x| '\\' + x }
|
45
|
+
Dir.glob(File.join([escaped_path, '*']))
|
46
|
+
end
|
47
|
+
|
48
|
+
def make_dir
|
49
|
+
Dir.mkdir @path unless File.exist? @path
|
50
|
+
ensure_dir_exists
|
51
|
+
end
|
52
|
+
|
53
|
+
def make_parent_dirs
|
54
|
+
parent = File.dirname @path
|
55
|
+
FileUtils.mkdir_p parent
|
56
|
+
end
|
57
|
+
|
58
|
+
def copy_to(destination)
|
59
|
+
ensure_exists
|
60
|
+
destination.ensure_dir_exists
|
61
|
+
FileUtils.cp @path, destination
|
62
|
+
end
|
63
|
+
|
64
|
+
def basename
|
65
|
+
File.basename @path
|
66
|
+
end
|
67
|
+
|
68
|
+
def directory?
|
69
|
+
File.directory? @path
|
70
|
+
end
|
71
|
+
|
72
|
+
def exists?
|
73
|
+
File.exist? @path
|
74
|
+
end
|
75
|
+
|
76
|
+
def contains?(path)
|
77
|
+
path.to_str.start_with? @path
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'begin/output'
|
2
|
+
require 'begin/path'
|
3
|
+
require 'begin/template'
|
4
|
+
require 'git'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module Begin
|
8
|
+
# Provides centralised access to the local repository of templates
|
9
|
+
# on the machine
|
10
|
+
class Repository
|
11
|
+
def initialize(name = '.begin', parent_dir = '~')
|
12
|
+
@parent_dir = Path.new(parent_dir, '.', 'Repository Parent')
|
13
|
+
@parent_dir.ensure_dir_exists
|
14
|
+
@repo_dir = Path.new(name, @parent_dir, 'Repository directory')
|
15
|
+
@template_dir = Path.new('templates', @repo_dir, 'Templates directory')
|
16
|
+
end
|
17
|
+
|
18
|
+
def install(source_uri, name)
|
19
|
+
path = install_prerequisites(name)
|
20
|
+
begin
|
21
|
+
return GitTemplate.install source_uri, path
|
22
|
+
rescue
|
23
|
+
unless source_uri.include? '://'
|
24
|
+
return SymlinkTemplate.install source_uri, path
|
25
|
+
end
|
26
|
+
raise
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def install_prerequisites(name)
|
31
|
+
@repo_dir.make_dir
|
32
|
+
@template_dir.make_dir
|
33
|
+
path = template_path name
|
34
|
+
raise "A template is already installed at: #{path}" if path.exists?
|
35
|
+
Output.info "Installing to '#{path}'"
|
36
|
+
path
|
37
|
+
end
|
38
|
+
|
39
|
+
def each
|
40
|
+
templates = @template_dir.dir_contents
|
41
|
+
templates.each { |x| yield template_name x }
|
42
|
+
end
|
43
|
+
|
44
|
+
def template(name)
|
45
|
+
path = template_path name
|
46
|
+
template_from_path path
|
47
|
+
end
|
48
|
+
|
49
|
+
def template_name(uri)
|
50
|
+
uri = URI(uri)
|
51
|
+
path_bits = uri.path.split '/'
|
52
|
+
name = path_bits.last
|
53
|
+
name.slice! 'begin-'
|
54
|
+
name.slice! 'begin_'
|
55
|
+
name.chomp! '.git'
|
56
|
+
name
|
57
|
+
end
|
58
|
+
|
59
|
+
def template_path(template_name)
|
60
|
+
Path.new template_name, @template_dir, 'Template directory'
|
61
|
+
end
|
62
|
+
|
63
|
+
def template_from_path(path)
|
64
|
+
return SymlinkTemplate.new(path) if File.symlink? path
|
65
|
+
GitTemplate.new path
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'begin/config'
|
2
|
+
require 'begin/output'
|
3
|
+
require 'begin/path'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'git'
|
6
|
+
require 'mustache'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
module Begin
|
10
|
+
# Represents an installed template on the user's machine.
|
11
|
+
class Template
|
12
|
+
CONFIG_NAME = '.begin.yml'.freeze
|
13
|
+
|
14
|
+
def initialize(path)
|
15
|
+
@path = path
|
16
|
+
@path.ensure_dir_exists
|
17
|
+
end
|
18
|
+
|
19
|
+
def uninstall
|
20
|
+
# Must be implemented in base class
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def update
|
25
|
+
# Must be implemented in base class
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def config_path
|
30
|
+
Path.new CONFIG_NAME, @path, 'Config'
|
31
|
+
end
|
32
|
+
|
33
|
+
def config
|
34
|
+
Config.from_file config_path
|
35
|
+
end
|
36
|
+
|
37
|
+
def run(target_dir, context)
|
38
|
+
target_dir = Path.new target_dir, '.', 'Directory'
|
39
|
+
target_dir.ensure_dir_exists
|
40
|
+
paths = process_path_names @path, target_dir, context
|
41
|
+
process_files paths, context
|
42
|
+
end
|
43
|
+
|
44
|
+
def ensure_no_back_references(source_path, expanded_path, target_dir)
|
45
|
+
return if target_dir.contains? expanded_path
|
46
|
+
err = 'Backward-reference detected in expanded ' \
|
47
|
+
"template path. Details to follow.\n"
|
48
|
+
err += "Source Path: #{source_path}\n"
|
49
|
+
err += "Expanded Path: #{expanded_path}\n"
|
50
|
+
err += "Expected Parent: #{target_dir}\n"
|
51
|
+
raise err
|
52
|
+
end
|
53
|
+
|
54
|
+
def ensure_no_conflicts(paths, source_path, target_path)
|
55
|
+
return unless paths.key? target_path
|
56
|
+
err = "File path collision detected. Details to follow.\n"
|
57
|
+
err += "(1) Source File: #{source_path}\n"
|
58
|
+
err += "(1) ..Writes To: #{target_path}\n"
|
59
|
+
err += "(2) Source File: #{paths[target_path]}\n"
|
60
|
+
err += "(2) ..Writes To: #{target_path}\n"
|
61
|
+
raise err
|
62
|
+
end
|
63
|
+
|
64
|
+
def ensure_name_not_empty(source_path, expanded_name)
|
65
|
+
return unless expanded_name.empty?
|
66
|
+
err = "Mustache evaluation resulted in an empty file name...\n"
|
67
|
+
err += "... whilst evaluating: #{source_path}"
|
68
|
+
raise err
|
69
|
+
end
|
70
|
+
|
71
|
+
def process_path_name(source_path, target_dir, context)
|
72
|
+
expanded_name = Mustache.render source_path.basename, context
|
73
|
+
ensure_name_not_empty source_path, expanded_name
|
74
|
+
expanded_path = Path.new expanded_name, target_dir, 'Target'
|
75
|
+
ensure_no_back_references source_path, expanded_path, target_dir
|
76
|
+
expanded_path
|
77
|
+
end
|
78
|
+
|
79
|
+
def process_path_names_in_dir(source, target, paths, working_set, context)
|
80
|
+
source.dir_contents.each do |entry|
|
81
|
+
source_path = Path.new entry, '.', 'Source'
|
82
|
+
target_path = process_path_name source_path, target, context
|
83
|
+
ensure_no_conflicts paths, source_path, target_path
|
84
|
+
paths[target_path] = source_path
|
85
|
+
working_set.push [source_path, target_path] if source_path.directory?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def process_path_names(source_dir, target_dir, context)
|
90
|
+
paths = {}
|
91
|
+
working_set = [[source_dir, target_dir]]
|
92
|
+
until working_set.empty?
|
93
|
+
source, target = working_set.pop
|
94
|
+
process_path_names_in_dir source, target, paths, working_set, context
|
95
|
+
end
|
96
|
+
paths
|
97
|
+
end
|
98
|
+
|
99
|
+
def process_file(source_path, target_path, context)
|
100
|
+
contents = File.read source_path
|
101
|
+
File.write target_path, Mustache.render(contents, context)
|
102
|
+
end
|
103
|
+
|
104
|
+
def process_files(paths, context)
|
105
|
+
paths.each do |target, source|
|
106
|
+
target.make_parent_dirs
|
107
|
+
if source.directory?
|
108
|
+
target.make_dir
|
109
|
+
else
|
110
|
+
process_file source, target, context
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Encapsulates the logic for templates that are installed as symlinks
|
117
|
+
# on the user's machine.
|
118
|
+
class SymlinkTemplate < Template
|
119
|
+
def initialize(path)
|
120
|
+
super path
|
121
|
+
@path.ensure_symlink_exists
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.install(source_uri, path)
|
125
|
+
source_path = Path.new source_uri, '.', 'Source path'
|
126
|
+
source_path.ensure_dir_exists
|
127
|
+
begin
|
128
|
+
File.symlink source_path, path
|
129
|
+
Output.success "Created symbolic link to '#{source_path}'"
|
130
|
+
rescue NotImplementedError
|
131
|
+
raise NotImplementedError, 'TODO: Copy tree when symlinks not supported'
|
132
|
+
end
|
133
|
+
SymlinkTemplate.new path
|
134
|
+
end
|
135
|
+
|
136
|
+
def update
|
137
|
+
# Do nothing. Symlink templates are always up-to-date.
|
138
|
+
end
|
139
|
+
|
140
|
+
def uninstall
|
141
|
+
File.unlink @path
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Encapsulates the logic for templates that are installed as cloned
|
146
|
+
# git repositories on the user's machine.
|
147
|
+
class GitTemplate < Template
|
148
|
+
def initialize(path)
|
149
|
+
super path
|
150
|
+
@repository = Git.open(path.to_s)
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.format_git_error_message(e)
|
154
|
+
partition = e.message.partition('2>&1:')
|
155
|
+
partition[2]
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.install(source_uri, path)
|
159
|
+
Git.clone(source_uri, path.to_s)
|
160
|
+
Output.success 'Template source was successfully git cloned'
|
161
|
+
GitTemplate.new path
|
162
|
+
rescue Git::GitExecuteError => e
|
163
|
+
raise format_git_error_message(e)
|
164
|
+
end
|
165
|
+
|
166
|
+
def uninstall
|
167
|
+
FileUtils.rm_rf @path
|
168
|
+
end
|
169
|
+
|
170
|
+
def check_repository
|
171
|
+
@repository.revparse('HEAD')
|
172
|
+
if @repository.current_branch.include? 'detached'
|
173
|
+
raise "HEAD is detached in local repository. Please fix: #{@path}"
|
174
|
+
end
|
175
|
+
rescue Git::GitExecuteError => e
|
176
|
+
error = "HEAD is not valid in local repository. Please fix: #{@path}\n"
|
177
|
+
error += format_git_error_message(e)
|
178
|
+
raise error
|
179
|
+
end
|
180
|
+
|
181
|
+
def check_tracking_branch
|
182
|
+
@repository.revparse('@{u}')
|
183
|
+
rescue
|
184
|
+
raise "Local branch '#{@repository.current_branch}' does not track " \
|
185
|
+
"an upstream branch in local repository: #{@path}"
|
186
|
+
end
|
187
|
+
|
188
|
+
def check_untracked_changes
|
189
|
+
message = 'Local repository contains untracked changes. ' \
|
190
|
+
"Please fix: #{@path}"
|
191
|
+
raise message unless @repository.status.untracked.empty?
|
192
|
+
end
|
193
|
+
|
194
|
+
def check_pending_changes
|
195
|
+
not_added = @repository.status.added.empty?
|
196
|
+
not_deleted = @repository.status.deleted.empty?
|
197
|
+
not_changed = @repository.status.changed.empty?
|
198
|
+
message = 'Local repository contains modified / staged files. ' \
|
199
|
+
"Please fix: #{@path}"
|
200
|
+
raise message unless not_added && not_deleted && not_changed
|
201
|
+
end
|
202
|
+
|
203
|
+
def update
|
204
|
+
check_repository
|
205
|
+
check_tracking_branch
|
206
|
+
check_untracked_changes
|
207
|
+
check_pending_changes
|
208
|
+
begin
|
209
|
+
@repository.pull
|
210
|
+
rescue Git::GitExecuteError => e
|
211
|
+
raise format_git_error_message(e)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: begin_cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Bird
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '12.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '12.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.49'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.49'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: colorize
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.8'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.8'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: git
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: mustache
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: thor
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.20'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.20'
|
125
|
+
description: A terminal command for running logic-less project templates. Templates
|
126
|
+
are just git repositories whose files and directories are copied to the working
|
127
|
+
directory when run. Directory names, file names, and file content can contain Mustache
|
128
|
+
tags - the values of which are prompted for in the terminal and substituted when
|
129
|
+
the template is run.
|
130
|
+
email:
|
131
|
+
- jbrd.git@outlook.com
|
132
|
+
executables:
|
133
|
+
- begin
|
134
|
+
extensions: []
|
135
|
+
extra_rdoc_files: []
|
136
|
+
files:
|
137
|
+
- CHANGELOG.md
|
138
|
+
- CODE_OF_CONDUCT.md
|
139
|
+
- LICENSE.md
|
140
|
+
- README.md
|
141
|
+
- begin_cli.gemspec
|
142
|
+
- exe/begin
|
143
|
+
- lib/begin.rb
|
144
|
+
- lib/begin/cli.rb
|
145
|
+
- lib/begin/config.rb
|
146
|
+
- lib/begin/input.rb
|
147
|
+
- lib/begin/output.rb
|
148
|
+
- lib/begin/path.rb
|
149
|
+
- lib/begin/repository.rb
|
150
|
+
- lib/begin/template.rb
|
151
|
+
- lib/begin/version.rb
|
152
|
+
homepage: https://jbrd.github.io/begin
|
153
|
+
licenses:
|
154
|
+
- MIT
|
155
|
+
metadata: {}
|
156
|
+
post_install_message:
|
157
|
+
rdoc_options: []
|
158
|
+
require_paths:
|
159
|
+
- lib
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
166
|
+
requirements:
|
167
|
+
- - ">="
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
requirements: []
|
171
|
+
rubyforge_project:
|
172
|
+
rubygems_version: 2.4.6
|
173
|
+
signing_key:
|
174
|
+
specification_version: 4
|
175
|
+
summary: A terminal command for running logic-less project templates.
|
176
|
+
test_files: []
|