From a9128e0ac60882ef8f2de67ce0f2cad1bacd048f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 23:40:28 +0000 Subject: [PATCH 1/3] fix(cli): replace existing expect folders with symlinks Co-authored-by: Aiden Bai --- apps/cli/src/commands/add-skill.ts | 12 +++++++---- apps/cli/tests/add-skill.test.ts | 34 ++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/cli/src/commands/add-skill.ts b/apps/cli/src/commands/add-skill.ts index dd486de0e..b5e0d3067 100644 --- a/apps/cli/src/commands/add-skill.ts +++ b/apps/cli/src/commands/add-skill.ts @@ -3,6 +3,7 @@ import { lstatSync, mkdirSync, readlinkSync, + rmSync, symlinkSync, unlinkSync, writeFileSync, @@ -114,7 +115,7 @@ const selectAgents = async (agents: readonly SupportedAgent[], nonInteractive: b return (response.agents ?? []) as SupportedAgent[]; }; -const ensureAgentSymlink = (projectRoot: string, agent: SupportedAgent): boolean | string => { +export const ensureAgentSymlink = (projectRoot: string, agent: SupportedAgent): boolean | string => { const skillSourceDir = join(projectRoot, AGENTS_SKILLS_DIR, SKILL_NAME); const agentSkillDir = join(projectRoot, toSkillDir(agent)); const symlinkPath = join(agentSkillDir, SKILL_NAME); @@ -123,9 +124,12 @@ const ensureAgentSymlink = (projectRoot: string, agent: SupportedAgent): boolean try { if (existsSync(symlinkPath)) { const stats = lstatSync(symlinkPath); - if (!stats.isSymbolicLink()) return `${symlinkPath} exists and is not a symlink`; - if (readlinkSync(symlinkPath) === targetPath) return true; - unlinkSync(symlinkPath); + if (stats.isSymbolicLink()) { + if (readlinkSync(symlinkPath) === targetPath) return true; + unlinkSync(symlinkPath); + } else { + rmSync(symlinkPath, { recursive: true, force: true }); + } } mkdirSync(dirname(symlinkPath), { recursive: true }); diff --git a/apps/cli/tests/add-skill.test.ts b/apps/cli/tests/add-skill.test.ts index db25f2311..4f72539a5 100644 --- a/apps/cli/tests/add-skill.test.ts +++ b/apps/cli/tests/add-skill.test.ts @@ -1,8 +1,17 @@ -import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { + lstatSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + readlinkSync, + rmSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { describe, expect, it, beforeEach, afterEach } from "vite-plus/test"; -import { extractTarEntries, readNullTerminated } from "../src/commands/add-skill"; +import { ensureAgentSymlink, extractTarEntries, readNullTerminated } from "../src/commands/add-skill"; const TAR_HEADER_SIZE = 512; @@ -108,3 +117,24 @@ describe("extractTarEntries", () => { expect(readdirSync(destDir)).toEqual([]); }); }); + +describe("ensureAgentSymlink", () => { + it("replaces an existing non-symlink expect directory with a symlink", () => { + const projectRoot = mkdtempSync(join(tmpdir(), "ensure-symlink-")); + + try { + mkdirSync(join(projectRoot, ".agents", "skills", "expect"), { recursive: true }); + mkdirSync(join(projectRoot, ".codex", "skills", "expect"), { recursive: true }); + writeFileSync(join(projectRoot, ".codex", "skills", "expect", "old-file.txt"), "legacy"); + + const result = ensureAgentSymlink(projectRoot, "codex"); + + expect(result).toBe(true); + const linkedPath = join(projectRoot, ".codex", "skills", "expect"); + expect(lstatSync(linkedPath).isSymbolicLink()).toBe(true); + expect(readlinkSync(linkedPath)).toBe("../../../.agents/skills/expect"); + } finally { + rmSync(projectRoot, { recursive: true, force: true }); + } + }); +}); From 71ae40607b8a72110286e2a36d043c4f60531923 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 23:42:10 +0000 Subject: [PATCH 2/3] test(cli): fix expected relative symlink target Co-authored-by: Aiden Bai --- apps/cli/tests/add-skill.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/tests/add-skill.test.ts b/apps/cli/tests/add-skill.test.ts index 4f72539a5..909a9bc77 100644 --- a/apps/cli/tests/add-skill.test.ts +++ b/apps/cli/tests/add-skill.test.ts @@ -132,7 +132,7 @@ describe("ensureAgentSymlink", () => { expect(result).toBe(true); const linkedPath = join(projectRoot, ".codex", "skills", "expect"); expect(lstatSync(linkedPath).isSymbolicLink()).toBe(true); - expect(readlinkSync(linkedPath)).toBe("../../../.agents/skills/expect"); + expect(readlinkSync(linkedPath)).toBe("../../.agents/skills/expect"); } finally { rmSync(projectRoot, { recursive: true, force: true }); } From c903e1c06a03ade1cb8a8678765395c3bbf42ce9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 05:18:55 +0000 Subject: [PATCH 3/3] fix(cli): replace broken expect symlinks during skill install Co-authored-by: Aiden Bai --- apps/cli/src/commands/add-skill.ts | 12 ++++++++++-- apps/cli/tests/add-skill.test.ts | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/cli/src/commands/add-skill.ts b/apps/cli/src/commands/add-skill.ts index b5e0d3067..6339c16d8 100644 --- a/apps/cli/src/commands/add-skill.ts +++ b/apps/cli/src/commands/add-skill.ts @@ -92,6 +92,14 @@ const downloadSkill = async (skillDir: string): Promise => { } }; +const getPathStats = (path: string) => { + try { + return lstatSync(path); + } catch { + return undefined; + } +}; + const selectAgents = async (agents: readonly SupportedAgent[], nonInteractive: boolean) => { if (nonInteractive) return [...agents]; @@ -122,8 +130,8 @@ export const ensureAgentSymlink = (projectRoot: string, agent: SupportedAgent): const targetPath = relative(dirname(symlinkPath), skillSourceDir); try { - if (existsSync(symlinkPath)) { - const stats = lstatSync(symlinkPath); + const stats = getPathStats(symlinkPath); + if (stats) { if (stats.isSymbolicLink()) { if (readlinkSync(symlinkPath) === targetPath) return true; unlinkSync(symlinkPath); diff --git a/apps/cli/tests/add-skill.test.ts b/apps/cli/tests/add-skill.test.ts index 909a9bc77..0fcb521f8 100644 --- a/apps/cli/tests/add-skill.test.ts +++ b/apps/cli/tests/add-skill.test.ts @@ -6,6 +6,7 @@ import { readdirSync, readlinkSync, rmSync, + symlinkSync, writeFileSync, } from "node:fs"; import { join } from "node:path"; @@ -137,4 +138,23 @@ describe("ensureAgentSymlink", () => { rmSync(projectRoot, { recursive: true, force: true }); } }); + + it("replaces a broken symlink expect path with a valid symlink", () => { + const projectRoot = mkdtempSync(join(tmpdir(), "ensure-symlink-broken-")); + + try { + mkdirSync(join(projectRoot, ".agents", "skills", "expect"), { recursive: true }); + mkdirSync(join(projectRoot, ".codex", "skills"), { recursive: true }); + symlinkSync("../../../.agents/skills/does-not-exist", join(projectRoot, ".codex", "skills", "expect")); + + const result = ensureAgentSymlink(projectRoot, "codex"); + + expect(result).toBe(true); + const linkedPath = join(projectRoot, ".codex", "skills", "expect"); + expect(lstatSync(linkedPath).isSymbolicLink()).toBe(true); + expect(readlinkSync(linkedPath)).toBe("../../.agents/skills/expect"); + } finally { + rmSync(projectRoot, { recursive: true, force: true }); + } + }); });