Skip to content

feat(commands): add doctor diagnostic command#192

Merged
ktn-jamf merged 4 commits intomainfrom
feat/doctor-command
May 8, 2026
Merged

feat(commands): add doctor diagnostic command#192
ktn-jamf merged 4 commits intomainfrom
feat/doctor-command

Conversation

@ktn-jamf
Copy link
Copy Markdown
Collaborator

@ktn-jamf ktn-jamf commented May 8, 2026

Summary

  • New jamf-cli doctor [profile] command — prints config path, resolved active profile (and the signal that selected it), every JAMF_* / JAMFPROTECT_* / JAMFSCHOOL_* env var (secrets fingerprinted, never full values), per-profile credential resolution status, and a 5-second unauthenticated HEAD probe of the configured URL
  • Inspired by cli-printing-press's auth doctor
  • Skips auth (added to chainSkip) so it works precisely when credentials are misconfigured — the case where it's most useful

Why

When an agent or human hits 401, the question is "is the token missing, expired, or shadowed by a stale env var?". Today that takes cat ~/.config/jamf-cli/config.yaml, env | grep JAMF, and a curl. jamf-cli doctor is one command.

Example output (-o table for the human view)

jamf-cli v1.16.1

CONFIG
  path:    /Users/keaton/.config/jamf-cli/config.yaml
  status:  present

ACTIVE PROFILE
  name:        my-pro
  source:      JAMF_PROFILE env var
  product:     pro
  url:         https://example.jamfcloud.com
  auth-method: oauth2
  client-id:   env:JAMF_CLIENT_ID  (resolved, fingerprint: abcd••••)
  client-secret: keychain:jamf-cli/my-pro-secret  (resolved, fingerprint: wxyz••••)

ENVIRONMENT
  JAMF_URL                       (unset)
  JAMF_PROFILE                   my-pro
  JAMF_TOKEN                     (unset)
  JAMF_CLIENT_ID                 set, fingerprint: abcd••••
  ...

CONNECTIVITY
  HEAD https://example.jamfcloud.com  →  200 OK (148ms)

JSON output via -o json (the default) returns the same data as a structured doctorReport.

Security

  • Secrets are never printed in full. fingerprint() returns first 4 chars + ••••. Values shorter than 4 chars are fully redacted.
  • The connectivity probe is HEAD-only, no auth headers, 5s timeout — pure DNS/TLS reachability.
  • Reads ~/.config/jamf-cli/config.yaml and OS env vars only. No writes.

Test plan

  • go test ./internal/commands/... -run 'Doctor|Fingerprint|ProbeEnvVars|ProbeProfileCredentials|ResolveProfileNameForDoctor' — 11 tests covering precedence, fingerprint redaction, env-var probing, missing-profile note, credential resolution success/failure
  • go test ./... — full suite green (including TestApplyRootGroups_AllCommandsGrouped after adding doctor to rootGroupMap)
  • make lint — 0 issues
  • Manual: jamf-cli doctor, jamf-cli doctor -o table, jamf-cli doctor missing-profile

Notes

This is the fourth and last "cheap win" pulled from cli-printing-press. Independent of #189 / #190 / #191 — can land in any order.

🤖 Generated with Claude Code

Inspired by cli-printing-press's `auth doctor`. New `jamf-cli doctor`
command prints resolved profile, env-var state, and a read-only HEAD
probe of the configured URL. Auth is bypassed (chainSkip) so it works
even when credentials are misconfigured — the case where it's most
useful.

Output:
  - Version
  - Config path + presence
  - Active profile: name, source ("--profile flag" / "JAMF_PROFILE env"
    / "config default-profile" / "positional argument"), URL, auth
    method, per-credential resolution status
  - Env vars: 12 known JAMF_* / JAMFPROTECT_* / JAMFSCHOOL_* entries.
    Secrets show first-4 fingerprint only; non-secret URLs/IDs show the
    actual value.
  - Connectivity: 5s HEAD probe with status code + latency

Default output format is JSON (consistent with the rest of the CLI).
Pass `-o table` (or any non-json/yaml format) for the human summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses PR #192 review findings:

(1) Resolve and display the *effective* URL the auth chain would use
(--url > JAMF_URL > profile), not just the profile URL. New
profileReport.EffectiveURL/URLSource fields and a per-credential
EnvOverride flag call out the "shadowed by env var" failure mode the
command exists to diagnose. Connectivity probe now hits the effective
URL.

(2) printDoctorHuman now takes io.Writer and routes through the
formatter's writer, so 'doctor -o table --out-file r.txt' captures the
human report instead of writing to stdout while leaving r.txt empty.
New Formatter.Writer() accessor exposes the destination.

(3) probeConnectivity sets a User-Agent header (some Jamf
proxies/CDNs reject UA-less requests) and stops at the first redirect
rather than following — for a reachability check, a 302 to /login is
more informative than its target.

(4) When no config file exists but JAMF_URL + a credential env var
are set, the note says "operating in env-var mode" instead of
misdirecting CI/CD users to 'pro setup'.

(5) Documents the format-routing decision (json/yaml → formatter,
everything else → human renderer) inline.

Adds 6 new tests covering effective-URL resolution, env-var override
flagging, no-config-with-env-vars detection, writer injection for
human output, and User-Agent + redirect handling on the probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ktn-jamf ktn-jamf enabled auto-merge (squash) May 8, 2026 05:15
@neilmartin83
Copy link
Copy Markdown
Member

Approved — live tested against platform-nmartin tenant.

Verified:

  • doctor [profile] resolves correctly across all three sources (positional arg, JAMF_PROFILE env, config default-profile) with source field reflecting which won
  • Fingerprint redaction: 4-char prefix + •••• for keychain-resolved client-id/client-secret, full bullet-redact for env values <4 chars (verified with JAMF_TOKEN=abc•••)
  • Missing profile: emits actionable notes entry, skips connectivity, doesn't crash
  • Connectivity probe is HEAD-only, no Authorization header, 5s timeout, redirects not followed — confirmed in source at internal/commands/doctor.go:305
  • Skips auth chain (works precisely when creds are broken — exactly when needed)
  • Plays nice with PR 189/190/191: --compact, --select profile.name,connectivity, --field version, -o table all behave as expected
  • go test ./internal/commands/... -run 'Doctor|Fingerprint|Probe...' green

Gap (non-blocking, file follow-up):
profileReport has no tenant-id field. For auth-method: platform profiles the table omits it entirely, even though the env section lists JAMF_TENANT_ID. Anyone diagnosing "why does my platform auth 401" will miss a missing/wrong tenant-id. Add TenantID string to profileReport, populate from p.TenantID (already on config.Profile at config.go:32), render conditionally in the table block. Low risk, high signal for platform users.

Same for product — field exists in struct (doctor.go:65) and rendering branch exists at doctor.go:418, but profiles don't carry Product for platform-auth so it never prints. Worth confirming intended behavior.

Ship — these are polish, not blockers.

🤖 Live-tested with Claude Code

Copy link
Copy Markdown
Member

@neilmartin83 neilmartin83 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Live-tested against platform-nmartin tenant. Profile resolution precedence, fingerprint redaction, missing-profile handling, HEAD-only unauthenticated probe, --compact/--select/--field interop, and unit tests all clean. Filed nit on missing tenant-id in profileReport (platform-auth gap) — non-blocking.

@ktn-jamf ktn-jamf merged commit b45a392 into main May 8, 2026
2 checks passed
@ktn-jamf ktn-jamf deleted the feat/doctor-command branch May 8, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants