Skip to content

Commit ef2f0a4

Browse files
committed
fix(skills): remove Codex mirror links left by earlier skill updates
1 parent dabb513 commit ef2f0a4

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

‎packages/cli/src/utils/skillsMirror.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,31 @@ describe("mirrorGlobalSkills", () => {
277277
expect(readFileSync(join(existing, "SKILL.md"), "utf8")).toBe("# locally managed\n");
278278
});
279279

280+
it("removes Codex links an earlier mirror created and keeps the user's own entries", () => {
281+
const home = makeHome();
282+
seedStore(home, ["hyperframes", "media-use"]);
283+
const codexSkills = join(home, ".codex", "skills");
284+
mkdirSync(join(codexSkills, ".system"), { recursive: true });
285+
mkdirSync(join(codexSkills, "my-skill"), { recursive: true });
286+
// What main's mirror wrote: a relative link back into the Claude store.
287+
symlinkSync(
288+
join("..", "..", ".claude", "skills", "hyperframes"),
289+
join(codexSkills, "hyperframes"),
290+
);
291+
// Same skill name, but the user's own link to somewhere else.
292+
const userCopy = join(home, "my-media-use");
293+
mkdirSync(userCopy);
294+
symlinkSync(userCopy, join(codexSkills, "media-use"));
295+
296+
mirrorGlobalSkills({ skills: ["hyperframes", "media-use"], home, platform: "linux", env: ENV });
297+
298+
expect(existsSync(join(codexSkills, "hyperframes"))).toBe(false);
299+
expect(realpathSync(join(codexSkills, "media-use"))).toBe(realpathSync(userCopy));
300+
expect(existsSync(join(codexSkills, ".system"))).toBe(true);
301+
expect(existsSync(join(codexSkills, "my-skill"))).toBe(true);
302+
expect(existsSync(join(home, ".claude", "skills", "hyperframes", "SKILL.md"))).toBe(true);
303+
});
304+
280305
// Pi natively discovers BOTH ~/.pi/agent/skills and the universal
281306
// ~/.agents/skills (pi's packages/coding-agent/docs/skills.md#locations).
282307
// A mirrored per-agent copy collides with the universal one and Pi skips

‎packages/cli/src/utils/skillsMirror.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
rmSync,
3333
statSync,
3434
symlinkSync,
35+
unlinkSync,
3536
} from "node:fs";
3637
import { homedir } from "node:os";
3738
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -237,6 +238,23 @@ function mirrorInto(
237238
return { mirrored: true };
238239
}
239240

241+
/**
242+
* Unlink symlinks an earlier mirror left in `targetDir` (they resolve to the same source skill).
243+
* Real dirs and Windows copies carry no ownership proof, so they stay.
244+
*/
245+
function removeMirrorLinks(targetDir: string, source: string, skills: string[]): void {
246+
for (const skill of skills) {
247+
const targetSkill = join(targetDir, skill);
248+
try {
249+
if (!lstatSync(targetSkill).isSymbolicLink()) continue;
250+
if (realpathSync(targetSkill) !== realpathSync(join(source, skill))) continue;
251+
unlinkSync(targetSkill);
252+
} catch {
253+
// absent or dangling: nothing of ours to remove
254+
}
255+
}
256+
}
257+
240258
/**
241259
* Mirror the global Claude store into every installed agent's global skills
242260
* dir. Best-effort and idempotent: a no-op when the store is absent, and per
@@ -283,7 +301,11 @@ export function mirrorGlobalSkills(opts: {
283301
for (const { agent, base, sub } of AGENT_GLOBAL_DIRS) {
284302
const targetDir = join(bases[base], ...sub.split("/").filter(Boolean));
285303
if (targetDir === source || targetDir === universalStore) continue; // install-owned
286-
if (UNIVERSAL_STORE_READERS.has(agent)) continue; // already reads the universal store (#3294)
304+
if (UNIVERSAL_STORE_READERS.has(agent)) {
305+
// Already reads the universal store (#3294); drop links an older version mirrored here.
306+
removeMirrorLinks(targetDir, source, skills);
307+
continue;
308+
}
287309
if (!existsSync(dirname(targetDir))) continue; // agent not installed (no marker)
288310
const attempt = mirrorInto(targetDir, source, skills, platform, () =>
289311
targetSafety(targetDir, resolvedProtectedPaths),

0 commit comments

Comments
 (0)