Skip to content

Commit 3ce4b6c

Browse files
committed
fix(skills): clean orphaned Claude projections
1 parent 8491532 commit 3ce4b6c

1 file changed

Lines changed: 38 additions & 1 deletion

File tree

scripts/sync-skills.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* bun run scripts/sync-skills.ts --check # fail (exit 1) if any projection is stale
2121
*/
2222
import { lstat, mkdir, readdir, readFile, readlink, rm, rmdir, symlink } from 'node:fs/promises'
23-
import { dirname, relative, resolve } from 'node:path'
23+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'
2424
import { fileURLToPath } from 'node:url'
2525

2626
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
@@ -73,6 +73,36 @@ async function loadCanonicalSkills(): Promise<Skill[]> {
7373
return skills
7474
}
7575

76+
/** Find generated Claude links whose canonical skill no longer exists. */
77+
async function findOrphanedClaudeLinks(expectedPaths: ReadonlySet<string>): Promise<string[]> {
78+
const entries = await readdir(CLAUDE_SKILLS_DIR, { withFileTypes: true }).catch(
79+
(error: unknown) => {
80+
const code =
81+
typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined
82+
if (code === 'ENOENT') return []
83+
throw error
84+
}
85+
)
86+
const orphaned: string[] = []
87+
88+
for (const entry of entries) {
89+
if (!entry.isSymbolicLink()) continue
90+
const path = resolve(CLAUDE_SKILLS_DIR, entry.name)
91+
if (expectedPaths.has(path)) continue
92+
93+
const target = resolve(dirname(path), await readlink(path))
94+
const canonicalRelative = relative(CANONICAL_DIR, target)
95+
const targetsCanonicalSkills =
96+
canonicalRelative === '' ||
97+
(!isAbsolute(canonicalRelative) &&
98+
canonicalRelative !== '..' &&
99+
!canonicalRelative.startsWith(`..${sep}`))
100+
if (targetsCanonicalSkills) orphaned.push(path)
101+
}
102+
103+
return orphaned
104+
}
105+
76106
async function main() {
77107
const check = process.argv.includes('--check')
78108
const skills = await loadCanonicalSkills()
@@ -90,6 +120,7 @@ async function main() {
90120
])
91121

92122
const stale: string[] = []
123+
const expectedClaudePaths = new Set(claudeLinks.map(({ path }) => path))
93124
for (const { path, target } of claudeLinks) {
94125
const current = await readlink(path).catch(() => null)
95126
if (current === target) continue
@@ -106,6 +137,12 @@ async function main() {
106137
}
107138
}
108139

140+
const orphanedClaudeLinks = await findOrphanedClaudeLinks(expectedClaudePaths)
141+
for (const path of orphanedClaudeLinks) {
142+
stale.push(`${path.replace(`${ROOT}/`, '')} (orphaned)`)
143+
if (!check) await rm(path)
144+
}
145+
109146
for (const path of deprecatedTargets) {
110147
const current = await readFile(path, 'utf8').catch(() => null)
111148
if (current === null) continue

0 commit comments

Comments
 (0)