cupertinopro 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 125d0575cf1aff5322d6896069f2f855dd7dd714
4
+ data.tar.gz: b68575108f6659ac4f2a40258805a7f8bec9a18a
5
+ SHA512:
6
+ metadata.gz: 78551d611468315678e0f2eb532e116aadda7cf7b47e5b5b6297d06992a7c344ff7fb5af7cffe4ca8b829d1e6b4bd6e1e0171cdc9191f382ac2b0bc86476f4e6
7
+ data.tar.gz: 84e418d022c43d29fc944fd72f583dc2af31fd6cdf94215d7a3f9ad5d8c17232fb78aae55a2e57c5ab8a8d0263eca1a02b91593a688afbae66ee3b42ba8c2c3c
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 arvystate.net
4
+
5
+ Copyright (c) 2012 Mattt Thompson (http://mattt.me/)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ ![Cupertino](https://raw.github.com/mattt/nomad-cli.com/assets/cupertino-banner.png)
2
+
3
+ Automate administrative tasks that you would normally have to do through the Apple Dev Center websites. Life's too short to manage device identifiers by hand!
4
+
5
+ > Cupertino is named after [Cupertino, CA](http://en.wikipedia.org/wiki/Cupertino,_California): home to Apple, Inc.'s world headquarters.
6
+ > It's part of a series of world-class command-line utilities for iOS development, which includes [Shenzhen](https://github.com/mattt/shenzhen) (Building & Distribution), [Houston](https://github.com/mattt/houston) (Push Notifications), [Venice](https://github.com/mattt/venice) (In-App Purchase Receipt Verification), and [Dubai](https://github.com/mattt/dubai) (Passbook pass generation).
7
+
8
+ ## Requirements
9
+
10
+ Cupertino requires the [Xcode Command Line Tools](https://developer.apple.com/xcode/), which can be installed with the following command:
11
+
12
+ $ xcode-select --install
13
+
14
+ ## Installation
15
+
16
+ $ gem install cupertino
17
+
18
+ ## Usage
19
+
20
+ ### Authentication
21
+
22
+ $ ios login
23
+
24
+
25
+ _Credentials are saved in the Keychain. You will not be prompted for your username or password by commands while you are logged in. (Mac only)_
26
+
27
+ ### Devices
28
+
29
+ $ ios devices:list
30
+
31
+ +------------------------------+---------------------------------------+
32
+ | Listing 2 devices. You can register 98 additional devices. |
33
+ +---------------------------+------------------------------------------+
34
+ | Device Name | Device Identifier |
35
+ +---------------------------+------------------------------------------+
36
+ | Johnny Appleseed iPad | 0123456789012345678901234567890123abcdef |
37
+ | Johnny Appleseed iPhone | abcdef0123456789012345678901234567890123 |
38
+ +---------------------------+------------------------------------------+
39
+
40
+ $ ios devices:add "iPad 1"=abc123
41
+ $ ios devices:add "iPad 2"=def456 "iPad 3"=ghi789 ...
42
+
43
+ ### Provisioning Profiles
44
+
45
+ $ ios profiles:list
46
+
47
+ +----------------------------------+--------------+---------+
48
+ | Profile | App ID | Status |
49
+ +----------------------------------+--------------+---------+
50
+ | iOS Team Provisioning Profile: * | ABCDEFG123.* | Valid |
51
+ +----------------------------------+--------------+---------+
52
+
53
+ ---
54
+
55
+ $ ios profiles:manage:devices
56
+
57
+ _Opens an editor with a list of devices, each of which can be commented / uncommented to turn them off / on for that provisioning profile._
58
+
59
+ # Comment / Uncomment Devices to Turn Off / On for Provisioning Profile
60
+ Johnny Appleseed iPad 0123456789012345678901234567890123abcdef
61
+ # Johnny Appleseed iPhone abcdef0123456789012345678901234567890123
62
+
63
+
64
+ $ ios profiles:devices:add MyApp_Development_Profile "Johnny Appleseed iPad"=0123456789012345678901234567890123abcdef "Johnny Appleseed iPhone"=abcdef0123456789012345678901234567890123
65
+
66
+ _Adds (without an editor) a list of devices to a provisioning profile_
67
+
68
+ $ ios profiles:devices:remove MyApp_Development_Profile "Johnny Old iPad"=0123456789012345678901234567890123abcdef "Johnny Old iPhone"=abcdef0123456789012345678901234567890123
69
+
70
+ _Removes (without an editor) a list of devices from a provisioning profile_
71
+
72
+ ### App IDs
73
+
74
+ $ ios app_ids:list
75
+
76
+ +-----------------------------+------------------------+-------------------+-------------------+
77
+ | Bundle Seed ID | Description | Development | Distribution |
78
+ +-----------------------------+------------------------+-------------------+-------------------+
79
+ | 123ABCDEFG.com.mattt.bundle | App Bundle Description | Passes | Passes |
80
+ | | | Data Protection | Data Protection |
81
+ | | | iCloud | iCloud |
82
+ | | | In-App Purchase | In-App Purchase |
83
+ | | | Game Center | Game Center |
84
+ | | | Push Notification | Push Notification |
85
+ +-----------------------------+------------------------+-------------------+-------------------+
86
+
87
+ ### Certificates
88
+
89
+ $ ios certificates:list
90
+
91
+ +------------------+----------------------------------+-----------------+--------+
92
+ | Name | Provisioning Profiles | Expiration Date | Status |
93
+ +------------------+----------------------------------+-----------------+--------+
94
+ | Johnny Appleseed | iOS Team Provisioning Profile: * | Dec 23, 2012 | Issued |
95
+ +------------------+----------------------------------+-----------------+--------+
96
+
97
+ ## Commands
98
+
99
+ - `login`
100
+ - `logout`
101
+ - `devices:add`
102
+ - `devices:list`
103
+ - `profiles:list`
104
+ - `profiles:manage:devices`
105
+ - `profiles:manage:devices:add`
106
+ - `profiles:manage:devices:remove`
107
+ - `profiles:download`
108
+ - `profiles:download:all`
109
+ - `certificates:list`
110
+ - `certificates:download`
111
+ - `app_ids:list`
112
+
113
+ ### Disabled Commands
114
+
115
+ > With the latest updates to the Apple Developer Portal, the following functionality has been removed.
116
+
117
+ - `pass_type_ids:list`
118
+ - `pass_type_ids:add`
119
+ - `pass_type_ids:certificates:list`
120
+ - `pass_type_ids:certificates:add`
121
+ - `pass_type_ids:certificates:download`
122
+
123
+ ## Proxies
124
+
125
+ Cupertino will access the provisioning portal through a proxy if the `HTTP_PROXY` environment variable is set, with optional credentials `HTTP_PROXY_USER` and `HTTP_PROXY_PASSWORD`.
126
+
127
+ ## Contact
128
+
129
+ Mattt Thompson
130
+
131
+ - http://github.com/mattt
132
+ - http://twitter.com/mattt
133
+ - m@mattt.me
134
+
135
+ ## License
136
+
137
+ Cupertino is available under the MIT license. See the LICENSE file for more info.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ gemspec = eval(File.read("cupertinopro.gemspec"))
5
+
6
+ task :build => "#{gemspec.full_name}.gem"
7
+
8
+ file "#{gemspec.full_name}.gem" => gemspec.files + ["cupertinopro.gemspec"] do
9
+ system "gem build cupertinopro.gemspec"
10
+ end
data/bin/iospro ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'commander/import'
4
+ require 'terminal-table'
5
+ require 'term/ansicolor'
6
+ require 'csv'
7
+
8
+ #$:.unshift(File.join(File.dirname(__FILE__), "/../lib"))
9
+
10
+ require 'cupertinopro'
11
+
12
+ HighLine.track_eof = false # Fix for built-in Ruby
13
+ Signal.trap("INT") {} # Suppress backtrace when exiting command
14
+
15
+ program :version, CupertinoPro::VERSION
16
+ program :description, 'A supercharged command-line interface for the iOS Provisioning Portal'
17
+
18
+ program :help, 'Author', 'Mattt Thompson <m@mattt.me>'
19
+ program :help, 'Website', 'https://github.com/mattt'
20
+ program :help_formatter, :compact
21
+
22
+ default_command :help
23
+
24
+ require 'cupertinopro/provisioning_portal'
25
+ require 'cupertinopro/provisioning_portal/commands'
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require "cupertinopro/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "cupertinopro"
8
+ s.license = "MIT"
9
+ s.authors = ["Mattt Thompson", "Dal Rupnik"]
10
+ s.email = "legoless@arvystate.net"
11
+ s.homepage = "http://nomad-cli.com"
12
+ s.version = CupertinoPro::VERSION
13
+ s.platform = Gem::Platform::RUBY
14
+ s.summary = "CupertinoPro"
15
+ s.description = "A supercharged command-line interface for the iOS Provisioning Portal"
16
+
17
+ s.add_dependency "commander", "~> 4.1.2"
18
+ s.add_dependency "terminal-table", "~> 1.4.5"
19
+ s.add_dependency "term-ansicolor", "~> 1.0.7"
20
+ s.add_dependency "mechanize", "~> 2.5.1"
21
+ s.add_dependency "nokogiri", "~> 1.5.9"
22
+ s.add_dependency "security", "~> 0.1.2"
23
+ s.add_dependency "shenzhen", ">= 0.0.1"
24
+ s.add_dependency "certified", ">= 0.1.0"
25
+
26
+ s.add_development_dependency "rspec"
27
+ s.add_development_dependency "rake"
28
+
29
+ s.files = Dir["./**/*"].reject { |file| file =~ /\.\/(bin|log|pkg|script|spec|test|vendor)/ }
30
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
31
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
32
+ s.require_paths = ["lib"]
33
+ end
@@ -0,0 +1,325 @@
1
+ require 'mechanize'
2
+ require 'security'
3
+ require 'uri'
4
+ require 'json'
5
+ require 'logger'
6
+
7
+ module CupertinoPro
8
+ module ProvisioningPortal
9
+ class Agent < ::Mechanize
10
+ attr_accessor :username, :password, :team, :format
11
+
12
+ def initialize
13
+ super
14
+
15
+ self.user_agent_alias = 'Mac Safari'
16
+
17
+ self.log ||= Logger.new(STDOUT)
18
+ self.log.level = Logger::ERROR
19
+
20
+ if ENV['HTTP_PROXY']
21
+ uri = URI.parse(ENV['HTTP_PROXY'])
22
+ user = ENV['HTTP_PROXY_USER'] if ENV['HTTP_PROXY_USER']
23
+ password = ENV['HTTP_PROXY_PASSWORD'] if ENV['HTTP_PROXY_PASSWORD']
24
+
25
+ set_proxy(uri.host, uri.port, user || uri.user, password || uri.password)
26
+ end
27
+
28
+ pw = Security::InternetPassword.find(:server => CupertinoPro::ProvisioningPortal::HOST)
29
+ @username, @password = pw.attributes['acct'], pw.password if pw
30
+ end
31
+
32
+ def username=(value)
33
+ @username = value
34
+
35
+ pw = Security::InternetPassword.find(:a => self.username, :server => CupertinoPro::ProvisioningPortal::HOST)
36
+ @password = pw.password if pw
37
+ end
38
+
39
+ def get(uri, parameters = [], referer = nil, headers = {})
40
+ uri = ::File.join("https://#{CupertinoPro::ProvisioningPortal::HOST}", uri) unless /^https?/ === uri
41
+
42
+ 3.times do
43
+ super(uri, parameters, referer, headers)
44
+
45
+ return page unless page.respond_to?(:title)
46
+
47
+ case page.title
48
+ when /Sign in with your Apple ID/
49
+ login! and redo
50
+ when /Select Team/
51
+ select_team! and redo
52
+ else
53
+ return page
54
+ end
55
+ end
56
+
57
+ raise UnsuccessfulAuthenticationError
58
+ end
59
+
60
+ def list_certificates(type = :development)
61
+ url = case type
62
+ when :development
63
+ "https://developer.apple.com/account/iospro/certificate/certificateList.action?type=development"
64
+ when :distribution
65
+ "https://developer.apple.com/account/iospro/certificate/certificateList.action?type=distribution"
66
+ else
67
+ raise ArgumentError, "Certificate type must be :development or :distribution"
68
+ end
69
+
70
+ get(url)
71
+
72
+ regex = /certificateDataURL = "([^"]*)"/
73
+ certificate_data_url = (page.body.match regex or raise UnexpectedContentError)[1]
74
+
75
+ regex = /certificateRequestTypes = "([^"]*)"/
76
+ certificate_request_types = (page.body.match regex or raise UnexpectedContentError)[1]
77
+
78
+ regex = /certificateStatuses = "([^"]*)"/
79
+ certificate_statuses = (page.body.match regex or raise UnexpectedContentError)[1]
80
+
81
+ certificate_data_url += certificate_request_types + certificate_statuses
82
+
83
+ post(certificate_data_url)
84
+ certificate_data = page.content
85
+ parsed_certificate_data = JSON.parse(certificate_data)
86
+
87
+ certificates = []
88
+ parsed_certificate_data['certRequests'].each do |row|
89
+ certificate = Certificate.new
90
+ certificate.name = row['name']
91
+ certificate.type = type
92
+ certificate.download_url = "https://developer.apple.com/account/iospro/certificate/certificateContentDownload.action?displayId=#{row['certificateId']}&type=#{row['certificateTypeDisplayId']}"
93
+ certificate.expiration_date = row['expirationDateString']
94
+ certificate.status = row['statusString']
95
+ certificates << certificate
96
+ end
97
+
98
+ certificates
99
+ end
100
+
101
+ def download_certificate(certificate)
102
+ list_certificates(certificate.type)
103
+
104
+ self.pluggable_parser.default = Mechanize::Download
105
+ download = post(certificate.download_url)
106
+ download.save
107
+ download.filename
108
+ end
109
+
110
+ def list_devices
111
+ get('https://developer.apple.com/account/iospro/device/deviceList.action')
112
+
113
+ regex = /deviceDataURL = "([^"]*)"/
114
+ device_data_url = (page.body.match regex or raise UnexpectedContentError)[1]
115
+
116
+ post(device_data_url)
117
+
118
+ device_data = page.content
119
+ parsed_device_data = JSON.parse(device_data)
120
+
121
+ devices = []
122
+ parsed_device_data['devices'].each do |row|
123
+ #puts row
124
+
125
+ device = Device.new
126
+ device.name = row['name']
127
+ device.enabled = (row['status'] == 'c' ? 'Y' : 'N')
128
+ device.device_id = row['deviceId']
129
+ device.udid = row['deviceNumber']
130
+ devices << device
131
+ end
132
+
133
+ devices
134
+ end
135
+
136
+ def add_devices(*devices)
137
+ return if devices.empty?
138
+
139
+ get('https://developer.apple.com/account/iospro/device/deviceCreate.action')
140
+
141
+ begin
142
+ file = Tempfile.new(%w(devices .txt))
143
+ file.write("Device ID\tDevice Name")
144
+ devices.each do |device|
145
+ file.write("\n#{device.udid}\t#{device.name}")
146
+ end
147
+ file.rewind
148
+
149
+ form = page.form_with(:name => 'deviceImport') or raise UnexpectedContentError
150
+
151
+ upload = form.file_uploads.first
152
+ upload.file_name = file.path
153
+ form.radiobuttons.first.check()
154
+ form.submit
155
+
156
+ if form = page.form_with(:name => 'deviceSubmit')
157
+ form.method = 'POST'
158
+ form.field_with(:name => 'deviceNames').name = 'name'
159
+ form.field_with(:name => 'deviceNumbers').name = 'deviceNumber'
160
+ form.submit
161
+ elsif form = page.form_with(:name => 'deviceImport')
162
+ form.submit
163
+ else
164
+ raise UnexpectedContentError
165
+ end
166
+
167
+ #puts page.body
168
+
169
+ ensure
170
+ file.close!
171
+ end
172
+ end
173
+
174
+ def list_profiles(type = :development)
175
+ url = case type
176
+ when :development
177
+ 'https://developer.apple.com/account/iospro/profile/profileList.action?type=limited'
178
+ when :distribution
179
+ 'https://developer.apple.com/account/iospro/profile/profileList.action?type=production'
180
+ else
181
+ raise ArgumentError, 'Provisioning profile type must be :development or :distribution'
182
+ end
183
+
184
+ self.pluggable_parser.default = Mechanize::File
185
+ get(url)
186
+
187
+ regex = /profileDataURL = "([^"]*)"/
188
+ profile_data_url = (page.body.match regex or raise UnexpectedContentError)[1]
189
+
190
+ profile_data_url += case type
191
+ when :development
192
+ '&type=limited'
193
+ when :distribution
194
+ '&type=production'
195
+ end
196
+
197
+ post(profile_data_url)
198
+
199
+ profile_data = page.content
200
+ parsed_profile_data = JSON.parse(profile_data)
201
+
202
+ profiles = []
203
+ parsed_profile_data['provisioningProfiles'].each do |row|
204
+ #puts row
205
+
206
+ profile = ProvisioningProfile.new
207
+ profile.name = row['name']
208
+ profile.type = type
209
+ profile.app_id = row['appId']['appIdId']
210
+ profile.status = row['status']
211
+ profile.download_url = "https://developer.apple.com/account/iospro/profile/profileContentDownload.action?displayId=#{row['provisioningProfileId']}"
212
+ profile.edit_url = "https://developer.apple.com/account/iospro/profile/profileEdit.action?provisioningProfileId=#{row['provisioningProfileId']}"
213
+ profile.uuid = row['UUID']
214
+ profiles << profile
215
+ end
216
+
217
+ profiles
218
+ end
219
+
220
+ def download_profile(profile)
221
+ self.pluggable_parser.default = Mechanize::Download
222
+ download = get(profile.download_url)
223
+ download.save
224
+ download.filename
225
+ end
226
+
227
+ def manage_devices_for_profile(profile)
228
+ raise ArgumentError unless block_given?
229
+
230
+ devices = list_devices
231
+
232
+ begin
233
+ get(profile.edit_url)
234
+ rescue Mechanize::ResponseCodeError
235
+ say_error "Cannot manage devices for #{profile}" and abort
236
+ end
237
+
238
+ on, off = [], []
239
+ page.search('dd.selectDevices div.rows div').each do |row|
240
+ checkbox = row.search('input[type="checkbox"]').first
241
+ device = devices.detect{|device| device.device_id == checkbox['value']}
242
+
243
+ if checkbox['checked']
244
+ on << device
245
+ else
246
+ off << device
247
+ end
248
+ end
249
+
250
+ devices = yield on, off
251
+
252
+ form = page.form_with(:name => 'profileEdit') or raise UnexpectedContentError
253
+ form.checkboxes_with(:name => 'deviceIds').each do |checkbox|
254
+ if devices.detect{|device| device.device_id == checkbox['value']}
255
+ checkbox.check
256
+ else
257
+ checkbox.uncheck
258
+ end
259
+ end
260
+
261
+ adssuv = cookies.find{|cookie| cookie.name == 'adssuv'}
262
+ form.add_field!('adssuv-value', Mechanize::Util::uri_unescape(adssuv.value))
263
+
264
+ form.method = 'POST'
265
+ form.submit
266
+ end
267
+
268
+ def list_app_ids
269
+ get('https://developer.apple.com/account/iospro/identifiers/bundle/bundleList.action')
270
+
271
+ regex = /bundleDataURL = "([^"]*)"/
272
+ bundle_data_url = (page.body.match regex or raise UnexpectedContentError)[1]
273
+
274
+ post(bundle_data_url)
275
+ bundle_data = page.content
276
+ parsed_bundle_data = JSON.parse(bundle_data)
277
+
278
+ app_ids = []
279
+ parsed_bundle_data['appIds'].each do |row|
280
+ app_id = AppID.new
281
+ app_id.bundle_seed_id = [row['prefix'], row['identifier']].join(".")
282
+ app_id.description = row['name']
283
+
284
+ app_id.development_properties, app_id.distribution_properties = [], []
285
+ row['features'].each do |feature, value|
286
+ if value == true
287
+ app_id.development_properties << feature
288
+ elsif value.kind_of?(String) && !value.empty?
289
+ app_id.development_properties << "#{feature}: #{value}"
290
+ end
291
+ end
292
+
293
+ row['enabledFeatures'].each do |feature|
294
+ app_id.distribution_properties << feature
295
+ end
296
+
297
+ app_ids << app_id
298
+ end
299
+
300
+ app_ids
301
+ end
302
+
303
+ private
304
+
305
+ def login!
306
+ if form = page.forms.first
307
+ form.fields_with(type: 'text').first.value = self.username
308
+ form.fields_with(type: 'password').first.value = self.password
309
+
310
+ form.submit
311
+ end
312
+ end
313
+
314
+ def select_team!
315
+ if form = page.form_with(:name => 'saveTeamSelection')
316
+ team_option = form.radiobutton_with(:value => self.team_id)
317
+ team_option.check
318
+
319
+ button = form.button_with(:name => 'action:saveTeamSelection!save')
320
+ form.click_button(button)
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,38 @@
1
+ COLORS_BY_PROPERTY_VALUES = {
2
+ "Enabled" => :green,
3
+ "Configurable" => :yellow,
4
+ "Unavailable" => :underline
5
+ }
6
+
7
+ command :'app_ids:list' do |c|
8
+ c.syntax = 'iospro app_ids:list'
9
+ c.summary = 'Lists the App IDs'
10
+ c.description = ''
11
+
12
+ c.action do |args, options|
13
+ app_ids = try{agent.list_app_ids}
14
+
15
+ title = "Legend: #{COLORS_BY_PROPERTY_VALUES.collect{|k, v| k.send(v)}.join(', ')}"
16
+ table = Terminal::Table.new :title => title do |t|
17
+ t << ["Bundle Seed ID", "Description", "Development", "Distribution"]
18
+ app_ids.each do |app_id|
19
+ t << :separator
20
+
21
+ row = [app_id.bundle_seed_id, app_id.description]
22
+ [app_id.development_properties, app_id.distribution_properties].each do |properties|
23
+ values = []
24
+ properties.each do |key, value|
25
+ color = COLORS_BY_PROPERTY_VALUES[value] || :reset
26
+ values << key.sub(/\:$/, "").send(color)
27
+ end
28
+ row << values.join("\n")
29
+ end
30
+ t << row
31
+ end
32
+ end
33
+
34
+ puts table
35
+ end
36
+ end
37
+
38
+ alias_command :app_ids, :'app_ids:list'
@@ -0,0 +1,51 @@
1
+ command :'certificates:list' do |c|
2
+ c.syntax = 'iospro certificates:list [development|distribution]'
3
+ c.summary = 'Lists the Certificates'
4
+ c.description = ''
5
+
6
+ c.action do |args, options|
7
+ type = args.first.downcase.to_sym rescue nil
8
+ certificates = try{agent.list_certificates(type ||= :development)}
9
+
10
+ say_warning "No #{type} certificates found." and abort if certificates.empty?
11
+
12
+ table = Terminal::Table.new do |t|
13
+ t << ["Name", "Type", "Expiration Date", "Status"]
14
+ t.add_separator
15
+ certificates.each do |certificate|
16
+ status = case certificate.status
17
+ when "Issued"
18
+ certificate.status.green
19
+ else
20
+ certificate.status.red
21
+ end
22
+
23
+ t << [certificate.name, certificate.type, certificate.expiration_date, status]
24
+ end
25
+ end
26
+
27
+ puts table
28
+ end
29
+ end
30
+
31
+ alias_command :certificates, :'certificates:list'
32
+
33
+ command :'certificates:download' do |c|
34
+ c.syntax = 'iospro certificates:download [development|distribution]'
35
+ c.summary = 'Downloads the Certificates'
36
+ c.description = ''
37
+
38
+ c.action do |args, options|
39
+ type = args.first.downcase.to_sym rescue nil
40
+ certificates = try{agent.list_certificates(type ||= :development)}
41
+
42
+ say_warning "No #{type} certificates found." and abort if certificates.empty?
43
+
44
+ certificate = choose "Select a certificate to download:", *certificates
45
+ if filename = agent.download_certificate(certificate)
46
+ say_ok "Successfully downloaded: '#{filename}'"
47
+ else
48
+ say_error "Could not download certificate"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,65 @@
1
+ command :'devices:list' do |c|
2
+ c.syntax = 'iospro devices:list'
3
+ c.summary = 'Lists the Name and ID of Devices in the Provisioning Portal'
4
+ c.description = ''
5
+
6
+ c.action do |args, options|
7
+ devices = try{agent.list_devices}
8
+
9
+ if (agent.format == "csv")
10
+ csv_string = CSV.generate do |csv|
11
+ csv << ["Device Name", "Device Identifier", "Enabled"]
12
+
13
+ devices.compact.each do |device|
14
+ csv << [device.name, device.udid, device.enabled]
15
+ end
16
+ end
17
+
18
+ puts csv_string
19
+ else
20
+ number_of_devices = devices.compact.length
21
+ number_of_additional_devices = devices.length - number_of_devices
22
+
23
+ title = "Listing #{pluralize(number_of_devices, 'device')} "
24
+ title += "(You can register #{pluralize(number_of_additional_devices, 'additional device')})" if number_of_additional_devices > 0
25
+
26
+ table = Terminal::Table.new :title => title do |t|
27
+ t << ["Device Name", "Device Identifier", "Enabled"]
28
+ t.add_separator
29
+ devices.compact.each do |device|
30
+ t << [device.name, device.udid, device.enabled]
31
+ end
32
+ end
33
+
34
+ table.align_column 2, :center
35
+
36
+ puts table
37
+ end
38
+ end
39
+ end
40
+
41
+ alias_command :devices, :'devices:list'
42
+
43
+ command :'devices:add' do |c|
44
+ c.syntax = 'iospro devices:add DEVICE_NAME=DEVICE_ID [...]'
45
+ c.summary = 'Adds a device to the Provisioning Portal'
46
+ c.description = ''
47
+
48
+ c.action do |args, options|
49
+ say_error "Missing arguments, expected DEVICE_NAME=DEVICE_ID" and abort if args.nil? or args.empty?
50
+
51
+ devices = []
52
+ args.each do |arg|
53
+ components = arg.strip.gsub(/"/, '').split(/\=/)
54
+ device = Device.new
55
+ device.name = components.first
56
+ device.udid = components.last
57
+ say_warning "Invalid UDID: #{device.udid}" and next unless /\h{40}/ === device.udid
58
+ devices << device
59
+ end
60
+
61
+ agent.add_devices(*devices)
62
+
63
+ say_ok "Added #{pluralize(devices.length, 'device')}"
64
+ end
65
+ end
@@ -0,0 +1,16 @@
1
+ command :login do |c|
2
+ c.syntax = 'iospro login'
3
+ c.summary = 'Save account credentials'
4
+ c.description = ''
5
+
6
+ c.action do |args, options|
7
+ say_warning "You are already authenticated" if Security::InternetPassword.find(:server => CupertinoPro::ProvisioningPortal::HOST)
8
+
9
+ user = ask "Username:"
10
+ pass = password "Password:"
11
+
12
+ Security::InternetPassword.add(CupertinoPro::ProvisioningPortal::HOST, user, pass)
13
+
14
+ say_ok "Account credentials saved"
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ command :logout do |c|
2
+ c.syntax = 'iospro logout'
3
+ c.summary = 'Remove account credentials'
4
+ c.description = ''
5
+
6
+ c.action do |args, options|
7
+ say_error "You are not authenticated" and abort unless Security::InternetPassword.find(:server => CupertinoPro::ProvisioningPortal::HOST)
8
+
9
+ Security::InternetPassword.delete(:server => CupertinoPro::ProvisioningPortal::HOST)
10
+
11
+ say_ok "Account credentials removed"
12
+ end
13
+ end
@@ -0,0 +1,200 @@
1
+ command :'profiles:list' do |c|
2
+ c.syntax = 'iospro profiles:list'
3
+ c.summary = 'Lists the Provisioning Profiles'
4
+ c.description = ''
5
+
6
+ c.option '--type [TYPE]', [:development, :distribution], "Type of profile (development or distribution; defaults to development)"
7
+
8
+ c.action do |args, options|
9
+ type = (options.type.downcase.to_sym if options.type) || :development
10
+ profiles = try{agent.list_profiles(type)}
11
+
12
+ if (agent.format == "csv")
13
+ csv_string = CSV.generate do |csv|
14
+ csv << ["Profile", "App ID", "UUID", "Status"]
15
+
16
+ profiles.each do |profile|
17
+ csv << [profile.name, profile.app_id, profile.uuid, profile.status]
18
+ end
19
+ end
20
+
21
+ puts csv_string
22
+ else
23
+ say_warning "No #{type} provisioning profiles found." and abort if profiles.empty?
24
+
25
+ table = Terminal::Table.new do |t|
26
+ t << ["Profile", "App ID", "UUID", "Status"]
27
+ t.add_separator
28
+ profiles.each do |profile|
29
+ status = case profile.status
30
+ when "Invalid"
31
+ profile.status.red
32
+ else
33
+ profile.status.green
34
+ end
35
+
36
+ t << [profile.name, profile.app_id, profile.uuid, status]
37
+ end
38
+ end
39
+
40
+ puts table
41
+ end
42
+ end
43
+ end
44
+
45
+ alias_command :profiles, :'profiles:list'
46
+
47
+ command :'profiles:download' do |c|
48
+ c.syntax = 'iospro profiles:download [PROFILE_NAME]'
49
+ c.summary = 'Downloads the Provisioning Profiles'
50
+ c.description = ''
51
+
52
+ c.option '--type [TYPE]', [:development, :distribution], "Type of profile (development or distribution; defaults to development)"
53
+
54
+ c.action do |args, options|
55
+ type = (options.type.downcase.to_sym if options.type) || :development
56
+ profiles = try{agent.list_profiles(type)}
57
+ profiles = profiles.select{|profile| profile.status == 'Active'}
58
+
59
+ say_warning "No active #{type} profiles found." and abort if profiles.empty?
60
+
61
+ profile = profiles.find{|p| p.name == args.join(" ")} || choose("Select a profile:", *profiles)
62
+
63
+ if filename = agent.download_profile(profile)
64
+ say_ok "Successfully downloaded: '#{filename}'"
65
+ else
66
+ say_error "Could not download profile"
67
+ end
68
+ end
69
+ end
70
+
71
+ command :'profiles:download:all' do |c|
72
+ c.syntax = 'iospro profiles:download:all'
73
+ c.summary = 'Downloads all the active Provisioning Profiles'
74
+ c.description = ''
75
+
76
+ c.option '--type [TYPE]', [:development, :distribution], "Type of profile (development or distribution; defaults to development)"
77
+
78
+ c.action do |args, options|
79
+ type = (options.type.downcase.to_sym if options.type) || :development
80
+ profiles = try{agent.list_profiles(type)}
81
+ profiles = profiles.select{|profile| profile.status == 'Active'}
82
+
83
+ say_warning "No active #{type} profiles found." and abort if profiles.empty?
84
+ profiles.each do |profile|
85
+ if filename = agent.download_profile(profile)
86
+ say_ok "Successfully downloaded: '#{filename}'"
87
+ else
88
+ say_error "Could not download profile: '#{profile.name}'"
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ command :'profiles:manage:devices' do |c|
95
+ c.syntax = 'iospro profiles:manage:devices [PROFILE_NAME]'
96
+ c.summary = 'Manage active devices for a development provisioning profile'
97
+ c.description = ''
98
+
99
+ c.option '--type [TYPE]', [:development, :distribution], "Type of profile (development or distribution; defaults to development)"
100
+
101
+ c.action do |args, options|
102
+ type = (options.type.downcase.to_sym if options.type) || :development
103
+ profiles = try{agent.list_profiles(type)}
104
+ profiles.delete_if{|profile| profile.status == "Invalid"}
105
+
106
+ say_warning "No valid #{type} provisioning profiles found." and abort if profiles.empty?
107
+
108
+ profile = profiles.find{|p| p.name == args.first} || choose("Select a profile:", *profiles)
109
+
110
+ agent.manage_devices_for_profile(profile) do |on, off|
111
+ lines = ["# Comment / Uncomment Devices to Turn Off / On for Provisioning Profile"]
112
+ lines += on.collect{|device| "#{device.name} #{device.device_id}"}
113
+ lines += off.collect{|device| "# #{device.name} #{device.device_id}"}
114
+ (result = ask_editor lines.join("\n")) or abort("EDITOR undefined. Try run 'export EDITOR=vi'")
115
+
116
+ devices = []
117
+ result.split(/\n+/).each do |line|
118
+ next if /^#/ === line
119
+ components = line.split(/\s+/)
120
+ device = Device.new
121
+ device.device_id = components.pop
122
+ device.name = components.join(" ")
123
+ devices << device
124
+ end
125
+
126
+ devices
127
+ end
128
+
129
+ say_ok "Successfully managed devices"
130
+ end
131
+ end
132
+
133
+ alias_command :'profiles:devices', :'profiles:manage:devices'
134
+
135
+ command :'profiles:manage:devices:add' do |c|
136
+ c.syntax = 'iospro profiles:manage:devices:add [PROFILE_NAME] DEVICE_NAME=DEVICE_ID [...]'
137
+ c.summary = 'Add active devices to a Provisioning Profile'
138
+ c.description = ''
139
+
140
+ c.action do |args, options|
141
+ profiles = try{agent.list_profiles(:development) + agent.list_profiles(:distribution)}
142
+ profile = profiles.find{|p| p.name == args.first} || choose("Select a profile:", *profiles)
143
+
144
+ names = args[1..-1].select{|arg| /\=/ === arg}.collect{|arg| arg.sub /\=.*/, ''}
145
+ devices = []
146
+
147
+ agent.manage_devices_for_profile(profile) do |on, off|
148
+ names.each_with_index do |name, idx|
149
+ next if idx == 0 and name == profile.name
150
+
151
+ device = (on + off).detect{|d| d.name === name}
152
+
153
+ if device
154
+ devices << Device.new(name, device.udid, "Y", device.device_id)
155
+ end
156
+
157
+ end
158
+
159
+ on + devices
160
+ end
161
+
162
+ case devices.length
163
+ when 0
164
+ say_warning "No devices were added"
165
+ else
166
+ say_ok "Successfully added #{pluralize(devices.length, 'device', 'devices')} to #{profile}."
167
+ end
168
+ end
169
+ end
170
+
171
+ alias_command :'profiles:devices:add', :'profiles:manage:devices:add'
172
+
173
+ command :'profiles:manage:devices:remove' do |c|
174
+ c.syntax = 'iospro profiles:manage:devices:remove PROFILE_NAME DEVICE_NAME=DEVICE_ID [...]'
175
+ c.summary = 'Remove active devices from a Provisioning Profile.'
176
+ c.description = ''
177
+
178
+ c.action do |args, options|
179
+ profiles = try{agent.list_profiles(:development) + agent.list_profiles(:distribution)}
180
+ profile = profiles.find{|p| p.name == args.first} || choose("Select a profile:", *profiles)
181
+
182
+ say_warning "No provisioning profiles named #{args.first} were found." and abort unless profile
183
+
184
+ names = args.collect{|arg| arg.gsub /\=.*/, ''}
185
+
186
+ devices = []
187
+ agent.manage_devices_for_profile(profile) do |on, off|
188
+ devices = on.delete_if{|device| names.include?(device.name)}
189
+ end
190
+
191
+ case devices.length
192
+ when 0
193
+ say_warning "No devices were removed"
194
+ else
195
+ say_ok "Successfully removed #{pluralize(devices.length, 'device', 'devices')} from #{profile}."
196
+ end
197
+ end
198
+ end
199
+
200
+ alias_command :'profiles:devices:remove', :'profiles:manage:devices:remove'
@@ -0,0 +1,18 @@
1
+ include CupertinoPro::ProvisioningPortal
2
+
3
+ require 'cupertinopro/provisioning_portal/helpers'
4
+ include CupertinoPro::ProvisioningPortal::Helpers
5
+
6
+ global_option('-u', '--username USER', 'Username') { |arg| agent.username = arg unless arg.nil? }
7
+ global_option('-p', '--password PASSWORD', 'Password') { |arg| agent.password = arg unless arg.nil? }
8
+ global_option('--team TEAM', 'Team') { |arg| agent.team = arg if arg }
9
+ global_option('--info', 'Set log level to INFO') { agent.log.level = Logger::INFO }
10
+ global_option('--debug', 'Set log level to DEBUG') { agent.log.level = Logger::DEBUG }
11
+ global_option('--format [table|csv]', 'Set output format (default: table)') { |arg| agent.format = arg if arg }
12
+
13
+ require 'cupertinopro/provisioning_portal/commands/certificates'
14
+ require 'cupertinopro/provisioning_portal/commands/devices'
15
+ require 'cupertinopro/provisioning_portal/commands/profiles'
16
+ require 'cupertinopro/provisioning_portal/commands/app_ids'
17
+ require 'cupertinopro/provisioning_portal/commands/login'
18
+ require 'cupertinopro/provisioning_portal/commands/logout'
@@ -0,0 +1,66 @@
1
+ # Monkey Patch Commander::UI to alias password to avoid conflicts
2
+ module Commander::UI
3
+ alias :pw :password
4
+ end
5
+
6
+ class String
7
+ include Term::ANSIColor
8
+ end
9
+
10
+ module CupertinoPro
11
+ module ProvisioningPortal
12
+ module Helpers
13
+ def agent
14
+ unless @agent
15
+ @agent = CupertinoPro::ProvisioningPortal::Agent.new
16
+
17
+ @agent.instance_eval do
18
+ def username
19
+ @username ||= ask "Username:"
20
+ end
21
+
22
+ def password
23
+ @password ||= pw "Password:"
24
+ end
25
+
26
+ def team_id
27
+ unless @team_id
28
+ teams = []
29
+ page.form_with(:name => 'saveTeamSelection').radiobuttons.each do |radio|
30
+ name = page.search(".label-primary[for=\"#{radio.dom_id}\"]").first.text.strip
31
+ programs = page.search(".label-secondary[for=\"#{radio.dom_id}\"]").first.text.strip.split(/\,\s+/)
32
+ team_id = radio.value
33
+ teams << Team.new(name, programs, radio.value)
34
+ end
35
+
36
+ unless team = teams.detect{|t| t.name == @team || t.identifier == @team}
37
+ team = choose "Select a team:", *teams
38
+ end
39
+
40
+ @team_id = team.identifier
41
+ end
42
+
43
+ @team_id
44
+ end
45
+ end
46
+ end
47
+
48
+ @agent
49
+ end
50
+
51
+ def pluralize(n, singular, plural = nil)
52
+ n.to_i == 1 ? "1 #{singular}" : "#{n} #{plural || singular + 's'}"
53
+ end
54
+
55
+ def try
56
+ return unless block_given?
57
+
58
+ begin
59
+ yield
60
+ rescue UnsuccessfulAuthenticationError
61
+ say_error "Could not authenticate with Apple Developer Center. Check that your username & password are correct, and that your membership is valid and all pending Terms of Service & agreements are accepted. If this problem continues, try logging into https://developer.apple.com/membercenter/ from a browser to see what's going on." and abort
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ require 'mechanize'
2
+ require 'certified'
3
+
4
+ module CupertinoPro
5
+ module ProvisioningPortal
6
+ HOST = "developer.apple.com"
7
+
8
+ class UnsuccessfulAuthenticationError < RuntimeError; end
9
+ class UnexpectedContentError < RuntimeError; end
10
+
11
+ class Device < Struct.new(:name, :udid, :enabled, :device_id)
12
+ def to_s
13
+ "#{self.name} #{self.udid} #{self.enabled}"
14
+ end
15
+ end
16
+
17
+ class Certificate < Struct.new(:name, :type, :expiration_date, :status, :download_url)
18
+ def to_s
19
+ "#{self.name}"
20
+ end
21
+ end
22
+
23
+ class AppID < Struct.new(:bundle_seed_id, :description, :development_properties, :distribution_properties)
24
+ def to_s
25
+ "#{self.bundle_seed_id}"
26
+ end
27
+ end
28
+
29
+ class ProvisioningProfile < Struct.new(:name, :type, :app_id, :status, :download_url, :edit_url, :uuid)
30
+ def to_s
31
+ "#{self.name}"
32
+ end
33
+ end
34
+
35
+ class PassTypeID < Struct.new(:description, :id, :pass_certificates, :card_id)
36
+ def to_s
37
+ "#{self.id} #{self.description}"
38
+ end
39
+ end
40
+
41
+ class PassCertificate < Struct.new(:name, :status, :expiration_date, :certificate_id)
42
+ def to_s
43
+ "#{self.certificate_id}"
44
+ end
45
+ end
46
+
47
+ class Team < Struct.new(:name, :programs, :identifier)
48
+ def to_s
49
+ "#{self.name} (#{self.identifier})" + (" [#{self.programs.join(', ')}]" unless self.programs.empty?).to_s
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ require 'cupertinopro/provisioning_portal/agent'
@@ -0,0 +1,3 @@
1
+ module CupertinoPro
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1 @@
1
+ require 'cupertinopro/version'
metadata ADDED
@@ -0,0 +1,203 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cupertinopro
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mattt Thompson
8
+ - Dal Rupnik
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-06-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: commander
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 4.1.2
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 4.1.2
28
+ - !ruby/object:Gem::Dependency
29
+ name: terminal-table
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 1.4.5
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 1.4.5
42
+ - !ruby/object:Gem::Dependency
43
+ name: term-ansicolor
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: 1.0.7
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: 1.0.7
56
+ - !ruby/object:Gem::Dependency
57
+ name: mechanize
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: 2.5.1
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 2.5.1
70
+ - !ruby/object:Gem::Dependency
71
+ name: nokogiri
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: 1.5.9
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 1.5.9
84
+ - !ruby/object:Gem::Dependency
85
+ name: security
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 0.1.2
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: 0.1.2
98
+ - !ruby/object:Gem::Dependency
99
+ name: shenzhen
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 0.0.1
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 0.0.1
112
+ - !ruby/object:Gem::Dependency
113
+ name: certified
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: 0.1.0
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 0.1.0
126
+ - !ruby/object:Gem::Dependency
127
+ name: rspec
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rake
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ description: A supercharged command-line interface for the iOS Provisioning Portal
155
+ email: legoless@arvystate.net
156
+ executables:
157
+ - iospro
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - "./cupertinopro.gemspec"
162
+ - "./Gemfile"
163
+ - "./lib/cupertinopro/provisioning_portal/agent.rb"
164
+ - "./lib/cupertinopro/provisioning_portal/commands/app_ids.rb"
165
+ - "./lib/cupertinopro/provisioning_portal/commands/certificates.rb"
166
+ - "./lib/cupertinopro/provisioning_portal/commands/devices.rb"
167
+ - "./lib/cupertinopro/provisioning_portal/commands/login.rb"
168
+ - "./lib/cupertinopro/provisioning_portal/commands/logout.rb"
169
+ - "./lib/cupertinopro/provisioning_portal/commands/profiles.rb"
170
+ - "./lib/cupertinopro/provisioning_portal/commands.rb"
171
+ - "./lib/cupertinopro/provisioning_portal/helpers.rb"
172
+ - "./lib/cupertinopro/provisioning_portal.rb"
173
+ - "./lib/cupertinopro/version.rb"
174
+ - "./lib/cupertinopro.rb"
175
+ - "./LICENSE"
176
+ - "./Rakefile"
177
+ - "./README.md"
178
+ - bin/iospro
179
+ homepage: http://nomad-cli.com
180
+ licenses:
181
+ - MIT
182
+ metadata: {}
183
+ post_install_message:
184
+ rdoc_options: []
185
+ require_paths:
186
+ - lib
187
+ required_ruby_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ required_rubygems_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ requirements: []
198
+ rubyforge_project:
199
+ rubygems_version: 2.0.14
200
+ signing_key:
201
+ specification_version: 4
202
+ summary: CupertinoPro
203
+ test_files: []