2026-03-30 11:51:07 +00:00
import { chmod , mkdir , readFile , stat as statFile , writeFile } from "fs/promises"
2026-02-19 16:32:32 +00:00
import { createWriteStream , existsSync , statSync } from "fs"
2025-12-16 00:01:03 +00:00
import { realpathSync } from "fs"
2026-03-30 03:10:01 +00:00
import { dirname , join , relative , resolve as pathResolve , win32 } from "path"
2026-02-19 16:32:32 +00:00
import { Readable } from "stream"
import { pipeline } from "stream/promises"
2026-04-25 14:59:17 +00:00
import { Glob } from "@opencode-ai/core/util/glob"
2025-06-01 18:40:44 +00:00
2026-04-16 03:15:58 +00:00
// Fast sync version for metadata checks
export async function exists ( p : string ) : Promise < boolean > {
return existsSync ( p )
}
2026-02-18 15:30:52 +00:00
2026-04-16 03:15:58 +00:00
export async function isDir ( p : string ) : Promise < boolean > {
try {
return statSync ( p ) . isDirectory ( )
} catch {
return false
2026-02-18 15:30:52 +00:00
}
2026-04-16 03:15:58 +00:00
}
2026-02-18 15:30:52 +00:00
2026-04-16 03:15:58 +00:00
export function stat ( p : string ) : ReturnType < typeof statSync > | undefined {
return statSync ( p , { throwIfNoEntry : false } ) ? ? undefined
}
2026-02-18 21:20:58 +00:00
2026-04-16 03:15:58 +00:00
export async function statAsync ( p : string ) : Promise < ReturnType < typeof statSync > | undefined > {
return statFile ( p ) . catch ( ( e ) = > {
if ( isEnoent ( e ) ) return undefined
throw e
} )
}
2026-03-30 11:51:07 +00:00
2026-04-16 03:15:58 +00:00
export async function size ( p : string ) : Promise < number > {
const s = stat ( p ) ? . size ? ? 0
return typeof s === "bigint" ? Number ( s ) : s
}
2026-02-18 15:30:52 +00:00
2026-04-16 03:15:58 +00:00
export async function readText ( p : string ) : Promise < string > {
return readFile ( p , "utf-8" )
}
2026-02-18 15:30:52 +00:00
2026-04-16 23:52:04 +00:00
export async function readJson < T = unknown > ( p : string ) : Promise < T > {
2026-04-16 03:15:58 +00:00
return JSON . parse ( await readFile ( p , "utf-8" ) )
}
2026-02-18 15:30:52 +00:00
2026-04-16 03:15:58 +00:00
export async function readBytes ( p : string ) : Promise < Buffer > {
return readFile ( p )
}
2026-02-18 15:30:52 +00:00
2026-04-16 03:15:58 +00:00
export async function readArrayBuffer ( p : string ) : Promise < ArrayBuffer > {
const buf = await readFile ( p )
return buf . buffer . slice ( buf . byteOffset , buf . byteOffset + buf . byteLength ) as ArrayBuffer
}
2026-02-19 16:32:32 +00:00
2026-04-16 03:15:58 +00:00
function isEnoent ( e : unknown ) : e is { code : "ENOENT" } {
return typeof e === "object" && e !== null && "code" in e && ( e as { code : string } ) . code === "ENOENT"
}
2026-02-18 15:30:52 +00:00
2026-04-16 03:15:58 +00:00
export async function write ( p : string , content : string | Buffer | Uint8Array , mode? : number ) : Promise < void > {
try {
if ( mode ) {
await writeFile ( p , content , { mode } )
} else {
await writeFile ( p , content )
}
} catch ( e ) {
if ( isEnoent ( e ) ) {
await mkdir ( dirname ( p ) , { recursive : true } )
2026-02-18 15:30:52 +00:00
if ( mode ) {
await writeFile ( p , content , { mode } )
} else {
await writeFile ( p , content )
}
2026-04-16 03:15:58 +00:00
return
2026-02-18 15:30:52 +00:00
}
2026-04-16 03:15:58 +00:00
throw e
2026-02-18 15:30:52 +00:00
}
2026-04-16 03:15:58 +00:00
}
2026-02-18 15:30:52 +00:00
2026-04-16 03:15:58 +00:00
export async function writeJson ( p : string , data : unknown , mode? : number ) : Promise < void > {
return write ( p , JSON . stringify ( data , null , 2 ) , mode )
}
2026-02-19 16:32:32 +00:00
2026-04-16 03:15:58 +00:00
export async function writeStream (
p : string ,
stream : ReadableStream < Uint8Array > | Readable ,
mode? : number ,
) : Promise < void > {
const dir = dirname ( p )
if ( ! existsSync ( dir ) ) {
await mkdir ( dir , { recursive : true } )
2026-02-19 16:32:32 +00:00
}
2026-04-16 03:15:58 +00:00
const nodeStream = stream instanceof ReadableStream ? Readable . fromWeb ( stream as any ) : stream
const writeStream = createWriteStream ( p )
await pipeline ( nodeStream , writeStream )
2026-02-18 15:30:52 +00:00
2026-04-16 03:15:58 +00:00
if ( mode ) {
await chmod ( p , mode )
2025-12-16 00:01:03 +00:00
}
2026-04-16 03:15:58 +00:00
}
2026-02-18 15:30:52 +00:00
2026-04-16 06:03:03 +00:00
export async function mimeType ( p : string ) : Promise < string > {
const { lookup } = await import ( "mime-types" )
2026-04-16 03:15:58 +00:00
return lookup ( p ) || "application/octet-stream"
}
2026-03-30 03:10:01 +00:00
2026-04-16 03:15:58 +00:00
/ * *
* 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
2026-03-07 05:42:14 +00:00
}
2026-04-16 03:15:58 +00:00
}
2026-03-07 05:42:14 +00:00
2026-04-16 03:15:58 +00:00
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 ) , "*" )
}
2025-07-08 22:14:24 +00:00
2026-04-16 03:15:58 +00:00
// 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
2025-07-08 22:14:24 +00:00
}
2026-04-16 03:15:58 +00:00
}
2025-07-08 22:14:24 +00:00
2026-04-16 03:15:58 +00:00
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 /<drive>/...
. replace ( /^\/([a-zA-Z])(?:\/|$)/ , ( _ , drive ) = > ` ${ drive . toUpperCase ( ) } :/ ` )
// Cygwin git paths are typically /cygdrive/<drive>/...
. replace ( /^\/cygdrive\/([a-zA-Z])(?:\/|$)/ , ( _ , drive ) = > ` ${ drive . toUpperCase ( ) } :/ ` )
// WSL paths are typically /mnt/<drive>/...
. 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 ( ".." )
}
2026-04-01 23:50:22 +00:00
2026-04-16 03:15:58 +00:00
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 < string [ ] >
export async function findUp (
target : string [ ] ,
start : string ,
stop? : string ,
options ? : { rootFirst? : boolean } ,
) : Promise < string [ ] >
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 )
2026-04-01 23:50:22 +00:00
}
2025-06-01 18:40:44 +00:00
}
2026-04-16 03:15:58 +00:00
return result
}
2025-07-03 03:27:04 +00:00
2026-04-16 03:15:58 +00:00
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
2025-07-10 13:37:28 +00:00
}
2026-04-16 03:15:58 +00:00
if ( stop === current ) break
const parent = dirname ( current )
if ( parent === current ) break
current = parent
2025-07-10 13:37:28 +00:00
}
2026-04-16 03:15:58 +00:00
}
2025-07-10 13:37:28 +00:00
2026-04-16 03:15:58 +00:00
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
2025-07-03 03:27:04 +00:00
}
2026-04-16 03:15:58 +00:00
if ( stop === current ) break
const parent = dirname ( current )
if ( parent === current ) break
current = parent
2025-07-03 03:27:04 +00:00
}
2026-04-16 03:15:58 +00:00
return result
2025-06-01 18:40:44 +00:00
}
2026-04-27 18:33:33 +00:00
export * as Filesystem from "./filesystem"