mysigner 0.1.1 → 0.1.3

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