asca 0.1.0 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +23 -2
- data/README.md +32 -8
- data/app-store-connect-openapi-spec.json +32545 -0
- data/asca.gemspec +2 -1
- data/exe/asca +62 -12
- data/lib/asca.rb +10 -3
- data/lib/asca/consts.rb +3 -0
- data/lib/asca/rest/provisioning/bundleids.rb +22 -0
- data/lib/asca/rest/provisioning/certificates.rb +32 -0
- data/lib/asca/rest/provisioning/devices.rb +40 -0
- data/lib/asca/rest/provisioning/profiles.rb +167 -0
- data/lib/asca/tools/configuration.rb +57 -0
- data/lib/asca/tools/log.rb +55 -0
- data/lib/asca/tools/token.rb +102 -0
- data/lib/asca/tools/tools.rb +22 -0
- data/lib/asca/version.rb +1 -1
- metadata +12 -6
- data/lib/asca/configuration.rb +0 -55
- data/lib/asca/profiles.rb +0 -36
- data/lib/asca/token.rb +0 -92
data/asca.gemspec
CHANGED
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.add_development_dependency "rake", "~> 10.0"
|
32
32
|
spec.add_development_dependency "minitest", "~> 5.0"
|
33
33
|
spec.add_dependency "json"
|
34
|
-
|
34
|
+
#https://github.com/httprb/http/wiki
|
35
|
+
spec.add_dependency "http"
|
35
36
|
spec.add_dependency 'jwt'
|
36
37
|
end
|
data/exe/asca
CHANGED
@@ -10,21 +10,71 @@ require 'optparse'
|
|
10
10
|
|
11
11
|
options = {}
|
12
12
|
option_parser = OptionParser.new do |opts|
|
13
|
-
opts.banner = 'This is an apple connect api wrapper cli.'
|
13
|
+
opts.banner = 'This is an apple connect api wrapper cli. It wraps most of the apple store connect REST api and also supplement some useful tools based on these apis. For how to use it, refer to https://github.com/xueminghao/appstoreconnectapi plase.'
|
14
14
|
|
15
|
-
opts.on('
|
16
|
-
options[:
|
15
|
+
opts.on('--tools tool-name', 'Call a specific tool by name. such as register-device') do |value|
|
16
|
+
options[:tool_name] = value
|
17
17
|
end
|
18
|
-
|
19
|
-
|
18
|
+
|
19
|
+
opts.on('-a api-name', '--api api-name', 'api name') do |value|
|
20
|
+
options[:api_name] = value
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on('-m method', '--method method', 'api request method') do |value|
|
24
|
+
options[:method] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-n name', '--name name', 'tool or api parameter: name') do |value|
|
28
|
+
options[:name] = value
|
29
|
+
end
|
30
|
+
opts.on('--udid udid', 'tool or api parameter: device udid') do |value|
|
31
|
+
options[:udid] = value
|
32
|
+
end
|
33
|
+
opts.on('--profile-names profile-name-array', 'tool or api parameter: profile name list') do |value|
|
34
|
+
options[:profile_names] = value.split(',')
|
35
|
+
end
|
36
|
+
opts.on('--auto-install', 'tool or api parameter: auto install the downloaded profiles') do
|
37
|
+
options[:auto_install] = true
|
38
|
+
end
|
39
|
+
opts.on('-v', '--version', 'current version') do |value|
|
40
|
+
options[:version] = 'version'
|
20
41
|
end
|
21
42
|
end.parse!
|
22
43
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
44
|
+
tool_name = options[:tool_name]
|
45
|
+
api_name = options[:api_name]
|
46
|
+
|
47
|
+
if tool_name
|
48
|
+
case tool_name
|
49
|
+
when 'register-device'
|
50
|
+
Asca::Tools.register_device :device_info => { :udid => options[:udid], :name => options[:name] }, :profile_names => options[:profile_names]
|
51
|
+
when 'download-profile'
|
52
|
+
if options[:auto_install]
|
53
|
+
Asca::REST::Provisioning::Profiles.install_profile :name => options[:name]
|
54
|
+
else
|
55
|
+
Asca::REST::Provisioning::Profiles.download_profile :name => options[:name]
|
56
|
+
end
|
57
|
+
else
|
58
|
+
Asca::Tools::Log.error("Unsupported tool name: #{tool_name}")
|
59
|
+
end
|
60
|
+
elsif api_name
|
61
|
+
method = options[:method]
|
62
|
+
method = method ? method : 'get'
|
63
|
+
case api_name
|
64
|
+
when 'device'
|
65
|
+
case method
|
66
|
+
when 'get'
|
67
|
+
Asca::REST::Provisioning::Devices.list_devices
|
68
|
+
when 'post'
|
69
|
+
Asca::REST::Provisioning::Devices.register_new_device options[:udid], options[:name]
|
70
|
+
else
|
71
|
+
Asca::Tools::Log.error("Unsupported request method #{method} for api #{api_name}, coming soon!!")
|
72
|
+
end
|
73
|
+
else
|
74
|
+
Asca::Tools::Log.error("Unsupported api request #{api_name}, coming soon!!")
|
75
|
+
end
|
76
|
+
elsif options[:version]
|
77
|
+
Asca::Tools::Log.info(Asca::VERSION)
|
28
78
|
else
|
29
|
-
|
30
|
-
end
|
79
|
+
puts Asca::Tools::Log.error('Wrong parameters')
|
80
|
+
end
|
data/lib/asca.rb
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
require "asca/version"
|
2
2
|
require "asca/consts.rb"
|
3
|
-
|
4
|
-
require "asca/
|
5
|
-
require "asca/
|
3
|
+
|
4
|
+
require "asca/rest/provisioning/bundleids.rb"
|
5
|
+
require "asca/rest/provisioning/devices.rb"
|
6
|
+
require "asca/rest/provisioning/certificates.rb"
|
7
|
+
require "asca/rest/provisioning/profiles.rb"
|
8
|
+
|
9
|
+
require "asca/tools/log.rb"
|
10
|
+
require "asca/tools/configuration.rb"
|
11
|
+
require "asca/tools/token"
|
12
|
+
require "asca/tools/tools.rb"
|
6
13
|
|
7
14
|
module Asca
|
8
15
|
class Error < StandardError; end
|
data/lib/asca/consts.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Asca
|
5
|
+
module REST
|
6
|
+
module Provisioning
|
7
|
+
class BundleIDs
|
8
|
+
class << self
|
9
|
+
def list_bundle_ids
|
10
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).get(URI_BUNDLEIDS)
|
11
|
+
if response.status.success?
|
12
|
+
bundleids = JSON.parse(response.body)
|
13
|
+
return bundleids
|
14
|
+
else
|
15
|
+
puts response.body
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'http'
|
2
|
+
|
3
|
+
module Asca
|
4
|
+
module REST
|
5
|
+
module Provisioning
|
6
|
+
class Certificates
|
7
|
+
class << self
|
8
|
+
def list_certificates(options = {})
|
9
|
+
# type enum : [ "DEVELOPMENT", "DISTRIBUTION" ]
|
10
|
+
type = options[:type]
|
11
|
+
types = !type ? "DEVELOPMENT, DISTRIBUTION" : type
|
12
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).get(URI_CERTIFICATES, :params => {
|
13
|
+
"filter[certificateType]" => types
|
14
|
+
})
|
15
|
+
if response.status.success?
|
16
|
+
certificates = JSON.parse(response.body)["data"]
|
17
|
+
for certificate in certificates do
|
18
|
+
id = certificate["id"]
|
19
|
+
name = certificate["attributes"]["name"]
|
20
|
+
type = certificate["attributes"]["certificateType"]
|
21
|
+
puts "Certificate id: #{id} name: #{name} type: #{type}"
|
22
|
+
end
|
23
|
+
else
|
24
|
+
puts response.body
|
25
|
+
end
|
26
|
+
return response.status.success?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'http'
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Asca
|
5
|
+
module REST
|
6
|
+
module Provisioning
|
7
|
+
class Devices
|
8
|
+
class << self
|
9
|
+
def list_devices
|
10
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).get(URI_DEVICES, :params => { "limit": 200 })
|
11
|
+
if response.status.success?
|
12
|
+
devices = JSON.parse(response.body)
|
13
|
+
puts "device count #{devices["data"].length()}"
|
14
|
+
return devices["data"]
|
15
|
+
end
|
16
|
+
return nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def register_new_device(options = {})
|
20
|
+
udid = options[:udid]
|
21
|
+
name = options[:name]
|
22
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).post(URI_DEVICES, :json => { "data" => {
|
23
|
+
"type" => "devices",
|
24
|
+
"attributes" => {
|
25
|
+
"name" => name,
|
26
|
+
"platform" => "IOS",
|
27
|
+
"udid" => udid
|
28
|
+
}
|
29
|
+
}})
|
30
|
+
if response.status.success?
|
31
|
+
return true
|
32
|
+
else
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'http'
|
2
|
+
require 'json'
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module Asca
|
6
|
+
module REST
|
7
|
+
module Provisioning
|
8
|
+
class Profiles
|
9
|
+
class << self
|
10
|
+
def download_profile(options = {})
|
11
|
+
profile_name = options[:name]
|
12
|
+
out_put_dir = options[:out_put_dir]
|
13
|
+
if !out_put_dir
|
14
|
+
out_put_dir = Asca::Tools::Configuration.get_config('out_put_dir')
|
15
|
+
if !out_put_dir
|
16
|
+
puts "Please enter your out put dir:"
|
17
|
+
out_put_dir = File.expand_path(gets.chomp)
|
18
|
+
Asca::Tools::Configuration.update_config('out_put_dir', out_put_dir)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).get(URI_PROFILES, :params => { 'filter[name]' => profile_name })
|
22
|
+
if response.status.success?
|
23
|
+
profile_obj = JSON.parse(response.body)
|
24
|
+
profile_content = profile_obj["data"][0]["attributes"]['profileContent']
|
25
|
+
File.open(File.expand_path(profile_name + ".mobileprovision", out_put_dir), 'w') do |file|
|
26
|
+
file.write(Base64.decode64(profile_content))
|
27
|
+
end
|
28
|
+
else
|
29
|
+
Asca::Tools::Log.error(response.body)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def install_profile(options = {})
|
34
|
+
profile_name = options[:name]
|
35
|
+
download_profile :name => profile_name, :out_put_dir => Asca::Tools::Configuration::CACHE_DIR
|
36
|
+
profile_file_path = File.expand_path(profile_name + ".mobileprovision", Asca::Tools::Configuration::CACHE_DIR)
|
37
|
+
|
38
|
+
# install profile
|
39
|
+
FileUtils.cp(profile_file_path, File.expand_path('~/Library/MobileDevice/Provisioning Profiles'))
|
40
|
+
Asca::Tools::Log.info("#{profile_name} installed successfully!")
|
41
|
+
end
|
42
|
+
|
43
|
+
# notion: bundle_id is not bundle identifier and device id is udid。They are the corresponding api id.
|
44
|
+
def create_new_profile(options = {})
|
45
|
+
if !options[:name]
|
46
|
+
Asca::Tools::Asca::Tools::Log.error('No profile name specified')
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
if !options[:type]
|
50
|
+
Asca::Tools::Asca::Tools::Log.error('No type specified')
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
if !options[:bundle_id]
|
54
|
+
Asca::Tools::Asca::Tools::Log.error('No bundle id specfied')
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
if !options[:certificate_ids] || options[:certificate_ids].length == 0
|
58
|
+
Asca::Tools::Asca::Tools::Log.error('No certificate id specified')
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
|
62
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).post(URI_PROFILES, :json => { "data" => {
|
63
|
+
"type" => "profiles",
|
64
|
+
"attributes" => {
|
65
|
+
"name" => options[:name],
|
66
|
+
"profileType" => options[:type],
|
67
|
+
},
|
68
|
+
"relationships" => {
|
69
|
+
"bundleId" => {
|
70
|
+
"data" => {
|
71
|
+
"type" => "bundleIds",
|
72
|
+
"id" => options[:bundle_id],
|
73
|
+
}
|
74
|
+
},
|
75
|
+
"certificates" => {
|
76
|
+
"data" => options[:certificate_ids].map { |certificate_id| { "type" => "certificates", "id" => certificate_id }}
|
77
|
+
},
|
78
|
+
"devices" => {
|
79
|
+
"data" => options[:device_ids] ? options[:device_ids].map { |device_id| { "type" => "devices", "id" => device_id } } : nil
|
80
|
+
},
|
81
|
+
}
|
82
|
+
}})
|
83
|
+
if response.status.success?
|
84
|
+
return true
|
85
|
+
else
|
86
|
+
puts response.body
|
87
|
+
return false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def delete_profile(options = {})
|
92
|
+
# query profile id
|
93
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).get(URI_PROFILES, :params => { 'filter[name]' => options[:name] })
|
94
|
+
if response.status.success?
|
95
|
+
responseObj = JSON.parse(response.body)
|
96
|
+
queried_profile_list = responseObj["data"]
|
97
|
+
if queried_profile_list.length() > 0
|
98
|
+
profile_id = queried_profile_list[0]["id"]
|
99
|
+
end
|
100
|
+
else
|
101
|
+
Asca::Tools::Log.error(response.body)
|
102
|
+
return
|
103
|
+
end
|
104
|
+
if !profile_id
|
105
|
+
puts "No profile named #{options[:name]} found!"
|
106
|
+
return
|
107
|
+
end
|
108
|
+
|
109
|
+
# delete profile
|
110
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).delete(URI_PROFILES + "/#{profile_id}")
|
111
|
+
if response.status.success?
|
112
|
+
Asca::Tools::Log.info("Profile named #{options[:name]} deleted successfully!")
|
113
|
+
else
|
114
|
+
puts response.body
|
115
|
+
end
|
116
|
+
return response.status.success?
|
117
|
+
end
|
118
|
+
|
119
|
+
# update profile. The new profile is almost the same as the old one, such as the same name, bundle id, certificate ids, with only exception that the new one always try to include all the currently reigstered devices.
|
120
|
+
def update_profile(options = {})
|
121
|
+
# query profile info
|
122
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).get(URI_PROFILES, :params => { 'filter[name]' => options[:name] })
|
123
|
+
if response.status.success?
|
124
|
+
responseObj = JSON.parse(response.body)
|
125
|
+
queried_profile_list = responseObj["data"]
|
126
|
+
if queried_profile_list.length() > 0
|
127
|
+
profile = queried_profile_list[0]
|
128
|
+
end
|
129
|
+
else
|
130
|
+
Asca::Tools::Log.error(response.body)
|
131
|
+
return
|
132
|
+
end
|
133
|
+
|
134
|
+
if !profile
|
135
|
+
Asca::Tools::Log.error("No profile named #{options[:name]} found")
|
136
|
+
return
|
137
|
+
end
|
138
|
+
# create new profile
|
139
|
+
profile_type = profile["attributes"]["profileType"]
|
140
|
+
|
141
|
+
# get bundle id
|
142
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).get(profile["relationships"]["bundleId"]["links"]["self"])
|
143
|
+
bundle_id = JSON.parse(response.body)["data"]["id"]
|
144
|
+
response = HTTP.auth('Bearer ' + Asca::Tools::Token.new_token).get(profile["relationships"]["certificates"]["links"]["self"])
|
145
|
+
certificate_ids = JSON.parse(response.body)["data"].map { |cer| cer["id"] }
|
146
|
+
|
147
|
+
# get all device ids
|
148
|
+
device_ids = Asca::REST::Provisioning::Devices.list_devices.map { |device|
|
149
|
+
device["id"]
|
150
|
+
}
|
151
|
+
|
152
|
+
# delete old prifile
|
153
|
+
delete_profile :name => options[:name]
|
154
|
+
|
155
|
+
if profile_type.include? 'APP_STORE'
|
156
|
+
create_new_profile :name => options[:name], :type => profile_type, :bundle_id => bundle_id, :certificate_ids => certificate_ids
|
157
|
+
else
|
158
|
+
create_new_profile :name => options[:name], :type => profile_type, :bundle_id => bundle_id, :device_ids => device_ids, :certificate_ids => certificate_ids
|
159
|
+
end
|
160
|
+
|
161
|
+
return true
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Asca
|
5
|
+
module Tools
|
6
|
+
class Configuration
|
7
|
+
ROOTDIR = File.expand_path '~/.com.hurryup.asca'
|
8
|
+
JSONFILE = File.expand_path 'config.json', ROOTDIR
|
9
|
+
CACHE_DIR = File.expand_path 'cache', ROOTDIR
|
10
|
+
class << self
|
11
|
+
# reset config file
|
12
|
+
def reset_config
|
13
|
+
# remove all
|
14
|
+
FileUtils.rm_rf(ROOTDIR)
|
15
|
+
|
16
|
+
# create root dir
|
17
|
+
Dir.mkdir ROOTDIR
|
18
|
+
|
19
|
+
# create cache dir
|
20
|
+
Dir.mkdir CACHE_DIR
|
21
|
+
|
22
|
+
# init config file
|
23
|
+
File.open(JSONFILE, 'w') { |file|
|
24
|
+
file.write("{}")
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
# update config
|
29
|
+
def update_config(key, value)
|
30
|
+
if !File.exist?(JSONFILE)
|
31
|
+
reset_config
|
32
|
+
end
|
33
|
+
file_content = File.read(JSONFILE)
|
34
|
+
configuration = JSON.parse(file_content)
|
35
|
+
if value
|
36
|
+
configuration[key] = value
|
37
|
+
else
|
38
|
+
configuration.delete(key)
|
39
|
+
end
|
40
|
+
File.open(JSONFILE, 'w') { |file|
|
41
|
+
file.write(JSON.pretty_generate(configuration))
|
42
|
+
}
|
43
|
+
return 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_config(key)
|
47
|
+
if !File.exist?(JSONFILE)
|
48
|
+
reset_config
|
49
|
+
end
|
50
|
+
file_content = File.read(JSONFILE)
|
51
|
+
configuration = JSON.parse(file_content)
|
52
|
+
return configuration[key]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Asca
|
2
|
+
module Tools
|
3
|
+
class Log
|
4
|
+
class Color
|
5
|
+
class << self
|
6
|
+
def time
|
7
|
+
"\e[37m"
|
8
|
+
end
|
9
|
+
|
10
|
+
def warn
|
11
|
+
"\e[33;1m"
|
12
|
+
end
|
13
|
+
|
14
|
+
def info
|
15
|
+
"\e[32m"
|
16
|
+
end
|
17
|
+
|
18
|
+
def error
|
19
|
+
"\e[31;1m"
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset
|
23
|
+
"\e[0m"
|
24
|
+
end
|
25
|
+
|
26
|
+
def command
|
27
|
+
"\e[34m"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def warn message
|
34
|
+
puts "#{Color.warn}WARN: #{message}#{Color.reset}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def info message
|
38
|
+
puts "#{Color.info}INFO: #{message}#{Color.reset}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def error message
|
42
|
+
puts "#{Color.error}ERROR: #{message}#{Color.reset}", STDERR
|
43
|
+
end
|
44
|
+
|
45
|
+
def command message
|
46
|
+
puts "#{Color.command}RUN: \e[4m#{message}#{Color.reset}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def puts message, io = STDOUT
|
50
|
+
io.puts "#{Color.time}[#{Time.new}]#{Color.reset} #{message}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|