mysigner 0.1.2 → 0.1.4
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/.githooks/pre-commit +15 -0
- data/.githooks/pre-push +21 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +126 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +38 -8
- data/README.md +14 -16
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/bin/setup +3 -0
- data/certificate_.cer +0 -0
- data/exe/mysigner +19 -2
- data/iOS_App_Store_Profile.mobileprovision +1 -0
- data/iOS_Distribution_Certificate.cer +1 -0
- data/lib/mysigner/build/android_executor.rb +83 -63
- data/lib/mysigner/build/android_parser.rb +33 -40
- data/lib/mysigner/build/configurator.rb +17 -16
- data/lib/mysigner/build/detector.rb +39 -50
- data/lib/mysigner/build/error_analyzer.rb +70 -68
- data/lib/mysigner/build/executor.rb +30 -37
- data/lib/mysigner/build/parser.rb +18 -18
- data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
- data/lib/mysigner/cli/auth_commands.rb +771 -764
- data/lib/mysigner/cli/build_commands.rb +962 -796
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
- data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
- data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
- data/lib/mysigner/cli/concerns/helpers.rb +44 -1
- data/lib/mysigner/cli/diagnostic_commands.rb +667 -636
- data/lib/mysigner/cli/resource_commands.rb +1153 -985
- data/lib/mysigner/cli/validate_commands.rb +25 -25
- data/lib/mysigner/cli.rb +11 -1
- data/lib/mysigner/client.rb +27 -19
- data/lib/mysigner/config.rb +161 -60
- data/lib/mysigner/export/exporter.rb +38 -37
- data/lib/mysigner/signing/certificate_checker.rb +18 -23
- data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
- data/lib/mysigner/signing/keystore_manager.rb +81 -61
- data/lib/mysigner/signing/validator.rb +38 -40
- data/lib/mysigner/signing/wizard.rb +329 -342
- data/lib/mysigner/upload/app_store_automation.rb +96 -49
- data/lib/mysigner/upload/app_store_submission.rb +87 -92
- data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
- data/lib/mysigner/upload/play_store_uploader.rb +164 -144
- data/lib/mysigner/upload/uploader.rb +136 -115
- data/lib/mysigner/version.rb +3 -1
- data/lib/mysigner.rb +13 -11
- data/mysigner.gemspec +36 -33
- data/profile_.mobileprovision +0 -0
- data/test_manual.rb +37 -36
- metadata +44 -17
- data/.DS_Store +0 -0
|
@@ -1,28 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mysigner
|
|
2
4
|
class CLI < Thor
|
|
3
5
|
module AuthCommands
|
|
4
6
|
def self.included(base)
|
|
5
7
|
base.class_eval do
|
|
6
|
-
desc
|
|
8
|
+
desc 'version', 'Show version information'
|
|
7
9
|
def version
|
|
8
10
|
say "My Signer CLI v#{Mysigner::VERSION}", :cyan
|
|
9
|
-
say
|
|
11
|
+
say ''
|
|
10
12
|
say "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})", :white
|
|
11
|
-
say "Install: #{File.expand_path('
|
|
13
|
+
say "Install: #{File.expand_path('../../..', __dir__)}", :white
|
|
12
14
|
say "Config: #{Config::CONFIG_FILE}", :white
|
|
13
|
-
say
|
|
14
|
-
say
|
|
15
|
-
say
|
|
15
|
+
say ''
|
|
16
|
+
say 'Repository: https://github.com/mysigner-dev/mysigner-cli', :white
|
|
17
|
+
say 'Issues: https://github.com/mysigner-dev/mysigner-cli/issues', :white
|
|
18
|
+
say ''
|
|
19
|
+
say 'Docs: https://mysigner.dev/docs/commands', :white
|
|
20
|
+
say 'Support: https://mysigner.dev/landing#contact', :white
|
|
16
21
|
end
|
|
17
22
|
|
|
18
|
-
desc
|
|
23
|
+
desc 'login', "Log in with existing API token (⭐ first-timers: use 'onboard' instead)"
|
|
19
24
|
long_desc <<~DESC
|
|
20
25
|
Authenticate with My Signer API using an API token.
|
|
21
|
-
|
|
26
|
+
|
|
22
27
|
New user? Run 'mysigner onboard' for step-by-step guidance.
|
|
23
|
-
|
|
28
|
+
|
|
24
29
|
Your credentials will be stored securely in ~/.mysigner/config.yml
|
|
25
|
-
|
|
30
|
+
|
|
26
31
|
Note: API tokens are organization-specific. This token will only
|
|
27
32
|
grant access to the organization it was created in.
|
|
28
33
|
DESC
|
|
@@ -31,86 +36,86 @@ module Mysigner
|
|
|
31
36
|
config = Config.new
|
|
32
37
|
if config.exists?
|
|
33
38
|
config.load
|
|
34
|
-
say
|
|
35
|
-
say
|
|
36
|
-
say
|
|
39
|
+
say '⚠️ Already logged in', :yellow
|
|
40
|
+
say ''
|
|
41
|
+
say 'Current configuration:', :yellow
|
|
37
42
|
say " User: #{config.user_email || '(unknown)'}"
|
|
38
43
|
say " Organization: #{config.org_name || '(unknown)'} (ID: #{config.current_organization_id})"
|
|
39
44
|
say " API URL: #{config.api_url}"
|
|
40
|
-
say
|
|
41
|
-
|
|
42
|
-
if yes?(
|
|
45
|
+
say ''
|
|
46
|
+
|
|
47
|
+
if yes?('Do you want to logout and login with different user? (y/n)')
|
|
43
48
|
config.clear
|
|
44
|
-
say
|
|
45
|
-
say
|
|
49
|
+
say '✓ Logged out successfully', :green
|
|
50
|
+
say ''
|
|
46
51
|
else
|
|
47
52
|
say "Login cancelled. Use 'mysigner logout' to logout first.", :yellow
|
|
48
|
-
say
|
|
53
|
+
say ''
|
|
49
54
|
say "💡 Tip: Use 'mysigner switch' to switch organizations for the same user", :yellow
|
|
50
55
|
return
|
|
51
56
|
end
|
|
52
57
|
end
|
|
53
58
|
|
|
54
|
-
say
|
|
55
|
-
say
|
|
56
|
-
say
|
|
59
|
+
say '🔐 My Signer Login', :cyan
|
|
60
|
+
say '=' * 80, :cyan
|
|
61
|
+
say ''
|
|
57
62
|
|
|
58
63
|
# Get API URL with smart default
|
|
59
64
|
api_url = prompt_api_url
|
|
60
|
-
say
|
|
61
|
-
|
|
65
|
+
say ''
|
|
66
|
+
|
|
62
67
|
# Get user email
|
|
63
68
|
user_email = prompt_for_email
|
|
64
|
-
say
|
|
65
|
-
|
|
69
|
+
say ''
|
|
70
|
+
|
|
66
71
|
# Show guidance for getting token
|
|
67
72
|
show_token_guidance(api_url)
|
|
68
|
-
|
|
69
|
-
api_token = ask(
|
|
70
|
-
say
|
|
71
|
-
|
|
73
|
+
|
|
74
|
+
api_token = ask('API Token:', echo: false)
|
|
75
|
+
say '' # New line after hidden input
|
|
76
|
+
|
|
72
77
|
if api_token.empty?
|
|
73
|
-
error
|
|
74
|
-
say
|
|
78
|
+
error 'API token cannot be empty'
|
|
79
|
+
say ''
|
|
75
80
|
say "💡 Tip: Run 'mysigner onboard' for detailed guidance", :yellow
|
|
76
81
|
exit 1
|
|
77
82
|
end
|
|
78
83
|
|
|
79
|
-
say
|
|
80
|
-
|
|
84
|
+
say 'Validating token and email...', :yellow
|
|
85
|
+
|
|
81
86
|
begin
|
|
82
87
|
client = Client.new(api_url: api_url, api_token: api_token, user_email: user_email)
|
|
83
88
|
response = client.test_connection
|
|
84
|
-
|
|
89
|
+
|
|
85
90
|
if response[:success]
|
|
86
|
-
say
|
|
91
|
+
say '✓ Token valid', :green
|
|
87
92
|
else
|
|
88
|
-
error
|
|
93
|
+
error 'Connection failed'
|
|
89
94
|
handle_connection_failure(api_url)
|
|
90
95
|
exit 1
|
|
91
96
|
end
|
|
92
|
-
|
|
97
|
+
|
|
93
98
|
# Fetch organization info (token can only access its own org)
|
|
94
|
-
say
|
|
95
|
-
|
|
99
|
+
say 'Detecting organization...', :yellow
|
|
100
|
+
|
|
96
101
|
# Try to fetch organizations - with org-specific tokens, this will return only the token's org
|
|
97
102
|
orgs_response = client.get('/api/v1/organizations')
|
|
98
103
|
organizations = orgs_response[:data]['organizations']
|
|
99
|
-
|
|
104
|
+
|
|
100
105
|
if ENV['DEBUG']
|
|
101
106
|
say "DEBUG: Found #{organizations.length} organizations", :cyan
|
|
102
107
|
organizations.each do |org|
|
|
103
108
|
say "DEBUG: - #{org['name']} (ID: #{org['id']})", :cyan
|
|
104
109
|
end
|
|
105
110
|
end
|
|
106
|
-
|
|
111
|
+
|
|
107
112
|
if organizations.empty?
|
|
108
|
-
error
|
|
109
|
-
say
|
|
110
|
-
say
|
|
113
|
+
error 'No organizations found for this token'
|
|
114
|
+
say ''
|
|
115
|
+
say 'This might mean:', :yellow
|
|
111
116
|
say " • Your token doesn't have access to any organizations", :yellow
|
|
112
|
-
say
|
|
113
|
-
say
|
|
117
|
+
say ' • The token was created but the organization was deleted', :yellow
|
|
118
|
+
say ''
|
|
114
119
|
show_create_org_guidance(api_url)
|
|
115
120
|
exit 1
|
|
116
121
|
end
|
|
@@ -118,19 +123,19 @@ module Mysigner
|
|
|
118
123
|
# With org-specific tokens, there should only be one organization
|
|
119
124
|
selected_org = organizations.first
|
|
120
125
|
org_id = selected_org['id']
|
|
121
|
-
|
|
126
|
+
|
|
122
127
|
say "DEBUG: Fetching details for organization #{org_id}...", :cyan if ENV['DEBUG']
|
|
123
|
-
|
|
128
|
+
|
|
124
129
|
# Get detailed org info to extract user email and token_organization_id
|
|
125
130
|
org_response = client.get("/api/v1/organizations/#{org_id}")
|
|
126
131
|
org_data = org_response[:data]
|
|
127
|
-
|
|
128
|
-
say
|
|
129
|
-
|
|
132
|
+
|
|
133
|
+
say 'DEBUG: Organization data received', :cyan if ENV['DEBUG']
|
|
134
|
+
|
|
130
135
|
say "✓ Organization detected: #{org_data['name']}", :green
|
|
131
136
|
say "✓ Email validated: #{user_email}", :green
|
|
132
|
-
say
|
|
133
|
-
|
|
137
|
+
say ''
|
|
138
|
+
|
|
134
139
|
# Save configuration with multi-token support
|
|
135
140
|
config = Config.new
|
|
136
141
|
config.api_url = api_url
|
|
@@ -139,49 +144,48 @@ module Mysigner
|
|
|
139
144
|
config.save_token_for_org(org_id, org_data['name'], api_token)
|
|
140
145
|
config.save
|
|
141
146
|
|
|
142
|
-
say
|
|
143
|
-
say
|
|
144
|
-
say
|
|
145
|
-
say
|
|
146
|
-
say
|
|
147
|
+
say ''
|
|
148
|
+
say '=' * 80, :green
|
|
149
|
+
say '✓ Successfully logged in!', :green
|
|
150
|
+
say '=' * 80, :green
|
|
151
|
+
say ''
|
|
147
152
|
say "Organization: #{org_data['name']} (ID: #{org_id})", :cyan
|
|
148
153
|
say "Role: #{org_data['role'] || 'viewer'}", :cyan
|
|
149
154
|
say "Config saved to: #{Config::CONFIG_FILE}", :cyan
|
|
150
|
-
say
|
|
151
|
-
say
|
|
152
|
-
say
|
|
155
|
+
say ''
|
|
156
|
+
say '🔒 Security Note:', :yellow
|
|
157
|
+
say ' Your token is organization-specific and can only access', :yellow
|
|
153
158
|
say " #{org_data['name']}. To access other organizations,", :yellow
|
|
154
159
|
say " use 'mysigner switch' to add tokens for those organizations.", :yellow
|
|
155
|
-
say
|
|
156
|
-
say
|
|
157
|
-
say
|
|
158
|
-
say
|
|
159
|
-
say
|
|
160
|
-
say
|
|
161
|
-
say
|
|
162
|
-
say
|
|
163
|
-
say
|
|
164
|
-
say
|
|
165
|
-
|
|
160
|
+
say ''
|
|
161
|
+
say '🚀 Next steps:', :bold
|
|
162
|
+
say ' cd your-ios-project'
|
|
163
|
+
say ' mysigner ship testflight'
|
|
164
|
+
say ''
|
|
165
|
+
say '💡 Helpful commands:', :cyan
|
|
166
|
+
say ' • mysigner doctor - Check your environment'
|
|
167
|
+
say ' • mysigner orgs - List all organizations'
|
|
168
|
+
say ' • mysigner switch - Switch to another organization'
|
|
169
|
+
say ''
|
|
166
170
|
rescue Mysigner::UnauthorizedError => e
|
|
167
|
-
error
|
|
168
|
-
say
|
|
169
|
-
|
|
171
|
+
error 'Authentication failed'
|
|
172
|
+
say ''
|
|
173
|
+
|
|
170
174
|
# Check if it's an email validation error
|
|
171
|
-
if e.message.include?("doesn't belong to") || e.message.include?(
|
|
172
|
-
say
|
|
173
|
-
say
|
|
175
|
+
if e.message.include?("doesn't belong to") || e.message.include?('use your own token')
|
|
176
|
+
say '⚠️ Token Email Mismatch', :yellow
|
|
177
|
+
say ''
|
|
174
178
|
say "The token you provided doesn't belong to #{user_email}.", :yellow
|
|
175
|
-
say
|
|
176
|
-
say
|
|
179
|
+
say ''
|
|
180
|
+
say 'This could mean:', :yellow
|
|
177
181
|
say " • You're using a token created by someone else", :yellow
|
|
178
182
|
say " • You're using a token from a different account", :yellow
|
|
179
|
-
say
|
|
180
|
-
say
|
|
181
|
-
say
|
|
183
|
+
say ''
|
|
184
|
+
say '💡 Solutions:', :cyan
|
|
185
|
+
say ' 1. Generate a new token from your own account at:', :cyan
|
|
182
186
|
say " #{api_url}", :cyan
|
|
183
187
|
say " 2. Make sure you're logged in as #{user_email} on the web", :cyan
|
|
184
|
-
say
|
|
188
|
+
say ' 3. Check that you entered the correct email address', :cyan
|
|
185
189
|
else
|
|
186
190
|
handle_unauthorized_error(api_url)
|
|
187
191
|
end
|
|
@@ -189,16 +193,16 @@ module Mysigner
|
|
|
189
193
|
rescue Mysigner::ConnectionError => e
|
|
190
194
|
handle_connection_error(e, api_url)
|
|
191
195
|
exit 1
|
|
192
|
-
rescue => e
|
|
196
|
+
rescue StandardError => e
|
|
193
197
|
handle_unexpected_error(e, api_url)
|
|
194
198
|
exit 1
|
|
195
199
|
end
|
|
196
200
|
end
|
|
197
201
|
|
|
198
|
-
desc
|
|
202
|
+
desc 'onboard', '⭐ START HERE - Complete setup wizard for new users'
|
|
199
203
|
long_desc <<~DESC
|
|
200
204
|
Step-by-step guide to get started with My Signer CLI.
|
|
201
|
-
|
|
205
|
+
|
|
202
206
|
This command will:
|
|
203
207
|
1. Check if you have an account
|
|
204
208
|
2. Guide you through creating an organization
|
|
@@ -206,273 +210,271 @@ module Mysigner
|
|
|
206
210
|
4. Configure your CLI
|
|
207
211
|
DESC
|
|
208
212
|
def onboard
|
|
209
|
-
say
|
|
210
|
-
say
|
|
211
|
-
say
|
|
213
|
+
say '🚀 My Signer Setup Guide', :cyan
|
|
214
|
+
say '=' * 80, :cyan
|
|
215
|
+
say ''
|
|
212
216
|
say "Welcome! Let's get you set up with My Signer.", :bold
|
|
213
|
-
say
|
|
217
|
+
say ''
|
|
214
218
|
|
|
215
219
|
# Check if already configured
|
|
216
220
|
config = Config.new
|
|
217
221
|
if config.exists? && config.api_token && config.current_organization_id
|
|
218
222
|
say "✓ You're already logged in!", :green
|
|
219
|
-
say
|
|
220
|
-
say
|
|
223
|
+
say ''
|
|
224
|
+
say 'Current configuration:', :cyan
|
|
221
225
|
say " Email: #{config.user_email}"
|
|
222
226
|
say " Organization ID: #{config.current_organization_id}"
|
|
223
227
|
say " API URL: #{config.api_url}"
|
|
224
|
-
say
|
|
225
|
-
|
|
228
|
+
say ''
|
|
229
|
+
|
|
226
230
|
# Check App Store Connect status
|
|
227
231
|
begin
|
|
228
232
|
client = Client.new(api_url: config.api_url, api_token: config.api_token, user_email: config.user_email)
|
|
229
233
|
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
230
234
|
org_data = org_response[:data]
|
|
231
|
-
|
|
235
|
+
|
|
232
236
|
asc_configured = org_data['app_store_connect_configured'] || false
|
|
233
|
-
|
|
234
|
-
if
|
|
235
|
-
#
|
|
236
|
-
say
|
|
237
|
-
say ""
|
|
238
|
-
say
|
|
239
|
-
say "
|
|
240
|
-
say
|
|
241
|
-
say
|
|
242
|
-
say
|
|
243
|
-
say
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
say
|
|
247
|
-
|
|
237
|
+
|
|
238
|
+
if asc_configured
|
|
239
|
+
# Already fully configured
|
|
240
|
+
say '✓ App Store Connect: Configured', :green
|
|
241
|
+
say " Team ID: #{org_data['app_store_connect_team_id']}" if org_data['app_store_connect_team_id']
|
|
242
|
+
say ''
|
|
243
|
+
say "You're all set! 🎉", :bold
|
|
244
|
+
say ''
|
|
245
|
+
say 'What would you like to do?', :bold
|
|
246
|
+
say ' 1. Check status'
|
|
247
|
+
say ' 2. Switch to another organization'
|
|
248
|
+
say ' 3. Log out and start fresh'
|
|
249
|
+
say ' 4. Exit'
|
|
250
|
+
say ''
|
|
251
|
+
|
|
252
|
+
choice = ask('Select (1-4):', limited_to: %w[1 2 3 4])
|
|
253
|
+
say ''
|
|
254
|
+
|
|
248
255
|
case choice
|
|
249
256
|
when '1'
|
|
250
|
-
|
|
251
|
-
say "🚀 Setting up App Store Connect credentials...", :cyan
|
|
252
|
-
say ""
|
|
253
|
-
asc_configured = setup_app_store_connect_credentials(client, config, config.current_organization_id)
|
|
254
|
-
|
|
255
|
-
say ""
|
|
256
|
-
say "=" * 80, :green
|
|
257
|
-
if asc_configured
|
|
258
|
-
say "✓ App Store Connect configured successfully!", :green
|
|
259
|
-
else
|
|
260
|
-
say "⚠️ Setup incomplete", :yellow
|
|
261
|
-
say "Run 'mysigner onboard' again or use 'mysigner doctor'", :yellow
|
|
262
|
-
end
|
|
263
|
-
say "=" * 80, :green
|
|
257
|
+
invoke :status
|
|
264
258
|
return
|
|
265
259
|
when '2'
|
|
266
|
-
invoke :
|
|
260
|
+
invoke :switch
|
|
267
261
|
return
|
|
268
262
|
when '3'
|
|
269
|
-
say
|
|
270
|
-
say
|
|
263
|
+
say 'Clearing configuration...', :yellow
|
|
264
|
+
say ''
|
|
271
265
|
# Continue with full onboarding
|
|
272
266
|
when '4'
|
|
273
|
-
say
|
|
267
|
+
say 'No changes made.', :green
|
|
274
268
|
return
|
|
275
269
|
end
|
|
276
270
|
else
|
|
277
|
-
#
|
|
278
|
-
say
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
say ""
|
|
283
|
-
say
|
|
284
|
-
say
|
|
285
|
-
say
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
say
|
|
289
|
-
|
|
290
|
-
say ""
|
|
291
|
-
|
|
292
|
-
choice = ask("Select (1-4):", limited_to: ['1', '2', '3', '4'])
|
|
293
|
-
say ""
|
|
294
|
-
|
|
271
|
+
# Missing ASC credentials - offer to add them
|
|
272
|
+
say '⚠️ App Store Connect: Not configured', :yellow
|
|
273
|
+
say ''
|
|
274
|
+
say 'What would you like to do?', :bold
|
|
275
|
+
say ' 1. Set up App Store Connect credentials now'
|
|
276
|
+
say " 2. Check status with 'mysigner status'"
|
|
277
|
+
say ' 3. Log out and start fresh'
|
|
278
|
+
say ' 4. Exit'
|
|
279
|
+
say ''
|
|
280
|
+
|
|
281
|
+
choice = ask('Select (1-4):', limited_to: %w[1 2 3 4])
|
|
282
|
+
say ''
|
|
283
|
+
|
|
295
284
|
case choice
|
|
296
285
|
when '1'
|
|
297
|
-
|
|
286
|
+
# Go directly to ASC setup
|
|
287
|
+
say '🚀 Setting up App Store Connect credentials...', :cyan
|
|
288
|
+
say ''
|
|
289
|
+
asc_configured = setup_app_store_connect_credentials(client, config, config.current_organization_id)
|
|
290
|
+
|
|
291
|
+
say ''
|
|
292
|
+
say '=' * 80, :green
|
|
293
|
+
if asc_configured
|
|
294
|
+
say '✓ App Store Connect configured successfully!', :green
|
|
295
|
+
else
|
|
296
|
+
say '⚠️ Setup incomplete', :yellow
|
|
297
|
+
say "Run 'mysigner onboard' again or use 'mysigner doctor'", :yellow
|
|
298
|
+
end
|
|
299
|
+
say '=' * 80, :green
|
|
298
300
|
return
|
|
299
301
|
when '2'
|
|
300
|
-
invoke :
|
|
302
|
+
invoke :status
|
|
301
303
|
return
|
|
302
304
|
when '3'
|
|
303
|
-
say
|
|
304
|
-
say
|
|
305
|
+
say 'Clearing configuration...', :yellow
|
|
306
|
+
say ''
|
|
305
307
|
# Continue with full onboarding
|
|
306
308
|
when '4'
|
|
307
|
-
say
|
|
309
|
+
say 'No changes made.', :green
|
|
308
310
|
return
|
|
309
311
|
end
|
|
310
312
|
end
|
|
311
|
-
rescue => e
|
|
313
|
+
rescue StandardError => e
|
|
312
314
|
say "⚠️ Could not check organization status: #{e.message}", :yellow
|
|
313
|
-
say
|
|
314
|
-
|
|
315
|
-
unless yes_with_default?(
|
|
316
|
-
say
|
|
317
|
-
say
|
|
318
|
-
say
|
|
315
|
+
say ''
|
|
316
|
+
|
|
317
|
+
unless yes_with_default?('Do you want to re-configure from scratch?', :yellow)
|
|
318
|
+
say ''
|
|
319
|
+
say 'Keeping existing configuration.', :green
|
|
320
|
+
say ''
|
|
319
321
|
say "💡 Tip: Use 'mysigner status' to check your setup", :cyan
|
|
320
322
|
say "💡 Tip: Use 'mysigner switch' to add another organization", :cyan
|
|
321
323
|
return
|
|
322
324
|
end
|
|
323
|
-
|
|
324
|
-
say
|
|
325
|
-
say
|
|
326
|
-
say
|
|
325
|
+
|
|
326
|
+
say ''
|
|
327
|
+
say 'Clearing existing configuration...', :yellow
|
|
328
|
+
say ''
|
|
327
329
|
end
|
|
328
330
|
end
|
|
329
331
|
|
|
330
332
|
# Get API URL
|
|
331
333
|
api_url = prompt_api_url
|
|
332
|
-
say
|
|
334
|
+
say ''
|
|
333
335
|
|
|
334
336
|
# Step 1: Check if user has account
|
|
335
|
-
say
|
|
336
|
-
say
|
|
337
|
-
say
|
|
338
|
-
say
|
|
339
|
-
say
|
|
340
|
-
say
|
|
341
|
-
say
|
|
342
|
-
|
|
343
|
-
choice = ask(
|
|
344
|
-
say
|
|
345
|
-
|
|
337
|
+
say 'Step 1: Account Setup', :cyan
|
|
338
|
+
say '-' * 80
|
|
339
|
+
say ''
|
|
340
|
+
say 'Do you have a My Signer account?', :bold
|
|
341
|
+
say ' 1. Yes, I have an account'
|
|
342
|
+
say ' 2. No, I need to sign up'
|
|
343
|
+
say ''
|
|
344
|
+
|
|
345
|
+
choice = ask('Select (1-2):', limited_to: %w[1 2])
|
|
346
|
+
say ''
|
|
347
|
+
|
|
346
348
|
if choice == '2'
|
|
347
349
|
# Guide to signup
|
|
348
350
|
say "📝 Let's create your account:", :cyan
|
|
349
|
-
say
|
|
350
|
-
say
|
|
351
|
+
say ''
|
|
352
|
+
say '1. Open your browser and go to:', :bold
|
|
351
353
|
say " #{api_url}", :green
|
|
352
|
-
say
|
|
354
|
+
say ''
|
|
353
355
|
say "2. Click 'Sign Up' and create your account", :bold
|
|
354
|
-
say
|
|
355
|
-
say
|
|
356
|
-
say
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
say
|
|
356
|
+
say ''
|
|
357
|
+
say '3. Verify your email (check your inbox)', :bold
|
|
358
|
+
say ''
|
|
359
|
+
|
|
360
|
+
unless yes_with_default?('Have you created your account?', :green)
|
|
361
|
+
say ''
|
|
362
|
+
say "Come back and run 'mysigner onboard' when you're ready!", :yellow
|
|
363
|
+
return
|
|
364
|
+
end
|
|
365
|
+
say ''
|
|
364
366
|
end
|
|
365
367
|
|
|
366
368
|
# Step 2: Organization
|
|
367
|
-
say
|
|
368
|
-
say
|
|
369
|
-
say
|
|
370
|
-
say
|
|
371
|
-
say
|
|
372
|
-
say
|
|
373
|
-
say
|
|
374
|
-
|
|
375
|
-
choice = ask(
|
|
376
|
-
say
|
|
377
|
-
|
|
369
|
+
say 'Step 2: Organization Setup', :cyan
|
|
370
|
+
say '-' * 80
|
|
371
|
+
say ''
|
|
372
|
+
say 'Do you have an organization?', :bold
|
|
373
|
+
say ' 1. Yes, I have an organization'
|
|
374
|
+
say ' 2. No, I need to create one'
|
|
375
|
+
say ''
|
|
376
|
+
|
|
377
|
+
choice = ask('Select (1-2):', limited_to: %w[1 2])
|
|
378
|
+
say ''
|
|
379
|
+
|
|
378
380
|
if choice == '2'
|
|
379
381
|
# Guide to create org
|
|
380
382
|
say "🏢 Let's create your organization:", :cyan
|
|
381
|
-
say
|
|
382
|
-
say
|
|
383
|
+
say ''
|
|
384
|
+
say '1. Go to the dashboard:', :bold
|
|
383
385
|
say " #{api_url}", :green
|
|
384
|
-
say
|
|
385
|
-
say
|
|
386
|
-
say
|
|
386
|
+
say ''
|
|
387
|
+
say '2. Sign in with your account', :bold
|
|
388
|
+
say ''
|
|
387
389
|
say "3. Click 'Create Organization'", :bold
|
|
388
|
-
say
|
|
390
|
+
say ''
|
|
389
391
|
say "4. Enter your organization name (e.g., 'My Startup')", :bold
|
|
390
|
-
say
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
say
|
|
392
|
+
say ''
|
|
393
|
+
|
|
394
|
+
unless yes_with_default?('Have you created your organization?', :green)
|
|
395
|
+
say ''
|
|
396
|
+
say "Come back and run 'mysigner onboard' when you're ready!", :yellow
|
|
397
|
+
return
|
|
398
|
+
end
|
|
399
|
+
say ''
|
|
398
400
|
end
|
|
399
401
|
|
|
400
402
|
# Step 3: API Token
|
|
401
|
-
say
|
|
402
|
-
say
|
|
403
|
-
say
|
|
403
|
+
say 'Step 3: Generate API Token', :cyan
|
|
404
|
+
say '-' * 80
|
|
405
|
+
say ''
|
|
404
406
|
say "Now let's generate your API token:", :bold
|
|
405
|
-
say
|
|
406
|
-
say
|
|
407
|
+
say ''
|
|
408
|
+
say '1. Go to API Tokens:', :bold
|
|
407
409
|
say " #{api_url}/organizations/YOUR_ORG_ID/api_tokens", :green
|
|
408
|
-
say
|
|
409
|
-
say
|
|
410
|
-
say
|
|
410
|
+
say ''
|
|
411
|
+
say ' Or navigate: Dashboard → Your Organization → API Tokens', :cyan
|
|
412
|
+
say ''
|
|
411
413
|
say "2. Click 'Create Token'", :bold
|
|
412
|
-
say
|
|
413
|
-
say
|
|
414
|
+
say ''
|
|
415
|
+
say '3. Fill in the details:', :bold
|
|
414
416
|
say " • Name: 'CLI Access' (or anything you like)"
|
|
415
|
-
say
|
|
417
|
+
say ' • Scopes: ✓ read ✓ write (minimum required)'
|
|
416
418
|
say " • Expiration: Choose 'Never' or '1 year'"
|
|
417
|
-
say
|
|
419
|
+
say ''
|
|
418
420
|
say "4. Click 'Create' and COPY the token", :bold
|
|
419
421
|
say " ⚠️ You'll only see it once!", :yellow
|
|
420
|
-
say
|
|
421
|
-
|
|
422
|
-
unless yes_with_default?(
|
|
423
|
-
say
|
|
422
|
+
say ''
|
|
423
|
+
|
|
424
|
+
unless yes_with_default?('Have you generated and copied your token?', :green)
|
|
425
|
+
say ''
|
|
424
426
|
say "Come back and run 'mysigner onboard' when you have your token!", :yellow
|
|
425
427
|
return
|
|
426
428
|
end
|
|
427
|
-
say
|
|
429
|
+
say ''
|
|
428
430
|
|
|
429
431
|
# Step 4: Login
|
|
430
|
-
say
|
|
431
|
-
say
|
|
432
|
-
say
|
|
432
|
+
say 'Step 4: Login to CLI', :cyan
|
|
433
|
+
say '-' * 80
|
|
434
|
+
say ''
|
|
433
435
|
say "Great! Now let's log you in.", :bold
|
|
434
|
-
say
|
|
435
|
-
|
|
436
|
+
say ''
|
|
437
|
+
|
|
436
438
|
# Get user email
|
|
437
439
|
user_email = prompt_for_email
|
|
438
|
-
say
|
|
439
|
-
|
|
440
|
-
api_token = ask(
|
|
441
|
-
say
|
|
442
|
-
|
|
440
|
+
say ''
|
|
441
|
+
|
|
442
|
+
api_token = ask('Paste your API Token:', echo: false)
|
|
443
|
+
say ''
|
|
444
|
+
|
|
443
445
|
if api_token.empty?
|
|
444
|
-
error
|
|
446
|
+
error 'Token cannot be empty'
|
|
445
447
|
say "Run 'mysigner onboard' again when you have your token", :yellow
|
|
446
448
|
return
|
|
447
449
|
end
|
|
448
450
|
|
|
449
|
-
say
|
|
450
|
-
|
|
451
|
+
say 'Validating token and email...', :yellow
|
|
452
|
+
|
|
451
453
|
begin
|
|
452
454
|
client = Client.new(api_url: api_url, api_token: api_token, user_email: user_email)
|
|
453
455
|
response = client.test_connection
|
|
454
|
-
|
|
456
|
+
|
|
455
457
|
unless response[:success]
|
|
456
|
-
error
|
|
458
|
+
error 'Connection test failed'
|
|
457
459
|
return
|
|
458
460
|
end
|
|
459
|
-
|
|
461
|
+
|
|
460
462
|
response = client.get('/api/v1/organizations')
|
|
461
463
|
organizations = response[:data]['organizations']
|
|
462
|
-
|
|
464
|
+
|
|
463
465
|
if organizations.empty?
|
|
464
|
-
error
|
|
465
|
-
say
|
|
466
|
+
error 'No organizations found'
|
|
467
|
+
say 'Please check that your token is associated with an organization', :yellow
|
|
466
468
|
return
|
|
467
469
|
end
|
|
468
470
|
|
|
469
471
|
selected_org = organizations.first
|
|
470
472
|
org_id = selected_org['id']
|
|
471
|
-
|
|
473
|
+
|
|
472
474
|
# Get detailed org info
|
|
473
475
|
org_response = client.get("/api/v1/organizations/#{org_id}")
|
|
474
476
|
org_data = org_response[:data]
|
|
475
|
-
|
|
477
|
+
|
|
476
478
|
config = Config.new
|
|
477
479
|
config.api_url = api_url
|
|
478
480
|
config.user_email = user_email # Save the verified email
|
|
@@ -481,228 +483,216 @@ module Mysigner
|
|
|
481
483
|
config.save
|
|
482
484
|
|
|
483
485
|
# Step 5: App Store Connect Setup (Optional)
|
|
484
|
-
say
|
|
485
|
-
say
|
|
486
|
-
say
|
|
487
|
-
say
|
|
488
|
-
|
|
486
|
+
say ''
|
|
487
|
+
say 'Step 5: App Store Connect Setup (optional but recommended)', :cyan
|
|
488
|
+
say '-' * 80
|
|
489
|
+
say ''
|
|
490
|
+
|
|
489
491
|
# Check if already configured
|
|
490
492
|
asc_configured = org_data['app_store_connect_configured'] || false
|
|
491
|
-
|
|
493
|
+
|
|
492
494
|
if asc_configured
|
|
493
|
-
say
|
|
494
|
-
say
|
|
495
|
-
say
|
|
496
|
-
if org_data['app_store_connect_team_id']
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
say
|
|
500
|
-
say ""
|
|
501
|
-
say "You can manage credentials in the web dashboard:", :cyan
|
|
495
|
+
say '✓ App Store Connect is already configured!', :green
|
|
496
|
+
say ''
|
|
497
|
+
say 'Current setup:', :cyan
|
|
498
|
+
say " • Team ID: #{org_data['app_store_connect_team_id']}" if org_data['app_store_connect_team_id']
|
|
499
|
+
say ' • Status: Active'
|
|
500
|
+
say ''
|
|
501
|
+
say 'You can manage credentials in the web dashboard:', :cyan
|
|
502
502
|
say " #{api_url}/organizations/#{org_id}", :green
|
|
503
|
-
say
|
|
504
|
-
say
|
|
505
|
-
say
|
|
503
|
+
say ''
|
|
504
|
+
say '💡 Tip: You can add multiple credentials (for different teams)', :cyan
|
|
505
|
+
say ''
|
|
506
506
|
else
|
|
507
|
-
say
|
|
508
|
-
say
|
|
509
|
-
say
|
|
510
|
-
say
|
|
511
|
-
say
|
|
512
|
-
say
|
|
513
|
-
|
|
514
|
-
asc_choice = ask(
|
|
515
|
-
say
|
|
516
|
-
|
|
507
|
+
say 'To upload to TestFlight/App Store, we need your API credentials.', :bold
|
|
508
|
+
say ''
|
|
509
|
+
say 'Do you want to set this up now?', :bold
|
|
510
|
+
say ' 1. Yes, guide me through it (recommended)'
|
|
511
|
+
say ' 2. Skip for now (you can do this later)'
|
|
512
|
+
say ''
|
|
513
|
+
|
|
514
|
+
asc_choice = ask('Select (1-2):', limited_to: %w[1 2])
|
|
515
|
+
say ''
|
|
516
|
+
|
|
517
517
|
if asc_choice == '1'
|
|
518
518
|
asc_configured = setup_app_store_connect_credentials(client, config, org_id)
|
|
519
519
|
else
|
|
520
|
-
say
|
|
521
|
-
say
|
|
522
|
-
say
|
|
523
|
-
say
|
|
524
|
-
say
|
|
525
|
-
say
|
|
520
|
+
say '⏭️ Skipped App Store Connect setup', :yellow
|
|
521
|
+
say ''
|
|
522
|
+
say 'You can set this up later by:', :cyan
|
|
523
|
+
say ' • Running: mysigner doctor'
|
|
524
|
+
say ' • Or via the web dashboard'
|
|
525
|
+
say ''
|
|
526
526
|
end
|
|
527
527
|
end
|
|
528
528
|
|
|
529
|
-
say
|
|
530
|
-
say
|
|
531
|
-
say
|
|
532
|
-
say
|
|
533
|
-
say
|
|
529
|
+
say ''
|
|
530
|
+
say '=' * 80, :green
|
|
531
|
+
say '🎉 Setup Complete!', :green
|
|
532
|
+
say '=' * 80, :green
|
|
533
|
+
say ''
|
|
534
534
|
say "You're all set up and ready to go!", :bold
|
|
535
|
-
say
|
|
535
|
+
say ''
|
|
536
536
|
say "User: #{user_email}", :cyan
|
|
537
537
|
say "Organization: #{org_data['name']} (ID: #{org_id})", :cyan
|
|
538
538
|
say "Config saved to: #{Config::CONFIG_FILE}", :cyan
|
|
539
|
-
|
|
539
|
+
|
|
540
540
|
# Show App Store Connect status
|
|
541
|
-
say
|
|
541
|
+
say ''
|
|
542
542
|
if asc_configured
|
|
543
|
-
say
|
|
543
|
+
say '✓ App Store Connect: Configured', :green
|
|
544
544
|
elsif defined?(asc_choice) && asc_choice == '1'
|
|
545
|
-
say
|
|
546
|
-
say
|
|
545
|
+
say '⚠️ App Store Connect:', :yellow
|
|
546
|
+
say ' Setup was attempted but not completed', :yellow
|
|
547
547
|
say " Run 'mysigner doctor' to configure it", :yellow
|
|
548
548
|
else
|
|
549
|
-
say
|
|
549
|
+
say '⚠️ App Store Connect: Not configured', :yellow
|
|
550
550
|
say " Run 'mysigner doctor' to set it up", :yellow
|
|
551
551
|
end
|
|
552
|
-
|
|
553
|
-
say
|
|
554
|
-
say
|
|
552
|
+
|
|
553
|
+
say ''
|
|
554
|
+
say '🔒 Security Note:', :yellow
|
|
555
555
|
say " Your token is organization-specific. Use 'mysigner switch'", :yellow
|
|
556
|
-
say
|
|
557
|
-
say
|
|
558
|
-
say
|
|
559
|
-
say
|
|
560
|
-
say
|
|
561
|
-
say
|
|
562
|
-
say
|
|
563
|
-
say
|
|
556
|
+
say ' to add tokens for other organizations.', :yellow
|
|
557
|
+
say ''
|
|
558
|
+
say '🚀 Try your first ship:', :bold
|
|
559
|
+
say ''
|
|
560
|
+
say ' cd your-ios-project'
|
|
561
|
+
say ' mysigner ship testflight'
|
|
562
|
+
say ''
|
|
563
|
+
say '💡 Tips:', :cyan
|
|
564
564
|
say " • Run 'mysigner doctor' to check your environment"
|
|
565
565
|
say " • Run 'mysigner --help' to see all commands"
|
|
566
566
|
say " • Run 'mysigner status' to verify your setup"
|
|
567
|
-
say
|
|
568
|
-
|
|
567
|
+
say ''
|
|
569
568
|
rescue Mysigner::UnauthorizedError => e
|
|
570
|
-
error
|
|
571
|
-
say
|
|
572
|
-
|
|
569
|
+
error 'Authentication failed'
|
|
570
|
+
say ''
|
|
571
|
+
|
|
573
572
|
# Check if it's an email validation error
|
|
574
|
-
if e.message.include?("doesn't belong to") || e.message.include?(
|
|
575
|
-
say
|
|
576
|
-
say
|
|
573
|
+
if e.message.include?("doesn't belong to") || e.message.include?('use your own token')
|
|
574
|
+
say '⚠️ Token Email Mismatch', :yellow
|
|
575
|
+
say ''
|
|
577
576
|
say "The token you provided doesn't belong to #{user_email}.", :yellow
|
|
578
|
-
say
|
|
579
|
-
say
|
|
577
|
+
say ''
|
|
578
|
+
say 'Please make sure you:', :yellow
|
|
580
579
|
say " 1. Are logged in as #{user_email} on the web dashboard", :yellow
|
|
581
580
|
say " 2. Generate the token while logged in as #{user_email}", :yellow
|
|
582
|
-
say
|
|
581
|
+
say ' 3. Enter the correct email address', :yellow
|
|
583
582
|
else
|
|
584
|
-
say
|
|
585
|
-
say
|
|
583
|
+
say 'The token you entered is invalid. Please:', :yellow
|
|
584
|
+
say ' 1. Check you copied the entire token'
|
|
586
585
|
say " 2. Make sure the token hasn't been revoked"
|
|
587
|
-
say
|
|
586
|
+
say ' 3. Generate a new token if needed'
|
|
588
587
|
end
|
|
589
|
-
say
|
|
588
|
+
say ''
|
|
590
589
|
say "Run 'mysigner onboard' to try again", :yellow
|
|
591
|
-
rescue => e
|
|
590
|
+
rescue StandardError => e
|
|
592
591
|
error "Setup failed: #{e.message}"
|
|
593
|
-
say
|
|
592
|
+
say ''
|
|
594
593
|
say "Run 'mysigner onboard' to try again", :yellow
|
|
595
594
|
end
|
|
596
595
|
end
|
|
597
596
|
|
|
598
|
-
desc
|
|
597
|
+
desc 'logout', 'Log out and clear stored credentials'
|
|
599
598
|
def logout
|
|
600
599
|
config = Config.new
|
|
601
|
-
|
|
600
|
+
|
|
602
601
|
unless config.exists?
|
|
603
|
-
say
|
|
602
|
+
say 'No stored credentials found', :yellow
|
|
604
603
|
return
|
|
605
604
|
end
|
|
606
605
|
|
|
607
|
-
if yes?(
|
|
606
|
+
if yes?('Are you sure you want to logout? (y/n)')
|
|
608
607
|
config.clear
|
|
609
|
-
say
|
|
608
|
+
say '✓ Successfully logged out', :green
|
|
610
609
|
say "Config file removed: #{Config::CONFIG_FILE}", :green
|
|
611
610
|
else
|
|
612
|
-
say
|
|
611
|
+
say 'Logout cancelled', :yellow
|
|
613
612
|
end
|
|
614
613
|
end
|
|
615
614
|
|
|
616
|
-
desc
|
|
615
|
+
desc 'status', 'Check connection, credentials, and App Store Connect setup'
|
|
617
616
|
def status
|
|
618
|
-
config =
|
|
619
|
-
|
|
620
|
-
unless config.exists?
|
|
621
|
-
error "Not logged in. Run 'mysigner login' first."
|
|
622
|
-
exit 1
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
config.load
|
|
617
|
+
config = load_config
|
|
626
618
|
|
|
627
|
-
say
|
|
628
|
-
say
|
|
629
|
-
say
|
|
619
|
+
say '📊 My Signer Status', :cyan
|
|
620
|
+
say ''
|
|
621
|
+
say 'Configuration:', :bold
|
|
630
622
|
say " API URL: #{config.api_url}"
|
|
631
623
|
say " User: #{config.user_email || '(unknown)'}"
|
|
632
624
|
say " Encryption: #{config.encrypted_config? ? '✓ Enabled' : '✗ Disabled'}"
|
|
633
|
-
say
|
|
625
|
+
say ''
|
|
634
626
|
|
|
635
627
|
# Show current organization
|
|
636
628
|
if config.current_organization_id
|
|
637
|
-
say
|
|
629
|
+
say 'Current Organization:', :bold
|
|
638
630
|
say " Name: #{config.org_name || '(unknown)'}"
|
|
639
631
|
say " ID: #{config.current_organization_id}"
|
|
640
632
|
say " Token: #{config.display[:current_token]}"
|
|
641
|
-
say
|
|
633
|
+
say ''
|
|
642
634
|
end
|
|
643
635
|
|
|
644
636
|
# Show all saved organizations
|
|
645
637
|
if config.organization_ids.length > 1
|
|
646
638
|
say "Saved Organizations: (#{config.organization_ids.length})", :bold
|
|
647
639
|
config.organization_ids.each do |org_id|
|
|
648
|
-
current_marker = org_id == config.current_organization_id ?
|
|
649
|
-
org_name = config.org_name(org_id) ||
|
|
640
|
+
current_marker = org_id == config.current_organization_id ? ' (current)' : ''
|
|
641
|
+
org_name = config.org_name(org_id) || 'Unknown'
|
|
650
642
|
say " • #{org_name}#{current_marker} (ID: #{org_id})"
|
|
651
643
|
end
|
|
652
|
-
say
|
|
644
|
+
say ''
|
|
653
645
|
end
|
|
654
646
|
|
|
655
647
|
# Test connection
|
|
656
|
-
say
|
|
657
|
-
|
|
648
|
+
say 'Connection:', :bold
|
|
649
|
+
|
|
658
650
|
begin
|
|
659
651
|
client = Client.new(api_url: config.api_url, api_token: config.api_token)
|
|
660
652
|
client.test_connection
|
|
661
|
-
|
|
662
|
-
say
|
|
663
|
-
|
|
653
|
+
|
|
654
|
+
say ' Status: ✓ Connected', :green
|
|
655
|
+
|
|
664
656
|
# Get organization details
|
|
665
657
|
if config.current_organization_id
|
|
666
658
|
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
667
659
|
org = org_response[:data]
|
|
668
|
-
|
|
660
|
+
|
|
669
661
|
say " Role: #{org['role'] || 'viewer'}"
|
|
670
662
|
say " Members: #{org['member_count'] || 0}"
|
|
671
|
-
say
|
|
672
|
-
|
|
663
|
+
say ''
|
|
664
|
+
|
|
673
665
|
# Show App Store Connect status
|
|
674
|
-
say
|
|
666
|
+
say 'App Store Connect:', :bold
|
|
675
667
|
if org['app_store_connect_configured']
|
|
676
|
-
say
|
|
677
|
-
if org['app_store_connect_team_id']
|
|
678
|
-
say " Team ID: #{org['app_store_connect_team_id']}"
|
|
679
|
-
end
|
|
668
|
+
say ' ✓ Configured', :green
|
|
669
|
+
say " Team ID: #{org['app_store_connect_team_id']}" if org['app_store_connect_team_id']
|
|
680
670
|
else
|
|
681
|
-
say
|
|
671
|
+
say ' ✗ Not configured', :yellow
|
|
682
672
|
say " Run 'mysigner doctor' to set it up"
|
|
683
673
|
end
|
|
684
674
|
end
|
|
685
675
|
rescue Mysigner::UnauthorizedError
|
|
686
|
-
say
|
|
676
|
+
say ' Status: ✗ Unauthorized (invalid token)', :red
|
|
687
677
|
exit 1
|
|
688
678
|
rescue Mysigner::ConnectionError => e
|
|
689
|
-
say
|
|
679
|
+
say ' Status: ✗ Connection failed', :red
|
|
690
680
|
say " Error: #{e.message}", :red
|
|
691
681
|
exit 1
|
|
692
|
-
rescue => e
|
|
693
|
-
say
|
|
682
|
+
rescue StandardError => e
|
|
683
|
+
say ' Status: ✗ Error', :red
|
|
694
684
|
say " Error: #{e.message}", :red
|
|
695
685
|
exit 1
|
|
696
686
|
end
|
|
697
687
|
end
|
|
698
688
|
|
|
699
|
-
desc
|
|
689
|
+
desc 'orgs', "List all organizations you're a member of"
|
|
700
690
|
def orgs
|
|
701
691
|
config = load_config
|
|
702
692
|
client = create_client(config)
|
|
703
693
|
|
|
704
|
-
say
|
|
705
|
-
say
|
|
694
|
+
say '📋 Organizations', :cyan
|
|
695
|
+
say ''
|
|
706
696
|
|
|
707
697
|
begin
|
|
708
698
|
# Fetch ALL organizations the user is a member of (not restricted by token's org)
|
|
@@ -713,37 +703,37 @@ module Mysigner
|
|
|
713
703
|
all_org_ids = (config.organization_ids + api_organizations.map { |o| o['id'] }).uniq
|
|
714
704
|
|
|
715
705
|
if all_org_ids.empty?
|
|
716
|
-
say
|
|
706
|
+
say 'No organizations found', :yellow
|
|
717
707
|
return
|
|
718
708
|
end
|
|
719
709
|
|
|
720
710
|
all_org_ids.each do |org_id|
|
|
721
711
|
has_token = config.has_token_for_org?(org_id)
|
|
722
712
|
is_current = org_id == config.current_organization_id
|
|
723
|
-
|
|
713
|
+
|
|
724
714
|
# Get org details
|
|
725
715
|
org_name = config.org_name(org_id)
|
|
726
716
|
api_org = api_organizations.find { |o| o['id'] == org_id }
|
|
727
717
|
org_name = api_org['name'] if api_org && (org_name.nil? || org_name == 'Unknown')
|
|
728
|
-
|
|
729
|
-
current_marker = is_current ?
|
|
730
|
-
token_status = has_token ?
|
|
731
|
-
|
|
718
|
+
|
|
719
|
+
current_marker = is_current ? ' (current)' : ''
|
|
720
|
+
token_status = has_token ? '✓' : '⚠️'
|
|
721
|
+
|
|
732
722
|
say " #{token_status} #{org_name}#{current_marker}", :green
|
|
733
|
-
|
|
723
|
+
|
|
734
724
|
if api_org
|
|
735
725
|
role = api_org['role'] || 'viewer'
|
|
736
726
|
say " ID: #{org_id} | Role: #{role} | Members: #{api_org['member_count'] || 0}"
|
|
737
727
|
else
|
|
738
728
|
say " ID: #{org_id} | #{has_token ? 'Token saved' : 'Need token to access'}"
|
|
739
729
|
end
|
|
740
|
-
say
|
|
730
|
+
say ''
|
|
741
731
|
end
|
|
742
732
|
|
|
743
733
|
say "Total: #{all_org_ids.length} organization(s)", :white
|
|
744
|
-
say
|
|
745
|
-
say
|
|
746
|
-
say
|
|
734
|
+
say ''
|
|
735
|
+
say 'Legend: ✓ = Has token | ⚠️ = Need token', :white
|
|
736
|
+
say ''
|
|
747
737
|
say "💡 Tip: Use 'mysigner switch' to change organizations", :yellow
|
|
748
738
|
rescue Mysigner::ClientError => e
|
|
749
739
|
error "Failed to fetch organizations: #{e.message}"
|
|
@@ -751,10 +741,14 @@ module Mysigner
|
|
|
751
741
|
end
|
|
752
742
|
end
|
|
753
743
|
|
|
754
|
-
desc
|
|
744
|
+
desc 'switch [ORG_ID]', 'Switch between organizations (for multi-org users)'
|
|
755
745
|
long_desc <<~DESC
|
|
756
746
|
Switch to a different organization.
|
|
757
|
-
|
|
747
|
+
|
|
748
|
+
Interactive (no argument): prompts you with a numbered list.
|
|
749
|
+
Non-interactive: pass an organization ID directly, e.g.
|
|
750
|
+
mysigner switch 7
|
|
751
|
+
|
|
758
752
|
With organization-specific tokens, you'll need a token for each
|
|
759
753
|
organization you want to access. This command will:
|
|
760
754
|
- Show all organizations you're a member of
|
|
@@ -762,51 +756,51 @@ module Mysigner
|
|
|
762
756
|
- Prompt for a token if switching to an org without one
|
|
763
757
|
- Validate the token belongs to the target organization
|
|
764
758
|
- Update your configuration
|
|
765
|
-
|
|
759
|
+
|
|
766
760
|
Note: You need to be the same user in all organizations. Tokens
|
|
767
761
|
from different user accounts will be rejected.
|
|
768
762
|
DESC
|
|
769
|
-
def switch
|
|
763
|
+
def switch(target_org_id = nil)
|
|
770
764
|
config = load_config
|
|
771
765
|
client = create_client(config)
|
|
772
766
|
|
|
773
|
-
say
|
|
774
|
-
say
|
|
767
|
+
say '🔄 Switch Organization', :cyan
|
|
768
|
+
say ''
|
|
775
769
|
|
|
776
770
|
begin
|
|
777
771
|
# Get current org details
|
|
778
772
|
current_org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
779
773
|
current_org = current_org_response[:data]
|
|
780
774
|
|
|
781
|
-
say
|
|
775
|
+
say 'Current organization:', :yellow
|
|
782
776
|
say " #{current_org['name']} (ID: #{config.current_organization_id})", :green
|
|
783
|
-
say
|
|
777
|
+
say ''
|
|
784
778
|
|
|
785
779
|
# Fetch ALL organizations the user is a member of (not restricted by token's org)
|
|
786
780
|
response = client.get('/api/v1/user/organizations')
|
|
787
781
|
api_organizations = response[:data]['organizations']
|
|
788
|
-
|
|
782
|
+
|
|
789
783
|
# Build comprehensive list: stored orgs + API orgs
|
|
790
784
|
all_org_ids = (config.organization_ids + api_organizations.map { |o| o['id'] }).uniq
|
|
791
|
-
|
|
785
|
+
|
|
792
786
|
if all_org_ids.length < 2
|
|
793
|
-
say
|
|
794
|
-
say
|
|
795
|
-
say
|
|
787
|
+
say 'You only have access to one organization.', :yellow
|
|
788
|
+
say 'Nothing to switch to!', :yellow
|
|
789
|
+
say ''
|
|
796
790
|
say "💡 Tip: If you're a member of other organizations, you'll need", :cyan
|
|
797
|
-
say
|
|
791
|
+
say ' to generate tokens for them first (in the web dashboard).', :cyan
|
|
798
792
|
return
|
|
799
793
|
end
|
|
800
794
|
|
|
801
795
|
# Show available organizations
|
|
802
|
-
say
|
|
803
|
-
say
|
|
796
|
+
say 'Available organizations:', :cyan
|
|
797
|
+
say ''
|
|
804
798
|
organizations_list = []
|
|
805
|
-
|
|
799
|
+
|
|
806
800
|
all_org_ids.each_with_index do |org_id, index|
|
|
807
801
|
has_token = config.has_token_for_org?(org_id)
|
|
808
802
|
is_current = org_id == config.current_organization_id
|
|
809
|
-
|
|
803
|
+
|
|
810
804
|
# Get org name from config or API
|
|
811
805
|
org_name = config.org_name(org_id)
|
|
812
806
|
if org_name.nil? || org_name == 'Unknown'
|
|
@@ -814,63 +808,74 @@ module Mysigner
|
|
|
814
808
|
api_org = api_organizations.find { |o| o['id'] == org_id }
|
|
815
809
|
org_name = api_org['name'] if api_org
|
|
816
810
|
end
|
|
817
|
-
|
|
818
|
-
status = has_token ?
|
|
819
|
-
current_marker = is_current ?
|
|
820
|
-
|
|
811
|
+
|
|
812
|
+
status = has_token ? '✓' : '⚠️ '
|
|
813
|
+
current_marker = is_current ? ' (current)' : ''
|
|
814
|
+
|
|
821
815
|
say " #{index + 1}. #{status} #{org_name}#{current_marker}"
|
|
822
816
|
say " ID: #{org_id} | #{has_token ? 'Token saved' : 'Need token'}", :white
|
|
823
|
-
|
|
817
|
+
|
|
824
818
|
organizations_list << { id: org_id, name: org_name, has_token: has_token }
|
|
825
819
|
end
|
|
826
|
-
|
|
827
|
-
say
|
|
828
|
-
say
|
|
829
|
-
say
|
|
830
|
-
|
|
831
|
-
#
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
820
|
+
|
|
821
|
+
say ''
|
|
822
|
+
say 'Legend: ✓ = Has token | ⚠️ = Needs token', :white
|
|
823
|
+
say ''
|
|
824
|
+
|
|
825
|
+
# Non-interactive selection via positional arg (`mysigner switch 7`)
|
|
826
|
+
selected_org = if target_org_id
|
|
827
|
+
match = organizations_list.find { |o| o[:id].to_s == target_org_id.to_s }
|
|
828
|
+
unless match
|
|
829
|
+
error "Organization #{target_org_id} not found among your memberships"
|
|
830
|
+
say ''
|
|
831
|
+
say " Available IDs: #{organizations_list.map { |o| o[:id] }.join(', ')}", :yellow
|
|
832
|
+
exit 1
|
|
833
|
+
end
|
|
834
|
+
match
|
|
835
|
+
else
|
|
836
|
+
org_index = ask("Select organization (1-#{organizations_list.length}, or 'q' to cancel):")
|
|
837
|
+
|
|
838
|
+
if org_index.downcase == 'q'
|
|
839
|
+
say 'Cancelled', :yellow
|
|
840
|
+
return
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
unless org_index.match(/^\d+$/) && org_index.to_i.between?(1, organizations_list.length)
|
|
844
|
+
error 'Invalid selection'
|
|
845
|
+
return
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
organizations_list[org_index.to_i - 1]
|
|
849
|
+
end
|
|
845
850
|
|
|
846
851
|
if selected_org[:id] == config.current_organization_id
|
|
847
|
-
say
|
|
848
|
-
say
|
|
852
|
+
say ''
|
|
853
|
+
say 'Already using this organization!', :yellow
|
|
849
854
|
return
|
|
850
855
|
end
|
|
851
856
|
|
|
852
857
|
# Check if we have a token for this org
|
|
853
858
|
unless selected_org[:has_token]
|
|
854
|
-
say
|
|
859
|
+
say ''
|
|
855
860
|
say "⚠️ You don't have a token for '#{selected_org[:name]}' yet.", :yellow
|
|
856
|
-
say
|
|
857
|
-
say
|
|
861
|
+
say ''
|
|
862
|
+
say 'To switch to this organization:', :cyan
|
|
858
863
|
say " 1. Go to: #{config.api_url}/organizations/#{selected_org[:id]}/api_tokens"
|
|
859
|
-
say
|
|
860
|
-
say
|
|
861
|
-
say
|
|
862
|
-
|
|
864
|
+
say ' 2. Generate a new API token'
|
|
865
|
+
say ' 3. Paste it below'
|
|
866
|
+
say ''
|
|
867
|
+
|
|
863
868
|
new_token = ask("Paste API token for '#{selected_org[:name]}' (or 'q' to cancel):", echo: false)
|
|
864
|
-
say
|
|
865
|
-
|
|
869
|
+
say ''
|
|
870
|
+
|
|
866
871
|
if new_token.downcase == 'q' || new_token.empty?
|
|
867
|
-
say
|
|
872
|
+
say 'Cancelled', :yellow
|
|
868
873
|
return
|
|
869
874
|
end
|
|
870
|
-
|
|
875
|
+
|
|
871
876
|
# Validate the new token (with email validation)
|
|
872
|
-
say
|
|
873
|
-
|
|
877
|
+
say 'Validating token...', :yellow
|
|
878
|
+
|
|
874
879
|
begin
|
|
875
880
|
# Use stored email from config for validation
|
|
876
881
|
temp_client = Client.new(
|
|
@@ -878,42 +883,42 @@ module Mysigner
|
|
|
878
883
|
api_token: new_token,
|
|
879
884
|
user_email: config.user_email
|
|
880
885
|
)
|
|
881
|
-
|
|
886
|
+
|
|
882
887
|
# Try to fetch the target organization with the new token
|
|
883
888
|
validation_response = temp_client.get("/api/v1/organizations/#{selected_org[:id]}")
|
|
884
889
|
token_org_data = validation_response[:data]
|
|
885
|
-
|
|
890
|
+
|
|
886
891
|
# Check if token_organization_id matches (new backend feature)
|
|
887
892
|
if token_org_data['token_organization_id'] && token_org_data['token_organization_id'] != selected_org[:id]
|
|
888
|
-
error
|
|
889
|
-
say
|
|
890
|
-
say "The token you provided is for organization ID #{token_org_data['token_organization_id']},",
|
|
893
|
+
error 'This token belongs to a different organization!'
|
|
894
|
+
say ''
|
|
895
|
+
say "The token you provided is for organization ID #{token_org_data['token_organization_id']},",
|
|
896
|
+
:yellow
|
|
891
897
|
say "but you're trying to access organization ID #{selected_org[:id]}.", :yellow
|
|
892
|
-
say
|
|
893
|
-
say
|
|
898
|
+
say ''
|
|
899
|
+
say 'Please generate a token from the correct organization.', :yellow
|
|
894
900
|
exit 1
|
|
895
901
|
end
|
|
896
|
-
|
|
897
|
-
say
|
|
898
|
-
|
|
902
|
+
|
|
903
|
+
say '✓ Token validated successfully', :green
|
|
904
|
+
|
|
899
905
|
# Save the token
|
|
900
906
|
config.save_token_for_org(selected_org[:id], selected_org[:name], new_token)
|
|
901
|
-
|
|
902
907
|
rescue Mysigner::UnauthorizedError => e
|
|
903
|
-
error
|
|
904
|
-
say
|
|
905
|
-
|
|
908
|
+
error 'Token validation failed'
|
|
909
|
+
say ''
|
|
910
|
+
|
|
906
911
|
# Check if it's an email validation error
|
|
907
|
-
if e.message.include?("doesn't belong to") || e.message.include?(
|
|
912
|
+
if e.message.include?("doesn't belong to") || e.message.include?('use your own token')
|
|
908
913
|
say "⚠️ This token doesn't belong to #{config.user_email}!", :yellow
|
|
909
|
-
say
|
|
914
|
+
say ''
|
|
910
915
|
say "You can only use tokens from your own account (#{config.user_email}).", :yellow
|
|
911
916
|
say "Please generate a token while logged in as #{config.user_email} on the web.", :yellow
|
|
912
917
|
else
|
|
913
|
-
say
|
|
918
|
+
say 'The token you provided is not valid.', :yellow
|
|
914
919
|
end
|
|
915
920
|
exit 1
|
|
916
|
-
rescue => e
|
|
921
|
+
rescue StandardError => e
|
|
917
922
|
error "Token validation failed: #{e.message}"
|
|
918
923
|
exit 1
|
|
919
924
|
end
|
|
@@ -923,18 +928,17 @@ module Mysigner
|
|
|
923
928
|
config.current_organization_id = selected_org[:id]
|
|
924
929
|
config.save
|
|
925
930
|
|
|
926
|
-
say
|
|
931
|
+
say ''
|
|
927
932
|
say "✓ Successfully switched to: #{selected_org[:name]}", :green
|
|
928
|
-
say
|
|
933
|
+
say ''
|
|
929
934
|
say "💡 Run 'mysigner status' to verify your new configuration", :cyan
|
|
930
|
-
|
|
931
935
|
rescue Mysigner::ClientError => e
|
|
932
936
|
error "Failed to switch organization: #{e.message}"
|
|
933
937
|
exit 1
|
|
934
938
|
end
|
|
935
939
|
end
|
|
936
940
|
|
|
937
|
-
desc
|
|
941
|
+
desc 'config', 'Show current CLI configuration (API URL, tokens, org)'
|
|
938
942
|
def config
|
|
939
943
|
config = Config.new
|
|
940
944
|
|
|
@@ -945,291 +949,296 @@ module Mysigner
|
|
|
945
949
|
|
|
946
950
|
config.load
|
|
947
951
|
|
|
948
|
-
say
|
|
949
|
-
say
|
|
952
|
+
say '⚙️ Configuration', :cyan
|
|
953
|
+
say ''
|
|
950
954
|
config.display.each do |key, value|
|
|
951
955
|
say " #{key.to_s.ljust(20)}: #{value}"
|
|
952
956
|
end
|
|
953
|
-
say
|
|
957
|
+
say ''
|
|
954
958
|
say "Config file: #{Config::CONFIG_FILE}"
|
|
955
959
|
end
|
|
956
960
|
|
|
957
961
|
no_commands do
|
|
958
|
-
# Helper method for yes/no prompts with Enter defaulting to yes
|
|
962
|
+
# Helper method for yes/no prompts with Enter defaulting to yes.
|
|
963
|
+
# Defaults to NO when stdin is not a TTY so automation (CI, pipes)
|
|
964
|
+
# never silently opts-in to mutating operations.
|
|
959
965
|
def yes_with_default?(statement, color = nil)
|
|
966
|
+
unless $stdin.tty?
|
|
967
|
+
say "#{statement} [Y/n] (non-interactive: assuming no)", color
|
|
968
|
+
return false
|
|
969
|
+
end
|
|
960
970
|
response = ask("#{statement} [Y/n]", color).to_s.strip.downcase
|
|
961
971
|
response.empty? || response == 'y' || response == 'yes'
|
|
962
972
|
end
|
|
963
973
|
|
|
964
974
|
# Helper method for App Store Connect credential setup
|
|
965
975
|
# Returns true if successfully configured, false otherwise
|
|
966
|
-
def setup_app_store_connect_credentials(client,
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
976
|
+
def setup_app_store_connect_credentials(client, _config, org_id)
|
|
977
|
+
say '📱 App Store Connect API Key Setup', :cyan
|
|
978
|
+
say ''
|
|
979
|
+
say "Let's set up your App Store Connect credentials.", :bold
|
|
980
|
+
say ''
|
|
981
|
+
say "Step 1: Create an API Key (if you don't have one)", :bold
|
|
982
|
+
say ''
|
|
983
|
+
say '1. Go to:', :cyan
|
|
984
|
+
say ' https://appstoreconnect.apple.com/access/api', :green
|
|
985
|
+
say ''
|
|
986
|
+
say "2. Click the '+' button to create a new key", :cyan
|
|
987
|
+
say ''
|
|
988
|
+
say '3. Select access:', :cyan
|
|
989
|
+
say ' • App Manager (for uploading builds)'
|
|
990
|
+
say ' • Or Admin (full access)'
|
|
991
|
+
say ''
|
|
992
|
+
say '4. Download the .p8 file', :cyan
|
|
993
|
+
say ' ⚠️ Save it securely - you can only download it once!', :yellow
|
|
994
|
+
say ''
|
|
995
|
+
|
|
996
|
+
unless yes_with_default?('Have you created and downloaded your API key?', :green)
|
|
997
|
+
say ''
|
|
998
|
+
say '⏭️ You can set this up later with:', :yellow
|
|
999
|
+
say ' • mysigner doctor', :cyan
|
|
1000
|
+
say ' • Or via the web dashboard', :cyan
|
|
1001
|
+
return false
|
|
1002
|
+
end
|
|
1003
|
+
say ''
|
|
1004
|
+
|
|
1005
|
+
# Prompt for .p8 file path with retry
|
|
1006
|
+
max_retries = 3
|
|
1007
|
+
attempts = 0
|
|
1008
|
+
p8_path = nil
|
|
1009
|
+
private_key = nil
|
|
1010
|
+
|
|
1011
|
+
while attempts < max_retries
|
|
1012
|
+
say 'Step 2: Locate your .p8 file', :bold
|
|
1013
|
+
say ''
|
|
1014
|
+
say '💡 Tip: You can drag & drop the file into terminal to get the path', :cyan
|
|
1015
|
+
say ''
|
|
1016
|
+
p8_path = ask('Enter the path to your .p8 file:').strip.gsub(/^['"]|['"]$/, '') # Remove quotes
|
|
1017
|
+
say ''
|
|
1018
|
+
|
|
1019
|
+
# Expand ~ to home directory
|
|
1020
|
+
p8_path = File.expand_path(p8_path)
|
|
1021
|
+
|
|
1022
|
+
if File.exist?(p8_path)
|
|
1023
|
+
# Read private key
|
|
1024
|
+
begin
|
|
1025
|
+
private_key = File.read(p8_path).strip
|
|
1026
|
+
|
|
1027
|
+
# Validate it looks like a private key
|
|
1028
|
+
unless private_key.include?('BEGIN PRIVATE KEY') || private_key.include?('BEGIN EC PRIVATE KEY')
|
|
1029
|
+
error "This doesn't look like a valid .p8 private key file"
|
|
1030
|
+
attempts += 1
|
|
1031
|
+
next
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
break # Success!
|
|
1035
|
+
rescue StandardError => e
|
|
1036
|
+
error "Failed to read file: #{e.message}"
|
|
1020
1037
|
attempts += 1
|
|
1021
1038
|
next
|
|
1022
1039
|
end
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
rescue => e
|
|
1026
|
-
error "Failed to read file: #{e.message}"
|
|
1040
|
+
else
|
|
1041
|
+
error "File not found: #{p8_path}"
|
|
1027
1042
|
attempts += 1
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
if attempts < max_retries
|
|
1035
|
-
say ""
|
|
1036
|
-
say "Please try again (attempt #{attempts + 1}/#{max_retries})", :yellow
|
|
1037
|
-
say ""
|
|
1043
|
+
|
|
1044
|
+
if attempts < max_retries
|
|
1045
|
+
say ''
|
|
1046
|
+
say "Please try again (attempt #{attempts + 1}/#{max_retries})", :yellow
|
|
1047
|
+
say ''
|
|
1048
|
+
end
|
|
1038
1049
|
end
|
|
1039
1050
|
end
|
|
1040
|
-
end
|
|
1041
|
-
|
|
1042
|
-
unless private_key
|
|
1043
|
-
say ""
|
|
1044
|
-
error "Could not read .p8 file after #{max_retries} attempts"
|
|
1045
|
-
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1046
|
-
return false
|
|
1047
|
-
end
|
|
1048
|
-
|
|
1049
|
-
# Auto-extract Key ID from filename (e.g., AuthKey_ABC123.p8 → ABC123)
|
|
1050
|
-
filename = File.basename(p8_path)
|
|
1051
|
-
key_id = nil
|
|
1052
|
-
if filename =~ /AuthKey_([A-Z0-9]+)\.p8/i
|
|
1053
|
-
key_id = $1
|
|
1054
|
-
say "✓ Auto-detected Key ID: #{key_id}", :green
|
|
1055
|
-
say ""
|
|
1056
|
-
end
|
|
1057
1051
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
say ""
|
|
1062
|
-
say "Find your Key ID in App Store Connect:", :cyan
|
|
1063
|
-
say " https://appstoreconnect.apple.com/access/api", :green
|
|
1064
|
-
say ""
|
|
1065
|
-
key_id = ask("Enter your Key ID (e.g., ABC12345):").strip
|
|
1066
|
-
say ""
|
|
1067
|
-
|
|
1068
|
-
if key_id.empty?
|
|
1069
|
-
error "Key ID cannot be empty"
|
|
1052
|
+
unless private_key
|
|
1053
|
+
say ''
|
|
1054
|
+
error "Could not read .p8 file after #{max_retries} attempts"
|
|
1070
1055
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1071
1056
|
return false
|
|
1072
1057
|
end
|
|
1073
|
-
end
|
|
1074
1058
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1059
|
+
# Auto-extract Key ID from filename (e.g., AuthKey_ABC123.p8 → ABC123)
|
|
1060
|
+
filename = File.basename(p8_path)
|
|
1061
|
+
key_id = nil
|
|
1062
|
+
if filename =~ /AuthKey_([A-Z0-9]+)\.p8/i
|
|
1063
|
+
key_id = ::Regexp.last_match(1)
|
|
1064
|
+
say "✓ Auto-detected Key ID: #{key_id}", :green
|
|
1065
|
+
say ''
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
# Prompt for Key ID if not auto-detected
|
|
1069
|
+
unless key_id
|
|
1070
|
+
say 'Could not auto-detect Key ID from filename.', :yellow
|
|
1071
|
+
say ''
|
|
1072
|
+
say 'Find your Key ID in App Store Connect:', :cyan
|
|
1073
|
+
say ' https://appstoreconnect.apple.com/access/api', :green
|
|
1074
|
+
say ''
|
|
1075
|
+
key_id = ask('Enter your Key ID (e.g., ABC12345):').strip
|
|
1076
|
+
say ''
|
|
1077
|
+
|
|
1078
|
+
if key_id.empty?
|
|
1079
|
+
error 'Key ID cannot be empty'
|
|
1080
|
+
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1081
|
+
return false
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
# Prompt for Issuer ID
|
|
1086
|
+
say 'Step 3: Find your Issuer ID', :bold
|
|
1087
|
+
say ''
|
|
1088
|
+
say 'Find it in App Store Connect (top right of Keys page):', :cyan
|
|
1089
|
+
say ' https://appstoreconnect.apple.com/access/api', :green
|
|
1090
|
+
say ''
|
|
1091
|
+
issuer_id = ask('Enter your Issuer ID (UUID format):').strip
|
|
1092
|
+
say ''
|
|
1093
|
+
|
|
1094
|
+
if issuer_id.empty?
|
|
1095
|
+
error 'Issuer ID cannot be empty'
|
|
1096
1096
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1097
1097
|
return false
|
|
1098
1098
|
end
|
|
1099
|
-
say ""
|
|
1100
|
-
end
|
|
1101
1099
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
credential_name = name_input.empty? ? "CLI Setup" : name_input
|
|
1113
|
-
|
|
1114
|
-
if credential_name.empty?
|
|
1115
|
-
error "Name cannot be empty"
|
|
1116
|
-
say ""
|
|
1100
|
+
# Basic UUID format validation
|
|
1101
|
+
unless issuer_id.match?(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
|
|
1102
|
+
say "⚠️ Warning: Issuer ID doesn't look like a UUID format", :yellow
|
|
1103
|
+
say ' Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', :yellow
|
|
1104
|
+
say ''
|
|
1105
|
+
unless yes_with_default?('Continue anyway?', :yellow)
|
|
1106
|
+
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1107
|
+
return false
|
|
1108
|
+
end
|
|
1109
|
+
say ''
|
|
1117
1110
|
end
|
|
1118
|
-
end
|
|
1119
|
-
say ""
|
|
1120
|
-
say "→ Using name: '#{credential_name}'", :cyan
|
|
1121
|
-
say ""
|
|
1122
|
-
|
|
1123
|
-
# Validate and upload
|
|
1124
|
-
say "Step 5: Validating credentials with Apple...", :bold
|
|
1125
|
-
say ""
|
|
1126
|
-
say "This may take a few seconds...", :yellow
|
|
1127
|
-
say ""
|
|
1128
1111
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
issuer_id: issuer_id,
|
|
1136
|
-
private_key: private_key
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
)
|
|
1112
|
+
# Prompt for credential name
|
|
1113
|
+
say 'Step 4: Name this credential', :bold
|
|
1114
|
+
say ''
|
|
1115
|
+
say "Choose a name to identify this API key (e.g., 'Production Key', 'Team A Key')", :cyan
|
|
1116
|
+
say "Default: 'CLI Setup' - just press Enter to use it", :cyan
|
|
1117
|
+
say ''
|
|
1140
1118
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1119
|
+
credential_name = nil
|
|
1120
|
+
while credential_name.nil? || credential_name.empty?
|
|
1121
|
+
name_input = ask('Credential name:').strip
|
|
1122
|
+
credential_name = name_input.empty? ? 'CLI Setup' : name_input
|
|
1123
|
+
|
|
1124
|
+
if credential_name.empty?
|
|
1125
|
+
error 'Name cannot be empty'
|
|
1126
|
+
say ''
|
|
1127
|
+
end
|
|
1128
|
+
end
|
|
1129
|
+
say ''
|
|
1130
|
+
say "→ Using name: '#{credential_name}'", :cyan
|
|
1131
|
+
say ''
|
|
1132
|
+
|
|
1133
|
+
# Validate and upload
|
|
1134
|
+
say 'Step 5: Validating credentials with Apple...', :bold
|
|
1135
|
+
say ''
|
|
1136
|
+
say 'This may take a few seconds...', :yellow
|
|
1137
|
+
say ''
|
|
1138
|
+
|
|
1139
|
+
begin
|
|
1140
|
+
response = client.post("/api/v1/organizations/#{org_id}/app_store_connect_credentials",
|
|
1141
|
+
body: {
|
|
1142
|
+
app_store_connect_credential: {
|
|
1143
|
+
name: credential_name,
|
|
1144
|
+
key_id: key_id,
|
|
1145
|
+
issuer_id: issuer_id,
|
|
1146
|
+
private_key: private_key
|
|
1147
|
+
}
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
if response[:success]
|
|
1151
|
+
data = response[:data]
|
|
1152
|
+
team_id = data['team_id']
|
|
1153
|
+
|
|
1154
|
+
say '✓ Credentials validated successfully!', :green
|
|
1155
|
+
say ''
|
|
1156
|
+
say 'Details:', :cyan
|
|
1157
|
+
say " • Name: #{credential_name}"
|
|
1158
|
+
say " • Key ID: #{key_id}"
|
|
1159
|
+
say " • Issuer ID: #{issuer_id}"
|
|
1160
|
+
if team_id
|
|
1161
|
+
say " • Team ID: #{team_id}"
|
|
1162
|
+
else
|
|
1163
|
+
say ' • Team ID: (will be extracted after first sync)'
|
|
1164
|
+
end
|
|
1165
|
+
say ' • Status: Active ✓'
|
|
1166
|
+
say ''
|
|
1167
|
+
say '🎉 App Store Connect is now configured!', :green
|
|
1168
|
+
say ''
|
|
1169
|
+
true # Success!
|
|
1153
1170
|
else
|
|
1154
|
-
|
|
1171
|
+
error 'Validation failed'
|
|
1172
|
+
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1173
|
+
false
|
|
1155
1174
|
end
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1175
|
+
rescue Mysigner::ClientError => e
|
|
1176
|
+
error_msg = e.message
|
|
1177
|
+
|
|
1178
|
+
# Check for duplicate name error
|
|
1179
|
+
if error_msg.include?('Name has already been taken') || error_msg.include?('validation_failed')
|
|
1180
|
+
error "A credential with the name '#{credential_name}' already exists"
|
|
1181
|
+
say ''
|
|
1182
|
+
say 'Please choose a different name and try again.', :yellow
|
|
1183
|
+
say 'Or manage credentials via the web dashboard:', :cyan
|
|
1184
|
+
say " #{client.api_url}/organizations/#{org_id}", :green
|
|
1185
|
+
else
|
|
1186
|
+
error "Failed to configure credentials: #{error_msg}"
|
|
1187
|
+
say ''
|
|
1188
|
+
say 'Common issues:', :yellow
|
|
1189
|
+
say ' • Invalid Key ID or Issuer ID'
|
|
1190
|
+
say ' • Incorrect .p8 file content'
|
|
1191
|
+
say " • API key doesn't have proper permissions"
|
|
1192
|
+
say ' • API key may be revoked or expired'
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
say ''
|
|
1163
1196
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
# Check for duplicate name error
|
|
1170
|
-
if error_msg.include?("Name has already been taken") || error_msg.include?("validation_failed")
|
|
1171
|
-
error "A credential with the name '#{credential_name}' already exists"
|
|
1172
|
-
say ""
|
|
1173
|
-
say "Please choose a different name and try again.", :yellow
|
|
1174
|
-
say "Or manage credentials via the web dashboard:", :cyan
|
|
1175
|
-
say " #{client.api_url}/organizations/#{org_id}", :green
|
|
1176
|
-
else
|
|
1177
|
-
error "Failed to configure credentials: #{error_msg}"
|
|
1178
|
-
say ""
|
|
1179
|
-
say "Common issues:", :yellow
|
|
1180
|
-
say " • Invalid Key ID or Issuer ID"
|
|
1181
|
-
say " • Incorrect .p8 file content"
|
|
1182
|
-
say " • API key doesn't have proper permissions"
|
|
1183
|
-
say " • API key may be revoked or expired"
|
|
1197
|
+
false
|
|
1198
|
+
rescue StandardError => e
|
|
1199
|
+
error "Unexpected error: #{e.message}"
|
|
1200
|
+
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1201
|
+
false
|
|
1184
1202
|
end
|
|
1185
|
-
|
|
1186
|
-
say ""
|
|
1187
|
-
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1188
|
-
return false
|
|
1189
|
-
rescue => e
|
|
1190
|
-
error "Unexpected error: #{e.message}"
|
|
1191
|
-
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1192
|
-
return false
|
|
1193
|
-
end
|
|
1194
1203
|
end
|
|
1195
1204
|
|
|
1196
1205
|
# Setup Google Play credentials
|
|
1197
|
-
def setup_google_play_credentials(client,
|
|
1198
|
-
say
|
|
1199
|
-
say
|
|
1206
|
+
def setup_google_play_credentials(client, _config, org_id)
|
|
1207
|
+
say '🤖 Google Play Service Account Setup', :cyan
|
|
1208
|
+
say ''
|
|
1200
1209
|
say "Let's set up your Google Play credentials.", :bold
|
|
1201
|
-
say
|
|
1210
|
+
say ''
|
|
1202
1211
|
say "Step 1: Create a Service Account (if you don't have one)", :bold
|
|
1203
|
-
say
|
|
1204
|
-
say
|
|
1205
|
-
say
|
|
1206
|
-
say
|
|
1207
|
-
say
|
|
1208
|
-
say
|
|
1212
|
+
say ''
|
|
1213
|
+
say '1. Go to Google Play Console:', :cyan
|
|
1214
|
+
say ' https://play.google.com/console', :green
|
|
1215
|
+
say ''
|
|
1216
|
+
say '2. Navigate to: Settings → API access', :cyan
|
|
1217
|
+
say ''
|
|
1209
1218
|
say "3. Click 'Create new service account' or 'Link existing service account'", :cyan
|
|
1210
|
-
say
|
|
1211
|
-
say
|
|
1219
|
+
say ''
|
|
1220
|
+
say '4. In Google Cloud Console, create a Service Account with:', :cyan
|
|
1212
1221
|
say " • Name: 'My Signer CLI' (or anything)"
|
|
1213
|
-
say
|
|
1214
|
-
say
|
|
1215
|
-
say
|
|
1216
|
-
say
|
|
1217
|
-
say
|
|
1218
|
-
say
|
|
1219
|
-
say
|
|
1222
|
+
say ' • Role: Editor or Admin'
|
|
1223
|
+
say ''
|
|
1224
|
+
say '5. Create a JSON key for the service account', :cyan
|
|
1225
|
+
say ' • Click on the service account → Keys → Add Key → JSON'
|
|
1226
|
+
say ' • Download the JSON file'
|
|
1227
|
+
say ''
|
|
1228
|
+
say '6. Back in Play Console, grant the service account access:', :cyan
|
|
1220
1229
|
say " • Click 'Done' in the modal"
|
|
1221
|
-
say
|
|
1222
|
-
say
|
|
1223
|
-
say
|
|
1224
|
-
|
|
1225
|
-
unless yes_with_default?(
|
|
1226
|
-
say
|
|
1227
|
-
say
|
|
1228
|
-
say
|
|
1229
|
-
say
|
|
1230
|
+
say ' • Click on the service account'
|
|
1231
|
+
say ' • Set permissions: Release apps, Manage production releases'
|
|
1232
|
+
say ''
|
|
1233
|
+
|
|
1234
|
+
unless yes_with_default?('Have you created and downloaded your service account JSON?', :green)
|
|
1235
|
+
say ''
|
|
1236
|
+
say '⏭️ You can set this up later with:', :yellow
|
|
1237
|
+
say ' • mysigner doctor (will prompt for setup)', :cyan
|
|
1238
|
+
say ' • Or via the web dashboard', :cyan
|
|
1230
1239
|
return false
|
|
1231
1240
|
end
|
|
1232
|
-
say
|
|
1241
|
+
say ''
|
|
1233
1242
|
|
|
1234
1243
|
# Prompt for JSON file path with retry
|
|
1235
1244
|
max_retries = 3
|
|
@@ -1238,12 +1247,12 @@ module Mysigner
|
|
|
1238
1247
|
service_account_json = nil
|
|
1239
1248
|
|
|
1240
1249
|
while attempts < max_retries
|
|
1241
|
-
say
|
|
1242
|
-
say
|
|
1243
|
-
say
|
|
1244
|
-
say
|
|
1245
|
-
json_path = ask(
|
|
1246
|
-
say
|
|
1250
|
+
say 'Step 2: Locate your service account JSON file', :bold
|
|
1251
|
+
say ''
|
|
1252
|
+
say '💡 Tip: You can drag & drop the file into terminal to get the path', :cyan
|
|
1253
|
+
say ''
|
|
1254
|
+
json_path = ask('Enter the path to your service account JSON file:').strip.gsub(/^['"]|['"]$/, '')
|
|
1255
|
+
say ''
|
|
1247
1256
|
|
|
1248
1257
|
# Expand ~ to home directory
|
|
1249
1258
|
json_path = File.expand_path(json_path)
|
|
@@ -1251,7 +1260,7 @@ module Mysigner
|
|
|
1251
1260
|
if File.exist?(json_path)
|
|
1252
1261
|
begin
|
|
1253
1262
|
service_account_json = File.read(json_path).strip
|
|
1254
|
-
|
|
1263
|
+
|
|
1255
1264
|
# Validate it looks like a service account JSON
|
|
1256
1265
|
parsed = JSON.parse(service_account_json)
|
|
1257
1266
|
unless parsed['type'] == 'service_account' && parsed['client_email'] && parsed['private_key']
|
|
@@ -1260,16 +1269,16 @@ module Mysigner
|
|
|
1260
1269
|
attempts += 1
|
|
1261
1270
|
next
|
|
1262
1271
|
end
|
|
1263
|
-
|
|
1264
|
-
say
|
|
1272
|
+
|
|
1273
|
+
say '✓ Valid service account JSON detected', :green
|
|
1265
1274
|
say " Email: #{parsed['client_email']}", :cyan
|
|
1266
|
-
say
|
|
1275
|
+
say ''
|
|
1267
1276
|
break # Success!
|
|
1268
1277
|
rescue JSON::ParserError => e
|
|
1269
1278
|
error "Invalid JSON file: #{e.message}"
|
|
1270
1279
|
attempts += 1
|
|
1271
1280
|
next
|
|
1272
|
-
rescue => e
|
|
1281
|
+
rescue StandardError => e
|
|
1273
1282
|
error "Failed to read file: #{e.message}"
|
|
1274
1283
|
attempts += 1
|
|
1275
1284
|
next
|
|
@@ -1277,100 +1286,98 @@ module Mysigner
|
|
|
1277
1286
|
else
|
|
1278
1287
|
error "File not found: #{json_path}"
|
|
1279
1288
|
attempts += 1
|
|
1280
|
-
|
|
1289
|
+
|
|
1281
1290
|
if attempts < max_retries
|
|
1282
|
-
say
|
|
1291
|
+
say ''
|
|
1283
1292
|
say "Please try again (attempt #{attempts + 1}/#{max_retries})", :yellow
|
|
1284
|
-
say
|
|
1293
|
+
say ''
|
|
1285
1294
|
end
|
|
1286
1295
|
end
|
|
1287
1296
|
end
|
|
1288
1297
|
|
|
1289
1298
|
unless service_account_json
|
|
1290
|
-
say
|
|
1299
|
+
say ''
|
|
1291
1300
|
error "Could not read JSON file after #{max_retries} attempts"
|
|
1292
1301
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1293
1302
|
return false
|
|
1294
1303
|
end
|
|
1295
1304
|
|
|
1296
1305
|
# Prompt for credential name
|
|
1297
|
-
say
|
|
1298
|
-
say
|
|
1306
|
+
say 'Step 3: Name this credential', :bold
|
|
1307
|
+
say ''
|
|
1299
1308
|
say "Choose a name to identify this service account (e.g., 'Production', 'CI/CD')", :cyan
|
|
1300
1309
|
say "Default: 'CLI Setup' - just press Enter to use it", :cyan
|
|
1301
|
-
say
|
|
1302
|
-
|
|
1310
|
+
say ''
|
|
1311
|
+
|
|
1303
1312
|
credential_name = nil
|
|
1304
1313
|
while credential_name.nil? || credential_name.empty?
|
|
1305
|
-
name_input = ask(
|
|
1306
|
-
credential_name = name_input.empty? ?
|
|
1307
|
-
|
|
1314
|
+
name_input = ask('Credential name:').strip
|
|
1315
|
+
credential_name = name_input.empty? ? 'CLI Setup' : name_input
|
|
1316
|
+
|
|
1308
1317
|
if credential_name.empty?
|
|
1309
|
-
error
|
|
1310
|
-
say
|
|
1318
|
+
error 'Name cannot be empty'
|
|
1319
|
+
say ''
|
|
1311
1320
|
end
|
|
1312
1321
|
end
|
|
1313
|
-
say
|
|
1322
|
+
say ''
|
|
1314
1323
|
say "→ Using name: '#{credential_name}'", :cyan
|
|
1315
|
-
say
|
|
1316
|
-
|
|
1324
|
+
say ''
|
|
1325
|
+
|
|
1317
1326
|
# Validate and upload
|
|
1318
|
-
say
|
|
1319
|
-
say
|
|
1327
|
+
say 'Step 4: Saving credentials...', :bold
|
|
1328
|
+
say ''
|
|
1320
1329
|
|
|
1321
1330
|
begin
|
|
1322
|
-
response = client.post("/api/v1/organizations/#{org_id}/google_play_credentials",
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
say
|
|
1333
|
-
say
|
|
1334
|
-
say "Details:", :cyan
|
|
1331
|
+
response = client.post("/api/v1/organizations/#{org_id}/google_play_credentials",
|
|
1332
|
+
body: {
|
|
1333
|
+
google_play_credential: {
|
|
1334
|
+
name: credential_name,
|
|
1335
|
+
service_account_json: service_account_json,
|
|
1336
|
+
active: true
|
|
1337
|
+
}
|
|
1338
|
+
})
|
|
1339
|
+
|
|
1340
|
+
say '✓ Google Play credentials saved!', :green
|
|
1341
|
+
say ''
|
|
1342
|
+
say 'Details:', :cyan
|
|
1335
1343
|
say " • Name: #{credential_name}"
|
|
1336
|
-
say
|
|
1337
|
-
say
|
|
1338
|
-
|
|
1344
|
+
say ' • Status: Active ✓'
|
|
1345
|
+
say ''
|
|
1346
|
+
|
|
1339
1347
|
# Test the connection
|
|
1340
|
-
say
|
|
1341
|
-
|
|
1348
|
+
say 'Testing connection to Google Play...', :yellow
|
|
1349
|
+
|
|
1342
1350
|
begin
|
|
1343
1351
|
cred_id = response[:data]['id']
|
|
1344
1352
|
client.post("/api/v1/organizations/#{org_id}/google_play_credentials/#{cred_id}/test")
|
|
1345
|
-
say
|
|
1346
|
-
rescue => e
|
|
1353
|
+
say '✓ Successfully connected to Google Play API!', :green
|
|
1354
|
+
rescue StandardError => e
|
|
1347
1355
|
say "⚠️ Connection test failed: #{e.message}", :yellow
|
|
1348
|
-
say
|
|
1356
|
+
say ' The credentials are saved but may need verification', :yellow
|
|
1349
1357
|
end
|
|
1350
|
-
|
|
1351
|
-
say ""
|
|
1352
|
-
say "🎉 Google Play is now configured!", :green
|
|
1353
|
-
say ""
|
|
1354
|
-
return true
|
|
1355
1358
|
|
|
1359
|
+
say ''
|
|
1360
|
+
say '🎉 Google Play is now configured!', :green
|
|
1361
|
+
say ''
|
|
1362
|
+
true
|
|
1356
1363
|
rescue Mysigner::ClientError => e
|
|
1357
1364
|
error_msg = e.message
|
|
1358
|
-
|
|
1359
|
-
if error_msg.include?(
|
|
1365
|
+
|
|
1366
|
+
if error_msg.include?('already been taken') || error_msg.include?('validation')
|
|
1360
1367
|
error "A credential with the name '#{credential_name}' already exists"
|
|
1361
|
-
say
|
|
1362
|
-
say
|
|
1368
|
+
say ''
|
|
1369
|
+
say 'Please choose a different name and try again.', :yellow
|
|
1363
1370
|
else
|
|
1364
1371
|
error "Failed to configure credentials: #{error_msg}"
|
|
1365
1372
|
end
|
|
1366
|
-
|
|
1367
|
-
say
|
|
1373
|
+
|
|
1374
|
+
say ''
|
|
1368
1375
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1369
|
-
|
|
1370
|
-
rescue => e
|
|
1376
|
+
false
|
|
1377
|
+
rescue StandardError => e
|
|
1371
1378
|
error "Unexpected error: #{e.message}"
|
|
1372
1379
|
say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
|
|
1373
|
-
|
|
1380
|
+
false
|
|
1374
1381
|
end
|
|
1375
1382
|
end
|
|
1376
1383
|
end
|