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 +7 -0
- data/Gemfile +3 -0
- data/LICENSE +23 -0
- data/README.md +137 -0
- data/Rakefile +10 -0
- data/bin/iospro +25 -0
- data/cupertinopro.gemspec +33 -0
- data/lib/cupertinopro/provisioning_portal/agent.rb +325 -0
- data/lib/cupertinopro/provisioning_portal/commands/app_ids.rb +38 -0
- data/lib/cupertinopro/provisioning_portal/commands/certificates.rb +51 -0
- data/lib/cupertinopro/provisioning_portal/commands/devices.rb +65 -0
- data/lib/cupertinopro/provisioning_portal/commands/login.rb +16 -0
- data/lib/cupertinopro/provisioning_portal/commands/logout.rb +13 -0
- data/lib/cupertinopro/provisioning_portal/commands/profiles.rb +200 -0
- data/lib/cupertinopro/provisioning_portal/commands.rb +18 -0
- data/lib/cupertinopro/provisioning_portal/helpers.rb +66 -0
- data/lib/cupertinopro/provisioning_portal.rb +55 -0
- data/lib/cupertinopro/version.rb +3 -0
- data/lib/cupertinopro.rb +1 -0
- metadata +203 -0
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
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
|
+

|
|
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'
|
data/lib/cupertinopro.rb
ADDED
|
@@ -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: []
|