azuki 0.0.1

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 (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