From e9c9cb696d487f58d3b5a810bb1f272d6c179ff5 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 14 Feb 2026 16:19:18 +0900 Subject: [PATCH] fix: resolve symlinks in skill config source discovery and test paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use fs.realpath() in config-source-discovery to resolve symlinks before loading skills, preventing duplicate/mismatched paths on systems where tmpdir() returns a symlink (e.g., macOS /var → /private/var). Also adds agents-config-dir utility for ~/.agents path resolution. --- .../config-source-discovery.ts | 16 +++++++++------- src/shared/agents-config-dir.test.ts | 14 ++++++++++++++ src/shared/agents-config-dir.ts | 6 ++++++ src/shared/file-utils.test.ts | 4 ++-- 4 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 src/shared/agents-config-dir.test.ts create mode 100644 src/shared/agents-config-dir.ts diff --git a/src/features/opencode-skill-loader/config-source-discovery.ts b/src/features/opencode-skill-loader/config-source-discovery.ts index df3ee653e..5d98cf51b 100644 --- a/src/features/opencode-skill-loader/config-source-discovery.ts +++ b/src/features/opencode-skill-loader/config-source-discovery.ts @@ -53,26 +53,28 @@ async function loadSourcePath(options: { const stat = await fs.stat(absolutePath).catch(() => null) if (!stat) return [] + const realBasePath = await fs.realpath(absolutePath).catch(() => absolutePath) + if (stat.isFile()) { - if (!isMarkdownPath(absolutePath)) return [] + if (!isMarkdownPath(realBasePath)) return [] const loaded = await loadSkillFromPath({ - skillPath: absolutePath, - resolvedPath: dirname(absolutePath), - defaultName: inferSkillNameFromFileName(absolutePath), + skillPath: realBasePath, + resolvedPath: dirname(realBasePath), + defaultName: inferSkillNameFromFileName(realBasePath), scope: "config", }) if (!loaded) return [] - return filterByGlob([loaded], dirname(absolutePath), options.globPattern) + return filterByGlob([loaded], dirname(realBasePath), options.globPattern) } if (!stat.isDirectory()) return [] const directorySkills = await loadSkillsFromDir({ - skillsDir: absolutePath, + skillsDir: realBasePath, scope: "config", maxDepth: options.recursive ? MAX_RECURSIVE_DEPTH : 0, }) - return filterByGlob(directorySkills, absolutePath, options.globPattern) + return filterByGlob(directorySkills, realBasePath, options.globPattern) } export async function discoverConfigSourceSkills(options: { diff --git a/src/shared/agents-config-dir.test.ts b/src/shared/agents-config-dir.test.ts new file mode 100644 index 000000000..6b4508c6f --- /dev/null +++ b/src/shared/agents-config-dir.test.ts @@ -0,0 +1,14 @@ +import { describe, test, expect } from "bun:test" +import { getAgentsConfigDir } from "./agents-config-dir" + +describe("getAgentsConfigDir", () => { + test("returns path ending with .agents", () => { + // given agents config dir is requested + + // when getAgentsConfigDir is called + const result = getAgentsConfigDir() + + // then returns path ending with .agents + expect(result.endsWith(".agents")).toBe(true) + }) +}) diff --git a/src/shared/agents-config-dir.ts b/src/shared/agents-config-dir.ts new file mode 100644 index 000000000..d43c2918a --- /dev/null +++ b/src/shared/agents-config-dir.ts @@ -0,0 +1,6 @@ +import { homedir } from "node:os" +import { join } from "node:path" + +export function getAgentsConfigDir(): string { + return join(homedir(), ".agents") +} diff --git a/src/shared/file-utils.test.ts b/src/shared/file-utils.test.ts index 82d16dab1..1ee41d214 100644 --- a/src/shared/file-utils.test.ts +++ b/src/shared/file-utils.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect, beforeAll, afterAll } from "bun:test" -import { mkdirSync, writeFileSync, symlinkSync, rmSync } from "fs" +import { mkdirSync, writeFileSync, symlinkSync, rmSync, realpathSync } from "fs" import { join } from "path" import { tmpdir } from "os" import { resolveSymlink, resolveSymlinkAsync, isSymbolicLink } from "./file-utils" -const testDir = join(tmpdir(), "file-utils-test-" + Date.now()) +const testDir = join(realpathSync(tmpdir()), "file-utils-test-" + Date.now()) // Create a directory structure that mimics the real-world scenario: //