"""Command-line interface for myclaude.""" from pathlib import Path from typing import Optional import typer from myclaude import config as cfg from myclaude import fs_ops, git_ops app = typer.Typer(help="Manage Claude skills across projects.") @app.command() def init( project_dir: Path = typer.Argument(..., help="Target project directory"), repo: Optional[str] = typer.Option(None, "--repo", help="Git repo SSH URL"), skill: Optional[list[str]] = typer.Option(None, "--skill", help="Skills to install"), force: bool = typer.Option(False, "--force", help="Overwrite existing skills"), keep_tmp: bool = typer.Option(False, "--keep-tmp", help="Keep temporary clone directory for inspection"), ) -> None: """Initialize a project with Claude skills from the central repo.""" if repo: cfg.save_config(repo) typer.echo(f"Repository URL saved: {repo}") try: repo_url = cfg.get_repo_url() except (FileNotFoundError, KeyError) as e: typer.echo(f"Error: {e}", err=True) raise typer.Exit(code=1) with git_ops.temp_clone(repo_url, keep=keep_tmp) as (repo_path, _): try: fs_ops.copy_claude_md_to_project(repo_path, project_dir) fs_ops.copy_skills_to_project(repo_path, project_dir, skills=skill, force=force) except (FileExistsError, ValueError) as e: typer.echo(f"Error: {e}", err=True) raise typer.Exit(code=1) skill_label = ", ".join(skill) if skill else "all" typer.echo(f"Initialized '{project_dir}' with skills: {skill_label}") @app.command() def push( skill: Optional[list[str]] = typer.Option(None, "--skill", help="Skills to push"), claude_md: bool = typer.Option(False, "--claude-md", help="Also push CLAUDE.md"), ) -> None: """Push local skills to the central repo.""" project_dir = Path.cwd() try: repo_url = cfg.get_repo_url() except (FileNotFoundError, KeyError) as e: typer.echo(f"Error: {e}", err=True) raise typer.Exit(code=1) with git_ops.temp_clone(repo_url) as (repo_path, repo): try: fs_ops.copy_skills_to_repo(project_dir, repo_path, skills=skill) if claude_md: fs_ops.copy_claude_md_to_repo(project_dir, repo_path) pushed_skills = skill if skill else fs_ops.collect_local_skills(project_dir) commit_message = f"chore: update skills {', '.join(pushed_skills)}" if claude_md: commit_message += " + CLAUDE.md" git_ops.commit_and_push(repo, commit_message) except (ValueError, RuntimeError) as e: typer.echo(f"Error: {e}", err=True) raise typer.Exit(code=1) typer.echo("Skills pushed successfully.") @app.command() def status() -> None: """Compare local skills with the central repo.""" project_dir = Path.cwd() try: repo_url = cfg.get_repo_url() except (FileNotFoundError, KeyError) as e: typer.echo(f"Error: {e}", err=True) raise typer.Exit(code=1) with git_ops.temp_clone(repo_url) as (repo_path, _): result = fs_ops.get_skills_status(project_dir, repo_path) if not any([result.in_both, result.local_only, result.remote_only]): typer.echo("No skills found locally or remotely.") return if result.in_both: typer.echo("Installed:") for s in result.in_both: typer.echo(f" [ok] {s}") if result.local_only: typer.echo("Local only (not pushed):") for s in result.local_only: typer.echo(f" [+] {s}") if result.remote_only: typer.echo("Remote only (not installed):") for s in result.remote_only: typer.echo(f" [-] {s}") @app.command(name="list") def list_skills() -> None: """List all skills available in the central repo.""" try: repo_url = cfg.get_repo_url() except (FileNotFoundError, KeyError) as e: typer.echo(f"Error: {e}", err=True) raise typer.Exit(code=1) with git_ops.temp_clone(repo_url) as (repo_path, _): skills = fs_ops.list_skills_in_repo(repo_path) if not skills: typer.echo("No skills found in the central repo.") return typer.echo("Available skills:") for s in skills: typer.echo(f" - {s}")