import { chmod, mkdir, readFile, stat as statFile, writeFile } from "fs/promises" import { createWriteStream, existsSync, statSync } from "fs" import { realpathSync } from "fs" import { dirname, join, relative, resolve as pathResolve, win32 } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" import { Glob } from "@opencode-ai/core/util/glob" // Fast sync version for metadata checks export async function exists(p: string): Promise { return existsSync(p) } export async function isDir(p: string): Promise { try { return statSync(p).isDirectory() } catch { return false } } export function stat(p: string): ReturnType | undefined { return statSync(p, { throwIfNoEntry: false }) ?? undefined } export async function statAsync(p: string): Promise | undefined> { return statFile(p).catch((e) => { if (isEnoent(e)) return undefined throw e }) } export async function size(p: string): Promise { const s = stat(p)?.size ?? 0 return typeof s === "bigint" ? Number(s) : s } export async function readText(p: string): Promise { return readFile(p, "utf-8") } export async function readJson(p: string): Promise { return JSON.parse(await readFile(p, "utf-8")) } export async function readBytes(p: string): Promise { return readFile(p) } export async function readArrayBuffer(p: string): Promise { const buf = await readFile(p) return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer } function isEnoent(e: unknown): e is { code: "ENOENT" } { return typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "ENOENT" } export async function write(p: string, content: string | Buffer | Uint8Array, mode?: number): Promise { try { if (mode) { await writeFile(p, content, { mode }) } else { await writeFile(p, content) } } catch (e) { if (isEnoent(e)) { await mkdir(dirname(p), { recursive: true }) if (mode) { await writeFile(p, content, { mode }) } else { await writeFile(p, content) } return } throw e } } export async function writeJson(p: string, data: unknown, mode?: number): Promise { return write(p, JSON.stringify(data, null, 2), mode) } export async function writeStream( p: string, stream: ReadableStream | Readable, mode?: number, ): Promise { const dir = dirname(p) if (!existsSync(dir)) { await mkdir(dir, { recursive: true }) } const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream const writeStream = createWriteStream(p) await pipeline(nodeStream, writeStream) if (mode) { await chmod(p, mode) } } export async function mimeType(p: string): Promise { const { lookup } = await import("mime-types") return lookup(p) || "application/octet-stream" } /** * On Windows, normalize a path to its canonical casing using the filesystem. * This is needed because Windows paths are case-insensitive but LSP servers * may return paths with different casing than what we send them. */ export function normalizePath(p: string): string { if (process.platform !== "win32") return p const resolved = win32.normalize(win32.resolve(windowsPath(p))) try { return realpathSync.native(resolved) } catch { return resolved } } export function normalizePathPattern(p: string): string { if (process.platform !== "win32") return p if (p === "*") return p const match = p.match(/^(.*)[\\/]\*$/) if (!match) return normalizePath(p) const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1] return join(normalizePath(dir), "*") } // We cannot rely on path.resolve() here because git.exe may come from Git Bash, Cygwin, or MSYS2, so we need to translate these paths at the boundary. // Also resolves symlinks so that callers using the result as a cache key // always get the same canonical path for a given physical directory. export function resolve(p: string): string { const resolved = pathResolve(windowsPath(p)) try { return normalizePath(realpathSync(resolved)) } catch (e) { if (isEnoent(e)) return normalizePath(resolved) throw e } } export function windowsPath(p: string): string { if (process.platform !== "win32") return p return ( p .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`) // Git Bash for Windows paths are typically //... .replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) // Cygwin git paths are typically /cygdrive//... .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) // WSL paths are typically /mnt//... .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) ) } export function overlaps(a: string, b: string) { const relA = relative(a, b) const relB = relative(b, a) return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..") } export function contains(parent: string, child: string) { return !relative(parent, child).startsWith("..") } export async function findUp( target: string, start: string, stop?: string, options?: { rootFirst?: boolean }, ): Promise export async function findUp( target: string[], start: string, stop?: string, options?: { rootFirst?: boolean }, ): Promise export async function findUp( target: string | string[], start: string, stop?: string, options?: { rootFirst?: boolean }, ) { const dirs = [start] let current = start while (true) { if (stop === current) break const parent = dirname(current) if (parent === current) break dirs.push(parent) current = parent } const targets = Array.isArray(target) ? target : [target] const result = [] for (const dir of options?.rootFirst ? dirs.toReversed() : dirs) { for (const item of targets) { const search = join(dir, item) if (await exists(search)) result.push(search) } } return result } export async function* up(options: { targets: string[]; start: string; stop?: string }) { const { targets, start, stop } = options let current = start while (true) { for (const target of targets) { const search = join(current, target) if (await exists(search)) yield search } if (stop === current) break const parent = dirname(current) if (parent === current) break current = parent } } export async function globUp(pattern: string, start: string, stop?: string) { let current = start const result = [] while (true) { try { const matches = await Glob.scan(pattern, { cwd: current, absolute: true, include: "file", dot: true, }) result.push(...matches) } catch { // Skip invalid glob patterns } if (stop === current) break const parent = dirname(current) if (parent === current) break current = parent } return result } export * as Filesystem from "./filesystem"