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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +23 -0
- data/Rakefile +2 -0
- data/bin/pebbles +14 -0
- data/lib/pebbles.rb +23 -0
- data/lib/pebbles/api.rb +122 -0
- data/lib/pebbles/api/apps.rb +71 -0
- data/lib/pebbles/api/config_vars.rb +33 -0
- data/lib/pebbles/api/errors.rb +27 -0
- data/lib/pebbles/api/login.rb +14 -0
- data/lib/pebbles/api/releases.rb +33 -0
- data/lib/pebbles/api/user.rb +14 -0
- data/lib/pebbles/auth.rb +302 -0
- data/lib/pebbles/cli.rb +35 -0
- data/lib/pebbles/command.rb +256 -0
- data/lib/pebbles/command/apps.rb +225 -0
- data/lib/pebbles/command/auth.rb +85 -0
- data/lib/pebbles/command/base.rb +231 -0
- data/lib/pebbles/command/config.rb +147 -0
- data/lib/pebbles/command/help.rb +124 -0
- data/lib/pebbles/git.rb +69 -0
- data/lib/pebbles/helpers.rb +284 -0
- data/lib/pebbles/version.rb +3 -0
- data/pebbles.gemspec +27 -0
- metadata +142 -0
@@ -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
|