Repository navigation
Install skills and plugins from any git host - #279
Conversation
PR Summary by QodoInstall skills and plugins from any Git host
AI Description
Diagram
High-Level Assessment
Files changed (26)
|
Code Review by Qodo
1. Git installs can use the wrong repository
|
| return [u.host, ...path.split("/")] | ||
| .filter((s) => s && s !== "." && s !== "..") | ||
| .map((s) => s.replace(/[<>:"|?*\\]/g, "_")) | ||
| .join("/"); |
There was a problem hiding this comment.
1. Git installs can use the wrong repository 🐞 Bug ≡ Correctness
gitRepoKey() omits the URL scheme and normalizes distinct URL components into the same cache key. When two clone URLs share that key, ensureMirrorClone() keeps the first URL's existing mirror, so a later skill or plugin install can receive content from the first repository.
Agent Prompt
## Issue description
Distinct clone URLs can share a generic git cache key, causing installs to use an existing mirror for another URL.
## Fix Focus Areas
- src/shared/git-url.ts[54-65]
- src/shared/cache/mirror.ts[137-160]
## Recommended Fix
Include all source-distinguishing URL components in a safe cache identity and verify that an existing mirror's origin matches the requested clone URL before reusing it.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| ref, | ||
| pinnedSha, | ||
| noCache, | ||
| repoUrl, |
There was a problem hiding this comment.
2. Changed plugin urls keep old content 🐞 Bug ≡ Correctness
resolvePlugins() passes a prior plugin lock's SHA to snapshot resolution without checking the lock entry's url against the current repoUrl. If a git plugin's clone URL changes but retains the same normalized cache key, the pinned-snapshot fast path can install the old content and then record the new URL in the lock.
Agent Prompt
## Issue description
A git plugin can reuse a lock pin created for a different clone URL with the same cache key.
## Fix Focus Areas
- src/cli/commands/plugin-install.ts[393-424]
- src/shared/lockfile.ts[382-415]
- src/shared/lockfile.ts[423-449]
## Recommended Fix
Require the previous git lock entry's URL to match the current clone URL in both plugin lock lookup paths before passing its resolved SHA to snapshot resolution.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // github/gitlab use `repo`; git uses `url` (validatePluginDef checks which). | ||
| repo: z.string().optional(), | ||
| url: z.string().optional(), |
There was a problem hiding this comment.
5. Plugins with no repository disappear 🐞 Bug ☼ Reliability
pluginDefSchema now makes both def.repo and def.url optional, allowing a plugin with neither source through capabilities-file validation. When that entry reaches resolvePlugins(), pluginSourceOf() returns no source and the loop silently continues before validatePluginDef() can report the missing field.
Agent Prompt
## Issue description
Plugin definitions with no repository or clone URL now validate but are silently skipped during installation.
## Fix Focus Areas
- src/shared/capabilities.ts[217-248]
- src/cli/commands/plugin-install.ts[360-387]
## Recommended Fix
Validate the required source field according to plugin type in the schema, and let source-less entries reach the plugin validator so an actionable warning is emitted.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| lockBuilder.upsertSkill({ | ||
| id: skill.id, | ||
| source: 'git', | ||
| repo: repoPath, | ||
| url, |
There was a problem hiding this comment.
8. Failed git skills retain lock pins 🐞 Bug ☼ Reliability
The git branch calls lockBuilder.upsertSkill() before checking for SKILL.md or copying the skill. If either later step fails, the install task records the failure but lockfile writing retains the new git skill's ID and saves its pin, which a subsequent install can reuse.
Agent Prompt
## Issue description
A failed generic-git skill install can persist a lock pin for content that was never installed.
## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/install-one-skill.ts[265-290]
- src/cli/commands/install-tasks/install-skills.ts[38-65]
- src/cli/commands/install-tasks/write-lockfile.ts[15-40]
## Recommended Fix
Stage the git skill lock entry until its path validation and installation succeed, or remove that entry when installation fails; avoid pruning based solely on declared IDs.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| skillMarkdown = await response.text(); | ||
| // A web page is never a SKILL.md: usually a repository page or a clone URL | ||
| // missing its .git suffix. Installing it would hand the agent HTML. | ||
| if (/^s*<(!doctype|html)[s>]/i.test(skillMarkdown)) { |
There was a problem hiding this comment.
3. Remote skills that return a web page still install 🐞 Bug ≡ Correctness
installOneSkill() uses /^s*<(!doctype|html)[s>]/i, which matches literal s characters instead of whitespace. Standard <!DOCTYPE html> and <html lang="en"> responses, including ones with leading whitespace, bypass the check and continue through installation as SKILL.md while the install reports success.
Agent Prompt
## Issue description
The remote-skill HTML check uses literal `s` characters where whitespace matching is needed, allowing standard HTML responses to proceed through skill installation.
## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/install-one-skill.ts[312-320]
## Recommended Fix
Change the expression to `/^\s*<(!doctype|html)[\s>]/i`, or use `skillMarkdown.trimStart().slice(0,200).toLowerCase()` with `startsWith('<!doctype') || startsWith('<html')`, as in `src/server/skill-content.ts`. Add tests for ordinary HTML responses with and without leading whitespace, including `<!DOCTYPE html>\n<html lang="en">`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (errorMessage.includes('not found') || errorMessage.includes('does not appear to be a git repository')) { | ||
| return new Error(`Repository not found: ${repoUrl}\nCheck the URL, or sign in with git if it is private.`); | ||
| } |
There was a problem hiding this comment.
9. A missing tag is reported as a missing repo 🐞 Bug ◔ Observability
explainGenericGitError treats any message that contains 'not found' as a missing repository. The cache's own ref-resolution errors (Tag/branch "v2" not found, Commit … not found) contain that phrase, so a bad def.version or def.ref becomes 'Repository not found … sign in with git', and the real cause is dropped.
Agent Prompt
## Issue description
The generic git error mapper rewrites 'Tag/branch … not found' and 'Commit … not found' as 'Repository not found'.
## Fix Focus Areas
- src/cli/commands/install-tasks/helpers/git.ts[39-70]
## Recommended Fix
Before the 'not found' check, return the original error when the message starts with 'Tag/branch', 'Commit ' or 'Pinned commit'. Otherwise match only git transport text such as `repository '` + `' not found` or 'does not appear to be a git repository'.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
A clone URL ending in .git on a host other than github.com or gitlab.com used to fall through to the raw-URL skill type, which saved the host's HTML page as SKILL.md and reported success. Skills and plugins get a `git` type with `def.url` (the clone URL). It reuses the existing mirror cache, keyed by host and path, so pinning by tag or commit, "latest version tag" and the lockfile work as for GitHub. Credentials come from the user's git credential helper. `capa add` recognizes `https://host/path/repo.git[::path][:version|#sha]`, rules and hooks refuse such URLs until they support them, and a remote skill that returns a web page now fails instead of installing HTML. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2dbead7 to
c727cd0
Compare
- Detect HTML responses with a real check (the regex matched a literal s) so a web page is never installed as SKILL.md. - Key the generic git cache by host/path plus a hash of the whole URL, and refuse a cached mirror whose origin is a different URL. - Drop a previous plugin lock pin when its clone URL changed. - Validate that git plugins have def.url and other plugins def.repo, and let entries without a source reach validatePluginDef. - Count generic git repositories in cache stats and clean. - Serve uninstalled git skill content in the web UI. - Record a git skill's lock pin only after it is installed. - Report missing tags and commits as such, not as a missing repository. - Guard the web UI plugin list and preview against plugins without repo.
new URL accepts git helper schemes such as ext::<command>, which git can run. Git blocks ext by default, but capabilities files can come from elsewhere, so allow only https, http, ssh and file, and separate the clone URL from options with --. Also accept ssh:// URLs in capa add.
- Server skill preview only clones public https git URLs and sends no git-host token, matching the remote skill rule. - Server plugin expansion does the same for git plugins, except that a snapshot capa install already pinned is used offline. - Stored tokens are only attached to https URLs (http only on loopback), and git clone URLs accept plain http only for localhost. - Credentials embedded in a git URL are stripped before the lockfile, errors and printed output. - The server preview refuses a SKILL.md that is a symlink or resolves outside the repo snapshot.
…ource # Conflicts: # src/cli/commands/plugin-install.ts
Summary
capa addonly understood github.com and gitlab.com. A clone URL on any other host (Gitea, Forgejo, self-hosted servers, internal registries) fell through to the raw-URL skill type, so capa downloaded the host's HTML page, saved it asSKILL.mdand reported success. This adds agitsource type so any git host works for skills and plugins.What changed
type: gitfor skills (def: { url, path?, version?, ref? }) and plugins (def: { url, subpath?, version?, ref? }), in the schema, types and lockfile (source: gitplusurl).CachePlatformgainsgit, keyed by<host>/<path>(file://URLs by name plus a hash to stay under Windows path limits), with the clone URL threaded throughgetRepoSnapshot. Pinning by tag or commit, "newest vX.Y.Z tag" and lock reuse behave exactly as for GitHub.capa addrecognizeshttps://host/path/repo.git[::path][:version|#sha]for skills and--plugin. GitHub and GitLab URLs still parse as before.SKILL.mdat the repo root by default. When it's missing, the error lists the skills found in the repo.def.repo, and the duplicate check incapa add --plugincompareddef.repoonly..gitURLs with a clear message until they support them. Aremoteskill whose response is an HTML page now fails instead of installing it.capabilities-managercommand and schema references.Not included (follow-ups): auth integration for generic hosts (
capa auth, env tokens), rules/hooks/instructions from git hosts, registry adapters, web UI source badges.Review follow-ups:
looksLikeHtml); the original regex matched a literals, so a normal<!DOCTYPE html>page still installed asSKILL.md.<host>/<path>-<hash of the full URL>, and a cached mirror whose origin is a different URL is refused, so two URLs can never share content or a lock pin.def.url, other pluginsdef.repo; entries with no source reachvalidatePluginDefinstead of silently disappearing.capa cachestats and clean include generic git repos; the web UI serves uninstalled git skill content and no longer crashes on plugins withoutdef.repo.https,http,sshandfile(new URLalso accepts helper transports likeext::<command>), and the clone URL is separated from options with--.capa addalso acceptsssh://URLs.Note: this conflicts with #286 in
src/cli/commands/plugin-install.ts; whichever merges second needs a rebase.Security audit (Trail of Bits differential-review method) follow-ups:
remoteskills); an already pinned snapshot is reused offline.httpURLs except loopback, andhttpgit URLs are limited to localhost.def.urlare stripped before they reach the lockfile, errors or the install summary.SKILL.mdthat is a symlink or resolves outside the cached repo.version/refnames; considerhttp.followRedirects=falseand an empty credential helper for server-side clones.Screenshots / logs
Missing or private repo:
Test plan
capa addskill/plugin parsing and plugin validation (src/shared/__tests__/git-url.test.ts)file://: root skill at the newest tag with the URL in the lock, nesteddef.path, and the "skills found" error (install-one-skill-git.test.ts)capa add+capa installof a skill from a live non-GitHub HTTPS git host (pinned to itsv0.1.0tag), a plugin from a bare repo (its skill installed), and the error for a missing repomainlocallybunx tsc --noEmit,bunx tsc --noEmit -p web-ui/tsconfig.json,biome linton changed filesChecklist
bunx tsc --noEmitpasses🤖 Generated with Claude Code