import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { ensureCursorSkillsInjected } from "@paperclipai/adapter-cursor-local/server"; async function makeTempDir(prefix: string): Promise { return fs.mkdtemp(path.join(os.tmpdir(), prefix)); } async function createSkillDir(root: string, name: string) { await fs.mkdir(path.join(root, name), { recursive: true }); } describe("cursor local adapter skill injection", () => { const cleanupDirs = new Set(); afterEach(async () => { await Promise.all(Array.from(cleanupDirs).map((dir) => fs.rm(dir, { recursive: true, force: true }))); cleanupDirs.clear(); }); it("links missing Paperclip skills into Cursor skills home", async () => { const skillsDir = await makeTempDir("paperclip-cursor-skills-src-"); const skillsHome = await makeTempDir("paperclip-cursor-skills-home-"); cleanupDirs.add(skillsDir); cleanupDirs.add(skillsHome); await createSkillDir(skillsDir, "paperclip"); await createSkillDir(skillsDir, "paperclip-create-agent"); await fs.writeFile(path.join(skillsDir, "README.txt"), "ignore", "utf8"); const logs: string[] = []; await ensureCursorSkillsInjected( async (_stream, chunk) => { logs.push(chunk); }, { skillsDir, skillsHome }, ); const injectedA = path.join(skillsHome, "paperclip"); const injectedB = path.join(skillsHome, "paperclip-create-agent"); expect((await fs.lstat(injectedA)).isSymbolicLink()).toBe(true); expect((await fs.lstat(injectedB)).isSymbolicLink()).toBe(true); expect(await fs.realpath(injectedA)).toBe(await fs.realpath(path.join(skillsDir, "paperclip"))); expect(await fs.realpath(injectedB)).toBe( await fs.realpath(path.join(skillsDir, "paperclip-create-agent")), ); expect(logs.some((line) => line.includes('Injected Cursor skill "paperclip"'))).toBe(true); expect(logs.some((line) => line.includes('Injected Cursor skill "paperclip-create-agent"'))).toBe(true); }); it("preserves existing targets and only links missing skills", async () => { const skillsDir = await makeTempDir("paperclip-cursor-preserve-src-"); const skillsHome = await makeTempDir("paperclip-cursor-preserve-home-"); cleanupDirs.add(skillsDir); cleanupDirs.add(skillsHome); await createSkillDir(skillsDir, "paperclip"); await createSkillDir(skillsDir, "paperclip-create-agent"); const existingTarget = path.join(skillsHome, "paperclip"); await fs.mkdir(existingTarget, { recursive: true }); await fs.writeFile(path.join(existingTarget, "keep.txt"), "keep", "utf8"); await ensureCursorSkillsInjected(async () => {}, { skillsDir, skillsHome }); expect((await fs.lstat(existingTarget)).isDirectory()).toBe(true); expect(await fs.readFile(path.join(existingTarget, "keep.txt"), "utf8")).toBe("keep"); expect((await fs.lstat(path.join(skillsHome, "paperclip-create-agent"))).isSymbolicLink()).toBe(true); }); it("logs per-skill link failures and continues without throwing", async () => { const skillsDir = await makeTempDir("paperclip-cursor-fail-src-"); const skillsHome = await makeTempDir("paperclip-cursor-fail-home-"); cleanupDirs.add(skillsDir); cleanupDirs.add(skillsHome); await createSkillDir(skillsDir, "ok-skill"); await createSkillDir(skillsDir, "fail-skill"); const logs: string[] = []; await ensureCursorSkillsInjected( async (_stream, chunk) => { logs.push(chunk); }, { skillsDir, skillsHome, linkSkill: async (source, target) => { if (target.endsWith(`${path.sep}fail-skill`)) { throw new Error("simulated link failure"); } await fs.symlink(source, target); }, }, ); expect((await fs.lstat(path.join(skillsHome, "ok-skill"))).isSymbolicLink()).toBe(true); await expect(fs.lstat(path.join(skillsHome, "fail-skill"))).rejects.toThrow(); expect(logs.some((line) => line.includes('Failed to inject Cursor skill "fail-skill"'))).toBe(true); }); });