feat(cli): multi-provider skill installation#21
Conversation
Add provider abstraction so `mpak skill install` and `mpak skill list` target the correct skills directory for Claude, Cursor, Copilot, Codex, Gemini, Goose, and OpenCode. Resolution priority: --provider flag > MPAK_PROVIDER env > config > auto-detect (single=use, zero=default claude, multiple=error). New commands: `mpak provider list|set|show` Fix: isValidProvider used `in` operator which traversed the prototype chain (constructor, toString returned true). Switched to Object.hasOwn.
There was a problem hiding this comment.
Review: feat(cli): multi-provider skill installation
Clean, well-structured work with thorough test coverage. The separation of concerns is good (providers.ts for resolution, config-manager for persistence, provider.ts for CLI handlers). The prototype-pollution fix (in → Object.hasOwn) is a real bug catch. A few items worth addressing before merge.
1. setProvider relies on mutable reference aliasing
File: packages/cli/src/utils/config-manager.ts
Severity: Low
setProvider(name: string): void {
const config = this.loadConfig();
config.provider = name;
this.saveConfig();
}loadConfig() returns the config object and caches it on this.config. Mutating the return value works because config and this.config are the same reference. If loadConfig() ever returned a copy (e.g., for immutability), this would silently break — saveConfig() would write the stale cached version without the new provider field.
Recommendation: Mutate this.config directly for clarity.
setProvider(name: string): void {
this.loadConfig();
this.config!.provider = name;
this.saveConfig();
}Worth noting: every other setter in the class uses the same implicit-alias pattern (setRegistryUrl, setPackageConfigValue, clearPackageConfig, clearPackageConfigValue). Probably worth changing all of them to this pattern as well.
2. Detection via parent directory may cause false positives
File: packages/cli/src/utils/providers.ts — detectProviders()
Severity: Medium
detectProviders checks for the existence of parent directories like ~/.cursor, ~/.config/agents, etc. On typical developer machines:
~/.claudeexists on any machine with Claude Code installed, even if the user doesn't want to target it for mpak.~/.config/agentsor~/.config/opencodemay exist for unrelated reasons.- Most providers' parent dirs (
~/.cursor,~/.copilot) are created by those tools for config/state regardless of skill support.
This means users with multiple coding tools installed will frequently hit the "Multiple providers detected" error and be forced to run mpak provider set before they can install anything.
Recommendation: Consider checking for the skills directory itself, or a tool-specific config file, rather than just the parent directory.
3. handleProviderShow test doesn't verify execution stops after process.exit
File: packages/cli/src/commands/provider.test.ts
Severity: Low
The handleProviderShow error test mocks process.exit as:
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);This mock returns silently, so the function continues executing after process.exit(1). The test asserts exitSpy was called but doesn't verify the function actually stopped. In contrast, the handleProviderSet test correctly uses a throwing mock:
vi.spyOn(process, "exit").mockImplementation(() => { throw exitError; });Recommendation: Make the handleProviderShow test use a throwing mock for consistency.
4. Copilot and Gemini skills directory paths may be incorrect
File: packages/cli/src/utils/providers.ts — PROVIDERS map
Severity: Medium
- Copilot: Mapped to
~/.copilot/skills. GitHub Copilot doesn't use~/.copilotas its config directory — it typically uses~/.config/github-copilotor VS Code's extension settings. - Gemini: Mapped to
~/.gemini/skills. Google's Gemini CLI uses~/.geminibut it's unclear whether it supports askills/subdirectory convention.
Recommendation: Verify these paths against actual tool documentation or behavior. If aspirational/placeholder, document that explicitly.
5. No --provider flag on skill search — rationale undocumented
File: packages/cli/src/program.ts
Severity: Informational
The test explicitly asserts skill search does NOT accept --provider, which makes sense since search hits the registry. But users may expect symmetry across all skill subcommands.
Recommendation: Add a brief code comment or CLI help text explaining why search doesn't accept --provider.
Minor / Nits
A. PROVIDER_PARENTS duplicates PROVIDERS logic — Both maps hardcode closely related paths. The parent could be derived from the skills dir using path.dirname(), reducing the risk of drift. The structural invariant test mitigates this, but DRY is still preferable.
B. setProvider validate-on-read design undocumented in storage layer — setProvider accepts arbitrary strings with no validation against known provider names. Validation is deferred to resolveProvider() on read — a reasonable design, documented in the test file ("should accept arbitrary strings"). However, config-manager.ts itself has no comment explaining why validation is absent. A one-liner on setProvider (e.g., // No validation — resolveProvider() validates on read) would help future contributors.
C. Empty string edge case is internal-only — The test "treats empty string explicit as no explicit" is good defensive coverage, but Commander will never pass "" for --provider (it requires a value). Worth a brief comment noting this exercises an internal API contract.
What's Good
- Prototype-pollution fix:
in→Object.hasOwnforisValidProvider—"constructor","toString"etc. would have returned true within. - Structural invariant tests: Verifying every provider in
PROVIDERShas a correspondingPROVIDER_PARENTSentry, and that all skills dirs end in/skillsunder$HOME— excellent for catching drift. - Per-layer validation tests: Each resolution layer (explicit, env, config) validates independently with source-specific error messages.
- Error UX: The ambiguity error suggests both
mpak provider setand--provideras remediation. - Resolution priority chain: The 4-level priority (flag > env > config > auto-detect) is well-designed and clearly documented.
JoeCardoso13
left a comment
There was a problem hiding this comment.
Requesting changes based on the findings in the review comment above.
…ection - Multiple detected providers now defaults to claude with a stderr warning instead of throwing, preserving backward compatibility - Goose detection uses ~/.config/goose instead of generic ~/.config/agents - Test mocks use vi.importActual to stay in sync with real provider list - Add test for empty MPAK_PROVIDER env var fallthrough
JoeCardoso13
left a comment
There was a problem hiding this comment.
APPROVE WITH COMMENTS
Well-structured provider abstraction with thorough test coverage. The Object.hasOwn fix, goose detection change, and ambiguous-detection-defaults-to-claude behavior are all solid. A few suggestions:
- Test coverage gap (non-blocking):
install.tsandlist.tsnow callresolveProvider(options.provider), but no tests verify the--provideroption flows end-to-end from CLI handler to resolution.program.test.tscovers flag registration andproviders.test.tscovers resolution in isolation, but the wiring between them isn't tested. Worth adding if you're doing another pass. - Observability (nitpick):
mpak provider listshows detected providers but not the active resolved provider.mpak provider showidentifies the provider but not which resolution tier selected it (env, config, auto-detect). Showing the source (e.g.,cursor (via MPAK_PROVIDER)) inshow, and anActive:footer inlist, would close the debugging loop. - Goose path divergence (nitpick): A brief comment in the
PROVIDERSmap noting that goose's skills dir (~/.config/agents/skills) intentionally differs from its detection parent (~/.config/goose) would help future maintainers.
Use ~/.config/goose/skills/ (goose-specific) instead of ~/.config/agents/skills/ (cross-agent portable) so the skills directory is consistent with the detection parent (~/.config/goose).
Add provider abstraction so
mpak skill installandmpak skill listtarget the correct skills directory for Claude, Cursor, Copilot, Codex, Gemini, Goose, and OpenCode.Resolution priority:
--providerflag >MPAK_PROVIDERenv > config > auto-detect (single=use, zero=default claude, multiple=default claude with warning).New commands:
mpak provider list|set|showFixes:
isValidProviderusedinoperator which traversed the prototype chain (constructor,toStringreturned true). Switched toObject.hasOwn.~/.config/agents(generic shared dir) to~/.config/goose(goose-specific) to avoid false positives.