smol 1.0.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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +697 -0
- data/Rakefile +12 -0
- data/lib/smol/app.rb +122 -0
- data/lib/smol/app_lookup.rb +13 -0
- data/lib/smol/check.rb +70 -0
- data/lib/smol/check_result.rb +18 -0
- data/lib/smol/cli.rb +119 -0
- data/lib/smol/coercion.rb +18 -0
- data/lib/smol/colors.rb +27 -0
- data/lib/smol/command.rb +292 -0
- data/lib/smol/config.rb +48 -0
- data/lib/smol/config_display.rb +31 -0
- data/lib/smol/input.rb +60 -0
- data/lib/smol/output.rb +105 -0
- data/lib/smol/repl.rb +207 -0
- data/lib/smol/version.rb +3 -0
- data/lib/smol.rb +55 -0
- metadata +92 -0
data/Rakefile
ADDED
data/lib/smol/app.rb
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smol
|
|
4
|
+
class App
|
|
5
|
+
class << self
|
|
6
|
+
def inherited(subclass)
|
|
7
|
+
super
|
|
8
|
+
subclass.instance_variable_set(:@commands, [])
|
|
9
|
+
subclass.instance_variable_set(:@checks, [])
|
|
10
|
+
subclass.instance_variable_set(:@mounts, {})
|
|
11
|
+
|
|
12
|
+
parent_module = find_parent_module(subclass)
|
|
13
|
+
setup_registry_methods(parent_module, subclass) if parent_module
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def banner(text = nil)
|
|
17
|
+
@banner = text if text
|
|
18
|
+
@banner || ""
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cli(enabled = nil)
|
|
22
|
+
@cli_enabled = enabled unless enabled.nil?
|
|
23
|
+
@cli_enabled.nil? ? true : @cli_enabled
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def repl(enabled = nil)
|
|
27
|
+
@repl_enabled = enabled unless enabled.nil?
|
|
28
|
+
@repl_enabled.nil? ? true : @repl_enabled
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def boot(mode = nil)
|
|
32
|
+
@boot_mode = mode if mode
|
|
33
|
+
@boot_mode || :help
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def history_file(path = nil)
|
|
37
|
+
@history_file = path if path
|
|
38
|
+
@history_file
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def config
|
|
42
|
+
@config ||= Config.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def commands
|
|
46
|
+
@commands ||= []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def checks
|
|
50
|
+
@checks ||= []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def mounts
|
|
54
|
+
@mounts ||= {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def mount(app_class, as:)
|
|
58
|
+
mounts[as.to_s] = app_class
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def find_command(name)
|
|
62
|
+
# Check for mounted app prefix (e.g., "admin:users")
|
|
63
|
+
if name.include?(":")
|
|
64
|
+
prefix, sub_name = name.split(":", 2)
|
|
65
|
+
if mounts[prefix]
|
|
66
|
+
return mounts[prefix].find_command(sub_name)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
commands.find { |c| c.matches?(name) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def find_mount(name)
|
|
74
|
+
mounts[name.to_s]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_command(command_class)
|
|
78
|
+
commands << command_class
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def register_check(check_class)
|
|
82
|
+
checks << check_class
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def register(command_class)
|
|
86
|
+
@explicit_registration = true
|
|
87
|
+
commands << command_class
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def explicit_registration?
|
|
91
|
+
@explicit_registration || false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def find_parent_module(subclass)
|
|
97
|
+
parts = subclass.name&.split("::")
|
|
98
|
+
return nil unless parts && parts.size > 1
|
|
99
|
+
|
|
100
|
+
parent_name = parts[0..-2].join("::")
|
|
101
|
+
begin
|
|
102
|
+
Object.const_get(parent_name)
|
|
103
|
+
rescue NameError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def setup_registry_methods(parent_module, app_class)
|
|
109
|
+
unless parent_module.respond_to?(:register_command)
|
|
110
|
+
parent_module.define_singleton_method(:register_command) do |cmd|
|
|
111
|
+
app_class.register_command(cmd)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
unless parent_module.respond_to?(:register_check)
|
|
115
|
+
parent_module.define_singleton_method(:register_check) do |check|
|
|
116
|
+
app_class.register_check(check)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/smol/check.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smol
|
|
4
|
+
class Check
|
|
5
|
+
include AppLookup
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def inherited(subclass)
|
|
9
|
+
super
|
|
10
|
+
register_to_app(subclass)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def register_to_app(subclass)
|
|
16
|
+
parts = subclass.name&.split("::")
|
|
17
|
+
return unless parts && parts.size > 1
|
|
18
|
+
|
|
19
|
+
parts[0..-2].size.times do |i|
|
|
20
|
+
candidate_name = parts[0..-(i + 2)].join("::")
|
|
21
|
+
begin
|
|
22
|
+
candidate = Object.const_get(candidate_name)
|
|
23
|
+
rescue NameError
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if candidate.respond_to?(:register_check)
|
|
28
|
+
app_class = find_app_class_for(candidate)
|
|
29
|
+
return if app_class&.explicit_registration?
|
|
30
|
+
|
|
31
|
+
candidate.register_check(subclass)
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_app_class_for(candidate)
|
|
38
|
+
return candidate if candidate.respond_to?(:explicit_registration?)
|
|
39
|
+
|
|
40
|
+
if candidate.const_defined?(:App, false)
|
|
41
|
+
app = candidate.const_get(:App)
|
|
42
|
+
return app if app.respond_to?(:explicit_registration?)
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
public
|
|
48
|
+
|
|
49
|
+
def check_name
|
|
50
|
+
name.split("::").last
|
|
51
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
52
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
53
|
+
.downcase
|
|
54
|
+
.tr("_", " ")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pass(message)
|
|
59
|
+
CheckResult.new(passed: true, message: message)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def fail(message)
|
|
63
|
+
CheckResult.new(passed: false, message: message)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def config
|
|
67
|
+
app_class.config
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smol
|
|
4
|
+
CheckResult = Struct.new(:passed, :message, keyword_init: true) do
|
|
5
|
+
def passed?
|
|
6
|
+
passed
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def failed?
|
|
10
|
+
!passed
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
status = passed? ? "passed" : "failed"
|
|
15
|
+
"#{status}: #{message}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/smol/cli.rb
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smol
|
|
4
|
+
class CLI
|
|
5
|
+
include Output
|
|
6
|
+
include ConfigDisplay
|
|
7
|
+
using Colors
|
|
8
|
+
|
|
9
|
+
def initialize(app, prompt:, history: true)
|
|
10
|
+
@app = app
|
|
11
|
+
@prompt = prompt
|
|
12
|
+
@history = history
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(args)
|
|
16
|
+
if args.empty?
|
|
17
|
+
if @app.repl
|
|
18
|
+
REPL.new(@app, prompt: @prompt, history: @history, history_file: history_file_path).run
|
|
19
|
+
else
|
|
20
|
+
usage
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless @app.cli
|
|
27
|
+
failure "CLI mode is disabled"
|
|
28
|
+
hint "run without arguments for interactive mode" if @app.repl
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
cmd_name, *cmd_args = args
|
|
33
|
+
|
|
34
|
+
case cmd_name
|
|
35
|
+
when "help", "-h", "--help"
|
|
36
|
+
usage
|
|
37
|
+
exit 1
|
|
38
|
+
when "config"
|
|
39
|
+
show_config
|
|
40
|
+
return
|
|
41
|
+
when "config:set"
|
|
42
|
+
set_config(cmd_args[0], cmd_args[1])
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
klass = @app.find_command(cmd_name)
|
|
47
|
+
|
|
48
|
+
if klass.nil?
|
|
49
|
+
usage
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
positional, opts = klass.parse_options(cmd_args)
|
|
54
|
+
result = klass.new.call(*positional, **opts)
|
|
55
|
+
|
|
56
|
+
if result == true || result == false
|
|
57
|
+
exit(result ? 0 : 1)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def usage
|
|
64
|
+
banner @app.banner
|
|
65
|
+
|
|
66
|
+
out.puts <<~USAGE
|
|
67
|
+
#{@prompt.bold} - CLI app
|
|
68
|
+
|
|
69
|
+
#{"usage:".bold}
|
|
70
|
+
./#{@prompt}.rb start interactive mode
|
|
71
|
+
./#{@prompt}.rb <command> run a single command
|
|
72
|
+
|
|
73
|
+
#{"commands:".bold}
|
|
74
|
+
USAGE
|
|
75
|
+
|
|
76
|
+
grouped = @app.commands.group_by(&:group)
|
|
77
|
+
ungrouped = grouped.delete(nil) || []
|
|
78
|
+
|
|
79
|
+
ungrouped.each do |cmd|
|
|
80
|
+
out.puts " #{cmd.usage.ljust(34)}#{cmd.desc}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
grouped.keys.sort.each do |group_name|
|
|
84
|
+
nl
|
|
85
|
+
out.puts " #{group_name}:".bold
|
|
86
|
+
grouped[group_name].each do |cmd|
|
|
87
|
+
out.puts " #{cmd.usage.ljust(32)}#{cmd.desc}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if @app.mounts.any?
|
|
92
|
+
nl
|
|
93
|
+
out.puts " #{"sub-apps:".bold}"
|
|
94
|
+
@app.mounts.each do |name, app_class|
|
|
95
|
+
out.puts " #{(name + ":*").ljust(32)}#{app_class.banner.empty? ? name : app_class.banner}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
out.puts " #{"config".ljust(34)}show current config"
|
|
100
|
+
out.puts " #{"config:set <key> <value>".ljust(34)}set a config value"
|
|
101
|
+
|
|
102
|
+
nl
|
|
103
|
+
show_config
|
|
104
|
+
nl
|
|
105
|
+
|
|
106
|
+
out.puts "#{"environment:".bold}"
|
|
107
|
+
|
|
108
|
+
@app.config.each do |key, _, setting|
|
|
109
|
+
line = " #{key.to_s.upcase}"
|
|
110
|
+
line += " - #{setting[:desc]}" if setting[:desc]
|
|
111
|
+
out.puts line
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def history_file_path
|
|
116
|
+
@app.history_file || File.expand_path("~/.smol_#{@prompt}_history")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smol
|
|
4
|
+
module Coercion
|
|
5
|
+
TRUTHY_VALUES = %w[true 1 yes].freeze
|
|
6
|
+
|
|
7
|
+
def coerce_value(raw, type)
|
|
8
|
+
case type
|
|
9
|
+
when :integer
|
|
10
|
+
raw.to_i
|
|
11
|
+
when :boolean
|
|
12
|
+
TRUTHY_VALUES.include?(raw.to_s.downcase)
|
|
13
|
+
else
|
|
14
|
+
raw
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/smol/colors.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smol
|
|
4
|
+
module Colors
|
|
5
|
+
refine String do
|
|
6
|
+
def green
|
|
7
|
+
"\e[32m#{self}\e[0m"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def red
|
|
11
|
+
"\e[31m#{self}\e[0m"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def yellow
|
|
15
|
+
"\e[33m#{self}\e[0m"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def bold
|
|
19
|
+
"\e[1m#{self}\e[0m"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dim
|
|
23
|
+
"\e[2m#{self}\e[0m"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/smol/command.rb
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smol
|
|
4
|
+
class Command
|
|
5
|
+
include AppLookup
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
include Coercion
|
|
9
|
+
|
|
10
|
+
def inherited(subclass)
|
|
11
|
+
super
|
|
12
|
+
subclass.prepend ErrorHandler
|
|
13
|
+
subclass.prepend Callbacks
|
|
14
|
+
subclass.prepend AutoMessage
|
|
15
|
+
register_to_app(subclass)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def register_to_app(subclass)
|
|
21
|
+
parts = subclass.name&.split("::")
|
|
22
|
+
return unless parts && parts.size > 1
|
|
23
|
+
|
|
24
|
+
parts[0..-2].size.times do |i|
|
|
25
|
+
candidate_name = parts[0..-(i + 2)].join("::")
|
|
26
|
+
begin
|
|
27
|
+
candidate = Object.const_get(candidate_name)
|
|
28
|
+
rescue NameError
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if candidate.respond_to?(:register_command)
|
|
33
|
+
app_class = find_app_class_for(candidate)
|
|
34
|
+
return if app_class&.explicit_registration?
|
|
35
|
+
|
|
36
|
+
candidate.register_command(subclass)
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_app_class_for(candidate)
|
|
43
|
+
return candidate if candidate.respond_to?(:explicit_registration?)
|
|
44
|
+
|
|
45
|
+
if candidate.const_defined?(:App, false)
|
|
46
|
+
app = candidate.const_get(:App)
|
|
47
|
+
return app if app.respond_to?(:explicit_registration?)
|
|
48
|
+
end
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
public
|
|
53
|
+
|
|
54
|
+
def command_name(text = nil)
|
|
55
|
+
if text
|
|
56
|
+
@command_name = text
|
|
57
|
+
else
|
|
58
|
+
@command_name || derive_command_name
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def title(text = nil)
|
|
63
|
+
@title = text if text
|
|
64
|
+
@title
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def explain(text = nil)
|
|
68
|
+
@explain = text if text
|
|
69
|
+
@explain
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def aliases(*args)
|
|
73
|
+
@aliases = args if args.any?
|
|
74
|
+
@aliases || []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def args(*args)
|
|
78
|
+
@args = args if args.any?
|
|
79
|
+
@args || []
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def option(name, short: nil, type: :string, default: nil, desc: nil)
|
|
83
|
+
@options ||= {}
|
|
84
|
+
@options[name] = { short: short, type: type, default: default, desc: desc }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def options
|
|
88
|
+
@options || {}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def desc(text = nil)
|
|
92
|
+
@desc = text if text
|
|
93
|
+
@desc || ""
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def group(text = nil)
|
|
97
|
+
@group = text if text
|
|
98
|
+
@group
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def before_action(method_name)
|
|
102
|
+
@before_actions ||= []
|
|
103
|
+
@before_actions << method_name
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def before_actions
|
|
107
|
+
@before_actions || []
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def after_action(method_name)
|
|
111
|
+
@after_actions ||= []
|
|
112
|
+
@after_actions << method_name
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def after_actions
|
|
116
|
+
@after_actions || []
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def matches?(input)
|
|
120
|
+
input == command_name.to_s || aliases.map(&:to_s).include?(input)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def usage
|
|
124
|
+
parts = [command_name]
|
|
125
|
+
parts += args.map { |a| "<#{a}>" }
|
|
126
|
+
options.each do |name, opt|
|
|
127
|
+
flag = opt[:short] ? "-#{opt[:short]}/--#{name}" : "--#{name}"
|
|
128
|
+
parts << "[#{flag}]"
|
|
129
|
+
end
|
|
130
|
+
parts.join(" ")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def parse_options(argv)
|
|
134
|
+
positional = []
|
|
135
|
+
opts = options.transform_values { |o| o[:default] }
|
|
136
|
+
|
|
137
|
+
i = 0
|
|
138
|
+
while i < argv.length
|
|
139
|
+
arg = argv[i]
|
|
140
|
+
if arg.start_with?("--")
|
|
141
|
+
key, value = arg[2..].split("=", 2)
|
|
142
|
+
key = key.tr("-", "_").to_sym
|
|
143
|
+
if options[key]
|
|
144
|
+
value ||= argv[i += 1]
|
|
145
|
+
opts[key] = coerce_value(value, options[key][:type])
|
|
146
|
+
end
|
|
147
|
+
elsif arg.start_with?("-") && arg.length == 2
|
|
148
|
+
short = arg[1]
|
|
149
|
+
opt_name = options.find { |_, o| o[:short]&.to_s == short }&.first
|
|
150
|
+
if opt_name
|
|
151
|
+
value = argv[i += 1]
|
|
152
|
+
opts[opt_name] = coerce_value(value, options[opt_name][:type])
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
positional << arg
|
|
156
|
+
end
|
|
157
|
+
i += 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
[positional, opts]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def rescue_from(*exceptions, with: nil, &block)
|
|
164
|
+
handler = block || with
|
|
165
|
+
raise ArgumentError, "rescue_from requires a block or :with handler" unless handler
|
|
166
|
+
|
|
167
|
+
@error_handlers ||= []
|
|
168
|
+
exceptions.each do |exception|
|
|
169
|
+
@error_handlers << [exception, handler]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def error_handlers
|
|
174
|
+
@error_handlers || []
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def derive_command_name
|
|
180
|
+
return "anonymous" unless name
|
|
181
|
+
|
|
182
|
+
name.split("::").last
|
|
183
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
184
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
185
|
+
.downcase
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
module AutoMessage
|
|
190
|
+
def call(*args, **opts)
|
|
191
|
+
if self.class.title
|
|
192
|
+
header self.class.title
|
|
193
|
+
desc self.class.explain if self.class.explain
|
|
194
|
+
nl
|
|
195
|
+
end
|
|
196
|
+
super
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
module Callbacks
|
|
201
|
+
def call(*args, **opts)
|
|
202
|
+
self.class.before_actions.each do |method_name|
|
|
203
|
+
result = send(method_name, *args, **opts)
|
|
204
|
+
return result if result == false
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
result = super
|
|
208
|
+
|
|
209
|
+
self.class.after_actions.each do |method_name|
|
|
210
|
+
send(method_name, *args, result: result, **opts)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
result
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
module ErrorHandler
|
|
218
|
+
def call(*args, **opts)
|
|
219
|
+
super
|
|
220
|
+
rescue => e
|
|
221
|
+
handler = find_error_handler(e)
|
|
222
|
+
if handler
|
|
223
|
+
if handler.is_a?(Symbol)
|
|
224
|
+
send(handler, e)
|
|
225
|
+
else
|
|
226
|
+
instance_exec(e, &handler)
|
|
227
|
+
end
|
|
228
|
+
else
|
|
229
|
+
raise
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def find_error_handler(error)
|
|
236
|
+
self.class.error_handlers.each do |exception_class, handler|
|
|
237
|
+
return handler if error.is_a?(exception_class)
|
|
238
|
+
end
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
include Output
|
|
244
|
+
include Input
|
|
245
|
+
|
|
246
|
+
def config
|
|
247
|
+
app_class.config
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def app
|
|
251
|
+
app_class
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def checking(name)
|
|
255
|
+
warning "checking: #{name}"
|
|
256
|
+
nl
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def dropping(target)
|
|
260
|
+
warning "dropping: #{target}"
|
|
261
|
+
nl
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def done(hint_text = nil)
|
|
265
|
+
nl
|
|
266
|
+
success "done"
|
|
267
|
+
hint hint_text if hint_text
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def checks_passed?(all_passed, pass_hint: nil, fail_hint: nil)
|
|
271
|
+
nl
|
|
272
|
+
if all_passed
|
|
273
|
+
success "all checks passed"
|
|
274
|
+
hint pass_hint if pass_hint
|
|
275
|
+
else
|
|
276
|
+
failure "some checks failed"
|
|
277
|
+
hint fail_hint if fail_hint
|
|
278
|
+
end
|
|
279
|
+
all_passed
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def run_checks(*check_classes, args: [])
|
|
283
|
+
results = check_classes.map do |klass|
|
|
284
|
+
result = klass.new(*args).call
|
|
285
|
+
check_result(klass.check_name, result)
|
|
286
|
+
nl
|
|
287
|
+
result.passed?
|
|
288
|
+
end
|
|
289
|
+
results.all?
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|