package_cloud 0.0.3 → 0.3.15

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,397 @@
1
+ require "cgi"
2
+ require "uri"
3
+
4
+ module PackageCloud
5
+ module CLI
6
+ JAVA_EXTS = ["jar", "aar", "war"]
7
+ PY_EXTS = ["gz", "bz2", "z", "tar", "egg-info", "whl", "egg"]
8
+ ANYFILE_EXTS = ["asc"]
9
+ NODE_EXTS = []
10
+ AMBIGUOUS_EXTS = ["tgz", "apk", "zip"]
11
+ SUPPORTED_EXTS = JAVA_EXTS + PY_EXTS + ANYFILE_EXTS + AMBIGUOUS_EXTS + ["gem", "deb", "rpm", "dsc"]
12
+
13
+ class Entry < Base
14
+ desc "repository SUBCMD ...ARGS", "manage repositories"
15
+ subcommand "repository", Repository
16
+
17
+ desc "distro SUBCMD ...ARGS", "manage repositories"
18
+ subcommand "distro", Distro
19
+
20
+ desc "master_token SUBCMD ...ARGS", "manage master tokens"
21
+ subcommand "master_token", MasterToken
22
+
23
+ desc "read_token SUBCMD ...ARGS", "manage read tokens"
24
+ subcommand "read_token", ReadToken
25
+
26
+ desc "gpg_key SUBCMD ...ARGS", "manage GPG keys"
27
+ subcommand "gpg_key", GpgKey
28
+
29
+ desc "promote user/repo[/distro/version] [@scope/]package_name user/destination_repo",
30
+ "promotes a package from user/repo [in dist/version] to user/destination_repo [also in dist/version]"
31
+ def promote(source_repo_desc, package_name, dest_repo_desc)
32
+ repo_name = source_repo_desc.split("/")[0..1].join("/")
33
+ dist = expand_dist_shortcut(source_repo_desc.split("/")[2..3].join("/"))
34
+
35
+ dest_repo_name = expand_dist_shortcut(dest_repo_desc.split("/")[0..1].join("/"))
36
+
37
+ if dist == "" && package_name =~ /\.gem$/
38
+ dist = "gems"
39
+ end
40
+
41
+ print "Looking for source repository at #{repo_name}... "
42
+ src_repo = client.repository(source_repo_desc)
43
+ print "success!\n"
44
+
45
+ print "Looking for destination repository at #{dest_repo_name}... "
46
+ repo = client.repository(dest_repo_desc)
47
+ print "success!\n"
48
+
49
+ if package_name.include?('@')
50
+ scope, unscoped_package_name = package_scope(package_name)
51
+
52
+ print "Attempting to promote scoped package #{scope}/#{unscoped_package_name} from #{repo_name}/#{dist} to #{dest_repo_name}..."
53
+ packages = src_repo.promote(dist, unscoped_package_name, dest_repo_name, scope)
54
+ else
55
+ print "Attempting to promote #{repo_name}/#{dist}/#{package_name} to #{dest_repo_name}..."
56
+ packages = src_repo.promote(dist, package_name, dest_repo_name)
57
+ end
58
+ puts "done!".color(:green)
59
+
60
+ if dist == "node/1"
61
+ puts "WARNING: This Node.js package will NOT be downloadable by clients until a dist tag is created. Read more: https://packagecloud.io/docs/#node_promote".color(:yellow)
62
+ end
63
+ end
64
+
65
+ desc "yank user/repo[/distro/version] [@scope/]package_name",
66
+ "yank package from user/repo [in dist/version]"
67
+ def yank(repo_desc, package_name)
68
+ ARGV.clear # otherwise gets explodes
69
+
70
+ # strip os/dist
71
+ repo_name = repo_desc.split("/")[0..1].join("/")
72
+ dist = expand_dist_shortcut(repo_desc.split("/")[2..3].join("/"))
73
+
74
+ if dist == "" && package_name =~ /\.gem$/
75
+ dist = "gems"
76
+ end
77
+
78
+ print "Looking for repository at #{repo_name}... "
79
+ repo = client.repository(repo_desc)
80
+ print "success!\n"
81
+
82
+ if package_name.include?('@')
83
+ scope, unscoped_package_name = package_scope(package_name)
84
+ print "Attempting to yank scoped package at #{repo_name}/#{dist} #{package_name}..."
85
+ repo.yank(dist, unscoped_package_name, scope)
86
+ else
87
+ print "Attempting to yank package at #{repo_name}/#{dist}/#{package_name}..."
88
+ repo.yank(dist, package_name)
89
+ end
90
+ puts "done!".color(:green)
91
+
92
+ if dist == "node/1"
93
+ puts "WARNING: Deleting Node.js packages can have unexpected side effects with dist tags. Read more: https://packagecloud.io/docs/#node_delete".color(:yellow)
94
+ end
95
+ end
96
+
97
+ desc "push user/repo[/distro/version] /path/to/packages",
98
+ "Push package(s) to repository (in distro/version, if required). Optional settings shown above."
99
+
100
+ option "skip-file-ext-validation", :type => :boolean,
101
+ :desc => "Skip checking validation of the file extension. Package upload will be attempted even if the extension is unrecognized."
102
+
103
+ option "yes", :type => :boolean,
104
+ :desc => "Automatically answer 'yes' prompted during package push. Useful for automating uploads."
105
+
106
+ option "skip-errors", :type => :boolean,
107
+ :desc => "Skip errors encountered during a package push and continue pushing the next package."
108
+
109
+ option "skip-duplicates", :type => :boolean,
110
+ :desc => "Skip packages which already exist and continue pushing the next package."
111
+
112
+ option "coordinates", :type => :string,
113
+ :desc => "Specify the exact maven coordinates to use for a JAR. Useful for JARs without coordinates, 'fat JARs', and WARs."
114
+
115
+ option "upload-before-push",
116
+ :type => :boolean,
117
+ :desc => "Upload package before push",
118
+ :default => false
119
+
120
+ def push(repo, package_file, *package_files)
121
+ total_time = Benchmark.measure do
122
+ ARGV.clear # otherwise gets explodes
123
+ package_files << package_file
124
+
125
+ exts = package_files.map { |f| f.split(".").last }.uniq
126
+
127
+ if package_files.length > 1 && exts.length > 1
128
+ abort("You can't push multiple packages of different types at the same time.\nFor example, use *.deb to push all your debs at once.".color(:red))
129
+ end
130
+
131
+ invalid_packages = package_files.select do |f|
132
+ !SUPPORTED_EXTS.include?(f.split(".").last.downcase)
133
+ end
134
+
135
+ if JAVA_EXTS.include?(exts.first.downcase) && options.has_key?("coordinates")
136
+ puts "Using coordinates #{options["coordinates"].color(:yellow)}"
137
+ end
138
+
139
+ if !options.has_key?("skip-file-ext-validation") && invalid_packages.any?
140
+ message = "I don't know how to push these packages:\n\n".color(:red)
141
+ invalid_packages.each do |p|
142
+ message << " #{p}\n"
143
+ end
144
+ message << "\npackage_cloud only supports node.js, deb, gem, java, python, "\
145
+ "rpm, alpine, helm, anyfile (.zip, .asc) packages".color(:red)
146
+ abort(message)
147
+ end
148
+
149
+ if !options.has_key?("yes") && exts.first == "gem" && package_files.length > 1
150
+ answer = get_valid("Are you sure you want to push #{package_files.length} packages? (y/n)") do |s|
151
+ s == "y" || s == "n"
152
+ end
153
+
154
+ if answer != "y"
155
+ abort("Aborting...".color(:red))
156
+ end
157
+ end
158
+
159
+ validator = Validator.new(client)
160
+ if PY_EXTS.include?(exts.first.downcase)
161
+ dist_id = validator.distribution_id(repo, package_files, 'py')
162
+ elsif ANYFILE_EXTS.include?(exts.first.downcase)
163
+ dist_id = validator.distribution_id(repo, package_files, 'anyfile')
164
+ elsif NODE_EXTS.include?(exts.first.downcase)
165
+ dist_id = validator.distribution_id(repo, package_files, 'node')
166
+ elsif JAVA_EXTS.include?(exts.first.downcase)
167
+ abort_if_snapshot!(package_files)
168
+ dist_id = validator.distribution_id(repo, package_files, 'jar')
169
+ elsif AMBIGUOUS_EXTS.include?(exts.first.downcase)
170
+ dist_id = validator.distribution_id_from_repo_url(repo, package_files)
171
+ else
172
+ dist_id = validator.distribution_id(repo, package_files, exts.first)
173
+ end
174
+
175
+ # strip os/dist
176
+ split_repo = repo.split("/")[0..1].join("/")
177
+
178
+ print "Looking for repository at #{split_repo}... "
179
+ client_repo = nil
180
+ measurement = Benchmark.measure do
181
+ client_repo = client.repository(split_repo)
182
+ end
183
+ print "success!\n"
184
+ $logger.debug("repository lookup request timing: #{measurement}")
185
+
186
+ package_files.each do |f|
187
+ url = nil
188
+ files = nil
189
+ urls = nil
190
+ ext = f.split(".").last
191
+
192
+ if ext == "dsc"
193
+ print "Checking source package #{f}... "
194
+ files = parse_and_verify_dsc(client_repo, f, dist_id)
195
+ end
196
+
197
+ if options["upload-before-push"]
198
+ print "Uploading #{f}... "
199
+ upload = client_repo.create_upload
200
+ begin
201
+ response = RestClient::Request.execute(
202
+ :method => upload.fetch("form").fetch("method", "POST"),
203
+ :url => upload.fetch("form").fetch("url"),
204
+ :timeout => nil,
205
+ :payload => upload.fetch("form").fetch("data", {}).merge(
206
+ upload.fetch("form").fetch("file_input") => File.open(f, "r"),
207
+ )
208
+ )
209
+ url = response.headers.fetch(:location)
210
+
211
+ # Amazon S3 uploads return urlencoded keys in the Location
212
+ # header, and we want to return the decoded object key for
213
+ # API operations.
214
+ uri = URI.parse(url)
215
+ if uri.host.end_with?(".amazonaws.com")
216
+ uri.path = CGI.unescape(uri.path)
217
+ url = uri.to_s
218
+ end
219
+
220
+ if files
221
+ urls = files.map do |file|
222
+ response = RestClient::Request.execute(
223
+ :method => upload.fetch("form").fetch("method", "POST"),
224
+ :url => upload.fetch("form").fetch("url"),
225
+ :timeout => nil,
226
+ :payload => upload.fetch("form").fetch("data", {}).merge(
227
+ upload.fetch("form").fetch("file_input") => File.open(file, "r"),
228
+ )
229
+ )
230
+ source_url = response.headers.fetch(:location)
231
+ source_uri = URI.parse(source_url)
232
+ if source_uri.host.end_with?(".amazonaws.com")
233
+ source_uri.path = CGI.unescape(source_uri.path)
234
+ source_url = source_uri.to_s
235
+ end
236
+ source_url
237
+ end
238
+ end
239
+ rescue
240
+ print "error!\n".color(:red)
241
+ puts $!, *$!.backtrace, $!.response, $!.response.body
242
+ exit(1)
243
+ end
244
+ end
245
+
246
+ print "Pushing #{f}... "
247
+ measurement = Benchmark.measure do
248
+ if options.has_key?("skip-errors")
249
+ create_package_skip_errors(client_repo, f, url, dist_id, files, urls, ext, options["coordinates"])
250
+ elsif options.has_key?("skip-duplicates")
251
+ create_package_skip_duplicates(client_repo, f, url, dist_id, files, urls, ext, options["coordinates"])
252
+ else
253
+ create_package(client_repo, f, url, dist_id, files, urls, ext, options["coordinates"])
254
+ end
255
+ end
256
+ $logger.debug("create package request timing: #{measurement}")
257
+ end
258
+ end
259
+ $logger.debug("push command total timing: #{total_time}")
260
+ end
261
+
262
+ desc "version",
263
+ "print version information"
264
+ def version
265
+ puts "package_cloud CLI #{VERSION}\nSee https://packagecloud.io/docs#cli for more details."
266
+ end
267
+
268
+ private
269
+ def expand_dist_shortcut(dist)
270
+ case dist
271
+ when 'java'
272
+ 'java/maven2'
273
+ when 'python'
274
+ 'python/1'
275
+ when 'anyfile'
276
+ 'anyfile/1'
277
+ when 'node'
278
+ 'node/1'
279
+ else
280
+ dist
281
+ end
282
+ end
283
+
284
+ def package_scope(package_name)
285
+ name_parts = package_name.split('/')
286
+ if name_parts.size != 2 || !package_name.include?('@')
287
+ abort("Could not determine scope for '#{package_name}', it should look like: @my-scope/package-1.0.tgz".color(:red))
288
+ end
289
+ scope = name_parts[0]
290
+ unscoped_package_name = name_parts[1]
291
+ [scope, unscoped_package_name]
292
+ end
293
+
294
+ def abort_if_snapshot!(files)
295
+ if files.any? { |file| file.include?("-SNAPSHOT") }
296
+ abort("SNAPSHOT uploads are not supported by the CLI, please use Maven instead: https://packagecloud.io/docs#wagon-instructions")
297
+ end
298
+ end
299
+
300
+ def create_package_skip_errors(client_repo, f, url, dist_id, files, urls, ext, coordinates=nil)
301
+ begin
302
+ client_repo.create_package(f, url, dist_id, files, urls, ext, coordinates=nil)
303
+ rescue RestClient::UnprocessableEntity => e
304
+ print "error (skipping):\n".color(:yellow)
305
+ json = JSON.parse(e.response)
306
+ json.each do |k,v|
307
+ puts "\n\t#{k}: #{v.join(", ")}\n"
308
+ end
309
+ puts ""
310
+ end
311
+ end
312
+
313
+ def create_package_skip_duplicates(client_repo, f, url, dist_id, files, urls, ext, coordinates=nil)
314
+ begin
315
+ client_repo.create_package(f, url, dist_id, files, urls, ext, coordinates=nil)
316
+ rescue RestClient::UnprocessableEntity => e
317
+ json = JSON.parse(e.response)
318
+ if json == { "filename" => ["has already been taken"] }
319
+ print "skipping, already exists.\n".color(:yellow)
320
+ else
321
+ print "error:\n".color(:red)
322
+ json.each do |k,v|
323
+ if v.is_a? String
324
+ puts "\n\t#{k}: #{v}\n"
325
+ elsif v.is_a? Array
326
+ puts "\n\t#{k}: #{v.join(", ")}\n"
327
+ end
328
+ end
329
+ puts ""
330
+ exit(1)
331
+ end
332
+ end
333
+ end
334
+
335
+ def create_package(client_repo, f, url, dist_id, files, urls, ext, coordinates=nil)
336
+ begin
337
+ client_repo.create_package(f, url, dist_id, files, urls, ext, coordinates)
338
+ rescue RestClient::UnprocessableEntity => e
339
+ print "error:\n".color(:red)
340
+ json = JSON.parse(e.response)
341
+ json.each do |k,v|
342
+ if v.is_a? String
343
+ puts "\n\t#{k}: #{v}\n"
344
+ elsif v.is_a? Array
345
+ puts "\n\t#{k}: #{v.join(", ")}\n"
346
+ end
347
+ end
348
+ puts ""
349
+ exit(1)
350
+ end
351
+ end
352
+
353
+ def parse_and_verify_dsc(repo, f, dist_id)
354
+ files = repo.parse_dsc(f, dist_id)
355
+ dirname = File.dirname(f)
356
+ find_and_verify(dirname, files)
357
+ end
358
+
359
+ def find_and_verify(dir, files)
360
+ file_paths = []
361
+ files.each do |f|
362
+ filepath = File.join(dir, f["filename"])
363
+ if !File.exist?(filepath)
364
+ print "Unable to find file name: #{f["filename"]} for source package: #{filepath}\n".color(:red)
365
+ abort("Aborting...".color(:red))
366
+ end
367
+
368
+ disk_size = File.stat(filepath).size
369
+ if disk_size != f["size"]
370
+ print "File #{f["filename"]} has size: #{disk_size}, expected: #{f["size"]}\n".color(:red)
371
+ abort("Aborting...".color(:red))
372
+ end
373
+
374
+ print "Found DSC package file #{f["filename"]} for upload\n"
375
+
376
+ file_paths << filepath
377
+ end
378
+ file_paths
379
+ end
380
+
381
+ def get_valid(prompt)
382
+ selection = ""
383
+ times = 0
384
+ until yield(selection)
385
+ if times > 0
386
+ puts "#{selection} is not a valid selection."
387
+ end
388
+ print "#{prompt}: "
389
+ selection = ::Kernel.gets.chomp
390
+ times += 1
391
+ end
392
+
393
+ selection
394
+ end
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,76 @@
1
+ module PackageCloud
2
+ module CLI
3
+ class GpgKey < Base
4
+ desc "list user/repository",
5
+ "list all GPG keys for specified repository"
6
+ def list(repo_name)
7
+ print "Looking for repository at #{repo_name}... "
8
+ repo = client.repository(repo_name)
9
+ print "success!\n"
10
+
11
+ keys = repo.gpg_keys
12
+ puts "GPG Keys for #{repo_name}:\n"
13
+
14
+ keys.each_with_index do |key, i|
15
+ if key.keytype == "package"
16
+ keytype = "Package signing key"
17
+ else
18
+ keytype = "Repository signing key"
19
+ end
20
+
21
+ puts "Key name: #{key.name}"
22
+ puts "Key type: #{keytype}"
23
+ puts "Key fingerprint: #{key.fingerprint}"
24
+ puts "GPG key url: #{key.download_url}"
25
+ puts
26
+ end
27
+ end
28
+
29
+ desc "create user/repository /path/to/gpg-key",
30
+ "create a package signing GPG key using the specified file"
31
+ def create(repo_name, file_path)
32
+ print "Looking for repository at #{repo_name}... "
33
+ repo = client.repository(repo_name)
34
+ print "success!\n"
35
+ repo.create_gpg_key(file_path)
36
+ end
37
+
38
+ desc "destroy user/repository keyname",
39
+ "destroy specified package signing GPG key"
40
+ def destroy(repo_name, keyname)
41
+ ARGV.clear
42
+ print "Looking for repository at #{repo_name}... "
43
+ repo = client.repository(repo_name)
44
+ print "success!\n"
45
+
46
+ key = repo.gpg_keys.detect do |key|
47
+ key.name == keyname
48
+ end
49
+
50
+ if key
51
+ msg = "\nAre you sure you want to delete the GPG key #{keyname}? (y/n)"
52
+ answer = get_valid(msg) do |s|
53
+ s == "y" || s == "n"
54
+ end
55
+
56
+ if answer == "y"
57
+ print "Attempting to destroy GPG key named #{keyname}... "
58
+ begin
59
+ key.destroy
60
+ rescue RestClient::ResourceNotFound =>e
61
+ print "\nError, could not find key: #{keyname}. No GPG keys deleted.\n".color(:red)
62
+ print "Please note that you cannot delete repository signing keys.\n"
63
+ exit(1)
64
+ else
65
+ print "success!\n".color(:green)
66
+ end
67
+ else
68
+ puts "Aborting...".color(:red)
69
+ end
70
+ else
71
+ puts "Wasn't able to find a GPG key name: #{keyname}".color(:red)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,75 @@
1
+ module PackageCloud
2
+ module CLI
3
+ class MasterToken < Base
4
+ desc "list user/repository",
5
+ "list master tokens for specified repository"
6
+ def list(repo_name)
7
+ print "Looking for repository at #{repo_name}... "
8
+ repo = client.repository(repo_name)
9
+ print "success!\n"
10
+
11
+ tokens = repo.master_tokens
12
+ puts "Tokens for #{repo_name}:"
13
+ puts ""
14
+ tokens.each_with_index do |token, i|
15
+ puts " #{token.name} (#{token.value})"
16
+ puts " read tokens:"
17
+ token.read_tokens.each do |read_token|
18
+ puts " { id: #{read_token.id}, name: #{read_token.name}, value: #{read_token.value} }"
19
+ puts
20
+ end
21
+ puts "" unless i == tokens.length - 1
22
+ end
23
+ end
24
+
25
+ desc "create user/repository token_name",
26
+ "create a master token for the specified repository"
27
+ def create(repo_name, token_name)
28
+ print "Looking for repository at #{repo_name}... "
29
+ repo = client.repository(repo_name)
30
+ print "success!\n"
31
+
32
+ print "Attempting to create token named #{token_name}... "
33
+ resp = repo.create_master_token(token_name)
34
+ print "success!\n".color(:green)
35
+ print "Master token #{resp['name']} with value #{resp['value']} created.\n"
36
+ end
37
+
38
+ desc "destroy user/repository token_name",
39
+ "destroy a master token for the specified repository"
40
+ def destroy(repo_name, token_name)
41
+ ARGV.clear # otherwise gets explodes
42
+
43
+ if token_name == "default"
44
+ abort("You can't delete the default master_token.".color(:red))
45
+ end
46
+
47
+ print "Looking for repository at #{repo_name}... "
48
+ repo = client.repository(repo_name)
49
+ print "success!\n"
50
+
51
+ token = repo.master_tokens.detect do |token|
52
+ token.name == token_name
53
+ end
54
+
55
+ if token
56
+ msg = "\nAre you sure you want to delete #{token_name}?"
57
+ msg << " #{token.read_tokens.length} read tokens will no longer work afterwards (y/n)" if token.read_tokens.length > 0
58
+ answer = get_valid(msg) do |s|
59
+ s == "y" || s == "n"
60
+ end
61
+ if answer == "y"
62
+ print "Attempting to destroy token named #{token_name}... "
63
+ token.destroy
64
+ print "success!\n".color(:green)
65
+ else
66
+ puts "Aborting...".color(:red)
67
+ end
68
+ else
69
+ puts "Wasn't able to find a token named #{token_name}.".color(:red)
70
+ exit(1)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ module PackageCloud
2
+ module CLI
3
+ class ReadToken < Base
4
+ desc "destroy user/repository mastertoken/readtoken",
5
+ "revokes read token associated to a master token"
6
+ def destroy(repo_name, master_and_read_token)
7
+ print "Looking for repository at #{repo_name}... "
8
+ repo = client.repository(repo_name)
9
+
10
+ given_master_token, given_read_token = master_and_read_token.split("/")
11
+
12
+ if given_master_token.nil? || given_read_token.nil?
13
+ print "invalid master token and/or read token!\n".color(:red)
14
+ exit(127)
15
+ end
16
+
17
+ master_token = repo.master_tokens.detect { |t| t.name == given_master_token }
18
+
19
+ if master_token.nil?
20
+ print "couldn't find master token named #{given_master_token}\n".color(:red)
21
+ exit(127)
22
+ end
23
+
24
+ read_token = master_token.read_tokens.detect { |t| t.name == given_read_token }
25
+
26
+ if read_token.nil?
27
+ print "couldn't find read token named #{given_read_token} for #{given_master_token}\n".color(:red)
28
+ exit(127)
29
+ end
30
+
31
+ master_token_path = master_token.paths["self"]
32
+
33
+ read_token.destroy(master_token_path, read_token.id)
34
+
35
+ print "success!\n"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,52 @@
1
+ module PackageCloud
2
+ module CLI
3
+ class Repository < Base
4
+ option :private
5
+ desc "create repo_name",
6
+ "create repository called repo_name"
7
+ def create(repo_name)
8
+ config.read_or_create
9
+ client = Client.new(config)
10
+
11
+ print "Looking for repository at #{repo_name}... "
12
+ repo = client.create_repo(repo_name, options[:private])
13
+ print "success!\n".color(:green)
14
+ puts "Your repository has been created at:"
15
+ puts " #{repo["url"]}"
16
+ end
17
+
18
+ desc "list",
19
+ "list your repositories"
20
+ def list
21
+ repos = client.repositories
22
+ if repos.length == 0
23
+ puts "You have no repositories at the moment. Create one!"
24
+ else
25
+ puts "Your repositories:"
26
+ puts ""
27
+ repos.each_with_index do |repo, i|
28
+ puts " #{repo.fqname} (#{repo.private_human})"
29
+ puts " last push: #{repo.last_push_human} | packages: #{repo.package_count_human}"
30
+ puts "" unless i == repos.length - 1
31
+ end
32
+ end
33
+ end
34
+
35
+ desc "install user/repo package_type",
36
+ "install user/repo for package_type"
37
+ def install(repo, package_type)
38
+ if Process.uid != 0 && package_type != "gem"
39
+ abort("You have to run install as root.".color(:red))
40
+ end
41
+
42
+ print "Locating repository at #{repo}... "
43
+ repo = client.repository(repo)
44
+ print "success!\n"
45
+ script = repo.install_script(package_type)
46
+ pid = fork { exec("/bin/bash", script.path) }
47
+ Process.waitpid(pid)
48
+ script.unlink
49
+ end
50
+ end
51
+ end
52
+ end