Skip to content

feat(cmd): layerx cache subcommand (list + prune) - #44

Merged
deveshctl merged 10 commits into
mainfrom
feat/i04-cache-subcommand
Jun 6, 2026
Merged

feat(cmd): layerx cache subcommand (list + prune)#44
deveshctl merged 10 commits into
mainfrom
feat/i04-cache-subcommand

Conversation

@deveshctl

Copy link
Copy Markdown
Owner

Adds layerx cache list and layerx cache prune so users can inspect
and reclaim disk explicitly without waiting for the next analyze run.

Builds on I-03 (auto-prune, 272c620). The eviction loop is shared via
a new public image.PruneCache API; the existing internal pruneCache
becomes a thin wrapper, so I-03 behaviour is unchanged.

Surface

  • layerx cache list — table of digest / size / cached-at + totals footer
  • layerx cache prune — dry run by default (lists what would be removed)
  • layerx cache prune --older-than 7d — duration syntax (s/m/h/d/w)
  • layerx cache prune --all — empties the cache
  • layerx cache prune --dry-run — preview alongside any of the above
  • --older-than and --all are mutually exclusive

Spec

docs/superpowers/specs/2026-06-06-i04-cache-subcommand-design.md (gitignored, local working-tree only)

Test plan

  • 9 new tests in image/cache_test.go covering ListCache happy/skip/missing/unreadable, PruneCache All/All-with-Keep/DryRun/partial-failure, and the wrapper-still-emits-overflow-warn drift guard
  • 12 new tests in cmd/cache_test.go covering parser happy/reject/overflow, renderer empty/populated/dry-run, relativeTime cases, flag mutual exclusion, end-to-end list, end-to-end prune-all-dry-run

CI runs all tests on ubuntu-latest. The chmod-based partial-failure test skips on Windows.

Closes I-04.

deveshctl added 10 commits June 6, 2026 12:45
Adds public CacheEntry, PruneOptions, PruneResult types and ListCache /
PruneCache exports. The existing internal pruneCache(root, keep, progress)
becomes a thin wrapper over PruneCache so I-03's auto-prune is unchanged.

Prepares image/ for a layerx cache subcommand (cmd/cache.go) that
inspects and explicitly evicts cache entries.

Refs I-04. Builds on I-03 (272c620).
Adds 'layerx cache list' (digest, size, cached-at footer) and
'layerx cache prune' (--older-than DURATION / --all / --dry-run; bare
prune is a dry run). Reuses image.PruneCache from I-04 commit 1, so
auto-prune and user-driven prune share one eviction implementation.

Help text documents LAYERX_CACHE_DIR override.

Closes I-04.
Adds a 'Cache management' subsection under the existing
'Caching & environment' section showing layerx cache list,
layerx cache prune, --older-than syntax, and --all.

Refs I-04.
Three independent issues surfaced on Linux CI (passed locally on
Windows because go test is blocked there):

1. cmd/cache_test.go drove cobra via cacheCmd.Execute(). Since
   cacheCmd is a subcommand of rootCmd, dispatch fell through to
   rootCmd's RunE which expects 1 arg; the three Execute()-based
   tests failed with 'accepts 1 arg(s), received 0'. Switched to
   rootCmd.SetArgs([]string{"cache", ...}) and rootCmd.Execute()
   to match the pattern in cmd/ci_test.go.

2. TestListCache_HappyPath compared a UTC time.Time from
   withFrozenNow against entries[0].CachedAt sourced from
   os.Stat().ModTime(), which returns Local on Linux. testify's
   assert.Equal uses reflect.DeepEqual and fails on Location even
   when the instants match. Switched to time.Time.Equal which
   compares instants regardless of zone.

3. TestPruneCache_PartialFailure_WarnsOnce relied on chmod 0o000
   to make os.RemoveAll fail. Recent Go RemoveAll chmod-ups
   unreadable directories and retries, so the failure path didn't
   trigger on Linux CI. Added a removeAllFn test seam in
   image/cache.go (parallel to nowFn) and use it to inject a
   synthetic error for one specific path. Deterministic across
   kernels and Go versions.

Test-only changes plus one new test seam. No production behaviour
change. Closes failing CI run on PR #44.
Test-only follow-up to f6b9049. Two issues remained on Linux CI:

1. TestCachePrune_FlagsMutuallyExclusive asserted the error string
   contained 'mutually exclusive', but cobra's actual phrasing is
   'if any flags in the group [...] are set none of the others can
   be; [...] were all set'. Updated the assertion to match what
   cobra emits.

2. TestCachePrune_AllDryRun_DoesNotTouchDisk failed because cobra
   keeps each flag's Changed state across Execute() calls. The
   prior FlagsMutuallyExclusive test marked --older-than and --all
   as Changed; the next test's bare 'cache prune' tripped the same
   mutual-exclusion check on the leftover state. resetCacheFlags
   now also resets cobra's Changed flag on each prune flag.

No production code change.
Two cache-UX fixes spotted on the second machine after I-04 landed:

1. `layerx --help` listed the `cache` subcommand under Available
   Commands but the root Long's Cache: section only mentioned the
   `--no-cache` bypass flag. Added a one-sentence pointer to
   `layerx cache list` and `layerx cache prune` so the management
   surface is discoverable without scrolling the subcommand list.

2. Bare `layerx cache prune` is a dry run by design — it lists every
   cached entry and exits without touching disk. The output looked
   identical to a real eviction ("Would remove ... 11.8 MB"), and a
   user could reasonably believe the cache had been cleared. Added a
   trailing two-line hint pointing at `--all` and `--older-than`
   that fires only on the bare-prune case; explicit `--all --dry-run`
   and `--older-than X --dry-run` are unchanged because the user
   already chose a scope.
The table format itself is fine (`tabwriter` aligns the columns), but
`ListCache` returns oldest-first to feed PruneCache's stale-eviction
order, and `renderListTable` was iterating the slice as-is. On a cache
of 18 entries the freshly-written one sat at the bottom and the user
had to scroll past two-week-old digests to see what just got cached.

Reverse the walk in the renderer so newest is at the top — same
ordering convention as `docker images`. `ListCache` is unchanged
because `PruneCache` and the image-package tests both depend on
oldest-first; only the human-facing render flips.
Adds a display-only IMAGE column to `layerx cache list` so users
can match a cached digest to the image ref they originally typed,
without scrolling a wall of `sha256:...` strings.

Storage: alongside `layers.gob`, saveCache now writes a small
`meta.json` sidecar (`{"image_ref":"nginx:latest"}`) using the
same temp-file + rename pattern, so a crash mid-write cannot leave
a half-written sidecar that ListCache would parse. The sidecar is
purely cosmetic — loadCache and PruneCache ignore it; a missing or
corrupt sidecar surfaces as "<unknown>" in the table and is
otherwise transparent. Older cache entries without a sidecar
continue to work; they re-acquire one on the next save.

Surfaces the field through `CacheEntry.ImageRef` and threads the
imageRef parameter through saveCache. Image-package tests cover
sidecar happy-path, missing-sidecar fallback, empty-imageRef-elides
the file, and tolerance of garbage hand-edited bodies. cmd-package
e2e test seeds one entry with a sidecar and one without to assert
both "nginx:latest" and "<unknown>" render in the table.

Help text and README updated to describe the new column.
Help-text review surfaced eight issues, all UX with no behaviour
change beyond the --refresh removal:

- Trim root Long from ~40 to ~20 lines. Open with a 'Common usage'
  block listing the three primary commands so the bare form's role
  (interactive TUI) is obvious without reading the subcommand list.
- Preserve f742493's explicit 'layerx cache list' / 'layerx cache prune'
  mention from the root Cache: section (collapsed earlier draft would
  have silently regressed that two-day-old UX fix).
- Drop the hidden --refresh persistent flag. It was registered then
  immediately MarkHidden'd, with no discoverability path; --no-cache
  is the documented form everywhere else. Updates root_test.go's
  resetPersistentFlags helper accordingly. README and CHANGELOG also
  drop the alias mention.
- ci/compare Long now end with a one-line pointer to 'layerx --help'
  for the persistent --engine, --json, --no-cache flags.
- ci help: add 'Example .layerx.yaml:' header above the YAML snippet
  so it isn't misread as flag prose.
- ci help: override pflag.Flag.DefValue on the three threshold flags
  so --help renders 'from config (built-in 0.9)' instead of the
  misleading -1 sentinel buildRules uses for Changed() detection.
- compare help: add a --no-cache example to surface the inherited
  persistent flag.
@deveshctl
deveshctl merged commit 44326d5 into main Jun 6, 2026
16 checks passed
@deveshctl
deveshctl deleted the feat/i04-cache-subcommand branch June 6, 2026 08:48
@deveshctl deveshctl mentioned this pull request Jun 6, 2026
8 tasks
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