"""File system operations for myclaude.""" import shutil from dataclasses import dataclass from pathlib import Path @dataclass class SkillsStatus: """Diff between local and remote skills. Attributes: local_only: Skills present locally but not in the remote repo. remote_only: Skills present in the remote repo but not locally. in_both: Skills present in both local and remote. """ local_only: list[str] remote_only: list[str] in_both: list[str] def copy_skills_to_project( repo_dir: Path, project_dir: Path, skills: list[str] | None = None, force: bool = False, ) -> None: """Copy skills from the repo to the project's .claude/skills directory. Args: repo_dir: Path to the cloned central repository. project_dir: Path to the target project directory. skills: List of skill names to copy. If None, copies all skills. force: If True, overwrites the existing skills directory. Raises: FileExistsError: If .claude/skills already exists and force is False. ValueError: If a requested skill does not exist in the repo. """ skills_dst = project_dir / ".claude" / "skills" if skills_dst.exists() and not force: raise FileExistsError( f"Directory '{skills_dst}' already exists. Use --force to overwrite." ) available = list_skills_in_dir(repo_dir / "skills") skills_to_copy = _resolve_skills(skills, available) if skills_dst.exists() and force: shutil.rmtree(skills_dst) skills_dst.mkdir(parents=True, exist_ok=True) for skill in skills_to_copy: shutil.copytree(repo_dir / "skills" / skill, skills_dst / skill) def copy_claude_md_to_project(repo_dir: Path, project_dir: Path) -> None: """Copy CLAUDE.md from the repo to the project root. Args: repo_dir: Path to the cloned central repository. project_dir: Path to the target project directory. """ shutil.copy2(repo_dir / "CLAUDE.md", project_dir / "CLAUDE.md") def copy_skills_to_repo( project_dir: Path, repo_dir: Path, skills: list[str] | None = None, ) -> None: """Copy skills from the project to the central repository. Args: project_dir: Path to the source project directory. repo_dir: Path to the cloned central repository. skills: List of skill names to push. If None, pushes all local skills. Raises: ValueError: If a requested skill does not exist locally. """ available = collect_local_skills(project_dir) skills_to_push = _resolve_skills(skills, available) for skill in skills_to_push: dst = repo_dir / "skills" / skill if dst.exists(): shutil.rmtree(dst) shutil.copytree(project_dir / ".claude" / "skills" / skill, dst) def copy_claude_md_to_repo(project_dir: Path, repo_dir: Path) -> None: """Copy CLAUDE.md from the project to the central repository. Args: project_dir: Path to the source project directory. repo_dir: Path to the cloned central repository. """ shutil.copy2(project_dir / "CLAUDE.md", repo_dir / "CLAUDE.md") def collect_local_skills(project_dir: Path) -> list[str]: """List skill names present in the project's .claude/skills directory. Args: project_dir: Path to the project directory. Returns: Sorted list of skill directory names. """ return list_skills_in_dir(project_dir / ".claude" / "skills") def list_skills_in_repo(repo_dir: Path) -> list[str]: """List skill names available in the central repository. Args: repo_dir: Path to the cloned central repository. Returns: Sorted list of skill directory names. """ return list_skills_in_dir(repo_dir / "skills") def get_skills_status(project_dir: Path, repo_dir: Path) -> SkillsStatus: """Compare local skills with those available in the central repo. Args: project_dir: Path to the local project directory. repo_dir: Path to the cloned central repository. Returns: A SkillsStatus instance describing the diff. """ local = set(collect_local_skills(project_dir)) remote = set(list_skills_in_repo(repo_dir)) return SkillsStatus( local_only=sorted(local - remote), remote_only=sorted(remote - local), in_both=sorted(local & remote), ) def list_skills_in_dir(directory: Path) -> list[str]: """List subdirectory names inside a directory. Args: directory: Path to inspect. Returns: Sorted list of subdirectory names, or empty list if directory does not exist. """ if not directory.exists(): return [] return sorted(d.name for d in directory.iterdir() if d.is_dir()) def _resolve_skills( requested: list[str] | None, available: list[str], ) -> list[str]: if requested is None: return available unknown = set(requested) - set(available) if unknown: raise ValueError( f"The following skills were not found: {', '.join(sorted(unknown))}" ) return requested