cli-kit 1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 194c6fb318a717adb92458539db84f4e82c0c7d8
4
+ data.tar.gz: ed39204760603166cb42990fef86db6f69f1e44a
5
+ SHA512:
6
+ metadata.gz: 325c4f7a072a9004275079d04b9925ef791a4d450ac63d757f9bf294234551a4b8f946e817d4287b146849d6823d46ae7a22a732b616bb1a2ceb1cbcd24f3fe2
7
+ data.tar.gz: 0507ec4b45eddf96c736c4b552a45bea2528cdff30666006c86d5796d71503b1e25ec9dea415d1b1220b86a055a29a12535e94ed671dec5719301a2e55ee7670
@@ -0,0 +1,14 @@
1
+ *.gem
2
+ build
3
+ .vagrant
4
+ .DS_Store
5
+ .bundle
6
+
7
+ .starscope.db
8
+ cscope.out
9
+ tags
10
+
11
+ .byebug_history
12
+
13
+ .rubocop-*
14
+ doc
@@ -0,0 +1,23 @@
1
+ inherit_from:
2
+ - http://shopify.github.io/ruby-style-guide/rubocop.yml
3
+
4
+ AllCops:
5
+ Exclude:
6
+ - 'vendor/**/*'
7
+ TargetRubyVersion: 2.0
8
+
9
+ # This doesn't understand that <<~ doesn't exist in 2.0
10
+ Style/IndentHeredoc:
11
+ Enabled: false
12
+
13
+ # This doesn't take into account retrying from an exception
14
+ Lint/HandleExceptions:
15
+ Enabled: false
16
+
17
+ # allow String.new to create mutable strings
18
+ Style/EmptyLiteral:
19
+ Enabled: false
20
+
21
+ # allow the use of globals which makes sense in a CLI app like this
22
+ Style/GlobalVars:
23
+ Enabled: false
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ before_install: gem install bundler -v 1.15.0
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # NOTE: These are development-only dependencies
2
+ source "https://rubygems.org"
3
+
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'rubocop'
8
+ gem 'byebug'
9
+ gem 'method_source'
10
+ end
11
+
12
+ group :test do
13
+ gem 'mocha', require: false
14
+ gem 'minitest', '>= 5.0.0', require: false
15
+ gem 'minitest-reporters', require: false
16
+ end
@@ -0,0 +1,57 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dev-kit (0.1.0)
5
+ dev-ui (>= 0.1.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ansi (1.5.0)
11
+ ast (2.3.0)
12
+ builder (3.2.3)
13
+ byebug (9.0.6)
14
+ dev-ui (0.1.0)
15
+ metaclass (0.0.4)
16
+ method_source (0.8.2)
17
+ minitest (5.10.2)
18
+ minitest-reporters (1.1.14)
19
+ ansi
20
+ builder
21
+ minitest (>= 5.0)
22
+ ruby-progressbar
23
+ mocha (1.2.1)
24
+ metaclass (~> 0.0.1)
25
+ parallel (1.11.2)
26
+ parser (2.4.0.0)
27
+ ast (~> 2.2)
28
+ powerpack (0.1.1)
29
+ rainbow (2.2.2)
30
+ rake
31
+ rake (10.5.0)
32
+ rubocop (0.49.1)
33
+ parallel (~> 1.10)
34
+ parser (>= 2.3.3.1, < 3.0)
35
+ powerpack (~> 0.1)
36
+ rainbow (>= 1.99.1, < 3.0)
37
+ ruby-progressbar (~> 1.7)
38
+ unicode-display_width (~> 1.0, >= 1.0.1)
39
+ ruby-progressbar (1.8.1)
40
+ unicode-display_width (1.3.0)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ bundler (~> 1.15)
47
+ byebug
48
+ dev-kit!
49
+ method_source
50
+ minitest (>= 5.0.0)
51
+ minitest-reporters
52
+ mocha
53
+ rake (~> 10.0)
54
+ rubocop
55
+
56
+ BUNDLED WITH
57
+ 1.16.0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Burke Libbey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ # cli-kit
2
+
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cli/kit"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ root = File.expand_path('../..', __FILE__)
7
+ CLI_TEST_ROOT = root + '/test'
8
+
9
+ $LOAD_PATH.unshift(CLI_TEST_ROOT)
10
+
11
+ def test_files
12
+ Dir.glob(CLI_TEST_ROOT + "/**/*_test.rb")
13
+ end
14
+
15
+ if ARGV.empty?
16
+ test_files.each { |f| require(f) }
17
+ exit 0
18
+ end
19
+
20
+ # A list of files is presumed to be specified
21
+ ARGV.each do |a|
22
+ require a.sub(%r{^test/}, '')
23
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "cli/kit/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cli-kit"
8
+ spec.version = CLI::Kit::VERSION
9
+ spec.authors = ["Burke Libbey", "Julian Nadeau"]
10
+ spec.email = ["burke.libbey@shopify.com", "julian.nadeau@shopify.com"]
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"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_runtime_dependency "cli-ui", ">= 1.0.0"
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"
29
+ end
data/dev.yml ADDED
@@ -0,0 +1,7 @@
1
+ up:
2
+ - ruby: 2.3.3
3
+ - bundler
4
+
5
+ commands:
6
+ test: bin/testunit
7
+ style: "bundle exec rubocop -D"
@@ -0,0 +1,7 @@
1
+ require 'cli/ui'
2
+
3
+ module CLI
4
+ module Kit
5
+ autoload :System, 'cli/kit/system'
6
+ end
7
+ end
@@ -0,0 +1,204 @@
1
+ require 'cli/kit'
2
+
3
+ require 'open3'
4
+ require 'English'
5
+
6
+ module CLI
7
+ module Kit
8
+ module System
9
+ SUDO_PROMPT = CLI::UI.fmt("{{info:(sudo)}} Password: ")
10
+ class << self
11
+
12
+ # Ask for sudo access with a message explaning the need for it
13
+ # Will make subsequent commands capable of running with sudo for a period of time
14
+ #
15
+ # #### Parameters
16
+ # - `msg`: A message telling the user why sudo is needed
17
+ #
18
+ # #### Usage
19
+ # `ctx.sudo_reason("We need to do a thing")`
20
+ #
21
+ def sudo_reason(msg)
22
+ # See if sudo has a cached password
23
+ `env SUDO_ASKPASS=/usr/bin/false sudo -A true`
24
+ return if $CHILD_STATUS.success?
25
+ CLI::UI.with_frame_color(:blue) do
26
+ puts(CLI::UI.fmt("{{i}} #{msg}"))
27
+ end
28
+ end
29
+
30
+ # Execute a command in the user's environment
31
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
32
+ # Captures the results of the command without output to the console
33
+ #
34
+ # #### Parameters
35
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
36
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
37
+ # - `env`: process environment with which to execute this command
38
+ # - `**kwargs`: additional arguments to pass to Open3.capture2
39
+ #
40
+ # #### Returns
41
+ # - `output`: output (STDOUT) of the command execution
42
+ # - `status`: boolean success status of the command execution
43
+ #
44
+ # #### Usage
45
+ # `out, stat = CLI::Kit::System.capture2('ls', 'a_folder')`
46
+ #
47
+ def capture2(*a, sudo: false, env: ENV, **kwargs)
48
+ delegate_open3(*a, sudo: sudo, env: env, method: :capture2, **kwargs)
49
+ end
50
+
51
+ # Execute a command in the user's environment
52
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
53
+ # Captures the results of the command without output to the console
54
+ #
55
+ # #### Parameters
56
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
57
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
58
+ # - `env`: process environment with which to execute this command
59
+ # - `**kwargs`: additional arguments to pass to Open3.capture2e
60
+ #
61
+ # #### Returns
62
+ # - `output`: output (STDOUT merged with STDERR) of the command execution
63
+ # - `status`: boolean success status of the command execution
64
+ #
65
+ # #### Usage
66
+ # `out_and_err, stat = CLI::Kit::System.capture2e('ls', 'a_folder')`
67
+ #
68
+ def capture2e(*a, sudo: false, env: ENV, **kwargs)
69
+ delegate_open3(*a, sudo: sudo, env: env, method: :capture2e, **kwargs)
70
+ end
71
+
72
+ # Execute a command in the user's environment
73
+ # This is meant to be largely equivalent to backticks, only with the env passed in.
74
+ # Captures the results of the command without output to the console
75
+ #
76
+ # #### Parameters
77
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
78
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
79
+ # - `env`: process environment with which to execute this command
80
+ # - `**kwargs`: additional arguments to pass to Open3.capture3
81
+ #
82
+ # #### Returns
83
+ # - `output`: STDOUT of the command execution
84
+ # - `error`: STDERR of the command execution
85
+ # - `status`: boolean success status of the command execution
86
+ #
87
+ # #### Usage
88
+ # `out, err, stat = CLI::Kit::System.capture3('ls', 'a_folder')`
89
+ #
90
+ def capture3(*a, sudo: false, env: ENV, **kwargs)
91
+ delegate_open3(*a, sudo: sudo, env: env, method: :capture3, **kwargs)
92
+ end
93
+
94
+ # Execute a command in the user's environment
95
+ # Outputs result of the command without capturing it
96
+ #
97
+ # #### Parameters
98
+ # - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
99
+ # - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
100
+ # - `env`: process environment with which to execute this command
101
+ # - `**kwargs`: additional keyword arguments to pass to Process.spawn
102
+ #
103
+ # #### Returns
104
+ # - `status`: boolean success status of the command execution
105
+ #
106
+ # #### Usage
107
+ # `stat = CLI::Kit::System.system('ls', 'a_folder')`
108
+ #
109
+ def system(*a, sudo: false, env: ENV, **kwargs)
110
+ a = apply_sudo(*a, sudo)
111
+
112
+ out_r, out_w = IO.pipe
113
+ err_r, err_w = IO.pipe
114
+ in_stream = STDIN.closed? ? :close : STDIN
115
+ pid = Process.spawn(env, *resolve_path(a, env), 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
116
+ out_w.close
117
+ err_w.close
118
+
119
+ handlers = if block_given?
120
+ { out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') },
121
+ err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) }, }
122
+ else
123
+ { out_r => ->(data) { STDOUT.write(data) },
124
+ err_r => ->(data) { STDOUT.write(data) }, }
125
+ end
126
+
127
+ previous_trailing = Hash.new('')
128
+ loop do
129
+ ios = [err_r, out_r].reject(&:closed?)
130
+ break if ios.empty?
131
+
132
+ readers, = IO.select(ios)
133
+ readers.each do |io|
134
+ begin
135
+ data, trailing = split_partial_characters(io.readpartial(4096))
136
+ handlers[io].call(previous_trailing[io] + data)
137
+ previous_trailing[io] = trailing
138
+ rescue IOError
139
+ io.close
140
+ end
141
+ end
142
+ end
143
+
144
+ Process.wait(pid)
145
+ $CHILD_STATUS
146
+ end
147
+
148
+ # Split off trailing partial UTF-8 Characters. UTF-8 Multibyte characters start with a 11xxxxxx byte that tells
149
+ # how many following bytes are part of this character, followed by some number of 10xxxxxx bytes. This simple
150
+ # algorithm will split off a whole trailing multi-byte character.
151
+ def split_partial_characters(data)
152
+ last_byte = data.getbyte(-1)
153
+ return [data, ''] if (last_byte & 0b1000_0000).zero?
154
+
155
+ # UTF-8 is up to 6 characters per rune, so we could never want to trim more than that, and we want to avoid
156
+ # allocating an array for the whole of data with bytes
157
+ min_bound = -[6, data.bytesize].min
158
+ final_bytes = data.byteslice(min_bound..-1).bytes
159
+ partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 }
160
+ # Bail out for non UTF-8
161
+ return [data, ''] unless partial_character_sub_index
162
+ partial_character_index = min_bound + partial_character_sub_index
163
+
164
+ [data.byteslice(0...partial_character_index), data.byteslice(partial_character_index..-1)]
165
+ end
166
+
167
+ private
168
+
169
+ def apply_sudo(*a, sudo)
170
+ a.unshift('sudo', '-S', '-p', SUDO_PROMPT, '--') if sudo
171
+ sudo_reason(sudo) if sudo.is_a?(String)
172
+ a
173
+ end
174
+
175
+ def delegate_open3(*a, sudo: raise, env: raise, method: raise, **kwargs)
176
+ a = apply_sudo(*a, sudo)
177
+ Open3.send(method, env, *resolve_path(a, env), **kwargs)
178
+ rescue Errno::EINTR
179
+ raise(Errno::EINTR, "command interrupted: #{a.join(' ')}")
180
+ end
181
+
182
+ # Ruby resolves the program to execute using its own PATH, but we want it to
183
+ # use the provided one, so we ensure ruby chooses to spawn a shell, which will
184
+ # parse our command and properly spawn our target using the provided environment.
185
+ #
186
+ # This is important because dev clobbers its own environment such that ruby
187
+ # means /usr/bin/ruby, but we want it to select the ruby targeted by the active
188
+ # project.
189
+ #
190
+ # See https://github.com/Shopify/dev/pull/625 for more details.
191
+ def resolve_path(a, env)
192
+ # If only one argument was provided, make sure it's interpreted by a shell.
193
+ return ["true ; " + a[0]] if a.size == 1
194
+ return a if a.first.include?('/')
195
+ item = env.fetch('PATH', '').split(':').detect do |f|
196
+ File.exist?("#{f}/#{a.first}")
197
+ end
198
+ a[0] = "#{item}/#{a.first}" if item
199
+ a
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,5 @@
1
+ module CLI
2
+ module Kit
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cli-kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Burke Libbey
8
+ - Julian Nadeau
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2017-12-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: cli-ui
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 1.0.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 1.0.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.15'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.15'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '10.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '5.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '5.0'
70
+ description: Terminal UI framework extensions
71
+ email:
72
+ - burke.libbey@shopify.com
73
+ - julian.nadeau@shopify.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".gitignore"
79
+ - ".rubocop.yml"
80
+ - ".travis.yml"
81
+ - Gemfile
82
+ - Gemfile.lock
83
+ - LICENSE.txt
84
+ - README.md
85
+ - bin/console
86
+ - bin/testunit
87
+ - cli-kit.gemspec
88
+ - dev.yml
89
+ - lib/cli/kit.rb
90
+ - lib/cli/kit/system.rb
91
+ - lib/cli/kit/version.rb
92
+ homepage: https://github.com/shopify/cli-kit
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.6.14
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Terminal UI framework extensions
116
+ test_files: []