-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathdevbox.ts
More file actions
1080 lines (1026 loc) · 39 KB
/
Copy pathdevbox.ts
File metadata and controls
1080 lines (1026 loc) · 39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { Runloop } from '../index';
import { RunloopError } from '../error';
import type * as Core from '../core';
import type {
DevboxView,
DevboxCreateParams,
DevboxAsyncExecutionDetailView,
DevboxSnapshotDiskParams,
DevboxEnableTunnelParams,
DevboxRemoveTunnelParams,
DevboxReadFileContentsParams,
DevboxWriteFileContentsParams,
DevboxDownloadFileParams,
DevboxUploadFileParams,
DevboxExecuteParams,
DevboxExecuteAsyncParams,
DevboxSnapshotView,
DevboxKeepAliveResponse,
TunnelView,
} from '../resources/devboxes/devboxes';
import type { DevboxLogsListView, LogListParams } from '../resources/devboxes/logs';
import { LongPollRequestOptions, PollingOptions } from '../lib/polling';
import { Snapshot } from './snapshot';
import { Execution } from './execution';
import { ExecutionResult } from './execution-result';
import { uuidv7 } from 'uuidv7';
// Re-export Execution and ExecutionResult for Devbox namespace
export { Execution } from './execution';
export { ExecutionResult } from './execution-result';
/**
* Streaming callbacks for real-time log processing.
*
* @category Execution Types
*/
export interface ExecuteStreamingCallbacks {
/** Callback invoked for each stdout log line */
stdout?: (line: string) => void;
/** Callback invoked for each stderr log line */
stderr?: (line: string) => void;
/** Callback invoked for all log lines (both stdout and stderr) */
output?: (line: string) => void;
}
/**
* Network operations for a devbox.
* Provides methods for managing SSH keys and network tunnels.
*
* @category Devbox
*/
export class DevboxNetOps {
/**
* @private
*/
constructor(
private client: Runloop,
private devboxId: string,
) {}
/**
* Create an SSH key for remote access to the devbox. The public key is installed on the devbox and the private key is returned.
* The key can be used to SSH into the devbox. To use this you must add the private key to your SSH agent and configure it like this:
*
* The ssh user is the same user as defined in the {@link DevboxCreateParams.launch_parameters.user_parameters user parameters} of the {@link DevboxCreateParams devbox creation parameters}.
*
* A special proxy command is required to allow SSH through the proxy. This is because the devbox is behind a proxy and the SSH client needs to be able to connect to the devbox through the proxy.
* The proxy command is:
* ```
* openssl s_client -quiet -servername %h -connect {sshUrl} 2>/dev/null
* ```
* This command uses the OpenSSL library to connect to the devbox through the proxy.
* The `-quiet` flag is used to suppress the output of the OpenSSL library.
* The `-servername %h` flag is used to specify the server name to connect to.
* The `-connect {sshUrl}` flag is used to specify the URL to connect to.
* The `2>/dev/null` flag is used to suppress the output of the OpenSSL library.
*
* @example
* ```typescript
* const sshKeyResponse = await devbox.net.createSSHKey();
* const sshUrl = sshKeyResponse.url;
* const sshKey = sshKeyResponse.ssh_private_key;
* ```
*
* **NOTE:** The ssh user is the same user defined in the {@link DevboxCreateParams.launch_parameters} launch parameters.
*
* ssh-config example:
* ```
*
* Host {devbox-id}
* Hostname {sshKeyResponse.url}
* User {user} # the user defined in the devbox params
* IdentityFile {keyfile_path} # the path to the `sshKeyResponse.sshKey` private key
* ProxyCommand openssl s_client -quiet -servername %h -connect ssh.runloop.pro:443 2>/dev/null # required to allow SSH through the proxy
* ```
*
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxCreateSSHKeyResponse>} SSH key creation result
*/
async createSSHKey(options?: Core.RequestOptions) {
return this.client.devboxes.createSSHKey(this.devboxId, options);
}
/**
* Enable a V2 tunnel for the devbox. V2 tunnels provide encrypted URL-based access
* to the devbox without exposing internal IDs. The tunnel URL format is:
* `https://{port}-{tunnel_key}.tunnel.runloop.ai`
*
* Each devbox can have one tunnel. Tunnels can be configured with different
* authentication modes:
* - `open`: No authentication required (default), returns `auth_mode: 'open'`
* - `authenticated`: Requires a token for access, returns `auth_token`
*
* @example
* ```typescript
* // Enable a public tunnel
* const tunnel = await devbox.net.enableTunnel();
* console.log(`Tunnel URL: https://8080-${tunnel.tunnel_key}.tunnel.runloop.ai`);
*
* // Enable an authenticated tunnel
* const authTunnel = await devbox.net.enableTunnel({ auth_mode: 'authenticated' });
* console.log(`Auth token: ${authTunnel.auth_token}`);
* ```
*
* @param {DevboxEnableTunnelParams} [params] - Optional tunnel configuration
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<TunnelView>} Tunnel details including tunnel_key and auth configuration
*/
async enableTunnel(params?: DevboxEnableTunnelParams, options?: Core.RequestOptions) {
return this.client.devboxes.enableTunnel(this.devboxId, params, options);
}
/**
* @deprecated Only works with legacy tunnels created via {@link createTunnel}.
* V2 tunnels (from {@link enableTunnel}) remain active until devbox shutdown and cannot be removed.
*
* Remove a legacy tunnel from the devbox.
*
* @example
* ```typescript
* // Deprecated - only for legacy tunnels
* await devbox.net.removeTunnel({ port: 8080 });
* ```
*
* @param {DevboxRemoveTunnelParams} params - Tunnel removal parameters including port
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxRemoveTunnelResponse>} Tunnel removal result
*/
async removeTunnel(params: DevboxRemoveTunnelParams, options?: Core.RequestOptions) {
return this.client.devboxes.removeTunnel(this.devboxId, params, options);
}
}
/**
* Command execution operations for a devbox.
* Provides methods for executing commands synchronously and asynchronously.
*
* @category Devbox
*/
export class DevboxCmdOps {
/**
* @private
*/
constructor(
private client: Runloop,
private devboxId: string,
private startStreamingWithCallbacks: (
executionId: string,
stdout?: (line: string) => void,
stderr?: (line: string) => void,
output?: (line: string) => void,
) => Promise<void>,
) {}
/**
* Execute a command on the devbox and wait for it to complete.
* Optionally provide callbacks to stream logs in real-time.
*
* When callbacks are provided, this method waits for both the command to complete
* AND all streaming data to be processed before returning.
*
* @example
* ```typescript
* // Simple execution
* const result = await devbox.cmd.exec('ls -la');
* console.log(await result.stdout());
*
* // With streaming callbacks
* const result = await devbox.cmd.exec('npm install', {
* stdout: (line) => process.stdout.write(line),
* stderr: (line) => process.stderr.write(line),
* });
* ```
*
* @param {string} command - The command to execute
* @param {Omit<DevboxExecuteParams, 'command' | 'command_id'> & ExecuteStreamingCallbacks} [params] - Optional parameters including shell name and callbacks
* @param {LongPollRequestOptions<DevboxAsyncExecutionDetailView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<ExecutionResult>} {@link ExecutionResult} with stdout, stderr, and exit status
*/
async exec(
command: string,
params?: Omit<DevboxExecuteParams, 'command' | 'command_id'> & ExecuteStreamingCallbacks,
options?: LongPollRequestOptions<DevboxAsyncExecutionDetailView>,
): Promise<ExecutionResult> {
const fullParams = { ...params, command };
const hasCallbacks = fullParams.stdout || fullParams.stderr || fullParams.output;
if (hasCallbacks) {
// With callbacks: use async execution workflow to enable streaming
const { stdout, stderr, output, ...executeParams } = fullParams;
const execution = await this.client.devboxes.executeAsync(this.devboxId, executeParams, options);
// Start streaming and await both completion and streaming
const streamingPromise = this.startStreamingWithCallbacks(
execution.execution_id,
stdout,
stderr,
output,
);
// Wait for both command completion and streaming to finish (using allSettled for robustness)
const results = await Promise.allSettled([
this.client.devboxes.executions.awaitCompleted(this.devboxId, execution.execution_id, options),
streamingPromise,
]);
// Extract command result (throw if it failed, ignore streaming errors)
if (results[0].status === 'rejected') {
throw results[0].reason;
}
const result = results[0].value;
return new ExecutionResult(this.client, this.devboxId, execution.execution_id, result);
} else {
// Without callbacks: use existing optimized workflow
const result = await this.client.devboxes.executeAndAwaitCompletion(this.devboxId, fullParams, options);
return new ExecutionResult(this.client, this.devboxId, result.execution_id, result);
}
}
/**
* Execute a command asynchronously without waiting for completion.
* Optionally provide callbacks to stream logs in real-time as they are produced.
*
* Callbacks fire in real-time as logs arrive. When you call execution.result(),
* it will wait for both the command to complete and all streaming to finish.
*
* @example
* ```typescript
* const execution = await devbox.cmd.execAsync('long-running-task.sh', {
* stdout: (line) => console.log(`[LOG] ${line}`),
* });
*
* // Do other work while command runs...
*
* const result = await execution.result();
* if (result.success) {
* console.log('Task completed successfully!');
* }
* ```
*
* @param {string} command - The command to execute
* @param {Omit<DevboxExecuteAsyncParams, 'command'> & ExecuteStreamingCallbacks} [params] - Optional parameters including shell name and callbacks
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<Execution>} {@link Execution} object for tracking and controlling the command
*/
async execAsync(
command: string,
params?: Omit<DevboxExecuteAsyncParams, 'command'> & ExecuteStreamingCallbacks,
options?: Core.RequestOptions,
): Promise<Execution> {
const fullParams = { ...params, command };
const { stdout, stderr, output, ...executeParams } = fullParams;
const execution = await this.client.devboxes.executeAsync(this.devboxId, executeParams, options);
// Start streaming in background if callbacks provided
let streamingPromise: Promise<void> | undefined;
if (stdout || stderr || output) {
// Start streaming - will be awaited when result() is called
streamingPromise = this.startStreamingWithCallbacks(execution.execution_id, stdout, stderr, output);
}
return new Execution(this.client, this.devboxId, execution.execution_id, execution, streamingPromise);
}
}
/**
* Named shell operations for a devbox.
* Provides methods for executing commands in a persistent, stateful shell session.
*
* @category Devbox
*
* @remarks
* Use {@link Devbox.shell} to create a named shell instance. If you use the same shell name,
* it will re-attach to the existing named shell, preserving its state (environment variables,
* current working directory, etc.).
*
* Named shells are stateful and maintain environment variables and the current working directory (CWD)
* across commands. Commands executed through the same
* named shell instance will execute sequentially - the shell can only run one command at a time with
* automatic queuing. This ensures that environment changes and directory changes from one command
* are preserved for the next command.
*
* @example
* ```typescript
* // Create a named shell
* const shell = devbox.shell('my-session');
*
* // Commands execute sequentially and share state
* await shell.exec('cd /app');
* await shell.exec('export MY_VAR=value');
* await shell.exec('echo $MY_VAR'); // Will output 'value' because env is preserved
* await shell.exec('pwd'); // Will output '/app' because CWD is preserved
* ```
*/
export class DevboxNamedShell {
/**
* @private
*/
constructor(
private devbox: Devbox,
private shellName: string,
) {}
/**
* Execute a command in the named shell and wait for it to complete.
* Optionally provide callbacks to stream logs in real-time.
*
* The command will execute in the persistent shell session, maintaining environment variables
* and the current working directory from previous commands. Commands are queued and execute
* sequentially - only one command runs at a time in the named shell.
*
* When callbacks are provided, this method waits for both the command to complete
* AND all streaming data to be processed before returning.
*
* @example
* ```typescript
* const shell = devbox.shell('my-session');
*
* // Simple execution
* const result = await shell.exec('ls -la');
* console.log(await result.stdout());
*
* // With streaming callbacks
* const result = await shell.exec('npm install', {
* stdout: (line) => process.stdout.write(line),
* stderr: (line) => process.stderr.write(line),
* });
*
* // Stateful execution - environment and CWD are preserved
* await shell.exec('cd /app');
* await shell.exec('export NODE_ENV=production');
* const result = await shell.exec('npm start'); // Runs in /app with NODE_ENV=production
* ```
*
* @param {string} command - The command to execute
* @param {Omit<DevboxExecuteParams, 'command' | 'command_id' | 'shell_name'> & ExecuteStreamingCallbacks} [params] - Optional parameters (shell_name is automatically set)
* @param {LongPollRequestOptions<DevboxAsyncExecutionDetailView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<ExecutionResult>} {@link ExecutionResult} with stdout, stderr, and exit status
*/
async exec(
command: string,
params?: Omit<DevboxExecuteParams, 'command' | 'command_id' | 'shell_name'> & ExecuteStreamingCallbacks,
options?: LongPollRequestOptions<DevboxAsyncExecutionDetailView>,
): Promise<ExecutionResult> {
return this.devbox.cmd.exec(command, { ...params, shell_name: this.shellName }, options);
}
/**
* Execute a command in the named shell asynchronously without waiting for completion.
* Optionally provide callbacks to stream logs in real-time as they are produced.
*
* The command will execute in the persistent shell session, maintaining environment variables
* and the current working directory from previous commands. Commands are queued and execute
* sequentially - only one command runs at a time in the named shell.
*
* Callbacks fire in real-time as logs arrive. When you call execution.result(),
* it will wait for both the command to complete and all streaming to finish.
*
* @example
* ```typescript
* const shell = devbox.shell('my-session');
*
* const execution = await shell.execAsync('long-running-task.sh', {
* stdout: (line) => console.log(`[LOG] ${line}`),
* });
*
* // Do other work while command runs...
* // Note: if you call shell.exec() or shell.execAsync() again, it will queue
* // and wait for this command to complete first
*
* const result = await execution.result();
* if (result.success) {
* console.log('Task completed successfully!');
* }
* ```
*
* @param {string} command - The command to execute
* @param {Omit<DevboxExecuteAsyncParams, 'command' | 'shell_name'> & ExecuteStreamingCallbacks} [params] - Optional parameters (shell_name is automatically set)
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<Execution>} {@link Execution} object for tracking and controlling the command
*/
async execAsync(
command: string,
params?: Omit<DevboxExecuteAsyncParams, 'command' | 'shell_name'> & ExecuteStreamingCallbacks,
options?: Core.RequestOptions,
): Promise<Execution> {
return this.devbox.cmd.execAsync(command, { ...params, shell_name: this.shellName }, options);
}
}
/**
* File operations for a devbox.
* Provides methods for reading, writing, uploading, and downloading files.
*
* @category Devbox
*/
export class DevboxFileOps {
/**
* @private
*/
constructor(
private client: Runloop,
private devboxId: string,
) {}
/**
* Read file contents from the devbox as a UTF-8 string.
*
* @example
* ```typescript
* const content = await devbox.file.read({ file_path: '/app/config.json' });
* const config = JSON.parse(content);
* ```
*
* @param {DevboxReadFileContentsParams} params - Parameters containing the file path
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<string>} File contents as a string
*/
async read(params: DevboxReadFileContentsParams, options?: Core.RequestOptions): Promise<string> {
return this.client.devboxes.readFileContents(this.devboxId, params, options);
}
/**
* Write UTF-8 string contents to a file on the devbox.
*
* @example
* ```typescript
* await devbox.file.write({
* path: '/app/config.json',
* contents: JSON.stringify({ key: 'value' }, null, 2),
* });
* ```
*
* @param {DevboxWriteFileContentsParams} params - Parameters containing the file path and contents
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxWriteFileContentsResponse>} Execution result
*/
async write(params: DevboxWriteFileContentsParams, options?: Core.RequestOptions) {
return this.client.devboxes.writeFileContents(this.devboxId, params, options);
}
/**
* Download file contents (supports binary files).
*
* @example
* ```typescript
* const response = await devbox.file.download({ path: '/app/data.bin' });
* const blob = await response.blob();
* // Process binary data...
* ```
*
* @param {DevboxDownloadFileParams} params - Parameters containing the file path
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxDownloadFileResponse>} Download file response
*/
async download(params: DevboxDownloadFileParams, options?: Core.RequestOptions) {
return this.client.devboxes.downloadFile(this.devboxId, params, options);
}
/**
* Upload a file to the devbox.
*
* @example
* ```typescript
* const file = new File(['content'], 'data.txt');
* await devbox.file.upload({
* path: '/app/data.txt',
* file: file,
* });
* ```
*
* @param {DevboxUploadFileParams} params - Parameters containing the file path and file to upload
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxUploadFileResponse>} Upload result
*/
async upload(params: DevboxUploadFileParams, options?: Core.RequestOptions) {
return this.client.devboxes.uploadFile(this.devboxId, params, options);
}
}
/**
* Object-oriented interface for working with Devboxes.
*
* @category Devbox
*
* @remarks
* ## Overview
*
* The `Devbox` class provides a high-level, object-oriented API for managing devboxes.
* Devboxes are containers that run your code in a consistent environment. They have the the following categories of operations:
* - {@link DevboxNetOps net} - Network operations
* - {@link DevboxCmdOps cmd} - Command execution operations
* - {@link DevboxFileOps file} - File operations
*
* ## Quickstart
*
* ```typescript
* import { RunloopSDK } from '@runloop/api-client-ts';
*
* const runloop = new RunloopSDK();
* const devbox = await runloop.devbox.create({ name: 'my-devbox' });
* devbox.cmd.exec('echo "Hello, World!"');
* ...
* ```
*
*/
export class Devbox {
private client: Runloop;
private _id: string;
/**
* Network operations on the devbox.
*/
public readonly net: DevboxNetOps;
/**
* Command execution operations on the devbox.
*/
public readonly cmd: DevboxCmdOps;
/**
* File operations on the devbox.
*/
public readonly file: DevboxFileOps;
private constructor(client: Runloop, id: string) {
this.client = client;
this._id = id;
this.net = new DevboxNetOps(this.client, this._id);
this.cmd = new DevboxCmdOps(this.client, this._id, this.startStreamingWithCallbacks.bind(this));
this.file = new DevboxFileOps(this.client, this._id);
}
/**
* Create a new Devbox and wait for it to reach the running state.
* This is the recommended way to create a devbox as it ensures it's ready to use.
*
* See the {@link DevboxOps.create} method for calling this
* @private
*
* @example
* ```typescript
* const runloop = new RunloopSDK();
* const devbox = await runloop.devbox.create({ name: 'my-devbox' });
*
* devbox.cmd.exec('echo "Hello, World!"');
* ...
* ```
*
* @param {Runloop} client - The Runloop client instance
* @param {DevboxCreateParams} [params] - Parameters for creating the devbox
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance in the running state
*/
static async create(
client: Runloop,
params?: DevboxCreateParams,
options?: LongPollRequestOptions<DevboxView>,
): Promise<Devbox> {
const devboxData = await client.devboxes.createAndAwaitRunning(params, options);
return new Devbox(client, devboxData.id);
}
/**
* Create a new Devbox from a Blueprint and wait for it to reach the running state.
*
* See the {@link DevboxOps.createFromBlueprintId} method for calling this
* @private
*
* @param {Runloop} client - The Runloop client instance
* @param {string} blueprintId - The blueprint ID to create from
* @param {Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>} [params] - Additional devbox creation parameters
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance in the running state
*/
static async createFromBlueprintId(
client: Runloop,
blueprintId: string,
params?: Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
): Promise<Devbox> {
const createParams: DevboxCreateParams = {
...params,
blueprint_id: blueprintId,
};
const devboxData = await client.devboxes.createAndAwaitRunning(createParams, options);
return new Devbox(client, devboxData.id);
}
/**
* Create a new Devbox from a Blueprint name and wait for it to reach the running state.
*
* See the {@link DevboxOps.createFromBlueprintName} method for calling this
* @private
*
* @param {Runloop} client - The Runloop client instance
* @param {string} blueprintName - The blueprint name to create from
* @param {Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>} [params] - Additional devbox creation parameters
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance in the running state
*/
static async createFromBlueprintName(
client: Runloop,
blueprintName: string,
params?: Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
): Promise<Devbox> {
const createParams: DevboxCreateParams = {
...params,
blueprint_name: blueprintName,
};
const devboxData = await client.devboxes.createAndAwaitRunning(createParams, options);
return new Devbox(client, devboxData.id);
}
/**
* Create a new Devbox from a Snapshot and wait for it to reach the running state.
*
* See the {@link DevboxOps.createFromSnapshot} method for calling this
* @private
*
* @example
* ```typescript
* const devbox = await Devbox.createFromSnapshot(
* runloop,
* snapshot.id,
* { name: 'restored-devbox' }
* );
* ```
*
* @param {Runloop} client - The Runloop client instance
* @param {string} snapshotId - The snapshot ID to create from
* @param {Omit<DevboxCreateParams, 'snapshot_id' | 'blueprint_id' | 'blueprint_name'>} [params] - Additional devbox creation parameters
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance in the running state
*/
static async createFromSnapshot(
client: Runloop,
snapshotId: string,
params?: Omit<DevboxCreateParams, 'snapshot_id' | 'blueprint_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
): Promise<Devbox> {
const createParams: DevboxCreateParams = {
...params,
snapshot_id: snapshotId,
};
const devboxData = await client.devboxes.createAndAwaitRunning(createParams, options);
return new Devbox(client, devboxData.id);
}
/**
* Create a Devbox instance by ID without retrieving from API.
* Use getInfo() to fetch the actual data when needed.
*
* See the {@link DevboxOps.fromId} method for calling this
* @private
*
* @example
* ```typescript
* const devbox = Devbox.fromId(runloop, 'devbox-123');
* const info = await devbox.getInfo();
* ```
*
* @param {Runloop} client - The Runloop client instance
* @param {string} id - The devbox ID
* @returns {Devbox} A {@link Devbox} instance
*/
static fromId(client: Runloop, id: string): Devbox {
return new Devbox(client, id);
}
/**
* Get the devbox ID.
* @returns {string} The devbox ID
*/
get id(): string {
return this._id;
}
/**
* Create a named shell instance for stateful command execution.
*
* Named shells are stateful and maintain environment variables and the current working directory (CWD)
* across commands, just like a real shell on your local computer. Commands executed through the same
* named shell instance will execute sequentially - the shell can only run one command at a time with
* automatic queuing. This ensures that environment changes and directory changes from one command
* are preserved for the next command.
*
* @example
* ```typescript
* // Create a named shell with a custom name
* const shell = devbox.shell('my-session');
*
* // Create a named shell with an auto-generated UUID name
* const shell2 = devbox.shell();
*
* // Commands execute sequentially and share state
* await shell.exec('cd /app');
* await shell.exec('export MY_VAR=value');
* await shell.exec('echo $MY_VAR'); // Will output 'value' because env is preserved
* await shell.exec('pwd'); // Will output '/app' because CWD is preserved
* ```
*
* @param {string} [shellName] - The name of the persistent shell session. If not provided, a UUID will be generated automatically.
* @returns {DevboxNamedShell} A {@link DevboxNamedShell} instance for executing commands in the named shell
*/
shell(shellName: string = uuidv7()): DevboxNamedShell {
return new DevboxNamedShell(this, shellName);
}
/**
* Start streaming logs with callbacks.
* Returns a promise that resolves when all streams complete.
* Uses SSE streams from the old SDK with auto-reconnect.
*
* @private
* @param {string} executionId - The execution ID to stream logs for
* @param {(line: string) => void} [stdout] - Callback for stdout log lines
* @param {(line: string) => void} [stderr] - Callback for stderr log lines
* @param {(line: string) => void} [output] - Callback for all log lines (both stdout and stderr)
* @returns {Promise<void>} Promise that resolves when all streams complete
*/
private startStreamingWithCallbacks(
executionId: string,
stdout?: (line: string) => void,
stderr?: (line: string) => void,
output?: (line: string) => void,
): Promise<void> {
const streamingPromises: Promise<void>[] = [];
// Stream stdout if stdout or output callback provided
if (stdout || output) {
const stdoutPromise = (async () => {
try {
const stream = await this.client.devboxes.executions.streamStdoutUpdates(this._id, executionId, {});
for await (const chunk of stream) {
if (stdout) stdout(chunk.output);
if (output) output(chunk.output);
}
} catch (error) {
// Silently handle streaming errors - don't block execution completion
console.error('Error streaming stdout:', error);
}
})();
streamingPromises.push(stdoutPromise);
}
// Stream stderr if stderr or output callback provided
if (stderr || output) {
const stderrPromise = (async () => {
try {
const stream = await this.client.devboxes.executions.streamStderrUpdates(this._id, executionId, {});
for await (const chunk of stream) {
if (stderr) stderr(chunk.output);
if (output) output(chunk.output);
}
} catch (error) {
// Silently handle streaming errors - don't block execution completion
console.error('Error streaming stderr:', error);
}
})();
streamingPromises.push(stderrPromise);
}
// Return promise that resolves when all streams complete
return Promise.allSettled(streamingPromises).then(() => undefined);
}
/**
* Get the complete devbox data from the API.
*
* @example
* ```typescript
* const info = await devbox.getInfo();
* console.log(`Devbox name: ${info.name}, status: ${info.status}`);
* ```
*
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxView>} The devbox data
*/
async getInfo(options?: Core.RequestOptions): Promise<DevboxView> {
return this.client.devboxes.retrieve(this._id, options);
}
/**
* Get the tunnel information for this devbox.
* Returns null if no tunnel has been enabled.
*
* @example
* ```typescript
* const tunnel = await devbox.getTunnel();
* if (tunnel) {
* console.log(`Tunnel key: ${tunnel.tunnel_key}`);
* }
* ```
*
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<TunnelView | null>} The tunnel information, or null if no tunnel exists
*/
async getTunnel(options?: Core.RequestOptions): Promise<TunnelView | null> {
const info = await this.getInfo(options);
return info.tunnel ?? null;
}
/**
* Get the tunnel URL for a specific port.
* Throws an error if no tunnel has been enabled.
*
* @example
* ```typescript
* const url = await devbox.getTunnelUrl(8080);
* console.log(`Access your app at: ${url}`);
* // Output: https://8080-abc123xyz.tunnel.runloop.ai
* ```
*
* @param {number} port - The port number to construct the URL for
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<string>} The tunnel URL for the specified port
* @throws {BadRequestError} If no tunnel has been enabled for this devbox
*/
async getTunnelUrl(port: number, options?: Core.RequestOptions): Promise<string> {
const tunnel = await this.getTunnel(options);
if (!tunnel) {
throw new RunloopError('No tunnel has been enabled for this devbox. Call net.enableTunnel() first.');
}
return `https://${port}-${tunnel.tunnel_key}.tunnel.runloop.ai`;
}
/**
* Get all logs from a running or completed devbox.
* Optionally filter by execution ID or shell name.
*
* @example
* ```typescript
* const logs = await devbox.logs();
* for (const log of logs.logs) {
* console.log(`[${log.level}] ${log.message}`);
* }
*
* // Filter by execution ID
* const execLogs = await devbox.logs({ execution_id: 'exec-123' });
*
* // Filter by shell name
* const shellLogs = await devbox.logs({ shell_name: 'my-shell' });
* ```
*
* @param {LogListParams} [params] - Optional filter parameters (execution_id, shell_name)
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxLogsListView>} The devbox logs
*/
async logs(params?: LogListParams, options?: Core.RequestOptions): Promise<DevboxLogsListView> {
return this.client.devboxes.logs.list(this._id, params, options);
}
/**
* Wait for the devbox to reach the running state.
* Uses optimized server-side polling for better performance.
*
* @example
* ```typescript
* const devbox = Devbox.fromId(runloop, 'devbox-123');
* await devbox.awaitRunning();
* console.log('Devbox is now running');
* ```
*
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<DevboxView>} The devbox data when running state is reached
*/
async awaitRunning(options?: LongPollRequestOptions<DevboxView>): Promise<DevboxView> {
return this.client.devboxes.awaitRunning(this._id, options);
}
/**
* Wait for the devbox to reach the suspended state.
* Uses optimized server-side polling for better performance.
*
* @example
* ```typescript
* await devbox.suspend();
* await devbox.awaitSuspended();
* console.log('Devbox is now suspended');
* ```
*
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<DevboxView>} The devbox data when suspended state is reached
*/
async awaitSuspended(options?: LongPollRequestOptions<DevboxView>): Promise<DevboxView> {
return this.client.devboxes.awaitSuspended(this._id, options);
}
/**
* Create a disk snapshot of the devbox. Returns a snapshot that is completed. If you don't want to block on completion, use snapshotDiskAsync().
*
* @example
* ```typescript
* const snapshot = await devbox.snapshotDisk({ name: 'pre-deployment' });
* console.log(`Snapshot ${snapshot.id} created successfully`);
* ```
* @param {DevboxSnapshotDiskParams} [params] - Snapshot creation parameters
* @param {Core.RequestOptions & { polling?: Partial<PollingOptions<DevboxSnapshotView>> }} [options] - Request options with optional polling configuration
* @returns {Promise<Snapshot>} A completed {@link Snapshot} instance
*/
async snapshotDisk(
params?: DevboxSnapshotDiskParams,
options?: Core.RequestOptions & { polling?: Partial<PollingOptions<DevboxSnapshotView>> },
): Promise<Snapshot> {
const snapshotData = await this.client.devboxes.snapshotDiskAsync(this._id, params, options);
const snapshot = Snapshot.fromId(this.client, snapshotData.id);
await snapshot.awaitCompleted();
return snapshot;
}
/**
* Create a disk snapshot of the devbox asynchronously. Returns a snapshot that is not yet completed but has started. You can await completion using snapshot.awaitCompleted().
*
* @example
* ```typescript
* const snapshot = await devbox.snapshotDiskAsync({ name: 'backup' });
* // Do other work...
* await snapshot.awaitCompleted();
* ```
* @param {DevboxSnapshotDiskParams} [params] - Snapshot creation parameters
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<Snapshot>} A {@link Snapshot} instance that has started but may not be completed
*/
async snapshotDiskAsync(
params?: DevboxSnapshotDiskParams,
options?: Core.RequestOptions,
): Promise<Snapshot> {
const snapshotData = await this.client.devboxes.snapshotDiskAsync(this._id, params, options);
return Snapshot.fromId(this.client, snapshotData.id);
}
/**
* Shutdown the devbox.
*
* @example
* ```typescript
* await devbox.shutdown();
* ```
*
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxShutdownResponse>} Shutdown result
*/
async shutdown(options?: Core.RequestOptions) {
return await this.client.devboxes.shutdown(this._id, options);
}
/**
* Suspend the devbox and create a disk snapshot.
*
* @example
* ```typescript
* await devbox.suspend();
* // Optionally, wait for the devbox to reach the suspended state
* await devbox.awaitSuspended();
* ```
*
* @param {Core.RequestOptions} [options] - Request options
* @returns {Promise<DevboxSuspendResponse>} Suspend result
*/
async suspend(options?: Core.RequestOptions) {
return this.client.devboxes.suspend(this._id, options);
}
/**
* Resume a suspended devbox and wait for it to reach the running state.
*
* @example
* ```typescript
* await devbox.resume();
* // Devbox is now running
* ```
*
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<DevboxView>} The devbox data when running state is reached
*/
async resume(options?: LongPollRequestOptions<DevboxView>): Promise<DevboxView> {
await this.resumeAsync(options);