a4tools 1.2.7

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/install.log +38 -0
  3. data/.gitignore +2 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +38 -0
  6. data/a4tools.gemspec +38 -0
  7. data/bin/deploy_latest_clients +32 -0
  8. data/bin/devsite_config_server +48 -0
  9. data/bin/netshell +23 -0
  10. data/bin/update_server +101 -0
  11. data/bin/usher +54 -0
  12. data/lib/a4tools.rb +61 -0
  13. data/lib/a4tools/version.rb +3 -0
  14. data/lib/acres_client.rb +376 -0
  15. data/lib/clients/caching_client.rb +151 -0
  16. data/lib/clients/deployment_client.rb +53 -0
  17. data/lib/clients/kai_config_client.rb +39 -0
  18. data/lib/clients/usher_client.rb +72 -0
  19. data/lib/clients/usher_mgmt_client.rb +201 -0
  20. data/lib/event_manager.rb +24 -0
  21. data/lib/events.json +1 -0
  22. data/lib/net_shell/builtin_command.rb +312 -0
  23. data/lib/net_shell/builtin_commands/build.rb +251 -0
  24. data/lib/net_shell/builtin_commands/cd.rb +12 -0
  25. data/lib/net_shell/builtin_commands/connect.rb +122 -0
  26. data/lib/net_shell/builtin_commands/deploy.rb +280 -0
  27. data/lib/net_shell/builtin_commands/disconnect.rb +15 -0
  28. data/lib/net_shell/builtin_commands/excerpt.rb +97 -0
  29. data/lib/net_shell/builtin_commands/exit.rb +7 -0
  30. data/lib/net_shell/builtin_commands/get.rb +38 -0
  31. data/lib/net_shell/builtin_commands/help.rb +40 -0
  32. data/lib/net_shell/builtin_commands/host.rb +126 -0
  33. data/lib/net_shell/builtin_commands/inject.rb +42 -0
  34. data/lib/net_shell/builtin_commands/jsoncache.rb +80 -0
  35. data/lib/net_shell/builtin_commands/kai_event.rb +151 -0
  36. data/lib/net_shell/builtin_commands/persist.rb +24 -0
  37. data/lib/net_shell/builtin_commands/pwd.rb +6 -0
  38. data/lib/net_shell/builtin_commands/recap.rb +188 -0
  39. data/lib/net_shell/builtin_commands/references.rb +63 -0
  40. data/lib/net_shell/builtin_commands/select.rb +36 -0
  41. data/lib/net_shell/builtin_commands/send.rb +74 -0
  42. data/lib/net_shell/builtin_commands/set.rb +29 -0
  43. data/lib/net_shell/builtin_commands/show.rb +183 -0
  44. data/lib/net_shell/builtin_commands/site.rb +122 -0
  45. data/lib/net_shell/builtin_commands/ssh.rb +62 -0
  46. data/lib/net_shell/builtin_commands/talk.rb +90 -0
  47. data/lib/net_shell/builtin_commands/translate.rb +45 -0
  48. data/lib/net_shell/builtin_commands/unset.rb +14 -0
  49. data/lib/net_shell/builtin_commands/usher.rb +55 -0
  50. data/lib/net_shell/builtin_commands/usher_device.rb +39 -0
  51. data/lib/net_shell/builtin_commands/usher_site.rb +245 -0
  52. data/lib/net_shell/builtin_commands/usherm_connect.rb +21 -0
  53. data/lib/net_shell/colors.rb +149 -0
  54. data/lib/net_shell/command.rb +97 -0
  55. data/lib/net_shell/io.rb +132 -0
  56. data/lib/net_shell/net_shell.rb +396 -0
  57. data/lib/net_shell/prompt.rb +335 -0
  58. data/lib/object_builder/definitions/app_info_for_script.rb +83 -0
  59. data/lib/object_builder/definitions/connection_request.rb +28 -0
  60. data/lib/object_builder/definitions/device_info_for_system.rb +37 -0
  61. data/lib/object_builder/object_builder.rb +145 -0
  62. data/lib/talk.json +1 -0
  63. data/lib/talk_consumer.rb +235 -0
  64. metadata +279 -0
@@ -0,0 +1,251 @@
1
+ category "Protocol manipulation"
2
+ description "Interactively build a JSON object of a given class"
3
+ usage "class_name"
4
+ help <<-EOS
5
+ Launches an interactive prompt to build the indicated Talk class. At the conclusion of this prompt, a JSON string will be printed to stdout.
6
+
7
+ This command is somewhat magical in that the interactive prompts do not appear on the command's standard output, nor does it gather prompt input from standard input; only the JSON appears on standard output.
8
+
9
+ Therefore, you can pipe the output of #{command.style(:command)} directly to #{"send".style(:command)} or some other command.
10
+ EOS
11
+
12
+
13
+ validate { args.length == 2 }
14
+ tab 0 do
15
+ talk.things_named_like(args[1], :class).map { |thing| talk.truncated_name thing[:name] }
16
+ end
17
+
18
+ run do
19
+ cls = talk.class_named(args[1])
20
+ return show_error("#{args[1]}: no such class defined") if cls.nil?
21
+
22
+ obj = build(cls)
23
+ obj[:__class] = talk.class_named(args[1])[:name] rescue args[1]
24
+ JSON.generate(obj)
25
+ end
26
+
27
+ ###
28
+
29
+ def prompt_for_path(path)
30
+ path.join(".").style(:class) + ") "
31
+ end
32
+
33
+ def prompt_for_value(path, type)
34
+ "#{path.join(".").style(:class)} #{type.style(:field_datatype)}) "
35
+ end
36
+
37
+ def get_action(allowed)
38
+ char = STDIN.gets.chomp rescue nil
39
+ char = nil unless char.nil? or allowed.include? char
40
+ char
41
+ end
42
+
43
+ def get_value_real(value)
44
+ case value
45
+ when nil, "null", ""
46
+ nil
47
+ else
48
+ value.to_f
49
+ end
50
+ end
51
+
52
+ def get_value_integer(value)
53
+ case value
54
+ when nil, "null", ""
55
+ nil
56
+ else
57
+ value.to_i
58
+ end
59
+ end
60
+
61
+ def get_value_boolean(value)
62
+ case value
63
+ when "0", "false"
64
+ false
65
+ when "1", "true"
66
+ true
67
+ when "", "null"
68
+ nil
69
+ else
70
+ true
71
+ end
72
+ end
73
+
74
+ def get_value_string(value)
75
+ case value
76
+ when nil, "null"
77
+ nil
78
+ else
79
+ value
80
+ end
81
+ end
82
+
83
+ def get_value(field, path)
84
+ type = field[:type].first
85
+ console_print prompt_for_value(path, type)
86
+
87
+ value = STDIN.gets
88
+ if value.nil? then
89
+ value = "null" # nil => CTRL+D. Take advantage that all get_value_* interpret null as nil-value to make CTRL+D -> null shortcut.
90
+ console_puts # need a newline since none was actually typed
91
+ end
92
+
93
+ value = value.chomp
94
+ if(value == "?") then
95
+ show_field_help(field)
96
+ return get_value(field, path)
97
+ end
98
+
99
+ return get_value_integer(value) if([ "uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64" ].include?(type))
100
+ return get_value_real(value) if([ "real" ].include?(type))
101
+ return get_value_boolean(value) if([ "bool" ].include?(type))
102
+ return get_value_string(value) if([ "string" ].include?(type))
103
+ end
104
+
105
+ def in_array?(field, depth)
106
+ field[:type][-1 - depth] == "[]"
107
+ end
108
+
109
+ def in_hash?(field, depth)
110
+ field[:type][-1 - depth] == "{}"
111
+ end
112
+
113
+ def in_container?(field, depth)
114
+ in_array?(field, depth) or in_hash?(field, depth)
115
+ end
116
+
117
+ def prompt_for_action(field, path, type, actions)
118
+ while true do
119
+ console_puts "#{path.join('.').style(:class)}: #{type.style(:field_datatype)}. #{actions.map { |a| "[#{a[0].bold}]#{a[1..-1]}" }.join(', ')}"
120
+ console_print prompt_for_path(path)
121
+ action = get_action(actions.map { |a| a[0] })
122
+ return (actions.select { |a| a[0] == action }).first unless action.nil?
123
+ end
124
+ end
125
+
126
+ def type_at_depth(field, depth)
127
+ field[:type][0 .. -1 - depth].join("")
128
+ end
129
+
130
+ def show_field_help(field, depth=-1)
131
+ depth = field[:type].length - 1 if depth < 0
132
+ console_puts "#{talk.truncated_name(type_at_depth(field, depth)).style(:field_datatype)} #{field[:name].style(:field_name)}\n\t#{field[:description]}"
133
+ end
134
+
135
+ def handle_array_start(field, path, depth)
136
+ actions = [ "build", "null", "?" ]
137
+
138
+ case prompt_for_action(field, path, type_at_depth(field, depth), actions)
139
+ when "?"
140
+ show_field_help(field, depth)
141
+ return handle_array_start(field, path, depth)
142
+ when nil, "null"
143
+ return nil
144
+ when "build"
145
+ collection = []
146
+ building = true
147
+ index = 0
148
+ while true do
149
+ element = handle_array_element(field, path[0..-2] + ["#{path.last}[#{index}]"], depth+1)
150
+ return collection if element.nil?
151
+ collection.push element[:value]
152
+ index += 1
153
+ end
154
+ end
155
+ end
156
+
157
+ def handle_array_element(field, path, depth)
158
+ actions = [ "build", "null", "done", "?" ]
159
+ case prompt_for_action(field, path, type_at_depth(field, depth), actions)
160
+ when "?"
161
+ show_field_help(field, depth)
162
+ return handle_array_element(field, path, depth)
163
+ when nil, "null"
164
+ return { value:nil }
165
+ when "done"
166
+ return nil
167
+ when "build"
168
+ return { value:handle_field(field, path, depth+1, true) }
169
+ end
170
+ end
171
+
172
+ def populate(field, path)
173
+ while true do
174
+ idx = 0
175
+ builders = ObjectBuilder.builders_for_name(field[:type].first)
176
+ builders.each do |builder|
177
+ console_puts("#{idx.to_s.style(:constant_value)} #{builder.identifier.to_s.style(:method_name)}: #{builder.description}")
178
+ idx += 1
179
+ end
180
+
181
+ console_print prompt_for_path(path)
182
+ action = STDIN.gets.chomp rescue nil
183
+
184
+ return handle_complex(field, path) if action.nil? # CTRL+D cancels populate
185
+ index = action.match(/^[0-9]+$/) ? action.to_i : nil
186
+ return builders[index].value if not index.nil? and index < builders.length
187
+ end
188
+ end
189
+
190
+ def has_cache?(field)
191
+ ObjectBuilder.supported_classes(false).each do |name|
192
+ return true if talk.name_matches?(name, field[:type].first)
193
+ end
194
+ false
195
+ end
196
+
197
+ def handle_complex(field, path, assume_build=false)
198
+ return build(talk.class_named(field[:type].first), path) if assume_build
199
+
200
+ actions = if has_cache?(field) then
201
+ [ "build", "populate", "null", "?" ]
202
+ else
203
+ [ "build", "null", "?" ]
204
+ end
205
+
206
+ case prompt_for_action(field, path, field[:type].first, actions)
207
+ when "build"
208
+ build(talk.class_named(field[:type].first), path)
209
+ when nil, "null"
210
+ nil
211
+ when "populate"
212
+ populate(field, path)
213
+ when "?"
214
+ show_field_help(field, field[:type].length-1)
215
+ return handle_complex(field, path)
216
+ end
217
+ end
218
+
219
+ def handle_primitive(field, path)
220
+ get_value(field, path)
221
+ end
222
+
223
+ def handle_field(field, path, depth=0, assume_build=false)
224
+ path += [field[:name]] if depth == 0
225
+ if depth+1 < field[:type].length then
226
+ if in_array?(field, depth)
227
+ return handle_array_start(field, path, depth)
228
+ else
229
+ # TODO: handle dictionary
230
+ return nil
231
+ end
232
+ end
233
+
234
+ if talk.primitive? field[:type].first
235
+ return handle_primitive(field, path)
236
+ else
237
+ return handle_complex(field, path, assume_build)
238
+ end
239
+ end
240
+
241
+ def build(cls, path=[])
242
+ if path.empty? then
243
+ console_puts "Building #{talk.truncated_name cls}..."
244
+ else
245
+ console_puts "#{talk.truncated_name cls} #{path.last}\n\t#{cls[:description]}"
246
+ end
247
+
248
+ result = {}
249
+ cls[:field].each { |field| result[field[:name]] = handle_field(field, path) }
250
+ result
251
+ end
@@ -0,0 +1,12 @@
1
+ category "Shell management"
2
+ description "Change working directory"
3
+ usage "directory"
4
+ validate { args.length <= 2 }
5
+ help "Behaves much like cd in your favorite shell."
6
+
7
+ run do
8
+ p = File.expand_path(args.length == 2 ? args[1] : "~")
9
+ return show_error("Directory does not exist") unless File.directory?(p)
10
+ Dir.chdir(p)
11
+ ""
12
+ end
@@ -0,0 +1,122 @@
1
+ category "Protocol manipulation"
2
+ description "Connect to a remote server"
3
+ usage "url [client_name]"
4
+
5
+ help <<-EOS
6
+ Connect to a remote server, indicated by the URL.
7
+ Supported schemes: http, https, ws, wss.
8
+
9
+ Once connected, commands like #{"send".style(:command)} will use the connected server as a target. Connecting also resets the #{"recap".style(:command)} log.
10
+
11
+ Comet support is not included; therefore, http and https provide synchronous communication only. ws and wss support fully asynchronous communication.
12
+
13
+ #{command.style(:command)} does make special effort to validate that a URL is accessible. For http and https clients, this means that attempts to use this connection will fail. Due to the nature of websockets, #{command.style(:command)} will display an error on exit if these services are unavailable.
14
+ EOS
15
+
16
+ validate { args.length >= 2 }
17
+
18
+ opt :kai, "Connect the Kai Mercury, Central and QATest clients to a particular site ID or host"
19
+
20
+ tab 0 do
21
+ if params[:kai] then
22
+ (usherm[:usherSystemId][:usherSites].map { |site| site[:idSite] } +
23
+ usherm[:usherSystemId][:usherSites].map { |site| site[:name] }) rescue []
24
+ else
25
+ []
26
+ end
27
+ end
28
+
29
+ run do
30
+ if params[:kai] then
31
+ connect_kai
32
+ else
33
+ connect_raw
34
+ end
35
+ end
36
+
37
+ ###
38
+
39
+ def connect_raw
40
+ key = args[2].to_sym rescue :user
41
+ clients = []
42
+ client = AcresClient.new(args[1])
43
+ return show_error("Unable to contact server at #{args[1]}") unless client.transport_connect
44
+
45
+ @shell.set_client(key, client)
46
+ client.on(:connect) { |trigger, msg| @shell.notify_connect(client) }
47
+ client.on(:sent) { |trigger,msg| @shell.notify_outgoing_message(client, msg) }
48
+ client.on(:message) { |trigger,msg| @shell.notify_incoming_message(client, msg) }
49
+ client.on(:disconnect) { |trigger,msg| @shell.notify_disconnect(client) }
50
+
51
+ @shell.set_active_client(key)
52
+ ""
53
+ end
54
+
55
+ def connect_kai
56
+ dest = args[1]
57
+
58
+ if dest.match(/^([a-zA-Z]+):\/\/[a-zA-Z0-9\-\.]+(:[0-9]+)?(\/)?/) then
59
+ connect_to_url(dest)
60
+ elsif dest.match(/^[0-9]+$/) then
61
+ connect_to_site_id(dest.to_i)
62
+ else
63
+ connect_to_usher_host(dest)
64
+ end
65
+ end
66
+
67
+ def request_caches
68
+ version = @shell.client(:config).site_version
69
+ client_interfaces.keys.each { |key| @shell.client(key).version = version }
70
+ @shell.client(:config).machines
71
+ end
72
+
73
+ def connect_to_url(base)
74
+ client_interfaces.each do |key, uri|
75
+ url = URI.join(base, uri).to_s
76
+ @shell.set_client(key, client_for_key(key, url))
77
+ end
78
+
79
+ request_caches
80
+ "Connected to #{@shell.client(:config).version.style(:host)} server"
81
+ end
82
+
83
+ def connect_to_site_id(site_id)
84
+ site = usherm.site_with_id(site_id)
85
+ return show_error("Site ##{site_id}: No such site ID in usher") if site.nil?
86
+ provider = usherm.provider_for_product(site_id, "Mercury")
87
+ return show_error("Site ##{site_id}: Does not offer Mercury") if provider.nil?
88
+ url = usherm.url_for_provider(provider)
89
+ return show_error("Site ##{site_id}: Site appears to be down") if url.nil?
90
+ puts "Connecting to Kai at #{url}"
91
+ connect_to_url(url)
92
+ end
93
+
94
+ def connect_to_usher_host(host)
95
+ site = usherm.site_with_name(host)
96
+ return show_error("Site #{host}: Does not appear to be listed in usher") if site.nil?
97
+ connect_to_site_id(site[:idSite])
98
+ end
99
+
100
+ def client_for_key(key, url)
101
+ case key
102
+ when :mercury
103
+ AcresClient.new(url, @shell.get_env(:kai_user), @shell.get_env(:kai_pass))
104
+ when :central
105
+ AcresClient.new(url, @shell.get_env(:usherm_user), @shell.get_env(:usherm_pass))
106
+ when :config
107
+ KaiConfigClient.new(url, @shell.get_env(:usherm_user), @shell.get_env(:usherm_pass))
108
+ when :qa
109
+ AcresClient.new(url, @shell.get_env(:kai_user), @shell.get_env(:kai_pass))
110
+ else
111
+ AcresClient.new(url, @shell.get_env(:usherm_user), @shell.get_env(:usherm_pass))
112
+ end
113
+ end
114
+
115
+ def client_interfaces
116
+ {
117
+ mercury:"/mercury/client?wrap",
118
+ central:"/central/central?wrap",
119
+ config:"/config/config?wrap",
120
+ qa:"/mercury/qatest?wrap"
121
+ }
122
+ end
@@ -0,0 +1,280 @@
1
+ require 'date'
2
+
3
+ category "Deployment"
4
+ description "Manage deployments"
5
+ usage [
6
+ "list -- list active publications",
7
+ "builds -- list builds",
8
+ "publish -- publish a build"
9
+ ]
10
+
11
+ opt :url, "Deployment server URL"
12
+
13
+ opt :app, "List builds for application", :type => :string
14
+ opt :jeb, "List builds for jeb (i.e. three-word name)", :type => :string
15
+ opt :branch, "List builds for branch", :type => :string
16
+ opt :commit, "List builds for commit hash", :type => :string
17
+ opt :yes, "Automatically include all apps in publication. No effect if --app is supplied."
18
+
19
+ opt :site, "When used with list, only list publications to given site. When used with publish, indicates site to deploy to.", :type => :integer
20
+
21
+
22
+ validate do
23
+ return false if args.length <= 1
24
+
25
+ case args[1]
26
+ when "builds"
27
+ args.length == 2
28
+ when "list"
29
+ true
30
+ when "publish"
31
+ return "Must supply at least one site ID" unless args.length >= 3
32
+ true
33
+ else
34
+ "#{args[1]}: unknown subcommand"
35
+ end
36
+ end
37
+
38
+ tab(:app) { all_apps }
39
+ tab(:jeb) { all_jebs }
40
+ tab(:branch) { all_branches }
41
+ tab(:commit) { all_commits }
42
+
43
+ tab 0 do
44
+ [:list, :builds, :publish]
45
+ end
46
+
47
+ run do
48
+ usherm.ensure_fresh
49
+ deployment.ensure_fresh
50
+ case args[1]
51
+ when "builds"
52
+ builds
53
+ when "list"
54
+ list
55
+ when "publish"
56
+ publish
57
+ end
58
+ end
59
+
60
+ ###
61
+ # Tab completors
62
+
63
+ def all_apps
64
+ all_of_key(:appId)
65
+ end
66
+
67
+ def all_branches
68
+ all_of_key(:branches)
69
+ end
70
+
71
+ def all_commits
72
+ all_of_key(:commitId)
73
+ end
74
+
75
+ def all_jebs
76
+ all_of_key(:buildId)
77
+ end
78
+
79
+ def all_of_key(key)
80
+ instances = {}
81
+
82
+ deployment[:buildList][:builds].each do |build|
83
+ if build[key].is_a? Array then
84
+ build[key].each { |item| instances[item] = true }
85
+ else
86
+ instances[build[key]] = true
87
+ end
88
+ end
89
+
90
+ instances.keys
91
+ end
92
+
93
+ ###
94
+ # List
95
+
96
+ def list
97
+ sites = if args.length >= 3 then
98
+ args[2..-1].map { |arg| arg.to_i }
99
+ else
100
+ (usherm[:usherSystemId][:usherSites].map { |site| site[:idSite] }).sort
101
+ end
102
+
103
+ sites.each do |site_id|
104
+ list_site(site_id, deployment.query(:buildPublicationList, site_id))
105
+ end
106
+ ""
107
+ end
108
+
109
+ def list_site(site_id, publications)
110
+ site = usherm.site_with_id(site_id) || {name:"Unknown site", siteDescription:"Not in usher"}
111
+ lines = []
112
+
113
+ lines.push "Site #{site_id} -- #{site[:name]} (#{site[:siteDescription]})"
114
+ publications[:orders].keys.sort.each do |app|
115
+ next if params[:app] and not talk.name_matches?(app, params[:app])
116
+ lines.push list_site_app(app.to_s, publications[:orders][app])
117
+ end
118
+
119
+ puts lines.join("\n")+"\n" unless lines.length == 1
120
+ end
121
+
122
+ def list_site_app(app, orders)
123
+ pub = orders.first
124
+ return if params[:jeb] and pub[:buildInfo][:buildId] != params[:jeb]
125
+ return if params[:commit] and pub[:buildInfo][:commitId] != params[:commit]
126
+ return if params[:branch] and not(pub[:buildInfo][:branches].include? params[:branch])
127
+ "\t#{timestamp(pub[:publicationTime])} #{app.style(:class)} -- #{build_info(pub[:buildInfo])}"
128
+ end
129
+
130
+ ###
131
+ # Builds
132
+
133
+ def builds
134
+ show_build_set(build_set)
135
+ ""
136
+ end
137
+
138
+ def build_set
139
+ if params[:jeb] then
140
+ builds_for_jeb(params[:jeb])
141
+ elsif params[:commit] then
142
+ builds_for_commit(params[:commit])
143
+ elsif params[:branch] then
144
+ builds_for_branch(params[:branch])
145
+ elsif params[:app] then
146
+ builds_for_app(params[:app])
147
+ else
148
+ deployment[:buildList][:builds]
149
+ end
150
+ end
151
+
152
+ def builds_for_app(app)
153
+ deployment[:buildList][:builds].select { |build| talk.name_matches?(build[:appId], app) }
154
+ end
155
+
156
+ def builds_for_commit(commit)
157
+ set =
158
+ if params[:app] then
159
+ builds_for_app(params[:app])
160
+ else
161
+ deployment[:buildList][:builds]
162
+ end
163
+
164
+ set.select { |build| build[:commitId].start_with? commit }
165
+ end
166
+
167
+ def builds_for_jeb(jeb)
168
+ set =
169
+ if params[:app] then
170
+ builds_for_app(params[:app])
171
+ else
172
+ deployment[:buildList][:builds]
173
+ end
174
+
175
+ set.select { |build| build[:buildId] == jeb }
176
+ end
177
+
178
+ def builds_for_branch(branch)
179
+ set =
180
+ if params[:app] then
181
+ builds_for_app(params[:app])
182
+ else
183
+ deployment[:buildList][:builds]
184
+ end
185
+
186
+ set.select { |build| build[:branches].include? branch }
187
+ end
188
+
189
+ def build_set_by_app(builds)
190
+ apps = {}
191
+ builds.each do |build|
192
+ apps[build[:appId]] ||= []
193
+ apps[build[:appId]].push build
194
+ end
195
+ apps
196
+ end
197
+
198
+ ###
199
+ # Publish
200
+
201
+ def prompt_for_apps(set)
202
+ apps = build_set_by_app(set)
203
+ return apps.keys.sort.map { |app| apps[app].last } if params[:yes]
204
+
205
+ selected = []
206
+ apps.keys.sort.each do |app|
207
+ build = apps[app].last
208
+ cls = sprintf("%s", app).style(:class)
209
+ name = sprintf("%s", build[:buildId]).style(:build_name)
210
+ branch = build[:branches].join(",").style(:branch)
211
+ line = sprintf("%99s", "#{cls}: #{name} [#{branch}]")
212
+
213
+ while true do
214
+ print "#{line} [y/N]> "
215
+ input = STDIN.gets.chomp.upcase rescue nil
216
+ input = "N" if not input.nil? and input.empty?
217
+
218
+ selected.push build if input == "Y"
219
+ puts if input.nil?
220
+ break if ["Y", "N"].include? input
221
+ end
222
+ end
223
+
224
+ selected
225
+ end
226
+
227
+ def publish
228
+ sites = args[2..-1].map { |arg| arg.to_i }
229
+ selected = prompt_for_apps(build_set)
230
+ selected.each do |build|
231
+ puts "Publishing #{build[:appId].style(:class)} \"#{build[:buildId].style(:build_name)}\" to site #{sites.join(",")}..."
232
+ deployment.publish_build(build, sites)
233
+ end
234
+ "Done."
235
+ end
236
+
237
+ ###
238
+ # Other stuff
239
+ def show_app(app, limit=50)
240
+ list = deployment.index_list_by_app[app]
241
+ return show_error("#{app}: No such app in deployment server") if list.nil?
242
+
243
+ list.each do |build|
244
+ puts show_build(build)
245
+ end
246
+
247
+ ""
248
+ end
249
+
250
+ def timestamp(ts)
251
+ DateTime.strptime(ts.to_s,'%s').strftime("%Y-%m-%d %H:%M:%S").style(:timestamp)
252
+ end
253
+
254
+ def build_id(build)
255
+ sprintf("%4d", build[:buildRecordId]).style(:identifier)
256
+ end
257
+
258
+ def branches(build)
259
+ build[:branches].join(",").style(:branch)
260
+ end
261
+
262
+ def author(build)
263
+ build[:author].style(:author) rescue ""
264
+ end
265
+
266
+ def build_info(build)
267
+ name = sprintf("%36s", build[:buildId]).style(:build_name)
268
+ "#{name} [#{branches(build)}] -- #{author(build)}"
269
+ end
270
+
271
+ def show_app(app, builds)
272
+ puts "#{app.style(:class)}: #{builds.length} builds"
273
+ builds.each { |build| puts timestamp(build[:postDate]) + " " + build_info(build) }
274
+ puts
275
+ end
276
+
277
+ def show_build_set(set)
278
+ apps = build_set_by_app(set)
279
+ apps.keys.sort.each { |app| show_app(app, apps[app]) }
280
+ end