cli-kit 3.0.0.pre → 3.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.
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