gorails 0.1.0 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/Gemfile +3 -1
  4. data/Gemfile.lock +65 -0
  5. data/README.md +41 -12
  6. data/bin/update-deps +95 -0
  7. data/exe/gorails +18 -0
  8. data/gorails.gemspec +4 -3
  9. data/lib/gorails/commands/episodes.rb +25 -0
  10. data/lib/gorails/commands/example.rb +19 -0
  11. data/lib/gorails/commands/help.rb +21 -0
  12. data/lib/gorails/commands/jobs.rb +25 -0
  13. data/lib/gorails/commands/jumpstart.rb +29 -0
  14. data/lib/gorails/commands/railsbytes.rb +67 -0
  15. data/lib/gorails/commands.rb +19 -0
  16. data/lib/gorails/entry_point.rb +10 -0
  17. data/lib/gorails/version.rb +1 -1
  18. data/lib/gorails.rb +22 -1
  19. data/vendor/deps/cli-kit/REVISION +1 -0
  20. data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
  21. data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
  22. data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
  23. data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
  24. data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
  25. data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
  26. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
  27. data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
  28. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
  29. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
  30. data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
  31. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
  32. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
  33. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
  34. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
  35. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
  36. data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
  37. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
  38. data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
  39. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
  40. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
  41. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
  42. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
  43. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
  44. data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
  45. data/vendor/deps/cli-ui/REVISION +1 -0
  46. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
  47. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
  48. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
  49. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
  50. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
  51. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
  52. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
  53. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
  54. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
  55. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
  56. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
  57. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
  58. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
  59. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
  60. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
  61. data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
  62. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
  63. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
  64. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
  65. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
  66. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
  67. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
  68. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
  69. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
  70. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
  71. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
  72. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
  73. data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
  74. metadata +114 -5
@@ -0,0 +1,128 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+
4
+ module CLI
5
+ module Kit
6
+ module Args
7
+ class Parser
8
+ extend T::Sig
9
+
10
+ autoload :Node, 'cli/kit/args/parser/node'
11
+
12
+ Error = Class.new(Args::Error)
13
+
14
+ class InvalidOptionError < Error
15
+ extend T::Sig
16
+ sig { params(option: String).void }
17
+ def initialize(option)
18
+ super("invalid option -- '#{option}'")
19
+ end
20
+ end
21
+
22
+ class OptionRequiresAnArgumentError < Error
23
+ extend T::Sig
24
+ sig { params(option: String).void }
25
+ def initialize(option)
26
+ super("option requires an argument -- '#{option}'")
27
+ end
28
+ end
29
+
30
+ sig { params(tokens: T::Array[Tokenizer::Token]).returns(T::Array[Node]) }
31
+ def parse(tokens)
32
+ nodes = T.let([], T::Array[Node])
33
+ args = T.let(tokens, T::Array[T.nilable(Tokenizer::Token)])
34
+ args << nil # to make each_cons pass (args.last, nil) on the final round.
35
+ state = :init
36
+ # TODO: test that "--height -- 3" is parsed correctly.
37
+ args.each_cons(2) do |(arg, next_arg)|
38
+ case state
39
+ when :skip
40
+ state = :init
41
+ when :init
42
+ state, val = parse_token(T.must(arg), next_arg)
43
+ nodes << val
44
+ when :unparsed
45
+ unless arg.is_a?(Tokenizer::Token::UnparsedArgument)
46
+ raise(Error, 'bug: non-unparsed argument after unparsed argument')
47
+ end
48
+
49
+ unparsed = nodes.last
50
+ unless unparsed.is_a?(Node::Unparsed)
51
+ # :nocov: not actually possible, in theory
52
+ raise(Error, 'bug: parser failed to recognize first unparsed argument')
53
+ # :nocov:
54
+ end
55
+
56
+ unparsed.value << arg.value
57
+ end
58
+ end
59
+ nodes
60
+ end
61
+
62
+ sig { params(definition: Definition).void }
63
+ def initialize(definition)
64
+ @defn = definition
65
+ end
66
+
67
+ private
68
+
69
+ sig do
70
+ params(token: Tokenizer::Token, next_token: T.nilable(Tokenizer::Token))
71
+ .returns([Symbol, Parser::Node])
72
+ end
73
+ def parse_token(token, next_token)
74
+ case token
75
+ when Tokenizer::Token::LongOptionName
76
+ case @defn.lookup_long(token.value)
77
+ when Definition::Option
78
+ [:skip, parse_option(token, next_token)]
79
+ when Definition::Flag
80
+ [:init, Node::LongFlag.new(token.value)]
81
+ else
82
+ raise(InvalidOptionError, token.value)
83
+ end
84
+ when Tokenizer::Token::ShortOptionName
85
+ case @defn.lookup_short(token.value)
86
+ when Definition::Option
87
+ [:skip, parse_option(token, next_token)]
88
+ when Definition::Flag
89
+ [:init, Node::ShortFlag.new(token.value)]
90
+ else
91
+ raise(InvalidOptionError, token.value)
92
+ end
93
+ when Tokenizer::Token::OptionValue
94
+ raise(Error, "bug: unexpected option value in argument parse sequence: #{token.value}")
95
+ when Tokenizer::Token::PositionalArgument
96
+ [:init, Node::Argument.new(token.value)]
97
+ when Tokenizer::Token::OptionValueOrPositionalArgument
98
+ [:init, Node::Argument.new(token.value)]
99
+ when Tokenizer::Token::UnparsedArgument
100
+ [:unparsed, Node::Unparsed.new([token.value])]
101
+ else
102
+ raise(Error, "bug: unexpected token type: #{token.class}")
103
+ end
104
+ end
105
+
106
+ sig { params(arg: Tokenizer::Token::OptionName, next_arg: T.nilable(Tokenizer::Token)).returns(Node) }
107
+ def parse_option(arg, next_arg)
108
+ case next_arg
109
+ when nil, Tokenizer::Token::LongOptionName,
110
+ Tokenizer::Token::ShortOptionName, Tokenizer::Token::PositionalArgument
111
+ raise(OptionRequiresAnArgumentError, arg.value)
112
+ when Tokenizer::Token::OptionValue, Tokenizer::Token::OptionValueOrPositionalArgument
113
+ case arg
114
+ when Tokenizer::Token::LongOptionName
115
+ Node::LongOption.new(arg.value, next_arg.value)
116
+ when Tokenizer::Token::ShortOptionName
117
+ Node::ShortOption.new(arg.value, next_arg.value)
118
+ else
119
+ raise(Error, "bug: unexpected token type: #{arg.class}")
120
+ end
121
+ else
122
+ raise(Error, "bug: unexpected argument type: #{next_arg.class}")
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,132 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+
4
+ module CLI
5
+ module Kit
6
+ module Args
7
+ module Tokenizer
8
+ extend T::Sig
9
+
10
+ Error = Class.new(Args::Error)
11
+
12
+ class InvalidShortOption < Error
13
+ extend T::Sig
14
+ sig { params(short_option: String).void }
15
+ def initialize(short_option)
16
+ super("invalid short option: '-#{short_option}'")
17
+ end
18
+ end
19
+
20
+ class InvalidCharInShortOption < Error
21
+ extend T::Sig
22
+ sig { params(short_option: String, char: String).void }
23
+ def initialize(short_option, char)
24
+ super("invalid character '#{char}' in short option: '-#{short_option}'")
25
+ end
26
+ end
27
+
28
+ class Token
29
+ extend T::Sig
30
+
31
+ sig { returns(String) }
32
+ attr_reader :value
33
+
34
+ sig { params(value: String).void }
35
+ def initialize(value)
36
+ @value = value
37
+ end
38
+
39
+ sig { returns(String) }
40
+ def inspect
41
+ "#<#{self.class.name} #{@value}>"
42
+ end
43
+
44
+ sig { params(other: T.untyped).returns(T::Boolean) }
45
+ def ==(other)
46
+ self.class == other.class && @value == other.value
47
+ end
48
+
49
+ OptionName = Class.new(Token)
50
+ LongOptionName = Class.new(OptionName)
51
+ ShortOptionName = Class.new(OptionName)
52
+
53
+ OptionValue = Class.new(Token)
54
+ PositionalArgument = Class.new(Token)
55
+ OptionValueOrPositionalArgument = Class.new(Token)
56
+ UnparsedArgument = Class.new(Token)
57
+ end
58
+
59
+ class << self
60
+ extend T::Sig
61
+
62
+ sig { params(raw_args: T::Array[String]).returns(T::Array[Token]) }
63
+ def tokenize(raw_args)
64
+ args = []
65
+
66
+ mode = :init
67
+
68
+ raw_args.each do |arg|
69
+ case mode
70
+ when :unparsed
71
+ args << Token::UnparsedArgument.new(arg)
72
+ when :init
73
+ case arg
74
+ when '--'
75
+ mode = :unparsed
76
+ when /\A--/
77
+ name, value = arg.split('=', 2)
78
+ args << Token::LongOptionName.new(T.must(T.must(name)[2..-1]))
79
+ if value
80
+ args << Token::OptionValue.new(value)
81
+ end
82
+ when /\A-/
83
+ args.concat(tokenize_short_option(T.must(arg[1..-1])))
84
+ else
85
+ args << if args.last.is_a?(Token::OptionName)
86
+ Token::OptionValueOrPositionalArgument.new(arg)
87
+ else
88
+ Token::PositionalArgument.new(arg)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ args
95
+ end
96
+
97
+ sig { params(arg: String).returns(T::Array[Token]) }
98
+ def tokenize_short_option(arg)
99
+ args = []
100
+ mode = :init
101
+ number = +''
102
+ arg.each_char do |char|
103
+ case mode
104
+ when :numeric
105
+ case char
106
+ when /[0-9]/
107
+ number << char
108
+ else
109
+ raise(InvalidShortOption, arg)
110
+ end
111
+ when :init
112
+ case char
113
+ when /[a-zA-Z]/
114
+ args << Token::ShortOptionName.new(char)
115
+ when /[0-9]/
116
+ mode = :numeric
117
+ number << char
118
+ else
119
+ raise(InvalidCharInShortOption.new(arg, char))
120
+ end
121
+ end
122
+ end
123
+ if number != ''
124
+ args << Token::OptionValue.new(number)
125
+ end
126
+ args
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,15 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+
4
+ module CLI
5
+ module Kit
6
+ module Args
7
+ Error = Class.new(StandardError)
8
+
9
+ autoload :Definition, 'cli/kit/args/definition'
10
+ autoload :Parser, 'cli/kit/args/parser'
11
+ autoload :Evaluation, 'cli/kit/args/evaluation'
12
+ autoload :Tokenizer, 'cli/kit/args/tokenizer'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+
4
+ module CLI
5
+ module Kit
6
+ class BaseCommand
7
+ extend T::Sig
8
+ extend T::Helpers
9
+ include CLI::Kit::CommandHelp
10
+ extend CLI::Kit::CommandHelp::ClassMethods
11
+ abstract!
12
+
13
+ sig { returns(T::Boolean) }
14
+ def self.defined?
15
+ true
16
+ end
17
+
18
+ sig { params(args: T::Array[String], command_name: String).void }
19
+ def self.call(args, command_name)
20
+ new.call(args, command_name)
21
+ end
22
+
23
+ sig { returns(T::Boolean) }
24
+ def has_subcommands?
25
+ false
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,256 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+
4
+ module CLI
5
+ module Kit
6
+ module CommandHelp
7
+ extend T::Sig
8
+ include Kernel # for sorbet
9
+
10
+ sig { params(args: T::Array[String], name: String).void }
11
+ def call(args, name)
12
+ begin
13
+ defn = Args::Definition.new
14
+ opts = self.class.opts_class
15
+ opts.new(defn).install_to_definition
16
+ tokens = Args::Tokenizer.tokenize(args)
17
+ parse = Args::Parser.new(defn).parse(tokens)
18
+ result = Args::Evaluation.new(defn, parse)
19
+ opts_inst = opts.new(result)
20
+ rescue Args::Evaluation::TooManyPositions, Args::Evaluation::MissingRequiredPosition => e
21
+ STDERR.puts CLI::UI.fmt("{{red:{{bold:Error: #{e.message}}}}}")
22
+ STDERR.puts
23
+ STDERR.puts self.class.build_help
24
+ raise(AbortSilent)
25
+ rescue Args::Error => e
26
+ raise(Abort, e)
27
+ end
28
+
29
+ if opts_inst.helpflag
30
+ puts self.class.build_help
31
+ else
32
+ res = begin
33
+ opts.new(result)
34
+ rescue Args::Error => e
35
+ raise(Abort, e)
36
+ end
37
+ invoke_wrapper(res, name)
38
+ end
39
+ end
40
+
41
+ # use to implement error handling
42
+ sig { params(op: T.untyped, name: String).void }
43
+ def invoke_wrapper(op, name)
44
+ invoke(op, name)
45
+ end
46
+
47
+ sig { params(op: T.untyped, name: String).void }
48
+ def invoke(op, name)
49
+ raise(NotImplementedError, '#invoke must be implemented, or #call overridden')
50
+ end
51
+
52
+ sig { params(name: String).void }
53
+ def self.tool_name=(name)
54
+ @tool_name = name
55
+ end
56
+
57
+ sig { returns(String) }
58
+ def self._tool_name
59
+ unless @tool_name
60
+ raise 'You must set CLI::Kit::CommandHelp.tool_name='
61
+ end
62
+
63
+ @tool_name
64
+ end
65
+
66
+ module ClassMethods
67
+ extend T::Sig
68
+ include Kernel # for sorbet
69
+
70
+ DEFAULT_HELP_SECTIONS = [
71
+ :desc,
72
+ :long_desc,
73
+ :usage,
74
+ :examples,
75
+ :options,
76
+ ]
77
+
78
+ sig { returns(String) }
79
+ def build_help
80
+ h = (@help_sections || DEFAULT_HELP_SECTIONS).map do |section|
81
+ case section
82
+ when :desc
83
+ build_desc
84
+ when :long_desc
85
+ @long_desc
86
+ when :usage
87
+ @usage_section ||= build_usage
88
+ when :examples
89
+ @examples_section ||= build_examples
90
+ when :options
91
+ @options_section ||= build_options
92
+ else
93
+ raise "Unknown help section: #{section}"
94
+ end
95
+ end.compact.map(&:chomp).join("\n\n") + "\n"
96
+ CLI::UI.fmt(h)
97
+ end
98
+
99
+ sig { returns(String) }
100
+ def _command_name
101
+ return @command_name if @command_name
102
+
103
+ last_camel = send(:name).split('::').last
104
+ last_camel.gsub(/([a-z])([A-Z])/, '\1-\2').downcase
105
+ end
106
+
107
+ sig { returns(String) }
108
+ def _desc
109
+ @desc
110
+ end
111
+
112
+ sig { returns(String) }
113
+ def build_desc
114
+ out = +"{{command:#{CommandHelp._tool_name} #{_command_name}}}"
115
+ if @desc
116
+ out << ": #{@desc}"
117
+ end
118
+ "{{bold:#{out}}}"
119
+ end
120
+
121
+ sig { returns(T.untyped) }
122
+ def opts_class
123
+ T.unsafe(self).const_get(:Opts) # rubocop:disable Sorbet/ConstantsFromStrings
124
+ rescue NameError
125
+ Class.new(CLI::Kit::Opts)
126
+ end
127
+
128
+ sig { returns(T.nilable(String)) }
129
+ def build_options
130
+ opts = opts_class
131
+ return(nil) unless opts
132
+
133
+ methods = []
134
+ loop do
135
+ methods.concat(opts.public_instance_methods(false))
136
+ break if opts.superclass == CLI::Kit::Opts
137
+
138
+ opts = opts.superclass
139
+ end
140
+
141
+ @defn = Args::Definition.new
142
+ o = opts.new(@defn)
143
+ o.install_to_definition
144
+
145
+ return nil if @defn.options.empty? && @defn.flags.empty?
146
+
147
+ merged = T.let(@defn.options, T::Array[T.any(Args::Definition::Option, Args::Definition::Flag)])
148
+ merged += @defn.flags
149
+ merged.sort_by!(&:name)
150
+ "{{bold:Options:}}\n" + merged.map do |o|
151
+ if o.is_a?(Args::Definition::Option)
152
+ z = ' ' + [o.short&.prepend('-'), o.long&.prepend('--')].compact.join(', ') + ' VALUE'
153
+ default = if o.dynamic_default?
154
+ '(generated default)'
155
+ elsif o.default.nil?
156
+ '(no default)'
157
+ else
158
+ "(default: #{o.default.inspect})"
159
+ end
160
+ z << if o.desc
161
+ " {{italic:{{gray:# #{o.desc} #{default}}}}}"
162
+ else
163
+ " {{italic:{{gray:# #{default}}}}}"
164
+ end
165
+ else
166
+ z = ' ' + [o.short&.prepend('-'), o.long&.prepend('--')].compact.join(', ')
167
+ if o.desc
168
+ z << " {{italic:{{gray:# #{o.desc}}}}}"
169
+ end
170
+ end
171
+ z
172
+ end.join("\n")
173
+ end
174
+
175
+ sig { params(sections: T::Array[Symbol]).void }
176
+ def help_sections(sections)
177
+ @help_sections = sections
178
+ end
179
+
180
+ sig { params(command_name: String).void }
181
+ def command_name(command_name)
182
+ if @command_name
183
+ raise(ArgumentError, "Command name already set to #{@command_name}")
184
+ end
185
+
186
+ @command_name = command_name
187
+ end
188
+
189
+ sig { params(desc: String).void }
190
+ def desc(desc)
191
+ if desc.size > 80
192
+ raise(ArgumentError, 'description must be 80 characters or less')
193
+ end
194
+ if @desc
195
+ raise(ArgumentError, 'description already set')
196
+ end
197
+
198
+ @desc = desc
199
+ end
200
+
201
+ sig { params(long_desc: String).void }
202
+ def long_desc(long_desc)
203
+ if @long_desc
204
+ raise(ArgumentError, 'long description already set')
205
+ end
206
+
207
+ @long_desc = long_desc
208
+ end
209
+
210
+ sig { returns(String) }
211
+ def build_usage
212
+ '{{bold:Usage:}}' + case (@usage || []).size
213
+ when 0
214
+ " {{command:#{CommandHelp._tool_name} #{_command_name}}} [options]\n"
215
+ when 1
216
+ " {{command:#{CommandHelp._tool_name} #{_command_name}}} #{@usage.first}\n"
217
+ else
218
+ "\n" + @usage.map do |usage|
219
+ " {{command:#{CommandHelp._tool_name} #{_command_name}}} #{usage}\n"
220
+ end.join
221
+ end
222
+ end
223
+
224
+ sig { returns(T.nilable(String)) }
225
+ def build_examples
226
+ return nil unless @examples
227
+
228
+ cmd_prefix = " {{command:#{CommandHelp._tool_name} #{_command_name}}}"
229
+ "{{bold:Examples:}}\n" + @examples.map do |command, explanation|
230
+ cmd = "#{cmd_prefix} #{command}"
231
+ exp = "{{italic:{{gray:# #{explanation}}}}}"
232
+
233
+ width = CLI::UI::ANSI.printing_width(CLI::UI.fmt("#{cmd} #{exp}"))
234
+ if width > CLI::UI::Terminal.width
235
+ " #{exp}\n#{cmd}"
236
+ else
237
+ "#{cmd} #{exp}"
238
+ end
239
+ end.join("\n\n")
240
+ end
241
+
242
+ sig { params(usage: String).void }
243
+ def usage(usage)
244
+ @usage ||= []
245
+ @usage << usage
246
+ end
247
+
248
+ sig { params(command: String, explanation: T.nilable(String)).void }
249
+ def example(command, explanation)
250
+ @examples ||= []
251
+ @examples << [command, explanation]
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,141 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+
4
+ module CLI
5
+ module Kit
6
+ class CommandRegistry
7
+ extend T::Sig
8
+
9
+ CommandOrProc = T.type_alias do
10
+ T.any(T.class_of(CLI::Kit::BaseCommand), T.proc.returns(T.class_of(CLI::Kit::BaseCommand)))
11
+ end
12
+
13
+ sig { returns(T::Hash[String, CommandOrProc]) }
14
+ attr_reader :commands
15
+
16
+ sig { returns(T::Hash[String, String]) }
17
+ attr_reader :aliases
18
+
19
+ module ContextualResolver
20
+ extend T::Sig
21
+ extend T::Helpers
22
+ interface!
23
+
24
+ sig { abstract.returns(T::Array[String]) }
25
+ def command_names; end
26
+
27
+ sig { abstract.returns(T::Hash[String, String]) }
28
+ def aliases; end
29
+
30
+ sig { abstract.params(_name: String).returns(T.class_of(CLI::Kit::BaseCommand)) }
31
+ def command_class(_name); end
32
+ end
33
+
34
+ module NullContextualResolver
35
+ extend T::Sig
36
+ extend ContextualResolver
37
+
38
+ sig { override.returns(T::Array[String]) }
39
+ def self.command_names
40
+ []
41
+ end
42
+
43
+ sig { override.returns(T::Hash[String, String]) }
44
+ def self.aliases
45
+ {}
46
+ end
47
+
48
+ sig { override.params(_name: String).returns(T.class_of(CLI::Kit::BaseCommand)) }
49
+ def self.command_class(_name)
50
+ raise(CLI::Kit::Abort, 'Cannot be called on the NullContextualResolver since command_names is empty')
51
+ end
52
+ end
53
+
54
+ sig { params(default: String, contextual_resolver: ContextualResolver).void }
55
+ def initialize(default:, contextual_resolver: NullContextualResolver)
56
+ @commands = {}
57
+ @aliases = {}
58
+ @default = default
59
+ @contextual_resolver = contextual_resolver
60
+ end
61
+
62
+ sig { returns(T::Hash[String, T.class_of(CLI::Kit::BaseCommand)]) }
63
+ def resolved_commands
64
+ @commands.each_with_object({}) do |(k, v), a|
65
+ a[k] = resolve_class(v)
66
+ end
67
+ end
68
+
69
+ sig { params(const: CommandOrProc, name: String).void }
70
+ def add(const, name)
71
+ commands[name] = const
72
+ end
73
+
74
+ sig { params(name: T.nilable(String)).returns([T.nilable(T.class_of(CLI::Kit::BaseCommand)), String]) }
75
+ def lookup_command(name)
76
+ name = @default if name.to_s.empty?
77
+ resolve_command(T.must(name))
78
+ end
79
+
80
+ sig { params(from: String, to: String).void }
81
+ def add_alias(from, to)
82
+ aliases[from] = to unless aliases[from]
83
+ end
84
+
85
+ sig { returns(T::Array[String]) }
86
+ def command_names
87
+ @contextual_resolver.command_names + commands.keys
88
+ end
89
+
90
+ sig { params(name: String).returns(T::Boolean) }
91
+ def exist?(name)
92
+ !resolve_command(name).first.nil?
93
+ end
94
+
95
+ private
96
+
97
+ sig { params(name: String).returns(String) }
98
+ def resolve_alias(name)
99
+ aliases[name] || @contextual_resolver.aliases.fetch(name, name)
100
+ end
101
+
102
+ sig { params(name: String).returns([T.nilable(T.class_of(CLI::Kit::BaseCommand)), String]) }
103
+ def resolve_command(name)
104
+ name = resolve_alias(name)
105
+ resolve_global_command(name) || \
106
+ resolve_contextual_command(name) || \
107
+ [nil, name]
108
+ end
109
+
110
+ sig { params(name: String).returns(T.nilable([T.class_of(CLI::Kit::BaseCommand), String])) }
111
+ def resolve_global_command(name)
112
+ klass = resolve_class(commands.fetch(name, nil))
113
+ return nil unless klass
114
+
115
+ [klass, name]
116
+ rescue NameError
117
+ nil
118
+ end
119
+
120
+ sig { params(name: String).returns(T.nilable([T.class_of(CLI::Kit::BaseCommand), String])) }
121
+ def resolve_contextual_command(name)
122
+ found = @contextual_resolver.command_names.include?(name)
123
+ return nil unless found
124
+
125
+ [@contextual_resolver.command_class(name), name]
126
+ end
127
+
128
+ sig { params(class_or_proc: T.nilable(CommandOrProc)).returns(T.nilable(T.class_of(CLI::Kit::BaseCommand))) }
129
+ def resolve_class(class_or_proc)
130
+ case class_or_proc
131
+ when nil
132
+ nil
133
+ when Proc
134
+ class_or_proc.call
135
+ else
136
+ class_or_proc
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end