@hanzo/runtime 0.0.0-dev

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.
package/src/Sandbox.ts ADDED
@@ -0,0 +1,478 @@
1
+ /*
2
+ * Copyright 2025 Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import {
7
+ ToolboxApi,
8
+ SandboxState,
9
+ SandboxApi,
10
+ Sandbox as SandboxDto,
11
+ PortPreviewUrl,
12
+ SandboxVolume,
13
+ BuildInfo,
14
+ SandboxBackupStateEnum,
15
+ } from '@daytonaio/api-client'
16
+ import { FileSystem } from './FileSystem'
17
+ import { Git } from './Git'
18
+ import { CodeRunParams, Process } from './Process'
19
+ import { LspLanguageId, LspServer } from './LspServer'
20
+ import { DaytonaError } from './errors/DaytonaError'
21
+ import { prefixRelativePath } from './utils/Path'
22
+ import { ComputerUse } from './ComputerUse'
23
+
24
+ /**
25
+ * Interface defining methods that a code toolbox must implement
26
+ * @interface
27
+ */
28
+ export interface SandboxCodeToolbox {
29
+ /** Generates a command to run the provided code */
30
+ getRunCommand(code: string, params?: CodeRunParams): string
31
+ }
32
+
33
+ /**
34
+ * Represents a Daytona Sandbox.
35
+ *
36
+ * @property {FileSystem} fs - File system operations interface
37
+ * @property {Git} git - Git operations interface
38
+ * @property {Process} process - Process execution interface
39
+ * @property {ComputerUse} computerUse - Computer use operations interface for desktop automation
40
+ * @property {string} id - Unique identifier for the Sandbox
41
+ * @property {string} organizationId - Organization ID of the Sandbox
42
+ * @property {string} [snapshot] - Daytona snapshot used to create the Sandbox
43
+ * @property {string} user - OS user running in the Sandbox
44
+ * @property {Record<string, string>} env - Environment variables set in the Sandbox
45
+ * @property {Record<string, string>} labels - Custom labels attached to the Sandbox
46
+ * @property {boolean} public - Whether the Sandbox is publicly accessible
47
+ * @property {string} target - Target location of the runner where the Sandbox runs
48
+ * @property {number} cpu - Number of CPUs allocated to the Sandbox
49
+ * @property {number} gpu - Number of GPUs allocated to the Sandbox
50
+ * @property {number} memory - Amount of memory allocated to the Sandbox in GiB
51
+ * @property {number} disk - Amount of disk space allocated to the Sandbox in GiB
52
+ * @property {SandboxState} state - Current state of the Sandbox (e.g., "started", "stopped")
53
+ * @property {string} [errorReason] - Error message if Sandbox is in error state
54
+ * @property {SandboxBackupStateEnum} [backupState] - Current state of Sandbox backup
55
+ * @property {string} [backupCreatedAt] - When the backup was created
56
+ * @property {number} [autoStopInterval] - Auto-stop interval in minutes
57
+ * @property {number} [autoArchiveInterval] - Auto-archive interval in minutes
58
+ * @property {number} [autoDeleteInterval] - Auto-delete interval in minutes
59
+ * @property {string} [runnerDomain] - Domain name of the Sandbox runner
60
+ * @property {Array<SandboxVolume>} [volumes] - Volumes attached to the Sandbox
61
+ * @property {BuildInfo} [buildInfo] - Build information for the Sandbox if it was created from dynamic build
62
+ * @property {string} [createdAt] - When the Sandbox was created
63
+ * @property {string} [updatedAt] - When the Sandbox was last updated
64
+ *
65
+ * @class
66
+ */
67
+ export class Sandbox implements SandboxDto {
68
+ public readonly fs: FileSystem
69
+ public readonly git: Git
70
+ public readonly process: Process
71
+ public readonly computerUse: ComputerUse
72
+
73
+ public id!: string
74
+ public organizationId!: string
75
+ public snapshot?: string
76
+ public user!: string
77
+ public env!: Record<string, string>
78
+ public labels!: Record<string, string>
79
+ public public!: boolean
80
+ public target!: string
81
+ public cpu!: number
82
+ public gpu!: number
83
+ public memory!: number
84
+ public disk!: number
85
+ public state?: SandboxState
86
+ public errorReason?: string
87
+ public backupState?: SandboxBackupStateEnum
88
+ public backupCreatedAt?: string
89
+ public autoStopInterval?: number
90
+ public autoArchiveInterval?: number
91
+ public autoDeleteInterval?: number
92
+ public runnerDomain?: string
93
+ public volumes?: Array<SandboxVolume>
94
+ public buildInfo?: BuildInfo
95
+ public createdAt?: string
96
+ public updatedAt?: string
97
+
98
+ private rootDir: string
99
+
100
+ /**
101
+ * Creates a new Sandbox instance
102
+ *
103
+ * @param {SandboxDto} sandboxDto - The API Sandbox instance
104
+ * @param {SandboxApi} sandboxApi - API client for Sandbox operations
105
+ * @param {ToolboxApi} toolboxApi - API client for toolbox operations
106
+ * @param {SandboxCodeToolbox} codeToolbox - Language-specific toolbox implementation
107
+ */
108
+ constructor(
109
+ sandboxDto: SandboxDto,
110
+ private readonly sandboxApi: SandboxApi,
111
+ private readonly toolboxApi: ToolboxApi,
112
+ private readonly codeToolbox: SandboxCodeToolbox,
113
+ ) {
114
+ this.processSandboxDto(sandboxDto)
115
+ this.rootDir = ''
116
+ this.fs = new FileSystem(this.id, this.toolboxApi, async () => await this.getRootDir())
117
+ this.git = new Git(this.id, this.toolboxApi, async () => await this.getRootDir())
118
+ this.process = new Process(this.id, this.codeToolbox, this.toolboxApi, async () => await this.getRootDir())
119
+ this.computerUse = new ComputerUse(this.id, this.toolboxApi)
120
+ }
121
+
122
+ /**
123
+ * Gets the root directory path for the logged in user inside the Sandbox.
124
+ *
125
+ * @returns {Promise<string | undefined>} The absolute path to the Sandbox root directory for the logged in user
126
+ *
127
+ * @example
128
+ * const rootDir = await sandbox.getUserRootDir();
129
+ * console.log(`Sandbox root: ${rootDir}`);
130
+ */
131
+ public async getUserRootDir(): Promise<string | undefined> {
132
+ const response = await this.toolboxApi.getProjectDir(this.id)
133
+ return response.data.dir
134
+ }
135
+
136
+ /**
137
+ * Creates a new Language Server Protocol (LSP) server instance.
138
+ *
139
+ * The LSP server provides language-specific features like code completion,
140
+ * diagnostics, and more.
141
+ *
142
+ * @param {LspLanguageId} languageId - The language server type (e.g., "typescript")
143
+ * @param {string} pathToProject - Path to the project root directory. Relative paths are resolved based on the user's
144
+ * root directory.
145
+ * @returns {LspServer} A new LSP server instance configured for the specified language
146
+ *
147
+ * @example
148
+ * const lsp = await sandbox.createLspServer('typescript', 'workspace/project');
149
+ */
150
+ public async createLspServer(languageId: LspLanguageId | string, pathToProject: string): Promise<LspServer> {
151
+ return new LspServer(
152
+ languageId as LspLanguageId,
153
+ prefixRelativePath(await this.getRootDir(), pathToProject),
154
+ this.toolboxApi,
155
+ this.id,
156
+ )
157
+ }
158
+
159
+ /**
160
+ * Sets labels for the Sandbox.
161
+ *
162
+ * Labels are key-value pairs that can be used to organize and identify Sandboxes.
163
+ *
164
+ * @param {Record<string, string>} labels - Dictionary of key-value pairs representing Sandbox labels
165
+ * @returns {Promise<void>}
166
+ *
167
+ * @example
168
+ * // Set sandbox labels
169
+ * await sandbox.setLabels({
170
+ * project: 'my-project',
171
+ * environment: 'development',
172
+ * team: 'backend'
173
+ * });
174
+ */
175
+ public async setLabels(labels: Record<string, string>): Promise<Record<string, string>> {
176
+ this.labels = (await this.sandboxApi.replaceLabels(this.id, { labels })).data.labels
177
+ return this.labels
178
+ }
179
+
180
+ /**
181
+ * Start the Sandbox.
182
+ *
183
+ * This method starts the Sandbox and waits for it to be ready.
184
+ *
185
+ * @param {number} [timeout] - Maximum time to wait in seconds. 0 means no timeout.
186
+ * Defaults to 60-second timeout.
187
+ * @returns {Promise<void>}
188
+ * @throws {DaytonaError} - `DaytonaError` - If Sandbox fails to start or times out
189
+ *
190
+ * @example
191
+ * const sandbox = await daytona.getCurrentSandbox('my-sandbox');
192
+ * await sandbox.start(40); // Wait up to 40 seconds
193
+ * console.log('Sandbox started successfully');
194
+ */
195
+ public async start(timeout = 60): Promise<void> {
196
+ if (timeout < 0) {
197
+ throw new DaytonaError('Timeout must be a non-negative number')
198
+ }
199
+ const startTime = Date.now()
200
+ const response = await this.sandboxApi.startSandbox(this.id, undefined, { timeout: timeout * 1000 })
201
+ this.processSandboxDto(response.data)
202
+ const timeElapsed = Date.now() - startTime
203
+ await this.waitUntilStarted(timeout ? timeout - timeElapsed / 1000 : 0)
204
+ }
205
+
206
+ /**
207
+ * Stops the Sandbox.
208
+ *
209
+ * This method stops the Sandbox and waits for it to be fully stopped.
210
+ *
211
+ * @param {number} [timeout] - Maximum time to wait in seconds. 0 means no timeout.
212
+ * Defaults to 60-second timeout.
213
+ * @returns {Promise<void>}
214
+ *
215
+ * @example
216
+ * const sandbox = await daytona.getCurrentSandbox('my-sandbox');
217
+ * await sandbox.stop();
218
+ * console.log('Sandbox stopped successfully');
219
+ */
220
+ public async stop(timeout = 60): Promise<void> {
221
+ if (timeout < 0) {
222
+ throw new DaytonaError('Timeout must be a non-negative number')
223
+ }
224
+ const startTime = Date.now()
225
+ await this.sandboxApi.stopSandbox(this.id, undefined, { timeout: timeout * 1000 })
226
+ await this.refreshData()
227
+ const timeElapsed = Date.now() - startTime
228
+ await this.waitUntilStopped(timeout ? timeout - timeElapsed / 1000 : 0)
229
+ }
230
+
231
+ /**
232
+ * Deletes the Sandbox.
233
+ * @returns {Promise<void>}
234
+ */
235
+ public async delete(timeout = 60): Promise<void> {
236
+ await this.sandboxApi.deleteSandbox(this.id, true, undefined, { timeout: timeout * 1000 })
237
+ await this.refreshData()
238
+ }
239
+
240
+ /**
241
+ * Waits for the Sandbox to reach the 'started' state.
242
+ *
243
+ * This method polls the Sandbox status until it reaches the 'started' state
244
+ * or encounters an error.
245
+ *
246
+ * @param {number} [timeout] - Maximum time to wait in seconds. 0 means no timeout.
247
+ * Defaults to 60 seconds.
248
+ * @returns {Promise<void>}
249
+ * @throws {DaytonaError} - `DaytonaError` - If the sandbox ends up in an error state or fails to start within the timeout period.
250
+ */
251
+ public async waitUntilStarted(timeout = 60) {
252
+ if (timeout < 0) {
253
+ throw new DaytonaError('Timeout must be a non-negative number')
254
+ }
255
+
256
+ const checkInterval = 100 // Wait 100 ms between checks
257
+ const startTime = Date.now()
258
+
259
+ while (this.state !== 'started') {
260
+ await this.refreshData()
261
+
262
+ // @ts-expect-error this.refreshData() can modify this.state so this check is fine
263
+ if (this.state === 'started') {
264
+ return
265
+ }
266
+
267
+ if (this.state === 'error') {
268
+ const errMsg = `Sandbox ${this.id} failed to start with status: ${this.state}, error reason: ${this.errorReason}`
269
+ throw new DaytonaError(errMsg)
270
+ }
271
+
272
+ if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
273
+ throw new DaytonaError('Sandbox failed to become ready within the timeout period')
274
+ }
275
+
276
+ await new Promise((resolve) => setTimeout(resolve, checkInterval))
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Wait for Sandbox to reach 'stopped' state.
282
+ *
283
+ * This method polls the Sandbox status until it reaches the 'stopped' state
284
+ * or encounters an error.
285
+ *
286
+ * @param {number} [timeout] - Maximum time to wait in seconds. 0 means no timeout.
287
+ * Defaults to 60 seconds.
288
+ * @returns {Promise<void>}
289
+ * @throws {DaytonaError} - `DaytonaError` - If the sandbox fails to stop within the timeout period.
290
+ */
291
+ public async waitUntilStopped(timeout = 60) {
292
+ if (timeout < 0) {
293
+ throw new DaytonaError('Timeout must be a non-negative number')
294
+ }
295
+
296
+ const checkInterval = 100 // Wait 100 ms between checks
297
+ const startTime = Date.now()
298
+
299
+ while (this.state !== 'stopped') {
300
+ await this.refreshData()
301
+
302
+ // @ts-expect-error this.refreshData() can modify this.state so this check is fine
303
+ if (this.state === 'stopped') {
304
+ return
305
+ }
306
+
307
+ if (this.state === 'error') {
308
+ const errMsg = `Sandbox failed to stop with status: ${this.state}, error reason: ${this.errorReason}`
309
+ throw new DaytonaError(errMsg)
310
+ }
311
+
312
+ if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
313
+ throw new DaytonaError('Sandbox failed to become stopped within the timeout period')
314
+ }
315
+
316
+ await new Promise((resolve) => setTimeout(resolve, checkInterval))
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Refreshes the Sandbox data from the API.
322
+ *
323
+ * @returns {Promise<void>}
324
+ *
325
+ * @example
326
+ * await sandbox.refreshData();
327
+ * console.log(`Sandbox ${sandbox.id}:`);
328
+ * console.log(`State: ${sandbox.state}`);
329
+ * console.log(`Resources: ${sandbox.cpu} CPU, ${sandbox.memory} GiB RAM`);
330
+ */
331
+ public async refreshData(): Promise<void> {
332
+ const response = await this.sandboxApi.getSandbox(this.id)
333
+ this.processSandboxDto(response.data)
334
+ }
335
+
336
+ /**
337
+ * Set the auto-stop interval for the Sandbox.
338
+ *
339
+ * The Sandbox will automatically stop after being idle (no new events) for the specified interval.
340
+ * Events include any state changes or interactions with the Sandbox through the sdk.
341
+ * Interactions using Sandbox Previews are not included.
342
+ *
343
+ * @param {number} interval - Number of minutes of inactivity before auto-stopping.
344
+ * Set to 0 to disable auto-stop. Default is 15 minutes.
345
+ * @returns {Promise<void>}
346
+ * @throws {DaytonaError} - `DaytonaError` - If interval is not a non-negative integer
347
+ *
348
+ * @example
349
+ * // Auto-stop after 1 hour
350
+ * await sandbox.setAutostopInterval(60);
351
+ * // Or disable auto-stop
352
+ * await sandbox.setAutostopInterval(0);
353
+ */
354
+ public async setAutostopInterval(interval: number): Promise<void> {
355
+ if (!Number.isInteger(interval) || interval < 0) {
356
+ throw new DaytonaError('autoStopInterval must be a non-negative integer')
357
+ }
358
+
359
+ await this.sandboxApi.setAutostopInterval(this.id, interval)
360
+ this.autoStopInterval = interval
361
+ }
362
+
363
+ /**
364
+ * Set the auto-archive interval for the Sandbox.
365
+ *
366
+ * The Sandbox will automatically archive after being continuously stopped for the specified interval.
367
+ *
368
+ * @param {number} interval - Number of minutes after which a continuously stopped Sandbox will be auto-archived.
369
+ * Set to 0 for the maximum interval. Default is 7 days.
370
+ * @returns {Promise<void>}
371
+ * @throws {DaytonaError} - `DaytonaError` - If interval is not a non-negative integer
372
+ *
373
+ * @example
374
+ * // Auto-archive after 1 hour
375
+ * await sandbox.setAutoArchiveInterval(60);
376
+ * // Or use the maximum interval
377
+ * await sandbox.setAutoArchiveInterval(0);
378
+ */
379
+ public async setAutoArchiveInterval(interval: number): Promise<void> {
380
+ if (!Number.isInteger(interval) || interval < 0) {
381
+ throw new DaytonaError('autoArchiveInterval must be a non-negative integer')
382
+ }
383
+ await this.sandboxApi.setAutoArchiveInterval(this.id, interval)
384
+ this.autoArchiveInterval = interval
385
+ }
386
+
387
+ /**
388
+ * Set the auto-delete interval for the Sandbox.
389
+ *
390
+ * The Sandbox will automatically delete after being continuously stopped for the specified interval.
391
+ *
392
+ * @param {number} interval - Number of minutes after which a continuously stopped Sandbox will be auto-deleted.
393
+ * Set to negative value to disable auto-delete. Set to 0 to delete immediately upon stopping.
394
+ * By default, auto-delete is disabled.
395
+ * @returns {Promise<void>}
396
+ *
397
+ * @example
398
+ * // Auto-delete after 1 hour
399
+ * await sandbox.setAutoDeleteInterval(60);
400
+ * // Or delete immediately upon stopping
401
+ * await sandbox.setAutoDeleteInterval(0);
402
+ * // Or disable auto-delete
403
+ * await sandbox.setAutoDeleteInterval(-1);
404
+ */
405
+ public async setAutoDeleteInterval(interval: number): Promise<void> {
406
+ await this.sandboxApi.setAutoDeleteInterval(this.id, interval)
407
+ this.autoDeleteInterval = interval
408
+ }
409
+
410
+ /**
411
+ * Retrieves the preview link for the sandbox at the specified port. If the port is closed,
412
+ * it will be opened automatically. For private sandboxes, a token is included to grant access
413
+ * to the URL.
414
+ *
415
+ * @param {number} port - The port to open the preview link on.
416
+ * @returns {PortPreviewUrl} The response object for the preview link, which includes the `url`
417
+ * and the `token` (to access private sandboxes).
418
+ *
419
+ * @example
420
+ * const previewLink = await sandbox.getPreviewLink(3000);
421
+ * console.log(`Preview URL: ${previewLink.url}`);
422
+ * console.log(`Token: ${previewLink.token}`);
423
+ */
424
+ public async getPreviewLink(port: number): Promise<PortPreviewUrl> {
425
+ return (await this.sandboxApi.getPortPreviewUrl(this.id, port)).data
426
+ }
427
+
428
+ /**
429
+ * Archives the sandbox, making it inactive and preserving its state. When sandboxes are archived, the entire filesystem
430
+ * state is moved to cost-effective object storage, making it possible to keep sandboxes available for an extended period.
431
+ * The tradeoff between archived and stopped states is that starting an archived sandbox takes more time, depending on its size.
432
+ * Sandbox must be stopped before archiving.
433
+ */
434
+ public async archive(): Promise<void> {
435
+ await this.sandboxApi.archiveSandbox(this.id)
436
+ await this.refreshData()
437
+ }
438
+
439
+ private async getRootDir(): Promise<string> {
440
+ if (!this.rootDir) {
441
+ this.rootDir = (await this.getUserRootDir()) || ''
442
+ }
443
+ return this.rootDir
444
+ }
445
+
446
+ /**
447
+ * Assigns the API sandbox data to the Sandbox object.
448
+ *
449
+ * @param {SandboxDto} sandboxDto - The API sandbox instance to assign data from
450
+ * @returns {void}
451
+ */
452
+ private processSandboxDto(sandboxDto: SandboxDto) {
453
+ this.id = sandboxDto.id
454
+ this.organizationId = sandboxDto.organizationId
455
+ this.snapshot = sandboxDto.snapshot
456
+ this.user = sandboxDto.user
457
+ this.env = sandboxDto.env
458
+ this.labels = sandboxDto.labels
459
+ this.public = sandboxDto.public
460
+ this.target = sandboxDto.target
461
+ this.cpu = sandboxDto.cpu
462
+ this.gpu = sandboxDto.gpu
463
+ this.memory = sandboxDto.memory
464
+ this.disk = sandboxDto.disk
465
+ this.state = sandboxDto.state
466
+ this.errorReason = sandboxDto.errorReason
467
+ this.backupState = sandboxDto.backupState
468
+ this.backupCreatedAt = sandboxDto.backupCreatedAt
469
+ this.autoStopInterval = sandboxDto.autoStopInterval
470
+ this.autoArchiveInterval = sandboxDto.autoArchiveInterval
471
+ this.autoDeleteInterval = sandboxDto.autoDeleteInterval
472
+ this.runnerDomain = sandboxDto.runnerDomain
473
+ this.volumes = sandboxDto.volumes
474
+ this.buildInfo = sandboxDto.buildInfo
475
+ this.createdAt = sandboxDto.createdAt
476
+ this.updatedAt = sandboxDto.updatedAt
477
+ }
478
+ }