Swadhin Biswas feat: implement mervelas cli engine, built-in tools, and reactive UI · 4/3/2026
0358986
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
import * 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 implementation
let 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()
}
}