@cloudflare/sandbox 0.0.0-0608f1e → 0.0.0-12bbd12

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/client.ts CHANGED
@@ -1,41 +1,23 @@
1
+ import type { ExecuteRequest } from "../container_src/types";
1
2
  import type { Sandbox } from "./index";
2
3
  import type {
4
+ BaseExecOptions,
5
+ DeleteFileResponse,
6
+ ExecuteResponse,
3
7
  GetProcessLogsResponse,
4
8
  GetProcessResponse,
9
+ GitCheckoutResponse,
10
+ ListFilesResponse,
5
11
  ListProcessesResponse,
12
+ MkdirResponse,
13
+ MoveFileResponse,
14
+ ReadFileResponse,
15
+ RenameFileResponse,
6
16
  StartProcessRequest,
7
- StartProcessResponse
17
+ StartProcessResponse,
18
+ WriteFileResponse,
8
19
  } from "./types";
9
20
 
10
- interface ExecuteRequest {
11
- command: string;
12
- sessionId?: string;
13
- }
14
-
15
- export interface ExecuteResponse {
16
- success: boolean;
17
- stdout: string;
18
- stderr: string;
19
- exitCode: number;
20
- command: string;
21
- timestamp: string;
22
- }
23
-
24
- interface SessionResponse {
25
- sessionId: string;
26
- message: string;
27
- timestamp: string;
28
- }
29
-
30
- interface SessionListResponse {
31
- sessions: Array<{
32
- sessionId: string;
33
- hasActiveProcess: boolean;
34
- createdAt: string;
35
- }>;
36
- count: number;
37
- timestamp: string;
38
- }
39
21
 
40
22
  interface CommandsResponse {
41
23
  availableCommands: string[];
@@ -49,16 +31,6 @@ interface GitCheckoutRequest {
49
31
  sessionId?: string;
50
32
  }
51
33
 
52
- export interface GitCheckoutResponse {
53
- success: boolean;
54
- stdout: string;
55
- stderr: string;
56
- exitCode: number;
57
- repoUrl: string;
58
- branch: string;
59
- targetDir: string;
60
- timestamp: string;
61
- }
62
34
 
63
35
  interface MkdirRequest {
64
36
  path: string;
@@ -66,15 +38,6 @@ interface MkdirRequest {
66
38
  sessionId?: string;
67
39
  }
68
40
 
69
- export interface MkdirResponse {
70
- success: boolean;
71
- stdout: string;
72
- stderr: string;
73
- exitCode: number;
74
- path: string;
75
- recursive: boolean;
76
- timestamp: string;
77
- }
78
41
 
79
42
  interface WriteFileRequest {
80
43
  path: string;
@@ -83,12 +46,6 @@ interface WriteFileRequest {
83
46
  sessionId?: string;
84
47
  }
85
48
 
86
- export interface WriteFileResponse {
87
- success: boolean;
88
- exitCode: number;
89
- path: string;
90
- timestamp: string;
91
- }
92
49
 
93
50
  interface ReadFileRequest {
94
51
  path: string;
@@ -96,25 +53,12 @@ interface ReadFileRequest {
96
53
  sessionId?: string;
97
54
  }
98
55
 
99
- export interface ReadFileResponse {
100
- success: boolean;
101
- exitCode: number;
102
- path: string;
103
- content: string;
104
- timestamp: string;
105
- }
106
56
 
107
57
  interface DeleteFileRequest {
108
58
  path: string;
109
59
  sessionId?: string;
110
60
  }
111
61
 
112
- export interface DeleteFileResponse {
113
- success: boolean;
114
- exitCode: number;
115
- path: string;
116
- timestamp: string;
117
- }
118
62
 
119
63
  interface RenameFileRequest {
120
64
  oldPath: string;
@@ -122,13 +66,6 @@ interface RenameFileRequest {
122
66
  sessionId?: string;
123
67
  }
124
68
 
125
- export interface RenameFileResponse {
126
- success: boolean;
127
- exitCode: number;
128
- oldPath: string;
129
- newPath: string;
130
- timestamp: string;
131
- }
132
69
 
133
70
  interface MoveFileRequest {
134
71
  sourcePath: string;
@@ -136,14 +73,17 @@ interface MoveFileRequest {
136
73
  sessionId?: string;
137
74
  }
138
75
 
139
- export interface MoveFileResponse {
140
- success: boolean;
141
- exitCode: number;
142
- sourcePath: string;
143
- destinationPath: string;
144
- timestamp: string;
76
+
77
+ interface ListFilesRequest {
78
+ path: string;
79
+ options?: {
80
+ recursive?: boolean;
81
+ includeHidden?: boolean;
82
+ };
83
+ sessionId?: string;
145
84
  }
146
85
 
86
+
147
87
  interface PreviewInfo {
148
88
  url: string;
149
89
  port: number;
@@ -212,7 +152,7 @@ export class HttpClient {
212
152
  this.baseUrl = this.options.baseUrl!;
213
153
  }
214
154
 
215
- private async doFetch(
155
+ protected async doFetch(
216
156
  path: string,
217
157
  options?: RequestInit
218
158
  ): Promise<Response> {
@@ -255,16 +195,19 @@ export class HttpClient {
255
195
 
256
196
  async execute(
257
197
  command: string,
258
- sessionId?: string
198
+ options: Pick<BaseExecOptions, "sessionId" | "cwd" | "env">
259
199
  ): Promise<ExecuteResponse> {
260
200
  try {
261
- const targetSessionId = sessionId || this.sessionId;
201
+ const targetSessionId = options.sessionId || this.sessionId;
202
+ const executeRequest = {
203
+ command,
204
+ sessionId: targetSessionId,
205
+ cwd: options.cwd,
206
+ env: options.env,
207
+ } satisfies ExecuteRequest;
262
208
 
263
209
  const response = await this.doFetch(`/api/execute`, {
264
- body: JSON.stringify({
265
- command,
266
- sessionId: targetSessionId,
267
- } as ExecuteRequest),
210
+ body: JSON.stringify(executeRequest),
268
211
  headers: {
269
212
  "Content-Type": "application/json",
270
213
  },
@@ -305,7 +248,6 @@ export class HttpClient {
305
248
  }
306
249
  }
307
250
 
308
-
309
251
  async executeCommandStream(
310
252
  command: string,
311
253
  sessionId?: string
@@ -320,7 +262,7 @@ export class HttpClient {
320
262
  }),
321
263
  headers: {
322
264
  "Content-Type": "application/json",
323
- "Accept": "text/event-stream",
265
+ Accept: "text/event-stream",
324
266
  },
325
267
  method: "POST",
326
268
  });
@@ -338,9 +280,7 @@ export class HttpClient {
338
280
  throw new Error("No response body for streaming request");
339
281
  }
340
282
 
341
- console.log(
342
- `[HTTP Client] Started command stream: ${command}`
343
- );
283
+ console.log(`[HTTP Client] Started command stream: ${command}`);
344
284
 
345
285
  return response.body;
346
286
  } catch (error) {
@@ -392,7 +332,6 @@ export class HttpClient {
392
332
  }
393
333
  }
394
334
 
395
-
396
335
  async mkdir(
397
336
  path: string,
398
337
  recursive: boolean = false,
@@ -434,7 +373,6 @@ export class HttpClient {
434
373
  }
435
374
  }
436
375
 
437
-
438
376
  async writeFile(
439
377
  path: string,
440
378
  content: string,
@@ -478,7 +416,6 @@ export class HttpClient {
478
416
  }
479
417
  }
480
418
 
481
-
482
419
  async readFile(
483
420
  path: string,
484
421
  encoding: string = "utf-8",
@@ -520,7 +457,6 @@ export class HttpClient {
520
457
  }
521
458
  }
522
459
 
523
-
524
460
  async deleteFile(
525
461
  path: string,
526
462
  sessionId?: string
@@ -560,7 +496,6 @@ export class HttpClient {
560
496
  }
561
497
  }
562
498
 
563
-
564
499
  async renameFile(
565
500
  oldPath: string,
566
501
  newPath: string,
@@ -602,7 +537,6 @@ export class HttpClient {
602
537
  }
603
538
  }
604
539
 
605
-
606
540
  async moveFile(
607
541
  sourcePath: string,
608
542
  destinationPath: string,
@@ -644,6 +578,49 @@ export class HttpClient {
644
578
  }
645
579
  }
646
580
 
581
+ async listFiles(
582
+ path: string,
583
+ options?: {
584
+ recursive?: boolean;
585
+ includeHidden?: boolean;
586
+ },
587
+ sessionId?: string
588
+ ): Promise<ListFilesResponse> {
589
+ try {
590
+ const targetSessionId = sessionId || this.sessionId;
591
+
592
+ const response = await this.doFetch(`/api/list-files`, {
593
+ body: JSON.stringify({
594
+ path,
595
+ options,
596
+ sessionId: targetSessionId,
597
+ } as ListFilesRequest),
598
+ headers: {
599
+ "Content-Type": "application/json",
600
+ },
601
+ method: "POST",
602
+ });
603
+
604
+ if (!response.ok) {
605
+ const errorData = (await response.json().catch(() => ({}))) as {
606
+ error?: string;
607
+ };
608
+ throw new Error(
609
+ errorData.error || `HTTP error! status: ${response.status}`
610
+ );
611
+ }
612
+
613
+ const data: ListFilesResponse = await response.json();
614
+ console.log(
615
+ `[HTTP Client] Listed ${data.files.length} files in: ${path}, Success: ${data.success}`
616
+ );
617
+
618
+ return data;
619
+ } catch (error) {
620
+ console.error("[HTTP Client] Error listing files:", error);
621
+ throw error;
622
+ }
623
+ }
647
624
 
648
625
  async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
649
626
  try {
@@ -670,7 +647,9 @@ export class HttpClient {
670
647
 
671
648
  const data: ExposePortResponse = await response.json();
672
649
  console.log(
673
- `[HTTP Client] Port exposed: ${port}${name ? ` (${name})` : ""}, Success: ${data.success}`
650
+ `[HTTP Client] Port exposed: ${port}${
651
+ name ? ` (${name})` : ""
652
+ }, Success: ${data.success}`
674
653
  );
675
654
 
676
655
  return data;
@@ -732,9 +711,7 @@ export class HttpClient {
732
711
  }
733
712
 
734
713
  const data: GetExposedPortsResponse = await response.json();
735
- console.log(
736
- `[HTTP Client] Got ${data.count} exposed ports`
737
- );
714
+ console.log(`[HTTP Client] Got ${data.count} exposed ports`);
738
715
 
739
716
  return data;
740
717
  } catch (error) {
@@ -871,9 +848,7 @@ export class HttpClient {
871
848
  }
872
849
 
873
850
  const data: ListProcessesResponse = await response.json();
874
- console.log(
875
- `[HTTP Client] Listed ${data.processes.length} processes`
876
- );
851
+ console.log(`[HTTP Client] Listed ${data.processes.length} processes`);
877
852
 
878
853
  return data;
879
854
  } catch (error) {
@@ -902,7 +877,9 @@ export class HttpClient {
902
877
 
903
878
  const data: GetProcessResponse = await response.json();
904
879
  console.log(
905
- `[HTTP Client] Got process ${processId}: ${data.process?.status || 'not found'}`
880
+ `[HTTP Client] Got process ${processId}: ${
881
+ data.process?.status || "not found"
882
+ }`
906
883
  );
907
884
 
908
885
  return data;
@@ -912,7 +889,9 @@ export class HttpClient {
912
889
  }
913
890
  }
914
891
 
915
- async killProcess(processId: string): Promise<{ success: boolean; message: string }> {
892
+ async killProcess(
893
+ processId: string
894
+ ): Promise<{ success: boolean; message: string }> {
916
895
  try {
917
896
  const response = await this.doFetch(`/api/process/${processId}`, {
918
897
  headers: {
@@ -930,10 +909,11 @@ export class HttpClient {
930
909
  );
931
910
  }
932
911
 
933
- const data = await response.json() as { success: boolean; message: string };
934
- console.log(
935
- `[HTTP Client] Killed process ${processId}`
936
- );
912
+ const data = (await response.json()) as {
913
+ success: boolean;
914
+ message: string;
915
+ };
916
+ console.log(`[HTTP Client] Killed process ${processId}`);
937
917
 
938
918
  return data;
939
919
  } catch (error) {
@@ -942,7 +922,11 @@ export class HttpClient {
942
922
  }
943
923
  }
944
924
 
945
- async killAllProcesses(): Promise<{ success: boolean; killedCount: number; message: string }> {
925
+ async killAllProcesses(): Promise<{
926
+ success: boolean;
927
+ killedCount: number;
928
+ message: string;
929
+ }> {
946
930
  try {
947
931
  const response = await this.doFetch("/api/process/kill-all", {
948
932
  headers: {
@@ -960,10 +944,12 @@ export class HttpClient {
960
944
  );
961
945
  }
962
946
 
963
- const data = await response.json() as { success: boolean; killedCount: number; message: string };
964
- console.log(
965
- `[HTTP Client] Killed ${data.killedCount} processes`
966
- );
947
+ const data = (await response.json()) as {
948
+ success: boolean;
949
+ killedCount: number;
950
+ message: string;
951
+ };
952
+ console.log(`[HTTP Client] Killed ${data.killedCount} processes`);
967
953
 
968
954
  return data;
969
955
  } catch (error) {
@@ -991,9 +977,7 @@ export class HttpClient {
991
977
  }
992
978
 
993
979
  const data: GetProcessLogsResponse = await response.json();
994
- console.log(
995
- `[HTTP Client] Got logs for process ${processId}`
996
- );
980
+ console.log(`[HTTP Client] Got logs for process ${processId}`);
997
981
 
998
982
  return data;
999
983
  } catch (error) {
@@ -1002,11 +986,13 @@ export class HttpClient {
1002
986
  }
1003
987
  }
1004
988
 
1005
- async streamProcessLogs(processId: string): Promise<ReadableStream<Uint8Array>> {
989
+ async streamProcessLogs(
990
+ processId: string
991
+ ): Promise<ReadableStream<Uint8Array>> {
1006
992
  try {
1007
993
  const response = await this.doFetch(`/api/process/${processId}/stream`, {
1008
994
  headers: {
1009
- "Accept": "text/event-stream",
995
+ Accept: "text/event-stream",
1010
996
  "Cache-Control": "no-cache",
1011
997
  },
1012
998
  method: "GET",
package/src/errors.ts ADDED
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Standard error response from the sandbox API
3
+ */
4
+ export interface SandboxErrorResponse {
5
+ error?: string;
6
+ status?: string;
7
+ progress?: number;
8
+ }
9
+
10
+ /**
11
+ * Base error class for all Sandbox-related errors
12
+ */
13
+ export class SandboxError extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = this.constructor.name;
17
+
18
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
19
+ if (Error.captureStackTrace) {
20
+ Error.captureStackTrace(this, this.constructor);
21
+ }
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Error thrown when Jupyter functionality is requested but the service is still initializing.
27
+ *
28
+ * Note: With the current implementation, requests wait for Jupyter to be ready.
29
+ * This error is only thrown when:
30
+ * 1. The request times out waiting for Jupyter (default: 30 seconds)
31
+ * 2. Jupyter initialization actually fails
32
+ *
33
+ * Most requests will succeed after a delay, not throw this error.
34
+ */
35
+ export class JupyterNotReadyError extends SandboxError {
36
+ public readonly code = "JUPYTER_NOT_READY";
37
+ public readonly retryAfter: number;
38
+ public readonly progress?: number;
39
+
40
+ constructor(
41
+ message?: string,
42
+ options?: { retryAfter?: number; progress?: number }
43
+ ) {
44
+ super(
45
+ message || "Jupyter is still initializing. Please retry in a few seconds."
46
+ );
47
+ this.retryAfter = options?.retryAfter || 5;
48
+ this.progress = options?.progress;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Error thrown when a context is not found
54
+ */
55
+ export class ContextNotFoundError extends SandboxError {
56
+ public readonly code = "CONTEXT_NOT_FOUND";
57
+ public readonly contextId: string;
58
+
59
+ constructor(contextId: string) {
60
+ super(`Context ${contextId} not found`);
61
+ this.contextId = contextId;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Error thrown when code execution fails
67
+ */
68
+ export class CodeExecutionError extends SandboxError {
69
+ public readonly code = "CODE_EXECUTION_ERROR";
70
+ public readonly executionError?: {
71
+ ename?: string;
72
+ evalue?: string;
73
+ traceback?: string[];
74
+ };
75
+
76
+ constructor(message: string, executionError?: any) {
77
+ super(message);
78
+ this.executionError = executionError;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Error thrown when the sandbox container is not ready
84
+ */
85
+ export class ContainerNotReadyError extends SandboxError {
86
+ public readonly code = "CONTAINER_NOT_READY";
87
+
88
+ constructor(message?: string) {
89
+ super(
90
+ message ||
91
+ "Container is not ready. Please wait for initialization to complete."
92
+ );
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Error thrown when a network request to the sandbox fails
98
+ */
99
+ export class SandboxNetworkError extends SandboxError {
100
+ public readonly code = "NETWORK_ERROR";
101
+ public readonly statusCode?: number;
102
+ public readonly statusText?: string;
103
+
104
+ constructor(message: string, statusCode?: number, statusText?: string) {
105
+ super(message);
106
+ this.statusCode = statusCode;
107
+ this.statusText = statusText;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Error thrown when service is temporarily unavailable (e.g., circuit breaker open)
113
+ */
114
+ export class ServiceUnavailableError extends SandboxError {
115
+ public readonly code = "SERVICE_UNAVAILABLE";
116
+ public readonly retryAfter?: number;
117
+
118
+ constructor(message?: string, retryAfter?: number) {
119
+ // Simple, user-friendly message without implementation details
120
+ super(message || "Service temporarily unavailable");
121
+ this.retryAfter = retryAfter;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Type guard to check if an error is a JupyterNotReadyError
127
+ */
128
+ export function isJupyterNotReadyError(
129
+ error: unknown
130
+ ): error is JupyterNotReadyError {
131
+ return error instanceof JupyterNotReadyError;
132
+ }
133
+
134
+ /**
135
+ * Type guard to check if an error is any SandboxError
136
+ */
137
+ export function isSandboxError(error: unknown): error is SandboxError {
138
+ return error instanceof SandboxError;
139
+ }
140
+
141
+ /**
142
+ * Helper to determine if an error is retryable
143
+ */
144
+ export function isRetryableError(error: unknown): boolean {
145
+ if (
146
+ error instanceof JupyterNotReadyError ||
147
+ error instanceof ContainerNotReadyError ||
148
+ error instanceof ServiceUnavailableError
149
+ ) {
150
+ return true;
151
+ }
152
+
153
+ if (error instanceof SandboxNetworkError) {
154
+ // Retry on 502, 503, 504 (gateway/service unavailable errors)
155
+ return error.statusCode
156
+ ? [502, 503, 504].includes(error.statusCode)
157
+ : false;
158
+ }
159
+
160
+ return false;
161
+ }
162
+
163
+ /**
164
+ * Parse error response from the sandbox API and return appropriate error instance
165
+ */
166
+ export async function parseErrorResponse(
167
+ response: Response
168
+ ): Promise<SandboxError> {
169
+ let data: SandboxErrorResponse;
170
+
171
+ try {
172
+ data = (await response.json()) as SandboxErrorResponse;
173
+ } catch {
174
+ // If JSON parsing fails, return a generic network error
175
+ return new SandboxNetworkError(
176
+ `Request failed with status ${response.status}`,
177
+ response.status,
178
+ response.statusText
179
+ );
180
+ }
181
+
182
+ // Check for specific error types based on response
183
+ if (response.status === 503) {
184
+ // Circuit breaker error
185
+ if (data.status === "circuit_open") {
186
+ return new ServiceUnavailableError(
187
+ "Service temporarily unavailable",
188
+ parseInt(response.headers.get("Retry-After") || "30")
189
+ );
190
+ }
191
+
192
+ // Jupyter initialization error
193
+ if (data.status === "initializing") {
194
+ return new JupyterNotReadyError(data.error, {
195
+ retryAfter: parseInt(response.headers.get("Retry-After") || "5"),
196
+ progress: data.progress,
197
+ });
198
+ }
199
+ }
200
+
201
+ // Check for context not found
202
+ if (
203
+ response.status === 404 &&
204
+ data.error?.includes("Context") &&
205
+ data.error?.includes("not found")
206
+ ) {
207
+ const contextId =
208
+ data.error.match(/Context (\S+) not found/)?.[1] || "unknown";
209
+ return new ContextNotFoundError(contextId);
210
+ }
211
+
212
+ // Default network error
213
+ return new SandboxNetworkError(
214
+ data.error || `Request failed with status ${response.status}`,
215
+ response.status,
216
+ response.statusText
217
+ );
218
+ }