fix: use fs.realpath instead of manual path.resolve for symlink resolution

resolveSymlink and resolveSymlinkAsync incorrectly resolved relative
symlinks by using path.resolve(filePath, '..', linkTarget). This fails
when symlinks use multi-level relative paths (e.g. ../../skills/...) or
when symlinks are chained (symlink pointing to a directory containing
more symlinks).

Replace with fs.realpathSync/fs.realpath which delegates to the OS for
correct resolution of all symlink types: relative, absolute, chained,
and nested.

Fixes #1738

AI-assisted-by: claude-opus-4.6 via opencode
AI-contribution: partial
AI-session: 20260212-120629-4gTXvDGV
This commit is contained in:
Bram van der Horn
2026-02-12 12:12:40 +01:00
parent 283c7e6cb7
commit 1511886c0c
2 changed files with 106 additions and 13 deletions
+3 -13
View File
@@ -1,6 +1,5 @@
import { lstatSync, readlinkSync } from "fs"
import { lstatSync, realpathSync } from "fs"
import { promises as fs } from "fs"
import { resolve } from "path"
export function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile()
@@ -16,11 +15,7 @@ export function isSymbolicLink(filePath: string): boolean {
export function resolveSymlink(filePath: string): string {
try {
const stats = lstatSync(filePath, { throwIfNoEntry: false })
if (stats?.isSymbolicLink()) {
return resolve(filePath, "..", readlinkSync(filePath))
}
return filePath
return realpathSync(filePath)
} catch {
return filePath
}
@@ -28,12 +23,7 @@ export function resolveSymlink(filePath: string): string {
export async function resolveSymlinkAsync(filePath: string): Promise<string> {
try {
const stats = await fs.lstat(filePath)
if (stats.isSymbolicLink()) {
const linkTarget = await fs.readlink(filePath)
return resolve(filePath, "..", linkTarget)
}
return filePath
return await fs.realpath(filePath)
} catch {
return filePath
}