feat(cmd): layerx cache subcommand (list + prune) - #44
Merged
Conversation
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.
…mand # Conflicts: # CHANGELOG.md
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
layerx cache listandlayerx cache pruneso users can inspectand 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.PruneCacheAPI; the existing internalpruneCachebecomes a thin wrapper, so I-03 behaviour is unchanged.
Surface
layerx cache list— table of digest / size / cached-at + totals footerlayerx 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 cachelayerx cache prune --dry-run— preview alongside any of the above--older-thanand--allare mutually exclusiveSpec
docs/superpowers/specs/2026-06-06-i04-cache-subcommand-design.md(gitignored, local working-tree only)Test plan
image/cache_test.gocovering ListCache happy/skip/missing/unreadable, PruneCache All/All-with-Keep/DryRun/partial-failure, and the wrapper-still-emits-overflow-warn drift guardcmd/cache_test.gocovering parser happy/reject/overflow, renderer empty/populated/dry-run, relativeTime cases, flag mutual exclusion, end-to-end list, end-to-end prune-all-dry-runCI runs all tests on ubuntu-latest. The chmod-based partial-failure test skips on Windows.
Closes I-04.