@cloudflare/sandbox 0.0.0-46eb4e6 → 0.0.0-485cf61
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/CHANGELOG.md +0 -6
- package/Dockerfile +82 -18
- package/README.md +89 -824
- package/dist/chunk-3NEP4CNV.js +99 -0
- package/dist/chunk-3NEP4CNV.js.map +1 -0
- package/dist/chunk-6IYG2RIN.js +117 -0
- package/dist/chunk-6IYG2RIN.js.map +1 -0
- package/dist/chunk-HB44YO2A.js +2331 -0
- package/dist/chunk-HB44YO2A.js.map +1 -0
- package/dist/chunk-KPVMMMIP.js +105 -0
- package/dist/chunk-KPVMMMIP.js.map +1 -0
- package/dist/chunk-NNGBXDMY.js +89 -0
- package/dist/chunk-NNGBXDMY.js.map +1 -0
- package/dist/file-stream.d.ts +43 -0
- package/dist/file-stream.js +9 -0
- package/dist/file-stream.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/interpreter.d.ts +33 -0
- package/dist/interpreter.js +8 -0
- package/dist/interpreter.js.map +1 -0
- package/dist/request-handler.d.ts +18 -0
- package/dist/request-handler.js +12 -0
- package/dist/request-handler.js.map +1 -0
- package/dist/sandbox-CtlKjZwf.d.ts +583 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +12 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/security.d.ts +35 -0
- package/dist/security.js +15 -0
- package/dist/security.js.map +1 -0
- package/dist/sse-parser.d.ts +28 -0
- package/dist/sse-parser.js +11 -0
- package/dist/sse-parser.js.map +1 -0
- package/package.json +11 -5
- package/src/clients/base-client.ts +297 -0
- package/src/clients/command-client.ts +118 -0
- package/src/clients/file-client.ts +272 -0
- package/src/clients/git-client.ts +95 -0
- package/src/clients/index.ts +63 -0
- package/src/{interpreter-client.ts → clients/interpreter-client.ts} +151 -171
- package/src/clients/port-client.ts +108 -0
- package/src/clients/process-client.ts +180 -0
- package/src/clients/sandbox-client.ts +41 -0
- package/src/clients/types.ts +81 -0
- package/src/clients/utility-client.ts +97 -0
- package/src/errors/adapter.ts +180 -0
- package/src/errors/classes.ts +469 -0
- package/src/errors/index.ts +105 -0
- package/src/file-stream.ts +119 -117
- package/src/index.ts +81 -69
- package/src/interpreter.ts +17 -8
- package/src/request-handler.ts +61 -7
- package/src/sandbox.ts +698 -495
- package/src/security.ts +20 -0
- package/startup.sh +7 -0
- package/tests/base-client.test.ts +328 -0
- package/tests/command-client.test.ts +407 -0
- package/tests/file-client.test.ts +643 -0
- package/tests/file-stream.test.ts +306 -0
- package/tests/git-client.test.ts +328 -0
- package/tests/port-client.test.ts +301 -0
- package/tests/process-client.test.ts +658 -0
- package/tests/sandbox.test.ts +465 -0
- package/tests/sse-parser.test.ts +291 -0
- package/tests/utility-client.test.ts +266 -0
- package/tests/wrangler.jsonc +35 -0
- package/tsconfig.json +9 -1
- package/vitest.config.ts +31 -0
- package/container_src/bun.lock +0 -76
- package/container_src/circuit-breaker.ts +0 -121
- package/container_src/control-process.ts +0 -784
- package/container_src/handler/exec.ts +0 -185
- package/container_src/handler/file.ts +0 -457
- package/container_src/handler/git.ts +0 -130
- package/container_src/handler/ports.ts +0 -314
- package/container_src/handler/process.ts +0 -568
- package/container_src/handler/session.ts +0 -92
- package/container_src/index.ts +0 -600
- package/container_src/interpreter-service.ts +0 -276
- package/container_src/isolation.ts +0 -1213
- package/container_src/mime-processor.ts +0 -255
- package/container_src/package.json +0 -18
- package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
- package/container_src/runtime/executors/python/ipython_executor.py +0 -338
- package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
- package/container_src/runtime/process-pool.ts +0 -464
- package/container_src/shell-escape.ts +0 -42
- package/container_src/startup.sh +0 -11
- package/container_src/types.ts +0 -131
- package/src/client.ts +0 -1048
- package/src/errors.ts +0 -219
- package/src/interpreter-types.ts +0 -390
- package/src/types.ts +0 -571
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
CreateContextOptions,
|
|
6
|
-
ExecutionError,
|
|
7
|
-
OutputMessage,
|
|
8
|
-
Result,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,10 +54,14 @@ export interface ExecutionCallbacks {
|
|
|
62
54
|
onError?: (error: ExecutionError) => void | Promise<void>;
|
|
63
55
|
}
|
|
64
56
|
|
|
65
|
-
export class InterpreterClient extends
|
|
57
|
+
export class InterpreterClient extends BaseHttpClient {
|
|
66
58
|
private readonly maxRetries = 3;
|
|
67
59
|
private readonly retryDelayMs = 1000;
|
|
68
60
|
|
|
61
|
+
constructor(options: HttpClientOptions = {}) {
|
|
62
|
+
super(options);
|
|
63
|
+
}
|
|
64
|
+
|
|
69
65
|
async createCodeContext(
|
|
70
66
|
options: CreateContextOptions = {}
|
|
71
67
|
): Promise<CodeContext> {
|
|
@@ -81,16 +77,21 @@ export class InterpreterClient extends HttpClient {
|
|
|
81
77
|
});
|
|
82
78
|
|
|
83
79
|
if (!response.ok) {
|
|
84
|
-
|
|
80
|
+
const error = await this.parseErrorResponse(response);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = (await response.json()) as ContextCreateResult;
|
|
85
|
+
if (!data.success) {
|
|
86
|
+
throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
const data = (await response.json()) as ContextResponse;
|
|
88
89
|
return {
|
|
89
|
-
id: data.
|
|
90
|
+
id: data.contextId,
|
|
90
91
|
language: data.language,
|
|
91
|
-
cwd: data.cwd,
|
|
92
|
-
createdAt: new Date(data.
|
|
93
|
-
lastUsed: new Date(data.
|
|
92
|
+
cwd: data.cwd || '/workspace',
|
|
93
|
+
createdAt: new Date(data.timestamp),
|
|
94
|
+
lastUsed: new Date(data.timestamp),
|
|
94
95
|
};
|
|
95
96
|
});
|
|
96
97
|
}
|
|
@@ -99,7 +100,8 @@ export class InterpreterClient extends HttpClient {
|
|
|
99
100
|
contextId: string | undefined,
|
|
100
101
|
code: string,
|
|
101
102
|
language: string | undefined,
|
|
102
|
-
callbacks: ExecutionCallbacks
|
|
103
|
+
callbacks: ExecutionCallbacks,
|
|
104
|
+
timeoutMs?: number
|
|
103
105
|
): Promise<void> {
|
|
104
106
|
return this.executeWithRetry(async () => {
|
|
105
107
|
const response = await this.doFetch("/api/execute/code", {
|
|
@@ -112,11 +114,13 @@ export class InterpreterClient extends HttpClient {
|
|
|
112
114
|
context_id: contextId,
|
|
113
115
|
code,
|
|
114
116
|
language,
|
|
117
|
+
...(timeoutMs !== undefined && { timeout_ms: timeoutMs })
|
|
115
118
|
}),
|
|
116
119
|
});
|
|
117
120
|
|
|
118
121
|
if (!response.ok) {
|
|
119
|
-
|
|
122
|
+
const error = await this.parseErrorResponse(response);
|
|
123
|
+
throw error;
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
if (!response.body) {
|
|
@@ -130,6 +134,111 @@ export class InterpreterClient extends HttpClient {
|
|
|
130
134
|
});
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
async listCodeContexts(): Promise<CodeContext[]> {
|
|
138
|
+
return this.executeWithRetry(async () => {
|
|
139
|
+
const response = await this.doFetch("/api/contexts", {
|
|
140
|
+
method: "GET",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
const error = await this.parseErrorResponse(response);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const data = (await response.json()) as ContextListResult;
|
|
150
|
+
if (!data.success) {
|
|
151
|
+
throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return data.contexts.map((ctx) => ({
|
|
155
|
+
id: ctx.id,
|
|
156
|
+
language: ctx.language,
|
|
157
|
+
cwd: ctx.cwd || '/workspace',
|
|
158
|
+
createdAt: new Date(data.timestamp),
|
|
159
|
+
lastUsed: new Date(data.timestamp),
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async deleteCodeContext(contextId: string): Promise<void> {
|
|
165
|
+
return this.executeWithRetry(async () => {
|
|
166
|
+
const response = await this.doFetch(`/api/contexts/${contextId}`, {
|
|
167
|
+
method: "DELETE",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const error = await this.parseErrorResponse(response);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Execute an operation with automatic retry for transient errors
|
|
180
|
+
*/
|
|
181
|
+
private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
|
|
182
|
+
let lastError: Error | undefined;
|
|
183
|
+
|
|
184
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
185
|
+
try {
|
|
186
|
+
return await operation();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
lastError = error as Error;
|
|
189
|
+
|
|
190
|
+
// Check if it's a retryable error (interpreter not ready)
|
|
191
|
+
if (this.isRetryableError(error)) {
|
|
192
|
+
// Don't retry on the last attempt
|
|
193
|
+
if (attempt < this.maxRetries - 1) {
|
|
194
|
+
// Exponential backoff with jitter
|
|
195
|
+
const delay =
|
|
196
|
+
this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Not retryable or last attempt - throw the error
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw lastError || new Error("Execution failed after retries");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private isRetryableError(error: unknown): boolean {
|
|
211
|
+
if (error instanceof InterpreterNotReadyError) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (error instanceof Error) {
|
|
216
|
+
return (
|
|
217
|
+
error.message.includes("not ready") ||
|
|
218
|
+
error.message.includes("initializing")
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async parseErrorResponse(response: Response): Promise<Error> {
|
|
226
|
+
try {
|
|
227
|
+
const errorData = await response.json() as ErrorResponse;
|
|
228
|
+
return createErrorFromResponse(errorData);
|
|
229
|
+
} catch {
|
|
230
|
+
// Fallback if response isn't JSON
|
|
231
|
+
const errorResponse: ErrorResponse = {
|
|
232
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
233
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
234
|
+
context: {},
|
|
235
|
+
httpStatus: response.status,
|
|
236
|
+
timestamp: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
return createErrorFromResponse(errorResponse);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
133
242
|
private async *readLines(
|
|
134
243
|
stream: ReadableStream<Uint8Array>
|
|
135
244
|
): AsyncGenerator<string> {
|
|
@@ -167,8 +276,13 @@ export class InterpreterClient extends HttpClient {
|
|
|
167
276
|
) {
|
|
168
277
|
if (!line.trim()) return;
|
|
169
278
|
|
|
279
|
+
// Skip lines that don't start with "data: " (SSE format)
|
|
280
|
+
if (!line.startsWith('data: ')) return;
|
|
281
|
+
|
|
170
282
|
try {
|
|
171
|
-
|
|
283
|
+
// Strip "data: " prefix and parse JSON
|
|
284
|
+
const jsonData = line.substring(6); // "data: " is 6 characters
|
|
285
|
+
const data = JSON.parse(jsonData) as StreamingExecutionData;
|
|
172
286
|
|
|
173
287
|
switch (data.type) {
|
|
174
288
|
case "stdout":
|
|
@@ -191,34 +305,8 @@ export class InterpreterClient extends HttpClient {
|
|
|
191
305
|
|
|
192
306
|
case "result":
|
|
193
307
|
if (callbacks.onResult) {
|
|
194
|
-
//
|
|
195
|
-
const 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
|
-
};
|
|
308
|
+
// Create a ResultImpl instance from the raw data
|
|
309
|
+
const result = new ResultImpl(data);
|
|
222
310
|
await callbacks.onResult(result);
|
|
223
311
|
}
|
|
224
312
|
break;
|
|
@@ -227,126 +315,18 @@ export class InterpreterClient extends HttpClient {
|
|
|
227
315
|
if (callbacks.onError) {
|
|
228
316
|
await callbacks.onError({
|
|
229
317
|
name: data.ename || "Error",
|
|
230
|
-
|
|
318
|
+
message: data.evalue || "Unknown error",
|
|
231
319
|
traceback: data.traceback || [],
|
|
232
|
-
lineNumber: data.lineNumber,
|
|
233
320
|
});
|
|
234
321
|
}
|
|
235
322
|
break;
|
|
236
323
|
|
|
237
324
|
case "execution_complete":
|
|
238
|
-
//
|
|
325
|
+
// Signal completion - callbacks can handle cleanup if needed
|
|
239
326
|
break;
|
|
240
327
|
}
|
|
241
328
|
} catch (error) {
|
|
242
|
-
console.error(
|
|
243
|
-
"[InterpreterClient] Error parsing execution result:",
|
|
244
|
-
error
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
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
|
-
}
|
|
329
|
+
console.error("Failed to parse execution result:", error);
|
|
316
330
|
}
|
|
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
331
|
}
|
|
352
332
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
constructor(options: HttpClientOptions = {}) {
|
|
36
|
+
super(options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Expose a port and get a preview URL
|
|
41
|
+
* @param port - Port number to expose
|
|
42
|
+
* @param sessionId - The session ID for this operation
|
|
43
|
+
* @param name - Optional name for the port
|
|
44
|
+
*/
|
|
45
|
+
async exposePort(
|
|
46
|
+
port: number,
|
|
47
|
+
sessionId: string,
|
|
48
|
+
name?: string
|
|
49
|
+
): Promise<PortExposeResult> {
|
|
50
|
+
try {
|
|
51
|
+
const data = { port, sessionId, name };
|
|
52
|
+
|
|
53
|
+
const response = await this.post<PortExposeResult>(
|
|
54
|
+
'/api/expose-port',
|
|
55
|
+
data
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
this.logSuccess(
|
|
59
|
+
'Port exposed',
|
|
60
|
+
`${port} exposed at ${response.url}${name ? ` (${name})` : ''}`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return response;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
this.logError('exposePort', error);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unexpose a port and remove its preview URL
|
|
72
|
+
* @param port - Port number to unexpose
|
|
73
|
+
* @param sessionId - The session ID for this operation
|
|
74
|
+
*/
|
|
75
|
+
async unexposePort(port: number, sessionId: string): Promise<PortCloseResult> {
|
|
76
|
+
try {
|
|
77
|
+
const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
|
|
78
|
+
const response = await this.delete<PortCloseResult>(url);
|
|
79
|
+
|
|
80
|
+
this.logSuccess('Port unexposed', `${port}`);
|
|
81
|
+
return response;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.logError('unexposePort', error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get all currently exposed ports
|
|
90
|
+
* @param sessionId - The session ID for this operation
|
|
91
|
+
*/
|
|
92
|
+
async getExposedPorts(sessionId: string): Promise<PortListResult> {
|
|
93
|
+
try {
|
|
94
|
+
const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
|
|
95
|
+
const response = await this.get<PortListResult>(url);
|
|
96
|
+
|
|
97
|
+
this.logSuccess(
|
|
98
|
+
'Exposed ports retrieved',
|
|
99
|
+
`${response.ports.length} ports exposed`
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return response;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.logError('getExposedPorts', error);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProcessCleanupResult,
|
|
3
|
+
ProcessInfoResult,
|
|
4
|
+
ProcessKillResult,
|
|
5
|
+
ProcessListResult,
|
|
6
|
+
ProcessLogsResult,
|
|
7
|
+
ProcessStartResult,
|
|
8
|
+
StartProcessRequest,
|
|
9
|
+
} from '@repo/shared';
|
|
10
|
+
import { BaseHttpClient } from './base-client';
|
|
11
|
+
import type { HttpClientOptions } from './types';
|
|
12
|
+
|
|
13
|
+
// Re-export for convenience
|
|
14
|
+
export type {
|
|
15
|
+
StartProcessRequest,
|
|
16
|
+
ProcessStartResult,
|
|
17
|
+
ProcessListResult,
|
|
18
|
+
ProcessInfoResult,
|
|
19
|
+
ProcessKillResult,
|
|
20
|
+
ProcessLogsResult,
|
|
21
|
+
ProcessCleanupResult,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Client for background process management
|
|
27
|
+
*/
|
|
28
|
+
export class ProcessClient extends BaseHttpClient {
|
|
29
|
+
constructor(options: HttpClientOptions = {}) {
|
|
30
|
+
super(options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Start a background process
|
|
35
|
+
* @param command - Command to execute as a background process
|
|
36
|
+
* @param sessionId - The session ID for this operation
|
|
37
|
+
* @param options - Optional settings (processId)
|
|
38
|
+
*/
|
|
39
|
+
async startProcess(
|
|
40
|
+
command: string,
|
|
41
|
+
sessionId: string,
|
|
42
|
+
options?: { processId?: string }
|
|
43
|
+
): Promise<ProcessStartResult> {
|
|
44
|
+
try {
|
|
45
|
+
const data: StartProcessRequest = {
|
|
46
|
+
command,
|
|
47
|
+
sessionId,
|
|
48
|
+
processId: options?.processId,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const response = await this.post<ProcessStartResult>(
|
|
52
|
+
'/api/process/start',
|
|
53
|
+
data
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
this.logSuccess(
|
|
57
|
+
'Process started',
|
|
58
|
+
`${command} (ID: ${response.processId})`
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return response;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.logError('startProcess', error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* List all processes (sandbox-scoped, not session-scoped)
|
|
70
|
+
*/
|
|
71
|
+
async listProcesses(): Promise<ProcessListResult> {
|
|
72
|
+
try {
|
|
73
|
+
const url = `/api/process/list`;
|
|
74
|
+
const response = await this.get<ProcessListResult>(url);
|
|
75
|
+
|
|
76
|
+
this.logSuccess('Processes listed', `${response.processes.length} processes`);
|
|
77
|
+
return response;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.logError('listProcesses', error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get information about a specific process (sandbox-scoped, not session-scoped)
|
|
86
|
+
* @param processId - ID of the process to retrieve
|
|
87
|
+
*/
|
|
88
|
+
async getProcess(processId: string): Promise<ProcessInfoResult> {
|
|
89
|
+
try {
|
|
90
|
+
const url = `/api/process/${processId}`;
|
|
91
|
+
const response = await this.get<ProcessInfoResult>(url);
|
|
92
|
+
|
|
93
|
+
this.logSuccess('Process retrieved', `ID: ${processId}`);
|
|
94
|
+
return response;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logError('getProcess', error);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Kill a specific process (sandbox-scoped, not session-scoped)
|
|
103
|
+
* @param processId - ID of the process to kill
|
|
104
|
+
*/
|
|
105
|
+
async killProcess(processId: string): Promise<ProcessKillResult> {
|
|
106
|
+
try {
|
|
107
|
+
const url = `/api/process/${processId}`;
|
|
108
|
+
const response = await this.delete<ProcessKillResult>(url);
|
|
109
|
+
|
|
110
|
+
this.logSuccess('Process killed', `ID: ${processId}`);
|
|
111
|
+
return response;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logError('killProcess', error);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Kill all running processes (sandbox-scoped, not session-scoped)
|
|
120
|
+
*/
|
|
121
|
+
async killAllProcesses(): Promise<ProcessCleanupResult> {
|
|
122
|
+
try {
|
|
123
|
+
const url = `/api/process/kill-all`;
|
|
124
|
+
const response = await this.delete<ProcessCleanupResult>(url);
|
|
125
|
+
|
|
126
|
+
this.logSuccess(
|
|
127
|
+
'All processes killed',
|
|
128
|
+
`${response.cleanedCount} processes terminated`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return response;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.logError('killAllProcesses', error);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get logs from a specific process (sandbox-scoped, not session-scoped)
|
|
140
|
+
* @param processId - ID of the process to get logs from
|
|
141
|
+
*/
|
|
142
|
+
async getProcessLogs(processId: string): Promise<ProcessLogsResult> {
|
|
143
|
+
try {
|
|
144
|
+
const url = `/api/process/${processId}/logs`;
|
|
145
|
+
const response = await this.get<ProcessLogsResult>(url);
|
|
146
|
+
|
|
147
|
+
this.logSuccess(
|
|
148
|
+
'Process logs retrieved',
|
|
149
|
+
`ID: ${processId}, stdout: ${response.stdout.length} chars, stderr: ${response.stderr.length} chars`
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return response;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logError('getProcessLogs', error);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Stream logs from a specific process (sandbox-scoped, not session-scoped)
|
|
161
|
+
* @param processId - ID of the process to stream logs from
|
|
162
|
+
*/
|
|
163
|
+
async streamProcessLogs(processId: string): Promise<ReadableStream<Uint8Array>> {
|
|
164
|
+
try {
|
|
165
|
+
const url = `/api/process/${processId}/stream`;
|
|
166
|
+
const response = await this.doFetch(url, {
|
|
167
|
+
method: 'GET',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const stream = await this.handleStreamResponse(response);
|
|
171
|
+
|
|
172
|
+
this.logSuccess('Process log stream started', `ID: ${processId}`);
|
|
173
|
+
|
|
174
|
+
return stream;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.logError('streamProcessLogs', error);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|