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.
Files changed (56) 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 +4 -0
  6. data/.rubocop.yml +55 -0
  7. data/.rubocop_todo.yml +126 -0
  8. data/CHANGELOG.md +96 -0
  9. data/Gemfile +5 -3
  10. data/Gemfile.lock +38 -8
  11. data/README.md +14 -16
  12. data/Rakefile +5 -3
  13. data/bin/console +4 -3
  14. data/bin/setup +3 -0
  15. data/certificate_.cer +0 -0
  16. data/exe/mysigner +19 -2
  17. data/iOS_App_Store_Profile.mobileprovision +1 -0
  18. data/iOS_Distribution_Certificate.cer +1 -0
  19. data/lib/mysigner/build/android_executor.rb +83 -63
  20. data/lib/mysigner/build/android_parser.rb +33 -40
  21. data/lib/mysigner/build/configurator.rb +17 -16
  22. data/lib/mysigner/build/detector.rb +39 -50
  23. data/lib/mysigner/build/error_analyzer.rb +70 -68
  24. data/lib/mysigner/build/executor.rb +30 -37
  25. data/lib/mysigner/build/parser.rb +18 -18
  26. data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
  27. data/lib/mysigner/cli/auth_commands.rb +771 -764
  28. data/lib/mysigner/cli/build_commands.rb +962 -796
  29. data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
  30. data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
  31. data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
  32. data/lib/mysigner/cli/concerns/helpers.rb +44 -1
  33. data/lib/mysigner/cli/diagnostic_commands.rb +667 -636
  34. data/lib/mysigner/cli/resource_commands.rb +1153 -985
  35. data/lib/mysigner/cli/validate_commands.rb +25 -25
  36. data/lib/mysigner/cli.rb +11 -1
  37. data/lib/mysigner/client.rb +27 -19
  38. data/lib/mysigner/config.rb +161 -60
  39. data/lib/mysigner/export/exporter.rb +38 -37
  40. data/lib/mysigner/signing/certificate_checker.rb +18 -23
  41. data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
  42. data/lib/mysigner/signing/keystore_manager.rb +81 -61
  43. data/lib/mysigner/signing/validator.rb +38 -40
  44. data/lib/mysigner/signing/wizard.rb +329 -342
  45. data/lib/mysigner/upload/app_store_automation.rb +96 -49
  46. data/lib/mysigner/upload/app_store_submission.rb +87 -92
  47. data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
  48. data/lib/mysigner/upload/play_store_uploader.rb +164 -144
  49. data/lib/mysigner/upload/uploader.rb +136 -115
  50. data/lib/mysigner/version.rb +3 -1
  51. data/lib/mysigner.rb +13 -11
  52. data/mysigner.gemspec +36 -33
  53. data/profile_.mobileprovision +0 -0
  54. data/test_manual.rb +37 -36
  55. metadata +44 -17
  56. 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 "version", "Show CLI version and system information"
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('../../../..', __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 '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 "login", "Log in with existing API token (⭐ first-timers: use 'onboard' instead)"
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 "⚠️ Already logged in", :yellow
35
- say ""
36
- say "Current configuration:", :yellow
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?("Do you want to logout and login with different user? (y/n)")
45
+ say ''
46
+
47
+ if yes?('Do you want to logout and login with different user? (y/n)')
43
48
  config.clear
44
- say "✓ Logged out successfully", :green
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 "🔐 My Signer Login", :cyan
55
- say "=" * 80, :cyan
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("API Token:", echo: false)
70
- say "" # New line after hidden input
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 "API token cannot be empty"
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 "Validating token and email...", :yellow
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 "✓ Token valid", :green
91
+ say '✓ Token valid', :green
87
92
  else
88
- error "Connection failed"
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 "Detecting organization...", :yellow
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 "No organizations found for this token"
109
- say ""
110
- say "This might mean:", :yellow
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 " • The token was created but the organization was deleted", :yellow
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 "DEBUG: Organization data received", :cyan if ENV['DEBUG']
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 "=" * 80, :green
144
- say "✓ Successfully logged in!", :green
145
- say "=" * 80, :green
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 "🔒 Security Note:", :yellow
152
- say " Your token is organization-specific and can only access", :yellow
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 "🚀 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
-
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 "Authentication failed"
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?("use your own token")
172
- say "⚠️ Token Email Mismatch", :yellow
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 "This could mean:", :yellow
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 "💡 Solutions:", :cyan
181
- say " 1. Generate a new token from your own account at:", :cyan
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 " 3. Check that you entered the correct email address", :cyan
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 "onboard", "⭐ START HERE - Complete setup wizard for new users"
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 "🚀 My Signer Setup Guide", :cyan
210
- say "=" * 80, :cyan
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 "Current configuration:", :cyan
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 !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
-
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
- # 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
257
+ invoke :status
264
258
  return
265
259
  when '2'
266
- invoke :status
260
+ invoke :switch
267
261
  return
268
262
  when '3'
269
- say "Clearing configuration...", :yellow
270
- say ""
263
+ say 'Clearing configuration...', :yellow
264
+ say ''
271
265
  # Continue with full onboarding
272
266
  when '4'
273
- say "No changes made.", :green
267
+ say 'No changes made.', :green
274
268
  return
275
269
  end
276
270
  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
-
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
- invoke :status
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 :switch
302
+ invoke :status
301
303
  return
302
304
  when '3'
303
- say "Clearing configuration...", :yellow
304
- say ""
305
+ say 'Clearing configuration...', :yellow
306
+ say ''
305
307
  # Continue with full onboarding
306
308
  when '4'
307
- say "No changes made.", :green
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?("Do you want to re-configure from scratch?", :yellow)
316
- say ""
317
- say "Keeping existing configuration.", :green
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 "Clearing existing configuration...", :yellow
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 "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
-
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 "1. Open your browser and go to:", :bold
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 "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 ""
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 "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
-
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 "1. Go to the dashboard:", :bold
383
+ say ''
384
+ say '1. Go to the dashboard:', :bold
383
385
  say " #{api_url}", :green
384
- say ""
385
- say "2. Sign in with your account", :bold
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
- 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 ""
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 "Step 3: Generate API Token", :cyan
402
- say "-" * 80
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 "1. Go to API Tokens:", :bold
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 " Or navigate: Dashboard → Your Organization → API Tokens", :cyan
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 "3. Fill in the details:", :bold
414
+ say ''
415
+ say '3. Fill in the details:', :bold
414
416
  say " • Name: 'CLI Access' (or anything you like)"
415
- say " • Scopes: ✓ read ✓ write (minimum required)"
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?("Have you generated and copied your token?", :green)
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 "Step 4: Login to CLI", :cyan
431
- say "-" * 80
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("Paste your API Token:", echo: false)
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 "Token cannot be empty"
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 "Validating token and email...", :yellow
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 "Connection test failed"
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 "No organizations found"
465
- say "Please check that your token is associated with an organization", :yellow
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 "Step 5: App Store Connect Setup (optional but recommended)", :cyan
486
- say "-" * 80
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 "✓ 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
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 "💡 Tip: You can add multiple credentials (for different teams)", :cyan
505
- say ""
503
+ say ''
504
+ say '💡 Tip: You can add multiple credentials (for different teams)', :cyan
505
+ say ''
506
506
  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
-
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 "⏭️ 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 ""
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 "=" * 80, :green
531
- say "🎉 Setup Complete!", :green
532
- say "=" * 80, :green
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 "✓ App Store Connect: Configured", :green
543
+ say '✓ App Store Connect: Configured', :green
544
544
  elsif defined?(asc_choice) && asc_choice == '1'
545
- say "⚠️ App Store Connect:", :yellow
546
- say " Setup was attempted but not completed", :yellow
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 "⚠️ App Store Connect: Not configured", :yellow
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 "🔒 Security Note:", :yellow
552
+
553
+ say ''
554
+ say '🔒 Security Note:', :yellow
555
555
  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
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 "Authentication failed"
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?("use your own token")
575
- say "⚠️ Token Email Mismatch", :yellow
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 "Please make sure you:", :yellow
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 " 3. Enter the correct email address", :yellow
581
+ say ' 3. Enter the correct email address', :yellow
583
582
  else
584
- say "The token you entered is invalid. Please:", :yellow
585
- say " 1. Check you copied the entire token"
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 " 3. Generate a new token if needed"
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 "logout", "Log out and clear stored credentials"
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 "No stored credentials found", :yellow
602
+ say 'No stored credentials found', :yellow
604
603
  return
605
604
  end
606
605
 
607
- if yes?("Are you sure you want to logout? (y/n)")
606
+ if yes?('Are you sure you want to logout? (y/n)')
608
607
  config.clear
609
- say "✓ Successfully logged out", :green
608
+ say '✓ Successfully logged out', :green
610
609
  say "Config file removed: #{Config::CONFIG_FILE}", :green
611
610
  else
612
- say "Logout cancelled", :yellow
611
+ say 'Logout cancelled', :yellow
613
612
  end
614
613
  end
615
614
 
616
- desc "status", "Check connection, credentials, and App Store Connect setup"
615
+ desc 'status', 'Check connection, credentials, and App Store Connect setup'
617
616
  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
617
+ config = load_config
626
618
 
627
- say "📊 My Signer Status", :cyan
628
- say ""
629
- say "Configuration:", :bold
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 "Current Organization:", :bold
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 ? " (current)" : ""
649
- org_name = config.org_name(org_id) || "Unknown"
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 "Connection:", :bold
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 " Status: ✓ Connected", :green
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 "App Store Connect:", :bold
666
+ say 'App Store Connect:', :bold
675
667
  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
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 " ✗ Not configured", :yellow
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 " Status: ✗ Unauthorized (invalid token)", :red
676
+ say ' Status: ✗ Unauthorized (invalid token)', :red
687
677
  exit 1
688
678
  rescue Mysigner::ConnectionError => e
689
- say " Status: ✗ Connection failed", :red
679
+ say ' Status: ✗ Connection failed', :red
690
680
  say " Error: #{e.message}", :red
691
681
  exit 1
692
- rescue => e
693
- say " Status: ✗ Error", :red
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 "orgs", "List all organizations you're a member of"
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 "📋 Organizations", :cyan
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 "No organizations found", :yellow
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 ? " (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 "Legend: ✓ = Has token | ⚠️ = Need token", :white
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 "switch", "Switch between organizations (for multi-org users)"
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 "🔄 Switch Organization", :cyan
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 "Current organization:", :yellow
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 "You only have access to one organization.", :yellow
794
- say "Nothing to switch to!", :yellow
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 " to generate tokens for them first (in the web dashboard).", :cyan
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 "Available organizations:", :cyan
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 ? " (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 "Legend: ✓ = Has token | ⚠️ = Needs token", :white
829
- say ""
830
-
831
- # Let user select
832
- org_index = ask("Select organization (1-#{organizations_list.length}, or 'q' to cancel):")
833
-
834
- if org_index.downcase == 'q'
835
- say "Cancelled", :yellow
836
- return
837
- end
838
-
839
- unless org_index.match(/^\d+$/) && org_index.to_i.between?(1, organizations_list.length)
840
- error "Invalid selection"
841
- return
842
- end
843
-
844
- selected_org = organizations_list[org_index.to_i - 1]
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 "Already using this organization!", :yellow
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 "To switch to this organization:", :cyan
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 " 2. Generate a new API token"
860
- say " 3. Paste it below"
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 "Cancelled", :yellow
872
+ say 'Cancelled', :yellow
868
873
  return
869
874
  end
870
-
875
+
871
876
  # Validate the new token (with email validation)
872
- say "Validating token...", :yellow
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 "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
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 "Please generate a token from the correct organization.", :yellow
898
+ say ''
899
+ say 'Please generate a token from the correct organization.', :yellow
894
900
  exit 1
895
901
  end
896
-
897
- say "✓ Token validated successfully", :green
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 "Token validation failed"
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?("use your own token")
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 "The token you provided is not valid.", :yellow
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 "config", "Show current CLI configuration (API URL, tokens, org)"
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 "⚙️ Configuration", :cyan
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, 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"
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
- break # Success!
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
- 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 ""
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
- # 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"
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
- # 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)
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
- # 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 ""
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
- 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
- )
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
- 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}"
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
- say " • Team ID: (will be extracted after first sync)"
1171
+ error 'Validation failed'
1172
+ say "Setup skipped. Run 'mysigner doctor' to try again.", :yellow
1173
+ false
1155
1174
  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"
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
- 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"
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, config, org_id)
1198
- say "🤖 Google Play Service Account Setup", :cyan
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 "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 ""
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 "4. In Google Cloud Console, create a Service Account with:", :cyan
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 " • 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
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 " • 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
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 "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 ""
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 "✓ Valid service account JSON detected", :green
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 "Step 3: Name this credential", :bold
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("Credential name:").strip
1306
- credential_name = name_input.empty? ? "CLI Setup" : name_input
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 "Name cannot be empty"
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 "Step 4: Saving credentials...", :bold
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
- 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
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 " • Status: Active ✓"
1337
- say ""
1338
-
1344
+ say ' • Status: Active ✓'
1345
+ say ''
1346
+
1339
1347
  # Test the connection
1340
- say "Testing connection to Google Play...", :yellow
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 "✓ Successfully connected to Google Play API!", :green
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 " The credentials are saved but may need verification", :yellow
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?("already been taken") || error_msg.include?("validation")
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 "Please choose a different name and try again.", :yellow
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
- return false
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
- return false
1380
+ false
1374
1381
  end
1375
1382
  end
1376
1383
  end