omudid 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/OneMoreUDID.gemspec +31 -0
- data/README.md +19 -0
- data/Rakefile +1 -0
- data/bin/omudid +297 -0
- data/lib/OneMoreUDID.rb +345 -0
- data/lib/cupertino_compatibility.rb +54 -0
- metadata +145 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 David Lawson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/OneMoreUDID.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'OneMoreUDID'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "omudid"
|
9
|
+
spec.version = OneMoreUDID::VERSION
|
10
|
+
spec.authors = ["David Lawson"]
|
11
|
+
spec.email = ["tech.lawson@gmail.com"]
|
12
|
+
spec.description = 'Conveniently add a UDID to the iOS Developer Portal, refresh a provisioning profile and download it. Uploading a new provisioning profile to TestFlight is also supported.'
|
13
|
+
spec.summary = 'Add one UDID to iOS Dev Portal & TestFlight'
|
14
|
+
spec.homepage = 'https://github.com/Papercloud/OneMoreUDID'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
|
25
|
+
spec.add_dependency "cupertino"
|
26
|
+
|
27
|
+
spec.add_dependency "spinning_cursor"
|
28
|
+
spec.add_dependency "rainbow"
|
29
|
+
|
30
|
+
spec.add_dependency "commander", "~> 4.1.2"
|
31
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
OneMoreUDID
|
2
|
+
===========
|
3
|
+
|
4
|
+
Conveniently add a UDID to the iOS Developer Portal, refresh a provisioning profile and download it.
|
5
|
+
|
6
|
+
Uploading a new provisioning profile to TestFlight is also supported.
|
7
|
+
|
8
|
+
Installation
|
9
|
+
-
|
10
|
+
```
|
11
|
+
$ gem install OneMoreUDID
|
12
|
+
```
|
13
|
+
|
14
|
+
Usage
|
15
|
+
-
|
16
|
+
```
|
17
|
+
$ omudid add
|
18
|
+
$ omudid testflight upload
|
19
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/omudid
ADDED
@@ -0,0 +1,297 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'commander/import'
|
5
|
+
|
6
|
+
require 'OneMoreUDID'
|
7
|
+
|
8
|
+
require 'terminal-table'
|
9
|
+
require 'term/ansicolor'
|
10
|
+
|
11
|
+
require 'cupertino_compatibility'
|
12
|
+
|
13
|
+
require 'shellwords'
|
14
|
+
|
15
|
+
program :name, 'OneMoreUDID'
|
16
|
+
program :version, OneMoreUDID::VERSION
|
17
|
+
program :description, '''Uses the developer portal to add one device to a provisioning profile,
|
18
|
+
then downloads and installs the new profile.
|
19
|
+
Also uploads provisioning profiles to TestFlight.'''
|
20
|
+
|
21
|
+
HighLine.track_eof = false # Fix for built-in Ruby
|
22
|
+
|
23
|
+
default_command :help
|
24
|
+
|
25
|
+
command :'list-teams' do |c|
|
26
|
+
c.syntax = 'omudid list-teams [username] [password]'
|
27
|
+
c.summary = 'Lists teams on the iOS developer portal'
|
28
|
+
c.description = ''
|
29
|
+
|
30
|
+
c.action do |args, options|
|
31
|
+
username = (args[0] or ask 'Apple Username:')
|
32
|
+
password = (args[1] or pw 'Apple Password:')
|
33
|
+
|
34
|
+
if !args[1]
|
35
|
+
puts
|
36
|
+
say_warning 'Note: you can repeat this process by running:'
|
37
|
+
say_warning ' omudid list-teams ' + Shellwords.escape(username) + ' [password]'
|
38
|
+
puts
|
39
|
+
end
|
40
|
+
|
41
|
+
agent = OneMoreUDID::PortalAgent.new
|
42
|
+
|
43
|
+
teams = agent.get_teams(username, password)
|
44
|
+
|
45
|
+
table = Terminal::Table.new do |t|
|
46
|
+
t << ['Team ID', 'Team Name']
|
47
|
+
t << :separator
|
48
|
+
|
49
|
+
teams.each do |id, name|
|
50
|
+
t << [id, name]
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
puts table
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
command :'list-profiles' do |c|
|
60
|
+
c.syntax = 'omudid list-profiles [username] [password] ([team ID])'
|
61
|
+
c.summary = 'Lists provisioning profiles on the iOS developer portal'
|
62
|
+
c.description = ''
|
63
|
+
|
64
|
+
c.action do |args, options|
|
65
|
+
|
66
|
+
username = (args[0] or ask 'Apple Username:')
|
67
|
+
password = (args[1] or pw 'Apple Password:')
|
68
|
+
team_name = args[2] rescue ''
|
69
|
+
|
70
|
+
if !args[1]
|
71
|
+
puts
|
72
|
+
end
|
73
|
+
|
74
|
+
agent = OneMoreUDID::PortalAgent.new
|
75
|
+
|
76
|
+
agent.setup_cupertino(username, password, team_name)
|
77
|
+
profiles = agent.list_profiles()
|
78
|
+
|
79
|
+
if !args[1]
|
80
|
+
puts
|
81
|
+
say_warning 'Note: you can repeat this process by running:'
|
82
|
+
|
83
|
+
if agent.agent.instance_variable_get('@team')
|
84
|
+
say_warning ' omudid list-profiles ' + Shellwords.escape(username) + ' [password] ' + agent.agent.instance_variable_get('@team')
|
85
|
+
else
|
86
|
+
say_warning ' omudid list-profiles ' + Shellwords.escape(username) + ' [password]'
|
87
|
+
end
|
88
|
+
|
89
|
+
puts
|
90
|
+
end
|
91
|
+
|
92
|
+
table = Terminal::Table.new do |t|
|
93
|
+
t << ['Name', 'Type', 'App ID', 'Status']
|
94
|
+
t << :separator
|
95
|
+
|
96
|
+
profiles.each do |profile|
|
97
|
+
t << [profile.name, profile.type, profile.app_id, profile.status]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
puts table
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
command :add do |c|
|
106
|
+
c.syntax = 'omudid add [username] [password] [profile name] [device name] [UDID] ([team ID])'
|
107
|
+
c.summary = 'Adds a device to the provisioning portal, then downloads and installs the new profile'
|
108
|
+
c.description = ''
|
109
|
+
|
110
|
+
c.action do |args, options|
|
111
|
+
|
112
|
+
username = (args[0] or ask 'Apple Username:')
|
113
|
+
password = (args[1] or pw 'Apple Password:')
|
114
|
+
|
115
|
+
profile_name = (args[2] or ask 'Profile name (leave blank to select from available profiles):')
|
116
|
+
device_name = (args[3] or ask 'Device name:')
|
117
|
+
udid = (args[4] or ask 'UDID:')
|
118
|
+
team_name = args[5] rescue ''
|
119
|
+
|
120
|
+
show_hint = false
|
121
|
+
|
122
|
+
if !args[4]
|
123
|
+
show_hint = true
|
124
|
+
puts
|
125
|
+
end
|
126
|
+
|
127
|
+
agent = OneMoreUDID::PortalAgent.new
|
128
|
+
agent.setup_cupertino(username, password, team_name)
|
129
|
+
|
130
|
+
agent.add_device(device_name, udid)
|
131
|
+
|
132
|
+
if profile_name == ''
|
133
|
+
puts
|
134
|
+
profile_name = choose "\nSelect a profile:", *agent.list_profiles().collect { |profile| profile.name }
|
135
|
+
puts
|
136
|
+
end
|
137
|
+
|
138
|
+
if show_hint
|
139
|
+
say_warning 'Note: you can repeat this process by running:'
|
140
|
+
|
141
|
+
if agent.agent.instance_variable_get('@team')
|
142
|
+
say_warning ' omudid add ' + Shellwords.escape(username) + ' [password] ' + Shellwords.escape(profile_name) + ' ' + Shellwords.escape(device_name) + ' ' + Shellwords.escape(udid) + ' ' + agent.agent.instance_variable_get('@team')
|
143
|
+
else
|
144
|
+
say_warning ' omudid add ' + Shellwords.escape(username) + ' [password] ' + Shellwords.escape(profile_name) + ' ' + Shellwords.escape(device_name) + ' ' + Shellwords.escape(udid)
|
145
|
+
end
|
146
|
+
|
147
|
+
puts
|
148
|
+
end
|
149
|
+
|
150
|
+
agent.update_profile(profile_name)
|
151
|
+
|
152
|
+
filename = agent.download_new_profile(profile_name)
|
153
|
+
|
154
|
+
local_agent = OneMoreUDID::LocalAgent.new
|
155
|
+
local_agent.install_profile(profile_name, filename)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
command :'testflight list-apps' do |c|
|
160
|
+
c.syntax = 'omudid testflight list-apps [username] [password]'
|
161
|
+
c.summary = 'Lists apps on TestFlight'
|
162
|
+
c.description = ''
|
163
|
+
|
164
|
+
c.action do |args, options|
|
165
|
+
|
166
|
+
username = (args[0] or ask 'TestFlight Username:')
|
167
|
+
password = (args[1] or pw 'TestFlight Password:')
|
168
|
+
|
169
|
+
if !args[1]
|
170
|
+
puts
|
171
|
+
say_warning 'Note: you can repeat this process by running:'
|
172
|
+
say_warning ' omudid testflight list-apps ' + Shellwords.escape(username) + ' [password]'
|
173
|
+
puts
|
174
|
+
end
|
175
|
+
|
176
|
+
agent = OneMoreUDID::TestFlightAgent.new(username, password)
|
177
|
+
apps = agent.get_apps()
|
178
|
+
|
179
|
+
table = Terminal::Table.new do |t|
|
180
|
+
t << ['App ID', 'App Name']
|
181
|
+
t << :separator
|
182
|
+
|
183
|
+
apps.each do |id, name|
|
184
|
+
t << [id, name]
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
puts table
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
command :'testflight list-builds' do |c|
|
194
|
+
c.syntax = 'omudid testflight list-builds [username] [password] [app ID]'
|
195
|
+
c.summary = 'Lists builds of an app on TestFlight'
|
196
|
+
c.description = ''
|
197
|
+
|
198
|
+
c.action do |args, options|
|
199
|
+
|
200
|
+
username = (args[0] or ask 'TestFlight Username:')
|
201
|
+
password = (args[1] or pw 'TestFlight Password:')
|
202
|
+
app_id = (args[2] or ask 'App ID (leave blank to select from available IDs):')
|
203
|
+
|
204
|
+
show_hint = false
|
205
|
+
|
206
|
+
if !args[2]
|
207
|
+
show_hint = true
|
208
|
+
end
|
209
|
+
|
210
|
+
agent = OneMoreUDID::TestFlightAgent.new(username, password)
|
211
|
+
|
212
|
+
if app_id == ''
|
213
|
+
puts
|
214
|
+
choice = choose 'Select an app ID:', *agent.get_apps().collect{ |id, name| id + ' ('+name+')' }
|
215
|
+
puts
|
216
|
+
regex = /^[^ ]*/
|
217
|
+
app_id = (choice.match regex)[0]
|
218
|
+
end
|
219
|
+
|
220
|
+
say_warning 'Note: you can repeat this process by running:'
|
221
|
+
say_warning ' omudid testflight list-builds ' + Shellwords.escape(username) + ' [password] ' + Shellwords.escape(app_id)
|
222
|
+
puts
|
223
|
+
|
224
|
+
builds = agent.get_builds(app_id)
|
225
|
+
|
226
|
+
table = Terminal::Table.new do |t|
|
227
|
+
t << ['Build ID', 'Build Name']
|
228
|
+
t << :separator
|
229
|
+
|
230
|
+
builds.each do |id, name|
|
231
|
+
t << [id, name]
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
puts table
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
command :'testflight upload' do |c|
|
241
|
+
c.syntax = 'omudid testflight upload [username] [password] [build ID] [profile name]'
|
242
|
+
c.summary = 'Uploads a provisioning profile to TestFlight'
|
243
|
+
c.description = ''
|
244
|
+
|
245
|
+
c.action do |args, options|
|
246
|
+
|
247
|
+
username = (args[0] or ask 'TestFlight Username:')
|
248
|
+
password = (args[1] or pw 'TestFlight Password:')
|
249
|
+
build_id = (args[2] or ask 'Build ID (leave blank to select from available IDs):')
|
250
|
+
profile_name = (args[3] or ask 'Profile name (leave blank to select from available local profiles):')
|
251
|
+
|
252
|
+
show_hint = false
|
253
|
+
|
254
|
+
if !args[3]
|
255
|
+
show_hint = true
|
256
|
+
end
|
257
|
+
|
258
|
+
agent = OneMoreUDID::TestFlightAgent.new(username, password)
|
259
|
+
|
260
|
+
if build_id == ''
|
261
|
+
app_id = ask 'Enter an App ID to list builds (or leave blank to list App IDs):'
|
262
|
+
|
263
|
+
if app_id == ''
|
264
|
+
puts
|
265
|
+
choice = choose 'Select an app ID:', *agent.get_apps().collect{ |id, name| id + ' ('+name+')' }
|
266
|
+
puts
|
267
|
+
regex = /^[^ ]*/
|
268
|
+
app_id = (choice.match regex)[0]
|
269
|
+
end
|
270
|
+
|
271
|
+
puts
|
272
|
+
choice = choose 'Select a build ID:', *agent.get_builds(app_id).collect{ |id, name| id + ' ('+name+')' }
|
273
|
+
puts
|
274
|
+
regex = /^[^ ]*/
|
275
|
+
build_id = (choice.match regex)[0]
|
276
|
+
|
277
|
+
show_hint = true
|
278
|
+
end
|
279
|
+
|
280
|
+
if profile_name == ''
|
281
|
+
local_agent = OneMoreUDID::LocalAgent.new
|
282
|
+
profile_name = choose 'Select a profile:', *local_agent.get_profiles()
|
283
|
+
puts
|
284
|
+
|
285
|
+
show_hint = true
|
286
|
+
end
|
287
|
+
|
288
|
+
if show_hint
|
289
|
+
say_warning 'Note: you can repeat this process by running:'
|
290
|
+
say_warning ' omudid testflight list-builds ' + Shellwords.escape(username) + ' [password] ' + Shellwords.escape(build_id) + ' ' + Shellwords.escape(profile_name)
|
291
|
+
puts
|
292
|
+
end
|
293
|
+
|
294
|
+
agent.upload(build_id, profile_name)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
data/lib/OneMoreUDID.rb
ADDED
@@ -0,0 +1,345 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'spinning_cursor'
|
3
|
+
require 'rainbow'
|
4
|
+
require 'iconv' unless String.method_defined?(:encode)
|
5
|
+
|
6
|
+
module OneMoreUDID
|
7
|
+
VERSION = "1.0.3"
|
8
|
+
|
9
|
+
class PortalAgent
|
10
|
+
|
11
|
+
attr_accessor :agent
|
12
|
+
|
13
|
+
def get_teams(username, password)
|
14
|
+
agent = Cupertino::ProvisioningPortal::Agent.new
|
15
|
+
|
16
|
+
agent.instance_eval do
|
17
|
+
def get(uri, parameters = [], referer = nil, headers = {})
|
18
|
+
uri = ::File.join("https://#{Cupertino::HOSTNAME}", uri) unless /^https?/ === uri
|
19
|
+
|
20
|
+
3.times do
|
21
|
+
SpinningCursor.start do
|
22
|
+
banner 'Loading page...'
|
23
|
+
action do
|
24
|
+
::Mechanize.instance_method(:get).bind(self).call(uri, parameters, referer, headers)
|
25
|
+
end
|
26
|
+
message 'Loading page... '+'done'.color(:green)
|
27
|
+
end
|
28
|
+
|
29
|
+
return page unless page.respond_to?(:title)
|
30
|
+
|
31
|
+
case page.title
|
32
|
+
when /Sign in with your Apple ID/
|
33
|
+
login! and next
|
34
|
+
else
|
35
|
+
return page
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
raise UnsuccessfulAuthenticationError
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
agent.username = username
|
44
|
+
agent.password = password
|
45
|
+
|
46
|
+
agent.get('https://developer.apple.com/account/selectTeam.action')
|
47
|
+
|
48
|
+
teams = agent.page.form_with(:name => 'saveTeamSelection').radiobuttons
|
49
|
+
|
50
|
+
formatted_teams = {}
|
51
|
+
teams.each do |team|
|
52
|
+
labels = agent.page.search("label[for=\"#{team.dom_id}\"]")
|
53
|
+
formatted_teams[team.value] = labels[0].text.strip
|
54
|
+
formatted_teams[team.value] += ' - ' + labels[1].text.strip if labels[1].text.strip != ''
|
55
|
+
end
|
56
|
+
|
57
|
+
formatted_teams
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_cupertino(username, password, team_name = '')
|
61
|
+
agent = Cupertino::ProvisioningPortal::Agent.new
|
62
|
+
|
63
|
+
agent.instance_eval do
|
64
|
+
def get(uri, parameters = [], referer = nil, headers = {})
|
65
|
+
uri = ::File.join("https://#{Cupertino::HOSTNAME}", uri) unless /^https?/ === uri
|
66
|
+
|
67
|
+
3.times do
|
68
|
+
SpinningCursor.start do
|
69
|
+
banner 'Loading page...'
|
70
|
+
action do
|
71
|
+
::Mechanize.instance_method(:get).bind(self).call(uri, parameters, referer, headers)
|
72
|
+
end
|
73
|
+
message 'Loading page... '+'done'.color(:green)
|
74
|
+
end
|
75
|
+
|
76
|
+
return page unless page.respond_to?(:title)
|
77
|
+
|
78
|
+
case page.title
|
79
|
+
when /Sign in with your Apple ID/
|
80
|
+
login! and next
|
81
|
+
when /Select Team/
|
82
|
+
select_team! and next
|
83
|
+
else
|
84
|
+
return page
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
raise UnsuccessfulAuthenticationError
|
89
|
+
end
|
90
|
+
|
91
|
+
def team
|
92
|
+
teams = page.form_with(:name => 'saveTeamSelection').radiobuttons
|
93
|
+
|
94
|
+
formatted_teams = {}
|
95
|
+
teams.each do |team|
|
96
|
+
labels = page.search("label[for=\"#{team.dom_id}\"]")
|
97
|
+
formatted_teams[team.value] = labels[0].text.strip
|
98
|
+
formatted_teams[team.value] += ' - ' + labels[1].text.strip if labels[1].text.strip != ''
|
99
|
+
end
|
100
|
+
|
101
|
+
if formatted_teams[@teamName]
|
102
|
+
@team = @teamName
|
103
|
+
else
|
104
|
+
puts
|
105
|
+
say_warning 'Note: you can specify the team with a command-line argument'
|
106
|
+
puts
|
107
|
+
chosen_team = choose 'Please select one of the following teams:', *formatted_teams.collect { |id, name| id + ': ' + name }
|
108
|
+
puts
|
109
|
+
regex = /^[^:]*/
|
110
|
+
@team = (chosen_team.match regex)[0]
|
111
|
+
end
|
112
|
+
|
113
|
+
@team
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
agent.username = username
|
118
|
+
agent.password = password
|
119
|
+
agent.instance_variable_set(:@teamName, team_name)
|
120
|
+
|
121
|
+
@agent = agent
|
122
|
+
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
def add_device(device_name, udid)
|
127
|
+
device = Device.new
|
128
|
+
device.name = device_name
|
129
|
+
device.udid = udid
|
130
|
+
|
131
|
+
try {@agent.add_devices(*[device])}
|
132
|
+
|
133
|
+
say_ok 'Device ' + device.name + ' (' + device.udid + ') added'
|
134
|
+
end
|
135
|
+
|
136
|
+
def list_profiles()
|
137
|
+
try{@agent.list_profiles(:distribution)}
|
138
|
+
end
|
139
|
+
|
140
|
+
def update_profile(profile_name)
|
141
|
+
profiles = try{@agent.list_profiles(:distribution)}
|
142
|
+
profile = (profiles.select { |profile| profile.name == profile_name }).first
|
143
|
+
|
144
|
+
if !profile
|
145
|
+
say_error 'Provisioning profile not found, profiles available:'
|
146
|
+
|
147
|
+
#modularise this?
|
148
|
+
table = Terminal::Table.new do |t|
|
149
|
+
t << ['Name', 'Type', 'App ID', 'Status']
|
150
|
+
t << :separator
|
151
|
+
|
152
|
+
profiles.each do |profile|
|
153
|
+
t << [profile.name, profile.type, profile.app_id, profile.status]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
puts table
|
158
|
+
|
159
|
+
abort
|
160
|
+
end
|
161
|
+
|
162
|
+
@agent.manage_devices_for_profile(profile) do |on, off|
|
163
|
+
#enable all devices
|
164
|
+
on + off
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def download_new_profile(profile_name)
|
169
|
+
profiles = try{@agent.list_profiles(:distribution)}
|
170
|
+
profile = (profiles.select { |profile| profile.name == profile_name }).first
|
171
|
+
|
172
|
+
if !profile
|
173
|
+
say_error 'New provisioning profile not found, profiles available:'
|
174
|
+
puts profiles
|
175
|
+
abort
|
176
|
+
end
|
177
|
+
|
178
|
+
filename = ''
|
179
|
+
5.times do
|
180
|
+
begin
|
181
|
+
sleep 5
|
182
|
+
if filename = @agent.download_profile(profile)
|
183
|
+
say_ok 'Downloaded new profile (' + Dir.pwd + '/' + filename + ')'
|
184
|
+
break
|
185
|
+
else
|
186
|
+
say_error 'Could not download profile'
|
187
|
+
end
|
188
|
+
rescue
|
189
|
+
say_error 'Could not download profile'
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
if filename == ''
|
194
|
+
abort
|
195
|
+
end
|
196
|
+
|
197
|
+
filename
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
class LocalAgent
|
202
|
+
def install_profile(profile_name, filename)
|
203
|
+
|
204
|
+
Dir.glob(File.expand_path('~') + '/Library/MobileDevice/Provisioning Profiles/*.mobileprovision') do |file|
|
205
|
+
|
206
|
+
delete_file = false
|
207
|
+
|
208
|
+
File.open(file, "r") do |_file|
|
209
|
+
|
210
|
+
file_contents = _file.read
|
211
|
+
if String.method_defined?(:encode)
|
212
|
+
file_contents.encode!('UTF-8', 'UTF-8', :invalid => :replace)
|
213
|
+
end
|
214
|
+
matches = /<key>Name<\/key>\s+<string>([^<]+)<\/string>/.match file_contents
|
215
|
+
|
216
|
+
if matches[1] == profile_name
|
217
|
+
delete_file = true
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
if delete_file
|
222
|
+
say_warning 'Old profile deleted ('+ file +')'
|
223
|
+
File.delete(file)
|
224
|
+
break
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
new_path = File.expand_path('~') + '/Library/MobileDevice/Provisioning Profiles/' + filename
|
230
|
+
File.rename(Dir.pwd + '/' + filename, new_path)
|
231
|
+
|
232
|
+
say_ok 'New profile installed ('+new_path+')'
|
233
|
+
end
|
234
|
+
|
235
|
+
def get_profiles
|
236
|
+
|
237
|
+
profiles = []
|
238
|
+
|
239
|
+
Dir.glob(File.expand_path('~') + '/Library/MobileDevice/Provisioning Profiles/*.mobileprovision') do |file|
|
240
|
+
|
241
|
+
File.open(file, "r") do |_file|
|
242
|
+
|
243
|
+
file_contents = _file.read
|
244
|
+
if String.method_defined?(:encode)
|
245
|
+
file_contents.encode!('UTF-8', 'UTF-8', :invalid => :replace)
|
246
|
+
end
|
247
|
+
|
248
|
+
matches = /<key>Name<\/key>\s+<string>([^<]+)<\/string>/.match file_contents
|
249
|
+
profiles << matches[1]
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
profiles
|
255
|
+
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
class TestFlightAgent < ::Mechanize
|
260
|
+
|
261
|
+
attr_accessor :username, :password
|
262
|
+
|
263
|
+
def initialize(username, password)
|
264
|
+
super()
|
265
|
+
@username = username
|
266
|
+
@password = password
|
267
|
+
self
|
268
|
+
end
|
269
|
+
|
270
|
+
def get(uri, parameters = [], referer = nil, headers = {})
|
271
|
+
3.times do
|
272
|
+
SpinningCursor.start do
|
273
|
+
banner 'Loading page...'
|
274
|
+
action do
|
275
|
+
super(uri, parameters, referer, headers)
|
276
|
+
end
|
277
|
+
message 'Loading page... '+'done'.color(:green)
|
278
|
+
end
|
279
|
+
|
280
|
+
return page unless page.respond_to?(:body)
|
281
|
+
|
282
|
+
case page.body
|
283
|
+
when /Login to TestFlight and FlightPath/
|
284
|
+
login! and next
|
285
|
+
else
|
286
|
+
return page
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
raise UnsuccessfulAuthenticationError
|
291
|
+
end
|
292
|
+
|
293
|
+
def get_apps
|
294
|
+
get('https://testflightapp.com/dashboard/applications/')
|
295
|
+
|
296
|
+
apps = page.search('tr.goapp')
|
297
|
+
regex = /([0-9]+)/
|
298
|
+
apps.collect { |app| [(app['id'].match regex)[1], app.search('h2 small').text.strip] }
|
299
|
+
end
|
300
|
+
|
301
|
+
def get_builds(app_id)
|
302
|
+
get("https://testflightapp.com/dashboard/applications/#{app_id}/builds/")
|
303
|
+
|
304
|
+
builds = page.search('tr.goversion')
|
305
|
+
regex = /([0-9]+)/
|
306
|
+
builds.collect { |build| [(build['id'].match regex)[1], build.search('td:first')[0].text] }
|
307
|
+
end
|
308
|
+
|
309
|
+
def upload(build_id, profile_name)
|
310
|
+
get("https://testflightapp.com/dashboard/builds/update/#{build_id}/")
|
311
|
+
|
312
|
+
profile_file = get_profile_file(profile_name)
|
313
|
+
|
314
|
+
form = page.form_with(:id => 'provision-form')
|
315
|
+
upload = form.file_uploads.first
|
316
|
+
upload.file_name = profile_file.path
|
317
|
+
form.submit
|
318
|
+
|
319
|
+
say_ok 'Submitted '+profile_file.path
|
320
|
+
end
|
321
|
+
|
322
|
+
def get_profile_file(profile_name)
|
323
|
+
Dir.glob(::File.expand_path('~') + '/Library/MobileDevice/Provisioning Profiles/*.mobileprovision') do |file|
|
324
|
+
|
325
|
+
::File.open(file, "r") do |_file|
|
326
|
+
matches = /<key>Name<\/key>\s+<string>([^<]+)<\/string>/.match _file.read
|
327
|
+
if matches[1] == profile_name
|
328
|
+
return _file
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
private
|
336
|
+
|
337
|
+
def login!
|
338
|
+
form = page.forms[0]
|
339
|
+
form.username = self.username
|
340
|
+
form.password = self.password
|
341
|
+
form.submit
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'cupertino'
|
2
|
+
require 'cupertino/provisioning_portal/helpers'
|
3
|
+
require 'cupertino/provisioning_portal/agent'
|
4
|
+
|
5
|
+
require 'mechanize'
|
6
|
+
require 'certified'
|
7
|
+
|
8
|
+
include Cupertino::ProvisioningPortal
|
9
|
+
include Cupertino::ProvisioningPortal::Helpers
|
10
|
+
|
11
|
+
module Cupertino
|
12
|
+
module ProvisioningPortal
|
13
|
+
HOST = 'developer.apple.com'
|
14
|
+
|
15
|
+
class UnsuccessfulAuthenticationError < RuntimeError; end
|
16
|
+
class UnexpectedContentError < RuntimeError; end
|
17
|
+
|
18
|
+
class Device < Struct.new(:name, :udid)
|
19
|
+
def to_s
|
20
|
+
"#{self.name} #{self.udid}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Certificate < Struct.new(:name, :type, :expiration_date, :status, :download_url) #:provisioning_profiles,
|
25
|
+
def to_s
|
26
|
+
"#{self.name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class AppID < Struct.new(:bundle_seed_id, :description, :development_properties, :distribution_properties)
|
31
|
+
def to_s
|
32
|
+
"#{self.bundle_seed_id}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class ProvisioningProfile < Struct.new(:name, :type, :app_id, :status, :download_url, :edit_url)
|
37
|
+
def to_s
|
38
|
+
"#{self.name}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class PassTypeID < Struct.new(:description, :id, :pass_certificates, :card_id)
|
43
|
+
def to_s
|
44
|
+
"#{self.id} #{self.description}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class PassCertificate < Struct.new(:name, :status, :expiration_date, :certificate_id)
|
49
|
+
def to_s
|
50
|
+
"#{self.certificate_id}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: omudid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
version: 1.0.3
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- David Lawson
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2013-05-10 00:00:00 +10:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: bundler
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 3
|
30
|
+
version: "1.3"
|
31
|
+
type: :development
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rake
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 0
|
42
|
+
version: "0"
|
43
|
+
type: :development
|
44
|
+
version_requirements: *id002
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: cupertino
|
47
|
+
prerelease: false
|
48
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
type: :runtime
|
56
|
+
version_requirements: *id003
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: spinning_cursor
|
59
|
+
prerelease: false
|
60
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id004
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rainbow
|
71
|
+
prerelease: false
|
72
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
type: :runtime
|
80
|
+
version_requirements: *id005
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: commander
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ~>
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
segments:
|
89
|
+
- 4
|
90
|
+
- 1
|
91
|
+
- 2
|
92
|
+
version: 4.1.2
|
93
|
+
type: :runtime
|
94
|
+
version_requirements: *id006
|
95
|
+
description: Conveniently add a UDID to the iOS Developer Portal, refresh a provisioning profile and download it. Uploading a new provisioning profile to TestFlight is also supported.
|
96
|
+
email:
|
97
|
+
- tech.lawson@gmail.com
|
98
|
+
executables:
|
99
|
+
- omudid
|
100
|
+
extensions: []
|
101
|
+
|
102
|
+
extra_rdoc_files: []
|
103
|
+
|
104
|
+
files:
|
105
|
+
- .gitignore
|
106
|
+
- Gemfile
|
107
|
+
- LICENSE.txt
|
108
|
+
- OneMoreUDID.gemspec
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- bin/omudid
|
112
|
+
- lib/OneMoreUDID.rb
|
113
|
+
- lib/cupertino_compatibility.rb
|
114
|
+
has_rdoc: true
|
115
|
+
homepage: https://github.com/Papercloud/OneMoreUDID
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
segments:
|
128
|
+
- 0
|
129
|
+
version: "0"
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
segments:
|
135
|
+
- 0
|
136
|
+
version: "0"
|
137
|
+
requirements: []
|
138
|
+
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 1.3.6
|
141
|
+
signing_key:
|
142
|
+
specification_version: 3
|
143
|
+
summary: Add one UDID to iOS Dev Portal & TestFlight
|
144
|
+
test_files: []
|
145
|
+
|