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,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mysigner
2
4
  class CLI < Thor
3
5
  module ResourceCommands
4
6
  def self.included(base)
5
7
  base.class_eval do
6
- desc "devices", "List registered test devices (UDIDs)"
8
+ desc 'devices', 'List registered test devices (UDIDs)'
7
9
  method_option :platform, type: :string, aliases: '-p', desc: 'Filter by platform (IOS, MAC_OS, TV_OS)'
8
10
  method_option :status, type: :string, aliases: '-s', desc: 'Filter by status (ENABLED, DISABLED)'
9
11
  method_option :search, type: :string, aliases: '-q', desc: 'Search by name or UDID'
@@ -13,8 +15,8 @@ module Mysigner
13
15
  config = load_config
14
16
  client = create_client(config)
15
17
 
16
- say "📱 Devices", :cyan
17
- say ""
18
+ say '📱 Devices', :cyan
19
+ say ''
18
20
 
19
21
  # Build query params
20
22
  params = {
@@ -31,8 +33,8 @@ module Mysigner
31
33
  pagination = response[:data]['pagination']
32
34
 
33
35
  if devices.empty?
34
- say "No devices found", :yellow
35
- say ""
36
+ say 'No devices found', :yellow
37
+ say ''
36
38
  say "Tip: Register a device with 'mysigner device add NAME UDID'", :yellow
37
39
  return
38
40
  end
@@ -41,21 +43,19 @@ module Mysigner
41
43
  devices.each do |device|
42
44
  status_icon = device['status'] == 'ENABLED' ? '✓' : '✗'
43
45
  status_color = device['status'] == 'ENABLED' ? :green : :red
44
-
46
+
45
47
  say " #{status_icon} #{device['name']} (ID: #{device['id']})", status_color
46
48
  say " UDID: #{device['udid']}"
47
49
  say " Platform: #{device['platform']} | Class: #{device['device_class']}"
48
50
  say " Status: #{device['status']}"
49
- say ""
51
+ say ''
50
52
  end
51
53
 
52
54
  # Show pagination
53
55
  if pagination
54
56
  say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)", :yellow
55
57
 
56
- if pagination['page'] < pagination['total_pages']
57
- say "Run with --page #{pagination['page'] + 1} to see more", :yellow
58
- end
58
+ say "Run with --page #{pagination['page'] + 1} to see more", :yellow if pagination['page'] < pagination['total_pages']
59
59
  end
60
60
  rescue Mysigner::ClientError => e
61
61
  error "Failed to fetch devices: #{e.message}"
@@ -63,57 +63,57 @@ module Mysigner
63
63
  end
64
64
  end
65
65
 
66
- desc "device SUBCOMMAND", "Manage test devices (detect, add, update)"
66
+ desc 'device SUBCOMMAND', 'Manage test devices (detect, add, update)'
67
67
  long_desc <<~DESC
68
68
  Register and manage test devices for development builds.
69
-
69
+
70
70
  WHY REGISTER DEVICES?
71
-
71
+
72
72
  To install development/adhoc builds on physical devices, you must register
73
73
  their UDIDs (Unique Device Identifiers) with Apple and include them in your
74
74
  provisioning profiles.
75
-
75
+
76
76
  SUBCOMMANDS:
77
-
77
+
78
78
  mysigner device detect
79
79
  Auto-detect connected iOS devices and show their UDIDs
80
-
80
+ #{' '}
81
81
  mysigner device add NAME UDID [--platform IOS]
82
82
  Register a new device for testing
83
-
83
+ #{' '}
84
84
  mysigner device update ID NEW_NAME
85
85
  Rename an existing device
86
-
86
+
87
87
  HOW TO GET A DEVICE UDID:
88
-
88
+
89
89
  Method 1 - Auto-detect (Recommended):
90
90
  mysigner device detect
91
-
91
+ #{' '}
92
92
  This will find all connected iOS devices and let you register them
93
93
  interactively. No need to open any other apps.
94
-
94
+
95
95
  Method 2 - Via Finder:
96
96
  1. Connect your iPhone/iPad to your Mac
97
97
  2. Open Finder and select your device in the sidebar
98
98
  3. Click on the device info to reveal UDID
99
99
  4. Right-click → Copy UDID
100
-
100
+
101
101
  EXAMPLES:
102
-
102
+
103
103
  # Register your iPhone
104
104
  mysigner device add "My iPhone 15" 00008030-001A1B2C3D4E567F
105
-
105
+ #{' '}
106
106
  # Register an iPad
107
107
  mysigner device add "Test iPad" da83bb40dba39e35d258988d856508798db7afba
108
-
108
+ #{' '}
109
109
  # Register a Mac for Mac Catalyst apps
110
110
  mysigner device add "MacBook Pro" ABC123... --platform MAC_OS
111
-
111
+ #{' '}
112
112
  # Rename a device (use ID from 'mysigner devices' list)
113
113
  mysigner device update 42 "John's iPhone"
114
-
114
+
115
115
  NOTES:
116
-
116
+
117
117
  • UDIDs are 40 hex characters (0-9, a-f) or 25 characters for newer devices
118
118
  • You can register up to 100 devices per year per account
119
119
  • After registering, regenerate your provisioning profiles to include the device
@@ -129,14 +129,14 @@ module Mysigner
129
129
  detect_connected_devices(config, client)
130
130
  when 'add'
131
131
  if args.length < 2
132
- error "Usage: mysigner device add NAME UDID [--platform IOS]"
133
- say ""
134
- say "Example: mysigner device add \"My iPhone\" 00008030-001A1B2C3D4E567F", :yellow
135
- say ""
132
+ error 'Usage: mysigner device add NAME UDID [--platform IOS]'
133
+ say ''
134
+ say 'Example: mysigner device add "My iPhone" 00008030-001A1B2C3D4E567F', :yellow
135
+ say ''
136
136
  say "💡 Don't know your UDID? Run:", :cyan
137
- say " mysigner device detect", :cyan
138
- say ""
139
- say " This will auto-detect connected devices and let you register them.", :cyan
137
+ say ' mysigner device detect', :cyan
138
+ say ''
139
+ say ' This will auto-detect connected devices and let you register them.', :cyan
140
140
  exit 1
141
141
  end
142
142
 
@@ -144,8 +144,25 @@ module Mysigner
144
144
  udid = args[1]
145
145
  platform = options[:platform].upcase
146
146
 
147
- say "📱 Registering device...", :cyan
148
- say ""
147
+ # Client-side UDID sanity check — catches obvious typos and
148
+ # copy-paste errors before they hit Apple's API (which, for IOS
149
+ # at least, has historically accepted synthetic/all-zeros UDIDs
150
+ # in sandbox environments). Skipped for non-IOS platforms.
151
+ if (platform == 'IOS') && !valid_ios_udid?(udid)
152
+ error "Invalid iOS UDID: #{udid}"
153
+ say ''
154
+ say 'iOS UDIDs must:', :yellow
155
+ say ' • Be either 25 chars (older devices) or 40 hex chars (newer)', :cyan
156
+ say ' • Be hexadecimal (0-9, A-F)', :cyan
157
+ say ' • Not be trivially synthetic (e.g. all zeros, single repeated char)', :cyan
158
+ say ''
159
+ say '💡 Get a real UDID:', :cyan
160
+ say ' mysigner device detect', :yellow
161
+ exit 1
162
+ end
163
+
164
+ say '📱 Registering device...', :cyan
165
+ say ''
149
166
 
150
167
  begin
151
168
  response = client.post(
@@ -158,15 +175,15 @@ module Mysigner
158
175
  )
159
176
 
160
177
  device = response[:data]['device']
161
- say "✓ Device registered successfully!", :green
162
- say ""
163
- say "Details:", :bold
178
+ say '✓ Device registered successfully!', :green
179
+ say ''
180
+ say 'Details:', :bold
164
181
  say " Name: #{device['name']}"
165
182
  say " UDID: #{device['udid']}"
166
183
  say " Platform: #{device['platform']}"
167
184
  say " Status: #{device['status']}"
168
185
  rescue Mysigner::ValidationError => e
169
- error "Validation failed:"
186
+ error 'Validation failed:'
170
187
  if e.details
171
188
  e.details.each do |field, errors|
172
189
  say " #{field}: #{errors.join(', ')}", :red
@@ -177,8 +194,8 @@ module Mysigner
177
194
  say " Suggestion: #{e.suggestion}", :yellow if e.suggestion
178
195
  exit 1
179
196
  rescue Mysigner::ClientError => e
180
- if e.message.include?("already exists")
181
- error "Device with this UDID already exists"
197
+ if e.message.include?('already exists')
198
+ error 'Device with this UDID already exists'
182
199
  else
183
200
  error "Failed to register device: #{e.message}"
184
201
  end
@@ -186,11 +203,11 @@ module Mysigner
186
203
  end
187
204
  when 'update'
188
205
  if args.length < 2
189
- error "Usage: mysigner device update ID NEW_NAME"
190
- say ""
206
+ error 'Usage: mysigner device update ID NEW_NAME'
207
+ say ''
191
208
  say "Example: mysigner device update 42 \"John's iPhone\"", :yellow
192
- say ""
193
- say "💡 To get device IDs:", :cyan
209
+ say ''
210
+ say '💡 To get device IDs:', :cyan
194
211
  say " Run 'mysigner devices' to see all devices with their IDs", :cyan
195
212
  exit 1
196
213
  end
@@ -198,8 +215,8 @@ module Mysigner
198
215
  device_id = args[0]
199
216
  new_name = args[1]
200
217
 
201
- say "📱 Updating device...", :cyan
202
- say ""
218
+ say '📱 Updating device...', :cyan
219
+ say ''
203
220
 
204
221
  begin
205
222
  # Get device details first
@@ -208,7 +225,7 @@ module Mysigner
208
225
 
209
226
  say "Current name: #{device['name']}", :yellow
210
227
  say "New name: #{new_name}", :green
211
- say ""
228
+ say ''
212
229
 
213
230
  # Update device
214
231
  response = client.patch(
@@ -217,9 +234,9 @@ module Mysigner
217
234
  )
218
235
 
219
236
  updated_device = response[:data]['device']
220
- say "✓ Device updated successfully!", :green
221
- say ""
222
- say "Details:", :bold
237
+ say '✓ Device updated successfully!', :green
238
+ say ''
239
+ say 'Details:', :bold
223
240
  say " Name: #{updated_device['name']}"
224
241
  say " UDID: #{updated_device['udid']}"
225
242
  say " Platform: #{updated_device['platform']}"
@@ -234,7 +251,7 @@ module Mysigner
234
251
  invoke :help, ['device']
235
252
  else
236
253
  error "Unknown action: #{action}"
237
- say "Available actions: detect, add, update, help", :yellow
254
+ say 'Available actions: detect, add, update, help', :yellow
238
255
  exit 1
239
256
  end
240
257
  end
@@ -242,34 +259,32 @@ module Mysigner
242
259
  private
243
260
 
244
261
  def detect_connected_devices(config, client)
245
- say "🔍 Detecting connected iOS devices...", :cyan
246
- say ""
262
+ say '🔍 Detecting connected iOS devices...', :cyan
263
+ say ''
247
264
 
248
265
  devices = []
249
266
 
250
267
  # Try system_profiler first (built-in macOS)
251
- if system("which system_profiler > /dev/null 2>&1")
268
+ if system('which system_profiler > /dev/null 2>&1')
252
269
  output = `system_profiler SPUSBDataType 2>/dev/null`
253
-
270
+
254
271
  # Parse iOS devices from system_profiler output
255
272
  current_device = nil
256
273
  output.each_line do |line|
257
274
  if line =~ /^\s{4}(\S.+):$/
258
275
  # New top-level USB device
259
- current_device = { name: $1.strip }
276
+ current_device = { name: ::Regexp.last_match(1).strip }
260
277
  elsif current_device
261
278
  if line =~ /Serial Number:\s*([A-Fa-f0-9-]+)/
262
- serial = $1.gsub('-', '')
279
+ serial = ::Regexp.last_match(1).gsub('-', '')
263
280
  # iOS device serials are typically 24-40 hex chars
264
- if serial.length >= 24 && serial.length <= 40
265
- current_device[:udid] = serial
266
- end
281
+ current_device[:udid] = serial if serial.length.between?(24, 40)
267
282
  elsif line =~ /Product ID:\s*(0x12[aA][0-9a-fA-F])/
268
283
  # Apple mobile device product IDs start with 0x12a
269
284
  current_device[:is_ios] = true
270
285
  end
271
286
  end
272
-
287
+
273
288
  if current_device && current_device[:udid] && current_device[:is_ios]
274
289
  devices << current_device
275
290
  current_device = nil
@@ -278,76 +293,78 @@ module Mysigner
278
293
  end
279
294
 
280
295
  # Also try idevice_id if available (more reliable)
281
- if system("which idevice_id > /dev/null 2>&1")
296
+ if system('which idevice_id > /dev/null 2>&1')
282
297
  output = `idevice_id -l 2>/dev/null`.strip
283
298
  output.each_line do |line|
284
299
  udid = line.strip
285
300
  next if udid.empty?
286
-
301
+
287
302
  # Get device name if ideviceinfo is available
288
- name = "iOS Device"
289
- if system("which ideviceinfo > /dev/null 2>&1")
303
+ name = 'iOS Device'
304
+ if system('which ideviceinfo > /dev/null 2>&1')
290
305
  device_name = `ideviceinfo -u #{udid} -k DeviceName 2>/dev/null`.strip
291
306
  name = device_name unless device_name.empty?
292
307
  end
293
-
308
+
294
309
  # Avoid duplicates
295
- unless devices.any? { |d| d[:udid] == udid }
296
- devices << { name: name, udid: udid }
297
- end
310
+ devices << { name: name, udid: udid } unless devices.any? { |d| d[:udid] == udid }
298
311
  end
299
312
  end
300
313
 
301
314
  # Also try xcrun xctrace (Xcode command line tools)
302
- if devices.empty? && system("which xcrun > /dev/null 2>&1")
303
- output = `xcrun xctrace list devices 2>/dev/null`
315
+ if devices.empty? && system('which xcrun > /dev/null 2>&1')
316
+ # xctrace output can contain non-ASCII bytes (emoji in device
317
+ # names, accented characters). Force UTF-8 + scrub invalid
318
+ # sequences so `.strip` / `.each_line` don't raise
319
+ # Encoding::CompatibilityError under the default US-ASCII locale.
320
+ output = `xcrun xctrace list devices 2>/dev/null`.force_encoding('UTF-8').scrub
304
321
  in_devices_section = false
305
-
322
+
306
323
  output.each_line do |line|
307
324
  line = line.strip
308
-
325
+
309
326
  # Track sections
310
- if line == "== Devices =="
327
+ if line == '== Devices =='
311
328
  in_devices_section = true
312
329
  next
313
- elsif line == "== Simulators =="
330
+ elsif line == '== Simulators =='
314
331
  in_devices_section = false
315
332
  next
316
333
  end
317
-
334
+
318
335
  next unless in_devices_section
319
336
  next if line.empty?
320
-
337
+
321
338
  # Format: "Name (Version) (UDID)" - iOS devices have UDIDs starting with 0000
322
339
  # Skip Macs which have UUID format
323
- if line =~ /^(.+?)\s+\([^)]+\)\s+\((0000[A-Fa-f0-9-]+)\)\s*$/
324
- name = $1.strip
325
- udid = $2.gsub('-', '')
326
- devices << { name: name, udid: udid }
327
- end
340
+ next unless line =~ /^(.+?)\s+\([^)]+\)\s+\((0000[A-Fa-f0-9-]+)\)\s*$/
341
+
342
+ name = ::Regexp.last_match(1).strip
343
+ udid = ::Regexp.last_match(2).gsub('-', '')
344
+ devices << { name: name, udid: udid }
328
345
  end
329
346
  end
330
347
 
331
348
  if devices.empty?
332
- say "No iOS devices detected.", :yellow
333
- say ""
334
- say "Make sure:", :cyan
335
- say " 1. Your device is connected via USB", :cyan
336
- say " 2. The device is unlocked", :cyan
349
+ say 'No iOS devices detected.', :yellow
350
+ say ''
351
+ say 'Make sure:', :cyan
352
+ say ' 1. Your device is connected via USB', :cyan
353
+ say ' 2. The device is unlocked', :cyan
337
354
  say " 3. You've trusted this computer on the device", :cyan
338
- say ""
339
- say "💡 For better detection, install libimobiledevice:", :yellow
340
- say " brew install libimobiledevice", :yellow
355
+ say ''
356
+ say '💡 For better detection, install libimobiledevice:', :yellow
357
+ say ' brew install libimobiledevice', :yellow
341
358
  return
342
359
  end
343
360
 
344
361
  say "Found #{devices.length} device(s):", :green
345
- say ""
362
+ say ''
346
363
 
347
364
  devices.each_with_index do |device, idx|
348
365
  say " #{idx + 1}. #{device[:name]}", :green
349
366
  say " UDID: #{device[:udid]}", :white
350
- say ""
367
+ say ''
351
368
  end
352
369
 
353
370
  # Check if running interactively
@@ -355,20 +372,20 @@ module Mysigner
355
372
 
356
373
  # Ask if user wants to register
357
374
  say "Would you like to register a device? (Enter number, or 'n' to skip)", :cyan
358
- choice = ask(">")&.strip || ''
375
+ choice = ask('>')&.strip || ''
359
376
 
360
377
  return if choice.downcase == 'n' || choice.empty?
361
378
 
362
379
  idx = choice.to_i - 1
363
380
  if idx >= 0 && idx < devices.length
364
381
  device = devices[idx]
365
-
366
- say ""
382
+
383
+ say ''
367
384
  say "Enter a name for this device (or press Enter to use '#{device[:name]}'):", :cyan
368
- custom_name = ask(">")&.strip || ''
385
+ custom_name = ask('>')&.strip || ''
369
386
  name = custom_name.empty? ? device[:name] : custom_name
370
387
 
371
- say ""
388
+ say ''
372
389
  say "📱 Registering '#{name}'...", :cyan
373
390
 
374
391
  begin
@@ -382,58 +399,59 @@ module Mysigner
382
399
  )
383
400
 
384
401
  registered = response[:data]['device']
385
- say ""
386
- say "✓ Device registered successfully!", :green
402
+ say ''
403
+ say '✓ Device registered successfully!', :green
387
404
  say " Name: #{registered['name']}"
388
405
  say " UDID: #{registered['udid']}"
389
406
  say " Platform: #{registered['platform']}"
390
407
  rescue Mysigner::ClientError => e
391
- if e.message.include?("already exists")
392
- say ""
393
- say "ℹ️ Device already registered", :yellow
408
+ if e.message.include?('already exists')
409
+ say ''
410
+ say 'ℹ️ Device already registered', :yellow
394
411
  else
395
412
  error "Failed to register: #{e.message}"
396
413
  end
397
414
  end
398
415
  else
399
- say "Invalid selection", :red
416
+ say 'Invalid selection', :red
400
417
  end
401
418
  end
402
419
 
403
420
  public
404
421
 
405
- desc "profiles", "List provisioning profiles (advanced - only needed for manual signing)"
422
+ desc 'profiles', 'List provisioning profiles (advanced - only needed for manual signing)'
406
423
  long_desc <<~DESC
407
424
  List all provisioning profiles in your organization.
408
-
425
+
409
426
  WHEN DO YOU NEED THIS?
410
-
427
+
411
428
  For Automatic Signing (Most Users):
412
429
  ❌ You DON'T need this - Xcode handles everything
413
-
430
+
414
431
  For Manual Signing (Advanced):
415
432
  ✅ View available profiles
416
433
  ✅ Check expiration dates
417
434
  ✅ Get profile IDs for download/delete
418
-
435
+
419
436
  EXAMPLES:
420
-
437
+
421
438
  # List all profiles
422
439
  mysigner profiles
423
-
440
+ #{' '}
424
441
  # Filter by type
425
442
  mysigner profiles --type APP_STORE
426
443
  mysigner profiles --type DEVELOPMENT
427
-
444
+ #{' '}
428
445
  # Filter by status
429
446
  mysigner profiles --status EXPIRED
430
-
447
+ #{' '}
431
448
  # Search by name
432
449
  mysigner profiles --search "MyApp"
433
-
450
+
434
451
  NOTE: Most users can skip this and just run 'mysigner build'
435
452
  DESC
436
- method_option :type, type: :string, aliases: '-t', desc: 'Filter by type (DEVELOPMENT, AD_HOC, APP_STORE, ENTERPRISE)'
453
+ method_option :type, type: :string, aliases: '-t',
454
+ desc: 'Filter by type (DEVELOPMENT, AD_HOC, APP_STORE, ENTERPRISE)'
437
455
  method_option :status, type: :string, aliases: '-s', desc: 'Filter by status (ACTIVE, EXPIRED, INVALID)'
438
456
  method_option :search, type: :string, aliases: '-q', desc: 'Search by name or identifier'
439
457
  method_option :page, type: :numeric, default: 1, desc: 'Page number'
@@ -442,8 +460,8 @@ module Mysigner
442
460
  config = load_config
443
461
  client = create_client(config)
444
462
 
445
- say "📄 Provisioning Profiles", :cyan
446
- say ""
463
+ say '📄 Provisioning Profiles', :cyan
464
+ say ''
447
465
 
448
466
  # Build query params
449
467
  params = {
@@ -460,9 +478,9 @@ module Mysigner
460
478
  pagination = response[:data]['pagination']
461
479
 
462
480
  if profiles.empty?
463
- say "No profiles found", :yellow
464
- say ""
465
- say "Tip: Profiles are created automatically when you request code signing", :yellow
481
+ say 'No profiles found', :yellow
482
+ say ''
483
+ say 'Tip: Profiles are created automatically when you request code signing', :yellow
466
484
  return
467
485
  end
468
486
 
@@ -475,22 +493,20 @@ module Mysigner
475
493
  say " ID: #{profile['id']} | Type: #{profile['profile_type'] || 'N/A'}"
476
494
  say " Bundle ID: #{profile['bundle_id_identifier'] || 'N/A'}"
477
495
  say " Status: #{profile['state'] || 'UNKNOWN'}"
478
-
496
+
479
497
  if profile['expires_at']
480
498
  expires = Time.parse(profile['expires_at']).strftime('%Y-%m-%d')
481
499
  say " Expires: #{expires}"
482
500
  end
483
-
484
- say ""
501
+
502
+ say ''
485
503
  end
486
504
 
487
505
  # Show pagination
488
506
  if pagination
489
507
  say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)", :yellow
490
508
 
491
- if pagination['page'] < pagination['total_pages']
492
- say "Run with --page #{pagination['page'] + 1} to see more", :yellow
493
- end
509
+ say "Run with --page #{pagination['page'] + 1} to see more", :yellow if pagination['page'] < pagination['total_pages']
494
510
  end
495
511
  rescue Mysigner::ClientError => e
496
512
  error "Failed to fetch profiles: #{e.message}"
@@ -498,68 +514,68 @@ module Mysigner
498
514
  end
499
515
  end
500
516
 
501
- desc "profile SUBCOMMAND", "Download or delete profiles (advanced - only needed for manual signing)"
517
+ desc 'profile SUBCOMMAND', 'Download or delete profiles (advanced - only needed for manual signing)'
502
518
  long_desc <<~DESC
503
519
  Manage provisioning profiles for code signing.
504
-
520
+
505
521
  WHAT ARE PROVISIONING PROFILES?
506
-
522
+
507
523
  Provisioning profiles are required for signing iOS apps. They link:
508
524
  - Your signing certificate
509
525
  - Your App ID (bundle ID)
510
526
  - Authorized devices (for development/ad-hoc)
511
-
527
+
512
528
  WHEN DO YOU NEED THIS?
513
-
529
+
514
530
  For Automatic Signing (Most Users):
515
531
  ❌ You DON'T need these commands
516
532
  ✅ Xcode handles profiles automatically
517
533
  ✅ Just run: mysigner build
518
-
534
+
519
535
  For Manual Signing (Advanced):
520
536
  ✅ Download profiles from My Signer
521
537
  ✅ Install them to ~/Library/MobileDevice/Provisioning Profiles/
522
538
  ✅ Delete old/expired profiles
523
-
539
+
524
540
  SUBCOMMANDS:
525
-
541
+
526
542
  mysigner profile download ID [--output path]
527
543
  Download a provisioning profile
528
-
544
+ #{' '}
529
545
  mysigner profile delete ID
530
546
  Delete a provisioning profile
531
-
547
+
532
548
  HOW TO USE:
533
-
549
+
534
550
  1. List available profiles:
535
551
  mysigner profiles
536
-
552
+
537
553
  2. Download a profile:
538
554
  mysigner profile download 1
539
-
555
+
540
556
  3. Install it (double-click or manual):
541
557
  open Profile_Name.mobileprovision
542
558
  # Or: cp *.mobileprovision ~/Library/MobileDevice/Provisioning\\ Profiles/
543
-
559
+
544
560
  EXAMPLES:
545
-
561
+
546
562
  # Download profile ID 1
547
563
  mysigner profile download 1
548
-
564
+ #{' '}
549
565
  # Download to specific location
550
566
  mysigner profile download 1 --output ~/Desktop/MyProfile.mobileprovision
551
-
567
+ #{' '}
552
568
  # Delete expired profile
553
569
  mysigner profile delete 5
554
-
570
+ #{' '}
555
571
  # List all profiles
556
572
  mysigner profiles
557
-
573
+ #{' '}
558
574
  # Filter by type
559
575
  mysigner profiles --type APP_STORE
560
-
576
+
561
577
  NOTES:
562
-
578
+
563
579
  • Most users with Automatic signing don't need this
564
580
  • Manual signing wizard tries to auto-install profiles
565
581
  • Profiles expire after 1 year and must be regenerated
@@ -574,13 +590,13 @@ module Mysigner
574
590
  case action
575
591
  when 'download'
576
592
  if args.empty?
577
- error "Usage: mysigner profile download ID [--output path.mobileprovision]"
578
- say ""
579
- say "Example: mysigner profile download 1", :yellow
580
- say ""
581
- say "💡 To get profile IDs:", :cyan
593
+ error 'Usage: mysigner profile download ID [--output path.mobileprovision]'
594
+ say ''
595
+ say 'Example: mysigner profile download 1', :yellow
596
+ say ''
597
+ say '💡 To get profile IDs:', :cyan
582
598
  say " Run 'mysigner profiles' to see all profiles with their IDs", :cyan
583
- say ""
599
+ say ''
584
600
  say "Note: Most users with Automatic signing don't need this", :yellow
585
601
  say "Run 'mysigner help profile' for more info", :cyan
586
602
  exit 1
@@ -588,48 +604,52 @@ module Mysigner
588
604
 
589
605
  profile_id = args[0]
590
606
 
591
- say "📄 Downloading profile...", :cyan
592
- say ""
607
+ say '📄 Downloading profile...', :cyan
608
+ say ''
593
609
 
594
610
  begin
595
611
  # Get profile details first
596
612
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile_id}")
597
613
  profile = response[:data]
598
614
 
599
- # Determine output path
615
+ # Determine output path. Default to ~/Downloads/ (created if
616
+ # missing) instead of the current working directory — users
617
+ # were ending up with .mobileprovision files sprinkled inside
618
+ # whatever project they ran the command from.
600
619
  output_path = if options[:output]
601
- options[:output]
602
- else
603
- # Use profile name, sanitize it for filename
604
- name = profile['name'] || "profile_#{profile['id']}"
605
- filename = name.gsub(/[^0-9A-Za-z.\-]/, '_')
606
- "#{filename}.mobileprovision"
607
- end
620
+ options[:output]
621
+ else
622
+ name = profile['name'] || "profile_#{profile['id']}"
623
+ filename = name.gsub(/[^0-9A-Za-z.-]/, '_')
624
+ downloads_dir = File.expand_path('~/Downloads')
625
+ FileUtils.mkdir_p(downloads_dir)
626
+ File.join(downloads_dir, "#{filename}.mobileprovision")
627
+ end
608
628
 
609
629
  # Download the profile content using the client's connection with auth
610
630
  download_url = "/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile_id}/download"
611
-
612
- say "Fetching profile content...", :yellow
613
-
631
+
632
+ say 'Fetching profile content...', :yellow
633
+
614
634
  # Use Faraday directly with proper auth for binary download
615
635
  conn = Faraday.new(url: config.api_url) do |f|
616
636
  f.request :authorization, 'Bearer', config.api_token
617
637
  f.headers['X-User-Email'] = config.user_email if config.user_email
618
638
  f.adapter Faraday.default_adapter
619
639
  end
620
-
640
+
621
641
  response = conn.get(download_url) do |req|
622
- req.options.timeout = 30 # 30 second timeout
623
- req.options.open_timeout = 10 # 10 second connection timeout
642
+ req.options.timeout = 30 # 30 second timeout
643
+ req.options.open_timeout = 10 # 10 second connection timeout
624
644
  end
625
-
645
+
626
646
  unless response.success?
627
647
  # Check if it's a JSON error response
628
648
  if response.headers['content-type']&.include?('json')
629
649
  begin
630
650
  error_data = JSON.parse(response.body)
631
651
  error "Download failed: #{error_data['message'] || error_data['error']}"
632
- rescue
652
+ rescue StandardError
633
653
  error "Download failed with status #{response.status}"
634
654
  end
635
655
  else
@@ -641,62 +661,62 @@ module Mysigner
641
661
  # Write binary content directly to file
642
662
  File.binwrite(output_path, response.body)
643
663
 
644
- say "✓ Profile downloaded successfully!", :green
645
- say ""
646
- say "Details:", :bold
664
+ say '✓ Profile downloaded successfully!', :green
665
+ say ''
666
+ say 'Details:', :bold
647
667
  say " Name: #{profile['name']}"
648
668
  say " Type: #{profile['profile_type'] || 'N/A'}"
649
669
  say " Bundle ID: #{profile['bundle_id_identifier'] || 'N/A'}"
650
670
  say " Status: #{profile['state'] || 'UNKNOWN'}"
651
671
  say " File: #{output_path}"
652
- say ""
672
+ say ''
653
673
  say "File size: #{response.body.bytesize} bytes", :yellow
654
674
  rescue Mysigner::NotFoundError
655
675
  error "Profile not found with ID: #{profile_id}"
656
- say ""
657
- say "💡 Profile Not Found: How to fix", :cyan
658
- say ""
659
- say " → List available profiles: mysigner profiles", :yellow
660
- say " → Sync from Apple: mysigner sync ios", :yellow
661
- say " → Check ID is correct (IDs are numeric)", :yellow
662
- say ""
676
+ say ''
677
+ say '💡 Profile Not Found: How to fix', :cyan
678
+ say ''
679
+ say ' → List available profiles: mysigner profiles', :yellow
680
+ say ' → Sync from Apple: mysigner sync ios', :yellow
681
+ say ' → Check ID is correct (IDs are numeric)', :yellow
682
+ say ''
663
683
  exit 1
664
684
  rescue Mysigner::ClientError => e
665
685
  error "Failed to download profile: #{e.message}"
666
- say ""
667
- say "💡 Download Failed: Try these steps", :cyan
668
- say ""
669
- say " → Check your network connection", :yellow
670
- say " → Verify API token is valid: mysigner status", :yellow
671
- say " → Re-authenticate if needed: mysigner login", :yellow
672
- say ""
673
- exit 1
674
- rescue => e
686
+ say ''
687
+ say '💡 Download Failed: Try these steps', :cyan
688
+ say ''
689
+ say ' → Check your network connection', :yellow
690
+ say ' → Verify API token is valid: mysigner status', :yellow
691
+ say ' → Re-authenticate if needed: mysigner login', :yellow
692
+ say ''
693
+ exit 1
694
+ rescue StandardError => e
675
695
  error "Failed to save file: #{e.message}"
676
- say ""
677
- say "💡 File Save Failed: Check these", :cyan
678
- say ""
679
- say " → Verify you have write permissions to the directory", :yellow
680
- say " → Check disk space is available", :yellow
681
- say " → Try specifying a different output path with --output", :yellow
682
- say ""
696
+ say ''
697
+ say '💡 File Save Failed: Check these', :cyan
698
+ say ''
699
+ say ' → Verify you have write permissions to the directory', :yellow
700
+ say ' → Check disk space is available', :yellow
701
+ say ' → Try specifying a different output path with --output', :yellow
702
+ say ''
683
703
  exit 1
684
704
  end
685
705
  when 'delete'
686
706
  if args.empty?
687
- error "Usage: mysigner profile delete ID"
688
- say ""
689
- say "Example: mysigner profile delete 5", :yellow
690
- say ""
691
- say "💡 To get profile IDs:", :cyan
707
+ error 'Usage: mysigner profile delete ID'
708
+ say ''
709
+ say 'Example: mysigner profile delete 5', :yellow
710
+ say ''
711
+ say '💡 To get profile IDs:', :cyan
692
712
  say " Run 'mysigner profiles' to see all profiles with their IDs", :cyan
693
713
  exit 1
694
714
  end
695
715
 
696
716
  profile_id = args[0]
697
717
 
698
- say "📄 Deleting profile...", :cyan
699
- say ""
718
+ say '📄 Deleting profile...', :cyan
719
+ say ''
700
720
 
701
721
  begin
702
722
  # Get profile details first
@@ -704,18 +724,18 @@ module Mysigner
704
724
  profile = response[:data]
705
725
 
706
726
  # Confirm deletion
707
- say "You are about to delete:", :yellow
727
+ say 'You are about to delete:', :yellow
708
728
  say " Name: #{profile['name']}"
709
729
  say " Type: #{profile['profile_type']}"
710
730
  say " Bundle ID: #{profile['bundle_id_identifier'] || 'N/A'}"
711
- say ""
731
+ say ''
712
732
 
713
- if yes?("Are you sure you want to delete this profile? (y/n)")
733
+ if yes?('Are you sure you want to delete this profile? (y/n)')
714
734
  client.delete("/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile_id}")
715
- say ""
716
- say "✓ Profile deleted successfully!", :green
735
+ say ''
736
+ say '✓ Profile deleted successfully!', :green
717
737
  else
718
- say "Deletion cancelled", :yellow
738
+ say 'Deletion cancelled', :yellow
719
739
  end
720
740
  rescue Mysigner::NotFoundError
721
741
  error "Profile not found with ID: #{profile_id}"
@@ -728,12 +748,12 @@ module Mysigner
728
748
  invoke :help, ['profile']
729
749
  else
730
750
  error "Unknown action: #{action}"
731
- say "Available actions: download, delete, help", :yellow
751
+ say 'Available actions: download, delete, help', :yellow
732
752
  exit 1
733
753
  end
734
754
  end
735
755
 
736
- desc "certificates", "List signing certificates from App Store Connect"
756
+ desc 'certificates', 'List signing certificates from App Store Connect'
737
757
  method_option :type, type: :string, aliases: '-p', desc: 'Filter by type (DEVELOPMENT, DISTRIBUTION)'
738
758
  method_option :status, type: :string, aliases: '-s', desc: 'Filter by status (ACTIVE, EXPIRED, REVOKED)'
739
759
  method_option :search, type: :string, aliases: '-q', desc: 'Search by name'
@@ -743,8 +763,8 @@ module Mysigner
743
763
  config = load_config
744
764
  client = create_client(config)
745
765
 
746
- say "🔐 Signing Certificates", :cyan
747
- say ""
766
+ say '🔐 Signing Certificates', :cyan
767
+ say ''
748
768
 
749
769
  # Build query params
750
770
  params = {
@@ -756,14 +776,15 @@ module Mysigner
756
776
  params[:q] = options[:search] if options[:search]
757
777
 
758
778
  begin
759
- response = client.get("/api/v1/organizations/#{config.current_organization_id}/certificates", params: params)
779
+ response = client.get("/api/v1/organizations/#{config.current_organization_id}/certificates",
780
+ params: params)
760
781
  certificates = response[:data]['certificates']
761
782
  pagination = response[:data]['pagination']
762
783
 
763
784
  if certificates.empty?
764
- say "No certificates found", :yellow
765
- say ""
766
- say "Tip: Certificates are synced automatically from App Store Connect", :yellow
785
+ say 'No certificates found', :yellow
786
+ say ''
787
+ say 'Tip: Certificates are synced automatically from App Store Connect', :yellow
767
788
  return
768
789
  end
769
790
 
@@ -771,27 +792,25 @@ module Mysigner
771
792
  certificates.each do |cert|
772
793
  status_icon = cert['status'] == 'ACTIVE' ? '✓' : '✗'
773
794
  status_color = cert['status'] == 'ACTIVE' ? :green : :red
774
-
795
+
775
796
  say " #{status_icon} #{cert['name']}", status_color
776
797
  say " ID: #{cert['id']} | Type: #{cert['certificate_type'] || 'N/A'}"
777
798
  say " Serial: #{cert['serial_number'] || 'N/A'}"
778
799
  say " Status: #{cert['status'] || 'UNKNOWN'}"
779
-
800
+
780
801
  if cert['expires_at']
781
802
  expires = Time.parse(cert['expires_at']).strftime('%Y-%m-%d')
782
803
  say " Expires: #{expires}"
783
804
  end
784
-
785
- say ""
805
+
806
+ say ''
786
807
  end
787
808
 
788
809
  # Show pagination
789
810
  if pagination
790
811
  say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)", :yellow
791
812
 
792
- if pagination['page'] < pagination['total_pages']
793
- say "Run with --page #{pagination['page'] + 1} to see more", :yellow
794
- end
813
+ say "Run with --page #{pagination['page'] + 1} to see more", :yellow if pagination['page'] < pagination['total_pages']
795
814
  end
796
815
  rescue Mysigner::ClientError => e
797
816
  error "Failed to fetch certificates: #{e.message}"
@@ -799,12 +818,12 @@ module Mysigner
799
818
  end
800
819
  end
801
820
 
802
- desc "certificate ACTION", "Check local keychain or download certificates (check, download)"
821
+ desc 'certificate ACTION', 'Check local keychain or download certificates (check, download)'
803
822
  long_desc <<~DESC
804
823
  Actions:
805
824
  check - Check certificates installed in your Mac's Keychain (not API)
806
825
  download ID - Download a certificate from My Signer API
807
-
826
+
808
827
  Note: 'check' scans your LOCAL Keychain, not certificates in My Signer API.
809
828
  Use 'mysigner certificates' to see API certificates.
810
829
  DESC
@@ -816,151 +835,157 @@ module Mysigner
816
835
  case action
817
836
  when 'check'
818
837
  require_relative '../signing/certificate_checker'
819
-
820
- say "🔍 Checking local certificates...", :cyan
821
- say ""
822
-
838
+
839
+ say '🔍 Checking local certificates...', :cyan
840
+ say ''
841
+
823
842
  checker = Signing::CertificateChecker.new
824
-
843
+
825
844
  begin
826
845
  certificates = checker.check!
827
-
846
+
828
847
  if certificates.empty?
829
- say "No code signing certificates found in local Keychain", :yellow
830
- say ""
831
- say "⚠️ Important:", :yellow
832
- say " This command checks certificates INSTALLED ON YOUR MAC.", :white
833
- say " Certificates in My Signer API are not automatically installed locally.", :white
834
- say ""
835
- say "To install certificates:", :cyan
836
- say " 1. List certificates in My Signer: mysigner certificates", :white
837
- say " 2. Download one: mysigner certificate download <ID>", :white
838
- say " 3. Double-click the .cer file to install in Keychain", :white
839
- say ""
840
- say "Or download from Apple Developer:", :cyan
841
- say " https://developer.apple.com/account/resources/certificates/list", :white
848
+ say 'No code signing certificates found in local Keychain', :yellow
849
+ say ''
850
+ say '⚠️ Important:', :yellow
851
+ say ' This command checks certificates INSTALLED ON YOUR MAC.', :white
852
+ say ' Certificates in My Signer API are not automatically installed locally.', :white
853
+ say ''
854
+ say 'To install certificates:', :cyan
855
+ say ' 1. List certificates in My Signer: mysigner certificates', :white
856
+ say ' 2. Download one: mysigner certificate download <ID>', :white
857
+ say ' 3. Double-click the .cer file to install in Keychain', :white
858
+ say ''
859
+ say 'Or download from Apple Developer:', :cyan
860
+ say ' https://developer.apple.com/account/resources/certificates/list', :white
842
861
  return
843
862
  end
844
-
863
+
845
864
  # Group by status
846
865
  by_status = checker.by_status
847
-
866
+
848
867
  # Show valid certificates
849
868
  if by_status[:valid].any?
850
869
  say "✓ Valid Certificates (#{by_status[:valid].count})", :green
851
- say ""
870
+ say ''
852
871
  by_status[:valid].each do |cert|
853
872
  say " #{cert[:name]}", :green
854
873
  say " Type: #{cert[:type]}"
855
874
  say " Team: #{cert[:team_id] || 'Unknown'}"
856
- say " Expires: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry]} days)", :white
857
- say ""
875
+ say " Expires: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry]} days)",
876
+ :white
877
+ say ''
858
878
  end
859
879
  end
860
-
880
+
861
881
  # Show expiring soon certificates
862
882
  if by_status[:expiring_soon].any?
863
883
  say "⚠️ Expiring Soon (#{by_status[:expiring_soon].count})", :yellow
864
- say ""
884
+ say ''
865
885
  by_status[:expiring_soon].each do |cert|
866
886
  say " #{cert[:name]}", :yellow
867
887
  say " Type: #{cert[:type]}"
868
888
  say " Team: #{cert[:team_id] || 'Unknown'}"
869
- say " Expires: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry]} days)", :yellow
870
- say ""
889
+ say " Expires: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry]} days)",
890
+ :yellow
891
+ say ''
871
892
  end
872
- say "Renew these certificates soon to avoid build failures!", :yellow
873
- say ""
893
+ say 'Renew these certificates soon to avoid build failures!', :yellow
894
+ say ''
874
895
  end
875
-
896
+
876
897
  # Show expired certificates
877
898
  if by_status[:expired].any?
878
899
  say "✗ Expired Certificates (#{by_status[:expired].count})", :red
879
- say ""
900
+ say ''
880
901
  by_status[:expired].each do |cert|
881
902
  say " #{cert[:name]}", :red
882
903
  say " Type: #{cert[:type]}"
883
904
  say " Team: #{cert[:team_id] || 'Unknown'}"
884
- say " Expired: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry].abs} days ago)", :red
885
- say ""
905
+ say " Expired: #{cert[:expires_at].strftime('%Y-%m-%d')} (#{cert[:days_until_expiry].abs} days ago)",
906
+ :red
907
+ say ''
886
908
  end
887
- say "These certificates will cause build failures. Renew them at:", :red
888
- say " https://developer.apple.com/account/resources/certificates/list", :white
889
- say ""
909
+ say 'These certificates will cause build failures. Renew them at:', :red
910
+ say ' https://developer.apple.com/account/resources/certificates/list', :white
911
+ say ''
890
912
  end
891
-
913
+
892
914
  # Summary
893
- say "" * 80, :cyan
894
- say "Total: #{certificates.count} certificate#{certificates.count == 1 ? '' : 's'} installed locally", :cyan
915
+ say '' * 80, :cyan
916
+ say "Total: #{certificates.count} certificate#{'s' unless certificates.one?} installed locally",
917
+ :cyan
895
918
  if checker.has_issues?
896
- say "Status: ⚠️ Action required", :yellow
919
+ say 'Status: ⚠️ Action required', :yellow
897
920
  else
898
- say "Status: ✓ All certificates valid", :green
921
+ say 'Status: ✓ All certificates valid', :green
899
922
  end
900
- say ""
901
- say "💡 Tip: These are certificates INSTALLED ON YOUR MAC.", :cyan
902
- say " To see all certificates in My Signer API, run: mysigner certificates", :white
903
-
923
+ say ''
924
+ say '💡 Tip: These are certificates INSTALLED ON YOUR MAC.', :cyan
925
+ say ' To see all certificates in My Signer API, run: mysigner certificates', :white
904
926
  rescue Signing::CertificateChecker::CheckError => e
905
927
  error "Certificate check failed: #{e.message}"
906
- say ""
907
- say "This usually means:", :yellow
908
- say " • Keychain is locked", :white
909
- say " • No certificates installed", :white
910
- say " • Security command not available", :white
928
+ say ''
929
+ say 'This usually means:', :yellow
930
+ say ' • Keychain is locked', :white
931
+ say ' • No certificates installed', :white
932
+ say ' • Security command not available', :white
911
933
  exit 1
912
934
  end
913
-
935
+
914
936
  when 'download'
915
937
  if args.empty?
916
- error "Usage: mysigner certificate download ID [--output path.cer]"
938
+ error 'Usage: mysigner certificate download ID [--output path.cer]'
917
939
  exit 1
918
940
  end
919
941
 
920
942
  certificate_id = args[0]
921
943
 
922
- say "🔐 Downloading certificate...", :cyan
923
- say ""
944
+ say '🔐 Downloading certificate...', :cyan
945
+ say ''
924
946
 
925
947
  begin
926
948
  # Get certificate details first
927
949
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/certificates/#{certificate_id}")
928
950
  certificate = response[:data]
929
951
 
930
- # Determine output path
952
+ # Determine output path. Default to ~/Downloads/ (mirrors
953
+ # profile download behavior) rather than the CWD to avoid
954
+ # dropping .cer files inside the user's project tree.
931
955
  output_path = if options[:output]
932
- options[:output]
933
- else
934
- # Use certificate name, sanitize it for filename
935
- name = certificate['name'] || "certificate_#{certificate['id']}"
936
- filename = name.gsub(/[^0-9A-Za-z.\-]/, '_')
937
- "#{filename}.cer"
938
- end
956
+ options[:output]
957
+ else
958
+ name = certificate['name'] || "certificate_#{certificate['id']}"
959
+ filename = name.gsub(/[^0-9A-Za-z.-]/, '_')
960
+ downloads_dir = File.expand_path('~/Downloads')
961
+ FileUtils.mkdir_p(downloads_dir)
962
+ File.join(downloads_dir, "#{filename}.cer")
963
+ end
939
964
 
940
965
  # Download the certificate content (binary response)
941
966
  download_url = "/api/v1/organizations/#{config.current_organization_id}/certificates/#{certificate_id}/download"
942
-
943
- say "Fetching certificate content...", :yellow
944
-
967
+
968
+ say 'Fetching certificate content...', :yellow
969
+
945
970
  # Use Faraday directly with proper auth for binary download
946
971
  conn = Faraday.new(url: config.api_url) do |f|
947
972
  f.request :authorization, 'Bearer', config.api_token
948
973
  f.headers['X-User-Email'] = config.user_email if config.user_email
949
974
  f.adapter Faraday.default_adapter
950
975
  end
951
-
976
+
952
977
  response = conn.get(download_url) do |req|
953
- req.options.timeout = 30 # 30 second timeout
954
- req.options.open_timeout = 10 # 10 second connection timeout
978
+ req.options.timeout = 30 # 30 second timeout
979
+ req.options.open_timeout = 10 # 10 second connection timeout
955
980
  end
956
-
981
+
957
982
  unless response.success?
958
983
  # Check if it's a JSON error response
959
984
  if response.headers['content-type']&.include?('json')
960
985
  begin
961
986
  error_data = JSON.parse(response.body)
962
987
  error "Download failed: #{error_data['message'] || error_data['error']}"
963
- rescue
988
+ rescue StandardError
964
989
  error "Download failed with status #{response.status}"
965
990
  end
966
991
  else
@@ -972,59 +997,59 @@ module Mysigner
972
997
  # Write binary content directly to file
973
998
  File.binwrite(output_path, response.body)
974
999
 
975
- say "✓ Certificate downloaded successfully!", :green
976
- say ""
977
- say "Details:", :bold
1000
+ say '✓ Certificate downloaded successfully!', :green
1001
+ say ''
1002
+ say 'Details:', :bold
978
1003
  say " Name: #{certificate['name']}"
979
1004
  say " Type: #{certificate['certificate_type'] || 'N/A'}"
980
1005
  say " Serial: #{certificate['serial_number'] || 'N/A'}"
981
1006
  say " Status: #{certificate['status'] || 'UNKNOWN'}"
982
1007
  say " File: #{output_path}"
983
- say ""
1008
+ say ''
984
1009
  say "File size: #{response.body.bytesize} bytes", :yellow
985
1010
  rescue Mysigner::NotFoundError
986
1011
  error "Certificate not found with ID: #{certificate_id}"
987
- say ""
988
- say "💡 Certificate Not Found: How to fix", :cyan
989
- say ""
990
- say " → List available certificates: mysigner certificates", :yellow
991
- say " → Sync from Apple: mysigner sync ios", :yellow
992
- say " → Check the ID is correct (IDs are numeric)", :yellow
993
- say ""
1012
+ say ''
1013
+ say '💡 Certificate Not Found: How to fix', :cyan
1014
+ say ''
1015
+ say ' → List available certificates: mysigner certificates', :yellow
1016
+ say ' → Sync from Apple: mysigner sync ios', :yellow
1017
+ say ' → Check the ID is correct (IDs are numeric)', :yellow
1018
+ say ''
994
1019
  exit 1
995
1020
  rescue Mysigner::ClientError => e
996
1021
  error "Failed to download certificate: #{e.message}"
997
- say ""
998
- say "💡 Download Failed: Try these steps", :cyan
999
- say ""
1000
- say " → Check your network connection", :yellow
1001
- say " → Verify API token is valid: mysigner status", :yellow
1002
- say " → Re-authenticate if needed: mysigner login", :yellow
1003
- say ""
1004
- exit 1
1005
- rescue => e
1022
+ say ''
1023
+ say '💡 Download Failed: Try these steps', :cyan
1024
+ say ''
1025
+ say ' → Check your network connection', :yellow
1026
+ say ' → Verify API token is valid: mysigner status', :yellow
1027
+ say ' → Re-authenticate if needed: mysigner login', :yellow
1028
+ say ''
1029
+ exit 1
1030
+ rescue StandardError => e
1006
1031
  error "Failed to save file: #{e.message}"
1007
- say ""
1008
- say "💡 File Save Failed: Check these", :cyan
1009
- say ""
1010
- say " → Verify you have write permissions to the directory", :yellow
1011
- say " → Check disk space is available", :yellow
1012
- say " → Try specifying a different output path with --output", :yellow
1013
- say ""
1032
+ say ''
1033
+ say '💡 File Save Failed: Check these', :cyan
1034
+ say ''
1035
+ say ' → Verify you have write permissions to the directory', :yellow
1036
+ say ' → Check disk space is available', :yellow
1037
+ say ' → Try specifying a different output path with --output', :yellow
1038
+ say ''
1014
1039
  exit 1
1015
1040
  end
1016
1041
  when 'help'
1017
1042
  invoke :help, ['certificate']
1018
1043
  else
1019
1044
  error "Unknown action: #{action}"
1020
- say "Available actions: check, download, help", :yellow
1045
+ say 'Available actions: check, download, help', :yellow
1021
1046
  exit 1
1022
1047
  end
1023
1048
  end
1024
1049
 
1025
1050
  # ==================== ANDROID KEYSTORES ====================
1026
1051
 
1027
- desc "keystore SUBCOMMAND", "Manage Android keystores (list, upload, download, delete, activate)"
1052
+ desc 'keystore SUBCOMMAND', 'Manage Android keystores (list, upload, download, delete, activate)'
1028
1053
  long_desc <<~DESC
1029
1054
  Manage Android keystores for signing your apps.
1030
1055
 
@@ -1087,38 +1112,38 @@ module Mysigner
1087
1112
 
1088
1113
  case action
1089
1114
  when 'list'
1090
- say "🔐 Android Keystores", :cyan
1091
- say ""
1115
+ say '🔐 Android Keystores', :cyan
1116
+ say ''
1092
1117
 
1093
1118
  keystores = manager.list(android_app_id: options[:app_id])
1094
1119
 
1095
1120
  if keystores.empty?
1096
- say "No keystores found", :yellow
1097
- say ""
1098
- say "Upload a keystore with: mysigner keystore upload PATH", :yellow
1121
+ say 'No keystores found', :yellow
1122
+ say ''
1123
+ say 'Upload a keystore with: mysigner keystore upload PATH', :yellow
1099
1124
  return
1100
1125
  end
1101
1126
 
1102
1127
  keystores.each do |ks|
1103
1128
  active_icon = ks['active'] ? '✓' : '○'
1104
1129
  active_color = ks['active'] ? :green : :white
1105
-
1130
+
1106
1131
  say " #{active_icon} #{ks['name']} (ID: #{ks['id']})", active_color
1107
1132
  say " Key Alias: #{ks['key_alias'] || 'N/A'}"
1108
1133
  say " App: #{ks['package_name']}" if ks['package_name']
1109
1134
  say " Active: #{ks['active'] ? 'Yes' : 'No'}"
1110
- say ""
1135
+ say ''
1111
1136
  end
1112
1137
 
1113
1138
  say "Total: #{keystores.count} keystore(s)", :yellow
1114
1139
 
1115
1140
  when 'upload'
1116
1141
  keystore_path = args[0]
1117
-
1142
+
1118
1143
  unless keystore_path
1119
- error "Usage: mysigner keystore upload PATH"
1120
- say ""
1121
- say "Example: mysigner keystore upload ~/keys/release.jks", :yellow
1144
+ error 'Usage: mysigner keystore upload PATH'
1145
+ say ''
1146
+ say 'Example: mysigner keystore upload ~/keys/release.jks', :yellow
1122
1147
  exit 1
1123
1148
  end
1124
1149
 
@@ -1127,17 +1152,36 @@ module Mysigner
1127
1152
  exit 1
1128
1153
  end
1129
1154
 
1130
- say "🔐 Uploading keystore...", :cyan
1131
- say ""
1155
+ say '🔐 Uploading keystore...', :cyan
1156
+ say ''
1132
1157
 
1133
- # Get keystore details
1158
+ # Phase 0: support non-TTY automation. `ask(echo: false)` raises
1159
+ # Errno::ENOTTY on piped stdin, which made `keystore upload`
1160
+ # unusable from CI. When stdin isn't a TTY, require passwords
1161
+ # to come from env vars (MYSIGNER_KEYSTORE_PASSWORD /
1162
+ # MYSIGNER_KEY_PASSWORD) so operators can script uploads
1163
+ # without also having to wrap every invocation in `expect`.
1134
1164
  name = options[:name] || ask("Keystore name (e.g., 'Release Key'):")
1135
- key_alias = options[:alias] || ask("Key alias:")
1136
- password = ask("Keystore password:", echo: false)
1137
- say ""
1138
- key_password = ask("Key password (press Enter if same as keystore):", echo: false)
1139
- say ""
1140
- key_password = password if key_password.empty?
1165
+ key_alias = options[:alias] || ask('Key alias:')
1166
+
1167
+ if $stdin.tty?
1168
+ password = ask('Keystore password:', echo: false)
1169
+ say ''
1170
+ key_password = ask('Key password (press Enter if same as keystore):', echo: false)
1171
+ say ''
1172
+ key_password = password if key_password.empty?
1173
+ else
1174
+ password = ENV.fetch('MYSIGNER_KEYSTORE_PASSWORD', nil)
1175
+ key_password = ENV['MYSIGNER_KEY_PASSWORD'] || password
1176
+ if password.nil? || password.empty?
1177
+ error 'Non-interactive upload requires MYSIGNER_KEYSTORE_PASSWORD (and optionally MYSIGNER_KEY_PASSWORD) in the environment.'
1178
+ say ''
1179
+ say 'Example:', :cyan
1180
+ say ' MYSIGNER_KEYSTORE_PASSWORD=... MYSIGNER_KEY_PASSWORD=... \\', :yellow
1181
+ say ' mysigner keystore upload ./release.jks --name "Release" --alias myalias', :yellow
1182
+ exit 1
1183
+ end
1184
+ end
1141
1185
 
1142
1186
  begin
1143
1187
  result = manager.upload(
@@ -1150,52 +1194,51 @@ module Mysigner
1150
1194
  active: true
1151
1195
  )
1152
1196
 
1153
- say "✓ Keystore uploaded successfully!", :green
1154
- say ""
1155
- say "Details:", :bold
1197
+ say '✓ Keystore uploaded successfully!', :green
1198
+ say ''
1199
+ say 'Details:', :bold
1156
1200
  say " ID: #{result['id']}"
1157
1201
  say " Name: #{result['name']}"
1158
1202
  say " Key Alias: #{result['key_alias']}"
1159
1203
  say " Active: #{result['active']}"
1160
- say ""
1161
-
1204
+ say ''
1162
1205
  rescue Signing::KeystoreManager::KeystoreError => e
1163
1206
  error "Upload failed: #{e.message}"
1164
- say ""
1165
- say "💡 Keystore Upload Failed: Common issues", :cyan
1166
- say ""
1167
- say " → Verify the keystore file is valid (.jks or .keystore)", :yellow
1168
- say " → Check keystore password is correct", :yellow
1169
- say " → Check key alias exists in the keystore", :yellow
1170
- say " → Verify key password is correct", :yellow
1171
- say ""
1207
+ say ''
1208
+ say '💡 Keystore Upload Failed: Common issues', :cyan
1209
+ say ''
1210
+ say ' → Verify the keystore file is valid (.jks or .keystore)', :yellow
1211
+ say ' → Check keystore password is correct', :yellow
1212
+ say ' → Check key alias exists in the keystore', :yellow
1213
+ say ' → Verify key password is correct', :yellow
1214
+ say ''
1172
1215
  say " Test with: keytool -list -keystore #{keystore_path}", :green
1173
- say ""
1216
+ say ''
1174
1217
  exit 1
1175
1218
  rescue Mysigner::ClientError => e
1176
1219
  error "API error: #{e.message}"
1177
- say ""
1178
- say "💡 API Error: Try these steps", :cyan
1179
- say ""
1180
- say " → Check your network connection", :yellow
1181
- say " → Verify API token is valid: mysigner status", :yellow
1182
- say " → Re-authenticate if needed: mysigner login", :yellow
1183
- say ""
1220
+ say ''
1221
+ say '💡 API Error: Try these steps', :cyan
1222
+ say ''
1223
+ say ' → Check your network connection', :yellow
1224
+ say ' → Verify API token is valid: mysigner status', :yellow
1225
+ say ' → Re-authenticate if needed: mysigner login', :yellow
1226
+ say ''
1184
1227
  exit 1
1185
1228
  end
1186
1229
 
1187
1230
  when 'download'
1188
1231
  keystore_id = args[0]
1189
-
1232
+
1190
1233
  unless keystore_id
1191
- error "Usage: mysigner keystore download ID"
1192
- say ""
1234
+ error 'Usage: mysigner keystore download ID'
1235
+ say ''
1193
1236
  say "Run 'mysigner keystore list' to see available IDs", :yellow
1194
1237
  exit 1
1195
1238
  end
1196
1239
 
1197
- say "🔐 Downloading keystore...", :cyan
1198
- say ""
1240
+ say '🔐 Downloading keystore...', :cyan
1241
+ say ''
1199
1242
 
1200
1243
  begin
1201
1244
  result = manager.download(keystore_id)
@@ -1206,110 +1249,109 @@ module Mysigner
1206
1249
  result[:path] = options[:output]
1207
1250
  end
1208
1251
 
1209
- say "✓ Keystore downloaded!", :green
1210
- say ""
1211
- say "Details:", :bold
1252
+ say '✓ Keystore downloaded!', :green
1253
+ say ''
1254
+ say 'Details:', :bold
1212
1255
  say " Name: #{result[:name]}"
1213
1256
  say " Key Alias: #{result[:key_alias]}"
1214
1257
  say " Path: #{result[:path]}"
1215
- say ""
1216
- say "⚠️ Keep this file secure and backed up!", :yellow
1217
-
1258
+ say ''
1259
+ say '⚠️ Keep this file secure and backed up!', :yellow
1218
1260
  rescue Signing::KeystoreManager::KeystoreNotFoundError => e
1219
1261
  error "Keystore not found: #{e.message}"
1220
- say ""
1221
- say "💡 Keystore Not Found: How to fix", :cyan
1222
- say ""
1223
- say " → List available keystores: mysigner keystore list", :yellow
1224
- say " → Upload a keystore: mysigner keystore upload <path>", :yellow
1225
- say " → Check the ID is correct (IDs are numeric)", :yellow
1226
- say ""
1262
+ say ''
1263
+ say '💡 Keystore Not Found: How to fix', :cyan
1264
+ say ''
1265
+ say ' → List available keystores: mysigner keystore list', :yellow
1266
+ say ' → Upload a keystore: mysigner keystore upload <path>', :yellow
1267
+ say ' → Check the ID is correct (IDs are numeric)', :yellow
1268
+ say ''
1227
1269
  exit 1
1228
1270
  rescue Signing::KeystoreManager::DownloadError => e
1229
1271
  error "Download failed: #{e.message}"
1230
- say ""
1231
- say "💡 Download Failed: Try these steps", :cyan
1232
- say ""
1233
- say " → Check your network connection", :yellow
1234
- say " → Verify API token is valid: mysigner status", :yellow
1235
- say " → Re-authenticate if needed: mysigner login", :yellow
1236
- say ""
1272
+ say ''
1273
+ say '💡 Download Failed: Try these steps', :cyan
1274
+ say ''
1275
+ say ' → Check your network connection', :yellow
1276
+ say ' → Verify API token is valid: mysigner status', :yellow
1277
+ say ' → Re-authenticate if needed: mysigner login', :yellow
1278
+ say ''
1237
1279
  exit 1
1238
1280
  end
1239
1281
 
1240
1282
  when 'delete'
1241
1283
  keystore_id = args[0]
1242
-
1284
+
1243
1285
  unless keystore_id
1244
- error "Usage: mysigner keystore delete ID"
1286
+ error 'Usage: mysigner keystore delete ID'
1245
1287
  exit 1
1246
1288
  end
1247
1289
 
1248
1290
  # Get keystore details first
1249
1291
  keystores = manager.list
1250
1292
  keystore = keystores.find { |k| k['id'].to_s == keystore_id.to_s }
1251
-
1293
+
1252
1294
  unless keystore
1253
1295
  error "Keystore not found with ID: #{keystore_id}"
1254
- say ""
1255
- say "💡 Keystore Not Found: How to fix", :cyan
1256
- say ""
1257
- say " → List available keystores: mysigner keystore list", :yellow
1258
- say " → Upload a keystore: mysigner keystore upload <path>", :yellow
1259
- say ""
1296
+ say ''
1297
+ say '💡 Keystore Not Found: How to fix', :cyan
1298
+ say ''
1299
+ say ' → List available keystores: mysigner keystore list', :yellow
1300
+ say ' → Upload a keystore: mysigner keystore upload <path>', :yellow
1301
+ say ''
1260
1302
  exit 1
1261
1303
  end
1262
1304
 
1263
- say "⚠️ You are about to delete:", :yellow
1305
+ say '⚠️ You are about to delete:', :yellow
1264
1306
  say " Name: #{keystore['name']}"
1265
1307
  say " Key Alias: #{keystore['key_alias']}"
1266
- say ""
1308
+ say ''
1267
1309
 
1268
- if yes?("Are you sure? This cannot be undone. (y/n)")
1310
+ if yes?('Are you sure? This cannot be undone. (y/n)')
1269
1311
  begin
1270
1312
  manager.delete(keystore_id)
1271
- say ""
1272
- say "✓ Keystore deleted", :green
1313
+ say ''
1314
+ say '✓ Keystore deleted', :green
1273
1315
  rescue Mysigner::ClientError => e
1274
1316
  error "Delete failed: #{e.message}"
1275
1317
  exit 1
1276
1318
  end
1277
1319
  else
1278
- say "Deletion cancelled", :yellow
1320
+ say 'Deletion cancelled', :yellow
1279
1321
  end
1280
1322
 
1281
1323
  when 'activate'
1282
1324
  keystore_id = args[0]
1283
-
1325
+
1284
1326
  unless keystore_id
1285
- error "Usage: mysigner keystore activate ID"
1327
+ error 'Usage: mysigner keystore activate ID'
1286
1328
  exit 1
1287
1329
  end
1288
1330
 
1289
- say "🔐 Activating keystore...", :cyan
1331
+ say '🔐 Activating keystore...', :cyan
1290
1332
 
1291
1333
  begin
1292
1334
  result = manager.activate(keystore_id)
1293
- say "✓ Keystore activated!", :green
1294
- say ""
1335
+ say '✓ Keystore activated!', :green
1336
+ say ''
1295
1337
  say "#{result['name']} is now the default keystore", :cyan
1296
1338
  rescue Mysigner::NotFoundError
1297
1339
  error "Keystore not found with ID: #{keystore_id}"
1298
- say ""
1299
- say "💡 Keystore Not Found: How to fix", :cyan
1300
- say ""
1301
- say " → List available keystores: mysigner keystore list", :yellow
1302
- say " → Upload a keystore: mysigner keystore upload <path>", :yellow
1303
- say ""
1340
+ say ''
1341
+ say '💡 Keystore Not Found: How to fix', :cyan
1342
+ say ''
1343
+ say ' → List available keystores: mysigner keystore list', :yellow
1344
+ say ' → Upload a keystore: mysigner keystore upload <path>', :yellow
1345
+ say ''
1304
1346
  exit 1
1305
1347
  rescue Mysigner::ClientError => e
1306
1348
  error "Activation failed: #{e.message}"
1307
- say ""
1308
- say "💡 Activation Failed: Try these steps", :cyan
1309
- say ""
1310
- say " → Verify keystore ID is correct: mysigner keystore list", :yellow
1311
- say " → Check API token is valid: mysigner status", :yellow
1312
- say ""
1349
+ say ''
1350
+ say '💡 Activation Failed: Try these steps', :cyan
1351
+ say ''
1352
+ say ' → Verify keystore ID is correct: mysigner keystore list', :yellow
1353
+ say ' → Check API token is valid: mysigner status', :yellow
1354
+ say ''
1313
1355
  exit 1
1314
1356
  end
1315
1357
 
@@ -1317,14 +1359,14 @@ module Mysigner
1317
1359
  invoke :help, ['keystore']
1318
1360
  else
1319
1361
  error "Unknown action: #{action}"
1320
- say "Available actions: list, upload, download, delete, activate, help", :yellow
1362
+ say 'Available actions: list, upload, download, delete, activate, help', :yellow
1321
1363
  exit 1
1322
1364
  end
1323
1365
  end
1324
1366
 
1325
1367
  # ==================== ANDROID APP REGISTRATION ====================
1326
1368
 
1327
- desc "android SUBCOMMAND", "Android commands (init, add, build, list)"
1369
+ desc 'android SUBCOMMAND', 'Android commands (init, add, build, list)'
1328
1370
  long_desc <<~DESC
1329
1371
  Register and manage Android apps with My Signer.
1330
1372
 
@@ -1375,9 +1417,9 @@ module Mysigner
1375
1417
  android_init(config, client)
1376
1418
  when 'add'
1377
1419
  if args.empty?
1378
- error "Usage: mysigner android add PACKAGE_NAME [--name NAME]"
1379
- say ""
1380
- say "Example: mysigner android add com.example.myapp --name \"My App\"", :yellow
1420
+ error 'Usage: mysigner android add PACKAGE_NAME [--name NAME]'
1421
+ say ''
1422
+ say 'Example: mysigner android add com.example.myapp --name "My App"', :yellow
1381
1423
  exit 1
1382
1424
  end
1383
1425
  android_add(config, client, args[0], options[:name])
@@ -1390,7 +1432,7 @@ module Mysigner
1390
1432
  invoke :help, ['android']
1391
1433
  else
1392
1434
  error "Unknown action: #{action}"
1393
- say "Available actions: init, add, build, list, help", :yellow
1435
+ say 'Available actions: init, add, build, list, help', :yellow
1394
1436
  exit 1
1395
1437
  end
1396
1438
  end
@@ -1400,8 +1442,8 @@ module Mysigner
1400
1442
  def android_init(config, client)
1401
1443
  require_relative '../build/android_parser'
1402
1444
 
1403
- say "🔍 Detecting project...", :cyan
1404
- say ""
1445
+ say '🔍 Detecting project...', :cyan
1446
+ say ''
1405
1447
 
1406
1448
  package_name = nil
1407
1449
  app_name = nil
@@ -1422,26 +1464,26 @@ module Mysigner
1422
1464
  app_name = expo_config[:name]
1423
1465
  project_type = 'Expo managed'
1424
1466
  else
1425
- error "No Android project or Expo config found"
1426
- say ""
1467
+ error 'No Android project or Expo config found'
1468
+ say ''
1427
1469
  say "For Expo managed projects, add 'android.package' to app.json:", :yellow
1428
- say ""
1429
- say " {", :white
1430
- say " \"expo\": {", :white
1431
- say " \"android\": { \"package\": \"com.yourcompany.app\" }", :white
1432
- say " }", :white
1433
- say " }", :white
1434
- say ""
1435
- say "Or run from a directory with an android/ folder (native/RN/Capacitor).", :yellow
1470
+ say ''
1471
+ say ' {', :white
1472
+ say ' "expo": {', :white
1473
+ say ' "android": { "package": "com.yourcompany.app" }', :white
1474
+ say ' }', :white
1475
+ say ' }', :white
1476
+ say ''
1477
+ say 'Or run from a directory with an android/ folder (native/RN/Capacitor).', :yellow
1436
1478
  exit 1
1437
1479
  end
1438
1480
  end
1439
1481
 
1440
1482
  say "✓ Found: #{project_type} project", :green
1441
- say ""
1483
+ say ''
1442
1484
  say "📦 Package: #{package_name}", :cyan
1443
1485
  say "📱 Name: #{app_name || '(not set)'}", :cyan
1444
- say ""
1486
+ say ''
1445
1487
 
1446
1488
  # Check if app already exists
1447
1489
  begin
@@ -1453,21 +1495,21 @@ module Mysigner
1453
1495
  existing = existing_apps.find { |a| a['package_name'] == package_name }
1454
1496
 
1455
1497
  if existing
1456
- say "ℹ️ App already registered!", :yellow
1457
- say ""
1458
- say "Details:", :bold
1498
+ say 'ℹ️ App already registered!', :yellow
1499
+ say ''
1500
+ say 'Details:', :bold
1459
1501
  say " ID: #{existing['id']}"
1460
1502
  say " Name: #{existing['name'] || '(not set)'}"
1461
1503
  say " Package: #{existing['package_name']}"
1462
1504
  say " Builds: #{existing['builds_count'] || 0}"
1463
- say ""
1464
- say "Next steps:", :cyan
1465
- if (existing['builds_count'] || 0) > 0
1466
- say " • Ship to Play Store: mysigner ship internal --platform android", :white
1505
+ say ''
1506
+ say 'Next steps:', :cyan
1507
+ if (existing['builds_count'] || 0).positive?
1508
+ say ' • Ship to Play Store: mysigner ship internal --platform android', :white
1467
1509
  else
1468
- say " 1. Build AAB: mysigner android build", :white
1469
- say " 2. Upload first build manually in Play Console (one-time requirement)", :white
1470
- say " 3. After that: mysigner ship internal --platform android", :white
1510
+ say ' 1. Build AAB: mysigner android build', :white
1511
+ say ' 2. Upload first build manually in Play Console (one-time requirement)', :white
1512
+ say ' 3. After that: mysigner ship internal --platform android', :white
1471
1513
  end
1472
1514
  return
1473
1515
  end
@@ -1476,7 +1518,7 @@ module Mysigner
1476
1518
  end
1477
1519
 
1478
1520
  # Register the app
1479
- say "🔗 Registering with My Signer...", :cyan
1521
+ say '🔗 Registering with My Signer...', :cyan
1480
1522
 
1481
1523
  begin
1482
1524
  response = client.post(
@@ -1488,21 +1530,20 @@ module Mysigner
1488
1530
  )
1489
1531
 
1490
1532
  app = response[:data]['android_app'] || response[:data]
1491
- say "✓ App registered successfully!", :green
1492
- say ""
1493
- say "Details:", :bold
1533
+ say '✓ App registered successfully!', :green
1534
+ say ''
1535
+ say 'Details:', :bold
1494
1536
  say " ID: #{app['id']}"
1495
1537
  say " Name: #{app['name'] || '(not set)'}"
1496
1538
  say " Package: #{app['package_name']}"
1497
- say ""
1498
- say "Next steps:", :cyan
1499
- say " 1. Create app in Google Play Console", :white
1500
- say " 2. Build AAB: mysigner android build", :white
1501
- say " 3. Upload first build manually in Play Console (one-time requirement)", :white
1502
- say " 4. After that: mysigner ship internal --platform android", :white
1503
-
1539
+ say ''
1540
+ say 'Next steps:', :cyan
1541
+ say ' 1. Create app in Google Play Console', :white
1542
+ say ' 2. Build AAB: mysigner android build', :white
1543
+ say ' 3. Upload first build manually in Play Console (one-time requirement)', :white
1544
+ say ' 4. After that: mysigner ship internal --platform android', :white
1504
1545
  rescue Mysigner::ValidationError => e
1505
- error "Validation failed:"
1546
+ error 'Validation failed:'
1506
1547
  if e.details
1507
1548
  e.details.each do |field, errors|
1508
1549
  say " #{field}: #{errors.join(', ')}", :red
@@ -1519,11 +1560,11 @@ module Mysigner
1519
1560
  end
1520
1561
 
1521
1562
  def android_add(config, client, package_name, name = nil)
1522
- say "📦 Registering Android app...", :cyan
1523
- say ""
1563
+ say '📦 Registering Android app...', :cyan
1564
+ say ''
1524
1565
  say " Package: #{package_name}", :white
1525
1566
  say " Name: #{name || '(will be synced from Play Store)'}", :white
1526
- say ""
1567
+ say ''
1527
1568
 
1528
1569
  begin
1529
1570
  response = client.post(
@@ -1535,25 +1576,24 @@ module Mysigner
1535
1576
  )
1536
1577
 
1537
1578
  app = response[:data]['android_app'] || response[:data]
1538
- say "✓ App registered successfully!", :green
1539
- say ""
1540
- say "Details:", :bold
1579
+ say '✓ App registered successfully!', :green
1580
+ say ''
1581
+ say 'Details:', :bold
1541
1582
  say " ID: #{app['id']}"
1542
1583
  say " Name: #{app['name'] || '(not set)'}"
1543
1584
  say " Package: #{app['package_name']}"
1544
- say ""
1545
- say "Next steps:", :cyan
1546
- if (app['builds_count'] || 0) > 0
1547
- say " • Ship to Play Store: mysigner ship internal --platform android", :white
1585
+ say ''
1586
+ say 'Next steps:', :cyan
1587
+ if (app['builds_count'] || 0).positive?
1588
+ say ' • Ship to Play Store: mysigner ship internal --platform android', :white
1548
1589
  else
1549
- say " 1. Create app in Google Play Console (if not done)", :white
1550
- say " 2. Build AAB: mysigner android build", :white
1551
- say " 3. Upload first build manually in Play Console", :white
1552
- say " 4. Then use: mysigner ship internal --platform android", :white
1590
+ say ' 1. Create app in Google Play Console (if not done)', :white
1591
+ say ' 2. Build AAB: mysigner android build', :white
1592
+ say ' 3. Upload first build manually in Play Console', :white
1593
+ say ' 4. Then use: mysigner ship internal --platform android', :white
1553
1594
  end
1554
-
1555
1595
  rescue Mysigner::ValidationError => e
1556
- error "Validation failed:"
1596
+ error 'Validation failed:'
1557
1597
  if e.details
1558
1598
  e.details.each do |field, errors|
1559
1599
  say " #{field}: #{errors.join(', ')}", :red
@@ -1564,10 +1604,10 @@ module Mysigner
1564
1604
  say " Suggestion: #{e.suggestion}", :yellow if e.suggestion
1565
1605
  exit 1
1566
1606
  rescue Mysigner::ClientError => e
1567
- if e.message.include?("already exists") || e.message.include?("taken")
1568
- error "An app with this package name already exists"
1569
- say ""
1570
- say "List your apps with: mysigner android list", :yellow
1607
+ if e.message.include?('already exists') || e.message.include?('taken')
1608
+ error 'An app with this package name already exists'
1609
+ say ''
1610
+ say 'List your apps with: mysigner android list', :yellow
1571
1611
  else
1572
1612
  error "Failed to register app: #{e.message}"
1573
1613
  end
@@ -1576,32 +1616,32 @@ module Mysigner
1576
1616
  end
1577
1617
 
1578
1618
  def android_build
1579
- say "🔨 Building Android App Bundle (AAB)...", :cyan
1580
- say ""
1619
+ say '🔨 Building Android App Bundle (AAB)...', :cyan
1620
+ say ''
1581
1621
 
1582
1622
  begin
1583
1623
  require_relative '../build/android_parser'
1584
1624
  require_relative '../signing/keystore_manager'
1585
-
1625
+
1586
1626
  project_dir = Dir.pwd
1587
1627
  is_expo = expo_project?(project_dir)
1588
-
1628
+
1589
1629
  # For Expo, we may need to regenerate android folder with correct versionCode
1590
1630
  # Get package name from app.json first if Expo
1591
1631
  if is_expo
1592
1632
  expo_config = parse_expo_config(project_dir)
1593
1633
  package_name = expo_config[:package_name]
1594
1634
  local_version_code = expo_config[:version_code] || 1
1595
- version_name = expo_config[:version] || "1.0.0"
1596
-
1635
+ version_name = expo_config[:version] || '1.0.0'
1636
+
1597
1637
  # Check highest version code from API
1598
1638
  highest_version_code = fetch_highest_version_code(package_name)
1599
1639
  version_code = local_version_code
1600
1640
  needs_increment = highest_version_code && local_version_code <= highest_version_code
1601
-
1641
+
1602
1642
  if needs_increment
1603
1643
  version_code = highest_version_code + 1
1604
-
1644
+
1605
1645
  # Check if android folder already has the correct versionCode
1606
1646
  android_dir = File.join(project_dir, 'android')
1607
1647
  current_android_version = nil
@@ -1609,49 +1649,54 @@ module Mysigner
1609
1649
  build_gradle = File.join(android_dir, 'app', 'build.gradle')
1610
1650
  if File.exist?(build_gradle)
1611
1651
  content = File.read(build_gradle)
1612
- if content =~ /versionCode\s+(\d+)/
1613
- current_android_version = $1.to_i
1614
- end
1652
+ current_android_version = ::Regexp.last_match(1).to_i if content =~ /versionCode\s+(\d+)/
1615
1653
  end
1616
1654
  end
1617
-
1618
- say "🔧 Framework: Expo (React Native)", :white
1655
+
1656
+ say '🔧 Framework: Expo (React Native)', :white
1619
1657
  say "📦 Package: #{package_name}", :white
1620
-
1658
+
1621
1659
  if current_android_version == version_code
1622
1660
  # Android folder already has correct version, no need to regenerate
1623
1661
  say "🔢 Version: #{version_name} (#{version_code})", :white
1624
- say " ↳ Already at correct version code", :green
1662
+ say ' ↳ Already at correct version code', :green
1625
1663
  else
1626
1664
  say "🔢 Version: #{version_name} (#{local_version_code} → #{version_code})", :white
1627
1665
  say " ↳ Auto-incremented (#{highest_version_code} already on Play Store)", :yellow
1628
- say ""
1629
-
1666
+ say ''
1667
+
1630
1668
  # Regenerate android folder with new versionCode
1631
1669
  say "🔄 Regenerating android folder with version code #{version_code}...", :yellow
1632
1670
  regenerate_expo_android(project_dir, version_code)
1633
1671
  end
1634
1672
  end
1635
1673
  end
1636
-
1674
+
1637
1675
  # Now detect project (android folder should exist)
1638
1676
  project_info = Build::Detector.detect_android(project_dir)
1639
1677
  framework = project_info[:framework]
1640
1678
  parser = Build::AndroidParser.new(project_info)
1641
-
1679
+
1642
1680
  package_name ||= parser.application_id
1643
1681
  version_name ||= parser.version_name
1644
1682
  local_version_code ||= parser.version_code.to_i
1645
1683
  android_dir = project_info[:android_directory] || File.join(project_info[:directory], 'android')
1646
1684
 
1647
1685
  # For non-Expo, check version code now
1648
- unless is_expo
1686
+ if is_expo
1687
+ # Expo - already printed above, just show if no increment was needed
1688
+ unless needs_increment
1689
+ say '🔧 Framework: Expo (React Native)', :white
1690
+ say "📦 Package: #{package_name}", :white
1691
+ say "🔢 Version: #{version_name} (#{version_code})", :white
1692
+ end
1693
+ else
1649
1694
  say "🔧 Framework: #{framework.to_s.gsub('_', ' ').capitalize}", :white
1650
1695
  say "📦 Package: #{package_name}", :white
1651
-
1696
+
1652
1697
  highest_version_code = fetch_highest_version_code(package_name)
1653
1698
  version_code = local_version_code
1654
-
1699
+
1655
1700
  if highest_version_code && local_version_code <= highest_version_code
1656
1701
  version_code = highest_version_code + 1
1657
1702
  say "🔢 Version: #{version_name} (#{local_version_code} → #{version_code})", :white
@@ -1659,84 +1704,78 @@ module Mysigner
1659
1704
  else
1660
1705
  say "🔢 Version: #{version_name} (#{version_code})", :white
1661
1706
  end
1662
- else
1663
- # Expo - already printed above, just show if no increment was needed
1664
- unless needs_increment
1665
- say "🔧 Framework: Expo (React Native)", :white
1666
- say "📦 Package: #{package_name}", :white
1667
- say "🔢 Version: #{version_name} (#{version_code})", :white
1668
- end
1669
1707
  end
1670
- say ""
1708
+ say ''
1671
1709
 
1672
1710
  # Try to get keystore from MySigner
1673
1711
  keystore_info = fetch_keystore_for_build(package_name)
1674
1712
  if keystore_info
1675
1713
  say "🔐 Keystore: #{keystore_info[:name]}", :green
1676
1714
  else
1677
- say "⚠️ No keystore configured - will use debug signing", :yellow
1715
+ say '⚠️ No keystore configured - will use debug signing', :yellow
1678
1716
  say " Run 'mysigner android init' to set up release signing", :yellow
1679
1717
  end
1680
- say ""
1681
- say "⏱️ This may take a few minutes...", :yellow
1682
- say ""
1718
+ say ''
1719
+ say '⏱️ This may take a few minutes...', :yellow
1720
+ say ''
1683
1721
 
1684
1722
  # Build based on framework (pass version_code override if incremented)
1685
1723
  # For Expo, we already regenerated with correct version, so no override needed
1686
1724
  version_code_override = nil
1687
1725
  unless is_expo
1688
- version_code_override = (version_code != local_version_code) ? version_code : nil
1726
+ version_code_override = version_code == local_version_code ? nil : version_code
1689
1727
  end
1690
-
1728
+
1691
1729
  aab_path = case framework
1692
- when :flutter
1693
- build_flutter_aab(project_dir, keystore_info, version_code_override)
1694
- when :maui, :xamarin, :xamarin_forms
1695
- build_dotnet_aab(project_dir, project_info[:csproj_path], framework, keystore_info, version_code_override)
1696
- when :react_native, :capacitor, :native
1697
- build_gradle_aab(android_dir, framework, keystore_info, version_code_override)
1698
- else
1699
- build_gradle_aab(android_dir, framework, keystore_info, version_code_override)
1700
- end
1730
+ when :flutter
1731
+ build_flutter_aab(project_dir, keystore_info, version_code_override)
1732
+ when :maui, :xamarin, :xamarin_forms
1733
+ build_dotnet_aab(project_dir, project_info[:csproj_path], framework, keystore_info,
1734
+ version_code_override)
1735
+ when :react_native, :capacitor, :native
1736
+ build_gradle_aab(android_dir, framework, keystore_info, version_code_override)
1737
+ else
1738
+ build_gradle_aab(android_dir, framework, keystore_info, version_code_override)
1739
+ end
1701
1740
 
1702
1741
  unless aab_path && File.exist?(aab_path)
1703
- error "AAB file not found after build"
1704
- say "Check build output for errors.", :yellow
1742
+ error 'AAB file not found after build'
1743
+ say 'Check build output for errors.', :yellow
1705
1744
  exit 1
1706
1745
  end
1707
1746
 
1708
- say ""
1709
- say "=" * 80, :green
1710
- say "✓ Build complete!", :green
1711
- say "=" * 80, :green
1712
- say ""
1747
+ say ''
1748
+ say '=' * 80, :green
1749
+ say '✓ Build complete!', :green
1750
+ say '=' * 80, :green
1751
+ say ''
1713
1752
  say "📦 AAB: #{aab_path}", :cyan
1714
1753
  say "📊 Size: #{format_bytes(File.size(aab_path))}", :cyan
1715
- say ""
1716
- say "Next step:", :bold
1717
- say " Upload this AAB to Google Play Console → Internal testing → Create release", :white
1718
- say ""
1719
- say "After uploading, you can use:", :cyan
1720
- say " mysigner ship internal --platform android", :green
1721
- say ""
1722
-
1754
+ say ''
1755
+ say 'Next step:', :bold
1756
+ say ' Upload this AAB to Google Play Console → Internal testing → Create release', :white
1757
+ say ''
1758
+ say 'After uploading, you can use:', :cyan
1759
+ say ' mysigner ship internal --platform android', :green
1760
+ say ''
1761
+
1723
1762
  # Open the folder containing the AAB
1724
1763
  aab_dir = File.dirname(aab_path)
1725
- say "📂 Opening folder...", :yellow
1726
- if RUBY_PLATFORM =~ /darwin/
1764
+ say '📂 Opening folder...', :yellow
1765
+ case RUBY_PLATFORM
1766
+ when /darwin/
1727
1767
  system('open', aab_dir)
1728
- elsif RUBY_PLATFORM =~ /linux/
1768
+ when /linux/
1729
1769
  system('xdg-open', aab_dir)
1730
- elsif RUBY_PLATFORM =~ /mingw|mswin/
1770
+ when /mingw|mswin/
1731
1771
  system('explorer', aab_dir.gsub('/', '\\'))
1732
1772
  end
1733
-
1734
1773
  rescue Build::Detector::NoProjectError => e
1735
1774
  error e.message
1736
- say ""
1737
- say "Run this command from an Android project directory.", :yellow
1775
+ say ''
1776
+ say 'Run this command from an Android project directory.', :yellow
1738
1777
  exit 1
1739
- rescue => e
1778
+ rescue StandardError => e
1740
1779
  error "Build failed: #{e.message}"
1741
1780
  exit 1
1742
1781
  end
@@ -1745,42 +1784,69 @@ module Mysigner
1745
1784
  def build_flutter_aab(project_dir, keystore_info = nil, version_code_override = nil)
1746
1785
  # Check for flutter
1747
1786
  unless system('which flutter > /dev/null 2>&1')
1748
- error "Flutter not found in PATH"
1749
- say "Install Flutter: https://flutter.dev/docs/get-started/install", :yellow
1787
+ error 'Flutter not found in PATH'
1788
+ say 'Install Flutter: https://flutter.dev/docs/get-started/install', :yellow
1750
1789
  exit 1
1751
1790
  end
1752
1791
 
1753
- Dir.chdir(project_dir) do
1754
- args = ['flutter', 'build', 'appbundle', '--release']
1755
-
1756
- # Add version code override
1757
- if version_code_override
1758
- args += ['--build-number', version_code_override.to_s]
1759
- end
1760
-
1761
- # Add signing if keystore provided (Flutter reads key.properties from android/)
1762
- if keystore_info
1763
- # Create key.properties for Flutter
1764
- key_props = File.join(project_dir, 'android/key.properties')
1765
- File.write(key_props, <<~PROPS)
1766
- storePassword=#{keystore_info[:password]}
1767
- keyPassword=#{keystore_info[:key_password]}
1768
- keyAlias=#{keystore_info[:key_alias]}
1769
- storeFile=#{keystore_info[:path]}
1770
- PROPS
1771
- end
1772
-
1773
- success = system(*args)
1774
- unless success
1775
- error "Flutter build failed"
1776
- exit 1
1792
+ # Phase 0: do NOT write plaintext key.properties into the user's
1793
+ # project tree. Use a two-step build so we can inject signing via
1794
+ # a Gradle init script on the Gradle step only. Env vars are set
1795
+ # on the child process; passwords never appear in argv or on disk.
1796
+ require_relative '../signing/gradle_signing_injector'
1797
+
1798
+ injector = nil
1799
+ env = {}
1800
+ init_path = nil
1801
+
1802
+ if keystore_info
1803
+ injector = Mysigner::Signing::GradleSigningInjector.new
1804
+ init_path = injector.write_init_script!
1805
+ env = keystore_info[:signing_env_vars] || injector.env_vars(
1806
+ keystore_path: keystore_info[:path],
1807
+ store_password: keystore_info[:password],
1808
+ key_password: keystore_info[:key_password],
1809
+ key_alias: keystore_info[:key_alias]
1810
+ )
1811
+ end
1812
+
1813
+ begin
1814
+ Dir.chdir(project_dir) do
1815
+ if keystore_info
1816
+ # Step 1: Flutter prepares the Gradle project (no signing needed).
1817
+ prepare_args = ['flutter', 'build', 'appbundle', '--release', '--config-only']
1818
+ prepare_args += ['--build-number', version_code_override.to_s] if version_code_override
1819
+ unless system(env, *prepare_args)
1820
+ error 'Flutter prebuild (--config-only) failed'
1821
+ exit 1
1822
+ end
1823
+
1824
+ # Step 2: invoke Gradle directly so we can pass --init-script.
1825
+ Dir.chdir(File.join(project_dir, 'android')) do
1826
+ gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all', '--init-script', init_path]
1827
+ gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
1828
+ unless system(env, *gradle_args)
1829
+ error 'Gradle bundleRelease failed'
1830
+ exit 1
1831
+ end
1832
+ end
1833
+ else
1834
+ # No keystore: plain Flutter build (debug signing).
1835
+ args = ['flutter', 'build', 'appbundle', '--release']
1836
+ args += ['--build-number', version_code_override.to_s] if version_code_override
1837
+ unless system(*args)
1838
+ error 'Flutter build failed'
1839
+ exit 1
1840
+ end
1841
+ end
1777
1842
  end
1843
+ ensure
1844
+ injector&.cleanup!
1778
1845
  end
1779
1846
 
1780
1847
  # Flutter outputs to build/app/outputs/bundle/release/
1781
1848
  aab_path = File.join(project_dir, 'build/app/outputs/bundle/release/app-release.aab')
1782
1849
  unless File.exist?(aab_path)
1783
- # Try alternate paths
1784
1850
  alt_paths = Dir.glob(File.join(project_dir, 'build/app/outputs/bundle/*/*.aab'))
1785
1851
  aab_path = alt_paths.first if alt_paths.any?
1786
1852
  end
@@ -1798,46 +1864,41 @@ module Mysigner
1798
1864
  app_json_path = File.join(project_dir, 'app.json')
1799
1865
  android_dir = File.join(project_dir, 'android')
1800
1866
  local_props_path = File.join(android_dir, 'local.properties')
1801
-
1867
+
1802
1868
  # Preserve local.properties if it exists
1803
1869
  local_props_content = File.read(local_props_path) if File.exist?(local_props_path)
1804
-
1870
+
1805
1871
  # Read original app.json
1806
1872
  original_content = File.read(app_json_path)
1807
1873
  config = JSON.parse(original_content)
1808
-
1874
+
1809
1875
  # Set the new versionCode
1810
1876
  config['expo'] ||= {}
1811
1877
  config['expo']['android'] ||= {}
1812
1878
  config['expo']['android']['versionCode'] = new_version_code
1813
-
1879
+
1814
1880
  # Write modified app.json
1815
1881
  File.write(app_json_path, JSON.pretty_generate(config))
1816
-
1882
+
1817
1883
  begin
1818
1884
  # Delete existing android folder
1819
- if Dir.exist?(android_dir)
1820
- FileUtils.rm_rf(android_dir)
1821
- end
1822
-
1885
+
1886
+ FileUtils.rm_rf(android_dir)
1887
+
1823
1888
  # Run expo prebuild
1824
1889
  Dir.chdir(project_dir) do
1825
1890
  success = system('npx', 'expo', 'prebuild', '--platform', 'android', '--clean')
1826
- unless success
1827
- raise "expo prebuild failed"
1828
- end
1891
+ raise 'expo prebuild failed' unless success
1829
1892
  end
1830
-
1893
+
1831
1894
  # Restore local.properties if we had one, or create default
1832
1895
  if local_props_content
1833
1896
  File.write(local_props_path, local_props_content)
1834
1897
  else
1835
1898
  # Try to detect Android SDK and create local.properties
1836
- sdk_path = ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT'] ||
1899
+ sdk_path = ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT'] ||
1837
1900
  File.expand_path('~/Library/Android/sdk')
1838
- if Dir.exist?(sdk_path)
1839
- File.write(local_props_path, "sdk.dir=#{sdk_path}\n")
1840
- end
1901
+ File.write(local_props_path, "sdk.dir=#{sdk_path}\n") if Dir.exist?(sdk_path)
1841
1902
  end
1842
1903
  ensure
1843
1904
  # Restore original app.json
@@ -1848,19 +1909,22 @@ module Mysigner
1848
1909
  def fetch_highest_version_code(package_name)
1849
1910
  config = Mysigner::Config.new
1850
1911
  return nil unless config.exists?
1912
+
1851
1913
  config.load
1852
1914
  return nil unless config.api_token && config.organization_id
1853
1915
 
1854
- client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token, user_email: config.user_email)
1916
+ client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token,
1917
+ user_email: config.user_email)
1855
1918
 
1856
1919
  # Find app by package name
1857
1920
  response = client.get("/api/v1/organizations/#{config.organization_id}/android_apps")
1858
1921
  apps = response[:data]['android_apps'] || []
1859
1922
  app = apps.find { |a| a['package_name'] == package_name }
1860
-
1923
+
1861
1924
  return app['highest_version_code'].to_i if app && app['highest_version_code']
1925
+
1862
1926
  nil
1863
- rescue => e
1927
+ rescue StandardError
1864
1928
  # Silently fail - we'll use local version
1865
1929
  nil
1866
1930
  end
@@ -1868,101 +1932,138 @@ module Mysigner
1868
1932
  def fetch_keystore_for_build(package_name)
1869
1933
  config = Mysigner::Config.new
1870
1934
  return nil unless config.exists?
1935
+
1871
1936
  config.load
1872
- return nil unless config.api_token && config.organization_id
1937
+ return nil unless config.api_token && config.current_organization_id
1873
1938
 
1874
- client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token, user_email: config.user_email)
1875
- keystore_manager = Signing::KeystoreManager.new(client, config.organization_id)
1939
+ client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token,
1940
+ user_email: config.user_email)
1941
+ keystore_manager = Signing::KeystoreManager.new(client, config.current_organization_id)
1876
1942
 
1877
1943
  # Find app by package name to get its keystore
1878
- response = client.get("/api/v1/organizations/#{config.organization_id}/android_apps")
1944
+ response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
1879
1945
  apps = response[:data]['android_apps'] || []
1880
1946
  app = apps.find { |a| a['package_name'] == package_name }
1881
1947
 
1882
- if app
1883
- # Get active keystore for this app with secrets
1884
- keystore = keystore_manager.active_keystore(android_app_id: app['id'], include_secrets: true)
1885
- if keystore
1886
- # Download the keystore file
1887
- downloaded = keystore_manager.get_or_download(keystore['id'])
1888
- return {
1889
- path: downloaded[:path],
1890
- name: keystore['name'],
1891
- password: keystore['keystore_password'],
1892
- key_alias: keystore['key_alias'],
1893
- key_password: keystore['key_password'] || keystore['keystore_password']
1894
- }
1895
- end
1896
- end
1897
-
1898
- # Try to get any active keystore
1899
- keystore = keystore_manager.active_keystore(include_secrets: true)
1900
- if keystore
1901
- downloaded = keystore_manager.get_or_download(keystore['id'])
1902
- return {
1903
- path: downloaded[:path],
1904
- name: keystore['name'],
1905
- password: keystore['keystore_password'],
1906
- key_alias: keystore['key_alias'],
1907
- key_password: keystore['key_password'] || keystore['keystore_password']
1948
+ keystore = nil
1949
+ keystore = keystore_manager.active_keystore(android_app_id: app['id']) if app
1950
+ keystore ||= keystore_manager.active_keystore
1951
+ return nil unless keystore
1952
+
1953
+ # Phase 0: fetch passwords via narrow audit-logged /secrets endpoint
1954
+ # instead of the deprecated ?include_secrets=true param on the list.
1955
+ secrets = keystore_manager.fetch_secrets(keystore['id'])
1956
+ downloaded = keystore_manager.get_or_download(keystore['id'])
1957
+ password = secrets['keystore_password']
1958
+ key_password = secrets['key_password'] || password
1959
+ key_alias = secrets['key_alias'] || keystore['key_alias']
1960
+ {
1961
+ path: downloaded[:path],
1962
+ name: keystore['name'],
1963
+ password: password,
1964
+ key_alias: key_alias,
1965
+ key_password: key_password,
1966
+ id: keystore['id'],
1967
+ # Ready-to-spawn env vars consumed by GradleSigningInjector in
1968
+ # build_gradle_aab / build_flutter_aab / Build::AndroidExecutor.
1969
+ signing_env_vars: {
1970
+ 'MYSIGNER_STORE_FILE' => downloaded[:path],
1971
+ 'MYSIGNER_STORE_PASSWORD' => password,
1972
+ 'MYSIGNER_KEY_PASSWORD' => key_password,
1973
+ 'MYSIGNER_KEY_ALIAS' => key_alias
1908
1974
  }
1909
- end
1910
-
1911
- nil
1912
- rescue => e
1975
+ }
1976
+ rescue StandardError
1913
1977
  # Silently fail - we'll use debug signing
1914
1978
  nil
1915
1979
  end
1916
1980
 
1917
- def build_dotnet_aab(project_dir, csproj_path, framework, keystore_info = nil, version_code_override = nil)
1981
+ def build_dotnet_aab(project_dir, _csproj_path, framework, keystore_info = nil, version_code_override = nil)
1918
1982
  # Check for dotnet
1919
1983
  unless system('which dotnet > /dev/null 2>&1')
1920
- error ".NET SDK not found in PATH"
1921
- say "Install .NET: https://dotnet.microsoft.com/download", :yellow
1984
+ error '.NET SDK not found in PATH'
1985
+ say 'Install .NET: https://dotnet.microsoft.com/download', :yellow
1922
1986
  exit 1
1923
1987
  end
1924
1988
 
1989
+ require 'tmpdir'
1990
+ pw_tmpdir = nil
1991
+ store_pw_path = nil
1992
+ key_pw_path = nil
1993
+
1925
1994
  Dir.chdir(project_dir) do
1926
1995
  base_args = []
1927
-
1928
- # Add signing args if keystore provided
1996
+
1997
+ # Phase 0: For AAB output, MSBuild only supports `file:<path>`
1998
+ # for AndroidSigning*Pass (env: is explicitly unsupported for AAB).
1999
+ # Write passwords to 0600 tempfiles whose *paths* go into argv
2000
+ # instead of the passwords themselves.
1929
2001
  if keystore_info
2002
+ pw_tmpdir = Dir.mktmpdir('mysigner-maui-pw-')
2003
+ store_pw_path = File.join(pw_tmpdir, 'store_pw.txt')
2004
+ File.write(store_pw_path, keystore_info[:password].to_s)
2005
+ File.chmod(0o600, store_pw_path)
2006
+
2007
+ # Reuse the same file if store/key passwords match (common)
2008
+ if keystore_info[:key_password] == keystore_info[:password]
2009
+ key_pw_path = store_pw_path
2010
+ else
2011
+ key_pw_path = File.join(pw_tmpdir, 'key_pw.txt')
2012
+ File.write(key_pw_path, keystore_info[:key_password].to_s)
2013
+ File.chmod(0o600, key_pw_path)
2014
+ end
2015
+
1930
2016
  base_args += [
1931
- "-p:AndroidKeyStore=true",
2017
+ '-p:AndroidKeyStore=true',
1932
2018
  "-p:AndroidSigningKeyStore=#{keystore_info[:path]}",
1933
2019
  "-p:AndroidSigningKeyAlias=#{keystore_info[:key_alias]}",
1934
- "-p:AndroidSigningKeyPass=#{keystore_info[:key_password]}",
1935
- "-p:AndroidSigningStorePass=#{keystore_info[:password]}"
2020
+ "-p:AndroidSigningKeyPass=file:#{key_pw_path}",
2021
+ "-p:AndroidSigningStorePass=file:#{store_pw_path}"
1936
2022
  ]
1937
2023
  end
1938
-
2024
+
1939
2025
  # Add version code override
1940
- if version_code_override
1941
- base_args << "-p:ApplicationVersion=#{version_code_override}"
1942
- end
1943
-
1944
- # MAUI uses dotnet publish with Android target
1945
- if framework == :maui
1946
- success = system(
1947
- 'dotnet', 'publish',
1948
- '-f', 'net8.0-android',
1949
- '-c', 'Release',
1950
- '-p:AndroidPackageFormat=aab',
1951
- *base_args
1952
- )
1953
- else
1954
- # Xamarin uses msbuild
1955
- success = system(
1956
- 'dotnet', 'build',
1957
- '-c', 'Release',
1958
- '-p:AndroidPackageFormat=aab',
1959
- *base_args
1960
- )
1961
- end
2026
+ base_args << "-p:ApplicationVersion=#{version_code_override}" if version_code_override
1962
2027
 
1963
- unless success
1964
- error ".NET build failed"
1965
- exit 1
2028
+ begin
2029
+ # MAUI uses dotnet publish with Android target
2030
+ success = if framework == :maui
2031
+ system(
2032
+ 'dotnet', 'publish',
2033
+ '-f', 'net8.0-android',
2034
+ '-c', 'Release',
2035
+ '-p:AndroidPackageFormat=aab',
2036
+ *base_args
2037
+ )
2038
+ else
2039
+ # Xamarin uses msbuild
2040
+ system(
2041
+ 'dotnet', 'build',
2042
+ '-c', 'Release',
2043
+ '-p:AndroidPackageFormat=aab',
2044
+ *base_args
2045
+ )
2046
+ end
2047
+
2048
+ unless success
2049
+ error '.NET build failed'
2050
+ exit 1
2051
+ end
2052
+ ensure
2053
+ # Clean up password files. On Windows NTFS the child may still
2054
+ # hold the file open; fall back to at_exit deferred delete.
2055
+ [store_pw_path, key_pw_path].compact.uniq.each do |p|
2056
+ File.delete(p) if File.exist?(p)
2057
+ rescue Errno::EACCES
2058
+ at_exit do
2059
+ File.delete(p)
2060
+ rescue StandardError
2061
+ nil
2062
+ end
2063
+ rescue StandardError
2064
+ # Best effort
2065
+ end
2066
+ FileUtils.rm_rf(pw_tmpdir) if pw_tmpdir && Dir.exist?(pw_tmpdir)
1966
2067
  end
1967
2068
  end
1968
2069
 
@@ -1983,35 +2084,43 @@ module Mysigner
1983
2084
  when :capacitor
1984
2085
  say "Run 'npx cap sync android' first.", :yellow
1985
2086
  else
1986
- say "Ensure the android/ folder has gradlew.", :yellow
2087
+ say 'Ensure the android/ folder has gradlew.', :yellow
1987
2088
  end
1988
2089
  exit 1
1989
2090
  end
1990
2091
 
1991
- # Build gradle command with signing via command-line properties
2092
+ # Phase 0: inject signing via Gradle init-script + env vars so
2093
+ # passwords never appear in `ps aux` (ORG_GRADLE_PROJECT_<name>
2094
+ # was the old workaround but doesn't support dotted names).
2095
+ require_relative '../signing/gradle_signing_injector'
2096
+
1992
2097
  gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all']
1993
-
2098
+ gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
2099
+
2100
+ injector = nil
2101
+ env = {}
1994
2102
  if keystore_info
1995
- # Pass signing config via command-line properties (no file modification needed)
1996
- gradle_args += [
1997
- "-Pandroid.injected.signing.store.file=#{keystore_info[:path]}",
1998
- "-Pandroid.injected.signing.store.password=#{keystore_info[:password]}",
1999
- "-Pandroid.injected.signing.key.alias=#{keystore_info[:key_alias]}",
2000
- "-Pandroid.injected.signing.key.password=#{keystore_info[:key_password]}"
2001
- ]
2002
- end
2003
-
2004
- # Pass version code override if provided (no file modification needed)
2005
- if version_code_override
2006
- gradle_args << "-PversionCode=#{version_code_override}"
2103
+ injector = Mysigner::Signing::GradleSigningInjector.new
2104
+ init_path = injector.write_init_script!
2105
+ env = keystore_info[:signing_env_vars] || injector.env_vars(
2106
+ keystore_path: keystore_info[:path],
2107
+ store_password: keystore_info[:password],
2108
+ key_password: keystore_info[:key_password],
2109
+ key_alias: keystore_info[:key_alias]
2110
+ )
2111
+ gradle_args.insert(1, '--init-script', init_path)
2007
2112
  end
2008
2113
 
2009
- Dir.chdir(android_dir) do
2010
- success = system(*gradle_args)
2011
- unless success
2012
- error "Gradle build failed"
2013
- exit 1
2114
+ begin
2115
+ Dir.chdir(android_dir) do
2116
+ success = system(env, *gradle_args)
2117
+ unless success
2118
+ error 'Gradle build failed'
2119
+ exit 1
2120
+ end
2014
2121
  end
2122
+ ensure
2123
+ injector&.cleanup!
2015
2124
  end
2016
2125
 
2017
2126
  # Find the AAB
@@ -2049,11 +2158,9 @@ module Mysigner
2049
2158
  content = File.read(app_config_path)
2050
2159
  # Basic regex extraction for package name
2051
2160
  if content =~ /android\s*:\s*\{[^}]*package\s*:\s*["']([^"']+)["']/m
2052
- package_name = $1
2161
+ package_name = ::Regexp.last_match(1)
2053
2162
  name = nil
2054
- if content =~ /name\s*:\s*["']([^"']+)["']/
2055
- name = $1
2056
- end
2163
+ name = ::Regexp.last_match(1) if content =~ /name\s*:\s*["']([^"']+)["']/
2057
2164
  return {
2058
2165
  package_name: package_name,
2059
2166
  bundle_id: nil,
@@ -2069,7 +2176,7 @@ module Mysigner
2069
2176
 
2070
2177
  # ==================== BUNDLE IDS ====================
2071
2178
 
2072
- desc "bundleid SUBCOMMAND", "Register and manage iOS Bundle IDs"
2179
+ desc 'bundleid SUBCOMMAND', 'Register and manage iOS Bundle IDs'
2073
2180
  long_desc <<~DESC
2074
2181
  Register and manage iOS Bundle IDs in App Store Connect.
2075
2182
 
@@ -2115,34 +2222,38 @@ module Mysigner
2115
2222
 
2116
2223
  case action
2117
2224
  when 'register'
2118
- if args.empty?
2119
- error "Usage: mysigner bundleid register IDENTIFIER [NAME]"
2120
- say ""
2121
- say "Example: mysigner bundleid register com.company.myapp", :yellow
2122
- say "Example: mysigner bundleid register com.company.myapp.widget \"My Widget\"", :yellow
2225
+ if args.empty? || args[0].nil? || args[0].to_s.empty?
2226
+ error 'Usage: mysigner bundleid register IDENTIFIER [NAME]'
2227
+ say ''
2228
+ say 'Example: mysigner bundleid register com.company.myapp', :yellow
2229
+ say 'Example: mysigner bundleid register com.company.myapp.widget "My Widget"', :yellow
2123
2230
  exit 1
2231
+ return
2124
2232
  end
2125
2233
 
2126
2234
  identifier = args[0]
2127
- # Default name is the last component of the identifier
2128
- name = args[1] || identifier.split('.').last.capitalize
2129
2235
 
2130
- # Validate bundle ID format
2236
+ # Validate bundle ID format BEFORE dereferencing (guards against
2237
+ # `.split` on a degenerate identifier like "123.com.app").
2131
2238
  unless identifier =~ /^[a-zA-Z][a-zA-Z0-9.-]*\.[a-zA-Z][a-zA-Z0-9.-]*$/
2132
2239
  error "Invalid Bundle ID format: #{identifier}"
2133
- say ""
2134
- say "Bundle IDs must:", :yellow
2135
- say " • Start with a letter", :cyan
2136
- say " • Use reverse domain notation (e.g., com.company.app)", :cyan
2137
- say " • Contain only letters, numbers, hyphens, and periods", :cyan
2240
+ say ''
2241
+ say 'Bundle IDs must:', :yellow
2242
+ say ' • Start with a letter', :cyan
2243
+ say ' • Use reverse domain notation (e.g., com.company.app)', :cyan
2244
+ say ' • Contain only letters, numbers, hyphens, and periods', :cyan
2138
2245
  exit 1
2246
+ return
2139
2247
  end
2140
2248
 
2141
- say "🔗 Registering Bundle ID...", :cyan
2142
- say ""
2249
+ # Default name is the last component of the identifier
2250
+ name = args[1] || identifier.split('.').last.capitalize
2251
+
2252
+ say '🔗 Registering Bundle ID...', :cyan
2253
+ say ''
2143
2254
  say " Identifier: #{identifier}", :white
2144
2255
  say " Name: #{name}", :white
2145
- say ""
2256
+ say ''
2146
2257
 
2147
2258
  begin
2148
2259
  response = client.post(
@@ -2155,18 +2266,18 @@ module Mysigner
2155
2266
  )
2156
2267
 
2157
2268
  bundle_id_data = response[:data]['bundle_id'] || response[:data]
2158
- say "✓ Bundle ID registered successfully!", :green
2159
- say ""
2160
- say "Details:", :bold
2269
+ say '✓ Bundle ID registered successfully!', :green
2270
+ say ''
2271
+ say 'Details:', :bold
2161
2272
  say " Identifier: #{bundle_id_data['identifier'] || identifier}"
2162
2273
  say " Name: #{bundle_id_data['name'] || name}"
2163
- say ""
2164
- say "Next steps:", :cyan
2165
- say " 1. Sync to update local cache: mysigner sync ios", :white
2166
- say " 2. Create a provisioning profile: mysigner doctor (will auto-create)", :white
2167
- say " 3. Or run: mysigner signing configure", :white
2274
+ say ''
2275
+ say 'Next steps:', :cyan
2276
+ say ' 1. Sync to update local cache: mysigner sync ios', :white
2277
+ say ' 2. Create a provisioning profile: mysigner doctor (will auto-create)', :white
2278
+ say ' 3. Or run: mysigner signing configure', :white
2168
2279
  rescue Mysigner::ValidationError => e
2169
- error "Validation failed:"
2280
+ error 'Validation failed:'
2170
2281
  if e.details
2171
2282
  e.details.each do |field, errors|
2172
2283
  say " #{field}: #{errors.join(', ')}", :red
@@ -2177,25 +2288,25 @@ module Mysigner
2177
2288
  say " Suggestion: #{e.suggestion}", :yellow if e.suggestion
2178
2289
  exit 1
2179
2290
  rescue Mysigner::ClientError => e
2180
- if e.message.include?("already exists") || e.message.include?("ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE")
2291
+ if e.message.include?('already exists') || e.message.include?('ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE')
2181
2292
  say "ℹ️ Bundle ID already registered: #{identifier}", :yellow
2182
- say ""
2183
- say "This Bundle ID already exists in App Store Connect.", :white
2293
+ say ''
2294
+ say 'This Bundle ID already exists in App Store Connect.', :white
2184
2295
  say "Run 'mysigner sync ios' to update your local cache.", :cyan
2185
2296
  else
2186
2297
  error "Failed to register Bundle ID: #{e.message}"
2187
- say ""
2188
- say "Common issues:", :yellow
2189
- say " • Bundle ID already exists (check App Store Connect)", :cyan
2190
- say " • Invalid format (must be like com.company.app)", :cyan
2191
- say " • API credentials may not have permission", :cyan
2298
+ say ''
2299
+ say 'Common issues:', :yellow
2300
+ say ' • Bundle ID already exists (check App Store Connect)', :cyan
2301
+ say ' • Invalid format (must be like com.company.app)', :cyan
2302
+ say ' • API credentials may not have permission', :cyan
2192
2303
  end
2193
2304
  exit 1
2194
2305
  end
2195
2306
 
2196
2307
  when 'list'
2197
- say "📦 Registered Bundle IDs", :cyan
2198
- say ""
2308
+ say '📦 Registered Bundle IDs', :cyan
2309
+ say ''
2199
2310
 
2200
2311
  begin
2201
2312
  response = client.get(
@@ -2204,16 +2315,16 @@ module Mysigner
2204
2315
  bundle_ids = response[:data]['bundle_ids'] || response[:data] || []
2205
2316
 
2206
2317
  if bundle_ids.empty?
2207
- say " No Bundle IDs found", :yellow
2208
- say ""
2209
- say " Register one with: mysigner bundleid register com.company.app", :cyan
2318
+ say ' No Bundle IDs found', :yellow
2319
+ say ''
2320
+ say ' Register one with: mysigner bundleid register com.company.app', :cyan
2210
2321
  else
2211
2322
  bundle_ids.each do |bid|
2212
2323
  identifier = bid['identifier'] || bid['bundle_id']
2213
2324
  name = bid['name'] || 'N/A'
2214
2325
  say " • #{name}", :green
2215
2326
  say " Identifier: #{identifier}"
2216
- say ""
2327
+ say ''
2217
2328
  end
2218
2329
  end
2219
2330
  rescue Mysigner::ClientError => e
@@ -2221,19 +2332,74 @@ module Mysigner
2221
2332
  exit 1
2222
2333
  end
2223
2334
 
2335
+ when 'delete'
2336
+ if args.empty? || args[0].nil? || args[0].to_s.empty?
2337
+ error 'Usage: mysigner bundleid delete IDENTIFIER'
2338
+ say ''
2339
+ say 'Example: mysigner bundleid delete com.company.myapp', :yellow
2340
+ exit 1
2341
+ return
2342
+ end
2343
+
2344
+ identifier = args[0]
2345
+
2346
+ say '🗑 Removing Bundle ID...', :cyan
2347
+ say " Identifier: #{identifier}", :white
2348
+ say ''
2349
+
2350
+ begin
2351
+ # Resolve the identifier → numeric id (the backend DELETE is keyed on id).
2352
+ list_response = client.get(
2353
+ "/api/v1/organizations/#{config.current_organization_id}/bundle_ids"
2354
+ )
2355
+ bundle_ids = list_response[:data]['bundle_ids'] || list_response[:data] || []
2356
+ bid = bundle_ids.find { |b| (b['identifier'] || b['bundle_id']) == identifier }
2357
+
2358
+ if bid.nil?
2359
+ error "Bundle ID not found: #{identifier}"
2360
+ say ''
2361
+ say ' → List existing: mysigner bundleid list', :yellow
2362
+ exit 1
2363
+ return
2364
+ end
2365
+
2366
+ client.delete(
2367
+ "/api/v1/organizations/#{config.current_organization_id}/bundle_ids/#{bid['id']}"
2368
+ )
2369
+
2370
+ say '✓ Bundle ID deleted from App Store Connect and local cache', :green
2371
+ say ''
2372
+ say 'Run `mysigner sync ios` to refresh your local cache if needed.', :cyan
2373
+ rescue Mysigner::ValidationError => e
2374
+ # Backend maps 409 Conflict → ValidationError. Apple refused
2375
+ # because of dependent resources (apps/profiles/capabilities).
2376
+ error e.message
2377
+ say ''
2378
+ say '💡 To delete this Bundle ID:', :cyan
2379
+ say ' 1. Remove any provisioning profiles that use it', :yellow
2380
+ say ' 2. Remove any apps tied to it in App Store Connect', :yellow
2381
+ say ' 3. Disable capabilities attached to it', :yellow
2382
+ say ' 4. Re-run `mysigner bundleid delete`', :yellow
2383
+ exit 1
2384
+ rescue Mysigner::ClientError => e
2385
+ error "Failed to delete Bundle ID: #{e.message}"
2386
+ exit 1
2387
+ end
2388
+
2224
2389
  else
2225
2390
  error "Unknown action: #{action}"
2226
- say ""
2227
- say "Available actions:", :yellow
2228
- say " mysigner bundleid register IDENTIFIER [NAME]", :cyan
2229
- say " mysigner bundleid list", :cyan
2391
+ say ''
2392
+ say 'Available actions:', :yellow
2393
+ say ' mysigner bundleid register IDENTIFIER [NAME]', :cyan
2394
+ say ' mysigner bundleid list', :cyan
2395
+ say ' mysigner bundleid delete IDENTIFIER', :cyan
2230
2396
  exit 1
2231
2397
  end
2232
2398
  end
2233
2399
 
2234
2400
  # ==================== APPS (iOS + Android) ====================
2235
2401
 
2236
- desc "apps", "List apps from App Store Connect and/or Google Play"
2402
+ desc 'apps', 'List apps from App Store Connect and/or Google Play'
2237
2403
  long_desc <<~DESC
2238
2404
  List apps synced from app stores.
2239
2405
 
@@ -2268,8 +2434,8 @@ module Mysigner
2268
2434
 
2269
2435
  # iOS Apps
2270
2436
  if show_ios
2271
- say "📱 iOS Apps", :cyan
2272
- say ""
2437
+ say '📱 iOS Apps', :cyan
2438
+ say ''
2273
2439
 
2274
2440
  begin
2275
2441
  response = client.get(
@@ -2279,78 +2445,78 @@ module Mysigner
2279
2445
  ios_apps = response[:data]['data']&.fetch('apps', nil) || []
2280
2446
 
2281
2447
  if ios_apps.empty?
2282
- say " No iOS apps found", :yellow
2283
- say ""
2448
+ say ' No iOS apps found', :yellow
2449
+ say ''
2284
2450
  say " Why don't my iOS apps appear?", :cyan
2285
- say " ─────────────────────────────", :cyan
2286
- say ""
2287
- say " Common reasons:", :yellow
2288
- say " • No apps registered in App Store Connect yet"
2289
- say " • Team ID not set on your credential"
2290
- say " • Bundle IDs exist but apps not created in App Store Connect"
2291
- say ""
2292
- say " How to register an iOS app:", :cyan
2293
- say ""
2294
- say " 1. Register a Bundle ID"
2295
- say " https://developer.apple.com/account/resources/identifiers/list"
2451
+ say ' ─────────────────────────────', :cyan
2452
+ say ''
2453
+ say ' Common reasons:', :yellow
2454
+ say ' • No apps registered in App Store Connect yet'
2455
+ say ' • Team ID not set on your credential'
2456
+ say ' • Bundle IDs exist but apps not created in App Store Connect'
2457
+ say ''
2458
+ say ' How to register an iOS app:', :cyan
2459
+ say ''
2460
+ say ' 1. Register a Bundle ID'
2461
+ say ' https://developer.apple.com/account/resources/identifiers/list'
2296
2462
  say " Click '+' → App IDs → Enter your Bundle ID (e.g., com.company.appname)"
2297
- say ""
2298
- say " 2. Create the app in App Store Connect"
2299
- say " https://appstoreconnect.apple.com/apps"
2463
+ say ''
2464
+ say ' 2. Create the app in App Store Connect'
2465
+ say ' https://appstoreconnect.apple.com/apps'
2300
2466
  say " My Apps → '+' → New App → Select your Bundle ID"
2301
- say ""
2302
- say " 3. Sync your organization"
2303
- say " Run: ", :white
2304
- say "mysigner sync ios", :green
2305
- say ""
2306
- say " 💡 Team ID tip:", :yellow
2467
+ say ''
2468
+ say ' 3. Sync your organization'
2469
+ say ' Run: ', :white
2470
+ say 'mysigner sync ios', :green
2471
+ say ''
2472
+ say ' 💡 Team ID tip:', :yellow
2307
2473
  say " Apple's API doesn't expose Team ID. Set it manually in the web dashboard."
2308
- say " Find yours at: https://developer.apple.com/account/#!/membership/"
2309
- say ""
2474
+ say ' Find yours at: https://developer.apple.com/account/#!/membership/'
2475
+ say ''
2310
2476
  else
2311
2477
  ios_apps.each do |app|
2312
2478
  say " • #{app['name'] || app['bundle_id']}", :green
2313
2479
  say " Bundle ID: #{app['bundle_id']}"
2314
- say ""
2480
+ say ''
2315
2481
  end
2316
2482
  end
2317
2483
  rescue Mysigner::ClientError => e
2318
2484
  say " Could not fetch iOS apps: #{e.message}", :yellow
2319
2485
  end
2320
- say ""
2486
+ say ''
2321
2487
  end
2322
2488
 
2323
2489
  # Android Apps
2324
- if show_android
2325
- say "🤖 Android Apps", :cyan
2326
- say ""
2490
+ return unless show_android
2327
2491
 
2328
- begin
2329
- response = client.get(
2330
- "/api/v1/organizations/#{config.current_organization_id}/android_apps",
2331
- params: params
2332
- )
2333
- android_apps = response[:data]['android_apps'] || []
2492
+ say '🤖 Android Apps', :cyan
2493
+ say ''
2334
2494
 
2335
- if android_apps.empty?
2336
- say " No Android apps found", :yellow
2337
- say " Sync with: mysigner sync android", :yellow
2338
- else
2339
- android_apps.each do |app|
2340
- say " • #{app['name'] || app['package_name']}", :green
2341
- say " Package: #{app['package_name']}"
2342
- say ""
2343
- end
2495
+ begin
2496
+ response = client.get(
2497
+ "/api/v1/organizations/#{config.current_organization_id}/android_apps",
2498
+ params: params
2499
+ )
2500
+ android_apps = response[:data]['android_apps'] || []
2501
+
2502
+ if android_apps.empty?
2503
+ say ' No Android apps found', :yellow
2504
+ say ' Sync with: mysigner sync android', :yellow
2505
+ else
2506
+ android_apps.each do |app|
2507
+ say " • #{app['name'] || app['package_name']}", :green
2508
+ say " Package: #{app['package_name']}"
2509
+ say ''
2344
2510
  end
2345
- rescue Mysigner::ClientError => e
2346
- say " Could not fetch Android apps: #{e.message}", :yellow
2347
2511
  end
2512
+ rescue Mysigner::ClientError => e
2513
+ say " Could not fetch Android apps: #{e.message}", :yellow
2348
2514
  end
2349
2515
  end
2350
2516
 
2351
2517
  # ==================== MERCHANT IDS (Apple Pay) ====================
2352
2518
 
2353
- desc "merchant-ids", "List Apple Pay Merchant IDs"
2519
+ desc 'merchant-ids', 'List Apple Pay Merchant IDs'
2354
2520
  method_option :search, type: :string, aliases: '-q', desc: 'Search by identifier or name'
2355
2521
  method_option :page, type: :numeric, default: 1, desc: 'Page number'
2356
2522
  method_option :per_page, type: :numeric, default: 50, desc: 'Items per page'
@@ -2358,8 +2524,8 @@ module Mysigner
2358
2524
  config = load_config
2359
2525
  client = create_client(config)
2360
2526
 
2361
- say "💳 Merchant IDs", :cyan
2362
- say ""
2527
+ say '💳 Merchant IDs', :cyan
2528
+ say ''
2363
2529
 
2364
2530
  params = {
2365
2531
  page: options[:page],
@@ -2376,19 +2542,20 @@ module Mysigner
2376
2542
  pagination = response[:data]['pagination']
2377
2543
 
2378
2544
  if merchant_ids.empty?
2379
- say " No Merchant IDs found", :yellow
2380
- say ""
2381
- say " Create one with: mysigner merchant-id create IDENTIFIER", :cyan
2545
+ say ' No Merchant IDs found', :yellow
2546
+ say ''
2547
+ say ' Create one with: mysigner merchant-id create IDENTIFIER', :cyan
2382
2548
  else
2383
2549
  merchant_ids.each do |m|
2384
2550
  say " • #{m['identifier']}", :green
2385
2551
  say " Name: #{m['name']}" if m['name'] && m['name'] != m['identifier']
2386
2552
  say " Team: #{m['team_id']}" if m['team_id']
2387
- say ""
2553
+ say ''
2388
2554
  end
2389
2555
 
2390
2556
  if pagination
2391
- say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)", :yellow
2557
+ say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)",
2558
+ :yellow
2392
2559
  end
2393
2560
  end
2394
2561
  rescue Mysigner::ClientError => e
@@ -2397,7 +2564,7 @@ module Mysigner
2397
2564
  end
2398
2565
  end
2399
2566
 
2400
- desc "merchant-id SUBCOMMAND", "Manage Apple Pay Merchant IDs"
2567
+ desc 'merchant-id SUBCOMMAND', 'Manage Apple Pay Merchant IDs'
2401
2568
  long_desc <<~DESC
2402
2569
  Create and delete Apple Pay Merchant IDs.
2403
2570
 
@@ -2422,22 +2589,24 @@ module Mysigner
2422
2589
 
2423
2590
  case action
2424
2591
  when 'create'
2425
- if identifier.nil?
2426
- error "Usage: mysigner merchant-id create IDENTIFIER [--name NAME]"
2427
- say ""
2428
- say "Example: mysigner merchant-id create merchant.com.company.app", :yellow
2592
+ if identifier.nil? || identifier.to_s.empty?
2593
+ error 'Usage: mysigner merchant-id create IDENTIFIER [--name NAME]'
2594
+ say ''
2595
+ say 'Example: mysigner merchant-id create merchant.com.company.app', :yellow
2429
2596
  exit 1
2597
+ return
2430
2598
  end
2431
2599
 
2432
2600
  unless identifier.start_with?('merchant.')
2433
2601
  error "Merchant ID must start with 'merchant.'"
2434
- say ""
2435
- say "Example: merchant.com.company.app", :cyan
2602
+ say ''
2603
+ say 'Example: merchant.com.company.app', :cyan
2436
2604
  exit 1
2605
+ return
2437
2606
  end
2438
2607
 
2439
- say "💳 Creating Merchant ID...", :cyan
2440
- say ""
2608
+ say '💳 Creating Merchant ID...', :cyan
2609
+ say ''
2441
2610
 
2442
2611
  begin
2443
2612
  response = client.post(
@@ -2449,12 +2618,12 @@ module Mysigner
2449
2618
  )
2450
2619
 
2451
2620
  m = response[:data]['merchant_id'] || response[:data]
2452
- say "✓ Merchant ID created successfully!", :green
2453
- say ""
2621
+ say '✓ Merchant ID created successfully!', :green
2622
+ say ''
2454
2623
  say " Identifier: #{m['identifier'] || identifier}", :white
2455
2624
  say " Name: #{m['name']}", :white if m['name']
2456
2625
  rescue Mysigner::ClientError => e
2457
- if e.message.include?("already exists")
2626
+ if e.message.include?('already exists')
2458
2627
  say "ℹ️ Merchant ID already exists: #{identifier}", :yellow
2459
2628
  else
2460
2629
  error "Failed to create Merchant ID: #{e.message}"
@@ -2463,13 +2632,14 @@ module Mysigner
2463
2632
  end
2464
2633
 
2465
2634
  when 'delete'
2466
- if identifier.nil?
2467
- error "Usage: mysigner merchant-id delete IDENTIFIER"
2635
+ if identifier.nil? || identifier.to_s.empty?
2636
+ error 'Usage: mysigner merchant-id delete IDENTIFIER'
2468
2637
  exit 1
2638
+ return
2469
2639
  end
2470
2640
 
2471
- say "💳 Deleting Merchant ID...", :cyan
2472
- say ""
2641
+ say '💳 Deleting Merchant ID...', :cyan
2642
+ say ''
2473
2643
 
2474
2644
  begin
2475
2645
  # First find the merchant ID by identifier
@@ -2483,6 +2653,7 @@ module Mysigner
2483
2653
  if m.nil?
2484
2654
  error "Merchant ID not found: #{identifier}"
2485
2655
  exit 1
2656
+ return
2486
2657
  end
2487
2658
 
2488
2659
  client.delete(
@@ -2497,43 +2668,43 @@ module Mysigner
2497
2668
 
2498
2669
  else
2499
2670
  error "Unknown action: #{action}"
2500
- say ""
2501
- say "Available actions:", :yellow
2502
- say " mysigner merchant-id create IDENTIFIER [--name NAME]", :cyan
2503
- say " mysigner merchant-id delete IDENTIFIER", :cyan
2671
+ say ''
2672
+ say 'Available actions:', :yellow
2673
+ say ' mysigner merchant-id create IDENTIFIER [--name NAME]', :cyan
2674
+ say ' mysigner merchant-id delete IDENTIFIER', :cyan
2504
2675
  exit 1
2505
2676
  end
2506
2677
  end
2507
2678
 
2508
2679
  # ==================== ANDROID TRACKS ====================
2509
2680
 
2510
- desc "tracks PACKAGE_NAME", "List Google Play tracks for an Android app"
2681
+ desc 'tracks PACKAGE_NAME', 'List Google Play tracks for an Android app'
2511
2682
  method_option :sort, type: :boolean, desc: 'Sort by track name'
2512
2683
  def tracks(package_name = nil)
2513
2684
  config = load_config
2514
2685
  client = create_client(config)
2515
2686
 
2516
2687
  if package_name.nil?
2517
- error "Usage: mysigner tracks PACKAGE_NAME"
2518
- say ""
2519
- say "Example: mysigner tracks com.example.myapp", :yellow
2520
- say ""
2521
- say "💡 To see your registered Android apps:", :cyan
2522
- say " mysigner apps --platform android", :cyan
2688
+ error 'Usage: mysigner tracks PACKAGE_NAME'
2689
+ say ''
2690
+ say 'Example: mysigner tracks com.example.myapp', :yellow
2691
+ say ''
2692
+ say '💡 To see your registered Android apps:', :cyan
2693
+ say ' mysigner apps --platform android', :cyan
2523
2694
  exit 1
2524
2695
  end
2525
2696
 
2526
2697
  say "🎯 Google Play Tracks for #{package_name}", :cyan
2527
- say ""
2698
+ say ''
2528
2699
 
2529
2700
  begin
2530
2701
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps/package/#{package_name}/tracks")
2531
2702
  tracks = response[:data]['tracks'] || []
2532
2703
 
2533
2704
  if tracks.empty?
2534
- say "No tracks found", :yellow
2535
- say ""
2536
- say "Tracks appear after you upload your app to Google Play Console", :cyan
2705
+ say 'No tracks found', :yellow
2706
+ say ''
2707
+ say 'Tracks appear after you upload your app to Google Play Console', :cyan
2537
2708
  say "and sync with: mysigner sync android --package #{package_name}", :cyan
2538
2709
  return
2539
2710
  end
@@ -2559,17 +2730,17 @@ module Mysigner
2559
2730
  updated = Time.parse(track['updated_at']).strftime('%Y-%m-%d %H:%M')
2560
2731
  say " Updated: #{updated}"
2561
2732
  end
2562
- say ""
2733
+ say ''
2563
2734
  end
2564
2735
 
2565
2736
  say "Total: #{tracks.count} track(s)", :yellow
2566
2737
  rescue Mysigner::NotFoundError => e
2567
- if e.message.include?("Android app")
2738
+ if e.message.include?('Android app')
2568
2739
  error "Android app not found: #{package_name}"
2569
- say ""
2570
- say "💡 App not found:", :cyan
2571
- say " → Check the package name is correct", :yellow
2572
- say " → List your apps: mysigner apps --platform android", :yellow
2740
+ say ''
2741
+ say '💡 App not found:', :cyan
2742
+ say ' → Check the package name is correct', :yellow
2743
+ say ' → List your apps: mysigner apps --platform android', :yellow
2573
2744
  say " → Register the app: mysigner android add #{package_name}", :yellow
2574
2745
  else
2575
2746
  error "Not found: #{e.message}"
@@ -2581,33 +2752,33 @@ module Mysigner
2581
2752
  end
2582
2753
  end
2583
2754
 
2584
- desc "track PACKAGE_NAME TRACK_NAME", "Show details for a specific Google Play track"
2755
+ desc 'track PACKAGE_NAME TRACK_NAME', 'Show details for a specific Google Play track'
2585
2756
  def track(package_name = nil, track_name = nil)
2586
2757
  config = load_config
2587
2758
  client = create_client(config)
2588
2759
 
2589
2760
  if package_name.nil? || track_name.nil?
2590
- error "Usage: mysigner track PACKAGE_NAME TRACK_NAME"
2591
- say ""
2592
- say "Example: mysigner track com.example.myapp production", :yellow
2593
- say " mysigner track com.example.myapp beta", :yellow
2594
- say ""
2595
- say "Common track names: production, beta, alpha, internal", :cyan
2596
- say ""
2597
- say "💡 To see available tracks:", :cyan
2598
- say " mysigner tracks com.example.myapp", :cyan
2761
+ error 'Usage: mysigner track PACKAGE_NAME TRACK_NAME'
2762
+ say ''
2763
+ say 'Example: mysigner track com.example.myapp production', :yellow
2764
+ say ' mysigner track com.example.myapp beta', :yellow
2765
+ say ''
2766
+ say 'Common track names: production, beta, alpha, internal', :cyan
2767
+ say ''
2768
+ say '💡 To see available tracks:', :cyan
2769
+ say ' mysigner tracks com.example.myapp', :cyan
2599
2770
  exit 1
2600
2771
  end
2601
2772
 
2602
2773
  say "🎯 Track: #{track_name}", :cyan
2603
2774
  say " Package: #{package_name}", :white
2604
- say ""
2775
+ say ''
2605
2776
 
2606
2777
  begin
2607
2778
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps/package/#{package_name}/tracks/#{track_name}")
2608
2779
  track = response[:data]
2609
2780
 
2610
- say "Details:", :bold
2781
+ say 'Details:', :bold
2611
2782
  say " Track Name: #{track['track_name']}"
2612
2783
  say " Status: #{track['status'] || 'unknown'}"
2613
2784
 
@@ -2617,9 +2788,9 @@ module Mysigner
2617
2788
  end
2618
2789
 
2619
2790
  # Show releases info
2791
+ say ''
2620
2792
  if track['releases'].is_a?(Array) && track['releases'].any?
2621
- say ""
2622
- say "Releases:", :bold
2793
+ say 'Releases:', :bold
2623
2794
  track['releases'].each_with_index do |release, idx|
2624
2795
  say " Release #{idx + 1}:", :white
2625
2796
  say " Status: #{release['status']}" if release['status']
@@ -2627,14 +2798,12 @@ module Mysigner
2627
2798
  version_codes = release['versionCodes'] || release['version_codes'] || []
2628
2799
  say " Version Codes: #{version_codes.join(', ')}" if version_codes.any?
2629
2800
 
2630
- if release['name']
2631
- say " Name: #{release['name']}"
2632
- end
2801
+ say " Name: #{release['name']}" if release['name']
2633
2802
 
2634
2803
  if release['releaseNotes'] || release['release_notes']
2635
2804
  notes = release['releaseNotes'] || release['release_notes']
2636
2805
  if notes.is_a?(Array) && notes.any?
2637
- say " Release Notes:"
2806
+ say ' Release Notes:'
2638
2807
  notes.each do |note|
2639
2808
  lang = note['language'] || 'en-US'
2640
2809
  text = note['text'] || ''
@@ -2649,24 +2818,22 @@ module Mysigner
2649
2818
  end
2650
2819
  end
2651
2820
  else
2652
- say ""
2653
- say "No releases found in this track", :yellow
2821
+ say 'No releases found in this track', :yellow
2654
2822
  end
2655
-
2656
2823
  rescue Mysigner::NotFoundError => e
2657
- if e.message.include?("Android app")
2824
+ if e.message.include?('Android app')
2658
2825
  error "Android app not found: #{package_name}"
2659
- say ""
2660
- say "💡 App not found:", :cyan
2661
- say " → Check the package name is correct", :yellow
2662
- say " → List your apps: mysigner apps --platform android", :yellow
2663
- elsif e.message.include?("Track")
2826
+ say ''
2827
+ say '💡 App not found:', :cyan
2828
+ say ' → Check the package name is correct', :yellow
2829
+ say ' → List your apps: mysigner apps --platform android', :yellow
2830
+ elsif e.message.include?('Track')
2664
2831
  error "Track not found: #{track_name}"
2665
- say ""
2666
- say "💡 Track not found:", :cyan
2667
- say " → Check the track name is correct", :yellow
2832
+ say ''
2833
+ say '💡 Track not found:', :cyan
2834
+ say ' → Check the track name is correct', :yellow
2668
2835
  say " → List available tracks: mysigner tracks #{package_name}", :yellow
2669
- say " → Common tracks: production, beta, alpha, internal", :yellow
2836
+ say ' → Common tracks: production, beta, alpha, internal', :yellow
2670
2837
  else
2671
2838
  error "Not found: #{e.message}"
2672
2839
  end
@@ -2679,7 +2846,7 @@ module Mysigner
2679
2846
 
2680
2847
  # ==================== APP GROUPS ====================
2681
2848
 
2682
- desc "app-groups", "List App Groups"
2849
+ desc 'app-groups', 'List App Groups'
2683
2850
  method_option :search, type: :string, aliases: '-q', desc: 'Search by identifier or name'
2684
2851
  method_option :page, type: :numeric, default: 1, desc: 'Page number'
2685
2852
  method_option :per_page, type: :numeric, default: 50, desc: 'Items per page'
@@ -2687,8 +2854,8 @@ module Mysigner
2687
2854
  config = load_config
2688
2855
  client = create_client(config)
2689
2856
 
2690
- say "📦 App Groups", :cyan
2691
- say ""
2857
+ say '📦 App Groups', :cyan
2858
+ say ''
2692
2859
 
2693
2860
  params = {
2694
2861
  page: options[:page],
@@ -2705,21 +2872,22 @@ module Mysigner
2705
2872
  pagination = response[:data]['pagination']
2706
2873
 
2707
2874
  if app_groups.empty?
2708
- say " No App Groups found", :yellow
2709
- say ""
2710
- say " Register one with: mysigner app-group register IDENTIFIER", :cyan
2711
- say ""
2712
- say " Note: App Groups must first be created in Apple Developer Portal", :yellow
2875
+ say ' No App Groups found', :yellow
2876
+ say ''
2877
+ say ' Register one with: mysigner app-group register IDENTIFIER', :cyan
2878
+ say ''
2879
+ say ' Note: App Groups must first be created in Apple Developer Portal', :yellow
2713
2880
  else
2714
2881
  app_groups.each do |g|
2715
2882
  say " • #{g['identifier']}", :green
2716
2883
  say " Name: #{g['name']}" if g['name'] && g['name'] != g['identifier']
2717
2884
  say " Team: #{g['team_id']}" if g['team_id']
2718
- say ""
2885
+ say ''
2719
2886
  end
2720
2887
 
2721
2888
  if pagination
2722
- say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)", :yellow
2889
+ say "Page #{pagination['page']} of #{pagination['total_pages']} (#{pagination['total']} total)",
2890
+ :yellow
2723
2891
  end
2724
2892
  end
2725
2893
  rescue Mysigner::ClientError => e
@@ -2728,7 +2896,7 @@ module Mysigner
2728
2896
  end
2729
2897
  end
2730
2898
 
2731
- desc "app-group SUBCOMMAND", "Manage App Groups"
2899
+ desc 'app-group SUBCOMMAND', 'Manage App Groups'
2732
2900
  long_desc <<~DESC
2733
2901
  Register and delete App Groups.
2734
2902
 
@@ -2757,24 +2925,26 @@ module Mysigner
2757
2925
 
2758
2926
  case action
2759
2927
  when 'register'
2760
- if identifier.nil?
2761
- error "Usage: mysigner app-group register IDENTIFIER [--name NAME]"
2762
- say ""
2763
- say "Example: mysigner app-group register group.com.company.shared", :yellow
2764
- say ""
2765
- say "Note: Create the App Group in Apple Developer Portal first!", :cyan
2928
+ if identifier.nil? || identifier.to_s.empty?
2929
+ error 'Usage: mysigner app-group register IDENTIFIER [--name NAME]'
2930
+ say ''
2931
+ say 'Example: mysigner app-group register group.com.company.shared', :yellow
2932
+ say ''
2933
+ say 'Note: Create the App Group in Apple Developer Portal first!', :cyan
2766
2934
  exit 1
2935
+ return
2767
2936
  end
2768
2937
 
2769
2938
  unless identifier.start_with?('group.')
2770
2939
  error "App Group identifier must start with 'group.'"
2771
- say ""
2772
- say "Example: group.com.company.shared", :cyan
2940
+ say ''
2941
+ say 'Example: group.com.company.shared', :cyan
2773
2942
  exit 1
2943
+ return
2774
2944
  end
2775
2945
 
2776
- say "📦 Registering App Group...", :cyan
2777
- say ""
2946
+ say '📦 Registering App Group...', :cyan
2947
+ say ''
2778
2948
 
2779
2949
  begin
2780
2950
  response = client.post(
@@ -2786,15 +2956,15 @@ module Mysigner
2786
2956
  )
2787
2957
 
2788
2958
  g = response[:data]['app_group'] || response[:data]
2789
- say "✓ App Group registered!", :green
2790
- say ""
2959
+ say '✓ App Group registered!', :green
2960
+ say ''
2791
2961
  say " Identifier: #{g['identifier'] || identifier}", :white
2792
2962
  say " Name: #{g['name']}", :white if g['name']
2793
- say ""
2794
- say " Remember: This only registers the App Group in My Signer.", :yellow
2795
- say " Make sure it exists in Apple Developer Portal.", :yellow
2963
+ say ''
2964
+ say ' Remember: This only registers the App Group in My Signer.', :yellow
2965
+ say ' Make sure it exists in Apple Developer Portal.', :yellow
2796
2966
  rescue Mysigner::ClientError => e
2797
- if e.message.include?("already exists")
2967
+ if e.message.include?('already exists')
2798
2968
  say "ℹ️ App Group already registered: #{identifier}", :yellow
2799
2969
  else
2800
2970
  error "Failed to register App Group: #{e.message}"
@@ -2803,13 +2973,14 @@ module Mysigner
2803
2973
  end
2804
2974
 
2805
2975
  when 'delete'
2806
- if identifier.nil?
2807
- error "Usage: mysigner app-group delete IDENTIFIER"
2976
+ if identifier.nil? || identifier.to_s.empty?
2977
+ error 'Usage: mysigner app-group delete IDENTIFIER'
2808
2978
  exit 1
2979
+ return
2809
2980
  end
2810
2981
 
2811
- say "📦 Removing App Group...", :cyan
2812
- say ""
2982
+ say '📦 Removing App Group...', :cyan
2983
+ say ''
2813
2984
 
2814
2985
  begin
2815
2986
  # First find the app group by identifier
@@ -2823,6 +2994,7 @@ module Mysigner
2823
2994
  if g.nil?
2824
2995
  error "App Group not found: #{identifier}"
2825
2996
  exit 1
2997
+ return
2826
2998
  end
2827
2999
 
2828
3000
  client.delete(
@@ -2830,9 +3002,9 @@ module Mysigner
2830
3002
  )
2831
3003
 
2832
3004
  say "✓ App Group removed from My Signer: #{identifier}", :green
2833
- say ""
2834
- say " Note: The App Group still exists in Apple Developer Portal.", :yellow
2835
- say " Delete it manually if needed.", :yellow
3005
+ say ''
3006
+ say ' Note: The App Group still exists in Apple Developer Portal.', :yellow
3007
+ say ' Delete it manually if needed.', :yellow
2836
3008
  rescue Mysigner::ClientError => e
2837
3009
  error "Failed to remove App Group: #{e.message}"
2838
3010
  exit 1
@@ -2840,17 +3012,17 @@ module Mysigner
2840
3012
 
2841
3013
  else
2842
3014
  error "Unknown action: #{action}"
2843
- say ""
2844
- say "Available actions:", :yellow
2845
- say " mysigner app-group register IDENTIFIER [--name NAME]", :cyan
2846
- say " mysigner app-group delete IDENTIFIER", :cyan
3015
+ say ''
3016
+ say 'Available actions:', :yellow
3017
+ say ' mysigner app-group register IDENTIFIER [--name NAME]', :cyan
3018
+ say ' mysigner app-group delete IDENTIFIER', :cyan
2847
3019
  exit 1
2848
3020
  end
2849
3021
  end
2850
3022
 
2851
3023
  # ==================== GOOGLE PLAY CREDENTIALS ====================
2852
3024
 
2853
- desc "gp-credential SUBCOMMAND", "Manage Google Play credentials (list, delete, activate, test)"
3025
+ desc 'gp-credential SUBCOMMAND', 'Manage Google Play credentials (list, delete, activate, test)'
2854
3026
  long_desc <<~DESC
2855
3027
  Manage Google Play API credentials for Android app distribution.
2856
3028
 
@@ -2888,17 +3060,17 @@ module Mysigner
2888
3060
 
2889
3061
  case action
2890
3062
  when 'list'
2891
- say "🔑 Google Play Credentials", :cyan
2892
- say ""
3063
+ say '🔑 Google Play Credentials', :cyan
3064
+ say ''
2893
3065
 
2894
3066
  begin
2895
3067
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/google_play_credentials")
2896
3068
  credentials = response[:data]['google_play_credentials'] || []
2897
3069
 
2898
3070
  if credentials.empty?
2899
- say "No Google Play credentials found", :yellow
2900
- say ""
2901
- say "Set up credentials with: mysigner onboard", :yellow
3071
+ say 'No Google Play credentials found', :yellow
3072
+ say ''
3073
+ say 'Set up credentials with: mysigner onboard', :yellow
2902
3074
  return
2903
3075
  end
2904
3076
 
@@ -2917,7 +3089,7 @@ module Mysigner
2917
3089
  sync_color = cred['last_sync_status'] == 'success' ? :green : :red
2918
3090
  say " Sync Status: #{cred['last_sync_status']}", sync_color
2919
3091
  end
2920
- say ""
3092
+ say ''
2921
3093
  end
2922
3094
 
2923
3095
  say "Total: #{credentials.count} credential(s)", :yellow
@@ -2930,23 +3102,23 @@ module Mysigner
2930
3102
  credential_id = args[0]
2931
3103
 
2932
3104
  unless credential_id
2933
- error "Usage: mysigner gp-credential delete ID"
2934
- say ""
3105
+ error 'Usage: mysigner gp-credential delete ID'
3106
+ say ''
2935
3107
  say "Run 'mysigner gp-credential list' to see available IDs", :yellow
2936
3108
  exit 1
2937
3109
  end
2938
3110
 
2939
3111
  say "⚠️ You are about to delete Google Play credential ID: #{credential_id}", :yellow
2940
- say ""
3112
+ say ''
2941
3113
 
2942
- if yes?("Are you sure? This cannot be undone. (y/n)")
3114
+ if yes?('Are you sure? This cannot be undone. (y/n)')
2943
3115
  begin
2944
3116
  client.delete("/api/v1/organizations/#{config.current_organization_id}/google_play_credentials/#{credential_id}")
2945
- say ""
2946
- say "✓ Google Play credential deleted", :green
3117
+ say ''
3118
+ say '✓ Google Play credential deleted', :green
2947
3119
  rescue Mysigner::NotFoundError
2948
3120
  error "Credential not found with ID: #{credential_id}"
2949
- say ""
3121
+ say ''
2950
3122
  say "Run 'mysigner gp-credential list' to see available IDs", :yellow
2951
3123
  exit 1
2952
3124
  rescue Mysigner::ClientError => e
@@ -2954,30 +3126,30 @@ module Mysigner
2954
3126
  exit 1
2955
3127
  end
2956
3128
  else
2957
- say "Deletion cancelled", :yellow
3129
+ say 'Deletion cancelled', :yellow
2958
3130
  end
2959
3131
 
2960
3132
  when 'activate'
2961
3133
  credential_id = args[0]
2962
3134
 
2963
3135
  unless credential_id
2964
- error "Usage: mysigner gp-credential activate ID"
2965
- say ""
3136
+ error 'Usage: mysigner gp-credential activate ID'
3137
+ say ''
2966
3138
  say "Run 'mysigner gp-credential list' to see available IDs", :yellow
2967
3139
  exit 1
2968
3140
  end
2969
3141
 
2970
- say "🔑 Activating credential...", :cyan
3142
+ say '🔑 Activating credential...', :cyan
2971
3143
 
2972
3144
  begin
2973
3145
  response = client.post("/api/v1/organizations/#{config.current_organization_id}/google_play_credentials/#{credential_id}/activate")
2974
3146
  credential = response[:data]['google_play_credential'] || response[:data]
2975
- say "✓ Credential activated!", :green
2976
- say ""
3147
+ say '✓ Credential activated!', :green
3148
+ say ''
2977
3149
  say "#{credential['name']} is now the active Google Play credential", :cyan
2978
3150
  rescue Mysigner::NotFoundError
2979
3151
  error "Credential not found with ID: #{credential_id}"
2980
- say ""
3152
+ say ''
2981
3153
  say "Run 'mysigner gp-credential list' to see available IDs", :yellow
2982
3154
  exit 1
2983
3155
  rescue Mysigner::ClientError => e
@@ -2989,37 +3161,37 @@ module Mysigner
2989
3161
  credential_id = args[0]
2990
3162
 
2991
3163
  unless credential_id
2992
- error "Usage: mysigner gp-credential test ID"
2993
- say ""
3164
+ error 'Usage: mysigner gp-credential test ID'
3165
+ say ''
2994
3166
  say "Run 'mysigner gp-credential list' to see available IDs", :yellow
2995
3167
  exit 1
2996
3168
  end
2997
3169
 
2998
- say "🔑 Testing credential connection...", :cyan
2999
- say ""
3170
+ say '🔑 Testing credential connection...', :cyan
3171
+ say ''
3000
3172
 
3001
3173
  begin
3002
3174
  response = client.post("/api/v1/organizations/#{config.current_organization_id}/google_play_credentials/#{credential_id}/test")
3003
3175
  result = response[:data]
3004
3176
 
3005
3177
  if result['success']
3006
- say "✓ Connection successful!", :green
3007
- say ""
3008
- say " Google Play API is reachable with this credential", :cyan
3178
+ say '✓ Connection successful!', :green
3179
+ say ''
3180
+ say ' Google Play API is reachable with this credential', :cyan
3009
3181
  else
3010
- say "✗ Connection failed", :red
3011
- say ""
3182
+ say '✗ Connection failed', :red
3183
+ say ''
3012
3184
  say " Error: #{result['error']}", :red if result['error']
3013
- say ""
3014
- say "💡 Check that:", :cyan
3015
- say " → The service account JSON key is valid", :yellow
3016
- say " → The service account has Google Play Console access", :yellow
3017
- say " → API access is enabled in Google Play Console", :yellow
3185
+ say ''
3186
+ say '💡 Check that:', :cyan
3187
+ say ' → The service account JSON key is valid', :yellow
3188
+ say ' → The service account has Google Play Console access', :yellow
3189
+ say ' → API access is enabled in Google Play Console', :yellow
3018
3190
  exit 1
3019
3191
  end
3020
3192
  rescue Mysigner::NotFoundError
3021
3193
  error "Credential not found with ID: #{credential_id}"
3022
- say ""
3194
+ say ''
3023
3195
  say "Run 'mysigner gp-credential list' to see available IDs", :yellow
3024
3196
  exit 1
3025
3197
  rescue Mysigner::ClientError => e
@@ -3031,14 +3203,14 @@ module Mysigner
3031
3203
  invoke :help, ['gp-credential']
3032
3204
  else
3033
3205
  error "Unknown action: #{action}"
3034
- say "Available actions: list, delete, activate, test, help", :yellow
3206
+ say 'Available actions: list, delete, activate, test, help', :yellow
3035
3207
  exit 1
3036
3208
  end
3037
3209
  end
3038
3210
 
3039
3211
  # ==================== APP STORE RELEASES ====================
3040
3212
 
3041
- desc "release SUBCOMMAND", "Manage App Store release configurations (list, show, create, update)"
3213
+ desc 'release SUBCOMMAND', 'Manage App Store release configurations (list, show, create, update)'
3042
3214
  long_desc <<~DESC
3043
3215
  Manage App Store release configurations for iOS app distribution.
3044
3216
 
@@ -3107,8 +3279,8 @@ module Mysigner
3107
3279
 
3108
3280
  case action
3109
3281
  when 'list'
3110
- say "🚀 App Store Releases", :cyan
3111
- say ""
3282
+ say '🚀 App Store Releases', :cyan
3283
+ say ''
3112
3284
 
3113
3285
  params = {}
3114
3286
  params[:bundle_id] = options[:bundle_id] if options[:bundle_id]
@@ -3121,9 +3293,9 @@ module Mysigner
3121
3293
  releases = response[:data]['app_store_releases'] || []
3122
3294
 
3123
3295
  if releases.empty?
3124
- say "No release configurations found", :yellow
3125
- say ""
3126
- say "Create one with: mysigner release create --bundle-id-id ID", :yellow
3296
+ say 'No release configurations found', :yellow
3297
+ say ''
3298
+ say 'Create one with: mysigner release create --bundle-id-id ID', :yellow
3127
3299
  return
3128
3300
  end
3129
3301
 
@@ -3134,7 +3306,7 @@ module Mysigner
3134
3306
  say " Auto Submit: #{rel['auto_submit'] ? 'Yes' : 'No'}"
3135
3307
  say " Phased Release: #{rel['phased_release'] ? 'Yes' : 'No'}"
3136
3308
  say " Version: #{rel['version_string']}" if rel['version_string']
3137
- say ""
3309
+ say ''
3138
3310
  end
3139
3311
 
3140
3312
  say "Total: #{releases.count} release(s)", :yellow
@@ -3147,8 +3319,8 @@ module Mysigner
3147
3319
  release_id = args[0]
3148
3320
 
3149
3321
  unless release_id
3150
- error "Usage: mysigner release show ID"
3151
- say ""
3322
+ error 'Usage: mysigner release show ID'
3323
+ say ''
3152
3324
  say "Run 'mysigner release list' to see available IDs", :yellow
3153
3325
  exit 1
3154
3326
  end
@@ -3158,31 +3330,31 @@ module Mysigner
3158
3330
  rel = response[:data]['app_store_release'] || response[:data]
3159
3331
 
3160
3332
  say "🚀 Release Configuration (ID: #{rel['id']})", :cyan
3161
- say ""
3162
- say "Details:", :bold
3333
+ say ''
3334
+ say 'Details:', :bold
3163
3335
  say " App Name: #{rel['app_name'] || 'N/A'}"
3164
3336
  say " Bundle ID: #{rel['bundle_identifier'] || 'N/A'}"
3165
3337
  say " Version: #{rel['version_string'] || 'N/A'}"
3166
3338
  say " Release Type: #{rel['release_type'] || 'N/A'}"
3167
3339
  say " Auto Submit: #{rel['auto_submit'] ? 'Yes' : 'No'}"
3168
3340
  say " Phased Release: #{rel['phased_release'] ? 'Yes' : 'No'}"
3169
- say ""
3341
+ say ''
3170
3342
  if rel['whats_new'] && !rel['whats_new'].empty?
3171
3343
  say "What's New:", :bold
3172
3344
  say " #{rel['whats_new']}"
3173
- say ""
3345
+ say ''
3174
3346
  end
3175
- say "URLs:", :bold
3347
+ say 'URLs:', :bold
3176
3348
  say " Support: #{rel['support_url'] || 'N/A'}"
3177
3349
  say " Marketing: #{rel['marketing_url'] || 'N/A'}"
3178
3350
  say " Privacy: #{rel['privacy_url'] || 'N/A'}"
3179
3351
  if rel['scheduled_date']
3180
- say ""
3352
+ say ''
3181
3353
  say "Scheduled Date: #{rel['scheduled_date']}"
3182
3354
  end
3183
3355
  rescue Mysigner::NotFoundError
3184
3356
  error "Release not found with ID: #{release_id}"
3185
- say ""
3357
+ say ''
3186
3358
  say "Run 'mysigner release list' to see available IDs", :yellow
3187
3359
  exit 1
3188
3360
  rescue Mysigner::ClientError => e
@@ -3191,8 +3363,8 @@ module Mysigner
3191
3363
  end
3192
3364
 
3193
3365
  when 'create'
3194
- say "🚀 Creating release configuration...", :cyan
3195
- say ""
3366
+ say '🚀 Creating release configuration...', :cyan
3367
+ say ''
3196
3368
 
3197
3369
  body = {}
3198
3370
  body[:bundle_id_id] = options[:bundle_id_id] if options[:bundle_id_id]
@@ -3212,9 +3384,9 @@ module Mysigner
3212
3384
  )
3213
3385
  rel = response[:data]['app_store_release'] || response[:data]
3214
3386
 
3215
- say "✓ Release configuration created!", :green
3216
- say ""
3217
- say "Details:", :bold
3387
+ say '✓ Release configuration created!', :green
3388
+ say ''
3389
+ say 'Details:', :bold
3218
3390
  say " ID: #{rel['id']}"
3219
3391
  say " Bundle ID: #{rel['bundle_identifier'] || 'N/A'}"
3220
3392
  say " Release Type: #{rel['release_type'] || 'N/A'}"
@@ -3222,24 +3394,22 @@ module Mysigner
3222
3394
  say " Phased Release: #{rel['phased_release'] ? 'Yes' : 'No'}"
3223
3395
  rescue Mysigner::ValidationError => e
3224
3396
  if e.message.include?('already exists') || (e.error_code && e.error_code.to_s == '409')
3225
- error "Release configuration already exists for this bundle ID"
3226
- say ""
3397
+ error 'Release configuration already exists for this bundle ID'
3398
+ say ''
3227
3399
  say "💡 Use 'mysigner release update ID' to modify it", :cyan
3228
3400
  say " Run 'mysigner release list' to find the ID", :yellow
3229
3401
  else
3230
3402
  error "Validation failed: #{e.message}"
3231
- if e.details
3232
- e.details.each do |field, errors|
3233
- errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
3234
- say " #{field}: #{errors_text}", :red
3235
- end
3403
+ e.details&.each do |field, errors|
3404
+ errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
3405
+ say " #{field}: #{errors_text}", :red
3236
3406
  end
3237
3407
  end
3238
3408
  exit 1
3239
3409
  rescue Mysigner::ClientError => e
3240
3410
  if e.message.include?('409') || e.message.include?('already exists')
3241
- error "Release configuration already exists for this bundle ID"
3242
- say ""
3411
+ error 'Release configuration already exists for this bundle ID'
3412
+ say ''
3243
3413
  say "💡 Use 'mysigner release update ID' to modify it", :cyan
3244
3414
  say " Run 'mysigner release list' to find the ID", :yellow
3245
3415
  else
@@ -3252,8 +3422,8 @@ module Mysigner
3252
3422
  release_id = args[0]
3253
3423
 
3254
3424
  unless release_id
3255
- error "Usage: mysigner release update ID [OPTIONS]"
3256
- say ""
3425
+ error 'Usage: mysigner release update ID [OPTIONS]'
3426
+ say ''
3257
3427
  say "Run 'mysigner release list' to see available IDs", :yellow
3258
3428
  exit 1
3259
3429
  end
@@ -3268,8 +3438,8 @@ module Mysigner
3268
3438
  body[:release_type] = options[:release_type] if options[:release_type]
3269
3439
  body[:scheduled_date] = options[:scheduled_date] if options[:scheduled_date]
3270
3440
 
3271
- say "🚀 Updating release configuration...", :cyan
3272
- say ""
3441
+ say '🚀 Updating release configuration...', :cyan
3442
+ say ''
3273
3443
 
3274
3444
  begin
3275
3445
  response = client.patch(
@@ -3278,9 +3448,9 @@ module Mysigner
3278
3448
  )
3279
3449
  rel = response[:data]['app_store_release'] || response[:data]
3280
3450
 
3281
- say "✓ Release configuration updated!", :green
3282
- say ""
3283
- say "Details:", :bold
3451
+ say '✓ Release configuration updated!', :green
3452
+ say ''
3453
+ say 'Details:', :bold
3284
3454
  say " ID: #{rel['id']}"
3285
3455
  say " Bundle ID: #{rel['bundle_identifier'] || 'N/A'}"
3286
3456
  say " Release Type: #{rel['release_type'] || 'N/A'}"
@@ -3288,16 +3458,14 @@ module Mysigner
3288
3458
  say " Phased Release: #{rel['phased_release'] ? 'Yes' : 'No'}"
3289
3459
  rescue Mysigner::NotFoundError
3290
3460
  error "Release not found with ID: #{release_id}"
3291
- say ""
3461
+ say ''
3292
3462
  say "Run 'mysigner release list' to see available IDs", :yellow
3293
3463
  exit 1
3294
3464
  rescue Mysigner::ValidationError => e
3295
3465
  error "Validation failed: #{e.message}"
3296
- if e.details
3297
- e.details.each do |field, errors|
3298
- errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
3299
- say " #{field}: #{errors_text}", :red
3300
- end
3466
+ e.details&.each do |field, errors|
3467
+ errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
3468
+ say " #{field}: #{errors_text}", :red
3301
3469
  end
3302
3470
  exit 1
3303
3471
  rescue Mysigner::ClientError => e
@@ -3309,7 +3477,7 @@ module Mysigner
3309
3477
  invoke :help, ['release']
3310
3478
  else
3311
3479
  error "Unknown action: #{action}"
3312
- say "Available actions: list, show, create, update, help", :yellow
3480
+ say 'Available actions: list, show, create, update, help', :yellow
3313
3481
  exit 1
3314
3482
  end
3315
3483
  end