Add native skill installation pipeline#50
Merged
albertodebortoli merged 46 commits intomainfrom Apr 8, 2026
Merged
Conversation
- Add Constants.skillsFolder = 'skills' for project-local skills cache folder
- Add FileManagerWrapper.skillsCacheFolder property pointing to {CWD}/.luca/skills/
- Add skillsCacheFolder to FileManaging protocol
- Update FileManagerWrapperMock to implement skillsCacheFolder
Issue 1: Add DocC comment to skillsCacheFolder property in FileManaging protocol to document it as project-local cache. Issue 2: Update skillsFolder constant comment to clarify it's used for project-local .luca/skills/ directory, not home cache.
Introduces `AgentInfo` (id + projectSkillsPath) and `AgentRegistry` with a static list of 27 known AI coding agents, plus `agents(for:)` and `allAgentIds()` helpers. Includes a matching test suite (6 tests).
1. Make AgentInfo and AgentRegistry public by adding public keyword to struct, properties, and static methods 2. Change test_all_hasExpectedCount to exact equality (27 agents instead of >= 20) 3. Add test_allAgentIds_isSorted to verify alphabetical sorting 4. Add test_agents_forIds_hasCorrectPaths spot-check test for known paths All tests pass, build succeeds.
…tories Implements GitHubSkillTreeClient using the GitHub Git Trees API to list SKILL.md blob paths and raw.githubusercontent.com to download individual skill files, with full test coverage via DataDownloaderMock and a fixture-based tree response.
- Rename protocol to GitHubSkillTreeFetching (gerund -ing convention) - Extract protocol to its own file GitHubSkillTreeFetching.swift - Add @unchecked Sendable to GitHubSkillTreeClientMock - Add treeTruncated error case with guard in skillPaths(owner:repo:) - Remove redundant CodingKeys from GitHubTreeItem - Add test_skillPaths_treeTruncated with GitHubTreeTruncated.json fixture
Add SkillFrontmatterParser component that parses YAML frontmatter from SKILL.md files to extract the skill name. The parser handles various error cases including missing UTF-8 encoding, missing frontmatter delimiters, invalid YAML syntax, and missing name field. - New file: Sources/LucaCore/Core/SkillFrontmatterParser/SkillFrontmatterParser.swift - New test file: Tests/Core/SkillFrontmatterParserTests.swift - Error enum: SkillFrontmatterParserError with cases missingNameField and invalidFrontmatter - Uses Yams library for YAML parsing with proper error handling - 5 comprehensive tests covering all error paths and the success case - All 244 tests passing
Implements `SkillDownloading` protocol and `SkillDownloader` struct that parses GitHub repository references (shorthand and HTTPS URLs), fetches SKILL.md paths via `GitHubSkillTreeFetching`, optionally filters by name, and downloads skill content. Root `SKILL.md` names are resolved from YAML frontmatter.
- Promote GitHubSkillTreeClientError to top-level type (decouples SkillDownloader from concrete struct name) - Introduce SkillFrontmatterParsing protocol and inject it into SkillDownloader (replaces concrete type dependency) - Fix greedy .git removal in parseRepository to suffix-only strip - Fix default catch case to re-throw the original error instead of a misleading repositoryNotFound - Add tests for downloadFailed and frontmatter parse failure fallback to repo name
Introduces SkillSymLinker, which creates symbolic links for installed skills in each agent's project skills directory (e.g. .claude/skills/) pointing to the canonical .luca/skills/ cache. Includes protocol, implementation, file manager protocol, mock, and tests.
Implements Task 7 (InstalledSkillsLister) and Task 8 (SkillUninstaller): - InstalledSkillsLister lists installed skills from the project-local skills cache, returning a sorted list of subdirectory names under `.luca/skills/`, skipping non-directories and returning [] if the folder doesn't exist - SkillUninstaller removes a skill's cache folder and all agent symlinks, throwing SkillUninstallerError.skillNotFound when the skill isn't installed - Both components have narrow *FileManaging protocols registered in the FileManaging umbrella - Full test coverage: 3 tests for InstalledSkillsLister, 3 tests for SkillUninstaller - SkillUninstallerMock added for CLI-layer tests
…nfoFactory - Add .individual case to SkillInstallationType enum for ad-hoc skill installs - Implement handler in SkillsInfoFactory for .individual case - Add two new tests covering all-skills and filtered-skills scenarios
Adds SkillDownloader and SkillSymLinker DI slots to Installer, and an `experimental` parameter to `install(installationType:SkillInstallationType)`. When `experimental: true`, uses the native pipeline (download SKILL.md files, write to cache, create symlinks) instead of delegating to the npx-based SkillInstaller. Adds three tests covering both paths.
- Fix broken DocC symbol reference (remove invalid InstallationType link, update step 4 to mention native/npx paths, add separate Topics entries for tool and skill install methods) - Replace content.write(to:) with fileManager.createFile(atPath:contents:) to route through the FileManaging dependency; add createFile to FileManaging protocol, FileManagerWrapper, and FileManagerWrapperMock - Add agent-resolution assertions to experimental pipeline tests (spec test verifies resolved agent IDs, individual test verifies AgentRegistry.all is used when agents is nil)
…into Installer Adds a new method to GitIgnoreManager that appends .luca/skills/ and each agent's projectSkillsPath to .gitignore when in a git repo. The Installer now calls this at the end of the native (experimental) skills pipeline, and 5 new Swift Testing tests cover all branches.
Move ensureGitIgnoreIncludesSkillFolders out of the per-SkillSet install loop so it runs once after all skill sets are processed, update the GitIgnoreManager type-level DocC comment to reflect both tools and skills folders, and add a test for the no-trailing-newline branch.
Exposes the native skills pipeline behind --experimental in three CLI commands: install gains --experimental, --skill, and --agent flags; uninstall and installed gain --only-skills and --experimental flags to route through SkillUninstaller and InstalledSkillsLister respectively.
- Guard `.individual` skill branch behind `onlySkills` flag to prevent it from firing in `.all` mode - Throw `invalidCombinationOfArguments` in `validate()` when `--skill`/`--agent` are used without `--experimental` - Throw `ValidationError` in `UninstallCommand` and `InstalledCommand` when `--only-skills` is used without `--experimental` - Fix `// mARK: -` typo to `// MARK: -` in InstallCommand.swift
Covers five new error types: GitHubSkillTreeClientError, SkillFrontmatterParser.SkillFrontmatterParserError, SkillDownloader.SkillDownloaderError, SkillSymLinker.SkillSymLinkerError, and SkillUninstaller.SkillUninstallerError. All test methods follow the existing pattern and verify errorDescription is non-nil for each case.
- Move two orphaned @test functions inside SkillDownloaderTests struct - Delete unused SkillUninstallerMock (no protocol conformance, no consumers) - Refactor Installer to compute resolvedAgents once before loop in both installQuietly and installVerbose skill paths - Remove unnecessary FoundationNetworking import from GitHubSkillTreeFetching protocol file - Fix InstalledSkillsListerTests to use mock fileManager instead of FileManager.default
… downloads Mirrors Vercel Labs' skill exclusion list: metadata.json is always skipped; .git, __pycache__, and __pypackages__ directory contents are never downloaded.
Replace the hand-curated agent list with the canonical data from the Vercel Labs skills README: correct project paths for cursor, cline, continue, windsurf and others; add globalSkillsPath to AgentInfo; add 27 new agents; remove agents absent from the registry.
Skill repositories are now cloned with `git clone --quiet --depth 1` via the system git binary, so any authentication already configured on the host (SSH keys, SSH agents, credential helpers) works transparently. This makes private repositories and GitHub Enterprise Server instances accessible without any token configuration. - Add `GitRepositorySkillFetcher` (actor) as the new default fetcher; caches clones by URL and cleans up temp dirs on deinit - Introduce `SkillRepositoryFetching` protocol (replaces `GitHubSkillTreeFetching`) with `repository: String` params so the full original URL is passed through unchanged - Update `GitHubSkillTreeClient` to implement the new protocol, parsing the repository reference internally; kept as an alternative for API-based access - Add `gitNotFound` and `cloneFailed` error cases to `SkillDownloader` - Mark `SubprocessRunning` as `Sendable` for Swift 6 actor isolation - Remove GITHUB_TOKEN auth added in the previous session Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Set GIT_TERMINAL_PROMPT=0 when running git clone so that authentication failures produce an immediate error rather than hanging indefinitely on a credential prompt that will never be answered (stdin is /dev/null). Extends SubprocessRunning/SubprocessRunner with an environment parameter (merged on top of the inherited env) to support this without coupling GitRepositorySkillFetcher to process manipulation directly. Existing callers use the convenience overload and are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a Skills section covering Lucafile configuration, installing from a repository via the native pipeline, listing, uninstalling, and the --only-tools / --only-skills flags for mixed Lucafiles.
Install: infer tool (org/repo@version) vs skill (org/repo or URL) from the identifier format; --only-tools and --only-skills are now reserved for spec-based installs and cannot be combined with an identifier. Uninstall: try tool first; if no tool folder is found, fall back to skill uninstall automatically. --only-skills and --experimental flags are removed from the uninstall command.
- Replaces --experimental with --use-npx in the install command; native Swift pipeline now runs by default and --use-npx opts into Vercel Labs' npx-based skills tool - Renames luca installed --only-skills to luca installed --skills - Removes --only-skills from identifier-based install examples (invalid combination) - Shows 'No tool or skill named X was found.' when uninstalling an unknown name
… flags - Remove erroneous `!useNpx` guard that prevented `.individual` install type from being returned when `--use-npx` was passed with a repository identifier - Remove validation that incorrectly blocked `--skill`/`--agent` flags when combined with `--use-npx`; npx skills add supports these flags natively
The `--experimental` CLI flag was replaced by `--use-npx`, so the `experimental: Bool` parameter in `Installer` was an inverted alias that no longer matched the CLI semantics. Renamed to `useNpx: Bool` (default `false`) with matching logic: `true` routes to the npx pipeline, `false` (the default) uses the native pipeline.
test_installSkillsSpec_installsAllSkillSets was calling install without useNpx: true, so it routed through the native pipeline (SkillDownloader) instead of the npx path (SkillInstaller), causing a real network lookup.
10f9354 to
e82e219
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Cover uncovered code paths across 8 files (44 missing lines): - GitHubSkillTreeClient: SSH/HTTPS URL parsing, GitHub Enterprise Server URLs, non-HTTP responses, invalid formats - SkillDownloader: fileReadFailed from skillPaths, 429 rate limit, unexpected status code re-throw, non-unexpectedResponse re-throw, auxiliary file download failure - SkillFrontmatterParser: single delimiter, empty frontmatter, YAML list instead of dictionary - SkillSymLinker: directory creation and symlink creation error paths - SubprocessRunner: environment variable merging - GitRepositorySkillFetcher: HTTPS/HTTP URL passthrough, __pypackages__ directory exclusion - Installer: no-skills spec path, individual install with all agents
- SkillDownloader: cover SSH/HTTPS/HTTP error paths in extractRepoName, cover .git suffix stripping for HTTPS URLs, cover HTTP scheme branch - SkillUninstaller: add mock-based test that covers removeItem(atPath:) path for agent symlink cleanup (fixes broken-symlink issue on Linux)
Skills are installed into .luca/skills/ and symlinked into agent-specific directories, not installed directly into them.
Replace generic vercel-labs/agent-skills references with a more representative Swift skill example in help text and doc comments.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request Title
Add native skill installation pipeline
Description
This PR introduces a fully native skill installation pipeline in Luca, replacing the previous approach that relied on Vercel Labs'
npx skills addCLI tool. Skills can now be installed, symlinked, and uninstalled directly via Luca without requiring Node.js ornpxon the host.The native pipeline is now the default for skill operations. The original npx-based path is preserved and accessible via
--use-npxfor users who prefer it or need it for compatibility reasons.What is changing and why?
Core motivation: Luca's prior skill installation delegated all work to an external
npx skills addcommand, introducing a hard dependency on Node.js and an extra network round-trip to the npm registry. This PR replaces that with an implementation entirely within Luca that usesgit cloneunder the hood — leveraging whatever authentication is already configured on the host (SSH keys, credential helpers,.netrc) with no extra token setup.Key changes:
New components (
LucaCore)GitRepositorySkillFetchergit cloneinto a temp directory, enumerates skill file paths (excluding metadata/infra files), and cleans up ondeinit. Acts as aSkillRepositoryFetchingactor.GitHubSkillTreeClientSkillDownloaderSkillSet, returns[(name, [SkillFile])]. Handlesowner/repo, HTTPS, and SSH URL formats.SkillFrontmatterParsername:field fromSKILL.mdfiles to derive canonical skill names.SkillSymLinkerprojectSkillsPath.InstalledSkillsListerSkillUninstallerAgentRegistryprojectSkillsPathandglobalSkillsPath.SkillFileData.Changes to existing components
Installer: Extended withskillDownloaderandskillSymLinkerdependencies.install(installationType:)gains anexperimentalparameter (now defaults totrue= native pipeline).installQuietlynow wraps everything in anoora.progressStepfor cleaner quiet-mode output.GitIgnoreManager: NewensureGitIgnoreIncludesSkillFolders(agents:)method that appends agent skill folder patterns (e.g..claude/skills/,.cursor/skills/) to the project's.gitignoreso installed skills are not accidentally committed.SkillInstallationType: New.individual(repository:skills:agents:)case for installing skills directly from a repository reference without a Lucafile.SkillsInfoFactory: Updated to resolve the new.individualcase.SubprocessRunner/SubprocessRunning: AddedGIT_TERMINAL_PROMPT=0andGIT_ASKPASS=/usr/bin/falseenv vars to disable interactive git credential prompts during clone operations.CLI changes (
LucaCLI)install:identifierargument now acceptsorg/repo(skill) in addition toorg/repo@version(tool). Auto-detection: presence of@→ tool, absence → skill.--use-npxflag: opt in to the legacy Vercel Labs npx path.--skill <name>(repeatable): install only specific named skills from a repository.--agent <id>(repeatable): override the agents list from the Lucafile.--only-tools/--only-skillsnow document they apply to spec-based installs only.uninstall: Falls back toSkillUninstallerwhen the identifier contains no@version suffix and no tool with that name is found in the cache.installed: No breaking change; minor documentation update.Documentation
README.mdextended with a dedicated "Skills" section covering the native pipeline,--use-npxfallback, per-agent targeting, and.gitignoremanagement.Type of Change
How Has This Been Tested?
New test files (all using Swift Testing):
AgentRegistryTestsGitHubSkillTreeClientTestsGitRepositorySkillFetcherTestsSkillDownloaderTestsSkillFrontmatterParserTestsSkillSymLinkerTestsSkillUninstallerTestsInstalledSkillsListerTestsGitIgnoreManagerSkillsTestsErrorDescriptionTests.errorDescriptionon new error enumsExisting
InstallerTestsandSkillsInfoFactoryTestsextended to cover new code paths.Manual test:
luca install vercel-labs/agent-skills --skill find-skillsinstalls the skill into.claude/skills/find-skills/and updates.gitignore.Checklist
CI Considerations
Breaking Changes?
The native pipeline is additive. The
experimentalparameter onInstaller.install(installationType:experimental:)defaults totruebut is not part of the public ABI (internal use via CLI). The--use-npxflag preserves the previous behaviour for anyone relying on the npx path.Additional Notes
GitRepositorySkillFetcheris anactorto safely cache clone paths across concurrent calls.GIT_TERMINAL_PROMPT=0andGIT_ASKPASS=/usr/bin/falseare injected into every subprocess to avoid interactive prompts hanging in CI or headless environments.metadata.json,.git/,__pycache__/,__pypackages__/— mirroring what the Vercel Labs npx tool excludes.AgentRegistryis synced with the Vercel Labs agent-skills registry as of the branch cut date; it is a static table and will need periodic updates as new agents are added upstream.