Skip to content

Commit 33b8c7a

Browse files
indexzeroclaude
andauthored
feat(cli): add cache clear command (#18)
Add `_all_docs cache clear` command to selectively clear cache entries with support for filtering by cache type, origin, package name, and age. - Add clear.js with full implementation supporting: - Target specific cache types (--packuments, --partitions, --checkpoints) - Filter by registry URL or origin key (--registry, --match-origin) - Clear specific package cache (--package) - Age-based filtering (--older-than with duration syntax) - Dry-run mode and interactive confirmation - Add clear() and info() methods to NodeStorageDriver - Update jack.js with new CLI flags and options - Update outputCommand to handle string usage exports - Add comprehensive test suite (26 tests) for cache clear functionality - Update doc/cli-reference.md with accurate command documentation Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a41c75c commit 33b8c7a

7 files changed

Lines changed: 871 additions & 18 deletions

File tree

cli/cli/src/cmd/cache/clear.js

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
import { rm, readdir } from 'node:fs/promises';
2+
import { createInterface } from 'node:readline';
3+
import { Cache, decodeCacheKey, createStorageDriver, createPackumentKey } from '@_all_docs/cache';
4+
5+
export const usage = `Usage: _all_docs cache clear [options]
6+
7+
Clear cache entries.
8+
9+
Options:
10+
--packuments Clear packument cache only
11+
--partitions Clear partition cache only
12+
--checkpoints Clear checkpoint files only
13+
--registry <url> Clear entries for specific registry origin
14+
--match-origin <key> Clear entries matching origin key (e.g., paces.exale.com~javpt)
15+
--package <name> Clear cache for specific package
16+
--older-than <dur> Clear entries older than duration (e.g., 7d, 24h, 30m)
17+
--dry-run Show what would be cleared without deleting
18+
--interactive Prompt for confirmation before clearing
19+
20+
Examples:
21+
_all_docs cache clear # Clear everything
22+
_all_docs cache clear --packuments # Clear only packuments
23+
_all_docs cache clear --partitions # Clear only partitions
24+
_all_docs cache clear --checkpoints # Clear only checkpoints
25+
_all_docs cache clear --registry https://registry.npmjs.com
26+
_all_docs cache clear --match-origin paces.exale.com~javpt
27+
_all_docs cache clear --dry-run # Preview what would be cleared
28+
_all_docs cache clear --interactive # Confirm before clearing
29+
_all_docs cache clear --packuments --older-than 7d
30+
_all_docs cache clear --package lodash
31+
`;
32+
33+
/**
34+
* Parse duration string to milliseconds
35+
* @param {string} duration - Duration string (e.g., "7d", "24h", "30m")
36+
* @returns {number} Duration in milliseconds
37+
*/
38+
function parseDuration(duration) {
39+
const match = duration.match(/^(\d+)(d|h|m|s)$/);
40+
if (!match) {
41+
throw new Error(`Invalid duration format: ${duration}. Use format like 7d, 24h, 30m, or 60s`);
42+
}
43+
44+
const value = parseInt(match[1], 10);
45+
const unit = match[2];
46+
47+
const multipliers = {
48+
s: 1000,
49+
m: 60 * 1000,
50+
h: 60 * 60 * 1000,
51+
d: 24 * 60 * 60 * 1000
52+
};
53+
54+
return value * multipliers[unit];
55+
}
56+
57+
/**
58+
* Prompt user for confirmation
59+
* @param {string} message - Confirmation message
60+
* @returns {Promise<boolean>} User's response
61+
*/
62+
async function confirm(message) {
63+
const rl = createInterface({
64+
input: process.stdin,
65+
output: process.stdout
66+
});
67+
68+
return new Promise(resolve => {
69+
rl.question(`${message} [y/N] `, answer => {
70+
rl.close();
71+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
72+
});
73+
});
74+
}
75+
76+
/**
77+
* Clear entries from a cache directory
78+
* @param {Object} options - Clear options
79+
* @returns {Promise<{cleared: number, skipped: number}>}
80+
*/
81+
async function clearCache({ cachePath, registry, origin, packageName, olderThan, dryRun, entityType }) {
82+
const env = { CACHE_DIR: cachePath };
83+
const driver = await createStorageDriver(env);
84+
const cache = new Cache({ path: cachePath, driver });
85+
86+
let cleared = 0;
87+
let skipped = 0;
88+
const now = Date.now();
89+
90+
// Fast path: clear everything if no filters
91+
if (!registry && !origin && !packageName && !olderThan) {
92+
// Count entries first
93+
for await (const key of cache.keys('')) {
94+
try {
95+
const decoded = decodeCacheKey(key);
96+
if (!entityType || decoded.type === entityType) {
97+
const displayName = decoded.type === 'packument'
98+
? decoded.packageName
99+
: `partition:${decoded.startKey || ''}..${decoded.endKey || ''}`;
100+
101+
if (dryRun) {
102+
console.log(` [dry-run] Would delete: ${displayName}`);
103+
}
104+
cleared++;
105+
}
106+
} catch {
107+
cleared++;
108+
}
109+
}
110+
111+
if (!dryRun && cleared > 0) {
112+
await driver.clear();
113+
console.log(` Cleared ${cleared} entries`);
114+
}
115+
116+
return { cleared, skipped };
117+
}
118+
119+
// If clearing specific package, construct the key directly
120+
if (packageName) {
121+
const registryUrl = registry || 'https://registry.npmjs.com';
122+
const key = createPackumentKey(packageName, registryUrl);
123+
124+
try {
125+
const exists = await cache.has(key);
126+
if (exists) {
127+
if (dryRun) {
128+
console.log(` [dry-run] Would delete: ${packageName} (${registryUrl})`);
129+
} else {
130+
await driver.delete(key);
131+
console.log(` Deleted: ${packageName}`);
132+
}
133+
cleared++;
134+
} else {
135+
console.log(` Not found: ${packageName}`);
136+
skipped++;
137+
}
138+
} catch (error) {
139+
console.error(` Error clearing ${packageName}: ${error.message}`);
140+
skipped++;
141+
}
142+
143+
return { cleared, skipped };
144+
}
145+
146+
// Iterate all entries with filters
147+
for await (const key of cache.keys('')) {
148+
try {
149+
const decoded = decodeCacheKey(key);
150+
151+
// Filter by entity type
152+
if (entityType && decoded.type !== entityType) {
153+
continue;
154+
}
155+
156+
// Filter by registry URL
157+
if (registry) {
158+
const keyForRegistry = createPackumentKey('test', registry);
159+
const decodedRegistry = decodeCacheKey(keyForRegistry);
160+
if (decoded.origin !== decodedRegistry.origin) {
161+
skipped++;
162+
continue;
163+
}
164+
}
165+
166+
// Filter by origin key directly
167+
if (origin && !key.includes(`:${origin}:`)) {
168+
skipped++;
169+
continue;
170+
}
171+
172+
// Filter by age
173+
if (olderThan) {
174+
try {
175+
const info = await driver.info(key);
176+
if (info && info.time) {
177+
const age = now - info.time;
178+
if (age < olderThan) {
179+
skipped++;
180+
continue;
181+
}
182+
}
183+
} catch {
184+
// If we can't get info, skip the age check
185+
}
186+
}
187+
188+
// Clear the entry
189+
const displayName = decoded.type === 'packument'
190+
? decoded.packageName
191+
: `partition:${decoded.startKey || ''}..${decoded.endKey || ''}`;
192+
193+
if (dryRun) {
194+
console.log(` [dry-run] Would delete: ${displayName} (${decoded.origin})`);
195+
} else {
196+
await driver.delete(key);
197+
console.log(` Deleted: ${displayName}`);
198+
}
199+
cleared++;
200+
} catch (error) {
201+
// Skip entries that can't be decoded or deleted
202+
skipped++;
203+
}
204+
}
205+
206+
return { cleared, skipped };
207+
}
208+
209+
/**
210+
* Clear checkpoint files
211+
* @param {string} checkpointDir - Checkpoint directory path
212+
* @param {boolean} dryRun - Whether to do a dry run
213+
* @returns {Promise<{cleared: number}>}
214+
*/
215+
async function clearCheckpoints(checkpointDir, dryRun) {
216+
let cleared = 0;
217+
218+
try {
219+
const files = await readdir(checkpointDir);
220+
221+
for (const file of files) {
222+
if (file.endsWith('.checkpoint.json')) {
223+
const filePath = `${checkpointDir}/${file}`;
224+
if (dryRun) {
225+
console.log(` [dry-run] Would delete: ${file}`);
226+
} else {
227+
await rm(filePath);
228+
console.log(` Deleted: ${file}`);
229+
}
230+
cleared++;
231+
}
232+
}
233+
} catch (error) {
234+
if (error.code !== 'ENOENT') {
235+
console.error(` Error clearing checkpoints: ${error.message}`);
236+
}
237+
}
238+
239+
return { cleared };
240+
}
241+
242+
export const command = async cli => {
243+
if (cli.values.help) {
244+
console.log(usage);
245+
return;
246+
}
247+
248+
const clearPackuments = cli.values.packuments;
249+
const clearPartitions = cli.values.partitions;
250+
const clearCheckpointsFlag = cli.values.checkpoints;
251+
const registry = cli.values.registry;
252+
const origin = cli.values['match-origin'];
253+
const packageName = cli.values.package;
254+
const olderThanStr = cli.values['older-than'];
255+
const dryRun = cli.values['dry-run'];
256+
const interactive = cli.values.interactive;
257+
258+
// Parse duration if provided
259+
let olderThan = null;
260+
if (olderThanStr) {
261+
try {
262+
olderThan = parseDuration(olderThanStr);
263+
} catch (error) {
264+
console.error(`Error: ${error.message}`);
265+
process.exit(1);
266+
}
267+
}
268+
269+
// If no specific type selected, clear all
270+
const clearAll = !clearPackuments && !clearPartitions && !clearCheckpointsFlag;
271+
272+
// Build summary of what will be cleared
273+
const targets = [];
274+
if (clearAll || clearPackuments) targets.push('packuments');
275+
if (clearAll || clearPartitions) targets.push('partitions');
276+
if (clearAll || clearCheckpointsFlag) targets.push('checkpoints');
277+
278+
let description = `Clear ${targets.join(', ')}`;
279+
if (registry) description += ` for registry ${registry}`;
280+
if (origin) description += ` matching origin ${origin}`;
281+
if (packageName) description += ` for package ${packageName}`;
282+
if (olderThan) description += ` older than ${olderThanStr}`;
283+
284+
console.log(description);
285+
console.log();
286+
287+
// Interactive confirmation
288+
if (interactive && !dryRun) {
289+
const confirmed = await confirm('Are you sure you want to proceed?');
290+
if (!confirmed) {
291+
console.log('Aborted.');
292+
process.exit(0);
293+
}
294+
console.log();
295+
}
296+
297+
let totalCleared = 0;
298+
let totalSkipped = 0;
299+
300+
// Clear packuments
301+
if (clearAll || clearPackuments) {
302+
const packumentsDir = cli.dir('packuments');
303+
console.log(`Packuments (${packumentsDir}):`);
304+
305+
const result = await clearCache({
306+
cachePath: packumentsDir,
307+
registry,
308+
origin,
309+
packageName,
310+
olderThan,
311+
dryRun,
312+
entityType: 'packument'
313+
});
314+
315+
totalCleared += result.cleared;
316+
totalSkipped += result.skipped;
317+
318+
if (result.cleared === 0 && result.skipped === 0) {
319+
console.log(' (empty)');
320+
}
321+
console.log();
322+
}
323+
324+
// Clear partitions
325+
if ((clearAll || clearPartitions) && !packageName) {
326+
const partitionsDir = cli.dir('partitions');
327+
console.log(`Partitions (${partitionsDir}):`);
328+
329+
const result = await clearCache({
330+
cachePath: partitionsDir,
331+
registry,
332+
origin,
333+
olderThan,
334+
dryRun,
335+
entityType: 'partition'
336+
});
337+
338+
totalCleared += result.cleared;
339+
totalSkipped += result.skipped;
340+
341+
if (result.cleared === 0 && result.skipped === 0) {
342+
console.log(' (empty)');
343+
}
344+
console.log();
345+
}
346+
347+
// Clear checkpoints
348+
if ((clearAll || clearCheckpointsFlag) && !packageName && !registry && !origin) {
349+
const checkpointsDir = `${cli.dir('packuments')}/../checkpoints`;
350+
console.log(`Checkpoints (${checkpointsDir}):`);
351+
352+
const result = await clearCheckpoints(checkpointsDir, dryRun);
353+
totalCleared += result.cleared;
354+
355+
if (result.cleared === 0) {
356+
console.log(' (empty)');
357+
}
358+
console.log();
359+
}
360+
361+
// Summary
362+
const action = dryRun ? 'Would clear' : 'Cleared';
363+
console.log(`${action} ${totalCleared} entries${totalSkipped > 0 ? `, skipped ${totalSkipped}` : ''}`);
364+
};

0 commit comments

Comments
 (0)