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