From 3664e9880122677aa8895462e700bd9cb52ebfdb Mon Sep 17 00:00:00 2001 From: tech Date: Wed, 18 Mar 2026 13:25:06 +0800 Subject: [PATCH] docs: Add config file support implementation plan Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-03-18-config-file-support.md | 494 ++++++++++++++++++ 1 file changed, 494 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-18-config-file-support.md diff --git a/docs/superpowers/plans/2026-03-18-config-file-support.md b/docs/superpowers/plans/2026-03-18-config-file-support.md new file mode 100644 index 0000000..9897860 --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-config-file-support.md @@ -0,0 +1,494 @@ +# 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 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 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 + +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 + +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 + +import tempfile + + +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 |