src/cli/cache.ts — vx cache prune
Purpose
Section titled “Purpose”Implements vx cache prune. Both parsers are util/size.ts’s,
re-exported here: the workspace’s cacheRetention field reads the
same spellings the flags do. Drives
Cache.prune({...}) from src/cache/cache.ts against the cache
directory a run would use.
Public surface
Section titled “Public surface”export async function cacheCmd(args: readonly string[]): Promise<number>
interface PruneArgs { olderThanMs?: number maxBytes?: number dryRun?: boolean format?: 'pretty' | 'json' cacheDir?: string // `--cache-dir`: prune the cache a run with the same flag uses error?: string}
export function parsePruneArgs(args: readonly string[]): PruneArgsexport { parseDuration, parseSize } from '../util/index.js'Subcommand surface
Section titled “Subcommand surface”vx cache prune --older-than <duration>vx cache prune --max-size <size>vx cache prune --older-than 7d --max-size 500M # bothvx cache prune --max-size 1G --dry-run # say what the policy would reap; delete nothingvx cache prune --max-size 1G --format json # schemas/cache.jsonvx cache prune --older-than 30d --cache-dir <path>At least one of --older-than / --max-size is required, and neither
may be zero — a policy that would evict every entry is refused with
“delete the cache directory instead”. A bare number for --max-size
is refused too: --max-size 10 would read as ten bytes and evict
nearly everything; give a unit. Both policies may be combined:
age-based eviction first, then LRU eviction if the total is still
over the size cap. The prune also reaps orphaned artifacts (an archive
with no index row) and stale temps, and says so — Pruned 12 entries (1.4 GB freed), reaped 3 orphaned artifacts (…); a dry run prints
Would prune … and would reap.
The directory is the one a run would use — --cache-dir,
defineWorkspace({ cacheDir }) and a config plugin’s edit of it,
through cliCacheDir — or a prune silently no-ops against the wrong
path. A cache this user cannot write is refused up front with the
directory named, as a run refuses it (a dry run only reads, and reads
a read-only cache fine), and an upgrade that reset the index is
announced once. A prune that deletes takes the workspace’s run lock
first (acquireRunLock, orchestrator.md), so it waits for a run on the
workspace — [vx] waiting for another vx run (pid N) on this workspace to finish… after a second — instead of evicting the hits that run has
just probed; the age cutoff is taken before the wait, so what the run
touched survives it. A dry run takes no lock.
Parsers
Section titled “Parsers”parseDuration(input): number | null
Section titled “parseDuration(input): number | null”/^(\d+)([smhd])$/iReturns ms. s (× 1000), m (× 60_000), h (× 3_600_000), d
(× 86_400_000), case-insensitively (30D is thirty days). null on
parse fail; the caller surfaces invalid duration: <value> (e.g. 30d, 24h, 60m).
parseSize(input): number | null
Section titled “parseSize(input): number | null”/^(\d+)([KMGT])?B?$/iReturns bytes. Multipliers are powers of 1024 (K, M, G, T).
Optional trailing B is allowed. null on parse fail — including
digits past 2^53, which would parse to a number the user did not type;
the caller surfaces invalid size: <value> (e.g. 500M, 1G).
tests/cli.test.ts: parseDuration and parseSize units and parse
failures; vx cache prune with no policy errors, reports 0 entries
pruned from an empty cache, and rejects an unknown subcommand.
Eviction, orphan reaping and dryRun are tested in
tests/cache.test.ts against Cache.prune.