pebblescape 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.
@@ -0,0 +1,225 @@
1
+ require "pebbles/command/base"
2
+
3
+ # manage apps (create, destroy)
4
+ #
5
+ class Pebbles::Command::Apps < Pebbles::Command::Base
6
+
7
+ # apps
8
+ #
9
+ # list your apps
10
+ #
11
+ #Example:
12
+ #
13
+ # $ pebbles apps
14
+ # === My Apps
15
+ # example
16
+ # example2
17
+ #
18
+ def index
19
+ validate_arguments!
20
+
21
+ apps = api.get_apps.body
22
+
23
+ unless apps.empty?
24
+ styled_header("My Apps")
25
+ styled_array(apps.map { |app| app_name(app) })
26
+ else
27
+ display("You have no apps.")
28
+ end
29
+ end
30
+
31
+ alias_command "list", "apps"
32
+
33
+ # apps:info
34
+ #
35
+ # show detailed app information
36
+ #
37
+ # -s, --shell # output more shell friendly key/value pairs
38
+ #
39
+ #Examples:
40
+ #
41
+ # $ pebbles apps:info
42
+ # === example
43
+ # Git URL: https://git.pebblescape.com/example.git
44
+ # Repo Size: 5M
45
+ # ...
46
+ #
47
+ # $ pebbles apps:info --shell
48
+ # git_url=https://git.pebblescape.com/example.git
49
+ # repo_size=5000000
50
+ # ...
51
+ #
52
+ def info
53
+ validate_arguments!
54
+ app_data = api.get_app(app).body
55
+
56
+ unless options[:shell]
57
+ styled_header(app_data["name"])
58
+ end
59
+
60
+ if options[:shell]
61
+ app_data['git_url'] = git_url(app_data['name'])
62
+ if app_data['domain_name']
63
+ app_data['domain_name'] = app_data['domain_name']['domain']
64
+ end
65
+ app_data['owner'].delete('id')
66
+ flatten_hash(app_data, 'owner')
67
+ app_data.keys.sort_by { |a| a.to_s }.each do |key|
68
+ hputs("#{key}=#{app_data[key]}")
69
+ end
70
+ else
71
+ data = {}
72
+
73
+ if app_data["create_status"] && app_data["create_status"] != "complete"
74
+ data["Create Status"] = app_data["create_status"]
75
+ end
76
+
77
+ data["Git URL"] = git_url(app_data['name'])
78
+
79
+
80
+ if app_data["owner"]
81
+ data["Owner Email"] = app_data["owner"]["email"]
82
+ data["Owner"] = app_data["owner"]["name"]
83
+ end
84
+ data["Repo Size"] = format_bytes(app_data["repo_size"]) if app_data["repo_size"]
85
+ data["Build Size"] = format_bytes(app_data["build_size"]) if app_data["build_size"]
86
+ data["Web URL"] = app_data["web_url"]
87
+
88
+ styled_hash(data)
89
+ end
90
+ end
91
+
92
+ alias_command "info", "apps:info"
93
+
94
+ # apps:create [NAME]
95
+ #
96
+ # create a new app
97
+ #
98
+ # --addons ADDONS # a comma-delimited list of addons to install
99
+ # -b, --buildpack BUILDPACK # a buildpack url to use for this app
100
+ # -n, --no-remote # don't create a git remote
101
+ # -r, --remote REMOTE # the git remote to create, default "pebbles"
102
+ # --ssh-git # Use SSH git protocol
103
+ # --http-git # HIDDEN: Use HTTP git protocol
104
+ #
105
+ #Examples:
106
+ #
107
+ # $ pebbles apps:create
108
+ # Creating floating-dragon-42... done, stack is cedar
109
+ # http://floating-dragon-42.pebblesinspace.com/ | https://git.pebblesinspace.com/floating-dragon-42.git
110
+ #
111
+ # # specify a name
112
+ # $ pebbles apps:create example
113
+ # Creating example... done, stack is cedar
114
+ # http://example.pebblesinspace.com/ | https://git.pebblesinspace.com/example.git
115
+ #
116
+ # # create a staging app
117
+ # $ pebbles apps:create example-staging --remote staging
118
+ #
119
+ def create
120
+ name = shift_argument || options[:app] || ENV['PEBBLES_APP']
121
+ validate_arguments!
122
+
123
+ params = {
124
+ "name" => name,
125
+ }
126
+
127
+ info = api.post_app(params).body
128
+
129
+ begin
130
+ action("Creating #{info['name']}") do
131
+ if info['create_status'] == 'creating'
132
+ Timeout::timeout(options[:timeout].to_i) do
133
+ loop do
134
+ break if api.get_app(info['name']).body['create_status'] == 'complete'
135
+ sleep 1
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # (options[:addons] || "").split(",").each do |addon|
142
+ # addon.strip!
143
+ # action("Adding #{addon} to #{info["name"]}") do
144
+ # api.post_addon(info["name"], addon)
145
+ # end
146
+ # end
147
+
148
+ if buildpack = options[:buildpack]
149
+ api.put_config_vars(info["name"], "BUILDPACK_URL" => buildpack)
150
+ display("BUILDPACK_URL=#{buildpack}")
151
+ end
152
+
153
+ hputs([ info["web_url"], git_url(info['name']) ].join(" | "))
154
+ rescue Timeout::Error
155
+ hputs("Timed Out! Run `pebbles status` to check for known platform issues.")
156
+ end
157
+
158
+ unless options[:no_remote].is_a? FalseClass
159
+ create_git_remote(options[:remote] || "pebbles", git_url(info['name']))
160
+ end
161
+ end
162
+
163
+ alias_command "create", "apps:create"
164
+
165
+ # apps:destroy --app APP
166
+ #
167
+ # permanently destroy an app
168
+ #
169
+ #Example:
170
+ #
171
+ # $ pebbles apps:destroy -a example --confirm example
172
+ # Destroying example (including all add-ons)... done
173
+ #
174
+ def destroy
175
+ @app = shift_argument || options[:app] || options[:confirm]
176
+ validate_arguments!
177
+
178
+ unless @app
179
+ error("Usage: pebbles apps:destroy --app APP\nMust specify APP to destroy.")
180
+ end
181
+
182
+ api.get_app(@app) # fail fast if no access or doesn't exist
183
+
184
+ message = "WARNING: Potentially Destructive Action\nThis command will destroy #{@app} (including all add-ons)."
185
+ if confirm_command(@app, message)
186
+ action("Destroying #{@app} (including all add-ons)") do
187
+ api.delete_app(@app)
188
+ if remotes = git_remotes(Dir.pwd)
189
+ remotes.each do |remote_name, remote_app|
190
+ next if @app != remote_app
191
+ git "remote rm #{remote_name}"
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ alias_command "destroy", "apps:destroy"
199
+ alias_command "apps:delete", "apps:destroy"
200
+
201
+ # apps:open --app APP
202
+ #
203
+ # open the app in a web browser
204
+ #
205
+ #Example:
206
+ #
207
+ # $ heroku apps:open
208
+ # Opening example... done
209
+ #
210
+ def open
211
+ path = shift_argument
212
+ validate_arguments!
213
+
214
+ app_data = api.get_app(app).body
215
+
216
+ url = [app_data['web_url'], path].join
217
+ launchy("Opening #{app}", url)
218
+ end
219
+
220
+ private
221
+
222
+ def app_name(app)
223
+ app["name"]
224
+ end
225
+ end
@@ -0,0 +1,85 @@
1
+ require "pebbles/command/base"
2
+
3
+ # authentication (login, logout)
4
+ #
5
+ class Pebbles::Command::Auth < Pebbles::Command::Base
6
+
7
+ # auth
8
+ #
9
+ # Authenticate, display token and current user
10
+ def index
11
+ validate_arguments!
12
+
13
+ Pebbles::Command::Help.new.send(:help_for_command, current_command)
14
+ end
15
+
16
+ # auth:login
17
+ #
18
+ # log in with your Pebblescape credentials
19
+ #
20
+ #Example:
21
+ #
22
+ # $ pebbles auth:login
23
+ # Enter your Pebblescape credentials:
24
+ # Email: email@example.com
25
+ # Password (typing will be hidden):
26
+ # Authentication successful.
27
+ #
28
+ def login
29
+ validate_arguments!
30
+
31
+ Pebbles::Auth.login
32
+ display "Authentication successful."
33
+ end
34
+
35
+ alias_command "login", "auth:login"
36
+
37
+ # auth:logout
38
+ #
39
+ # clear local authentication credentials
40
+ #
41
+ #Example:
42
+ #
43
+ # $ pebbles auth:logout
44
+ # Local credentials cleared.
45
+ #
46
+ def logout
47
+ validate_arguments!
48
+
49
+ Pebbles::Auth.logout
50
+ display "Local credentials cleared."
51
+ end
52
+
53
+ alias_command "logout", "auth:logout"
54
+
55
+ # auth:token
56
+ #
57
+ # display your api token
58
+ #
59
+ #Example:
60
+ #
61
+ # $ pebbles auth:token
62
+ # ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD
63
+ #
64
+ def token
65
+ validate_arguments!
66
+
67
+ display Pebbles::Auth.api_key
68
+ end
69
+
70
+ # auth:whoami
71
+ #
72
+ # display your Pebblescape email address
73
+ #
74
+ #Example:
75
+ #
76
+ # $ pebbles auth:whoami
77
+ # email@example.com
78
+ #
79
+ def whoami
80
+ validate_arguments!
81
+
82
+ display Pebbles::Auth.user
83
+ end
84
+
85
+ end
@@ -0,0 +1,231 @@
1
+ require "fileutils"
2
+ require "pebbles/auth"
3
+ require "pebbles/command"
4
+
5
+ class Pebbles::Command::Base
6
+ include Pebbles::Helpers
7
+
8
+ def self.namespace
9
+ self.to_s.split("::").last.downcase
10
+ end
11
+
12
+ attr_reader :args
13
+ attr_reader :options
14
+
15
+ def initialize(args=[], options={})
16
+ @args = args
17
+ @options = options
18
+ end
19
+
20
+ def app
21
+ @app ||= Pebbles.app_name = if options[:confirm].is_a?(String)
22
+ if options[:app] && (options[:app] != options[:confirm])
23
+ error("Mismatch between --app and --confirm")
24
+ end
25
+ options[:confirm]
26
+ elsif options[:app].is_a?(String)
27
+ options[:app]
28
+ elsif ENV.has_key?('PEBBLES_APP')
29
+ ENV['PEBBLES_APP']
30
+ elsif app_from_dir = extract_app_in_dir(Dir.pwd)
31
+ app_from_dir
32
+ else
33
+ # raise instead of using error command to enable rescuing when app is optional
34
+ raise Pebbles::Command::CommandFailed.new("No app specified.\nRun this command from an app folder or specify which app to use with --app APP.") unless options[:ignore_no_app]
35
+ end
36
+ end
37
+
38
+ def api
39
+ Pebbles::Auth.api
40
+ end
41
+
42
+ protected
43
+
44
+ def self.inherited(klass)
45
+ unless klass == Pebbles::Command::Base
46
+ help = extract_help_from_caller(caller.first)
47
+
48
+ Pebbles::Command.register_namespace(
49
+ :name => klass.namespace,
50
+ :description => help.first
51
+ )
52
+ end
53
+ end
54
+
55
+ def self.method_added(method)
56
+ return if self == Pebbles::Command::Base
57
+ return if private_method_defined?(method)
58
+ return if protected_method_defined?(method)
59
+
60
+ help = extract_help_from_caller(caller.first)
61
+ resolved_method = (method.to_s == "index") ? nil : method.to_s
62
+ command = [ self.namespace, resolved_method ].compact.join(":")
63
+ banner = extract_banner(help) || command
64
+
65
+ Pebbles::Command.register_command(
66
+ :klass => self,
67
+ :method => method,
68
+ :namespace => self.namespace,
69
+ :command => command,
70
+ :banner => banner.strip,
71
+ :help => help.join("\n"),
72
+ :summary => extract_summary(help),
73
+ :description => extract_description(help),
74
+ :options => extract_options(help)
75
+ )
76
+
77
+ alias_command command.gsub(/_/, '-'), command if command =~ /_/
78
+ end
79
+
80
+ def self.alias_command(new, old)
81
+ raise "no such command: #{old}" unless Pebbles::Command.commands[old]
82
+ Pebbles::Command.command_aliases[new] = old
83
+ end
84
+
85
+ #
86
+ # Parse the caller format and identify the file and line number as identified
87
+ # in : http://www.ruby-doc.org/core/classes/Kernel.html#M001397. This will
88
+ # look for a colon followed by a digit as the delimiter. The biggest
89
+ # complication is windows paths, which have a colon after the drive letter.
90
+ # This regex will match paths as anything from the beginning to a colon
91
+ # directly followed by a number (the line number).
92
+ #
93
+ # Examples of the caller format :
94
+ # * c:/Ruby192/lib/.../lib/heroku/command/addons.rb:8:in `<module:Command>'
95
+ # * c:/Ruby192/lib/.../heroku-2.0.1/lib/heroku/command/pg.rb:96:in `<class:Pg>'
96
+ # * /Users/ph7/...../xray-1.1/lib/xray/thread_dump_signal_handler.rb:9
97
+ #
98
+ def self.extract_help_from_caller(line)
99
+ # pull out of the caller the information for the file path and line number
100
+ if line =~ /^(.+?):(\d+)/
101
+ extract_help($1, $2)
102
+ else
103
+ raise("unable to extract help from caller: #{line}")
104
+ end
105
+ end
106
+
107
+ def self.extract_help(file, line_number)
108
+ buffer = []
109
+ lines = Pebbles::Command.files[file]
110
+
111
+ (line_number.to_i-2).downto(0) do |i|
112
+ line = lines[i]
113
+ case line[0..0]
114
+ when ""
115
+ when "#"
116
+ buffer.unshift(line[1..-1])
117
+ else
118
+ break
119
+ end
120
+ end
121
+
122
+ buffer
123
+ end
124
+
125
+ def self.extract_banner(help)
126
+ help.first
127
+ end
128
+
129
+ def self.extract_summary(help)
130
+ extract_description(help).split("\n")[2].to_s.split("\n").first
131
+ end
132
+
133
+ def self.extract_description(help)
134
+ help.reject do |line|
135
+ line =~ /^\s+-(.+)#(.+)/
136
+ end.join("\n")
137
+ end
138
+
139
+ def self.extract_options(help)
140
+ help.select do |line|
141
+ line =~ /^\s+-(.+)#(.+)/
142
+ end.inject([]) do |options, line|
143
+ args = line.split('#', 2).first
144
+ args = args.split(/,\s*/).map {|arg| arg.strip}.sort.reverse
145
+ name = args.last.split(' ', 2).first[2..-1]
146
+ options << { :name => name, :args => args }
147
+ end
148
+ end
149
+
150
+ def current_command
151
+ Pebbles::Command.current_command
152
+ end
153
+
154
+ def extract_option(key)
155
+ options[key.dup.gsub('-','_').to_sym]
156
+ end
157
+
158
+ def invalid_arguments
159
+ Pebbles::Command.invalid_arguments
160
+ end
161
+
162
+ def shift_argument
163
+ Pebbles::Command.shift_argument
164
+ end
165
+
166
+ def validate_arguments!
167
+ Pebbles::Command.validate_arguments!
168
+ end
169
+
170
+ def extract_app_in_dir(dir)
171
+ return unless remotes = git_remotes(dir)
172
+
173
+ if remote = options[:remote]
174
+ remotes[remote]
175
+ elsif remote = extract_remote_from_git_config
176
+ remotes[remote]
177
+ else
178
+ apps = remotes.values.uniq
179
+ if apps.size == 1
180
+ apps.first
181
+ else
182
+ raise(Pebbles::Command::CommandFailed, "Multiple apps in folder and no app specified.\nSpecify app with --app APP.") unless options[:ignore_no_app]
183
+ end
184
+ end
185
+ end
186
+
187
+ def extract_remote_from_git_config
188
+ remote = git("config pebbles.remote")
189
+ remote == "" ? nil : remote
190
+ end
191
+
192
+ def git_url(app_name)
193
+ if options[:ssh_git]
194
+ "git@#{Pebbles::Auth.git_host}:#{app_name}.git"
195
+ else
196
+ unless has_http_git_entry_in_netrc
197
+ warn "WARNING: Incomplete credentials detected, git may not work with Pebblescape. Run `pebbles login` to update your credentials."
198
+ exit 1
199
+ end
200
+ "https://#{Pebbles::Auth.http_git_host}/#{app_name}.git"
201
+ end
202
+ end
203
+
204
+ def git_remotes(base_dir=Dir.pwd)
205
+ remotes = {}
206
+ original_dir = Dir.pwd
207
+ Dir.chdir(base_dir)
208
+
209
+ return unless File.exists?(".git")
210
+ git("remote -v").split("\n").each do |remote|
211
+ name, url, _ = remote.split(/\s/)
212
+ if url =~ /^git@#{Pebbles::Auth.git_host}(?:[\.\w]*):([\w\d-]+)\.git$/ ||
213
+ url =~ /^https:\/\/#{Pebbles::Auth.http_git_host}\/([\w\d-]+)\.git$/
214
+ remotes[name] = $1
215
+ end
216
+ end
217
+
218
+ Dir.chdir(original_dir)
219
+ if remotes.empty?
220
+ nil
221
+ else
222
+ remotes
223
+ end
224
+ end
225
+ end
226
+
227
+ module Pebbles::Command
228
+ unless const_defined?(:BaseWithApp)
229
+ BaseWithApp = Base
230
+ end
231
+ end