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,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,599 +23,626 @@ 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
614
- # Helper method for yes/no prompts with Enter defaulting to yes
637
+ # Helper method for yes/no prompts with Enter defaulting to yes.
638
+ # When stdin is not a TTY (pipe, redirect, CI), default to NO so
639
+ # `mysigner doctor` never silently mutates user files (e.g. ~/.zshrc)
640
+ # without an interactive confirmation.
615
641
  def yes_with_default?(statement, color = nil)
642
+ unless $stdin.tty?
643
+ say "#{statement} [Y/n] (non-interactive: assuming no)", color
644
+ return false
645
+ end
616
646
  response = ask("#{statement} [Y/n]", color).to_s.strip.downcase
617
647
  response.empty? || response == 'y' || response == 'yes'
618
648
  end
@@ -620,62 +650,66 @@ module Mysigner
620
650
  # Generate a Certificate Signing Request (CSR)
621
651
  def generate_csr(email)
622
652
  require 'openssl'
623
-
624
- say " Generating CSR...", :cyan
625
-
653
+
654
+ say ' Generating CSR...', :cyan
655
+
626
656
  begin
627
657
  # Save to Downloads (visible in file picker)
628
- csr_dir = File.expand_path("~/Downloads")
658
+ csr_dir = File.expand_path('~/Downloads')
629
659
  FileUtils.mkdir_p(csr_dir)
630
-
660
+
631
661
  # Generate RSA key pair
632
662
  key = OpenSSL::PKey::RSA.new(2048)
633
-
663
+
634
664
  # Create CSR
635
665
  csr = OpenSSL::X509::Request.new
636
666
  csr.version = 0
637
667
  csr.subject = OpenSSL::X509::Name.new([
638
- ['CN', email || 'My Signer User'],
639
- ['emailAddress', email || 'user@example.com']
640
- ])
668
+ ['CN', email || 'My Signer User'],
669
+ ['emailAddress', email || 'user@example.com']
670
+ ])
641
671
  csr.public_key = key.public_key
642
- csr.sign(key, OpenSSL::Digest::SHA256.new)
643
-
672
+ csr.sign(key, OpenSSL::Digest.new('SHA256'))
673
+
644
674
  # Generate unique filename with timestamp
645
675
  timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
646
676
  csr_filename = "CertificateSigningRequest_#{timestamp}.certSigningRequest"
647
677
  key_filename = "private_key_#{timestamp}.pem"
648
-
678
+
649
679
  # Save CSR to Downloads (visible)
650
680
  csr_path = File.join(csr_dir, csr_filename)
651
-
681
+
652
682
  # Save private key to hidden location (secure)
653
- key_dir = File.expand_path("~/.mysigner/keys")
683
+ key_dir = File.expand_path('~/.mysigner/keys')
654
684
  FileUtils.mkdir_p(key_dir)
655
685
  key_path = File.join(key_dir, key_filename)
656
-
686
+
657
687
  # Save CSR file
658
688
  File.write(csr_path, csr.to_pem)
659
-
689
+
660
690
  # Import private key directly to keychain (so certificate can pair)
661
691
  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
-
692
+
693
+ `security import #{key_path} -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign -T /usr/bin/security 2>&1`
694
+ import_success = $CHILD_STATUS.success?
695
+
696
+ say ' ✓ CSR saved to Downloads', :green
666
697
  if import_success
667
- say "CSR saved to Downloads", :green
668
- say " ✓ Private key imported to keychain", :green
698
+ say 'Private key imported to keychain', :green
669
699
  # Clean up the file after importing
670
- File.delete(key_path) rescue nil
700
+ begin
701
+ File.delete(key_path)
702
+ rescue StandardError
703
+ nil
704
+ end
671
705
  else
672
- say " ✓ CSR saved to Downloads", :green
673
706
  say " ✓ Private key saved to: #{key_path}", :green
674
- say " ⚠️ Import it with: security import #{key_path} -k ~/Library/Keychains/login.keychain-db", :yellow
707
+ say " ⚠️ Import it with: security import #{key_path} -k ~/Library/Keychains/login.keychain-db",
708
+ :yellow
675
709
  end
676
-
710
+
677
711
  csr_path
678
- rescue => e
712
+ rescue StandardError => e
679
713
  say " ✗ Failed to generate CSR: #{e.message}", :red
680
714
  nil
681
715
  end
@@ -684,8 +718,8 @@ module Mysigner
684
718
  # Helper to auto-create a provisioning profile
685
719
  def auto_create_profile(client, config, bundle_id, profile_type)
686
720
  say "Creating #{profile_type} profile for #{bundle_id}...", :yellow
687
- say ""
688
-
721
+ say ''
722
+
689
723
  # Map friendly names to Apple's profile types
690
724
  apple_profile_type = case profile_type.to_s.downcase
691
725
  when 'appstore', 'store' then 'IOS_APP_STORE'
@@ -693,40 +727,40 @@ module Mysigner
693
727
  when 'adhoc' then 'IOS_APP_ADHOC'
694
728
  else profile_type
695
729
  end
696
-
730
+
697
731
  begin
698
732
  # First, ensure resources are synced
699
- say " Syncing organization resources...", :cyan
733
+ say ' Syncing organization resources...', :cyan
700
734
  client.post("/api/v1/organizations/#{config.current_organization_id}/sync_app_store_connect")
701
-
735
+
702
736
  # Wait a bit for sync to complete
703
737
  sleep 2
704
-
738
+
705
739
  # Check sync status
706
740
  max_wait = 15 # seconds
707
741
  waited = 0
708
742
  sync_complete = false
709
-
743
+
710
744
  while waited < max_wait
711
745
  status_response = client.get("/api/v1/organizations/#{config.current_organization_id}/sync/status")
712
746
  sync_data = status_response[:data]['sync']
713
-
714
- if !sync_data['running']
747
+
748
+ unless sync_data['running']
715
749
  sync_complete = true
716
750
  break
717
751
  end
718
-
752
+
719
753
  sleep 1
720
754
  waited += 1
721
755
  end
722
-
756
+
723
757
  if sync_complete
724
- say " ✓ Sync complete", :green
758
+ say ' ✓ Sync complete', :green
725
759
  else
726
- say " ⚠️ Sync still running, continuing anyway...", :yellow
760
+ say ' ⚠️ Sync still running, continuing anyway...', :yellow
727
761
  end
728
- say ""
729
-
762
+ say ''
763
+
730
764
  # Create profile
731
765
  say " Creating #{apple_profile_type} profile...", :cyan
732
766
  response = client.post(
@@ -736,16 +770,16 @@ module Mysigner
736
770
  profile_type: apple_profile_type
737
771
  }
738
772
  )
739
-
773
+
740
774
  if response[:success]
741
775
  profile = response[:data]['profile']
742
776
  say " ✓ Created profile: #{profile['name']}", :green
743
777
  say " UUID: #{profile['uuid']}", :cyan
744
778
  say " Expires: #{profile['expires_at']}", :cyan
745
- say ""
746
-
779
+ say ''
780
+
747
781
  # Download and install the profile using direct Faraday for binary data
748
- say " Downloading profile...", :cyan
782
+ say ' Downloading profile...', :cyan
749
783
  download_url = "/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile['id']}/download"
750
784
 
751
785
  conn = Faraday.new(url: config.api_url) do |f|
@@ -760,86 +794,87 @@ module Mysigner
760
794
 
761
795
  if download_response.success?
762
796
  # Install to Xcode's provisioning profiles directory
763
- profiles_dir = File.expand_path("~/Library/MobileDevice/Provisioning Profiles")
797
+ profiles_dir = File.expand_path('~/Library/MobileDevice/Provisioning Profiles')
764
798
  FileUtils.mkdir_p(profiles_dir)
765
799
  profile_path = File.join(profiles_dir, "#{profile['uuid']}.mobileprovision")
766
800
  File.binwrite(profile_path, download_response.body)
767
801
 
768
- say " ✓ Profile installed to Xcode", :green
802
+ say ' ✓ Profile installed to Xcode', :green
769
803
  else
770
804
  say " ⚠️ Could not download profile: HTTP #{download_response.status}", :yellow
771
805
  end
772
- say ""
806
+ say ''
773
807
  true
774
808
  else
775
- say " ✗ Failed to create profile", :red
809
+ say ' ✗ Failed to create profile', :red
776
810
  false
777
811
  end
778
812
  rescue Mysigner::ClientError => e
779
813
  error_msg = e.message
780
-
781
- if error_msg.include?("bundle_id_not_found")
814
+
815
+ if error_msg.include?('bundle_id_not_found')
782
816
  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
817
+ say ''
818
+ say ' You need to register this bundle ID first:', :yellow
819
+ say ' 1. Go to: https://developer.apple.com/account/resources/identifiers/list', :cyan
786
820
  say " 2. Register bundle ID: #{bundle_id}", :cyan
787
821
  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
-
822
+ elsif error_msg.include?('certificates found') || error_msg.include?('no_certificates')
823
+ cert_type = apple_profile_type == 'IOS_APP_STORE' ? 'Distribution' : 'Development'
824
+ cert_name = apple_profile_type == 'IOS_APP_STORE' ? 'Apple Distribution' : 'Apple Development'
825
+
792
826
  say " ✗ No #{cert_type} certificates found", :red
793
- say ""
794
-
827
+ say ''
828
+
795
829
  # Offer to generate CSR automatically
796
- if yes_with_default?("Generate CSR and get step-by-step guide?", :green)
797
- say ""
830
+ say ''
831
+ if yes_with_default?('Generate CSR and get step-by-step guide?', :green)
798
832
  csr_path = generate_csr(config.user_email)
799
-
833
+
800
834
  if csr_path
801
- say ""
835
+ say ''
802
836
  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
837
+ say ''
838
+ say ' 📋 Next steps:', :cyan
839
+ say ' 1. Go to: https://developer.apple.com/account/resources/certificates/add',
840
+ :green
806
841
  say " 2. Select: '#{cert_name}'", :green
807
842
  say " 3. Upload CSR: #{csr_path}", :green
808
- say " 4. Download .cer file and double-click to install", :green
843
+ say ' 4. Download .cer file and double-click to install', :green
809
844
  say " 5. Sync in My Signer → Run 'mysigner doctor' again", :green
810
- say ""
845
+ say ''
811
846
  end
812
847
  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
848
+ say ' 📋 Quick guide:', :cyan
849
+ say ' 1. Open Keychain Access → Request Certificate (save as CSR)', :green
850
+ say ' 2. https://developer.apple.com/account/resources/certificates/add',
851
+ :green
817
852
  say " 3. Select '#{cert_name}' → Upload CSR → Download .cer", :green
818
- say " 4. Double-click .cer to install → Sync My Signer", :green
819
- say ""
853
+ say ' 4. Double-click .cer to install → Sync My Signer', :green
854
+ say ''
820
855
  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 <NAME> <UDID>", :green
856
+ elsif error_msg.include?('no_devices') || error_msg.include?('devices found')
857
+ say ' ✗ No test devices (needed for dev profiles)', :red
858
+ say ''
859
+ say ' 📋 Quick fix:', :cyan
860
+ say ' • Get UDID: Connect device → Finder → Click serial number', :green
861
+ say ' • Run: mysigner device add <NAME> <UDID>', :green
827
862
  say " • Or add in: #{client.api_url}/organizations/#{config.current_organization_id}", :green
828
- say ""
863
+ say ''
829
864
  else
830
865
  say " ✗ Failed: #{error_msg}", :red
831
866
  end
832
- say ""
867
+ say ''
833
868
  false
834
- rescue => e
869
+ rescue StandardError => e
835
870
  say " ✗ Unexpected error: #{e.message}", :red
836
- say ""
871
+ say ''
837
872
  false
838
873
  end
839
874
  end
840
875
  end
841
876
 
842
- desc "sync [PLATFORM]", "🔄 Sync data from App Store Connect or Google Play"
877
+ desc 'sync [PLATFORM]', '🔄 Sync data from App Store Connect or Google Play'
843
878
  long_desc <<~DESC
844
879
  Sync your organization's data from app stores.
845
880
 
@@ -870,19 +905,19 @@ module Mysigner
870
905
  sync_android(client, config)
871
906
  when 'all', 'both'
872
907
  sync_ios(client, config)
873
- say ""
908
+ say ''
874
909
  sync_android(client, config)
875
910
  else
876
911
  error "Unknown platform: #{platform}"
877
- say "Valid platforms: ios, android, all", :yellow
912
+ say 'Valid platforms: ios, android, all', :yellow
878
913
  exit 1
879
914
  end
880
915
  end
881
916
 
882
917
  no_commands do
883
918
  def sync_ios(client, config)
884
- say "🔄 Syncing data from App Store Connect...", :cyan
885
- say ""
919
+ say '🔄 Syncing data from App Store Connect...', :cyan
920
+ say ''
886
921
 
887
922
  begin
888
923
  response = client.post(
@@ -892,16 +927,14 @@ module Mysigner
892
927
 
893
928
  if response[:success]
894
929
  data = response[:data]['data'] || response[:data]
895
- say "✓ iOS sync completed!", :green
896
- say ""
930
+ say '✓ iOS sync completed!', :green
931
+ say ''
897
932
 
898
- if data['synced_at']
899
- say "Last synced: #{data['synced_at']}", :cyan
900
- end
933
+ say "Last synced: #{data['synced_at']}", :cyan if data['synced_at']
901
934
 
902
935
  if data['summary']
903
- say ""
904
- say "📊 Summary:", :cyan
936
+ say ''
937
+ say '📊 Summary:', :cyan
905
938
  summary = data['summary']
906
939
  say " • Apps: #{summary['apps']}" if summary['apps']
907
940
  say " • Builds: #{summary['builds']}" if summary['builds']
@@ -912,14 +945,14 @@ module Mysigner
912
945
  else
913
946
  say "✗ iOS sync failed: #{response[:error]}", :red
914
947
  end
915
- rescue => e
948
+ rescue StandardError => e
916
949
  say "✗ iOS sync failed: #{e.message}", :red
917
950
  end
918
951
  end
919
952
 
920
953
  def sync_android(client, config)
921
- say "🔄 Syncing data from Google Play...", :cyan
922
- say ""
954
+ say '🔄 Syncing data from Google Play...', :cyan
955
+ say ''
923
956
 
924
957
  begin
925
958
  response = client.post(
@@ -928,29 +961,29 @@ module Mysigner
928
961
  )
929
962
 
930
963
  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 apps --platform android", :green
935
- say ""
936
-
964
+ say '✓ Android sync started!', :green
965
+ say ''
966
+ say 'Sync runs in the background. Check status with:', :cyan
967
+ say ' mysigner apps --platform android', :green
968
+ say ''
969
+
937
970
  # Optionally wait and show status
938
- say "💡 Google Play sync may take a few minutes.", :yellow
971
+ say '💡 Google Play sync may take a few minutes.', :yellow
939
972
  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
973
+ say ' If no apps appear, add them in the web dashboard first.', :yellow
941
974
  else
942
975
  say "✗ Android sync failed: #{response[:error] || 'Unknown error'}", :red
943
976
  end
944
977
  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
978
+ if e.message.include?('No active Google Play credential')
979
+ say '✗ Google Play not configured', :red
980
+ say ''
981
+ say 'Set up credentials first:', :yellow
982
+ say ' Configure Google Play in My Signer dashboard', :green
950
983
  else
951
984
  say "✗ Android sync failed: #{e.message}", :red
952
985
  end
953
- rescue => e
986
+ rescue StandardError => e
954
987
  say "✗ Android sync failed: #{e.message}", :red
955
988
  end
956
989
  end
@@ -995,14 +1028,12 @@ module Mysigner
995
1028
  # Fix JAVA_HOME in shell config
996
1029
  def fix_java_home(java_home)
997
1030
  shell_config = File.expand_path('~/.zshrc')
998
-
1031
+
999
1032
  # 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
1033
+ shell_config = File.expand_path('~/.bash_profile') unless File.exist?(shell_config)
1003
1034
 
1004
1035
  # Read existing content
1005
- content = File.exist?(shell_config) ? File.read(shell_config) : ""
1036
+ content = File.exist?(shell_config) ? File.read(shell_config) : ''
1006
1037
 
1007
1038
  # Check if JAVA_HOME is already set
1008
1039
  if content.include?('export JAVA_HOME=')
@@ -1013,18 +1044,18 @@ module Mysigner
1013
1044
  else
1014
1045
  # Append JAVA_HOME
1015
1046
  File.open(shell_config, 'a') do |f|
1016
- f.puts ""
1017
- f.puts "# Added by mysigner doctor"
1047
+ f.puts ''
1048
+ f.puts '# Added by mysigner doctor'
1018
1049
  f.puts "export JAVA_HOME=\"#{java_home}\""
1019
1050
  end
1020
1051
  say " ✓ Added JAVA_HOME to #{shell_config}", :green
1021
1052
  end
1022
1053
 
1023
- say ""
1024
- say " To apply now, run:", :yellow
1054
+ say ''
1055
+ say ' To apply now, run:', :yellow
1025
1056
  say " source #{shell_config}", :cyan
1026
- say ""
1027
- say " Or restart your terminal.", :yellow
1057
+ say ''
1058
+ say ' Or restart your terminal.', :yellow
1028
1059
  end
1029
1060
  end
1030
1061
  end