2020 * bun run scripts/sync-skills.ts --check # fail (exit 1) if any projection is stale
2121 */
2222import { 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'
2424import { fileURLToPath } from 'node:url'
2525
2626const 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+
76106async 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