begin_cli 1.0.0

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
+ 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)
@@ -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
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'begin'
4
+ require 'begin/output'
5
+
6
+ begin
7
+ Begin::CLI.start(ARGV)
8
+ rescue Interrupt
9
+ Begin::Output.error 'Process Interrupted'
10
+ exit 1
11
+ rescue StandardError => e
12
+ Begin::Output.error e.message
13
+ exit 1
14
+ end
data/lib/begin.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'begin/cli'
2
+ require 'begin/version'
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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Begin
2
+ VERSION = '1.0.0'.freeze
3
+ 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: []