cli-kit 3.0.0.pre → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d38479bdf5b6e03725e44c04e6b44c306bad6a59
4
- data.tar.gz: 4268a52a77172588b586e1e5e21cee635e7ed99a
3
+ metadata.gz: 88690eeba1179ef8cb31ebd13b68d978e71f5c8a
4
+ data.tar.gz: f63da3dc52f120f759241232fb64953deb0e0b3f
5
5
  SHA512:
6
- metadata.gz: ddd5316dc74a06d96a35ad8953b198de161b1700822f15ff62e9987e46957fc493a7c6af1e605d435235567805a2bc230d4d287677a372619d3653e12c31e66e
7
- data.tar.gz: e61a9cac88db2728fd81c5d782288788a8b6a95579e00b3c27b0c86129e0afcee3fbdde18104e3c8beb997fc9d9ee9190f7de6e4f22570679b93f8ed494ad1d7
6
+ metadata.gz: abd4591db97ad44a9ebdf3645181ad90b443ad70d3849b3f247cf577cc394b04638646d14ab1cc18273d05a3a55cb1cdb3022ce9052792b025a7498c6d94a309
7
+ data.tar.gz: 6c8571d319162adfeb0dd66c294fb225565c10e50fdcb70ab60dcd4bbd705131ec43916691c8ce160939348491ec453d5b1054a2504bc13594e76aecc20cdf01
@@ -2,12 +2,17 @@ inherit_from:
2
2
  - http://shopify.github.io/ruby-style-guide/rubocop.yml
3
3
 
4
4
  AllCops:
5
- Exclude:
6
- - 'vendor/**/*'
7
- TargetRubyVersion: 2.0
5
+ Exclude: [ 'gen/template/**/*' ]
6
+ TargetRubyVersion: 2.3
7
+
8
+ Style/FrozenStringLiteralComment:
9
+ Enabled: false
10
+
11
+ Shopify/RubocopComments:
12
+ Enabled: false
8
13
 
9
14
  # This doesn't understand that <<~ doesn't exist in 2.0
10
- Style/IndentHeredoc:
15
+ Layout/IndentHeredoc:
11
16
  Enabled: false
12
17
 
13
18
  # This doesn't take into account retrying from an exception
@@ -21,3 +26,15 @@ Style/EmptyLiteral:
21
26
  # allow the use of globals which makes sense in a CLI app like this
22
27
  Style/GlobalVars:
23
28
  Enabled: false
29
+
30
+ # allow using %r{} for regexes
31
+ Style/RegexpLiteral:
32
+ Enabled: false
33
+
34
+ # allow readable Dev::Util.begin formatting
35
+ Style/MultilineBlockChain:
36
+ Enabled: false
37
+
38
+ # allow using names to be more expressive
39
+ Performance/RedundantBlockCall:
40
+ Enabled: false
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cli-kit (2.0.0)
5
- cli-ui (>= 1.0.0)
4
+ cli-kit (3.0.0)
5
+ cli-ui (>= 1.1.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
@@ -11,7 +11,7 @@ GEM
11
11
  ast (2.3.0)
12
12
  builder (3.2.3)
13
13
  byebug (9.0.6)
14
- cli-ui (1.0.0)
14
+ cli-ui (1.1.0)
15
15
  metaclass (0.0.4)
16
16
  method_source (0.8.2)
17
17
  minitest (5.10.2)
@@ -1,29 +1,29 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
2
+ lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "cli/kit/version"
4
+ require 'cli/kit/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "cli-kit"
7
+ spec.name = 'cli-kit'
8
8
  spec.version = CLI::Kit::VERSION
9
- spec.authors = ["Burke Libbey", "Julian Nadeau"]
10
- spec.email = ["burke.libbey@shopify.com", "julian.nadeau@shopify.com"]
9
+ spec.authors = ['Burke Libbey', 'Julian Nadeau', 'Lisa Ugray']
10
+ spec.email = ['burke.libbey@shopify.com', 'julian.nadeau@shopify.com', 'lisa.ugray@shopify.com']
11
11
 
12
- spec.summary = %q{Terminal UI framework extensions}
13
- spec.description = %q{Terminal UI framework extensions}
14
- spec.homepage = "https://github.com/shopify/cli-kit"
15
- spec.license = "MIT"
12
+ spec.summary = 'Terminal UI framework extensions'
13
+ spec.description = 'Terminal UI framework extensions'
14
+ spec.homepage = 'https://github.com/shopify/cli-kit'
15
+ spec.license = 'MIT'
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
18
  f.match(%r{^(test|spec|features)/})
19
19
  end
20
- spec.bindir = "exe"
20
+ spec.bindir = 'exe'
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
- spec.require_paths = ["lib"]
22
+ spec.require_paths = ['lib']
23
23
 
24
- spec.add_runtime_dependency "cli-ui", ">= 1.0.0"
24
+ spec.add_runtime_dependency 'cli-ui', '>= 1.1.0'
25
25
 
26
- spec.add_development_dependency "bundler", "~> 1.15"
27
- spec.add_development_dependency "rake", "~> 10.0"
28
- spec.add_development_dependency "minitest", "~> 5.0"
26
+ spec.add_development_dependency 'bundler', '~> 1.15'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
28
+ spec.add_development_dependency 'minitest', '~> 5.0'
29
29
  end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby --disable-gems
2
+
3
+ Encoding.default_external = Encoding::UTF_8
4
+ Encoding.default_internal = Encoding::UTF_8
5
+
6
+ unshift_path = ->(path) {
7
+ p = File.expand_path("../../#{path}", __FILE__)
8
+ $LOAD_PATH.unshift(p) unless $LOAD_PATH.include?(p)
9
+ }
10
+ unshift_path.call('gen/lib')
11
+ unshift_path.call('lib')
12
+
13
+ require 'gen'
14
+
15
+ exit(Gen::ErrorHandler.call { Gen::EntryPoint.call(ARGV) })
@@ -0,0 +1,35 @@
1
+ require 'cli/ui'
2
+ require 'cli/kit'
3
+
4
+ CLI::UI::StdoutRouter.enable
5
+
6
+ module Gen
7
+ extend CLI::Kit::Autocall
8
+
9
+ TOOL_NAME = 'cli-kit'
10
+ ROOT = File.expand_path('../../..', __FILE__)
11
+ LOG_FILE = '/tmp/cli-kit.log'
12
+
13
+ autoload(:Generator, 'gen/generator')
14
+
15
+ autoload(:EntryPoint, 'gen/entry_point')
16
+ autoload(:Commands, 'gen/commands')
17
+
18
+ autocall(:Config) { CLI::Kit::Config.new(tool_name: TOOL_NAME) }
19
+ autocall(:Command) { CLI::Kit::BaseCommand }
20
+
21
+ autocall(:Executor) { CLI::Kit::Executor.new(log_file: LOG_FILE) }
22
+ autocall(:Resolver) do
23
+ CLI::Kit::Resolver.new(
24
+ tool_name: TOOL_NAME,
25
+ command_registry: Gen::Commands::Registry
26
+ )
27
+ end
28
+
29
+ autocall(:ErrorHandler) do
30
+ CLI::Kit::ErrorHandler.new(
31
+ log_file: LOG_FILE,
32
+ exception_reporter: nil
33
+ )
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ require 'gen'
2
+
3
+ module Gen
4
+ module Commands
5
+ Registry = CLI::Kit::CommandRegistry.new(
6
+ default: 'help',
7
+ contextual_resolver: nil
8
+ )
9
+
10
+ def self.register(const, cmd, path)
11
+ autoload(const, path)
12
+ Registry.add(->() { const_get(const) }, cmd)
13
+ end
14
+
15
+ register :Help, 'help', 'gen/commands/help'
16
+ register :New, 'new', 'gen/commands/new'
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ require 'gen'
2
+
3
+ module Gen
4
+ module Commands
5
+ class Help < Gen::Command
6
+ def call(_args, _name)
7
+ puts CLI::UI.fmt("{{bold:Available commands}}")
8
+ puts ""
9
+
10
+ Gen::Commands::Registry.resolved_commands.each do |name, klass|
11
+ puts CLI::UI.fmt("{{command:#{Gen::TOOL_NAME} #{name}}}")
12
+ if help = klass.help
13
+ puts CLI::UI.fmt(help)
14
+ end
15
+ puts ""
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ require 'gen'
2
+
3
+ module Gen
4
+ module Commands
5
+ 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
12
+
13
+ Gen::Generator.run(project)
14
+ end
15
+
16
+ def self.help
17
+ "Generate a new cli-kit project.\nUsage: {{command:#{Gen::TOOL_NAME} new <name>}}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ require 'gen'
2
+
3
+ module Gen
4
+ module EntryPoint
5
+ def self.call(args)
6
+ cmd, command_name, args = Gen::Resolver.call(args)
7
+ Gen::Executor.call(cmd, command_name, args)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,163 @@
1
+ require 'gen'
2
+ require 'fileutils'
3
+ require 'open3'
4
+ require 'pathname'
5
+ require 'tmpdir'
6
+
7
+ module Gen
8
+ class Generator
9
+ def self.run(project_name)
10
+ new(project_name).run
11
+ end
12
+
13
+ TEMPLATE_ROOT = File.expand_path('gen/template', Gen::ROOT)
14
+
15
+ VALID_PROJECT_NAME = /\A[a-z][a-z0-9]*\z/
16
+ private_constant :VALID_PROJECT_NAME
17
+
18
+ # false -> delete file
19
+ # string -> rename file before applying template substitutions
20
+ VENDOR_TRANSLATIONS = {
21
+ 'Gemfile' => false,
22
+ 'exe/__app__-gems' => false,
23
+ 'exe/__app__-vendor' => 'exe/__app__',
24
+ 'dev-gems.yml' => false,
25
+ 'dev-vendor.yml' => 'dev.yml',
26
+ }.freeze
27
+ private_constant :VENDOR_TRANSLATIONS
28
+
29
+ BUNDLER_TRANSLATIONS = {
30
+ 'bin' => false,
31
+ 'bin/update-deps' => false,
32
+ 'exe/__app__-gems' => 'exe/__app__',
33
+ 'exe/__app__-vendor' => false,
34
+ 'dev-gems.yml' => 'dev.yml',
35
+ 'dev-vendor.yml' => false,
36
+ }.freeze
37
+ private_constant :BUNDLER_TRANSLATIONS
38
+
39
+ def initialize(project_name)
40
+ raise(
41
+ CLI::Kit::Abort,
42
+ "project name must match {{bold:#{VALID_PROJECT_NAME}}} (but can be changed later)"
43
+ ) unless project_name =~ VALID_PROJECT_NAME
44
+ @project_name = project_name
45
+ @title_case_project_name = @project_name.sub(/^./, &:upcase)
46
+ end
47
+
48
+ def run
49
+ vendor = ask_vendor?
50
+ create_project_dir
51
+ if vendor
52
+ copy_files(translations: VENDOR_TRANSLATIONS)
53
+ update_deps
54
+ else
55
+ copy_files(translations: BUNDLER_TRANSLATIONS)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def ask_vendor?
62
+ vendor = nil
63
+ CLI::UI::Frame.open('Configuration') do
64
+ q = 'How would you like the application to consume {{command:cli-kit}} and {{command:cli-ui}}?'
65
+ vendor = CLI::UI::Prompt.ask(q) do |c|
66
+ c.option('Vendor {{italic:(faster execution, more difficult to update deps)}}') { 'vendor' }
67
+ c.option('Bundler {{italic:(slower execution, easier dep management)}}') { 'bundler' }
68
+ end
69
+ end
70
+ vendor == 'vendor'
71
+ end
72
+
73
+ def create_project_dir
74
+ info(create: '')
75
+ FileUtils.mkdir(@project_name)
76
+ rescue Errno::EEXIST
77
+ error("directory already exists: #{@project_name}")
78
+ end
79
+
80
+ def copy_files(translations:)
81
+ each_template_file do |source_name|
82
+ target_name = translations.fetch(source_name, source_name)
83
+ next if target_name == false
84
+ target_name = apply_template_variables(target_name)
85
+
86
+ source = File.join(TEMPLATE_ROOT, source_name)
87
+ target = File.join(@project_name, target_name)
88
+
89
+ info(create: target_name)
90
+
91
+ if Dir.exist?(source)
92
+ FileUtils.mkdir(target)
93
+ else
94
+ content = apply_template_variables(File.read(source))
95
+ File.write(target, content)
96
+ end
97
+ File.chmod(File.stat(source).mode, target)
98
+ end
99
+ end
100
+
101
+ def update_deps
102
+ Dir.mktmpdir do |tmp|
103
+ clone(tmp, 'cli-ui')
104
+ clone(tmp, 'cli-kit')
105
+ info(run: 'bin/update-deps')
106
+ Dir.chdir(@project_name) do
107
+ system({ 'SOURCE_ROOT' => tmp }, 'bin/update-deps')
108
+ end
109
+ end
110
+ end
111
+
112
+ def clone(dir, repo)
113
+ info(clone: repo)
114
+ out, stat = Open3.capture2e('git', '-C', dir, 'clone', "https://github.com/shopify/#{repo}")
115
+ unless stat.success?
116
+ STDERR.puts(out)
117
+ error("git clone failed")
118
+ end
119
+ end
120
+
121
+ def each_template_file
122
+ return enum_for(:each_template_file) unless block_given?
123
+
124
+ root = Pathname.new(TEMPLATE_ROOT)
125
+ Dir.glob("#{TEMPLATE_ROOT}/**/*").each do |f|
126
+ el = Pathname.new(f)
127
+ yield(el.relative_path_from(root).to_s)
128
+ end
129
+ end
130
+
131
+ def apply_template_variables(s)
132
+ s
133
+ .gsub(/__app__/, @project_name)
134
+ .gsub(/__App__/, @title_case_project_name)
135
+ .gsub(/__cli-kit-version__/, cli_kit_version)
136
+ .gsub(/__cli-ui-version__/, cli_ui_version)
137
+ end
138
+
139
+ def cli_kit_version
140
+ require 'cli/kit/version'
141
+ CLI::Kit::VERSION.to_s
142
+ end
143
+
144
+ def cli_ui_version
145
+ require 'cli/ui/version'
146
+ CLI::UI::VERSION.to_s
147
+ end
148
+
149
+ def info(create: nil, clone: nil, run: nil)
150
+ if clone
151
+ puts(CLI::UI.fmt("\t{{bold:{{yellow:clone}}\t#{clone}}}"))
152
+ elsif create
153
+ puts(CLI::UI.fmt("\t{{bold:{{blue:create}}\t#{create}}}"))
154
+ elsif run
155
+ puts(CLI::UI.fmt("\t{{bold:{{green:run}}\t#{run}}}"))
156
+ end
157
+ end
158
+
159
+ def error(msg)
160
+ raise(CLI::Kit::Abort, msg)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,2 @@
1
+ /Gemfile.lock
2
+ /.bundle
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'cli-kit', '~> __cli-kit-version__'
4
+ gem 'cli-ui', '~> __cli-ui-version__'
@@ -0,0 +1 @@
1
+ # __app__
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby --disable-gems
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../../vendor/deps/cli-ui/lib", __FILE__))
4
+ require 'open3'
5
+ require 'fileutils'
6
+
7
+ def fmt(tag, msg)
8
+ # Not really CLI::UI.fmt compatible: no nesting support, for example.
9
+ # While we could pull in CLI::UI here, it makes it more difficult to
10
+ # bootstrap a new project and to fix a broken vendor.
11
+ fmt_msg = msg
12
+ .gsub(/{{yellow:(.*?)}}/, "\x1b[33m\\1\x1b[31m")
13
+ .gsub(/{{green:(.*?)}}/, "\x1b[32m\\1\x1b[31m")
14
+ .gsub(/{{blue:(.*?)}}/, "\x1b[34m\\1\x1b[31m")
15
+ .gsub(/{{bold_blue:(.*?)}}/, "\x1b[1;34m\\1\x1b[0;31m")
16
+ .gsub(/{{bold_green:(.*?)}}/, "\x1b[1;32m\\1\x1b[0;31m")
17
+ "\x1b[1;31m[#{tag}] \x1b[0;31m#{fmt_msg}\x1b[0m"
18
+ end
19
+
20
+ def bail(msg)
21
+ STDERR.puts(fmt("ERROR", msg))
22
+ exit 1
23
+ end
24
+
25
+ def warn(msg)
26
+ STDERR.puts(fmt("WARNING", msg))
27
+ end
28
+
29
+ def source_path
30
+ File.expand_path(ENV.fetch('SOURCE_ROOT', File.expand_path('../../..', __FILE__)))
31
+ end
32
+
33
+ deps = %w(cli-ui cli-kit)
34
+
35
+ deps.each do |dep|
36
+ path = File.expand_path(dep, source_path)
37
+
38
+ unless Dir.exist?(path)
39
+ bail(
40
+ "dependency is not checked out: {{yellow:#{dep}}}.\n" \
41
+ " This repo {{bold_blue:(github.com/shopify/#{dep})}} must be cloned at {{bold_blue:#{path}}} for this script to succeed.\n" \
42
+ " Alternatively, you can set {{bold_blue:SOURCE_ROOT}} to a directory containing {{yellow:#{dep}}}.\n" \
43
+ " {{bold_blue:SOURCE_ROOT}} defaults to {{bold_blue:../}}."
44
+ )
45
+ end
46
+
47
+ head_sha = nil
48
+ dirty = false
49
+
50
+ Dir.chdir(path) do
51
+ _, _, stat = Open3.capture3('git fetch origin master')
52
+ bail("couldn't git fetch in dependency: {{yellow:#{dep}}}") unless stat.success?
53
+
54
+ head_sha, stat = Open3.capture2('git rev-parse HEAD')
55
+ bail("couldn't determine HEAD: {{yellow:#{dep}}}") unless stat.success?
56
+ head_sha.chomp!
57
+
58
+ fetch_head_sha, stat = Open3.capture2('git rev-parse FETCH_HEAD')
59
+ bail("couldn't determine FETCH_HEAD: {{yellow:#{dep}}}") unless stat.success?
60
+ fetch_head_sha.chomp!
61
+
62
+ git_status, stat = Open3.capture2('git status --porcelain')
63
+ bail("couldn't determine git status: {{yellow:#{dep}}}") unless stat.success?
64
+
65
+ if head_sha != fetch_head_sha
66
+ warn(
67
+ "Copying files from {{yellow:#{path}}} to satisfy dependency {{yellow:#{dep}}}.\n" \
68
+ " However, the repo at {{yellow:#{path}}} isn't up to date.\n" \
69
+ " The checked-out revision is {{yellow:#{head_sha[0..8]}}}, and "\
70
+ "{{yellow:origin/master}} is {{yellow:#{fetch_head_sha[0..8]}}}.\n" \
71
+ " Unless you know what you're doing, you should {{green:cd}} to that repo and {{green:git pull}}, then run this again."
72
+ )
73
+ end
74
+
75
+ unless git_status.chomp.empty?
76
+ dirty = true
77
+ warn("importing uncommitted changes from dependency: {{yellow:#{dep}}}")
78
+ end
79
+ end
80
+
81
+ depdir = File.expand_path("../../vendor/deps/#{dep}", __FILE__)
82
+ FileUtils.rm_rf(depdir)
83
+ FileUtils.mkdir_p(depdir)
84
+ dstlib = File.expand_path('lib', depdir)
85
+ srclib = File.expand_path('lib', path)
86
+
87
+ FileUtils.cp_r(srclib, dstlib)
88
+
89
+ rev = head_sha
90
+ rev << " (dirty)" if dirty
91
+ rev << "\n"
92
+
93
+ File.write("#{depdir}/REVISION", rev)
94
+ end
@@ -0,0 +1,3 @@
1
+ up:
2
+ - ruby: 2.5.0
3
+ - bundler
@@ -0,0 +1,4 @@
1
+ up:
2
+ - ruby: 2.5.0
3
+
4
+ build: bin/update-deps
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ Encoding.default_external = Encoding::UTF_8
4
+ Encoding.default_internal = Encoding::UTF_8
5
+
6
+ unshift_path = ->(path) {
7
+ p = File.expand_path("../../#{path}", __FILE__)
8
+ $LOAD_PATH.unshift(p) unless $LOAD_PATH.include?(p)
9
+ }
10
+ unshift_path.call('lib')
11
+
12
+ require 'bundler/setup'
13
+ require '__app__'
14
+
15
+ exit(__App__::ErrorHandler.call do
16
+ __App__::EntryPoint.call(ARGV.dup)
17
+ end)
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby --disable-gems
2
+
3
+ Encoding.default_external = Encoding::UTF_8
4
+ Encoding.default_internal = Encoding::UTF_8
5
+
6
+ unshift_path = ->(path) {
7
+ p = File.expand_path("../../#{path}", __FILE__)
8
+ $LOAD_PATH.unshift(p) unless $LOAD_PATH.include?(p)
9
+ }
10
+ unshift_path.call('vendor/deps/cli-ui/lib')
11
+ unshift_path.call('vendor/deps/cli-kit/lib')
12
+ unshift_path.call('lib')
13
+
14
+ require '__app__'
15
+
16
+ exit(__App__::ErrorHandler.call do
17
+ __App__::EntryPoint.call(ARGV.dup)
18
+ end)
@@ -0,0 +1,33 @@
1
+ require 'cli/ui'
2
+ require 'cli/kit'
3
+
4
+ CLI::UI::StdoutRouter.enable
5
+
6
+ module __App__
7
+ extend CLI::Kit::Autocall
8
+
9
+ TOOL_NAME = '__app__'
10
+ ROOT = File.expand_path('../..', __FILE__)
11
+ LOG_FILE = '/tmp/__app__.log'
12
+
13
+ autoload(:EntryPoint, '__app__/entry_point')
14
+ autoload(:Commands, '__app__/commands')
15
+
16
+ autocall(:Config) { CLI::Kit::Config.new(tool_name: TOOL_NAME) }
17
+ autocall(:Command) { CLI::Kit::BaseCommand }
18
+
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
26
+
27
+ autocall(:ErrorHandler) do
28
+ CLI::Kit::ErrorHandler.new(
29
+ log_file: LOG_FILE,
30
+ exception_reporter: nil
31
+ )
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ require '__app__'
2
+
3
+ module __App__
4
+ module Commands
5
+ Registry = CLI::Kit::CommandRegistry.new(
6
+ default: 'help',
7
+ contextual_resolver: nil
8
+ )
9
+
10
+ def self.register(const, cmd, path)
11
+ autoload(const, path)
12
+ Registry.add(->() { const_get(const) }, cmd)
13
+ end
14
+
15
+ register :Example, 'example', '__app__/commands/example'
16
+ register :Help, 'help', '__app__/commands/help'
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ require '__app__'
2
+ require 'json'
3
+
4
+ module __App__
5
+ module Commands
6
+ class Example < __App__::Command
7
+ def call(_args, _name)
8
+ puts 'neato'
9
+
10
+ if rand < 0.05
11
+ raise(CLI::Kit::Abort, "you got unlucky!")
12
+ end
13
+ end
14
+
15
+ def self.help
16
+ "A dummy command.\nUsage: {{command:#{__App__::TOOL_NAME} example}}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ require '__app__'
2
+
3
+ module __App__
4
+ module Commands
5
+ class Help < __App__::Command
6
+ def call(args, _name)
7
+ puts CLI::UI.fmt("{{bold:Available commands}}")
8
+ puts ""
9
+
10
+ __App__::Commands::Registry.resolved_commands.each do |name, klass|
11
+ next if name == 'help'
12
+ puts CLI::UI.fmt("{{command:#{__App__::TOOL_NAME} #{name}}}")
13
+ if help = klass.help
14
+ puts CLI::UI.fmt(help)
15
+ end
16
+ puts ""
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ require '__app__'
2
+
3
+ module __App__
4
+ module EntryPoint
5
+ def self.call(args)
6
+ cmd, command_name, args = __App__::Resolver.call(args)
7
+ __App__::Executor.call(cmd, command_name, args)
8
+ end
9
+ end
10
+ end
@@ -3,6 +3,7 @@ require 'cli/kit/ruby_backports/enumerable'
3
3
 
4
4
  module CLI
5
5
  module Kit
6
+ autoload :Autocall, 'cli/kit/autocall'
6
7
  autoload :BaseCommand, 'cli/kit/base_command'
7
8
  autoload :CommandRegistry, 'cli/kit/command_registry'
8
9
  autoload :Config, 'cli/kit/config'
@@ -0,0 +1,21 @@
1
+ require 'cli/kit'
2
+
3
+ module CLI
4
+ module Kit
5
+ module Autocall
6
+ def autocall(const, &block)
7
+ @autocalls ||= {}
8
+ @autocalls[const] = block
9
+ end
10
+
11
+ def const_missing(const)
12
+ block = begin
13
+ @autocalls.fetch(const)
14
+ rescue KeyError
15
+ return super
16
+ end
17
+ const_set(const, block.call)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -7,11 +7,11 @@ module CLI
7
7
  true
8
8
  end
9
9
 
10
- def self.statsd_increment(metric, **kwargs)
10
+ def self.statsd_increment(_metric, **_kwargs)
11
11
  nil
12
12
  end
13
13
 
14
- def self.statsd_time(metric, **kwargs)
14
+ def self.statsd_time(_metric, **_kwargs)
15
15
  yield
16
16
  end
17
17
 
@@ -31,7 +31,7 @@ module CLI
31
31
  end
32
32
  end
33
33
 
34
- def call(args, command_name)
34
+ def call(_args, _command_name)
35
35
  raise NotImplementedError
36
36
  end
37
37
 
@@ -19,11 +19,11 @@ module CLI
19
19
  end
20
20
  end
21
21
 
22
- def initialize(default:, contextual_resolver: NullContextualResolver)
22
+ def initialize(default:, contextual_resolver: nil)
23
23
  @commands = {}
24
24
  @aliases = {}
25
25
  @default = default
26
- @contextual_resolver = contextual_resolver
26
+ @contextual_resolver = contextual_resolver || NullContextualResolver
27
27
  end
28
28
 
29
29
  def resolved_commands
@@ -45,47 +45,48 @@ module CLI
45
45
  aliases[from] = to unless aliases[from]
46
46
  end
47
47
 
48
- def resolve_command(name)
49
- resolve_global_command(name) || \
50
- resolve_contextual_command(name) || \
51
- [nil, resolve_alias(name)]
48
+ def command_names
49
+ @contextual_resolver.command_names + commands.keys
50
+ end
51
+
52
+ def exist?(name)
53
+ !resolve_command(name).first.nil?
52
54
  end
53
55
 
56
+ private
57
+
54
58
  def resolve_alias(name)
55
59
  aliases[name] || @contextual_resolver.aliases.fetch(name, name)
56
60
  end
57
61
 
58
- def resolve_global_command(name)
62
+ def resolve_command(name)
59
63
  name = resolve_alias(name)
60
- klass = resolve_class(commands.fetch(name, ""))
61
- return nil unless klass.defined? # (BaseCommand)
64
+ resolve_global_command(name) || \
65
+ resolve_contextual_command(name) || \
66
+ [nil, name]
67
+ end
68
+
69
+ def resolve_global_command(name)
70
+ klass = resolve_class(commands.fetch(name, nil))
71
+ return nil unless klass
62
72
  [klass, name]
63
73
  rescue NameError
64
74
  nil
65
75
  end
66
76
 
67
77
  def resolve_contextual_command(name)
68
- name = resolve_alias(name)
69
78
  found = @contextual_resolver.command_names.include?(name)
70
79
  return nil unless found
71
80
  [@contextual_resolver.command_class(name), name]
72
81
  end
73
82
 
74
- def command_names
75
- @contextual_resolver.command_names + commands.keys
76
- end
77
-
78
- def exist?(name)
79
- !resolve_command(name).first.nil?
80
- end
81
-
82
- private
83
-
84
83
  def resolve_class(class_or_proc)
85
84
  if class_or_proc.is_a?(Class)
86
85
  class_or_proc
87
- else
86
+ elsif class_or_proc.respond_to?(:call)
88
87
  class_or_proc.call
88
+ else
89
+ class_or_proc
89
90
  end
90
91
  end
91
92
  end
@@ -1,25 +1,34 @@
1
1
  require 'cli/kit'
2
+ require 'English'
2
3
 
3
4
  module CLI
4
5
  module Kit
5
6
  class ErrorHandler
6
- def initialize(log_file: nil, exception_reporter: NullExceptionReporter)
7
+ def initialize(log_file:, exception_reporter:)
7
8
  @log_file = log_file
8
- @exception_reporter_or_proc = exception_reporter
9
+ @exception_reporter_or_proc = exception_reporter || NullExceptionReporter
9
10
  end
10
11
 
11
12
  module NullExceptionReporter
12
- def self.report(exception, logs)
13
+ def self.report(_exception, _logs)
13
14
  nil
14
15
  end
15
16
  end
16
17
 
18
+ def call(&block)
19
+ install!
20
+ handle_abort(&block)
21
+ end
22
+
23
+ private
24
+
17
25
  def install!
18
26
  at_exit { handle_final_exception(@exception || $ERROR_INFO) }
19
27
  end
20
28
 
21
29
  def handle_abort
22
30
  yield
31
+ CLI::Kit::EXIT_SUCCESS
23
32
  rescue CLI::Kit::GenericAbort => e
24
33
  is_bug = e.is_a?(CLI::Kit::Bug) || e.is_a?(CLI::Kit::BugSilent)
25
34
  is_silent = e.is_a?(CLI::Kit::AbortSilent) || e.is_a?(CLI::Kit::BugSilent)
@@ -29,12 +38,10 @@ module CLI
29
38
 
30
39
  CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
31
40
  rescue Interrupt
32
- STDERR.puts(format_error_message("Interrupt"))
41
+ $stderr.puts(format_error_message("Interrupt"))
33
42
  return CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
34
43
  end
35
44
 
36
- private
37
-
38
45
  def handle_final_exception(error)
39
46
  notify_with = nil
40
47
 
@@ -47,13 +54,13 @@ module CLI
47
54
  unless skip.include?(error.message)
48
55
  notify_with = error
49
56
  end
50
- when SystemExit # "exit N" called
57
+ when SystemExit # "exit N" called
51
58
  case error.status
52
- when CLI::Kit::EXIT_SUCCESS # submit nothing if it was `exit 0`
59
+ when CLI::Kit::EXIT_SUCCESS # submit nothing if it was `exit 0`
53
60
  when CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
54
- # if it was `exit 30`, translate the exit code to 1, and submit nothing
61
+ # if it was `exit 30`, translate the exit code to 1, and submit nothing.
55
62
  # 30 is used to signal normal failures that are not indicative of bugs.
56
- # But users should see it presented as 1.
63
+ # However, users should see it presented as 1.
57
64
  exit 1
58
65
  else
59
66
  # A weird termination status happened. `error.exception "message"` will maintain backtrace
@@ -70,7 +77,7 @@ module CLI
70
77
  rescue => e
71
78
  "(#{e.class}: #{e.message})"
72
79
  end
73
- exceptiono_reporter.report(notify_with, logs)
80
+ exception_reporter.report(notify_with, logs)
74
81
  end
75
82
  end
76
83
 
@@ -86,9 +93,8 @@ module CLI
86
93
  CLI::UI.fmt("{{red:#{msg}}}")
87
94
  end
88
95
 
89
-
90
96
  def print_error_message(e)
91
- STDERR.puts(format_error_message(e.message))
97
+ $stderr.puts(format_error_message(e.message))
92
98
  end
93
99
  end
94
100
  end
@@ -4,82 +4,50 @@ require 'English'
4
4
  module CLI
5
5
  module Kit
6
6
  class Executor
7
- def initialize(
8
- tool_name:, command_registry:, error_handler:, log_file: nil
9
- )
10
- @tool_name = tool_name
11
- @command_registry = command_registry
12
- @error_handler = error_handler
7
+ def initialize(log_file:)
13
8
  @log_file = log_file
14
9
  end
15
10
 
11
+ def call(command, command_name, args)
12
+ with_traps { with_logging { command.call(args, command_name) } }
13
+ end
14
+
15
+ private
16
+
16
17
  def with_logging(&block)
17
18
  return yield unless @log_file
18
19
  CLI::UI.log_output_to(@log_file, &block)
19
20
  end
20
21
 
21
- def commands_and_aliases
22
- @command_registry.command_names + @command_registry.aliases.keys
23
- end
24
-
25
- def trap_signals
26
- trap('QUIT') do
27
- z = caller
28
- CLI::UI.raw do
29
- STDERR.puts('SIGQUIT: quit')
30
- STDERR.puts(z)
31
- end
32
- exit 1
33
- end
34
- trap('INFO') do
35
- z = caller
36
- CLI::UI.raw do
37
- STDERR.puts('SIGINFO:')
38
- STDERR.puts(z)
39
- # Thread.list.map { |t| t.backtrace }
22
+ def with_traps
23
+ twrap('QUIT', method(:quit_handler)) do
24
+ twrap('INFO', method(:info_handler)) do
25
+ yield
40
26
  end
41
27
  end
42
28
  end
43
29
 
44
- def call(command, command_name, args)
45
- trap_signals
46
- with_logging do
47
- @error_handler.handle_abort do
48
- if command.nil?
49
- command_not_found(command_name)
50
- raise CLI::Kit::AbortSilent # Already output message
51
- end
52
- command.call(args, command_name)
53
- CLI::Kit::EXIT_SUCCESS # unless an exception was raised
54
- end
55
- end
30
+ def twrap(signal, handler)
31
+ prev_handler = trap(signal, handler)
32
+ yield
33
+ ensure
34
+ trap(signal, prev_handler)
56
35
  end
57
36
 
58
- def command_not_found(name)
59
- CLI::UI::Frame.open("Command not found", color: :red, timing: false) do
60
- STDERR.puts(CLI::UI.fmt("{{command:#{@tool_name} #{name}}} was not found"))
37
+ def quit_handler(_sig)
38
+ z = caller
39
+ CLI::UI.raw do
40
+ $stderr.puts('SIGQUIT: quit')
41
+ $stderr.puts(z)
61
42
  end
43
+ exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
44
+ end
62
45
 
63
- cmds = commands_and_aliases
64
- if cmds.all? { |cmd| cmd.is_a?(String) }
65
- possible_matches = cmds.min_by(2) do |cmd|
66
- CLI::Kit::Levenshtein.distance(cmd, name)
67
- end
68
-
69
- # We don't want to match against any possible command
70
- # so reject anything that is too far away
71
- possible_matches.reject! do |possible_match|
72
- CLI::Kit::Levenshtein.distance(possible_match, name) > 3
73
- end
74
-
75
- # If we have any matches left, tell the user
76
- if possible_matches.any?
77
- CLI::UI::Frame.open("{{bold:Did you mean?}}", timing: false, color: :blue) do
78
- possible_matches.each do |possible_match|
79
- STDERR.puts CLI::UI.fmt("{{command:#{@tool_name} #{possible_match}}}")
80
- end
81
- end
82
- end
46
+ def info_handler(_sig)
47
+ z = caller
48
+ CLI::UI.raw do
49
+ $stderr.puts('SIGINFO:')
50
+ $stderr.puts(z)
83
51
  end
84
52
  end
85
53
  end
@@ -3,21 +3,57 @@ require 'cli/kit'
3
3
  module CLI
4
4
  module Kit
5
5
  class Resolver
6
- def initialize(command_registry:, error_handler:)
6
+ def initialize(tool_name:, command_registry:)
7
+ @tool_name = tool_name
7
8
  @command_registry = command_registry
8
- @error_handler = error_handler
9
9
  end
10
10
 
11
11
  def call(args)
12
12
  args = args.dup
13
13
  command_name = args.shift
14
14
 
15
- @error_handler.handle_abort do
16
- command, command_name = @command_registry.lookup_command(command_name)
17
- return [command, command_name, args]
15
+ command, resolved_name = @command_registry.lookup_command(command_name)
16
+
17
+ if command.nil?
18
+ command_not_found(command_name)
19
+ raise CLI::Kit::AbortSilent # Already output message
20
+ end
21
+
22
+ [command, resolved_name, args]
23
+ end
24
+
25
+ private
26
+
27
+ def command_not_found(name)
28
+ CLI::UI::Frame.open("Command not found", color: :red, timing: false) do
29
+ $stderr.puts(CLI::UI.fmt("{{command:#{@tool_name} #{name}}} was not found"))
18
30
  end
19
31
 
20
- exit CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
32
+ cmds = commands_and_aliases
33
+ if cmds.all? { |cmd| cmd.is_a?(String) }
34
+ possible_matches = cmds.min_by(2) do |cmd|
35
+ CLI::Kit::Levenshtein.distance(cmd, name)
36
+ end
37
+
38
+ # We don't want to match against any possible command
39
+ # so reject anything that is too far away
40
+ possible_matches.reject! do |possible_match|
41
+ CLI::Kit::Levenshtein.distance(possible_match, name) > 3
42
+ end
43
+
44
+ # If we have any matches left, tell the user
45
+ if possible_matches.any?
46
+ CLI::UI::Frame.open("{{bold:Did you mean?}}", timing: false, color: :blue) do
47
+ possible_matches.each do |possible_match|
48
+ $stderr.puts CLI::UI.fmt("{{command:#{@tool_name} #{possible_match}}}")
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def commands_and_aliases
56
+ @command_registry.command_names + @command_registry.aliases.keys
21
57
  end
22
58
  end
23
59
  end
@@ -1,5 +1,5 @@
1
1
  module Enumerable
2
- def min_by(n=nil, &block)
2
+ def min_by(n = nil, &block)
3
3
  return sort_by(&block).first unless n
4
4
  sort_by(&block).first(n)
5
5
  end if instance_method(:min_by).arity == 0
@@ -8,7 +8,6 @@ module CLI
8
8
  module System
9
9
  SUDO_PROMPT = CLI::UI.fmt("{{info:(sudo)}} Password: ")
10
10
  class << self
11
-
12
11
  # Ask for sudo access with a message explaning the need for it
13
12
  # Will make subsequent commands capable of running with sudo for a period of time
14
13
  #
@@ -1,5 +1,5 @@
1
1
  module CLI
2
2
  module Kit
3
- VERSION = "3.0.0.pre"
3
+ VERSION = "3.0.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cli-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.pre
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  - Julian Nadeau
9
+ - Lisa Ugray
9
10
  autorequire:
10
11
  bindir: exe
11
12
  cert_chain: []
12
- date: 2018-02-21 00:00:00.000000000 Z
13
+ date: 2018-02-27 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: cli-ui
@@ -17,14 +18,14 @@ dependencies:
17
18
  requirements:
18
19
  - - ">="
19
20
  - !ruby/object:Gem::Version
20
- version: 1.0.0
21
+ version: 1.1.0
21
22
  type: :runtime
22
23
  prerelease: false
23
24
  version_requirements: !ruby/object:Gem::Requirement
24
25
  requirements:
25
26
  - - ">="
26
27
  - !ruby/object:Gem::Version
27
- version: 1.0.0
28
+ version: 1.1.0
28
29
  - !ruby/object:Gem::Dependency
29
30
  name: bundler
30
31
  requirement: !ruby/object:Gem::Requirement
@@ -71,7 +72,9 @@ description: Terminal UI framework extensions
71
72
  email:
72
73
  - burke.libbey@shopify.com
73
74
  - julian.nadeau@shopify.com
74
- executables: []
75
+ - lisa.ugray@shopify.com
76
+ executables:
77
+ - cli-kit
75
78
  extensions: []
76
79
  extra_rdoc_files: []
77
80
  files:
@@ -86,7 +89,28 @@ files:
86
89
  - bin/testunit
87
90
  - cli-kit.gemspec
88
91
  - dev.yml
92
+ - exe/cli-kit
93
+ - gen/lib/gen.rb
94
+ - gen/lib/gen/commands.rb
95
+ - gen/lib/gen/commands/help.rb
96
+ - gen/lib/gen/commands/new.rb
97
+ - gen/lib/gen/entry_point.rb
98
+ - gen/lib/gen/generator.rb
99
+ - gen/template/.gitignore
100
+ - gen/template/Gemfile
101
+ - gen/template/README.md
102
+ - gen/template/bin/update-deps
103
+ - gen/template/dev-gems.yml
104
+ - gen/template/dev-vendor.yml
105
+ - gen/template/exe/__app__-gems
106
+ - gen/template/exe/__app__-vendor
107
+ - gen/template/lib/__app__.rb
108
+ - gen/template/lib/__app__/commands.rb
109
+ - gen/template/lib/__app__/commands/example.rb
110
+ - gen/template/lib/__app__/commands/help.rb
111
+ - gen/template/lib/__app__/entry_point.rb
89
112
  - lib/cli/kit.rb
113
+ - lib/cli/kit/autocall.rb
90
114
  - lib/cli/kit/base_command.rb
91
115
  - lib/cli/kit/command_registry.rb
92
116
  - lib/cli/kit/config.rb
@@ -113,9 +137,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
113
137
  version: '0'
114
138
  required_rubygems_version: !ruby/object:Gem::Requirement
115
139
  requirements:
116
- - - ">"
140
+ - - ">="
117
141
  - !ruby/object:Gem::Version
118
- version: 1.3.1
142
+ version: '0'
119
143
  requirements: []
120
144
  rubyforge_project:
121
145
  rubygems_version: 2.6.14