dockerun 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -11
  3. data/Rakefile +0 -2
  4. data/Rakefile.bak +8 -0
  5. data/dockerspec.sample +104 -0
  6. data/{dockerun.gemspec → dockerun.gemspec.bak} +20 -22
  7. data/exe/dockerun +8 -115
  8. data/lib/dockerun/cli/command.rb +133 -0
  9. data/lib/dockerun/cli/command_factory.rb +313 -0
  10. data/lib/dockerun/cli/command_result.rb +67 -0
  11. data/lib/dockerun/cli/delete_container.rb +55 -0
  12. data/lib/dockerun/cli/delete_image.rb +99 -0
  13. data/lib/dockerun/cli/run.rb +39 -0
  14. data/lib/dockerun/cli/user_info.rb +39 -0
  15. data/lib/dockerun/cli_engine.rb +104 -0
  16. data/lib/dockerun/config.rb +40 -70
  17. data/lib/dockerun/context/rubygems.rb +142 -0
  18. data/lib/dockerun/dfile.rb +204 -0
  19. data/lib/dockerun/docker_cli.rb +124 -0
  20. data/lib/dockerun/dsl.rb +291 -0
  21. data/lib/dockerun/version.rb +1 -1
  22. data/lib/dockerun.rb +31 -24
  23. data/sig/dockerun.rbs +4 -0
  24. metadata +62 -73
  25. data/.release_history.yml +0 -56
  26. data/Dockerfile.dockerun +0 -38
  27. data/Gemfile +0 -15
  28. data/Gemfile.lock +0 -94
  29. data/bin/console +0 -15
  30. data/bin/setup +0 -8
  31. data/lib/dockerun/bundler_helper.rb +0 -25
  32. data/lib/dockerun/cli_prompt.rb +0 -18
  33. data/lib/dockerun/command/dockerun.rb +0 -30
  34. data/lib/dockerun/command/init.rb +0 -79
  35. data/lib/dockerun/command/remove_container.rb +0 -99
  36. data/lib/dockerun/command/reset_image.rb +0 -107
  37. data/lib/dockerun/command/run.rb +0 -221
  38. data/lib/dockerun/command/run_new_container.rb +0 -94
  39. data/lib/dockerun/command/run_new_image.rb +0 -89
  40. data/lib/dockerun/command/stop_container.rb +0 -90
  41. data/lib/dockerun/docker_command_factory_helper.rb +0 -19
  42. data/lib/dockerun/docker_container_helper.rb +0 -204
  43. data/lib/dockerun/docker_image_helper.rb +0 -381
  44. data/lib/dockerun/template/general_template_writer.rb +0 -14
  45. data/lib/dockerun/template/jruby_template_writer.rb +0 -24
  46. data/lib/dockerun/template/template_engine.rb +0 -27
  47. data/lib/dockerun/template/template_writer.rb +0 -102
  48. data/lib/dockerun/user_info.rb +0 -37
  49. data/template/Dockerfile_general.erb +0 -41
  50. data/template/setup_ruby_devenv.rb.erb +0 -22
@@ -0,0 +1,313 @@
1
+
2
+ require 'erb'
3
+
4
+ require_relative 'command'
5
+ require_relative 'user_info'
6
+
7
+ module Dockerun
8
+ module Cli
9
+ class CommandFactory
10
+ include TR::CondUtils
11
+
12
+ def build_image(name = "", opts = { }, &block)
13
+
14
+ opts = { } if opts.nil?
15
+ cmd = []
16
+ cmd << DockerCli.docker_exe
17
+ cmd << "build"
18
+ if not_empty?(name)
19
+ cmd << "-t #{name}"
20
+ end
21
+
22
+ if not_empty?(opts[:dockerfile])
23
+ cmd << "-f #{opts[:dockerfile]}"
24
+ end
25
+
26
+ root = opts[:context_root]
27
+ root = "." if is_empty?(root)
28
+ cmd << root
29
+
30
+ logger.debug "Build Image : #{cmd.join(" ")}"
31
+ Command.new(cmd)
32
+
33
+ end # build_image
34
+
35
+
36
+ def find_image(name, tag = "", opts = { })
37
+ name = "" if name.nil?
38
+ cmd = []
39
+ cmd << DockerCli.docker_exe
40
+ cmd << "images"
41
+ cmd << "-q"
42
+ if not_empty?(tag)
43
+ cmd << "#{name}:#{tag}"
44
+ else
45
+ cmd << name
46
+ end
47
+
48
+ logger.debug "Find image: #{cmd.join(" ")}"
49
+
50
+ Command.new(cmd)
51
+ end # find_image
52
+
53
+ def delete_image(name, tag = "", opts = { })
54
+
55
+ if not_empty?(name)
56
+
57
+ cmd = []
58
+ cmd << DockerCli.docker_exe
59
+ cmd << "rmi"
60
+ if not_empty?(tag)
61
+ cmd << "#{name}:#{tag}"
62
+ else
63
+ cmd << name
64
+ end
65
+
66
+ logger.debug "Delete image: #{cmd.join(" ")}"
67
+ Command.new(cmd)
68
+
69
+ else
70
+ raise Error, "Name is required for delete operation"
71
+ end
72
+
73
+ end # delete_image
74
+
75
+
76
+ def find_running_container(name, opts = { })
77
+
78
+ raise Error, "Name is mandatory" if is_empty?(name)
79
+
80
+ cmd = []
81
+ cmd << DockerCli.docker_exe
82
+ cmd << "ps"
83
+ cmd << "-q"
84
+ cmd << "-f"
85
+ cmd << "name=\"#{name}\""
86
+
87
+ logger.debug "Find container: #{cmd.join(" ")}"
88
+
89
+ Command.new(cmd)
90
+
91
+ end
92
+
93
+ # Find from container even if it is already stopped
94
+ def find_from_all_container(name, opts = { })
95
+ raise Error, "Name is required" if is_empty?(name)
96
+ cmd = []
97
+ cmd << DockerCli.docker_exe
98
+ cmd << "ps"
99
+ # return all info instead of only the container ID
100
+ #cmd << "-a"
101
+ cmd << "-aq"
102
+ cmd << "-f"
103
+ # From little testing seems the command by default already support regex formatting
104
+ # So can use the regex marker to get exact match
105
+ # e.g. if want exact match, pass in ^#{name}\z
106
+ cmd << "name=\"#{name}\""
107
+
108
+ logger.debug "Find from all container: #{cmd.join(" ")}"
109
+ Command.new(cmd)
110
+ end
111
+
112
+
113
+ def find_container_names_by_image_name(image_name, opts = { })
114
+
115
+ raise Error, "Image name is mandatory" if is_empty?(image_name)
116
+
117
+ cmd = []
118
+ cmd << DockerCli.docker_exe
119
+ cmd << "ps"
120
+ cmd << "-a" if opts[:all_containers] == true
121
+ cmd << "-f"
122
+ cmd << "ancestor=\"#{image_name}\""
123
+ cmd << "--format \"{{.Names}}\""
124
+
125
+ logger.debug "Find container by image name: #{cmd.join(" ")}"
126
+
127
+ Command.new(cmd)
128
+
129
+ end
130
+
131
+
132
+ #
133
+ # Create container from image directly
134
+ # e.g. > docker run -it <image> "/bin/bash"
135
+ #
136
+ def create_container_from_image(image, opts = { })
137
+ opts = {} if opts.nil?
138
+ cmd = []
139
+ cmd << DockerCli.docker_exe
140
+ cmd << "run"
141
+ cmd << "-i" if opts[:interactive] == true
142
+ cmd << "-t" if opts[:tty] == true
143
+ cmd << "-d" if opts[:detached] == true
144
+ cmd << "--rm" if opts[:del] == true
145
+ if not (opts[:container_name].nil? or opts[:container_name].empty?)
146
+ cmd << "--name \"#{opts[:container_name]}\""
147
+ end
148
+
149
+ cmd << process_mount(opts)
150
+ cmd << process_port(opts)
151
+
152
+ if opts[:match_user] == true
153
+ ui = UserInfo.user_info
154
+ gi = UserInfo.group_info
155
+ cmd << "-u #{ui[:uid]}:#{gi[:gid]}"
156
+ end
157
+
158
+ cmd << image
159
+
160
+ if not_empty?(opts[:command])
161
+ cmd << "\"#{opts[:command]}\""
162
+ end
163
+
164
+ interactive = false
165
+ interactive = true if opts[:interactive] or opts[:tty]
166
+
167
+ logger.debug "Run Container from Image : #{cmd.join(" ")}"
168
+ Command.new(cmd, (interactive ? true : false))
169
+ end # run_container_from_image
170
+
171
+ def start_container(container, opts = { })
172
+
173
+ opts = {} if opts.nil?
174
+ cmd = []
175
+ cmd << DockerCli.docker_exe
176
+ cmd << "container"
177
+ cmd << "start"
178
+ cmd << container
179
+
180
+ logger.debug "Start Container : #{cmd.join(" ")}"
181
+ Command.new(cmd)
182
+ end
183
+
184
+ def attach_container(container, opts = { })
185
+
186
+ opts = {} if opts.nil?
187
+ cmd = []
188
+ cmd << DockerCli.docker_exe
189
+ cmd << "container"
190
+ cmd << "attach"
191
+ cmd << container
192
+
193
+ logger.debug "Attach Container : #{cmd.join(" ")}"
194
+ # this is a bit difficult to juggle
195
+ # it depending on the previous docker configuration
196
+ # but to be save, just open up a new terminal
197
+ Command.new(cmd, true)
198
+ end
199
+
200
+
201
+ def stop_container(container, opts = { })
202
+
203
+ cmd = []
204
+ cmd << DockerCli.docker_exe
205
+ cmd << "container"
206
+ cmd << "stop"
207
+ cmd << container
208
+
209
+ logger.debug "Stop Container : #{cmd.join(" ")}"
210
+ Command.new(cmd)
211
+ end
212
+
213
+
214
+ def delete_container(container, opts = { })
215
+
216
+ cmd = []
217
+ cmd << DockerCli.docker_exe
218
+ cmd << "container"
219
+ cmd << "rm"
220
+ cmd << container
221
+
222
+ logger.debug "Delete Container : #{cmd.join(" ")}"
223
+ Command.new(cmd)
224
+ end
225
+
226
+
227
+ def run_command_in_running_container(container, command, opts = { })
228
+ cmd = []
229
+ cmd << DockerCli.docker_exe
230
+ cmd << "container"
231
+ cmd << "exec"
232
+
233
+ isTty = false
234
+ isInteractive = false
235
+ if not_empty?(opts[:tty]) and opts[:tty] == true
236
+ cmd << "-t"
237
+ isTty = true
238
+ end
239
+ if not_empty?(opts[:interactive]) and opts[:interactive] == true
240
+ cmd << "-i"
241
+ isInteractive = true
242
+ end
243
+
244
+ cmd << container
245
+
246
+ if is_empty?(command)
247
+ cmd << "/bin/bash --login"
248
+ else
249
+ cmd << command
250
+ end
251
+
252
+ logger.debug "Run command in running container : #{cmd.join(" ")}"
253
+ Command.new(cmd, ((isTty or isInteractive) ? true : false))
254
+ end # container_prompt
255
+
256
+
257
+ private
258
+ # expecting :mounts => { "/dir/local" => "/dir/inside/docker" }
259
+ def process_mount(opts)
260
+ if not_empty?(opts[:mounts]) #not (opts[:mounts].nil? or opts[:mounts].empty?)
261
+ m = opts[:mounts]
262
+ if m.is_a?(Hash)
263
+ res = []
264
+ m.each do |local, docker|
265
+ res << "-v #{local}:#{docker}"
266
+ end
267
+ res.join(" ")
268
+ end
269
+ else
270
+ ""
271
+ end
272
+
273
+ end # process_mount
274
+
275
+ def process_port(opts)
276
+ if not_empty?(opts[:ports]) #not (opts[:ports].nil? or opts[:ports].empty?)
277
+ po = opts[:ports]
278
+ res = []
279
+ if po.is_a?(Hash)
280
+ po.each do |host, docker|
281
+ res << "-p #{host}:#{docker}"
282
+ end
283
+ end
284
+ #po = [po] if not po.is_a?(Array)
285
+ #po.each do |e|
286
+ # # 1st is port on host
287
+ # # 2nd is port inside container
288
+ # res << "-p #{e.keys.first}:#{e.values.first}"
289
+ #end
290
+ res.join(" ")
291
+ else
292
+ ""
293
+ end
294
+
295
+ end
296
+
297
+ def logger
298
+ Dockerun.logger(:cmdFact)
299
+ end
300
+
301
+ def build_add_user_script
302
+ path = File.join(File.dirname(__FILE__),"..","..","..","scripts","create_user.sh.erb")
303
+ if File.exist?(path)
304
+ ui = UserInfo.user_info
305
+ gi = UserInfo.group_info
306
+
307
+ ERB.new(File.read(path)).result_with_hash({ user_group_id: gi[:gid], user_group_name: gi[:group_name], user_id: ui[:uid], user_login: ui[:login] })
308
+ end
309
+ end
310
+
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,67 @@
1
+
2
+ require 'toolrack'
3
+
4
+ module Dockerun
5
+
6
+ module Cli
7
+
8
+ class CommandResult
9
+ include TR::CondUtils
10
+
11
+ attr_reader :out, :err, :result
12
+ def initialize(result, out, err)
13
+ @result = result
14
+ @out = out
15
+ @err = err
16
+ end
17
+
18
+ def is_out_stream_empty?
19
+ is_empty?(@out)
20
+ end
21
+
22
+ def is_err_stream_empty?
23
+ is_empty?(@err)
24
+ end
25
+
26
+ def out_stream
27
+ @out.join("\n")
28
+ end
29
+
30
+ def err_stream
31
+ @err.join("\n")
32
+ end
33
+
34
+ def failed?
35
+ if @result.nil?
36
+ true
37
+ else
38
+ @result.failed?
39
+ end
40
+ end
41
+
42
+ def success?
43
+ not failed?
44
+ end
45
+ alias_method :successful?, :success?
46
+
47
+ def each_output_line(&block)
48
+ out_stream.each_line(&block)
49
+ end
50
+
51
+ def each_err_lines(&block)
52
+ err_stream.each_line(&block)
53
+ end
54
+
55
+ def output_lines
56
+ out_stream.lines
57
+ end
58
+
59
+ def err_lines
60
+ err_stream.lines
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,55 @@
1
+
2
+ require_relative 'command_factory'
3
+
4
+ module Dockerun
5
+ module Cli
6
+ class DSLProxy
7
+ include DSL
8
+ end
9
+
10
+ class DeleteContainer
11
+
12
+ def start
13
+ begin
14
+ pmt.puts " Dockerun version #{Dockerun::VERSION}".yellow
15
+ pmt.puts " Operational : Delete Container"
16
+ delete_container
17
+ rescue TTY::Reader::InputInterrupt
18
+ end
19
+ end
20
+
21
+ def delete_container
22
+ selSpec = CliEngine.select_spec
23
+ cont = File.read(selSpec)
24
+ proxy = DSLProxy.new
25
+ proxy.set_dry_run_mode
26
+ proxy.instance_eval(cont)
27
+
28
+ skip = pmt.no?(" Delete container named '#{proxy.container_name}'?")
29
+ if not skip
30
+ cf.stop_container(proxy.container_name).run
31
+ res = cf.delete_container(proxy.container_name).run
32
+ if res.success?
33
+ pmt.puts " Container '#{proxy.container_name}' deleted".green
34
+ else
35
+ pmt.puts " Container '#{proxy.container_name}' deletion failed. Error was : #{res.err_lines.join("\n")}".red
36
+ end
37
+ else
38
+ pmt.puts " Container deletion of name '#{proxy.container_name}' aborted.".yellow
39
+ end
40
+ end
41
+
42
+ def pmt
43
+ CliEngine.pmt
44
+ end
45
+
46
+ def cf
47
+ if @_cf.nil?
48
+ @_cf = CommandFactory.new
49
+ end
50
+ @_cf
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,99 @@
1
+
2
+ require_relative 'command_factory'
3
+
4
+ module Dockerun
5
+ module Cli
6
+ class DSLProxy
7
+ include DSL
8
+ end
9
+
10
+ class DeleteImage
11
+ include TR::CondUtils
12
+
13
+ def start
14
+ begin
15
+ pmt.puts " Dockerun version #{Dockerun::VERSION}".yellow
16
+ pmt.puts " Operational : Delete Image"
17
+ delete_image
18
+ rescue TTY::Reader::InputInterrupt
19
+ end
20
+ end
21
+
22
+ def delete_image
23
+ selSpec = CliEngine.select_spec
24
+ cont = File.read(selSpec)
25
+ proxy = DSLProxy.new
26
+ proxy.set_dry_run_mode
27
+ proxy.instance_eval(cont)
28
+
29
+ res = cf.find_image(proxy.image_name).run
30
+ if res.success?
31
+ if not_empty?(res.output_lines)
32
+
33
+ skip = pmt.no?(" Delete image named '#{proxy.image_name}'?")
34
+ if not skip
35
+ res = cf.find_container_names_by_image_name(proxy.image_name, all_containers: true).run
36
+ raise Error, "Failed to extract container name by image name '#{proxy.image_name}'" if not res.success?
37
+
38
+ if not_empty?(res.output_lines)
39
+ cont = []
40
+ cnt = 0
41
+ res.output_lines.each do |l|
42
+ cont << " #{cnt += 1}. #{l}"
43
+ end
44
+ skip = pmt.no?(" All the containers shall be deleted prior to image '#{proxy.image_name}' deletion. Proceed?\n#{cont.join("\n")}")
45
+ if not skip
46
+ res.output_lines.each do |ci|
47
+ cf.stop_container(ci).run
48
+ cf.delete_container(ci).run
49
+ end
50
+
51
+ res = cf.delete_image(proxy.image_name).run
52
+ if res.success?
53
+ pmt.puts " Image '#{proxy.image_name}' deleted".green
54
+ else
55
+ raise Error, " Image '#{proxy.image_name}' deletion failed. Error was : #{res.err_lines.join("\n")}"
56
+ end
57
+
58
+ else
59
+ pmt.puts " Delete of image '#{proxy.image_name}' aborted".yellow
60
+ end
61
+
62
+ else
63
+ res = cf.delete_image(proxy.image_name).run
64
+ if res.success?
65
+ pmt.puts " Image '#{proxy.image_name}' deleted".green
66
+ else
67
+ raise Error, " Image '#{proxy.image_name}' deletion failed. Error was : #{res.err_lines.join("\n")}"
68
+ end
69
+
70
+ end
71
+
72
+ else
73
+ pmt.puts " Delete of image '#{proxy.image_name}' aborted".yellow
74
+
75
+ end
76
+
77
+ else
78
+ pmt.puts " Image '#{proxy.image_name}' does not exist".green
79
+ end
80
+
81
+ else
82
+ pmt.puts " Failed to find image '#{proxy.image_name}'. Error was : #{res.err_lines.join("\n")}"
83
+ end
84
+ end
85
+
86
+ def pmt
87
+ CliEngine.pmt
88
+ end
89
+
90
+ def cf
91
+ if @_cf.nil?
92
+ @_cf = CommandFactory.new
93
+ end
94
+ @_cf
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,39 @@
1
+
2
+ require 'tty/prompt'
3
+ require 'colorize'
4
+
5
+ module Dockerun
6
+ module Cli
7
+ class Run
8
+ #include TR::ArgUtils
9
+ include DSL
10
+
11
+ def start
12
+ begin
13
+ pmt.puts " Dockerun version #{Dockerun::VERSION}".yellow
14
+ pmt.puts " Operational : Run Spec"
15
+ load_spec
16
+ rescue TTY::Reader::InputInterrupt
17
+ end
18
+ end
19
+
20
+
21
+ private
22
+ def load_spec
23
+ selSpec = CliEngine.select_spec
24
+ pmt.puts " Loading spec '#{selSpec}"
25
+ cont = File.read(selSpec)
26
+ self.instance_eval(cont)
27
+ go if not is_go_done?
28
+ end
29
+
30
+ def pmt
31
+ if @_pmt.nil?
32
+ @_pmt = TTY::Prompt.new
33
+ end
34
+ @_pmt
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+
2
+ require 'etc'
3
+
4
+ module Dockerun
5
+ module Cli
6
+
7
+ module UserInfo
8
+ include TR::CondUtils
9
+
10
+ def self.user_info(login = nil)
11
+ login = Etc.getlogin if is_empty?(login)
12
+ res = { login: login }
13
+ begin
14
+ res[:uid] = Etc.getpwnam(login).uid
15
+ rescue Exception => ex
16
+ res[:uid] = nil
17
+ end
18
+ res
19
+ end
20
+
21
+ def self.group_info(login = nil)
22
+ login = Etc.getlogin if is_empty?(login)
23
+ res = { }
24
+ begin
25
+ gnm = Etc.getgrnam(login)
26
+ res[:group_name] = gnm.name
27
+ res[:gid] = gnm.gid
28
+ rescue Exception => ex
29
+ p ex
30
+ res[:group_name] = ""
31
+ res[:gid] = nil
32
+ end
33
+ res
34
+ end
35
+
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,104 @@
1
+
2
+ require 'tty/prompt'
3
+ require_relative 'cli/run'
4
+ require_relative 'cli/delete_container'
5
+ require_relative 'cli/delete_image'
6
+
7
+ module Dockerun
8
+ class CliEngine
9
+ include TR::ArgUtils
10
+
11
+ arg_spec do
12
+ opt "i", "Initialize sample dockerspec at current directory" do
13
+ init
14
+ end
15
+ opt_alias "i", "init"
16
+
17
+ opt "r", "Run the dockerspec" do
18
+ run
19
+ end
20
+ opt_alias "r","run"
21
+
22
+ opt "dc", "Delete container" do
23
+ delete_container
24
+ end
25
+
26
+ opt "di", "Delete image and its associated container(s)" do
27
+ delete_image
28
+ end
29
+
30
+ opt 'clean', "Clean generated Dockerfile and temporary files" do
31
+ clean_env
32
+ end
33
+
34
+ opt 'help', "Command help" do
35
+ print_help
36
+ end
37
+
38
+ end
39
+
40
+ def print_help
41
+ pmt = self.class.pmt
42
+ pmt.puts ""
43
+ pmt.puts " Dockerun version #{Dockerun::VERSION}".magenta
44
+ pmt.puts ""
45
+ pmt.puts " Supported options : "
46
+ self.class.arg_options.each do |key, val|
47
+ pmt.puts " #{key}\t\t#{val[:desc]}"
48
+ end
49
+ pmt.puts ""
50
+ end
51
+
52
+ def self.select_spec
53
+ sel = Dir.entries(Dir.getwd).sort
54
+ default = sel.select { |e| (e =~ /^dockerspec/) != nil }
55
+ selSpec = pmt.select(" Please select the dockerspec to proceed : ", filter: true, default: default.first) do |m|
56
+ sel.each do |s|
57
+ next if sel == "." or sel == ".." or File.directory?(s)
58
+ m.choice s, File.expand_path(s)
59
+ end
60
+ end
61
+
62
+ selSpec
63
+ end
64
+
65
+
66
+ def init
67
+ template = File.join(File.dirname(__FILE__),"..","..","dockerspec.sample")
68
+ if File.exist?(template)
69
+ FileUtils.cp template, File.join(Dir.getwd, "dockerspec.sample")
70
+ end
71
+ end
72
+
73
+ def run
74
+ r = Dockerun::Cli::Run.new
75
+ r.start
76
+ end
77
+
78
+ def delete_container
79
+ d = Dockerun::Cli::DeleteContainer.new
80
+ d.start
81
+ end
82
+
83
+ def delete_image
84
+ d = Dockerun::Cli::DeleteImage.new
85
+ d.start
86
+ end
87
+
88
+ def clean_env
89
+ Dir.glob("Dockerfile-*").each do |f|
90
+ FileUtils.rm(f)
91
+ end
92
+ FileUtils.rm("script_for_gem.sh") if File.exist?("script_for_gem.sh")
93
+ self.class.pmt.puts " Generated Dockerfile-* and script_for_gem.sh is deleted".green
94
+ end
95
+
96
+ def self.pmt
97
+ if @_pmt.nil?
98
+ @_pmt = TTY::Prompt.new
99
+ end
100
+ @_pmt
101
+ end
102
+
103
+ end
104
+ end