# Hardening CLI And Sync Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make Gitea mirror fail reliably on API/git/config/path errors and expose a proper package CLI entrypoint. **Architecture:** Keep the existing module layout, but move executable orchestration into an importable `src.cli` module. Preserve `gitea-mirror.py` as a compatibility wrapper while making `process_repo` return success/failure for aggregate exit handling. **Tech Stack:** Python 3.12, requests, pydantic v2, pytest, ruff, uv. --- ### Task 1: API Errors Are Fatal **Files:** - Modify: `src/gitea_api.py` - Test: `tests/test_gitea_api.py` - [ ] **Step 1: Write failing tests** ```python def test_get_repositories_raises_on_http_error(monkeypatch): api = GiteaApi(endpoint="https://example.com", api_token="token") session = FakeSession([FakeResponse(status_code=500, data=[])]) monkeypatch.setattr(requests, "session", lambda: session) with pytest.raises(GiteaApiError): api.get_repositories() ``` - [ ] **Step 2: Run test to verify it fails** Run: `uv run pytest tests/test_gitea_api.py -v` Expected: FAIL because `GiteaApiError` does not exist or is not raised. - [ ] **Step 3: Implement minimal code** Add `GiteaApiError`, request timeout, network exception handling, JSON/schema exception handling, and raise instead of returning `[]`. - [ ] **Step 4: Run test to verify it passes** Run: `uv run pytest tests/test_gitea_api.py -v` Expected: PASS. ### Task 2: Git Failures Affect Exit Code **Files:** - Create: `src/cli.py` - Modify: `gitea-mirror.py` - Test: `tests/test_mirror_process.py`, `tests/test_cli.py` - [ ] **Step 1: Write failing tests** ```python def test_process_repo_returns_false_when_clone_fails(monkeypatch, tmp_path): monkeypatch.setattr(cli, "git_clone", lambda **kwargs: False) assert not cli.process_repo(config=make_config(tmp_path, FetchConfig(ssh_key="/tmp/key")), repo=make_repo()) ``` - [ ] **Step 2: Run test to verify it fails** Run: `uv run pytest tests/test_mirror_process.py tests/test_cli.py -v` Expected: FAIL because `process_repo` currently returns `None` and no importable CLI module exists. - [ ] **Step 3: Implement minimal code** Move orchestration to `src.cli`, return booleans from `process_repo`, aggregate failures in `run`, and make `main` return non-zero for failures. - [ ] **Step 4: Run test to verify it passes** Run: `uv run pytest tests/test_mirror_process.py tests/test_cli.py -v` Expected: PASS. ### Task 3: Validate Repository Paths And Config **Files:** - Modify: `src/models.py` - Modify: `src/repository_name.py` - Test: `tests/test_config.py`, `tests/test_repository_name.py` - [ ] **Step 1: Write failing tests** ```python def test_output_path_rejects_parent_traversal(tmp_path): repo = make_repo() with pytest.raises(ValueError): get_repository_path(str(tmp_path), "../{name}", repo) ``` - [ ] **Step 2: Run test to verify it fails** Run: `uv run pytest tests/test_config.py tests/test_repository_name.py -v` Expected: FAIL because validation is missing. - [ ] **Step 3: Implement minimal code** Add repository format validation in `Config`, log level validation in `LoggingConfig`, and a safe output path helper that rejects absolute paths and paths escaping `out_dir`. - [ ] **Step 4: Run test to verify it passes** Run: `uv run pytest tests/test_config.py tests/test_repository_name.py -v` Expected: PASS. ### Task 4: Package CLI Entrypoint **Files:** - Modify: `pyproject.toml` - Modify: `gitea-mirror.py` - Test: `tests/test_cli.py` - [ ] **Step 1: Write failing test** ```python def test_project_defines_console_script(): data = tomllib.loads(Path("pyproject.toml").read_text()) assert data["project"]["scripts"]["gitea-mirror"] == "src.cli:main" ``` - [ ] **Step 2: Run test to verify it fails** Run: `uv run pytest tests/test_cli.py -v` Expected: FAIL because `[project.scripts]` is absent. - [ ] **Step 3: Implement minimal code** Add `[project.scripts] gitea-mirror = "src.cli:main"` and make `gitea-mirror.py` delegate to `src.cli.main`. - [ ] **Step 4: Run full verification** Run: `make check` Expected: PASS.