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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'faraday'
2
4
  require 'json'
3
5
 
@@ -15,11 +17,11 @@ module Mysigner
15
17
 
16
18
  # Run the interactive wizard
17
19
  def run!
18
- puts ""
19
- puts "🧙 Manual Signing Setup Wizard"
20
- puts "=" * 80
21
- puts ""
22
-
20
+ puts ''
21
+ puts '🧙 Manual Signing Setup Wizard'
22
+ puts '=' * 80
23
+ puts ''
24
+
23
25
  # Check if we're configuring all targets
24
26
  if @options[:all_targets]
25
27
  configure_all_targets
@@ -30,107 +32,105 @@ module Mysigner
30
32
 
31
33
  def configure_all_targets
32
34
  targets = @parser.signable_targets
33
-
35
+
34
36
  if targets.empty?
35
- error "No signable targets found in project"
37
+ error 'No signable targets found in project'
36
38
  return
37
39
  end
38
-
40
+
39
41
  puts "Found #{targets.count} signable target(s):"
40
42
  targets.each do |info|
41
43
  type_label = info[:type] == :app ? '📱 App' : '🧩 Extension'
42
44
  puts " #{type_label}: #{info[:name]}"
43
45
  end
44
- puts ""
45
-
46
- print "Configure all targets? (y/n): "
46
+ puts ''
47
+
48
+ print 'Configure all targets? (y/n): '
47
49
  confirm = get_input.downcase
48
-
49
- unless confirm == 'y' || confirm == 'yes'
50
- puts "Cancelled"
50
+
51
+ unless %w[y yes].include?(confirm)
52
+ puts 'Cancelled'
51
53
  return
52
54
  end
53
-
54
- puts ""
55
-
55
+
56
+ puts ''
57
+
56
58
  # Configure each target
57
59
  successful = 0
58
60
  failed = 0
59
-
61
+
60
62
  targets.each_with_index do |info, index|
61
- puts ""
62
- puts "=" * 80
63
+ puts ''
64
+ puts '=' * 80
63
65
  puts "Configuring #{index + 1}/#{targets.count}: #{info[:name]}"
64
- puts "=" * 80
65
- puts ""
66
-
66
+ puts '=' * 80
67
+ puts ''
68
+
67
69
  if configure_single_target(info[:name], skip_header: true)
68
70
  successful += 1
69
71
  else
70
72
  failed += 1
71
- puts ""
72
- print "Continue with remaining targets? (y/n): "
73
+ puts ''
74
+ print 'Continue with remaining targets? (y/n): '
73
75
  continue = get_input.downcase
74
- unless continue == 'y' || continue == 'yes'
75
- break
76
- end
76
+ break unless %w[y yes].include?(continue)
77
77
  end
78
78
  end
79
-
80
- puts ""
81
- puts "=" * 80
79
+
80
+ puts ''
81
+ puts '=' * 80
82
82
  puts "✅ Completed: #{successful} successful, #{failed} failed"
83
- puts "=" * 80
84
- puts ""
85
- puts "Next steps:"
86
- puts " 1. Test build: mysigner build"
87
- puts " 2. Or ship to TestFlight: mysigner ship testflight"
88
- puts ""
83
+ puts '=' * 80
84
+ puts ''
85
+ puts 'Next steps:'
86
+ puts ' 1. Test build: mysigner build'
87
+ puts ' 2. Or ship to TestFlight: mysigner ship testflight'
88
+ puts ''
89
89
  end
90
90
 
91
91
  def configure_single_target(target_name = nil, skip_header: false)
92
92
  unless skip_header
93
93
  # No header needed, already printed in run!
94
94
  end
95
-
95
+
96
96
  # Step 1: Detect or validate target
97
97
  target_name = detect_target(target_name)
98
98
  return false unless target_name
99
-
99
+
100
100
  @current_target = target_name
101
-
101
+
102
102
  # Step 1.5: Check if project's team matches current org
103
103
  check_org_team_match(target_name) if @options[:check_org_match] != false
104
-
104
+
105
105
  # Step 2: Show current configuration
106
106
  show_current_config(target_name)
107
-
107
+
108
108
  # Step 3: Select team
109
109
  team_id = select_team(target_name)
110
110
  return false unless team_id
111
-
111
+
112
112
  # Step 4: Select provisioning profile
113
113
  profile = select_profile(target_name, team_id)
114
114
  return false unless profile
115
-
115
+
116
116
  # Step 5: Apply configuration
117
117
  apply_configuration(target_name, team_id, profile)
118
-
118
+
119
119
  # Step 6: Validate
120
120
  validate_configuration(target_name, team_id)
121
-
121
+
122
122
  unless skip_header
123
- puts ""
124
- puts "=" * 80
125
- puts "✅ Signing configuration complete!"
126
- puts "=" * 80
127
- puts ""
128
- puts "Next steps:"
129
- puts " 1. Test build: mysigner build"
130
- puts " 2. Or ship to TestFlight: mysigner ship testflight"
131
- puts ""
123
+ puts ''
124
+ puts '=' * 80
125
+ puts '✅ Signing configuration complete!'
126
+ puts '=' * 80
127
+ puts ''
128
+ puts 'Next steps:'
129
+ puts ' 1. Test build: mysigner build'
130
+ puts ' 2. Or ship to TestFlight: mysigner ship testflight'
131
+ puts ''
132
132
  end
133
-
133
+
134
134
  true
135
135
  end
136
136
 
@@ -142,98 +142,96 @@ module Mysigner
142
142
  begin
143
143
  @parser.find_target(target_name)
144
144
  puts "📱 Target: #{target_name}"
145
- puts ""
145
+ puts ''
146
146
  return target_name
147
- rescue => e
147
+ rescue StandardError => e
148
148
  error "Target '#{target_name}' not found: #{e.message}"
149
149
  return nil
150
150
  end
151
151
  end
152
-
152
+
153
153
  # No target provided, auto-detect or let user choose
154
154
  targets = @parser.app_targets
155
-
155
+
156
156
  if targets.empty?
157
- error "No app targets found in project"
157
+ error 'No app targets found in project'
158
158
  return nil
159
159
  end
160
-
161
- if targets.count == 1
160
+
161
+ if targets.one?
162
162
  target = targets.first
163
163
  puts "📱 Target: #{target.name}"
164
- puts ""
164
+ puts ''
165
165
  return target.name
166
166
  end
167
-
167
+
168
168
  # Multiple targets - let user choose
169
- puts "Multiple app targets found:"
169
+ puts 'Multiple app targets found:'
170
170
  targets.each_with_index do |target, i|
171
171
  puts " #{i + 1}. #{target.name}"
172
172
  end
173
- puts ""
174
-
173
+ puts ''
174
+
175
175
  print "Select target (1-#{targets.count}): "
176
176
  choice = get_input.to_i
177
-
177
+
178
178
  if choice < 1 || choice > targets.count
179
- error "Invalid selection"
179
+ error 'Invalid selection'
180
180
  return nil
181
181
  end
182
-
182
+
183
183
  targets[choice - 1].name
184
184
  end
185
185
 
186
186
  def show_current_config(target_name)
187
- puts "Current Configuration:"
188
- puts "-" * 80
189
-
187
+ puts 'Current Configuration:'
188
+ puts '-' * 80
189
+
190
190
  bundle_id = @parser.bundle_id(target_name)
191
191
  puts " Bundle ID: #{bundle_id || 'Not set'}"
192
-
192
+
193
193
  team_id = @parser.team_id(target_name)
194
194
  puts " Team: #{team_id || 'Not set'}"
195
-
195
+
196
196
  sign_style = @parser.code_sign_style(target_name)
197
197
  puts " Signing: #{sign_style || 'Not set'}"
198
-
198
+
199
199
  if @parser.signing_configured?(target_name)
200
200
  profile_name = @parser.project.targets.find { |t| t.name == target_name }
201
- &.build_configurations&.first
201
+ &.build_configurations&.first
202
202
  &.build_settings&.[]('PROVISIONING_PROFILE_SPECIFIER')
203
203
  puts " Profile: #{profile_name || 'Auto'}"
204
204
  end
205
-
206
- puts ""
205
+
206
+ puts ''
207
207
  end
208
208
 
209
209
  def select_team(target_name)
210
- puts "Step 1: Select Development Team"
211
- puts "-" * 80
212
- puts ""
213
-
210
+ puts 'Step 1: Select Development Team'
211
+ puts '-' * 80
212
+ puts ''
213
+
214
214
  current_team = @parser.team_id(target_name)
215
-
215
+
216
216
  # Option 1: Keep current team
217
- if current_team && !current_team.empty?
218
- puts " 1. Keep current team: #{current_team}"
219
- end
220
-
217
+ puts " 1. Keep current team: #{current_team}" if current_team && !current_team.empty?
218
+
221
219
  # Option 2: Fetch from API
222
220
  puts " #{current_team ? '2' : '1'}. Fetch from My Signer API"
223
-
221
+
224
222
  # Option 3: Enter manually
225
223
  puts " #{current_team ? '3' : '2'}. Enter team ID manually"
226
-
227
- puts ""
228
- print "Select option: "
224
+
225
+ puts ''
226
+ print 'Select option: '
229
227
  choice = get_input.to_i
230
228
 
231
229
  case choice
232
230
  when 1
233
231
  if current_team
234
232
  puts "✓ Using current team: #{current_team}"
235
- puts ""
236
- return current_team
233
+ puts ''
234
+ current_team
237
235
  else
238
236
  # Fetch from API
239
237
  fetch_team_from_api
@@ -247,172 +245,169 @@ module Mysigner
247
245
  when 3
248
246
  enter_team_manually
249
247
  else
250
- error "Invalid selection"
248
+ error 'Invalid selection'
251
249
  nil
252
250
  end
253
251
  end
254
252
 
255
253
  def fetch_team_from_api
256
- puts ""
257
- puts "Fetching team from My Signer..."
258
-
254
+ puts ''
255
+ puts 'Fetching team from My Signer...'
256
+
259
257
  begin
260
258
  response = @client.get("/api/v1/organizations/#{@organization_id}")
261
259
  team_id = response.dig(:data, 'app_store_connect_team_id') || response['app_store_connect_team_id']
262
-
260
+
263
261
  if team_id && !team_id.empty?
264
262
  puts "✓ Found team: #{team_id}"
265
- puts ""
266
- return team_id
263
+ puts ''
264
+ team_id
267
265
  else
268
- error "Team ID not saved in My Signer API (database)"
269
- puts ""
266
+ error 'Team ID not saved in My Signer API (database)'
267
+ puts ''
270
268
  current_team = @parser.team_id(@current_target)
271
269
  puts "Note: Your Xcode project already has team: #{current_team}" if current_team
272
- puts ""
273
- puts "You can either:"
274
- puts " 1. Keep your current Xcode team (go back and select option 1)"
275
- puts " 2. Add it to My Signer web: https://mysigner.dev"
276
- puts " → Open your organization → App Store Connect → Edit/Add credentials → Team ID field"
277
- puts " 3. Enter it manually (go back and select option 3)"
278
- puts ""
270
+ puts ''
271
+ puts 'You can either:'
272
+ puts ' 1. Keep your current Xcode team (go back and select option 1)'
273
+ puts ' 2. Add it to My Signer web: https://mysigner.dev'
274
+ puts ' → Open your organization → App Store Connect → Edit/Add credentials → Team ID field'
275
+ puts ' 3. Enter it manually (go back and select option 3)'
276
+ puts ''
279
277
  nil
280
278
  end
281
- rescue => e
279
+ rescue StandardError => e
282
280
  error "Failed to fetch team: #{e.message}"
283
281
  nil
284
282
  end
285
283
  end
286
284
 
287
285
  def enter_team_manually
288
- puts ""
289
- print "Enter Team ID (10 characters): "
286
+ puts ''
287
+ print 'Enter Team ID (10 characters): '
290
288
  team_id = get_input
291
-
289
+
292
290
  if team_id =~ /^[A-Z0-9]{10}$/
293
291
  puts "✓ Team ID: #{team_id}"
294
- puts ""
295
- return team_id
292
+ puts ''
293
+ team_id
296
294
  else
297
- error "Invalid Team ID format (must be 10 alphanumeric characters)"
295
+ error 'Invalid Team ID format (must be 10 alphanumeric characters)'
298
296
  nil
299
297
  end
300
298
  end
301
299
 
302
- def select_profile(target_name, team_id)
303
- puts "Step 2: Select Provisioning Profile"
304
- puts "-" * 80
305
- puts ""
306
-
300
+ def select_profile(target_name, _team_id)
301
+ puts 'Step 2: Select Provisioning Profile'
302
+ puts '-' * 80
303
+ puts ''
304
+
307
305
  # Fetch profiles from API
308
- puts "Fetching provisioning profiles..."
309
-
306
+ puts 'Fetching provisioning profiles...'
307
+
310
308
  begin
311
309
  bundle_id = @parser.bundle_id(target_name)
312
-
310
+
313
311
  # Get profiles for this bundle ID
314
- response = @client.get("/api/v1/organizations/#{@organization_id}/profiles",
312
+ response = @client.get("/api/v1/organizations/#{@organization_id}/profiles",
315
313
  params: { bundle_id: bundle_id })
316
-
314
+
317
315
  profiles = response[:data]['profiles'] || []
318
-
316
+
319
317
  if profiles.empty?
320
318
  puts "No provisioning profiles found for bundle ID: #{bundle_id}"
321
- puts ""
322
- puts "Options:"
323
- puts " 1. Auto-create App Store profile (recommended)"
324
- puts " 2. Auto-create Development profile"
325
- puts " 3. Create manually and sync"
326
- puts " 4. Skip"
327
- puts ""
328
-
329
- print "Select option (1-4): "
319
+ puts ''
320
+ puts 'Options:'
321
+ puts ' 1. Auto-create App Store profile (recommended)'
322
+ puts ' 2. Auto-create Development profile'
323
+ puts ' 3. Create manually and sync'
324
+ puts ' 4. Skip'
325
+ puts ''
326
+
327
+ print 'Select option (1-4): '
330
328
  choice = get_input
331
- puts ""
332
-
329
+ puts ''
330
+
333
331
  case choice
334
- when "1"
332
+ when '1'
335
333
  profile = auto_create_profile(bundle_id, :appstore)
336
334
  return profile if profile
337
- return nil
338
- when "2"
335
+
336
+ when '2'
339
337
  profile = auto_create_profile(bundle_id, :development)
340
338
  return profile if profile
341
- return nil
342
- when "3"
343
- puts "Create profile at: https://developer.apple.com/account/resources/profiles/add"
344
- puts "Then sync from My Signer web dashboard"
345
- puts ""
346
- return nil
347
- when "4"
348
- puts "Skipped profile selection"
349
- return nil
339
+
340
+ when '3'
341
+ puts 'Create profile at: https://developer.apple.com/account/resources/profiles/add'
342
+ puts 'Then sync from My Signer web dashboard'
343
+ puts ''
344
+ when '4'
345
+ puts 'Skipped profile selection'
350
346
  else
351
- error "Invalid selection"
352
- return nil
347
+ error 'Invalid selection'
353
348
  end
349
+ return nil
354
350
  end
355
-
351
+
356
352
  # Filter profiles by type (development vs distribution)
357
353
  dev_profiles = profiles.select { |p| p['profile_type']&.include?('DEVELOPMENT') }
358
354
  dist_profiles = profiles.select do |p|
359
355
  type = p['profile_type']
360
356
  type&.include?('DISTRIBUTION') || type&.include?('APP_STORE') || type&.include?('ADHOC') || type&.include?('INHOUSE')
361
357
  end
362
-
363
- puts ""
364
- puts "Available Profiles:"
365
- puts ""
366
-
358
+
359
+ puts ''
360
+ puts 'Available Profiles:'
361
+ puts ''
362
+
367
363
  all_profiles = []
368
-
364
+
369
365
  if dev_profiles.any?
370
- puts " Development Profiles:"
371
- dev_profiles.each_with_index do |profile, i|
366
+ puts ' Development Profiles:'
367
+ dev_profiles.each_with_index do |profile, _i|
372
368
  all_profiles << profile
373
369
  status = profile['status'] == 'ACTIVE' ? '✓' : '✗'
374
370
  puts " #{all_profiles.count}. #{status} #{profile['name']}"
375
371
  puts " Expires: #{profile['expires_at']&.split('T')&.first || 'Unknown'}"
376
372
  end
377
- puts ""
373
+ puts ''
378
374
  end
379
-
375
+
380
376
  if dist_profiles.any?
381
- puts " Distribution Profiles:"
382
- dist_profiles.each_with_index do |profile, i|
377
+ puts ' Distribution Profiles:'
378
+ dist_profiles.each_with_index do |profile, _i|
383
379
  all_profiles << profile
384
380
  status = profile['status'] == 'ACTIVE' ? '✓' : '✗'
385
381
  puts " #{all_profiles.count}. #{status} #{profile['name']}"
386
382
  puts " Expires: #{profile['expires_at']&.split('T')&.first || 'Unknown'}"
387
383
  end
388
- puts ""
384
+ puts ''
389
385
  end
390
-
386
+
391
387
  print "Select profile (1-#{all_profiles.count}): "
392
388
  choice = get_input.to_i
393
-
389
+
394
390
  if choice < 1 || choice > all_profiles.count
395
- error "Invalid selection"
391
+ error 'Invalid selection'
396
392
  return nil
397
393
  end
398
-
394
+
399
395
  selected = all_profiles[choice - 1]
400
396
  puts "✓ Selected: #{selected['name']}"
401
- puts ""
402
-
397
+ puts ''
398
+
403
399
  # Download and install the profile
404
400
  download_and_install_profile(selected)
405
-
401
+
406
402
  selected
407
-
408
- rescue => e
403
+ rescue StandardError => e
409
404
  error "Failed to fetch profiles: #{e.message}"
410
405
  nil
411
406
  end
412
407
  end
413
408
 
414
409
  def download_and_install_profile(profile)
415
- puts "Downloading and installing profile..."
410
+ puts 'Downloading and installing profile...'
416
411
 
417
412
  begin
418
413
  # Download profile using direct Faraday connection for binary data
@@ -430,23 +425,22 @@ module Mysigner
430
425
  end
431
426
 
432
427
  unless response.success?
433
- if response.headers['content-type']&.include?('json')
434
- begin
435
- error_data = JSON.parse(response.body)
436
- raise "Download failed: #{error_data['message'] || error_data['error']}"
437
- rescue JSON::ParserError
438
- raise "Download failed with status #{response.status}"
439
- end
440
- else
428
+ raise "Download failed with status #{response.status}" unless response.headers['content-type']&.include?('json')
429
+
430
+ begin
431
+ error_data = JSON.parse(response.body)
432
+ raise "Download failed: #{error_data['message'] || error_data['error']}"
433
+ rescue JSON::ParserError
441
434
  raise "Download failed with status #{response.status}"
442
435
  end
436
+
443
437
  end
444
438
 
445
439
  profile_content = response.body
446
440
 
447
441
  # Create profiles directory if it doesn't exist
448
- profiles_dir = File.expand_path("~/Library/MobileDevice/Provisioning Profiles")
449
- FileUtils.mkdir_p(profiles_dir) unless Dir.exist?(profiles_dir)
442
+ profiles_dir = File.expand_path('~/Library/MobileDevice/Provisioning Profiles')
443
+ FileUtils.mkdir_p(profiles_dir)
450
444
 
451
445
  # Generate filename (use UUID if available, otherwise sanitized name)
452
446
  uuid = profile['uuid'] || profile['id']
@@ -457,156 +451,153 @@ module Mysigner
457
451
  File.binwrite(output_path, profile_content)
458
452
 
459
453
  puts "✓ Profile installed: #{output_path}"
460
- puts ""
461
-
462
- rescue => e
454
+ puts ''
455
+ rescue StandardError => e
463
456
  # Non-fatal error - profile might still work if already installed
464
457
  puts "⚠️ Could not auto-install profile: #{e.message}"
465
- puts " You may need to install it manually by double-clicking the .mobileprovision file"
466
- puts ""
458
+ puts ' You may need to install it manually by double-clicking the .mobileprovision file'
459
+ puts ''
467
460
  end
468
461
  end
469
462
 
470
463
  def apply_configuration(target_name, team_id, profile)
471
- puts "Step 3: Applying Configuration"
472
- puts "-" * 80
473
- puts ""
474
-
464
+ puts 'Step 3: Applying Configuration'
465
+ puts '-' * 80
466
+ puts ''
467
+
475
468
  puts "Setting up manual signing for target: #{target_name}"
476
469
  puts " Team: #{team_id}"
477
470
  puts " Profile: #{profile['name']}"
478
- puts ""
479
-
471
+ puts ''
472
+
480
473
  target = @parser.project.targets.find { |t| t.name == target_name }
481
-
482
- unless target
483
- raise WizardError, "Target not found: #{target_name}"
484
- end
485
-
474
+
475
+ raise WizardError, "Target not found: #{target_name}" unless target
476
+
486
477
  # Update build configurations
487
478
  target.build_configurations.each do |config|
488
479
  puts " Configuring #{config.name}..."
489
-
480
+
490
481
  # Set manual signing
491
482
  config.build_settings['CODE_SIGN_STYLE'] = 'Manual'
492
-
483
+
493
484
  # Set team
494
485
  config.build_settings['DEVELOPMENT_TEAM'] = team_id
495
-
486
+
496
487
  # Set provisioning profile
497
488
  config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = profile['name']
498
-
489
+
499
490
  # Set code sign identity
500
491
  profile_type = profile['profile_type']
501
- if profile_type&.include?('DEVELOPMENT')
502
- config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Development'
503
- else
504
- config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Distribution'
505
- end
492
+ config.build_settings['CODE_SIGN_IDENTITY'] = if profile_type&.include?('DEVELOPMENT')
493
+ 'Apple Development'
494
+ else
495
+ 'Apple Distribution'
496
+ end
506
497
  end
507
-
498
+
508
499
  # Save project
509
500
  @parser.project.save
510
-
511
- puts "✓ Configuration applied"
512
- puts ""
501
+
502
+ puts '✓ Configuration applied'
503
+ puts ''
513
504
  end
514
505
 
515
506
  def validate_configuration(target_name, team_id)
516
- puts "Step 4: Validating Configuration"
517
- puts "-" * 80
518
- puts ""
519
-
507
+ puts 'Step 4: Validating Configuration'
508
+ puts '-' * 80
509
+ puts ''
510
+
520
511
  validator = Validator.new(@parser, target_name, 'Release', team_id: team_id)
521
512
  result = validator.validate
522
-
513
+
523
514
  if result[:valid]
524
- puts "✓ Configuration is valid"
525
-
515
+ puts '✓ Configuration is valid'
516
+
526
517
  if result[:warnings].any?
527
- puts ""
528
- puts "Warnings:"
518
+ puts ''
519
+ puts 'Warnings:'
529
520
  result[:warnings].each do |warning|
530
521
  puts " ⚠️ #{warning}"
531
522
  end
532
523
  end
533
524
  else
534
- puts "✗ Configuration has errors:"
535
- puts ""
525
+ puts '✗ Configuration has errors:'
526
+ puts ''
536
527
  result[:errors].each do |error|
537
528
  puts " • #{error}"
538
529
  end
539
- puts ""
540
- raise WizardError, "Configuration validation failed"
530
+ puts ''
531
+ raise WizardError, 'Configuration validation failed'
541
532
  end
542
-
543
- puts ""
533
+
534
+ puts ''
544
535
  end
545
536
 
546
537
  def check_org_team_match(target_name)
547
538
  project_team = @parser.team_id(target_name)
548
539
  return unless project_team # No team set in project yet
549
-
540
+
550
541
  begin
551
542
  # Fetch current org's team
552
543
  response = @client.get("/api/v1/organizations/#{@organization_id}")
553
544
  org_name = response.dig(:data, 'name') || response['name']
554
545
  org_team = response.dig(:data, 'app_store_connect_team_id') || response['app_store_connect_team_id']
555
-
546
+
556
547
  if org_team && org_team != project_team
557
- puts ""
558
- puts "⚠️ Warning: Organization / Team Mismatch", :yellow
559
- puts "=" * 80
560
- puts ""
548
+ puts ''
549
+ puts '⚠️ Warning: Organization / Team Mismatch', :yellow
550
+ puts '=' * 80
551
+ puts ''
561
552
  puts "Your Xcode project uses team: #{project_team}"
562
553
  puts "Current My Signer org: #{org_name} (Team: #{org_team})"
563
- puts ""
564
- puts "This means your project belongs to a different Apple Developer account"
554
+ puts ''
555
+ puts 'This means your project belongs to a different Apple Developer account'
565
556
  puts "than the organization you're currently using in My Signer."
566
- puts ""
567
- puts "What this means:"
557
+ puts ''
558
+ puts 'What this means:'
568
559
  puts " • Profiles/certificates fetched will be for team #{org_team}"
569
560
  puts " • But your project needs resources for team #{project_team}"
570
- puts " • This will likely cause signing errors"
571
- puts ""
572
- puts "To fix this:"
573
- puts " 1. Exit this wizard (Ctrl+C)"
574
- puts " 2. Run: mysigner switch"
561
+ puts ' • This will likely cause signing errors'
562
+ puts ''
563
+ puts 'To fix this:'
564
+ puts ' 1. Exit this wizard (Ctrl+C)'
565
+ puts ' 2. Run: mysigner switch'
575
566
  puts " 3. Select the organization that has team #{project_team}"
576
- puts " 4. Run this wizard again"
577
- puts ""
578
- print "Continue anyway? (y/N): "
567
+ puts ' 4. Run this wizard again'
568
+ puts ''
569
+ print 'Continue anyway? (y/N): '
579
570
  answer = get_input.downcase
580
571
 
581
- unless answer == 'y' || answer == 'yes'
582
- puts ""
583
- puts "Wizard cancelled. Please switch organizations and try again."
572
+ unless %w[y yes].include?(answer)
573
+ puts ''
574
+ puts 'Wizard cancelled. Please switch organizations and try again.'
584
575
  exit 0
585
576
  end
586
- puts ""
577
+ puts ''
587
578
  elsif !org_team
588
579
  # Current org has no team configured
589
- puts ""
590
- puts "ℹ️ Note: Current organization has no Team ID configured", :cyan
591
- puts "=" * 80
592
- puts ""
580
+ puts ''
581
+ puts 'ℹ️ Note: Current organization has no Team ID configured', :cyan
582
+ puts '=' * 80
583
+ puts ''
593
584
  puts "Your Xcode project uses team: #{project_team}"
594
585
  puts "Current My Signer org: #{org_name} (No team configured)"
595
- puts ""
586
+ puts ''
596
587
  puts "You can continue, but the wizard won't be able to fetch the team from My Signer."
597
- puts "Consider adding Team ID to this org at: https://mysigner.dev"
598
- puts ""
599
- print "Continue? (Y/n): "
588
+ puts 'Consider adding Team ID to this org at: https://mysigner.dev'
589
+ puts ''
590
+ print 'Continue? (Y/n): '
600
591
  answer = get_input.downcase
601
592
 
602
- if answer == 'n' || answer == 'no'
603
- puts ""
604
- puts "Wizard cancelled."
593
+ if %w[n no].include?(answer)
594
+ puts ''
595
+ puts 'Wizard cancelled.'
605
596
  exit 0
606
597
  end
607
- puts ""
598
+ puts ''
608
599
  end
609
- rescue => e
600
+ rescue StandardError => e
610
601
  # Ignore errors in org checking - don't block the wizard
611
602
  puts "Warning: Could not verify organization match: #{e.message}" if ENV['DEBUG']
612
603
  end
@@ -614,37 +605,35 @@ module Mysigner
614
605
 
615
606
  def auto_create_profile(bundle_id, type)
616
607
  puts "Creating #{type} profile for #{bundle_id}..."
617
- puts ""
618
-
608
+ puts ''
609
+
619
610
  profile_type = type == :appstore ? 'IOS_APP_STORE' : 'IOS_APP_DEVELOPMENT'
620
-
611
+
621
612
  begin
622
613
  # Sync first to ensure we have latest resources
623
- puts " Syncing organization resources..."
614
+ puts ' Syncing organization resources...'
624
615
  @client.post("/api/v1/organizations/#{@organization_id}/sync_app_store_connect")
625
-
616
+
626
617
  # Wait for sync
627
618
  sleep 2
628
-
619
+
629
620
  # Check sync status
630
621
  max_wait = 15
631
622
  waited = 0
632
-
623
+
633
624
  while waited < max_wait
634
625
  status_response = @client.get("/api/v1/organizations/#{@organization_id}/sync/status")
635
626
  sync_data = status_response[:data]['sync']
636
-
637
- if !sync_data['running']
638
- break
639
- end
640
-
627
+
628
+ break unless sync_data['running']
629
+
641
630
  sleep 1
642
631
  waited += 1
643
632
  end
644
-
645
- puts " ✓ Sync complete"
646
- puts ""
647
-
633
+
634
+ puts ' ✓ Sync complete'
635
+ puts ''
636
+
648
637
  # Create profile
649
638
  puts " Creating #{profile_type} profile..."
650
639
  response = @client.post(
@@ -654,116 +643,115 @@ module Mysigner
654
643
  profile_type: profile_type
655
644
  }
656
645
  )
657
-
646
+
658
647
  profile = response[:data]['profile']
659
648
  puts " ✓ Created profile: #{profile['name']}"
660
- puts ""
661
-
649
+ puts ''
650
+
662
651
  # Download and install
663
652
  download_and_install_profile(profile)
664
-
653
+
665
654
  profile
666
655
  rescue Mysigner::ClientError => e
667
656
  error_msg = e.message
668
-
669
- if error_msg.include?("bundle_id_not_found")
657
+
658
+ if error_msg.include?('bundle_id_not_found')
670
659
  error "Bundle ID '#{bundle_id}' not found"
671
- puts ""
672
- puts "Register it at: https://developer.apple.com/account/resources/identifiers/add"
673
- puts "Then sync in the web dashboard"
674
- elsif error_msg.include?("certificates found") || error_msg.include?("no_certificates")
675
- cert_name = type == :appstore ? "Apple Distribution" : "Apple Development"
676
-
660
+ puts ''
661
+ puts 'Register it at: https://developer.apple.com/account/resources/identifiers/add'
662
+ puts 'Then sync in the web dashboard'
663
+ elsif error_msg.include?('certificates found') || error_msg.include?('no_certificates')
664
+ cert_name = type == :appstore ? 'Apple Distribution' : 'Apple Development'
665
+
677
666
  error "No #{cert_name} certificates found"
678
- puts ""
679
-
680
- print "Generate CSR automatically? [Y/n] "
667
+ puts ''
668
+
669
+ print 'Generate CSR automatically? [Y/n] '
681
670
  response = get_input.downcase
682
671
 
672
+ puts ''
683
673
  if response.empty? || response == 'y' || response == 'yes'
684
- puts ""
685
674
  csr_path = generate_csr_for_wizard
686
-
675
+
687
676
  if csr_path
688
- puts ""
677
+ puts ''
689
678
  puts " ✓ CSR ready: #{File.basename(csr_path)}"
690
- puts ""
691
- puts " 📋 Next steps:"
692
- puts " 1. https://developer.apple.com/account/resources/certificates/add"
693
- puts " 2. Select: '#{cert_name}' (or older 'iOS' variant if available)"
694
- puts " 3. Upload: #{csr_path}"
695
- puts " 4. Download .cer → Double-click → Sync → Try again"
696
- puts ""
679
+ puts ''
680
+ puts ' 📋 Next steps:'
681
+ puts ' 1. https://developer.apple.com/account/resources/certificates/add'
682
+ puts " 2. Select: '#{cert_name}' (or older 'iOS' variant if available)"
683
+ puts " 3. Upload: #{csr_path}"
684
+ puts ' 4. Download .cer → Double-click → Sync → Try again'
685
+ puts ''
697
686
  end
698
687
  else
699
- puts ""
700
- puts "Quick fix:"
701
- puts " 1. Open Keychain Access → Request Certificate (save CSR)"
702
- puts " 2. https://developer.apple.com/account/resources/certificates/add"
688
+ puts 'Quick fix:'
689
+ puts ' 1. Open Keychain Access → Request Certificate (save CSR)'
690
+ puts ' 2. https://developer.apple.com/account/resources/certificates/add'
703
691
  puts " 3. Select '#{cert_name}' → Upload CSR → Download .cer"
704
- puts " 4. Double-click .cer → Sync My Signer → Try again"
705
- puts ""
692
+ puts ' 4. Double-click .cer → Sync My Signer → Try again'
693
+ puts ''
706
694
  end
707
- elsif error_msg.include?("no_devices") || error_msg.include?("devices found")
708
- error "No test devices registered"
709
- puts ""
710
- puts "Quick fix:"
711
- puts " • Get UDID: Connect device → Finder → Click serial number"
712
- puts " • Run: mysigner device add <UDID> <NAME>"
713
- puts ""
695
+ elsif error_msg.include?('no_devices') || error_msg.include?('devices found')
696
+ error 'No test devices registered'
697
+ puts ''
698
+ puts 'Quick fix:'
699
+ puts ' • Get UDID: Connect device → Finder → Click serial number'
700
+ puts ' • Run: mysigner device add <UDID> <NAME>'
701
+ puts ''
714
702
  else
715
703
  error "Failed to create profile: #{error_msg}"
716
704
  end
717
- puts ""
705
+ puts ''
718
706
  nil
719
- rescue => e
707
+ rescue StandardError => e
720
708
  error "Unexpected error: #{e.message}"
721
- puts ""
709
+ puts ''
722
710
  nil
723
711
  end
724
712
  end
725
713
 
726
714
  def generate_csr_for_wizard
727
715
  require 'openssl'
728
-
716
+
729
717
  begin
730
718
  # Save to Downloads (visible in file picker)
731
- csr_dir = File.expand_path("~/Downloads")
719
+ csr_dir = File.expand_path('~/Downloads')
732
720
  FileUtils.mkdir_p(csr_dir)
733
-
721
+
734
722
  # Generate RSA key pair
735
723
  key = OpenSSL::PKey::RSA.new(2048)
736
-
724
+
737
725
  # Create CSR
738
726
  csr = OpenSSL::X509::Request.new
739
727
  csr.version = 0
740
728
  csr.subject = OpenSSL::X509::Name.new([
741
- ['CN', 'My Signer User'],
742
- ['emailAddress', 'user@example.com']
743
- ])
729
+ ['CN', 'My Signer User'],
730
+ ['emailAddress', 'user@example.com']
731
+ ])
744
732
  csr.public_key = key.public_key
745
- csr.sign(key, OpenSSL::Digest::SHA256.new)
746
-
733
+ csr.sign(key, OpenSSL::Digest.new('SHA256'))
734
+
747
735
  # Generate unique filename
748
736
  timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
749
737
  csr_filename = "CertificateSigningRequest_#{timestamp}.certSigningRequest"
750
738
  key_filename = "private_key_#{timestamp}.pem"
751
-
739
+
752
740
  # Save CSR to Downloads (visible)
753
741
  csr_path = File.join(csr_dir, csr_filename)
754
-
742
+
755
743
  # Save private key to hidden location (secure)
756
- key_dir = File.expand_path("~/.mysigner/keys")
744
+ key_dir = File.expand_path('~/.mysigner/keys')
757
745
  FileUtils.mkdir_p(key_dir)
758
746
  key_path = File.join(key_dir, key_filename)
759
-
747
+
760
748
  # Save files
761
749
  File.write(csr_path, csr.to_pem)
762
750
  File.write(key_path, key.to_pem)
763
- File.chmod(0600, key_path)
764
-
751
+ File.chmod(0o600, key_path)
752
+
765
753
  csr_path
766
- rescue => e
754
+ rescue StandardError => e
767
755
  puts " ✗ Failed to generate CSR: #{e.message}"
768
756
  nil
769
757
  end
@@ -771,7 +759,7 @@ module Mysigner
771
759
 
772
760
  # Safely get user input, returns empty string if STDIN is closed or nil
773
761
  def get_input
774
- input = STDIN.gets
762
+ input = $stdin.gets
775
763
  input ? input.strip : ''
776
764
  end
777
765
 
@@ -781,4 +769,3 @@ module Mysigner
781
769
  end
782
770
  end
783
771
  end
784
-