Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions skill_manager/application/marketplace/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class MarketplaceCatalog:
DETAIL_MISSING_FALLBACK = "No summary available on skills.sh."
_LEADERBOARD_TTL_SECONDS = 3600
_DETAIL_TTL_SECONDS = 86400
_DETAIL_NAMESPACE = "details-v2"
_DETAIL_NAMESPACE = "details-v3"
_SEARCH_TTL_SECONDS = 900
_SEARCH_FETCH_FLOOR = 40
_SEARCH_CACHE_LIMIT = 24
Expand Down Expand Up @@ -153,7 +153,7 @@ def repo_metadata(self, repo: str) -> RepoDisplayMetadata:

def detail_enrichment(self, record: SkillsShSkill) -> DetailEnrichment:
cached = self._cached_detail(record)
if cached is not None and cached.folder_resolution_complete and not self._needs_folder_refresh(cached):
if cached is not None and cached.folder_resolution_complete:
return cached

summary = cached
Expand Down Expand Up @@ -289,10 +289,6 @@ def _cached_detail(self, record: SkillsShSkill) -> DetailEnrichment | None:
return None
return DetailEnrichment.from_dict(detail_cache.payload)

@staticmethod
def _needs_folder_refresh(detail: DetailEnrichment) -> bool:
return bool(detail.folder_url and "/tree/HEAD/" in detail.folder_url)

def _resolve_summary_enrichment(self, record: SkillsShSkill) -> DetailEnrichment:
description = record.description_hint.strip() if self._is_usable_description(record.description_hint) else ""
document = self._detail_fetcher(record.detail_url)
Expand Down
25 changes: 6 additions & 19 deletions skill_manager/application/marketplace/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

from dataclasses import dataclass
from pathlib import Path
import subprocess
from tempfile import TemporaryDirectory
from urllib.parse import quote

from skill_manager.sources import GitHubSource
from skill_manager.sources import GitHubSource, github_folder_url as build_github_folder_url
from .repo_snapshots import GitHubRepoSnapshotService

from .models import RepoDisplayMetadata
Expand Down Expand Up @@ -55,20 +53,9 @@ def github_folder_url(self, repo: str, skill_id: str, *, default_branch: str | N
locator = f"{owner}/{repo_name}/{skill_id}"
with TemporaryDirectory(prefix="skill-manager-marketplace-") as temp_dir:
work_dir = Path(temp_dir)
skill_path = GitHubSource().fetch(locator, work_dir)
clone_dir = work_dir / f"{owner}--{repo_name}"
branch = default_branch or self._checked_out_branch(clone_dir) or "HEAD"
relative_path = skill_path.relative_to(clone_dir).as_posix()
return f"https://github.com/{repo}/tree/{quote(branch, safe='')}/{quote(relative_path, safe='/')}"

@staticmethod
def _checked_out_branch(clone_dir: Path) -> str | None:
result = subprocess.run(
["git", "-C", str(clone_dir), "branch", "--show-current"],
check=True,
capture_output=True,
text=True,
timeout=10,
resolved = GitHubSource().resolve(locator, work_dir)
return build_github_folder_url(
repo,
ref=default_branch or resolved.ref,
relative_path=resolved.relative_path,
)
branch = result.stdout.strip()
return branch or None
4 changes: 4 additions & 0 deletions skill_manager/application/skills/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class InventoryEntry:
source: SourceDescriptor
current_revision: str | None = None
recorded_revision: str | None = None
source_ref: str | None = None
source_path: str | None = None
package_dir: str | None = None
package_path: Path | None = None
sightings: list[InventorySighting] = field(default_factory=list)
Expand Down Expand Up @@ -104,6 +106,8 @@ def from_snapshot(cls, *, store_scan: StoreScan, harness_scans: tuple[HarnessSca
source=package.source,
current_revision=package.revision,
recorded_revision=store_package.recorded_revision,
source_ref=store_package.recorded_source_ref,
source_path=store_package.recorded_source_path,
package_dir=package.root_path.name,
package_path=package.root_path,
)
Expand Down
17 changes: 12 additions & 5 deletions skill_manager/application/skills/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,18 @@ def update_skill(self, skill_ref: str) -> dict[str, bool]:
if entry.package_dir is None:
raise MutationError("managed skill is missing its package directory name", status=500)
with TemporaryDirectory(prefix="skill-update-") as work_dir:
skill_path = self.source_fetcher.fetch(
fetched = self.source_fetcher.fetch_package(
source_kind=entry.source.kind,
source_locator=entry.source.locator,
work_dir=Path(work_dir),
)
try:
self.read_models.store.update(entry.package_dir, source_path=skill_path)
self.read_models.store.update(
entry.package_dir,
source_path=fetched.package_path,
source_ref=fetched.source_ref,
source_path_hint=fetched.source_path,
)
except ValueError as error:
raise MutationError(str(error), status=409) from error
self.read_models.invalidate()
Expand Down Expand Up @@ -176,21 +181,23 @@ def delete_skill(self, skill_ref: str) -> dict[str, bool]:

def install_skill(self, *, source_kind: str, source_locator: str) -> dict[str, bool]:
with TemporaryDirectory(prefix="skill-install-") as work_dir:
skill_path = self.source_fetcher.fetch(
fetched = self.source_fetcher.fetch_package(
source_kind=source_kind,
source_locator=source_locator,
work_dir=Path(work_dir),
)
package = parse_skill_package(
skill_path,
fetched.package_path,
default_source=SourceDescriptor(kind=source_kind, locator=source_locator),
)
try:
self.read_models.store.ingest(
source_path=skill_path,
source_path=fetched.package_path,
declared_name=package.declared_name,
source_kind=source_kind,
source_locator=source_locator,
source_ref=fetched.source_ref,
source_path_hint=fetched.source_path,
)
except ValueError as error:
raise MutationError(str(error), status=409) from error
Expand Down
25 changes: 18 additions & 7 deletions skill_manager/application/skills/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from skill_manager.domain import fingerprint_package
from skill_manager.errors import MutationError
from skill_manager.sources import github_repo_from_locator, github_repo_url, github_skill_dir_from_locator
from skill_manager.sources import github_folder_url, github_repo_from_locator, github_repo_url

from ..document_utils import read_skill_document_markdown
from ..read_model_service import ReadModelService
Expand Down Expand Up @@ -101,17 +101,28 @@ def build_source_links(self, entry: InventoryEntry) -> dict[str, str | None] | N
if repo is None:
return None

skill_dir = github_skill_dir_from_locator(entry.source.locator)
folder_url = None
if skill_dir:
folder_url = f"{github_repo_url(repo)}/tree/HEAD/{skill_dir}"

return {
"repoLabel": repo,
"repoUrl": github_repo_url(repo),
"folderUrl": folder_url,
"folderUrl": self._github_folder_url(entry, repo),
}

def _github_folder_url(self, entry: InventoryEntry, repo: str) -> str | None:
if entry.source_ref is not None and entry.source_path is not None:
return github_folder_url(repo, ref=entry.source_ref, relative_path=entry.source_path)
if entry.source.locator.removeprefix("github:").count("/") < 2:
return None
with TemporaryDirectory(prefix="skill-source-links-") as work_dir:
try:
fetched = self.source_fetcher.fetch_package(
source_kind=entry.source.kind,
source_locator=entry.source.locator,
work_dir=Path(work_dir),
)
except MutationError:
return None
return github_folder_url(repo, ref=fetched.source_ref, relative_path=fetched.source_path)

def resolve_update_status(
self,
entry: InventoryEntry,
Expand Down
24 changes: 22 additions & 2 deletions skill_manager/application/source_fetch_service.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

from skill_manager.errors import MutationError
from skill_manager.sources import GitHubSource


@dataclass(frozen=True)
class FetchedSourcePackage:
package_path: Path
source_ref: str | None = None
source_path: str | None = None


class SourceFetchService:
def __init__(self, *, github: GitHubSource | None = None) -> None:
self._github = github or GitHubSource()

def fetch(self, *, source_kind: str, source_locator: str, work_dir: Path) -> Path:
def fetch_package(self, *, source_kind: str, source_locator: str, work_dir: Path) -> FetchedSourcePackage:
try:
if source_kind == "github":
locator = source_locator.removeprefix("github:")
return self._github.fetch(locator, work_dir)
resolved = self._github.resolve(locator, work_dir)
return FetchedSourcePackage(
package_path=resolved.package_path,
source_ref=resolved.ref,
source_path=resolved.relative_path,
)
except MutationError:
raise
except Exception as error: # noqa: BLE001
raise MutationError(str(error), status=400) from error
raise MutationError(f"unsupported source kind: {source_kind}", status=400)

def fetch(self, *, source_kind: str, source_locator: str, work_dir: Path) -> Path:
return self.fetch_package(
source_kind=source_kind,
source_locator=source_locator,
work_dir=work_dir,
).package_path
2 changes: 2 additions & 0 deletions skill_manager/domain/observations.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class BuiltinObservation:
class StorePackageObservation:
package: SkillPackage
recorded_revision: str | None = None
recorded_source_ref: str | None = None
recorded_source_path: str | None = None


@dataclass(frozen=True)
Expand Down
6 changes: 4 additions & 2 deletions skill_manager/sources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
GitHubRepoMetadataError,
GitHubRepoMetadataClient,
GitHubSource,
ResolvedGitHubSkill,
github_folder_url,
github_owner_avatar_url,
github_repo_owner,
github_repo_from_locator,
github_skill_dir_from_locator,
github_repo_url,
is_valid_github_repo,
)
Expand All @@ -16,10 +17,11 @@
"GitHubRepoMetadataError",
"GitHubRepoMetadataClient",
"GitHubSource",
"ResolvedGitHubSkill",
"github_folder_url",
"github_owner_avatar_url",
"github_repo_owner",
"github_repo_from_locator",
"github_skill_dir_from_locator",
"github_repo_url",
"is_valid_github_repo",
]
Loading