@cloudflare/sandbox 0.5.4 → 0.6.0
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/Dockerfile +54 -59
- package/README.md +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/package.json +13 -8
- package/.turbo/turbo-build.log +0 -23
- package/CHANGELOG.md +0 -441
- package/src/clients/base-client.ts +0 -356
- package/src/clients/command-client.ts +0 -133
- package/src/clients/file-client.ts +0 -300
- package/src/clients/git-client.ts +0 -98
- package/src/clients/index.ts +0 -64
- package/src/clients/interpreter-client.ts +0 -333
- package/src/clients/port-client.ts +0 -105
- package/src/clients/process-client.ts +0 -198
- package/src/clients/sandbox-client.ts +0 -39
- package/src/clients/types.ts +0 -88
- package/src/clients/utility-client.ts +0 -156
- package/src/errors/adapter.ts +0 -238
- package/src/errors/classes.ts +0 -594
- package/src/errors/index.ts +0 -109
- package/src/file-stream.ts +0 -169
- package/src/index.ts +0 -121
- package/src/interpreter.ts +0 -168
- package/src/openai/index.ts +0 -465
- package/src/request-handler.ts +0 -184
- package/src/sandbox.ts +0 -1937
- package/src/security.ts +0 -119
- package/src/sse-parser.ts +0 -144
- package/src/storage-mount/credential-detection.ts +0 -41
- package/src/storage-mount/errors.ts +0 -51
- package/src/storage-mount/index.ts +0 -17
- package/src/storage-mount/provider-detection.ts +0 -93
- package/src/storage-mount/types.ts +0 -17
- package/src/version.ts +0 -6
- package/tests/base-client.test.ts +0 -582
- package/tests/command-client.test.ts +0 -444
- package/tests/file-client.test.ts +0 -831
- package/tests/file-stream.test.ts +0 -310
- package/tests/get-sandbox.test.ts +0 -172
- package/tests/git-client.test.ts +0 -455
- package/tests/openai-shell-editor.test.ts +0 -434
- package/tests/port-client.test.ts +0 -283
- package/tests/process-client.test.ts +0 -649
- package/tests/request-handler.test.ts +0 -292
- package/tests/sandbox.test.ts +0 -890
- package/tests/sse-parser.test.ts +0 -291
- package/tests/storage-mount/credential-detection.test.ts +0 -119
- package/tests/storage-mount/provider-detection.test.ts +0 -77
- package/tests/utility-client.test.ts +0 -339
- package/tests/version.test.ts +0 -16
- package/tests/wrangler.jsonc +0 -35
- package/tsconfig.json +0 -11
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -31
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@repo/shared';
|
|
2
|
-
import { createNoOpLogger } from '@repo/shared';
|
|
3
|
-
import { getHttpStatus } from '@repo/shared/errors';
|
|
4
|
-
import type { ErrorResponse as NewErrorResponse } from '../errors';
|
|
5
|
-
import { createErrorFromResponse, ErrorCode } from '../errors';
|
|
6
|
-
import type { SandboxError } from '../errors/classes';
|
|
7
|
-
import type { HttpClientOptions, ResponseHandler } from './types';
|
|
8
|
-
|
|
9
|
-
// Container startup retry configuration
|
|
10
|
-
const TIMEOUT_MS = 120_000; // 2 minutes total retry budget
|
|
11
|
-
const MIN_TIME_FOR_RETRY_MS = 15_000; // Need at least 15s remaining to retry (allows for longer container startups)
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Abstract base class providing common HTTP functionality for all domain clients
|
|
15
|
-
*/
|
|
16
|
-
export abstract class BaseHttpClient {
|
|
17
|
-
protected baseUrl: string;
|
|
18
|
-
protected options: HttpClientOptions;
|
|
19
|
-
protected logger: Logger;
|
|
20
|
-
|
|
21
|
-
constructor(options: HttpClientOptions = {}) {
|
|
22
|
-
this.options = options;
|
|
23
|
-
this.logger = options.logger ?? createNoOpLogger();
|
|
24
|
-
this.baseUrl = this.options.baseUrl!;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Core HTTP request method with automatic retry for container startup delays
|
|
29
|
-
* Retries both 503 (provisioning) and 500 (startup failure) errors when they're container-related
|
|
30
|
-
*/
|
|
31
|
-
protected async doFetch(
|
|
32
|
-
path: string,
|
|
33
|
-
options?: RequestInit
|
|
34
|
-
): Promise<Response> {
|
|
35
|
-
const startTime = Date.now();
|
|
36
|
-
let attempt = 0;
|
|
37
|
-
|
|
38
|
-
while (true) {
|
|
39
|
-
const response = await this.executeFetch(path, options);
|
|
40
|
-
|
|
41
|
-
// Check if this is a retryable container error (both 500 and 503)
|
|
42
|
-
const shouldRetry = await this.isRetryableContainerError(response);
|
|
43
|
-
|
|
44
|
-
if (shouldRetry) {
|
|
45
|
-
const elapsed = Date.now() - startTime;
|
|
46
|
-
const remaining = TIMEOUT_MS - elapsed;
|
|
47
|
-
|
|
48
|
-
// Check if we have enough time for another attempt
|
|
49
|
-
if (remaining > MIN_TIME_FOR_RETRY_MS) {
|
|
50
|
-
// Exponential backoff with longer delays for container ops: 3s, 6s, 12s, 24s, 30s
|
|
51
|
-
const delay = Math.min(3000 * 2 ** attempt, 30000);
|
|
52
|
-
|
|
53
|
-
this.logger.info('Container not ready, retrying', {
|
|
54
|
-
status: response.status,
|
|
55
|
-
attempt: attempt + 1,
|
|
56
|
-
delayMs: delay,
|
|
57
|
-
remainingSec: Math.floor(remaining / 1000)
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
61
|
-
attempt++;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Timeout exhausted
|
|
66
|
-
this.logger.error(
|
|
67
|
-
'Container failed to become ready',
|
|
68
|
-
new Error(
|
|
69
|
-
`Failed after ${attempt + 1} attempts over ${Math.floor(elapsed / 1000)}s`
|
|
70
|
-
)
|
|
71
|
-
);
|
|
72
|
-
return response;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Not a retryable error or request succeeded
|
|
76
|
-
return response;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Make a POST request with JSON body
|
|
82
|
-
*/
|
|
83
|
-
protected async post<T>(
|
|
84
|
-
endpoint: string,
|
|
85
|
-
data: unknown,
|
|
86
|
-
responseHandler?: ResponseHandler<T>
|
|
87
|
-
): Promise<T> {
|
|
88
|
-
const response = await this.doFetch(endpoint, {
|
|
89
|
-
method: 'POST',
|
|
90
|
-
headers: {
|
|
91
|
-
'Content-Type': 'application/json'
|
|
92
|
-
},
|
|
93
|
-
body: JSON.stringify(data)
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
return this.handleResponse(response, responseHandler);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Make a GET request
|
|
101
|
-
*/
|
|
102
|
-
protected async get<T>(
|
|
103
|
-
endpoint: string,
|
|
104
|
-
responseHandler?: ResponseHandler<T>
|
|
105
|
-
): Promise<T> {
|
|
106
|
-
const response = await this.doFetch(endpoint, {
|
|
107
|
-
method: 'GET'
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
return this.handleResponse(response, responseHandler);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Make a DELETE request
|
|
115
|
-
*/
|
|
116
|
-
protected async delete<T>(
|
|
117
|
-
endpoint: string,
|
|
118
|
-
responseHandler?: ResponseHandler<T>
|
|
119
|
-
): Promise<T> {
|
|
120
|
-
const response = await this.doFetch(endpoint, {
|
|
121
|
-
method: 'DELETE'
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
return this.handleResponse(response, responseHandler);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Handle HTTP response with error checking and parsing
|
|
129
|
-
*/
|
|
130
|
-
protected async handleResponse<T>(
|
|
131
|
-
response: Response,
|
|
132
|
-
customHandler?: ResponseHandler<T>
|
|
133
|
-
): Promise<T> {
|
|
134
|
-
if (!response.ok) {
|
|
135
|
-
await this.handleErrorResponse(response);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (customHandler) {
|
|
139
|
-
return customHandler(response);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
return await response.json();
|
|
144
|
-
} catch (error) {
|
|
145
|
-
// Handle malformed JSON responses gracefully
|
|
146
|
-
const errorResponse: NewErrorResponse = {
|
|
147
|
-
code: ErrorCode.INVALID_JSON_RESPONSE,
|
|
148
|
-
message: `Invalid JSON response: ${
|
|
149
|
-
error instanceof Error ? error.message : 'Unknown parsing error'
|
|
150
|
-
}`,
|
|
151
|
-
context: {},
|
|
152
|
-
httpStatus: response.status,
|
|
153
|
-
timestamp: new Date().toISOString()
|
|
154
|
-
};
|
|
155
|
-
throw createErrorFromResponse(errorResponse);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Handle error responses with consistent error throwing
|
|
161
|
-
*/
|
|
162
|
-
protected async handleErrorResponse(response: Response): Promise<never> {
|
|
163
|
-
let errorData: NewErrorResponse;
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
errorData = await response.json();
|
|
167
|
-
} catch {
|
|
168
|
-
// Fallback if response isn't JSON or parsing fails
|
|
169
|
-
errorData = {
|
|
170
|
-
code: ErrorCode.INTERNAL_ERROR,
|
|
171
|
-
message: `HTTP error! status: ${response.status}`,
|
|
172
|
-
context: { statusText: response.statusText },
|
|
173
|
-
httpStatus: response.status,
|
|
174
|
-
timestamp: new Date().toISOString()
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Convert ErrorResponse to appropriate Error class
|
|
179
|
-
const error = createErrorFromResponse(errorData);
|
|
180
|
-
|
|
181
|
-
// Call error callback if provided
|
|
182
|
-
this.options.onError?.(errorData.message, undefined);
|
|
183
|
-
|
|
184
|
-
throw error;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Create a streaming response handler for Server-Sent Events
|
|
189
|
-
*/
|
|
190
|
-
protected async handleStreamResponse(
|
|
191
|
-
response: Response
|
|
192
|
-
): Promise<ReadableStream<Uint8Array>> {
|
|
193
|
-
if (!response.ok) {
|
|
194
|
-
await this.handleErrorResponse(response);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!response.body) {
|
|
198
|
-
throw new Error('No response body for streaming');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return response.body;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Utility method to log successful operations
|
|
206
|
-
*/
|
|
207
|
-
protected logSuccess(operation: string, details?: string): void {
|
|
208
|
-
this.logger.info(
|
|
209
|
-
`${operation} completed successfully`,
|
|
210
|
-
details ? { details } : undefined
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Utility method to log errors intelligently
|
|
216
|
-
* Only logs unexpected errors (5xx), not expected errors (4xx)
|
|
217
|
-
*
|
|
218
|
-
* - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
|
|
219
|
-
* - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
|
|
220
|
-
*/
|
|
221
|
-
protected logError(operation: string, error: unknown): void {
|
|
222
|
-
// Check if it's a SandboxError with HTTP status
|
|
223
|
-
if (error && typeof error === 'object' && 'httpStatus' in error) {
|
|
224
|
-
const httpStatus = (error as SandboxError).httpStatus;
|
|
225
|
-
|
|
226
|
-
// Only log server errors (5xx), not client errors (4xx)
|
|
227
|
-
if (httpStatus >= 500) {
|
|
228
|
-
this.logger.error(
|
|
229
|
-
`Unexpected error in ${operation}`,
|
|
230
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
231
|
-
{ httpStatus }
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
// 4xx errors are expected (validation, not found, etc.) - don't log
|
|
235
|
-
} else {
|
|
236
|
-
// Non-SandboxError (unexpected) - log it
|
|
237
|
-
this.logger.error(
|
|
238
|
-
`Error in ${operation}`,
|
|
239
|
-
error instanceof Error ? error : new Error(String(error))
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Check if response indicates a retryable container error
|
|
246
|
-
* Uses fail-safe strategy: only retry known transient errors
|
|
247
|
-
*
|
|
248
|
-
* TODO: This relies on string matching error messages, which is brittle.
|
|
249
|
-
* Ideally, the container API should return structured errors with a
|
|
250
|
-
* `retryable: boolean` field to avoid coupling to error message format.
|
|
251
|
-
*
|
|
252
|
-
* @param response - HTTP response to check
|
|
253
|
-
* @returns true if error is retryable container error, false otherwise
|
|
254
|
-
*/
|
|
255
|
-
private async isRetryableContainerError(
|
|
256
|
-
response: Response
|
|
257
|
-
): Promise<boolean> {
|
|
258
|
-
// Only consider 500 and 503 status codes
|
|
259
|
-
if (response.status !== 500 && response.status !== 503) {
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
const cloned = response.clone();
|
|
265
|
-
const text = await cloned.text();
|
|
266
|
-
const textLower = text.toLowerCase();
|
|
267
|
-
|
|
268
|
-
// Step 1: Check for permanent errors (fail fast)
|
|
269
|
-
const permanentErrors = [
|
|
270
|
-
'no such image', // Missing Docker image
|
|
271
|
-
'container already exists', // Name collision
|
|
272
|
-
'malformed containerinspect' // Docker API issue
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
if (permanentErrors.some((err) => textLower.includes(err))) {
|
|
276
|
-
this.logger.debug('Detected permanent error, not retrying', { text });
|
|
277
|
-
return false; // Don't retry
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Step 2: Check for known transient errors (do retry)
|
|
281
|
-
const transientErrors = [
|
|
282
|
-
// Platform provisioning (503)
|
|
283
|
-
'no container instance available',
|
|
284
|
-
'currently provisioning',
|
|
285
|
-
|
|
286
|
-
// Port mapping race conditions (500)
|
|
287
|
-
'container port not found',
|
|
288
|
-
'connection refused: container port',
|
|
289
|
-
|
|
290
|
-
// Application startup delays (500)
|
|
291
|
-
'the container is not listening',
|
|
292
|
-
'failed to verify port',
|
|
293
|
-
'container did not start',
|
|
294
|
-
|
|
295
|
-
// Network transients (500)
|
|
296
|
-
'network connection lost',
|
|
297
|
-
'container suddenly disconnected',
|
|
298
|
-
|
|
299
|
-
// Monitor race conditions (500)
|
|
300
|
-
'monitor failed to find container',
|
|
301
|
-
|
|
302
|
-
// General timeouts (500)
|
|
303
|
-
'timed out',
|
|
304
|
-
'timeout'
|
|
305
|
-
];
|
|
306
|
-
|
|
307
|
-
const shouldRetry = transientErrors.some((err) =>
|
|
308
|
-
textLower.includes(err)
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
if (!shouldRetry) {
|
|
312
|
-
this.logger.debug('Unknown error pattern, not retrying', {
|
|
313
|
-
status: response.status,
|
|
314
|
-
text: text.substring(0, 200) // Log first 200 chars
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return shouldRetry;
|
|
319
|
-
} catch (error) {
|
|
320
|
-
this.logger.error(
|
|
321
|
-
'Error checking if response is retryable',
|
|
322
|
-
error instanceof Error ? error : new Error(String(error))
|
|
323
|
-
);
|
|
324
|
-
// If we can't read response, don't retry (fail fast)
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private async executeFetch(
|
|
330
|
-
path: string,
|
|
331
|
-
options?: RequestInit
|
|
332
|
-
): Promise<Response> {
|
|
333
|
-
const url = this.options.stub
|
|
334
|
-
? `http://localhost:${this.options.port}${path}`
|
|
335
|
-
: `${this.baseUrl}${path}`;
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
if (this.options.stub) {
|
|
339
|
-
return await this.options.stub.containerFetch(
|
|
340
|
-
url,
|
|
341
|
-
options || {},
|
|
342
|
-
this.options.port
|
|
343
|
-
);
|
|
344
|
-
} else {
|
|
345
|
-
return await fetch(url, options);
|
|
346
|
-
}
|
|
347
|
-
} catch (error) {
|
|
348
|
-
this.logger.error(
|
|
349
|
-
'HTTP request error',
|
|
350
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
351
|
-
{ method: options?.method || 'GET', url }
|
|
352
|
-
);
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import type { ExecuteRequest } from '@repo/shared';
|
|
2
|
-
import { BaseHttpClient } from './base-client';
|
|
3
|
-
import type { BaseApiResponse } from './types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Request interface for command execution
|
|
7
|
-
*/
|
|
8
|
-
export type { ExecuteRequest };
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Response interface for command execution
|
|
12
|
-
*/
|
|
13
|
-
export interface ExecuteResponse extends BaseApiResponse {
|
|
14
|
-
stdout: string;
|
|
15
|
-
stderr: string;
|
|
16
|
-
exitCode: number;
|
|
17
|
-
command: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Client for command execution operations
|
|
22
|
-
*/
|
|
23
|
-
export class CommandClient extends BaseHttpClient {
|
|
24
|
-
/**
|
|
25
|
-
* Execute a command and return the complete result
|
|
26
|
-
* @param command - The command to execute
|
|
27
|
-
* @param sessionId - The session ID for this command execution
|
|
28
|
-
* @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
|
|
29
|
-
* @param env - Optional environment variables for this command
|
|
30
|
-
* @param cwd - Optional working directory for this command
|
|
31
|
-
*/
|
|
32
|
-
async execute(
|
|
33
|
-
command: string,
|
|
34
|
-
sessionId: string,
|
|
35
|
-
options?: {
|
|
36
|
-
timeoutMs?: number;
|
|
37
|
-
env?: Record<string, string>;
|
|
38
|
-
cwd?: string;
|
|
39
|
-
}
|
|
40
|
-
): Promise<ExecuteResponse> {
|
|
41
|
-
try {
|
|
42
|
-
const data: ExecuteRequest = {
|
|
43
|
-
command,
|
|
44
|
-
sessionId,
|
|
45
|
-
...(options?.timeoutMs !== undefined && {
|
|
46
|
-
timeoutMs: options.timeoutMs
|
|
47
|
-
}),
|
|
48
|
-
...(options?.env !== undefined && { env: options.env }),
|
|
49
|
-
...(options?.cwd !== undefined && { cwd: options.cwd })
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const response = await this.post<ExecuteResponse>('/api/execute', data);
|
|
53
|
-
|
|
54
|
-
this.logSuccess(
|
|
55
|
-
'Command executed',
|
|
56
|
-
`${command}, Success: ${response.success}`
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
// Call the callback if provided
|
|
60
|
-
this.options.onCommandComplete?.(
|
|
61
|
-
response.success,
|
|
62
|
-
response.exitCode,
|
|
63
|
-
response.stdout,
|
|
64
|
-
response.stderr,
|
|
65
|
-
response.command
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return response;
|
|
69
|
-
} catch (error) {
|
|
70
|
-
this.logError('execute', error);
|
|
71
|
-
|
|
72
|
-
// Call error callback if provided
|
|
73
|
-
this.options.onError?.(
|
|
74
|
-
error instanceof Error ? error.message : String(error),
|
|
75
|
-
command
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Execute a command and return a stream of events
|
|
84
|
-
* @param command - The command to execute
|
|
85
|
-
* @param sessionId - The session ID for this command execution
|
|
86
|
-
* @param options - Optional per-command execution settings
|
|
87
|
-
*/
|
|
88
|
-
async executeStream(
|
|
89
|
-
command: string,
|
|
90
|
-
sessionId: string,
|
|
91
|
-
options?: {
|
|
92
|
-
timeoutMs?: number;
|
|
93
|
-
env?: Record<string, string>;
|
|
94
|
-
cwd?: string;
|
|
95
|
-
}
|
|
96
|
-
): Promise<ReadableStream<Uint8Array>> {
|
|
97
|
-
try {
|
|
98
|
-
const data = {
|
|
99
|
-
command,
|
|
100
|
-
sessionId,
|
|
101
|
-
...(options?.timeoutMs !== undefined && {
|
|
102
|
-
timeoutMs: options.timeoutMs
|
|
103
|
-
}),
|
|
104
|
-
...(options?.env !== undefined && { env: options.env }),
|
|
105
|
-
...(options?.cwd !== undefined && { cwd: options.cwd })
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const response = await this.doFetch('/api/execute/stream', {
|
|
109
|
-
method: 'POST',
|
|
110
|
-
headers: {
|
|
111
|
-
'Content-Type': 'application/json'
|
|
112
|
-
},
|
|
113
|
-
body: JSON.stringify(data)
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const stream = await this.handleStreamResponse(response);
|
|
117
|
-
|
|
118
|
-
this.logSuccess('Command stream started', command);
|
|
119
|
-
|
|
120
|
-
return stream;
|
|
121
|
-
} catch (error) {
|
|
122
|
-
this.logError('executeStream', error);
|
|
123
|
-
|
|
124
|
-
// Call error callback if provided
|
|
125
|
-
this.options.onError?.(
|
|
126
|
-
error instanceof Error ? error.message : String(error),
|
|
127
|
-
command
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|