(opts: SyncOptions)
| 85 | // ─── Main ──────────────────────────────────────────────────────────────────── |
| 86 | |
| 87 | export async function syncPluginSkills(opts: SyncOptions): Promise<SyncResult> { |
| 88 | const skills = await findPackageSkills(opts.cwd); |
| 89 | |
| 90 | // Collision check. |
| 91 | for (const s of skills) { |
| 92 | if (RESERVED_LIFECYCLE_SLUGS.has(s.slug)) { |
| 93 | return { |
| 94 | exitCode: 2, |
| 95 | message: `package skill slug "${s.slug}" collides with reserved lifecycle slug. Rename the package skill.`, |
| 96 | changed: [], |
| 97 | orphans: [], |
| 98 | }; |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | const changed: string[] = []; |
| 103 | const orphans: string[] = []; |
| 104 | |
| 105 | for (const s of skills) { |
| 106 | const files = await listFilesRec(s.sourceDir); |
| 107 | for (const relPath of files) { |
| 108 | const srcPath = join(s.sourceDir, relPath); |
| 109 | const dstPath = join(s.mirrorDir, relPath); |
| 110 | const src = await readFile(srcPath); |
| 111 | |
| 112 | if (opts.mode === "check") { |
| 113 | if (!existsSync(dstPath)) { |
| 114 | changed.push(toPosix(join("skills", s.slug, relPath))); |
| 115 | continue; |
| 116 | } |
| 117 | const dst = await readFile(dstPath); |
| 118 | if (!src.equals(dst)) |
| 119 | changed.push(toPosix(join("skills", s.slug, relPath))); |
| 120 | } else { |
| 121 | await mkdir(dirname(dstPath), { recursive: true }); |
| 122 | await writeFile(dstPath, src); |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | // Detect orphan files — files in mirror that are not in source. |
| 127 | if (existsSync(s.mirrorDir)) { |
| 128 | const mirrorFiles = await listFilesRec(s.mirrorDir); |
| 129 | const sourceSet = new Set(files); |
| 130 | for (const mf of mirrorFiles) { |
| 131 | if (!sourceSet.has(mf)) |
| 132 | orphans.push(toPosix(join("skills", s.slug, mf))); |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | // Full-dir orphan scan — detect mirror skills directories whose source package |
| 138 | // was removed entirely. The main loop cannot catch these because it only |
| 139 | // iterates over currently discovered source skills. |
| 140 | const mirrorRoot = join(opts.cwd, "skills"); |
| 141 | if (existsSync(mirrorRoot)) { |
| 142 | const sourceSlugs = new Set(skills.map((s) => s.slug)); |
| 143 | const mirrorEntries = await readdir(mirrorRoot, { withFileTypes: true }); |
| 144 | for (const entry of mirrorEntries) { |
no test coverage detected
searching dependent graphs…