Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
24 kB · 770 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771import * as fs from 'fs'import { mkdir as mkdirPromise, open, readdir as readdirPromise, readFile as readFilePromise, rename as renamePromise, rmdir as rmdirPromise, rm as rmPromise, stat as statPromise, unlink as unlinkPromise,} from 'fs/promises'import { homedir } from 'os'import * as nodePath from 'path'import { getErrnoCode } from './errors.js'import { slowLogging } from './slowOperations.js'
/** * Simplified filesystem operations interface based on Node.js fs module. * Provides a subset of commonly used sync operations with type safety. * Allows abstraction for alternative implementations (e.g., mock, virtual). */export type FsOperations = { // File access and information operations /** Gets the current working directory */ cwd(): string /** Checks if a file or directory exists */ existsSync(path: string): boolean /** Gets file stats asynchronously */ stat(path: string): Promise<fs.Stats> /** Lists directory contents with file type information asynchronously */ readdir(path: string): Promise<fs.Dirent[]> /** Deletes file asynchronously */ unlink(path: string): Promise<void> /** Removes an empty directory asynchronously */ rmdir(path: string): Promise<void> /** Removes files and directories asynchronously (with recursive option) */ rm( path: string, options?: { recursive?: boolean; force?: boolean }, ): Promise<void> /** Creates directory recursively asynchronously. */ mkdir(path: string, options?: { mode?: number }): Promise<void> /** Reads file content as string asynchronously */ readFile(path: string, options: { encoding: BufferEncoding }): Promise<string> /** Renames/moves file asynchronously */ rename(oldPath: string, newPath: string): Promise<void> /** Gets file stats */ statSync(path: string): fs.Stats /** Gets file stats without following symlinks */ lstatSync(path: string): fs.Stats
// File content operations /** Reads file content as string with specified encoding */ readFileSync( path: string, options: { encoding: BufferEncoding }, ): string /** Reads raw file bytes as Buffer */ readFileBytesSync(path: string): Buffer /** Reads specified number of bytes from file start */ readSync( path: string, options: { length: number }, ): { buffer: Buffer bytesRead: number } /** Appends string to file */ appendFileSync(path: string, data: string, options?: { mode?: number }): void /** Copies file from source to destination */ copyFileSync(src: string, dest: string): void /** Deletes file */ unlinkSync(path: string): void /** Renames/moves file */ renameSync(oldPath: string, newPath: string): void /** Creates hard link */ linkSync(target: string, path: string): void /** Creates symbolic link */ symlinkSync( target: string, path: string, type?: 'dir' | 'file' | 'junction', ): void /** Reads symbolic link */ readlinkSync(path: string): string /** Resolves symbolic links and returns the canonical pathname */ realpathSync(path: string): string
// Directory operations /** Creates directory recursively. Mode defaults to 0o777 & ~umask if not specified. */ mkdirSync( path: string, options?: { mode?: number }, ): void /** Lists directory contents with file type information */ readdirSync(path: string): fs.Dirent[] /** Lists directory contents as strings */ readdirStringSync(path: string): string[] /** Checks if the directory is empty */ isDirEmptySync(path: string): boolean /** Removes an empty directory */ rmdirSync(path: string): void /** Removes files and directories (with recursive option) */ rmSync( path: string, options?: { recursive?: boolean force?: boolean }, ): void /** Create a writable stream for writing data to a file. */ createWriteStream(path: string): fs.WriteStream /** Reads raw file bytes as Buffer asynchronously. * When maxBytes is set, only reads up to that many bytes. */ readFileBytes(path: string, maxBytes?: number): Promise<Buffer>}
/** * Safely resolves a file path, handling symlinks and errors gracefully. * * Error handling strategy: * - If the file doesn't exist, returns the original path (allows for file creation) * - If symlink resolution fails (broken symlink, permission denied, circular links), * returns the original path and marks it as not a symlink * - This ensures operations can continue with the original path rather than failing * * @param fs The filesystem implementation to use * @param filePath The path to resolve * @returns Object containing the resolved path and whether it was a symlink */export function safeResolvePath( fs: FsOperations, filePath: string,): { resolvedPath: string; isSymlink: boolean; isCanonical: boolean } { // Block UNC paths before any filesystem access to prevent network // requests (DNS/SMB) during validation on Windows if (filePath.startsWith('//') || filePath.startsWith('\\\\')) { return { resolvedPath: filePath, isSymlink: false, isCanonical: false } }
try { // Check for special file types (FIFOs, sockets, devices) before calling realpathSync. // realpathSync can block on FIFOs waiting for a writer, causing hangs. // If the file doesn't exist, lstatSync throws ENOENT which the catch // below handles by returning the original path (allows file creation). const stats = fs.lstatSync(filePath) if ( stats.isFIFO() || stats.isSocket() || stats.isCharacterDevice() || stats.isBlockDevice() ) { return { resolvedPath: filePath, isSymlink: false, isCanonical: false } }
const resolvedPath = fs.realpathSync(filePath) return { resolvedPath, isSymlink: resolvedPath !== filePath, // realpathSync returned: resolvedPath is canonical (all symlinks in // all path components resolved). Callers can skip further symlink // resolution on this path. isCanonical: true, } } catch (_error) { // If lstat/realpath fails for any reason (ENOENT, broken symlink, // EACCES, ELOOP, etc.), return the original path to allow operations // to proceed return { resolvedPath: filePath, isSymlink: false, isCanonical: false } }}
/** * Check if a file path is a duplicate and should be skipped. * Resolves symlinks to detect duplicates pointing to the same file. * If not a duplicate, adds the resolved path to loadedPaths. * * @returns true if the file should be skipped (is duplicate) */export function isDuplicatePath( fs: FsOperations, filePath: string, loadedPaths: Set<string>,): boolean { const { resolvedPath } = safeResolvePath(fs, filePath) if (loadedPaths.has(resolvedPath)) { return true } loadedPaths.add(resolvedPath) return false}
/** * Resolve the deepest existing ancestor of a path via realpathSync, walking * up until it succeeds. Detects dangling symlinks (link entry exists, target * doesn't) via lstat and resolves them via readlink. * * Use when the input path may not exist (new file writes) and you need to * know where the write would ACTUALLY land after the OS follows symlinks. * * Returns the resolved absolute path with non-existent tail segments * rejoined, or undefined if no symlink was found in any existing ancestor * (the path's existing ancestors all resolve to themselves). * * Handles: live parent symlinks, dangling file symlinks, dangling parent * symlinks. Same core algorithm as teamMemPaths.ts:realpathDeepestExisting. */export function resolveDeepestExistingAncestorSync( fs: FsOperations, absolutePath: string,): string | undefined { let dir = absolutePath const segments: string[] = [] // Walk up using lstat (cheap, O(1)) to find the first existing component. // lstat does not follow symlinks, so dangling symlinks are detected here. // Only call realpathSync (expensive, O(depth)) once at the end. while (dir !== nodePath.dirname(dir)) { let st: fs.Stats try { st = fs.lstatSync(dir) } catch { // lstat failed: truly non-existent. Walk up. segments.unshift(nodePath.basename(dir)) dir = nodePath.dirname(dir) continue } if (st.isSymbolicLink()) { // Found a symlink (live or dangling). Try realpath first (resolves // chained symlinks); fall back to readlink for dangling symlinks. try { const resolved = fs.realpathSync(dir) return segments.length === 0 ? resolved : nodePath.join(resolved, ...segments) } catch { // Dangling: realpath failed but lstat saw the link entry. const target = fs.readlinkSync(dir) const absTarget = nodePath.isAbsolute(target) ? target : nodePath.resolve(nodePath.dirname(dir), target) return segments.length === 0 ? absTarget : nodePath.join(absTarget, ...segments) } } // Existing non-symlink component. One realpath call resolves any // symlinks in its ancestors. If none, return undefined (no symlink). try { const resolved = fs.realpathSync(dir) if (resolved !== dir) { return segments.length === 0 ? resolved : nodePath.join(resolved, ...segments) } } catch { // realpath can still fail (e.g. EACCES in ancestors). Return // undefined — we can't resolve, and the logical path is already // in pathSet for the caller. } return undefined } return undefined}
/** * Gets all paths that should be checked for permissions. * This includes the original path, all intermediate symlink targets in the chain, * and the final resolved path. * * For example, if test.txt -> /etc/passwd -> /private/etc/passwd: * - test.txt (original path) * - /etc/passwd (intermediate symlink target) * - /private/etc/passwd (final resolved path) * * This is important for security: a deny rule for /etc/passwd should block * access even if the file is actually at /private/etc/passwd (as on macOS). * * @param path - The path to check (will be converted to absolute) * @returns An array of absolute paths to check permissions for */export function getPathsForPermissionCheck(inputPath: string): string[] { // Expand tilde notation defensively - tools should do this in getPath(), // but we normalize here as defense in depth for permission checking let path = inputPath if (path === '~') { path = homedir().normalize('NFC') } else if (path.startsWith('~/')) { path = nodePath.join(homedir().normalize('NFC'), path.slice(2)) }
const pathSet = new Set<string>() const fsImpl = getFsImplementation()
// Always check the original path pathSet.add(path)
// Block UNC paths before any filesystem access to prevent network // requests (DNS/SMB) during validation on Windows if (path.startsWith('//') || path.startsWith('\\\\')) { return Array.from(pathSet) }
// Follow the symlink chain, collecting ALL intermediate targets // This handles cases like: test.txt -> /etc/passwd -> /private/etc/passwd // We want to check all three paths, not just test.txt and /private/etc/passwd try { let currentPath = path const visited = new Set<string>() const maxDepth = 40 // Prevent runaway loops, matches typical SYMLOOP_MAX
for (let depth = 0; depth < maxDepth; depth++) { // Prevent infinite loops from circular symlinks if (visited.has(currentPath)) { break } visited.add(currentPath)
if (!fsImpl.existsSync(currentPath)) { // Path doesn't exist (new file case). existsSync follows symlinks, // so this is also reached for DANGLING symlinks (link entry exists, // target doesn't). Resolve symlinks in the path and its ancestors // so permission checks see the real destination. Without this, // `./data -> /etc/cron.d/` (live parent symlink) or // `./evil.txt -> ~/.ssh/authorized_keys2` (dangling file symlink) // would allow writes that escape the working directory. if (currentPath === path) { const resolved = resolveDeepestExistingAncestorSync(fsImpl, path) if (resolved !== undefined) { pathSet.add(resolved) } } break }
const stats = fsImpl.lstatSync(currentPath)
// Skip special file types that can cause issues if ( stats.isFIFO() || stats.isSocket() || stats.isCharacterDevice() || stats.isBlockDevice() ) { break }
if (!stats.isSymbolicLink()) { break }
// Get the immediate symlink target const target = fsImpl.readlinkSync(currentPath)
// If target is relative, resolve it relative to the symlink's directory const absoluteTarget = nodePath.isAbsolute(target) ? target : nodePath.resolve(nodePath.dirname(currentPath), target)
// Add this intermediate target to the set pathSet.add(absoluteTarget) currentPath = absoluteTarget } } catch { // If anything fails during chain traversal, continue with what we have }
// Also add the final resolved path using realpathSync for completeness // This handles any remaining symlinks in directory components const { resolvedPath, isSymlink } = safeResolvePath(fsImpl, path) if (isSymlink && resolvedPath !== path) { pathSet.add(resolvedPath) }
return Array.from(pathSet)}
export const NodeFsOperations: FsOperations = { cwd() { return process.cwd() },
existsSync(fsPath) { using _ = slowLogging`fs.existsSync(${fsPath})` return fs.existsSync(fsPath) },
async stat(fsPath) { return statPromise(fsPath) },
async readdir(fsPath) { return readdirPromise(fsPath, { withFileTypes: true }) },
async unlink(fsPath) { return unlinkPromise(fsPath) },
async rmdir(fsPath) { return rmdirPromise(fsPath) },
async rm(fsPath, options) { return rmPromise(fsPath, options) },
async mkdir(dirPath, options) { try { await mkdirPromise(dirPath, { recursive: true, ...options }) } catch (e) { // Bun/Windows: recursive:true throws EEXIST on directories with the // FILE_ATTRIBUTE_READONLY bit set (Group Policy, OneDrive, desktop.ini). // Bun's directoryExistsAt misclassifies DIRECTORY+READONLY as not-a-dir // (bun-internal src/sys.zig existsAtType). The dir exists; ignore. // https://github.com/anthropics/claude-code/issues/30924 if (getErrnoCode(e) !== 'EEXIST') throw e } },
async readFile(fsPath, options) { return readFilePromise(fsPath, { encoding: options.encoding }) },
async rename(oldPath, newPath) { return renamePromise(oldPath, newPath) },
statSync(fsPath) { using _ = slowLogging`fs.statSync(${fsPath})` return fs.statSync(fsPath) },
lstatSync(fsPath) { using _ = slowLogging`fs.lstatSync(${fsPath})` return fs.lstatSync(fsPath) },
readFileSync(fsPath, options) { using _ = slowLogging`fs.readFileSync(${fsPath})` return fs.readFileSync(fsPath, { encoding: options.encoding }) },
readFileBytesSync(fsPath) { using _ = slowLogging`fs.readFileBytesSync(${fsPath})` return fs.readFileSync(fsPath) },
readSync(fsPath, options) { using _ = slowLogging`fs.readSync(${fsPath}, ${options.length} bytes)` let fd: number | undefined = undefined try { fd = fs.openSync(fsPath, 'r') const buffer = Buffer.alloc(options.length) const bytesRead = fs.readSync(fd, buffer, 0, options.length, 0) return { buffer, bytesRead } } finally { if (fd) fs.closeSync(fd) } },
appendFileSync(path, data, options) { using _ = slowLogging`fs.appendFileSync(${path}, ${data.length} chars)` // For new files with explicit mode, use 'ax' (atomic create-with-mode) to avoid // TOCTOU race between existence check and open. Fall back to normal append if exists. if (options?.mode !== undefined) { try { const fd = fs.openSync(path, 'ax', options.mode) try { fs.appendFileSync(fd, data) } finally { fs.closeSync(fd) } return } catch (e) { if (getErrnoCode(e) !== 'EEXIST') throw e // File exists — fall through to normal append } } fs.appendFileSync(path, data) },
copyFileSync(src, dest) { using _ = slowLogging`fs.copyFileSync(${src} → ${dest})` fs.copyFileSync(src, dest) },
unlinkSync(path: string) { using _ = slowLogging`fs.unlinkSync(${path})` fs.unlinkSync(path) },
renameSync(oldPath: string, newPath: string) { using _ = slowLogging`fs.renameSync(${oldPath} → ${newPath})` fs.renameSync(oldPath, newPath) },
linkSync(target: string, path: string) { using _ = slowLogging`fs.linkSync(${target} → ${path})` fs.linkSync(target, path) },
symlinkSync( target: string, path: string, type?: 'dir' | 'file' | 'junction', ) { using _ = slowLogging`fs.symlinkSync(${target} → ${path})` fs.symlinkSync(target, path, type) },
readlinkSync(path: string) { using _ = slowLogging`fs.readlinkSync(${path})` return fs.readlinkSync(path) },
realpathSync(path: string) { using _ = slowLogging`fs.realpathSync(${path})` return fs.realpathSync(path).normalize('NFC') },
mkdirSync(dirPath, options) { using _ = slowLogging`fs.mkdirSync(${dirPath})` const mkdirOptions: { recursive: boolean; mode?: number } = { recursive: true, } if (options?.mode !== undefined) { mkdirOptions.mode = options.mode } try { fs.mkdirSync(dirPath, mkdirOptions) } catch (e) { // Bun/Windows: recursive:true throws EEXIST on directories with the // FILE_ATTRIBUTE_READONLY bit set (Group Policy, OneDrive, desktop.ini). // Bun's directoryExistsAt misclassifies DIRECTORY+READONLY as not-a-dir // (bun-internal src/sys.zig existsAtType). The dir exists; ignore. // https://github.com/anthropics/claude-code/issues/30924 if (getErrnoCode(e) !== 'EEXIST') throw e } },
readdirSync(dirPath) { using _ = slowLogging`fs.readdirSync(${dirPath})` return fs.readdirSync(dirPath, { withFileTypes: true }) },
readdirStringSync(dirPath) { using _ = slowLogging`fs.readdirStringSync(${dirPath})` return fs.readdirSync(dirPath) },
isDirEmptySync(dirPath) { using _ = slowLogging`fs.isDirEmptySync(${dirPath})` const files = this.readdirSync(dirPath) return files.length === 0 },
rmdirSync(dirPath) { using _ = slowLogging`fs.rmdirSync(${dirPath})` fs.rmdirSync(dirPath) },
rmSync(path, options) { using _ = slowLogging`fs.rmSync(${path})` fs.rmSync(path, options) },
createWriteStream(path: string) { return fs.createWriteStream(path) },
async readFileBytes(fsPath: string, maxBytes?: number) { if (maxBytes === undefined) { return readFilePromise(fsPath) } const handle = await open(fsPath, 'r') try { const { size } = await handle.stat() const readSize = Math.min(size, maxBytes) const buffer = Buffer.allocUnsafe(readSize) let offset = 0 while (offset < readSize) { const { bytesRead } = await handle.read( buffer, offset, readSize - offset, offset, ) if (bytesRead === 0) break offset += bytesRead } return offset < readSize ? buffer.subarray(0, offset) : buffer } finally { await handle.close() } },}
// The currently active filesystem implementationlet activeFs: FsOperations = NodeFsOperations
/** * Overrides the filesystem implementation. Note: This function does not * automatically update cwd. * @param implementation The filesystem implementation to use */export function setFsImplementation(implementation: FsOperations): void { activeFs = implementation}
/** * Gets the currently active filesystem implementation * @returns The currently active filesystem implementation */export function getFsImplementation(): FsOperations { return activeFs}
/** * Resets the filesystem implementation to the default Node.js implementation. * Note: This function does not automatically update cwd. */export function setOriginalFsImplementation(): void { activeFs = NodeFsOperations}
export type ReadFileRangeResult = { content: string bytesRead: number bytesTotal: number}
/** * Read up to `maxBytes` from a file starting at `offset`. * Returns a flat string from Buffer — no sliced string references to a * larger parent. Returns null if the file is smaller than the offset. */export async function readFileRange( path: string, offset: number, maxBytes: number,): Promise<ReadFileRangeResult | null> { await using fh = await open(path, 'r') const size = (await fh.stat()).size if (size <= offset) { return null } const bytesToRead = Math.min(size - offset, maxBytes) const buffer = Buffer.allocUnsafe(bytesToRead)
let totalRead = 0 while (totalRead < bytesToRead) { const { bytesRead } = await fh.read( buffer, totalRead, bytesToRead - totalRead, offset + totalRead, ) if (bytesRead === 0) { break } totalRead += bytesRead }
return { content: buffer.toString('utf8', 0, totalRead), bytesRead: totalRead, bytesTotal: size, }}
/** * Read the last `maxBytes` of a file. * Returns the whole file if it's smaller than maxBytes. */export async function tailFile( path: string, maxBytes: number,): Promise<ReadFileRangeResult> { await using fh = await open(path, 'r') const size = (await fh.stat()).size if (size === 0) { return { content: '', bytesRead: 0, bytesTotal: 0 } } const offset = Math.max(0, size - maxBytes) const bytesToRead = size - offset const buffer = Buffer.allocUnsafe(bytesToRead)
let totalRead = 0 while (totalRead < bytesToRead) { const { bytesRead } = await fh.read( buffer, totalRead, bytesToRead - totalRead, offset + totalRead, ) if (bytesRead === 0) { break } totalRead += bytesRead }
return { content: buffer.toString('utf8', 0, totalRead), bytesRead: totalRead, bytesTotal: size, }}
/** * Async generator that yields lines from a file in reverse order. * Reads the file backwards in chunks to avoid loading the entire file into memory. * @param path - The path to the file to read * @returns An async generator that yields lines in reverse order */export async function* readLinesReverse( path: string,): AsyncGenerator<string, void, undefined> { const CHUNK_SIZE = 1024 * 4 const fileHandle = await open(path, 'r') try { const stats = await fileHandle.stat() let position = stats.size // Carry raw bytes (not a decoded string) across chunk boundaries so that // multi-byte UTF-8 sequences split by the 4KB boundary are not corrupted. // Decoding per-chunk would turn a split sequence into U+FFFD on both sides, // which for history.jsonl means JSON.parse throws and the entry is dropped. let remainder = Buffer.alloc(0) const buffer = Buffer.alloc(CHUNK_SIZE)
while (position > 0) { const currentChunkSize = Math.min(CHUNK_SIZE, position) position -= currentChunkSize
await fileHandle.read(buffer, 0, currentChunkSize, position) const combined = Buffer.concat([ buffer.subarray(0, currentChunkSize), remainder, ])
const firstNewline = combined.indexOf(0x0a) if (firstNewline === -1) { remainder = combined continue }
remainder = Buffer.from(combined.subarray(0, firstNewline)) const lines = combined.toString('utf8', firstNewline + 1).split('\n')
for (let i = lines.length - 1; i >= 0; i--) { const line = lines[i]! if (line) { yield line } } }
if (remainder.length > 0) { yield remainder.toString('utf8') } } finally { await fileHandle.close() }}