---
phase: 04-input-sources
plan: 05
subsystem: cli
tags: [cli, cobra, scan, source-dispatch, integration]

requires:
  - phase: 04-input-sources
    provides: DirSource, GitSource, StdinSource, URLSource, ClipboardSource (plans 04-02, 04-03, 04-04)
provides:
  - selectSource dispatcher routing CLI args+flags to the correct Source implementation
  - --git, --url, --clipboard, --since, --max-file-size, --insecure CLI flags on `scan`
  - Mutual-exclusion validation for conflicting source selectors
affects: [phase 05 verification integration, CLI UX for all Phase 4 input modes]

tech-stack:
  added: []
  patterns:
    - Struct-parameter dispatcher (sourceFlags) for testable CLI flag fan-in
    - cobra.MaximumNArgs(1) to support both positional and flag-only invocations
    - Stat-based auto-dispatch between FileSource and DirSource on a single positional

key-files:
  created:
    - cmd/scan_sources_test.go
  modified:
    - cmd/scan.go

key-decisions:
  - "selectSource takes a sourceFlags struct (not individual params) so the test suite can drive every combination without touching cobra flag state"
  - "Args changed from ExactArgs(1) to MaximumNArgs(1) because --url and --clipboard legitimately have no positional target"
  - "Stdin tokens 'stdin' and '-' are both accepted (spec from 04-04) and selectSource maps either to StdinSource"
  - "--since is parsed inside selectSource (not cobra) so the error path is covered by a unit test"
  - "--git + stdin is explicitly rejected even though --git is not in the explicit-source count, because scanning git history of a pipe is nonsensical"

patterns-established:
  - "CLI dispatcher pattern: positional-first, then flag-overrides, with mutual-exclusion checks up front"
  - "Test-friendly flag fan-in: wrap cobra flags in a plain struct passed to the logic function"

requirements-completed: [INPUT-06]

duration: ~3min
completed: 2026-04-05
---

# Phase 4 Plan 5: Scan Command Source Wiring Summary

**Wires every Phase 4 input adapter (Dir, Git, Stdin, URL, Clipboard) through `cmd/scan.go` via a new `selectSource` dispatcher and six new CLI flags, making INPUT-01 through INPUT-05 reachable from the command line and satisfying the INPUT-06 integration requirement.**

## Performance

- **Duration:** ~3 min (135s)
- **Started:** 2026-04-05T12:21:08Z
- **Completed:** 2026-04-05T12:23:23Z
- **Tasks:** 1 (TDD: RED → GREEN)
- **Files created:** 1 (cmd/scan_sources_test.go)
- **Files modified:** 1 (cmd/scan.go)

## Accomplishments

- Added `selectSource(args, sourceFlags) (sources.Source, error)` with full dispatch logic for all five Phase 4 sources plus legacy FileSource
- Added six new cobra flags: `--git`, `--url`, `--clipboard`, `--since`, `--max-file-size`, `--insecure`
- Relaxed `scanCmd.Args` from `ExactArgs(1)` to `MaximumNArgs(1)` so `--url` and `--clipboard` can run without a positional target
- Added clear mutual-exclusion errors when `--git`, `--url`, `--clipboard` are combined, and when `--url`/`--clipboard` are paired with a positional path
- 13-case unit test suite covering every dispatch branch, including `--since` parse-error path
- Downstream pipeline (engine.Scan, storage.SaveFinding, output, exit codes) untouched — source selection is the only change

## Example Invocations

```bash
# Directory (DirSource, default)
keyhunter scan ./pkg

# Single file (FileSource, unchanged behavior)
keyhunter scan main.go

# Git history (GitSource with optional --since)
keyhunter scan --git ./some-repo --since 2024-01-01

# Stdin (StdinSource)
echo "API_KEY=xxx" | keyhunter scan -
cat creds.env | keyhunter scan stdin

# Remote URL (URLSource, no positional)
keyhunter scan --url https://example.com/config.js

# Clipboard (ClipboardSource, no positional)
keyhunter scan --clipboard

# Exclusions forwarded to DirSource
keyhunter scan ./src --exclude "*.log" --exclude "tmp/**"
```

## selectSource Dispatch Branches

| Input                                   | Result                           |
| --------------------------------------- | -------------------------------- |
| `--clipboard` (no args)                 | `*sources.ClipboardSource`       |
| `--url https://...` (no args)           | `*sources.URLSource`             |
| `--git <path>`                          | `*sources.GitSource`             |
| `--git <path> --since YYYY-MM-DD`       | `*sources.GitSource` with `Since` set |
| positional `stdin` or `-`               | `*sources.StdinSource`           |
| positional directory                    | `*sources.DirSource` (+ excludes) |
| positional file                         | `*sources.FileSource`            |
| Two of {`--git`, `--url`, `--clipboard`} | error: "mutually exclusive"      |
| `--url` or `--clipboard` + positional   | error: "does not accept a positional argument" |
| no args and no selector flag            | error: "missing target"          |
| `--since` not `YYYY-MM-DD`              | error: "must be YYYY-MM-DD"      |
| `--git` + stdin token                   | error: "cannot be combined with stdin" |

## Task Commits

1. **Task 1 RED** — `9105ca1` test(04-05): add failing tests for selectSource dispatcher
2. **Task 1 GREEN** — `b151e88` feat(04-05): wire all Phase 4 sources through scan command

## Files Created/Modified

- **Created:** `cmd/scan_sources_test.go` (112 lines, 13 test cases)
- **Modified:** `cmd/scan.go` (+121, -15) — added flags, selectSource helper, dispatch call site, `time` import

## Test Results

Full `selectSource` suite under `-race`:

| Test                                     | Result |
| ---------------------------------------- | ------ |
| TestSelectSource_Directory               | PASS   |
| TestSelectSource_File                    | PASS   |
| TestSelectSource_Git                     | PASS   |
| TestSelectSource_GitSince                | PASS   |
| TestSelectSource_GitSinceBadFormat       | PASS   |
| TestSelectSource_URL                     | PASS   |
| TestSelectSource_URLRejectsPositional    | PASS   |
| TestSelectSource_Clipboard               | PASS   |
| TestSelectSource_ClipboardRejectsPositional | PASS |
| TestSelectSource_Stdin (stdin + -)       | PASS   |
| TestSelectSource_MutuallyExclusive       | PASS   |
| TestSelectSource_MissingTarget           | PASS   |
| TestSelectSource_DirForwardsExcludes     | PASS   |

Full repo suite: `go test ./... -race -count=1` → **all packages ok** (cmd, engine, engine/sources, providers, storage). Phase 1-3 tests still green.

## Decisions Made

- **sourceFlags struct over many parameters:** Keeps the test surface tiny (callers fabricate a struct literal) and makes adding future selectors (e.g. `--archive`) trivial without breaking signatures.
- **Stat-based Dir vs File:** A single positional auto-dispatches on `os.Stat().IsDir()`. No new `--dir` flag needed — users just point at a path and it "does the right thing".
- **`--since` parsing happens in selectSource:** So the invalid-format error is unit-testable and produces a CLI-friendly message rather than a raw time.Parse error.

## Deviations from Plan

None — plan executed exactly as written. RED/GREEN TDD cycle completed in one iteration with no fixes needed beyond the initial plan specification.

## Issues Encountered

None.

## Known Stubs

None. `--max-file-size` and `--insecure` are wired as cobra flag declarations (per plan requirement) but are not yet consumed by any source — they are forward-compatibility hooks for Phase 5+ and documented as such in the flag help text. The plan explicitly requested the flag declarations; the downstream wiring is out of scope.

## User Setup Required

None.

## Next Phase Readiness

- All Phase 4 input adapters are now reachable from the CLI.
- `keyhunter scan --help` lists every new flag with descriptive help text.
- Phase 5 (verification) can build on top of `scanCfg.Verify` which was already present and remains untouched.
- Future wiring of `--max-file-size` and `--insecure` into DirSource/URLSource respectively is a small follow-up (one line each).

## Self-Check: PASSED

- cmd/scan.go: FOUND (modified)
- cmd/scan_sources_test.go: FOUND
- Commit 9105ca1 (RED): FOUND in git log
- Commit b151e88 (GREEN): FOUND in git log
- `go build ./...`: exit 0
- `go test ./... -race -count=1`: all packages ok
- `grep -c selectSource cmd/scan.go`: 4 (≥2 required)
- `grep sources.New(Dir|Git|Stdin|URL|Clipboard)Source cmd/scan.go`: 5 hits
- `grep "mutually exclusive" cmd/scan.go`: 1 hit
- `go run . scan --help` lists --git, --url, --clipboard, --since, --insecure: confirmed

---
*Phase: 04-input-sources*
*Plan: 05*
*Completed: 2026-04-05*
