@cloudflare/sandbox 0.0.0-6d1b288 → 0.0.0-6f16c81
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 +57 -0
- package/Dockerfile +86 -9
- package/README.md +3 -0
- package/container_src/index.ts +846 -214
- package/package.json +16 -3
- package/src/client.ts +396 -31
- package/src/index.ts +14 -79
- package/src/request-handler.ts +95 -0
- package/src/sandbox.ts +252 -0
- package/tests/test2.ts +219 -0
package/src/index.ts
CHANGED
|
@@ -1,79 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
16
|
-
);
|
|
17
|
-
},
|
|
18
|
-
onCommandStart: (command, args) => {
|
|
19
|
-
console.log(`[Container] Command started: ${command} ${args.join(" ")}`);
|
|
20
|
-
},
|
|
21
|
-
onError: (error, command, args) => {
|
|
22
|
-
console.error(`[Container] Command error: ${error}`);
|
|
23
|
-
},
|
|
24
|
-
onOutput: (stream, data, command) => {
|
|
25
|
-
console.log(`[Container] [${stream}] ${data}`);
|
|
26
|
-
},
|
|
27
|
-
port: this.defaultPort,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
envVars = {
|
|
31
|
-
MESSAGE: "I was passed in via the Sandbox class!",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
override onStart() {
|
|
35
|
-
console.log("Sandbox successfully started");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
override onStop() {
|
|
39
|
-
console.log("Sandbox successfully shut down");
|
|
40
|
-
if (this.client) {
|
|
41
|
-
this.client.clearSession();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
override onError(error: unknown) {
|
|
46
|
-
console.log("Sandbox error:", error);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async exec(command: string, args: string[], options?: { stream?: boolean }) {
|
|
50
|
-
if (options?.stream) {
|
|
51
|
-
return this.client.executeStream(command, args);
|
|
52
|
-
}
|
|
53
|
-
return this.client.execute(command, args);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async gitCheckout(
|
|
57
|
-
repoUrl: string,
|
|
58
|
-
options: { branch?: string; targetDir?: string; stream?: boolean }
|
|
59
|
-
) {
|
|
60
|
-
if (options?.stream) {
|
|
61
|
-
return this.client.gitCheckoutStream(
|
|
62
|
-
repoUrl,
|
|
63
|
-
options.branch,
|
|
64
|
-
options.targetDir
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async mkdir(
|
|
71
|
-
path: string,
|
|
72
|
-
options: { recursive?: boolean; stream?: boolean }
|
|
73
|
-
) {
|
|
74
|
-
if (options?.stream) {
|
|
75
|
-
return this.client.mkdirStream(path, options.recursive);
|
|
76
|
-
}
|
|
77
|
-
return this.client.mkdir(path, options.recursive);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
// Export types from client
|
|
2
|
+
export type {
|
|
3
|
+
DeleteFileResponse, ExecuteResponse,
|
|
4
|
+
GitCheckoutResponse,
|
|
5
|
+
MkdirResponse, MoveFileResponse,
|
|
6
|
+
ReadFileResponse, RenameFileResponse, WriteFileResponse
|
|
7
|
+
} from "./client";
|
|
8
|
+
|
|
9
|
+
// Re-export request handler utilities
|
|
10
|
+
export {
|
|
11
|
+
proxyToSandbox, type RouteInfo, type SandboxEnv
|
|
12
|
+
} from './request-handler';
|
|
13
|
+
|
|
14
|
+
export { getSandbox, Sandbox } from "./sandbox";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getSandbox, type Sandbox } from "./sandbox";
|
|
2
|
+
|
|
3
|
+
export interface SandboxEnv {
|
|
4
|
+
Sandbox: DurableObjectNamespace<Sandbox>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RouteInfo {
|
|
8
|
+
port: number;
|
|
9
|
+
sandboxId: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function proxyToSandbox<E extends SandboxEnv>(
|
|
14
|
+
request: Request,
|
|
15
|
+
env: E
|
|
16
|
+
): Promise<Response | null> {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
const routeInfo = extractSandboxRoute(url);
|
|
20
|
+
|
|
21
|
+
if (!routeInfo) {
|
|
22
|
+
return null; // Not a request to an exposed container port
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { sandboxId, port, path } = routeInfo;
|
|
26
|
+
const sandbox = getSandbox(env.Sandbox, sandboxId);
|
|
27
|
+
|
|
28
|
+
// Build proxy request with proper headers
|
|
29
|
+
let proxyUrl: string;
|
|
30
|
+
|
|
31
|
+
// Route based on the target port
|
|
32
|
+
if (port !== 3000) {
|
|
33
|
+
// Route directly to user's service on the specified port
|
|
34
|
+
proxyUrl = `http://localhost:${port}${path}${url.search}`;
|
|
35
|
+
} else {
|
|
36
|
+
// Port 3000 is our control plane - route normally
|
|
37
|
+
proxyUrl = `http://localhost:3000${path}${url.search}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const proxyRequest = new Request(proxyUrl, {
|
|
41
|
+
method: request.method,
|
|
42
|
+
headers: {
|
|
43
|
+
...Object.fromEntries(request.headers),
|
|
44
|
+
'X-Original-URL': request.url,
|
|
45
|
+
'X-Forwarded-Host': url.hostname,
|
|
46
|
+
'X-Forwarded-Proto': url.protocol.replace(':', ''),
|
|
47
|
+
'X-Sandbox-Name': sandboxId, // Pass the friendly name
|
|
48
|
+
},
|
|
49
|
+
body: request.body,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return sandbox.containerFetch(proxyRequest, port);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('[Sandbox] Proxy routing error:', error);
|
|
55
|
+
return new Response('Proxy routing error', { status: 500 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractSandboxRoute(url: URL): RouteInfo | null {
|
|
60
|
+
// Production: subdomain pattern {port}-{sandboxId}.{domain}
|
|
61
|
+
const subdomainMatch = url.hostname.match(/^(\d+)-([a-zA-Z0-9-]+)\./);
|
|
62
|
+
if (subdomainMatch) {
|
|
63
|
+
return {
|
|
64
|
+
port: parseInt(subdomainMatch[1]),
|
|
65
|
+
sandboxId: subdomainMatch[2],
|
|
66
|
+
path: url.pathname,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Development: path pattern /preview/{port}/{sandboxId}/*
|
|
71
|
+
if (isLocalhostPattern(url.hostname)) {
|
|
72
|
+
const pathMatch = url.pathname.match(/^\/preview\/(\d+)\/([^/]+)(\/.*)?$/);
|
|
73
|
+
if (pathMatch) {
|
|
74
|
+
return {
|
|
75
|
+
port: parseInt(pathMatch[1]),
|
|
76
|
+
sandboxId: pathMatch[2],
|
|
77
|
+
path: pathMatch[3] || "/",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isLocalhostPattern(hostname: string): boolean {
|
|
86
|
+
const hostPart = hostname.split(":")[0];
|
|
87
|
+
return (
|
|
88
|
+
hostPart === "localhost" ||
|
|
89
|
+
hostPart === "127.0.0.1" ||
|
|
90
|
+
hostPart === "::1" ||
|
|
91
|
+
hostPart === "[::1]" ||
|
|
92
|
+
hostPart === "0.0.0.0"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Container, getContainer } from "@cloudflare/containers";
|
|
2
|
+
import { HttpClient } from "./client";
|
|
3
|
+
import { isLocalhostPattern } from "./request-handler";
|
|
4
|
+
|
|
5
|
+
export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
6
|
+
const stub = getContainer(ns, id);
|
|
7
|
+
|
|
8
|
+
// Store the name on first access
|
|
9
|
+
stub.setSandboxName?.(id);
|
|
10
|
+
|
|
11
|
+
return stub;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Sandbox<Env = unknown> extends Container<Env> {
|
|
15
|
+
sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
|
|
16
|
+
client: HttpClient;
|
|
17
|
+
private workerHostname: string | null = null;
|
|
18
|
+
private sandboxName: string | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(ctx: DurableObjectState, env: Env) {
|
|
21
|
+
super(ctx, env);
|
|
22
|
+
this.client = new HttpClient({
|
|
23
|
+
onCommandComplete: (success, exitCode, _stdout, _stderr, command, _args) => {
|
|
24
|
+
console.log(
|
|
25
|
+
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
onCommandStart: (command, args) => {
|
|
29
|
+
console.log(
|
|
30
|
+
`[Container] Command started: ${command} ${args.join(" ")}`
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
onError: (error, _command, _args) => {
|
|
34
|
+
console.error(`[Container] Command error: ${error}`);
|
|
35
|
+
},
|
|
36
|
+
onOutput: (stream, data, _command) => {
|
|
37
|
+
console.log(`[Container] [${stream}] ${data}`);
|
|
38
|
+
},
|
|
39
|
+
port: 3000, // Control plane port
|
|
40
|
+
stub: this,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Load the sandbox name from storage on initialization
|
|
44
|
+
this.ctx.blockConcurrencyWhile(async () => {
|
|
45
|
+
this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// RPC method to set the sandbox name
|
|
50
|
+
async setSandboxName(name: string): Promise<void> {
|
|
51
|
+
if (!this.sandboxName) {
|
|
52
|
+
this.sandboxName = name;
|
|
53
|
+
await this.ctx.storage.put('sandboxName', name);
|
|
54
|
+
console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override onStart() {
|
|
59
|
+
console.log("Sandbox successfully started");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override onStop() {
|
|
63
|
+
console.log("Sandbox successfully shut down");
|
|
64
|
+
if (this.client) {
|
|
65
|
+
this.client.clearSession();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override onError(error: unknown) {
|
|
70
|
+
console.log("Sandbox error:", error);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Override fetch to capture the hostname and route to appropriate ports
|
|
74
|
+
override async fetch(request: Request): Promise<Response> {
|
|
75
|
+
const url = new URL(request.url);
|
|
76
|
+
|
|
77
|
+
// Capture the hostname from the first request
|
|
78
|
+
if (!this.workerHostname) {
|
|
79
|
+
this.workerHostname = url.hostname;
|
|
80
|
+
console.log(`[Sandbox] Captured hostname: ${this.workerHostname}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Capture and store the sandbox name from the header if present
|
|
84
|
+
if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {
|
|
85
|
+
const name = request.headers.get('X-Sandbox-Name')!;
|
|
86
|
+
this.sandboxName = name;
|
|
87
|
+
await this.ctx.storage.put('sandboxName', name);
|
|
88
|
+
console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Determine which port to route to
|
|
92
|
+
const port = this.determinePort(url);
|
|
93
|
+
|
|
94
|
+
// Route to the appropriate port
|
|
95
|
+
return await this.containerFetch(request, port);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private determinePort(url: URL): number {
|
|
99
|
+
// Extract port from proxy requests (e.g., /proxy/8080/*)
|
|
100
|
+
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
101
|
+
if (proxyMatch) {
|
|
102
|
+
return parseInt(proxyMatch[1]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// All other requests go to control plane on port 3000
|
|
106
|
+
// This includes /api/* endpoints and any other control requests
|
|
107
|
+
return 3000;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async exec(command: string, args: string[], options?: { stream?: boolean; background?: boolean }) {
|
|
111
|
+
if (options?.stream) {
|
|
112
|
+
return this.client.executeStream(command, args, undefined, options?.background);
|
|
113
|
+
}
|
|
114
|
+
return this.client.execute(command, args, undefined, options?.background);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async gitCheckout(
|
|
118
|
+
repoUrl: string,
|
|
119
|
+
options: { branch?: string; targetDir?: string; stream?: boolean }
|
|
120
|
+
) {
|
|
121
|
+
if (options?.stream) {
|
|
122
|
+
return this.client.gitCheckoutStream(
|
|
123
|
+
repoUrl,
|
|
124
|
+
options.branch,
|
|
125
|
+
options.targetDir
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async mkdir(
|
|
132
|
+
path: string,
|
|
133
|
+
options: { recursive?: boolean; stream?: boolean } = {}
|
|
134
|
+
) {
|
|
135
|
+
if (options?.stream) {
|
|
136
|
+
return this.client.mkdirStream(path, options.recursive);
|
|
137
|
+
}
|
|
138
|
+
return this.client.mkdir(path, options.recursive);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async writeFile(
|
|
142
|
+
path: string,
|
|
143
|
+
content: string,
|
|
144
|
+
options: { encoding?: string; stream?: boolean } = {}
|
|
145
|
+
) {
|
|
146
|
+
if (options?.stream) {
|
|
147
|
+
return this.client.writeFileStream(path, content, options.encoding);
|
|
148
|
+
}
|
|
149
|
+
return this.client.writeFile(path, content, options.encoding);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async deleteFile(path: string, options: { stream?: boolean } = {}) {
|
|
153
|
+
if (options?.stream) {
|
|
154
|
+
return this.client.deleteFileStream(path);
|
|
155
|
+
}
|
|
156
|
+
return this.client.deleteFile(path);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async renameFile(
|
|
160
|
+
oldPath: string,
|
|
161
|
+
newPath: string,
|
|
162
|
+
options: { stream?: boolean } = {}
|
|
163
|
+
) {
|
|
164
|
+
if (options?.stream) {
|
|
165
|
+
return this.client.renameFileStream(oldPath, newPath);
|
|
166
|
+
}
|
|
167
|
+
return this.client.renameFile(oldPath, newPath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async moveFile(
|
|
171
|
+
sourcePath: string,
|
|
172
|
+
destinationPath: string,
|
|
173
|
+
options: { stream?: boolean } = {}
|
|
174
|
+
) {
|
|
175
|
+
if (options?.stream) {
|
|
176
|
+
return this.client.moveFileStream(sourcePath, destinationPath);
|
|
177
|
+
}
|
|
178
|
+
return this.client.moveFile(sourcePath, destinationPath);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async readFile(
|
|
182
|
+
path: string,
|
|
183
|
+
options: { encoding?: string; stream?: boolean } = {}
|
|
184
|
+
) {
|
|
185
|
+
if (options?.stream) {
|
|
186
|
+
return this.client.readFileStream(path, options.encoding);
|
|
187
|
+
}
|
|
188
|
+
return this.client.readFile(path, options.encoding);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async exposePort(port: number, options?: { name?: string }) {
|
|
192
|
+
await this.client.exposePort(port, options?.name);
|
|
193
|
+
|
|
194
|
+
// We need the sandbox name to construct preview URLs
|
|
195
|
+
if (!this.sandboxName) {
|
|
196
|
+
throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hostname = this.getHostname();
|
|
200
|
+
const url = this.constructPreviewUrl(port, this.sandboxName, hostname);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
url,
|
|
204
|
+
port,
|
|
205
|
+
name: options?.name,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async unexposePort(port: number) {
|
|
210
|
+
await this.client.unexposePort(port);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async getExposedPorts() {
|
|
214
|
+
const response = await this.client.getExposedPorts();
|
|
215
|
+
|
|
216
|
+
// We need the sandbox name to construct preview URLs
|
|
217
|
+
if (!this.sandboxName) {
|
|
218
|
+
throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const hostname = this.getHostname();
|
|
222
|
+
|
|
223
|
+
return response.ports.map(port => ({
|
|
224
|
+
url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
|
|
225
|
+
port: port.port,
|
|
226
|
+
name: port.name,
|
|
227
|
+
exposedAt: port.exposedAt,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private getHostname(): string {
|
|
232
|
+
// Use the captured hostname or fall back to localhost for development
|
|
233
|
+
return this.workerHostname || "localhost:8787";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
|
|
237
|
+
// Check if this is a localhost pattern
|
|
238
|
+
const isLocalhost = isLocalhostPattern(hostname);
|
|
239
|
+
|
|
240
|
+
if (isLocalhost) {
|
|
241
|
+
// For local development, we need to use a different approach
|
|
242
|
+
// Since subdomains don't work with localhost, we'll use the base URL
|
|
243
|
+
// with a note that the user needs to handle routing differently
|
|
244
|
+
return `http://${hostname}/preview/${port}/${sandboxId}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// For all other domains (workers.dev, custom domains, etc.)
|
|
248
|
+
// Use subdomain-based routing pattern
|
|
249
|
+
const protocol = hostname.includes(":") ? "http" : "https";
|
|
250
|
+
return `${protocol}://${port}-${sandboxId}.${hostname}`;
|
|
251
|
+
}
|
|
252
|
+
}
|
package/tests/test2.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
quickExecuteStream,
|
|
8
8
|
quickMoveFile,
|
|
9
9
|
quickMoveFileStream,
|
|
10
|
+
quickReadFile,
|
|
11
|
+
quickReadFileStream,
|
|
10
12
|
quickRenameFile,
|
|
11
13
|
quickRenameFileStream,
|
|
12
14
|
quickWriteFile,
|
|
@@ -683,6 +685,223 @@ async function testHttpClient() {
|
|
|
683
685
|
);
|
|
684
686
|
}
|
|
685
687
|
|
|
688
|
+
// Test 25: File reading
|
|
689
|
+
console.log("Test 25: File reading");
|
|
690
|
+
try {
|
|
691
|
+
// First create a file to read
|
|
692
|
+
const readContent = "This is a test file for reading\nLine 2\nLine 3";
|
|
693
|
+
await quickWriteFile("file-to-read.txt", readContent);
|
|
694
|
+
console.log("✅ Test file created for reading");
|
|
695
|
+
|
|
696
|
+
// Read the file
|
|
697
|
+
const result = await quickReadFile("file-to-read.txt");
|
|
698
|
+
console.log("✅ File read successfully:", result.success);
|
|
699
|
+
console.log(" Path:", result.path);
|
|
700
|
+
console.log(" Content length:", result.content.length, "characters");
|
|
701
|
+
console.log(" Exit code:", result.exitCode);
|
|
702
|
+
|
|
703
|
+
// Verify the content
|
|
704
|
+
console.log("✅ File content verified:", result.content === readContent);
|
|
705
|
+
console.log(" Content:", result.content);
|
|
706
|
+
console.log("✅ File reading test completed\n");
|
|
707
|
+
} catch (error) {
|
|
708
|
+
console.error("❌ Test 25 failed:", error);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Test 26: File reading with custom encoding
|
|
712
|
+
console.log("Test 26: File reading with custom encoding");
|
|
713
|
+
try {
|
|
714
|
+
// Create a file with special characters
|
|
715
|
+
const specialContent = "Hello 世界! 🌍 Test with emojis and unicode";
|
|
716
|
+
await quickWriteFile("special-chars.txt", specialContent, "utf-8");
|
|
717
|
+
console.log("✅ Special characters file created");
|
|
718
|
+
|
|
719
|
+
// Read with explicit UTF-8 encoding
|
|
720
|
+
const result = await quickReadFile("special-chars.txt", "utf-8");
|
|
721
|
+
console.log(
|
|
722
|
+
"✅ Special characters file read successfully:",
|
|
723
|
+
result.success
|
|
724
|
+
);
|
|
725
|
+
console.log(
|
|
726
|
+
"✅ Special characters verified:",
|
|
727
|
+
result.content === specialContent
|
|
728
|
+
);
|
|
729
|
+
console.log(" Content:", result.content);
|
|
730
|
+
console.log("✅ Custom encoding test completed\n");
|
|
731
|
+
} catch (error) {
|
|
732
|
+
console.error("❌ Test 26 failed:", error);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Test 27: File reading in nested directories
|
|
736
|
+
console.log("Test 27: File reading in nested directories");
|
|
737
|
+
try {
|
|
738
|
+
// Create a file in a nested directory
|
|
739
|
+
const nestedContent = "This file is in a nested directory for reading";
|
|
740
|
+
await quickWriteFile("nested/read/test-nested-read.txt", nestedContent);
|
|
741
|
+
console.log("✅ Nested file created for reading");
|
|
742
|
+
|
|
743
|
+
// Read the nested file
|
|
744
|
+
const result = await quickReadFile("nested/read/test-nested-read.txt");
|
|
745
|
+
console.log("✅ Nested file read successfully:", result.success);
|
|
746
|
+
console.log(
|
|
747
|
+
"✅ Nested file content verified:",
|
|
748
|
+
result.content === nestedContent
|
|
749
|
+
);
|
|
750
|
+
console.log(" Content:", result.content);
|
|
751
|
+
console.log("✅ Nested directory reading test completed\n");
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error("❌ Test 27 failed:", error);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Test 28: Streaming file reading
|
|
757
|
+
console.log("Test 28: Streaming file reading");
|
|
758
|
+
try {
|
|
759
|
+
const client = createClient();
|
|
760
|
+
await client.createSession();
|
|
761
|
+
|
|
762
|
+
// Create a file to read via streaming
|
|
763
|
+
const streamReadContent =
|
|
764
|
+
"This file will be read via streaming\nLine 2\nLine 3";
|
|
765
|
+
await client.writeFile("stream-read-file.txt", streamReadContent);
|
|
766
|
+
console.log("✅ Test file created for streaming reading");
|
|
767
|
+
|
|
768
|
+
let readContent = "";
|
|
769
|
+
let readCompleted = false;
|
|
770
|
+
|
|
771
|
+
// Set up event handlers for streaming
|
|
772
|
+
client.setOnStreamEvent((event) => {
|
|
773
|
+
if (event.type === "command_complete" && event.content) {
|
|
774
|
+
readContent = event.content;
|
|
775
|
+
readCompleted = true;
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
console.log(" Starting streaming file reading...");
|
|
780
|
+
await client.readFileStream("stream-read-file.txt");
|
|
781
|
+
console.log("✅ Streaming file reading completed");
|
|
782
|
+
|
|
783
|
+
// Verify the content was read correctly
|
|
784
|
+
console.log(
|
|
785
|
+
"✅ Streaming read content verified:",
|
|
786
|
+
readContent === streamReadContent
|
|
787
|
+
);
|
|
788
|
+
console.log(" Content length:", readContent.length, "characters");
|
|
789
|
+
|
|
790
|
+
client.clearSession();
|
|
791
|
+
console.log("✅ Streaming file reading test completed\n");
|
|
792
|
+
} catch (error) {
|
|
793
|
+
console.error("❌ Test 28 failed:", error);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Test 29: Quick streaming file reading
|
|
797
|
+
console.log("Test 29: Quick streaming file reading");
|
|
798
|
+
try {
|
|
799
|
+
// Create a file for quick streaming read
|
|
800
|
+
const quickReadContent = "Quick streaming read test content";
|
|
801
|
+
await quickWriteFile("quick-read-stream.txt", quickReadContent);
|
|
802
|
+
console.log("✅ Test file created for quick streaming reading");
|
|
803
|
+
|
|
804
|
+
let quickReadContentReceived = "";
|
|
805
|
+
let quickReadCompleted = false;
|
|
806
|
+
|
|
807
|
+
console.log(" Starting quick streaming file reading...");
|
|
808
|
+
await quickReadFileStream("quick-read-stream.txt", "utf-8", {
|
|
809
|
+
onStreamEvent: (event) => {
|
|
810
|
+
if (event.type === "command_complete" && event.content) {
|
|
811
|
+
quickReadContentReceived = event.content;
|
|
812
|
+
quickReadCompleted = true;
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
console.log("✅ Quick streaming file reading completed");
|
|
817
|
+
|
|
818
|
+
// Verify the content
|
|
819
|
+
console.log(
|
|
820
|
+
"✅ Quick streaming read verified:",
|
|
821
|
+
quickReadContentReceived === quickReadContent
|
|
822
|
+
);
|
|
823
|
+
console.log(" Content:", quickReadContentReceived);
|
|
824
|
+
console.log("✅ Quick streaming file reading test completed\n");
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.error("❌ Test 29 failed:", error);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Test 30: File reading with session management
|
|
830
|
+
console.log("Test 30: File reading with session management");
|
|
831
|
+
try {
|
|
832
|
+
const client = createClient();
|
|
833
|
+
const sessionId = await client.createSession();
|
|
834
|
+
|
|
835
|
+
// Create a file in session
|
|
836
|
+
const sessionReadContent =
|
|
837
|
+
"This file was created and read with session management";
|
|
838
|
+
await client.writeFile(
|
|
839
|
+
"session-read-file.txt",
|
|
840
|
+
sessionReadContent,
|
|
841
|
+
"utf-8",
|
|
842
|
+
sessionId
|
|
843
|
+
);
|
|
844
|
+
console.log("✅ Session file created for reading");
|
|
845
|
+
|
|
846
|
+
// Read the file in the same session
|
|
847
|
+
const result = await client.readFile(
|
|
848
|
+
"session-read-file.txt",
|
|
849
|
+
"utf-8",
|
|
850
|
+
sessionId
|
|
851
|
+
);
|
|
852
|
+
console.log("✅ Session file read successfully:", result.success);
|
|
853
|
+
console.log(
|
|
854
|
+
"✅ Session file content verified:",
|
|
855
|
+
result.content === sessionReadContent
|
|
856
|
+
);
|
|
857
|
+
console.log(" Session ID:", sessionId);
|
|
858
|
+
console.log(" Content:", result.content);
|
|
859
|
+
|
|
860
|
+
client.clearSession();
|
|
861
|
+
console.log("✅ Session file reading test completed\n");
|
|
862
|
+
} catch (error) {
|
|
863
|
+
console.error("❌ Test 30 failed:", error);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Test 31: Error handling for file reading
|
|
867
|
+
console.log("Test 31: Error handling for file reading");
|
|
868
|
+
try {
|
|
869
|
+
// Try to read a non-existent file
|
|
870
|
+
await quickReadFile("non-existent-read-file.txt");
|
|
871
|
+
console.log("❌ Should have failed for non-existent file");
|
|
872
|
+
} catch (error) {
|
|
873
|
+
console.log("✅ Error handling works for non-existent files");
|
|
874
|
+
console.log(
|
|
875
|
+
" Error:",
|
|
876
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
// Try to read from a dangerous path
|
|
882
|
+
await quickReadFile("/etc/passwd");
|
|
883
|
+
console.log("❌ Should have failed for dangerous path");
|
|
884
|
+
} catch (error) {
|
|
885
|
+
console.log("✅ Error handling works for dangerous paths");
|
|
886
|
+
console.log(
|
|
887
|
+
" Error:",
|
|
888
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
try {
|
|
893
|
+
// Try to read with empty path
|
|
894
|
+
await quickReadFile("");
|
|
895
|
+
console.log("❌ Should have failed for empty path");
|
|
896
|
+
} catch (error) {
|
|
897
|
+
console.log("✅ Error handling works for empty paths");
|
|
898
|
+
console.log(
|
|
899
|
+
" Error:",
|
|
900
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
901
|
+
"\n"
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
686
905
|
console.log("🎉 All tests completed!");
|
|
687
906
|
}
|
|
688
907
|
|