@cloudflare/sandbox 0.0.0-cbb7fcd → 0.0.0-cecde0a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @cloudflare/sandbox
2
2
 
3
+ ## 0.0.9
4
+
5
+ ### Patch Changes
6
+
7
+ - [#20](https://github.com/cloudflare/sandbox-sdk/pull/20) [`f106fda`](https://github.com/cloudflare/sandbox-sdk/commit/f106fdac98e7ef35677326290d45cbf3af88982c) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - add preview URLs and dynamic port forwarding
8
+
9
+ ## 0.0.8
10
+
11
+ ### Patch Changes
12
+
13
+ - [`60af265`](https://github.com/cloudflare/sandbox-sdk/commit/60af265d834e83fd30a921a3e1be232f13fe24da) Thanks [@threepointone](https://github.com/threepointone)! - update dependencies
14
+
15
+ ## 0.0.7
16
+
17
+ ### Patch Changes
18
+
19
+ - [`d1c7c99`](https://github.com/cloudflare/sandbox-sdk/commit/d1c7c99df6555eff71bcd59852e4b8eed2ad8cb6) Thanks [@threepointone](https://github.com/threepointone)! - fix file operations
20
+
21
+ ## 0.0.6
22
+
23
+ ### Patch Changes
24
+
25
+ - [#9](https://github.com/cloudflare/sandbox-sdk/pull/9) [`24f5470`](https://github.com/cloudflare/sandbox-sdk/commit/24f547048d5a26137de4656cea13d83ad2cc0b43) Thanks [@ItsWendell](https://github.com/ItsWendell)! - fix baseUrl for stub and stub forwarding
26
+
3
27
  ## 0.0.5
4
28
 
5
29
  ### Patch Changes
package/Dockerfile CHANGED
@@ -1,16 +1,80 @@
1
- # syntax=docker/dockerfile:1
1
+ # Sandbox base image with development tools, Python, Node.js, and Bun
2
+ FROM ubuntu:22.04
2
3
 
3
- FROM oven/bun:latest
4
- # Set destination for COPY
4
+ # Prevent interactive prompts during package installation
5
+ ENV DEBIAN_FRONTEND=noninteractive
6
+
7
+ # Install essential system packages and development tools
8
+ RUN apt-get update && apt-get install -y \
9
+ # Basic utilities
10
+ curl \
11
+ wget \
12
+ git \
13
+ unzip \
14
+ zip \
15
+ # Process management
16
+ procps \
17
+ htop \
18
+ # Build tools
19
+ build-essential \
20
+ pkg-config \
21
+ # Network tools
22
+ net-tools \
23
+ iputils-ping \
24
+ dnsutils \
25
+ # Text processing
26
+ jq \
27
+ vim \
28
+ nano \
29
+ # Python dependencies
30
+ python3.11 \
31
+ python3.11-dev \
32
+ python3-pip \
33
+ # Other useful tools
34
+ sudo \
35
+ ca-certificates \
36
+ gnupg \
37
+ lsb-release \
38
+ && rm -rf /var/lib/apt/lists/*
39
+
40
+ # Set Python 3.11 as default python3
41
+ RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
42
+
43
+ # Install Node.js 22 LTS
44
+ # Using the official NodeSource repository setup script
45
+ RUN apt-get update && apt-get install -y ca-certificates curl gnupg \
46
+ && mkdir -p /etc/apt/keyrings \
47
+ && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
48
+ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
49
+ && apt-get update \
50
+ && apt-get install -y nodejs \
51
+ && rm -rf /var/lib/apt/lists/*
52
+
53
+ # Install Bun using the official installation script
54
+ RUN curl -fsSL https://bun.sh/install | bash \
55
+ && mv /root/.bun/bin/bun /usr/local/bin/bun \
56
+ && mv /root/.bun/bin/bunx /usr/local/bin/bunx \
57
+ && rm -rf /root/.bun
58
+
59
+ # Install global npm packages as root
60
+ RUN npm install -g yarn pnpm
61
+
62
+ # Set up working directory
5
63
  WORKDIR /app
6
64
 
7
- # Install git
8
- RUN apt-get update && apt-get install -y git
65
+ # Verify installations
66
+ RUN python3 --version && \
67
+ node --version && \
68
+ npm --version && \
69
+ bun --version && \
70
+ yarn --version && \
71
+ pnpm --version
9
72
 
10
- COPY container_src/* ./
11
- # RUN bun install
73
+ # Copy container source files
74
+ COPY container_src/ ./
12
75
 
76
+ # Expose the application port
13
77
  EXPOSE 3000
14
- # Run
15
- CMD ["bun", "index.ts"]
16
78
 
79
+ # Run the application
80
+ CMD ["bun", "index.ts"]
package/README.md CHANGED
@@ -47,19 +47,319 @@ import { getSandbox } from "@cloudflare/sandbox";
47
47
  export default {
48
48
  async fetch(request: Request, env: Env) {
49
49
  const sandbox = getSandbox(env.Sandbox, "my-sandbox");
50
- return sandbox.exec("ls", ["-la"]);
50
+ const result = await sandbox.exec("ls -la");
51
+ return Response.json(result);
51
52
  },
52
53
  };
53
54
  ```
54
55
 
55
- ### Methods:
56
-
57
- - `exec(command: string, args: string[], options?: { stream?: boolean })`: Execute a command in the sandbox.
58
- - `gitCheckout(repoUrl: string, options: { branch?: string; targetDir?: string; stream?: boolean })`: Checkout a git repository in the sandbox.
59
- - `mkdir(path: string, options: { recursive?: boolean; stream?: boolean })`: Create a directory in the sandbox.
60
- - `writeFile(path: string, content: string, options: { encoding?: string; stream?: boolean })`: Write content to a file in the sandbox.
61
- - `readFile(path: string, options: { encoding?: string; stream?: boolean })`: Read content from a file in the sandbox.
62
- - `deleteFile(path: string, options?: { stream?: boolean })`: Delete a file from the sandbox.
63
- - `renameFile(oldPath: string, newPath: string, options?: { stream?: boolean })`: Rename a file in the sandbox.
64
- - `moveFile(sourcePath: string, destinationPath: string, options?: { stream?: boolean })`: Move a file from one location to another in the sandbox.
65
- - `ping()`: Ping the sandbox.
56
+ ### Core Methods
57
+
58
+ #### Command Execution
59
+ - `exec(command: string, options?: ExecOptions)`: Execute a command and return the complete result.
60
+ - `execStream(command: string, options?: StreamOptions)`: Execute a command with real-time streaming (returns ReadableStream).
61
+
62
+ #### Process Management
63
+ - `startProcess(command: string, options?: ProcessOptions)`: Start a background process.
64
+ - `listProcesses()`: List all running processes.
65
+ - `getProcess(id: string)`: Get details of a specific process.
66
+ - `killProcess(id: string, signal?: string)`: Kill a specific process.
67
+ - `killAllProcesses()`: Kill all running processes.
68
+ - `streamProcessLogs(processId: string, options?: { signal?: AbortSignal })`: Stream logs from a running process (returns ReadableStream).
69
+
70
+ #### File Operations
71
+ - `gitCheckout(repoUrl: string, options: { branch?: string; targetDir?: string })`: Checkout a git repository.
72
+ - `mkdir(path: string, options?: { recursive?: boolean })`: Create a directory.
73
+ - `writeFile(path: string, content: string, options?: { encoding?: string })`: Write content to a file.
74
+ - `readFile(path: string, options?: { encoding?: string })`: Read content from a file.
75
+ - `deleteFile(path: string)`: Delete a file.
76
+ - `renameFile(oldPath: string, newPath: string)`: Rename a file.
77
+ - `moveFile(sourcePath: string, destinationPath: string)`: Move a file.
78
+
79
+ #### Port Management
80
+ - `exposePort(port: number, options: { name?: string; hostname: string })`: Expose a port for external access.
81
+ - `unexposePort(port: number)`: Unexpose a previously exposed port.
82
+ - `getExposedPorts(hostname: string)`: List all exposed ports with their preview URLs.
83
+
84
+ ### Beautiful AsyncIterable Streaming APIs ✨
85
+
86
+ The SDK provides streaming methods that return `ReadableStream` for RPC compatibility, along with a `parseSSEStream` utility to convert them to typed AsyncIterables:
87
+
88
+ #### Stream Command Output
89
+ ```typescript
90
+ import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
91
+
92
+ // Get the stream and convert to AsyncIterable
93
+ const stream = await sandbox.execStream('npm run build');
94
+ for await (const event of parseSSEStream<ExecEvent>(stream)) {
95
+ switch (event.type) {
96
+ case 'start':
97
+ console.log(`Build started: ${event.command}`);
98
+ break;
99
+ case 'stdout':
100
+ console.log(`[OUT] ${event.data}`);
101
+ break;
102
+ case 'stderr':
103
+ console.error(`[ERR] ${event.data}`);
104
+ break;
105
+ case 'complete':
106
+ console.log(`Build finished with exit code: ${event.exitCode}`);
107
+ break;
108
+ case 'error':
109
+ console.error(`Build error: ${event.error}`);
110
+ break;
111
+ }
112
+ }
113
+ ```
114
+
115
+ #### Stream Process Logs
116
+ ```typescript
117
+ import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';
118
+
119
+ // Monitor background process logs
120
+ const webServer = await sandbox.startProcess('node server.js');
121
+
122
+ const logStream = await sandbox.streamProcessLogs(webServer.id);
123
+ for await (const log of parseSSEStream<LogEvent>(logStream)) {
124
+ if (log.type === 'stdout') {
125
+ console.log(`Server: ${log.data}`);
126
+ } else if (log.type === 'stderr' && log.data.includes('ERROR')) {
127
+ // React to errors
128
+ await handleError(log);
129
+ } else if (log.type === 'exit') {
130
+ console.log(`Server exited with code: ${log.exitCode}`);
131
+ break;
132
+ }
133
+ }
134
+ ```
135
+
136
+ #### Why parseSSEStream?
137
+
138
+ The streaming methods return `ReadableStream<Uint8Array>` to ensure compatibility across Durable Object RPC boundaries. The `parseSSEStream` utility converts these streams into typed AsyncIterables, giving you the best of both worlds:
139
+
140
+ - **RPC Compatibility**: ReadableStream can be serialized across process boundaries
141
+ - **Beautiful APIs**: AsyncIterable provides clean `for await` syntax with typed events
142
+ - **Type Safety**: Full TypeScript support with `ExecEvent` and `LogEvent` types
143
+
144
+ #### Advanced Examples
145
+
146
+ ##### CI/CD Build System
147
+ ```typescript
148
+ import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
149
+
150
+ export async function runBuild(env: Env, buildId: string) {
151
+ const sandbox = getSandbox(env.Sandbox, buildId);
152
+ const buildLog: string[] = [];
153
+
154
+ try {
155
+ const stream = await sandbox.execStream('npm run build');
156
+ for await (const event of parseSSEStream<ExecEvent>(stream)) {
157
+ buildLog.push(`[${event.type}] ${event.data || ''}`);
158
+
159
+ if (event.type === 'complete') {
160
+ await env.BUILDS.put(buildId, {
161
+ status: event.exitCode === 0 ? 'success' : 'failed',
162
+ exitCode: event.exitCode,
163
+ logs: buildLog.join('\n'),
164
+ duration: Date.now() - new Date(event.timestamp).getTime()
165
+ });
166
+ }
167
+ }
168
+ } catch (error) {
169
+ await env.BUILDS.put(buildId, {
170
+ status: 'error',
171
+ error: error.message,
172
+ logs: buildLog.join('\n')
173
+ });
174
+ }
175
+ }
176
+ ```
177
+
178
+ ##### System Monitoring
179
+ ```typescript
180
+ import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';
181
+
182
+ export default {
183
+ async scheduled(controller: ScheduledController, env: Env) {
184
+ const sandbox = getSandbox(env.Sandbox, 'monitor');
185
+
186
+ // Monitor system logs
187
+ const monitor = await sandbox.startProcess('journalctl -f');
188
+
189
+ const logStream = await sandbox.streamProcessLogs(monitor.id);
190
+ for await (const log of parseSSEStream<LogEvent>(logStream)) {
191
+ if (log.type === 'stdout') {
192
+ // Check for critical errors
193
+ if (log.data.includes('CRITICAL')) {
194
+ await env.ALERTS.send({
195
+ severity: 'critical',
196
+ message: log.data,
197
+ timestamp: log.timestamp
198
+ });
199
+ }
200
+
201
+ // Store logs
202
+ await env.LOGS.put(`${log.timestamp}-${monitor.id}`, log.data);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ ```
208
+
209
+ ##### Streaming to Frontend via SSE
210
+ ```typescript
211
+ // Worker endpoint that streams to frontend
212
+ app.get('/api/build/:id/stream', async (req, env) => {
213
+ const sandbox = getSandbox(env.Sandbox, req.params.id);
214
+ const encoder = new TextEncoder();
215
+
216
+ return new Response(
217
+ new ReadableStream({
218
+ async start(controller) {
219
+ try {
220
+ for await (const event of sandbox.execStream('npm run build')) {
221
+ // Forward events to frontend as SSE
222
+ const sseEvent = `data: ${JSON.stringify(event)}\n\n`;
223
+ controller.enqueue(encoder.encode(sseEvent));
224
+ }
225
+ } catch (error) {
226
+ const errorEvent = `data: ${JSON.stringify({
227
+ type: 'error',
228
+ error: error.message
229
+ })}\n\n`;
230
+ controller.enqueue(encoder.encode(errorEvent));
231
+ } finally {
232
+ controller.close();
233
+ }
234
+ }
235
+ }),
236
+ {
237
+ headers: {
238
+ 'Content-Type': 'text/event-stream',
239
+ 'Cache-Control': 'no-cache'
240
+ }
241
+ }
242
+ );
243
+ });
244
+ ```
245
+
246
+ ### Streaming Utilities
247
+
248
+ The SDK exports additional utilities for working with SSE streams:
249
+
250
+ #### `parseSSEStream`
251
+ Converts a `ReadableStream<Uint8Array>` (from SSE endpoints) into a typed `AsyncIterable<T>`. This is the primary utility for consuming streams from the SDK.
252
+
253
+ ```typescript
254
+ import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
255
+
256
+ const stream = await sandbox.execStream('npm build');
257
+ for await (const event of parseSSEStream<ExecEvent>(stream)) {
258
+ console.log(event);
259
+ }
260
+ ```
261
+
262
+ #### `responseToAsyncIterable`
263
+ Converts a `Response` object with SSE content directly to `AsyncIterable<T>`. Useful when fetching from external SSE endpoints.
264
+
265
+ ```typescript
266
+ import { responseToAsyncIterable, type LogEvent } from '@cloudflare/sandbox';
267
+
268
+ // Fetch from an external SSE endpoint
269
+ const response = await fetch('https://api.example.com/logs/stream', {
270
+ headers: { 'Accept': 'text/event-stream' }
271
+ });
272
+
273
+ // Convert Response to typed AsyncIterable
274
+ for await (const event of responseToAsyncIterable<LogEvent>(response)) {
275
+ console.log(`[${event.type}] ${event.data}`);
276
+ }
277
+ ```
278
+
279
+ #### `asyncIterableToSSEStream`
280
+ Converts an `AsyncIterable<T>` into an SSE-formatted `ReadableStream<Uint8Array>`. Perfect for Worker endpoints that need to transform or filter events before sending to clients.
281
+
282
+ ```typescript
283
+ import { getSandbox, parseSSEStream, asyncIterableToSSEStream, type LogEvent } from '@cloudflare/sandbox';
284
+
285
+ export async function handleFilteredLogs(request: Request, env: Env) {
286
+ const sandbox = getSandbox(env.SANDBOX);
287
+
288
+ // Custom async generator that filters logs
289
+ async function* filterLogs() {
290
+ const stream = await sandbox.streamProcessLogs('web-server');
291
+
292
+ for await (const log of parseSSEStream<LogEvent>(stream)) {
293
+ // Only forward error logs to the client
294
+ if (log.type === 'stderr' || log.data.includes('ERROR')) {
295
+ yield log;
296
+ }
297
+ }
298
+ }
299
+
300
+ // Convert filtered AsyncIterable back to SSE stream for the response
301
+ const sseStream = asyncIterableToSSEStream(filterLogs());
302
+
303
+ return new Response(sseStream, {
304
+ headers: {
305
+ 'Content-Type': 'text/event-stream',
306
+ 'Cache-Control': 'no-cache',
307
+ }
308
+ });
309
+ }
310
+ ```
311
+
312
+ **Advanced Example - Merging Multiple Streams:**
313
+ ```typescript
314
+ async function* mergeBuilds(env: Env) {
315
+ const sandbox1 = getSandbox(env.SANDBOX1);
316
+ const sandbox2 = getSandbox(env.SANDBOX2);
317
+
318
+ // Start builds in parallel
319
+ const [stream1, stream2] = await Promise.all([
320
+ sandbox1.execStream('npm run build:frontend'),
321
+ sandbox2.execStream('npm run build:backend')
322
+ ]);
323
+
324
+ // Parse and merge events
325
+ const frontend = parseSSEStream<ExecEvent>(stream1);
326
+ const backend = parseSSEStream<ExecEvent>(stream2);
327
+
328
+ // Merge with source identification
329
+ for await (const event of frontend) {
330
+ yield { ...event, source: 'frontend' };
331
+ }
332
+ for await (const event of backend) {
333
+ yield { ...event, source: 'backend' };
334
+ }
335
+ }
336
+
337
+ // Convert merged stream to SSE for client
338
+ const mergedSSE = asyncIterableToSSEStream(mergeBuilds(env));
339
+ ```
340
+
341
+ ### Cancellation Support
342
+
343
+ Both streaming methods support cancellation via AbortSignal:
344
+
345
+ ```typescript
346
+ const controller = new AbortController();
347
+
348
+ // Cancel after 30 seconds
349
+ setTimeout(() => controller.abort(), 30000);
350
+
351
+ try {
352
+ for await (const event of sandbox.execStream('long-running-task', {
353
+ signal: controller.signal
354
+ })) {
355
+ // Process events
356
+ if (shouldCancel(event)) {
357
+ controller.abort();
358
+ }
359
+ }
360
+ } catch (error) {
361
+ if (error.message.includes('aborted')) {
362
+ console.log('Operation cancelled');
363
+ }
364
+ }
365
+ ```