geny 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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