Skip to content

Commit f4e9cf3

Browse files
authored
Merge pull request #227 from codesandbox/CSB-669-integrate-pint-fs-operations-in-sdk
chore: integrate pint fs operations in the new client
2 parents 2a38dd2 + 578ca28 commit f4e9cf3

File tree

3 files changed

+714
-3
lines changed

3 files changed

+714
-3
lines changed

src/PintClient/fs.ts

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
import { Client, createClient, createConfig } from "../api-clients/pint/client";
2+
import {
3+
IAgentClientFS,
4+
PickRawFsResult,
5+
} from "../agent-client-interface";
6+
import {
7+
createFile,
8+
readFile,
9+
performFileAction,
10+
listDirectory,
11+
createDirectory,
12+
deleteDirectory,
13+
getFileStat,
14+
} from "../api-clients/pint";
15+
export class PintFsClient implements IAgentClientFS {
16+
constructor(private apiClient: Client) {}
17+
18+
async readFile(path: string): Promise<PickRawFsResult<"fs/readFile">> {
19+
try {
20+
const response = await readFile({
21+
client: this.apiClient,
22+
path: {
23+
path: path,
24+
},
25+
});
26+
27+
if (response.data) {
28+
// Convert string content to Uint8Array to match FSReadFileResult type
29+
const encoder = new TextEncoder();
30+
const content = encoder.encode(response.data.content);
31+
32+
return {
33+
type: "ok",
34+
result: {
35+
content: content,
36+
},
37+
};
38+
} else {
39+
return {
40+
type: "error",
41+
error: response.error?.message || "Failed to read file",
42+
errno: null,
43+
};
44+
}
45+
} catch (error) {
46+
return {
47+
type: "error",
48+
error: error instanceof Error ? error.message : "Unknown error",
49+
errno: null,
50+
};
51+
}
52+
}
53+
54+
async readdir(path: string): Promise<PickRawFsResult<"fs/readdir">> {
55+
try {
56+
const response = await listDirectory({
57+
client: this.apiClient,
58+
path: {
59+
path: path,
60+
},
61+
});
62+
63+
if (response.data) {
64+
const entries = response.data.files.map((fileInfo) => ({
65+
name: fileInfo.name,
66+
type: fileInfo.isDir ? (1 as const) : (0 as const), // 1 = directory, 0 = file
67+
isSymlink: false, // API doesn't provide symlink info, defaulting to false
68+
}));
69+
70+
return {
71+
type: "ok",
72+
result: {
73+
entries: entries,
74+
},
75+
};
76+
} else {
77+
return {
78+
type: "error",
79+
error: response.error?.message || "Failed to read directory",
80+
errno: null,
81+
};
82+
}
83+
} catch (error) {
84+
return {
85+
type: "error",
86+
error: error instanceof Error ? error.message : "Unknown error",
87+
errno: null,
88+
};
89+
}
90+
}
91+
92+
async writeFile(
93+
path: string,
94+
content: Uint8Array,
95+
create?: boolean,
96+
overwrite?: boolean
97+
): Promise<PickRawFsResult<"fs/writeFile">> {
98+
try {
99+
// Convert Uint8Array content to string for the API
100+
const decoder = new TextDecoder();
101+
const contentString = decoder.decode(content);
102+
103+
const response = await createFile({
104+
client: this.apiClient,
105+
path: {
106+
path: path,
107+
},
108+
body: {
109+
content: contentString,
110+
},
111+
});
112+
113+
if (response.data) {
114+
// FSWriteFileResult is an empty object (Record<string, never>)
115+
return {
116+
type: "ok",
117+
result: {},
118+
};
119+
} else {
120+
return {
121+
type: "error",
122+
error: response.error?.message || "Failed to write file",
123+
errno: null,
124+
};
125+
}
126+
} catch (error) {
127+
return {
128+
type: "error",
129+
error: error instanceof Error ? error.message : "Unknown error",
130+
errno: null,
131+
};
132+
}
133+
}
134+
135+
async remove(
136+
path: string,
137+
recursive?: boolean
138+
): Promise<PickRawFsResult<"fs/remove">> {
139+
try {
140+
const response = await deleteDirectory({
141+
client: this.apiClient,
142+
path: {
143+
path: path,
144+
},
145+
});
146+
147+
if (response.data) {
148+
// FSRemoveResult is an empty object (Record<string, never>)
149+
return {
150+
type: "ok",
151+
result: {},
152+
};
153+
} else {
154+
return {
155+
type: "error",
156+
error: response.error?.message || "Failed to remove directory",
157+
errno: null,
158+
};
159+
}
160+
} catch (error) {
161+
return {
162+
type: "error",
163+
error: error instanceof Error ? error.message : "Unknown error",
164+
errno: null,
165+
};
166+
}
167+
}
168+
169+
async mkdir(
170+
path: string,
171+
recursive?: boolean
172+
): Promise<PickRawFsResult<"fs/mkdir">> {
173+
try {
174+
const response = await createDirectory({
175+
client: this.apiClient,
176+
path: {
177+
path: path,
178+
},
179+
});
180+
181+
if (response.data) {
182+
// FSMkdirResult is an empty object (Record<string, never>)
183+
return {
184+
type: "ok",
185+
result: {},
186+
};
187+
} else {
188+
return {
189+
type: "error",
190+
error: response.error?.message || "Failed to create directory",
191+
errno: null,
192+
};
193+
}
194+
} catch (error) {
195+
return {
196+
type: "error",
197+
error: error instanceof Error ? error.message : "Unknown error",
198+
errno: null,
199+
};
200+
}
201+
}
202+
203+
async stat(path: string): Promise<PickRawFsResult<"fs/stat">> {
204+
try {
205+
const response = await getFileStat({
206+
client: this.apiClient,
207+
path: {
208+
path: path,
209+
},
210+
});
211+
212+
if (response.data) {
213+
// Parse modTime string to timestamp (assuming ISO string format)
214+
const modTimeMs = new Date(response.data.modTime).getTime();
215+
216+
return {
217+
type: "ok",
218+
result: {
219+
type: response.data.isDir ? 1 : 0, // 1 = directory, 0 = file
220+
isSymlink: false, // API doesn't provide symlink info, defaulting to false
221+
size: response.data.size,
222+
mtime: modTimeMs,
223+
ctime: modTimeMs, // Using modTime as fallback since API doesn't provide ctime
224+
atime: modTimeMs, // Using modTime as fallback since API doesn't provide atime
225+
},
226+
};
227+
} else {
228+
return {
229+
type: "error",
230+
error: response.error?.message || "Failed to get file stat",
231+
errno: null,
232+
};
233+
}
234+
} catch (error) {
235+
return {
236+
type: "error",
237+
error: error instanceof Error ? error.message : "Unknown error",
238+
errno: null,
239+
};
240+
}
241+
}
242+
243+
async copy(
244+
from: string,
245+
to: string,
246+
recursive?: boolean,
247+
overwrite?: boolean
248+
): Promise<PickRawFsResult<"fs/copy">> {
249+
try {
250+
const response = await performFileAction({
251+
client: this.apiClient,
252+
path: {
253+
path: from,
254+
},
255+
body: {
256+
action: 'copy',
257+
destination: to,
258+
},
259+
});
260+
261+
if (response.data) {
262+
// FSCopyResult is an empty object (Record<string, never>)
263+
return {
264+
type: "ok",
265+
result: {},
266+
};
267+
} else {
268+
return {
269+
type: "error",
270+
error: response.error?.message || "Failed to copy file",
271+
errno: null,
272+
};
273+
}
274+
} catch (error) {
275+
return {
276+
type: "error",
277+
error: error instanceof Error ? error.message : "Unknown error",
278+
errno: null,
279+
};
280+
}
281+
}
282+
283+
async rename(
284+
from: string,
285+
to: string,
286+
overwrite?: boolean
287+
): Promise<PickRawFsResult<"fs/rename">> {
288+
try {
289+
const response = await performFileAction({
290+
client: this.apiClient,
291+
path: {
292+
path: from,
293+
},
294+
body: {
295+
action: 'move',
296+
destination: to,
297+
},
298+
});
299+
300+
if (response.data) {
301+
// FSRenameResult is an empty object (Record<string, never>)
302+
return {
303+
type: "ok",
304+
result: {},
305+
};
306+
} else {
307+
return {
308+
type: "error",
309+
error: response.error?.message || "Failed to rename/move file",
310+
errno: null,
311+
};
312+
}
313+
} catch (error) {
314+
return {
315+
type: "error",
316+
error: error instanceof Error ? error.message : "Unknown error",
317+
errno: null,
318+
};
319+
}
320+
}
321+
322+
async watch(
323+
path: string,
324+
options: {
325+
readonly recursive?: boolean;
326+
readonly excludes?: readonly string[];
327+
},
328+
onEvent: (watchEvent: any) => void
329+
): Promise<
330+
| (PickRawFsResult<"fs/watch"> & { type: "error" })
331+
| { type: "success"; dispose(): void }
332+
> {
333+
throw new Error("Not implemented");
334+
}
335+
336+
async download(path?: string): Promise<{ downloadUrl: string }> {
337+
throw new Error("Not implemented");
338+
}
339+
}

src/PintClient/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Emitter, EmitterSubscription, Event } from "../utils/event";
33
import { SandboxSession } from "../types";
44
import { Disposable } from "../utils/disposable";
55
import { Client, createClient, createConfig } from "../api-clients/pint/client";
6+
import {PintFsClient} from "./fs";
67
import {
78
IAgentClient,
89
IAgentClientPorts,
@@ -20,8 +21,6 @@ import {
2021
streamPortsList,
2122
} from "../api-clients/pint";
2223

23-
24-
2524
function parseStreamEvent<T>(evt: unknown): T {
2625
if (typeof evt !== "string") {
2726
return evt as T;
@@ -117,7 +116,7 @@ export class PintClient implements IAgentClient {
117116

118117
this.ports = new PintPortsClient(apiClient, this.sandboxId);
119118
this.shells = {} as IAgentClientShells; // Not implemented for Pint
120-
this.fs = {} as IAgentClientFS; // Not implemented for Pint
119+
this.fs = new PintFsClient(apiClient);
121120
this.tasks = {} as IAgentClientTasks; // Not implemented for Pint
122121
this.setup = {} as IAgentClientSetup; // Not implemented for Pint
123122
this.system = {} as IAgentClientSystem; // Not implemented for Pint

0 commit comments

Comments
 (0)