Files
ClaudeInit/myclaude/fs_ops.py
T
2026-04-13 22:04:05 +02:00

175 lines
4.8 KiB
Python

"""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