diff --git a/.github/workflows/catalog-e2e.yml b/.github/workflows/catalog-e2e.yml new file mode 100644 index 00000000..add569e3 --- /dev/null +++ b/.github/workflows/catalog-e2e.yml @@ -0,0 +1,59 @@ +name: Catalog E2E + +on: + workflow_dispatch: + schedule: + - cron: "0 8 * * 1" + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + RUN_E2E: 1 + +jobs: + catalog-installation: + name: Verify catalog skill installation + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Checkout dallay skills repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + repository: dallay/agents-skills + path: vendor/agents-skills + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 + with: + toolchain: stable + + - name: Cache cargo registry + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-catalog-e2e-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-catalog-e2e- + ${{ runner.os }}-cargo-test- + + - name: Run catalog installation E2E + env: + AGENTSYNC_LOCAL_SKILLS_REPO: ${{ github.workspace }}/vendor/agents-skills + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ ! -d "$AGENTSYNC_LOCAL_SKILLS_REPO" ]; then + echo "Expected vendored skills repository at '$AGENTSYNC_LOCAL_SKILLS_REPO', but the directory does not exist." + exit 1 + fi + + cargo test --test test_catalog_integration -- --ignored --nocapture diff --git a/README.md b/README.md index 027743e9..4d325b2e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![CI](https://github.com/dallay/agentsync/actions/workflows/ci.yml/badge.svg)](https://github.com/dallay/agentsync/actions/workflows/ci.yml) [![Release](https://github.com/dallay/agentsync/actions/workflows/release.yml/badge.svg)](https://github.com/dallay/agentsync/actions/workflows/release.yml) +[![Catalog E2E](https://github.com/dallay/agentsync/actions/workflows/catalog-e2e.yml/badge.svg)](https://github.com/dallay/agentsync/actions/workflows/catalog-e2e.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![GitHub release](https://img.shields.io/github/v/release/dallay/agentsync)](https://github.com/dallay/agentsync/releases) [![Codecov](https://codecov.io/gh/dallay/agentsync/graph/badge.svg)](https://codecov.io/gh/dallay/agentsync) @@ -50,6 +51,28 @@ locations. - ⚡ **Fast** - Single static binary, no runtime dependencies - 🧩 **Curated skills** - Install from the [dallay/agents-skills](https://github.com/dallay/agents-skills) collection or external providers +### Catalog validation + +AgentSync ships a full catalog installation E2E check that validates every skill entry can still be +resolved, installed, and registered correctly. + +- GitHub Actions workflow: `Catalog E2E` +- Manual run: Actions → **Catalog E2E** → **Run workflow** +- Scheduled run: every Monday at 08:00 UTC +- Local run: + +```bash +git clone https://github.com/dallay/agents-skills ../agents-skills +export AGENTSYNC_LOCAL_SKILLS_REPO="$(pwd)/../agents-skills" +RUN_E2E=1 cargo test --test test_catalog_integration -- --ignored --nocapture +``` + +If you already keep `agents-skills` as a sibling checkout next to this repository, you can skip the +environment variable and let the test auto-discover that sibling path instead. + +This check is intentionally separate from normal CI because it depends on external networks and +third-party skill repositories. + ## Installation ### Node.js Package Managers (Recommended) diff --git a/src/commands/skill.rs b/src/commands/skill.rs index 2d5ae19f..f9e44282 100644 --- a/src/commands/skill.rs +++ b/src/commands/skill.rs @@ -1,5 +1,6 @@ use crate::commands::skill_fmt::{self, HumanFormatter, LabelKind, OutputMode}; -use agentsync::skills::provider::{Provider, SkillsShProvider}; +use agentsync::skills::catalog::EmbeddedSkillCatalog; +use agentsync::skills::provider::{Provider, SkillsShProvider, resolve_catalog_install_source}; use agentsync::skills::registry; use agentsync::skills::suggest::{ SuggestInstallJsonResponse, SuggestInstallMode, SuggestInstallPhase, @@ -924,6 +925,23 @@ impl Provider for SuggestInstallProvider { } fn resolve(&self, id: &str) -> Result { + let catalog = EmbeddedSkillCatalog::default(); + if let Some(definition) = catalog.get_skill_definition(id) { + let download_url = resolve_catalog_install_source( + &catalog, + &self.fallback, + &definition.provider_skill_id, + &definition.local_skill_id, + None, + )?; + + return Ok(agentsync::skills::provider::SkillInstallInfo { + download_url: download_url.clone(), + // Informational only today: install pipeline infers behavior from the source string. + format: infer_install_source_format(&download_url), + }); + } + if let Ok(source_root) = std::env::var("AGENTSYNC_TEST_SKILL_SOURCE_DIR") { // The id may be a qualified provider_skill_id (e.g., "dallay/agents-skills/foo") // or a simple local name. Extract the last segment to find the local source directory. @@ -1032,6 +1050,27 @@ fn resolve_source(skill_id: &str, source_arg: Option) -> Result // If it doesn't look like a URL or a path, try to resolve via skills.sh if !skill_id.contains("://") && !skill_id.starts_with('/') && !skill_id.starts_with('.') { + let catalog = EmbeddedSkillCatalog::default(); + if let Some(definition) = catalog.get_skill_definition_by_local_id(skill_id) { + let provider = SkillsShProvider; + return resolve_catalog_install_source( + &catalog, + &provider, + &definition.provider_skill_id, + &definition.local_skill_id, + None, + ) + .map_err(|e| { + tracing::warn!(skill_id = %skill_id, provider_skill_id = %definition.provider_skill_id, ?e, "Failed to resolve catalog skill via skills provider"); + anyhow::anyhow!( + "failed to resolve skill '{}' via provider '{}': {}", + skill_id, + definition.provider_skill_id, + e + ) + }); + } + let provider = SkillsShProvider; match provider.resolve(skill_id) { Ok(info) => Ok(info.download_url), @@ -1049,6 +1088,22 @@ fn resolve_source(skill_id: &str, source_arg: Option) -> Result } } +fn infer_install_source_format(source: &str) -> String { + if source.starts_with("http://") || source.starts_with("https://") { + if source.ends_with(".tar.gz") || source.ends_with(".tgz") { + return "tar.gz".to_string(); + } + + if source.ends_with(".zip") { + return "zip".to_string(); + } + + return "url".to_string(); + } + + "dir".to_string() +} + /// Attempts to convert a GitHub URL to a downloadable ZIP URL. /// /// Supports the following GitHub URL formats: diff --git a/src/skills/catalog.rs b/src/skills/catalog.rs index 8cc18009..e12a7c5e 100644 --- a/src/skills/catalog.rs +++ b/src/skills/catalog.rs @@ -117,7 +117,7 @@ const APPROVED_EMBEDDED_EXTERNAL_SKILL_IDS: &[&str] = &[ "mindrally/skills/deno-typescript", "mongodb/agent-skills", "neondatabase/agent-skills/neon-postgres", - "nodnarbnitram/claude-code-extensions/tauri-v2", + "delexw/claude-code-misc/tauri-v2", "nrwl/nx-ai-agents-config", "openai/skills", "openai/skills/cloudflare-deploy", @@ -172,6 +172,9 @@ pub struct CatalogSkillDefinition { pub local_skill_id: String, pub title: String, pub summary: String, + pub archive_subpath: Option, + pub legacy_local_skill_ids: Vec, + pub install_source: Option, } #[derive(Debug, Clone, PartialEq)] @@ -199,6 +202,7 @@ pub struct ResolvedSkillCatalog { source_name: String, metadata_version: String, skill_definitions: BTreeMap, + local_to_provider: BTreeMap, local_skills: BTreeMap, technologies: BTreeMap, combos: BTreeMap, @@ -221,6 +225,25 @@ impl ResolvedSkillCatalog { self.skill_definitions.get(provider_skill_id) } + pub fn get_archive_subpath(&self, provider_skill_id: &str) -> Option<&str> { + self.get_skill_definition(provider_skill_id) + .and_then(|definition| definition.archive_subpath.as_deref()) + } + + pub fn get_install_source(&self, provider_skill_id: &str) -> Option<&str> { + self.get_skill_definition(provider_skill_id) + .and_then(|definition| definition.install_source.as_deref()) + } + + pub fn get_skill_definition_by_local_id( + &self, + skill_id: &str, + ) -> Option<&CatalogSkillDefinition> { + self.local_to_provider + .get(skill_id) + .and_then(|provider_skill_id| self.skill_definitions.get(provider_skill_id)) + } + pub fn get_technology(&self, technology: &TechnologyId) -> Option<&CatalogTechnologyEntry> { self.technologies.get(technology) } @@ -288,6 +311,12 @@ struct RawCatalogSkill { local_skill_id: String, title: String, summary: String, + #[serde(default)] + archive_subpath: Option, + #[serde(default)] + legacy_local_skill_ids: Vec, + #[serde(default)] + install_source: Option, } #[derive(Debug, Clone, Deserialize)] @@ -327,6 +356,9 @@ impl From for RawCatalogDocument { local_skill_id: skill.local_skill_id, title: skill.title, summary: skill.summary, + archive_subpath: skill.archive_subpath, + legacy_local_skill_ids: skill.legacy_local_skill_ids, + install_source: skill.install_source, }) .collect(), technologies: metadata @@ -458,7 +490,10 @@ pub fn recommend_skills( let suggestion = suggestions .entry(definition.local_skill_id.clone()) - .or_insert_with(|| SkillSuggestion::new(&metadata, catalog)); + .or_insert_with(|| { + SkillSuggestion::new(&metadata, catalog) + .with_legacy_local_skill_ids(&definition.legacy_local_skill_ids) + }); suggestion.add_match(detection, &entry.reason_template); } @@ -495,7 +530,10 @@ pub fn recommend_skills( let suggestion = suggestions .entry(definition.local_skill_id.clone()) - .or_insert_with(|| SkillSuggestion::new(&metadata, catalog)); + .or_insert_with(|| { + SkillSuggestion::new(&metadata, catalog) + .with_legacy_local_skill_ids(&definition.legacy_local_skill_ids) + }); let reason = combo .reason_template @@ -588,6 +626,7 @@ fn normalize_catalog( source_name: source_name.to_string(), metadata_version: metadata_version.to_string(), skill_definitions, + local_to_provider: BTreeMap::new(), local_skills: BTreeMap::new(), technologies, combos, @@ -824,6 +863,9 @@ fn normalize_skill_definition(raw_skill: &RawCatalogSkill) -> Result Result<()> { let mut local_skills = BTreeMap::new(); + let mut local_to_provider = BTreeMap::new(); for definition in catalog.skill_definitions.values() { if local_skills.contains_key(&definition.local_skill_id) { @@ -938,8 +981,14 @@ fn rebuild_local_skill_index(catalog: &mut ResolvedSkillCatalog) -> Result<()> { summary: definition.summary.clone(), }, ); + + local_to_provider.insert( + definition.local_skill_id.clone(), + definition.provider_skill_id.clone(), + ); } + catalog.local_to_provider = local_to_provider; catalog.local_skills = local_skills; Ok(()) } diff --git a/src/skills/catalog.v1.toml b/src/skills/catalog.v1.toml index 6b422389..d1d5b4be 100644 --- a/src/skills/catalog.v1.toml +++ b/src/skills/catalog.v1.toml @@ -155,36 +155,35 @@ provider_skill_id = "angular/skills/angular-developer" local_skill_id = "angular-developer" title = "Angular Developer" summary = "Build Angular applications following official best practices and patterns." +install_source = "https://github.com/angular/skills/archive/HEAD.zip#angular-developer" [[skills]] provider_skill_id = "angular/angular/reference-core" local_skill_id = "angular-reference-core" title = "Angular Core Reference" summary = "Reference guide for Angular core APIs and architecture." +install_source = "https://github.com/angular/angular/archive/HEAD.zip#.agent/skills/reference-core" [[skills]] provider_skill_id = "angular/angular/reference-signal-forms" local_skill_id = "angular-reference-signal-forms" title = "Angular Signal Forms Reference" summary = "Reference guide for Angular signal-based forms." +install_source = "https://github.com/angular/angular/archive/HEAD.zip#.agent/skills/reference-signal-forms" [[skills]] provider_skill_id = "angular/angular/reference-compiler-cli" local_skill_id = "angular-reference-compiler-cli" title = "Angular Compiler CLI Reference" summary = "Reference guide for the Angular compiler CLI." +install_source = "https://github.com/angular/angular/archive/HEAD.zip#.agent/skills/reference-compiler-cli" [[skills]] provider_skill_id = "angular/angular/adev-writing-guide" local_skill_id = "angular-adev-writing-guide" title = "Angular Dev Writing Guide" summary = "Writing guide for Angular documentation contributions." - -[[skills]] -provider_skill_id = "angular/angular/PR Review" -local_skill_id = "angular-pr-review" -title = "Angular PR Review" -summary = "Guidelines for reviewing Angular pull requests." +install_source = "https://github.com/angular/angular/archive/HEAD.zip#.agent/skills/adev-writing-guide" # --- Astro (external) --- @@ -201,24 +200,21 @@ provider_skill_id = "microsoft/github-copilot-for-azure/azure-deploy" local_skill_id = "azure-deploy" title = "Azure Deploy" summary = "Deploy applications to Azure cloud services." +install_source = "https://github.com/microsoft/github-copilot-for-azure/archive/HEAD.zip#plugin/skills/azure-deploy" [[skills]] provider_skill_id = "microsoft/github-copilot-for-azure/azure-ai" local_skill_id = "azure-ai" title = "Azure AI" summary = "Build AI solutions with Azure AI services." - -[[skills]] -provider_skill_id = "microsoft/github-copilot-for-azure/azure-cost-optimization" -local_skill_id = "azure-cost-optimization" -title = "Azure Cost Optimization" -summary = "Optimize Azure resource costs and spending." +install_source = "https://github.com/microsoft/github-copilot-for-azure/archive/HEAD.zip#plugin/skills/azure-ai" [[skills]] provider_skill_id = "microsoft/github-copilot-for-azure/azure-diagnostics" local_skill_id = "azure-diagnostics" title = "Azure Diagnostics" summary = "Diagnose and troubleshoot Azure resource issues." +install_source = "https://github.com/microsoft/github-copilot-for-azure/archive/HEAD.zip#plugin/skills/azure-diagnostics" # --- Better Auth --- @@ -227,24 +223,28 @@ provider_skill_id = "better-auth/skills/better-auth-best-practices" local_skill_id = "better-auth-best-practices" title = "Better Auth Best Practices" summary = "Implement authentication with Better Auth following best practices." +install_source = "https://github.com/better-auth/skills/archive/HEAD.zip#better-auth/best-practices" [[skills]] provider_skill_id = "better-auth/skills/email-and-password-best-practices" local_skill_id = "better-auth-email-password" title = "Better Auth Email & Password" summary = "Implement email and password authentication with Better Auth." +install_source = "https://github.com/better-auth/skills/archive/HEAD.zip#better-auth/emailAndPassword" [[skills]] provider_skill_id = "better-auth/skills/organization-best-practices" local_skill_id = "better-auth-organization" title = "Better Auth Organizations" summary = "Implement organization management with Better Auth." +install_source = "https://github.com/better-auth/skills/archive/HEAD.zip#better-auth/organization" [[skills]] provider_skill_id = "better-auth/skills/two-factor-authentication-best-practices" local_skill_id = "better-auth-two-factor" title = "Better Auth Two-Factor" summary = "Implement two-factor authentication with Better Auth." +install_source = "https://github.com/better-auth/skills/archive/HEAD.zip#better-auth/twoFactor" # --- Clerk --- @@ -259,36 +259,42 @@ provider_skill_id = "clerk/skills/clerk-setup" local_skill_id = "clerk-setup" title = "Clerk Setup" summary = "Set up and configure Clerk authentication." +install_source = "https://github.com/clerk/skills/archive/HEAD.zip#skills/setup" [[skills]] provider_skill_id = "clerk/skills/clerk-custom-ui" local_skill_id = "clerk-custom-ui" title = "Clerk Custom UI" summary = "Build custom authentication UI with Clerk components." +install_source = "https://github.com/clerk/skills/archive/HEAD.zip#skills/custom-ui" [[skills]] provider_skill_id = "clerk/skills/clerk-nextjs-patterns" local_skill_id = "clerk-nextjs-patterns" title = "Clerk Next.js Patterns" summary = "Implement Clerk authentication patterns in Next.js apps." +install_source = "https://github.com/clerk/skills/archive/HEAD.zip#skills/nextjs-patterns" [[skills]] provider_skill_id = "clerk/skills/clerk-orgs" local_skill_id = "clerk-orgs" title = "Clerk Organizations" summary = "Implement organization management with Clerk." +install_source = "https://github.com/clerk/skills/archive/HEAD.zip#skills/orgs" [[skills]] provider_skill_id = "clerk/skills/clerk-webhooks" local_skill_id = "clerk-webhooks" title = "Clerk Webhooks" summary = "Handle Clerk webhooks for event-driven authentication flows." +install_source = "https://github.com/clerk/skills/archive/HEAD.zip#skills/webhooks" [[skills]] provider_skill_id = "clerk/skills/clerk-testing" local_skill_id = "clerk-testing" title = "Clerk Testing" summary = "Test Clerk authentication integrations." +install_source = "https://github.com/clerk/skills/archive/HEAD.zip#skills/testing" # --- Cloudflare --- @@ -321,6 +327,7 @@ provider_skill_id = "openai/skills/cloudflare-deploy" local_skill_id = "openai-cloudflare-deploy" title = "Cloudflare Deploy (OpenAI)" summary = "Deploy applications to Cloudflare Workers." +install_source = "https://github.com/openai/skills/archive/HEAD.zip#skills/.curated/cloudflare-deploy" [[skills]] provider_skill_id = "cloudflare/skills/durable-objects" @@ -389,15 +396,10 @@ provider_skill_id = "mindrally/skills/deno-typescript" local_skill_id = "deno-typescript" title = "Deno TypeScript" summary = "Write TypeScript applications for the Deno runtime." +install_source = "https://github.com/mindrally/skills/archive/HEAD.zip#deno-typescript" # --- Drizzle --- -[[skills]] -provider_skill_id = "bobmatnyc/claude-mpm-skills/drizzle-orm" -local_skill_id = "drizzle-orm" -title = "Drizzle ORM" -summary = "Use Drizzle ORM for type-safe database access." - # --- ElevenLabs --- [[skills]] @@ -405,12 +407,14 @@ provider_skill_id = "inferen-sh/skills/elevenlabs-tts" local_skill_id = "elevenlabs-tts" title = "ElevenLabs TTS" summary = "Integrate ElevenLabs text-to-speech APIs." +install_source = "https://github.com/inferen-sh/skills/archive/HEAD.zip#tools/audio/elevenlabs-tts" [[skills]] provider_skill_id = "inferen-sh/skills/elevenlabs-music" local_skill_id = "elevenlabs-music" title = "ElevenLabs Music" summary = "Generate music with ElevenLabs AI." +install_source = "https://github.com/inferen-sh/skills/archive/HEAD.zip#tools/audio/elevenlabs-music" # --- Expo --- @@ -419,54 +423,63 @@ provider_skill_id = "expo/skills/building-native-ui" local_skill_id = "expo-building-native-ui" title = "Expo Building Native UI" summary = "Build native UI components with Expo." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/building-native-ui" [[skills]] provider_skill_id = "expo/skills/native-data-fetching" local_skill_id = "expo-native-data-fetching" title = "Expo Native Data Fetching" summary = "Implement data fetching in Expo apps." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/native-data-fetching" [[skills]] provider_skill_id = "expo/skills/upgrading-expo" local_skill_id = "expo-upgrading" title = "Expo Upgrading" summary = "Upgrade Expo SDK versions safely." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/upgrading-expo" [[skills]] provider_skill_id = "expo/skills/expo-tailwind-setup" local_skill_id = "expo-tailwind-setup" title = "Expo Tailwind Setup" summary = "Set up Tailwind CSS in Expo projects." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/expo-tailwind-setup" [[skills]] provider_skill_id = "expo/skills/expo-dev-client" local_skill_id = "expo-dev-client" title = "Expo Dev Client" summary = "Configure and use Expo development client." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/expo-dev-client" [[skills]] provider_skill_id = "expo/skills/expo-deployment" local_skill_id = "expo-deployment" title = "Expo Deployment" summary = "Deploy Expo applications to app stores." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/expo-deployment" [[skills]] provider_skill_id = "expo/skills/expo-cicd-workflows" local_skill_id = "expo-cicd-workflows" title = "Expo CI/CD Workflows" summary = "Set up CI/CD pipelines for Expo projects." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/expo-cicd-workflows" [[skills]] provider_skill_id = "expo/skills/expo-api-routes" local_skill_id = "expo-api-routes" title = "Expo API Routes" summary = "Build API routes in Expo applications." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/expo-api-routes" [[skills]] provider_skill_id = "expo/skills/use-dom" local_skill_id = "expo-use-dom" title = "Expo use-dom" summary = "Use DOM components in Expo applications." +install_source = "https://github.com/expo/skills/archive/HEAD.zip#plugins/expo/skills/use-dom" # --- GSAP --- @@ -525,6 +538,7 @@ provider_skill_id = "yusukebe/hono-skill/hono" local_skill_id = "hono" title = "Hono" summary = "Build fast web applications with the Hono framework." +install_source = "https://github.com/yusukebe/hono-skill/archive/HEAD.zip#skills/hono" # --- Java --- @@ -533,12 +547,14 @@ provider_skill_id = "github/awesome-copilot/java-docs" local_skill_id = "java-docs" title = "Java Documentation" summary = "Write and maintain Java documentation." +install_source = "https://github.com/github/awesome-copilot/archive/HEAD.zip#skills/java-docs" [[skills]] provider_skill_id = "affaan-m/everything-claude-code/java-coding-standards" local_skill_id = "java-coding-standards" title = "Java Coding Standards" summary = "Follow Java coding standards and conventions." +install_source = "https://github.com/affaan-m/everything-claude-code/archive/HEAD.zip#skills/java-coding-standards" # --- Spring Boot --- @@ -547,6 +563,7 @@ provider_skill_id = "github/awesome-copilot/java-springboot" local_skill_id = "java-springboot" title = "Java Spring Boot" summary = "Build Spring Boot applications following best practices." +install_source = "https://github.com/github/awesome-copilot/archive/HEAD.zip#skills/java-springboot" # --- Android --- @@ -619,6 +636,7 @@ provider_skill_id = "kadajett/agent-nestjs-skills/nestjs-best-practices" local_skill_id = "nestjs-best-practices" title = "NestJS Best Practices" summary = "Build NestJS applications following best practices." +install_source = "https://github.com/kadajett/agent-nestjs-skills/archive/HEAD.zip" # --- Neon --- @@ -665,6 +683,7 @@ provider_skill_id = "delexw/claude-code-misc/oxlint" local_skill_id = "oxlint" title = "OxLint" summary = "Configure and use OxLint for fast JavaScript/TypeScript linting." +install_source = "https://github.com/delexw/claude-code-misc/archive/HEAD.zip#skills/oxlint" # --- Pinia --- @@ -681,6 +700,7 @@ provider_skill_id = "currents-dev/playwright-best-practices-skill/playwright-bes local_skill_id = "playwright-best-practices" title = "Playwright Best Practices" summary = "Write reliable end-to-end tests with Playwright." +archive_subpath = "" # --- Prisma --- @@ -689,24 +709,28 @@ provider_skill_id = "prisma/skills/prisma-database-setup" local_skill_id = "prisma-database-setup" title = "Prisma Database Setup" summary = "Set up databases with Prisma ORM." +install_source = "https://github.com/prisma/skills/archive/HEAD.zip#prisma-database-setup" [[skills]] provider_skill_id = "prisma/skills/prisma-client-api" local_skill_id = "prisma-client-api" title = "Prisma Client API" summary = "Use the Prisma Client API for type-safe database queries." +install_source = "https://github.com/prisma/skills/archive/HEAD.zip#prisma-client-api" [[skills]] provider_skill_id = "prisma/skills/prisma-cli" local_skill_id = "prisma-cli" title = "Prisma CLI" summary = "Use the Prisma CLI for database management." +install_source = "https://github.com/prisma/skills/archive/HEAD.zip#prisma-cli" [[skills]] provider_skill_id = "prisma/skills/prisma-postgres" local_skill_id = "prisma-postgres" title = "Prisma Postgres" summary = "Use Prisma with PostgreSQL databases." +install_source = "https://github.com/prisma/skills/archive/HEAD.zip#prisma-postgres" # --- React --- @@ -715,12 +739,14 @@ provider_skill_id = "vercel-labs/agent-skills/vercel-react-best-practices" local_skill_id = "vercel-react-best-practices" title = "React Best Practices" summary = "Build React applications following Vercel best practices." +install_source = "https://github.com/vercel-labs/agent-skills/archive/HEAD.zip#skills/react-best-practices" [[skills]] provider_skill_id = "vercel-labs/agent-skills/vercel-composition-patterns" local_skill_id = "vercel-composition-patterns" title = "React Composition Patterns" summary = "Use React composition patterns for scalable architectures." +install_source = "https://github.com/vercel-labs/agent-skills/archive/HEAD.zip#skills/composition-patterns" # --- React Native --- @@ -729,6 +755,7 @@ provider_skill_id = "sleekdotdesign/agent-skills/sleek-design-mobile-apps" local_skill_id = "sleek-design-mobile-apps" title = "Sleek Design Mobile Apps" summary = "Design and build polished React Native mobile applications." +install_source = "https://github.com/sleekdotdesign/agent-skills/archive/HEAD.zip#skills/design-mobile-apps" # --- Remotion --- @@ -737,29 +764,10 @@ provider_skill_id = "remotion-dev/skills/remotion-best-practices" local_skill_id = "remotion-best-practices" title = "Remotion Best Practices" summary = "Create programmatic videos with Remotion following best practices." +install_source = "https://github.com/remotion-dev/skills/archive/HEAD.zip#skills/remotion" # --- shadcn/ui --- -[[skills]] -provider_skill_id = "shadcn/ui/shadcn" -local_skill_id = "shadcn" -title = "shadcn/ui" -summary = "Build UIs with shadcn/ui components." - -# --- Stripe --- - -[[skills]] -provider_skill_id = "stripe/ai/stripe-best-practices" -local_skill_id = "stripe-best-practices" -title = "Stripe Best Practices" -summary = "Integrate Stripe payments following best practices." - -[[skills]] -provider_skill_id = "stripe/ai/upgrade-stripe" -local_skill_id = "stripe-upgrade" -title = "Stripe Upgrade" -summary = "Upgrade Stripe SDK versions safely." - # --- Supabase --- [[skills]] @@ -775,12 +783,14 @@ provider_skill_id = "ejirocodes/agent-skills/svelte5-best-practices" local_skill_id = "svelte5-best-practices" title = "Svelte 5 Best Practices" summary = "Build Svelte 5 applications following best practices." +install_source = "https://github.com/ejirocodes/agent-skills/archive/HEAD.zip#svelte/skills/svelte5-best-practices" [[skills]] provider_skill_id = "sveltejs/ai-tools/svelte-code-writer" local_skill_id = "svelte-code-writer" title = "Svelte Code Writer" summary = "Generate Svelte code with AI-assisted tooling." +install_source = "https://github.com/sveltejs/ai-tools/archive/HEAD.zip#packages/opencode/skills/svelte-code-writer" # --- SwiftUI --- @@ -792,38 +802,21 @@ summary = "Build SwiftUI applications with expert-level patterns." # --- Tailwind CSS --- -[[skills]] -provider_skill_id = "giuseppe-trisciuoglio/developer-kit/tailwind-css-patterns" -local_skill_id = "tailwind-css-patterns" -title = "Tailwind CSS Patterns" -summary = "Use Tailwind CSS utility patterns for efficient styling." - # --- Tauri --- [[skills]] -provider_skill_id = "nodnarbnitram/claude-code-extensions/tauri-v2" +provider_skill_id = "delexw/claude-code-misc/tauri-v2" local_skill_id = "tauri-v2" title = "Tauri v2" summary = "Build desktop applications with Tauri v2." +install_source = "https://github.com/delexw/claude-code-misc/archive/HEAD.zip#skills/tauri-v2" # --- Turborepo --- -[[skills]] -provider_skill_id = "vercel/turborepo/turborepo" -local_skill_id = "turborepo" -title = "Turborepo" -summary = "Manage monorepos with Turborepo build system." - # --- TypeScript --- # --- Vercel AI SDK --- -[[skills]] -provider_skill_id = "vercel/ai/ai-sdk" -local_skill_id = "vercel-ai-sdk" -title = "Vercel AI SDK" -summary = "Build AI-powered features with the Vercel AI SDK." - # --- Vercel Deploy --- [[skills]] @@ -864,9 +857,10 @@ summary = "Debug Vue applications effectively." [[skills]] provider_skill_id = "antfu/skills/vue" -local_skill_id = "antfu-vue" +local_skill_id = "vue" title = "Vue (antfu)" summary = "Build Vue applications with antfu's patterns and tooling." +legacy_local_skill_ids = ["antfu-vue"] [[skills]] provider_skill_id = "antfu/skills/vue-best-practices" @@ -931,18 +925,7 @@ provider_skill_id = "secondsky/claude-skills/tailwind-v4-shadcn" local_skill_id = "tailwind-v4-shadcn" title = "Tailwind v4 + shadcn/ui" summary = "Use Tailwind CSS v4 with shadcn/ui components." - -[[skills]] -provider_skill_id = "cloudflare/vinext/migrate-to-vinext" -local_skill_id = "cloudflare-migrate-to-vinext" -title = "Cloudflare Vite Migration" -summary = "Migrate to Cloudflare's Vite-based deployment." - -[[skills]] -provider_skill_id = "aj-geddes/useful-ai-prompts/nodejs-express-server" -local_skill_id = "nodejs-express-server" -title = "Node.js Express Server" -summary = "Build Express.js server applications." +install_source = "https://github.com/secondsky/claude-skills/archive/HEAD.zip#plugins/tailwind-v4-shadcn/skills/tailwind-v4-shadcn" [[skills]] provider_skill_id = "apollographql/skills/graphql-schema" @@ -967,12 +950,7 @@ provider_skill_id = "github/awesome-copilot/openapi-to-application-code" local_skill_id = "openapi-to-application-code" title = "OpenAPI to Application Code" summary = "Generate application code from OpenAPI/Swagger specifications." - -[[skills]] -provider_skill_id = "dotnet/skills" -local_skill_id = "dotnet-skills" -title = "C# / .NET Development" -summary = "Official .NET skills for C#, EF Core, ASP.NET, and more." +install_source = "https://github.com/github/awesome-copilot/archive/HEAD.zip#skills/openapi-to-application-code" [[skills]] provider_skill_id = "dallay/agents-skills/go-development" @@ -1016,12 +994,6 @@ local_skill_id = "rails-development" title = "Ruby on Rails" summary = "Rails conventions, ActiveRecord, migrations, testing with RSpec." -[[skills]] -provider_skill_id = "laravel/boost" -local_skill_id = "laravel-boost" -title = "Laravel Development" -summary = "Official Laravel skills for Livewire, Flux UI, and best practices." - [[skills]] provider_skill_id = "dallay/agents-skills/jest-testing" local_skill_id = "jest-testing" @@ -1052,30 +1024,6 @@ local_skill_id = "helm-charts" title = "Helm Charts" summary = "Helm chart development, templates, values, hooks, and best practices." -[[skills]] -provider_skill_id = "awslabs/agent-plugins" -local_skill_id = "aws-agent-plugins" -title = "AWS Development" -summary = "Official AWS Labs skills for Lambda, CDK, and cloud services." - -[[skills]] -provider_skill_id = "googlecloudplatform/devrel-demos" -local_skill_id = "gcp-devrel" -title = "Google Cloud Platform" -summary = "Official GCP skills for Cloud Run, agent architecture, and more." - -[[skills]] -provider_skill_id = "hashicorp/agent-skills" -local_skill_id = "hashicorp-terraform" -title = "Terraform" -summary = "Official HashiCorp Terraform skills for IaC, modules, testing, and providers." - -[[skills]] -provider_skill_id = "pulumi/agent-skills" -local_skill_id = "pulumi-iac" -title = "Pulumi IaC" -summary = "Official Pulumi skills for infrastructure as code across cloud providers." - [[skills]] provider_skill_id = "dallay/agents-skills/solidjs-development" local_skill_id = "solidjs-development" @@ -1094,18 +1042,6 @@ local_skill_id = "swift-ios-development" title = "Swift/iOS Development" summary = "Swift and iOS patterns, UIKit, SwiftUI, async/await, testing." -[[skills]] -provider_skill_id = "remix-run/agent-skills" -local_skill_id = "remix-agent-skills" -title = "Remix/React Router" -summary = "Official Remix skills for React Router framework, declarative, and data modes." - -[[skills]] -provider_skill_id = "flutter/skills" -local_skill_id = "flutter-skills" -title = "Flutter Development" -summary = "Official Flutter skills for layouts, state, animation, testing, and more." - [[skills]] provider_skill_id = "dallay/agents-skills/eslint-config" local_skill_id = "eslint-config" @@ -1130,54 +1066,6 @@ local_skill_id = "tensorflow-ml" title = "TensorFlow ML" summary = "TensorFlow/Keras model building, training, data pipelines, transfer learning." -[[skills]] -provider_skill_id = "nrwl/nx-ai-agents-config" -local_skill_id = "nx-workspace" -title = "Nx Workspace" -summary = "Official Nx skills for monorepo workspace management and CI." - -[[skills]] -provider_skill_id = "storybookjs/react-native" -local_skill_id = "storybook-rn" -title = "Storybook" -summary = "Official Storybook skills for component development and testing." - -[[skills]] -provider_skill_id = "mongodb/agent-skills" -local_skill_id = "mongodb-skills" -title = "MongoDB Development" -summary = "Official MongoDB skills for schema design, connections, and queries." - -[[skills]] -provider_skill_id = "redis/agent-skills" -local_skill_id = "redis-skills" -title = "Redis Development" -summary = "Official Redis skills for caching, data structures, and best practices." - -[[skills]] -provider_skill_id = "langchain-ai/langchain-skills" -local_skill_id = "langchain-skills" -title = "LangChain" -summary = "Official LangChain skills for AI agents, orchestration, and LangSmith." - -[[skills]] -provider_skill_id = "openai/skills" -local_skill_id = "openai-skills" -title = "OpenAI SDK" -summary = "Official OpenAI skills for API usage, assistants, and tools." - -[[skills]] -provider_skill_id = "pytorch/pytorch" -local_skill_id = "pytorch-skills" -title = "PyTorch" -summary = "Official PyTorch skills for ML development, PR review, and triaging." - -[[skills]] -provider_skill_id = "huggingface/skills" -local_skill_id = "huggingface-skills" -title = "Hugging Face" -summary = "Official Hugging Face skills for transformers, model training, and publishing." - # ============================================================================= # Technologies # ============================================================================= @@ -1242,16 +1130,6 @@ min_confidence = "medium" config_files = ["composer.json", "composer.lock"] file_extensions = [".php"] -[[technologies]] -id = "dotnet" -name = "C# / .NET" -skills = ["dotnet/skills"] -min_confidence = "medium" - -[technologies.detect] -config_files = ["global.json"] -file_extensions = [".cs", ".csproj", ".sln"] - [[technologies]] id = "elixir" name = "Elixir" @@ -1294,29 +1172,6 @@ config_files = ["Gemfile", "config/routes.rb", "Rakefile"] files = ["Gemfile"] patterns = ["rails"] -[[technologies]] -id = "laravel" -name = "Laravel" -skills = ["laravel/boost"] -min_confidence = "medium" - -[technologies.detect] -config_files = ["artisan"] - -[technologies.detect.config_file_content] -files = ["composer.json"] -patterns = ["laravel/framework"] - -[[technologies]] -id = "aspnet" -name = "ASP.NET" -skills = ["dotnet/skills"] -min_confidence = "medium" - -[technologies.detect.config_file_content] -files = ["*.csproj"] -patterns = ["Microsoft\\.AspNetCore"] - # --- Testing frameworks --- [[technologies]] @@ -1350,36 +1205,6 @@ packages = ["@testing-library/react", "@testing-library/vue", "@testing-library/ # --- Cloud/Infra --- -[[technologies]] -id = "aws" -name = "AWS" -skills = ["awslabs/agent-plugins"] -min_confidence = "medium" - -[technologies.detect] -package_patterns = ["^@aws-sdk/", "^aws-"] -config_files = ["samconfig.toml", "cdk.json", "serverless.yml", "template.yaml"] - -[[technologies]] -id = "gcp" -name = "Google Cloud Platform" -skills = ["googlecloudplatform/devrel-demos"] -min_confidence = "medium" - -[technologies.detect] -package_patterns = ["^@google-cloud/"] -config_files = ["app.yaml"] - -[[technologies]] -id = "terraform" -name = "Terraform" -skills = ["hashicorp/agent-skills"] -min_confidence = "medium" - -[technologies.detect] -config_files = ["terraform.tfvars", ".terraform.lock.hcl"] -file_extensions = [".tf"] - [[technologies]] id = "kubernetes" name = "Kubernetes" @@ -1398,16 +1223,6 @@ min_confidence = "medium" [technologies.detect] config_files = ["Chart.yaml"] -[[technologies]] -id = "pulumi" -name = "Pulumi" -skills = ["pulumi/agent-skills"] -min_confidence = "medium" - -[technologies.detect] -packages = ["@pulumi/pulumi", "@pulumi/aws", "@pulumi/azure", "@pulumi/gcp"] -config_files = ["Pulumi.yaml"] - # --- Additional frontend/mobile --- [[technologies]] @@ -1420,15 +1235,6 @@ min_confidence = "medium" packages = ["solid-js"] config_files = ["solid-start.config.ts", "solid-start.config.js"] -[[technologies]] -id = "remix" -name = "Remix" -skills = ["dallay/agents-skills/nothing-design", "remix-run/agent-skills"] -min_confidence = "medium" - -[technologies.detect] -packages = ["@remix-run/react", "@remix-run/node", "@remix-run/cloudflare"] - [[technologies]] id = "qwik" name = "Qwik" @@ -1438,19 +1244,6 @@ min_confidence = "medium" [technologies.detect] packages = ["@builder.io/qwik"] -[[technologies]] -id = "flutter" -name = "Flutter" -skills = ["flutter/skills"] -min_confidence = "medium" - -[technologies.detect] -config_files = ["pubspec.yaml"] - -[technologies.detect.config_file_content] -files = ["pubspec.yaml"] -patterns = ["flutter"] - [[technologies]] id = "swift_ios" name = "Swift/iOS" @@ -1462,26 +1255,6 @@ file_extensions = [".swift"] # --- Dev tooling --- -[[technologies]] -id = "nx" -name = "Nx" -skills = ["nrwl/nx-ai-agents-config"] -min_confidence = "medium" - -[technologies.detect] -packages = ["nx", "@nx/workspace"] -config_files = ["nx.json"] - -[[technologies]] -id = "storybook" -name = "Storybook" -skills = ["storybookjs/react-native"] -min_confidence = "medium" - -[technologies.detect] -packages = ["storybook", "@storybook/react", "@storybook/vue3", "@storybook/angular"] -config_files = [".storybook/"] - [[technologies]] id = "eslint" name = "ESLint" @@ -1514,24 +1287,6 @@ config_files = [".prettierrc", ".prettierrc.json", ".prettierrc.yml", ".prettier # --- Databases --- -[[technologies]] -id = "mongodb" -name = "MongoDB" -skills = ["mongodb/agent-skills"] -min_confidence = "medium" - -[technologies.detect] -packages = ["mongoose", "mongodb"] - -[[technologies]] -id = "redis" -name = "Redis" -skills = ["redis/agent-skills"] -min_confidence = "medium" - -[technologies.detect] -packages = ["redis", "ioredis", "@redis/client", "@upstash/redis"] - [[technologies]] id = "postgresql" name = "PostgreSQL" @@ -1543,24 +1298,6 @@ packages = ["pg", "postgres", "@vercel/postgres"] # --- AI/ML --- -[[technologies]] -id = "langchain" -name = "LangChain" -skills = ["langchain-ai/langchain-skills"] -min_confidence = "medium" - -[technologies.detect] -packages = ["langchain", "@langchain/core", "@langchain/openai", "@langchain/anthropic"] - -[[technologies]] -id = "openai_sdk" -name = "OpenAI SDK" -skills = ["openai/skills"] -min_confidence = "medium" - -[technologies.detect] -packages = ["openai"] - [[technologies]] id = "tensorflow" name = "TensorFlow" @@ -1570,24 +1307,6 @@ min_confidence = "medium" [technologies.detect] packages = ["@tensorflow/tfjs", "@tensorflow/tfjs-node"] -[[technologies]] -id = "pytorch" -name = "PyTorch" -skills = ["pytorch/pytorch"] -min_confidence = "medium" - -[technologies.detect] -file_extensions = [".pth"] - -[[technologies]] -id = "huggingface" -name = "Hugging Face" -skills = ["huggingface/skills"] -min_confidence = "medium" - -[technologies.detect] -packages = ["@huggingface/inference", "@huggingface/hub"] - [[technologies]] id = "astro" name = "Astro" @@ -1703,7 +1422,6 @@ skills = [ "angular/angular/reference-signal-forms", "angular/angular/reference-compiler-cli", "angular/angular/adev-writing-guide", - "angular/angular/PR Review", ] min_confidence = "medium" @@ -1711,25 +1429,6 @@ min_confidence = "medium" packages = ["@angular/core"] config_files = ["angular.json"] -[[technologies]] -id = "tailwind" -name = "Tailwind CSS" -skills = ["giuseppe-trisciuoglio/developer-kit/tailwind-css-patterns"] -min_confidence = "medium" - -[technologies.detect] -packages = ["tailwindcss", "@tailwindcss/vite"] -config_files = ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.cjs"] - -[[technologies]] -id = "shadcn" -name = "shadcn/ui" -skills = ["shadcn/ui/shadcn"] -min_confidence = "medium" - -[technologies.detect] -config_files = ["components.json"] - [[technologies]] id = "typescript" name = "TypeScript" @@ -1880,16 +1579,6 @@ min_confidence = "medium" [technologies.detect] packages = ["better-auth"] -[[technologies]] -id = "turborepo" -name = "Turborepo" -skills = ["vercel/turborepo/turborepo"] -min_confidence = "medium" - -[technologies.detect] -packages = ["turbo"] -config_files = ["turbo.json"] - [[technologies]] id = "vite" name = "Vite" @@ -1906,7 +1595,6 @@ name = "Azure" skills = [ "microsoft/github-copilot-for-azure/azure-deploy", "microsoft/github-copilot-for-azure/azure-ai", - "microsoft/github-copilot-for-azure/azure-cost-optimization", "microsoft/github-copilot-for-azure/azure-diagnostics", ] min_confidence = "medium" @@ -1914,15 +1602,6 @@ min_confidence = "medium" [technologies.detect] package_patterns = ["^@azure/"] -[[technologies]] -id = "vercel_ai" -name = "Vercel AI SDK" -skills = ["vercel/ai/ai-sdk"] -min_confidence = "medium" - -[technologies.detect] -packages = ["ai", "@ai-sdk/openai", "@ai-sdk/anthropic", "@ai-sdk/google"] - [[technologies]] id = "elevenlabs" name = "ElevenLabs" @@ -2114,15 +1793,6 @@ min_confidence = "medium" [technologies.detect] packages = ["prisma", "@prisma/client"] -[[technologies]] -id = "stripe" -name = "Stripe" -skills = ["stripe/ai/stripe-best-practices", "stripe/ai/upgrade-stripe"] -min_confidence = "medium" - -[technologies.detect] -packages = ["stripe", "@stripe/stripe-js", "@stripe/react-stripe-js"] - [[technologies]] id = "hono" name = "Hono" @@ -2142,15 +1812,6 @@ min_confidence = "medium" packages = ["vitest"] config_files = ["vitest.config.ts", "vitest.config.js", "vitest.config.mts"] -[[technologies]] -id = "drizzle" -name = "Drizzle" -skills = ["bobmatnyc/claude-mpm-skills/drizzle-orm"] -min_confidence = "medium" - -[technologies.detect] -packages = ["drizzle-orm", "drizzle-kit"] - [[technologies]] id = "nestjs" name = "NestJS" @@ -2163,7 +1824,7 @@ packages = ["@nestjs/core"] [[technologies]] id = "tauri" name = "Tauri" -skills = ["nodnarbnitram/claude-code-extensions/tauri-v2"] +skills = ["delexw/claude-code-misc/tauri-v2"] min_confidence = "medium" [technologies.detect] @@ -2277,13 +1938,6 @@ requires = ["react_native", "expo"] skills = ["expo/skills/building-native-ui", "sleekdotdesign/agent-skills/sleek-design-mobile-apps"] enabled = true -[[combos]] -id = "nextjs-vercel-ai" -name = "Next.js + Vercel AI SDK" -requires = ["nextjs", "vercel_ai"] -skills = ["vercel/ai/ai-sdk", "vercel-labs/next-skills/next-best-practices"] -enabled = true - [[combos]] id = "nextjs-playwright" name = "Next.js + Playwright" @@ -2291,20 +1945,6 @@ requires = ["nextjs", "playwright"] skills = ["currents-dev/playwright-best-practices-skill/playwright-best-practices"] enabled = true -[[combos]] -id = "react-shadcn" -name = "React + shadcn/ui" -requires = ["react", "shadcn"] -skills = ["shadcn/ui/shadcn", "vercel-labs/agent-skills/vercel-react-best-practices"] -enabled = true - -[[combos]] -id = "tailwind-shadcn" -name = "Tailwind CSS + shadcn/ui" -requires = ["tailwind", "shadcn"] -skills = ["secondsky/claude-skills/tailwind-v4-shadcn"] -enabled = true - [[combos]] id = "gsap-react" name = "GSAP + React" @@ -2312,20 +1952,6 @@ requires = ["gsap", "react"] skills = ["greensock/gsap-skills/gsap-react"] enabled = true -[[combos]] -id = "cloudflare-vite" -name = "Cloudflare + Vite" -requires = ["cloudflare", "vite"] -skills = ["cloudflare/vinext/migrate-to-vinext"] -enabled = true - -[[combos]] -id = "node-express" -name = "Node.js + Express" -requires = ["node", "express"] -skills = ["aj-geddes/useful-ai-prompts/nodejs-express-server"] -enabled = true - [[combos]] id = "nextjs-clerk" name = "Next.js + Clerk" diff --git a/src/skills/provider.rs b/src/skills/provider.rs index 32874ec9..00e7bdc8 100644 --- a/src/skills/provider.rs +++ b/src/skills/provider.rs @@ -1,5 +1,8 @@ use anyhow::Result; use serde::Deserialize; +use std::path::{Path, PathBuf}; + +use crate::skills::catalog::EmbeddedSkillCatalog; /// Provider trait for resolving skills pub trait Provider { @@ -41,6 +44,12 @@ pub struct ProviderCatalogSkill { pub local_skill_id: String, pub title: String, pub summary: String, + #[serde(default)] + pub archive_subpath: Option, + #[serde(default)] + pub legacy_local_skill_ids: Vec, + #[serde(default)] + pub install_source: Option, } #[derive(Debug, Clone, Deserialize)] @@ -81,9 +90,65 @@ struct SearchSkill { pub struct SkillsShProvider; +pub const DALLAY_AGENTS_SKILLS_PREFIX: &str = "dallay/agents-skills/"; + /// Well-known repo names where skills live in a `skills/` subdirectory. const SKILLS_REPO_NAMES: &[&str] = &["skills", "agent-skills", "agentic-skills", "agents-skills"]; +fn repo_uses_skills_subdirectory(repo: &str) -> bool { + SKILLS_REPO_NAMES.contains(&repo) + || repo.ends_with("-skills") + || repo.ends_with("-agent-skills") + || repo.ends_with("-agentic-skills") + || repo.ends_with("-agents-skills") +} + +fn local_catalog_skill_source_dir( + local_skill_id: &str, + project_root: Option<&Path>, +) -> Option { + if let Ok(path) = std::env::var("AGENTSYNC_TEST_SKILL_SOURCE_DIR") { + let candidate = PathBuf::from(path).join(local_skill_id); + if candidate.exists() { + return Some(candidate); + } + } + + if let Ok(path) = std::env::var("AGENTSYNC_LOCAL_SKILLS_REPO") { + return Some(PathBuf::from(path).join("skills").join(local_skill_id)); + } + + project_root + .and_then(Path::parent) + .map(|parent| { + parent + .join("agents-skills") + .join("skills") + .join(local_skill_id) + }) + .filter(|path| path.exists()) +} + +pub fn resolve_catalog_install_source( + catalog: &EmbeddedSkillCatalog, + provider: &dyn Provider, + provider_skill_id: &str, + local_skill_id: &str, + project_root: Option<&Path>, +) -> Result { + if let Some(install_source) = catalog.get_install_source(provider_skill_id) { + return Ok(install_source.to_string()); + } + + if provider_skill_id.starts_with(DALLAY_AGENTS_SKILLS_PREFIX) + && let Some(path) = local_catalog_skill_source_dir(local_skill_id, project_root) + { + return Ok(path.to_string_lossy().into_owned()); + } + + Ok(provider.resolve(provider_skill_id)?.download_url) +} + impl SkillsShProvider { /// Resolve a catalog-style `owner/repo/skill-name` ID deterministically by /// constructing the GitHub download URL directly — no network call needed. @@ -105,16 +170,23 @@ impl SkillsShProvider { anyhow::bail!("invalid skill id (empty component): {}", id); } - // Construct the subpath fragment for the archive unpacker. - // For repos named "skills", "agent-skills", etc., the skill typically - // lives under a `skills/` directory inside the repo. - let subpath = if SKILLS_REPO_NAMES.contains(&repo) { - format!("skills/{}", skill_name) - } else { - skill_name.to_string() - }; + let embedded_catalog = EmbeddedSkillCatalog::default(); + let subpath = embedded_catalog + .get_archive_subpath(id) + .map(str::to_string) + .unwrap_or_else(|| { + if repo_uses_skills_subdirectory(repo) { + format!("skills/{skill_name}") + } else { + skill_name.to_string() + } + }); - let final_url = format!("https://github.com/{owner}/{repo}/archive/HEAD.zip#{subpath}"); + let mut final_url = format!("https://github.com/{owner}/{repo}/archive/HEAD.zip"); + if !subpath.is_empty() { + final_url.push('#'); + final_url.push_str(&subpath); + } Ok(SkillInstallInfo { download_url: final_url, @@ -158,7 +230,7 @@ impl SkillsShProvider { // If the repo name is a well-known skills repo, prefix 'skills/' let final_subpath = if !subpath.is_empty() && !subpath.starts_with("skills/") { let repo_name = skill.source.split('/').next_back().unwrap_or(""); - if SKILLS_REPO_NAMES.contains(&repo_name) { + if repo_uses_skills_subdirectory(repo_name) { format!("skills/{}", subpath) } else { subpath diff --git a/src/skills/suggest.rs b/src/skills/suggest.rs index bd443db8..0d092e68 100644 --- a/src/skills/suggest.rs +++ b/src/skills/suggest.rs @@ -94,6 +94,7 @@ pub struct SkillSuggestion { pub provider_skill_id: String, pub title: String, pub summary: String, + legacy_local_skill_ids: Vec, pub reasons: Vec, pub matched_technologies: Vec, pub installed: bool, @@ -108,6 +109,7 @@ impl SkillSuggestion { provider_skill_id: metadata.provider_skill_id.clone(), title: metadata.title.clone(), summary: metadata.summary.clone(), + legacy_local_skill_ids: Vec::new(), reasons: Vec::new(), matched_technologies: Vec::new(), installed: false, @@ -148,6 +150,11 @@ impl SkillSuggestion { let _ = combo_name; } + pub(crate) fn with_legacy_local_skill_ids(mut self, legacy_local_skill_ids: &[String]) -> Self { + self.legacy_local_skill_ids = legacy_local_skill_ids.to_vec(); + self + } + pub fn annotate_installed_state(&mut self, installed_skill: Option<&InstalledSkillState>) { if let Some(installed_skill) = installed_skill { self.installed = installed_skill.installed; @@ -159,6 +166,20 @@ impl SkillSuggestion { } } +fn installed_state_for_recommendation<'a>( + installed_skill_states: &'a BTreeMap, + recommendation: &SkillSuggestion, +) -> Option<&'a InstalledSkillState> { + std::iter::once(recommendation.skill_id.as_str()) + .chain( + recommendation + .legacy_local_skill_ids + .iter() + .map(std::string::String::as_str), + ) + .find_map(|skill_id| installed_skill_states.get(skill_id)) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SuggestSummary { pub detected_count: usize, @@ -307,8 +328,10 @@ impl SuggestionService { let mut recommendations = recommend_skills(catalog, &detections); for recommendation in &mut recommendations { - recommendation - .annotate_installed_state(installed_skill_states.get(&recommendation.skill_id)); + recommendation.annotate_installed_state(installed_state_for_recommendation( + &installed_skill_states, + recommendation, + )); } let summary = SuggestSummary { @@ -442,8 +465,7 @@ impl SuggestionService { .get(skill_id.as_str()) .expect("skill_id should be in recommendation_map - this is a bug"); - if installed_state - .get(&recommendation.skill_id) + if installed_state_for_recommendation(&installed_state, recommendation) .is_some_and(|state| state.installed) { reporter.on_event(SuggestInstallProgressEvent::SkippedAlreadyInstalled { diff --git a/tests/integration/skill_suggest.rs b/tests/integration/skill_suggest.rs index c850cf2e..ae868165 100644 --- a/tests/integration/skill_suggest.rs +++ b/tests/integration/skill_suggest.rs @@ -438,6 +438,9 @@ impl Provider for CanonicalOverlayProvider { local_skill_id: "custom-rust".to_string(), title: "Custom Rust".to_string(), summary: "Custom Rust guidance".to_string(), + archive_subpath: None, + legacy_local_skill_ids: Vec::new(), + install_source: None, }], technologies: vec![ProviderCatalogTechnology { id: "rust".to_string(), diff --git a/tests/test_catalog_integration.rs b/tests/test_catalog_integration.rs index 5abd1d86..c58e22fc 100644 --- a/tests/test_catalog_integration.rs +++ b/tests/test_catalog_integration.rs @@ -1,324 +1,131 @@ -//! Integration tests for skill catalog entries. +//! End-to-end catalog installation verification. //! -//! These tests verify that skills from the embedded catalog can be installed. -//! By default, only a subset is tested to keep CI fast. Set environment variables to run more: -//! -//! - `E2E_RUN_ALL_CATALOG_SKILLS=1` - Run all skills (slow, requires network) -//! - `E2E_CATALOG_SKILL_LIMIT=N` - Run only first N skills -//! -//! Skills are tested against the skills.sh provider (external provider). -//! Local dallay skills are tested by installing from the local `.agents/skills/` directory. - -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; +//! This suite is intentionally opt-in because it exercises every catalog entry, +//! including external providers that depend on network availability and third-party +//! repositories staying valid. + +use agentsync::skills::catalog::EmbeddedSkillCatalog; +use agentsync::skills::install::blocking_fetch_and_install_skill; +use agentsync::skills::provider::{SkillsShProvider, resolve_catalog_install_source}; +use agentsync::skills::registry::read_registry; +use std::path::Path; +use std::thread; +use std::time::Duration; use tempfile::TempDir; -/// Represents a skill from the catalog to test. -#[derive(Debug, Clone)] -struct CatalogSkill { - /// The provider skill ID (e.g., "dallay/agents-skills/accessibility") - provider_skill_id: String, - /// The local skill ID (e.g., "accessibility") - local_skill_id: String, - /// Whether this skill is from dallay (local) or external - is_local: bool, -} - -/// TOML structure for deserializing catalog entries -#[derive(Debug, serde::Deserialize)] -struct CatalogFile { - skills: Vec, -} - -#[derive(Debug, serde::Deserialize)] -struct CatalogEntry { - provider_skill_id: String, - local_skill_id: String, -} - -/// Parse the catalog.v1.toml and extract skills to test. -/// Uses proper TOML deserialization. -fn extract_catalog_skills() -> Vec { - let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - - let catalog_path = if manifest_dir.join("src").exists() { - manifest_dir - .join("src") - .join("skills") - .join("catalog.v1.toml") - } else { - std::env::current_dir() - .unwrap_or_else(|_| PathBuf::from(".")) - .join("src") - .join("skills") - .join("catalog.v1.toml") - }; - - let content = fs::read_to_string(&catalog_path).expect("Failed to read catalog.v1.toml"); - - // Parse using toml - let catalog: CatalogFile = toml::from_str(&content).expect("Failed to parse catalog.v1.toml"); - - catalog - .skills - .into_iter() - .map(|entry| { - let is_local = entry.provider_skill_id.starts_with("dallay/"); - CatalogSkill { - provider_skill_id: entry.provider_skill_id, - local_skill_id: entry.local_skill_id, - is_local, - } - }) - .collect() -} - -/// Determine if we should run this test based on environment variables. -fn should_run_skill_test(skill_index: usize, _total_skills: usize) -> bool { - // Check if we should run all - if std::env::var("E2E_RUN_ALL_CATALOG_SKILLS").is_ok() { - return true; - } - - // Check if we have a limit - if let Ok(limit) = std::env::var("E2E_CATALOG_SKILL_LIMIT") - && let Ok(n) = limit.parse::() - { - return skill_index < n; - } - - // Default: run only first 5 skills for quick sanity check - skill_index < 5 -} - -/// Initialize a temporary project with agentsync. -/// Returns Ok(()) on success, or an error string on failure. -fn init_temp_project(root: &Path) -> Result<(), String> { - let output = Command::new("cargo") - .args(["run", "--", "init", "--path"]) - .arg(root) - .output() - .map_err(|e| format!("failed to execute init: {}", e))?; - - if output.status.success() { - Ok(()) - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - Err(format!("init failed: {}", stderr)) - } +fn project_root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) } -/// Install a skill in the given project. -/// Returns the command output, or an error string on failure. -fn install_skill(root: &Path, skill_id: &str, source: Option<&str>) -> Result { - let mut cmd = Command::new("cargo"); - cmd.args(["run", "--", "skill"]); - - if let Some(source_path) = source { - cmd.args(["install", skill_id, "--source", source_path]); - } else { - cmd.args(["install", skill_id]); - } - - cmd.arg("--project-root").arg(root); - - cmd.output() - .map_err(|e| format!("failed to execute install: {}", e)) +fn resolve_install_source( + provider: &SkillsShProvider, + provider_skill_id: &str, + local_skill_id: &str, +) -> anyhow::Result { + let catalog = EmbeddedSkillCatalog::default(); + resolve_catalog_install_source( + &catalog, + provider, + provider_skill_id, + local_skill_id, + Some(project_root()), + ) } -/// Verify a skill is installed in the given project. -fn verify_skill_installed(root: &Path, skill_id: &str) -> bool { - let skill_dir = root.join(".agents/skills").join(skill_id); - skill_dir.exists() && skill_dir.join("SKILL.md").exists() -} - -/// Test installation of a skill from the catalog. -/// This test is parameterized - can run many skills. -#[test] -fn test_install_skill_from_catalog() { - let skills = extract_catalog_skills(); - - // Filter skills to test based on environment - let skills_to_test: Vec<_> = skills - .iter() - .enumerate() - .filter(|(idx, _)| should_run_skill_test(*idx, skills.len())) - .collect(); - - println!( - "Testing {} out of {} skills from catalog", - skills_to_test.len(), - skills.len() - ); - - for (idx, (_, skill)) in skills_to_test.into_iter().enumerate() { - println!( - "[{}/{}] Testing skill: {} ({})", - idx + 1, - skills.len(), - skill.local_skill_id, - skill.provider_skill_id - ); - - let temp = TempDir::new().unwrap(); - let root = temp.path(); - - // 1. Init agentsync - if let Err(e) = init_temp_project(root) { - eprintln!(" ⚠️ Init failed: {}, skipping skill", e); - continue; - } - - // 2. Try to install the skill - let result = install_skill(root, &skill.local_skill_id, None); - - match result { - Ok(output) => { - if output.status.success() { - // 3. Verify installation - if verify_skill_installed(root, &skill.local_skill_id) { - println!(" ✅ Installed successfully"); - } else { - println!( - " ⚠️ Command succeeded but skill files not found at {:?}", - root.join(".agents/skills").join(&skill.local_skill_id) - ); - } - } else { - // Installation failed - could be network issue or skill doesn't exist - let stderr = String::from_utf8_lossy(&output.stderr); - println!( - " ⚠️ Install failed: {}", - if stderr.len() > 200 { - format!("{}...", &stderr[..200]) - } else { - stderr.to_string() - } - ); - } - } - Err(e) => { - println!(" ⚠️ Failed to run install command: {}", e); - } +fn install_with_retry(skill_id: &str, source: &str, target_root: &Path) -> anyhow::Result<()> { + match blocking_fetch_and_install_skill(skill_id, source, target_root) { + Ok(()) => Ok(()), + Err(first_error) => { + eprintln!( + "Initial install attempt failed for {skill_id} from {source}: {first_error}. Retrying once..." + ); + thread::sleep(Duration::from_secs(2)); + blocking_fetch_and_install_skill(skill_id, source, target_root).map_err( + |second_error| { + anyhow::anyhow!("first attempt: {first_error}; retry: {second_error}") + }, + ) } } - - // Test passes as long as we tried - actual verification happens via logs - println!("✅ Catalog skill installation test completed"); } -/// Test that local dallay skills can be installed from the local .agents/skills directory. -/// These are the skills that come bundled with agentsync itself. #[test] -fn test_install_local_dallay_skills() { - // Get the agentsync project root (where this test is running) - let project_root = Path::new(env!("CARGO_MANIFEST_DIR")); - - // Check if local skills exist - let local_skills_dir = project_root.join(".agents").join("skills"); - if !local_skills_dir.exists() { - println!( - "⚠️ Local skills directory not found at {:?}", - local_skills_dir - ); +#[ignore] +fn every_catalog_skill_installs_successfully() { + if std::env::var("RUN_E2E").is_err() { + eprintln!("Skipping catalog installation test (set RUN_E2E=1 to enable)"); return; } - // Read the catalog to find local dallay skills - let skills = extract_catalog_skills(); - let local_skills: Vec<_> = skills - .iter() - .filter(|s| s.is_local && !s.local_skill_id.is_empty()) - .collect(); - - println!("Testing {} local dallay skills", local_skills.len()); + let catalog = EmbeddedSkillCatalog::default(); + let provider = SkillsShProvider; + let mut failures = Vec::new(); + + for definition in catalog.skill_definitions() { + let temp = TempDir::new().expect("temp dir should be created"); + let target_root = temp.path().join(".agents").join("skills"); + std::fs::create_dir_all(&target_root).expect("target root should be created"); + + let source = match resolve_install_source( + &provider, + &definition.provider_skill_id, + &definition.local_skill_id, + ) { + Ok(source) => source, + Err(error) => { + failures.push(format!( + "{} [{}] failed to resolve source: {}", + definition.local_skill_id, definition.provider_skill_id, error + )); + continue; + } + }; - for skill in local_skills { - let source_dir = local_skills_dir.join(&skill.local_skill_id); - if !source_dir.exists() { - println!( - " ⚠️ Local skill not found: {} at {:?}", - skill.local_skill_id, source_dir - ); + if let Err(error) = install_with_retry(&definition.local_skill_id, &source, &target_root) { + failures.push(format!( + "{} [{}] failed to install from {}: {}", + definition.local_skill_id, definition.provider_skill_id, source, error + )); continue; } - println!(" Testing local skill: {}", skill.local_skill_id); - - // Create temp project and install from local source - let temp = TempDir::new().unwrap(); - let root = temp.path(); - - // Init - if let Err(e) = init_temp_project(root) { - println!(" ⚠️ Init failed for {}: {}", skill.local_skill_id, e); + let skill_dir = target_root.join(&definition.local_skill_id); + let manifest_path = skill_dir.join("SKILL.md"); + if !manifest_path.exists() { + failures.push(format!( + "{} [{}] installed without SKILL.md at {}", + definition.local_skill_id, + definition.provider_skill_id, + manifest_path.display() + )); continue; } - // Install from local path - fix the flag order here - let result = install_skill( - root, - &skill.local_skill_id, - Some(source_dir.to_str().unwrap()), - ); - - match result { - Ok(output) => { - if output.status.success() { - if verify_skill_installed(root, &skill.local_skill_id) { - println!(" ✅ Installed successfully"); - } else { - println!(" ⚠️ Install succeeded but files missing"); - } - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - println!( - " ⚠️ Install failed: {}", - if stderr.len() > 100 { - format!("{}...", &stderr[..100]) - } else { - stderr.to_string() - } - ); + let registry_path = target_root.join("registry.json"); + match read_registry(®istry_path) { + Ok(registry) => { + let has_entry = registry + .skills + .unwrap_or_default() + .contains_key(&definition.local_skill_id); + if !has_entry { + failures.push(format!( + "{} [{}] installed but registry.json is missing its canonical key", + definition.local_skill_id, definition.provider_skill_id + )); } } - Err(e) => { - println!(" ⚠️ Command error: {}", e); - } + Err(error) => failures.push(format!( + "{} [{}] installed but registry.json could not be read: {}", + definition.local_skill_id, definition.provider_skill_id, error + )), } } - println!("✅ Local dallay skills test completed"); -} - -/// Quick sanity check: verify catalog is readable and has expected structure. -#[test] -fn test_catalog_structure() { - let skills = extract_catalog_skills(); - - // Should have at least 100 skills in the catalog - assert!( - skills.len() >= 100, - "Expected at least 100 skills in catalog, found {}", - skills.len() - ); - - // Should have local dallay skills - let local_count = skills.iter().filter(|s| s.is_local).count(); assert!( - local_count > 0, - "Expected at least some local dallay skills" - ); - - // Should have external skills - let external_count = skills.iter().filter(|s| !s.is_local).count(); - assert!(external_count > 0, "Expected at least some external skills"); - - println!( - "✅ Catalog has {} skills ({} local, {} external)", - skills.len(), - local_count, - external_count + failures.is_empty(), + "{} catalog skills failed installation validation:\n- {}", + failures.len(), + failures.join("\n- ") ); } diff --git a/tests/unit/provider.rs b/tests/unit/provider.rs index f612ef8c..83e51b71 100644 --- a/tests/unit/provider.rs +++ b/tests/unit/provider.rs @@ -57,6 +57,36 @@ fn resolve_deterministic_non_skills_repo_omits_skills_prefix() { assert_eq!(info.format, "zip"); } +#[test] +fn resolve_deterministic_repo_suffix_skills_adds_skills_prefix() { + let provider = SkillsShProvider; + + let info = provider + .resolve("krutikJain/android-agent-skills/android-kotlin-core") + .unwrap(); + + assert_eq!( + info.download_url, + "https://github.com/krutikJain/android-agent-skills/archive/HEAD.zip#skills/android-kotlin-core" + ); + assert_eq!(info.format, "zip"); +} + +#[test] +fn resolve_deterministic_embedded_catalog_can_omit_fragment_for_repo_root_skill() { + let provider = SkillsShProvider; + + let info = provider + .resolve("currents-dev/playwright-best-practices-skill/playwright-best-practices") + .unwrap(); + + assert_eq!( + info.download_url, + "https://github.com/currents-dev/playwright-best-practices-skill/archive/HEAD.zip" + ); + assert_eq!(info.format, "zip"); +} + #[test] fn resolve_deterministic_skills_repo_adds_skills_prefix() { let provider = SkillsShProvider; diff --git a/tests/unit/suggest_catalog.rs b/tests/unit/suggest_catalog.rs index c1e40635..f793c4a1 100644 --- a/tests/unit/suggest_catalog.rs +++ b/tests/unit/suggest_catalog.rs @@ -635,6 +635,9 @@ fn provider_skill(provider_skill_id: &str, local_skill_id: &str) -> ProviderCata local_skill_id: local_skill_id.to_string(), title: local_skill_id.to_string(), summary: format!("Summary for {local_skill_id}"), + archive_subpath: None, + legacy_local_skill_ids: Vec::new(), + install_source: None, } } @@ -854,32 +857,32 @@ impl Provider for NoMatchCatalogProvider { fn combo_triggers_when_all_required_technologies_detected() { let catalog = EmbeddedSkillCatalog::default(); - // react-shadcn combo requires both "react" and "shadcn" + // nextjs-clerk combo requires both "nextjs" and "clerk" let detections = vec![ detection( - TechnologyId::new("react"), + TechnologyId::new("nextjs"), DetectionConfidence::High, "package.json", ), detection( - TechnologyId::new("shadcn"), + TechnologyId::new("clerk"), DetectionConfidence::High, - "components.json", + "package.json", ), ]; let recommendations = recommend_skills(&catalog, &detections); - // The react-shadcn combo should inject its skills into recommendations. + // The nextjs-clerk combo should inject its skills into recommendations. // At minimum, the combo's reason should appear. let has_combo_reason = recommendations.iter().any(|r| { r.reasons .iter() - .any(|reason| reason.contains("React + shadcn/ui")) + .any(|reason| reason.contains("Next.js + Clerk")) }); assert!( has_combo_reason, - "should have a combo-based recommendation mentioning 'React + shadcn/ui', got reasons: {:?}", + "should have a combo-based recommendation mentioning 'Next.js + Clerk', got reasons: {:?}", recommendations .iter() .flat_map(|r| r.reasons.iter()) @@ -891,9 +894,9 @@ fn combo_triggers_when_all_required_technologies_detected() { fn combo_does_not_trigger_with_partial_requirements() { let catalog = EmbeddedSkillCatalog::default(); - // Only "react" detected, no "shadcn" — the react-shadcn combo should NOT trigger + // Only "nextjs" detected, no "clerk" — the nextjs-clerk combo should NOT trigger let detections = vec![detection( - TechnologyId::new("react"), + TechnologyId::new("nextjs"), DetectionConfidence::High, "package.json", )]; @@ -903,7 +906,7 @@ fn combo_does_not_trigger_with_partial_requirements() { let has_combo_reason = recommendations.iter().any(|r| { r.reasons .iter() - .any(|reason| reason.contains("React + shadcn/ui")) + .any(|reason| reason.contains("Next.js + Clerk")) }); assert!( !has_combo_reason, @@ -940,8 +943,8 @@ fn expanded_catalog_has_minimum_expected_counts() { "expected at least 40 technologies, got {technology_count}" ); assert!( - combo_count >= 10, - "expected at least 10 combos, got {combo_count}" + combo_count >= 7, + "expected at least 7 combos, got {combo_count}" ); } diff --git a/tests/unit/suggest_install.rs b/tests/unit/suggest_install.rs index 51befef1..278546b0 100644 --- a/tests/unit/suggest_install.rs +++ b/tests/unit/suggest_install.rs @@ -200,6 +200,103 @@ fn install_flow_rechecks_registry_before_installing() { assert_eq!(install_response.results[0].error_message, None); } +#[test] +fn suggest_marks_canonical_vue_skill_as_installed_when_legacy_alias_exists() { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + fs::write( + root.join("package.json"), + r#"{"dependencies":{"vue":"^3.4.0"}}"#, + ) + .unwrap(); + + let skills_dir = root.join(".agents/skills"); + fs::create_dir_all(&skills_dir).unwrap(); + fs::write( + skills_dir.join("registry.json"), + serde_json::to_string_pretty(&serde_json::json!({ + "schemaVersion": 1, + "last_updated": "2026-04-06T00:00:00Z", + "skills": { + "antfu-vue": { + "name": "antfu-vue", + "version": "1.0.0" + } + } + })) + .unwrap(), + ) + .unwrap(); + + let response = SuggestionService + .suggest(root) + .expect("vue recommendation should be generated"); + + let vue = response + .recommendations + .iter() + .find(|recommendation| recommendation.provider_skill_id == "antfu/skills/vue") + .expect("antfu vue recommendation should exist"); + + assert_eq!(vue.skill_id, "vue"); + assert!(vue.installed); + assert_eq!(vue.installed_version.as_deref(), Some("1.0.0")); +} + +#[test] +fn install_flow_skips_canonical_vue_when_legacy_alias_is_installed() { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + fs::write( + root.join("package.json"), + r#"{"dependencies":{"vue":"^3.4.0"}}"#, + ) + .unwrap(); + + let skills_dir = root.join(".agents/skills"); + fs::create_dir_all(&skills_dir).unwrap(); + fs::write( + skills_dir.join("registry.json"), + serde_json::to_string_pretty(&serde_json::json!({ + "schemaVersion": 1, + "last_updated": "2026-04-06T00:00:00Z", + "skills": { + "antfu-vue": { + "name": "antfu-vue", + "version": "1.0.0" + } + } + })) + .unwrap(), + ) + .unwrap(); + + let provider = LocalSkillProvider::new(root, &[("antfu/skills/vue", "vue")]); + let response = SuggestionService + .suggest(root) + .expect("vue recommendation should be generated"); + + let install_response = SuggestionService + .install_selected_with( + root, + &response, + &provider, + SuggestInstallMode::InstallAll, + &["vue".to_string()], + |_skill_id, _source, _target_root| { + panic!("canonical vue install should be skipped when legacy alias exists") + }, + ) + .unwrap(); + + assert_eq!(install_response.results.len(), 1); + assert_eq!(install_response.results[0].skill_id, "vue"); + assert_eq!( + install_response.results[0].status, + SuggestInstallStatus::AlreadyInstalled + ); +} + #[test] fn install_flow_records_failures_and_continues() { let temp_dir = TempDir::new().unwrap();