# Credential Helper Rewrite ## Context The current credential helper (`cmd/credential-helper/main.go`, ~1070 lines) is a monolithic single-file binary with a manual `switch` dispatch. It has no help text, hangs silently when run without stdin, embeds interactive device auth inside the Docker protocol `get` command (blocking pushes for up to 2 minutes while polling), and only supports one account per registry. Users want multi-account support (e.g., `evan.jarrett.net` and `michelle.jarrett.net` on the same `atcr.io`) and multi-registry support (e.g., `atcr.io` + `buoy.cr`). ## Approach Rewrite using **Cobra** (already a project dependency) for the CLI framework and **charmbracelet/huh** for interactive prompts (select menus, confirmations, spinners). Separate Docker protocol commands (machine-readable, hidden) from user-facing commands (interactive, discoverable). Model after `gh auth` UX patterns. **Smart account auto-detection**: The `get` command inspects the parent process command line (`/proc//cmdline` on Linux, `ps` on macOS) to determine which image Docker is pushing/pulling. Since ATCR URLs are `host//repo:tag`, we can extract the identity and auto-select the matching account — no prompts, no manual switching needed in the common case. ## Command Tree ``` docker-credential-atcr ├── get (Docker protocol — stdin/stdout, hidden, smart account detection) ├── store (Docker protocol — stdin, hidden) ├── erase (Docker protocol — stdin, hidden) ├── list (Docker protocol extension, hidden) ├── login (Interactive device flow with huh prompts) ├── logout (Remove account credentials) ├── status (Show all accounts with active indicators) ├── switch (Switch active account — auto-toggle for 2, select for 3+) ├── configure-docker (Auto-edit ~/.docker/config.json credHelpers) ├── update (Self-update, existing logic preserved) └── version (Built-in via cobra) ``` ## Smart Account Resolution (`get` command) The `get` command resolves which account to use with this priority chain — fully non-interactive: ``` 1. Parse parent process cmdline → extract identity from image ref docker push atcr.io/evan.jarrett.net/test:latest → parent cmdline contains "evan.jarrett.net" → use that account 2. Fall back to active account (set by `switch` command) 3. Fall back to sole account (if only one exists for this registry) 4. Error with helpful message: "Multiple accounts for atcr.io. Run: docker-credential-atcr switch" ``` **Parent process detection** (in `helpers.go`): - Linux: read `/proc//cmdline` (null-separated args) - macOS: `ps -o args= -p ` - Windows: best-effort via `wmic` or skip (fall to active account) - Parse image ref: find the arg matching `//...`, extract `` - Graceful failure: if parent isn't Docker, cmdline unreadable, or image ref not parseable → fall through to active account ## File Structure ``` cmd/credential-helper/ main.go — Cobra root command, version vars, subcommand registration config.go — Config types, load/save/migrate, getConfigPath device_auth.go — authorizeDevice(), validateCredentials() HTTP logic protocol.go — Docker protocol: get, store, erase, list (all hidden) cmd_login.go — login command (huh prompts + device flow) cmd_logout.go — logout command (huh confirm) cmd_status.go — status display cmd_switch.go — switch command (huh select) cmd_configure.go — configure-docker (edit ~/.docker/config.json) cmd_update.go — update command (moved from existing code) helpers.go — openBrowser, buildAppViewURL, isInsecureRegistry, parentCmdline, etc. ``` ## Config Format (`~/.atcr/device.json`) ```json { "version": 2, "registries": { "https://atcr.io": { "active": "evan.jarrett.net", "accounts": { "evan.jarrett.net": { "handle": "evan.jarrett.net", "did": "did:plc:abc123", "device_secret": "atcr_device_..." }, "michelle.jarrett.net": { "handle": "michelle.jarrett.net", "did": "did:plc:def456", "device_secret": "atcr_device_..." } } }, "https://buoy.cr": { "active": "evan.jarrett.net", "accounts": { ... } } } } ``` **Migration**: `loadConfig()` auto-detects and migrates from old formats: - Legacy single-device `{handle, device_secret, appview_url}` → v2 - Current multi-registry `{credentials: {url: {...}}}` → v2 - Writes back migrated config on first load ## Key Behavioral Changes | Command | Current | New | |---------|---------|-----| | `get` | Opens browser, polls 2min if no creds | Smart detection → active account → error | | `get` (multi-account) | N/A (single account only) | Auto-detects identity from parent cmdline | | `get` (no stdin) | Hangs forever | Detects terminal, prints help, exits 1 | | `get` (OAuth expired) | Auto-opens browser, polls | Prints login URL, exits 1 | | `store` | No-op | Stores if secret is device secret (`atcr_device_*`) | | `erase` | Removes all creds for host | Removes active account only | | No args | Prints bare usage | Prints full cobra help with all commands | ## Dependencies - `github.com/spf13/cobra` — already in go.mod - `github.com/charmbracelet/huh` — new (pure Go, CGO_ENABLED=0 safe) No changes to `.goreleaser.yaml` needed. ## Implementation Order ### Phase 1: Foundation 1. `helpers.go` — move utility functions verbatim + add `getParentCmdline()` and `detectIdentityFromParent(registryHost)` 2. `config.go` — new config types + migration from old formats 3. `main.go` — Cobra root command, register all subcommands ### Phase 2: Docker Protocol (must work for existing users) 4. `device_auth.go` — extract `authorizeDevice()` + `validateCredentials()` 5. `protocol.go` — `get`/`store`/`erase`/`list` using new config with smart account resolution ### Phase 3: User Commands 6. `cmd_login.go` — interactive device flow with huh spinner 7. `cmd_status.go` — display all registries/accounts 8. `cmd_switch.go` — huh select for account switching 9. `cmd_logout.go` — huh confirm for removal 10. `cmd_configure.go` — Docker config.json manipulation 11. `cmd_update.go` — move existing update logic ### Phase 4: Polish 12. Add `huh` to go.mod 13. Delete old `main.go` contents (replaced by new files) ## What to Keep vs Rewrite **Keep** (move to new files): `openBrowser()`, `buildAppViewURL()`, `isInsecureRegistry()`, `getDockerInsecureRegistries()`, `readDockerDaemonConfig()`, `stripPort()`, `isTerminal()`, `authorizeDevice()` HTTP logic, `validateCredentials()`, all update/version check functions. **Rewrite**: `main()`, `handleGet()` (split into non-interactive `get` with smart detection + interactive `login`), `handleStore()` (implement actual storage), `handleErase()` (multi-account aware), config types and loading. **New**: `list`, `login`, `logout`, `status`, `switch`, `configure-docker` commands. Config migration. Parent process identity detection. huh integration. ## Verification 1. Build: `go build -o bin/docker-credential-atcr ./cmd/credential-helper` 2. Help works: `bin/docker-credential-atcr --help` shows all user commands 3. Protocol works: `echo "atcr.io" | bin/docker-credential-atcr get` returns credentials or helpful error 4. No hang: `bin/docker-credential-atcr get` (no stdin pipe) detects terminal, prints help, exits 5. Smart detection: `docker push atcr.io/evan.jarrett.net/test:latest` auto-selects `evan.jarrett.net` 6. Login flow: `bin/docker-credential-atcr login` triggers device auth with huh prompts 7. Status: `bin/docker-credential-atcr status` shows configured accounts 8. Config migration: Place old-format `~/.atcr/device.json`, run any command, verify auto-migration 9. GoReleaser: `CGO_ENABLED=0 go build ./cmd/credential-helper` succeeds