@cloudflare/sandbox 0.3.7 → 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.
- package/.turbo/turbo-build.log +44 -0
- package/CHANGELOG.md +6 -14
- package/Dockerfile +82 -18
- package/README.md +89 -824
- package/dist/{chunk-JTKON2SH.js → chunk-BCJ7SF3Q.js} +9 -5
- package/dist/chunk-BCJ7SF3Q.js.map +1 -0
- package/dist/chunk-BFVUNTP4.js +104 -0
- package/dist/chunk-BFVUNTP4.js.map +1 -0
- package/dist/{chunk-NNGBXDMY.js → chunk-EKSWCBCA.js} +3 -6
- package/dist/chunk-EKSWCBCA.js.map +1 -0
- package/dist/chunk-HGF554LH.js +2236 -0
- package/dist/chunk-HGF554LH.js.map +1 -0
- package/dist/{chunk-6UAWTJ5S.js → chunk-Z532A7QC.js} +13 -20
- package/dist/{chunk-6UAWTJ5S.js.map → chunk-Z532A7QC.js.map} +1 -1
- package/dist/file-stream.d.ts +16 -38
- package/dist/file-stream.js +1 -2
- package/dist/index.d.ts +6 -5
- package/dist/index.js +35 -39
- package/dist/index.js.map +1 -1
- package/dist/interpreter.d.ts +3 -3
- package/dist/interpreter.js +2 -2
- package/dist/request-handler.d.ts +4 -3
- package/dist/request-handler.js +4 -7
- package/dist/sandbox-D9K2ypln.d.ts +583 -0
- package/dist/sandbox.d.ts +3 -3
- package/dist/sandbox.js +4 -7
- package/dist/security.d.ts +4 -3
- package/dist/security.js +3 -3
- package/dist/sse-parser.js +1 -1
- package/package.json +11 -5
- package/src/clients/base-client.ts +280 -0
- package/src/clients/command-client.ts +115 -0
- package/src/clients/file-client.ts +269 -0
- package/src/clients/git-client.ts +92 -0
- package/src/clients/index.ts +63 -0
- package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
- package/src/clients/port-client.ts +105 -0
- package/src/clients/process-client.ts +177 -0
- package/src/clients/sandbox-client.ts +41 -0
- package/src/clients/types.ts +84 -0
- package/src/clients/utility-client.ts +94 -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 +69 -43
- package/src/sandbox.ts +694 -533
- package/src/security.ts +14 -23
- package/src/sse-parser.ts +4 -8
- package/startup.sh +3 -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 +290 -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 -601
- 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/dist/chunk-32UDXUPC.js +0 -671
- package/dist/chunk-32UDXUPC.js.map +0 -1
- package/dist/chunk-5DILEXGY.js +0 -85
- package/dist/chunk-5DILEXGY.js.map +0 -1
- package/dist/chunk-D3U63BZP.js +0 -240
- package/dist/chunk-D3U63BZP.js.map +0 -1
- package/dist/chunk-FXYPFGOZ.js +0 -129
- package/dist/chunk-FXYPFGOZ.js.map +0 -1
- package/dist/chunk-JTKON2SH.js.map +0 -1
- package/dist/chunk-NNGBXDMY.js.map +0 -1
- package/dist/chunk-SQLJNZ3K.js +0 -674
- package/dist/chunk-SQLJNZ3K.js.map +0 -1
- package/dist/chunk-W7TVRPBG.js +0 -108
- package/dist/chunk-W7TVRPBG.js.map +0 -1
- package/dist/client-B3RUab0s.d.ts +0 -225
- package/dist/client.d.ts +0 -4
- package/dist/client.js +0 -7
- package/dist/client.js.map +0 -1
- package/dist/errors.d.ts +0 -95
- package/dist/errors.js +0 -27
- package/dist/errors.js.map +0 -1
- package/dist/interpreter-client.d.ts +0 -4
- package/dist/interpreter-client.js +0 -9
- package/dist/interpreter-client.js.map +0 -1
- package/dist/interpreter-types.d.ts +0 -259
- package/dist/interpreter-types.js +0 -9
- package/dist/interpreter-types.js.map +0 -1
- package/dist/types.d.ts +0 -453
- package/dist/types.js +0 -45
- package/dist/types.js.map +0 -1
- 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
|
@@ -0,0 +1,280 @@
|
|
|
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 {
|
|
8
|
+
HttpClientOptions,
|
|
9
|
+
ResponseHandler
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
// Container provisioning retry configuration
|
|
13
|
+
const TIMEOUT_MS = 60_000; // 60 seconds total timeout budget
|
|
14
|
+
const MIN_TIME_FOR_RETRY_MS = 10_000; // Need at least 10s remaining to retry (8s Container + 2s delay)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Abstract base class providing common HTTP functionality for all domain clients
|
|
18
|
+
*/
|
|
19
|
+
export abstract class BaseHttpClient {
|
|
20
|
+
protected baseUrl: string;
|
|
21
|
+
protected options: HttpClientOptions;
|
|
22
|
+
protected logger: Logger;
|
|
23
|
+
|
|
24
|
+
constructor(options: HttpClientOptions = {}) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.logger = options.logger ?? createNoOpLogger();
|
|
27
|
+
this.baseUrl = this.options.baseUrl!;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Core HTTP request method with automatic retry for container provisioning delays
|
|
32
|
+
*/
|
|
33
|
+
protected async doFetch(
|
|
34
|
+
path: string,
|
|
35
|
+
options?: RequestInit
|
|
36
|
+
): Promise<Response> {
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
let attempt = 0;
|
|
39
|
+
|
|
40
|
+
while (true) {
|
|
41
|
+
const response = await this.executeFetch(path, options);
|
|
42
|
+
|
|
43
|
+
// Only retry container provisioning 503s, not user app 503s
|
|
44
|
+
if (response.status === 503) {
|
|
45
|
+
const isContainerProvisioning = await this.isContainerProvisioningError(response);
|
|
46
|
+
|
|
47
|
+
if (isContainerProvisioning) {
|
|
48
|
+
const elapsed = Date.now() - startTime;
|
|
49
|
+
const remaining = TIMEOUT_MS - elapsed;
|
|
50
|
+
|
|
51
|
+
// Check if we have enough time for another attempt
|
|
52
|
+
// (Need at least 10s: 8s for Container timeout + 2s delay)
|
|
53
|
+
if (remaining > MIN_TIME_FOR_RETRY_MS) {
|
|
54
|
+
// Exponential backoff: 2s, 4s, 8s, 16s (capped at 16s)
|
|
55
|
+
const delay = Math.min(2000 * 2 ** attempt, 16000);
|
|
56
|
+
|
|
57
|
+
this.logger.info('Container provisioning in progress, retrying', {
|
|
58
|
+
attempt: attempt + 1,
|
|
59
|
+
delayMs: delay,
|
|
60
|
+
remainingSec: Math.floor(remaining / 1000)
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
64
|
+
attempt++;
|
|
65
|
+
continue;
|
|
66
|
+
} else {
|
|
67
|
+
// Exhausted retries - log error and return response
|
|
68
|
+
// Let existing error handling convert to proper error
|
|
69
|
+
this.logger.error('Container failed to provision after multiple attempts', new Error(`Failed after ${attempt + 1} attempts over 60s`));
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Make a POST request with JSON body
|
|
81
|
+
*/
|
|
82
|
+
protected async post<T>(
|
|
83
|
+
endpoint: string,
|
|
84
|
+
data: Record<string, any>,
|
|
85
|
+
responseHandler?: ResponseHandler<T>
|
|
86
|
+
): Promise<T> {
|
|
87
|
+
const response = await this.doFetch(endpoint, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify(data),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return this.handleResponse(response, responseHandler);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Make a GET request
|
|
100
|
+
*/
|
|
101
|
+
protected async get<T>(
|
|
102
|
+
endpoint: string,
|
|
103
|
+
responseHandler?: ResponseHandler<T>
|
|
104
|
+
): Promise<T> {
|
|
105
|
+
const response = await this.doFetch(endpoint, {
|
|
106
|
+
method: 'GET',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return this.handleResponse(response, responseHandler);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Make a DELETE request
|
|
114
|
+
*/
|
|
115
|
+
protected async delete<T>(
|
|
116
|
+
endpoint: string,
|
|
117
|
+
responseHandler?: ResponseHandler<T>
|
|
118
|
+
): Promise<T> {
|
|
119
|
+
const response = await this.doFetch(endpoint, {
|
|
120
|
+
method: 'DELETE',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return this.handleResponse(response, responseHandler);
|
|
124
|
+
}
|
|
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: ${error instanceof Error ? error.message : 'Unknown parsing error'}`,
|
|
149
|
+
context: {},
|
|
150
|
+
httpStatus: response.status,
|
|
151
|
+
timestamp: new Date().toISOString()
|
|
152
|
+
};
|
|
153
|
+
throw createErrorFromResponse(errorResponse);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handle error responses with consistent error throwing
|
|
159
|
+
*/
|
|
160
|
+
protected async handleErrorResponse(response: Response): Promise<never> {
|
|
161
|
+
let errorData: NewErrorResponse;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
errorData = await response.json();
|
|
165
|
+
} catch {
|
|
166
|
+
// Fallback if response isn't JSON or parsing fails
|
|
167
|
+
errorData = {
|
|
168
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
169
|
+
message: `HTTP error! status: ${response.status}`,
|
|
170
|
+
context: { statusText: response.statusText },
|
|
171
|
+
httpStatus: response.status,
|
|
172
|
+
timestamp: new Date().toISOString()
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Convert ErrorResponse to appropriate Error class
|
|
177
|
+
const error = createErrorFromResponse(errorData);
|
|
178
|
+
|
|
179
|
+
// Call error callback if provided
|
|
180
|
+
this.options.onError?.(errorData.message, undefined);
|
|
181
|
+
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
|
|
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(`${operation} completed successfully`, details ? { details } : undefined);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Utility method to log errors intelligently
|
|
213
|
+
* Only logs unexpected errors (5xx), not expected errors (4xx)
|
|
214
|
+
*
|
|
215
|
+
* - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
|
|
216
|
+
* - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
|
|
217
|
+
*/
|
|
218
|
+
protected logError(operation: string, error: unknown): void {
|
|
219
|
+
// Check if it's a SandboxError with HTTP status
|
|
220
|
+
if (error && typeof error === 'object' && 'httpStatus' in error) {
|
|
221
|
+
const httpStatus = (error as SandboxError).httpStatus;
|
|
222
|
+
|
|
223
|
+
// Only log server errors (5xx), not client errors (4xx)
|
|
224
|
+
if (httpStatus >= 500) {
|
|
225
|
+
this.logger.error(
|
|
226
|
+
`Unexpected error in ${operation}`,
|
|
227
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
228
|
+
{ httpStatus }
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
// 4xx errors are expected (validation, not found, etc.) - don't log
|
|
232
|
+
} else {
|
|
233
|
+
// Non-SandboxError (unexpected) - log it
|
|
234
|
+
this.logger.error(
|
|
235
|
+
`Error in ${operation}`,
|
|
236
|
+
error instanceof Error ? error : new Error(String(error))
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if 503 response is from container provisioning (retryable)
|
|
243
|
+
* vs user application (not retryable)
|
|
244
|
+
*/
|
|
245
|
+
private async isContainerProvisioningError(response: Response): Promise<boolean> {
|
|
246
|
+
try {
|
|
247
|
+
// Clone response so we don't consume the original body
|
|
248
|
+
const cloned = response.clone();
|
|
249
|
+
const text = await cloned.text();
|
|
250
|
+
|
|
251
|
+
// Container package returns specific message for provisioning errors
|
|
252
|
+
return text.includes('There is no Container instance available');
|
|
253
|
+
} catch (error) {
|
|
254
|
+
this.logger.error('Error checking response body', error instanceof Error ? error : new Error(String(error)));
|
|
255
|
+
// If we can't read the body, don't retry to be safe
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async executeFetch(path: string, options?: RequestInit): Promise<Response> {
|
|
261
|
+
const url = this.options.stub
|
|
262
|
+
? `http://localhost:${this.options.port}${path}`
|
|
263
|
+
: `${this.baseUrl}${path}`;
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
if (this.options.stub) {
|
|
267
|
+
return await this.options.stub.containerFetch(
|
|
268
|
+
url,
|
|
269
|
+
options || {},
|
|
270
|
+
this.options.port
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
return await fetch(url, options);
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.logger.error('HTTP request error', error instanceof Error ? error : new Error(String(error)), { method: options?.method || 'GET', url });
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { BaseHttpClient } from './base-client';
|
|
2
|
+
import type { BaseApiResponse, HttpClientOptions, SessionRequest } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Request interface for command execution
|
|
6
|
+
*/
|
|
7
|
+
export interface ExecuteRequest extends SessionRequest {
|
|
8
|
+
command: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Response interface for command execution
|
|
14
|
+
*/
|
|
15
|
+
export interface ExecuteResponse extends BaseApiResponse {
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
command: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Client for command execution operations
|
|
24
|
+
*/
|
|
25
|
+
export class CommandClient extends BaseHttpClient {
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Execute a command and return the complete result
|
|
29
|
+
* @param command - The command to execute
|
|
30
|
+
* @param sessionId - The session ID for this command execution
|
|
31
|
+
* @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
|
|
32
|
+
*/
|
|
33
|
+
async execute(
|
|
34
|
+
command: string,
|
|
35
|
+
sessionId: string,
|
|
36
|
+
timeoutMs?: number
|
|
37
|
+
): Promise<ExecuteResponse> {
|
|
38
|
+
try {
|
|
39
|
+
const data: ExecuteRequest = {
|
|
40
|
+
command,
|
|
41
|
+
sessionId,
|
|
42
|
+
...(timeoutMs !== undefined && { timeoutMs })
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const response = await this.post<ExecuteResponse>(
|
|
46
|
+
'/api/execute',
|
|
47
|
+
data
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
this.logSuccess(
|
|
51
|
+
'Command executed',
|
|
52
|
+
`${command}, Success: ${response.success}`
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Call the callback if provided
|
|
56
|
+
this.options.onCommandComplete?.(
|
|
57
|
+
response.success,
|
|
58
|
+
response.exitCode,
|
|
59
|
+
response.stdout,
|
|
60
|
+
response.stderr,
|
|
61
|
+
response.command
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return response;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
this.logError('execute', error);
|
|
67
|
+
|
|
68
|
+
// Call error callback if provided
|
|
69
|
+
this.options.onError?.(
|
|
70
|
+
error instanceof Error ? error.message : String(error),
|
|
71
|
+
command
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Execute a command and return a stream of events
|
|
80
|
+
* @param command - The command to execute
|
|
81
|
+
* @param sessionId - The session ID for this command execution
|
|
82
|
+
*/
|
|
83
|
+
async executeStream(
|
|
84
|
+
command: string,
|
|
85
|
+
sessionId: string
|
|
86
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
87
|
+
try {
|
|
88
|
+
const data = { command, sessionId };
|
|
89
|
+
|
|
90
|
+
const response = await this.doFetch('/api/execute/stream', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify(data),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const stream = await this.handleStreamResponse(response);
|
|
99
|
+
|
|
100
|
+
this.logSuccess('Command stream started', command);
|
|
101
|
+
|
|
102
|
+
return stream;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.logError('executeStream', error);
|
|
105
|
+
|
|
106
|
+
// Call error callback if provided
|
|
107
|
+
this.options.onError?.(
|
|
108
|
+
error instanceof Error ? error.message : String(error),
|
|
109
|
+
command
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeleteFileResult,
|
|
3
|
+
ListFilesOptions,
|
|
4
|
+
ListFilesResult,
|
|
5
|
+
MkdirResult,
|
|
6
|
+
MoveFileResult,
|
|
7
|
+
ReadFileResult,
|
|
8
|
+
RenameFileResult,
|
|
9
|
+
WriteFileResult
|
|
10
|
+
} from '@repo/shared';
|
|
11
|
+
import { BaseHttpClient } from './base-client';
|
|
12
|
+
import type { HttpClientOptions, SessionRequest } from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Request interface for creating directories
|
|
16
|
+
*/
|
|
17
|
+
export interface MkdirRequest extends SessionRequest {
|
|
18
|
+
path: string;
|
|
19
|
+
recursive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Request interface for writing files
|
|
24
|
+
*/
|
|
25
|
+
export interface WriteFileRequest extends SessionRequest {
|
|
26
|
+
path: string;
|
|
27
|
+
content: string;
|
|
28
|
+
encoding?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Request interface for reading files
|
|
33
|
+
*/
|
|
34
|
+
export interface ReadFileRequest extends SessionRequest {
|
|
35
|
+
path: string;
|
|
36
|
+
encoding?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Request interface for file operations (delete, rename, move)
|
|
41
|
+
*/
|
|
42
|
+
export interface FileOperationRequest extends SessionRequest {
|
|
43
|
+
path: string;
|
|
44
|
+
newPath?: string; // For rename/move operations
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Client for file system operations
|
|
49
|
+
*/
|
|
50
|
+
export class FileClient extends BaseHttpClient {
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a directory
|
|
54
|
+
* @param path - Directory path to create
|
|
55
|
+
* @param sessionId - The session ID for this operation
|
|
56
|
+
* @param options - Optional settings (recursive)
|
|
57
|
+
*/
|
|
58
|
+
async mkdir(
|
|
59
|
+
path: string,
|
|
60
|
+
sessionId: string,
|
|
61
|
+
options?: { recursive?: boolean }
|
|
62
|
+
): Promise<MkdirResult> {
|
|
63
|
+
try {
|
|
64
|
+
const data = {
|
|
65
|
+
path,
|
|
66
|
+
sessionId,
|
|
67
|
+
recursive: options?.recursive ?? false,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const response = await this.post<MkdirResult>('/api/mkdir', data);
|
|
71
|
+
|
|
72
|
+
this.logSuccess('Directory created', `${path} (recursive: ${data.recursive})`);
|
|
73
|
+
return response;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.logError('mkdir', error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Write content to a file
|
|
82
|
+
* @param path - File path to write to
|
|
83
|
+
* @param content - Content to write
|
|
84
|
+
* @param sessionId - The session ID for this operation
|
|
85
|
+
* @param options - Optional settings (encoding)
|
|
86
|
+
*/
|
|
87
|
+
async writeFile(
|
|
88
|
+
path: string,
|
|
89
|
+
content: string,
|
|
90
|
+
sessionId: string,
|
|
91
|
+
options?: { encoding?: string }
|
|
92
|
+
): Promise<WriteFileResult> {
|
|
93
|
+
try {
|
|
94
|
+
const data = {
|
|
95
|
+
path,
|
|
96
|
+
content,
|
|
97
|
+
sessionId,
|
|
98
|
+
encoding: options?.encoding ?? 'utf8',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const response = await this.post<WriteFileResult>('/api/write', data);
|
|
102
|
+
|
|
103
|
+
this.logSuccess('File written', `${path} (${content.length} chars)`);
|
|
104
|
+
return response;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.logError('writeFile', error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read content from a file
|
|
113
|
+
* @param path - File path to read from
|
|
114
|
+
* @param sessionId - The session ID for this operation
|
|
115
|
+
* @param options - Optional settings (encoding)
|
|
116
|
+
*/
|
|
117
|
+
async readFile(
|
|
118
|
+
path: string,
|
|
119
|
+
sessionId: string,
|
|
120
|
+
options?: { encoding?: string }
|
|
121
|
+
): Promise<ReadFileResult> {
|
|
122
|
+
try {
|
|
123
|
+
const data = {
|
|
124
|
+
path,
|
|
125
|
+
sessionId,
|
|
126
|
+
encoding: options?.encoding ?? 'utf8',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const response = await this.post<ReadFileResult>('/api/read', data);
|
|
130
|
+
|
|
131
|
+
this.logSuccess('File read', `${path} (${response.content.length} chars)`);
|
|
132
|
+
return response;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this.logError('readFile', error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Stream a file using Server-Sent Events
|
|
141
|
+
* Returns a ReadableStream of SSE events containing metadata, chunks, and completion
|
|
142
|
+
* @param path - File path to stream
|
|
143
|
+
* @param sessionId - The session ID for this operation
|
|
144
|
+
*/
|
|
145
|
+
async readFileStream(
|
|
146
|
+
path: string,
|
|
147
|
+
sessionId: string
|
|
148
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
149
|
+
try {
|
|
150
|
+
const data = {
|
|
151
|
+
path,
|
|
152
|
+
sessionId,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const response = await this.doFetch('/api/read/stream', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify(data),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const stream = await this.handleStreamResponse(response);
|
|
164
|
+
this.logSuccess('File stream started', path);
|
|
165
|
+
return stream;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.logError('readFileStream', error);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Delete a file
|
|
174
|
+
* @param path - File path to delete
|
|
175
|
+
* @param sessionId - The session ID for this operation
|
|
176
|
+
*/
|
|
177
|
+
async deleteFile(
|
|
178
|
+
path: string,
|
|
179
|
+
sessionId: string
|
|
180
|
+
): Promise<DeleteFileResult> {
|
|
181
|
+
try {
|
|
182
|
+
const data = { path, sessionId };
|
|
183
|
+
|
|
184
|
+
const response = await this.post<DeleteFileResult>('/api/delete', data);
|
|
185
|
+
|
|
186
|
+
this.logSuccess('File deleted', path);
|
|
187
|
+
return response;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
this.logError('deleteFile', error);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Rename a file
|
|
196
|
+
* @param path - Current file path
|
|
197
|
+
* @param newPath - New file path
|
|
198
|
+
* @param sessionId - The session ID for this operation
|
|
199
|
+
*/
|
|
200
|
+
async renameFile(
|
|
201
|
+
path: string,
|
|
202
|
+
newPath: string,
|
|
203
|
+
sessionId: string
|
|
204
|
+
): Promise<RenameFileResult> {
|
|
205
|
+
try {
|
|
206
|
+
const data = { oldPath: path, newPath, sessionId };
|
|
207
|
+
|
|
208
|
+
const response = await this.post<RenameFileResult>('/api/rename', data);
|
|
209
|
+
|
|
210
|
+
this.logSuccess('File renamed', `${path} -> ${newPath}`);
|
|
211
|
+
return response;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.logError('renameFile', error);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Move a file
|
|
220
|
+
* @param path - Current file path
|
|
221
|
+
* @param newPath - Destination file path
|
|
222
|
+
* @param sessionId - The session ID for this operation
|
|
223
|
+
*/
|
|
224
|
+
async moveFile(
|
|
225
|
+
path: string,
|
|
226
|
+
newPath: string,
|
|
227
|
+
sessionId: string
|
|
228
|
+
): Promise<MoveFileResult> {
|
|
229
|
+
try {
|
|
230
|
+
const data = { sourcePath: path, destinationPath: newPath, sessionId };
|
|
231
|
+
|
|
232
|
+
const response = await this.post<MoveFileResult>('/api/move', data);
|
|
233
|
+
|
|
234
|
+
this.logSuccess('File moved', `${path} -> ${newPath}`);
|
|
235
|
+
return response;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
this.logError('moveFile', error);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* List files in a directory
|
|
244
|
+
* @param path - Directory path to list
|
|
245
|
+
* @param sessionId - The session ID for this operation
|
|
246
|
+
* @param options - Optional settings (recursive, includeHidden)
|
|
247
|
+
*/
|
|
248
|
+
async listFiles(
|
|
249
|
+
path: string,
|
|
250
|
+
sessionId: string,
|
|
251
|
+
options?: ListFilesOptions
|
|
252
|
+
): Promise<ListFilesResult> {
|
|
253
|
+
try {
|
|
254
|
+
const data = {
|
|
255
|
+
path,
|
|
256
|
+
sessionId,
|
|
257
|
+
options: options || {},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const response = await this.post<ListFilesResult>('/api/list-files', data);
|
|
261
|
+
|
|
262
|
+
this.logSuccess('Files listed', `${path} (${response.count} files)`);
|
|
263
|
+
return response;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
this.logError('listFiles', error);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|