content.ts

 1import { mkdir, writeFile } from "node:fs/promises";
 2import { dirname, join, resolve, sep } from "node:path";
 3import { ToolInputError } from "../util/errors.js";
 4
 5export interface WorkspaceContent {
 6  filePath: string;
 7  content: string;
 8  bytes: number;
 9}
10
11function ensureContained(workspacePath: string, targetPath: string): void {
12  const resolved = resolve(workspacePath, targetPath);
13  const root = workspacePath.endsWith(sep) ? workspacePath : `${workspacePath}${sep}`;
14
15  if (resolved !== workspacePath && !resolved.startsWith(root)) {
16    throw new ToolInputError(`Path escapes workspace: ${targetPath}`);
17  }
18}
19
20export async function writeWorkspaceFile(
21  workspacePath: string,
22  relativePath: string,
23  content: string,
24): Promise<WorkspaceContent> {
25  const filePath = join(workspacePath, relativePath);
26  ensureContained(workspacePath, filePath);
27  await mkdir(dirname(filePath), { recursive: true });
28  await writeFile(filePath, content, "utf8");
29
30  return {
31    filePath,
32    content,
33    bytes: Buffer.byteLength(content, "utf8"),
34  };
35}