mysigner 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) 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/.rubocop.yml +55 -0
  6. data/.rubocop_todo.yml +112 -0
  7. data/CHANGELOG.md +96 -0
  8. data/Gemfile +5 -3
  9. data/Gemfile.lock +38 -8
  10. data/README.md +13 -15
  11. data/Rakefile +5 -3
  12. data/bin/console +4 -3
  13. data/bin/setup +3 -0
  14. data/exe/mysigner +2 -1
  15. data/lib/mysigner/build/android_executor.rb +46 -52
  16. data/lib/mysigner/build/android_parser.rb +33 -40
  17. data/lib/mysigner/build/configurator.rb +17 -16
  18. data/lib/mysigner/build/detector.rb +39 -50
  19. data/lib/mysigner/build/error_analyzer.rb +70 -68
  20. data/lib/mysigner/build/executor.rb +30 -37
  21. data/lib/mysigner/build/parser.rb +18 -18
  22. data/lib/mysigner/cli/auth_commands.rb +735 -752
  23. data/lib/mysigner/cli/build_commands.rb +697 -721
  24. data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
  25. data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
  26. data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
  27. data/lib/mysigner/cli/concerns/helpers.rb +12 -1
  28. data/lib/mysigner/cli/diagnostic_commands.rb +659 -635
  29. data/lib/mysigner/cli/resource_commands.rb +880 -902
  30. data/lib/mysigner/cli/validate_commands.rb +25 -25
  31. data/lib/mysigner/cli.rb +3 -1
  32. data/lib/mysigner/client.rb +27 -19
  33. data/lib/mysigner/config.rb +93 -56
  34. data/lib/mysigner/export/exporter.rb +32 -36
  35. data/lib/mysigner/signing/certificate_checker.rb +18 -23
  36. data/lib/mysigner/signing/keystore_manager.rb +34 -39
  37. data/lib/mysigner/signing/validator.rb +38 -40
  38. data/lib/mysigner/signing/wizard.rb +329 -342
  39. data/lib/mysigner/upload/app_store_automation.rb +51 -49
  40. data/lib/mysigner/upload/app_store_submission.rb +87 -92
  41. data/lib/mysigner/upload/play_store_uploader.rb +98 -115
  42. data/lib/mysigner/upload/uploader.rb +101 -109
  43. data/lib/mysigner/version.rb +3 -1
  44. data/lib/mysigner.rb +13 -11
  45. data/mysigner.gemspec +36 -33
  46. data/test_manual.rb +37 -36
  47. metadata +37 -16
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mysigner
2
4
  class CLI < Thor
3
5
  module ValidateCommands
4
6
  def self.included(base)
5
7
  base.class_eval do
6
- desc "validate", "Validate signing configuration on the server"
8
+ desc 'validate', 'Validate signing configuration on the server'
7
9
  long_desc <<~DESC
8
10
  Check if your bundle ID, certificate, and provisioning profile exist and
9
11
  are valid on the My Signer server.
@@ -42,15 +44,15 @@ module Mysigner
42
44
  signing_type = options[:type]
43
45
 
44
46
  unless bundle_id
45
- error "Bundle ID is required. Use --bundle-id or run from an Xcode project directory."
46
- say ""
47
- say "Example: mysigner validate --bundle-id com.example.app --type development", :yellow
47
+ error 'Bundle ID is required. Use --bundle-id or run from an Xcode project directory.'
48
+ say ''
49
+ say 'Example: mysigner validate --bundle-id com.example.app --type development', :yellow
48
50
  exit 1
49
51
  end
50
52
 
51
53
  unless signing_type
52
- error "Signing type is required. Use --type with one of: development, appstore, adhoc, inhouse"
53
- say ""
54
+ error 'Signing type is required. Use --type with one of: development, appstore, adhoc, inhouse'
55
+ say ''
54
56
  say "Example: mysigner validate --bundle-id #{bundle_id} --type development", :yellow
55
57
  exit 1
56
58
  end
@@ -62,11 +64,11 @@ module Mysigner
62
64
  exit 1
63
65
  end
64
66
 
65
- say "🔍 Validating signing configuration...", :cyan
66
- say ""
67
+ say '🔍 Validating signing configuration...', :cyan
68
+ say ''
67
69
  say " Bundle ID: #{bundle_id}", :white
68
70
  say " Type: #{signing_type}", :white
69
- say ""
71
+ say ''
70
72
 
71
73
  begin
72
74
  response = client.post(
@@ -93,17 +95,17 @@ module Mysigner
93
95
  end
94
96
  end
95
97
 
96
- say ""
98
+ say ''
97
99
 
98
100
  if valid
99
- say "✓ All checks passed! Signing configuration is valid.", :green
101
+ say '✓ All checks passed! Signing configuration is valid.', :green
100
102
  else
101
- say "✗ Validation failed. Some checks did not pass.", :red
103
+ say '✗ Validation failed. Some checks did not pass.', :red
102
104
 
103
105
  suggestions = result['suggestions'] || []
104
106
  if suggestions.any?
105
- say ""
106
- say "💡 Suggestions:", :cyan
107
+ say ''
108
+ say '💡 Suggestions:', :cyan
107
109
  suggestions.each do |suggestion|
108
110
  say " → #{suggestion}", :yellow
109
111
  end
@@ -113,26 +115,24 @@ module Mysigner
113
115
  end
114
116
  rescue Mysigner::NotFoundError => e
115
117
  error "Not found: #{e.message}"
116
- say ""
117
- say "💡 Make sure your bundle ID is synced:", :cyan
118
+ say ''
119
+ say '💡 Make sure your bundle ID is synced:', :cyan
118
120
  say " → Run 'mysigner sync ios' to sync from Apple Developer Portal", :yellow
119
121
  say " → Run 'mysigner bundleid list' to list registered bundle IDs", :yellow
120
122
  exit 1
121
123
  rescue Mysigner::ValidationError => e
122
124
  error "Validation error: #{e.message}"
123
- if e.details
124
- e.details.each do |field, errors|
125
- errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
126
- say " #{field}: #{errors_text}", :red
127
- end
125
+ e.details&.each do |field, errors|
126
+ errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
127
+ say " #{field}: #{errors_text}", :red
128
128
  end
129
129
  exit 1
130
130
  rescue Mysigner::ClientError => e
131
131
  error "Validation request failed: #{e.message}"
132
- say ""
133
- say "💡 Try these steps:", :cyan
134
- say " → Check your network connection", :yellow
135
- say " → Verify API token: mysigner status", :yellow
132
+ say ''
133
+ say '💡 Try these steps:', :cyan
134
+ say ' → Check your network connection', :yellow
135
+ say ' → Verify API token: mysigner status', :yellow
136
136
  exit 1
137
137
  end
138
138
  end
data/lib/mysigner/cli.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'json'
3
5
  require 'time'
@@ -42,4 +44,4 @@ module Mysigner
42
44
  map 'st' => :status
43
45
  map 'd' => :doctor
44
46
  end
45
- end
47
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'faraday'
2
4
  require 'faraday/retry'
3
5
  require 'json'
@@ -57,7 +59,7 @@ module Mysigner
57
59
  get('/api/v1/status')
58
60
  rescue ClientError => e
59
61
  raise e
60
- rescue => e
62
+ rescue StandardError => e
61
63
  raise ConnectionError, "Failed to connect: #{e.message}"
62
64
  end
63
65
 
@@ -69,9 +71,7 @@ module Mysigner
69
71
  f.request :json
70
72
 
71
73
  # Add X-User-Email header if email is present
72
- if @user_email
73
- f.headers['X-User-Email'] = @user_email
74
- end
74
+ f.headers['X-User-Email'] = @user_email if @user_email
75
75
 
76
76
  # Retry failed requests
77
77
  f.request :retry, {
@@ -80,7 +80,7 @@ module Mysigner
80
80
  interval_randomness: 0.5,
81
81
  backoff_factor: 2,
82
82
  retry_statuses: [429, 502, 503, 504],
83
- methods: [:get, :post, :patch, :delete]
83
+ methods: %i[get post patch delete]
84
84
  }
85
85
 
86
86
  # Response middleware
@@ -120,21 +120,28 @@ module Mysigner
120
120
 
121
121
  case response.status
122
122
  when 401
123
- raise UnauthorizedError.new("Unauthorized: #{error_message}", error_code: error_code, suggestion: suggestion, details: error_data['details'], timestamp: timestamp)
123
+ raise UnauthorizedError.new("Unauthorized: #{error_message}", error_code: error_code, suggestion: suggestion,
124
+ details: error_data['details'], timestamp: timestamp)
124
125
  when 403
125
- raise ForbiddenError.new("Forbidden: #{error_message}", error_code: error_code, suggestion: suggestion, details: error_data['details'], timestamp: timestamp)
126
+ raise ForbiddenError.new("Forbidden: #{error_message}", error_code: error_code, suggestion: suggestion,
127
+ details: error_data['details'], timestamp: timestamp)
126
128
  when 404
127
- raise NotFoundError.new("Not found: #{error_message}", error_code: error_code, suggestion: suggestion, details: error_data['details'], timestamp: timestamp)
129
+ raise NotFoundError.new("Not found: #{error_message}", error_code: error_code, suggestion: suggestion,
130
+ details: error_data['details'], timestamp: timestamp)
128
131
  when 409
129
- raise ValidationError.new(error_message, error_data['details'], suggestion: suggestion, error_code: error_code, timestamp: timestamp)
132
+ raise ValidationError.new(error_message, error_data['details'], suggestion: suggestion, error_code: error_code,
133
+ timestamp: timestamp)
130
134
  when 422
131
- raise ValidationError.new(error_message, error_data['details'], suggestion: suggestion, error_code: error_code, timestamp: timestamp)
135
+ raise ValidationError.new(error_message, error_data['details'], suggestion: suggestion, error_code: error_code,
136
+ timestamp: timestamp)
132
137
  when 429
133
138
  raise RateLimitError.new(error_message, error_data['retry_after'])
134
139
  when 500..599
135
- raise ServerError.new("Server error (#{response.status}): #{error_message}", error_code: error_code, timestamp: timestamp)
140
+ raise ServerError.new("Server error (#{response.status}): #{error_message}", error_code: error_code,
141
+ timestamp: timestamp)
136
142
  else
137
- raise ClientError.new("Request failed (#{response.status}): #{error_message}", error_code: error_code, timestamp: timestamp)
143
+ raise ClientError.new("Request failed (#{response.status}): #{error_message}", error_code: error_code,
144
+ timestamp: timestamp)
138
145
  end
139
146
  end
140
147
 
@@ -146,15 +153,16 @@ module Mysigner
146
153
  # Check if it's a wrapped timeout error
147
154
  if error.wrapped_exception.is_a?(Net::OpenTimeout) || error.wrapped_exception.is_a?(Net::ReadTimeout)
148
155
  raise TimeoutError, "Request timeout: #{error.message}"
149
- else
150
- raise ConnectionError, "Connection failed: #{error.message}"
151
156
  end
157
+
158
+ raise ConnectionError, "Connection failed: #{error.message}"
159
+
152
160
  when Faraday::UnauthorizedError
153
- raise UnauthorizedError, "Invalid or missing API token"
161
+ raise UnauthorizedError, 'Invalid or missing API token'
154
162
  when Faraday::ForbiddenError
155
- raise ForbiddenError, "Access forbidden"
163
+ raise ForbiddenError, 'Access forbidden'
156
164
  when Faraday::ResourceNotFound
157
- raise NotFoundError, "Resource not found"
165
+ raise NotFoundError, 'Resource not found'
158
166
  when Faraday::ClientError
159
167
  raise ClientError, "Client error: #{error.message}"
160
168
  when Faraday::ServerError
@@ -177,13 +185,14 @@ module Mysigner
177
185
  @timestamp = timestamp
178
186
  end
179
187
  end
188
+
180
189
  class ConnectionError < ClientError; end
181
190
  class TimeoutError < ClientError; end
182
191
  class UnauthorizedError < ClientError; end
183
192
  class ForbiddenError < ClientError; end
184
193
  class NotFoundError < ClientError; end
185
194
  class ServerError < ClientError; end
186
-
195
+
187
196
  class ValidationError < ClientError
188
197
  def initialize(message, details = nil, suggestion: nil, error_code: nil, timestamp: nil)
189
198
  super(message, error_code: error_code, suggestion: suggestion, details: details, timestamp: timestamp)
@@ -199,4 +208,3 @@ module Mysigner
199
208
  end
200
209
  end
201
210
  end
202
-
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
1
5
  require 'yaml'
2
6
  require 'fileutils'
3
7
  require 'openssl'
@@ -7,33 +11,68 @@ require 'securerandom'
7
11
 
8
12
  module Mysigner
9
13
  class Config
10
- CONFIG_DIR = File.expand_path("~/.mysigner").freeze
11
- CONFIG_FILE = File.join(CONFIG_DIR, "config.yml").freeze
12
- KEYCHAIN_SERVICE = "com.mysigner.cli".freeze
13
- KEYCHAIN_ACCOUNT = "config_encryption_key".freeze
14
-
15
- attr_accessor :api_url, :user_email, :current_organization_id
14
+ CONFIG_DIR = File.expand_path('~/.mysigner').freeze
15
+ CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml').freeze
16
+ KEYCHAIN_SERVICE = 'com.mysigner.cli'
17
+ KEYCHAIN_ACCOUNT = 'config_encryption_key'
18
+
19
+ # Environment variable names for CI/CD support
20
+ ENV_API_TOKEN = 'MYSIGNER_API_TOKEN'
21
+ ENV_API_URL = 'MYSIGNER_API_URL'
22
+ ENV_EMAIL = 'MYSIGNER_EMAIL'
23
+ ENV_ORG_ID = 'MYSIGNER_ORG_ID'
24
+
25
+ attr_accessor :api_url, :user_email, :current_organization_id, :encryption_enabled
16
26
  attr_reader :organizations
17
- attr_accessor :encryption_enabled
18
27
 
19
28
  def initialize
20
29
  @api_url = nil
21
30
  @user_email = nil
22
31
  @current_organization_id = nil
23
32
  @organizations = {}
24
- @encryption_enabled = true # Enable by default for security
33
+ @encryption_enabled = true # Enable by default for security
34
+ @from_env = false
25
35
  load if exists?
26
36
  end
27
37
 
38
+ # Check if all required env vars are set for CI/CD mode
39
+ def self.env_configured?
40
+ ENV.fetch(ENV_API_TOKEN, nil) && !ENV[ENV_API_TOKEN].empty? &&
41
+ ENV.fetch(ENV_ORG_ID, nil) && !ENV[ENV_ORG_ID].empty?
42
+ end
43
+
44
+ # Create a Config from environment variables (for CI/CD)
45
+ def self.from_env
46
+ config = allocate
47
+ config.instance_variable_set(:@encryption_enabled, false)
48
+ config.instance_variable_set(:@from_env, true)
49
+
50
+ org_id = ENV.fetch(ENV_ORG_ID, nil)
51
+ token = ENV.fetch(ENV_API_TOKEN, nil)
52
+ config.instance_variable_set(:@api_url, ENV[ENV_API_URL] || 'https://mysigner.dev')
53
+ config.instance_variable_set(:@user_email, ENV.fetch(ENV_EMAIL, nil))
54
+ config.instance_variable_set(:@current_organization_id, org_id.to_i)
55
+ config.instance_variable_set(:@organizations, {
56
+ org_id.to_s => { 'name' => 'CI', 'token' => token }
57
+ })
58
+
59
+ config
60
+ end
61
+
62
+ # Whether this config was loaded from environment variables
63
+ def from_env?
64
+ @from_env
65
+ end
66
+
28
67
  # Get API token for current organization (or specific org)
29
68
  def api_token(org_id = nil)
30
69
  org_id ||= @current_organization_id
31
70
  return nil if org_id.nil?
32
-
71
+
33
72
  org_data = @organizations[org_id.to_s]
34
73
  token = org_data&.dig('token')
35
74
  return nil if token.nil?
36
-
75
+
37
76
  # Decrypt if encrypted
38
77
  encrypted?(token) ? decrypt_token(token) : token
39
78
  end
@@ -57,7 +96,7 @@ module Mysigner
57
96
  def org_name(org_id = nil)
58
97
  org_id ||= @current_organization_id
59
98
  return nil if org_id.nil?
60
-
99
+
61
100
  org_data = @organizations[org_id.to_s]
62
101
  org_data&.dig('name')
63
102
  end
@@ -81,17 +120,17 @@ module Mysigner
81
120
  return false unless exists?
82
121
 
83
122
  data = YAML.load_file(CONFIG_FILE)
84
-
123
+
85
124
  @api_url = data['api_url']
86
125
  @user_email = data['user_email']
87
126
  @current_organization_id = data['current_organization_id']
88
127
  @organizations = data['organizations'] || {}
89
-
128
+
90
129
  # Auto-detect encryption from config
91
130
  @encryption_enabled = encrypted_config?
92
-
131
+
93
132
  true
94
- rescue => e
133
+ rescue StandardError => e
95
134
  raise ConfigError, "Failed to load config: #{e.message}"
96
135
  end
97
136
 
@@ -107,9 +146,9 @@ module Mysigner
107
146
  }
108
147
 
109
148
  File.write(CONFIG_FILE, data.to_yaml)
110
- File.chmod(0600, CONFIG_FILE) # Make file readable only by owner
149
+ File.chmod(0o600, CONFIG_FILE) # Make file readable only by owner
111
150
  true
112
- rescue => e
151
+ rescue StandardError => e
113
152
  raise ConfigError, "Failed to save config: #{e.message}"
114
153
  end
115
154
 
@@ -119,12 +158,10 @@ module Mysigner
119
158
  @user_email = nil
120
159
  @current_organization_id = nil
121
160
  @organizations = {}
122
-
123
- if exists?
124
- File.delete(CONFIG_FILE)
125
- end
161
+
162
+ File.delete(CONFIG_FILE) if exists?
126
163
  true
127
- rescue => e
164
+ rescue StandardError => e
128
165
  raise ConfigError, "Failed to clear config: #{e.message}"
129
166
  end
130
167
 
@@ -136,8 +173,8 @@ module Mysigner
136
173
  # Check if configuration is complete (has required fields)
137
174
  def valid?
138
175
  !@api_url.nil? && !@api_url.empty? &&
139
- !@current_organization_id.nil? &&
140
- has_token_for_org?(@current_organization_id)
176
+ !@current_organization_id.nil? &&
177
+ has_token_for_org?(@current_organization_id)
141
178
  end
142
179
 
143
180
  # Get config as hash
@@ -153,14 +190,14 @@ module Mysigner
153
190
  def display
154
191
  current_org_name = org_name(@current_organization_id) || '(not set)'
155
192
  current_token = api_token(@current_organization_id)
156
-
193
+
157
194
  display_data = {
158
195
  api_url: @api_url || '(not set)',
159
196
  user_email: @user_email || '(not set)',
160
197
  current_organization: "#{current_org_name} (ID: #{@current_organization_id || 'not set'})",
161
198
  current_token: current_token ? mask_token(current_token) : '(not set)'
162
199
  }
163
-
200
+
164
201
  # Show all organizations
165
202
  if @organizations.any?
166
203
  display_data[:all_organizations] = @organizations.map do |org_id, org_data|
@@ -168,24 +205,24 @@ module Mysigner
168
205
  "#{org_data['name']} (ID: #{org_id}) #{token_status}"
169
206
  end.join(', ')
170
207
  end
171
-
208
+
172
209
  display_data
173
210
  end
174
211
 
175
212
  # Enable encryption and re-encrypt all tokens
176
213
  def enable_encryption!
177
214
  return true if @encryption_enabled
178
-
215
+
179
216
  @encryption_enabled = true
180
-
217
+
181
218
  # Re-encrypt all existing tokens
182
- @organizations.each do |org_id, org_data|
219
+ @organizations.each_value do |org_data|
183
220
  token = org_data['token']
184
221
  next if token.nil? || encrypted?(token)
185
-
222
+
186
223
  org_data['token'] = encrypt_token(token)
187
224
  end
188
-
225
+
189
226
  save
190
227
  true
191
228
  end
@@ -193,15 +230,15 @@ module Mysigner
193
230
  # Disable encryption and decrypt all tokens
194
231
  def disable_encryption!
195
232
  return true unless @encryption_enabled
196
-
233
+
197
234
  # Decrypt all tokens first
198
- @organizations.each do |org_id, org_data|
235
+ @organizations.each_value do |org_data|
199
236
  token = org_data['token']
200
237
  next if token.nil? || !encrypted?(token)
201
-
238
+
202
239
  org_data['token'] = decrypt_token(token)
203
240
  end
204
-
241
+
205
242
  @encryption_enabled = false
206
243
  save
207
244
  true
@@ -215,12 +252,13 @@ module Mysigner
215
252
  private
216
253
 
217
254
  def ensure_config_dir_exists
218
- FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
255
+ FileUtils.mkdir_p(CONFIG_DIR)
219
256
  end
220
257
 
221
258
  def mask_token(token)
222
259
  return token if token.length < 8
223
- "#{token[0..3]}...#{token[-4..-1]}"
260
+
261
+ "#{token[0..3]}...#{token[-4..]}"
224
262
  end
225
263
 
226
264
  # Encryption methods
@@ -230,34 +268,34 @@ module Mysigner
230
268
  cipher.encrypt
231
269
  cipher.key = key
232
270
  iv = cipher.random_iv
233
-
271
+
234
272
  encrypted = cipher.update(token) + cipher.final
235
273
  auth_tag = cipher.auth_tag
236
-
274
+
237
275
  # Format: encrypted:base64(iv):base64(auth_tag):base64(encrypted_data)
238
276
  "encrypted:#{Base64.strict_encode64(iv)}:#{Base64.strict_encode64(auth_tag)}:#{Base64.strict_encode64(encrypted)}"
239
277
  end
240
278
 
241
279
  def decrypt_token(encrypted_token)
242
280
  return encrypted_token unless encrypted?(encrypted_token)
243
-
281
+
244
282
  # Parse format
245
283
  parts = encrypted_token.split(':', 4)
246
284
  return encrypted_token if parts.length != 4 || parts[0] != 'encrypted'
247
-
285
+
248
286
  iv = Base64.strict_decode64(parts[1])
249
287
  auth_tag = Base64.strict_decode64(parts[2])
250
288
  encrypted_data = Base64.strict_decode64(parts[3])
251
-
289
+
252
290
  key = get_or_create_encryption_key
253
291
  decipher = OpenSSL::Cipher.new('aes-256-gcm')
254
292
  decipher.decrypt
255
293
  decipher.key = key
256
294
  decipher.iv = iv
257
295
  decipher.auth_tag = auth_tag
258
-
296
+
259
297
  decipher.update(encrypted_data) + decipher.final
260
- rescue => e
298
+ rescue StandardError => e
261
299
  raise ConfigError, "Failed to decrypt token: #{e.message}"
262
300
  end
263
301
 
@@ -269,7 +307,7 @@ module Mysigner
269
307
  # Try to get key from keychain
270
308
  key = get_key_from_keychain
271
309
  return key if key
272
-
310
+
273
311
  # Generate new key and store in keychain
274
312
  new_key = SecureRandom.bytes(32) # 256-bit key
275
313
  store_key_in_keychain(new_key)
@@ -280,32 +318,31 @@ module Mysigner
280
318
  # Use macOS security command to get key from keychain
281
319
  cmd = "security find-generic-password -s '#{KEYCHAIN_SERVICE}' -a '#{KEYCHAIN_ACCOUNT}' -w 2>/dev/null"
282
320
  result = `#{cmd}`.strip
283
-
284
- return nil if result.empty? || $?.exitstatus != 0
285
-
321
+
322
+ return nil if result.empty? || $CHILD_STATUS.exitstatus != 0
323
+
286
324
  # Decode from base64
287
325
  Base64.strict_decode64(result)
288
- rescue => e
326
+ rescue StandardError
289
327
  nil
290
328
  end
291
329
 
292
330
  def store_key_in_keychain(key)
293
331
  # Encode key as base64
294
332
  encoded_key = Base64.strict_encode64(key)
295
-
333
+
296
334
  # Delete existing key if present
297
335
  `security delete-generic-password -s '#{KEYCHAIN_SERVICE}' -a '#{KEYCHAIN_ACCOUNT}' 2>/dev/null`
298
-
336
+
299
337
  # Add new key to keychain
300
338
  cmd = "security add-generic-password -s '#{KEYCHAIN_SERVICE}' -a '#{KEYCHAIN_ACCOUNT}' -w '#{encoded_key}'"
301
339
  system(cmd)
302
-
303
- $?.exitstatus == 0
304
- rescue => e
340
+
341
+ $CHILD_STATUS.exitstatus.zero?
342
+ rescue StandardError => e
305
343
  raise ConfigError, "Failed to store encryption key in keychain: #{e.message}"
306
344
  end
307
345
  end
308
346
 
309
347
  class ConfigError < StandardError; end
310
348
  end
311
-