@rayhanadev/katto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +186 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +518 -0
- package/dist/index.mjs.map +1 -0
- package/dist/sdk-JYaqBNlb.mjs +422 -0
- package/dist/sdk-JYaqBNlb.mjs.map +1 -0
- package/dist/sdk.d.mts +71 -0
- package/dist/sdk.d.mts.map +1 -0
- package/dist/sdk.mjs +2 -0
- package/package.json +67 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { lstat, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
//#region src/constants.ts
|
|
5
|
+
const VERSION = "0.1.0";
|
|
6
|
+
const DEFAULT_TARGETS = ["node_modules"];
|
|
7
|
+
const DEFAULT_PRUNES = [
|
|
8
|
+
".git",
|
|
9
|
+
".hg",
|
|
10
|
+
".svn"
|
|
11
|
+
];
|
|
12
|
+
const CPU_COUNT = navigator.hardwareConcurrency || 8;
|
|
13
|
+
const WALK_BASE_CONCURRENCY = CPU_COUNT * 32;
|
|
14
|
+
const WALK_CONCURRENCY = WALK_BASE_CONCURRENCY < 32 ? 32 : WALK_BASE_CONCURRENCY > 256 ? 256 : WALK_BASE_CONCURRENCY;
|
|
15
|
+
const SIZE_CONCURRENCY = CPU_COUNT < 4 ? 4 : CPU_COUNT > 8 ? 8 : CPU_COUNT;
|
|
16
|
+
const DELETE_CONCURRENCY = CPU_COUNT < 2 ? 2 : CPU_COUNT > 8 ? 8 : CPU_COUNT;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/core/semaphore.ts
|
|
19
|
+
var Semaphore = class {
|
|
20
|
+
limit;
|
|
21
|
+
active = 0;
|
|
22
|
+
queue = [];
|
|
23
|
+
constructor(limit) {
|
|
24
|
+
this.limit = limit;
|
|
25
|
+
}
|
|
26
|
+
async run(fn) {
|
|
27
|
+
if (this.active >= this.limit) await new Promise((resolveQueued) => this.queue.push(resolveQueued));
|
|
28
|
+
this.active++;
|
|
29
|
+
try {
|
|
30
|
+
return await fn();
|
|
31
|
+
} finally {
|
|
32
|
+
this.active--;
|
|
33
|
+
this.queue.shift()?.();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/core/delete.ts
|
|
39
|
+
async function deleteEntry(entry, options) {
|
|
40
|
+
if (!isSafeTarget(entry.path, options.targets)) {
|
|
41
|
+
entry.status = "failed";
|
|
42
|
+
entry.error = "refused: path basename is not a target";
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (options.dryRun) {
|
|
46
|
+
await Bun.sleep(80);
|
|
47
|
+
entry.status = "deleted";
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
entry.status = "deleting";
|
|
51
|
+
try {
|
|
52
|
+
await rm(entry.path, {
|
|
53
|
+
recursive: true,
|
|
54
|
+
force: true,
|
|
55
|
+
maxRetries: 2,
|
|
56
|
+
retryDelay: 20
|
|
57
|
+
});
|
|
58
|
+
entry.status = "deleted";
|
|
59
|
+
return true;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
entry.status = "failed";
|
|
62
|
+
entry.error = error instanceof Error ? error.message : String(error);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function deleteAll(entries, options, stats, onChange) {
|
|
67
|
+
const semaphore = new Semaphore(DELETE_CONCURRENCY);
|
|
68
|
+
await Promise.all(entries.filter((entry) => entry.status !== "deleted").map((entry) => semaphore.run(async () => {
|
|
69
|
+
const size = entry.size ?? 0;
|
|
70
|
+
onChange(entry);
|
|
71
|
+
if (await deleteEntry(entry, options)) {
|
|
72
|
+
stats.deleted++;
|
|
73
|
+
stats.reclaimed += size;
|
|
74
|
+
}
|
|
75
|
+
onChange(entry);
|
|
76
|
+
})));
|
|
77
|
+
}
|
|
78
|
+
function isSafeTarget(path, targets) {
|
|
79
|
+
return targets.includes(basename(path));
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/core/options.ts
|
|
83
|
+
function createOptions(input = {}) {
|
|
84
|
+
const targets = uniqueList(input.targets ?? DEFAULT_TARGETS);
|
|
85
|
+
if (targets.length === 0) throw new Error("At least one target is required.");
|
|
86
|
+
return {
|
|
87
|
+
root: resolveRoot(input.root),
|
|
88
|
+
targets,
|
|
89
|
+
exclude: uniqueList(input.exclude ?? []),
|
|
90
|
+
excludeSensitive: input.excludeSensitive ?? false,
|
|
91
|
+
dryRun: input.dryRun ?? false,
|
|
92
|
+
noSize: input.noSize ?? false,
|
|
93
|
+
sizeStrategy: input.noSize ? "none" : input.sizeStrategy ?? "auto",
|
|
94
|
+
sort: input.sort ?? "size",
|
|
95
|
+
sizeUnit: input.sizeUnit ?? "auto"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function uniqueList(values) {
|
|
99
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
100
|
+
}
|
|
101
|
+
function resolveRoot(root) {
|
|
102
|
+
const value = root ?? process.cwd();
|
|
103
|
+
if (value === "~") return homedir();
|
|
104
|
+
const homeRelative = stripPrefix(value, "~/");
|
|
105
|
+
if (homeRelative !== null) return resolve(homedir(), homeRelative);
|
|
106
|
+
return resolve(value);
|
|
107
|
+
}
|
|
108
|
+
function stripPrefix(value, prefix) {
|
|
109
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : null;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/core/results.ts
|
|
113
|
+
function sortedEntries(entries, sort) {
|
|
114
|
+
const copy = [...entries];
|
|
115
|
+
switch (sort) {
|
|
116
|
+
case "size": return copy.sort((a, b) => (b.size ?? -1) - (a.size ?? -1));
|
|
117
|
+
case "path": return copy.sort((a, b) => a.path.localeCompare(b.path));
|
|
118
|
+
case "age": return copy.sort((a, b) => (a.mtime ?? 0) - (b.mtime ?? 0));
|
|
119
|
+
case "found": return copy.sort((a, b) => a.id - b.id);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function serializeEntry(entry) {
|
|
123
|
+
return {
|
|
124
|
+
path: entry.path,
|
|
125
|
+
size: entry.size,
|
|
126
|
+
mtime: entry.mtime,
|
|
127
|
+
status: entry.status,
|
|
128
|
+
error: entry.error
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/core/scanner.ts
|
|
133
|
+
async function scanEntries(options, onEntryChange) {
|
|
134
|
+
const entries = [];
|
|
135
|
+
const seen = /* @__PURE__ */ new Set();
|
|
136
|
+
const targetNames = new Set(options.targets);
|
|
137
|
+
const pruneNames = new Set([...DEFAULT_PRUNES, ...options.exclude]);
|
|
138
|
+
const prunePaths = new Set(options.exclude.filter((item) => item.includes("/")).map((item) => resolve(options.root, item)));
|
|
139
|
+
if (options.excludeSensitive) for (const item of sensitivePrunes()) pruneNames.add(item);
|
|
140
|
+
let id = 0;
|
|
141
|
+
await walkDirs(options.root, (parent, direntName) => {
|
|
142
|
+
const path = `${parent}/${direntName}`;
|
|
143
|
+
if (prunePaths.has(path) || pruneNames.has(direntName) && !targetNames.has(direntName)) return "prune";
|
|
144
|
+
if (!targetNames.has(direntName)) return "descend";
|
|
145
|
+
if (seen.has(path)) return "prune";
|
|
146
|
+
seen.add(path);
|
|
147
|
+
const entry = {
|
|
148
|
+
id: id++,
|
|
149
|
+
path,
|
|
150
|
+
name: direntName,
|
|
151
|
+
size: null,
|
|
152
|
+
mtime: null,
|
|
153
|
+
status: shouldHydrateSize(options) ? "sizing" : "ready"
|
|
154
|
+
};
|
|
155
|
+
entries.push(entry);
|
|
156
|
+
onEntryChange?.(entry);
|
|
157
|
+
return "prune";
|
|
158
|
+
});
|
|
159
|
+
if (shouldHydrateSize(options)) await hydrateEntryMetadata(entries, options, onEntryChange);
|
|
160
|
+
return entries;
|
|
161
|
+
}
|
|
162
|
+
function sensitivePrunes() {
|
|
163
|
+
return [
|
|
164
|
+
"Library",
|
|
165
|
+
"Applications",
|
|
166
|
+
"System",
|
|
167
|
+
"Volumes",
|
|
168
|
+
".Trash",
|
|
169
|
+
".cache",
|
|
170
|
+
".config",
|
|
171
|
+
".local",
|
|
172
|
+
".ssh",
|
|
173
|
+
".gnupg"
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
async function hydrateEntryMetadata(entries, options, onEntryChange) {
|
|
177
|
+
const semaphore = new Semaphore(SIZE_CONCURRENCY);
|
|
178
|
+
await Promise.allSettled(entries.map((entry) => semaphore.run(() => hydrateOneEntryMetadata(entry, options, onEntryChange))));
|
|
179
|
+
}
|
|
180
|
+
async function hydrateOneEntryMetadata(entry, options, onEntryChange) {
|
|
181
|
+
try {
|
|
182
|
+
const [size, mtime] = await Promise.all([entrySize(entry.path, options), statMtime(entry.path)]);
|
|
183
|
+
entry.size = size;
|
|
184
|
+
entry.mtime = mtime;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
entry.error = error instanceof Error ? error.message : String(error);
|
|
187
|
+
}
|
|
188
|
+
entry.status = "ready";
|
|
189
|
+
onEntryChange?.(entry);
|
|
190
|
+
}
|
|
191
|
+
function shouldHydrateSize(options) {
|
|
192
|
+
return !options.noSize && options.sizeStrategy !== "none";
|
|
193
|
+
}
|
|
194
|
+
async function entrySize(path, options) {
|
|
195
|
+
switch (options.sizeStrategy) {
|
|
196
|
+
case "native": return await duSize(path);
|
|
197
|
+
case "js": return await jsSize(path);
|
|
198
|
+
case "auto": return await duSize(path) ?? await jsSize(path);
|
|
199
|
+
case "none": return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function walkDirs(root, visit) {
|
|
203
|
+
const queue = [root];
|
|
204
|
+
let active = 0;
|
|
205
|
+
let index = 0;
|
|
206
|
+
let rejectWalk = null;
|
|
207
|
+
await new Promise((resolveWalk, reject) => {
|
|
208
|
+
rejectWalk = reject;
|
|
209
|
+
const pump = () => {
|
|
210
|
+
if (index >= queue.length && active === 0) {
|
|
211
|
+
resolveWalk();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
while (active < WALK_CONCURRENCY && index < queue.length) {
|
|
215
|
+
const dir = queue[index++];
|
|
216
|
+
active++;
|
|
217
|
+
readdir(dir, { withFileTypes: true }).then((dirents) => {
|
|
218
|
+
for (const dirent of dirents) {
|
|
219
|
+
if (dirent.isSymbolicLink() || !dirent.isDirectory()) continue;
|
|
220
|
+
if (visit(dir, dirent.name) === "descend") queue.push(`${dir}/${dirent.name}`);
|
|
221
|
+
}
|
|
222
|
+
}).catch((error) => {
|
|
223
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : "";
|
|
224
|
+
if (code !== "EACCES" && code !== "EPERM" && code !== "ENOENT" && rejectWalk) rejectWalk(error);
|
|
225
|
+
}).finally(() => {
|
|
226
|
+
active--;
|
|
227
|
+
pump();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
pump();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function duSize(path) {
|
|
235
|
+
return await runDu([
|
|
236
|
+
"du",
|
|
237
|
+
"-skA",
|
|
238
|
+
path
|
|
239
|
+
]) ?? await runDu([
|
|
240
|
+
"du",
|
|
241
|
+
"-sk",
|
|
242
|
+
"--apparent-size",
|
|
243
|
+
path
|
|
244
|
+
]) ?? await runDu([
|
|
245
|
+
"du",
|
|
246
|
+
"-sk",
|
|
247
|
+
path
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
250
|
+
async function runDu(command) {
|
|
251
|
+
try {
|
|
252
|
+
const proc = Bun.spawn(command, {
|
|
253
|
+
stdout: "pipe",
|
|
254
|
+
stderr: "ignore"
|
|
255
|
+
});
|
|
256
|
+
const output = await new Response(proc.stdout).text();
|
|
257
|
+
if (await proc.exited !== 0) return null;
|
|
258
|
+
const match = /^(\d+)/.exec(output);
|
|
259
|
+
return match ? Number(match[1]) * 1024 : null;
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function jsSize(path) {
|
|
265
|
+
let total = 0;
|
|
266
|
+
const queue = [path];
|
|
267
|
+
let active = 0;
|
|
268
|
+
let index = 0;
|
|
269
|
+
await new Promise((resolveSize) => {
|
|
270
|
+
const pump = () => {
|
|
271
|
+
if (index >= queue.length && active === 0) {
|
|
272
|
+
resolveSize();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
while (active < SIZE_CONCURRENCY && index < queue.length) {
|
|
276
|
+
const dir = queue[index++];
|
|
277
|
+
active++;
|
|
278
|
+
readdir(dir, { withFileTypes: true }).then(async (dirents) => {
|
|
279
|
+
await Promise.all(dirents.map((dirent) => addDirentSize(dir, dirent, queue)));
|
|
280
|
+
}).catch(() => {}).finally(() => {
|
|
281
|
+
active--;
|
|
282
|
+
pump();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const addDirentSize = async (dir, dirent, queue) => {
|
|
287
|
+
if (dirent.isSymbolicLink()) return;
|
|
288
|
+
const child = `${dir}/${dirent.name}`;
|
|
289
|
+
if (dirent.isDirectory()) {
|
|
290
|
+
queue.push(child);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
total += (await lstat(child)).size;
|
|
295
|
+
} catch {}
|
|
296
|
+
};
|
|
297
|
+
pump();
|
|
298
|
+
});
|
|
299
|
+
return total;
|
|
300
|
+
}
|
|
301
|
+
async function statMtime(path) {
|
|
302
|
+
try {
|
|
303
|
+
return (await lstat(path)).mtimeMs;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/core/stats.ts
|
|
310
|
+
function freshStats() {
|
|
311
|
+
return {
|
|
312
|
+
scanned: 0,
|
|
313
|
+
found: 0,
|
|
314
|
+
sized: 0,
|
|
315
|
+
deleted: 0,
|
|
316
|
+
reclaimed: 0,
|
|
317
|
+
startedAt: Date.now(),
|
|
318
|
+
done: false
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/sdk.ts
|
|
323
|
+
var Katto = class {
|
|
324
|
+
options;
|
|
325
|
+
entries = [];
|
|
326
|
+
stats = freshStats();
|
|
327
|
+
constructor(options = {}) {
|
|
328
|
+
this.options = createOptions(options);
|
|
329
|
+
}
|
|
330
|
+
async scan() {
|
|
331
|
+
this.entries = [];
|
|
332
|
+
this.stats = freshStats();
|
|
333
|
+
this.entries = await scanEntries(this.options);
|
|
334
|
+
this.refreshScanStats();
|
|
335
|
+
this.stats.scanned = this.entries.length;
|
|
336
|
+
this.stats.done = true;
|
|
337
|
+
return this.entries;
|
|
338
|
+
}
|
|
339
|
+
async *scanWithProgress() {
|
|
340
|
+
const seen = /* @__PURE__ */ new Set();
|
|
341
|
+
const queue = [];
|
|
342
|
+
let notify;
|
|
343
|
+
let done = false;
|
|
344
|
+
let error;
|
|
345
|
+
let result = [];
|
|
346
|
+
this.entries = [];
|
|
347
|
+
this.stats = freshStats();
|
|
348
|
+
const wake = () => {
|
|
349
|
+
const resolve = notify;
|
|
350
|
+
notify = void 0;
|
|
351
|
+
resolve?.();
|
|
352
|
+
};
|
|
353
|
+
const scanPromise = scanEntries(this.options, (entry) => {
|
|
354
|
+
const phase = seen.has(entry.id) ? "updated" : "found";
|
|
355
|
+
seen.add(entry.id);
|
|
356
|
+
if (!this.entries.includes(entry)) this.entries.push(entry);
|
|
357
|
+
this.refreshScanStats();
|
|
358
|
+
queue.push({
|
|
359
|
+
phase,
|
|
360
|
+
entry,
|
|
361
|
+
entries: this.entries,
|
|
362
|
+
stats: this.stats
|
|
363
|
+
});
|
|
364
|
+
wake();
|
|
365
|
+
}).then((entries) => {
|
|
366
|
+
result = entries;
|
|
367
|
+
this.entries = entries;
|
|
368
|
+
this.refreshScanStats();
|
|
369
|
+
this.stats.scanned = entries.length;
|
|
370
|
+
this.stats.done = true;
|
|
371
|
+
}).catch((scanError) => {
|
|
372
|
+
error = scanError;
|
|
373
|
+
}).finally(() => {
|
|
374
|
+
done = true;
|
|
375
|
+
wake();
|
|
376
|
+
});
|
|
377
|
+
while (!done || queue.length > 0) {
|
|
378
|
+
const progress = queue.shift();
|
|
379
|
+
if (progress) {
|
|
380
|
+
yield progress;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
await new Promise((resolve) => {
|
|
384
|
+
notify = resolve;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
await scanPromise;
|
|
388
|
+
if (error) throw error;
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
async deleteEntry(entry) {
|
|
392
|
+
const size = entry.size ?? 0;
|
|
393
|
+
const ok = await deleteEntry(entry, this.options);
|
|
394
|
+
if (ok) {
|
|
395
|
+
this.stats.deleted++;
|
|
396
|
+
this.stats.reclaimed += size;
|
|
397
|
+
}
|
|
398
|
+
return ok;
|
|
399
|
+
}
|
|
400
|
+
async deleteAll(entries = this.entries, onProgress) {
|
|
401
|
+
await deleteAll(entries, this.options, this.stats, (entry) => {
|
|
402
|
+
onProgress?.({
|
|
403
|
+
entry,
|
|
404
|
+
stats: this.stats
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
sort(entries = this.entries, sort = this.options.sort) {
|
|
409
|
+
return sortedEntries(entries, sort);
|
|
410
|
+
}
|
|
411
|
+
serialize(entry) {
|
|
412
|
+
return serializeEntry(entry);
|
|
413
|
+
}
|
|
414
|
+
refreshScanStats() {
|
|
415
|
+
this.stats.found = this.entries.length;
|
|
416
|
+
this.stats.sized = this.entries.filter((entry) => entry.size !== null).length;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
//#endregion
|
|
420
|
+
export { DEFAULT_TARGETS as a, createOptions as i, freshStats as n, VERSION as o, sortedEntries as r, Katto as t };
|
|
421
|
+
|
|
422
|
+
//# sourceMappingURL=sdk-JYaqBNlb.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-JYaqBNlb.mjs","names":["deleteOneEntry","deleteEntries"],"sources":["../src/constants.ts","../src/core/semaphore.ts","../src/core/delete.ts","../src/core/options.ts","../src/core/results.ts","../src/core/scanner.ts","../src/core/stats.ts","../src/sdk.ts"],"sourcesContent":["export const VERSION = \"0.1.0\";\nexport const DEFAULT_TARGETS = [\"node_modules\"];\nexport const DEFAULT_PRUNES = [\".git\", \".hg\", \".svn\"];\n\nconst CPU_COUNT = navigator.hardwareConcurrency || 8;\nconst WALK_BASE_CONCURRENCY = CPU_COUNT * 32;\n\nexport const WALK_CONCURRENCY =\n WALK_BASE_CONCURRENCY < 32 ? 32 : WALK_BASE_CONCURRENCY > 256 ? 256 : WALK_BASE_CONCURRENCY;\nexport const SIZE_CONCURRENCY = CPU_COUNT < 4 ? 4 : CPU_COUNT > 8 ? 8 : CPU_COUNT;\nexport const DELETE_CONCURRENCY = CPU_COUNT < 2 ? 2 : CPU_COUNT > 8 ? 8 : CPU_COUNT;\nexport const RENDER_INTERVAL_MS = 66;\n","export class Semaphore {\n private active = 0;\n private readonly queue: Array<() => void> = [];\n\n constructor(private readonly limit: number) {}\n\n async run<T>(fn: () => Promise<T>): Promise<T> {\n if (this.active >= this.limit) {\n await new Promise<void>((resolveQueued) => this.queue.push(resolveQueued));\n }\n\n this.active++;\n try {\n return await fn();\n } finally {\n this.active--;\n this.queue.shift()?.();\n }\n }\n}\n","import { rm } from \"node:fs/promises\";\nimport { basename } from \"node:path\";\n\nimport type { Entry, Options, Stats } from \"../types\";\n\nimport { DELETE_CONCURRENCY } from \"../constants\";\nimport { Semaphore } from \"./semaphore\";\n\nexport async function deleteEntry(entry: Entry, options: Options): Promise<boolean> {\n if (!isSafeTarget(entry.path, options.targets)) {\n entry.status = \"failed\";\n entry.error = \"refused: path basename is not a target\";\n return false;\n }\n\n if (options.dryRun) {\n await Bun.sleep(80);\n entry.status = \"deleted\";\n return true;\n }\n\n entry.status = \"deleting\";\n try {\n await rm(entry.path, {\n recursive: true,\n force: true,\n maxRetries: 2,\n retryDelay: 20,\n });\n entry.status = \"deleted\";\n return true;\n } catch (error) {\n entry.status = \"failed\";\n entry.error = error instanceof Error ? error.message : String(error);\n return false;\n }\n}\n\nexport async function deleteAll(\n entries: Entry[],\n options: Options,\n stats: Stats,\n onChange: (entry?: Entry) => void,\n): Promise<void> {\n const semaphore = new Semaphore(DELETE_CONCURRENCY);\n\n await Promise.all(\n entries\n .filter((entry) => entry.status !== \"deleted\")\n .map((entry) =>\n semaphore.run(async () => {\n const size = entry.size ?? 0;\n onChange(entry);\n\n const ok = await deleteEntry(entry, options);\n if (ok) {\n stats.deleted++;\n stats.reclaimed += size;\n }\n\n onChange(entry);\n }),\n ),\n );\n}\n\nfunction isSafeTarget(path: string, targets: string[]): boolean {\n return targets.includes(basename(path));\n}\n","import { homedir } from \"node:os\";\nimport { resolve } from \"node:path\";\n\nimport type { KattoOptions, Options } from \"../types\";\n\nimport { DEFAULT_TARGETS } from \"../constants\";\n\nexport function createOptions(input: KattoOptions = {}): Options {\n const targets = uniqueList(input.targets ?? DEFAULT_TARGETS);\n if (targets.length === 0) throw new Error(\"At least one target is required.\");\n\n return {\n root: resolveRoot(input.root),\n targets,\n exclude: uniqueList(input.exclude ?? []),\n excludeSensitive: input.excludeSensitive ?? false,\n dryRun: input.dryRun ?? false,\n noSize: input.noSize ?? false,\n sizeStrategy: input.noSize ? \"none\" : (input.sizeStrategy ?? \"auto\"),\n sort: input.sort ?? \"size\",\n sizeUnit: input.sizeUnit ?? \"auto\",\n };\n}\n\nfunction uniqueList(values: string[]): string[] {\n return [...new Set(values.map((value) => value.trim()).filter(Boolean))];\n}\n\nfunction resolveRoot(root: string | undefined): string {\n const value = root ?? process.cwd();\n if (value === \"~\") return homedir();\n const homeRelative = stripPrefix(value, \"~/\");\n if (homeRelative !== null) return resolve(homedir(), homeRelative);\n return resolve(value);\n}\n\nfunction stripPrefix(value: string, prefix: string): string | null {\n return value.startsWith(prefix) ? value.slice(prefix.length) : null;\n}\n","import type { Entry, SortMode } from \"../types\";\n\nexport function sortedEntries(entries: Entry[], sort: SortMode): Entry[] {\n const copy = [...entries];\n\n switch (sort) {\n case \"size\":\n return copy.sort((a, b) => (b.size ?? -1) - (a.size ?? -1));\n case \"path\":\n return copy.sort((a, b) => a.path.localeCompare(b.path));\n case \"age\":\n return copy.sort((a, b) => (a.mtime ?? 0) - (b.mtime ?? 0));\n case \"found\":\n return copy.sort((a, b) => a.id - b.id);\n }\n}\n\nexport function serializeEntry(entry: Entry): object {\n return {\n path: entry.path,\n size: entry.size,\n mtime: entry.mtime,\n status: entry.status,\n error: entry.error,\n };\n}\n","import type { Dirent } from \"node:fs\";\n\nimport { lstat, readdir } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\n\nimport type { Entry, Options } from \"../types\";\n\nimport { DEFAULT_PRUNES, SIZE_CONCURRENCY, WALK_CONCURRENCY } from \"../constants\";\nimport { Semaphore } from \"./semaphore\";\n\nexport async function scanEntries(\n options: Options,\n onEntryChange?: (entry: Entry) => void,\n): Promise<Entry[]> {\n const entries: Entry[] = [];\n const seen = new Set<string>();\n const targetNames = new Set(options.targets);\n const pruneNames = new Set([...DEFAULT_PRUNES, ...options.exclude]);\n const prunePaths = new Set(\n options.exclude.filter((item) => item.includes(\"/\")).map((item) => resolve(options.root, item)),\n );\n\n if (options.excludeSensitive) {\n for (const item of sensitivePrunes()) pruneNames.add(item);\n }\n\n let id = 0;\n\n await walkDirs(options.root, (parent, direntName) => {\n const path = `${parent}/${direntName}`;\n\n if (prunePaths.has(path) || (pruneNames.has(direntName) && !targetNames.has(direntName))) {\n return \"prune\";\n }\n\n if (!targetNames.has(direntName)) return \"descend\";\n if (seen.has(path)) return \"prune\";\n\n seen.add(path);\n const entry: Entry = {\n id: id++,\n path,\n name: direntName,\n size: null,\n mtime: null,\n status: shouldHydrateSize(options) ? \"sizing\" : \"ready\",\n };\n\n entries.push(entry);\n onEntryChange?.(entry);\n\n return \"prune\";\n });\n\n if (shouldHydrateSize(options)) {\n await hydrateEntryMetadata(entries, options, onEntryChange);\n }\n\n return entries;\n}\n\nfunction sensitivePrunes(): string[] {\n return [\n \"Library\",\n \"Applications\",\n \"System\",\n \"Volumes\",\n \".Trash\",\n \".cache\",\n \".config\",\n \".local\",\n \".ssh\",\n \".gnupg\",\n ];\n}\n\nasync function hydrateEntryMetadata(\n entries: Entry[],\n options: Options,\n onEntryChange?: (entry: Entry) => void,\n): Promise<void> {\n const semaphore = new Semaphore(SIZE_CONCURRENCY);\n\n await Promise.allSettled(\n entries.map((entry) =>\n semaphore.run(() => hydrateOneEntryMetadata(entry, options, onEntryChange)),\n ),\n );\n}\n\nasync function hydrateOneEntryMetadata(\n entry: Entry,\n options: Options,\n onEntryChange?: (entry: Entry) => void,\n): Promise<void> {\n try {\n const [size, mtime] = await Promise.all([\n entrySize(entry.path, options),\n statMtime(entry.path),\n ]);\n entry.size = size;\n entry.mtime = mtime;\n } catch (error) {\n entry.error = error instanceof Error ? error.message : String(error);\n }\n\n entry.status = \"ready\";\n onEntryChange?.(entry);\n}\n\nfunction shouldHydrateSize(options: Options): boolean {\n return !options.noSize && options.sizeStrategy !== \"none\";\n}\n\nasync function entrySize(path: string, options: Options): Promise<number | null> {\n switch (options.sizeStrategy) {\n case \"native\":\n return await duSize(path);\n case \"js\":\n return await jsSize(path);\n case \"auto\":\n return (await duSize(path)) ?? (await jsSize(path));\n case \"none\":\n return null;\n }\n}\n\nasync function walkDirs(\n root: string,\n visit: (parent: string, direntName: string) => \"descend\" | \"prune\",\n): Promise<void> {\n const queue = [root];\n let active = 0;\n let index = 0;\n let rejectWalk: ((error: unknown) => void) | null = null;\n\n await new Promise<void>((resolveWalk, reject) => {\n rejectWalk = reject;\n\n const pump = (): void => {\n if (index >= queue.length && active === 0) {\n resolveWalk();\n return;\n }\n\n while (active < WALK_CONCURRENCY && index < queue.length) {\n const dir = queue[index++]!;\n active++;\n\n void readdir(dir, { withFileTypes: true })\n .then((dirents) => {\n for (const dirent of dirents) {\n if (dirent.isSymbolicLink() || !dirent.isDirectory()) continue;\n const decision = visit(dir, dirent.name);\n if (decision === \"descend\") queue.push(`${dir}/${dirent.name}`);\n }\n })\n .catch((error: unknown) => {\n const code =\n typeof error === \"object\" && error !== null && \"code\" in error ? error.code : \"\";\n if (code !== \"EACCES\" && code !== \"EPERM\" && code !== \"ENOENT\" && rejectWalk) {\n rejectWalk(error);\n }\n })\n .finally(() => {\n active--;\n pump();\n });\n }\n };\n\n pump();\n });\n}\n\nasync function duSize(path: string): Promise<number | null> {\n return (\n (await runDu([\"du\", \"-skA\", path])) ??\n (await runDu([\"du\", \"-sk\", \"--apparent-size\", path])) ??\n (await runDu([\"du\", \"-sk\", path]))\n );\n}\n\nasync function runDu(command: string[]): Promise<number | null> {\n try {\n const proc = Bun.spawn(command, {\n stdout: \"pipe\",\n stderr: \"ignore\",\n });\n const output = await new Response(proc.stdout).text();\n const code = await proc.exited;\n if (code !== 0) return null;\n\n const match = /^(\\d+)/.exec(output);\n return match ? Number(match[1]) * 1024 : null;\n } catch {\n return null;\n }\n}\n\nasync function jsSize(path: string): Promise<number | null> {\n let total = 0;\n const queue = [path];\n let active = 0;\n let index = 0;\n\n await new Promise<void>((resolveSize) => {\n const pump = (): void => {\n if (index >= queue.length && active === 0) {\n resolveSize();\n return;\n }\n\n while (active < SIZE_CONCURRENCY && index < queue.length) {\n const dir = queue[index++]!;\n active++;\n\n void readdir(dir, { withFileTypes: true })\n .then(async (dirents) => {\n await Promise.all(dirents.map((dirent) => addDirentSize(dir, dirent, queue)));\n })\n .catch(() => {})\n .finally(() => {\n active--;\n pump();\n });\n }\n };\n\n const addDirentSize = async (\n dir: string,\n dirent: Dirent<string>,\n queue: string[],\n ): Promise<void> => {\n if (dirent.isSymbolicLink()) return;\n\n const child = `${dir}/${dirent.name}`;\n if (dirent.isDirectory()) {\n queue.push(child);\n return;\n }\n\n try {\n total += (await lstat(child)).size;\n } catch {}\n };\n\n pump();\n });\n\n return total;\n}\n\nasync function statMtime(path: string): Promise<number | null> {\n try {\n return (await lstat(path)).mtimeMs;\n } catch {\n return null;\n }\n}\n","import type { Stats } from \"../types\";\n\nexport function freshStats(): Stats {\n return {\n scanned: 0,\n found: 0,\n sized: 0,\n deleted: 0,\n reclaimed: 0,\n startedAt: Date.now(),\n done: false,\n };\n}\n","import type { Entry, KattoOptions, Options, ScanProgress, SortMode, Stats } from \"./types\";\n\nimport { deleteAll as deleteEntries, deleteEntry as deleteOneEntry } from \"./core/delete\";\nimport { createOptions } from \"./core/options\";\nimport { sortedEntries, serializeEntry } from \"./core/results\";\nimport { scanEntries } from \"./core/scanner\";\nimport { freshStats } from \"./core/stats\";\n\nexport class Katto {\n readonly options: Options;\n entries: Entry[] = [];\n stats: Stats = freshStats();\n\n constructor(options: KattoOptions = {}) {\n this.options = createOptions(options);\n }\n\n async scan(): Promise<Entry[]> {\n this.entries = [];\n this.stats = freshStats();\n this.entries = await scanEntries(this.options);\n this.refreshScanStats();\n this.stats.scanned = this.entries.length;\n this.stats.done = true;\n return this.entries;\n }\n\n async *scanWithProgress(): AsyncGenerator<ScanProgress, Entry[]> {\n const seen = new Set<number>();\n const queue: ScanProgress[] = [];\n let notify: (() => void) | undefined;\n let done = false;\n let error: unknown;\n let result: Entry[] = [];\n\n this.entries = [];\n this.stats = freshStats();\n\n const wake = (): void => {\n const resolve = notify;\n notify = undefined;\n resolve?.();\n };\n\n const scanPromise = scanEntries(this.options, (entry) => {\n const phase = seen.has(entry.id) ? \"updated\" : \"found\";\n seen.add(entry.id);\n\n if (!this.entries.includes(entry)) this.entries.push(entry);\n this.refreshScanStats();\n queue.push({\n phase,\n entry,\n entries: this.entries,\n stats: this.stats,\n });\n wake();\n })\n .then((entries) => {\n result = entries;\n this.entries = entries;\n this.refreshScanStats();\n this.stats.scanned = entries.length;\n this.stats.done = true;\n })\n .catch((scanError: unknown) => {\n error = scanError;\n })\n .finally(() => {\n done = true;\n wake();\n });\n\n while (!done || queue.length > 0) {\n const progress = queue.shift();\n if (progress) {\n yield progress;\n continue;\n }\n\n await new Promise<void>((resolve) => {\n notify = resolve;\n });\n }\n\n await scanPromise;\n if (error) throw error;\n return result;\n }\n\n async deleteEntry(entry: Entry): Promise<boolean> {\n const size = entry.size ?? 0;\n const ok = await deleteOneEntry(entry, this.options);\n\n if (ok) {\n this.stats.deleted++;\n this.stats.reclaimed += size;\n }\n\n return ok;\n }\n\n async deleteAll(\n entries: Entry[] = this.entries,\n onProgress?: (progress: { entry?: Entry; stats: Stats }) => void,\n ): Promise<void> {\n await deleteEntries(entries, this.options, this.stats, (entry) => {\n onProgress?.({ entry, stats: this.stats });\n });\n }\n\n sort(entries: Entry[] = this.entries, sort: SortMode = this.options.sort): Entry[] {\n return sortedEntries(entries, sort);\n }\n\n serialize(entry: Entry): object {\n return serializeEntry(entry);\n }\n\n private refreshScanStats(): void {\n this.stats.found = this.entries.length;\n this.stats.sized = this.entries.filter((entry) => entry.size !== null).length;\n }\n}\n\nexport type {\n Entry,\n KattoOptions,\n Options,\n ScanProgress,\n SizeStrategy,\n SizeUnit,\n SortMode,\n Stats,\n} from \"./types\";\n"],"mappings":"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"}
|
package/dist/sdk.d.mts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type SortMode = "found" | "size" | "path" | "age";
|
|
3
|
+
type SizeUnit = "auto" | "mb" | "gb" | "bytes";
|
|
4
|
+
type SizeStrategy = "auto" | "native" | "js" | "none";
|
|
5
|
+
interface Options {
|
|
6
|
+
root: string;
|
|
7
|
+
targets: string[];
|
|
8
|
+
exclude: string[];
|
|
9
|
+
excludeSensitive: boolean;
|
|
10
|
+
dryRun: boolean;
|
|
11
|
+
noSize: boolean;
|
|
12
|
+
sizeStrategy: SizeStrategy;
|
|
13
|
+
sort: SortMode;
|
|
14
|
+
sizeUnit: SizeUnit;
|
|
15
|
+
}
|
|
16
|
+
interface KattoOptions {
|
|
17
|
+
root?: string;
|
|
18
|
+
targets?: string[];
|
|
19
|
+
exclude?: string[];
|
|
20
|
+
excludeSensitive?: boolean;
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
noSize?: boolean;
|
|
23
|
+
sizeStrategy?: SizeStrategy;
|
|
24
|
+
sort?: SortMode;
|
|
25
|
+
sizeUnit?: SizeUnit;
|
|
26
|
+
}
|
|
27
|
+
interface Entry {
|
|
28
|
+
id: number;
|
|
29
|
+
path: string;
|
|
30
|
+
name: string;
|
|
31
|
+
size: number | null;
|
|
32
|
+
mtime: number | null;
|
|
33
|
+
status: "found" | "sizing" | "ready" | "deleting" | "deleted" | "failed";
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
interface Stats {
|
|
37
|
+
scanned: number;
|
|
38
|
+
found: number;
|
|
39
|
+
sized: number;
|
|
40
|
+
deleted: number;
|
|
41
|
+
reclaimed: number;
|
|
42
|
+
startedAt: number;
|
|
43
|
+
done: boolean;
|
|
44
|
+
}
|
|
45
|
+
interface ScanProgress {
|
|
46
|
+
phase: "found" | "updated";
|
|
47
|
+
entry: Entry;
|
|
48
|
+
entries: Entry[];
|
|
49
|
+
stats: Stats;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/sdk.d.ts
|
|
53
|
+
declare class Katto {
|
|
54
|
+
readonly options: Options;
|
|
55
|
+
entries: Entry[];
|
|
56
|
+
stats: Stats;
|
|
57
|
+
constructor(options?: KattoOptions);
|
|
58
|
+
scan(): Promise<Entry[]>;
|
|
59
|
+
scanWithProgress(): AsyncGenerator<ScanProgress, Entry[]>;
|
|
60
|
+
deleteEntry(entry: Entry): Promise<boolean>;
|
|
61
|
+
deleteAll(entries?: Entry[], onProgress?: (progress: {
|
|
62
|
+
entry?: Entry;
|
|
63
|
+
stats: Stats;
|
|
64
|
+
}) => void): Promise<void>;
|
|
65
|
+
sort(entries?: Entry[], sort?: SortMode): Entry[];
|
|
66
|
+
serialize(entry: Entry): object;
|
|
67
|
+
private refreshScanStats;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { type Entry, Katto, type KattoOptions, type Options, type ScanProgress, type SizeStrategy, type SizeUnit, type SortMode, type Stats };
|
|
71
|
+
//# sourceMappingURL=sdk.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.d.mts","names":[],"sources":["../src/types.ts","../src/sdk.ts"],"mappings":";KAAY,QAAA;AAAA,KACA,QAAA;AAAA,KACA,YAAA;AAAA,UAEK,OAAA;EACf,IAAA;EACA,OAAA;EACA,OAAA;EACA,gBAAA;EACA,MAAA;EACA,MAAA;EACA,YAAA,EAAc,YAAA;EACd,IAAA,EAAM,QAAA;EACN,QAAA,EAAU,QAAA;AAAA;AAAA,UAUK,YAAA;EACf,IAAA;EACA,OAAA;EACA,OAAA;EACA,gBAAA;EACA,MAAA;EACA,MAAA;EACA,YAAA,GAAe,YAAA;EACf,IAAA,GAAO,QAAA;EACP,QAAA,GAAW,QAAA;AAAA;AAAA,UAGI,KAAA;EACf,EAAA;EACA,IAAA;EACA,IAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAA,UAGe,KAAA;EACf,OAAA;EACA,KAAA;EACA,KAAA;EACA,OAAA;EACA,SAAA;EACA,SAAA;EACA,IAAA;AAAA;AAAA,UAGe,YAAA;EACf,KAAA;EACA,KAAA,EAAO,KAAA;EACP,OAAA,EAAS,KAAA;EACT,KAAA,EAAO,KAAA;AAAA;;;cCnDI,KAAA;EAAA,SACF,OAAA,EAAS,OAAA;EAClB,OAAA,EAAS,KAAA;EACT,KAAA,EAAO,KAAA;cAEK,OAAA,GAAS,YAAA;EAIf,IAAA,CAAA,GAAQ,OAAA,CAAQ,KAAA;EAUf,gBAAA,CAAA,GAAoB,cAAA,CAAe,YAAA,EAAc,KAAA;EA+DlD,WAAA,CAAY,KAAA,EAAO,KAAA,GAAQ,OAAA;EAY3B,SAAA,CACJ,OAAA,GAAS,KAAA,IACT,UAAA,IAAc,QAAA;IAAY,KAAA,GAAQ,KAAA;IAAO,KAAA,EAAO,KAAA;EAAA,aAC/C,OAAA;EAMH,IAAA,CAAK,OAAA,GAAS,KAAA,IAAwB,IAAA,GAAM,QAAA,GAA+B,KAAA;EAI3E,SAAA,CAAU,KAAA,EAAO,KAAA;EAAA,QAIT,gBAAA;AAAA"}
|
package/dist/sdk.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rayhanadev/katto",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fast interactive cleanup for generated dependency and build folders.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"cleanup",
|
|
8
|
+
"cli",
|
|
9
|
+
"disk-space",
|
|
10
|
+
"node_modules",
|
|
11
|
+
"opentui",
|
|
12
|
+
"tui"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Rayhan Noufal Arayilakath <me@rayhanadev.com>",
|
|
16
|
+
"bin": {
|
|
17
|
+
"katto": "./dist/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/sdk.mjs",
|
|
27
|
+
"types": "./dist/sdk.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/sdk.d.ts",
|
|
31
|
+
"import": "./dist/sdk.mjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"dev": "bun src/index.ts",
|
|
39
|
+
"start": "bun src/index.ts",
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"prepublishOnly": "bun run build",
|
|
42
|
+
"lint": "oxlint --type-aware src",
|
|
43
|
+
"format": "oxfmt",
|
|
44
|
+
"typecheck": "tsgo --noEmit",
|
|
45
|
+
"version": "changeset version",
|
|
46
|
+
"release": "bun run build && changeset publish"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@opentui/core": "^0.2.15",
|
|
50
|
+
"sade": "^1.8.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@changesets/cli": "^2.31.0",
|
|
54
|
+
"@factory/eslint-plugin": "latest",
|
|
55
|
+
"@types/bun": "latest",
|
|
56
|
+
"@typescript/native-preview": "^7.0.0-dev.20260510.1",
|
|
57
|
+
"oxclippy": "^0.2.0",
|
|
58
|
+
"oxfmt": "latest",
|
|
59
|
+
"oxlint": "latest",
|
|
60
|
+
"oxlint-tsgolint": "latest",
|
|
61
|
+
"tsdown": "^0.22.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"bun": ">=1.3.0"
|
|
65
|
+
},
|
|
66
|
+
"packageManager": "bun@1.3.14"
|
|
67
|
+
}
|