gorails 0.1.0 → 0.1.3

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.
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