|
2 | 2 | * Cache management commands |
3 | 3 | */ |
4 | 4 |
|
| 5 | +import type { Dirent } from 'node:fs' |
5 | 6 | import { existsSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs' |
6 | 7 | import * as p from '@clack/prompts' |
7 | 8 | import { defineCommand } from 'citty' |
8 | 9 | import { join } from 'pathe' |
9 | | -import { CACHE_DIR } from '../cache/index.ts' |
| 10 | +import { CACHE_DIR, REFERENCES_DIR, REPOS_DIR } from '../cache/index.ts' |
10 | 11 | import { clearEmbeddingCache } from '../retriv/embedding-cache.ts' |
11 | 12 |
|
12 | 13 | const LLM_CACHE_DIR = join(CACHE_DIR, 'llm-cache') |
@@ -75,17 +76,94 @@ export async function cacheCleanCommand(): Promise<void> { |
75 | 76 | } |
76 | 77 | } |
77 | 78 |
|
| 79 | +function dirEntries(dir: string): Dirent[] { |
| 80 | + if (!existsSync(dir)) |
| 81 | + return [] |
| 82 | + return readdirSync(dir, { withFileTypes: true, recursive: true }) |
| 83 | +} |
| 84 | + |
| 85 | +function sumFileBytes(entries: Dirent[]): number { |
| 86 | + return entries |
| 87 | + .filter(e => e.isFile()) |
| 88 | + .reduce((sum, e) => { |
| 89 | + try { |
| 90 | + return sum + statSync(join(e.parentPath, e.name)).size |
| 91 | + } |
| 92 | + catch { return sum } |
| 93 | + }, 0) |
| 94 | +} |
| 95 | + |
| 96 | +function fmtBytes(n: number): string { |
| 97 | + const units = ['B', 'KB', 'MB', 'GB'] as const |
| 98 | + let i = 0 |
| 99 | + while (n >= 1024 && i < units.length - 1) { |
| 100 | + n /= 1024 |
| 101 | + i++ |
| 102 | + } |
| 103 | + return i === 0 ? `${n}${units[i]}` : `${n.toFixed(1)}${units[i]}` |
| 104 | +} |
| 105 | + |
| 106 | +export function cacheStatsCommand(): void { |
| 107 | + const dim = (s: string) => `\x1B[90m${s}\x1B[0m` |
| 108 | + |
| 109 | + const refs = dirEntries(REFERENCES_DIR) |
| 110 | + const repos = dirEntries(REPOS_DIR) |
| 111 | + const llm = dirEntries(LLM_CACHE_DIR) |
| 112 | + const embPath = join(CACHE_DIR, 'embeddings.db') |
| 113 | + const embSize = existsSync(embPath) ? statSync(embPath).size : 0 |
| 114 | + |
| 115 | + // Count packages: top-level non-scoped dirs + dirs inside @scope/ dirs |
| 116 | + const packages = refs.filter(e => |
| 117 | + e.isDirectory() |
| 118 | + && (e.parentPath === REFERENCES_DIR |
| 119 | + ? !e.name.startsWith('@') |
| 120 | + : e.parentPath.startsWith(REFERENCES_DIR)), |
| 121 | + ).length |
| 122 | + |
| 123 | + const llmFiles = llm.filter(e => e.isFile()) |
| 124 | + const sizes = { refs: sumFileBytes(refs), repos: sumFileBytes(repos), llm: sumFileBytes(llmFiles), emb: embSize } |
| 125 | + const total = sizes.refs + sizes.repos + sizes.llm + sizes.emb |
| 126 | + |
| 127 | + const lines = [ |
| 128 | + `References ${fmtBytes(sizes.refs)} ${dim(`${packages} packages`)}`, |
| 129 | + ...(sizes.repos > 0 ? [`Repos ${fmtBytes(sizes.repos)}`] : []), |
| 130 | + `LLM cache ${fmtBytes(sizes.llm)} ${dim(`${llmFiles.length} entries`)}`, |
| 131 | + ...(sizes.emb > 0 ? [`Embeddings ${fmtBytes(sizes.emb)}`] : []), |
| 132 | + '', |
| 133 | + `Total ${fmtBytes(total)} ${dim(CACHE_DIR)}`, |
| 134 | + ] |
| 135 | + p.log.message(lines.join('\n')) |
| 136 | +} |
| 137 | + |
78 | 138 | export const cacheCommandDef = defineCommand({ |
79 | 139 | meta: { name: 'cache', description: 'Cache management', hidden: true }, |
80 | 140 | args: { |
81 | 141 | clean: { |
82 | 142 | type: 'boolean', |
| 143 | + alias: 'c', |
83 | 144 | description: 'Remove expired enhancement cache entries', |
84 | | - default: true, |
| 145 | + default: false, |
| 146 | + }, |
| 147 | + stats: { |
| 148 | + type: 'boolean', |
| 149 | + alias: 's', |
| 150 | + description: 'Show cache disk usage', |
| 151 | + default: false, |
85 | 152 | }, |
86 | 153 | }, |
87 | | - async run() { |
88 | | - p.intro(`\x1B[1m\x1B[35mskilld\x1B[0m cache clean`) |
89 | | - await cacheCleanCommand() |
| 154 | + async run({ args }) { |
| 155 | + if (args.stats) { |
| 156 | + p.intro(`\x1B[1m\x1B[35mskilld\x1B[0m cache stats`) |
| 157 | + cacheStatsCommand() |
| 158 | + return |
| 159 | + } |
| 160 | + if (args.clean) { |
| 161 | + p.intro(`\x1B[1m\x1B[35mskilld\x1B[0m cache clean`) |
| 162 | + await cacheCleanCommand() |
| 163 | + return |
| 164 | + } |
| 165 | + // No flag: show usage |
| 166 | + p.intro(`\x1B[1m\x1B[35mskilld\x1B[0m cache`) |
| 167 | + p.log.message('Usage:\n skilld cache --clean Remove expired cache entries\n skilld cache --stats Show cache disk usage') |
90 | 168 | }, |
91 | 169 | }) |
0 commit comments