ergane 0.0.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f27bb356ea374b87bf0a12ad73bc945e518dee66aa18797b5f9b06a197de896
4
- data.tar.gz: 2de58095dd7014c321d13fcac71f9d16019db72cd6feb0896bafea5f9ae19d6e
3
+ metadata.gz: 53b7a242e903c28ff9ddb5f33ee399c044fd618f0420cd3868cecb02c4e277e6
4
+ data.tar.gz: 9f8e7b16cc0f64f3547b98e41d973f4b90528215e516873600eb6e953bd2141a
5
5
  SHA512:
6
- metadata.gz: 3a3431e9dd63b745dc3765a83c98c0d3ed3d60837373d12b69371f1df05e5718be8b2fd5007808e4383ba8d77c0fd6997ba7dc867e317b5b598fc80921429161
7
- data.tar.gz: fa7972b5a4019cff455ca2f0631181cb3ce49192a56ee01eb549c22827fbc8d41b77cc86d089178d04e2b63474626faab18dd3acbe45be12aedaec5a37254955
6
+ metadata.gz: fdcf833f1190263dc9f37f470c03d7d150e566175d927e2fec39c036a22111fca8b59c7af4d64a1c1c66af1a455eecb257f66cf2c5276f0f672a513eda123e9a
7
+ data.tar.gz: c85a0eb19d99c6c5424e40655a6c64f33591e04a0d11d576076e2e9a03d2e9ed4f8b26080c21b052145fdff3097e4bd01687226e31abaf84f77013e155954dd9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
+ # CHANGELOG
2
+
1
3
  ## [Unreleased]
2
4
 
3
- ## [0.1.0] - 2023-04-26
5
+ ## [0.1.0] - 2026-05-25
6
+
7
+ First release of the rewritten framework. A near-complete rewrite of the
8
+ original `0.0.1` proof of concept.
9
+
10
+ ### Added
11
+ - Dual DSL: class-based and block-based command definitions, both producing the same command tree
12
+ - Zeitwerk autoloading
13
+ - Recursive subcommand resolution via Runner
14
+ - Tool base class with auto-created command base (`MyTool::Command`)
15
+ - Custom `command_class` for shared command behavior
16
+ - Abstract commands for grouping shared options
17
+ - Colorized help output with box-drawing characters
18
+ - Did-you-mean suggestions for unknown commands (Levenshtein)
19
+ - `--help` and `--version` flag handling
20
+ - Options, flags, and positional arguments, with optional values for options
21
+ - Path abbreviation registry (`Ergane.paths` / `PathRegistry`): collapses registered prefixes (default `$HOME` → `~`), longest-prefix-wins and boundary-safe; commands call `abbreviate_path`
22
+ - Interactive output helpers: `Ergane::Formatter.confirm?` and `Ergane::Formatter.time_ago`
23
+ - Core extensions (`blank?`, `present?`, `try`, `underscore`, `demodulize`, `Array.wrap`, `Hash#&`)
24
+ - `OptionParser#order_recognized!` for multi-level flag passthrough
25
+
26
+ ## [0.0.1] - 2023-05-08
4
27
 
5
28
  - Initial release
data/LICENSE CHANGED
@@ -1,21 +1,22 @@
1
- The MIT License (MIT)
1
+ Copyright (c) 2026 Twilight Coders, LLC
2
2
 
3
- Copyright (c) 2023 Dale Stevens
3
+ MIT License
4
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:
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
11
12
 
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
14
15
 
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
21
- THE SOFTWARE.
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,18 +1,267 @@
1
+ <img src="https://github.com/TwilightCoders/ergane/blob/main/media/ergane.png?raw=true" alt="Athena Ergane" width="400" style="float: right"/>
2
+
1
3
  # Ergane
2
- **The patron goddess of craftsmen and artisans**
3
4
 
4
- Library for creating lightweight, yet powerful CLI tools in Ruby.
5
- Emphasis and priority on load speed and flexibility.
5
+ [![Version](https://img.shields.io/gem/v/ergane.svg)](https://rubygems.org/gems/ergane)
6
+ [![CI](https://github.com/TwilightCoders/ergane/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/TwilightCoders/ergane/actions/workflows/ci.yml)
7
+ [![Code Coverage](https://qlty.sh/badges/43c39f23-168e-475f-a9fd-1754eaca83e6/coverage.svg)](https://qlty.sh/gh/TwilightCoders/projects/ergane)
8
+ [![Maintainability](https://qlty.sh/badges/43c39f23-168e-475f-a9fd-1754eaca83e6/maintainability.svg)](https://qlty.sh/gh/TwilightCoders/projects/ergane)
9
+
10
+ A lightweight, powerful CLI framework for Ruby. Define commands using class inheritance or block DSL — both produce the same command tree.
11
+
12
+ An alternative to other similar utilities with a cleaner, more Ruby-native design.
13
+
14
+ Named after [Athena Ergane](https://en.wikipedia.org/wiki/Athena#Ergane), patron of craftsmen and toolmakers.
6
15
 
7
16
  ## Requirements
8
- Ruby 2.7+
9
17
 
10
- May work with older versions.
18
+ Ruby 3.1+
19
+
20
+ ## Installation
21
+
22
+ ```ruby
23
+ gem "ergane"
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```ruby
29
+ require "ergane"
30
+
31
+ class MyCLI < Ergane::Tool
32
+ tool_name :mycli
33
+ version "1.0.0"
34
+ description "My awesome CLI tool"
35
+
36
+ command :greet do
37
+ description "Say hello"
38
+ option :name, String, short: :n, default: "world"
39
+
40
+ run { puts "Hello #{options[:name]}!" }
41
+ end
42
+ end
43
+
44
+ MyCLI.start(ARGV)
45
+ ```
46
+
47
+ ```
48
+ $ mycli greet -n 'Johnny Appleseed'
49
+ Hello Johnny Appleseed!
50
+
51
+ $ mycli --help
52
+ My awesome CLI tool
53
+
54
+ Version: 1.0.0
55
+
56
+ Usage: mycli [options] [subcommand]
57
+
58
+ Subcommands:
59
+ greet Say hello
60
+
61
+ $ mycli greet --help
62
+ Say hello
63
+
64
+ Usage: greet [options]
65
+
66
+ Options:
67
+ -n, --name=VALUE (default: world)
68
+ ```
69
+
70
+ ## Defining Commands
71
+
72
+ Ergane supports two equivalent styles for defining commands. Both produce the same underlying `Command` subclass.
73
+
74
+ ### Block-based DSL
75
+
76
+ Best for quick, self-contained commands:
77
+
78
+ ```ruby
79
+ class MyCLI < Ergane::Tool
80
+ tool_name :mycli
81
+ version "1.0.0"
82
+
83
+ command :deploy do
84
+ description "Deploy the application"
85
+ option :env, String, short: :e, default: "staging"
86
+ flag :force, short: :f, description: "Skip confirmation"
87
+
88
+ run do |*targets|
89
+ puts "Deploying #{targets.join(', ')} to #{options[:env]}"
90
+ puts "Force mode!" if options[:force]
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Class-based
97
+
98
+ Best for complex commands that need helper methods, mixins, or testing in isolation.
99
+
100
+ Defining a Tool automatically creates a `MyCLI::Command` base class for your commands to inherit from:
101
+
102
+ ```ruby
103
+ class Deploy < MyCLI::Command
104
+ self.command_name = :deploy
105
+ description "Deploy the application"
106
+ option :env, String, short: :e, default: "staging"
107
+ flag :force, short: :f, description: "Skip confirmation"
108
+
109
+ def run(*targets)
110
+ puts "Deploying #{targets.join(', ')} to #{options[:env]}"
111
+ puts "Force mode!" if options[:force]
112
+ end
113
+
114
+ private
115
+
116
+ def confirm?
117
+ return true if options[:force]
118
+ # ...
119
+ end
120
+ end
121
+ ```
122
+
123
+ ### Nested Subcommands
124
+
125
+ Both styles support nesting to arbitrary depth:
126
+
127
+ ```ruby
128
+ class MyCLI < Ergane::Tool
129
+ tool_name :mycli
130
+ version "1.0.0"
131
+
132
+ command :server do
133
+ description "Server management"
134
+
135
+ command :start do
136
+ description "Start the server"
137
+ option :port, String, short: :p, default: "3000"
138
+
139
+ run { puts "Starting on port #{options[:port]}" }
140
+ end
141
+
142
+ command :stop do
143
+ description "Stop the server"
144
+ run { puts "Stopping server" }
145
+ end
146
+ end
147
+ end
148
+ ```
149
+
150
+ ```
151
+ $ mycli server start -p 8080
152
+ Starting on port 8080
153
+ ```
154
+
155
+ ## Options and Flags
156
+
157
+ ```ruby
158
+ # Typed option (requires a value)
159
+ option :env, String, short: :e, description: "Target environment", default: "staging"
160
+
161
+ # Boolean flag (no value, defaults to false)
162
+ flag :verbose, short: :v, description: "Enable verbose output"
163
+
164
+ # Positional argument
165
+ argument :target, description: "Deploy target"
166
+ ```
167
+
168
+ Options are accessed via the `options` hash:
169
+
170
+ ```ruby
171
+ run do |*args|
172
+ puts options[:env] # => "staging"
173
+ puts options[:verbose] # => false
174
+ end
175
+ ```
176
+
177
+ ## Loading Commands from Files
178
+
179
+ For larger CLIs, organize commands in separate files:
180
+
181
+ ```ruby
182
+ class MyCLI < Ergane::Tool
183
+ tool_name :mycli
184
+ version "1.0.0"
185
+
186
+ load_commands(File.expand_path("commands/**/*.rb", __dir__))
187
+ end
188
+ ```
189
+
190
+ Each file defines a Command subclass that auto-registers via Ruby's `inherited` hook:
191
+
192
+ ```ruby
193
+ # commands/deploy.rb
194
+ class Deploy < MyCLI::Command
195
+ self.command_name = :deploy
196
+ description "Deploy the application"
197
+
198
+ def run(*targets)
199
+ # ...
200
+ end
201
+ end
202
+ ```
203
+
204
+ ## Custom Command Base
205
+
206
+ For shared behavior across all commands, provide your own base class:
207
+
208
+ ```ruby
209
+ class MyBaseCommand < Ergane::Command
210
+ self.abstract_class = true
211
+ flag :verbose, short: :v, description: "Verbose output"
212
+
213
+ def log(msg)
214
+ puts msg if options[:verbose]
215
+ end
216
+ end
217
+
218
+ class MyCLI < Ergane::Tool
219
+ tool_name :mycli
220
+ version "1.0.0"
221
+ command_class MyBaseCommand
222
+ end
223
+
224
+ class Deploy < MyBaseCommand
225
+ self.command_name = :deploy
226
+ description "Deploy the application"
227
+
228
+ def run(*targets)
229
+ log "Deploying #{targets.join(', ')}"
230
+ end
231
+ end
232
+ ```
233
+
234
+ ## Abstract Commands
235
+
236
+ Group related commands with shared options:
237
+
238
+ ```ruby
239
+ class DatabaseCommand < MyCLI::Command
240
+ self.abstract_class = true
241
+ option :database, String, short: :d, default: "primary"
242
+ end
243
+
244
+ class Migrate < DatabaseCommand
245
+ self.command_name = :migrate
246
+ description "Run migrations"
247
+
248
+ def run
249
+ puts "Migrating #{options[:database]}"
250
+ end
251
+ end
252
+ ```
253
+
254
+ ## Development
255
+
256
+ After checking out the repo, run `bundle` to install dependencies. Then, run `bundle exec rspec` to run the tests.
11
257
 
12
258
  ## Contributing
13
259
 
14
- 1. Fork it ( https://github.com/TwilightCoders/ergane/fork )
15
- 2. Create your feature branch (`git checkout -b my-new-feature`)
16
- 3. Commit your changes (`git commit -am 'Add some feature'`)
17
- 4. Push to the branch (`git push origin my-new-feature`)
18
- 5. Create a new Pull Request
260
+ Bug reports and pull requests are welcome on GitHub at
261
+ <https://github.com/TwilightCoders/ergane>. This project is intended to be a safe,
262
+ welcoming space for collaboration, and contributors are expected to adhere to the
263
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct.
264
+
265
+ ## License
266
+
267
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ergane
4
+ class ArgumentDefinition
5
+ attr_reader :name, :type, :description, :required, :default
6
+
7
+ def initialize(name, type = String, description: nil, required: true, default: nil)
8
+ @name = name.to_sym
9
+ @type = type
10
+ @description = description
11
+ @required = required
12
+ @default = default
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ergane
4
+ class Command
5
+ include Concerns::Inheritance
6
+ include Concerns::OptionHandling
7
+ extend DSL::CommandDSL
8
+
9
+ self.abstract_class = true
10
+
11
+ class << self
12
+ def command_name=(name)
13
+ @command_name = name&.to_sym
14
+ return unless @command_name
15
+
16
+ parent = superclass
17
+ if parent.respond_to?(:tool) && parent.abstract_class?
18
+ parent.inherited_command_name_set(self)
19
+ else
20
+ register_subcommand(self)
21
+ end
22
+ end
23
+
24
+ def command_name
25
+ @command_name || derive_command_name
26
+ end
27
+
28
+ def terms
29
+ [command_name, *aliases].compact.uniq
30
+ end
31
+
32
+ def subcommands
33
+ @subcommands ||= {}
34
+ end
35
+
36
+ def inherited(subclass)
37
+ super
38
+ subclass.instance_variable_set(:@option_definitions, option_definitions.dup)
39
+ subclass.instance_variable_set(:@argument_definitions, argument_definitions.dup)
40
+ subclass.instance_variable_set(:@subcommands, {})
41
+ register_subcommand(subclass)
42
+ end
43
+
44
+ private
45
+
46
+ def derive_command_name
47
+ return nil if self == Command || abstract_class?
48
+ base = name&.demodulize
49
+ return nil unless base
50
+ base.sub(/Command$/, "").underscore.to_sym
51
+ end
52
+
53
+ def register_subcommand(subclass)
54
+ parent = subclass.superclass
55
+ return if parent == Command || parent.abstract_class?
56
+ cmd_name = subclass.command_name
57
+ return unless cmd_name
58
+ parent.subcommands[cmd_name] = subclass
59
+ end
60
+ end
61
+
62
+ attr_reader :options
63
+
64
+ def abbreviate_path(path)
65
+ Ergane.paths.abbreviate(path)
66
+ end
67
+
68
+ def initialize(argv = [])
69
+ @options = self.class.build_default_options
70
+ @argv = parse_options(argv.dup)
71
+ end
72
+
73
+ def args
74
+ @argv
75
+ end
76
+
77
+ def run(*run_args)
78
+ if self.class.subcommands.any?
79
+ $stdout.puts HelpFormatter.new(self.class).format
80
+ else
81
+ raise AbstractCommand, "#{self.class.name}#run is not implemented"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ergane
4
+ module Concerns
5
+ module Inheritance
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def abstract_class=(value)
12
+ @abstract_class = value
13
+ # Unregister from parent's subcommands when marked abstract
14
+ if value && respond_to?(:command_name) && self < Ergane::Command
15
+ parent = superclass
16
+ parent.subcommands.delete(command_name) if parent.respond_to?(:subcommands)
17
+ end
18
+ end
19
+
20
+ def abstract_class?
21
+ @abstract_class == true
22
+ end
23
+
24
+ # Returns the class descending directly from Ergane::Command, or
25
+ # an abstract class, if any, in the inheritance hierarchy.
26
+ #
27
+ # If A extends Command, A.base_class returns A.
28
+ # If B < A through some hierarchy, B.base_class returns A.
29
+ # If A is abstract, both B.base_class and C.base_class return B.
30
+ def base_class
31
+ unless self < Ergane::Command
32
+ raise Ergane::Error, "#{name} doesn't belong in a hierarchy descending from Ergane::Command"
33
+ end
34
+
35
+ if superclass == Ergane::Command || superclass.abstract_class?
36
+ self
37
+ else
38
+ superclass.base_class
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ergane
4
+ module Concerns
5
+ module OptionHandling
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def option_definitions
12
+ @option_definitions ||= {}
13
+ end
14
+
15
+ def argument_definitions
16
+ @argument_definitions ||= []
17
+ end
18
+
19
+ def build_default_options
20
+ option_definitions.each_with_object({}) do |(name, defn), hash|
21
+ hash[name] = defn.default_value
22
+ end
23
+ end
24
+
25
+ def build_option_parser(store)
26
+ ::OptionParser.new do |parser|
27
+ option_definitions.each_value do |defn|
28
+ defn.attach(parser, store)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def parse_options(argv)
37
+ parser = self.class.build_option_parser(@options)
38
+ parser.order_recognized!(argv)
39
+ argv
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Array
4
+ def self.wrap(object)
5
+ if object.nil?
6
+ []
7
+ elsif object.respond_to?(:to_ary)
8
+ object.to_ary || [object]
9
+ else
10
+ [object]
11
+ end
12
+ end unless respond_to?(:wrap)
13
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hash
4
+ # Hash intersection by keys. Values come from the receiver.
5
+ # {a: 1, b: 2} & {a: 10, c: 3} # => {a: 1}
6
+ def &(other)
7
+ shared = keys & other.keys
8
+ shared.each_with_object({}) { |k, h| h[k] = self[k] }
9
+ end unless method_defined?(:&)
10
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Object
4
+ def blank?
5
+ respond_to?(:empty?) ? !!empty? : !self
6
+ end unless method_defined?(:blank?)
7
+
8
+ def present?
9
+ !blank?
10
+ end unless method_defined?(:present?)
11
+
12
+ def try(method_name = nil, *args, &block)
13
+ if method_name
14
+ respond_to?(method_name) ? public_send(method_name, *args, &block) : nil
15
+ elsif block
16
+ yield self
17
+ end
18
+ end unless method_defined?(:try)
19
+ end
20
+
21
+ class NilClass
22
+ def blank? = true unless method_defined?(:blank?)
23
+ def try(*) = nil unless method_defined?(:try)
24
+ end
25
+
26
+ class FalseClass
27
+ def blank? = true unless method_defined?(:blank?)
28
+ end
29
+
30
+ class TrueClass
31
+ def blank? = false unless method_defined?(:blank?)
32
+ end
33
+
34
+ class String
35
+ # Override Object#blank? to also catch whitespace-only strings
36
+ def blank?
37
+ empty? || /\A[[:space:]]*\z/.match?(self)
38
+ end
39
+ end
40
+
41
+ class Numeric
42
+ def blank? = false unless method_defined?(:blank?)
43
+ end
@@ -1,6 +1,8 @@
1
- class OptionParser
1
+ # frozen_string_literal: true
2
2
 
3
+ class OptionParser
3
4
  # Like order!, but leave any unrecognized --switches alone
5
+ # instead of raising InvalidOption.
4
6
  def order_recognized!(args)
5
7
  extra_opts = []
6
8
  begin
@@ -11,5 +13,4 @@ class OptionParser
11
13
  end
12
14
  args[0, 0] = extra_opts
13
15
  end
14
-
15
16
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ # "DeployCommand" -> "deploy_command"
5
+ # "Ergane::Deploy" -> "ergane/deploy"
6
+ def underscore
7
+ gsub("::", "/")
8
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
9
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
10
+ .tr("-", "_")
11
+ .downcase
12
+ end unless method_defined?(:underscore)
13
+
14
+ # "Ergane::Deploy" -> "Deploy"
15
+ def demodulize
16
+ if (index = rindex("::"))
17
+ self[(index + 2)..]
18
+ else
19
+ dup
20
+ end
21
+ end unless method_defined?(:demodulize)
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ergane
4
+ module DSL
5
+ class BlockDSL
6
+ def initialize(command_class)
7
+ @command_class = command_class
8
+ end
9
+
10
+ def run(&block)
11
+ @command_class.define_method(:run) { |*args| instance_exec(*args, &block) }
12
+ end
13
+
14
+ private
15
+
16
+ def respond_to_missing?(name, include_private = false)
17
+ @command_class.respond_to?(name) || super
18
+ end
19
+
20
+ def method_missing(name, *args, **opts, &block)
21
+ if @command_class.respond_to?(name)
22
+ @command_class.public_send(name, *args, **opts, &block)
23
+ else
24
+ super
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end