# Plan: read target URLs from a file for hxprobe ## Context `hxprobe` currently only accepts URLs as positional CLI arguments (`hxprobe/hxprobe/cli.py:368`, `nargs="+"`). The user wants a file-based input mode too, matching the existing convention `simple.py`/`phases.py` already use (`python/simple.py:20-30`'s `load_sites()`: plain text, one URL per line, `#`-comments and blank lines skipped, first whitespace-separated token taken per line). User-confirmed decisions: - **Mutually exclusive** with positional URL args (either pass URLs on the command line, or `-f FILE`, never both). - **hxprobe only** — `latprobe` is intentionally left untouched. - **Add example fixture files** (`hxprobe/configs/*.txt`, mirroring `python/configs/*.txt`'s exact set: all-ok, dns-failure, connection-refused, timeout, tls-errors, http-errors, mixed) plus one new section in the existing `hxprobe/USAGE.md` demonstrating the flag. ## Implementation **`hxprobe/hxprobe/cli.py`:** - `urls` positional becomes `nargs="*"` (was `nargs="+"`) — no longer required on its own, since `-f` is now a second valid source. - New flag: `-f, --file PATH` — "read URLs from a file, one per line, `#` comments allowed (mutually exclusive with positional url args)". Placed right after the `urls` positional definition in the argparse block, since the two are the two ways of specifying what to probe. - New helper `_load_urls(path: str) -> list[str]`, duplicating (not importing) `simple.py`'s `load_sites()` logic — consistent with hxprobe's established "imports nothing outside its own directory" rule from the standalone-extraction work. - After `parser.parse_args()`, manual validation (mirrors the existing `--timeout` invalid-value handling style — write to the injected `stderr`, `return EXIT_USAGE`, rather than routing through `argparse`'s mutually-exclusive-group machinery, which doesn't mix cleanly with a variadic positional): - both `ns.urls` and `ns.file` given → `parser.error(...)` (usage error, consistent with how `_Parser.error()` already handles bad usage) - neither given → `parser.error(...)` - `ns.file` given but unreadable (`FileNotFoundError`/`OSError`) → `stderr.write(...)`; `return EXIT_USAGE` - `ns.file` given but yields zero URLs → same treatment - otherwise `urls = _load_urls(ns.file)` or `urls = ns.urls` **`hxprobe/tests/test_cli.py`:** new hermetic tests — successful multi-URL run from a file, missing-file error, empty-file error, and the both-sources-given usage error. Uses a temp file (`tempfile`), no network needed for the parsing-error cases. **`hxprobe/configs/*.txt`** (new directory) — same 7 fixtures as `python/configs/`, adapted: - `all-ok.txt`, `dns-failure.txt`, `connection-refused.txt`, `tls-errors.txt` — same URLs, same behavior (DNS/TCP/TLS failures are identical regardless of HTTP client sophistication); only the header comments change (`hxprobe -f configs/.txt` instead of `python3.14 python/simple.py ...`). - `timeout.txt` — same two targets (`10.255.255.1`, `192.0.2.1`, RFC 5737 TEST-NET-1) as the original; "expected exit code" documents normal-network behavior (exit 4), same caveat the original file already carries about network-dependent behavior. - `http-errors.txt` — same 404 URLs; header comment updated to show the demo command with `--fail` (hxprobe treats 4xx as success without `--fail`, unlike `simple.py`, which always raises on HTTPError) — "expected exit code" becomes 6, not `simple.py`'s blanket 1. - `mixed.txt` — same mixed set; demo command includes `--fail`; expected exit code recalculated as the worst code across the included classes (dns=2, connect=3, tls=5, http=6 with `--fail`) → 6. - Each header's "Expected exit code" will be verified by actually running the fixture through `hxprobe -f ...` during implementation, not assumed from the `simple.py` originals — hxprobe's worst-code-wins exit scheme (`hxprobe/hxprobe/cli.py:455-481`) differs fundamentally from `simple.py`'s blanket 0/1. **`hxprobe/USAGE.md`:** new section "Reading URLs from a file (`-f`)", placed after the "Multiple URLs" case (same family of "what to probe" examples) — command + real captured output using `configs/all-ok.txt` or `configs/mixed.txt`, plus a short list of the other fixture files available and what each demonstrates. **Docs housekeeping** (per CLAUDE.md convention): save this plan under `docs/plans/`, write a summary under `docs/summaries/` after implementation, append a `CHANGELOG.md` entry. ## Verification 1. `cd hxprobe && uv run python -m hxprobe -f configs/all-ok.txt` — probes all 3 URLs, exit 0. 2. `cd hxprobe && uv run python -m hxprobe -f configs/dns-failure.txt` — exit 2; `connection-refused.txt` → exit 3; `tls-errors.txt` → exit 5; `--fail -f configs/http-errors.txt` → exit 6; `--fail -f configs/mixed.txt` → exit 6 (confirms the worst-code documented in each header is accurate). 3. `uv run python -m hxprobe https://example.com -f configs/all-ok.txt` — usage error (both sources given). 4. `uv run python -m hxprobe -f /no/such/file` — usage error, clear message. 5. `cd hxprobe && make check` — new hermetic tests pass alongside the existing 28. 6. Re-run the new `hxprobe/USAGE.md` section's command and confirm captured output matches what's printed in the doc. 7. `grep -n "import" hxprobe/hxprobe/cli.py` — confirm no new import from `python/simple.py` or anywhere outside `hxprobe/`.