omudid 1.0.3
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.
- 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
|
+
|