daytona 0.126.0.pre.alpha.5 → 0.134.0.alpha.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bddc30387b54eb59515ee8d562a84f60cc8897dada6124e4b810d87591628a33
4
- data.tar.gz: fc2fed7fb2321cf274be4b4ac69165a155e9982bacd936522745adbdeec057e1
3
+ metadata.gz: e7ccf2ea96861ef33410f2dc49e0e85243aabf6608d9d1b10e657507f4030d02
4
+ data.tar.gz: 9816f06a64493152d37bd202d00a8180212e6b9925af3c9d3b8deb11cad2d8f6
5
5
  SHA512:
6
- metadata.gz: 4d9a431c134fb2eb226fe43500e41e2c00f434c725b8c5f68a5fbba57a11b0a111680be4ecb06e97896c5294015ee08bc91357c8e3f03564bc23f9d2c231372b
7
- data.tar.gz: dda0466e92992558b6bcb53aec5a7971be1d8da8a0843c2952c958a01433ef9e2f4073116d54ed5e1eb51ddaacbe78455205e271445cb19cfcf262421cbae86d
6
+ metadata.gz: ccafd445c1c0f927eb9991cc051b2ad0d063adcd960615b1f21d6a154e2b9d5d3fbd424b7ffbd10aedcb7ec1dc2c2e30202dcd2182305f251f50c093baa03303
7
+ data.tar.gz: 6ee5305671de7d7c351a28b29f1a5210f08bae5fe6d992dffb888ca93e0795e21f55dd5f845b97c853b31323c0daf557c4e651ea9de65f9763854e7747cbda41
data/.rubocop.yml CHANGED
@@ -7,7 +7,7 @@ AllCops:
7
7
 
8
8
  Layout/LineLength:
9
9
  AllowedPatterns: ['\A\s*#']
10
- IgnoreCopDirectives: true
10
+ AllowCopDirectives: true
11
11
 
12
12
  Style/Documentation:
13
13
  Enabled: false
data/README.md CHANGED
@@ -86,13 +86,14 @@ From the repository root:
86
86
  ```bash
87
87
  # Set your RubyGems API key and version
88
88
  export RUBYGEMS_API_KEY="your-rubygems-api-key"
89
- export RUBYGEMS_PKG_VERSION="X.Y.Z" # pre-release format example: "X.Y.Z-alpha.1"
89
+ export RUBYGEMS_PKG_VERSION="X.Y.Z" # pre-release format example: "X.Y.Z.alpha.1"
90
90
 
91
91
  # Publish (builds and publishes all Ruby gems)
92
92
  yarn nx publish sdk-ruby
93
93
  ```
94
94
 
95
95
  This will automatically:
96
+
96
97
  - Set the version for all Ruby gems (api-client, toolbox-api-client, sdk)
97
98
  - Build all gems in the correct dependency order
98
99
  - Publish to RubyGems
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'websocket-client-simple'
5
+ require 'timeout'
6
+
7
+ module Daytona
8
+ # Handles code interpretation and execution within a Sandbox. Currently supports only Python.
9
+ #
10
+ # This class provides methods to execute code in isolated interpreter contexts,
11
+ # manage contexts, and stream execution output via callbacks. If subsequent code executions
12
+ # are performed in the same context, the variables, imports, and functions defined in
13
+ # the previous execution will be available.
14
+ #
15
+ # For other languages, use the `code_run` method from the `Process` interface,
16
+ # or execute the appropriate command directly in the sandbox terminal.
17
+ class CodeInterpreter
18
+ WEBSOCKET_TIMEOUT_CODE = 4008
19
+ WS_PORT = 2280
20
+ private_constant :WS_PORT
21
+
22
+ # @param sandbox_id [String]
23
+ # @param toolbox_api [DaytonaToolboxApiClient::InterpreterApi]
24
+ # @param get_preview_link [Proc]
25
+ def initialize(sandbox_id:, toolbox_api:, get_preview_link:)
26
+ @sandbox_id = sandbox_id
27
+ @toolbox_api = toolbox_api
28
+ @get_preview_link = get_preview_link
29
+ end
30
+
31
+ # Execute Python code in the sandbox.
32
+ #
33
+ # By default, code runs in the default shared context which persists variables,
34
+ # imports, and functions across executions. To run in an isolated context,
35
+ # create a new context with `create_context` and pass it as the `context` argument.
36
+ #
37
+ # @param code [String] Code to execute
38
+ # @param context [DaytonaToolboxApiClient::InterpreterContext, nil] Context to run code in
39
+ # @param on_stdout [Proc, nil] Callback for stdout messages (receives OutputMessage)
40
+ # @param on_stderr [Proc, nil] Callback for stderr messages (receives OutputMessage)
41
+ # @param on_error [Proc, nil] Callback for execution errors (receives ExecutionError)
42
+ # @param envs [Hash<String, String>, nil] Environment variables for this execution
43
+ # @param timeout [Integer, nil] Timeout in seconds. 0 means no timeout. Default is 10 minutes.
44
+ # @return [Daytona::ExecutionResult]
45
+ # @raise [Daytona::Sdk::Error]
46
+ #
47
+ # @example
48
+ # def handle_stdout(msg)
49
+ # print "STDOUT: #{msg.output}"
50
+ # end
51
+ #
52
+ # def handle_stderr(msg)
53
+ # print "STDERR: #{msg.output}"
54
+ # end
55
+ #
56
+ # def handle_error(err)
57
+ # puts "ERROR: #{err.name}: #{err.value}"
58
+ # end
59
+ #
60
+ # code = <<~PYTHON
61
+ # import sys
62
+ # import time
63
+ # for i in range(5):
64
+ # print(i)
65
+ # time.sleep(1)
66
+ # sys.stderr.write("Counting done!")
67
+ # PYTHON
68
+ #
69
+ # result = sandbox.code_interpreter.run_code(
70
+ # code,
71
+ # on_stdout: method(:handle_stdout),
72
+ # on_stderr: method(:handle_stderr),
73
+ # on_error: method(:handle_error),
74
+ # timeout: 10
75
+ # )
76
+ def run_code(code, context: nil, on_stdout: nil, on_stderr: nil, on_error: nil, envs: nil, timeout: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
77
+ # Get WebSocket URL via preview link
78
+ preview_link = @get_preview_link.call(WS_PORT)
79
+ url = URI.parse(preview_link.url)
80
+ url.scheme = url.scheme == 'https' ? 'wss' : 'ws'
81
+ url.path = '/process/interpreter/execute'
82
+ ws_url = url.to_s
83
+
84
+ result = ExecutionResult.new
85
+
86
+ # Create request payload
87
+ request = { code: }
88
+ request[:contextId] = context.id if context
89
+ request[:envs] = envs if envs
90
+ request[:timeout] = timeout if timeout
91
+
92
+ # Build headers with preview token
93
+ headers = @toolbox_api.api_client.default_headers.dup.merge(
94
+ 'X-Daytona-Preview-Token' => preview_link.token,
95
+ 'Content-Type' => 'application/json',
96
+ 'Accept' => 'application/json'
97
+ )
98
+
99
+ # Use queue for synchronization
100
+ completion_queue = Queue.new
101
+ interpreter = self # Capture self for use in blocks
102
+ last_message_time = Time.now
103
+ message_mutex = Mutex.new
104
+
105
+ puts "[DEBUG] Connecting to WebSocket: #{ws_url}" if ENV['DEBUG']
106
+
107
+ # Connect to WebSocket and execute
108
+ ws = WebSocket::Client::Simple.connect(ws_url, headers:)
109
+
110
+ ws.on :open do
111
+ puts '[DEBUG] WebSocket opened, sending request' if ENV['DEBUG']
112
+ ws.send(JSON.dump(request))
113
+ end
114
+
115
+ ws.on :message do |msg|
116
+ message_mutex.synchronize { last_message_time = Time.now }
117
+
118
+ puts "[DEBUG] Received message (length=#{msg.data.length}): #{msg.data.inspect[0..200]}" if ENV['DEBUG']
119
+
120
+ interpreter.send(:handle_message, msg.data, result, on_stdout, on_stderr, on_error, completion_queue)
121
+ end
122
+
123
+ ws.on :error do |e|
124
+ puts "[DEBUG] WebSocket error: #{e.message}" if ENV['DEBUG']
125
+ completion_queue.push({ type: :error, error: e })
126
+ end
127
+
128
+ ws.on :close do |e|
129
+ if ENV['DEBUG']
130
+ code = e&.code || 'nil'
131
+ reason = e&.reason || 'nil'
132
+ puts "[DEBUG] WebSocket closed: code=#{code}, reason=#{reason}"
133
+ end
134
+ error_info = interpreter.send(:handle_close, e)
135
+ if error_info
136
+ completion_queue.push({ type: :error_from_close, error: error_info })
137
+ else
138
+ completion_queue.push({ type: :close })
139
+ end
140
+ end
141
+
142
+ # Wait for completion signal with idle timeout
143
+ # If timeout is specified, wait longer to detect actual timeout errors
144
+ # Otherwise use short idle timeout for normal completion
145
+ idle_timeout = timeout ? (timeout + 2.0) : 1.0
146
+ max_wait = (timeout || 300) + 3 # Add buffer to configured timeout
147
+ start_time = Time.now
148
+ completion_reason = nil
149
+
150
+ # Wait for completion or close event
151
+ loop do
152
+ begin
153
+ completion = completion_queue.pop(true) # non-blocking
154
+ puts "[DEBUG] Got completion signal: #{completion[:type]}" if ENV['DEBUG']
155
+
156
+ # Control message (completed/interrupted) = normal completion
157
+ if completion[:type] == :completed
158
+ completion_reason = :completed
159
+ break
160
+ # If it's an error from close event (like timeout), raise it
161
+ elsif completion[:type] == :error_from_close
162
+ error_msg = completion[:error]
163
+ # Raise TimeoutError for timeout cases, regular Error for others
164
+ if error_msg.include?('timed out') || error_msg.include?('Execution timed out')
165
+ raise Sdk::TimeoutError, error_msg
166
+ end
167
+
168
+ raise Sdk::Error, error_msg
169
+
170
+ # Close event during execution (before control message) = likely timeout or error
171
+ elsif completion[:type] == :close
172
+ elapsed = Time.now - start_time
173
+ # If we got close near the timeout, it's likely a timeout
174
+ if timeout && elapsed >= timeout && elapsed < (timeout + 2)
175
+ raise Sdk::TimeoutError,
176
+ 'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
177
+ end
178
+ # Otherwise normal close
179
+ completion_reason = :close
180
+ break
181
+ # WebSocket errors
182
+ elsif completion[:type] == :error && !completion[:error].message.include?('stream closed')
183
+ raise Sdk::Error, "WebSocket error: #{completion[:error].message}"
184
+ end
185
+ rescue ThreadError
186
+ # Queue is empty, check idle timeout
187
+ end
188
+
189
+ # Check idle timeout (no messages for N seconds = completion)
190
+ time_since_last_message = message_mutex.synchronize { Time.now - last_message_time }
191
+ if time_since_last_message > idle_timeout
192
+ puts "[DEBUG] Idle timeout reached (#{idle_timeout}s), assuming completion" if ENV['DEBUG']
193
+ completion_reason = :idle_complete
194
+ break
195
+ end
196
+
197
+ # Check for absolute timeout (safety net)
198
+ if Time.now - start_time > max_wait
199
+ ws.close
200
+ raise Sdk::TimeoutError,
201
+ 'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
202
+ end
203
+
204
+ sleep 0.05 # Check every 50ms
205
+ end
206
+
207
+ # Close WebSocket if not already closed
208
+ ws.close if completion_reason != :close
209
+ sleep 0.05
210
+
211
+ result
212
+ rescue Sdk::Error
213
+ # Re-raise SDK errors as-is
214
+ raise
215
+ rescue StandardError => e
216
+ # Wrap unexpected errors
217
+ raise Sdk::Error, "Failed to run code: #{e.message}"
218
+ end
219
+
220
+ # Create a new isolated interpreter context.
221
+ #
222
+ # Contexts provide isolated execution environments with their own global namespace.
223
+ # Variables, imports, and functions defined in one context don't affect others.
224
+ #
225
+ # @param cwd [String, nil] Working directory for the context
226
+ # @return [DaytonaToolboxApiClient::InterpreterContext]
227
+ # @raise [Daytona::Sdk::Error]
228
+ #
229
+ # @example
230
+ # # Create isolated context
231
+ # ctx = sandbox.code_interpreter.create_context
232
+ #
233
+ # # Execute code in this context
234
+ # sandbox.code_interpreter.run_code("x = 100", context: ctx)
235
+ #
236
+ # # Variable only exists in this context
237
+ # result = sandbox.code_interpreter.run_code("print(x)", context: ctx) # OK
238
+ #
239
+ # # Won't see the variable in default context
240
+ # result = sandbox.code_interpreter.run_code("print(x)") # NameError
241
+ #
242
+ # # Clean up
243
+ # sandbox.code_interpreter.delete_context(ctx)
244
+ def create_context(cwd: nil)
245
+ request = DaytonaToolboxApiClient::CreateContextRequest.new(cwd:)
246
+ @toolbox_api.create_interpreter_context(request)
247
+ rescue StandardError => e
248
+ raise Sdk::Error, "Failed to create interpreter context: #{e.message}"
249
+ end
250
+
251
+ # List all user-created interpreter contexts.
252
+ #
253
+ # The default context is not included in this list. Only contexts created
254
+ # via `create_context` are returned.
255
+ #
256
+ # @return [Array<DaytonaToolboxApiClient::InterpreterContext>]
257
+ # @raise [Daytona::Sdk::Error]
258
+ #
259
+ # @example
260
+ # contexts = sandbox.code_interpreter.list_contexts
261
+ # contexts.each do |ctx|
262
+ # puts "Context #{ctx.id}: #{ctx.language} at #{ctx.cwd}"
263
+ # end
264
+ def list_contexts
265
+ response = @toolbox_api.list_interpreter_contexts
266
+ response.contexts || []
267
+ rescue StandardError => e
268
+ raise Sdk::Error, "Failed to list interpreter contexts: #{e.message}"
269
+ end
270
+
271
+ # Delete an interpreter context and shut down all associated processes.
272
+ #
273
+ # This permanently removes the context and all its state (variables, imports, etc.).
274
+ # The default context cannot be deleted.
275
+ #
276
+ # @param context [DaytonaToolboxApiClient::InterpreterContext]
277
+ # @return [void]
278
+ # @raise [Daytona::Sdk::Error]
279
+ #
280
+ # @example
281
+ # ctx = sandbox.code_interpreter.create_context
282
+ # # ... use context ...
283
+ # sandbox.code_interpreter.delete_context(ctx)
284
+ def delete_context(context)
285
+ @toolbox_api.delete_interpreter_context(context.id)
286
+ nil
287
+ rescue StandardError => e
288
+ raise Sdk::Error, "Failed to delete interpreter context: #{e.message}"
289
+ end
290
+
291
+ private
292
+
293
+ # @return [Hash<String, String>]
294
+ def build_headers
295
+ headers = {}
296
+ @toolbox_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
297
+ headers
298
+ end
299
+
300
+ # @param data [String]
301
+ # @param result [Daytona::ExecutionResult]
302
+ # @param on_stdout [Proc, nil]
303
+ # @param on_stderr [Proc, nil]
304
+ # @param on_error [Proc, nil]
305
+ # @param completion_queue [Queue, nil] Queue to signal completion
306
+ # @return [void]
307
+ def handle_message(data, result, on_stdout, on_stderr, on_error, completion_queue = nil) # rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
308
+ # Empty messages are just keepalives or noise, ignore them
309
+ if data.nil? || data.empty?
310
+ puts '[DEBUG] Received empty message, ignoring' if ENV['DEBUG']
311
+ return
312
+ end
313
+
314
+ chunk = JSON.parse(data)
315
+ chunk_type = chunk['type']
316
+
317
+ case chunk_type
318
+ when 'stdout'
319
+ stdout = chunk['text'] || ''
320
+ result.stdout += stdout
321
+ on_stdout&.call(OutputMessage.new(output: stdout))
322
+ when 'stderr'
323
+ stderr = chunk['text'] || ''
324
+ result.stderr += stderr
325
+ on_stderr&.call(OutputMessage.new(output: stderr))
326
+ when 'error'
327
+ error = ExecutionError.new(
328
+ name: chunk['name'] || '',
329
+ value: chunk['value'] || '',
330
+ traceback: chunk['traceback'] || ''
331
+ )
332
+ result.error = error
333
+ on_error&.call(error)
334
+ when 'control'
335
+ control_text = chunk['text'] || ''
336
+ if %w[completed interrupted].include?(control_text)
337
+ puts "[DEBUG] Received control message: #{control_text}" if ENV['DEBUG']
338
+ completion_queue&.push({ type: :completed })
339
+ end
340
+ end
341
+ rescue JSON::ParserError => e
342
+ # Skip malformed messages
343
+ warn "Warning: Failed to parse message: #{e.message}" if ENV['DEBUG']
344
+ end
345
+
346
+ # @param event [Object]
347
+ # @return [void]
348
+ def handle_close(event)
349
+ return nil unless event # Skip if event is nil (manual close)
350
+
351
+ code = event.respond_to?(:code) ? event.code : nil
352
+ reason = event.respond_to?(:reason) ? event.reason : nil
353
+
354
+ if code == WEBSOCKET_TIMEOUT_CODE
355
+ return 'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
356
+ end
357
+
358
+ return nil if code == 1000 || code.nil? # Normal closure or no code
359
+
360
+ detail = reason.to_s.empty? ? 'WebSocket connection closed unexpectedly' : reason.to_s
361
+ detail = "#{detail} (close code #{code})" if code
362
+ detail
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Daytona
6
+ class SandboxJsCodeToolbox
7
+ def get_run_command(code, params = nil)
8
+ # Encode the provided code in base64
9
+ base64_code = Base64.strict_encode64(code)
10
+
11
+ # Build command-line arguments string
12
+ argv = ''
13
+ argv = params.argv.join(' ') if params&.argv && !params.argv.empty?
14
+
15
+ # Combine everything into the final command for JavaScript
16
+ " sh -c 'echo #{base64_code} | base64 --decode | node -e \"$(cat)\" #{argv} 2>&1 | grep -vE \"npm notice\"' "
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Daytona
4
+ # Represents stdout or stderr output from code execution
5
+ class OutputMessage
6
+ # @return [String] The output content
7
+ attr_reader :output
8
+
9
+ # @param output [String]
10
+ def initialize(output:)
11
+ @output = output
12
+ end
13
+ end
14
+
15
+ # Represents an error that occurred during code execution
16
+ class ExecutionError
17
+ # @return [String] The error type/class name (e.g., "ValueError", "SyntaxError")
18
+ attr_reader :name
19
+
20
+ # @return [String] The error value
21
+ attr_reader :value
22
+
23
+ # @return [String] Full traceback of the error
24
+ attr_reader :traceback
25
+
26
+ # @param name [String]
27
+ # @param value [String]
28
+ # @param traceback [String]
29
+ def initialize(name:, value:, traceback: '')
30
+ @name = name
31
+ @value = value
32
+ @traceback = traceback
33
+ end
34
+ end
35
+
36
+ # Result of code execution
37
+ class ExecutionResult
38
+ # @return [String] Standard output from the code execution
39
+ attr_accessor :stdout
40
+
41
+ # @return [String] Standard error output from the code execution
42
+ attr_accessor :stderr
43
+
44
+ # @return [ExecutionError, nil] Error details if execution failed, nil otherwise
45
+ attr_accessor :error
46
+
47
+ def initialize(stdout: '', stderr: '', error: nil)
48
+ @stdout = stdout
49
+ @stderr = stderr
50
+ @error = error
51
+ end
52
+ end
53
+ end
@@ -16,15 +16,21 @@ module Daytona
16
16
  # @return [Array<String>, nil] Entrypoint of the snapshot
17
17
  attr_reader :entrypoint
18
18
 
19
+ # @return [String, nil] ID of the region where the snapshot will be available.
20
+ # Defaults to organization default region if not specified.
21
+ attr_reader :region_id
22
+
19
23
  # @param name [String] Name of the snapshot
20
24
  # @param image [String, Daytona::Image] Image of the snapshot
21
25
  # @param resources [Daytona::Resources, nil] Resources of the snapshot
22
26
  # @param entrypoint [Array<String>, nil] Entrypoint of the snapshot
23
- def initialize(name:, image:, resources: nil, entrypoint: nil)
27
+ # @param region_id [String, nil] ID of the region where the snapshot will be available
28
+ def initialize(name:, image:, resources: nil, entrypoint: nil, region_id: nil)
24
29
  @name = name
25
30
  @image = image
26
31
  @resources = resources
27
32
  @entrypoint = entrypoint
33
+ @region_id = region_id
28
34
  end
29
35
  end
30
36
 
@@ -67,7 +67,7 @@ module Daytona
67
67
  # right_click = sandbox.computer_use.mouse.click(x: 100, y: 200, button: 'right')
68
68
  def click(x:, y:, button: 'left', double: false) # rubocop:disable Naming/MethodParameterName
69
69
  request = DaytonaToolboxApiClient::MouseClickRequest.new(x:, y:, button:, double:)
70
- toolbox_api.click_mouse(request)
70
+ toolbox_api.click(request)
71
71
  rescue StandardError => e
72
72
  raise Sdk::Error, "Failed to click mouse: #{e.message}"
73
73
  end
@@ -87,7 +87,7 @@ module Daytona
87
87
  # puts "Dragged from #{result.from_x},#{result.from_y} to #{result.to_x},#{result.to_y}"
88
88
  def drag(start_x:, start_y:, end_x:, end_y:, button: 'left')
89
89
  request = DaytonaToolboxApiClient::MouseDragRequest.new(start_x:, start_y:, end_x:, end_y:, button:)
90
- toolbox_api.drag_mouse(request)
90
+ toolbox_api.drag(request)
91
91
  rescue StandardError => e
92
92
  raise Sdk::Error, "Failed to drag mouse: #{e.message}"
93
93
  end
@@ -109,7 +109,7 @@ module Daytona
109
109
  # scroll_down = sandbox.computer_use.mouse.scroll(x: 100, y: 200, direction: 'down', amount: 5)
110
110
  def scroll(x:, y:, direction:, amount: 1) # rubocop:disable Naming/MethodParameterName
111
111
  request = DaytonaToolboxApiClient::MouseScrollRequest.new(x:, y:, direction:, amount:)
112
- toolbox_api.scroll_mouse(request)
112
+ toolbox_api.scroll(request)
113
113
  true
114
114
  rescue StandardError => e
115
115
  raise Sdk::Error, "Failed to scroll mouse: #{e.message}"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dotenv'
4
+
3
5
  module Daytona
4
6
  class Config
5
7
  API_URL = 'https://app.daytona.io/api'
@@ -37,17 +39,51 @@ module Daytona
37
39
  # @param organization_id [String, nil] Daytona organization ID. Defaults to ENV['DAYTONA_ORGANIZATION_ID'].
38
40
  # @param target [String, nil] Daytona target. Defaults to ENV['DAYTONA_TARGET'].
39
41
  def initialize(
40
- api_key: ENV.fetch('DAYTONA_API_KEY', nil),
41
- jwt_token: ENV.fetch('DAYTONA_JWT_TOKEN', nil),
42
- api_url: ENV.fetch('DAYTONA_API_URL', API_URL),
43
- organization_id: ENV.fetch('DAYTONA_ORGANIZATION_ID', nil),
44
- target: ENV.fetch('DAYTONA_TARGET', nil)
42
+ api_key: nil,
43
+ jwt_token: nil,
44
+ api_url: nil,
45
+ organization_id: nil,
46
+ target: nil
45
47
  )
46
- @api_key = api_key
47
- @jwt_token = jwt_token
48
- @api_url = api_url
49
- @target = target
50
- @organization_id = organization_id
48
+ # Load environment variables from .env and .env.local files
49
+ # Files are loaded from the current working directory (where the code is executed)
50
+ load_env_files
51
+
52
+ @api_key = api_key || ENV.fetch('DAYTONA_API_KEY', nil)
53
+ @jwt_token = jwt_token || ENV.fetch('DAYTONA_JWT_TOKEN', nil)
54
+ @api_url = api_url || ENV.fetch('DAYTONA_API_URL', API_URL)
55
+ @target = target || ENV.fetch('DAYTONA_TARGET', nil)
56
+ @organization_id = organization_id || ENV.fetch('DAYTONA_ORGANIZATION_ID', nil)
57
+ end
58
+
59
+ private
60
+
61
+ # Load only Daytona-specific environment variables from .env and .env.local files
62
+ # Only loads variables that are not already set in the runtime environment
63
+ # .env.local overrides .env
64
+ # Files are loaded from the current working directory
65
+ def load_env_files
66
+ # Daytona-specific variables we want to load
67
+ daytona_vars = %w[
68
+ DAYTONA_API_KEY
69
+ DAYTONA_API_URL
70
+ DAYTONA_TARGET
71
+ DAYTONA_JWT_TOKEN
72
+ DAYTONA_ORGANIZATION_ID
73
+ ]
74
+
75
+ env_file = File.join(Dir.pwd, '.env')
76
+ env_local_file = File.join(Dir.pwd, '.env.local')
77
+
78
+ # Parse .env files using dotenv (doesn't set ENV automatically)
79
+ env_from_file = {}
80
+ env_from_file.merge!(Dotenv.parse(env_file)) if File.exist?(env_file)
81
+ env_from_file.merge!(Dotenv.parse(env_local_file)) if File.exist?(env_local_file)
82
+
83
+ # Only set Daytona-specific variables that aren't already in runtime
84
+ daytona_vars.each do |var|
85
+ ENV[var] = env_from_file[var] if env_from_file.key?(var) && !ENV.key?(var)
86
+ end
51
87
  end
52
88
  end
53
89
  end
@@ -36,8 +36,9 @@ module Daytona
36
36
  @volume = VolumeService.new(DaytonaApiClient::VolumesApi.new(api_client))
37
37
  @object_storage_api = DaytonaApiClient::ObjectStorageApi.new(api_client)
38
38
  @snapshots_api = DaytonaApiClient::SnapshotsApi.new(api_client)
39
- @snapshot = SnapshotService.new(snapshots_api:, object_storage_api:)
40
- @proxy_toolbox_url = nil
39
+ @snapshot = SnapshotService.new(snapshots_api:, object_storage_api:, default_region_id: config.target)
40
+ @proxy_toolbox_url_cache = {}
41
+ @proxy_toolbox_url_mutex = Mutex.new
41
42
  end
42
43
 
43
44
  # Creates a sandbox with the specified parameters
@@ -180,9 +181,15 @@ module Daytona
180
181
  response = sandbox_api.create_sandbox(create_sandbox)
181
182
 
182
183
  if response.state == DaytonaApiClient::SandboxState::PENDING_BUILD && on_snapshot_create_logs
183
- uri = URI.parse(sandbox_api.api_client.config.base_url)
184
- uri.path = "/api/sandbox/#{response.id}/build-logs"
185
- uri.query = 'follow=true'
184
+ # Wait for state to change from PENDING_BUILD before fetching logs
185
+ while response.state == DaytonaApiClient::SandboxState::PENDING_BUILD
186
+ sleep(1)
187
+ response = sandbox_api.get_sandbox(response.id)
188
+ end
189
+
190
+ # Get build logs URL from API
191
+ build_logs_response = sandbox_api.get_build_logs_url(response.id)
192
+ uri = URI.parse("#{build_logs_response.url}?follow=true")
186
193
 
187
194
  headers = {}
188
195
  sandbox_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
@@ -238,15 +245,27 @@ module Daytona
238
245
  config:,
239
246
  sandbox_api:,
240
247
  code_toolbox:,
241
- get_proxy_toolbox_url: method(:proxy_toolbox_url)
248
+ get_proxy_toolbox_url: proc { |sandbox_id, region_id| proxy_toolbox_url(sandbox_id, region_id) }
242
249
  )
243
250
  end
244
251
 
245
- # Gets the proxy toolbox URL from the config API (lazy loaded)
252
+ # Gets the proxy toolbox URL from the sandbox API (cached per region)
246
253
  #
254
+ # @param sandbox_id [String] The sandbox ID
255
+ # @param region_id [String] The region ID
247
256
  # @return [String] The proxy toolbox URL
248
- def proxy_toolbox_url
249
- @proxy_toolbox_url ||= @config_api.config_controller_get_config.proxy_toolbox_url
257
+ def proxy_toolbox_url(sandbox_id, region_id)
258
+ # Return cached URL if available
259
+ return @proxy_toolbox_url_cache[region_id] if @proxy_toolbox_url_cache.key?(region_id)
260
+
261
+ # Use mutex to ensure thread-safe caching
262
+ @proxy_toolbox_url_mutex.synchronize do
263
+ # Double-check after acquiring lock
264
+ return @proxy_toolbox_url_cache[region_id] if @proxy_toolbox_url_cache.key?(region_id)
265
+
266
+ # Fetch and cache the URL
267
+ @proxy_toolbox_url_cache[region_id] = sandbox_api.get_toolbox_proxy_url(sandbox_id).url
268
+ end
250
269
  end
251
270
 
252
271
  # Converts a language to a code toolbox
@@ -260,6 +279,8 @@ module Daytona
260
279
  SandboxPythonCodeToolbox.new
261
280
  when SandboxTsCodeToolbox, CodeLanguage::TYPESCRIPT
262
281
  SandboxTsCodeToolbox.new
282
+ when CodeLanguage::JAVASCRIPT
283
+ SandboxJsCodeToolbox.new
263
284
  else
264
285
  raise Sdk::Error, "Unsupported language: #{language}"
265
286
  end