mysigner 0.1.0
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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +137 -0
- data/LICENSE +201 -0
- data/MANUAL_TEST.md +341 -0
- data/README.md +493 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/mysigner +5 -0
- data/lib/mysigner/build/android_executor.rb +367 -0
- data/lib/mysigner/build/android_parser.rb +293 -0
- data/lib/mysigner/build/configurator.rb +126 -0
- data/lib/mysigner/build/detector.rb +388 -0
- data/lib/mysigner/build/error_analyzer.rb +193 -0
- data/lib/mysigner/build/executor.rb +176 -0
- data/lib/mysigner/build/parser.rb +206 -0
- data/lib/mysigner/cli/auth_commands.rb +1381 -0
- data/lib/mysigner/cli/build_commands.rb +2095 -0
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +500 -0
- data/lib/mysigner/cli/concerns/api_helpers.rb +131 -0
- data/lib/mysigner/cli/concerns/error_handlers.rb +446 -0
- data/lib/mysigner/cli/concerns/helpers.rb +63 -0
- data/lib/mysigner/cli/diagnostic_commands.rb +1034 -0
- data/lib/mysigner/cli/resource_commands.rb +2670 -0
- data/lib/mysigner/cli.rb +43 -0
- data/lib/mysigner/client.rb +189 -0
- data/lib/mysigner/config.rb +311 -0
- data/lib/mysigner/export/exporter.rb +150 -0
- data/lib/mysigner/signing/certificate_checker.rb +148 -0
- data/lib/mysigner/signing/keystore_manager.rb +239 -0
- data/lib/mysigner/signing/validator.rb +150 -0
- data/lib/mysigner/signing/wizard.rb +784 -0
- data/lib/mysigner/upload/app_store_automation.rb +402 -0
- data/lib/mysigner/upload/app_store_submission.rb +312 -0
- data/lib/mysigner/upload/play_store_uploader.rb +378 -0
- data/lib/mysigner/upload/uploader.rb +373 -0
- data/lib/mysigner/version.rb +3 -0
- data/lib/mysigner.rb +15 -0
- data/mysigner.gemspec +78 -0
- data/test_manual.rb +102 -0
- metadata +286 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
module Mysigner
|
|
2
|
+
class CLI < Thor
|
|
3
|
+
module DiagnosticCommands
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.class_eval do
|
|
6
|
+
desc "doctor", "🩺 Run health check and diagnose setup issues (run this if stuck)"
|
|
7
|
+
method_option :platform, type: :string, desc: 'Check specific platform only: ios, android, or all (default)'
|
|
8
|
+
def doctor
|
|
9
|
+
say "🩺 My Signer Health Check", :cyan
|
|
10
|
+
say "=" * 80, :cyan
|
|
11
|
+
say ""
|
|
12
|
+
|
|
13
|
+
issues = []
|
|
14
|
+
warnings = []
|
|
15
|
+
|
|
16
|
+
# Determine which platforms to check
|
|
17
|
+
platform_filter = options[:platform]&.downcase
|
|
18
|
+
check_ios = platform_filter.nil? || platform_filter == 'all' || platform_filter == 'ios'
|
|
19
|
+
check_android = platform_filter.nil? || platform_filter == 'all' || platform_filter == 'android'
|
|
20
|
+
|
|
21
|
+
if platform_filter && !%w[ios android all].include?(platform_filter)
|
|
22
|
+
error "Invalid platform: #{platform_filter}"
|
|
23
|
+
say "Valid options: ios, android, all", :yellow
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check 1: Xcode (iOS only)
|
|
28
|
+
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)
|
|
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]
|
|
91
|
+
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
|
|
104
|
+
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
|
|
122
|
+
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"
|
|
133
|
+
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
|
|
162
|
+
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
163
|
+
org_data = org_response[:data]
|
|
164
|
+
else
|
|
165
|
+
say ""
|
|
166
|
+
issues << "App Store Connect setup incomplete - run 'mysigner onboard' to try again"
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
issues << "App Store Connect not configured - run 'mysigner onboard' to set it up"
|
|
170
|
+
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
|
+
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
|
|
179
|
+
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
|
|
195
|
+
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"
|
|
199
|
+
end
|
|
200
|
+
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"
|
|
218
|
+
else
|
|
219
|
+
say " ✓ Sufficient disk space: #{usage}% used", :green
|
|
220
|
+
end
|
|
221
|
+
else
|
|
222
|
+
say " ⚠️ Could not check disk space", :yellow
|
|
223
|
+
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"
|
|
251
|
+
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
|
|
271
|
+
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
|
|
280
|
+
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
|
|
289
|
+
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
|
|
303
|
+
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 }
|
|
373
|
+
)
|
|
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'"
|
|
389
|
+
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
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
if expired.any?
|
|
400
|
+
say " ⚠️ #{expired.length} profile(s) expired", :yellow
|
|
401
|
+
warnings << "Some profiles are expired - sync to refresh"
|
|
402
|
+
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')
|
|
420
|
+
else
|
|
421
|
+
warnings << "No development profile - you won't be able to test on devices"
|
|
422
|
+
end
|
|
423
|
+
else
|
|
424
|
+
say " ✓ Development profile exists", :green
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
rescue => e
|
|
429
|
+
say " ⚠️ Could not check project signing: #{e.message}", :yellow
|
|
430
|
+
warnings << "Project signing check failed"
|
|
431
|
+
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
|
+
end
|
|
437
|
+
end # end of check_ios block
|
|
438
|
+
|
|
439
|
+
# ==================== ANDROID CHECKS ====================
|
|
440
|
+
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
|
|
455
|
+
else
|
|
456
|
+
say " ✗ JAVA_HOME invalid: #{java_home}", :red
|
|
457
|
+
|
|
458
|
+
# Try to auto-detect correct JAVA_HOME
|
|
459
|
+
detected_java_home = detect_java_home
|
|
460
|
+
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)
|
|
464
|
+
fix_java_home(detected_java_home)
|
|
465
|
+
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"
|
|
469
|
+
end
|
|
470
|
+
else
|
|
471
|
+
issues << "JAVA_HOME points to non-existent directory and no Java found"
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
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
|
|
489
|
+
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
|
+
|
|
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
|
|
508
|
+
else
|
|
509
|
+
say " ✓ #{gradle_version}", :green
|
|
510
|
+
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
|
|
522
|
+
else
|
|
523
|
+
say " ℹ️ Google Play not configured", :cyan
|
|
524
|
+
say " Configure in My Signer dashboard or run 'mysigner doctor'", :cyan
|
|
525
|
+
end
|
|
526
|
+
|
|
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
|
|
537
|
+
else
|
|
538
|
+
say " ⚠️ #{keystores.count} keystores, none active", :yellow
|
|
539
|
+
warnings << "No active keystore - activate one with: mysigner keystore activate ID"
|
|
540
|
+
end
|
|
541
|
+
else
|
|
542
|
+
say " ℹ️ No Android keystores", :cyan
|
|
543
|
+
say " Upload with: mysigner keystore upload PATH", :cyan
|
|
544
|
+
end
|
|
545
|
+
rescue => e
|
|
546
|
+
say " ⚠️ Could not check keystores: #{e.message}", :yellow
|
|
547
|
+
end
|
|
548
|
+
say ""
|
|
549
|
+
end
|
|
550
|
+
|
|
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"
|
|
560
|
+
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
|
+
end
|
|
576
|
+
end # end of check_android block
|
|
577
|
+
|
|
578
|
+
# Final Report
|
|
579
|
+
say "=" * 80, :cyan
|
|
580
|
+
say "Health Report", :bold
|
|
581
|
+
say "=" * 80, :cyan
|
|
582
|
+
say ""
|
|
583
|
+
|
|
584
|
+
if issues.empty? && warnings.empty?
|
|
585
|
+
say "🎉 All checks passed! You're good to go!", :green
|
|
586
|
+
say ""
|
|
587
|
+
say "Try: mysigner ship testflight", :cyan
|
|
588
|
+
elsif issues.empty?
|
|
589
|
+
say "⚠️ #{warnings.length} warning(s), but you're mostly good!", :yellow
|
|
590
|
+
say ""
|
|
591
|
+
warnings.each do |warning|
|
|
592
|
+
say " • #{warning}", :yellow
|
|
593
|
+
end
|
|
594
|
+
else
|
|
595
|
+
say "✗ #{issues.length} issue(s) found:", :red
|
|
596
|
+
say ""
|
|
597
|
+
issues.each do |issue|
|
|
598
|
+
say " • #{issue}", :red
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
if warnings.any?
|
|
602
|
+
say ""
|
|
603
|
+
say "⚠️ #{warnings.length} warning(s):", :yellow
|
|
604
|
+
warnings.each do |warning|
|
|
605
|
+
say " • #{warning}", :yellow
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
say ""
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
no_commands do
|
|
614
|
+
# Helper method for yes/no prompts with Enter defaulting to yes
|
|
615
|
+
def yes_with_default?(statement, color = nil)
|
|
616
|
+
response = ask("#{statement} [Y/n]", color).to_s.strip.downcase
|
|
617
|
+
response.empty? || response == 'y' || response == 'yes'
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Generate a Certificate Signing Request (CSR)
|
|
621
|
+
def generate_csr(email)
|
|
622
|
+
require 'openssl'
|
|
623
|
+
|
|
624
|
+
say " Generating CSR...", :cyan
|
|
625
|
+
|
|
626
|
+
begin
|
|
627
|
+
# Save to Downloads (visible in file picker)
|
|
628
|
+
csr_dir = File.expand_path("~/Downloads")
|
|
629
|
+
FileUtils.mkdir_p(csr_dir)
|
|
630
|
+
|
|
631
|
+
# Generate RSA key pair
|
|
632
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
633
|
+
|
|
634
|
+
# Create CSR
|
|
635
|
+
csr = OpenSSL::X509::Request.new
|
|
636
|
+
csr.version = 0
|
|
637
|
+
csr.subject = OpenSSL::X509::Name.new([
|
|
638
|
+
['CN', email || 'My Signer User'],
|
|
639
|
+
['emailAddress', email || 'user@example.com']
|
|
640
|
+
])
|
|
641
|
+
csr.public_key = key.public_key
|
|
642
|
+
csr.sign(key, OpenSSL::Digest::SHA256.new)
|
|
643
|
+
|
|
644
|
+
# Generate unique filename with timestamp
|
|
645
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
|
646
|
+
csr_filename = "CertificateSigningRequest_#{timestamp}.certSigningRequest"
|
|
647
|
+
key_filename = "private_key_#{timestamp}.pem"
|
|
648
|
+
|
|
649
|
+
# Save CSR to Downloads (visible)
|
|
650
|
+
csr_path = File.join(csr_dir, csr_filename)
|
|
651
|
+
|
|
652
|
+
# Save private key to hidden location (secure)
|
|
653
|
+
key_dir = File.expand_path("~/.mysigner/keys")
|
|
654
|
+
FileUtils.mkdir_p(key_dir)
|
|
655
|
+
key_path = File.join(key_dir, key_filename)
|
|
656
|
+
|
|
657
|
+
# Save CSR file
|
|
658
|
+
File.write(csr_path, csr.to_pem)
|
|
659
|
+
|
|
660
|
+
# Import private key directly to keychain (so certificate can pair)
|
|
661
|
+
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
|
+
|
|
666
|
+
if import_success
|
|
667
|
+
say " ✓ CSR saved to Downloads", :green
|
|
668
|
+
say " ✓ Private key imported to keychain", :green
|
|
669
|
+
# Clean up the file after importing
|
|
670
|
+
File.delete(key_path) rescue nil
|
|
671
|
+
else
|
|
672
|
+
say " ✓ CSR saved to Downloads", :green
|
|
673
|
+
say " ✓ Private key saved to: #{key_path}", :green
|
|
674
|
+
say " ⚠️ Import it with: security import #{key_path} -k ~/Library/Keychains/login.keychain-db", :yellow
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
csr_path
|
|
678
|
+
rescue => e
|
|
679
|
+
say " ✗ Failed to generate CSR: #{e.message}", :red
|
|
680
|
+
nil
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Helper to auto-create a provisioning profile
|
|
685
|
+
def auto_create_profile(client, config, bundle_id, profile_type)
|
|
686
|
+
say "Creating #{profile_type} profile for #{bundle_id}...", :yellow
|
|
687
|
+
say ""
|
|
688
|
+
|
|
689
|
+
# Map friendly names to Apple's profile types
|
|
690
|
+
apple_profile_type = case profile_type.to_s.downcase
|
|
691
|
+
when 'appstore', 'store' then 'IOS_APP_STORE'
|
|
692
|
+
when 'development', 'dev' then 'IOS_APP_DEVELOPMENT'
|
|
693
|
+
when 'adhoc' then 'IOS_APP_ADHOC'
|
|
694
|
+
else profile_type
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
begin
|
|
698
|
+
# First, ensure resources are synced
|
|
699
|
+
say " Syncing organization resources...", :cyan
|
|
700
|
+
client.post("/api/v1/organizations/#{config.current_organization_id}/sync_app_store_connect")
|
|
701
|
+
|
|
702
|
+
# Wait a bit for sync to complete
|
|
703
|
+
sleep 2
|
|
704
|
+
|
|
705
|
+
# Check sync status
|
|
706
|
+
max_wait = 15 # seconds
|
|
707
|
+
waited = 0
|
|
708
|
+
sync_complete = false
|
|
709
|
+
|
|
710
|
+
while waited < max_wait
|
|
711
|
+
status_response = client.get("/api/v1/organizations/#{config.current_organization_id}/sync/status")
|
|
712
|
+
sync_data = status_response[:data]['sync']
|
|
713
|
+
|
|
714
|
+
if !sync_data['running']
|
|
715
|
+
sync_complete = true
|
|
716
|
+
break
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
sleep 1
|
|
720
|
+
waited += 1
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
if sync_complete
|
|
724
|
+
say " ✓ Sync complete", :green
|
|
725
|
+
else
|
|
726
|
+
say " ⚠️ Sync still running, continuing anyway...", :yellow
|
|
727
|
+
end
|
|
728
|
+
say ""
|
|
729
|
+
|
|
730
|
+
# Create profile
|
|
731
|
+
say " Creating #{apple_profile_type} profile...", :cyan
|
|
732
|
+
response = client.post(
|
|
733
|
+
"/api/v1/organizations/#{config.current_organization_id}/profiles/auto_create",
|
|
734
|
+
body: {
|
|
735
|
+
bundle_id: bundle_id,
|
|
736
|
+
profile_type: apple_profile_type
|
|
737
|
+
}
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
if response[:success]
|
|
741
|
+
profile = response[:data]['profile']
|
|
742
|
+
say " ✓ Created profile: #{profile['name']}", :green
|
|
743
|
+
say " UUID: #{profile['uuid']}", :cyan
|
|
744
|
+
say " Expires: #{profile['expires_at']}", :cyan
|
|
745
|
+
say ""
|
|
746
|
+
|
|
747
|
+
# Download and install the profile using direct Faraday for binary data
|
|
748
|
+
say " Downloading profile...", :cyan
|
|
749
|
+
download_url = "/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile['id']}/download"
|
|
750
|
+
|
|
751
|
+
conn = Faraday.new(url: config.api_url) do |f|
|
|
752
|
+
f.request :authorization, 'Bearer', config.api_token
|
|
753
|
+
f.adapter Faraday.default_adapter
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
download_response = conn.get(download_url) do |req|
|
|
757
|
+
req.options.timeout = 30
|
|
758
|
+
req.options.open_timeout = 10
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
if download_response.success?
|
|
762
|
+
# Install to Xcode's provisioning profiles directory
|
|
763
|
+
profiles_dir = File.expand_path("~/Library/MobileDevice/Provisioning Profiles")
|
|
764
|
+
FileUtils.mkdir_p(profiles_dir)
|
|
765
|
+
profile_path = File.join(profiles_dir, "#{profile['uuid']}.mobileprovision")
|
|
766
|
+
File.binwrite(profile_path, download_response.body)
|
|
767
|
+
|
|
768
|
+
say " ✓ Profile installed to Xcode", :green
|
|
769
|
+
else
|
|
770
|
+
say " ⚠️ Could not download profile: HTTP #{download_response.status}", :yellow
|
|
771
|
+
end
|
|
772
|
+
say ""
|
|
773
|
+
true
|
|
774
|
+
else
|
|
775
|
+
say " ✗ Failed to create profile", :red
|
|
776
|
+
false
|
|
777
|
+
end
|
|
778
|
+
rescue Mysigner::ClientError => e
|
|
779
|
+
error_msg = e.message
|
|
780
|
+
|
|
781
|
+
if error_msg.include?("bundle_id_not_found")
|
|
782
|
+
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
|
|
786
|
+
say " 2. Register bundle ID: #{bundle_id}", :cyan
|
|
787
|
+
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
|
+
|
|
792
|
+
say " ✗ No #{cert_type} certificates found", :red
|
|
793
|
+
say ""
|
|
794
|
+
|
|
795
|
+
# Offer to generate CSR automatically
|
|
796
|
+
if yes_with_default?("Generate CSR and get step-by-step guide?", :green)
|
|
797
|
+
say ""
|
|
798
|
+
csr_path = generate_csr(config.user_email)
|
|
799
|
+
|
|
800
|
+
if csr_path
|
|
801
|
+
say ""
|
|
802
|
+
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
|
|
806
|
+
say " 2. Select: '#{cert_name}'", :green
|
|
807
|
+
say " 3. Upload CSR: #{csr_path}", :green
|
|
808
|
+
say " 4. Download .cer file and double-click to install", :green
|
|
809
|
+
say " 5. Sync in My Signer → Run 'mysigner doctor' again", :green
|
|
810
|
+
say ""
|
|
811
|
+
end
|
|
812
|
+
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
|
|
817
|
+
say " 3. Select '#{cert_name}' → Upload CSR → Download .cer", :green
|
|
818
|
+
say " 4. Double-click .cer to install → Sync My Signer", :green
|
|
819
|
+
say ""
|
|
820
|
+
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
|
|
827
|
+
say " • Or add in: #{client.api_url}/organizations/#{config.current_organization_id}", :green
|
|
828
|
+
say ""
|
|
829
|
+
else
|
|
830
|
+
say " ✗ Failed: #{error_msg}", :red
|
|
831
|
+
end
|
|
832
|
+
say ""
|
|
833
|
+
false
|
|
834
|
+
rescue => e
|
|
835
|
+
say " ✗ Unexpected error: #{e.message}", :red
|
|
836
|
+
say ""
|
|
837
|
+
false
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
desc "sync [PLATFORM]", "🔄 Sync data from App Store Connect or Google Play"
|
|
843
|
+
long_desc <<~DESC
|
|
844
|
+
Sync your organization's data from app stores.
|
|
845
|
+
|
|
846
|
+
PLATFORMS (optional):
|
|
847
|
+
ios : Sync from App Store Connect (default)
|
|
848
|
+
android : Sync from Google Play
|
|
849
|
+
all : Sync from both platforms
|
|
850
|
+
|
|
851
|
+
Without a platform argument, syncs iOS (App Store Connect) data.
|
|
852
|
+
|
|
853
|
+
EXAMPLES:
|
|
854
|
+
mysigner sync # Sync iOS data
|
|
855
|
+
mysigner sync ios # Sync iOS data
|
|
856
|
+
mysigner sync android # Sync Android data
|
|
857
|
+
mysigner sync all # Sync both platforms
|
|
858
|
+
DESC
|
|
859
|
+
option :force, type: :boolean, aliases: '-f', desc: 'Force sync even if recently synced'
|
|
860
|
+
def sync(platform = 'ios')
|
|
861
|
+
config = load_config
|
|
862
|
+
client = create_client(config)
|
|
863
|
+
|
|
864
|
+
platform = platform.to_s.downcase
|
|
865
|
+
|
|
866
|
+
case platform
|
|
867
|
+
when 'ios', 'apple', 'appstore'
|
|
868
|
+
sync_ios(client, config)
|
|
869
|
+
when 'android', 'google', 'googleplay', 'play'
|
|
870
|
+
sync_android(client, config)
|
|
871
|
+
when 'all', 'both'
|
|
872
|
+
sync_ios(client, config)
|
|
873
|
+
say ""
|
|
874
|
+
sync_android(client, config)
|
|
875
|
+
else
|
|
876
|
+
error "Unknown platform: #{platform}"
|
|
877
|
+
say "Valid platforms: ios, android, all", :yellow
|
|
878
|
+
exit 1
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
no_commands do
|
|
883
|
+
def sync_ios(client, config)
|
|
884
|
+
say "🔄 Syncing data from App Store Connect...", :cyan
|
|
885
|
+
say ""
|
|
886
|
+
|
|
887
|
+
begin
|
|
888
|
+
response = client.post(
|
|
889
|
+
"/api/v1/organizations/#{config.current_organization_id}/sync",
|
|
890
|
+
body: { force: options[:force] }
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
if response[:success]
|
|
894
|
+
data = response[:data]
|
|
895
|
+
say "✓ iOS sync completed!", :green
|
|
896
|
+
say ""
|
|
897
|
+
|
|
898
|
+
if data['synced_at']
|
|
899
|
+
say "Last synced: #{data['synced_at']}", :cyan
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
if data['summary']
|
|
903
|
+
say ""
|
|
904
|
+
say "📊 Summary:", :cyan
|
|
905
|
+
summary = data['summary']
|
|
906
|
+
say " • Apps: #{summary['apps']}" if summary['apps']
|
|
907
|
+
say " • Builds: #{summary['builds']}" if summary['builds']
|
|
908
|
+
say " • Certificates: #{summary['certificates']}" if summary['certificates']
|
|
909
|
+
say " • Devices: #{summary['devices']}" if summary['devices']
|
|
910
|
+
say " • Profiles: #{summary['profiles']}" if summary['profiles']
|
|
911
|
+
end
|
|
912
|
+
else
|
|
913
|
+
say "✗ iOS sync failed: #{response[:error]}", :red
|
|
914
|
+
end
|
|
915
|
+
rescue => e
|
|
916
|
+
say "✗ iOS sync failed: #{e.message}", :red
|
|
917
|
+
end
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
def sync_android(client, config)
|
|
921
|
+
say "🔄 Syncing data from Google Play...", :cyan
|
|
922
|
+
say ""
|
|
923
|
+
|
|
924
|
+
begin
|
|
925
|
+
response = client.post(
|
|
926
|
+
"/api/v1/organizations/#{config.current_organization_id}/sync_google_play",
|
|
927
|
+
body: { force: options[:force] }
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
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
|
+
|
|
937
|
+
# Optionally wait and show status
|
|
938
|
+
say "💡 Google Play sync may take a few minutes.", :yellow
|
|
939
|
+
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
|
|
941
|
+
else
|
|
942
|
+
say "✗ Android sync failed: #{response[:error] || 'Unknown error'}", :red
|
|
943
|
+
end
|
|
944
|
+
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
|
|
950
|
+
else
|
|
951
|
+
say "✗ Android sync failed: #{e.message}", :red
|
|
952
|
+
end
|
|
953
|
+
rescue => e
|
|
954
|
+
say "✗ Android sync failed: #{e.message}", :red
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
# Detect valid JAVA_HOME using macOS java_home utility or common paths
|
|
959
|
+
def detect_java_home(version: nil)
|
|
960
|
+
# Try macOS java_home utility first (most reliable)
|
|
961
|
+
if system('which /usr/libexec/java_home > /dev/null 2>&1')
|
|
962
|
+
cmd = '/usr/libexec/java_home'
|
|
963
|
+
cmd += " -v #{version}" if version
|
|
964
|
+
java_home = `#{cmd} 2>/dev/null`.strip
|
|
965
|
+
return java_home if !java_home.empty? && Dir.exist?(java_home)
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
# Try common Homebrew paths (Apple Silicon)
|
|
969
|
+
homebrew_paths = %w[
|
|
970
|
+
/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
|
|
971
|
+
/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
|
|
972
|
+
/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home
|
|
973
|
+
]
|
|
974
|
+
homebrew_paths.each do |path|
|
|
975
|
+
return path if Dir.exist?(path)
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Try common Homebrew paths (Intel)
|
|
979
|
+
intel_paths = %w[
|
|
980
|
+
/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
|
|
981
|
+
/usr/local/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
|
|
982
|
+
/usr/local/opt/openjdk/libexec/openjdk.jdk/Contents/Home
|
|
983
|
+
]
|
|
984
|
+
intel_paths.each do |path|
|
|
985
|
+
return path if Dir.exist?(path)
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
# Try system Java
|
|
989
|
+
system_paths = Dir.glob('/Library/Java/JavaVirtualMachines/*/Contents/Home')
|
|
990
|
+
return system_paths.first if system_paths.any?
|
|
991
|
+
|
|
992
|
+
nil
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
# Fix JAVA_HOME in shell config
|
|
996
|
+
def fix_java_home(java_home)
|
|
997
|
+
shell_config = File.expand_path('~/.zshrc')
|
|
998
|
+
|
|
999
|
+
# 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
|
|
1003
|
+
|
|
1004
|
+
# Read existing content
|
|
1005
|
+
content = File.exist?(shell_config) ? File.read(shell_config) : ""
|
|
1006
|
+
|
|
1007
|
+
# Check if JAVA_HOME is already set
|
|
1008
|
+
if content.include?('export JAVA_HOME=')
|
|
1009
|
+
# Replace existing JAVA_HOME line
|
|
1010
|
+
new_content = content.gsub(/^export JAVA_HOME=.*$/, "export JAVA_HOME=\"#{java_home}\"")
|
|
1011
|
+
File.write(shell_config, new_content)
|
|
1012
|
+
say " ✓ Updated JAVA_HOME in #{shell_config}", :green
|
|
1013
|
+
else
|
|
1014
|
+
# Append JAVA_HOME
|
|
1015
|
+
File.open(shell_config, 'a') do |f|
|
|
1016
|
+
f.puts ""
|
|
1017
|
+
f.puts "# Added by mysigner doctor"
|
|
1018
|
+
f.puts "export JAVA_HOME=\"#{java_home}\""
|
|
1019
|
+
end
|
|
1020
|
+
say " ✓ Added JAVA_HOME to #{shell_config}", :green
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
say ""
|
|
1024
|
+
say " To apply now, run:", :yellow
|
|
1025
|
+
say " source #{shell_config}", :cyan
|
|
1026
|
+
say ""
|
|
1027
|
+
say " Or restart your terminal.", :yellow
|
|
1028
|
+
end
|
|
1029
|
+
end
|
|
1030
|
+
end
|
|
1031
|
+
end
|
|
1032
|
+
end
|
|
1033
|
+
end
|
|
1034
|
+
end
|