begin_cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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: []
|