Skip to content

refactor(cli): unify CLI presentation and redesign images prune#76

Merged
bnema merged 6 commits intomainfrom
refactor/cli-ui-style-unification
Feb 13, 2026
Merged

refactor(cli): unify CLI presentation and redesign images prune#76
bnema merged 6 commits intomainfrom
refactor/cli-ui-style-unification

Conversation

@bnema
Copy link
Owner

@bnema bnema commented Feb 13, 2026

Summary

  • Shared presentation layer: Introduce presentation.go with styled helpers (cliWriteLine, cliRenderTitle, cliRenderMuted, etc.) and migrate root, backup, serve, and images commands from raw fmt.Print* to consistent Lipgloss-styled output.
  • Images list table: Render images list with fixed-width columns and grapheme-safe truncation so long repositories, tags, and IDs stay readable.
  • Images prune redesign: Running images prune with no flags now prunes both dangling runtime images and registry tags (default retention: latest + 3 previous non-latest tags). New flags: --dangling/--registry to scope, --keep-releases for retention control, --no-confirm to skip the interactive confirmation prompt. Reuses the existing RunConfirm Bubble Tea component.
  • UI adoption guardrails: AST-based tests (ui_adoption_test.go, parity_matrix_test.go) enforce that CLI commands use presentation helpers, preventing regressions.

Options contract

The prune options flow through all layers:

CLI flags → dto.ImagePruneRequest → remote client → admin handler → domain.ImagePruneOptions → usecase service

The DTO uses *bool optional fields so the handler distinguishes "not provided" (both default true) from explicit false. The handler rejects both-false with HTTP 400.

Breaking changes

  • images prune --keep is replaced by --keep-releases (default 3, aligned with scheduled job / admin API defaults).
  • images prune with no args now actually prunes registry tags (previously it only ran dangling cleanup because --keep defaulted to 0).

Test coverage

  • 20+ tests for scope resolution, dry-run variants, keep-releases, confirmation flow (call/reject/skip/dry-run-never), error handling
  • Table truncation and ANSI passthrough tests
  • Handler scope-defaults sub-tests (both/dangling-only/registry-only/both-false-rejected)
  • Service selective-execution tests
  • AST guardrail tests for presentation helper adoption

Files changed

22 files, +1400 / -151 lines across CLI, remote client, admin handler, domain, usecase, and docs.

Summary by CodeRabbit

  • New Features

    • gordon images prune adds flags: --keep-releases, --dangling, --registry, --no-confirm; latest tag always preserved; default keeps latest + 3 releases and enables both runtime and registry cleanup.
  • Behavior

    • Dry-run supported; explicit confirmation prompt added when not using --no-confirm.
  • UI/UX

    • CLI output now includes styled titles, muted/meta lines, and a fixed-width table with truncation for long values.
  • Documentation

    • Prune docs and examples updated to reflect new flags, defaults, and retention semantics.

Route root, backup, and images command output through shared presentation helpers and add UI adoption guardrails to prevent regressions in styling usage.
Detect forbidden fmt Fprint writes to stdout paths, switch matrix coverage to AST-based function detection, and deduplicate empty-state rendering helper.
Use the shared table component with fixed column widths and grapheme-safe truncation so long repositories, tags, and image IDs stay readable without breaking row shape.
Make 'gordon images prune' run both dangling-runtime and registry cleanup
by default, keeping latest + 3 previous release tags per repository.

Add --dangling and --registry scope flags to restrict which subsystems
run, --keep-releases to control tag retention, --no-confirm to skip the
interactive confirmation prompt, and reuse the existing RunConfirm
component for the destructive-operation gate.

The options contract flows CLI -> remote client -> admin handler ->
usecase service via domain.ImagePruneOptions, with the handler and
scheduled job both defaulting to both-scopes-enabled.
Copilot AI review requested due to automatic review settings February 13, 2026 06:09
@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Replaces scalar image prune parameter with ImagePruneOptions (KeepLast, PruneDangling, PruneRegistry); updates CLI to build/send new request shape, adds CLI presentation helpers and table truncation, adapts HTTP client/handler, domain, usecase, boundaries, and extensive tests to the options-based pruning API.

Changes

Cohort / File(s) Summary
Documentation
docs/cli/images.md, docs/config/images.md
Docs updated for new CLI flags (--keep-releases, --dangling, --registry, --no-confirm), default scope (runtime + registry), and retention semantics.
DTO / API
internal/adapters/dto/admin_images.go
Added prune_dangling and prune_registry fields to ImagePruneRequest JSON model.
CLI command & rendering
internal/adapters/in/cli/images.go, internal/adapters/in/cli/images_test.go, internal/adapters/in/cli/presentation.go, internal/adapters/in/cli/root.go, internal/adapters/in/cli/serve.go, internal/adapters/in/cli/backup.go
Prune call switched to request object; new options (KeepReleases, Dangling, Registry, NoConfirm, DryRun); confirmation helper introduced; list/prune output moved to styled rendering and lipgloss table; tests updated and expanded.
UI table component & tests
internal/adapters/in/cli/ui/components/table.go, internal/adapters/in/cli/ui/components/table_test.go
Added truncateCell, enforced per-column widths, grapheme/ANSI-aware truncation; tests added for truncation and unicode/ANSI edge cases.
Remote client & HTTP admin
internal/adapters/in/cli/remote/client.go, internal/adapters/in/cli/remote/client_test.go, internal/adapters/in/http/admin/images.go, internal/adapters/in/http/admin/handler_test.go
Client and HTTP handler now accept structured prune requests/options, validate inputs (keep_last >= 0 if present), and tests adapted for scope validation and defaults.
Domain, Usecase & Boundaries
internal/domain/images.go, internal/usecase/images/service.go, internal/usecase/images/service_test.go, internal/boundaries/in/images.go, internal/app/run.go
Added ImagePruneOptions and DefaultImagePruneOptions; changed service and boundary Prune signatures to accept options; usecase implements conditional runtime/registry pruning and error handling per options; tests updated.
CLI UI adoption & parity tests
internal/adapters/in/cli/parity_matrix_test.go, internal/adapters/in/cli/ui_adoption_test.go
New AST- and runtime-based tests to enforce use of CLI presentation helpers and prevent raw printing; added coverage expectations.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as CLI
    participant HTTP as HTTP Admin
    participant Service as Image Service
    participant Runtime as Runtime Pruner
    participant Registry as Registry Pruner

    User->>CLI: gordon images prune --dangling --keep-releases=3
    CLI->>CLI: build ImagePruneRequest / ImagePruneOptions
    CLI->>HTTP: POST /admin/images/prune (request)
    HTTP->>Service: Prune(ctx, opts)

    alt PruneDangling enabled
        Service->>Runtime: prune runtime images
        Runtime-->>Service: runtime report
    else
        Note over Service: skip runtime pruning
    end

    alt PruneRegistry enabled
        Service->>Registry: prune registry tags (keepLast)
        Registry-->>Service: registry report
    else
        Note over Service: skip registry pruning
    end

    Service-->>HTTP: ImagePruneReport
    HTTP-->>CLI: response
    CLI->>User: render results (title, table, summary)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibble options neat and small,
I split runtime from registry's haul,
Flags hop freely, tables trim tight,
Dry-run winks, confirmations light—
A tidy prune, hopped through the night.

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: refactoring CLI presentation layer and redesigning the images prune command with new flags and behavior.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/cli-ui-style-unification

No actionable comments were generated in the recent review. 🎉


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the CLI to use a shared presentation layer (Lipgloss-styled helpers), redesigns gordon images list to render a fixed-width, grapheme-safe table, and changes gordon images prune to support scoped pruning (dangling/runtime vs registry) with new defaults and flags. It also threads new prune options through the DTO → remote client → HTTP admin handler → domain/usecase layers and adds AST-based guardrail tests to prevent UI regression.

Changes:

  • Introduces presentation.go helpers and migrates multiple commands away from raw fmt.Print* to consistent styled output.
  • Reworks images list output using a table component with fixed column widths + grapheme-safe truncation.
  • Redesigns images prune options/flags (default both scopes, --keep-releases, --dangling/--registry, confirmation flow) and adds broad test coverage + UI adoption guardrails.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/usecase/images/service.go Updates prune entrypoint to accept domain.ImagePruneOptions and conditionally run runtime/registry prunes.
internal/usecase/images/service_test.go Extends prune tests to cover dangling-only and registry-only scope behavior.
internal/domain/images.go Introduces ImagePruneOptions + defaults helper.
internal/boundaries/in/images.go Updates ImageService port signature to use prune options object.
internal/app/run.go Updates scheduled prune to call service with explicit prune options.
internal/adapters/in/http/admin/images.go Parses new DTO scope fields, applies defaults, rejects both-scopes-disabled, forwards options to service.
internal/adapters/in/http/admin/handler_test.go Updates stubs and adds tests for scope defaulting/validation.
internal/adapters/dto/admin_images.go Extends prune DTO with optional *bool scope fields.
internal/adapters/in/cli/presentation.go Adds shared CLI write/render helpers (testable seams).
internal/adapters/in/cli/images.go Implements new images list table rendering and redesigned images prune flag/flow.
internal/adapters/in/cli/images_test.go Adds coverage for scope resolution, confirmation flow, retention semantics, and table truncation behaviors.
internal/adapters/in/cli/remote/client.go Updates remote client prune API to accept ImagePruneRequest.
internal/adapters/in/cli/remote/client_test.go Updates remote client tests to validate new request fields.
internal/adapters/in/cli/ui/components/table.go Adds fixed-width rendering + truncation (grapheme-safe) to the shared table component.
internal/adapters/in/cli/ui/components/table_test.go Adds unit tests for truncation and width behavior.
internal/adapters/in/cli/root.go Migrates version/log output to presentation helpers.
internal/adapters/in/cli/serve.go Migrates deprecated start warning to presentation helpers.
internal/adapters/in/cli/backup.go Adds consistent titles/empty-states via presentation helpers.
internal/adapters/in/cli/ui_adoption_test.go Adds AST guardrails + seam tests ensuring commands adopt presentation helpers.
internal/adapters/in/cli/parity_matrix_test.go Adds expectation matrix + coverage test for UI adoption guardrails.
docs/config/images.md Documents CLI defaults aligning with scheduled prune defaults.
docs/cli/images.md Updates CLI docs for new prune flags, defaults, scope resolution, and retention semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 112 to 116
cmd.Flags().IntVar(&opts.KeepReleases, "keep-releases", domain.DefaultImagePruneKeepLast,
"Number of previous non-latest tags to keep per repository (latest is always kept)")
cmd.Flags().BoolVar(&opts.Dangling, "dangling", false, "Prune dangling runtime images only")
cmd.Flags().BoolVar(&opts.Registry, "registry", false, "Prune old registry tags only")
cmd.Flags().BoolVar(&opts.NoConfirm, "no-confirm", false, "Skip confirmation prompt")
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The --dangling/--registry flag help strings say "only", but both flags can be combined (and combining them means both scopes run). Consider rewording these descriptions to reflect that they restrict scope when used alone (or that they enable each scope) to avoid confusing CLI users.

Copilot uses AI. Check for mistakes.
Comment on lines 233 to 237
func runImagesPruneDryRun(ctx context.Context, client imagesClient, opts imagesPruneOptions, pruneDangling, pruneRegistry bool, out io.Writer) error {
images, err := client.ListImages(ctx)
if err != nil {
return fmt.Errorf("failed to prune images: %w", err)
return fmt.Errorf("failed to list images: %w", err)
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

In dry-run mode, runImagesPruneDryRun always calls client.ListImages even when runtime pruning is disabled (registry-only scope). For remote mode this is an unnecessary network call and can be expensive on large registries. Consider only listing images when pruneDangling is true (or when you actually need the dangling count).

Copilot uses AI. Check for mistakes.
if keepLast < 0 {
func (c *Client) PruneImages(ctx context.Context, req dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
if req.KeepLast != nil && *req.KeepLast < 0 {
return nil, fmt.Errorf("keepLast must be >= 0")
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The client-side validation error message uses "keepLast" (camelCase) rather than the API field name (keep_last) or CLI flag name (--keep-releases). Since this can surface to end users, consider aligning the message with the public contract (e.g., keep_last must be >= 0 or --keep-releases must be >= 0).

Suggested change
return nil, fmt.Errorf("keepLast must be >= 0")
return nil, fmt.Errorf("--keep-releases must be >= 0")

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +176
applyWidth := func(s lipgloss.Style) lipgloss.Style {
if width > 0 {
return s.Width(width).MaxWidth(width)
}
return s
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

In TableModel.Render, truncateCell uses the raw column width, but applyWidth later sets a Lipgloss style width that may also include the default left/right padding (NewTable sets Padding(0, 1)). This can cause cells that were truncated to col.Width (ending with ...) to be clipped again at render time, potentially cutting off the ellipsis and breaking the intended fixed-width behavior. Consider truncating to the available content width (column width minus horizontal padding) or adjusting the style width to account for padding so the rendered cell width matches the truncation width.

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +198
func truncateCell(value string, maxWidth int) string {
if strings.Contains(value, "\x1b[") {
return value
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

truncateCell currently returns the original value unchanged whenever it contains an ANSI escape sequence ("\x1b["). This avoids breaking ANSI codes, but it also means long styled cell values can exceed the configured column width and defeat the fixed-width table layout. Consider using an ANSI-aware truncation approach (measure/trim printable width while preserving escape sequences) so styled cells remain bounded.

Copilot uses AI. Check for mistakes.
if opts.PruneDangling {
runtimeReport, err := s.PruneRuntime(ctx)
if err != nil {
log.Warn().Err(err).Msg("runtime prune failed; continuing with registry prune")
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The warning log message says "continuing with registry prune" even when opts.PruneRegistry is false (dangling-only mode). This makes logs misleading for scoped prunes. Consider changing the message to be scope-agnostic or conditional on opts.PruneRegistry.

Suggested change
log.Warn().Err(err).Msg("runtime prune failed; continuing with registry prune")
msg := "runtime prune failed"
if opts.PruneRegistry {
msg += "; continuing with registry prune"
} else {
msg += "; continuing"
}
log.Warn().Err(err).Msg(msg)

Copilot uses AI. Check for mistakes.
- Reword --dangling/--registry help to clarify they restrict scope, not
  exclusive
- Skip ListImages call in dry-run when pruneDangling is false
  (registry-only)
- Use API field name keep_last in remote client validation error
- Make service log message conditional on PruneRegistry when runtime
  prune fails
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/usecase/images/service.go`:
- Around line 334-366: In Prune, when calling s.PruneRuntime(ctx) inside the
Prune method, do not swallow the error if opts.PruneRegistry is false: if
PruneRuntime returns an error and opts.PruneRegistry is false, return that error
(along with the partially built report) instead of only logging it; if
opts.PruneRegistry is true keep the current behavior of logging the runtime
error and continuing to call s.PruneRegistry(ctx, opts.KeepLast). Ensure
references to the symbols Prune, PruneRuntime, PruneRegistry, opts, and report
are used to locate and adjust the control flow accordingly.

When PruneRegistry is false, return the PruneRuntime error to the caller
instead of swallowing it into a warning log. Both-scopes mode keeps the
existing behavior of logging and continuing to registry cleanup.
@bnema bnema merged commit 5d7c0a1 into main Feb 13, 2026
4 checks passed
@bnema bnema deleted the refactor/cli-ui-style-unification branch February 13, 2026 08:06
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.

1 participant