geny 0.1.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/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/.yardopts +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/geny +11 -0
- data/geny.gemspec +36 -0
- data/lib/generators/new/generator.rb +15 -0
- data/lib/generators/new/templates/generator.rb.erb +14 -0
- data/lib/geny/actions/files.rb +96 -0
- data/lib/geny/actions/git.rb +53 -0
- data/lib/geny/actions/shell.rb +62 -0
- data/lib/geny/actions/templates.rb +68 -0
- data/lib/geny/actions/ui.rb +59 -0
- data/lib/geny/cli.rb +99 -0
- data/lib/geny/command.rb +119 -0
- data/lib/geny/context/base.rb +30 -0
- data/lib/geny/context/invoke.rb +56 -0
- data/lib/geny/context/view.rb +15 -0
- data/lib/geny/dsl.rb +44 -0
- data/lib/geny/error.rb +20 -0
- data/lib/geny/registry.rb +71 -0
- data/lib/geny/version.rb +3 -0
- data/lib/geny.rb +4 -0
- metadata +190 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
require "tty-prompt"
|
2
|
+
|
3
|
+
module Geny
|
4
|
+
module Actions
|
5
|
+
# Utillities for printing to the console.
|
6
|
+
# @see https://rubydoc.info/github/piotrmurach/tty-prompt/TTY/Prompt TTY::Prompt
|
7
|
+
class UI < TTY::Prompt
|
8
|
+
attr_reader :color
|
9
|
+
|
10
|
+
# Create a new UI
|
11
|
+
# @param color [Pastel]
|
12
|
+
def initialize(color:, **opts)
|
13
|
+
super(opts)
|
14
|
+
@color = color
|
15
|
+
end
|
16
|
+
|
17
|
+
# Print a heading
|
18
|
+
# @param message [String]
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# ui.heading "Files"
|
22
|
+
def heading(message)
|
23
|
+
say "#{@color.dim("==")} #{@color.bold(message)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Print a status
|
27
|
+
# @param label [String]
|
28
|
+
# @param message [String]
|
29
|
+
# @param color [Symbol]
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# ui.status "create", "hello.txt"
|
33
|
+
# ui.status "remove", "hello.txt", color: :red
|
34
|
+
def status(label, message, color: :green)
|
35
|
+
say "#{@color.send(color, label.rjust(12))} #{message}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Print an error
|
39
|
+
# @param message [String]
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# ui.error "the world is ending"
|
43
|
+
def error(message)
|
44
|
+
stderr.puts "#{@color.red("ERROR:")} #{message}"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Print and error and abort
|
48
|
+
# @param message [String]
|
49
|
+
# @raise [SystemExit]
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# ui.abort! "command failed, exiting"
|
53
|
+
def abort!(message)
|
54
|
+
error(message)
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/geny/cli.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require "argy"
|
2
|
+
require "geny"
|
3
|
+
require "geny/registry"
|
4
|
+
require "geny/version"
|
5
|
+
require "geny/actions/ui"
|
6
|
+
|
7
|
+
module Geny
|
8
|
+
class CLI
|
9
|
+
# The registry used for locating commands
|
10
|
+
# @return [Registry]
|
11
|
+
attr_reader :registry
|
12
|
+
|
13
|
+
# The version of your program
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :version
|
16
|
+
|
17
|
+
# The name of your program
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :program_name
|
20
|
+
|
21
|
+
# A description for your program
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :description
|
24
|
+
|
25
|
+
# The column width for help information
|
26
|
+
# @return [Integer]
|
27
|
+
attr_reader :column
|
28
|
+
|
29
|
+
# Create a new CLI
|
30
|
+
# @param registry [Registry]
|
31
|
+
# @param version [String]
|
32
|
+
# @param program_name [String]
|
33
|
+
# @param description [String]
|
34
|
+
# @param column [Integer]
|
35
|
+
def initialize(
|
36
|
+
registry: Registry.new,
|
37
|
+
version: VERSION,
|
38
|
+
program_name: "geny",
|
39
|
+
description: nil,
|
40
|
+
column: 20
|
41
|
+
)
|
42
|
+
@registry = registry
|
43
|
+
@version = version
|
44
|
+
@program_name = program_name
|
45
|
+
@description = description
|
46
|
+
@column = column
|
47
|
+
end
|
48
|
+
|
49
|
+
# Parse arguments and invoke a command
|
50
|
+
# @param argv [Array<String>]
|
51
|
+
def run(argv)
|
52
|
+
opts = parser.parse(argv, strategy: :order)
|
53
|
+
help! unless opts.command?
|
54
|
+
|
55
|
+
command = registry.find!(opts.command)
|
56
|
+
command.run(opts.unused_args)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Print an error and abort the program.
|
60
|
+
# @param message [String] error message
|
61
|
+
# @raise [SystemExit]
|
62
|
+
def abort!(message)
|
63
|
+
color = Pastel.new(enabled: $stdout.tty?)
|
64
|
+
ui = Actions::UI.new(color: color)
|
65
|
+
ui.abort!(message)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def parser
|
71
|
+
@parser ||= Argy.new do |o|
|
72
|
+
o.usage "#{program_name} [COMMAND]"
|
73
|
+
o.description description
|
74
|
+
o.argument :command, desc: "generator to run"
|
75
|
+
|
76
|
+
o.on "-v", "--version", "print version and exit" do
|
77
|
+
puts version
|
78
|
+
exit
|
79
|
+
end
|
80
|
+
|
81
|
+
o.on "-h", "--help", "show this help and exit" do
|
82
|
+
help!
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def help!
|
88
|
+
help = parser.help(column: column)
|
89
|
+
puts help
|
90
|
+
puts help.section("COMMANDS")
|
91
|
+
|
92
|
+
registry.scan.each do |cmd|
|
93
|
+
puts help.entry(cmd.name, desc: cmd.description)
|
94
|
+
end
|
95
|
+
|
96
|
+
exit
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/geny/command.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require "geny/dsl"
|
2
|
+
require "geny/context/invoke"
|
3
|
+
|
4
|
+
module Geny
|
5
|
+
class Command
|
6
|
+
# The filename for a generator, relative to the root
|
7
|
+
FILENAME = "generator.rb"
|
8
|
+
|
9
|
+
# The directory where templates are stored, relative to the root
|
10
|
+
TEMPLATES = "templates"
|
11
|
+
|
12
|
+
# The name of the command
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
# The root directory for the command
|
17
|
+
# @return [String]
|
18
|
+
attr_reader :root
|
19
|
+
|
20
|
+
# Create a new command
|
21
|
+
# @param name [String] name of the command
|
22
|
+
# @param root [String] name of the command
|
23
|
+
def initialize(name:, root:)
|
24
|
+
@name = name
|
25
|
+
@root = root
|
26
|
+
end
|
27
|
+
|
28
|
+
# The path where the command is located
|
29
|
+
# @return [String]
|
30
|
+
def file
|
31
|
+
File.join(root, FILENAME)
|
32
|
+
end
|
33
|
+
|
34
|
+
# The path where templates are located
|
35
|
+
# @return [String]
|
36
|
+
def templates_path
|
37
|
+
File.join(root, TEMPLATES)
|
38
|
+
end
|
39
|
+
|
40
|
+
# The command's option parser
|
41
|
+
# @return [Argy::Parser]
|
42
|
+
def parser
|
43
|
+
dsl.parser
|
44
|
+
end
|
45
|
+
|
46
|
+
# The command's helper modules
|
47
|
+
# @return [Array<Module>]
|
48
|
+
def helpers
|
49
|
+
dsl.helpers
|
50
|
+
end
|
51
|
+
|
52
|
+
# The description for a command
|
53
|
+
# @return [String]
|
54
|
+
def description
|
55
|
+
parser.description
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parse command-line options
|
59
|
+
# @param argv [Array<String>]
|
60
|
+
# @return [Argy::Options]
|
61
|
+
def parse(argv)
|
62
|
+
parser.parse(argv)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Parse command-line options and run the command
|
66
|
+
# @param argv [Array<String>]
|
67
|
+
def run(argv)
|
68
|
+
options = parse(argv)
|
69
|
+
invoke!(options.to_h)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Invoke a command with options
|
73
|
+
# @param options [Hash{Symbol => Object}]
|
74
|
+
def invoke(**options)
|
75
|
+
options = parser.default_values.merge(options)
|
76
|
+
parser.validate!(options)
|
77
|
+
invoke!(options)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Defines the behavior of a command. The block is evaluated
|
81
|
+
# within the context of a DSL.
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# command = Commmand.new(name: "foo", root: "path/to/root")
|
85
|
+
# command.define do
|
86
|
+
# parse {}
|
87
|
+
# invoke {}
|
88
|
+
# end
|
89
|
+
def define(&block)
|
90
|
+
@dsl = DSL.new
|
91
|
+
@dsl.instance_eval(&block)
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def invoke!(options)
|
98
|
+
context = Context::Invoke.new(
|
99
|
+
command: self,
|
100
|
+
locals: extend_with_queries(options)
|
101
|
+
)
|
102
|
+
|
103
|
+
context.instance_eval(&dsl.invoke)
|
104
|
+
end
|
105
|
+
|
106
|
+
def extend_with_queries(options)
|
107
|
+
queries = options.map { |k, v| [:"#{k}?", !!v] }
|
108
|
+
options.merge(queries.to_h)
|
109
|
+
end
|
110
|
+
|
111
|
+
def dsl
|
112
|
+
return @dsl if @dsl
|
113
|
+
|
114
|
+
@dsl = DSL.new
|
115
|
+
@dsl.instance_eval File.read(file)
|
116
|
+
@dsl
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Geny
|
2
|
+
module Context
|
3
|
+
# @api private
|
4
|
+
class Base
|
5
|
+
attr_reader :command, :locals
|
6
|
+
|
7
|
+
def initialize(command:, locals: {})
|
8
|
+
@locals = locals
|
9
|
+
@command = command
|
10
|
+
@command.helpers.each { |h| extend h }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def respond_to_missing?(meth, *)
|
16
|
+
locals.key?(meth) || super
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(meth, *args)
|
20
|
+
return super unless locals.key?(meth)
|
21
|
+
|
22
|
+
unless args.empty?
|
23
|
+
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 0)"
|
24
|
+
end
|
25
|
+
|
26
|
+
locals[meth]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "pastel"
|
2
|
+
require "geny/context/base"
|
3
|
+
require "geny/actions/ui"
|
4
|
+
require "geny/actions/git"
|
5
|
+
require "geny/actions/shell"
|
6
|
+
require "geny/actions/files"
|
7
|
+
require "geny/actions/templates"
|
8
|
+
|
9
|
+
module Geny
|
10
|
+
module Context
|
11
|
+
# The `invoke` behavior for all commands is evaluated in
|
12
|
+
# the context of this class. All methods that are defined
|
13
|
+
# here are available inside `invoke`.
|
14
|
+
class Invoke < Base
|
15
|
+
# A utility for colored output
|
16
|
+
# @return [Pastel]
|
17
|
+
# @see https://github.com/piotrmurach/pastel
|
18
|
+
def color
|
19
|
+
Pastel.new(enabled: $stdout.tty?)
|
20
|
+
end
|
21
|
+
|
22
|
+
# A utility for printing messages to the console
|
23
|
+
# @return [Actions::UI]
|
24
|
+
def ui
|
25
|
+
Actions::UI.new(color: color)
|
26
|
+
end
|
27
|
+
|
28
|
+
# A utility for interacting with files
|
29
|
+
# @return [Actions::Files]
|
30
|
+
def files
|
31
|
+
Actions::Files.new
|
32
|
+
end
|
33
|
+
|
34
|
+
# A utility for running shell commands
|
35
|
+
# @return [Actions::Shell]
|
36
|
+
def shell
|
37
|
+
Actions::Shell.new(ui: ui)
|
38
|
+
end
|
39
|
+
|
40
|
+
# A utility for interacting with git repositories
|
41
|
+
# @return [Actions::Git]
|
42
|
+
def git
|
43
|
+
Actions::Git.new(shell: shell)
|
44
|
+
end
|
45
|
+
|
46
|
+
# A utility for rendering and copying templates
|
47
|
+
# @return [Actions::Templates]
|
48
|
+
def templates
|
49
|
+
Actions::Templates.new(
|
50
|
+
root: command.templates_path,
|
51
|
+
view: View.new(command: command, locals: locals)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "geny/context/base"
|
2
|
+
|
3
|
+
module Geny
|
4
|
+
module Context
|
5
|
+
# All templates are evaluated in the context of a View.
|
6
|
+
# All command-line options, locals, and helper methods
|
7
|
+
# wil be available in templates.
|
8
|
+
class View < Base
|
9
|
+
# @private
|
10
|
+
def merge(updates)
|
11
|
+
View.new(command: command, locals: locals.merge(updates))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/geny/dsl.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require "argy"
|
2
|
+
require "pathname"
|
3
|
+
|
4
|
+
module Geny
|
5
|
+
# The top-level command file is evaulated in the context
|
6
|
+
# of this class.
|
7
|
+
class DSL
|
8
|
+
# @private
|
9
|
+
def initialize
|
10
|
+
@helpers = []
|
11
|
+
@invoke = proc { warn "I don't know what to do!" }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Define arguments and options that the command accepts.
|
15
|
+
# The block is evaluated in the context of an {https://rubydoc.info/github/rzane/argy/Argy/Parser Argy::Parser}.
|
16
|
+
def parse(&block)
|
17
|
+
parser.instance_eval(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Define the behavior for when the command runs. The block is
|
21
|
+
# evaluated in the context of a {Context::Invoke}.
|
22
|
+
def invoke(&block)
|
23
|
+
@invoke = block if block_given?
|
24
|
+
@invoke
|
25
|
+
end
|
26
|
+
|
27
|
+
# Define helper methods. These methods are available within
|
28
|
+
# the {#invoke} block and all templates.
|
29
|
+
def helpers(&block)
|
30
|
+
@helpers << Module.new(&block) if block_given?
|
31
|
+
@helpers
|
32
|
+
end
|
33
|
+
|
34
|
+
# @private
|
35
|
+
def parser
|
36
|
+
@parser ||= Argy.new do |o|
|
37
|
+
o.on "-h", "--help", "show this help and exit" do
|
38
|
+
puts o.help
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/geny/error.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Geny
|
2
|
+
# A base class for all errors
|
3
|
+
class Error < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# Raised when a command cannot be found.
|
7
|
+
class NotFoundError < Error
|
8
|
+
end
|
9
|
+
|
10
|
+
# Raised when a shell command exits with a non-zero status.
|
11
|
+
class ExitError < Error
|
12
|
+
attr_reader :code, :command
|
13
|
+
|
14
|
+
def initialize(code:, command:)
|
15
|
+
@code = code
|
16
|
+
@command = command
|
17
|
+
super "Command `#{command}` failed (exit code: #{code})"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "geny/error"
|
3
|
+
require "geny/command"
|
4
|
+
|
5
|
+
module Geny
|
6
|
+
class Registry
|
7
|
+
# The default load path. By default, Geny
|
8
|
+
# will search
|
9
|
+
LOAD_PATH = [
|
10
|
+
File.join(Dir.pwd, ".geny"),
|
11
|
+
*ENV.fetch("CODE_HEN_PATH", "").split(":"),
|
12
|
+
File.expand_path("../generators", __dir__)
|
13
|
+
]
|
14
|
+
|
15
|
+
# The directories to search for commands in
|
16
|
+
# @return [Array<String>]
|
17
|
+
attr_reader :load_path
|
18
|
+
|
19
|
+
# Create a new registry
|
20
|
+
# @param load_path [Array<String>]
|
21
|
+
def initialize(load_path: LOAD_PATH)
|
22
|
+
@load_path = load_path
|
23
|
+
end
|
24
|
+
|
25
|
+
# Iterate over all load paths and find all commands
|
26
|
+
# @return [Array<Command>]
|
27
|
+
def scan
|
28
|
+
glob = File.join("**", Command::FILENAME)
|
29
|
+
|
30
|
+
commands = load_path.flat_map do |path|
|
31
|
+
path = Pathname.new(path)
|
32
|
+
|
33
|
+
path.glob(glob).map do |file|
|
34
|
+
root = file.dirname
|
35
|
+
name = root.relative_path_from(path)
|
36
|
+
name = name.to_s.tr(File::SEPARATOR, ":")
|
37
|
+
Command.new(name: name, root: root.to_s)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
commands.sort_by(&:name)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Find a command by name
|
45
|
+
# @param name [String] name of the command
|
46
|
+
# @return [Command,nil]
|
47
|
+
def find(name)
|
48
|
+
load_path.each do |path|
|
49
|
+
file = File.join(path, *name.split(":"), Command::FILENAME)
|
50
|
+
root = File.dirname(file)
|
51
|
+
return Command.new(name: name, root: root) if File.exist?(file)
|
52
|
+
end
|
53
|
+
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# Find a command by name or raise an error
|
58
|
+
# @param name [String] name of the command
|
59
|
+
# @raise [NotFoundError] when the command is not found
|
60
|
+
# @return [Command]
|
61
|
+
def find!(name)
|
62
|
+
find(name) || command_not_found!(name)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def command_not_found!(name)
|
68
|
+
raise NotFoundError, "There doesn't appear to be a generator named '#{name}'"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/geny/version.rb
ADDED
data/lib/geny.rb
ADDED