azuki 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +71 -0
  3. data/bin/azuki +17 -0
  4. data/data/cacert.pem +3988 -0
  5. data/lib/azuki.rb +17 -0
  6. data/lib/azuki/auth.rb +339 -0
  7. data/lib/azuki/cli.rb +38 -0
  8. data/lib/azuki/client.rb +764 -0
  9. data/lib/azuki/client/azuki_postgresql.rb +141 -0
  10. data/lib/azuki/client/cisaurus.rb +26 -0
  11. data/lib/azuki/client/pgbackups.rb +113 -0
  12. data/lib/azuki/client/rendezvous.rb +108 -0
  13. data/lib/azuki/client/ssl_endpoint.rb +25 -0
  14. data/lib/azuki/command.rb +294 -0
  15. data/lib/azuki/command/account.rb +23 -0
  16. data/lib/azuki/command/accounts.rb +34 -0
  17. data/lib/azuki/command/addons.rb +305 -0
  18. data/lib/azuki/command/apps.rb +393 -0
  19. data/lib/azuki/command/auth.rb +86 -0
  20. data/lib/azuki/command/base.rb +230 -0
  21. data/lib/azuki/command/certs.rb +209 -0
  22. data/lib/azuki/command/config.rb +137 -0
  23. data/lib/azuki/command/db.rb +218 -0
  24. data/lib/azuki/command/domains.rb +85 -0
  25. data/lib/azuki/command/drains.rb +46 -0
  26. data/lib/azuki/command/fork.rb +164 -0
  27. data/lib/azuki/command/git.rb +64 -0
  28. data/lib/azuki/command/help.rb +179 -0
  29. data/lib/azuki/command/keys.rb +115 -0
  30. data/lib/azuki/command/labs.rb +147 -0
  31. data/lib/azuki/command/logs.rb +45 -0
  32. data/lib/azuki/command/maintenance.rb +61 -0
  33. data/lib/azuki/command/pg.rb +269 -0
  34. data/lib/azuki/command/pgbackups.rb +329 -0
  35. data/lib/azuki/command/plugins.rb +110 -0
  36. data/lib/azuki/command/ps.rb +232 -0
  37. data/lib/azuki/command/regions.rb +22 -0
  38. data/lib/azuki/command/releases.rb +124 -0
  39. data/lib/azuki/command/run.rb +180 -0
  40. data/lib/azuki/command/sharing.rb +89 -0
  41. data/lib/azuki/command/ssl.rb +43 -0
  42. data/lib/azuki/command/stack.rb +62 -0
  43. data/lib/azuki/command/status.rb +51 -0
  44. data/lib/azuki/command/update.rb +47 -0
  45. data/lib/azuki/command/version.rb +23 -0
  46. data/lib/azuki/deprecated.rb +5 -0
  47. data/lib/azuki/deprecated/help.rb +38 -0
  48. data/lib/azuki/distribution.rb +9 -0
  49. data/lib/azuki/excon.rb +9 -0
  50. data/lib/azuki/helpers.rb +517 -0
  51. data/lib/azuki/helpers/azuki_postgresql.rb +165 -0
  52. data/lib/azuki/helpers/log_displayer.rb +70 -0
  53. data/lib/azuki/plugin.rb +163 -0
  54. data/lib/azuki/updater.rb +171 -0
  55. data/lib/azuki/version.rb +3 -0
  56. data/lib/vendor/azuki/okjson.rb +598 -0
  57. data/spec/azuki/auth_spec.rb +256 -0
  58. data/spec/azuki/client/azuki_postgresql_spec.rb +71 -0
  59. data/spec/azuki/client/pgbackups_spec.rb +43 -0
  60. data/spec/azuki/client/rendezvous_spec.rb +62 -0
  61. data/spec/azuki/client/ssl_endpoint_spec.rb +48 -0
  62. data/spec/azuki/client_spec.rb +564 -0
  63. data/spec/azuki/command/addons_spec.rb +601 -0
  64. data/spec/azuki/command/apps_spec.rb +351 -0
  65. data/spec/azuki/command/auth_spec.rb +38 -0
  66. data/spec/azuki/command/base_spec.rb +109 -0
  67. data/spec/azuki/command/certs_spec.rb +178 -0
  68. data/spec/azuki/command/config_spec.rb +144 -0
  69. data/spec/azuki/command/db_spec.rb +110 -0
  70. data/spec/azuki/command/domains_spec.rb +87 -0
  71. data/spec/azuki/command/drains_spec.rb +34 -0
  72. data/spec/azuki/command/fork_spec.rb +56 -0
  73. data/spec/azuki/command/git_spec.rb +144 -0
  74. data/spec/azuki/command/help_spec.rb +93 -0
  75. data/spec/azuki/command/keys_spec.rb +120 -0
  76. data/spec/azuki/command/labs_spec.rb +100 -0
  77. data/spec/azuki/command/logs_spec.rb +60 -0
  78. data/spec/azuki/command/maintenance_spec.rb +51 -0
  79. data/spec/azuki/command/pg_spec.rb +236 -0
  80. data/spec/azuki/command/pgbackups_spec.rb +307 -0
  81. data/spec/azuki/command/plugins_spec.rb +104 -0
  82. data/spec/azuki/command/ps_spec.rb +195 -0
  83. data/spec/azuki/command/releases_spec.rb +130 -0
  84. data/spec/azuki/command/run_spec.rb +83 -0
  85. data/spec/azuki/command/sharing_spec.rb +59 -0
  86. data/spec/azuki/command/stack_spec.rb +46 -0
  87. data/spec/azuki/command/status_spec.rb +48 -0
  88. data/spec/azuki/command/version_spec.rb +16 -0
  89. data/spec/azuki/command_spec.rb +211 -0
  90. data/spec/azuki/helpers/azuki_postgresql_spec.rb +155 -0
  91. data/spec/azuki/helpers_spec.rb +48 -0
  92. data/spec/azuki/plugin_spec.rb +172 -0
  93. data/spec/azuki/updater_spec.rb +44 -0
  94. data/spec/helper/legacy_help.rb +16 -0
  95. data/spec/spec.opts +1 -0
  96. data/spec/spec_helper.rb +224 -0
  97. data/spec/support/display_message_matcher.rb +49 -0
  98. data/spec/support/openssl_mock_helper.rb +8 -0
  99. metadata +211 -0
@@ -0,0 +1,62 @@
1
+ require "azuki/command/base"
2
+
3
+ module Azuki::Command
4
+
5
+ # manage the stack for an app
6
+ class Stack < Base
7
+
8
+ # stack
9
+ #
10
+ # show the list of available stacks
11
+ #
12
+ #Example:
13
+ #
14
+ # $ azuki stack
15
+ # === example Available Stacks
16
+ # bamboo-mri-1.9.2
17
+ # bamboo-ree-1.8.7
18
+ # * cedar
19
+ #
20
+ def index
21
+ validate_arguments!
22
+
23
+ stacks_data = api.get_stack(app).body
24
+
25
+ styled_header("#{app} Available Stacks")
26
+ stacks = stacks_data.map do |stack|
27
+ row = [stack['current'] ? '*' : ' ', stack['name']]
28
+ row << '(beta)' if stack['beta']
29
+ row << '(deprecated)' if stack['deprecated']
30
+ row << '(prepared, will migrate on next git push)' if stack['requested']
31
+ row.join(' ')
32
+ end
33
+ styled_array(stacks)
34
+ end
35
+
36
+ # stack:migrate STACK
37
+ #
38
+ # prepare migration of this app to a new stack
39
+ #
40
+ #Example:
41
+ #
42
+ # $ azuki stack:migrate cedar
43
+ # -----> Preparing to migrate evening-warrior-2345
44
+ # bamboo-mri-1.9.2 -> bamboo-ree-1.8.7
45
+ #
46
+ # NOTE: You must specify ALL gems (including Rails) in manifest
47
+ #
48
+ # Please read the migration guide:
49
+ # http://devcenter.azukiapp.com/articles/bamboo
50
+ #
51
+ # -----> Migration prepared.
52
+ # Run 'git push azuki master' to execute migration.
53
+ #
54
+ def migrate
55
+ unless stack = shift_argument
56
+ error("Usage: azuki stack:migrate STACK.\nMust specify target stack.")
57
+ end
58
+
59
+ display(api.put_stack(app, stack).body)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,51 @@
1
+ require "azuki/command/base"
2
+
3
+ # check status of azuki platform
4
+ #
5
+ class Azuki::Command::Status < Azuki::Command::Base
6
+
7
+ # status
8
+ #
9
+ # display current status of azuki platform
10
+ #
11
+ #Example:
12
+ #
13
+ # $ azuki status
14
+ # === Azuki Status
15
+ # Development: No known issues at this time.
16
+ # Production: No known issues at this time.
17
+ #
18
+ def index
19
+ validate_arguments!
20
+
21
+ azuki_status_host = ENV['AZUKI_STATUS_HOST'] || "status.azukiapp.com"
22
+ require('excon')
23
+ status = json_decode(Excon.get("https://#{azuki_status_host}/api/v3/current-status.json", :nonblock => false).body)
24
+
25
+ styled_header("Azuki Status")
26
+
27
+ status['status'].each do |key, value|
28
+ if value == 'green'
29
+ status['status'][key] = 'No known issues at this time.'
30
+ end
31
+ end
32
+ styled_hash(status['status'])
33
+
34
+ unless status['issues'].empty?
35
+ display
36
+ status['issues'].each do |issue|
37
+ duration = time_ago(issue['created_at']).gsub(' ago', '+')
38
+ styled_header("#{issue['title']} #{duration}")
39
+ changes = issue['updates'].map do |issue|
40
+ [
41
+ time_ago(issue['created_at']),
42
+ issue['update_type'],
43
+ issue['contents']
44
+ ]
45
+ end
46
+ styled_array(changes, :sort => false)
47
+ end
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,47 @@
1
+ require "azuki/command/base"
2
+ require "azuki/updater"
3
+
4
+ # update the azuki client
5
+ #
6
+ class Azuki::Command::Update < Azuki::Command::Base
7
+
8
+ # update
9
+ #
10
+ # update the azuki client
11
+ #
12
+ # Example:
13
+ #
14
+ # $ azuki update
15
+ # Updating from v1.2.3... done, updated to v2.3.4
16
+ #
17
+ def index
18
+ validate_arguments!
19
+ update_from_url("https://toolbelt.azukiapp.com/download/zip")
20
+ end
21
+
22
+ # update:beta
23
+ #
24
+ # update to the latest beta client
25
+ #
26
+ # $ azuki update
27
+ # Updating from v1.2.3... done, updated to v2.3.4.pre
28
+ #
29
+ def beta
30
+ validate_arguments!
31
+ update_from_url("https://toolbelt.azukiapp.com/download/beta-zip")
32
+ end
33
+
34
+ private
35
+
36
+ def update_from_url(url)
37
+ Azuki::Updater.check_disabled!
38
+ action("Updating from #{Azuki::VERSION}") do
39
+ if new_version = Azuki::Updater.update(url)
40
+ status("updated to #{new_version}")
41
+ else
42
+ status("nothing to update")
43
+ end
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,23 @@
1
+ require "azuki/command/base"
2
+ require "azuki/version"
3
+
4
+ # display version
5
+ #
6
+ class Azuki::Command::Version < Azuki::Command::Base
7
+
8
+ # version
9
+ #
10
+ # show azuki client version
11
+ #
12
+ #Example:
13
+ #
14
+ # $ azuki version
15
+ # azuki-toolbelt/1.2.3 (x86_64-darwin11.2.0) ruby/1.9.3
16
+ #
17
+ def index
18
+ validate_arguments!
19
+
20
+ display(Azuki.user_agent)
21
+ end
22
+
23
+ end
@@ -0,0 +1,5 @@
1
+ require "azuki"
2
+
3
+ module Azuki::Deprecated
4
+ end
5
+
@@ -0,0 +1,38 @@
1
+ require "azuki/deprecated"
2
+
3
+ module Azuki::Deprecated::Help
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ class HelpGroup < Array
9
+ attr_reader :title
10
+
11
+ def initialize(title)
12
+ @title = title
13
+ end
14
+
15
+ def command(name, description)
16
+ self << [name, description]
17
+ end
18
+
19
+ def space
20
+ self << ['', '']
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def groups
26
+ @groups ||= []
27
+ end
28
+
29
+ def group(title, &block)
30
+ groups << begin
31
+ group = HelpGroup.new(title)
32
+ yield group
33
+ group
34
+ end
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,9 @@
1
+ module Azuki
2
+ module Distribution
3
+ def self.files
4
+ Dir[File.expand_path("../../../{bin,data,lib}/**/*", __FILE__)].select do |file|
5
+ File.file?(file)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Excon
2
+
3
+ def self.get_with_redirect(url, options={})
4
+ res = Excon.get(url, options)
5
+ return self.get_with_redirect(res.headers["Location"], options) if res.status == 302
6
+ res
7
+ end
8
+
9
+ end
@@ -0,0 +1,517 @@
1
+ require "vendor/azuki/okjson"
2
+
3
+ module Azuki
4
+ module Helpers
5
+
6
+ extend self
7
+
8
+ def home_directory
9
+ running_on_windows? ? ENV['USERPROFILE'].gsub("\\","/") : ENV['HOME']
10
+ end
11
+
12
+ def running_on_windows?
13
+ RUBY_PLATFORM =~ /mswin32|mingw32/
14
+ end
15
+
16
+ def running_on_a_mac?
17
+ RUBY_PLATFORM =~ /-darwin\d/
18
+ end
19
+
20
+ def display(msg="", new_line=true)
21
+ if new_line
22
+ puts(msg)
23
+ else
24
+ print(msg)
25
+ $stdout.flush
26
+ end
27
+ end
28
+
29
+ def redisplay(line, line_break = false)
30
+ display("\r\e[0K#{line}", line_break)
31
+ end
32
+
33
+ def deprecate(message)
34
+ display "WARNING: #{message}"
35
+ end
36
+
37
+ def confirm_billing
38
+ display
39
+ display "This action will cause your account to be billed at the end of the month"
40
+ display "For more information, see https://devcenter.azukiapp.com/articles/usage-and-billing"
41
+ if confirm
42
+ Azuki::Auth.client.confirm_billing
43
+ true
44
+ end
45
+ end
46
+
47
+ def confirm(message="Are you sure you wish to continue? (y/n)")
48
+ display("#{message} ", false)
49
+ ['y', 'yes'].include?(ask.downcase)
50
+ end
51
+
52
+ def confirm_command(app_to_confirm = app, message=nil)
53
+ if confirmed_app = Azuki::Command.current_options[:confirm]
54
+ unless confirmed_app == app_to_confirm
55
+ raise(Azuki::Command::CommandFailed, "Confirmed app #{confirmed_app} did not match the selected app #{app_to_confirm}.")
56
+ end
57
+ return true
58
+ else
59
+ display
60
+ message ||= "WARNING: Destructive Action\nThis command will affect the app: #{app_to_confirm}"
61
+ message << "\nTo proceed, type \"#{app_to_confirm}\" or re-run this command with --confirm #{app_to_confirm}"
62
+ output_with_bang(message)
63
+ display
64
+ display "> ", false
65
+ if ask.downcase != app_to_confirm
66
+ error("Confirmation did not match #{app_to_confirm}. Aborted.")
67
+ else
68
+ true
69
+ end
70
+ end
71
+ end
72
+
73
+ def format_date(date)
74
+ date = Time.parse(date).utc if date.is_a?(String)
75
+ date.strftime("%Y-%m-%d %H:%M %Z").gsub('GMT', 'UTC')
76
+ end
77
+
78
+ def ask
79
+ $stdin.gets.to_s.strip
80
+ end
81
+
82
+ def shell(cmd)
83
+ FileUtils.cd(Dir.pwd) {|d| return `#{cmd}`}
84
+ end
85
+
86
+ def run_command(command, args=[])
87
+ Azuki::Command.run(command, args)
88
+ end
89
+
90
+ def retry_on_exception(*exceptions)
91
+ retry_count = 0
92
+ begin
93
+ yield
94
+ rescue *exceptions => ex
95
+ raise ex if retry_count >= 3
96
+ sleep 3
97
+ retry_count += 1
98
+ retry
99
+ end
100
+ end
101
+
102
+ def has_git?
103
+ %x{ git --version }
104
+ $?.success?
105
+ end
106
+
107
+ def git(args)
108
+ return "" unless has_git?
109
+ flattened_args = [args].flatten.compact.join(" ")
110
+ %x{ git #{flattened_args} 2>&1 }.strip
111
+ end
112
+
113
+ def time_ago(since)
114
+ if since.is_a?(String)
115
+ since = Time.parse(since)
116
+ end
117
+
118
+ elapsed = Time.now - since
119
+
120
+ message = since.strftime("%Y/%m/%d %H:%M:%S")
121
+ if elapsed <= 60
122
+ message << " (~ #{elapsed.floor}s ago)"
123
+ elsif elapsed <= (60 * 60)
124
+ message << " (~ #{(elapsed / 60).floor}m ago)"
125
+ elsif elapsed <= (60 * 60 * 25)
126
+ message << " (~ #{(elapsed / 60 / 60).floor}h ago)"
127
+ end
128
+ message
129
+ end
130
+
131
+ def truncate(text, length)
132
+ if text.size > length
133
+ text[0, length - 2] + '..'
134
+ else
135
+ text
136
+ end
137
+ end
138
+
139
+ @@kb = 1024
140
+ @@mb = 1024 * @@kb
141
+ @@gb = 1024 * @@mb
142
+ def format_bytes(amount)
143
+ amount = amount.to_i
144
+ return '(empty)' if amount == 0
145
+ return amount if amount < @@kb
146
+ return "#{(amount / @@kb).round}k" if amount < @@mb
147
+ return "#{(amount / @@mb).round}M" if amount < @@gb
148
+ return "#{(amount / @@gb).round}G"
149
+ end
150
+
151
+ def quantify(string, num)
152
+ "%d %s" % [ num, num.to_i == 1 ? string : "#{string}s" ]
153
+ end
154
+
155
+ def create_git_remote(remote, url)
156
+ return if git('remote').split("\n").include?(remote)
157
+ return unless File.exists?(".git")
158
+ git "remote add #{remote} #{url}"
159
+ display "Git remote #{remote} added"
160
+ end
161
+
162
+ def longest(items)
163
+ items.map { |i| i.to_s.length }.sort.last
164
+ end
165
+
166
+ def display_table(objects, columns, headers)
167
+ lengths = []
168
+ columns.each_with_index do |column, index|
169
+ header = headers[index]
170
+ lengths << longest([header].concat(objects.map { |o| o[column].to_s }))
171
+ end
172
+ lines = lengths.map {|length| "-" * length}
173
+ lengths[-1] = 0 # remove padding from last column
174
+ display_row headers, lengths
175
+ display_row lines, lengths
176
+ objects.each do |row|
177
+ display_row columns.map { |column| row[column] }, lengths
178
+ end
179
+ end
180
+
181
+ def display_row(row, lengths)
182
+ row_data = []
183
+ row.zip(lengths).each do |column, length|
184
+ format = column.is_a?(Fixnum) ? "%#{length}s" : "%-#{length}s"
185
+ row_data << format % column
186
+ end
187
+ display(row_data.join(" "))
188
+ end
189
+
190
+ def json_encode(object)
191
+ Azuki::OkJson.encode(object)
192
+ rescue Azuki::OkJson::Error
193
+ nil
194
+ end
195
+
196
+ def json_decode(json)
197
+ Azuki::OkJson.decode(json)
198
+ rescue Azuki::OkJson::Error
199
+ nil
200
+ end
201
+
202
+ def set_buffer(enable)
203
+ with_tty do
204
+ if enable
205
+ `stty icanon echo`
206
+ else
207
+ `stty -icanon -echo`
208
+ end
209
+ end
210
+ end
211
+
212
+ def with_tty(&block)
213
+ return unless $stdin.isatty
214
+ begin
215
+ yield
216
+ rescue
217
+ # fails on windows
218
+ end
219
+ end
220
+
221
+ def get_terminal_environment
222
+ { "TERM" => ENV["TERM"], "COLUMNS" => `tput cols`.strip, "LINES" => `tput lines`.strip }
223
+ rescue
224
+ { "TERM" => ENV["TERM"] }
225
+ end
226
+
227
+ def fail(message)
228
+ raise Azuki::Command::CommandFailed, message
229
+ end
230
+
231
+ ## DISPLAY HELPERS
232
+
233
+ def action(message, options={})
234
+ display("#{message}... ", false)
235
+ Azuki::Helpers.error_with_failure = true
236
+ ret = yield
237
+ Azuki::Helpers.error_with_failure = false
238
+ display((options[:success] || "done"), false)
239
+ if @status
240
+ display(", #{@status}", false)
241
+ @status = nil
242
+ end
243
+ display
244
+ ret
245
+ end
246
+
247
+ def status(message)
248
+ @status = message
249
+ end
250
+
251
+ def format_with_bang(message)
252
+ return '' if message.to_s.strip == ""
253
+ " ! " + message.split("\n").join("\n ! ")
254
+ end
255
+
256
+ def output_with_bang(message="", new_line=true)
257
+ return if message.to_s.strip == ""
258
+ display(format_with_bang(message), new_line)
259
+ end
260
+
261
+ def error(message)
262
+ if Azuki::Helpers.error_with_failure
263
+ display("failed")
264
+ Azuki::Helpers.error_with_failure = false
265
+ end
266
+ $stderr.puts(format_with_bang(message))
267
+ exit(1)
268
+ end
269
+
270
+ def self.error_with_failure
271
+ @@error_with_failure ||= false
272
+ end
273
+
274
+ def self.error_with_failure=(new_error_with_failure)
275
+ @@error_with_failure = new_error_with_failure
276
+ end
277
+
278
+ def self.included_into
279
+ @@included_into ||= []
280
+ end
281
+
282
+ def self.extended_into
283
+ @@extended_into ||= []
284
+ end
285
+
286
+ def self.included(base)
287
+ included_into << base
288
+ end
289
+
290
+ def self.extended(base)
291
+ extended_into << base
292
+ end
293
+
294
+ def display_header(message="", new_line=true)
295
+ return if message.to_s.strip == ""
296
+ display("=== " + message.to_s.split("\n").join("\n=== "), new_line)
297
+ end
298
+
299
+ def display_object(object)
300
+ case object
301
+ when Array
302
+ # list of objects
303
+ object.each do |item|
304
+ display_object(item)
305
+ end
306
+ when Hash
307
+ # if all values are arrays, it is a list with headers
308
+ # otherwise it is a single header with pairs of data
309
+ if object.values.all? {|value| value.is_a?(Array)}
310
+ object.keys.sort_by {|key| key.to_s}.each do |key|
311
+ display_header(key)
312
+ display_object(object[key])
313
+ hputs
314
+ end
315
+ end
316
+ else
317
+ hputs(object.to_s)
318
+ end
319
+ end
320
+
321
+ def hputs(string='')
322
+ Kernel.puts(string)
323
+ end
324
+
325
+ def hprint(string='')
326
+ Kernel.print(string)
327
+ $stdout.flush
328
+ end
329
+
330
+ def spinner(ticks)
331
+ %w(/ - \\ |)[ticks % 4]
332
+ end
333
+
334
+ def launchy(message, url)
335
+ action(message) do
336
+ require("launchy")
337
+ launchy = Launchy.open(url)
338
+ if launchy.respond_to?(:join)
339
+ launchy.join
340
+ end
341
+ end
342
+ end
343
+
344
+ # produces a printf formatter line for an array of items
345
+ # if an individual line item is an array, it will create columns
346
+ # that are lined-up
347
+ #
348
+ # line_formatter(["foo", "barbaz"]) # => "%-6s"
349
+ # line_formatter(["foo", "barbaz"], ["bar", "qux"]) # => "%-3s %-6s"
350
+ #
351
+ def line_formatter(array)
352
+ if array.any? {|item| item.is_a?(Array)}
353
+ cols = []
354
+ array.each do |item|
355
+ if item.is_a?(Array)
356
+ item.each_with_index { |val,idx| cols[idx] = [cols[idx]||0, (val || '').length].max }
357
+ end
358
+ end
359
+ cols.map { |col| "%-#{col}s" }.join(" ")
360
+ else
361
+ "%s"
362
+ end
363
+ end
364
+
365
+ def styled_array(array, options={})
366
+ fmt = line_formatter(array)
367
+ array = array.sort unless options[:sort] == false
368
+ array.each do |element|
369
+ display((fmt % element).rstrip)
370
+ end
371
+ display
372
+ end
373
+
374
+ def format_error(error, message='Azuki client internal error.')
375
+ formatted_error = []
376
+ formatted_error << " ! #{message}"
377
+ formatted_error << ' ! Search for help at: https://help.azukiapp.com'
378
+ formatted_error << ' ! Or report a bug at: https://github.com/azuki/azuki/issues/new'
379
+ formatted_error << ''
380
+ formatted_error << " Error: #{error.message} (#{error.class})"
381
+ formatted_error << " Backtrace: #{error.backtrace.first}"
382
+ error.backtrace[1..-1].each do |line|
383
+ formatted_error << " #{line}"
384
+ end
385
+ if error.backtrace.length > 1
386
+ formatted_error << ''
387
+ end
388
+ command = ARGV.map do |arg|
389
+ if arg.include?(' ')
390
+ arg = %{"#{arg}"}
391
+ else
392
+ arg
393
+ end
394
+ end.join(' ')
395
+ formatted_error << " Command: azuki #{command}"
396
+ require 'azuki/auth'
397
+ unless Azuki::Auth.host == Azuki::Auth.default_host
398
+ formatted_error << " Host: #{Azuki::Auth.host}"
399
+ end
400
+ if http_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
401
+ formatted_error << " HTTP Proxy: #{http_proxy}"
402
+ end
403
+ if https_proxy = ENV['https_proxy'] || ENV['HTTPS_PROXY']
404
+ formatted_error << " HTTPS Proxy: #{https_proxy}"
405
+ end
406
+ plugins = Azuki::Plugin.list.sort
407
+ unless plugins.empty?
408
+ formatted_error << " Plugins: #{plugins.first}"
409
+ plugins[1..-1].each do |plugin|
410
+ formatted_error << " #{plugin}"
411
+ end
412
+ if plugins.length > 1
413
+ formatted_error << ''
414
+ $stderr.puts
415
+ end
416
+ end
417
+ formatted_error << " Version: #{Azuki.user_agent}"
418
+ formatted_error << "\n"
419
+ formatted_error.join("\n")
420
+ end
421
+
422
+ def styled_error(error, message='Azuki client internal error.')
423
+ if Azuki::Helpers.error_with_failure
424
+ display("failed")
425
+ Azuki::Helpers.error_with_failure = false
426
+ end
427
+ $stderr.puts(format_error(error, message))
428
+ end
429
+
430
+ def styled_header(header)
431
+ display("=== #{header}")
432
+ end
433
+
434
+ def styled_hash(hash, keys=nil)
435
+ max_key_length = hash.keys.map {|key| key.to_s.length}.max + 2
436
+ keys ||= hash.keys.sort {|x,y| x.to_s <=> y.to_s}
437
+ keys.each do |key|
438
+ case value = hash[key]
439
+ when Array
440
+ if value.empty?
441
+ next
442
+ else
443
+ elements = value.sort {|x,y| x.to_s <=> y.to_s}
444
+ display("#{key}: ".ljust(max_key_length), false)
445
+ display(elements[0])
446
+ elements[1..-1].each do |element|
447
+ display("#{' ' * max_key_length}#{element}")
448
+ end
449
+ if elements.length > 1
450
+ display
451
+ end
452
+ end
453
+ when nil
454
+ next
455
+ else
456
+ display("#{key}: ".ljust(max_key_length), false)
457
+ display(value)
458
+ end
459
+ end
460
+ end
461
+
462
+ def string_distance(first, last)
463
+ distances = [] # 0x0s
464
+ 0.upto(first.length) do |index|
465
+ distances << [index] + [0] * last.length
466
+ end
467
+ distances[0] = 0.upto(last.length).to_a
468
+ 1.upto(last.length) do |last_index|
469
+ 1.upto(first.length) do |first_index|
470
+ first_char = first[first_index - 1, 1]
471
+ last_char = last[last_index - 1, 1]
472
+ if first_char == last_char
473
+ distances[first_index][last_index] = distances[first_index - 1][last_index - 1] # noop
474
+ else
475
+ distances[first_index][last_index] = [
476
+ distances[first_index - 1][last_index], # deletion
477
+ distances[first_index][last_index - 1], # insertion
478
+ distances[first_index - 1][last_index - 1] # substitution
479
+ ].min + 1 # cost
480
+ if first_index > 1 && last_index > 1
481
+ first_previous_char = first[first_index - 2, 1]
482
+ last_previous_char = last[last_index - 2, 1]
483
+ if first_char == last_previous_char && first_previous_char == last_char
484
+ distances[first_index][last_index] = [
485
+ distances[first_index][last_index],
486
+ distances[first_index - 2][last_index - 2] + 1 # transposition
487
+ ].min
488
+ end
489
+ end
490
+ end
491
+ end
492
+ end
493
+ distances[first.length][last.length]
494
+ end
495
+
496
+ def suggestion(actual, possibilities)
497
+ distances = Hash.new {|hash,key| hash[key] = []}
498
+
499
+ possibilities.each do |suggestion|
500
+ distances[string_distance(actual, suggestion)] << suggestion
501
+ end
502
+
503
+ minimum_distance = distances.keys.min
504
+ if minimum_distance < 4
505
+ suggestions = distances[minimum_distance].sort
506
+ if suggestions.length == 1
507
+ "Perhaps you meant `#{suggestions.first}`."
508
+ else
509
+ "Perhaps you meant #{suggestions[0...-1].map {|suggestion| "`#{suggestion}`"}.join(', ')} or `#{suggestions.last}`."
510
+ end
511
+ else
512
+ nil
513
+ end
514
+ end
515
+
516
+ end
517
+ end