mysigner 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.githooks/pre-commit +15 -0
  3. data/.githooks/pre-push +21 -0
  4. data/.github/workflows/ci.yml +29 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +55 -0
  7. data/.rubocop_todo.yml +112 -0
  8. data/CHANGELOG.md +96 -0
  9. data/Gemfile +5 -3
  10. data/Gemfile.lock +38 -8
  11. data/README.md +87 -17
  12. data/Rakefile +5 -3
  13. data/bin/console +4 -3
  14. data/bin/setup +3 -0
  15. data/exe/mysigner +2 -1
  16. data/lib/mysigner/build/android_executor.rb +46 -52
  17. data/lib/mysigner/build/android_parser.rb +33 -40
  18. data/lib/mysigner/build/configurator.rb +17 -16
  19. data/lib/mysigner/build/detector.rb +39 -50
  20. data/lib/mysigner/build/error_analyzer.rb +70 -68
  21. data/lib/mysigner/build/executor.rb +30 -37
  22. data/lib/mysigner/build/parser.rb +18 -18
  23. data/lib/mysigner/cli/auth_commands.rb +735 -752
  24. data/lib/mysigner/cli/build_commands.rb +697 -721
  25. data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
  26. data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
  27. data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
  28. data/lib/mysigner/cli/concerns/helpers.rb +12 -1
  29. data/lib/mysigner/cli/diagnostic_commands.rb +659 -635
  30. data/lib/mysigner/cli/resource_commands.rb +1266 -822
  31. data/lib/mysigner/cli/validate_commands.rb +161 -0
  32. data/lib/mysigner/cli.rb +5 -1
  33. data/lib/mysigner/client.rb +27 -19
  34. data/lib/mysigner/config.rb +93 -56
  35. data/lib/mysigner/export/exporter.rb +32 -36
  36. data/lib/mysigner/signing/certificate_checker.rb +18 -23
  37. data/lib/mysigner/signing/keystore_manager.rb +34 -39
  38. data/lib/mysigner/signing/validator.rb +38 -40
  39. data/lib/mysigner/signing/wizard.rb +329 -342
  40. data/lib/mysigner/upload/app_store_automation.rb +51 -49
  41. data/lib/mysigner/upload/app_store_submission.rb +87 -92
  42. data/lib/mysigner/upload/play_store_uploader.rb +98 -115
  43. data/lib/mysigner/upload/uploader.rb +101 -109
  44. data/lib/mysigner/version.rb +3 -1
  45. data/lib/mysigner.rb +13 -11
  46. data/mysigner.gemspec +36 -33
  47. data/test_manual.rb +37 -36
  48. metadata +38 -16
@@ -1,18 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
1
4
  module Mysigner
2
5
  class CLI < Thor
3
6
  module DiagnosticCommands
4
7
  def self.included(base)
5
8
  base.class_eval do
6
- desc "doctor", "🩺 Run health check and diagnose setup issues (run this if stuck)"
9
+ desc 'doctor', '🩺 Run health check and diagnose setup issues (run this if stuck)'
7
10
  method_option :platform, type: :string, desc: 'Check specific platform only: ios, android, or all (default)'
8
11
  def doctor
9
- say "🩺 My Signer Health Check", :cyan
10
- say "=" * 80, :cyan
11
- say ""
12
-
12
+ say '🩺 My Signer Health Check', :cyan
13
+ say '=' * 80, :cyan
14
+ say ''
15
+
13
16
  issues = []
14
17
  warnings = []
15
-
18
+
16
19
  # Determine which platforms to check
17
20
  platform_filter = options[:platform]&.downcase
18
21
  check_ios = platform_filter.nil? || platform_filter == 'all' || platform_filter == 'ios'
@@ -20,594 +23,614 @@ module Mysigner
20
23
 
21
24
  if platform_filter && !%w[ios android all].include?(platform_filter)
22
25
  error "Invalid platform: #{platform_filter}"
23
- say "Valid options: ios, android, all", :yellow
26
+ say 'Valid options: ios, android, all', :yellow
24
27
  exit 1
25
28
  end
26
29
 
27
30
  # Check 1: Xcode (iOS only)
28
31
  if check_ios
29
- say "Checking Xcode...", :yellow
30
- if system('which xcodebuild > /dev/null 2>&1')
31
- xcode_version = `xcodebuild -version`.lines.first.strip rescue "Unknown"
32
- say " ✓ Xcode installed: #{xcode_version}", :green
33
- else
34
- say " ✗ Xcode not found", :red
35
- issues << "Xcode is not installed or not in PATH"
36
- end
37
- say ""
38
-
39
- # Check 2: Command Line Tools
40
- say "Checking Command Line Tools...", :yellow
41
- if system('xcode-select -p > /dev/null 2>&1')
42
- say " ✓ Command Line Tools installed", :green
43
- else
44
- say " ✗ Command Line Tools not found", :red
45
- issues << "Install with: xcode-select --install"
46
- end
47
- say ""
48
-
49
- # Check 3: xcrun altool
50
- say "Checking upload tools...", :yellow
51
- if system('xcrun --find altool > /dev/null 2>&1')
52
- say " ✓ xcrun altool available", :green
53
- else
54
- say " ⚠️ xcrun altool not found", :yellow
55
- warnings << "altool not available (upload may fail)"
56
- end
57
-
58
- # Check for iTMSTransporter
59
- transporter_paths = [
60
- '/Applications/Xcode.app/Contents/Developer/usr/bin/iTMSTransporter',
61
- '/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter'
62
- ]
63
- transporter_found = transporter_paths.any? { |path| File.exist?(path) }
64
-
65
- if transporter_found
66
- say " ✓ iTMSTransporter available (fallback)", :green
67
- else
68
- say " ⚠️ iTMSTransporter not found (optional)", :yellow
69
- end
70
- say ""
71
-
72
- # Check 4: My Signer Configuration
73
- say "Checking My Signer configuration...", :yellow
74
- config = Config.new
75
- client = nil
76
- org_data = nil
77
-
78
- if config.exists?
79
- config.load
80
- say " ✓ Logged in", :green
81
-
82
- begin
83
- client = Client.new(api_url: config.api_url, api_token: config.api_token, user_email: config.user_email)
84
- client.test_connection
85
- say " ✓ API connection working", :green
86
-
87
- # Get organization details
88
- if config.current_organization_id
89
- org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
90
- org_data = org_response[:data]
32
+ say 'Checking Xcode...', :yellow
33
+ if system('which xcodebuild > /dev/null 2>&1')
34
+ xcode_version = begin
35
+ `xcodebuild -version`.lines.first.strip
36
+ rescue StandardError
37
+ 'Unknown'
91
38
  end
92
- rescue Mysigner::UnauthorizedError
93
- say " ✗ Token is invalid or expired", :red
94
- issues << "Token authentication failed - run 'mysigner onboard' to re-authenticate"
95
- client = nil
96
- rescue Mysigner::ConnectionError => e
97
- say " ✗ Cannot connect to API: #{e.message}", :red
98
- issues << "API connection failed - check your network or API URL"
99
- client = nil
100
- rescue => e
101
- say " ✗ API error: #{e.message}", :red
102
- issues << "API connection failed - check your configuration"
103
- client = nil
39
+ say " ✓ Xcode installed: #{xcode_version}", :green
40
+ else
41
+ say ' ✗ Xcode not found', :red
42
+ issues << 'Xcode is not installed or not in PATH'
104
43
  end
105
- else
106
- say " ✗ Not logged in", :red
107
- issues << "Run 'mysigner onboard' to authenticate"
108
- end
109
- say ""
110
-
111
- # Check 4a: Signing Identity in Keychain (CRITICAL)
112
- if client && org_data && org_data['app_store_connect_team_id']
113
- say "Checking signing identity for team...", :yellow
114
- team_id = org_data['app_store_connect_team_id']
115
-
116
- # Check if signing identities exist in keychain for this team
117
- identities = `security find-identity -v -p codesigning 2>/dev/null | grep -i "#{team_id}"`
118
- has_identity = $?.success? && !identities.strip.empty?
119
-
120
- if has_identity
121
- say " ✓ Signing identity found for team #{team_id}", :green
44
+ say ''
45
+
46
+ # Check 2: Command Line Tools
47
+ say 'Checking Command Line Tools...', :yellow
48
+ if system('xcode-select -p > /dev/null 2>&1')
49
+ say ' ✓ Command Line Tools installed', :green
122
50
  else
123
- say "No signing identity for team #{team_id}", :red
124
- say ""
125
- say " CRITICAL: You need to sign into Xcode and download certificates:", :red
126
- say " 1. Open Xcode → Settings → Accounts", :yellow
127
- say " 2. Verify you're signed in with your Apple ID", :yellow
128
- say " 3. Select team '#{team_id}'", :yellow
129
- say " 4. Click 'Download Manual Profiles' (or 'Manage Certificates')", :yellow
130
- say " 5. The certificate should appear in keychain", :yellow
131
- say ""
132
- issues << "No signing identity for team #{team_id} in keychain"
51
+ say 'Command Line Tools not found', :red
52
+ issues << 'Install with: xcode-select --install'
133
53
  end
134
- say ""
135
- end
136
-
137
- # Check 4b: App Store Connect Credentials (with auto-fix)
138
- if client && org_data
139
- say "Checking App Store Connect credentials...", :yellow
140
- creds_status = org_data['credentials_status'] || {}
141
-
142
- if creds_status['needs_setup'] || !org_data['app_store_connect_configured']
143
- say " ✗ App Store Connect not configured", :red
144
- say ""
145
-
146
- if yes_with_default?("Would you like to set it up now?", :green)
147
- say ""
148
- # Call the setup helper (available from AuthCommands module)
149
- if respond_to?(:setup_app_store_connect_credentials, true)
150
- asc_configured = setup_app_store_connect_credentials(client, config, config.current_organization_id)
151
- else
152
- say " ✗ Setup helper not available", :red
153
- say " Please run 'mysigner onboard' instead", :yellow
154
- asc_configured = false
155
- end
156
-
157
- if asc_configured
158
- say ""
159
- say " ✓ App Store Connect configured successfully!", :green
160
- say ""
161
- # Refresh org data
54
+ say ''
55
+
56
+ # Check 3: xcrun altool
57
+ say 'Checking upload tools...', :yellow
58
+ if system('xcrun --find altool > /dev/null 2>&1')
59
+ say ' ✓ xcrun altool available', :green
60
+ else
61
+ say ' ⚠️ xcrun altool not found', :yellow
62
+ warnings << 'altool not available (upload may fail)'
63
+ end
64
+
65
+ # Check for iTMSTransporter
66
+ transporter_paths = [
67
+ '/Applications/Xcode.app/Contents/Developer/usr/bin/iTMSTransporter',
68
+ '/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter'
69
+ ]
70
+ transporter_found = transporter_paths.any? { |path| File.exist?(path) }
71
+
72
+ if transporter_found
73
+ say ' iTMSTransporter available (fallback)', :green
74
+ else
75
+ say ' ⚠️ iTMSTransporter not found (optional)', :yellow
76
+ end
77
+ say ''
78
+
79
+ # Check 4: My Signer Configuration
80
+ say 'Checking My Signer configuration...', :yellow
81
+ client = nil
82
+ org_data = nil
83
+
84
+ config = if Config.env_configured?
85
+ Config.from_env
86
+ else
87
+ Config.new
88
+ end
89
+
90
+ if config.from_env? || config.exists?
91
+ config.load unless config.from_env?
92
+ say config.from_env? ? ' ✓ Configured via environment variables' : ' ✓ Logged in', :green
93
+
94
+ begin
95
+ client = Client.new(api_url: config.api_url, api_token: config.api_token,
96
+ user_email: config.user_email)
97
+ client.test_connection
98
+ say ' ✓ API connection working', :green
99
+
100
+ # Get organization details
101
+ if config.current_organization_id
162
102
  org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
163
103
  org_data = org_response[:data]
164
- else
165
- say ""
166
- issues << "App Store Connect setup incomplete - run 'mysigner onboard' to try again"
167
104
  end
168
- else
169
- issues << "App Store Connect not configured - run 'mysigner onboard' to set it up"
105
+ rescue Mysigner::UnauthorizedError
106
+ say ' ✗ Token is invalid or expired', :red
107
+ issues << "Token authentication failed - run 'mysigner onboard' to re-authenticate"
108
+ client = nil
109
+ rescue Mysigner::ConnectionError => e
110
+ say " ✗ Cannot connect to API: #{e.message}", :red
111
+ issues << 'API connection failed - check your network or API URL'
112
+ client = nil
113
+ rescue StandardError => e
114
+ say " ✗ API error: #{e.message}", :red
115
+ issues << 'API connection failed - check your configuration'
116
+ client = nil
170
117
  end
171
- elsif !creds_status['team_id_set']
172
- say " ⚠️ Team ID not set", :yellow
173
- warnings << "Team ID missing - may cause issues. Re-sync to extract it."
174
118
  else
175
- say " App Store Connect configured", :green
176
- if org_data['app_store_connect_team_id']
177
- say " Team ID: #{org_data['app_store_connect_team_id']}", :cyan
178
- end
119
+ say ' Not logged in', :red
120
+ issues << "Run 'mysigner onboard' to authenticate"
179
121
  end
180
- say ""
181
- end
182
-
183
- # Check 5: Xcode License Agreement
184
- say "Checking Xcode license...", :yellow
185
- begin
186
- license_check = `sudo -n xcodebuild -checkFirstLaunchStatus 2>&1`
187
- license_status = $?.success?
188
-
189
- if license_status
190
- say " ✓ Xcode license accepted", :green
191
- else
192
- # Check if it's a permission issue or actual license issue
193
- if license_check.include?("password") || license_check.include?("sudo")
194
- say " ℹ️ Cannot check license (needs sudo)", :cyan
122
+ say ''
123
+
124
+ # Check 4a: Signing Identity in Keychain (CRITICAL)
125
+ if client && org_data && org_data['app_store_connect_team_id']
126
+ say 'Checking signing identity for team...', :yellow
127
+ team_id = org_data['app_store_connect_team_id']
128
+
129
+ # Check if signing identities exist in keychain for this team
130
+ identities = `security find-identity -v -p codesigning 2>/dev/null | grep -i "#{team_id}"`
131
+ has_identity = $CHILD_STATUS.success? && !identities.strip.empty?
132
+
133
+ if has_identity
134
+ say " ✓ Signing identity found for team #{team_id}", :green
195
135
  else
196
- say " ⚠️ Xcode license may not be accepted", :yellow
197
- say " Run: sudo xcodebuild -license accept", :cyan
198
- warnings << "Xcode license may need acceptance"
136
+ say " No signing identity for team #{team_id}", :red
137
+ say ''
138
+ say ' CRITICAL: You need to sign into Xcode and download certificates:', :red
139
+ say ' 1. Open Xcode → Settings → Accounts', :yellow
140
+ say " 2. Verify you're signed in with your Apple ID", :yellow
141
+ say " 3. Select team '#{team_id}'", :yellow
142
+ say " 4. Click 'Download Manual Profiles' (or 'Manage Certificates')", :yellow
143
+ say ' 5. The certificate should appear in keychain', :yellow
144
+ say ''
145
+ issues << "No signing identity for team #{team_id} in keychain"
199
146
  end
147
+ say ''
200
148
  end
201
- rescue
202
- say " ℹ️ Could not check Xcode license", :cyan
203
- end
204
- say ""
205
-
206
- # Check 6: Disk Space
207
- say "Checking disk space...", :yellow
208
- begin
209
- df_output = `df -h . 2>/dev/null | tail -1`.strip
210
- if df_output =~ /(\d+)%/
211
- usage = $1.to_i
212
- if usage > 95
213
- say " ✗ Critical: Disk space very low (#{usage}% used)", :red
214
- issues << "Free up disk space before building"
215
- elsif usage > 90
216
- say " ⚠️ Low disk space: #{usage}% used", :yellow
217
- warnings << "Low disk space may cause build failures"
149
+
150
+ # Check 4b: App Store Connect Credentials (with auto-fix)
151
+ if client && org_data
152
+ say 'Checking App Store Connect credentials...', :yellow
153
+ creds_status = org_data['credentials_status'] || {}
154
+
155
+ if creds_status['needs_setup'] || !org_data['app_store_connect_configured']
156
+ say ' ✗ App Store Connect not configured', :red
157
+ say ''
158
+
159
+ if yes_with_default?('Would you like to set it up now?', :green)
160
+ say ''
161
+ # Call the setup helper (available from AuthCommands module)
162
+ if respond_to?(:setup_app_store_connect_credentials, true)
163
+ asc_configured = setup_app_store_connect_credentials(client, config,
164
+ config.current_organization_id)
165
+ else
166
+ say ' ✗ Setup helper not available', :red
167
+ say " Please run 'mysigner onboard' instead", :yellow
168
+ asc_configured = false
169
+ end
170
+
171
+ say ''
172
+ if asc_configured
173
+ say ' ✓ App Store Connect configured successfully!', :green
174
+ say ''
175
+ # Refresh org data
176
+ org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
177
+ org_data = org_response[:data]
178
+ else
179
+ issues << "App Store Connect setup incomplete - run 'mysigner onboard' to try again"
180
+ end
181
+ else
182
+ issues << "App Store Connect not configured - run 'mysigner onboard' to set it up"
183
+ end
184
+ elsif !creds_status['team_id_set']
185
+ say ' ⚠️ Team ID not set', :yellow
186
+ warnings << 'Team ID missing - may cause issues. Re-sync to extract it.'
218
187
  else
219
- say "Sufficient disk space: #{usage}% used", :green
188
+ say 'App Store Connect configured', :green
189
+ say " Team ID: #{org_data['app_store_connect_team_id']}", :cyan if org_data['app_store_connect_team_id']
220
190
  end
221
- else
222
- say " ⚠️ Could not check disk space", :yellow
191
+ say ''
223
192
  end
224
- rescue
225
- say " ⚠️ Could not check disk space", :yellow
226
- end
227
- say ""
228
-
229
- # Check 7: Network Connectivity
230
- say "Checking network connectivity...", :yellow
231
- begin
232
- require 'socket'
233
- Socket.tcp("apple.com", 443, connect_timeout: 5) { |sock| sock.close }
234
- say " ✓ Internet connection working", :green
235
- rescue => e
236
- say " ✗ No internet connection", :red
237
- issues << "Cannot reach Apple servers - check your network connection"
238
- end
239
- say ""
240
-
241
- # Check 8: Project Detection (if in a project directory)
242
- say "Checking current directory...", :yellow
243
- project_info = nil
244
- begin
245
- project_info = Build::Detector.detect
246
- framework = case project_info[:framework]
247
- when :capacitor then "Capacitor/Ionic"
248
- when :react_native then "React Native"
249
- when :flutter then "Flutter"
250
- else "Native iOS"
193
+
194
+ # Check 5: Xcode License Agreement
195
+ say 'Checking Xcode license...', :yellow
196
+ begin
197
+ license_check = `sudo -n xcodebuild -checkFirstLaunchStatus 2>&1`
198
+ license_status = $CHILD_STATUS.success?
199
+
200
+ if license_status
201
+ say ' ✓ Xcode license accepted', :green
202
+ elsif license_check.include?('password') || license_check.include?('sudo')
203
+ # Check if it's a permission issue or actual license issue
204
+ say ' ℹ️ Cannot check license (needs sudo)', :cyan
205
+ else
206
+ say ' ⚠️ Xcode license may not be accepted', :yellow
207
+ say ' Run: sudo xcodebuild -license accept', :cyan
208
+ warnings << 'Xcode license may need acceptance'
209
+ end
210
+ rescue StandardError
211
+ say ' ℹ️ Could not check Xcode license', :cyan
251
212
  end
252
- say " ✓ Found #{framework} project: #{File.basename(project_info[:path])}", :green
253
- rescue
254
- say " ℹ️ No project detected in current directory", :cyan
255
- end
256
- say ""
257
-
258
- # Check 9: Organization Resources Health (if logged in)
259
- if client && org_data && org_data['app_store_connect_configured']
260
- say "Checking organization resources...", :yellow
261
-
262
- stats = org_data['stats'] || {}
263
-
264
- # Check certificates
265
- certs_count = stats['certificates_count'] || 0
266
- if certs_count == 0
267
- say " ⚠️ No certificates synced", :yellow
268
- warnings << "Run sync in web dashboard to fetch certificates from Apple"
269
- else
270
- say " Certificates: #{certs_count}", :green
213
+ say ''
214
+
215
+ # Check 6: Disk Space
216
+ say 'Checking disk space...', :yellow
217
+ begin
218
+ df_output = `df -h . 2>/dev/null | tail -1`.strip
219
+ if df_output =~ /(\d+)%/
220
+ usage = ::Regexp.last_match(1).to_i
221
+ if usage > 95
222
+ say " ✗ Critical: Disk space very low (#{usage}% used)", :red
223
+ issues << 'Free up disk space before building'
224
+ elsif usage > 90
225
+ say " ⚠️ Low disk space: #{usage}% used", :yellow
226
+ warnings << 'Low disk space may cause build failures'
227
+ else
228
+ say " Sufficient disk space: #{usage}% used", :green
229
+ end
230
+ else
231
+ say ' ⚠️ Could not check disk space', :yellow
232
+ end
233
+ rescue StandardError
234
+ say ' ⚠️ Could not check disk space', :yellow
271
235
  end
272
-
273
- # Check devices
274
- devices_count = stats['devices_count'] || 0
275
- if devices_count == 0
276
- say " ⚠️ No devices registered", :yellow
277
- warnings << "Add devices for development/adhoc builds"
278
- else
279
- say "Devices: #{devices_count}", :green
236
+ say ''
237
+
238
+ # Check 7: Network Connectivity
239
+ say 'Checking network connectivity...', :yellow
240
+ begin
241
+ require 'socket'
242
+ Socket.tcp('apple.com', 443, connect_timeout: 5, &:close)
243
+ say 'Internet connection working', :green
244
+ rescue StandardError => e
245
+ say ' ✗ No internet connection', :red
246
+ issues << 'Cannot reach Apple servers - check your network connection'
280
247
  end
281
-
282
- # Check bundle IDs
283
- bundle_ids_count = stats['bundle_ids_count'] || 0
284
- if bundle_ids_count == 0
285
- say " ⚠️ No bundle IDs synced", :yellow
286
- say " This is normal for new accounts", :cyan
287
- else
288
- say " ✓ Bundle IDs: #{bundle_ids_count}", :green
248
+ say ''
249
+
250
+ # Check 8: Project Detection (if in a project directory)
251
+ say 'Checking current directory...', :yellow
252
+ project_info = nil
253
+ begin
254
+ project_info = Build::Detector.detect
255
+ framework = case project_info[:framework]
256
+ when :capacitor then 'Capacitor/Ionic'
257
+ when :react_native then 'React Native'
258
+ when :flutter then 'Flutter'
259
+ else 'Native iOS'
260
+ end
261
+ say " ✓ Found #{framework} project: #{File.basename(project_info[:path])}", :green
262
+ rescue StandardError
263
+ say ' ℹ️ No project detected in current directory', :cyan
289
264
  end
290
-
291
- # Check profiles
292
- profiles_count = stats['profiles_count'] || 0
293
- invalid_profiles = stats['invalid_profiles_count'] || 0
294
-
295
- if profiles_count == 0
296
- say " ⚠️ No provisioning profiles", :yellow
297
- warnings << "Create profiles for your projects"
298
- elsif invalid_profiles > 0
299
- say " ✓ Profiles: #{profiles_count} (⚠️ #{invalid_profiles} invalid)", :yellow
300
- warnings << "#{invalid_profiles} profile(s) invalid - may need regeneration"
301
- else
302
- say " ✓ Profiles: #{profiles_count}", :green
265
+ say ''
266
+
267
+ # Check 9: Organization Resources Health (if logged in)
268
+ if client && org_data && org_data['app_store_connect_configured']
269
+ say 'Checking organization resources...', :yellow
270
+
271
+ stats = org_data['stats'] || {}
272
+
273
+ # Check certificates
274
+ certs_count = stats['certificates_count'] || 0
275
+ if certs_count.zero?
276
+ say ' ⚠️ No certificates synced', :yellow
277
+ warnings << 'Run sync in web dashboard to fetch certificates from Apple'
278
+ else
279
+ say " ✓ Certificates: #{certs_count}", :green
280
+ end
281
+
282
+ # Check devices
283
+ devices_count = stats['devices_count'] || 0
284
+ if devices_count.zero?
285
+ say ' ⚠️ No devices registered', :yellow
286
+ warnings << 'Add devices for development/adhoc builds'
287
+ else
288
+ say " ✓ Devices: #{devices_count}", :green
289
+ end
290
+
291
+ # Check bundle IDs
292
+ bundle_ids_count = stats['bundle_ids_count'] || 0
293
+ if bundle_ids_count.zero?
294
+ say ' ⚠️ No bundle IDs synced', :yellow
295
+ say ' This is normal for new accounts', :cyan
296
+ else
297
+ say " ✓ Bundle IDs: #{bundle_ids_count}", :green
298
+ end
299
+
300
+ # Check profiles
301
+ profiles_count = stats['profiles_count'] || 0
302
+ invalid_profiles = stats['invalid_profiles_count'] || 0
303
+
304
+ if profiles_count.zero?
305
+ say ' ⚠️ No provisioning profiles', :yellow
306
+ warnings << 'Create profiles for your projects'
307
+ elsif invalid_profiles.positive?
308
+ say " ✓ Profiles: #{profiles_count} (⚠️ #{invalid_profiles} invalid)", :yellow
309
+ warnings << "#{invalid_profiles} profile(s) invalid - may need regeneration"
310
+ else
311
+ say " ✓ Profiles: #{profiles_count}", :green
312
+ end
313
+
314
+ say ''
303
315
  end
304
-
305
- say ""
306
- end
307
-
308
- # Check 10: Project Signing Setup (if project detected and logged in)
309
- if project_info && client && org_data && org_data['app_store_connect_configured']
310
- say "Checking project signing setup...", :yellow
311
-
312
- begin
313
- parser = Build::Parser.new(project_info)
314
- main_target = parser.main_target
315
-
316
- if main_target
317
- target_name = main_target.name
318
- bundle_id = parser.bundle_id(target_name, 'Release')
319
-
320
- say " Project: #{target_name}", :cyan
321
- say " Bundle ID: #{bundle_id}", :cyan
322
- say ""
323
-
324
- # First check if bundle ID is registered
325
- say " Checking bundle ID registration...", :yellow
326
-
327
- bundle_ids_response = client.get(
328
- "/api/v1/organizations/#{config.current_organization_id}/bundle_ids",
329
- params: { q: bundle_id }
330
- )
331
-
332
- bundle_id_exists = (bundle_ids_response[:data]['bundle_ids'] || []).any? do |bid|
333
- bid['identifier'] == bundle_id
334
- end
335
-
336
- if !bundle_id_exists
337
- say " ✗ Bundle ID '#{bundle_id}' not registered in App Store Connect", :red
338
- say ""
339
-
340
- # Show what bundle IDs ARE registered
341
- all_bundle_ids = bundle_ids_response[:data]['bundle_ids'] || []
342
- if all_bundle_ids.any?
343
- say " Registered bundle IDs in your organization:", :cyan
344
- all_bundle_ids.first(5).each do |bid|
345
- say " • #{bid['identifier']}", :cyan
346
- end
347
- if all_bundle_ids.length > 5
348
- say " ... and #{all_bundle_ids.length - 5} more", :cyan
349
- end
350
- say ""
351
- end
352
-
353
- say " Options:", :yellow
354
- say " A. Register '#{bundle_id}' in App Store Connect:", :yellow
355
- say " 1. Go to: https://developer.apple.com/account/resources/identifiers/add", :cyan
356
- say " 2. Select 'App IDs'", :cyan
357
- say " 3. Register: #{bundle_id}", :cyan
358
- say " 4. Sync in web dashboard", :cyan
359
- say " 5. Run 'mysigner doctor' again", :cyan
360
- say ""
361
- say " B. Or change your Xcode project to use an existing bundle ID", :yellow
362
- say ""
363
- issues << "Bundle ID #{bundle_id} not registered in App Store Connect"
364
- else
365
- say " ✓ Bundle ID registered", :green
366
-
367
- # Check if profiles exist for this bundle ID
368
- say " Checking provisioning profiles...", :yellow
369
-
370
- profiles_response = client.get(
371
- "/api/v1/organizations/#{config.current_organization_id}/profiles",
372
- params: { bundle_id: bundle_id }
316
+
317
+ # Check 10: Project Signing Setup (if project detected and logged in)
318
+ if project_info && client && org_data && org_data['app_store_connect_configured']
319
+ say 'Checking project signing setup...', :yellow
320
+
321
+ begin
322
+ parser = Build::Parser.new(project_info)
323
+ main_target = parser.main_target
324
+
325
+ if main_target
326
+ target_name = main_target.name
327
+ bundle_id = parser.bundle_id(target_name, 'Release')
328
+
329
+ say " Project: #{target_name}", :cyan
330
+ say " Bundle ID: #{bundle_id}", :cyan
331
+ say ''
332
+
333
+ # First check if bundle ID is registered
334
+ say ' Checking bundle ID registration...', :yellow
335
+
336
+ bundle_ids_response = client.get(
337
+ "/api/v1/organizations/#{config.current_organization_id}/bundle_ids",
338
+ params: { q: bundle_id }
373
339
  )
374
-
375
- profiles = profiles_response[:data]['profiles'] || []
376
-
377
- # Check for App Store profile
378
- appstore_profiles = profiles.select { |p| p['profile_type'] == 'IOS_APP_STORE' && p['state'] == 'ACTIVE' }
379
-
380
- if appstore_profiles.empty?
381
- say " ✗ No App Store provisioning profile for #{bundle_id}", :red
382
- say ""
383
-
384
- if yes_with_default?("Create App Store profile automatically?", :green)
385
- say ""
386
- auto_create_profile(client, config, bundle_id, 'appstore')
387
- else
388
- issues << "Missing App Store profile for #{bundle_id} - run 'mysigner signing configure'"
340
+
341
+ bundle_id_exists = (bundle_ids_response[:data]['bundle_ids'] || []).any? do |bid|
342
+ bid['identifier'] == bundle_id
343
+ end
344
+
345
+ if bundle_id_exists
346
+ say ' ✓ Bundle ID registered', :green
347
+
348
+ # Check if profiles exist for this bundle ID
349
+ say ' Checking provisioning profiles...', :yellow
350
+
351
+ profiles_response = client.get(
352
+ "/api/v1/organizations/#{config.current_organization_id}/profiles",
353
+ params: { bundle_id: bundle_id }
354
+ )
355
+
356
+ profiles = profiles_response[:data]['profiles'] || []
357
+
358
+ # Check for App Store profile
359
+ appstore_profiles = profiles.select do |p|
360
+ p['profile_type'] == 'IOS_APP_STORE' && p['state'] == 'ACTIVE'
389
361
  end
390
- else
391
- say " ✓ App Store provisioning profile exists", :green
392
-
393
- # Check if expired
394
- expired = appstore_profiles.select do |p|
395
- expires_at = Time.parse(p['expires_at']) rescue nil
396
- expires_at && expires_at < Time.now
362
+
363
+ if appstore_profiles.empty?
364
+ say " ✗ No App Store provisioning profile for #{bundle_id}", :red
365
+ say ''
366
+
367
+ if yes_with_default?('Create App Store profile automatically?', :green)
368
+ say ''
369
+ auto_create_profile(client, config, bundle_id, 'appstore')
370
+ else
371
+ issues << "Missing App Store profile for #{bundle_id} - run 'mysigner signing configure'"
372
+ end
373
+ else
374
+ say ' ✓ App Store provisioning profile exists', :green
375
+
376
+ # Check if expired
377
+ expired = appstore_profiles.select do |p|
378
+ expires_at = begin
379
+ Time.parse(p['expires_at'])
380
+ rescue StandardError
381
+ nil
382
+ end
383
+ expires_at && expires_at < Time.now
384
+ end
385
+
386
+ if expired.any?
387
+ say " ⚠️ #{expired.length} profile(s) expired", :yellow
388
+ warnings << 'Some profiles are expired - sync to refresh'
389
+ end
397
390
  end
398
-
399
- if expired.any?
400
- say " ⚠️ #{expired.length} profile(s) expired", :yellow
401
- warnings << "Some profiles are expired - sync to refresh"
391
+
392
+ # Check for development profile (helpful for testing)
393
+ dev_profiles = profiles.select do |p|
394
+ p['profile_type'] == 'IOS_APP_DEVELOPMENT' && p['state'] == 'ACTIVE'
402
395
  end
403
- end
404
-
405
- # Check for development profile (helpful for testing)
406
- dev_profiles = profiles.select { |p| p['profile_type'] == 'IOS_APP_DEVELOPMENT' && p['state'] == 'ACTIVE' }
407
-
408
- if dev_profiles.empty?
409
- say " ⚠️ No Development profile (optional but recommended)", :yellow
410
- say ""
411
- say " 📱 Development profiles let you:", :cyan
412
- say " • Test your app on physical devices (iPhone/iPad)", :cyan
413
- say " • Debug before uploading to TestFlight", :cyan
414
- say " • Share with your team for testing", :cyan
415
- say ""
416
-
417
- if yes_with_default?("Create Development profile for local testing?", :yellow)
418
- say ""
419
- auto_create_profile(client, config, bundle_id, 'development')
396
+
397
+ if dev_profiles.empty?
398
+ say ' ⚠️ No Development profile (optional but recommended)', :yellow
399
+ say ''
400
+ say ' 📱 Development profiles let you:', :cyan
401
+ say ' • Test your app on physical devices (iPhone/iPad)', :cyan
402
+ say ' • Debug before uploading to TestFlight', :cyan
403
+ say ' • Share with your team for testing', :cyan
404
+ say ''
405
+
406
+ if yes_with_default?('Create Development profile for local testing?', :yellow)
407
+ say ''
408
+ auto_create_profile(client, config, bundle_id, 'development')
409
+ else
410
+ warnings << "No development profile - you won't be able to test on devices"
411
+ end
420
412
  else
421
- warnings << "No development profile - you won't be able to test on devices"
413
+ say ' ✓ Development profile exists', :green
422
414
  end
423
415
  else
424
- say " Development profile exists", :green
416
+ say " Bundle ID '#{bundle_id}' not registered in App Store Connect", :red
417
+ say ''
418
+
419
+ # Show what bundle IDs ARE registered
420
+ all_bundle_ids = bundle_ids_response[:data]['bundle_ids'] || []
421
+ if all_bundle_ids.any?
422
+ say ' Registered bundle IDs in your organization:', :cyan
423
+ all_bundle_ids.first(5).each do |bid|
424
+ say " • #{bid['identifier']}", :cyan
425
+ end
426
+ say " ... and #{all_bundle_ids.length - 5} more", :cyan if all_bundle_ids.length > 5
427
+ say ''
428
+ end
429
+
430
+ say ' Options:', :yellow
431
+ say " A. Register '#{bundle_id}' in App Store Connect:", :yellow
432
+ say ' 1. Go to: https://developer.apple.com/account/resources/identifiers/add', :cyan
433
+ say " 2. Select 'App IDs'", :cyan
434
+ say " 3. Register: #{bundle_id}", :cyan
435
+ say ' 4. Sync in web dashboard', :cyan
436
+ say " 5. Run 'mysigner doctor' again", :cyan
437
+ say ''
438
+ say ' B. Or change your Xcode project to use an existing bundle ID', :yellow
439
+ say ''
440
+ issues << "Bundle ID #{bundle_id} not registered in App Store Connect"
425
441
  end
426
442
  end
443
+ rescue StandardError => e
444
+ say " ⚠️ Could not check project signing: #{e.message}", :yellow
445
+ warnings << 'Project signing check failed'
427
446
  end
428
- rescue => e
429
- say " ⚠️ Could not check project signing: #{e.message}", :yellow
430
- warnings << "Project signing check failed"
447
+ say ''
448
+ elsif project_info && (!client || !org_data)
449
+ say '⚠️ Project detected but cannot check signing (not logged in)', :yellow
450
+ say ''
431
451
  end
432
- say ""
433
- elsif project_info && (!client || !org_data)
434
- say "⚠️ Project detected but cannot check signing (not logged in)", :yellow
435
- say ""
436
452
  end
437
- end # end of check_ios block
438
453
 
439
454
  # ==================== ANDROID CHECKS ====================
440
455
  if check_android
441
- say "Checking Android development environment...", :yellow
442
- android_available = false
443
-
444
- # Check 11: Java/JDK
445
- if system('which java > /dev/null 2>&1')
446
- java_version = `java -version 2>&1`.lines.first.strip rescue "Unknown"
447
- say " ✓ Java installed: #{java_version}", :green
448
- android_available = true
449
-
450
- # Check JAVA_HOME validity
451
- java_home = ENV['JAVA_HOME']
452
- if java_home && !java_home.empty?
453
- if Dir.exist?(java_home)
454
- say " ✓ JAVA_HOME: #{java_home}", :green
456
+ say 'Checking Android development environment...', :yellow
457
+ android_available = false
458
+
459
+ # Check 11: Java/JDK
460
+ if system('which java > /dev/null 2>&1')
461
+ java_version = begin
462
+ `java -version 2>&1`.lines.first.strip
463
+ rescue StandardError
464
+ 'Unknown'
465
+ end
466
+ say " ✓ Java installed: #{java_version}", :green
467
+ android_available = true
468
+
469
+ # Check JAVA_HOME validity
470
+ java_home = ENV.fetch('JAVA_HOME', nil)
471
+ if java_home && !java_home.empty?
472
+ if Dir.exist?(java_home)
473
+ say " ✓ JAVA_HOME: #{java_home}", :green
474
+ else
475
+ say " ✗ JAVA_HOME invalid: #{java_home}", :red
476
+
477
+ # Try to auto-detect correct JAVA_HOME
478
+ detected_java_home = detect_java_home
479
+ if detected_java_home
480
+ say " 💡 Detected valid Java at: #{detected_java_home}", :yellow
481
+ say ''
482
+ if yes_with_default?(' Would you like to fix JAVA_HOME in your shell config?', :green)
483
+ fix_java_home(detected_java_home)
484
+ else
485
+ say ' To fix manually, add to ~/.zshrc:', :yellow
486
+ say " export JAVA_HOME=#{detected_java_home}", :cyan
487
+ issues << 'JAVA_HOME points to non-existent directory'
488
+ end
489
+ else
490
+ issues << 'JAVA_HOME points to non-existent directory and no Java found'
491
+ end
492
+ end
455
493
  else
456
- say " ✗ JAVA_HOME invalid: #{java_home}", :red
457
-
458
- # Try to auto-detect correct JAVA_HOME
494
+ # JAVA_HOME not set - try to detect and suggest
459
495
  detected_java_home = detect_java_home
460
496
  if detected_java_home
461
- say " 💡 Detected valid Java at: #{detected_java_home}", :yellow
462
- say ""
463
- if yes_with_default?(" Would you like to fix JAVA_HOME in your shell config?", :green)
497
+ say ' ⚠️ JAVA_HOME not set', :yellow
498
+ say " 💡 Detected Java at: #{detected_java_home}", :yellow
499
+ say ''
500
+ if yes_with_default?(' Would you like to set JAVA_HOME in your shell config?', :green)
464
501
  fix_java_home(detected_java_home)
465
502
  else
466
- say " To fix manually, add to ~/.zshrc:", :yellow
467
- say " export JAVA_HOME=#{detected_java_home}", :cyan
468
- issues << "JAVA_HOME points to non-existent directory"
503
+ warnings << "JAVA_HOME not set (recommended: export JAVA_HOME=#{detected_java_home})"
469
504
  end
470
505
  else
471
- issues << "JAVA_HOME points to non-existent directory and no Java found"
506
+ say ' ⚠️ JAVA_HOME not set', :yellow
472
507
  end
473
508
  end
474
509
  else
475
- # JAVA_HOME not set - try to detect and suggest
476
- detected_java_home = detect_java_home
477
- if detected_java_home
478
- say " ⚠️ JAVA_HOME not set", :yellow
479
- say " 💡 Detected Java at: #{detected_java_home}", :yellow
480
- say ""
481
- if yes_with_default?(" Would you like to set JAVA_HOME in your shell config?", :green)
482
- fix_java_home(detected_java_home)
483
- else
484
- warnings << "JAVA_HOME not set (recommended: export JAVA_HOME=#{detected_java_home})"
485
- end
486
- else
487
- say " ⚠️ JAVA_HOME not set", :yellow
488
- end
510
+ say ' ℹ️ Java not found (required for Android)', :cyan
489
511
  end
490
- else
491
- say " ℹ️ Java not found (required for Android)", :cyan
492
- end
493
-
494
- # Check 12: Android SDK
495
- android_home = ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT']
496
- if android_home && Dir.exist?(android_home)
497
- say " ✓ Android SDK: #{android_home}", :green
498
- android_available = true
499
- else
500
- say " ℹ️ Android SDK not found (set ANDROID_HOME)", :cyan
501
- end
502
512
 
503
- # Check 13: Gradle
504
- if system('which gradle > /dev/null 2>&1') || (android_home && File.exist?("#{android_home}/../gradle"))
505
- gradle_version = `gradle --version 2>&1 | grep 'Gradle '`.strip rescue ""
506
- if gradle_version.empty?
507
- say " ✓ Gradle available (version check skipped)", :green
513
+ # Check 12: Android SDK
514
+ android_home = ENV['ANDROID_HOME'] || ENV.fetch('ANDROID_SDK_ROOT', nil)
515
+ if android_home && Dir.exist?(android_home)
516
+ say " ✓ Android SDK: #{android_home}", :green
517
+ android_available = true
508
518
  else
509
- say " #{gradle_version}", :green
519
+ say ' ℹ️ Android SDK not found (set ANDROID_HOME)', :cyan
510
520
  end
511
- else
512
- say " ℹ️ Gradle not found (will use project gradlew)", :cyan
513
- end
514
- say ""
515
-
516
- # Check 14: Google Play credentials (if logged in)
517
- if client && org_data
518
- say "Checking Google Play configuration...", :yellow
519
-
520
- if org_data['google_play_configured']
521
- say " ✓ Google Play credentials configured", :green
521
+
522
+ # Check 13: Gradle
523
+ if system('which gradle > /dev/null 2>&1') || (android_home && File.exist?("#{android_home}/../gradle"))
524
+ gradle_version = begin
525
+ `gradle --version 2>&1 | grep 'Gradle '`.strip
526
+ rescue StandardError
527
+ ''
528
+ end
529
+ if gradle_version.empty?
530
+ say ' ✓ Gradle available (version check skipped)', :green
531
+ else
532
+ say " ✓ #{gradle_version}", :green
533
+ end
522
534
  else
523
- say " ℹ️ Google Play not configured", :cyan
524
- say " Configure in My Signer dashboard or run 'mysigner doctor'", :cyan
535
+ say ' ℹ️ Gradle not found (will use project gradlew)', :cyan
525
536
  end
537
+ say ''
526
538
 
527
- # Check for keystores
528
- begin
529
- require_relative '../signing/keystore_manager'
530
- manager = Signing::KeystoreManager.new(client, config.current_organization_id)
531
- keystores = manager.list
532
-
533
- if keystores.any?
534
- active = keystores.find { |k| k['active'] }
535
- if active
536
- say " ✓ Active keystore: #{active['name']}", :green
539
+ # Check 14: Google Play credentials (if logged in)
540
+ if client && org_data
541
+ say 'Checking Google Play configuration...', :yellow
542
+
543
+ if org_data['google_play_configured']
544
+ say ' ✓ Google Play credentials configured', :green
545
+ else
546
+ say ' ℹ️ Google Play not configured', :cyan
547
+ say " Configure in My Signer dashboard or run 'mysigner doctor'", :cyan
548
+ end
549
+
550
+ # Check for keystores
551
+ begin
552
+ require_relative '../signing/keystore_manager'
553
+ manager = Signing::KeystoreManager.new(client, config.current_organization_id)
554
+ keystores = manager.list
555
+
556
+ if keystores.any?
557
+ active = keystores.find { |k| k['active'] }
558
+ if active
559
+ say " ✓ Active keystore: #{active['name']}", :green
560
+ else
561
+ say " ⚠️ #{keystores.count} keystores, none active", :yellow
562
+ warnings << 'No active keystore - activate one with: mysigner keystore activate ID'
563
+ end
537
564
  else
538
- say " ⚠️ #{keystores.count} keystores, none active", :yellow
539
- warnings << "No active keystore - activate one with: mysigner keystore activate ID"
565
+ say ' ℹ️ No Android keystores', :cyan
566
+ say ' Upload with: mysigner keystore upload PATH', :cyan
540
567
  end
541
- else
542
- say " ℹ️ No Android keystores", :cyan
543
- say " Upload with: mysigner keystore upload PATH", :cyan
568
+ rescue StandardError => e
569
+ say " ⚠️ Could not check keystores: #{e.message}", :yellow
544
570
  end
545
- rescue => e
546
- say " ⚠️ Could not check keystores: #{e.message}", :yellow
571
+ say ''
547
572
  end
548
- say ""
549
- end
550
573
 
551
- # Check 15: Android Project Detection
552
- android_project = nil
553
- begin
554
- android_project = Build::Detector.detect_android
555
- framework = case android_project[:framework]
556
- when :capacitor then "Capacitor/Ionic"
557
- when :react_native then "React Native"
558
- when :flutter then "Flutter"
559
- else "Native Android"
574
+ # Check 15: Android Project Detection
575
+ nil
576
+ begin
577
+ android_project = Build::Detector.detect_android
578
+ framework = case android_project[:framework]
579
+ when :capacitor then 'Capacitor/Ionic'
580
+ when :react_native then 'React Native'
581
+ when :flutter then 'Flutter'
582
+ else 'Native Android'
583
+ end
584
+ say 'Checking Android project...', :yellow
585
+ say " ✓ Found #{framework} Android project", :green
586
+
587
+ # Parse project details
588
+ require_relative '../build/android_parser'
589
+ parser = Build::AndroidParser.new(android_project)
590
+ say " Package: #{parser.application_id}", :cyan
591
+ say " Version: #{parser.version_name} (#{parser.version_code})", :cyan
592
+ say " Gradle wrapper: #{parser.gradle_wrapper_exists? ? '✓' : '✗'}", :cyan
593
+ say ''
594
+ rescue Build::Detector::NoProjectError
595
+ # Not an Android project, that's fine
596
+ rescue StandardError => e
597
+ say " ⚠️ Could not analyze Android project: #{e.message}", :yellow if android_available
560
598
  end
561
- say "Checking Android project...", :yellow
562
- say " ✓ Found #{framework} Android project", :green
563
-
564
- # Parse project details
565
- require_relative '../build/android_parser'
566
- parser = Build::AndroidParser.new(android_project)
567
- say " Package: #{parser.application_id}", :cyan
568
- say " Version: #{parser.version_name} (#{parser.version_code})", :cyan
569
- say " Gradle wrapper: #{parser.gradle_wrapper_exists? ? '✓' : '✗'}", :cyan
570
- say ""
571
- rescue Build::Detector::NoProjectError
572
- # Not an Android project, that's fine
573
- rescue => e
574
- say " ⚠️ Could not analyze Android project: #{e.message}", :yellow if android_available
575
599
  end
576
- end # end of check_android block
577
-
600
+
578
601
  # Final Report
579
- say "=" * 80, :cyan
580
- say "Health Report", :bold
581
- say "=" * 80, :cyan
582
- say ""
583
-
602
+ say '=' * 80, :cyan
603
+ say 'Health Report', :bold
604
+ say '=' * 80, :cyan
605
+ say ''
606
+
584
607
  if issues.empty? && warnings.empty?
585
608
  say "🎉 All checks passed! You're good to go!", :green
586
- say ""
587
- say "Try: mysigner ship testflight", :cyan
609
+ say ''
610
+ say 'Try: mysigner ship testflight', :cyan
588
611
  elsif issues.empty?
589
612
  say "⚠️ #{warnings.length} warning(s), but you're mostly good!", :yellow
590
- say ""
613
+ say ''
591
614
  warnings.each do |warning|
592
615
  say " • #{warning}", :yellow
593
616
  end
594
617
  else
595
618
  say "✗ #{issues.length} issue(s) found:", :red
596
- say ""
619
+ say ''
597
620
  issues.each do |issue|
598
621
  say " • #{issue}", :red
599
622
  end
600
-
623
+
601
624
  if warnings.any?
602
- say ""
625
+ say ''
603
626
  say "⚠️ #{warnings.length} warning(s):", :yellow
604
627
  warnings.each do |warning|
605
628
  say " • #{warning}", :yellow
606
629
  end
607
630
  end
608
631
  end
609
-
610
- say ""
632
+
633
+ say ''
611
634
  end
612
635
 
613
636
  no_commands do
@@ -620,62 +643,66 @@ module Mysigner
620
643
  # Generate a Certificate Signing Request (CSR)
621
644
  def generate_csr(email)
622
645
  require 'openssl'
623
-
624
- say " Generating CSR...", :cyan
625
-
646
+
647
+ say ' Generating CSR...', :cyan
648
+
626
649
  begin
627
650
  # Save to Downloads (visible in file picker)
628
- csr_dir = File.expand_path("~/Downloads")
651
+ csr_dir = File.expand_path('~/Downloads')
629
652
  FileUtils.mkdir_p(csr_dir)
630
-
653
+
631
654
  # Generate RSA key pair
632
655
  key = OpenSSL::PKey::RSA.new(2048)
633
-
656
+
634
657
  # Create CSR
635
658
  csr = OpenSSL::X509::Request.new
636
659
  csr.version = 0
637
660
  csr.subject = OpenSSL::X509::Name.new([
638
- ['CN', email || 'My Signer User'],
639
- ['emailAddress', email || 'user@example.com']
640
- ])
661
+ ['CN', email || 'My Signer User'],
662
+ ['emailAddress', email || 'user@example.com']
663
+ ])
641
664
  csr.public_key = key.public_key
642
- csr.sign(key, OpenSSL::Digest::SHA256.new)
643
-
665
+ csr.sign(key, OpenSSL::Digest.new('SHA256'))
666
+
644
667
  # Generate unique filename with timestamp
645
668
  timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
646
669
  csr_filename = "CertificateSigningRequest_#{timestamp}.certSigningRequest"
647
670
  key_filename = "private_key_#{timestamp}.pem"
648
-
671
+
649
672
  # Save CSR to Downloads (visible)
650
673
  csr_path = File.join(csr_dir, csr_filename)
651
-
674
+
652
675
  # Save private key to hidden location (secure)
653
- key_dir = File.expand_path("~/.mysigner/keys")
676
+ key_dir = File.expand_path('~/.mysigner/keys')
654
677
  FileUtils.mkdir_p(key_dir)
655
678
  key_path = File.join(key_dir, key_filename)
656
-
679
+
657
680
  # Save CSR file
658
681
  File.write(csr_path, csr.to_pem)
659
-
682
+
660
683
  # Import private key directly to keychain (so certificate can pair)
661
684
  File.write(key_path, key.to_pem)
662
-
663
- import_result = `security import #{key_path} -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign -T /usr/bin/security 2>&1`
664
- import_success = $?.success?
665
-
685
+
686
+ `security import #{key_path} -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign -T /usr/bin/security 2>&1`
687
+ import_success = $CHILD_STATUS.success?
688
+
689
+ say ' ✓ CSR saved to Downloads', :green
666
690
  if import_success
667
- say "CSR saved to Downloads", :green
668
- say " ✓ Private key imported to keychain", :green
691
+ say 'Private key imported to keychain', :green
669
692
  # Clean up the file after importing
670
- File.delete(key_path) rescue nil
693
+ begin
694
+ File.delete(key_path)
695
+ rescue StandardError
696
+ nil
697
+ end
671
698
  else
672
- say " ✓ CSR saved to Downloads", :green
673
699
  say " ✓ Private key saved to: #{key_path}", :green
674
- say " ⚠️ Import it with: security import #{key_path} -k ~/Library/Keychains/login.keychain-db", :yellow
700
+ say " ⚠️ Import it with: security import #{key_path} -k ~/Library/Keychains/login.keychain-db",
701
+ :yellow
675
702
  end
676
-
703
+
677
704
  csr_path
678
- rescue => e
705
+ rescue StandardError => e
679
706
  say " ✗ Failed to generate CSR: #{e.message}", :red
680
707
  nil
681
708
  end
@@ -684,8 +711,8 @@ module Mysigner
684
711
  # Helper to auto-create a provisioning profile
685
712
  def auto_create_profile(client, config, bundle_id, profile_type)
686
713
  say "Creating #{profile_type} profile for #{bundle_id}...", :yellow
687
- say ""
688
-
714
+ say ''
715
+
689
716
  # Map friendly names to Apple's profile types
690
717
  apple_profile_type = case profile_type.to_s.downcase
691
718
  when 'appstore', 'store' then 'IOS_APP_STORE'
@@ -693,40 +720,40 @@ module Mysigner
693
720
  when 'adhoc' then 'IOS_APP_ADHOC'
694
721
  else profile_type
695
722
  end
696
-
723
+
697
724
  begin
698
725
  # First, ensure resources are synced
699
- say " Syncing organization resources...", :cyan
726
+ say ' Syncing organization resources...', :cyan
700
727
  client.post("/api/v1/organizations/#{config.current_organization_id}/sync_app_store_connect")
701
-
728
+
702
729
  # Wait a bit for sync to complete
703
730
  sleep 2
704
-
731
+
705
732
  # Check sync status
706
733
  max_wait = 15 # seconds
707
734
  waited = 0
708
735
  sync_complete = false
709
-
736
+
710
737
  while waited < max_wait
711
738
  status_response = client.get("/api/v1/organizations/#{config.current_organization_id}/sync/status")
712
739
  sync_data = status_response[:data]['sync']
713
-
714
- if !sync_data['running']
740
+
741
+ unless sync_data['running']
715
742
  sync_complete = true
716
743
  break
717
744
  end
718
-
745
+
719
746
  sleep 1
720
747
  waited += 1
721
748
  end
722
-
749
+
723
750
  if sync_complete
724
- say " ✓ Sync complete", :green
751
+ say ' ✓ Sync complete', :green
725
752
  else
726
- say " ⚠️ Sync still running, continuing anyway...", :yellow
753
+ say ' ⚠️ Sync still running, continuing anyway...', :yellow
727
754
  end
728
- say ""
729
-
755
+ say ''
756
+
730
757
  # Create profile
731
758
  say " Creating #{apple_profile_type} profile...", :cyan
732
759
  response = client.post(
@@ -736,16 +763,16 @@ module Mysigner
736
763
  profile_type: apple_profile_type
737
764
  }
738
765
  )
739
-
766
+
740
767
  if response[:success]
741
768
  profile = response[:data]['profile']
742
769
  say " ✓ Created profile: #{profile['name']}", :green
743
770
  say " UUID: #{profile['uuid']}", :cyan
744
771
  say " Expires: #{profile['expires_at']}", :cyan
745
- say ""
746
-
772
+ say ''
773
+
747
774
  # Download and install the profile using direct Faraday for binary data
748
- say " Downloading profile...", :cyan
775
+ say ' Downloading profile...', :cyan
749
776
  download_url = "/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile['id']}/download"
750
777
 
751
778
  conn = Faraday.new(url: config.api_url) do |f|
@@ -760,86 +787,87 @@ module Mysigner
760
787
 
761
788
  if download_response.success?
762
789
  # Install to Xcode's provisioning profiles directory
763
- profiles_dir = File.expand_path("~/Library/MobileDevice/Provisioning Profiles")
790
+ profiles_dir = File.expand_path('~/Library/MobileDevice/Provisioning Profiles')
764
791
  FileUtils.mkdir_p(profiles_dir)
765
792
  profile_path = File.join(profiles_dir, "#{profile['uuid']}.mobileprovision")
766
793
  File.binwrite(profile_path, download_response.body)
767
794
 
768
- say " ✓ Profile installed to Xcode", :green
795
+ say ' ✓ Profile installed to Xcode', :green
769
796
  else
770
797
  say " ⚠️ Could not download profile: HTTP #{download_response.status}", :yellow
771
798
  end
772
- say ""
799
+ say ''
773
800
  true
774
801
  else
775
- say " ✗ Failed to create profile", :red
802
+ say ' ✗ Failed to create profile', :red
776
803
  false
777
804
  end
778
805
  rescue Mysigner::ClientError => e
779
806
  error_msg = e.message
780
-
781
- if error_msg.include?("bundle_id_not_found")
807
+
808
+ if error_msg.include?('bundle_id_not_found')
782
809
  say " ✗ Bundle ID '#{bundle_id}' not found in App Store Connect", :red
783
- say ""
784
- say " You need to register this bundle ID first:", :yellow
785
- say " 1. Go to: https://developer.apple.com/account/resources/identifiers/list", :cyan
810
+ say ''
811
+ say ' You need to register this bundle ID first:', :yellow
812
+ say ' 1. Go to: https://developer.apple.com/account/resources/identifiers/list', :cyan
786
813
  say " 2. Register bundle ID: #{bundle_id}", :cyan
787
814
  say " 3. Run 'mysigner doctor' again", :cyan
788
- elsif error_msg.include?("certificates found") || error_msg.include?("no_certificates")
789
- cert_type = apple_profile_type == 'IOS_APP_STORE' ? "Distribution" : "Development"
790
- cert_name = apple_profile_type == 'IOS_APP_STORE' ? "Apple Distribution" : "Apple Development"
791
-
815
+ elsif error_msg.include?('certificates found') || error_msg.include?('no_certificates')
816
+ cert_type = apple_profile_type == 'IOS_APP_STORE' ? 'Distribution' : 'Development'
817
+ cert_name = apple_profile_type == 'IOS_APP_STORE' ? 'Apple Distribution' : 'Apple Development'
818
+
792
819
  say " ✗ No #{cert_type} certificates found", :red
793
- say ""
794
-
820
+ say ''
821
+
795
822
  # Offer to generate CSR automatically
796
- if yes_with_default?("Generate CSR and get step-by-step guide?", :green)
797
- say ""
823
+ say ''
824
+ if yes_with_default?('Generate CSR and get step-by-step guide?', :green)
798
825
  csr_path = generate_csr(config.user_email)
799
-
826
+
800
827
  if csr_path
801
- say ""
828
+ say ''
802
829
  say " ✓ CSR ready: #{File.basename(csr_path)}", :green
803
- say ""
804
- say " 📋 Next steps:", :cyan
805
- say " 1. Go to: https://developer.apple.com/account/resources/certificates/add", :green
830
+ say ''
831
+ say ' 📋 Next steps:', :cyan
832
+ say ' 1. Go to: https://developer.apple.com/account/resources/certificates/add',
833
+ :green
806
834
  say " 2. Select: '#{cert_name}'", :green
807
835
  say " 3. Upload CSR: #{csr_path}", :green
808
- say " 4. Download .cer file and double-click to install", :green
836
+ say ' 4. Download .cer file and double-click to install', :green
809
837
  say " 5. Sync in My Signer → Run 'mysigner doctor' again", :green
810
- say ""
838
+ say ''
811
839
  end
812
840
  else
813
- say ""
814
- say " 📋 Quick guide:", :cyan
815
- say " 1. Open Keychain Access → Request Certificate (save as CSR)", :green
816
- say " 2. https://developer.apple.com/account/resources/certificates/add", :green
841
+ say ' 📋 Quick guide:', :cyan
842
+ say ' 1. Open Keychain Access → Request Certificate (save as CSR)', :green
843
+ say ' 2. https://developer.apple.com/account/resources/certificates/add',
844
+ :green
817
845
  say " 3. Select '#{cert_name}' → Upload CSR → Download .cer", :green
818
- say " 4. Double-click .cer to install → Sync My Signer", :green
819
- say ""
846
+ say ' 4. Double-click .cer to install → Sync My Signer', :green
847
+ say ''
820
848
  end
821
- elsif error_msg.include?("no_devices") || error_msg.include?("devices found")
822
- say " ✗ No test devices (needed for dev profiles)", :red
823
- say ""
824
- say " 📋 Quick fix:", :cyan
825
- say " • Get UDID: Connect device → Finder → Click serial number", :green
826
- say " • Run: mysigner device add <UDID> <NAME>", :green
849
+ elsif error_msg.include?('no_devices') || error_msg.include?('devices found')
850
+ say ' ✗ No test devices (needed for dev profiles)', :red
851
+ say ''
852
+ say ' 📋 Quick fix:', :cyan
853
+ say ' • Get UDID: Connect device → Finder → Click serial number', :green
854
+ say ' • Run: mysigner device add <NAME> <UDID>', :green
827
855
  say " • Or add in: #{client.api_url}/organizations/#{config.current_organization_id}", :green
828
- say ""
856
+ say ''
829
857
  else
830
858
  say " ✗ Failed: #{error_msg}", :red
831
859
  end
832
- say ""
860
+ say ''
833
861
  false
834
- rescue => e
862
+ rescue StandardError => e
835
863
  say " ✗ Unexpected error: #{e.message}", :red
836
- say ""
864
+ say ''
837
865
  false
838
866
  end
839
867
  end
840
868
  end
841
869
 
842
- desc "sync [PLATFORM]", "🔄 Sync data from App Store Connect or Google Play"
870
+ desc 'sync [PLATFORM]', '🔄 Sync data from App Store Connect or Google Play'
843
871
  long_desc <<~DESC
844
872
  Sync your organization's data from app stores.
845
873
 
@@ -870,19 +898,19 @@ module Mysigner
870
898
  sync_android(client, config)
871
899
  when 'all', 'both'
872
900
  sync_ios(client, config)
873
- say ""
901
+ say ''
874
902
  sync_android(client, config)
875
903
  else
876
904
  error "Unknown platform: #{platform}"
877
- say "Valid platforms: ios, android, all", :yellow
905
+ say 'Valid platforms: ios, android, all', :yellow
878
906
  exit 1
879
907
  end
880
908
  end
881
909
 
882
910
  no_commands do
883
911
  def sync_ios(client, config)
884
- say "🔄 Syncing data from App Store Connect...", :cyan
885
- say ""
912
+ say '🔄 Syncing data from App Store Connect...', :cyan
913
+ say ''
886
914
 
887
915
  begin
888
916
  response = client.post(
@@ -892,16 +920,14 @@ module Mysigner
892
920
 
893
921
  if response[:success]
894
922
  data = response[:data]['data'] || response[:data]
895
- say "✓ iOS sync completed!", :green
896
- say ""
923
+ say '✓ iOS sync completed!', :green
924
+ say ''
897
925
 
898
- if data['synced_at']
899
- say "Last synced: #{data['synced_at']}", :cyan
900
- end
926
+ say "Last synced: #{data['synced_at']}", :cyan if data['synced_at']
901
927
 
902
928
  if data['summary']
903
- say ""
904
- say "📊 Summary:", :cyan
929
+ say ''
930
+ say '📊 Summary:', :cyan
905
931
  summary = data['summary']
906
932
  say " • Apps: #{summary['apps']}" if summary['apps']
907
933
  say " • Builds: #{summary['builds']}" if summary['builds']
@@ -912,14 +938,14 @@ module Mysigner
912
938
  else
913
939
  say "✗ iOS sync failed: #{response[:error]}", :red
914
940
  end
915
- rescue => e
941
+ rescue StandardError => e
916
942
  say "✗ iOS sync failed: #{e.message}", :red
917
943
  end
918
944
  end
919
945
 
920
946
  def sync_android(client, config)
921
- say "🔄 Syncing data from Google Play...", :cyan
922
- say ""
947
+ say '🔄 Syncing data from Google Play...', :cyan
948
+ say ''
923
949
 
924
950
  begin
925
951
  response = client.post(
@@ -928,29 +954,29 @@ module Mysigner
928
954
  )
929
955
 
930
956
  if response[:success]
931
- say "✓ Android sync started!", :green
932
- say ""
933
- say "Sync runs in the background. Check status with:", :cyan
934
- say " mysigner android-apps", :green
935
- say ""
936
-
957
+ say '✓ Android sync started!', :green
958
+ say ''
959
+ say 'Sync runs in the background. Check status with:', :cyan
960
+ say ' mysigner apps --platform android', :green
961
+ say ''
962
+
937
963
  # Optionally wait and show status
938
- say "💡 Google Play sync may take a few minutes.", :yellow
964
+ say '💡 Google Play sync may take a few minutes.', :yellow
939
965
  say " Unlike iOS, Google Play doesn't auto-discover apps.", :yellow
940
- say " If no apps appear, add them in the web dashboard first.", :yellow
966
+ say ' If no apps appear, add them in the web dashboard first.', :yellow
941
967
  else
942
968
  say "✗ Android sync failed: #{response[:error] || 'Unknown error'}", :red
943
969
  end
944
970
  rescue Mysigner::ClientError => e
945
- if e.message.include?("No active Google Play credential")
946
- say "✗ Google Play not configured", :red
947
- say ""
948
- say "Set up credentials first:", :yellow
949
- say " Configure Google Play in My Signer dashboard", :green
971
+ if e.message.include?('No active Google Play credential')
972
+ say '✗ Google Play not configured', :red
973
+ say ''
974
+ say 'Set up credentials first:', :yellow
975
+ say ' Configure Google Play in My Signer dashboard', :green
950
976
  else
951
977
  say "✗ Android sync failed: #{e.message}", :red
952
978
  end
953
- rescue => e
979
+ rescue StandardError => e
954
980
  say "✗ Android sync failed: #{e.message}", :red
955
981
  end
956
982
  end
@@ -995,14 +1021,12 @@ module Mysigner
995
1021
  # Fix JAVA_HOME in shell config
996
1022
  def fix_java_home(java_home)
997
1023
  shell_config = File.expand_path('~/.zshrc')
998
-
1024
+
999
1025
  # Use ~/.bash_profile if zsh config doesn't exist
1000
- unless File.exist?(shell_config)
1001
- shell_config = File.expand_path('~/.bash_profile')
1002
- end
1026
+ shell_config = File.expand_path('~/.bash_profile') unless File.exist?(shell_config)
1003
1027
 
1004
1028
  # Read existing content
1005
- content = File.exist?(shell_config) ? File.read(shell_config) : ""
1029
+ content = File.exist?(shell_config) ? File.read(shell_config) : ''
1006
1030
 
1007
1031
  # Check if JAVA_HOME is already set
1008
1032
  if content.include?('export JAVA_HOME=')
@@ -1013,18 +1037,18 @@ module Mysigner
1013
1037
  else
1014
1038
  # Append JAVA_HOME
1015
1039
  File.open(shell_config, 'a') do |f|
1016
- f.puts ""
1017
- f.puts "# Added by mysigner doctor"
1040
+ f.puts ''
1041
+ f.puts '# Added by mysigner doctor'
1018
1042
  f.puts "export JAVA_HOME=\"#{java_home}\""
1019
1043
  end
1020
1044
  say " ✓ Added JAVA_HOME to #{shell_config}", :green
1021
1045
  end
1022
1046
 
1023
- say ""
1024
- say " To apply now, run:", :yellow
1047
+ say ''
1048
+ say ' To apply now, run:', :yellow
1025
1049
  say " source #{shell_config}", :cyan
1026
- say ""
1027
- say " Or restart your terminal.", :yellow
1050
+ say ''
1051
+ say ' Or restart your terminal.', :yellow
1028
1052
  end
1029
1053
  end
1030
1054
  end