cf 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright (c)2012, Alex Suraci
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above
12
+ copyright notice, this list of conditions and the following
13
+ disclaimer in the documentation and/or other materials provided
14
+ with the distribution.
15
+
16
+ * Neither the name of Alex Suraci nor the names of other
17
+ contributors may be used to endorse or promote products derived
18
+ from this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/cf ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: ft=ruby
3
+
4
+ require "rubygems"
5
+ require "thor"
6
+
7
+ require "cf"
8
+ require "cf/plugin"
9
+
10
+ CF::Plugin.load_all
11
+
12
+ $exit_status = 0
13
+ CF::CLI.start(ARGV)
14
+ exit($exit_status)
@@ -0,0 +1,2 @@
1
+ require "cf/version"
2
+ require "cf/cli"
@@ -0,0 +1,249 @@
1
+ require "cf/cli/command"
2
+ require "cf/cli/app"
3
+ require "cf/cli/service"
4
+ require "cf/cli/user"
5
+
6
+ module CF
7
+ class CLI < App # subclass App since we operate on Apps by default
8
+ class_option :verbose,
9
+ :type => :boolean, :aliases => "-v", :desc => "Verbose"
10
+
11
+ class_option :force,
12
+ :type => :boolean, :aliases => "-f", :desc => "Force (no interaction)"
13
+
14
+ class_option :simple_output,
15
+ :type => :boolean, :desc => "Simplified output format."
16
+
17
+ class_option :script, :type => :boolean, :aliases => "-s",
18
+ :desc => "--simple-output and --force"
19
+
20
+ class_option :color, :type => :boolean, :desc => "Colored output"
21
+
22
+ desc "service SUBCOMMAND ...ARGS", "Manage your services"
23
+ subcommand "service", Service
24
+
25
+ desc "user SUBCOMMAND ...ARGS", "User management"
26
+ subcommand "user", User
27
+
28
+ desc "info", "Display information on the current target, user, et."
29
+ flag(:runtimes)
30
+ flag(:services)
31
+ def info
32
+ info =
33
+ with_progress("Getting target information") do
34
+ client.info
35
+ end
36
+
37
+ if input(:runtimes)
38
+ runtimes = {}
39
+ info["frameworks"].each do |_, f|
40
+ f["runtimes"].each do |r|
41
+ runtimes[r["name"]] = r
42
+ end
43
+ end
44
+
45
+ runtimes = runtimes.values.sort_by { |x| x["name"] }
46
+
47
+ if simple_output?
48
+ runtimes.each do |r|
49
+ puts r["name"]
50
+ end
51
+ return
52
+ end
53
+
54
+ runtimes.each do |r|
55
+ puts ""
56
+ puts "#{c(r["name"], :blue)}:"
57
+ puts " version: #{b(r["version"])}"
58
+ puts " description: #{b(r["description"])}"
59
+ end
60
+
61
+ return
62
+ end
63
+
64
+ if input(:services)
65
+ services = {}
66
+ client.system_services.each do |_, svcs|
67
+ svcs.each do |name, versions|
68
+ services[name] = versions.values
69
+ end
70
+ end
71
+
72
+ if simple_output?
73
+ services.each do |name, _|
74
+ puts name
75
+ end
76
+
77
+ return
78
+ end
79
+
80
+ services.each do |name, versions|
81
+ puts ""
82
+ puts "#{c(name, :blue)}:"
83
+ puts " versions: #{versions.collect { |v| v["version"] }.join ", "}"
84
+ puts " description: #{versions[0]["description"]}"
85
+ puts " type: #{versions[0]["type"]}"
86
+ end
87
+
88
+ return
89
+ end
90
+
91
+ puts ""
92
+
93
+ puts info["description"]
94
+ puts ""
95
+ puts "target: #{b(client.target)}"
96
+ puts " version: #{info["version"]}"
97
+ puts " support: #{info["support"]}"
98
+ puts ""
99
+ puts "user: #{b(info["user"])}"
100
+ puts " usage:"
101
+
102
+ limits = info["limits"]
103
+ info["usage"].each do |k, v|
104
+ m = limits[k]
105
+ if k == "memory"
106
+ puts " #{k}: #{usage(v * 1024 * 1024, m * 1024 * 1024)}"
107
+ else
108
+ puts " #{k}: #{b(v)} of #{b(m)} limit"
109
+ end
110
+ end
111
+ end
112
+
113
+ desc "target URL", "Set target cloud"
114
+ def target(url)
115
+ target = sane_target_url(url)
116
+ display = c(target.sub(/https?:\/\//, ""), :blue)
117
+ with_progress("Setting target to #{display}") do
118
+ unless force?
119
+ # check that the target is valid
120
+ Conduit::Client.new(target).info
121
+ end
122
+
123
+ set_target(target)
124
+ end
125
+ end
126
+
127
+ desc "login [EMAIL]", "Authenticate with the target"
128
+ flag(:email) {
129
+ ask("Email")
130
+ }
131
+ flag(:password)
132
+ # TODO: implement new authentication scheme
133
+ def login(email = nil)
134
+ unless simple_output?
135
+ puts "Target: #{c(client_target, :blue)}"
136
+ puts ""
137
+ end
138
+
139
+ email ||= input(:email)
140
+ password = input(:password)
141
+
142
+ authenticated = false
143
+ failed = false
144
+ until authenticated
145
+ unless force?
146
+ if failed || !password
147
+ password = ask("Password", :echo => "*", :forget => true)
148
+ end
149
+ end
150
+
151
+ with_progress("Authenticating") do |s|
152
+ begin
153
+ save_token(client.login(email, password))
154
+ authenticated = true
155
+ rescue Conduit::Denied
156
+ return if force?
157
+
158
+ s.fail do
159
+ failed = true
160
+ end
161
+ end
162
+ end
163
+ end
164
+ ensure
165
+ $exit_status = 1 if not authenticated
166
+ end
167
+
168
+ desc "logout", "Log out from the target"
169
+ def logout
170
+ with_progress("Logging out") do
171
+ remove_token
172
+ end
173
+ end
174
+
175
+ desc "register [EMAIL]", "Create a user and log in"
176
+ flag(:email) {
177
+ ask("Email")
178
+ }
179
+ flag(:password) {
180
+ ask("Password", :echo => "*", :forget => true)
181
+ }
182
+ flag(:no_login, :type => :boolean)
183
+ def register(email = nil)
184
+ unless simple_output?
185
+ puts "Target: #{c(client_target, :blue)}"
186
+ puts ""
187
+ end
188
+
189
+ email ||= input(:email)
190
+ password = input(:password)
191
+
192
+ with_progress("Creating user") do
193
+ client.register(email, password)
194
+ end
195
+
196
+ unless input(:skip_login)
197
+ with_progress("Logging in") do
198
+ save_token(client.login(email, password))
199
+ end
200
+ end
201
+ end
202
+
203
+ desc "services", "List your services"
204
+ def services
205
+ services =
206
+ with_progress("Getting services") do
207
+ client.services
208
+ end
209
+
210
+ puts "" unless simple_output?
211
+
212
+ services.each do |s|
213
+ display_service(s)
214
+ end
215
+ end
216
+
217
+ desc "users", "List all users"
218
+ def users
219
+ users =
220
+ with_progress("Getting users") do
221
+ client.users
222
+ end
223
+
224
+ users.each do |u|
225
+ display_user(u)
226
+ end
227
+ end
228
+
229
+ private
230
+
231
+ def display_service(s)
232
+ if simple_output?
233
+ puts s.name
234
+ else
235
+ puts "#{c(s.name, :blue)}: #{s.vendor} v#{s.version}"
236
+ end
237
+ end
238
+
239
+ def display_user(u)
240
+ if simple_output?
241
+ puts u.email
242
+ else
243
+ puts ""
244
+ puts "#{c(u.email, :blue)}:"
245
+ puts " admin?: #{c(u.admin?, u.admin? ? :green : :red)}"
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,545 @@
1
+ require "cf/cli/command"
2
+ require "cf/detect"
3
+
4
+ module CF
5
+ class App < Command
6
+ MEM_CHOICES = ["64M", "128M", "256M", "512M"]
7
+
8
+ desc "apps", "List your applications"
9
+ def apps
10
+ apps =
11
+ with_progress("Getting applications") do
12
+ client.apps
13
+ end
14
+
15
+ apps.each.with_index do |a, num|
16
+ display_app(a)
17
+ end
18
+ end
19
+
20
+ desc "stop [APP]", "Stop an application"
21
+ def stop(name)
22
+ with_progress("Stopping #{c(name, :blue)}") do |s|
23
+ app = client.app(name)
24
+
25
+ unless app.exists?
26
+ s.fail do
27
+ err "Unknown application."
28
+ end
29
+ end
30
+
31
+ if app.stopped?
32
+ s.skip do
33
+ err "Application is not running."
34
+ end
35
+ end
36
+
37
+ app.stop!
38
+ end
39
+ end
40
+
41
+ desc "start [APP]", "Start an application"
42
+ flag(:debug_mode)
43
+ def start(name)
44
+ app = client.app(name)
45
+
46
+ unless app.exists?
47
+ err "Unknown application."
48
+ return
49
+ end
50
+
51
+ switch_mode(app, input(:debug_mode))
52
+
53
+ with_progress("Starting #{c(name, :blue)}") do |s|
54
+ if app.running?
55
+ s.skip do
56
+ err "Already started."
57
+ return
58
+ end
59
+ end
60
+
61
+ app.start!
62
+ end
63
+
64
+ check_application(app)
65
+
66
+ if app.debug_mode && !simple_output?
67
+ puts ""
68
+ instances(name)
69
+ end
70
+ end
71
+
72
+ desc "restart [APP]", "Stop and start an application"
73
+ flag(:debug_mode)
74
+ def restart(name)
75
+ stop(name)
76
+ start(name)
77
+ end
78
+
79
+ desc "delete [APP]", "Delete an application"
80
+ flag(:really) { |name|
81
+ force? || ask("Really delete #{c(name, :blue)}?", :default => false)
82
+ }
83
+ def delete(name)
84
+ return unless input(:really, name)
85
+
86
+ with_progress("Deleting #{c(name, :blue)}") do
87
+ client.app(name).delete!
88
+ end
89
+ ensure
90
+ forget(:really)
91
+ end
92
+
93
+ desc "instances [APP]", "List an app's instances"
94
+ def instances(name)
95
+ instances =
96
+ with_progress("Getting instances for #{c(name, :blue)}") do
97
+ client.app(name).instances
98
+ end
99
+
100
+ instances.each do |i|
101
+ if simple_output?
102
+ puts i.index
103
+ else
104
+ puts ""
105
+ display_instance(i)
106
+ end
107
+ end
108
+ end
109
+
110
+ desc "files [APP] [PATH]", "Examine an app's files"
111
+ def files(name, path = "/")
112
+ files =
113
+ with_progress("Getting file listing") do
114
+ client.app(name).files(*path.split("/"))
115
+ end
116
+
117
+ puts "" unless simple_output?
118
+
119
+ files.each do |file|
120
+ puts file
121
+ end
122
+ end
123
+
124
+ desc "file [APP] [PATH]", "Print out an app's file contents"
125
+ def file(name, path = "/")
126
+ file =
127
+ with_progress("Getting file contents") do
128
+ client.app(name).file(*path.split("/"))
129
+ end
130
+
131
+ puts "" unless simple_output?
132
+
133
+ print file
134
+ end
135
+
136
+ desc "logs [APP]", "Print out an app's logs"
137
+ flag(:instance, :type => :numeric, :default => 0)
138
+ def logs(name)
139
+ app = client.app(name)
140
+ unless app.exists?
141
+ err "Unknown application."
142
+ return
143
+ end
144
+
145
+ instances =
146
+ if input(:instance) == "all"
147
+ app.instances
148
+ else
149
+ app.instances.select { |i| i.index == input(:instance) }
150
+ end
151
+
152
+ if instances.empty?
153
+ if input(:instance) == "all"
154
+ err "No instances found."
155
+ else
156
+ err "Instance #{name} \##{input(:instance)} not found."
157
+ end
158
+
159
+ return
160
+ end
161
+
162
+ instances.each do |i|
163
+ logs =
164
+ with_progress(
165
+ "Getting logs for " +
166
+ c(name, :blue) + " " +
167
+ c("\##{i.index}", :yellow)) do
168
+ i.files("logs")
169
+ end
170
+
171
+ puts "" unless simple_output?
172
+
173
+ logs.each do |log|
174
+ body =
175
+ with_progress("Reading " + b(log.join("/"))) do
176
+ i.file(*log)
177
+ end
178
+
179
+ puts body
180
+ puts "" unless body.empty?
181
+ end
182
+ end
183
+ end
184
+
185
+ desc "push [NAME]", "Push an application, syncing changes if it exists"
186
+ flag(:name) { ask("Name") }
187
+ flag(:path) {
188
+ ask("Push from...", :default => ".")
189
+ }
190
+ flag(:url) { |name, target|
191
+ ask("URL", :default => "#{name}.#{target}")
192
+ }
193
+ flag(:memory) {
194
+ ask("Memory Limit",
195
+ :choices => MEM_CHOICES,
196
+
197
+ # TODO: base this on framework choice
198
+ :default => "64M")
199
+ }
200
+ flag(:instances) {
201
+ ask("Instances", :default => 1)
202
+ }
203
+ flag(:framework) { |choices, default|
204
+ ask("Framework", :choices => choices, :default => default)
205
+ }
206
+ flag(:runtime) { |choices|
207
+ ask("Runtime", :choices => choices)
208
+ }
209
+ flag(:start, :default => true)
210
+ flag(:restart, :default => true)
211
+ def push(name = nil)
212
+ path = File.expand_path(input(:path))
213
+
214
+ name ||= input(:name)
215
+
216
+ detector = Detector.new(client, path)
217
+ frameworks = detector.all_frameworks
218
+ detected, default = detector.frameworks
219
+
220
+ app = client.app(name)
221
+
222
+ if app.exists?
223
+ upload_app(app, path)
224
+ restart(app.name) if input(:restart)
225
+ return
226
+ end
227
+
228
+ app.total_instances = input(:instances)
229
+
230
+ domain = client.target.sub(/^https?:\/\/api\.(.+)\/?/, '\1')
231
+ app.urls = [input(:url, name, domain)]
232
+
233
+ framework = input(:framework, ["other"] + detected.keys, default)
234
+ if framework == "other"
235
+ forget(:framework)
236
+ framework = input(:framework, frameworks.keys)
237
+ end
238
+
239
+ framework_runtimes =
240
+ frameworks[framework]["runtimes"].collect do |k|
241
+ "#{k["name"]} (#{k["description"]})"
242
+ end
243
+
244
+ # TODO: include descriptions
245
+ runtime = input(:runtime, framework_runtimes).split.first
246
+
247
+ app.framework = framework
248
+ app.runtime = runtime
249
+
250
+ app.memory = megabytes(input(:memory))
251
+
252
+ with_progress("Creating #{c(name, :blue)}") do
253
+ app.create!
254
+ end
255
+
256
+ begin
257
+ upload_app(app, path)
258
+ rescue
259
+ err "Upload failed. Try again with 'vmc push'."
260
+ raise
261
+ end
262
+
263
+ start(name) if input(:start)
264
+ end
265
+
266
+ desc "update", "DEPRECATED", :hide => true
267
+ def update(*args)
268
+ err "The 'update' command is no longer used; use 'push' instead."
269
+ end
270
+
271
+ desc "stats", "Display application instance status"
272
+ def stats(name)
273
+ stats =
274
+ with_progress("Getting stats") do
275
+ client.app(name).stats
276
+ end
277
+
278
+ stats.sort_by { |k, _| k }.each do |idx, info|
279
+ stats = info["stats"]
280
+ usage = stats["usage"]
281
+ puts ""
282
+ puts "instance #{c("#" + idx, :blue)}:"
283
+ print " cpu: #{percentage(usage["cpu"])} of"
284
+ puts " #{b(stats["cores"])} cores"
285
+ puts " memory: #{usage(usage["mem"] * 1024, stats["mem_quota"])}"
286
+ puts " disk: #{usage(usage["disk"], stats["disk_quota"])}"
287
+ end
288
+ end
289
+
290
+ desc "scale [APP]", "Update the instances/memory limit for an application"
291
+ flag(:instances, :type => :numeric) { |default|
292
+ ask("Instances", :default => default)
293
+ }
294
+ flag(:memory) { |default|
295
+ ask("Memory Limit",
296
+ :default => human_size(default * 1024 * 1024, 0),
297
+ :choices => MEM_CHOICES)
298
+ }
299
+ def scale(name)
300
+ app = client.app(name)
301
+
302
+ instances = passed_value(:instances)
303
+ memory = passed_value(:memory)
304
+
305
+ unless instances || memory
306
+ instances = input(:instances, app.total_instances)
307
+ memory = input(:memory, app.memory)
308
+ end
309
+
310
+ with_progress("Scaling #{c(name, :blue)}") do
311
+ app.total_instances = instances.to_i if instances
312
+ app.memory = megabytes(memory) if memory
313
+ app.update!
314
+ end
315
+ end
316
+
317
+ desc "map NAME URL", "Add a URL mapping for an app"
318
+ def map(name, url)
319
+ simple = url.sub(/^https?:\/\/(.*)\/?/i, '\1')
320
+
321
+ with_progress("Updating #{c(name, :blue)}") do
322
+ app = client.app(name)
323
+ app.urls << simple
324
+ app.update!
325
+ end
326
+ end
327
+
328
+ desc "unmap NAME URL", "Remove a URL mapping from an app"
329
+ def unmap(name, url)
330
+ simple = url.sub(/^https?:\/\/(.*)\/?/i, '\1')
331
+
332
+ with_progress("Updating #{c(name, :blue)}") do |s|
333
+ app = client.app(name)
334
+
335
+ unless app.urls.delete(simple)
336
+ s.fail do
337
+ err "URL #{url} is not mapped to this application."
338
+ return
339
+ end
340
+ end
341
+
342
+ app.update!
343
+ end
344
+ end
345
+
346
+ class Env < Command
347
+ VALID_NAME = /^[a-zA-Za-z_][[:alnum:]_]*$/
348
+
349
+ desc "set [APP] [NAME] [VALUE]", "Set an environment variable"
350
+ def set(appname, name, value)
351
+ app = client.app(appname)
352
+ unless name =~ VALID_NAME
353
+ err "Invalid variable name; must match #{VALID_NAME.inspect}"
354
+ return
355
+ end
356
+
357
+ unless app.exists?
358
+ err "Unknown application."
359
+ return
360
+ end
361
+
362
+ with_progress("Updating #{c(app.name, :blue)}") do
363
+ app.update!("env" =>
364
+ app.env.reject { |v|
365
+ v.start_with?("#{name}=")
366
+ }.push("#{name}=#{value}"))
367
+ end
368
+ end
369
+
370
+ desc "unset [APP] [NAME]", "Remove an environment variable"
371
+ def unset(appname, name)
372
+ app = client.app(appname)
373
+
374
+ unless app.exists?
375
+ err "Unknown application."
376
+ return
377
+ end
378
+
379
+ with_progress("Updating #{c(app.name, :blue)}") do
380
+ app.update!("env" =>
381
+ app.env.reject { |v|
382
+ v.start_with?("#{name}=")
383
+ })
384
+ end
385
+ end
386
+
387
+ desc "list [APP]", "Show all environment variables set for an app"
388
+ def list(appname)
389
+ vars =
390
+ with_progress("Getting variables") do |s|
391
+ app = client.app(appname)
392
+
393
+ unless app.exists?
394
+ s.fail do
395
+ err "Unknown application."
396
+ return
397
+ end
398
+ end
399
+
400
+ app.env
401
+ end
402
+
403
+ puts "" unless simple_output?
404
+
405
+ vars.each do |pair|
406
+ name, val = pair.split("=", 2)
407
+ puts "#{c(name, :blue)}: #{val}"
408
+ end
409
+ end
410
+ end
411
+
412
+ desc "env SUBCOMMAND ...ARGS", "Manage application environment variables"
413
+ subcommand "env", Env
414
+
415
+ private
416
+
417
+ def upload_app(app, path)
418
+ with_progress("Uploading #{c(app.name, :blue)}") do
419
+ app.upload(path)
420
+ end
421
+ end
422
+
423
+ # set app debug mode, ensuring it's valid, and shutting it down
424
+ def switch_mode(app, mode)
425
+ mode = nil if mode == "none"
426
+
427
+ return false if app.debug_mode == mode
428
+
429
+ if mode.nil?
430
+ with_progress("Removing debug mode") do
431
+ app.debug_mode = nil
432
+ app.stop! if app.running?
433
+ end
434
+
435
+ return true
436
+ end
437
+
438
+ with_progress("Switching mode to #{c(mode, :blue)}") do |s|
439
+ runtimes = client.system_runtimes
440
+ modes = runtimes[app.runtime]["debug_modes"] || []
441
+ if modes.include?(mode)
442
+ app.debug_mode = mode
443
+ app.stop! if app.running?
444
+ true
445
+ else
446
+ s.fail do
447
+ err "Unknown mode '#{mode}'; available: #{modes.inspect}"
448
+ false
449
+ end
450
+ end
451
+ end
452
+ end
453
+
454
+ APP_CHECK_LIMIT = 60
455
+
456
+ def check_application(app)
457
+ with_progress("Checking #{c(app.name, :blue)}") do |s|
458
+ if app.debug_mode == "suspend"
459
+ s.skip do
460
+ puts "Application is in suspended debugging mode."
461
+ puts "It will wait for you to attach to it before starting."
462
+ end
463
+ end
464
+
465
+ seconds = 0
466
+ until app.healthy?
467
+ sleep 1
468
+ seconds += 1
469
+ if seconds == APP_CHECK_LIMIT
470
+ s.give_up do
471
+ err "Application failed to start."
472
+ # TODO: print logs
473
+ end
474
+ end
475
+ end
476
+ end
477
+ end
478
+
479
+ # choose the right color for app/instance state
480
+ def state_color(s)
481
+ case s
482
+ when "STARTING"
483
+ :blue
484
+ when "STARTED", "RUNNING"
485
+ :green
486
+ when "DOWN"
487
+ :red
488
+ when "FLAPPING"
489
+ :magenta
490
+ when "N/A"
491
+ :cyan
492
+ else
493
+ :yellow
494
+ end
495
+ end
496
+
497
+ def display_app(a)
498
+ if simple_output?
499
+ puts a.name
500
+ return
501
+ end
502
+
503
+ puts ""
504
+
505
+ health = a.health
506
+
507
+ if a.debug_mode == "suspend" && health == "0%"
508
+ status = c("suspended", :yellow)
509
+ else
510
+ status = c(health.downcase, state_color(health))
511
+ end
512
+
513
+ print "#{c(a.name, :blue)}: #{status}"
514
+
515
+ unless a.total_instances == 1
516
+ print ", #{b(a.total_instances)} instances"
517
+ end
518
+
519
+ puts ""
520
+
521
+ unless a.urls.empty?
522
+ puts " urls: #{a.urls.collect { |u| b(u) }.join(", ")}"
523
+ end
524
+
525
+ unless a.services.empty?
526
+ puts " services: #{a.services.collect { |s| b(s) }.join(", ")}"
527
+ end
528
+ end
529
+
530
+ def display_instance(i)
531
+ print "instance #{c("\##{i.index}", :blue)}: "
532
+ puts "#{b(c(i.state.downcase, state_color(i.state)))} "
533
+
534
+ puts " started: #{c(i.since.strftime("%F %r"), :cyan)}"
535
+
536
+ if d = i.debugger
537
+ puts " debugger: port #{c(d["port"], :blue)} at #{c(d["ip"], :blue)}"
538
+ end
539
+
540
+ if c = i.console
541
+ puts " console: port #{b(c["port"])} at #{b(c["ip"])}"
542
+ end
543
+ end
544
+ end
545
+ end