cli-kit 4.0.0 → 5.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/cla.yml +22 -0
  3. data/.github/workflows/ruby.yml +34 -2
  4. data/.gitignore +2 -0
  5. data/.rubocop.sorbet.yml +47 -0
  6. data/.rubocop.yml +16 -1
  7. data/Gemfile +10 -1
  8. data/Gemfile.lock +94 -18
  9. data/README.md +46 -3
  10. data/Rakefile +1 -0
  11. data/bin/onchange +30 -0
  12. data/bin/tapioca +29 -0
  13. data/bin/testunit +1 -0
  14. data/cli-kit.gemspec +2 -2
  15. data/dev.yml +35 -3
  16. data/examples/minimal/example.rb +3 -1
  17. data/examples/single-file/example.rb +25 -35
  18. data/gen/lib/gen/commands/help.rb +8 -10
  19. data/gen/lib/gen/commands/new.rb +23 -9
  20. data/gen/lib/gen/commands.rb +21 -9
  21. data/gen/lib/gen/entry_point.rb +12 -3
  22. data/gen/lib/gen/generator.rb +28 -7
  23. data/gen/lib/gen/help.rb +63 -0
  24. data/gen/lib/gen.rb +18 -23
  25. data/gen/template/bin/update-deps +2 -2
  26. data/gen/template/lib/__app__/commands.rb +1 -4
  27. data/gen/template/lib/__app__.rb +8 -17
  28. data/gen/template/test/example_test.rb +1 -1
  29. data/lib/cli/kit/args/definition.rb +344 -0
  30. data/lib/cli/kit/args/evaluation.rb +245 -0
  31. data/lib/cli/kit/args/parser/node.rb +132 -0
  32. data/lib/cli/kit/args/parser.rb +129 -0
  33. data/lib/cli/kit/args/tokenizer.rb +133 -0
  34. data/lib/cli/kit/args.rb +16 -0
  35. data/lib/cli/kit/base_command.rb +17 -32
  36. data/lib/cli/kit/command_help.rb +271 -0
  37. data/lib/cli/kit/command_registry.rb +69 -17
  38. data/lib/cli/kit/config.rb +25 -22
  39. data/lib/cli/kit/core_ext.rb +30 -0
  40. data/lib/cli/kit/error_handler.rb +131 -70
  41. data/lib/cli/kit/executor.rb +19 -3
  42. data/lib/cli/kit/ini.rb +31 -38
  43. data/lib/cli/kit/levenshtein.rb +12 -4
  44. data/lib/cli/kit/logger.rb +16 -2
  45. data/lib/cli/kit/opts.rb +301 -0
  46. data/lib/cli/kit/resolver.rb +8 -0
  47. data/lib/cli/kit/sorbet_runtime_stub.rb +156 -0
  48. data/lib/cli/kit/support/test_helper.rb +23 -14
  49. data/lib/cli/kit/support.rb +2 -0
  50. data/lib/cli/kit/system.rb +188 -54
  51. data/lib/cli/kit/util.rb +48 -103
  52. data/lib/cli/kit/version.rb +3 -1
  53. data/lib/cli/kit.rb +103 -7
  54. metadata +22 -10
  55. data/.github/probots.yml +0 -2
  56. data/lib/cli/kit/autocall.rb +0 -21
  57. data/lib/cli/kit/ruby_backports/enumerable.rb +0 -6
@@ -1,19 +1,17 @@
1
+ # typed: true
2
+
1
3
  require 'gen'
2
4
 
3
5
  module Gen
4
6
  module Commands
5
7
  class Help < Gen::Command
6
- def call(_args, _name)
7
- puts CLI::UI.fmt('{{bold:Available commands}}')
8
- puts ''
8
+ extend T::Sig
9
+
10
+ desc('Show help for a command, or this page')
9
11
 
10
- Gen::Commands::Registry.resolved_commands.each do |name, klass|
11
- puts CLI::UI.fmt("{{command:#{Gen::TOOL_NAME} #{name}}}")
12
- if klass.respond_to?(:help) && (help = klass.help)
13
- puts CLI::UI.fmt(help)
14
- end
15
- puts ''
16
- end
12
+ sig { params(args: T::Array[String], _name: String).void }
13
+ def call(args, _name)
14
+ Gen::Help.generate(args)
17
15
  end
18
16
  end
19
17
  end
@@ -1,20 +1,34 @@
1
+ # typed: true
2
+
1
3
  require 'gen'
2
4
 
3
5
  module Gen
4
6
  module Commands
5
7
  class New < Gen::Command
6
- def call(args, _name)
7
- unless args.size == 1
8
- puts CLI::UI.fmt(self.class.help)
9
- raise(CLI::Kit::AbortSilent)
10
- end
11
- project = args.first
8
+ extend T::Sig
9
+
10
+ command_name('new')
11
+ desc('Create a new project')
12
+ long_desc(<<~LONGDESC)
13
+ This is currently a very simple command. In theory it could be extended
14
+ to support a number of flags like {{command:bundle gem}}, etc. As it is, it only
15
+ takes an application name.
16
+ LONGDESC
17
+ usage('[app_name]')
18
+ example('mycliapp', "create a new project called 'mycliapp'")
12
19
 
13
- Gen::Generator.run(project)
20
+ class Opts < CLI::Kit::Opts
21
+ extend(T::Sig)
22
+
23
+ sig { returns(String) }
24
+ def project_name
25
+ position!
26
+ end
14
27
  end
15
28
 
16
- def self.help
17
- "Generate a new cli-kit project.\nUsage: {{command:#{Gen::TOOL_NAME} new <name>}}"
29
+ sig { params(op: Opts, _name: T.untyped).returns(T.untyped) }
30
+ def invoke(op, _name)
31
+ Gen::Generator.run(op.project_name)
18
32
  end
19
33
  end
20
34
  end
@@ -1,18 +1,30 @@
1
+ # typed: true
2
+
1
3
  require 'gen'
2
4
 
3
5
  module Gen
4
6
  module Commands
5
- Registry = CLI::Kit::CommandRegistry.new(
6
- default: 'help',
7
- contextual_resolver: nil
8
- )
7
+ extend T::Sig
8
+
9
+ Registry = CLI::Kit::CommandRegistry.new(default: 'help')
9
10
 
10
- def self.register(const, cmd, path)
11
- autoload(const, path)
12
- Registry.add(->() { const_get(const) }, cmd)
11
+ class << self
12
+ extend T::Sig
13
+
14
+ sig do
15
+ params(const: Symbol, cmd: String, path: String, lamda_const: T.proc.returns(T.class_of(Gen::Command))).void
16
+ end
17
+ def register(const, cmd, path, lamda_const)
18
+ autoload(const, path)
19
+ Registry.add(lamda_const, cmd)
20
+ end
13
21
  end
14
22
 
15
- register :Help, 'help', 'gen/commands/help'
16
- register :New, 'new', 'gen/commands/new'
23
+ register :Help, 'help', 'gen/commands/help', -> { Commands::Help }
24
+ register :New, 'new', 'gen/commands/new', -> { Commands::New }
25
+
26
+ # TODO(burke): Really, cli-kit needs to handle global flags/options.
27
+ Registry.add_alias('-h', 'help')
28
+ Registry.add_alias('--help', 'help')
17
29
  end
18
30
  end
@@ -1,10 +1,19 @@
1
+ # typed: true
2
+
1
3
  require 'gen'
2
4
 
3
5
  module Gen
4
6
  module EntryPoint
5
- def self.call(args)
6
- cmd, command_name, args = Gen::Resolver.call(args)
7
- Gen::Executor.call(cmd, command_name, args)
7
+ extend T::Sig
8
+
9
+ class << self
10
+ extend T::Sig
11
+
12
+ sig { params(args: T::Array[String]).void }
13
+ def call(args)
14
+ cmd, command_name, args = Gen::Resolver.call(args)
15
+ Gen::Executor.call(cmd, command_name, args)
16
+ end
8
17
  end
9
18
  end
10
19
  end
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  require 'gen'
2
4
  require 'fileutils'
3
5
  require 'open3'
@@ -6,8 +8,15 @@ require 'tmpdir'
6
8
 
7
9
  module Gen
8
10
  class Generator
9
- def self.run(project_name)
10
- new(project_name).run
11
+ extend T::Sig
12
+
13
+ class << self
14
+ extend T::Sig
15
+
16
+ sig { params(project_name: String).void }
17
+ def run(project_name)
18
+ new(project_name).run
19
+ end
11
20
  end
12
21
 
13
22
  TEMPLATE_ROOT = File.expand_path('gen/template', Gen::ROOT)
@@ -35,15 +44,17 @@ module Gen
35
44
  }.freeze
36
45
  private_constant :BUNDLER_TRANSLATIONS
37
46
 
47
+ sig { params(project_name: String).void }
38
48
  def initialize(project_name)
39
49
  raise(
40
50
  CLI::Kit::Abort,
41
- "project name must match {{bold:#{VALID_PROJECT_NAME}}} (but can be changed later)"
51
+ "project name must match {{bold:#{VALID_PROJECT_NAME}}} (but can be changed later)",
42
52
  ) unless project_name =~ VALID_PROJECT_NAME
43
53
  @project_name = project_name
44
54
  @title_case_project_name = @project_name.sub(/^./, &:upcase)
45
55
  end
46
56
 
57
+ sig { void }
47
58
  def run
48
59
  vendor = ask_vendor?
49
60
  create_project_dir
@@ -57,11 +68,12 @@ module Gen
57
68
 
58
69
  private
59
70
 
71
+ sig { returns(T::Boolean) }
60
72
  def ask_vendor?
61
73
  return true if ENV['DEPS'] == 'vendor'
62
74
  return false if ENV['DEPS'] == 'bundler'
63
75
 
64
- vendor = nil
76
+ vendor = T.let(nil, T.nilable(String))
65
77
  CLI::UI::Frame.open('Configuration') do
66
78
  q = 'How would you like the application to consume {{command:cli-kit}} and {{command:cli-ui}}?'
67
79
  vendor = CLI::UI::Prompt.ask(q) do |c|
@@ -72,6 +84,7 @@ module Gen
72
84
  vendor == 'vendor'
73
85
  end
74
86
 
87
+ sig { void }
75
88
  def create_project_dir
76
89
  info(create: '')
77
90
  FileUtils.mkdir(@project_name)
@@ -79,10 +92,12 @@ module Gen
79
92
  error("directory already exists: #{@project_name}")
80
93
  end
81
94
 
95
+ sig { params(translations: T::Hash[String, T.any(FalseClass, String)]).void }
82
96
  def copy_files(translations:)
83
97
  each_template_file do |source_name|
84
98
  target_name = translations.fetch(source_name, source_name)
85
99
  next if target_name == false
100
+
86
101
  target_name = apply_template_variables(target_name)
87
102
 
88
103
  source = File.join(TEMPLATE_ROOT, source_name)
@@ -100,6 +115,7 @@ module Gen
100
115
  end
101
116
  end
102
117
 
118
+ sig { void }
103
119
  def update_deps
104
120
  Dir.mktmpdir do |tmp|
105
121
  clone(tmp, 'cli-ui')
@@ -111,6 +127,7 @@ module Gen
111
127
  end
112
128
  end
113
129
 
130
+ sig { params(dir: String, repo: String).void }
114
131
  def clone(dir, repo)
115
132
  info(clone: repo)
116
133
  out, stat = Open3.capture2e('git', '-C', dir, 'clone', "https://github.com/shopify/#{repo}")
@@ -120,9 +137,8 @@ module Gen
120
137
  end
121
138
  end
122
139
 
123
- def each_template_file
124
- return enum_for(:each_template_file) unless block_given?
125
-
140
+ sig { params(block: T.proc.params(rel_path: String).void).void }
141
+ def each_template_file(&block)
126
142
  root = Pathname.new(TEMPLATE_ROOT)
127
143
  Dir.glob("#{TEMPLATE_ROOT}/**/*").each do |f|
128
144
  el = Pathname.new(f)
@@ -130,6 +146,7 @@ module Gen
130
146
  end
131
147
  end
132
148
 
149
+ sig { params(s: String).returns(String) }
133
150
  def apply_template_variables(s)
134
151
  s
135
152
  .gsub(/__app__/, @project_name)
@@ -138,16 +155,19 @@ module Gen
138
155
  .gsub(/__cli-ui-version__/, cli_ui_version)
139
156
  end
140
157
 
158
+ sig { returns(String) }
141
159
  def cli_kit_version
142
160
  require 'cli/kit/version'
143
161
  CLI::Kit::VERSION.to_s
144
162
  end
145
163
 
164
+ sig { returns(String) }
146
165
  def cli_ui_version
147
166
  require 'cli/ui/version'
148
167
  CLI::UI::VERSION.to_s
149
168
  end
150
169
 
170
+ sig { params(create: T.nilable(String), clone: T.nilable(String), run: T.nilable(String)).void }
151
171
  def info(create: nil, clone: nil, run: nil)
152
172
  if clone
153
173
  puts(CLI::UI.fmt("\t{{bold:{{yellow:clone}}\t#{clone}}}"))
@@ -158,6 +178,7 @@ module Gen
158
178
  end
159
179
  end
160
180
 
181
+ sig { params(msg: String).void }
161
182
  def error(msg)
162
183
  raise(CLI::Kit::Abort, msg)
163
184
  end
@@ -0,0 +1,63 @@
1
+ # typed: true
2
+
3
+ require 'gen'
4
+
5
+ module Gen
6
+ module Help
7
+ extend T::Sig
8
+
9
+ class << self
10
+ extend T::Sig
11
+
12
+ sig { params(path: T::Array[String], to: IO).void }
13
+ def generate(path, to: STDOUT)
14
+ case path.size
15
+ when 0
16
+ generate_toplevel(to: to)
17
+ when 1
18
+ generate_command_help(T.must(path.first), to: to)
19
+ else
20
+ raise(NotImplementedError, 'subcommand help not implemented')
21
+ end
22
+ end
23
+
24
+ sig { params(to: IO).void }
25
+ def generate_toplevel(to: STDOUT)
26
+ to.write(CLI::UI.fmt(<<~HELP))
27
+ {{bold:{{command:cli-kit}} generates new cli-kit apps.}}
28
+
29
+ It basically only has one command: {{command:cli-kit new}}.
30
+
31
+ See {{command:cli-kit new --help}} for more information.
32
+
33
+ {{bold:Available commands:}}
34
+ HELP
35
+
36
+ cmds = Gen::Commands::Registry.resolved_commands.map do |name, klass|
37
+ [name, klass._desc]
38
+ end
39
+
40
+ max_len = cmds.map(&:first).map(&:length).max
41
+
42
+ cmds.each do |name, desc|
43
+ to.write(CLI::UI.fmt(" {{command:#{name.ljust(max_len)}}} #{desc}\n"))
44
+ end
45
+ end
46
+
47
+ sig { params(cmd_name: String, to: IO).void }
48
+ def generate_command_help(cmd_name, to: STDOUT)
49
+ klass = Gen::Commands::Registry.resolved_commands[cmd_name]
50
+ unless klass
51
+ to.write(CLI::UI.fmt(<<~HELP))
52
+ {{red:{{bold:No help found for: #{cmd_name}}}}}
53
+
54
+ HELP
55
+ generate_toplevel(to: to)
56
+ raise(CLI::Kit::AbortSilent)
57
+ end
58
+
59
+ klass.new.call(['--help'], cmd_name)
60
+ end
61
+ end
62
+ end
63
+ end
data/gen/lib/gen.rb CHANGED
@@ -1,39 +1,34 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui'
2
4
  require 'cli/kit'
3
5
 
4
6
  CLI::UI::StdoutRouter.enable
5
7
 
6
8
  module Gen
7
- extend CLI::Kit::Autocall
8
-
9
9
  TOOL_NAME = 'cli-kit'
10
- ROOT = File.expand_path('../../..', __FILE__)
10
+ CLI::Kit::CommandHelp.tool_name = TOOL_NAME
11
+
12
+ ROOT = File.expand_path('../../..', __FILE__)
11
13
 
12
14
  TOOL_CONFIG_PATH = File.expand_path(File.join('~', '.config', TOOL_NAME))
13
15
  LOG_FILE = File.join(TOOL_CONFIG_PATH, 'logs', 'log.log')
14
16
  DEBUG_LOG_FILE = File.join(TOOL_CONFIG_PATH, 'logs', 'debug.log')
15
17
 
16
- autoload(:Generator, 'gen/generator')
17
-
18
+ autoload(:Generator, 'gen/generator')
18
19
  autoload(:EntryPoint, 'gen/entry_point')
20
+ autoload(:Help, 'gen/help')
19
21
  autoload(:Commands, 'gen/commands')
20
22
 
21
- autocall(:Config) { CLI::Kit::Config.new(tool_name: TOOL_NAME) }
22
- autocall(:Command) { CLI::Kit::BaseCommand }
23
- autocall(:Logger) { CLI::Kit::Logger.new(debug_log_file: DEBUG_LOG_FILE) }
24
-
25
- autocall(:Executor) { CLI::Kit::Executor.new(log_file: LOG_FILE) }
26
- autocall(:Resolver) do
27
- CLI::Kit::Resolver.new(
28
- tool_name: TOOL_NAME,
29
- command_registry: Gen::Commands::Registry
30
- )
31
- end
32
-
33
- autocall(:ErrorHandler) do
34
- CLI::Kit::ErrorHandler.new(
35
- log_file: LOG_FILE,
36
- exception_reporter: nil
37
- )
38
- end
23
+ Config = CLI::Kit::Config.new(tool_name: TOOL_NAME)
24
+ Command = CLI::Kit::BaseCommand
25
+ Logger = CLI::Kit::Logger.new(debug_log_file: DEBUG_LOG_FILE)
26
+
27
+ Executor = CLI::Kit::Executor.new(log_file: LOG_FILE)
28
+ Resolver = CLI::Kit::Resolver.new(
29
+ tool_name: TOOL_NAME,
30
+ command_registry: Gen::Commands::Registry,
31
+ )
32
+
33
+ ErrorHandler = CLI::Kit::ErrorHandler.new(log_file: LOG_FILE)
39
34
  end
@@ -49,7 +49,7 @@ deps.each do |dep|
49
49
  dirty = false
50
50
 
51
51
  Dir.chdir(path) do
52
- _, _, stat = Open3.capture3('git fetch origin master')
52
+ _, _, stat = Open3.capture3('git fetch origin main')
53
53
  bail("couldn't git fetch in dependency: {{yellow:#{dep}}}") unless stat.success?
54
54
 
55
55
  head_sha, stat = Open3.capture2('git rev-parse HEAD')
@@ -68,7 +68,7 @@ deps.each do |dep|
68
68
  "Copying files from {{yellow:#{path}}} to satisfy dependency {{yellow:#{dep}}}.\n" \
69
69
  " However, the repo at {{yellow:#{path}}} isn't up to date.\n" \
70
70
  " The checked-out revision is {{yellow:#{head_sha[0..8]}}}, and "\
71
- "{{yellow:origin/master}} is {{yellow:#{fetch_head_sha[0..8]}}}.\n" \
71
+ "{{yellow:origin/main}} is {{yellow:#{fetch_head_sha[0..8]}}}.\n" \
72
72
  " Unless you know what you're doing, you should {{green:cd}} to that repo and {{green:git pull}}, then run this again."
73
73
  )
74
74
  end
@@ -2,10 +2,7 @@ require '__app__'
2
2
 
3
3
  module __App__
4
4
  module Commands
5
- Registry = CLI::Kit::CommandRegistry.new(
6
- default: 'help',
7
- contextual_resolver: nil
8
- )
5
+ Registry = CLI::Kit::CommandRegistry.new(default: 'help')
9
6
 
10
7
  def self.register(const, cmd, path)
11
8
  autoload(const, path)
@@ -4,8 +4,6 @@ require 'cli/kit'
4
4
  CLI::UI::StdoutRouter.enable
5
5
 
6
6
  module __App__
7
- extend CLI::Kit::Autocall
8
-
9
7
  TOOL_NAME = '__app__'
10
8
  ROOT = File.expand_path('../..', __FILE__)
11
9
  LOG_FILE = '/tmp/__app__.log'
@@ -13,21 +11,14 @@ module __App__
13
11
  autoload(:EntryPoint, '__app__/entry_point')
14
12
  autoload(:Commands, '__app__/commands')
15
13
 
16
- autocall(:Config) { CLI::Kit::Config.new(tool_name: TOOL_NAME) }
17
- autocall(:Command) { CLI::Kit::BaseCommand }
14
+ Config = CLI::Kit::Config.new(tool_name: TOOL_NAME)
15
+ Command = CLI::Kit::BaseCommand
18
16
 
19
- autocall(:Executor) { CLI::Kit::Executor.new(log_file: LOG_FILE) }
20
- autocall(:Resolver) do
21
- CLI::Kit::Resolver.new(
22
- tool_name: TOOL_NAME,
23
- command_registry: __App__::Commands::Registry
24
- )
25
- end
17
+ Executor = CLI::Kit::Executor.new(log_file: LOG_FILE)
18
+ Resolver = CLI::Kit::Resolver.new(
19
+ tool_name: TOOL_NAME,
20
+ command_registry: __App__::Commands::Registry
21
+ )
26
22
 
27
- autocall(:ErrorHandler) do
28
- CLI::Kit::ErrorHandler.new(
29
- log_file: LOG_FILE,
30
- exception_reporter: nil
31
- )
32
- end
23
+ ErrorHandler = CLI::Kit::ErrorHandler.new(log_file: LOG_FILE)
33
24
  end
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module __App__
4
- class ExampleTest < MiniTest::Test
4
+ class ExampleTest < Minitest::Test
5
5
  include CLI::Kit::Support::TestHelper
6
6
 
7
7
  def test_example