hanami-cli 2.0.0.rc1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.yardopts +3 -0
  4. data/CHANGELOG.md +12 -0
  5. data/Gemfile +3 -4
  6. data/exe/hanami +6 -1
  7. data/hanami-cli.gemspec +3 -2
  8. data/lib/hanami/cli/bundler.rb +95 -5
  9. data/lib/hanami/cli/command.rb +41 -0
  10. data/lib/hanami/cli/commands/app/command.rb +63 -2
  11. data/lib/hanami/cli/commands/app/console.rb +7 -3
  12. data/lib/hanami/cli/commands/app/db/create.rb +2 -0
  13. data/lib/hanami/cli/commands/app/db/create_migration.rb +3 -0
  14. data/lib/hanami/cli/commands/app/db/drop.rb +2 -0
  15. data/lib/hanami/cli/commands/app/db/migrate.rb +2 -0
  16. data/lib/hanami/cli/commands/app/db/reset.rb +2 -0
  17. data/lib/hanami/cli/commands/app/db/rollback.rb +2 -0
  18. data/lib/hanami/cli/commands/app/db/sample_data.rb +2 -0
  19. data/lib/hanami/cli/commands/app/db/seed.rb +2 -0
  20. data/lib/hanami/cli/commands/app/db/setup.rb +2 -0
  21. data/lib/hanami/cli/commands/app/db/structure/dump.rb +3 -0
  22. data/lib/hanami/cli/commands/app/db/utils/database.rb +155 -0
  23. data/lib/hanami/cli/commands/app/db/utils/database_config.rb +60 -0
  24. data/lib/hanami/cli/commands/app/db/utils/mysql.rb +33 -0
  25. data/lib/hanami/cli/commands/app/db/utils/postgres.rb +64 -0
  26. data/lib/hanami/cli/commands/app/db/utils/sqlite.rb +45 -0
  27. data/lib/hanami/cli/commands/app/db/version.rb +2 -0
  28. data/lib/hanami/cli/commands/app/generate/action.rb +10 -3
  29. data/lib/hanami/cli/commands/app/generate/slice.rb +9 -6
  30. data/lib/hanami/cli/commands/app/generate.rb +2 -0
  31. data/lib/hanami/cli/commands/app/install.rb +13 -2
  32. data/lib/hanami/cli/commands/app/middleware.rb +7 -1
  33. data/lib/hanami/cli/commands/app/routes.rb +8 -0
  34. data/lib/hanami/cli/commands/app/server.rb +10 -0
  35. data/lib/hanami/cli/commands/app/version.rb +4 -2
  36. data/lib/hanami/cli/commands/app.rb +6 -19
  37. data/lib/hanami/cli/commands/gem/new.rb +12 -13
  38. data/lib/hanami/cli/commands/gem/version.rb +4 -2
  39. data/lib/hanami/cli/commands/gem.rb +8 -5
  40. data/lib/hanami/cli/commands.rb +19 -3
  41. data/lib/hanami/cli/errors.rb +65 -0
  42. data/lib/hanami/cli/files.rb +10 -0
  43. data/lib/hanami/cli/generators/app/action.rb +11 -5
  44. data/lib/hanami/cli/generators/app/action_context.rb +22 -0
  45. data/lib/hanami/cli/generators/app/slice.rb +6 -1
  46. data/lib/hanami/cli/generators/app/slice_context.rb +8 -0
  47. data/lib/hanami/cli/generators/context.rb +14 -0
  48. data/lib/hanami/cli/generators/gem/app.rb +9 -5
  49. data/lib/hanami/cli/generators/version.rb +8 -6
  50. data/lib/hanami/cli/middleware_stack_inspector.rb +5 -0
  51. data/lib/hanami/cli/rake_tasks.rb +0 -1
  52. data/lib/hanami/cli/repl/core.rb +12 -4
  53. data/lib/hanami/cli/repl/irb.rb +3 -2
  54. data/lib/hanami/cli/repl/pry.rb +1 -3
  55. data/lib/hanami/cli/server.rb +11 -0
  56. data/lib/hanami/cli/system_call.rb +65 -7
  57. data/lib/hanami/cli/url.rb +9 -2
  58. data/lib/hanami/cli/version.rb +4 -1
  59. data/lib/hanami/cli.rb +26 -3
  60. data/lib/hanami/console/context.rb +4 -2
  61. data/lib/hanami/console/plugins/slice_readers.rb +1 -0
  62. data/lib/hanami-cli.rb +3 -0
  63. metadata +46 -25
  64. data/lib/hanami/cli/command_line.rb +0 -17
  65. data/lib/hanami/cli/commands/db/utils/database.rb +0 -133
  66. data/lib/hanami/cli/commands/db/utils/database_config.rb +0 -48
  67. data/lib/hanami/cli/commands/db/utils/mysql.rb +0 -27
  68. data/lib/hanami/cli/commands/db/utils/postgres.rb +0 -54
  69. data/lib/hanami/cli/commands/db/utils/sqlite.rb +0 -37
  70. data/lib/hanami/cli/error.rb +0 -8
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "database_config"
4
+
5
+ module Hanami
6
+ module CLI
7
+ module Commands
8
+ module App
9
+ module DB
10
+ module Utils
11
+ # @api private
12
+ class Database
13
+ # @api private
14
+ attr_reader :app
15
+
16
+ # @api private
17
+ attr_reader :config
18
+
19
+ # @api private
20
+ SCHEME_MAP = {
21
+ "sqlite" => -> {
22
+ require_relative("sqlite")
23
+ Sqlite
24
+ },
25
+ "postgres" => -> {
26
+ require_relative("postgres")
27
+ Postgres
28
+ },
29
+ "postgresql" => -> {
30
+ require_relative("postgres")
31
+ Postgres
32
+ },
33
+ "mysql" => -> {
34
+ require_relative("mysql")
35
+ Mysql
36
+ }
37
+ }.freeze
38
+
39
+ # @api private
40
+ def self.[](app)
41
+ database_url =
42
+ if app.key?(:settings) && app[:settings].respond_to?(:database_url)
43
+ app[:settings].database_url
44
+ else
45
+ ENV.fetch("DATABASE_URL")
46
+ end
47
+
48
+ config = DatabaseConfig.new(database_url)
49
+
50
+ resolver = SCHEME_MAP.fetch(config.db_type) do
51
+ raise "#{config.db_type} is not a supported db scheme"
52
+ end
53
+
54
+ klass = resolver.()
55
+
56
+ klass.new(app: app, config: config)
57
+ end
58
+
59
+ # @api private
60
+ def initialize(app:, config:)
61
+ @app = app
62
+ @config = config
63
+ end
64
+
65
+ # @api private
66
+ def create_command
67
+ raise Hanami::CLI::NotImplementedError
68
+ end
69
+
70
+ # @api private
71
+ def drop_command
72
+ raise Hanami::CLI::NotImplementedError
73
+ end
74
+
75
+ # @api private
76
+ def dump_command
77
+ raise Hanami::CLI::NotImplementedError
78
+ end
79
+
80
+ # @api private
81
+ def load_command
82
+ raise Hanami::CLI::NotImplementedError
83
+ end
84
+
85
+ # @api private
86
+ def root_path
87
+ app.root
88
+ end
89
+
90
+ # @api private
91
+ def rom_config
92
+ @rom_config ||=
93
+ begin
94
+ app.prepare(:persistence)
95
+ app.container["persistence.config"]
96
+ end
97
+ end
98
+
99
+ # @api private
100
+ def name
101
+ config.db_name
102
+ end
103
+
104
+ # @api private
105
+ def gateway
106
+ rom_config.gateways[:default]
107
+ end
108
+
109
+ # @api private
110
+ def connection
111
+ gateway.connection
112
+ end
113
+
114
+ # @api private
115
+ def run_migrations(**options)
116
+ require "rom/sql"
117
+ ROM::SQL.with_gateway(gateway) do
118
+ migrator.run(options)
119
+ end
120
+ end
121
+
122
+ # @api private
123
+ def migrator
124
+ @migrator ||=
125
+ begin
126
+ require "rom/sql"
127
+ ROM::SQL::Migration::Migrator.new(connection, path: File.join(root_path, "db/migrate"))
128
+ end
129
+ end
130
+
131
+ # @api private
132
+ def applied_migrations
133
+ sequel_migrator.applied_migrations
134
+ end
135
+
136
+ private
137
+
138
+ def sequel_migrator
139
+ @sequel_migrator ||= begin
140
+ require "sequel"
141
+ Sequel.extension :migration
142
+ Sequel::TimestampMigrator.new(migrator.connection, migrations_path, {})
143
+ end
144
+ end
145
+
146
+ def migrations_path
147
+ File.join(root_path, "db/migrate")
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Hanami
6
+ module CLI
7
+ module Commands
8
+ module App
9
+ module DB
10
+ # @api private
11
+ module Utils
12
+ # @api private
13
+ class DatabaseConfig
14
+ # @api private
15
+ attr_reader :uri
16
+
17
+ # @api private
18
+ def initialize(database_url)
19
+ @uri = URI(database_url)
20
+ end
21
+
22
+ # @api private
23
+ def hostname
24
+ uri.hostname
25
+ end
26
+ alias_method :host, :hostname
27
+
28
+ # @api private
29
+ def user
30
+ uri.user
31
+ end
32
+ alias_method :username, :user
33
+
34
+ # @api private
35
+ def password
36
+ uri.password
37
+ end
38
+ alias_method :pass, :password
39
+
40
+ # @api private
41
+ def port
42
+ uri.port
43
+ end
44
+
45
+ # @api private
46
+ def db_name
47
+ @db_name ||= uri.path.gsub(/\A\//, "")
48
+ end
49
+
50
+ # @api private
51
+ def db_type
52
+ uri.scheme
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "database"
4
+
5
+ module Hanami
6
+ module CLI
7
+ module Commands
8
+ module App
9
+ module DB
10
+ module Utils
11
+ # @api private
12
+ class Mysql < Database
13
+ # @api private
14
+ def create_command
15
+ raise Hanami::CLI::NotImplementedError
16
+ end
17
+
18
+ # @api private
19
+ def dump_command
20
+ raise Hanami::CLI::NotImplementedError
21
+ end
22
+
23
+ # @api private
24
+ def load_command
25
+ raise Hanami::CLI::NotImplementedError
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require "open3"
5
+ require_relative "database"
6
+
7
+ module Hanami
8
+ module CLI
9
+ module Commands
10
+ module App
11
+ module DB
12
+ module Utils
13
+ # @api private
14
+ class Postgres < Database
15
+ # @api private
16
+ def create_command
17
+ existing_stdout, status = Open3.capture2(cli_env_vars, "psql -t -c '\\l #{escaped_name}'")
18
+
19
+ return true if status.success? && existing_stdout.include?(escaped_name)
20
+
21
+ system(cli_env_vars, "createdb #{escaped_name}")
22
+ end
23
+
24
+ # @api private
25
+ def drop_command
26
+ system(cli_env_vars, "dropdb #{escaped_name}")
27
+ end
28
+
29
+ # @api private
30
+ def dump_command
31
+ system(cli_env_vars, "pg_dump --schema-only --no-owner #{escaped_name} > #{dump_file}")
32
+ end
33
+
34
+ # @api private
35
+ def load_command
36
+ raise "Not Implemented Yet"
37
+ end
38
+
39
+ # @api private
40
+ def escaped_name
41
+ Shellwords.escape(name)
42
+ end
43
+
44
+ # @api private
45
+ def cli_env_vars
46
+ @cli_env_vars ||= {}.tap do |vars|
47
+ vars["PGHOST"] = config.host.to_s
48
+ vars["PGPORT"] = config.port.to_s if config.port
49
+ vars["PGUSER"] = config.user.to_s if config.user
50
+ vars["PGPASSWORD"] = config.pass.to_s if config.pass
51
+ end
52
+ end
53
+
54
+ # @api private
55
+ def dump_file
56
+ "#{root_path}/db/structure.sql"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "database"
4
+
5
+ module Hanami
6
+ module CLI
7
+ module Commands
8
+ module App
9
+ module DB
10
+ module Utils
11
+ # @api private
12
+ class Sqlite < Database
13
+ # @api private
14
+ def create_command
15
+ rom_config
16
+ true
17
+ end
18
+
19
+ # @api private
20
+ def drop_command
21
+ file_path.unlink
22
+ true
23
+ end
24
+
25
+ # @api private
26
+ def dump_command
27
+ raise Hanami::CLI::NotImplementedError
28
+ end
29
+
30
+ # @api private
31
+ def load_command
32
+ raise Hanami::CLI::NotImplementedError
33
+ end
34
+
35
+ # @api private
36
+ def file_path
37
+ @file_path ||= Pathname("#{root_path}#{config.uri.path}").realpath
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -7,11 +7,13 @@ module Hanami
7
7
  module Commands
8
8
  module App
9
9
  module DB
10
+ # @api private
10
11
  class Version < App::Command
11
12
  desc "Print schema version"
12
13
 
13
14
  option :target, desc: "Target migration number", aliases: ["-t"]
14
15
 
16
+ # @api private
15
17
  def call(target: nil, **) # rubocop:disable Lint/UnusedMethodArgument
16
18
  migration = database.applied_migrations.last
17
19
  version = migration ? File.basename(migration, ".*") : "not available"
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/cli/commands/app/command"
4
- require "hanami/cli/generators/app/action"
5
3
  require "dry/inflector"
6
4
  require "dry/files"
7
5
  require "shellwords"
6
+ require_relative "../../../errors"
8
7
 
9
8
  module Hanami
10
9
  module CLI
11
10
  module Commands
12
11
  module App
13
12
  module Generate
13
+ # @since 2.0.0
14
+ # @api private
14
15
  class Action < App::Command
15
16
  # TODO: ideally the default format should lookup
16
17
  # slice configuration (Action's `default_response_format`)
@@ -44,6 +45,8 @@ module Hanami
44
45
  ]
45
46
  # rubocop:enable Layout/LineLength
46
47
 
48
+ # @since 2.0.0
49
+ # @api private
47
50
  def initialize(fs: Hanami::CLI::Files.new, inflector: Dry::Inflector.new,
48
51
  generator: Generators::App::Action.new(fs: fs, inflector: inflector), **)
49
52
  @generator = generator
@@ -51,17 +54,21 @@ module Hanami
51
54
  end
52
55
 
53
56
  # rubocop:disable Metrics/ParameterLists
57
+
58
+ # @since 2.0.0
59
+ # @api private
54
60
  def call(name:, url: nil, http: nil, format: DEFAULT_FORMAT, skip_view: DEFAULT_SKIP_VIEW, slice: nil, **)
55
61
  slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
56
62
  name = inflector.underscore(Shellwords.shellescape(name))
57
63
  *controller, action = name.split(ACTION_SEPARATOR)
58
64
 
59
65
  if controller.empty?
60
- raise ArgumentError.new("cannot parse controller and action name: `#{name}'\n\texample: users.show")
66
+ raise InvalidActionNameError.new(name)
61
67
  end
62
68
 
63
69
  generator.call(app.namespace, controller, action, url, http, format, skip_view, slice)
64
70
  end
71
+
65
72
  # rubocop:enable Metrics/ParameterLists
66
73
 
67
74
  private
@@ -1,18 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/cli/command"
4
- require "hanami/cli/generators/app/slice"
5
3
  require "dry/inflector"
6
4
  require "dry/files"
7
5
  require "shellwords"
8
- require "hanami/cli/files"
9
- require "hanami/cli/url"
6
+ require_relative "../../../errors"
10
7
 
11
8
  module Hanami
12
9
  module CLI
13
10
  module Commands
14
11
  module App
15
12
  module Generate
13
+ # @since 2.0.0
14
+ # @api private
16
15
  class Slice < Command
17
16
  argument :name, required: true, desc: "The slice name"
18
17
  option :url, required: false, type: :string, desc: "The slice URL prefix"
@@ -22,12 +21,16 @@ module Hanami
22
21
  "users --url=/u # Users slice (/u URL prefix)",
23
22
  ]
24
23
 
24
+ # @since 2.0.0
25
+ # @api private
25
26
  def initialize(fs: Hanami::CLI::Files.new, inflector: Dry::Inflector.new,
26
27
  generator: Generators::App::Slice.new(fs: fs, inflector: inflector), **)
27
28
  @generator = generator
28
29
  super(fs: fs)
29
30
  end
30
31
 
32
+ # @since 2.0.0
33
+ # @api private
31
34
  def call(name:, url: nil, **)
32
35
  require "hanami/setup"
33
36
 
@@ -51,8 +54,8 @@ module Hanami
51
54
 
52
55
  result ||= DEFAULT_URL_PREFIX + name
53
56
  CLI::URL.call(result)
54
- rescue ArgumentError
55
- raise ArgumentError.new("invalid URL prefix: `#{url}'")
57
+ rescue InvalidURLError
58
+ raise InvalidURLPrefixError.new(url)
56
59
  end
57
60
 
58
61
  def valid_url?(url)
@@ -4,6 +4,8 @@ module Hanami
4
4
  module CLI
5
5
  module Commands
6
6
  module App
7
+ # @since 2.0.0
8
+ # @api private
7
9
  module Generate
8
10
  require_relative "./generate/slice"
9
11
  require_relative "./generate/action"
@@ -1,11 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/cli/command"
4
-
5
3
  module Hanami
6
4
  module CLI
7
5
  module Commands
8
6
  module App
7
+ # The `install` command exists to provide third parties a hook for their own installation
8
+ # behaviour to be run as part of `hanami new`.
9
+ #
10
+ # Third parties should register their install commands like so:
11
+ #
12
+ # ```
13
+ # if Hanami::CLI.within_hanami_app?
14
+ # Hanami::CLI.after "install", MyHanamiGem::CLI::Commands::Install
15
+ # end
16
+ # ````
17
+ #
18
+ # @since 2.0.0
19
+ # @api private
9
20
  class Install < Command
10
21
  desc "Install Hanami third-party plugins"
11
22
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "hanami"
4
- require "hanami/cli/middleware_stack_inspector"
5
4
 
6
5
  module Hanami
7
6
  module CLI
@@ -23,10 +22,16 @@ module Hanami
23
22
  # $ bundle exec hanami middleware --with-arguments
24
23
  # / Rack::Session::Cookie args: [{:secret=>"foo"}]
25
24
  # ```
25
+ #
26
+ # @since 2.0.0
27
+ # @api private
26
28
  class Middleware < Hanami::CLI::Command
27
29
  desc "Print app Rack middleware stack"
28
30
 
31
+ # @since 2.0.0
32
+ # @api private
29
33
  DEFAULT_WITH_ARGUMENTS = false
34
+ private_constant :DEFAULT_WITH_ARGUMENTS
30
35
 
31
36
  option :with_arguments, default: DEFAULT_WITH_ARGUMENTS, required: false,
32
37
  desc: "Include inspected arguments", type: :boolean
@@ -36,6 +41,7 @@ module Hanami
36
41
  "middleware --with-arguments # Print app Rack middleware stack, including initialize arguments",
37
42
  ]
38
43
 
44
+ # @since 2.0.0
39
45
  # @api private
40
46
  def call(with_arguments: DEFAULT_WITH_ARGUMENTS)
41
47
  require "hanami/prepare"
@@ -20,10 +20,17 @@ module Hanami
20
20
  # ```
21
21
  # $ bundle exec hanami routes --format=custom_routes_formatter
22
22
  # ```
23
+ #
24
+ # @since 2.0.0
25
+ # @api private
23
26
  class Routes < Hanami::CLI::Command
27
+ # @since 2.0.0
28
+ # @api private
24
29
  DEFAULT_FORMAT = "human_friendly"
25
30
  private_constant :DEFAULT_FORMAT
26
31
 
32
+ # @since 2.0.0
33
+ # @api private
27
34
  VALID_FORMATS = [
28
35
  DEFAULT_FORMAT,
29
36
  "csv"
@@ -42,6 +49,7 @@ module Hanami
42
49
  "routes --format=csv # Print app routes, using CSV format",
43
50
  ]
44
51
 
52
+ # @since 2.0.0
45
53
  # @api private
46
54
  def call(format: DEFAULT_FORMAT, **)
47
55
  require "hanami/router/inspector"
@@ -24,10 +24,17 @@ module Hanami
24
24
  # - All others are always given by the Hanami command.
25
25
  #
26
26
  # Run `bundle exec hanami server -h` to see all the supported options.
27
+ #
28
+ # @since 2.0.0
29
+ # @api private
27
30
  class Server < Command
31
+ # @since 2.0.0
32
+ # @api private
28
33
  DEFAULT_PORT = 2300
29
34
  private_constant :DEFAULT_PORT
30
35
 
36
+ # @since 2.0.0
37
+ # @api private
31
38
  DEFAULT_CONFIG_PATH = "config.ru"
32
39
  private_constant :DEFAULT_CONFIG_PATH
33
40
 
@@ -41,11 +48,14 @@ module Hanami
41
48
  option :debug, default: false, required: false, desc: "Turn on/off debug output", type: :boolean
42
49
  option :warn, default: false, required: false, desc: "Turn on/off warnings", type: :boolean
43
50
 
51
+ # @since 2.0.0
52
+ # @api private
44
53
  def initialize(server: Hanami::CLI::Server.new)
45
54
  super()
46
55
  @server = server
47
56
  end
48
57
 
58
+ # @since 2.0.0
49
59
  # @api private
50
60
  def call(port: DEFAULT_PORT, **kwargs)
51
61
  server.call(port: port, **kwargs)
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/cli/command"
4
-
5
3
  module Hanami
6
4
  module CLI
7
5
  module Commands
8
6
  module App
7
+ # @since 2.0.0
8
+ # @api private
9
9
  class Version < Command
10
10
  desc "Print Hanami app version"
11
11
 
12
+ # @since 2.0.0
13
+ # @api private
12
14
  def call(*)
13
15
  require "hanami/version"
14
16
  out.puts "v#{Hanami::VERSION}"
@@ -3,26 +3,13 @@
3
3
  module Hanami
4
4
  module CLI
5
5
  module Commands
6
+ # Commands made available when the `hanami` CLI is executed within an Hanami app.
7
+ #
8
+ # @api private
9
+ # @since 2.0.0
6
10
  module App
7
- require_relative "app/version"
8
- require_relative "app/install"
9
- require_relative "app/console"
10
- require_relative "app/server"
11
- require_relative "app/routes"
12
- require_relative "app/generate"
13
- require_relative "app/middleware"
14
- # require_relative "app/db/create"
15
- # require_relative "app/db/create_migration"
16
- # require_relative "app/db/drop"
17
- # require_relative "app/db/migrate"
18
- # require_relative "app/db/setup"
19
- # require_relative "app/db/reset"
20
- # require_relative "app/db/rollback"
21
- # require_relative "app/db/sample_data"
22
- # require_relative "app/db/seed"
23
- # require_relative "app/db/structure/dump"
24
- # require_relative "app/db/version"
25
-
11
+ # @since 2.0.0
12
+ # @api private
26
13
  def self.extended(base)
27
14
  base.module_eval do
28
15
  register "version", Commands::App::Version, aliases: ["v", "-v", "--version"]