# AUTOGENERATED! DO NOT EDIT! File to edit: ../../../pts/mod/_cli/main.pct.py.

# %% auto 0
__all__ = ['entrypoint', 'cli_init', 'cli_new', 'cli_sync', 'cli_multi_sync', 'cli_sync_meta', 'cli_add_to_group',
           'cli_remove_from_group', 'cli_include', 'cli_exclude', 'cli_delete', 'cli_repo_status', 'cli_yard_status',
           'cli_list', 'cli_path', 'cli_create_user_symlinks']

# %% ../../../pts/mod/_cli/main.pct.py 3
import os
import typer
from typer import Argument, Option
from typing_extensions import Annotated
from types import FunctionType
from typing import Callable, Union, List, Literal
from pathlib import Path
from enum import Enum
import asyncio

import repoyard as proj
from .. import const
from ..config import get_config
from .._utils import async_throttler, check_interrupted, enable_soft_interruption, SoftInterruption
from .._utils.sync_helper import SyncSetting, SyncDirection
from .._models import RepoPart
from .app import app, app_state

# %% ../../../pts/mod/_cli/main.pct.py 5
@app.callback()
def entrypoint(
    ctx: typer.Context,
    config_path: Path|None = Option(None, "--config", help=f"The path to the config file. Will be '{const.DEFAULT_CONFIG_PATH}' if not provided.")
):
    app_state['config_path'] = config_path if config_path is not None else const.DEFAULT_CONFIG_PATH
    if ctx.invoked_subcommand is not None: return
    typer.echo(ctx.get_help())

# %% ../../../pts/mod/_cli/main.pct.py 8
def _is_subsequence_match(term: str, name: str) -> bool:
    j = 0
    m = len(term)

    for ch in name:
        if j < m and ch == term[j]:
            j += 1
            if j == m:
                return True
    return j == m

# %% ../../../pts/mod/_cli/main.pct.py 10
class NameMatchMode(Enum):
    EXACT = "exact"
    CONTAINS = "contains"
    SUBSEQUENCE = "subsequence"

def _get_full_repo_name(
    repo_name: str|None,
    repo_id: str|None,
    repo_full_name: str|None,
    name_match_mode: NameMatchMode|None,
    name_match_case: bool,
    repo_metas = None,
) -> str:
    from repoyard._models import RepoyardMeta
    if sum(1 for x in [repo_name, repo_full_name, repo_id] if x is not None) > 1:
        raise typer.Exit("Cannot provide more than one of `repo-name`, `repo-full-name` or `repo-id`.")
    
    if name_match_mode is not None and repo_name is None:
        raise typer.Exit("`repo-name` must be provided if `name-match-mode` is provided.")
    
    search_mode = (repo_id is None) and (repo_name is None) and (repo_full_name is None)

    from repoyard._models import get_repoyard_meta
    config = get_config(app_state['config_path'])
    if repo_metas is None:
        repo_metas = get_repoyard_meta(config).repo_metas
    repoyard_meta = RepoyardMeta(repo_metas=repo_metas)

    if (repo_id is not None or repo_name is not None) or search_mode:        
        if repo_id is not None:
            if not repo_id in repoyard_meta.by_id:
                raise typer.Exit(f"Repository with id `{repo_id}` not found.")
            repo_full_name = repoyard_meta.by_id[repo_id].full_name
        else:
            if repo_name is not None:
                if name_match_mode is None: name_match_mode = NameMatchMode.SUBSEQUENCE
                if name_match_mode == NameMatchMode.EXACT:
                    cmp = lambda x: x.name == repo_name if not name_match_case else x.name.lower() == repo_name.lower()
                    repos_with_name = [x for x in repoyard_meta.repo_metas if cmp(x)]
                elif name_match_mode == NameMatchMode.CONTAINS:
                    cmp = lambda x: repo_name in x.name if not name_match_case else repo_name.lower() in x.name.lower()
                    repos_with_name = [x for x in repoyard_meta.repo_metas if cmp(x)]
                elif name_match_mode == NameMatchMode.SUBSEQUENCE:
                    cmp = lambda x: _is_subsequence_match(repo_name, x.name) if not name_match_case else _is_subsequence_match(repo_name.lower(), x.name.lower())
                    repos_with_name = [x for x in repoyard_meta.repo_metas if cmp(x)]
            else:
                repos_with_name = repoyard_meta.repo_metas

            repos_with_name = sorted(repos_with_name, key=lambda x: x.full_name)
            
            if len(repos_with_name) == 0:
                typer.echo(f"Repository not found.", err=True)
                raise typer.Exit(code=1)
            elif len(repos_with_name) == 1:
                repo_full_name = repos_with_name[0].full_name
            else:
                from repoyard._utils import run_fzf
                _, repo_full_name = run_fzf(
                    terms=[r.full_name for r in repos_with_name],
                    disp_terms=[f"{r.name} ({r.repo_id}) groups: {', '.join(r.groups)}" for r in repos_with_name],
                )
        
    if repo_full_name is None:
        from repoyard._utils import get_repo_full_name_from_cwd
        repo_full_name = get_repo_full_name_from_cwd(
            config=config,
        )
        if repo_full_name is None:
            raise typer.Exit("Repo not specified and could not be inferred from current working directory.")
        
    return repo_full_name

# %% ../../../pts/mod/_cli/main.pct.py 12
@app.command(name='init')
def cli_init(
    config_path: Path|None = Option(None, "--config-path", help=f"The path to the config file. Will be {const.DEFAULT_CONFIG_PATH} if not provided."),
    data_path: Path|None = Option(None, "--data-path", help=f"The path to the data directory. Will be {const.DEFAULT_DATA_PATH} if not provided."),
):
    """
    Create a new repository.
    """
    from repoyard.cmds import init_repoyard
    init_repoyard(
        config_path=config_path,
        data_path=data_path,
        verbose=True,
    )

# %% ../../../pts/mod/_cli/main.pct.py 14
@app.command(name='new')
def cli_new(
    storage_location: str|None = Option(None, "--storage-location", "-s", help="The storage location to create the new repository in."),
    repo_name: str|None = Option(None, "--repo", "-n", help="The name of the repository, the id or the path of the repo."),
    from_path: Path|None = Option(None, "--from", "-f", help="Path to a local directory to move into repoyard as a new repository."),
    copy_from_path: bool = Option(False, "--copy", "-c", help="Copy the contents of the from_path into the new repository."),
    creator_hostname: str|None = Option(None, "--creator-hostname", help="Used to explicitly set the creator hostname of the new repository."),
    creation_timestamp_utc: str|None = Option(
        None,
        "--creation-timestamp-utc",
        help="The timestamp of the new repository. Should be in the form '%Y%m%d_%H%M%S' (e.g. '20251116_105532') or '%Y%m%d' (e.g. '20251116'). If not provided, the current UTC timestamp will be used."
    ),
    groups: list[str]|None = Option(None, "--groups", "-g", help="The groups to add the new repository to."),
    initialise_git: bool = Option(True, help="Initialise a git repository in the new repository."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
):
    """
    Create a new repository.
    """
    from repoyard.cmds import new_repo

    if repo_name is None and from_path is not None:
        repo_name = Path(from_path).name
        
    if repo_name is None:
        typer.echo("No repository name provided.")
        raise typer.Exit(code=1)

    if creation_timestamp_utc is not None:
        from datetime import datetime
        try:
            creation_timestamp_utc = datetime.strptime(creation_timestamp_utc, const.REPO_TIMESTAMP_FORMAT)
        except ValueError:
            try:
                creation_timestamp_utc = datetime.strptime(creation_timestamp_utc, const.REPO_TIMESTAMP_FORMAT_DATE_ONLY)
            except ValueError:
                typer.echo(f"Invalid creation timestamp: {creation_timestamp_utc}")
                raise typer.Exit(code=1)
    
    repo_full_name = new_repo(
        config_path=app_state['config_path'],
        storage_location=storage_location,
        repo_name=repo_name,
        from_path=from_path,
        copy_from_path=copy_from_path,
        creator_hostname=creator_hostname,
        initialise_git=initialise_git,
        creation_timestamp_utc=creation_timestamp_utc,
        verbose=False,
    )
    typer.echo(repo_full_name)

    if groups:
        from repoyard.cmds import modify_repometa
        modify_repometa(
            config_path=app_state['config_path'],
            repo_full_name=repo_full_name,
            modifications={
                'groups': groups,
            }
        )

    from repoyard.cmds import create_user_symlinks
    create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 16
@app.command(name='sync')
def cli_sync(
    repo_path: Path|None = Option(None, "--repo-path", "-p", help="The path to the repository to sync."),
    repo_full_name: str|None = Option(None, "--repo", "-r", help="The full name of the repository."),
    repo_id: str|None = Option(None, "--repo-id", "-i", help="The id of the repository to sync."),
    repo_name: str|None = Option(None, "--repo-name", "-n", help="The name of the repository to sync."),
    name_match_mode: NameMatchMode|None = Option(None, "--name-match-mode", "-m", help="The mode to use for matching the repository name."),
    name_match_case: bool = Option(False, "--name-match-case", help="Whether to match the repository name case-sensitively."),
    sync_direction: SyncDirection|None = Option(None, "--sync-direction", "-d", help="The direction of the sync. If not provided, the appropriate direction will be automatically determined based on the sync status. This mode is only available for the 'CAREFUL' sync setting."),
    sync_setting: SyncSetting = Option(SyncSetting.CAREFUL, "--sync-setting", "-s", help="The sync setting to use."),
    sync_choices: list[RepoPart]|None = Option(None, "--sync-choices", "-c", help="The parts of the repository to sync. If not provided, all parts will be synced. By default, all parts are synced."),
    show_rclone_progress: bool = Option(False, "--progress", help="Show the progress of the sync in rclone."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Sync a repository.
    """
    from repoyard.cmds import sync_repo

    if repo_path is not None:
        from repoyard._utils import get_repo_full_name_from_sub_path
        config = get_config(app_state['config_path'])
        repo_full_name = get_repo_full_name_from_sub_path(
            config=config,
            sub_path=repo_path,
        )

    repo_full_name = _get_full_repo_name(
        repo_name=repo_name,
        repo_id=repo_id,
        repo_full_name=repo_full_name,
        name_match_mode=name_match_mode,
        name_match_case=name_match_case,
    )

    if sync_choices is None:
        sync_choices = [repo_part for repo_part in RepoPart]
    
    asyncio.run(sync_repo(
        config_path=app_state['config_path'],
        repo_full_name=repo_full_name,
        sync_direction=sync_direction,
        sync_setting=sync_setting,
        sync_choices=sync_choices,
        verbose=True,
        show_rclone_progress=show_rclone_progress,
        soft_interruption_enabled=soft_interruption_enabled,
    ))

    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 18
@app.command(name='multi-sync')
def cli_multi_sync(
    repo_full_names: list[str]|None = Option(None, "--repo", "-r", help="The full names of the repository, in the form."),
    storage_locations: list[str]|None = Option(None, "--storage-location", "-s", help="The storage locations to sync."),
    max_concurrent_rclone_ops: int|None = Option(None, "--max-concurrent", "-m", help="The maximum number of concurrent rclone operations. If not provided, the default specified in the config will be used."),
    sync_direction: SyncDirection|None = Option(None, "--sync-direction", help="The direction of the sync. If not provided, the appropriate direction will be automatically determined based on the sync status. This mode is only available for the 'CAREFUL' sync setting."),
    sync_setting: SyncSetting = Option(SyncSetting.CAREFUL, "--sync-setting", help="The sync setting to use."),
    sync_choices: list[RepoPart]|None = Option(None, "--sync-choices", "-c", help="The parts of the repository to sync. If not provided, all parts will be synced. By default, all parts are synced."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    show_progress: bool = Option(True, help="Show the progress of the sync."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Sync multiple repositories.
    """
    from repoyard._models import get_repoyard_meta
    from repoyard.cmds import sync_repo
    from rich.live import Live
    from rich.text import Text
    from rich.console import Console

    if soft_interruption_enabled:
        enable_soft_interruption()

    if repo_full_names is not None and storage_locations is not None:
        typer.echo("Cannot provide both `--repo` and `--storage-location`.", err=True)
        raise typer.Exit(code=1)

    config = get_config(app_state['config_path'])

    if storage_locations is None and repo_full_names is None:
        storage_locations = list(config.storage_locations.keys())
    if storage_locations is not None and any(sl not in config.storage_locations for sl in storage_locations):
        typer.echo(f"Invalid storage location: {storage_locations}")
        raise typer.Exit(code=1)

    if max_concurrent_rclone_ops is None:
        max_concurrent_rclone_ops = config.max_concurrent_rclone_ops
    
    repoyard_meta = get_repoyard_meta(config)
    if repo_full_names is None:
        repo_metas = [repo_meta for repo_meta in repoyard_meta.repo_metas if repo_meta.storage_location in storage_locations]
    else:
        if any(repo_full_name not in repoyard_meta.by_full_name for repo_full_name in repo_full_names):
            typer.echo(f"Non-existent repository: {repo_full_names}")
            raise typer.Exit(code=1)
        repo_metas = [repoyard_meta.by_full_name[repo_full_name] for repo_full_name in repo_full_names]

    async def _task(repo_meta):
        sync_stats[repo_meta.full_name] =("Syncing...", None)
        try:
            await sync_repo(
                config_path=app_state['config_path'],
                repo_full_name=repo_meta.full_name,
                sync_direction=sync_direction,
                sync_setting=sync_setting,
                sync_choices=sync_choices,
                verbose=False,
            )
            sync_stats[repo_meta.full_name] = ("Success", None)
        except SoftInterruption:
            sync_stats[repo_meta.full_name] = ("Interrupted", None)
        except Exception as e:
            sync_stats[repo_meta.full_name] = ("Error", str(e))
        
    sync_stats = {}

    finish_monitoring = False
    async def _progress_monitor_task():
        nonlocal finish_monitoring
        console = Console()
        with Live(console=console, refresh_per_second=4) as live:
            def _update_live():
                lines = []
                for repo_full_name, (sync_stat, e) in sync_stats.items():
                    color = {
                        "Syncing...": "yellow",
                        "Success": "green",
                        "Interrupted": "magenta",
                        "Error": "red",
                    }.get(sync_stat, "white")

                    lines.append(f"{repo_full_name}")
                    lines.append(f"    [bold {color}]{sync_stat}[/bold {color}]")
                    if e:
                        lines.append(f"    [red]{e}[/red]")

                rendered = Text.from_markup("\n".join(lines))
                live.update(rendered)
            while not finish_monitoring:
                _update_live()
                await asyncio.sleep(0.2)
            await asyncio.sleep(0.2)
            _update_live()

    sync_task = async_throttler(
        [_task(repo_meta) for repo_meta in repo_metas],
        max_concurrency=max_concurrent_rclone_ops,
    )

    async def _runner():
        nonlocal finish_monitoring
        if show_progress:
            monitor_task = asyncio.create_task(_progress_monitor_task())
            await sync_task
            finish_monitoring = True
            await monitor_task
        else:
            await sync_task

    asyncio.run(_runner())

    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 20
@app.command(name='sync-meta')
def cli_sync_meta(
    repo_full_names: list[str]|None = Option(None, "--repo", "-r", help="The full name of the repository, in the form '{ULID}__{REPO_NAME}'."),
    storage_locations: list[str]|None = Option(None, "--storage-location", "-s", help="The storage location to sync the metadata from."),
    sync_all: bool = Option(False, "--all", "-a", help="Sync all repositories."),
    sync_setting: SyncSetting = Option(SyncSetting.CAREFUL, "--sync-setting", help="The sync setting to use."),
    sync_direction: SyncDirection|None = Option(None, "--sync-direction", "-d", help="The direction of the sync. If not provided, the appropriate direction will be automatically determined based on the sync status. This mode is only available for the 'CAREFUL' sync setting."),
    max_concurrent_rclone_ops: int|None = Option(None, "--max-concurrent", "-m", help="The maximum number of concurrent rclone operations. If not provided, the default specified in the config will be used."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Syncs the metadata of a repository.
    """
    from repoyard.cmds import sync_repometas
    
    if sync_all and (repo_full_names is not None or storage_locations is not None):
        raise typer.Exit("Cannot provide both `--all` and `--repo` or `--storage-location`.")

    if not sync_all and repo_full_names is None and storage_locations is None:
        from repoyard._utils import get_repo_full_name_from_cwd
        repo_full_name = get_repo_full_name_from_cwd(
            config=get_config(app_state['config_path']),
        )
        if repo_full_name is None:
            raise typer.Exit("Repo names to sync not specified and could not be inferred from current working directory.")
        repo_full_names = [repo_full_name]

    asyncio.run(sync_repometas(
        config_path=app_state['config_path'],
        repo_full_names=repo_full_names,
        storage_locations=storage_locations,
        sync_setting=sync_setting,
        sync_direction=sync_direction,
        verbose=True,
        max_concurrent_rclone_ops=max_concurrent_rclone_ops,
        soft_interruption_enabled=soft_interruption_enabled,
    ))

    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 22
@app.command(name='add-to-group')
def cli_add_to_group(
    repo_path: Path|None = Option(None, "--repo-path", "-p", help="The path to the repository to sync."),
    repo_full_name: str|None = Option(None, "--repo", "-r", help="The full name of the repository, in the form '{ULID}__{REPO_NAME}'."),
    repo_id: str|None = Option(None, "--repo-id", "-i", help="The id of the repository to sync."),
    repo_name: str|None = Option(None, "--repo-name", "-n", help="The name of the repository to sync."),
    name_match_mode: NameMatchMode|None = Option(None, "--name-match-mode", "-m", help="The mode to use for matching the repository name."),
    name_match_case: bool = Option(False, "--name-match-case", "-c", help="Whether to match the repository name case-sensitively."),
    group_name: str = Option(..., "--group-name", "-g", help="The name of the group to add the repository to."),
    sync_after: bool = Option(False, "--sync-after", "-s", help="Sync the repository after adding it to the group."),
    sync_setting: SyncSetting = Option(SyncSetting.CAREFUL, "--sync-setting", help="The sync setting to use."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Modify the metadata of a repository.
    """
    from repoyard.cmds import modify_repometa
    from repoyard._models import get_repoyard_meta
    
    if repo_path is not None:
        from repoyard._utils import get_repo_full_name_from_sub_path
        config = get_config(app_state['config_path'])
        repo_full_name = get_repo_full_name_from_sub_path(
            config=config,
            sub_path=repo_path,
        )

    repo_full_name = _get_full_repo_name(
        repo_name=repo_name,
        repo_id=repo_id,
        repo_full_name=repo_full_name,
        name_match_mode=name_match_mode,
        name_match_case=name_match_case,
    )
    
    repoyard_meta = get_repoyard_meta(get_config(app_state['config_path']))
    if repo_full_name not in repoyard_meta.by_full_name:
        typer.echo(f"Repository with full name `{repo_full_name}` not found.")
        raise typer.Exit(code=1)
    repo_meta = repoyard_meta.by_full_name[repo_full_name]
    if group_name in repo_meta.groups:
        raise typer.echo(f"Repository `{repo_full_name}` already in group `{group_name}`.")
    else:
        modify_repometa(
            config_path=app_state['config_path'],
            repo_full_name=repo_full_name,
            modifications={
                'groups': [*repo_meta.groups, group_name]
            }
        )
        
        if sync_after:
            from repoyard.cmds import sync_repometas
            asyncio.run(sync_repometas(
                config_path=app_state['config_path'],
                repo_full_names=[repo_full_name],
                sync_setting=sync_setting,
                sync_direction=SyncDirection.PUSH,
                verbose=True,
                soft_interruption_enabled=soft_interruption_enabled,
            ))

    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 24
@app.command(name='remove-from-group')
def cli_remove_from_group(
    repo_path: Path|None = Option(None, "--repo-path", "-p", help="The path to the repository to sync."),
    repo_full_name: str|None = Option(None, "--repo", "-r", help="The full name of the repository, in the form '{ULID}__{REPO_NAME}'."),
    repo_id: str|None = Option(None, "--repo-id", "-i", help="The id of the repository to sync."),
    repo_name: str|None = Option(None, "--repo-name", "-n", help="The name of the repository to sync."),
    name_match_mode: NameMatchMode|None = Option(None, "--name-match-mode", "-m", help="The mode to use for matching the repository name."),
    name_match_case: bool = Option(False, "--name-match-case", "-c", help="Whether to match the repository name case-sensitively."),
    group_name: str = Option(..., "--group-name", "-g", help="The name of the group to add the repository to."),
    sync_after: bool = Option(False, "--sync-after", "-s", help="Sync the repository after adding it to the group."),
    sync_setting: SyncSetting = Option(SyncSetting.CAREFUL, "--sync-setting", help="The sync setting to use."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Modify the metadata of a repository.
    """
    from repoyard.cmds import modify_repometa
    from repoyard._models import get_repoyard_meta

    if repo_path is not None:
        from repoyard._utils import get_repo_full_name_from_sub_path
        config = get_config(app_state['config_path'])
        repo_full_name = get_repo_full_name_from_sub_path(
            config=config,
            sub_path=repo_path,
        )
    
    repo_full_name = _get_full_repo_name(
        repo_name=repo_name,
        repo_id=repo_id,
        repo_full_name=repo_full_name,
        name_match_mode=name_match_mode,
        name_match_case=name_match_case,
    )
    
    repoyard_meta = get_repoyard_meta(get_config(app_state['config_path']))
    if repo_full_name not in repoyard_meta.by_full_name:
        typer.echo(f"Repository with full name `{repo_full_name}` not found.")
        raise typer.Exit(code=1)
    repo_meta = repoyard_meta.by_full_name[repo_full_name]
    if group_name not in repo_meta.groups:
        raise typer.echo(f"Repository `{repo_full_name}` not in group `{group_name}`.")
    else:
        modify_repometa(
            config_path=app_state['config_path'],
            repo_full_name=repo_full_name,
            modifications={
                'groups': [g for g in repo_meta.groups if g != group_name]
            }
        )
        
        if sync_after:
            from repoyard.cmds import sync_repometas
            asyncio.run(sync_repometas(
                config_path=app_state['config_path'],
                repo_full_names=[repo_full_name],
                sync_setting=sync_setting,
                sync_direction=SyncDirection.PUSH,
                verbose=True,
                soft_interruption_enabled=soft_interruption_enabled,
            ))

    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 26
@app.command(name='include')
def cli_include(
    repo_full_name: str|None = Option(None, "--repo", "-r", help="The full name of the repository, in the form '{ULID}__{REPO_NAME}'."),
    repo_id: str|None = Option(None, "--repo-id", "-i", help="The id of the repository to sync."),
    repo_name: str|None = Option(None, "--repo-name", "-n", help="The name of the repository to sync."),
    name_match_mode: NameMatchMode|None = Option(None, "--name-match-mode", "-m", help="The mode to use for matching the repository name."),
    name_match_case: bool = Option(False, "--name-match-case", "-c", help="Whether to match the repository name case-sensitively."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Include a repository in the local store.
    """
    from repoyard.cmds import include_repo
    from repoyard._models import get_repoyard_meta
    
    repo_full_name = _get_full_repo_name(
        repo_name=repo_name,
        repo_id=repo_id,
        repo_full_name=repo_full_name,
        name_match_mode=name_match_mode,
        name_match_case=name_match_case,
    )
    
    repoyard_meta = get_repoyard_meta(get_config(app_state['config_path']))
    if repo_full_name not in repoyard_meta.by_full_name:
        typer.echo(f"Repository with full name `{repo_full_name}` not found.")
        raise typer.Exit(code=1)
    
    asyncio.run(include_repo(
        config_path=app_state['config_path'],
        repo_full_name=repo_full_name,
        soft_interruption_enabled=soft_interruption_enabled,
    ))

    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 28
@app.command(name='exclude')
def cli_exclude(
    repo_full_name: str|None = Option(None, "--repo", "-r", help="The full name of the repository, in the form '{ULID}__{REPO_NAME}'."),
    repo_id: str|None = Option(None, "--repo-id", "-i", help="The id of the repository to sync."),
    repo_name: str|None = Option(None, "--repo-name", "-n", help="The name of the repository to sync."),
    name_match_mode: NameMatchMode|None = Option(None, "--name-match-mode", "-m", help="The mode to use for matching the repository name."),
    name_match_case: bool = Option(False, "--name-match-case", "-c", help="Whether to match the repository name case-sensitively."),
    skip_sync: bool = Option(False, "--skip-sync", "-s", help="Skip the sync before excluding the repository."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Exclude a repository from the local store.
    """
    from repoyard.cmds import exclude_repo
    from repoyard._models import get_repoyard_meta
    
    repo_full_name = _get_full_repo_name(
        repo_name=repo_name,
        repo_id=repo_id,
        repo_full_name=repo_full_name,
        name_match_mode=name_match_mode,
        name_match_case=name_match_case,
    )
    
    repoyard_meta = get_repoyard_meta(get_config(app_state['config_path']))
    if repo_full_name not in repoyard_meta.by_full_name:
        typer.echo(f"Repository with full name `{repo_full_name}` not found.")
        raise typer.Exit(code=1)
    
    asyncio.run(exclude_repo(
        config_path=app_state['config_path'],
        repo_full_name=repo_full_name,
        skip_sync=skip_sync,
        soft_interruption_enabled=soft_interruption_enabled,
    ))

    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 30
@app.command(name='delete')
def cli_delete(
    repo_full_name: str|None = Option(None, "--repo", "-r", help="The full name of the repository, in the form '{ULID}__{REPO_NAME}'."),
    repo_id: str|None = Option(None, "--repo-id", "-i", help="The id of the repository to sync."),
    repo_name: str|None = Option(None, "--repo-name", "-n", help="The name of the repository to sync."),
    name_match_mode: NameMatchMode|None = Option(None, "--name-match-mode", "-m", help="The mode to use for matching the repository name."),
    name_match_case: bool = Option(False, "--name-match-case", "-c", help="Whether to match the repository name case-sensitively."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Delete a repository.
    """
    from repoyard.cmds import delete_repo
    from repoyard._models import get_repoyard_meta
    
    repo_full_name = _get_full_repo_name(
        repo_name=repo_name,
        repo_id=repo_id,
        repo_full_name=repo_full_name,
        name_match_mode=name_match_mode,
        name_match_case=name_match_case,
    )
    
    repoyard_meta = get_repoyard_meta(get_config(app_state['config_path']))
    if repo_full_name not in repoyard_meta.by_full_name:
        typer.echo(f"Repository with full name `{repo_full_name}` not found.")
        raise typer.Exit(code=1)
    
    asyncio.run(delete_repo(
        config_path=app_state['config_path'],
        repo_full_name=repo_full_name,
        soft_interruption_enabled=soft_interruption_enabled,
    ))

    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])

# %% ../../../pts/mod/_cli/main.pct.py 32
def _dict_to_hierarchical_text(data: dict, indents: int=0, lines: list[str]=None) -> list[str]:
    if lines is None:
        lines = []
    for k, v in data.items():
        if isinstance(v, dict):
            lines.append(f"{' ' *4*indents}{k}:")
            _dict_to_hierarchical_text(v, indents+1, lines)
        else:
            lines.append(f"{' ' *4*indents}{k}: {v}")
    return lines

# %% ../../../pts/mod/_cli/main.pct.py 34
async def get_formatted_repo_status(config_path, repo_full_name):
    from repoyard.cmds import get_repo_sync_status
    from pydantic import BaseModel
    import json
    sync_status = await get_repo_sync_status(
        config_path=app_state['config_path'],
        repo_full_name=repo_full_name,
    )

    data = {}
    for repo_part, part_sync_status in sync_status.items():
        part_sync_status_dump = part_sync_status._asdict()
        for k, v in part_sync_status_dump.items():
            if isinstance(v, BaseModel):
                part_sync_status_dump[k] = json.loads(v.model_dump_json())
            if isinstance(v, Enum):
                part_sync_status_dump[k] = v.value
        data[repo_part.value] = part_sync_status_dump

    return data

# %% ../../../pts/mod/_cli/main.pct.py 35
@app.command(name='repo-status')
def cli_repo_status(
    repo_path: Path|None = Option(None, "--repo-path", "-p", help="The path to the repository to sync."),
    repo_full_name: str|None = Option(None, "--repo", "-r", help="The full name of the repository, in the form '{ULID}__{REPO_NAME}'."),
    repo_id: str|None = Option(None, "--repo-id", "-i", help="The id of the repository to sync."),
    repo_name: str|None = Option(None, "--repo-name", "-n", help="The name of the repository to sync."),
    name_match_mode: NameMatchMode|None = Option(None, "--name-match-mode", "-m", help="The mode to use for matching the repository name."),
    name_match_case: bool = Option(False, "--name-match-case", "-c", help="Whether to match the repository name case-sensitively."),
    output_format: Literal['text', 'json'] = Option('text', "--output-format", "-o", help="The format of the output."),
    max_concurrent_rclone_ops: int|None = Option(None, "--max-concurrent", help="The maximum number of concurrent rclone operations. If not provided, the default specified in the config will be used."),
):
    """
    Get the sync status of a repository.
    """
    from repoyard._models import get_repoyard_meta
    import json

    if repo_path is not None:
        from repoyard._utils import get_repo_full_name_from_sub_path
        config = get_config(app_state['config_path'])
        repo_full_name = get_repo_full_name_from_sub_path(
            config=config,
            sub_path=repo_path,
        )
    
    repo_full_name = _get_full_repo_name(
        repo_name=repo_name,
        repo_id=repo_id,
        repo_full_name=repo_full_name,
        name_match_mode=name_match_mode,
        name_match_case=name_match_case,
    )
    
    repoyard_meta = get_repoyard_meta(get_config(app_state['config_path']))
    if repo_full_name not in repoyard_meta.by_full_name:
        typer.echo(f"Repository with full name `{repo_full_name}` not found.")
        raise typer.Exit(code=1)
    
    sync_status_data = asyncio.run(get_formatted_repo_status(
        config_path=app_state['config_path'],
        repo_full_name=repo_full_name,
    ))

    if output_format == 'json':
        typer.echo(json.dumps(sync_status_data, indent=2))
    else:
       typer.echo("\n".join(_dict_to_hierarchical_text(sync_status_data)))



# %% ../../../pts/mod/_cli/main.pct.py 37
@app.command(name='yard-status')
def cli_yard_status(
    storage_locations: list[str]|None = Option(None, "--storage-location", "-s", help="The storage location to get the status of. If not provided, the status of all storage locations will be shown."),
    output_format: Literal['text', 'json'] = Option('text', "--output-format", "-o", help="The format of the output."),
    max_concurrent_rclone_ops: int|None = Option(None, "--max-concurrent", "-m", help="The maximum number of concurrent rclone operations. If not provided, the default specified in the config will be used."),
):
    """
    Get the sync status of all repositories in the yard.
    """
    from repoyard._models import get_repoyard_meta
    import json

    config = get_config(app_state['config_path'])
    if storage_locations is None:
        storage_locations = list(config.storage_locations.keys())
    if storage_locations is not None and any(sl not in config.storage_locations for sl in storage_locations):
        typer.echo(f"Invalid storage location: {storage_locations}")
        raise typer.Exit(code=1)

    if max_concurrent_rclone_ops is None:
        max_concurrent_rclone_ops = config.max_concurrent_rclone_ops
    
    repo_metas = [repo_meta for repo_meta in get_repoyard_meta(config).repo_metas if repo_meta.storage_location in storage_locations]

    repo_sync_statuses = asyncio.run(
        async_throttler(
            [get_formatted_repo_status(config, repo_meta.full_name) for repo_meta in repo_metas],
            max_concurrency=max_concurrent_rclone_ops,
        )
    )
    
    repo_sync_statuses_by_sl = {}
    for repo_sync_status, repo_meta in zip(repo_sync_statuses, repo_metas):
        repo_sync_statuses_by_sl.setdefault(repo_meta.storage_location, {})[repo_meta.full_name] = repo_sync_status

    if output_format == 'json':
        typer.echo(json.dumps(repo_sync_statuses_by_sl, indent=2))
    else:
        for sl_name, repo_sync_statuses in repo_sync_statuses_by_sl.items():
            typer.echo(f"{sl_name}:")
            typer.echo("\n".join(_dict_to_hierarchical_text(repo_sync_statuses, indents=1)))
            typer.echo("\n")

# %% ../../../pts/mod/_cli/main.pct.py 39
def _get_filtered_repo_metas(repo_metas, include_groups, exclude_groups, group_filter):
    if include_groups:
        repo_metas = [repo_meta for repo_meta in repo_metas if any(group in repo_meta.groups for group in include_groups)]
    if exclude_groups:
        repo_metas = [repo_meta for repo_meta in repo_metas if not any(group in repo_meta.groups for group in exclude_groups)]
    if group_filter:
        from repoyard._utils.logical_expressions import get_group_filter_func
        _filter_func = get_group_filter_func(group_filter)
        repo_metas = [repo_meta for repo_meta in repo_metas if _filter_func(repo_meta.groups)]
    return repo_metas

# %% ../../../pts/mod/_cli/main.pct.py 40
@app.command(name='list')
def cli_list(
    storage_locations: list[str]|None = Option(None, "--storage-location", "-s", help="The storage location to get the status of. If not provided, the status of all storage locations will be shown."),
    output_format: Literal['text', 'json'] = Option('text', "--output-format", "-o", help="The format of the output."),
    include_groups: list[str]|None = Option(None, "--include-group", "-g", help="The group to include in the output."),
    exclude_groups: list[str]|None = Option(None, "--exclude-group", "-e", help="The group to exclude from the output."),
    group_filter: str|None = Option(None, "--group-filter", "-f", help="The filter to apply to the groups. The filter is a boolean expression over the groups of the repositories. Allowed operators are `AND`, `OR`, `NOT`, and parentheses for grouping.."),
):
    """
    List all repositories in the yard.
    """
    from repoyard._models import get_repoyard_meta
    import json

    config = get_config(app_state['config_path'])
    if storage_locations is None:
        storage_locations = list(config.storage_locations.keys())
    if storage_locations is not None and any(sl not in config.storage_locations for sl in storage_locations):
        typer.echo(f"Invalid storage location: {storage_locations}")
        raise typer.Exit(code=1)

    repo_metas = [repo_meta for repo_meta in get_repoyard_meta(config).repo_metas if repo_meta.storage_location in storage_locations]
    repo_metas = _get_filtered_repo_metas(repo_metas, include_groups, exclude_groups, group_filter)

    if output_format == 'json':
        typer.echo(json.dumps([rm.model_dump() for rm in repo_metas], indent=2))
    else:
        for repo_meta in repo_metas:
            typer.echo(repo_meta.full_name)



# %% ../../../pts/mod/_cli/main.pct.py 42
@app.command(name='list-groups')
def cli_list(
    storage_locations: list[str]|None = Option(None, "--storage-location", "-s", help="The storage location to get the status of. If not provided, the status of all storage locations will be shown."),
):
    from repoyard._models import get_repoyard_meta, get_repo_group_configs
    config = get_config(app_state['config_path'])
    if storage_locations is None:
        storage_locations = list(config.storage_locations.keys())
    repo_metas = [repo_meta for repo_meta in get_repoyard_meta(config).repo_metas if repo_meta.storage_location in storage_locations]
    group_configs = get_repo_group_configs(config, repo_metas)

    for group_name in sorted(group_configs.keys()):
        typer.echo(group_name)

# %% ../../../pts/mod/_cli/main.pct.py 44
@app.command(name='path')
def cli_path(
    repo_full_name: str|None = Option(None, "--repo", "-r", help="The full name of the repository, in the form '{ULID}__{REPO_NAME}'."),
    repo_id: str|None = Option(None, "--repo-id", "-i", help="The id of the repository to sync."),
    repo_name: str|None = Option(None, "--repo-name", "-n", help="What repo path to show."),
    name_match_mode: NameMatchMode|None = Option(None, "--name-match-mode", "-m", help="The mode to use for matching the repository name."),
    name_match_case: bool = Option(False, "--name-match-case", "-c", help="Whether to match the repository name case-sensitively."),
    path_option: Literal[
            'data-user',
            'data',
            'meta',
            'conf',
            'root',
            'sync-record-data',
            'sync-record-meta',
            'sync-record-conf',

        ] = Option('data-user', "--path-option", "-p", help="The part of the repository to get the path of."),
    include_groups: list[str]|None = Option(None, "--include-group", "-g", help="The group to include in the output."),
    exclude_groups: list[str]|None = Option(None, "--exclude-group", "-e", help="The group to exclude from the output."),
    group_filter: str|None = Option(None, "--group-filter", "-f", help="The filter to apply to the groups. The filter is a boolean expression over the groups of the repositories. Allowed operators are `AND`, `OR`, `NOT`, and parentheses for grouping.."),
):
    """
    Get the path of a repository.
    """
    from repoyard.cmds import get_repo_sync_status
    from repoyard._models import get_repoyard_meta
    from pydantic import BaseModel
    import json

    repoyard_meta = get_repoyard_meta(get_config(app_state['config_path']))
    repo_metas = _get_filtered_repo_metas(
        repo_metas=repoyard_meta.repo_metas,
        include_groups=include_groups,
        exclude_groups=exclude_groups,
        group_filter=group_filter,
    )
    
    repo_full_name = _get_full_repo_name(
        repo_name=repo_name,
        repo_id=repo_id,
        repo_full_name=repo_full_name,
        name_match_mode=name_match_mode,
        name_match_case=name_match_case,
        repo_metas=repo_metas,
    )
    
    if repo_full_name not in repoyard_meta.by_full_name:
        typer.echo(f"Repository with full name `{repo_full_name}` not found.")
        raise typer.Exit(code=1)
    repo_meta = repoyard_meta.by_full_name[repo_full_name]  

    config = get_config(app_state['config_path'])

    if path_option == 'data-user':
        typer.echo(repo_meta.get_user_path(config).as_posix())
    elif path_option == 'data':
        typer.echo(repo_meta.get_local_repodata_path(config).as_posix())
    elif path_option == 'meta':
        typer.echo(repo_meta.get_local_repometa_path(config).as_posix())
    elif path_option == 'conf':
        typer.echo(repo_meta.get_local_repoconf_path(config).as_posix())
    elif path_option == 'root':
        typer.echo(config.get_local_path(config).as_posix())
    elif path_option == 'sync-record-data':
        typer.echo(repo_meta.get_local_sync_record_path(config, RepoPart.DATA).as_posix())
    elif path_option == 'sync-record-meta':
        typer.echo(repo_meta.get_local_sync_record_path(config, RepoPart.META).as_posix())
    elif path_option == 'sync-record-conf':
        typer.echo(repo_meta.get_local_sync_record_path(config, RepoPart.CONF).as_posix())
    else:
        typer.echo(f"Invalid path option: {path_option}")
        raise typer.Exit(code=1)

# %% ../../../pts/mod/_cli/main.pct.py 46
@app.command(name='create-user-symlinks')
def cli_create_user_symlinks(
    user_repos_path: Path|None = Option(None, "--user-repos-path", "-u", help="The path to the user repositories. If not provided, the default specified in the config will be used."),
    user_repo_groups_path: Path|None = Option(None, "--user-repo-groups-path", "-g", help="The path to the user repository groups. If not provided, the default specified in the config will be used."),
):
    """
    Create symlinks to the user repositories in the user repositories path.
    """
    from repoyard.cmds import create_user_symlinks
    create_user_symlinks(
        config_path=app_state['config_path'],
        user_repos_path=user_repos_path,
        user_repo_groups_path=user_repo_groups_path,
    )
