Guard the OpenCode legacy migration against symlinks and home-rooted repos

Both review bots caught real hazards in the migration: a symlinked
~/.opencode/skills (shared skill storage) would have its target emptied
through the link, and in a home-rooted repo that path is a live
project-scope install, not a stranded pre-#406 global copy. The
migration now requires a real directory (lstat), compares the
just-written dir by realpath instead of string, and skips entirely when
the home dir is itself a repo. Two regression tests cover the symlink
and dotfiles-repo shapes.

Prepared with AI assistance (Claude Code), directed by @pbakaus.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-25 19:03:32 -07:00
co-authored by Claude Code
parent caef4b8e4c
commit cda1c572d9
2 changed files with 60 additions and 1 deletions
+13 -1
View File
@@ -1162,9 +1162,21 @@ function copyProviderSkills(bundleDir, root, targets, { scope } = {}) {
// location OpenCode never reads. Now that the real copy sits in the
// config dir, drop exactly the skills just written from the stranded
// location; sibling skills and everything else in ~/.opencode stay.
// Guards (both flagged in review): a symlinked skills dir is shared
// storage whose target must not be emptied through the link, the
// just-written dir must be compared by realpath rather than string,
// and a home-rooted repo makes `.opencode/skills` a live
// project-scope install rather than a stranded global one.
if (scope === 'user' && provider === '.opencode') {
const legacyDir = join(root, '.opencode', 'skills');
if (legacyDir !== localSkillsDir && existsSync(legacyDir)) {
let migratable = false;
try {
migratable = existsSync(legacyDir)
&& !lstatSync(legacyDir).isSymbolicLink()
&& realpathSync(legacyDir) !== realpathSync(localSkillsDir)
&& !existsSync(join(root, '.git'));
} catch { migratable = false; }
if (migratable) {
for (const skill of readdirSync(srcDir, { withFileTypes: true })) {
if (!skill.isDirectory()) continue;
rmSync(join(legacyDir, skill.name), { recursive: true, force: true });