# Config File Support Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace CLI credential options with TOML config file support. **Architecture:** Read Jenkins server configurations from `~/.config/zzpyjenkins/config.toml`. CLI uses `-s/--server` option to select which configured server to use. **Tech Stack:** Python 3.11+ (tomllib built-in), Click, Rich --- ## File Structure | File | Action | Responsibility | |------|--------|----------------| | `src/zzpyjenkins/config.py` | Create | Load and parse TOML config file | | `src/zzpyjenkins/cli.py` | Modify | Replace credential options with `--server` option | | `tests/test_config.py` | Create | Test config loading and validation | --- ## Chunk 1: Config Module ### Task 1: Config Loading **Files:** - Create: `src/zzpyjenkins/config.py` - Create: `tests/test_config.py` - [ ] **Step 0: Create tests directory** ```bash mkdir -p tests touch tests/__init__.py ``` - [ ] **Step 1: Write the failing test for load_config with missing file** ```python # tests/test_config.py """Tests for config module.""" import pytest from pathlib import Path from zzpyjenkins.config import load_config, ConfigError def test_load_config_file_not_found(): """Test load_config raises error when config file doesn't exist.""" with pytest.raises(ConfigError) as exc_info: load_config(Path("/nonexistent/path/config.toml")) assert "Config file not found" in str(exc_info.value) ``` - [ ] **Step 2: Run test to verify it fails** Run: `uv run pytest tests/test_config.py -v` Expected: FAIL with "ModuleNotFoundError" or "ImportError" - [ ] **Step 3: Write minimal implementation - ConfigError and load_config skeleton** ```python # src/zzpyjenkins/config.py """Configuration file handling.""" from pathlib import Path import tomllib from rich.console import Console console = Console() class ConfigError(Exception): """Raised when configuration is invalid or missing.""" pass def load_config(config_path: Path) -> dict: """Load and parse TOML config file. Args: config_path: Path to config.toml file Returns: Dictionary of server configurations Raises: ConfigError: If file doesn't exist or is invalid """ if not config_path.exists(): raise ConfigError(f"Config file not found: {config_path}") try: with open(config_path, "rb") as f: return tomllib.load(f) except tomllib.TOMLDecodeError as e: raise ConfigError(f"Invalid TOML syntax: {e}") from e ``` - [ ] **Step 4: Run test to verify it passes** Run: `uv run pytest tests/test_config.py -v` Expected: PASS - [ ] **Step 4.5: Add test for invalid TOML syntax** ```python # Add to tests/test_config.py import tempfile def test_load_config_invalid_toml(): """Test load_config raises error for invalid TOML syntax.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write("invalid toml [[[[") temp_path = Path(f.name) try: with pytest.raises(ConfigError) as exc_info: load_config(temp_path) assert "Invalid TOML syntax" in str(exc_info.value) finally: temp_path.unlink() ``` - [ ] **Step 5: Commit** ```bash git add src/zzpyjenkins/config.py tests/test_config.py git commit -m "feat: add config module with load_config function" ``` --- ### Task 2: Get Server Config **Files:** - Modify: `src/zzpyjenkins/config.py` - Modify: `tests/test_config.py` - [ ] **Step 1: Write the failing test for get_server_config** ```python # Add to tests/test_config.py # Update import to include get_server_config: from zzpyjenkins.config import load_config, ConfigError, get_server_config def test_get_server_config_success(): """Test getting a valid server config.""" config = { "work": { "url": "https://jenkins.example.com", "username": "user", "password": "pass" } } result = get_server_config(config, "work") assert result == ("https://jenkins.example.com", "user", "pass") def test_get_server_config_not_found(): """Test error when server name doesn't exist.""" config = { "work": { "url": "https://jenkins.example.com", "username": "user", "password": "pass" } } with pytest.raises(ConfigError) as exc_info: get_server_config(config, "home") assert "Server 'home' not found" in str(exc_info.value) assert "work" in str(exc_info.value) def test_get_server_config_missing_field(): """Test error when required field is missing.""" config = { "incomplete": { "url": "https://jenkins.example.com", "username": "user" # missing password } } with pytest.raises(ConfigError) as exc_info: get_server_config(config, "incomplete") assert "password" in str(exc_info.value) ``` - [ ] **Step 2: Run test to verify it fails** Run: `uv run pytest tests/test_config.py -v` Expected: FAIL with "NameError: name 'get_server_config' is not defined" - [ ] **Step 3: Write implementation for get_server_config** ```python # Add to src/zzpyjenkins/config.py REQUIRED_FIELDS = ["url", "username", "password"] def get_server_config(config: dict, server_name: str) -> tuple[str, str, str]: """Extract server configuration from loaded config. Args: config: Loaded config dictionary server_name: Name of server section to use Returns: Tuple of (url, username, password) Raises: ConfigError: If server not found or missing required fields """ if server_name not in config: available = ", ".join(config.keys()) raise ConfigError( f"Server '{server_name}' not found. Available servers: {available}" ) server_config = config[server_name] for field in REQUIRED_FIELDS: if field not in server_config: raise ConfigError( f"Missing required field '{field}' in server '{server_name}'" ) return ( server_config["url"], server_config["username"], server_config["password"], ) ``` - [ ] **Step 4: Run test to verify it passes** Run: `uv run pytest tests/test_config.py -v` Expected: PASS - [ ] **Step 5: Commit** ```bash git add src/zzpyjenkins/config.py tests/test_config.py git commit -m "feat: add get_server_config function with validation" ``` --- ### Task 3: Default Config Path **Files:** - Modify: `src/zzpyjenkins/config.py` - Modify: `tests/test_config.py` - [ ] **Step 1: Write the failing test for get_default_config_path** ```python # Add to tests/test_config.py # Update import to include get_default_config_path: from zzpyjenkins.config import ( load_config, ConfigError, get_server_config, get_default_config_path, ) def test_get_default_config_path(): """Test default config path is correct.""" path = get_default_config_path() assert path.name == "config.toml" assert ".config" in str(path) assert "zzpyjenkins" in str(path) ``` - [ ] **Step 2: Run test to verify it fails** Run: `uv run pytest tests/test_config.py::test_get_default_config_path -v` Expected: FAIL with "NameError: name 'get_default_config_path' is not defined" - [ ] **Step 3: Write implementation for get_default_config_path** ```python # Add to src/zzpyjenkins/config.py import os def get_default_config_path() -> Path: """Get the default config file path. Returns: Path to ~/.config/zzpyjenkins/config.toml """ xdg_config = os.environ.get("XDG_CONFIG_HOME") if xdg_config: config_dir = Path(xdg_config) else: config_dir = Path.home() / ".config" return config_dir / "zzpyjenkins" / "config.toml" ``` - [ ] **Step 4: Run test to verify it passes** Run: `uv run pytest tests/test_config.py::test_get_default_config_path -v` Expected: PASS - [ ] **Step 5: Commit** ```bash git add src/zzpyjenkins/config.py tests/test_config.py git commit -m "feat: add get_default_config_path function" ``` --- ## Chunk 2: CLI Integration ### Task 4: Update CLI Options **Files:** - Modify: `src/zzpyjenkins/cli.py` - [ ] **Step 1: Update imports and add server option** Replace the current imports and main function with: ```python # src/zzpyjenkins/cli.py """Command-line interface for zzpyjenkins.""" from typing import Optional import click from rich.console import Console from .config import ConfigError, get_default_config_path, get_server_config, load_config from .jenkins_client import JenkinsClient console = Console() @click.group() @click.option("--server", "-s", required=True, help="Server name from config file") @click.version_option() @click.pass_context def main(ctx: click.Context, server: str): """A CLI tool to interact with Jenkins server for building jobs.""" ctx.ensure_object(dict) ctx.obj["server"] = server ``` - [ ] **Step 2: Update get_client function** Replace the current get_client function with: ```python # src/zzpyjenkins/cli.py (replace get_client function) def get_client(ctx: click.Context) -> JenkinsClient: """Create Jenkins client from config file.""" server_name = ctx.obj.get("server") if not server_name: console.print("[red]Error: Server name is required. Use -s/--server option.[/red]") raise SystemExit(1) config_path = get_default_config_path() try: config = load_config(config_path) url, username, password = get_server_config(config, server_name) except ConfigError as e: console.print(f"[red]Error: {e}[/red]") raise SystemExit(1) return JenkinsClient(url, username, password) ``` - [ ] **Step 3: Remove old environment variable references** Remove the `import os` line (no longer needed) and any remaining references to environment variables. - [ ] **Step 4: Run tests to verify existing functionality still works** Run: `uv run pytest tests/ -v` Expected: All tests pass - [ ] **Step 5: Commit** ```bash git add src/zzpyjenkins/cli.py git commit -m "feat: replace credential options with --server config option" ``` --- ## Chunk 3: Integration Test and Finalization ### Task 5: Integration Test **Files:** - Modify: `tests/test_config.py` - [ ] **Step 1: Write integration test with temp config file** ```python # Add to tests/test_config.py # tempfile is already imported in Task 1 def test_full_config_flow(): """Test complete config loading flow with temp file.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" [work] url = "https://jenkins.example.com" username = "testuser" password = "testpass" """) temp_path = Path(f.name) try: config = load_config(temp_path) url, username, password = get_server_config(config, "work") assert url == "https://jenkins.example.com" assert username == "testuser" assert password == "testpass" finally: temp_path.unlink() ``` - [ ] **Step 2: Run all tests** Run: `uv run pytest tests/ -v` Expected: All tests pass - [ ] **Step 3: Commit** ```bash git add tests/test_config.py git commit -m "test: add integration test for config flow" ``` --- ### Task 6: Update CLAUDE.md **Files:** - Modify: `CLAUDE.md` - [ ] **Step 1: Update Configuration section** Replace the Configuration section with: ```markdown ## Configuration Credentials are read from `~/.config/zzpyjenkins/config.toml`. Create this file with your Jenkins server configurations: ```toml [work] url = "https://jenkins.work.com" username = "myuser" password = "mypassword" [home] url = "http://localhost:8080" username = "admin" password = "admin123" ``` Use `-s/--server` option to select which server to use: ```bash zzpyjenkins -s work info zzpyjenkins --server home list ``` ``` - [ ] **Step 2: Commit** ```bash git add CLAUDE.md git commit -m "docs: update CLAUDE.md with config file usage" ``` --- ### Task 7: Run Lint and Format - [ ] **Step 1: Format code** Run: `uv run ruff format .` - [ ] **Step 2: Lint code** Run: `uv run ruff check . --fix` - [ ] **Step 3: Run all tests** Run: `uv run pytest tests/ -v` Expected: All tests pass - [ ] **Step 4: Commit any formatting changes** ```bash git add -A git commit -m "style: format and lint code" ``` --- ## Summary | Task | Description | |------|-------------| | 1 | Create config module with `load_config` | | 2 | Add `get_server_config` with validation | | 3 | Add `get_default_config_path` | | 4 | Update CLI to use `--server` option | | 5 | Integration test | | 6 | Update documentation | | 7 | Lint and format |