Load daemon configuration from a TOML file. Args: path: Explicit config path. Falls back to :data:`CONFIG_PATH`. Returns: A fully-validated :class:`DaemonConfig`. Raises: RuntimeError: If ``tomllib`` / ``tomli`` is unavailable on Python < 3.11.
(path: Path | None = None)
| 82 | |
| 83 | |
| 84 | def load_config(path: Path | None = None) -> DaemonConfig: |
| 85 | """Load daemon configuration from a TOML file. |
| 86 | |
| 87 | Args: |
| 88 | path: Explicit config path. Falls back to :data:`CONFIG_PATH`. |
| 89 | |
| 90 | Returns: |
| 91 | A fully-validated :class:`DaemonConfig`. |
| 92 | |
| 93 | Raises: |
| 94 | RuntimeError: If ``tomllib`` / ``tomli`` is unavailable on Python < 3.11. |
| 95 | """ |
| 96 | if tomllib is None: |
| 97 | raise RuntimeError( |
| 98 | "TOML parsing requires the 'tomli' package on Python < 3.11. " |
| 99 | "Install it with: pip install tomli" |
| 100 | ) |
| 101 | |
| 102 | config_path = path or CONFIG_PATH |
| 103 | |
| 104 | if not config_path.exists(): |
| 105 | logger.info("Config file not found at %s — using defaults", config_path) |
| 106 | return DaemonConfig() |
| 107 | |
| 108 | with open(config_path, "rb") as fh: |
| 109 | raw: dict[str, Any] = tomllib.load(fh) |
| 110 | |
| 111 | # -- [daemon] section --------------------------------------------------- |
| 112 | daemon_section: dict[str, Any] = raw.get("daemon", {}) |
| 113 | session_name: str = daemon_section.get("session_name", "crg-watch") |
| 114 | log_dir = Path(daemon_section.get("log_dir", str(DaemonConfig().log_dir))) |
| 115 | poll_interval: int = int(daemon_section.get("poll_interval", 2)) |
| 116 | |
| 117 | # -- [[repos]] array ---------------------------------------------------- |
| 118 | repos: list[WatchRepo] = [] |
| 119 | seen_aliases: set[str] = set() |
| 120 | |
| 121 | for entry in raw.get("repos", []): |
| 122 | repo_path_str: str = entry.get("path", "") |
| 123 | if not repo_path_str: |
| 124 | logger.warning("Skipping repo entry with empty path") |
| 125 | continue |
| 126 | |
| 127 | repo_path = Path(repo_path_str).expanduser().resolve() |
| 128 | |
| 129 | if not repo_path.is_dir(): |
| 130 | logger.warning("Skipping repo %s — directory does not exist", repo_path) |
| 131 | continue |
| 132 | |
| 133 | has_repo_marker = ( |
| 134 | (repo_path / ".git").exists() |
| 135 | or (repo_path / ".svn").exists() |
| 136 | or (repo_path / ".code-review-graph").exists() |
| 137 | ) |
| 138 | if not has_repo_marker: |
| 139 | logger.warning( |
| 140 | "Skipping repo %s — no .git, .svn, or .code-review-graph directory found", |
| 141 | repo_path, |