e2b 0.3.4 → 0.3.5

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.
@@ -15,7 +15,7 @@ module E2B
15
15
  # Connect RPC protocol (gRPC-over-HTTP with JSON encoding).
16
16
  class BaseService
17
17
  # Default envd port
18
- ENVD_PORT = 49983
18
+ ENVD_PORT = 49_983
19
19
  DEFAULT_USERNAME = "user"
20
20
  ENVD_DEFAULT_USER_VERSION = Gem::Version.new("0.4.0")
21
21
  ENVD_RECURSIVE_WATCH_VERSION = Gem::Version.new("0.1.4")
@@ -150,7 +150,7 @@ module E2B
150
150
  end
151
151
  end
152
152
 
153
- def initialize(base_url:, api_key:, access_token: nil, sandbox_id:, logger: nil)
153
+ def initialize(base_url:, api_key:, sandbox_id:, access_token: nil, logger: nil)
154
154
  @base_url = base_url.end_with?("/") ? base_url : "#{base_url}/"
155
155
  @api_key = api_key
156
156
  @access_token = access_token
@@ -203,16 +203,14 @@ module E2B
203
203
 
204
204
  log_debug("RPC #{service}/#{method}")
205
205
 
206
- if on_event
207
- return handle_streaming_rpc(path, envelope, timeout, on_event, headers)
208
- end
206
+ return handle_streaming_rpc(path, envelope, timeout, on_event, headers) if on_event
209
207
 
210
208
  # Unary RPCs: try Connect protocol first, fall back to plain JSON.
211
209
  # Some envd versions (e.g., 0.5.4 on self-hosted) reject
212
210
  # application/connect+json for unary calls but accept application/json.
213
211
  handle_rpc_response(service, method) do
214
212
  with_retry("RPC #{service}/#{method}") do
215
- url = URI.parse("#{@base_url.chomp('/')}#{path}")
213
+ url = URI.parse("#{@base_url.chomp("/")}#{path}")
216
214
  http = build_http(url, timeout)
217
215
 
218
216
  request = Net::HTTP::Post.new(url.request_uri)
@@ -251,17 +249,35 @@ module E2B
251
249
  # inherit proxy configuration and SSL settings. The streaming is handled
252
250
  # via Faraday's on_data callback for chunked response processing.
253
251
  #
254
- # Streaming RPCs are NOT idempotent (e.g. process.Process/Start spawns a
255
- # process), so we only retry while no events have been emitted to the
256
- # caller yet. Once any byte has been delivered via on_event, a retry
257
- # would replay output AND start a second process server-side.
252
+ # Retry policy:
253
+ #
254
+ # * `process.Process/Start` is **never retried**. The POST may have
255
+ # already reached envd and triggered a process spawn before the
256
+ # transport error surfaced; retrying would race a second process
257
+ # against the first. We've seen `git clone` fail with "destination
258
+ # already exists" exactly this way — first attempt spawns a process
259
+ # that mkdir's the target, transport blips, retry spawns process #2
260
+ # that finds the target non-empty. Caller decides what to do.
261
+ #
262
+ # * Other streaming paths (`Connect` re-attach, etc.) retry while no
263
+ # events have been emitted yet. Once any byte has been delivered
264
+ # via on_event, a retry would replay output to the caller, so we
265
+ # abort.
258
266
  def handle_streaming_rpc(path, envelope, timeout, on_event, headers)
259
267
  result = { events: [], stdout: "", stderr: "", exit_code: nil }
260
268
  buffer = "".b
261
269
 
262
270
  full_path = normalize_path(path)
271
+ non_idempotent = path.end_with?("/Start")
263
272
 
264
- with_retry("Streaming RPC #{path}", abort_if: -> { result[:events].any? }) do
273
+ retry_opts =
274
+ if non_idempotent
275
+ { max_retries: 0 }
276
+ else
277
+ { abort_if: -> { result[:events].any? } }
278
+ end
279
+
280
+ with_retry("Streaming RPC #{path}", **retry_opts) do
265
281
  ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
266
282
 
267
283
  streaming_conn = Faraday.new(url: @base_url, ssl: { verify: ssl_verify }) do |conn|
@@ -287,10 +303,11 @@ module E2B
287
303
  req.body = envelope
288
304
  req.options.on_data = proc do |chunk, _overall_size, _env|
289
305
  next if chunk.nil? || chunk.empty?
306
+
290
307
  buffer << chunk
291
308
 
292
309
  while buffer.bytesize >= 5
293
- flags = buffer.getbyte(0)
310
+ buffer.getbyte(0)
294
311
  length = buffer.byteslice(1, 4).unpack1("N")
295
312
 
296
313
  break if length.nil? || buffer.bytesize < 5 + length
@@ -324,7 +341,8 @@ module E2B
324
341
 
325
342
  end_event = event["End"] || event["end"]
326
343
  if end_event
327
- result[:exit_code] = parse_exit_code(end_event["exitCode"] || end_event["exit_code"] || end_event["status"])
344
+ result[:exit_code] =
345
+ parse_exit_code(end_event["exitCode"] || end_event["exit_code"] || end_event["status"])
328
346
  end
329
347
  end
330
348
 
@@ -336,9 +354,7 @@ module E2B
336
354
  stderr_data = EnvdBase64.decode_process_output(msg["stderr"])
337
355
  result[:stderr] += stderr_data
338
356
  end
339
- if msg["exitCode"] || msg["exit_code"]
340
- result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"])
341
- end
357
+ result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"]) if msg["exitCode"] || msg["exit_code"]
342
358
 
343
359
  on_event.call(
344
360
  stdout: stdout_data,
@@ -353,9 +369,7 @@ module E2B
353
369
  end
354
370
  end
355
371
 
356
- unless response.status.between?(200, 299)
357
- handle_error(response)
358
- end
372
+ handle_error(response) unless response.status.between?(200, 299)
359
373
  end
360
374
 
361
375
  result
@@ -410,13 +424,20 @@ module E2B
410
424
  end
411
425
 
412
426
  def resolve_proxy(url)
413
- no_proxy = ENV["no_proxy"] || ENV["NO_PROXY"]
427
+ no_proxy = ENV["no_proxy"] || ENV.fetch("NO_PROXY", nil)
414
428
  if no_proxy
415
429
  no_proxy_hosts = no_proxy.split(",").map(&:strip)
416
430
  return nil if no_proxy_hosts.any? { |h| url.host.end_with?(h) || h == "*" }
417
431
  end
418
432
 
419
- proxy_env = url.scheme == "https" ? (ENV["https_proxy"] || ENV["HTTPS_PROXY"]) : (ENV["http_proxy"] || ENV["HTTP_PROXY"])
433
+ proxy_env = if url.scheme == "https"
434
+ ENV["https_proxy"] || ENV.fetch("HTTPS_PROXY",
435
+ nil)
436
+ else
437
+ ENV["http_proxy"] || ENV.fetch(
438
+ "HTTP_PROXY", nil
439
+ )
440
+ end
420
441
  return nil unless proxy_env
421
442
 
422
443
  URI.parse(proxy_env)
@@ -437,14 +458,12 @@ module E2B
437
458
 
438
459
  retry_count += 1
439
460
 
440
- if retry_count <= max_retries
441
- sleep_time = 2**retry_count
442
- log_debug("#{operation}: retry #{retry_count}/#{max_retries} after #{e.class}: #{e.message}")
443
- sleep(sleep_time)
444
- retry
445
- else
446
- raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}"
447
- end
461
+ raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}" unless retry_count <= max_retries
462
+
463
+ sleep_time = 2**retry_count
464
+ log_debug("#{operation}: retry #{retry_count}/#{max_retries} after #{e.class}: #{e.message}")
465
+ sleep(sleep_time)
466
+ retry
448
467
  end
449
468
  end
450
469
 
@@ -455,7 +474,11 @@ module E2B
455
474
  body = response.body
456
475
 
457
476
  if body.is_a?(String) && !body.empty?
458
- content_type = response.headers["content-type"] rescue "unknown"
477
+ content_type = begin
478
+ response.headers["content-type"]
479
+ rescue StandardError
480
+ "unknown"
481
+ end
459
482
  if content_type&.include?("json") || body.start_with?("{", "[")
460
483
  begin
461
484
  return JSON.parse(body)
@@ -472,7 +495,7 @@ module E2B
472
495
  raise E2B::E2BError, "Connection to sandbox failed: #{e.message}"
473
496
  end
474
497
 
475
- def handle_rpc_response(service, method)
498
+ def handle_rpc_response(_service, _method)
476
499
  response = yield
477
500
 
478
501
  handle_error(response) unless response.success?
@@ -485,36 +508,32 @@ module E2B
485
508
  messages = parse_connect_stream(body)
486
509
 
487
510
  messages.each do |msg_str|
488
- begin
489
- msg = JSON.parse(msg_str)
490
- msg = msg["result"] if msg["result"]
511
+ msg = JSON.parse(msg_str)
512
+ msg = msg["result"] if msg["result"]
491
513
 
492
- result[:events] << msg
514
+ result[:events] << msg
493
515
 
494
- if msg["event"]
495
- event = msg["event"]
516
+ if msg["event"]
517
+ event = msg["event"]
496
518
 
497
- data_event = event["Data"] || event["data"]
498
- if data_event
499
- result[:stdout] += EnvdBase64.decode_process_output(data_event["stdout"]) if data_event["stdout"]
500
- result[:stderr] += EnvdBase64.decode_process_output(data_event["stderr"]) if data_event["stderr"]
501
- end
502
-
503
- end_event = event["End"] || event["end"]
504
- if end_event
505
- exit_value = end_event["exitCode"] || end_event["exit_code"] || end_event["status"]
506
- result[:exit_code] = parse_exit_code(exit_value)
507
- end
519
+ data_event = event["Data"] || event["data"]
520
+ if data_event
521
+ result[:stdout] += EnvdBase64.decode_process_output(data_event["stdout"]) if data_event["stdout"]
522
+ result[:stderr] += EnvdBase64.decode_process_output(data_event["stderr"]) if data_event["stderr"]
508
523
  end
509
524
 
510
- result[:stdout] += EnvdBase64.decode_process_output(msg["stdout"]) if msg["stdout"]
511
- result[:stderr] += EnvdBase64.decode_process_output(msg["stderr"]) if msg["stderr"]
512
- if msg["exitCode"] || msg["exit_code"]
513
- result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"])
525
+ end_event = event["End"] || event["end"]
526
+ if end_event
527
+ exit_value = end_event["exitCode"] || end_event["exit_code"] || end_event["status"]
528
+ result[:exit_code] = parse_exit_code(exit_value)
514
529
  end
515
- rescue JSON::ParserError
516
- # Skip unparseable messages
517
530
  end
531
+
532
+ result[:stdout] += EnvdBase64.decode_process_output(msg["stdout"]) if msg["stdout"]
533
+ result[:stderr] += EnvdBase64.decode_process_output(msg["stderr"]) if msg["stderr"]
534
+ result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"]) if msg["exitCode"] || msg["exit_code"]
535
+ rescue JSON::ParserError
536
+ # Skip unparseable messages
518
537
  end
519
538
 
520
539
  result
@@ -569,9 +588,9 @@ module E2B
569
588
 
570
589
  str = value.to_s
571
590
  if str =~ /exit status (\d+)/i
572
- $1.to_i
591
+ ::Regexp.last_match(1).to_i
573
592
  elsif str =~ /^(\d+)$/
574
- $1.to_i
593
+ ::Regexp.last_match(1).to_i
575
594
  else
576
595
  1
577
596
  end
@@ -189,9 +189,7 @@ module E2B
189
189
  # @raise [CommandExitError] if exit code is non-zero
190
190
  def wait(on_stdout: nil, on_stderr: nil, on_pty: nil)
191
191
  consume_events(on_stdout: on_stdout, on_stderr: on_stderr, on_pty: on_pty)
192
- unless @disconnected || completed?
193
- raise E2BError, "Command ended without an end event"
194
- end
192
+ raise E2BError, "Command ended without an end event" unless @disconnected || completed?
195
193
 
196
194
  build_result.tap do |cmd_result|
197
195
  unless cmd_result.success?
@@ -269,15 +267,9 @@ module E2B
269
267
  return if @finished
270
268
 
271
269
  each do |stdout_chunk, stderr_chunk, pty_chunk|
272
- if stdout_chunk
273
- on_stdout&.call(stdout_chunk)
274
- end
275
- if stderr_chunk
276
- on_stderr&.call(stderr_chunk)
277
- end
278
- if pty_chunk
279
- on_pty&.call(pty_chunk)
280
- end
270
+ on_stdout&.call(stdout_chunk) if stdout_chunk
271
+ on_stderr&.call(stderr_chunk) if stderr_chunk
272
+ on_pty&.call(pty_chunk) if pty_chunk
281
273
  end
282
274
 
283
275
  @finished = true
@@ -306,7 +298,7 @@ module E2B
306
298
  #
307
299
  # @yield [stdout, stderr, pty]
308
300
  # @return [void]
309
- def iterate_materialized_events
301
+ def iterate_materialized_events(&block)
310
302
  events = result_value(:events) || []
311
303
  while @materialized_event_index < events.length
312
304
  break if @disconnected
@@ -314,9 +306,7 @@ module E2B
314
306
  event_hash = events[@materialized_event_index]
315
307
  @materialized_event_index += 1
316
308
 
317
- process_message(event_hash) do |stdout_chunk, stderr_chunk, pty_chunk|
318
- yield stdout_chunk, stderr_chunk, pty_chunk
319
- end
309
+ process_message(event_hash, &block)
320
310
  end
321
311
  end
322
312
 
@@ -327,14 +317,12 @@ module E2B
327
317
  #
328
318
  # @yield [stdout, stderr, pty]
329
319
  # @return [void]
330
- def iterate_streaming_events
320
+ def iterate_streaming_events(&block)
331
321
  catch(:stop_iteration) do
332
322
  @events_proc.call do |event_hash|
333
323
  throw :stop_iteration if @disconnected
334
324
 
335
- process_message(event_hash) do |stdout_chunk, stderr_chunk, pty_chunk|
336
- yield stdout_chunk, stderr_chunk, pty_chunk
337
- end
325
+ process_message(event_hash, &block)
338
326
  end
339
327
  end
340
328
  end
@@ -353,11 +341,11 @@ module E2B
353
341
  # @param message [Hash] A raw stream message
354
342
  # @yield [stdout, stderr, pty]
355
343
  # @return [void]
356
- def process_message(message)
344
+ def process_message(message, &block)
357
345
  return unless message.is_a?(Hash)
358
346
 
359
347
  event = message["event"]
360
- process_event(event) { |stdout_chunk, stderr_chunk, pty_chunk| yield stdout_chunk, stderr_chunk, pty_chunk } if event.is_a?(Hash)
348
+ process_event(event, &block) if event.is_a?(Hash)
361
349
 
362
350
  if event.nil?
363
351
  stdout_chunk = EnvdBase64.decode_process_output(message["stdout"])
@@ -25,6 +25,7 @@ module E2B
25
25
  # handle.kill
26
26
  class Commands < BaseService
27
27
  include LiveStreamable
28
+
28
29
  # Run a command in the sandbox
29
30
  #
30
31
  # @param cmd [String] Command to execute (run via /bin/bash -l -c)
@@ -93,10 +94,10 @@ module E2B
93
94
  end
94
95
 
95
96
  response = envd_rpc("process.Process", "Start",
96
- body: body,
97
- timeout: effective_timeout,
98
- headers: headers,
99
- on_event: streaming_callback)
97
+ body: body,
98
+ timeout: effective_timeout,
99
+ headers: headers,
100
+ on_event: streaming_callback)
100
101
 
101
102
  # Return CommandResult for foreground processes
102
103
  result = build_result(response)
@@ -120,16 +121,15 @@ module E2B
120
121
  # @return [Array<Hash>] List of running processes with pid, config, tag
121
122
  def list(request_timeout: nil)
122
123
  response = envd_rpc("process.Process", "List",
123
- body: {},
124
- timeout: request_timeout || 30)
124
+ body: {},
125
+ timeout: request_timeout || 30)
125
126
 
126
127
  processes = []
127
128
  events = response[:events] || []
128
129
  events.each do |event|
129
130
  next unless event.is_a?(Hash)
130
- if event["processes"]
131
- processes.concat(Array(event["processes"]))
132
- end
131
+
132
+ processes.concat(Array(event["processes"])) if event["processes"]
133
133
  end
134
134
  processes
135
135
  end
@@ -141,12 +141,12 @@ module E2B
141
141
  # @return [Boolean] true if killed, false if not found
142
142
  def kill(pid, request_timeout: nil, headers: nil)
143
143
  envd_rpc("process.Process", "SendSignal",
144
- body: {
145
- process: { pid: pid },
146
- signal: 9 # SIGKILL
147
- },
148
- headers: headers,
149
- timeout: request_timeout || 30)
144
+ body: {
145
+ process: { pid: pid },
146
+ signal: 9 # SIGKILL
147
+ },
148
+ headers: headers,
149
+ timeout: request_timeout || 30)
150
150
  true
151
151
  rescue E2B::NotFoundError
152
152
  false
@@ -166,12 +166,12 @@ module E2B
166
166
  def send_stdin(pid, data, request_timeout: nil, headers: nil)
167
167
  encoded = Base64.strict_encode64(data.to_s)
168
168
  envd_rpc("process.Process", "SendInput",
169
- body: {
170
- process: { pid: pid },
171
- input: { stdin: encoded }
172
- },
173
- headers: headers,
174
- timeout: request_timeout || 30)
169
+ body: {
170
+ process: { pid: pid },
171
+ input: { stdin: encoded }
172
+ },
173
+ headers: headers,
174
+ timeout: request_timeout || 30)
175
175
  end
176
176
 
177
177
  # Close the stdin of a running process.
@@ -185,9 +185,9 @@ module E2B
185
185
  # @raise [E2B::E2BError] if the process is not found
186
186
  def close_stdin(pid, request_timeout: nil, headers: nil)
187
187
  envd_rpc("process.Process", "CloseStdin",
188
- body: { process: { pid: pid } },
189
- headers: headers,
190
- timeout: request_timeout || 30)
188
+ body: { process: { pid: pid } },
189
+ headers: headers,
190
+ timeout: request_timeout || 30)
191
191
  end
192
192
 
193
193
  # Connect to a running process
@@ -222,10 +222,9 @@ module E2B
222
222
  events = response[:events] || []
223
223
  events.each do |event|
224
224
  next unless event.is_a?(Hash) && event["event"]
225
+
225
226
  end_event = event["event"]["End"] || event["event"]["end"]
226
- if end_event
227
- error = end_event["error"] if end_event["error"] && !end_event["error"].empty?
228
- end
227
+ error = end_event["error"] if end_event && end_event["error"] && !end_event["error"].empty?
229
228
  end
230
229
 
231
230
  CommandResult.new(
@@ -99,9 +99,9 @@ module E2B
99
99
  # entries.each { |e| puts "#{e.name} (#{e.type})" }
100
100
  def list(path, depth: 1, user: nil, request_timeout: 60)
101
101
  response = envd_rpc("filesystem.Filesystem", "ListDir",
102
- body: { path: path, depth: depth },
103
- timeout: request_timeout,
104
- headers: user_auth_headers(user))
102
+ body: { path: path, depth: depth },
103
+ timeout: request_timeout,
104
+ headers: user_auth_headers(user))
105
105
 
106
106
  entries = extract_entries(response)
107
107
  entries.map { |e| Models::EntryInfo.from_hash(e) }
@@ -132,9 +132,9 @@ module E2B
132
132
  # @return [Models::EntryInfo] File/directory info
133
133
  def get_info(path, user: nil, request_timeout: 30)
134
134
  response = envd_rpc("filesystem.Filesystem", "Stat",
135
- body: { path: path },
136
- timeout: request_timeout,
137
- headers: user_auth_headers(user))
135
+ body: { path: path },
136
+ timeout: request_timeout,
137
+ headers: user_auth_headers(user))
138
138
 
139
139
  entry_data = extract_entry(response)
140
140
  Models::EntryInfo.from_hash(entry_data)
@@ -147,9 +147,9 @@ module E2B
147
147
  # @param request_timeout [Integer] Request timeout in seconds
148
148
  def remove(path, user: nil, request_timeout: 30)
149
149
  envd_rpc("filesystem.Filesystem", "Remove",
150
- body: { path: path },
151
- timeout: request_timeout,
152
- headers: user_auth_headers(user))
150
+ body: { path: path },
151
+ timeout: request_timeout,
152
+ headers: user_auth_headers(user))
153
153
  end
154
154
 
155
155
  # Rename/move a file or directory
@@ -161,9 +161,9 @@ module E2B
161
161
  # @return [Models::EntryInfo] Info about the moved entry
162
162
  def rename(old_path, new_path, user: nil, request_timeout: 30)
163
163
  response = envd_rpc("filesystem.Filesystem", "Move",
164
- body: { source: old_path, destination: new_path },
165
- timeout: request_timeout,
166
- headers: user_auth_headers(user))
164
+ body: { source: old_path, destination: new_path },
165
+ timeout: request_timeout,
166
+ headers: user_auth_headers(user))
167
167
 
168
168
  entry_data = extract_entry(response)
169
169
  Models::EntryInfo.from_hash(entry_data)
@@ -177,9 +177,9 @@ module E2B
177
177
  # @return [Boolean] true if created successfully
178
178
  def make_dir(path, user: nil, request_timeout: 30)
179
179
  envd_rpc("filesystem.Filesystem", "MakeDir",
180
- body: { path: path },
181
- timeout: request_timeout,
182
- headers: user_auth_headers(user))
180
+ body: { path: path },
181
+ timeout: request_timeout,
182
+ headers: user_auth_headers(user))
183
183
  true
184
184
  end
185
185
 
@@ -202,13 +202,13 @@ module E2B
202
202
  def watch_dir(path, recursive: false, user: nil, request_timeout: 30)
203
203
  if recursive && !supports_recursive_watch?
204
204
  raise E2B::TemplateError,
205
- "You need to update the template to use recursive watching. You can do this by running `e2b template build` in the directory with the template."
205
+ "You need to update the template to use recursive watching. You can do this by running `e2b template build` in the directory with the template."
206
206
  end
207
207
 
208
208
  response = envd_rpc("filesystem.Filesystem", "CreateWatcher",
209
- body: { path: path, recursive: recursive },
210
- timeout: request_timeout,
211
- headers: user_auth_headers(user))
209
+ body: { path: path, recursive: recursive },
210
+ timeout: request_timeout,
211
+ headers: user_auth_headers(user))
212
212
 
213
213
  watcher_id = response[:events]&.first&.dig("watcherId") ||
214
214
  response["watcherId"] ||
@@ -257,9 +257,8 @@ module E2B
257
257
 
258
258
  response = execute_http_request(uri, request, timeout: timeout)
259
259
  unless successful_response?(response)
260
- if response.code.to_i == 404
261
- raise E2B::NotFoundError.new("File not found", status_code: 404)
262
- end
260
+ raise E2B::NotFoundError.new("File not found", status_code: 404) if response.code.to_i == 404
261
+
263
262
  raise E2B::E2BError, "File read failed: HTTP #{response.code}"
264
263
  end
265
264
 
@@ -281,9 +280,7 @@ module E2B
281
280
  apply_request_headers(request)
282
281
 
283
282
  response = execute_http_request(uri, request, timeout: timeout)
284
- unless successful_response?(response)
285
- raise E2B::E2BError, "File upload failed: HTTP #{response.code}"
286
- end
283
+ raise E2B::E2BError, "File upload failed: HTTP #{response.code}" unless successful_response?(response)
287
284
 
288
285
  parse_upload_response(response.body)
289
286
  end
@@ -344,13 +341,11 @@ module E2B
344
341
  rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET, EOFError, Net::OpenTimeout, Net::ReadTimeout => e
345
342
  retry_count += 1
346
343
 
347
- if retry_count <= max_retries
348
- sleep_time = 2**retry_count
349
- sleep(sleep_time)
350
- retry
351
- else
352
- raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}"
353
- end
344
+ raise E2B::E2BError, "#{operation} failed after #{max_retries} retries: #{e.message}" unless retry_count <= max_retries
345
+
346
+ sleep_time = 2**retry_count
347
+ sleep(sleep_time)
348
+ retry
354
349
  end
355
350
  end
356
351
 
@@ -365,6 +360,7 @@ module E2B
365
360
 
366
361
  events.each do |event|
367
362
  next unless event.is_a?(Hash)
363
+
368
364
  # Direct entries field
369
365
  if event["entries"]
370
366
  entries.concat(Array(event["entries"]))
@@ -400,6 +396,7 @@ module E2B
400
396
  events.each do |event|
401
397
  next unless event.is_a?(Hash)
402
398
  return event["watcherId"] || event["watcher_id"] if event["watcherId"] || event["watcher_id"]
399
+
403
400
  result = event["result"]
404
401
  return result["watcherId"] || result["watcher_id"] if result.is_a?(Hash) && (result["watcherId"] || result["watcher_id"])
405
402
  end
@@ -84,7 +84,7 @@ module E2B
84
84
  #
85
85
  # @return [Boolean]
86
86
  def has_conflicts?
87
- file_status.any? { |f| f.index_status == "u" || f.index_status == "U" }
87
+ file_status.any? { |f| %w[u U].include?(f.index_status) }
88
88
  end
89
89
 
90
90
  # Number of staged files
@@ -105,7 +105,7 @@ module E2B
105
105
  #
106
106
  # @return [Integer]
107
107
  def conflict_count
108
- file_status.count { |f| f.index_status == "u" || f.index_status == "U" }
108
+ file_status.count { |f| %w[u U].include?(f.index_status) }
109
109
  end
110
110
 
111
111
  # Number of modified files (in the working tree)
@@ -571,7 +571,7 @@ module E2B
571
571
  envs: nil, user: nil, cwd: nil, timeout: nil)
572
572
  # Configure credential helper to use the store
573
573
  set_config("credential.helper", "store", scope: "global",
574
- envs: envs, user: user, cwd: cwd, timeout: timeout)
574
+ envs: envs, user: user, cwd: cwd, timeout: timeout)
575
575
 
576
576
  # Write credentials to the credential store via git credential approve
577
577
  credential_input = [
@@ -583,7 +583,7 @@ module E2B
583
583
  ].join("\n")
584
584
 
585
585
  escaped_input = Shellwords.escape(credential_input)
586
- args = ["credential", "approve"]
586
+ args = %w[credential approve]
587
587
  cmd = build_git_command(args, nil)
588
588
  full_cmd = "echo #{escaped_input} | #{cmd}"
589
589
 
@@ -604,9 +604,9 @@ module E2B
604
604
  # @return [void]
605
605
  def configure_user(name, email, scope: "global", path: nil, envs: nil, user: nil, cwd: nil, timeout: nil)
606
606
  set_config("user.name", name, scope: scope, path: path,
607
- envs: envs, user: user, cwd: cwd, timeout: timeout)
607
+ envs: envs, user: user, cwd: cwd, timeout: timeout)
608
608
  set_config("user.email", email, scope: scope, path: path,
609
- envs: envs, user: user, cwd: cwd, timeout: timeout)
609
+ envs: envs, user: user, cwd: cwd, timeout: timeout)
610
610
  end
611
611
 
612
612
  private
@@ -781,7 +781,7 @@ module E2B
781
781
  def validate_scope!(scope)
782
782
  return if VALID_SCOPES.include?(scope)
783
783
 
784
- raise E2B::E2BError, "Invalid git config scope '#{scope}'. Must be one of: #{VALID_SCOPES.join(', ')}"
784
+ raise E2B::E2BError, "Invalid git config scope '#{scope}'. Must be one of: #{VALID_SCOPES.join(", ")}"
785
785
  end
786
786
 
787
787
  # Convert a scope name to its git CLI flag
@@ -835,7 +835,7 @@ module E2B
835
835
  file_status << GitFileStatus.new(path: filepath.split("\t").first, index_status: idx, work_tree_status: wt)
836
836
  when /\Au (.)(.) .+ .+ .+ .+ .+ (.+)\z/
837
837
  # Unmerged entry
838
- idx = Regexp.last_match(1)
838
+ Regexp.last_match(1)
839
839
  wt = Regexp.last_match(2)
840
840
  filepath = Regexp.last_match(3)
841
841
  file_status << GitFileStatus.new(path: filepath, index_status: "u", work_tree_status: wt)