Skip to content

fix(registries): resolve GitLab marketplace plugins - #314

Merged
Minitour merged 2 commits into
developfrom
fix/gitlab-marketplace-plugins-312
Oct 15, 2026
Merged

Minitour merged 2 commits into
developfrom
fix/gitlab-marketplace-plugins-312

Conversation

@Minitour

@Minitour Minitour commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Plugins from GitLab's marketplace (gitlab-org/ai/skills) failed with "Plugin subpath not found". There were two bugs; this PR fixes both. Closes #312.

What changed

Leading dot stripped twice

  • The marketplace lists ./.claude-plugin/plugins/glab. The parser stripped ./, then the path join stripped a leading . again, so capa looked for claude-plugin/plugins/glab.
  • Only a leading ./ (or a bare .) is stripped now. Fixed in the generic claude-marketplace parser and the bundled claude-plugins adapter, with its pin updated in bundled.ts.

Symlinked plugin skills

  • In that repo, each plugin's skills/<name> is a symlink to the repo's top-level skills/<name>.
  • Skill discovery skipped symlinked dirs, so the plugin had no skills and resolution failed with "No plugin manifest found".
  • The stable plugin copy used cpSync, which keeps the relative link, and the link breaks once copied.
  • Discovery (getSkillEntriesFromPath) now accepts symlinked skill dirs.
  • The stable plugin copy now goes through copySkillTree with the snapshot as the root:
    • Symlinks are replaced by what they point to when they resolve inside the cloned repo.
    • Links that point outside it are still rejected.
    • There is no per-file size cap, matching the old cpSync behavior.
  • copySkillTree decided file vs directory from lstat, so it silently dropped every symlink. It now follows contained links and skips links back to an enclosing directory, so a cycle can't recurse forever.

Review follow-ups

  • Link fan-out: links pointing at each other used to copy 2^N trees. The copy now walks the real tree first, then follows links, and enters each real directory once. A link to a directory that was already copied is skipped, and a real directory always lands at its own path.
  • Bad links: the plugin copy skips broken links, and links leaving the repository, with an install warning instead of failing the whole plugin.
  • Stamp: the stable-copy stamp is versioned (copy-v2), so plugins copied by the old cpSync path are copied again once.
  • Preview: the marketplace plugin preview checks the subpath with assertSafeRepoPath, like install does.

Notes

  • On Windows without Developer Mode, git checks symlinks out as plain text files, so plugins built from symlinks still can't be installed there. The reporter is on Linux.
  • Projects that already saved the stripped path (claude-plugin/plugins/...) in capabilities.yaml need to re-add the plugin or fix the path.

Test plan

  • Marketplace test: ./.claude-plugin/..., .agents/... and . map to the right subpaths.

  • copySkillTree test (junctions, so it also runs on Windows):

    • a symlinked dir inside root is copied;
    • the same copy without root is rejected;
    • a link cycle terminates.
  • Integration test of GitLab's layout:

    • detectAndParseManifest finds the linked skill;
    • ensureStablePluginCopy writes a real directory.

    The test fails without the fix.

  • Windows build, isolated HOME, real gitlab-org/ai/skills: the subpath now resolves. The symlinked skill couldn't be checked end to end on this machine, because Windows checks the links out as text files.

  • exec bits preserved by the new copy path on Linux (checked with a compiled Linux binary).

  • bunx tsc --noEmit. On Windows the full suite fails only on the usual EBUSY temp cleanup and on symlink creation without privilege.

Checklist

  • Tests added or updated
  • bunx tsc --noEmit passes
  • Docs updated (if user-facing): not needed

🤖 Generated with Claude Code

Two bugs kept plugins from gitlab-org/ai/skills from installing:

- "./.claude-plugin/plugins/glab" lost its leading dot. The parser
  stripped "./" and the path join stripped a leading "." again, so
  capa looked for "claude-plugin/plugins/glab". Only a leading "./"
  (or a bare ".") is stripped now, in the generic marketplace parser
  and the bundled claude-plugins adapter (pin updated).
- Each plugin's skills/<name> is a symlink to the repo's top-level
  skills/. Skill discovery skipped symlinked dirs, and the stable plugin
  copy kept the relative link, which broke once copied. Discovery now
  accepts symlinked skill dirs, and the plugin copy goes through
  copySkillTree, which dereferences symlinks that resolve inside the
  repo snapshot and still rejects ones that point outside it.
  copySkillTree also stops silently dropping symlinks and guards
  against link cycles.

Closes #312
Review follow-ups for the symlink support in this PR:

- copySkillTree walks the real tree first, then follows symlinks, and
  enters each real directory once per walk. The per-chain cycle guard
  still allowed links pointing at each other to copy 2^N trees (a hang
  and a full disk for capa install and the server's re-resolve); a link
  to a directory already copied is now skipped, and a real directory
  always lands at its own path.
- The plugin copy skips broken links and links leaving the repository
  with an install warning instead of failing the whole plugin, as cpSync
  used to copy them. Other copySkillTree callers still throw.
- The stable-copy stamp is versioned (copy-v2) so plugins copied by the
  old cpSync path, with broken relative links, are copied again once.
- The marketplace plugin preview checks the subpath with
  assertSafeRepoPath, like install does.
@Minitour
Minitour merged commit ea91842 into develop Oct 15, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant