Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(harness): remote provider fs tweaks
  • Loading branch information
sanchitrk committed Jun 23, 2026
commit a35eb43ed370cdb1bf35347ed4734f7116ef9e37
12 changes: 9 additions & 3 deletions packages/harness/src/sandbox/filesystem/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import { SandboxFileSystem } from "./filesystem";
// it into the same runtime surface the local vfs backend exposes.
export type FileStat = SandboxFileSystem.FileStat;

// extend Interface; for extending
// allowing for future expansion
export interface Interface extends SandboxFileSystem.Interface {}
// Remote-aware callers can opt into metadata that is not part of the generic
// SandboxFileSystem surface. In particular, `stat` keeps the familiar
// provider-level meaning, while `lstat` explicitly asks for the directory entry
// itself so symlink identity is not inferred from a method with mixed provider
// semantics.
export interface Interface extends SandboxFileSystem.Interface {
readonly lstat?: (path: string) => Promise<FileStat>;
}

export interface Options {
/**
Expand Down Expand Up @@ -46,6 +51,7 @@ export const make = (provider: Interface, options?: Options): Interface => {
}
},
stat: (path) => provider.stat(resolve(path)),
...(provider.lstat === undefined ? {} : { lstat: (path: string) => provider.lstat!(resolve(path)) }),
readdir: (path) => provider.readdir(resolve(path)),
exists: (path) => provider.exists(resolve(path)),
mkdir: (path, mkdirOptions) => provider.mkdir(resolve(path), mkdirOptions),
Expand Down
6 changes: 5 additions & 1 deletion packages/harness/src/sandbox/providers/daytona.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const statsFrom = (info: FileInfo): RemoteFileSystem.FileStat => {

type RemoteFilesystemProvider = Pick<
RemoteFileSystem.Interface,
"readFile" | "readFileBuffer" | "writeFile" | "stat" | "readdir" | "exists" | "mkdir" | "rm"
"readFile" | "readFileBuffer" | "writeFile" | "stat" | "lstat" | "readdir" | "exists" | "mkdir" | "rm"
>;

const providerFrom = (sandbox: RemoteSandbox, options: Options) => {
Expand All @@ -130,6 +130,10 @@ const providerFrom = (sandbox: RemoteSandbox, options: Options) => {
writeFile: (path: string, content: string | Uint8Array) =>
sandbox.fs.uploadFile(typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content), path),
stat: async (path: string) => statsFrom(await sandbox.fs.getFileDetails(path)),
// Daytona's toolbox exposes a single file-details endpoint that reports
// the entry mode when available. Expose it as remote `lstat` too so
// symlink-aware callers can ask for that intent explicitly.
lstat: async (path: string) => statsFrom(await sandbox.fs.getFileDetails(path)),
readdir: async (path: string) => (await sandbox.fs.listFiles(path)).map((entry) => entry.name),
exists: async (path: string) => {
try {
Expand Down
6 changes: 5 additions & 1 deletion packages/harness/src/sandbox/providers/vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const statsFrom = (stats: Stats): RemoteFileSystem.FileStat => {

type RemoteFilesystemProvider = Pick<
RemoteFileSystem.Interface,
"readFile" | "readFileBuffer" | "writeFile" | "stat" | "readdir" | "exists" | "mkdir" | "rm"
"readFile" | "readFileBuffer" | "writeFile" | "stat" | "lstat" | "readdir" | "exists" | "mkdir" | "rm"
>;

// The Vercel `fs` surface is `node:fs/promises`-compatible, so the provider
Expand All @@ -139,6 +139,10 @@ const providerFrom = (sandbox: RemoteSandbox): RemoteFilesystemProvider => {
writeFile: (path: string, content: string | Uint8Array) =>
sandbox.fs.writeFile(path, typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content)),
stat: async (path: string) => statsFrom(await sandbox.fs.stat(path)),
// Keep symlink identity explicit: Vercel's `stat` follows symlinks
// (`stat -L`), so remote-aware callers must ask for `lstat` when they
// need the entry itself instead of the target's metadata.
lstat: async (path: string) => statsFrom(await sandbox.fs.lstat(path)),
// `withFileTypes` lists via `find`, which includes dotfiles (`.git` …);
// the bare `readdir` shells out to `ls -1` and would drop them.
readdir: async (path: string) => (await sandbox.fs.readdir(path, { withFileTypes: true })).map((e) => e.name),
Expand Down
24 changes: 24 additions & 0 deletions packages/harness/test/filesystem.remote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const enoent = (path: string) => {
const makeProvider = () => {
const files = new Map<string, Buffer>();
const directories = new Set(["/"]);
const symlinks = new Set<string>();

const normalize = (path: string) => posix.normalize(path);
const mkdirp = (path: string) => {
Expand All @@ -25,11 +26,18 @@ const makeProvider = () => {
const stat = async (path: string): Promise<RemoteFileSystem.FileStat> => {
const>
if (directories.has(target)) return { isFile: false, isDirectory: true };
if (symlinks.has(target)) return { isFile: true, isDirectory: false, isSymbolicLink: false };
const content = files.get(target);
if (content !== undefined) return { size: content.byteLength, isFile: true, isDirectory: false };
throw enoent(target);
};

const lstat = async (path: string): Promise<RemoteFileSystem.FileStat> => {
const>
if (symlinks.has(target)) return { isFile: false, isDirectory: false, isSymbolicLink: true };
return stat(target);
};

const readdir = async (path: string) => {
const>
if (!directories.has(target)) throw enoent(target);
Expand Down Expand Up @@ -63,6 +71,7 @@ const makeProvider = () => {
return {
files,
directories,
symlinks,
provider: {
readFile: async (path: string) =>
Buffer.from(await stat(path).then(() => files.get(normalize(path))!)).toString("utf8"),
Expand All @@ -74,6 +83,7 @@ const makeProvider = () => {
files.set(target, typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content));
},
stat,
lstat,
readdir,
exists: async (path: string) => {
try {
Expand Down Expand Up @@ -114,6 +124,20 @@ describe("RemoteFileSystem", () => {
expect(remote.files.has("/workspace/src/index.ts")).toBe(true);
});

it("resolves explicit remote lstat paths against cwd when the provider supports it", async () => {
const remote = makeProvider();
remote.directories.add("/workspace");
remote.symlinks.add("/workspace/link.txt");
const filesystem = RemoteFileSystem.make(remote.provider, { cwd: "/workspace" });

expect(filesystem.lstat).toBeDefined();
const stat = await filesystem.stat("link.txt");
const lstat = await filesystem.lstat!("link.txt");

expect(stat.isSymbolicLink).toBe(false);
expect(lstat.isSymbolicLink).toBe(true);
});

it("passes relative paths through when no cwd is configured", async () => {
const remote = makeProvider();
const filesystem = RemoteFileSystem.make(remote.provider);
Expand Down
14 changes: 14 additions & 0 deletions packages/harness/test/sandbox.vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Stats } from "node:fs";
import { Effect } from "effect";
import { describe, expect, it } from "vite-plus/test";
import { SandboxFileSystem } from "../src/sandbox/filesystem/filesystem";
import { RemoteFileSystem } from "../src/sandbox/filesystem/remote";
import { EnvVercel, statsFrom } from "../src/sandbox/providers/vercel";
import { Shell } from "../src/sandbox/shell";
import "./utils/env";
Expand Down Expand Up @@ -69,6 +70,12 @@ suite("Sandbox.EnvVercel", () => {
yield* Effect.promise(() => filesystem.writeFile(`${dir}/workspace/project/package.json`, "{}"));
const workspace = yield* Effect.promise(() => filesystem.readdir(`${dir}/workspace`));
const project = yield* Effect.promise(() => filesystem.readdir(`${dir}/workspace/project`));
yield* Effect.promise(() => filesystem.writeFile(`${dir}/target.txt`, "target"));
const linked = yield* shell.exec(`ln -s "$(pwd)/${dir}/target.txt" ${dir}/link.txt`);
const linkStat = yield* Effect.promise(() => filesystem.stat(`${dir}/link.txt`));
const linkLstat = yield* Effect.promise(() =>
(filesystem as RemoteFileSystem.Interface).lstat!(`${dir}/link.txt`),
);

return {
cat,
Expand All @@ -81,6 +88,9 @@ suite("Sandbox.EnvVercel", () => {
missing: yield* Effect.promise(() => filesystem.exists(`${dir}/nope.txt`)),
workspace,
project,
linked,
linkStat,
linkLstat,
};
}).pipe(Effect.provide(EnvVercel.services({ persist: false }))),
);
Expand All @@ -99,6 +109,10 @@ suite("Sandbox.EnvVercel", () => {
expect(result.workspace).toContain("package.json");
expect(result.workspace).toContain("project");
expect(result.project).toContain("package.json");
expect(result.linked.exitCode).toBe(0);
expect(result.linkStat.isFile).toBe(true);
expect(result.linkStat.isSymbolicLink).toBe(false);
expect(result.linkLstat.isSymbolicLink).toBe(true);
},
PROVISION_TIMEOUT,
);
Expand Down
2 changes: 2 additions & 0 deletions packages/harness/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const ignoredPaths = [
".zed/**",
".idea/**",
".vscode/**",
".vercel/**",
"**/.vercel/**",
];
const aliases = {
"@codeworksh/utils": fileURLToPath(new URL("../utils/src/index.ts", import.meta.url)),
Expand Down
2 changes: 2 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const ignoredPaths = [
"**/node_modules/**",
".pnpm-store/**",
".zed/**",
".vercel/**",
"**/.vercel/**",
"packages/webui/src/routeTree.gen.ts",
];
const aliases = {
Expand Down