Summary
NIC's current CLI output is a mix of raw JSON structured logs (slog to stderr), interleaved third-party tool output (hetzner-k3s, OpenTofu), and ASCII box-art banners. The result is hard to follow during a deploy — operators see a wall of {"level":"INFO","msg":"Status","message":"..."} lines with no visual hierarchy, progress indication, or color.
Proposal: Replace the default output with a phase-oriented, tree-structured display while preserving full backward compatibility via --output flag and automatic TTY detection. Full design doc: docs/design-doc/nic-logging-redesign.md.
Current output
{"level":"INFO","msg":"Starting deployment","config_file":"config.yaml"}
{"level":"INFO","msg":"Configuration parsed successfully","provider":"hetzner","project_name":"nic-nebari"}
{"level":"INFO","msg":"Status","message":"Creating Hetzner k3s cluster","resource":"provider","action":"deploy"}
[Instance nic-nebari-master1] Instance status: running
[Instance nic-nebari-master1] Installing k3s...
{"level":"INFO","msg":"Infrastructure deployment completed","provider":"hetzner"}
{"level":"INFO","msg":"Installing Argo CD on cluster"}
{"level":"INFO","msg":"Success","message":"Argo CD is ready","resource":"argocd","action":"ready"}
Proposed output (TTY / pretty mode)
nebari v0.2.0
Deploying nic-nebari (hetzner)
✓ Configuration validated 0.2s
● Infrastructure 3m 42s
├─ ✓ SSH keys verified 0.8s
├─ ✓ k3s version resolved (v1.32.13+k3s1) 1.2s
├─ ✓ Cluster created 3m 38s
│ master1 (10.0.0.2) ✓ worker1 (10.0.0.3) ✓
└─ ✓ Kubeconfig saved 0.4s
● GitOps 4.1s
├─ ✓ Repository cloned 2.8s
├─ ✓ Manifests generated 0.9s
└─ ✓ Changes pushed 0.4s
● ArgoCD 28.3s
├─ ✓ Helm chart installed (v9.4.1) 12.1s
├─ ✓ Project created (nebari) 0.3s
└─ ✓ Root app-of-apps applied 15.9s
● Foundational Services 1m 12s
├─ ✓ cert-manager Healthy 18.4s
├─ ✓ envoy-gateway Healthy 22.1s
├─ ✓ keycloak Healthy 31.2s
└─ ✓ nebari-operator Healthy 0.5s
● DNS 2.4s
├─ ✓ nebari.example.com → 1.2.3.4 1.2s
└─ ✓ *.nebari.example.com → 1.2.3.4 1.2s
✓ Deployment complete 5m 29s
┌─────────────────────────────────────────────────────────────┐
│ ArgoCD https://argocd.nebari.example.com │
│ Keycloak https://keycloak.nebari.example.com │
│ │
│ Kubeconfig: ~/.cache/nic/hetzner-k3s/nic-nebari/kubeconfig │
└─────────────────────────────────────────────────────────────┘
Proposed output (error case)
● ArgoCD 12.1s
├─ ✓ Helm chart installed (v9.4.1) 12.1s
└─ ✗ Project creation failed 0.3s
Error: failed to create ArgoCD project "nebari":
admission webhook "validate.argocd" denied the request:
project "nebari" already exists
Hint: Run `nic destroy` first, or use `--force` to overwrite.
✗ Deployment failed 3m 54s
Output mode flag (--output)
To ensure backward compatibility and support terminals that don't render Unicode/ANSI well, a global --output flag controls the rendering mode:
nic deploy -f config.yaml --output=pretty # tree-structured, colored (default on TTY)
nic deploy -f config.yaml --output=json # current JSON structured logs (default on pipe/CI)
nic deploy -f config.yaml --output=plain # human-readable but no color/unicode (for dumb terminals)
Auto-detection (default):
stdout is a TTY → pretty
stdout is a pipe or NO_COLOR env is set → json
Override: --output flag always wins over auto-detection. This ensures:
- Users on terminals that don't handle Unicode can force
--output=plain
- CI pipelines that want pretty logs (e.g., for GitHub Actions summaries) can force
--output=pretty
- Scripts that parse JSON can force
--output=json regardless of TTY state
The plain mode is identical to pretty but uses ASCII-only characters (+/x/* instead of ✓/✗/●, - instead of ─, | instead of │) and no ANSI color codes.
Implementation approach
Architecture
cmd/nic/
├── main.go # --output flag + isatty check → select renderer
├── status_handler.go # current: slog bridge → new: renderer dispatch
└── renderer/
├── renderer.go # Renderer interface
├── pretty.go # TTY renderer (pterm-based, color + unicode)
├── plain.go # ASCII-only, no color (extends pretty with different symbols)
└── json.go # Pipe/CI renderer (current slog JSON behavior)
Renderer interface
type Renderer interface {
StartPhase(name string)
EndPhase(status PhaseStatus, duration time.Duration)
StartStep(name string)
EndStep(status StepStatus, duration time.Duration, detail string)
Detail(line string) // verbose sub-output (hetzner-k3s, OpenTofu lines)
Summary(items []SummaryItem)
Error(err error, hint string)
Confirm(message string, expected string) bool
}
Dependencies
Migration path
Each step is independently shippable — the json renderer preserves current behavior throughout:
- Add
Renderer interface + json.go (wraps current slog) + pretty.go + plain.go
- Wire
--output flag + isatty check in main.go
- Migrate
status_handler.go to dispatch to renderer
- Migrate
print*Instructions → renderer.Summary()
- Redirect third-party stdout to
renderer.Detail()
Summary
NIC's current CLI output is a mix of raw JSON structured logs (
slogto stderr), interleaved third-party tool output (hetzner-k3s, OpenTofu), and ASCII box-art banners. The result is hard to follow during a deploy — operators see a wall of{"level":"INFO","msg":"Status","message":"..."}lines with no visual hierarchy, progress indication, or color.Proposal: Replace the default output with a phase-oriented, tree-structured display while preserving full backward compatibility via
--outputflag and automatic TTY detection. Full design doc:docs/design-doc/nic-logging-redesign.md.Current output
Proposed output (TTY / pretty mode)
Proposed output (error case)
Output mode flag (
--output)To ensure backward compatibility and support terminals that don't render Unicode/ANSI well, a global
--outputflag controls the rendering mode:Auto-detection (default):
stdoutis a TTY →prettystdoutis a pipe orNO_COLORenv is set →jsonOverride:
--outputflag always wins over auto-detection. This ensures:--output=plain--output=pretty--output=jsonregardless of TTY stateThe
plainmode is identical toprettybut uses ASCII-only characters (+/x/*instead of✓/✗/●,-instead of─,|instead of│) and no ANSI color codes.Implementation approach
Architecture
Renderer interface
Dependencies
pterm— Go terminal formatting (MIT, no CGo, 5k+ stars)mattn/go-isatty— TTY detectionMigration path
Each step is independently shippable — the
jsonrenderer preserves current behavior throughout:Rendererinterface +json.go(wraps current slog) +pretty.go+plain.go--outputflag + isatty check inmain.gostatus_handler.goto dispatch to rendererprint*Instructions→renderer.Summary()renderer.Detail()