atlantispro 0.1.2 → 0.2.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 +4 -4
- data/Atlantis.gemspec +3 -3
- data/Gemfile.lock +50 -0
- data/bin/{testflight → distribution} +1 -2
- data/lib/Atlantis/portal.rb +18 -9
- data/lib/Atlantis/portal/agent.rb +1 -197
- data/lib/Atlantis/portal/commands.rb +7 -5
- data/lib/Atlantis/portal/commands/devices.rb +4 -35
- data/lib/Atlantis/portal/commands/groups.rb +11 -0
- data/lib/Atlantis/portal/commands/login.rb +4 -4
- data/lib/Atlantis/portal/commands/logout.rb +4 -4
- data/lib/Atlantis/portal/commands/people.rb +5 -35
- data/lib/Atlantis/portal/crashlytics/crashlyticsservice.rb +315 -0
- data/lib/Atlantis/portal/helpers.rb +74 -1
- data/lib/Atlantis/portal/service.rb +46 -0
- data/lib/Atlantis/portal/testflight/testflightservice.rb +207 -0
- data/lib/Atlantis/version.rb +1 -1
- metadata +15 -10
- data/lib/Atlantis/portal/commands/invites.rb +0 -16
- data/lib/Atlantis/portal/commands/lists.rb +0 -36
- data/lib/Atlantis/portal/commands/teams.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: deee38e26d7c80ed8bcf9aa7ff0625c34431f004
|
4
|
+
data.tar.gz: ce4b735ebb4927cf32732743a0666f8a2aa16cad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4d95d18bb76ef9cbc37576226cc5f168d56eabf022a093ad8735d766e2f27ee06ffa67f09d036b9881d3c6d538dee7655b281059da544e04ad32f197a600674
|
7
|
+
data.tar.gz: 6ef003a0b3a0ca393ee8d9eaab686b60ac405437bf97cc19510795d3f222af296d9b3eb43c3c00c7a95722a000c77a48d58bd9a2ac4fe759847a47ccf9ebc00c
|
data/Atlantis.gemspec
CHANGED
@@ -8,9 +8,9 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = Atlantis::VERSION
|
9
9
|
spec.authors = ["Dal Rupnik"]
|
10
10
|
spec.email = ["legoless@gmail.com"]
|
11
|
-
spec.summary = "
|
12
|
-
spec.description = "
|
13
|
-
spec.homepage = ""
|
11
|
+
spec.summary = "A command line interface to connect to distribution services such as TestFlight or Crashlytics."
|
12
|
+
spec.description = "A simple command line interface to work with multiple application distribution services, such as TestFlight or Crashlytics. Allows for downloading of device identifiers, registered testers and more."
|
13
|
+
spec.homepage = "https://github.com/legoless/Atlantis"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.add_dependency "commander", "~> 4.1.2"
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
atlantispro (0.1.2)
|
5
|
+
certified (>= 0.1.0)
|
6
|
+
commander (~> 4.1.2)
|
7
|
+
mechanize (~> 2.5.1)
|
8
|
+
nokogiri (~> 1.5.9)
|
9
|
+
security (~> 0.1.2)
|
10
|
+
term-ansicolor (~> 1.0.7)
|
11
|
+
terminal-table (~> 1.4.5)
|
12
|
+
|
13
|
+
GEM
|
14
|
+
remote: https://rubygems.org/
|
15
|
+
specs:
|
16
|
+
certified (0.1.2)
|
17
|
+
commander (4.1.6)
|
18
|
+
highline (~> 1.6.11)
|
19
|
+
domain_name (0.5.18)
|
20
|
+
unf (>= 0.0.5, < 1.0.0)
|
21
|
+
highline (1.6.21)
|
22
|
+
mechanize (2.5.1)
|
23
|
+
domain_name (~> 0.5, >= 0.5.1)
|
24
|
+
mime-types (~> 1.17, >= 1.17.2)
|
25
|
+
net-http-digest_auth (~> 1.1, >= 1.1.1)
|
26
|
+
net-http-persistent (~> 2.5, >= 2.5.2)
|
27
|
+
nokogiri (~> 1.4)
|
28
|
+
ntlm-http (~> 0.1, >= 0.1.1)
|
29
|
+
webrobots (~> 0.0, >= 0.0.9)
|
30
|
+
mime-types (1.25.1)
|
31
|
+
net-http-digest_auth (1.4)
|
32
|
+
net-http-persistent (2.9.4)
|
33
|
+
nokogiri (1.5.11)
|
34
|
+
ntlm-http (0.1.1)
|
35
|
+
rake (10.3.2)
|
36
|
+
security (0.1.2)
|
37
|
+
term-ansicolor (1.0.7)
|
38
|
+
terminal-table (1.4.5)
|
39
|
+
unf (0.1.4)
|
40
|
+
unf_ext
|
41
|
+
unf_ext (0.0.6)
|
42
|
+
webrobots (0.1.1)
|
43
|
+
|
44
|
+
PLATFORMS
|
45
|
+
ruby
|
46
|
+
|
47
|
+
DEPENDENCIES
|
48
|
+
atlantispro!
|
49
|
+
bundler (~> 1.6)
|
50
|
+
rake
|
@@ -16,8 +16,7 @@ Signal.trap("INT") {} # Suppress backtrace when exiting command
|
|
16
16
|
|
17
17
|
program :name, 'atlantis'
|
18
18
|
program :version, Atlantis::VERSION
|
19
|
-
program :description, 'A command-line interface for
|
20
|
-
|
19
|
+
program :description, 'A command-line interface for Distribution Portals (Crashlytics, TestFlight)'
|
21
20
|
program :help, 'Author', 'Dal Rupnik <legoless@gmail.com>'
|
22
21
|
program :help, 'Website', 'https://github.com/legoless'
|
23
22
|
program :help_formatter, :compact
|
data/lib/Atlantis/portal.rb
CHANGED
@@ -3,26 +3,26 @@ require 'certified'
|
|
3
3
|
|
4
4
|
module Atlantis
|
5
5
|
module Portal
|
6
|
-
HOST = "testflightapp.com"
|
7
|
-
|
8
6
|
class UnsuccessfulAuthenticationError < RuntimeError; end
|
7
|
+
class NetworkConnectionError < RuntimeError; end
|
9
8
|
class UnexpectedContentError < RuntimeError; end
|
9
|
+
class UnknownTeamError < RuntimeError; end
|
10
10
|
|
11
|
-
class Person < Struct.new(:id, :name, :email, :devices)
|
11
|
+
class Person < Struct.new(:id, :name, :email, :devices, :groups)
|
12
12
|
def to_s
|
13
|
-
"#{self.id} #{self.name} #{self.email}
|
13
|
+
"#{self.id} #{self.name} #{self.email}"
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
class Device < Struct.new(:
|
17
|
+
class Device < Struct.new(:manufacturer, :model, :os_version, :identifier, :name, :platform, :model_name)
|
18
18
|
def to_s
|
19
|
-
"#{self.
|
19
|
+
"#{self.identifier} #{self.name}"
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
class
|
23
|
+
class Group < Struct.new(:id, :alias, :name, :count)
|
24
24
|
def to_s
|
25
|
-
"#{self.id} #{self.name}
|
25
|
+
"#{self.id} #{self.name}"
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -31,7 +31,16 @@ module Atlantis
|
|
31
31
|
"#{self.id} #{self.name} #{self.token}"
|
32
32
|
end
|
33
33
|
end
|
34
|
+
|
35
|
+
class CommandArguments < Struct.new(:username, :password, :team, :format, :service, :group)
|
36
|
+
def to_s
|
37
|
+
"#{self.username} #{self.team} #{self.format} #{self.service}"
|
38
|
+
end
|
39
|
+
end
|
34
40
|
end
|
35
41
|
end
|
36
42
|
|
37
|
-
require 'atlantis/portal/agent'
|
43
|
+
require 'atlantis/portal/agent'
|
44
|
+
require 'atlantis/portal/service'
|
45
|
+
require 'atlantis/portal/crashlytics/crashlyticsservice'
|
46
|
+
require 'atlantis/portal/testflight/testflightservice'
|
@@ -8,7 +8,7 @@ require 'nokogiri'
|
|
8
8
|
module Atlantis
|
9
9
|
module Portal
|
10
10
|
class Agent < ::Mechanize
|
11
|
-
attr_accessor :
|
11
|
+
attr_accessor :format, :service, :service_instance
|
12
12
|
|
13
13
|
def initialize
|
14
14
|
super
|
@@ -25,203 +25,7 @@ module Atlantis
|
|
25
25
|
|
26
26
|
set_proxy(uri.host, uri.port, user || uri.user, password || uri.password)
|
27
27
|
end
|
28
|
-
|
29
|
-
pw = Security::InternetPassword.find(:server => Atlantis::Portal::HOST)
|
30
|
-
@username, @password = pw.attributes['acct'], pw.password if pw
|
31
|
-
end
|
32
|
-
|
33
|
-
def username=(value)
|
34
|
-
@username = value
|
35
|
-
|
36
|
-
pw = Security::InternetPassword.find(:a => self.username, :server => Atlantis::Portal::HOST)
|
37
|
-
@password = pw.password if pw
|
38
|
-
end
|
39
|
-
|
40
|
-
def get(uri, parameters = [], referer = nil, headers = {})
|
41
|
-
uri = ::File.join("https://#{Atlantis::Portal::HOST}", uri) unless /^https?/ === uri
|
42
|
-
|
43
|
-
3.times do
|
44
|
-
super(uri, parameters, referer, headers)
|
45
|
-
|
46
|
-
#puts page.body
|
47
|
-
|
48
|
-
return page unless page.respond_to?(:title)
|
49
|
-
|
50
|
-
unless page.parser.at_css('#id_username').nil?
|
51
|
-
#puts "Logging in"
|
52
|
-
login! and next
|
53
|
-
else
|
54
|
-
if !@team.nil? && !@team.empty?
|
55
|
-
select_team! and next
|
56
|
-
else
|
57
|
-
return page
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
raise UnsuccessfulAuthenticationError
|
63
|
-
end
|
64
|
-
|
65
|
-
def list_people(distribution_list)
|
66
|
-
|
67
|
-
if (distribution_list == 'all')
|
68
|
-
get('https://testflightapp.com/dashboard/team')
|
69
|
-
else
|
70
|
-
lists = list_lists
|
71
|
-
|
72
|
-
list = lists.find{|p| p.name == distribution_list}
|
73
|
-
|
74
|
-
say_warning "No #{distribution_list} distribution list found." and abort if list.nil?
|
75
|
-
|
76
|
-
get('https://testflightapp.com/dashboard/team/list/' + list.id)
|
77
|
-
end
|
78
|
-
|
79
|
-
people = []
|
80
|
-
|
81
|
-
page.parser.css("table#member-table tr").each do |row|
|
82
|
-
cols = row.css('td')
|
83
|
-
|
84
|
-
#puts cols.length
|
85
|
-
|
86
|
-
if (cols.length > 0)
|
87
|
-
person = Person.new
|
88
|
-
|
89
|
-
person.id = cols[0].css("input")[0]['value'];
|
90
|
-
person.name = cols[1].text.strip.split.map(&:capitalize).join(' ')
|
91
|
-
person.email = cols[2].text.strip
|
92
|
-
person.devices = cols[3].text.strip
|
93
|
-
|
94
|
-
people << person
|
95
|
-
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
people
|
100
|
-
end
|
101
|
-
|
102
|
-
def list_devices(distribution_list)
|
103
|
-
people = list_people(distribution_list)
|
104
|
-
|
105
|
-
people_list = []
|
106
|
-
|
107
|
-
people.each do |person|
|
108
|
-
people_list << person.id
|
109
|
-
end
|
110
|
-
|
111
|
-
post('https://testflightapp.com/dashboard/team/export/devices/', { "members" => people_list.join('|'), "csrfmiddlewaretoken" => page.parser.css("[name='csrfmiddlewaretoken']")[0]['value'] } )
|
112
|
-
|
113
|
-
device_list = page.body.split( /\r?\n/ )
|
114
|
-
|
115
|
-
# Remove first one
|
116
|
-
device_list.shift
|
117
|
-
|
118
|
-
devices = []
|
119
|
-
|
120
|
-
device_list.each do |dev|
|
121
|
-
#puts dev
|
122
|
-
|
123
|
-
device = Device.new
|
124
|
-
device.udid = dev.split(/\t/)[0]
|
125
|
-
device.name = dev.split(/\t/)[1]
|
126
|
-
|
127
|
-
devices << device
|
128
|
-
end
|
129
|
-
|
130
|
-
devices
|
131
28
|
end
|
132
|
-
|
133
|
-
def list_lists
|
134
|
-
get('https://testflightapp.com/dashboard/team')
|
135
|
-
|
136
|
-
lists = []
|
137
|
-
|
138
|
-
page.parser.css("nav.vert-nav li a").each do |row|
|
139
|
-
|
140
|
-
unless (row['href'].nil?)
|
141
|
-
url = row['href'].split('/')
|
142
|
-
|
143
|
-
#puts url.length
|
144
|
-
|
145
|
-
if (url.length >= 5) && (url[4].is_i?)
|
146
|
-
#puts url[3]
|
147
|
-
|
148
|
-
list = DistributionList.new
|
149
|
-
list.id = url[4]
|
150
|
-
list.name = row.css('> text()').text.strip
|
151
|
-
list.count = row.css('span').text.strip
|
152
|
-
|
153
|
-
lists << list
|
154
|
-
end
|
155
|
-
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
lists
|
160
|
-
end
|
161
|
-
|
162
|
-
def list_teams
|
163
|
-
teams = []
|
164
|
-
|
165
|
-
# Selected team
|
166
|
-
|
167
|
-
selected_team = page.parser.css('ul.dropdown-menu li div div.textcontain180 text()')
|
168
|
-
|
169
|
-
team = Team.new
|
170
|
-
team.name = selected_team
|
171
|
-
|
172
|
-
teams << team
|
173
|
-
|
174
|
-
all_teams = page.parser.css('ul.ddteamlist li a')
|
175
|
-
all_teams.each do |row|
|
176
|
-
team = Team.new
|
177
|
-
team.id = row['data-team-id']
|
178
|
-
team.name = row.text
|
179
|
-
|
180
|
-
teams << team
|
181
|
-
end
|
182
|
-
|
183
|
-
teams
|
184
|
-
end
|
185
|
-
|
186
|
-
private
|
187
|
-
|
188
|
-
def login!
|
189
|
-
if form = page.forms.first
|
190
|
-
#puts "LOGGING IN" + self.username
|
191
|
-
|
192
|
-
form.fields_with(type: 'email').first.value = self.username
|
193
|
-
form.fields_with(type: 'password').first.value = self.password
|
194
|
-
|
195
|
-
form.submit
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
def select_team!
|
200
|
-
teams = list_teams
|
201
|
-
|
202
|
-
#puts teams
|
203
|
-
|
204
|
-
# Check if desired team is different to selected team
|
205
|
-
|
206
|
-
if @team != teams[0].name
|
207
|
-
#puts "Selecting team..."
|
208
|
-
|
209
|
-
if form = page.form_with(:id => 'team-change')
|
210
|
-
|
211
|
-
team = teams.find{|t| t.name == @team}
|
212
|
-
|
213
|
-
#puts team
|
214
|
-
|
215
|
-
unless (team.nil?)
|
216
|
-
form.team = team.id
|
217
|
-
form.submit
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
@team = ''
|
223
|
-
end
|
224
|
-
|
225
29
|
end
|
226
30
|
end
|
227
31
|
end
|
@@ -3,15 +3,17 @@ include Atlantis::Portal
|
|
3
3
|
require 'atlantis/portal/helpers'
|
4
4
|
include Atlantis::Portal::Helpers
|
5
5
|
|
6
|
-
global_option('-u', '--username USER', 'Username') { |arg|
|
7
|
-
global_option('-p', '--password PASSWORD', 'Password') { |arg|
|
8
|
-
global_option('--team TEAM', 'Team') { |arg|
|
6
|
+
global_option('-u', '--username USER', 'Username') { |arg| arguments.username = arg unless arg.nil? }
|
7
|
+
global_option('-p', '--password PASSWORD', 'Password') { |arg| arguments.password = arg unless arg.nil? }
|
8
|
+
global_option('--team TEAM', 'Team or Organisation') { |arg| arguments.team = arg if arg }
|
9
9
|
global_option('--info', 'Set log level to INFO') { agent.log.level = Logger::INFO }
|
10
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|
|
11
|
+
global_option('--format [table|csv|json]', 'Set output format (default: table)') { |arg| arguments.format = arg if arg }
|
12
|
+
global_option('--group GROUP', 'Set group or distribution list') { |arg| arguments.group = arg if arg }
|
13
|
+
global_option('--service [crashlytics/testflight]', 'Set service (default: testflight)') { |arg| arguments.service = arg if arg }
|
12
14
|
|
13
15
|
require 'atlantis/portal/commands/login'
|
14
16
|
require 'atlantis/portal/commands/logout'
|
15
17
|
require 'atlantis/portal/commands/people'
|
16
18
|
require 'atlantis/portal/commands/devices'
|
17
|
-
require 'atlantis/portal/commands/
|
19
|
+
require 'atlantis/portal/commands/groups'
|
@@ -1,43 +1,12 @@
|
|
1
1
|
command :'devices' do |c|
|
2
|
-
c.syntax = '
|
3
|
-
c.summary = 'Lists devices on
|
2
|
+
c.syntax = 'distribution devices'
|
3
|
+
c.summary = 'Lists devices registered on specified service'
|
4
4
|
c.description = ''
|
5
5
|
|
6
6
|
c.action do |args, options|
|
7
|
-
distribution_list = args.join(" ").strip
|
8
7
|
|
9
|
-
|
10
|
-
distribution_list = 'all'
|
11
|
-
end
|
12
|
-
devices = try{agent.list_devices(distribution_list)}
|
8
|
+
devices = try{service.list_devices(arguments.group)}
|
13
9
|
|
14
|
-
|
15
|
-
|
16
|
-
if (agent.format == "csv")
|
17
|
-
csv_string = CSV.generate do |csv|
|
18
|
-
csv << ["Name", "UDID"]
|
19
|
-
|
20
|
-
devices.each do |device|
|
21
|
-
csv << [device.name, device.udid]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
puts csv_string
|
26
|
-
else
|
27
|
-
|
28
|
-
title = "Listing devices"
|
29
|
-
|
30
|
-
table = Terminal::Table.new :title => title do |t|
|
31
|
-
t << ["Name", "UDID"]
|
32
|
-
t.add_separator
|
33
|
-
devices.each do |device|
|
34
|
-
t << [device.name, device.udid]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
#table.align_column 2, :center
|
39
|
-
|
40
|
-
puts table
|
41
|
-
end
|
10
|
+
output('Devices', ["Name", "Identifier"], devices)
|
42
11
|
end
|
43
12
|
end
|
@@ -1,15 +1,15 @@
|
|
1
1
|
command :login do |c|
|
2
|
-
c.syntax = '
|
3
|
-
c.summary = 'Save account credentials'
|
2
|
+
c.syntax = 'distribution login'
|
3
|
+
c.summary = 'Save account credentials for specific service'
|
4
4
|
c.description = ''
|
5
5
|
|
6
6
|
c.action do |args, options|
|
7
|
-
say_warning "You are already authenticated" if Security::InternetPassword.find(:server =>
|
7
|
+
say_warning "You are already authenticated" if Security::InternetPassword.find(:server => service.host)
|
8
8
|
|
9
9
|
user = ask "Username:"
|
10
10
|
pass = password "Password:"
|
11
11
|
|
12
|
-
Security::InternetPassword.add(
|
12
|
+
Security::InternetPassword.add(service.host, user, pass)
|
13
13
|
|
14
14
|
say_ok "Account credentials saved"
|
15
15
|
end
|