@cloudflare/sandbox 0.3.6 → 0.4.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.
Files changed (120) hide show
  1. package/.turbo/turbo-build.log +44 -0
  2. package/CHANGELOG.md +6 -8
  3. package/Dockerfile +88 -18
  4. package/README.md +89 -824
  5. package/dist/{chunk-JTKON2SH.js → chunk-BCJ7SF3Q.js} +9 -5
  6. package/dist/chunk-BCJ7SF3Q.js.map +1 -0
  7. package/dist/chunk-BFVUNTP4.js +104 -0
  8. package/dist/chunk-BFVUNTP4.js.map +1 -0
  9. package/dist/{chunk-NNGBXDMY.js → chunk-EKSWCBCA.js} +3 -6
  10. package/dist/chunk-EKSWCBCA.js.map +1 -0
  11. package/dist/chunk-HGF554LH.js +2236 -0
  12. package/dist/chunk-HGF554LH.js.map +1 -0
  13. package/dist/{chunk-6UAWTJ5S.js → chunk-Z532A7QC.js} +13 -20
  14. package/dist/{chunk-6UAWTJ5S.js.map → chunk-Z532A7QC.js.map} +1 -1
  15. package/dist/file-stream.d.ts +16 -38
  16. package/dist/file-stream.js +1 -2
  17. package/dist/index.d.ts +6 -5
  18. package/dist/index.js +35 -39
  19. package/dist/index.js.map +1 -1
  20. package/dist/interpreter.d.ts +3 -3
  21. package/dist/interpreter.js +2 -2
  22. package/dist/request-handler.d.ts +4 -3
  23. package/dist/request-handler.js +4 -7
  24. package/dist/sandbox-D9K2ypln.d.ts +583 -0
  25. package/dist/sandbox.d.ts +3 -3
  26. package/dist/sandbox.js +4 -7
  27. package/dist/security.d.ts +4 -3
  28. package/dist/security.js +3 -3
  29. package/dist/sse-parser.js +1 -1
  30. package/package.json +11 -5
  31. package/src/clients/base-client.ts +280 -0
  32. package/src/clients/command-client.ts +115 -0
  33. package/src/clients/file-client.ts +269 -0
  34. package/src/clients/git-client.ts +92 -0
  35. package/src/clients/index.ts +63 -0
  36. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
  37. package/src/clients/port-client.ts +105 -0
  38. package/src/clients/process-client.ts +177 -0
  39. package/src/clients/sandbox-client.ts +41 -0
  40. package/src/clients/types.ts +84 -0
  41. package/src/clients/utility-client.ts +94 -0
  42. package/src/errors/adapter.ts +180 -0
  43. package/src/errors/classes.ts +469 -0
  44. package/src/errors/index.ts +105 -0
  45. package/src/file-stream.ts +119 -117
  46. package/src/index.ts +81 -69
  47. package/src/interpreter.ts +17 -8
  48. package/src/request-handler.ts +69 -43
  49. package/src/sandbox.ts +694 -533
  50. package/src/security.ts +14 -23
  51. package/src/sse-parser.ts +4 -8
  52. package/startup.sh +3 -0
  53. package/tests/base-client.test.ts +328 -0
  54. package/tests/command-client.test.ts +407 -0
  55. package/tests/file-client.test.ts +643 -0
  56. package/tests/file-stream.test.ts +306 -0
  57. package/tests/git-client.test.ts +328 -0
  58. package/tests/port-client.test.ts +301 -0
  59. package/tests/process-client.test.ts +658 -0
  60. package/tests/sandbox.test.ts +465 -0
  61. package/tests/sse-parser.test.ts +290 -0
  62. package/tests/utility-client.test.ts +266 -0
  63. package/tests/wrangler.jsonc +35 -0
  64. package/tsconfig.json +9 -1
  65. package/vitest.config.ts +31 -0
  66. package/container_src/bun.lock +0 -76
  67. package/container_src/circuit-breaker.ts +0 -121
  68. package/container_src/control-process.ts +0 -784
  69. package/container_src/handler/exec.ts +0 -185
  70. package/container_src/handler/file.ts +0 -457
  71. package/container_src/handler/git.ts +0 -130
  72. package/container_src/handler/ports.ts +0 -314
  73. package/container_src/handler/process.ts +0 -568
  74. package/container_src/handler/session.ts +0 -92
  75. package/container_src/index.ts +0 -601
  76. package/container_src/interpreter-service.ts +0 -276
  77. package/container_src/isolation.ts +0 -1213
  78. package/container_src/mime-processor.ts +0 -255
  79. package/container_src/package.json +0 -18
  80. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  81. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  82. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  83. package/container_src/runtime/process-pool.ts +0 -464
  84. package/container_src/shell-escape.ts +0 -42
  85. package/container_src/startup.sh +0 -11
  86. package/container_src/types.ts +0 -131
  87. package/dist/chunk-32UDXUPC.js +0 -671
  88. package/dist/chunk-32UDXUPC.js.map +0 -1
  89. package/dist/chunk-5DILEXGY.js +0 -85
  90. package/dist/chunk-5DILEXGY.js.map +0 -1
  91. package/dist/chunk-D3U63BZP.js +0 -240
  92. package/dist/chunk-D3U63BZP.js.map +0 -1
  93. package/dist/chunk-FXYPFGOZ.js +0 -129
  94. package/dist/chunk-FXYPFGOZ.js.map +0 -1
  95. package/dist/chunk-JTKON2SH.js.map +0 -1
  96. package/dist/chunk-NNGBXDMY.js.map +0 -1
  97. package/dist/chunk-SQLJNZ3K.js +0 -674
  98. package/dist/chunk-SQLJNZ3K.js.map +0 -1
  99. package/dist/chunk-W7TVRPBG.js +0 -108
  100. package/dist/chunk-W7TVRPBG.js.map +0 -1
  101. package/dist/client-B3RUab0s.d.ts +0 -225
  102. package/dist/client.d.ts +0 -4
  103. package/dist/client.js +0 -7
  104. package/dist/client.js.map +0 -1
  105. package/dist/errors.d.ts +0 -95
  106. package/dist/errors.js +0 -27
  107. package/dist/errors.js.map +0 -1
  108. package/dist/interpreter-client.d.ts +0 -4
  109. package/dist/interpreter-client.js +0 -9
  110. package/dist/interpreter-client.js.map +0 -1
  111. package/dist/interpreter-types.d.ts +0 -259
  112. package/dist/interpreter-types.js +0 -9
  113. package/dist/interpreter-types.js.map +0 -1
  114. package/dist/types.d.ts +0 -453
  115. package/dist/types.js +0 -45
  116. package/dist/types.js.map +0 -1
  117. package/src/client.ts +0 -1048
  118. package/src/errors.ts +0 -219
  119. package/src/interpreter-types.ts +0 -390
  120. package/src/types.ts +0 -571
@@ -0,0 +1,92 @@
1
+ import type { GitCheckoutResult } from '@repo/shared';
2
+ import { BaseHttpClient } from './base-client';
3
+ import type { HttpClientOptions, SessionRequest } from './types';
4
+
5
+ // Re-export for convenience
6
+ export type { GitCheckoutResult };
7
+
8
+ /**
9
+ * Request interface for Git checkout operations
10
+ */
11
+ export interface GitCheckoutRequest extends SessionRequest {
12
+ repoUrl: string;
13
+ branch?: string;
14
+ targetDir?: string;
15
+ }
16
+
17
+ /**
18
+ * Client for Git repository operations
19
+ */
20
+ export class GitClient extends BaseHttpClient {
21
+
22
+ /**
23
+ * Clone a Git repository
24
+ * @param repoUrl - URL of the Git repository to clone
25
+ * @param sessionId - The session ID for this operation
26
+ * @param options - Optional settings (branch, targetDir)
27
+ */
28
+ async checkout(
29
+ repoUrl: string,
30
+ sessionId: string,
31
+ options?: {
32
+ branch?: string;
33
+ targetDir?: string;
34
+ }
35
+ ): Promise<GitCheckoutResult> {
36
+ try {
37
+ // Determine target directory - use provided path or generate from repo name
38
+ let targetDir = options?.targetDir;
39
+ if (!targetDir) {
40
+ const repoName = this.extractRepoName(repoUrl);
41
+ // Ensure absolute path in /workspace
42
+ targetDir = `/workspace/${repoName}`;
43
+ }
44
+
45
+ const data: GitCheckoutRequest = {
46
+ repoUrl,
47
+ sessionId,
48
+ targetDir,
49
+ };
50
+
51
+ // Only include branch if explicitly specified
52
+ // This allows Git to use the repository's default branch
53
+ if (options?.branch) {
54
+ data.branch = options.branch;
55
+ }
56
+
57
+ const response = await this.post<GitCheckoutResult>(
58
+ '/api/git/checkout',
59
+ data
60
+ );
61
+
62
+ this.logSuccess(
63
+ 'Repository cloned',
64
+ `${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`
65
+ );
66
+
67
+ return response;
68
+ } catch (error) {
69
+ this.logError('checkout', error);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Extract repository name from URL for default directory name
76
+ */
77
+ private extractRepoName(repoUrl: string): string {
78
+ try {
79
+ const url = new URL(repoUrl);
80
+ const pathParts = url.pathname.split('/');
81
+ const repoName = pathParts[pathParts.length - 1];
82
+
83
+ // Remove .git extension if present
84
+ return repoName.replace(/\.git$/, '');
85
+ } catch {
86
+ // Fallback for invalid URLs
87
+ const parts = repoUrl.split('/');
88
+ const repoName = parts[parts.length - 1];
89
+ return repoName.replace(/\.git$/, '') || 'repo';
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,63 @@
1
+ // Main client exports
2
+
3
+
4
+ // Command client types
5
+ export type {
6
+ ExecuteRequest,
7
+ ExecuteResponse,
8
+ } from './command-client';
9
+
10
+ // Domain-specific clients
11
+ export { CommandClient } from './command-client';
12
+ // File client types
13
+ export type {
14
+ FileOperationRequest,
15
+ MkdirRequest,
16
+ ReadFileRequest,
17
+ WriteFileRequest,
18
+ } from './file-client';
19
+ export { FileClient } from './file-client';
20
+ // Git client types
21
+ export type {
22
+ GitCheckoutRequest,
23
+ GitCheckoutResult,
24
+ } from './git-client';
25
+ export { GitClient } from './git-client';
26
+ export { type ExecutionCallbacks, InterpreterClient } from './interpreter-client';
27
+ // Port client types
28
+ export type {
29
+ ExposePortRequest,
30
+ PortCloseResult,
31
+ PortExposeResult,
32
+ PortListResult,
33
+ UnexposePortRequest,
34
+ } from './port-client';
35
+ export { PortClient } from './port-client';
36
+ // Process client types
37
+ export type {
38
+ ProcessCleanupResult,
39
+ ProcessInfoResult,
40
+ ProcessKillResult,
41
+ ProcessListResult,
42
+ ProcessLogsResult,
43
+ ProcessStartResult,
44
+ StartProcessRequest,
45
+ } from './process-client';
46
+ export { ProcessClient } from './process-client';
47
+ export { SandboxClient } from './sandbox-client';
48
+ // Types and interfaces
49
+ export type {
50
+ BaseApiResponse,
51
+ ContainerStub,
52
+ ErrorResponse,
53
+ HttpClientOptions,
54
+ RequestConfig,
55
+ ResponseHandler,
56
+ SessionRequest,
57
+ } from './types';
58
+ // Utility client types
59
+ export type {
60
+ CommandsResponse,
61
+ PingResponse,
62
+ } from './utility-client';
63
+ export { UtilityClient } from './utility-client';
@@ -1,25 +1,17 @@
1
- import { HttpClient } from "./client.js";
2
- import { isRetryableError, parseErrorResponse } from "./errors.js";
3
- import type {
4
- CodeContext,
5
- CreateContextOptions,
6
- ExecutionError,
7
- OutputMessage,
8
- Result,
9
- } from "./interpreter-types.js";
10
-
11
- // API Response types
12
- interface ContextResponse {
13
- id: string;
14
- language: string;
15
- cwd: string;
16
- createdAt: string; // ISO date string from JSON
17
- lastUsed: string; // ISO date string from JSON
18
- }
19
-
20
- interface ContextListResponse {
21
- contexts: ContextResponse[];
22
- }
1
+ import {
2
+ type CodeContext,
3
+ type ContextCreateResult,
4
+ type ContextListResult,
5
+ type CreateContextOptions,
6
+ type ExecutionError,
7
+ type OutputMessage,
8
+ type Result,
9
+ ResultImpl,
10
+ } from '@repo/shared';
11
+ import type { ErrorResponse } from '../errors';
12
+ import { createErrorFromResponse, ErrorCode, InterpreterNotReadyError } from '../errors';
13
+ import { BaseHttpClient } from './base-client.js';
14
+ import type { HttpClientOptions } from './types.js';
23
15
 
24
16
  // Streaming execution data from the server
25
17
  interface StreamingExecutionData {
@@ -62,7 +54,7 @@ export interface ExecutionCallbacks {
62
54
  onError?: (error: ExecutionError) => void | Promise<void>;
63
55
  }
64
56
 
65
- export class InterpreterClient extends HttpClient {
57
+ export class InterpreterClient extends BaseHttpClient {
66
58
  private readonly maxRetries = 3;
67
59
  private readonly retryDelayMs = 1000;
68
60
 
@@ -81,16 +73,21 @@ export class InterpreterClient extends HttpClient {
81
73
  });
82
74
 
83
75
  if (!response.ok) {
84
- throw await parseErrorResponse(response);
76
+ const error = await this.parseErrorResponse(response);
77
+ throw error;
78
+ }
79
+
80
+ const data = (await response.json()) as ContextCreateResult;
81
+ if (!data.success) {
82
+ throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
85
83
  }
86
84
 
87
- const data = (await response.json()) as ContextResponse;
88
85
  return {
89
- id: data.id,
86
+ id: data.contextId,
90
87
  language: data.language,
91
- cwd: data.cwd,
92
- createdAt: new Date(data.createdAt),
93
- lastUsed: new Date(data.lastUsed),
88
+ cwd: data.cwd || '/workspace',
89
+ createdAt: new Date(data.timestamp),
90
+ lastUsed: new Date(data.timestamp),
94
91
  };
95
92
  });
96
93
  }
@@ -99,7 +96,8 @@ export class InterpreterClient extends HttpClient {
99
96
  contextId: string | undefined,
100
97
  code: string,
101
98
  language: string | undefined,
102
- callbacks: ExecutionCallbacks
99
+ callbacks: ExecutionCallbacks,
100
+ timeoutMs?: number
103
101
  ): Promise<void> {
104
102
  return this.executeWithRetry(async () => {
105
103
  const response = await this.doFetch("/api/execute/code", {
@@ -112,11 +110,13 @@ export class InterpreterClient extends HttpClient {
112
110
  context_id: contextId,
113
111
  code,
114
112
  language,
113
+ ...(timeoutMs !== undefined && { timeout_ms: timeoutMs })
115
114
  }),
116
115
  });
117
116
 
118
117
  if (!response.ok) {
119
- throw await parseErrorResponse(response);
118
+ const error = await this.parseErrorResponse(response);
119
+ throw error;
120
120
  }
121
121
 
122
122
  if (!response.body) {
@@ -130,6 +130,112 @@ export class InterpreterClient extends HttpClient {
130
130
  });
131
131
  }
132
132
 
133
+ async listCodeContexts(): Promise<CodeContext[]> {
134
+ return this.executeWithRetry(async () => {
135
+ const response = await this.doFetch("/api/contexts", {
136
+ method: "GET",
137
+ headers: { "Content-Type": "application/json" },
138
+ });
139
+
140
+ if (!response.ok) {
141
+ const error = await this.parseErrorResponse(response);
142
+ throw error;
143
+ }
144
+
145
+ const data = (await response.json()) as ContextListResult;
146
+ if (!data.success) {
147
+ throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
148
+ }
149
+
150
+ return data.contexts.map((ctx) => ({
151
+ id: ctx.id,
152
+ language: ctx.language,
153
+ cwd: ctx.cwd || '/workspace',
154
+ createdAt: new Date(data.timestamp),
155
+ lastUsed: new Date(data.timestamp),
156
+ }));
157
+ });
158
+ }
159
+
160
+ async deleteCodeContext(contextId: string): Promise<void> {
161
+ return this.executeWithRetry(async () => {
162
+ const response = await this.doFetch(`/api/contexts/${contextId}`, {
163
+ method: "DELETE",
164
+ headers: { "Content-Type": "application/json" },
165
+ });
166
+
167
+ if (!response.ok) {
168
+ const error = await this.parseErrorResponse(response);
169
+ throw error;
170
+ }
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Execute an operation with automatic retry for transient errors
176
+ */
177
+ private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
178
+ let lastError: Error | undefined;
179
+
180
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
181
+ try {
182
+ return await operation();
183
+ } catch (error) {
184
+ this.logError('executeWithRetry', error);
185
+ lastError = error as Error;
186
+
187
+ // Check if it's a retryable error (interpreter not ready)
188
+ if (this.isRetryableError(error)) {
189
+ // Don't retry on the last attempt
190
+ if (attempt < this.maxRetries - 1) {
191
+ // Exponential backoff with jitter
192
+ const delay =
193
+ this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
194
+ await new Promise((resolve) => setTimeout(resolve, delay));
195
+ continue;
196
+ }
197
+ }
198
+
199
+ // Not retryable or last attempt - throw the error
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ throw lastError || new Error("Execution failed after retries");
205
+ }
206
+
207
+ private isRetryableError(error: unknown): boolean {
208
+ if (error instanceof InterpreterNotReadyError) {
209
+ return true;
210
+ }
211
+
212
+ if (error instanceof Error) {
213
+ return (
214
+ error.message.includes("not ready") ||
215
+ error.message.includes("initializing")
216
+ );
217
+ }
218
+
219
+ return false;
220
+ }
221
+
222
+ private async parseErrorResponse(response: Response): Promise<Error> {
223
+ try {
224
+ const errorData = await response.json() as ErrorResponse;
225
+ return createErrorFromResponse(errorData);
226
+ } catch {
227
+ // Fallback if response isn't JSON
228
+ const errorResponse: ErrorResponse = {
229
+ code: ErrorCode.INTERNAL_ERROR,
230
+ message: `HTTP ${response.status}: ${response.statusText}`,
231
+ context: {},
232
+ httpStatus: response.status,
233
+ timestamp: new Date().toISOString()
234
+ };
235
+ return createErrorFromResponse(errorResponse);
236
+ }
237
+ }
238
+
133
239
  private async *readLines(
134
240
  stream: ReadableStream<Uint8Array>
135
241
  ): AsyncGenerator<string> {
@@ -167,8 +273,13 @@ export class InterpreterClient extends HttpClient {
167
273
  ) {
168
274
  if (!line.trim()) return;
169
275
 
276
+ // Skip lines that don't start with "data: " (SSE format)
277
+ if (!line.startsWith('data: ')) return;
278
+
170
279
  try {
171
- const data = JSON.parse(line) as StreamingExecutionData;
280
+ // Strip "data: " prefix and parse JSON
281
+ const jsonData = line.substring(6); // "data: " is 6 characters
282
+ const data = JSON.parse(jsonData) as StreamingExecutionData;
172
283
 
173
284
  switch (data.type) {
174
285
  case "stdout":
@@ -191,34 +302,8 @@ export class InterpreterClient extends HttpClient {
191
302
 
192
303
  case "result":
193
304
  if (callbacks.onResult) {
194
- // Convert raw result to Result interface
195
- const result: Result = {
196
- text: data.text,
197
- html: data.html,
198
- png: data.png,
199
- jpeg: data.jpeg,
200
- svg: data.svg,
201
- latex: data.latex,
202
- markdown: data.markdown,
203
- javascript: data.javascript,
204
- json: data.json,
205
- chart: data.chart,
206
- data: data.data,
207
- formats: () => {
208
- const formats: string[] = [];
209
- if (data.text) formats.push("text");
210
- if (data.html) formats.push("html");
211
- if (data.png) formats.push("png");
212
- if (data.jpeg) formats.push("jpeg");
213
- if (data.svg) formats.push("svg");
214
- if (data.latex) formats.push("latex");
215
- if (data.markdown) formats.push("markdown");
216
- if (data.javascript) formats.push("javascript");
217
- if (data.json) formats.push("json");
218
- if (data.chart) formats.push("chart");
219
- return formats;
220
- },
221
- };
305
+ // Create a ResultImpl instance from the raw data
306
+ const result = new ResultImpl(data);
222
307
  await callbacks.onResult(result);
223
308
  }
224
309
  break;
@@ -227,126 +312,18 @@ export class InterpreterClient extends HttpClient {
227
312
  if (callbacks.onError) {
228
313
  await callbacks.onError({
229
314
  name: data.ename || "Error",
230
- value: data.evalue || data.text || "Unknown error",
315
+ message: data.evalue || "Unknown error",
231
316
  traceback: data.traceback || [],
232
- lineNumber: data.lineNumber,
233
317
  });
234
318
  }
235
319
  break;
236
320
 
237
321
  case "execution_complete":
238
- // Execution completed successfully
322
+ // Signal completion - callbacks can handle cleanup if needed
239
323
  break;
240
324
  }
241
325
  } catch (error) {
242
- console.error(
243
- "[InterpreterClient] Error parsing execution result:",
244
- error
245
- );
326
+ this.logError('parseExecutionResult', error);
246
327
  }
247
328
  }
248
-
249
- async listCodeContexts(): Promise<CodeContext[]> {
250
- return this.executeWithRetry(async () => {
251
- const response = await this.doFetch("/api/contexts", {
252
- method: "GET",
253
- headers: { "Content-Type": "application/json" },
254
- });
255
-
256
- if (!response.ok) {
257
- throw await parseErrorResponse(response);
258
- }
259
-
260
- const data = (await response.json()) as ContextListResponse;
261
- return data.contexts.map((ctx) => ({
262
- id: ctx.id,
263
- language: ctx.language,
264
- cwd: ctx.cwd,
265
- createdAt: new Date(ctx.createdAt),
266
- lastUsed: new Date(ctx.lastUsed),
267
- }));
268
- });
269
- }
270
-
271
- async deleteCodeContext(contextId: string): Promise<void> {
272
- return this.executeWithRetry(async () => {
273
- const response = await this.doFetch(`/api/contexts/${contextId}`, {
274
- method: "DELETE",
275
- headers: { "Content-Type": "application/json" },
276
- });
277
-
278
- if (!response.ok) {
279
- throw await parseErrorResponse(response);
280
- }
281
- });
282
- }
283
-
284
- // Override parent doFetch to be public for this class
285
- public async doFetch(path: string, options?: RequestInit): Promise<Response> {
286
- return super.doFetch(path, options);
287
- }
288
-
289
- /**
290
- * Execute an operation with automatic retry for transient errors
291
- */
292
- private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
293
- let lastError: Error | undefined;
294
-
295
- for (let attempt = 0; attempt < this.maxRetries; attempt++) {
296
- try {
297
- return await operation();
298
- } catch (error) {
299
- lastError = error as Error;
300
-
301
- // Check if it's a retryable error (circuit breaker or interpreter not ready)
302
- if (this.isRetryableError(error)) {
303
- // Don't retry on the last attempt
304
- if (attempt < this.maxRetries - 1) {
305
- // Exponential backoff with jitter
306
- const delay =
307
- this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
308
- await new Promise((resolve) => setTimeout(resolve, delay));
309
- continue;
310
- }
311
- }
312
-
313
- // Non-retryable error or last attempt - throw immediately
314
- throw error;
315
- }
316
- }
317
-
318
- // All retries exhausted - throw a clean error without implementation details
319
- if (lastError?.message.includes("Code execution")) {
320
- // If the error already has a clean message about code execution, use it
321
- throw lastError;
322
- }
323
-
324
- // Otherwise, throw a generic but user-friendly error
325
- throw new Error("Unable to execute code at this time");
326
- }
327
-
328
- /**
329
- * Check if an error is retryable
330
- */
331
- private isRetryableError(error: unknown): boolean {
332
- // Use the SDK's built-in retryable check
333
- if (isRetryableError(error)) {
334
- return true;
335
- }
336
-
337
- // Also check for circuit breaker specific errors
338
- if (error instanceof Error) {
339
- // Circuit breaker errors (from the container's response)
340
- if (error.message.includes("Circuit breaker is open")) {
341
- return true;
342
- }
343
-
344
- // Check if error has a status property
345
- if ("status" in error && error.status === "circuit_open") {
346
- return true;
347
- }
348
- }
349
-
350
- return false;
351
- }
352
329
  }
@@ -0,0 +1,105 @@
1
+ import type {
2
+ PortCloseResult,
3
+ PortExposeResult,
4
+ PortListResult,
5
+ } from '@repo/shared';
6
+ import { BaseHttpClient } from './base-client';
7
+ import type { HttpClientOptions } from './types';
8
+
9
+ // Re-export for convenience
10
+ export type {
11
+ PortExposeResult,
12
+ PortCloseResult,
13
+ PortListResult,
14
+ };
15
+
16
+ /**
17
+ * Request interface for exposing ports
18
+ */
19
+ export interface ExposePortRequest {
20
+ port: number;
21
+ name?: string;
22
+ }
23
+
24
+ /**
25
+ * Request interface for unexposing ports
26
+ */
27
+ export interface UnexposePortRequest {
28
+ port: number;
29
+ }
30
+
31
+ /**
32
+ * Client for port management and preview URL operations
33
+ */
34
+ export class PortClient extends BaseHttpClient {
35
+
36
+ /**
37
+ * Expose a port and get a preview URL
38
+ * @param port - Port number to expose
39
+ * @param sessionId - The session ID for this operation
40
+ * @param name - Optional name for the port
41
+ */
42
+ async exposePort(
43
+ port: number,
44
+ sessionId: string,
45
+ name?: string
46
+ ): Promise<PortExposeResult> {
47
+ try {
48
+ const data = { port, sessionId, name };
49
+
50
+ const response = await this.post<PortExposeResult>(
51
+ '/api/expose-port',
52
+ data
53
+ );
54
+
55
+ this.logSuccess(
56
+ 'Port exposed',
57
+ `${port} exposed at ${response.url}${name ? ` (${name})` : ''}`
58
+ );
59
+
60
+ return response;
61
+ } catch (error) {
62
+ this.logError('exposePort', error);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Unexpose a port and remove its preview URL
69
+ * @param port - Port number to unexpose
70
+ * @param sessionId - The session ID for this operation
71
+ */
72
+ async unexposePort(port: number, sessionId: string): Promise<PortCloseResult> {
73
+ try {
74
+ const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
75
+ const response = await this.delete<PortCloseResult>(url);
76
+
77
+ this.logSuccess('Port unexposed', `${port}`);
78
+ return response;
79
+ } catch (error) {
80
+ this.logError('unexposePort', error);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get all currently exposed ports
87
+ * @param sessionId - The session ID for this operation
88
+ */
89
+ async getExposedPorts(sessionId: string): Promise<PortListResult> {
90
+ try {
91
+ const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
92
+ const response = await this.get<PortListResult>(url);
93
+
94
+ this.logSuccess(
95
+ 'Exposed ports retrieved',
96
+ `${response.ports.length} ports exposed`
97
+ );
98
+
99
+ return response;
100
+ } catch (error) {
101
+ this.logError('getExposedPorts', error);
102
+ throw error;
103
+ }
104
+ }
105
+ }