nimbu 0.4 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/nimbu.rb CHANGED
@@ -1,28 +1,12 @@
1
1
  require "nimbu/version"
2
- require "nimbu/client"
2
+ require "nimbu-api"
3
3
 
4
4
  module Nimbu
5
5
  def self.debug=(value)
6
- @@debug = value
6
+ @debug = value
7
7
  end
8
8
 
9
9
  def self.debug
10
- @@debug
11
- end
12
-
13
- def self.development=(value)
14
- @@development = value
15
- end
16
-
17
- def self.development
18
- @@development
19
- end
20
-
21
- def self.v2=(value)
22
- @@v2 = value
23
- end
24
-
25
- def self.v2
26
- @@v2
10
+ @debug || false
27
11
  end
28
12
  end
data/lib/nimbu/auth.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require "yaml"
2
2
  require "nimbu"
3
- require "nimbu/client"
4
3
  require "nimbu/helpers"
5
4
 
6
5
  class Nimbu::Auth
@@ -12,9 +11,7 @@ class Nimbu::Auth
12
11
 
13
12
  def client
14
13
  @client ||= begin
15
- client = Nimbu::Client.new(user, password, host)
16
- client.on_warning { |msg| self.display("\n#{msg}\n\n") }
17
- client
14
+ Nimbu::Client.new(:oauth_token => token, :endpoint => host)
18
15
  end
19
16
  end
20
17
 
@@ -29,34 +26,38 @@ class Nimbu::Auth
29
26
 
30
27
  # just a stub; will raise if not authenticated
31
28
  def check
32
- client.list
29
+ client.sites.list
30
+ end
31
+
32
+ def host
33
+ ENV['NIMBU_HOST'] || "https://api.nimbu.io"
33
34
  end
34
35
 
35
36
  def default_host
36
- "getnimbu.com"
37
+ "https://api.nimbu.io"
37
38
  end
38
39
 
39
- def host
40
- @host ||= ENV['NIMBU_HOST'] || get_nimbu_host
40
+ def admin_host
41
+ @admin_host ||= host.gsub(/https?\:\/\/api\./,'')
42
+ end
43
+
44
+ def site
45
+ @site ||= ENV['NIMBU_SITE'] || get_nimbu_site
41
46
  end
42
47
 
43
48
  def theme
44
49
  @theme ||= ENV['NIMBU_THEME'] || get_nimbu_theme
45
50
  end
46
51
 
47
- def get_nimbu_host
48
- if Nimbu.development
49
- get_configuration[:development]
50
- else
51
- get_configuration[:hostname]
52
- end
52
+ def get_nimbu_site
53
+ get_configuration["site"]
53
54
  end
54
55
 
55
56
  def get_nimbu_theme
56
- get_configuration[:theme] || "default-theme"
57
+ get_configuration["theme"] || "default-theme"
57
58
  end
58
59
 
59
- def get_configuration # :nodoc:
60
+ def get_configuration
60
61
  @configuration ||= (read_configuration || ask_for_and_save_configuration)
61
62
  end
62
63
 
@@ -75,20 +76,75 @@ class Nimbu::Auth
75
76
  @host = nil
76
77
  end
77
78
 
78
- def ask_for_configuration
79
- puts "What is the hostname for this Nimbu site?"
80
- print "Hostname: "
81
- hostname = ask
79
+ def ask_for_configuration
82
80
 
83
- puts "What is the theme you are developing in this directory?"
84
- print "Theme (i.e. default): "
85
- theme = ask
81
+ subdomain = nil
82
+ sites = client.sites.list
86
83
 
87
- {:hostname => hostname, :theme => theme}
84
+ unless sites.respond_to?(:any?) && sites.any?
85
+ display "You don't have access to any Nimbu sites you can edit yet..."
86
+ display ""
87
+ display "Please visit http://nimbu.io, start your 30-day trial and discover our amazing platform!"
88
+ exit(1)
89
+ else
90
+ print_separator
91
+ display "\nLet's first setup the configuration for this directory..."
92
+ display "\nYou have access to following sites:\n"
93
+ sites.each_with_index do |site,i|
94
+ display " #{i+1}) #{site.name.white.bold} => http://#{site.domain}"
95
+ end
96
+ site_number = 0
97
+ retry_site = false
98
+ while site_number < 1 || site_number > sites.length
99
+ unless retry_site
100
+ print "\nOn which site would you like to work? "
101
+ else
102
+ print "\nPlease enter the number of your site (between 1-#{sites.length}): "
103
+ end
104
+ site_number_string = ask
105
+ site_number = site_number_string.to_i rescue 0
106
+ retry_site = true
107
+ end
108
+ puts ""
109
+ site = sites[site_number-1]
110
+ display "Site chosen => #{site.name.white.bold} (http://#{site.domain})"
111
+ subdomain = site.subdomain
112
+ @site = subdomain
113
+ end
114
+
115
+ themes = client.themes(:subdomain => subdomain).list
116
+ current_theme = if themes.length > 1
117
+ theme_number = 0
118
+ retry_theme = false
119
+ while theme_number < 1 || theme_number > themes.length
120
+ unless retry_theme
121
+ print "\nOn which theme would you like to work in this directory? "
122
+ else
123
+ print "\nPlease enter the number of your theme (between 1-#{themes.length}): "
124
+ end
125
+ theme_number_string = ask
126
+ theme_number = theme_number_string.to_i rescue 0
127
+ retry_theme = true
128
+ end
129
+ puts ""
130
+ display "Theme chosen => #{themes[theme_number-1].name}"
131
+ themes[theme_number-1]
132
+ else
133
+ themes.first
134
+ end
135
+ @theme = current_theme.short
136
+ print_separator
137
+
138
+ { "site" => subdomain, "theme" => current_theme.short }
88
139
  end
89
140
 
90
141
  def read_configuration
91
- File.exists?(configuration_file) and YAML::load(File.open( configuration_file ))
142
+ existing_config = YAML::load(File.open( configuration_file )) if File.exists?(configuration_file)
143
+ if existing_config && ! existing_config["site"].nil?
144
+ existing_config
145
+ else
146
+ nil
147
+ end
92
148
  end
93
149
 
94
150
  def write_configuration
@@ -102,23 +158,15 @@ class Nimbu::Auth
102
158
  @credentials = ask_for_and_save_credentials
103
159
  end
104
160
 
105
- def user # :nodoc:
106
- get_credentials[0]
107
- end
108
-
109
- def password # :nodoc:
110
- get_credentials[1]
111
- end
112
-
113
- def api_key
114
- Nimbu::Client.auth(user, password)["api_key"]
161
+ def token # :nodoc:
162
+ get_credentials
115
163
  end
116
164
 
117
165
  def credentials_file
118
166
  if host == default_host
119
167
  "#{home_directory}/.nimbu/credentials"
120
168
  else
121
- "#{home_directory}/.nimbu/credentials.#{CGI.escape(host)}"
169
+ "#{home_directory}/.nimbu/credentials.#{CGI.escape(host.gsub(/https?\:\/\//,''))}"
122
170
  end
123
171
  end
124
172
 
@@ -132,16 +180,17 @@ class Nimbu::Auth
132
180
  end
133
181
 
134
182
  def read_credentials
135
- if ENV['NIMBU_API_KEY']
136
- ['', ENV['NIMBU_API_KEY']]
183
+ credentials = File.read(credentials_file) if File.exists?(credentials_file)
184
+ if credentials && credentials =~ /^(bearer|oauth2|token) ([\w]+)$/i
185
+ $2
137
186
  else
138
- File.exists?(credentials_file) and File.read(credentials_file).split("\n")
187
+ nil
139
188
  end
140
189
  end
141
190
 
142
191
  def write_credentials
143
192
  FileUtils.mkdir_p(File.dirname(credentials_file))
144
- File.open(credentials_file, 'w') {|credentials| credentials.puts(self.credentials)}
193
+ File.open(credentials_file, 'w') {|credentials| credentials.print("token #{self.credentials}")}
145
194
  FileUtils.chmod(0700, File.dirname(credentials_file))
146
195
  FileUtils.chmod(0600, credentials_file)
147
196
  end
@@ -159,16 +208,21 @@ class Nimbu::Auth
159
208
  end
160
209
 
161
210
  def ask_for_credentials
162
- puts "Enter your Nimbu credentials."
163
-
164
- print "Email: "
211
+ print "Login: "
165
212
  user = ask
166
213
 
167
214
  print "Password: "
168
215
  password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
169
- api_key = Nimbu::Client.auth(user, password)['api_key']
170
216
 
171
- [user, api_key]
217
+ begin
218
+ oauth = Nimbu::Client.new(:basic_auth => "#{user}:#{password}", :endpoint => host).authenticate
219
+ oauth.token
220
+ rescue Exception => e
221
+ if e.respond_to?(:http_status_code) && e.http_status_code == 401
222
+ display " => could not login... please check your username and/or password!\n\n"
223
+ end
224
+ nil
225
+ end
172
226
  end
173
227
 
174
228
  def ask_for_password_on_windows
@@ -202,15 +256,11 @@ class Nimbu::Auth
202
256
  end
203
257
 
204
258
  def ask_for_and_save_credentials
259
+ display "Please authenticate with Nimbu.io:"
205
260
  begin
206
261
  @credentials = ask_for_credentials
207
262
  write_credentials
208
263
  check
209
- rescue ::RestClient::Unauthorized, ::RestClient::ResourceNotFound => e
210
- delete_credentials
211
- display "Authentication failed."
212
- retry if retry_login?
213
- exit 1
214
264
  rescue Exception => e
215
265
  delete_credentials
216
266
  raise e
@@ -218,55 +268,16 @@ class Nimbu::Auth
218
268
  @credentials
219
269
  end
220
270
 
221
- def check_for_associated_ssh_key
222
- return unless client.keys.empty?
223
- associate_or_generate_ssh_key
224
- end
225
-
226
- def associate_or_generate_ssh_key
227
- public_keys = Dir.glob("#{home_directory}/.ssh/*.pub").sort
228
-
229
- case public_keys.length
230
- when 0 then
231
- display "Could not find an existing public key."
232
- display "Would you like to generate one? [Yn] ", false
233
- unless ask.strip.downcase == "n"
234
- display "Generating new SSH public key."
235
- generate_ssh_key("id_rsa")
236
- associate_key("#{home_directory}/.ssh/id_rsa.pub")
237
- end
238
- when 1 then
239
- display "Found existing public key: #{public_keys.first}"
240
- associate_key(public_keys.first)
241
- else
242
- display "Found the following SSH public keys:"
243
- public_keys.each_with_index do |key, index|
244
- display "#{index+1}) #{File.basename(key)}"
245
- end
246
- display "Which would you like to use with your Nimbu account? ", false
247
- chosen = public_keys[ask.to_i-1] rescue error("Invalid choice")
248
- associate_key(chosen)
249
- end
250
- end
251
-
252
- def generate_ssh_key(keyfile)
253
- ssh_dir = File.join(home_directory, ".ssh")
254
- unless File.exists?(ssh_dir)
255
- FileUtils.mkdir_p ssh_dir
256
- File.chmod(0700, ssh_dir)
257
- end
258
- `ssh-keygen -t rsa -N "" -f \"#{home_directory}/.ssh/#{keyfile}\" 2>&1`
259
- end
260
-
261
- def associate_key(key)
262
- display "Uploading SSH public key #{key}"
263
- client.add_key(File.read(key))
264
- end
265
-
266
271
  def retry_login?
267
272
  @login_attempts ||= 0
268
273
  @login_attempts += 1
269
274
  @login_attempts < 3
270
275
  end
276
+
277
+ def print_separator
278
+ print "\n"
279
+ 60.times { print "#"}
280
+ print "\n"
281
+ end
271
282
  end
272
283
  end
data/lib/nimbu/cli.rb CHANGED
@@ -1,12 +1,29 @@
1
1
  require "nimbu"
2
2
  require "nimbu/command"
3
+ require "nimbu/helpers"
3
4
 
4
5
  class Nimbu::CLI
5
6
 
7
+ extend Nimbu::Helpers
8
+
6
9
  def self.start(*args)
7
- command = args.shift.strip rescue "help"
8
- Nimbu::Command.load
9
- Nimbu::Command.run(command, args)
10
+ begin
11
+ if $stdin.isatty
12
+ $stdin.sync = true
13
+ end
14
+ if $stdout.isatty
15
+ $stdout.sync = true
16
+ end
17
+ command = args.shift.strip rescue "help"
18
+ Nimbu::Command.load
19
+ Nimbu::Command.run(command, args)
20
+ rescue Interrupt
21
+ `stty icanon echo`
22
+ error("Command cancelled.")
23
+ rescue => error
24
+ styled_error(error)
25
+ exit(1)
26
+ end
10
27
  end
11
28
 
12
29
  end
data/lib/nimbu/command.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  require 'nimbu/helpers'
2
2
  require 'nimbu/version'
3
+ require 'term/ansicolor'
3
4
  require "optparse"
4
5
 
6
+ class String
7
+ include Term::ANSIColor
8
+ end
9
+
5
10
  module Nimbu
6
11
  module Command
7
12
  class CommandFailed < RuntimeError; end
@@ -22,6 +27,10 @@ module Nimbu
22
27
  @@command_aliases ||= {}
23
28
  end
24
29
 
30
+ def self.files
31
+ @@files ||= Hash.new {|hash,key| hash[key] = File.readlines(key).map {|line| line.strip}}
32
+ end
33
+
25
34
  def self.namespaces
26
35
  @@namespaces ||= {}
27
36
  end
@@ -38,72 +47,70 @@ module Nimbu
38
47
  @current_command
39
48
  end
40
49
 
50
+ def self.current_command=(new_current_command)
51
+ @current_command = new_current_command
52
+ end
53
+
41
54
  def self.current_args
42
55
  @current_args
43
56
  end
44
57
 
45
58
  def self.current_options
46
- @current_options
59
+ @current_options ||= {}
47
60
  end
48
61
 
49
62
  def self.global_options
50
63
  @global_options ||= []
51
64
  end
52
65
 
53
- def self.global_option(name, *args)
54
- global_options << { :name => name, :args => args }
66
+ def self.global_option(name, *args, &blk)
67
+ global_options << { :name => name.to_s, :args => args.sort.reverse, :proc => blk }
68
+ end
69
+
70
+ def self.warnings
71
+ @warnings ||= []
72
+ end
73
+
74
+ def self.display_warnings
75
+ unless warnings.empty?
76
+ $stderr.puts(warnings.map {|warning| " ! #{warning}"}.join("\n"))
77
+ end
55
78
  end
56
79
 
57
- global_option :app, "--app APP", "-a"
58
- global_option :confirm, "--confirm APP"
59
80
  global_option :help, "--help", "-h"
60
- global_option :remote, "--remote REMOTE"
81
+ global_option :debug, "--debug"
61
82
 
62
83
  def self.prepare_run(cmd, args=[])
63
84
  command = parse(cmd)
64
85
 
65
- unless command
66
- if %w( -v --version ).include?(cmd)
67
- display Nimbu::VERSION
68
- exit
69
- end
70
-
71
- output_with_bang("`#{cmd}` is not a nimbu command.")
72
-
73
- distances = {}
74
- (commands.keys + command_aliases.keys).each do |suggestion|
75
- distance = string_distance(cmd, suggestion)
76
- distances[distance] ||= []
77
- distances[distance] << suggestion
78
- end
79
-
80
- if distances.keys.min < 4
81
- suggestions = distances[distances.keys.min].sort
82
- if suggestions.length == 1
83
- output_with_bang("Perhaps you meant `#{suggestions.first}`.")
84
- else
85
- output_with_bang("Perhaps you meant #{suggestions[0...-1].map {|suggestion| "`#{suggestion}`"}.join(', ')} or `#{suggestions.last}`.")
86
- end
87
- end
86
+ if args.include?('-h') || args.include?('--help')
87
+ args.unshift(cmd) unless cmd =~ /^-.*/
88
+ cmd = 'help'
89
+ command = parse(cmd)
90
+ end
88
91
 
89
- output_with_bang("See `nimbu help` for additional details.")
90
- exit(1)
92
+ if cmd == '--version'
93
+ cmd = 'version'
94
+ command = parse(cmd)
91
95
  end
92
96
 
93
97
  @current_command = cmd
98
+ @anonymized_args, @normalized_args = [], []
94
99
 
95
100
  opts = {}
96
101
  invalid_options = []
97
102
 
98
103
  parser = OptionParser.new do |parser|
99
- global_options.each do |global_option|
100
- parser.on(*global_option[:args]) do |value|
101
- opts[global_option[:name]] = value
102
- end
103
- end
104
- command[:options].each do |name, option|
105
- parser.on("-#{option[:short]}", "--#{option[:long]}", option[:desc]) do |value|
106
- opts[name.gsub("-", "_").to_sym] = value
104
+ parser.base.long.delete('version')
105
+ (global_options + (command && command[:options] || [])).each do |option|
106
+ parser.on(*option[:args]) do |value|
107
+ if option[:proc]
108
+ option[:proc].call(value)
109
+ end
110
+ opts[option[:name].gsub('-', '_').to_sym] = value
111
+ ARGV.join(' ') =~ /(#{option[:args].map {|arg| arg.split(' ', 2).first}.join('|')})/
112
+ @anonymized_args << "#{$1} _"
113
+ @normalized_args << "#{option[:args].last.split(' ', 2).first} _"
107
114
  end
108
115
  end
109
116
  end
@@ -111,53 +118,116 @@ module Nimbu
111
118
  begin
112
119
  parser.order!(args) do |nonopt|
113
120
  invalid_options << nonopt
121
+ @anonymized_args << '!'
122
+ @normalized_args << '!'
114
123
  end
115
124
  rescue OptionParser::InvalidOption => ex
116
125
  invalid_options << ex.args.first
126
+ @anonymized_args << '!'
127
+ @normalized_args << '!'
117
128
  retry
118
129
  end
119
130
 
120
- raise OptionParser::ParseError if opts[:help]
121
-
122
131
  args.concat(invalid_options)
123
132
 
124
133
  @current_args = args
125
134
  @current_options = opts
135
+ @invalid_arguments = invalid_options
136
+
137
+ if opts[:debug]
138
+ Nimbu.debug = true
139
+ end
140
+
141
+ @anonymous_command = [ARGV.first, *@anonymized_args].join(' ')
142
+ begin
143
+ usage_directory = "#{home_directory}/.nimbu/usage"
144
+ FileUtils.mkdir_p(usage_directory)
145
+ usage_file = usage_directory << "/#{Nimbu::VERSION}"
146
+ usage = if File.exists?(usage_file)
147
+ json_decode(File.read(usage_file))
148
+ else
149
+ {}
150
+ end
151
+ usage[@anonymous_command] ||= 0
152
+ usage[@anonymous_command] += 1
153
+ File.write(usage_file, json_encode(usage) + "\n")
154
+ rescue
155
+ # usage writing is not important, allow failures
156
+ end
157
+
158
+ if command
159
+ command_instance = command[:klass].new(args.dup, opts.dup)
126
160
 
127
- [ command[:klass].new(args.dup, opts.dup), command[:method] ]
161
+ if !@normalized_args.include?('--app _') && (implied_app = command_instance.app rescue nil)
162
+ @normalized_args << '--app _'
163
+ end
164
+ @normalized_command = [ARGV.first, @normalized_args.sort_by {|arg| arg.gsub('-', '')}].join(' ')
165
+
166
+ [ command_instance, command[:method] ]
167
+ else
168
+ error([
169
+ "`#{cmd}` is not a Nimbu command.",
170
+ suggestion(cmd, commands.keys + command_aliases.keys),
171
+ "See `Nimbu help` for a list of available commands."
172
+ ].compact.join("\n"))
173
+ end
128
174
  end
129
175
 
130
176
  def self.run(cmd, arguments=[])
131
- object, method = prepare_run(cmd, arguments.dup)
132
- object.send(method)
133
- rescue RestClient::Unauthorized
134
- puts "Authentication failure"
135
- unless ENV['HEROKU_API_KEY']
136
- run "login"
137
- retry
138
- end
139
- rescue RestClient::PaymentRequired => e
140
- retry if run('account:confirm_billing', arguments.dup)
141
- rescue RestClient::ResourceNotFound => e
142
- error extract_error(e.http_body) {
143
- e.http_body =~ /^[\w\s]+ not found$/ ? e.http_body : "Resource not found"
144
- }
145
- rescue RestClient::Locked => e
146
- app = e.response.headers[:x_confirmation_required]
147
- if confirm_command(app, extract_error(e.response.body))
148
- arguments << '--confirm' << app
149
- retry
177
+ begin
178
+ object, method = prepare_run(cmd, arguments.dup)
179
+ object.send(method)
180
+ rescue Interrupt, StandardError, SystemExit => error
181
+ # load likely error classes, as they may not be loaded yet due to defered loads
182
+ require 'rest_client'
183
+ raise(error)
150
184
  end
151
- rescue RestClient::RequestFailed => e
152
- error extract_error(e.http_body)
153
- rescue RestClient::RequestTimeout
154
- error "API request timed out. Please try again, or contact support@nimbu.com if this issue persists."
185
+ # rescue Nimbu::API::Errors::Unauthorized, RestClient::Unauthorized
186
+ # puts "Authentication failure"
187
+ # unless ENV['Nimbu_API_KEY']
188
+ # run "login"
189
+ # retry
190
+ # end
191
+ # rescue Nimbu::API::Errors::VerificationRequired, RestClient::PaymentRequired => e
192
+ # retry if Nimbu::Helpers.confirm_billing
193
+ # rescue Nimbu::API::Errors::NotFound => e
194
+ # error extract_error(e.response.body) {
195
+ # e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
196
+ # }
197
+ # rescue RestClient::ResourceNotFound => e
198
+ # error extract_error(e.http_body) {
199
+ # e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
200
+ # }
201
+ # rescue Nimbu::API::Errors::Locked => e
202
+ # app = e.response.headers[:x_confirmation_required]
203
+ # if confirm_command(app, extract_error(e.response.body))
204
+ # arguments << '--confirm' << app
205
+ # retry
206
+ # end
207
+ # rescue RestClient::Locked => e
208
+ # app = e.response.headers[:x_confirmation_required]
209
+ # if confirm_command(app, extract_error(e.http_body))
210
+ # arguments << '--confirm' << app
211
+ # retry
212
+ # end
213
+ # rescue Nimbu::API::Errors::Timeout, RestClient::RequestTimeout
214
+ # error "API request timed out. Please try again, or contact support@Nimbu.com if this issue persists."
215
+ # rescue Nimbu::API::Errors::ErrorWithResponse => e
216
+ # error extract_error(e.response.body)
217
+ # rescue RestClient::RequestFailed => e
218
+ # error extract_error(e.http_body)
155
219
  rescue CommandFailed => e
156
220
  error e.message
157
- rescue OptionParser::ParseError => ex
221
+ rescue OptionParser::ParseError
158
222
  commands[cmd] ? run("help", [cmd]) : run("help")
159
- rescue Interrupt => e
160
- error "\n[exited]"
223
+ rescue Excon::Errors::SocketError => e
224
+ if e.message == 'getaddrinfo: nodename nor servname provided, or not known (SocketError)'
225
+ error("Unable to connect to Nimbu API, please check internet connectivity and try again.")
226
+ else
227
+ raise(e)
228
+ end
229
+ ensure
230
+ display_warnings
161
231
  end
162
232
 
163
233
  def self.parse(cmd)