@cloudflare/sandbox 0.0.0-cecde0a → 0.0.0-d4bb3b7

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 (59) hide show
  1. package/CHANGELOG.md +314 -0
  2. package/Dockerfile +179 -69
  3. package/LICENSE +176 -0
  4. package/README.md +119 -315
  5. package/dist/index.d.ts +1953 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3280 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +16 -7
  10. package/src/clients/base-client.ts +295 -0
  11. package/src/clients/command-client.ts +115 -0
  12. package/src/clients/file-client.ts +300 -0
  13. package/src/clients/git-client.ts +98 -0
  14. package/src/clients/index.ts +64 -0
  15. package/src/clients/interpreter-client.ts +333 -0
  16. package/src/clients/port-client.ts +105 -0
  17. package/src/clients/process-client.ts +180 -0
  18. package/src/clients/sandbox-client.ts +39 -0
  19. package/src/clients/types.ts +88 -0
  20. package/src/clients/utility-client.ts +156 -0
  21. package/src/errors/adapter.ts +238 -0
  22. package/src/errors/classes.ts +594 -0
  23. package/src/errors/index.ts +109 -0
  24. package/src/file-stream.ts +169 -0
  25. package/src/index.ts +98 -14
  26. package/src/interpreter.ts +168 -0
  27. package/src/request-handler.ts +94 -55
  28. package/src/sandbox.ts +938 -315
  29. package/src/security.ts +34 -28
  30. package/src/sse-parser.ts +8 -11
  31. package/src/version.ts +6 -0
  32. package/startup.sh +3 -0
  33. package/tests/base-client.test.ts +364 -0
  34. package/tests/command-client.test.ts +444 -0
  35. package/tests/file-client.test.ts +831 -0
  36. package/tests/file-stream.test.ts +310 -0
  37. package/tests/get-sandbox.test.ts +149 -0
  38. package/tests/git-client.test.ts +487 -0
  39. package/tests/port-client.test.ts +293 -0
  40. package/tests/process-client.test.ts +683 -0
  41. package/tests/request-handler.test.ts +292 -0
  42. package/tests/sandbox.test.ts +739 -0
  43. package/tests/sse-parser.test.ts +291 -0
  44. package/tests/utility-client.test.ts +339 -0
  45. package/tests/version.test.ts +16 -0
  46. package/tests/wrangler.jsonc +35 -0
  47. package/tsconfig.json +9 -1
  48. package/tsdown.config.ts +12 -0
  49. package/vitest.config.ts +31 -0
  50. package/container_src/handler/exec.ts +0 -337
  51. package/container_src/handler/file.ts +0 -844
  52. package/container_src/handler/git.ts +0 -182
  53. package/container_src/handler/ports.ts +0 -314
  54. package/container_src/handler/process.ts +0 -640
  55. package/container_src/index.ts +0 -361
  56. package/container_src/package.json +0 -9
  57. package/container_src/types.ts +0 -103
  58. package/src/client.ts +0 -1038
  59. package/src/types.ts +0 -386
@@ -0,0 +1,295 @@
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 provisioning retry configuration
10
+ const TIMEOUT_MS = 60_000; // 60 seconds total timeout budget
11
+ const MIN_TIME_FOR_RETRY_MS = 10_000; // Need at least 10s remaining to retry (8s Container + 2s delay)
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 provisioning delays
29
+ */
30
+ protected async doFetch(
31
+ path: string,
32
+ options?: RequestInit
33
+ ): Promise<Response> {
34
+ const startTime = Date.now();
35
+ let attempt = 0;
36
+
37
+ while (true) {
38
+ const response = await this.executeFetch(path, options);
39
+
40
+ // Only retry container provisioning 503s, not user app 503s
41
+ if (response.status === 503) {
42
+ const isContainerProvisioning =
43
+ await this.isContainerProvisioningError(response);
44
+
45
+ if (isContainerProvisioning) {
46
+ const elapsed = Date.now() - startTime;
47
+ const remaining = TIMEOUT_MS - elapsed;
48
+
49
+ // Check if we have enough time for another attempt
50
+ // (Need at least 10s: 8s for Container timeout + 2s delay)
51
+ if (remaining > MIN_TIME_FOR_RETRY_MS) {
52
+ // Exponential backoff: 2s, 4s, 8s, 16s (capped at 16s)
53
+ const delay = Math.min(2000 * 2 ** attempt, 16000);
54
+
55
+ this.logger.info('Container provisioning in progress, retrying', {
56
+ attempt: attempt + 1,
57
+ delayMs: delay,
58
+ remainingSec: Math.floor(remaining / 1000)
59
+ });
60
+
61
+ await new Promise((resolve) => setTimeout(resolve, delay));
62
+ attempt++;
63
+ continue;
64
+ } else {
65
+ // Exhausted retries - log error and return response
66
+ // Let existing error handling convert to proper error
67
+ this.logger.error(
68
+ 'Container failed to provision after multiple attempts',
69
+ new Error(`Failed after ${attempt + 1} attempts over 60s`)
70
+ );
71
+ return response;
72
+ }
73
+ }
74
+ }
75
+
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: Record<string, any>,
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 503 response is from container provisioning (retryable)
246
+ * vs user application (not retryable)
247
+ */
248
+ private async isContainerProvisioningError(
249
+ response: Response
250
+ ): Promise<boolean> {
251
+ try {
252
+ // Clone response so we don't consume the original body
253
+ const cloned = response.clone();
254
+ const text = await cloned.text();
255
+
256
+ // Container package returns specific message for provisioning errors
257
+ return text.includes('There is no Container instance available');
258
+ } catch (error) {
259
+ this.logger.error(
260
+ 'Error checking response body',
261
+ error instanceof Error ? error : new Error(String(error))
262
+ );
263
+ // If we can't read the body, don't retry to be safe
264
+ return false;
265
+ }
266
+ }
267
+
268
+ private async executeFetch(
269
+ path: string,
270
+ options?: RequestInit
271
+ ): Promise<Response> {
272
+ const url = this.options.stub
273
+ ? `http://localhost:${this.options.port}${path}`
274
+ : `${this.baseUrl}${path}`;
275
+
276
+ try {
277
+ if (this.options.stub) {
278
+ return await this.options.stub.containerFetch(
279
+ url,
280
+ options || {},
281
+ this.options.port
282
+ );
283
+ } else {
284
+ return await fetch(url, options);
285
+ }
286
+ } catch (error) {
287
+ this.logger.error(
288
+ 'HTTP request error',
289
+ error instanceof Error ? error : new Error(String(error)),
290
+ { method: options?.method || 'GET', url }
291
+ );
292
+ throw error;
293
+ }
294
+ }
295
+ }
@@ -0,0 +1,115 @@
1
+ import { BaseHttpClient } from './base-client';
2
+ import type {
3
+ BaseApiResponse,
4
+ HttpClientOptions,
5
+ SessionRequest
6
+ } from './types';
7
+
8
+ /**
9
+ * Request interface for command execution
10
+ */
11
+ export interface ExecuteRequest extends SessionRequest {
12
+ command: string;
13
+ timeoutMs?: number;
14
+ }
15
+
16
+ /**
17
+ * Response interface for command execution
18
+ */
19
+ export interface ExecuteResponse extends BaseApiResponse {
20
+ stdout: string;
21
+ stderr: string;
22
+ exitCode: number;
23
+ command: string;
24
+ }
25
+
26
+ /**
27
+ * Client for command execution operations
28
+ */
29
+ export class CommandClient extends BaseHttpClient {
30
+ /**
31
+ * Execute a command and return the complete result
32
+ * @param command - The command to execute
33
+ * @param sessionId - The session ID for this command execution
34
+ * @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
35
+ */
36
+ async execute(
37
+ command: string,
38
+ sessionId: string,
39
+ timeoutMs?: number
40
+ ): Promise<ExecuteResponse> {
41
+ try {
42
+ const data: ExecuteRequest = {
43
+ command,
44
+ sessionId,
45
+ ...(timeoutMs !== undefined && { timeoutMs })
46
+ };
47
+
48
+ const response = await this.post<ExecuteResponse>('/api/execute', data);
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,300 @@
1
+ import type {
2
+ DeleteFileResult,
3
+ FileExistsResult,
4
+ ListFilesOptions,
5
+ ListFilesResult,
6
+ MkdirResult,
7
+ MoveFileResult,
8
+ ReadFileResult,
9
+ RenameFileResult,
10
+ WriteFileResult
11
+ } from '@repo/shared';
12
+ import { BaseHttpClient } from './base-client';
13
+ import type { HttpClientOptions, SessionRequest } from './types';
14
+
15
+ /**
16
+ * Request interface for creating directories
17
+ */
18
+ export interface MkdirRequest extends SessionRequest {
19
+ path: string;
20
+ recursive?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Request interface for writing files
25
+ */
26
+ export interface WriteFileRequest extends SessionRequest {
27
+ path: string;
28
+ content: string;
29
+ encoding?: string;
30
+ }
31
+
32
+ /**
33
+ * Request interface for reading files
34
+ */
35
+ export interface ReadFileRequest extends SessionRequest {
36
+ path: string;
37
+ encoding?: string;
38
+ }
39
+
40
+ /**
41
+ * Request interface for file operations (delete, rename, move)
42
+ */
43
+ export interface FileOperationRequest extends SessionRequest {
44
+ path: string;
45
+ newPath?: string; // For rename/move operations
46
+ }
47
+
48
+ /**
49
+ * Client for file system operations
50
+ */
51
+ export class FileClient extends BaseHttpClient {
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(
73
+ 'Directory created',
74
+ `${path} (recursive: ${data.recursive})`
75
+ );
76
+ return response;
77
+ } catch (error) {
78
+ this.logError('mkdir', error);
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Write content to a file
85
+ * @param path - File path to write to
86
+ * @param content - Content to write
87
+ * @param sessionId - The session ID for this operation
88
+ * @param options - Optional settings (encoding)
89
+ */
90
+ async writeFile(
91
+ path: string,
92
+ content: string,
93
+ sessionId: string,
94
+ options?: { encoding?: string }
95
+ ): Promise<WriteFileResult> {
96
+ try {
97
+ const data = {
98
+ path,
99
+ content,
100
+ sessionId,
101
+ encoding: options?.encoding
102
+ };
103
+
104
+ const response = await this.post<WriteFileResult>('/api/write', data);
105
+
106
+ this.logSuccess('File written', `${path} (${content.length} chars)`);
107
+ return response;
108
+ } catch (error) {
109
+ this.logError('writeFile', error);
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Read content from a file
116
+ * @param path - File path to read from
117
+ * @param sessionId - The session ID for this operation
118
+ * @param options - Optional settings (encoding)
119
+ */
120
+ async readFile(
121
+ path: string,
122
+ sessionId: string,
123
+ options?: { encoding?: string }
124
+ ): Promise<ReadFileResult> {
125
+ try {
126
+ const data = {
127
+ path,
128
+ sessionId,
129
+ encoding: options?.encoding
130
+ };
131
+
132
+ const response = await this.post<ReadFileResult>('/api/read', data);
133
+
134
+ this.logSuccess(
135
+ 'File read',
136
+ `${path} (${response.content.length} chars)`
137
+ );
138
+ return response;
139
+ } catch (error) {
140
+ this.logError('readFile', error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Stream a file using Server-Sent Events
147
+ * Returns a ReadableStream of SSE events containing metadata, chunks, and completion
148
+ * @param path - File path to stream
149
+ * @param sessionId - The session ID for this operation
150
+ */
151
+ async readFileStream(
152
+ path: string,
153
+ sessionId: string
154
+ ): Promise<ReadableStream<Uint8Array>> {
155
+ try {
156
+ const data = {
157
+ path,
158
+ sessionId
159
+ };
160
+
161
+ const response = await this.doFetch('/api/read/stream', {
162
+ method: 'POST',
163
+ headers: {
164
+ 'Content-Type': 'application/json'
165
+ },
166
+ body: JSON.stringify(data)
167
+ });
168
+
169
+ const stream = await this.handleStreamResponse(response);
170
+ this.logSuccess('File stream started', path);
171
+ return stream;
172
+ } catch (error) {
173
+ this.logError('readFileStream', error);
174
+ throw error;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Delete a file
180
+ * @param path - File path to delete
181
+ * @param sessionId - The session ID for this operation
182
+ */
183
+ async deleteFile(path: string, sessionId: string): Promise<DeleteFileResult> {
184
+ try {
185
+ const data = { path, sessionId };
186
+
187
+ const response = await this.post<DeleteFileResult>('/api/delete', data);
188
+
189
+ this.logSuccess('File deleted', path);
190
+ return response;
191
+ } catch (error) {
192
+ this.logError('deleteFile', error);
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Rename a file
199
+ * @param path - Current file path
200
+ * @param newPath - New file path
201
+ * @param sessionId - The session ID for this operation
202
+ */
203
+ async renameFile(
204
+ path: string,
205
+ newPath: string,
206
+ sessionId: string
207
+ ): Promise<RenameFileResult> {
208
+ try {
209
+ const data = { oldPath: path, newPath, sessionId };
210
+
211
+ const response = await this.post<RenameFileResult>('/api/rename', data);
212
+
213
+ this.logSuccess('File renamed', `${path} -> ${newPath}`);
214
+ return response;
215
+ } catch (error) {
216
+ this.logError('renameFile', error);
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Move a file
223
+ * @param path - Current file path
224
+ * @param newPath - Destination file path
225
+ * @param sessionId - The session ID for this operation
226
+ */
227
+ async moveFile(
228
+ path: string,
229
+ newPath: string,
230
+ sessionId: string
231
+ ): Promise<MoveFileResult> {
232
+ try {
233
+ const data = { sourcePath: path, destinationPath: newPath, sessionId };
234
+
235
+ const response = await this.post<MoveFileResult>('/api/move', data);
236
+
237
+ this.logSuccess('File moved', `${path} -> ${newPath}`);
238
+ return response;
239
+ } catch (error) {
240
+ this.logError('moveFile', error);
241
+ throw error;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * List files in a directory
247
+ * @param path - Directory path to list
248
+ * @param sessionId - The session ID for this operation
249
+ * @param options - Optional settings (recursive, includeHidden)
250
+ */
251
+ async listFiles(
252
+ path: string,
253
+ sessionId: string,
254
+ options?: ListFilesOptions
255
+ ): Promise<ListFilesResult> {
256
+ try {
257
+ const data = {
258
+ path,
259
+ sessionId,
260
+ options: options || {}
261
+ };
262
+
263
+ const response = await this.post<ListFilesResult>(
264
+ '/api/list-files',
265
+ data
266
+ );
267
+
268
+ this.logSuccess('Files listed', `${path} (${response.count} files)`);
269
+ return response;
270
+ } catch (error) {
271
+ this.logError('listFiles', error);
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Check if a file or directory exists
278
+ * @param path - Path to check
279
+ * @param sessionId - The session ID for this operation
280
+ */
281
+ async exists(path: string, sessionId: string): Promise<FileExistsResult> {
282
+ try {
283
+ const data = {
284
+ path,
285
+ sessionId
286
+ };
287
+
288
+ const response = await this.post<FileExistsResult>('/api/exists', data);
289
+
290
+ this.logSuccess(
291
+ 'Path existence checked',
292
+ `${path} (exists: ${response.exists})`
293
+ );
294
+ return response;
295
+ } catch (error) {
296
+ this.logError('exists', error);
297
+ throw error;
298
+ }
299
+ }
300
+ }