Skip to content

Commit 0556ea3

Browse files
authored
Fix macOS config path alias detection in workspace resolver
- normalize workspace/config path comparisons using realpath-aware canonicalization\n- harden macOS case-variant path handling during config-dir detection\n- add regression coverage for symlink-alias config path resolution
1 parent 714b4d6 commit 0556ea3

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

src/plugin.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
22
import { tool } from "@opencode-ai/plugin";
33
import type { Auth } from "@opencode-ai/sdk";
4+
import { realpathSync } from "fs";
45
import { mkdir } from "fs/promises";
56
import { homedir } from "os";
67
import { isAbsolute, join, relative, resolve } from "path";
@@ -75,8 +76,29 @@ function getOpenCodeConfigPrefix(): string {
7576
return join(configHome, "opencode");
7677
}
7778

79+
function canonicalizePathForCompare(pathValue: string): string {
80+
const resolvedPath = resolve(pathValue);
81+
let normalizedPath = resolvedPath;
82+
83+
try {
84+
normalizedPath = typeof realpathSync.native === "function"
85+
? realpathSync.native(resolvedPath)
86+
: realpathSync(resolvedPath);
87+
} catch {
88+
normalizedPath = resolvedPath;
89+
}
90+
91+
if (process.platform === "darwin") {
92+
return normalizedPath.toLowerCase();
93+
}
94+
95+
return normalizedPath;
96+
}
97+
7898
function isWithinPath(root: string, candidate: string): boolean {
79-
const rel = relative(root, candidate);
99+
const normalizedRoot = canonicalizePathForCompare(root);
100+
const normalizedCandidate = canonicalizePathForCompare(candidate);
101+
const rel = relative(normalizedRoot, normalizedCandidate);
80102
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
81103
}
82104

tests/unit/plugin-tools-hook.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from "bun:test";
2-
import { mkdtempSync, readFileSync, realpathSync, rmSync, mkdirSync, existsSync } from "fs";
2+
import { mkdtempSync, readFileSync, realpathSync, rmSync, mkdirSync, existsSync, symlinkSync } from "fs";
33
import { tmpdir } from "os";
44
import { join } from "path";
55
import { CursorPlugin } from "../../src/plugin";
@@ -197,4 +197,45 @@ describe("Plugin tool hook", () => {
197197
rmSync(unexpectedDir, { recursive: true, force: true });
198198
}
199199
});
200+
201+
it("treats config path aliases (symlink/case variants) as config and falls back to workspace", async () => {
202+
const projectDir = mkdtempSync(join(tmpdir(), "plugin-hook-config-alias-project-"));
203+
const xdgConfigHome = mkdtempSync(join(tmpdir(), "plugin-hook-config-alias-xdg-"));
204+
const aliasParentDir = mkdtempSync(join(tmpdir(), "plugin-hook-config-alias-parent-"));
205+
const aliasXdgHome = join(aliasParentDir, "xdg-home-alias");
206+
const prevXdg = process.env.XDG_CONFIG_HOME;
207+
208+
try {
209+
process.env.XDG_CONFIG_HOME = xdgConfigHome;
210+
symlinkSync(xdgConfigHome, aliasXdgHome);
211+
212+
const configDir = join(xdgConfigHome, "opencode");
213+
mkdirSync(configDir, { recursive: true });
214+
215+
const aliasConfigDir = join(aliasXdgHome, "opencode");
216+
const filename = `symlink-alias-${Date.now()}.txt`;
217+
218+
const hooks = await CursorPlugin(createMockInput(configDir, projectDir));
219+
const out = await hooks.tool?.write?.execute(
220+
{ path: `nested/${filename}`, content: "alias fallback" },
221+
createToolContext(aliasConfigDir, undefined, "session-alias-1"),
222+
);
223+
224+
const expectedPath = join(projectDir, `nested/${filename}`);
225+
const unexpectedPath = join(configDir, `nested/${filename}`);
226+
227+
expect(readFileSync(expectedPath, "utf-8")).toBe("alias fallback");
228+
expect(out).toContain(expectedPath);
229+
expect(existsSync(unexpectedPath)).toBe(false);
230+
} finally {
231+
if (prevXdg === undefined) {
232+
delete process.env.XDG_CONFIG_HOME;
233+
} else {
234+
process.env.XDG_CONFIG_HOME = prevXdg;
235+
}
236+
rmSync(projectDir, { recursive: true, force: true });
237+
rmSync(xdgConfigHome, { recursive: true, force: true });
238+
rmSync(aliasParentDir, { recursive: true, force: true });
239+
}
240+
});
200241
});

0 commit comments

Comments
 (0)