@cloudflare/sandbox 0.0.0-02ee8fe

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 (80) hide show
  1. package/CHANGELOG.md +311 -0
  2. package/Dockerfile +143 -0
  3. package/README.md +162 -0
  4. package/dist/chunk-BFVUNTP4.js +104 -0
  5. package/dist/chunk-BFVUNTP4.js.map +1 -0
  6. package/dist/chunk-EKSWCBCA.js +86 -0
  7. package/dist/chunk-EKSWCBCA.js.map +1 -0
  8. package/dist/chunk-JXZMAU2C.js +559 -0
  9. package/dist/chunk-JXZMAU2C.js.map +1 -0
  10. package/dist/chunk-UJ3TV4M6.js +7 -0
  11. package/dist/chunk-UJ3TV4M6.js.map +1 -0
  12. package/dist/chunk-YE265ASX.js +2484 -0
  13. package/dist/chunk-YE265ASX.js.map +1 -0
  14. package/dist/chunk-Z532A7QC.js +78 -0
  15. package/dist/chunk-Z532A7QC.js.map +1 -0
  16. package/dist/file-stream.d.ts +43 -0
  17. package/dist/file-stream.js +9 -0
  18. package/dist/file-stream.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +67 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/interpreter.d.ts +33 -0
  23. package/dist/interpreter.js +8 -0
  24. package/dist/interpreter.js.map +1 -0
  25. package/dist/request-handler.d.ts +18 -0
  26. package/dist/request-handler.js +13 -0
  27. package/dist/request-handler.js.map +1 -0
  28. package/dist/sandbox-CLZWpfGc.d.ts +613 -0
  29. package/dist/sandbox.d.ts +4 -0
  30. package/dist/sandbox.js +13 -0
  31. package/dist/sandbox.js.map +1 -0
  32. package/dist/security.d.ts +31 -0
  33. package/dist/security.js +13 -0
  34. package/dist/security.js.map +1 -0
  35. package/dist/sse-parser.d.ts +28 -0
  36. package/dist/sse-parser.js +11 -0
  37. package/dist/sse-parser.js.map +1 -0
  38. package/dist/version.d.ts +8 -0
  39. package/dist/version.js +7 -0
  40. package/dist/version.js.map +1 -0
  41. package/package.json +44 -0
  42. package/src/clients/base-client.ts +280 -0
  43. package/src/clients/command-client.ts +115 -0
  44. package/src/clients/file-client.ts +295 -0
  45. package/src/clients/git-client.ts +92 -0
  46. package/src/clients/index.ts +64 -0
  47. package/src/clients/interpreter-client.ts +329 -0
  48. package/src/clients/port-client.ts +105 -0
  49. package/src/clients/process-client.ts +177 -0
  50. package/src/clients/sandbox-client.ts +41 -0
  51. package/src/clients/types.ts +84 -0
  52. package/src/clients/utility-client.ts +119 -0
  53. package/src/errors/adapter.ts +180 -0
  54. package/src/errors/classes.ts +469 -0
  55. package/src/errors/index.ts +105 -0
  56. package/src/file-stream.ts +164 -0
  57. package/src/index.ts +93 -0
  58. package/src/interpreter.ts +159 -0
  59. package/src/request-handler.ts +180 -0
  60. package/src/sandbox.ts +1045 -0
  61. package/src/security.ts +104 -0
  62. package/src/sse-parser.ts +143 -0
  63. package/src/version.ts +6 -0
  64. package/startup.sh +3 -0
  65. package/tests/base-client.test.ts +328 -0
  66. package/tests/command-client.test.ts +407 -0
  67. package/tests/file-client.test.ts +719 -0
  68. package/tests/file-stream.test.ts +306 -0
  69. package/tests/get-sandbox.test.ts +149 -0
  70. package/tests/git-client.test.ts +328 -0
  71. package/tests/port-client.test.ts +301 -0
  72. package/tests/process-client.test.ts +658 -0
  73. package/tests/request-handler.test.ts +240 -0
  74. package/tests/sandbox.test.ts +554 -0
  75. package/tests/sse-parser.test.ts +290 -0
  76. package/tests/utility-client.test.ts +332 -0
  77. package/tests/version.test.ts +16 -0
  78. package/tests/wrangler.jsonc +35 -0
  79. package/tsconfig.json +11 -0
  80. package/vitest.config.ts +31 -0
@@ -0,0 +1,13 @@
1
+ import {
2
+ Sandbox,
3
+ getSandbox
4
+ } from "./chunk-YE265ASX.js";
5
+ import "./chunk-JXZMAU2C.js";
6
+ import "./chunk-Z532A7QC.js";
7
+ import "./chunk-EKSWCBCA.js";
8
+ import "./chunk-UJ3TV4M6.js";
9
+ export {
10
+ Sandbox,
11
+ getSandbox
12
+ };
13
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Security utilities for URL construction and input validation
3
+ *
4
+ * This module contains critical security functions to prevent:
5
+ * - URL injection attacks
6
+ * - SSRF (Server-Side Request Forgery) attacks
7
+ * - DNS rebinding attacks
8
+ * - Host header injection
9
+ * - Open redirect vulnerabilities
10
+ */
11
+ declare class SecurityError extends Error {
12
+ readonly code?: string | undefined;
13
+ constructor(message: string, code?: string | undefined);
14
+ }
15
+ /**
16
+ * Validates port numbers for sandbox services
17
+ * Only allows non-system ports to prevent conflicts and security issues
18
+ */
19
+ declare function validatePort(port: number): boolean;
20
+ /**
21
+ * Sanitizes and validates sandbox IDs for DNS compliance and security
22
+ * Only enforces critical requirements - allows maximum developer flexibility
23
+ */
24
+ declare function sanitizeSandboxId(id: string): string;
25
+ /**
26
+ * Validates language for code interpreter
27
+ * Only allows supported languages
28
+ */
29
+ declare function validateLanguage(language: string | undefined): void;
30
+
31
+ export { SecurityError, sanitizeSandboxId, validateLanguage, validatePort };
@@ -0,0 +1,13 @@
1
+ import {
2
+ SecurityError,
3
+ sanitizeSandboxId,
4
+ validateLanguage,
5
+ validatePort
6
+ } from "./chunk-Z532A7QC.js";
7
+ export {
8
+ SecurityError,
9
+ sanitizeSandboxId,
10
+ validateLanguage,
11
+ validatePort
12
+ };
13
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Server-Sent Events (SSE) parser for streaming responses
3
+ * Converts ReadableStream<Uint8Array> to typed AsyncIterable<T>
4
+ */
5
+ /**
6
+ * Parse a ReadableStream of SSE events into typed AsyncIterable
7
+ * @param stream - The ReadableStream from fetch response
8
+ * @param signal - Optional AbortSignal for cancellation
9
+ */
10
+ declare function parseSSEStream<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncIterable<T>;
11
+ /**
12
+ * Helper to convert a Response with SSE stream directly to AsyncIterable
13
+ * @param response - Response object with SSE stream
14
+ * @param signal - Optional AbortSignal for cancellation
15
+ */
16
+ declare function responseToAsyncIterable<T>(response: Response, signal?: AbortSignal): AsyncIterable<T>;
17
+ /**
18
+ * Create an SSE-formatted ReadableStream from an AsyncIterable
19
+ * (Useful for Worker endpoints that need to forward AsyncIterable as SSE)
20
+ * @param events - AsyncIterable of events
21
+ * @param options - Stream options
22
+ */
23
+ declare function asyncIterableToSSEStream<T>(events: AsyncIterable<T>, options?: {
24
+ signal?: AbortSignal;
25
+ serialize?: (event: T) => string;
26
+ }): ReadableStream<Uint8Array>;
27
+
28
+ export { asyncIterableToSSEStream, parseSSEStream, responseToAsyncIterable };
@@ -0,0 +1,11 @@
1
+ import {
2
+ asyncIterableToSSEStream,
3
+ parseSSEStream,
4
+ responseToAsyncIterable
5
+ } from "./chunk-EKSWCBCA.js";
6
+ export {
7
+ asyncIterableToSSEStream,
8
+ parseSSEStream,
9
+ responseToAsyncIterable
10
+ };
11
+ //# sourceMappingURL=sse-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SDK version - automatically synchronized with package.json by Changesets
3
+ * This file is auto-updated by .github/changeset-version.ts during releases
4
+ * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
5
+ */
6
+ declare const SDK_VERSION = "0.4.12";
7
+
8
+ export { SDK_VERSION };
@@ -0,0 +1,7 @@
1
+ import {
2
+ SDK_VERSION
3
+ } from "./chunk-UJ3TV4M6.js";
4
+ export {
5
+ SDK_VERSION
6
+ };
7
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@cloudflare/sandbox",
3
+ "version": "0.0.0-02ee8fe",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/cloudflare/sandbox-sdk"
7
+ },
8
+ "description": "A sandboxed environment for running commands",
9
+ "dependencies": {
10
+ "@cloudflare/containers": "^0.0.29"
11
+ },
12
+ "devDependencies": {
13
+ "@repo/shared": "^0.0.0"
14
+ },
15
+ "tags": [
16
+ "sandbox",
17
+ "codegen",
18
+ "containers",
19
+ "cloudflare",
20
+ "durable objects"
21
+ ],
22
+ "scripts": {
23
+ "build": "rm -rf dist && tsup src/*.ts --outDir dist --dts --sourcemap --format esm",
24
+ "check": "biome check && npm run typecheck",
25
+ "fix": "biome check --fix && npm run typecheck",
26
+ "typecheck": "tsc --noEmit",
27
+ "docker:local": "cd ../.. && docker build -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox-test:$npm_package_version .",
28
+ "docker:publish": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox:$npm_package_version --push .",
29
+ "docker:publish:beta": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox:$npm_package_version-beta --push .",
30
+ "test": "vitest run --config vitest.config.ts \"$@\"",
31
+ "test:e2e": "cd ../.. && vitest run --config vitest.e2e.config.ts \"$@\""
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "require": "./dist/index.js"
38
+ }
39
+ },
40
+ "keywords": [],
41
+ "author": "",
42
+ "license": "ISC",
43
+ "type": "module"
44
+ }
@@ -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
+ }