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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Geny
2
+ VERSION = "0.1.0"
3
+ end
data/lib/geny.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "geny/version"
2
+
3
+ module Geny
4
+ end