cli-kit 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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