@lexmanh/shed-cli 0.2.0-beta.3 → 0.2.0-beta.5
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/dist/cli.js +159 -13
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/clean.ts
|
|
@@ -395,6 +395,8 @@ async function doctorCommand() {
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
// src/commands/scan.ts
|
|
398
|
+
import { createRequire } from "module";
|
|
399
|
+
import { hostname } from "os";
|
|
398
400
|
import { resolve as resolve2 } from "path";
|
|
399
401
|
import * as p4 from "@clack/prompts";
|
|
400
402
|
import {
|
|
@@ -419,6 +421,80 @@ import {
|
|
|
419
421
|
XcodeDetector as XcodeDetector2
|
|
420
422
|
} from "@lexmanh/shed-core";
|
|
421
423
|
import pc4 from "picocolors";
|
|
424
|
+
|
|
425
|
+
// src/commands/scan-aggregate.ts
|
|
426
|
+
import { dirname } from "path";
|
|
427
|
+
var AGGREGATE_THRESHOLD = 3;
|
|
428
|
+
function aggregateForDisplay(items) {
|
|
429
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
430
|
+
const singles = [];
|
|
431
|
+
for (const item of items) {
|
|
432
|
+
const kind = item.metadata?.kind ?? null;
|
|
433
|
+
if (!kind) {
|
|
434
|
+
singles.push(item);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const key = `${dirname(item.path)}::${item.detector}::${kind}`;
|
|
438
|
+
const arr = buckets.get(key) ?? [];
|
|
439
|
+
arr.push(item);
|
|
440
|
+
buckets.set(key, arr);
|
|
441
|
+
}
|
|
442
|
+
const result = [];
|
|
443
|
+
for (const arr of buckets.values()) {
|
|
444
|
+
if (arr.length >= AGGREGATE_THRESHOLD) {
|
|
445
|
+
const first = arr[0];
|
|
446
|
+
if (!first) continue;
|
|
447
|
+
const totalBytes = arr.reduce((s, i) => s + i.sizeBytes, 0);
|
|
448
|
+
const kind = first.metadata?.kind;
|
|
449
|
+
result.push({
|
|
450
|
+
type: "aggregate",
|
|
451
|
+
risk: first.risk,
|
|
452
|
+
displayPath: dirname(first.path),
|
|
453
|
+
description: `${arr.length} ${kind} files`,
|
|
454
|
+
totalBytes,
|
|
455
|
+
detector: first.detector,
|
|
456
|
+
itemCount: arr.length,
|
|
457
|
+
items: arr
|
|
458
|
+
});
|
|
459
|
+
} else {
|
|
460
|
+
for (const item of arr) result.push(toSingle(item));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const item of singles) result.push(toSingle(item));
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
function toSingle(item) {
|
|
467
|
+
return {
|
|
468
|
+
type: "single",
|
|
469
|
+
risk: item.risk,
|
|
470
|
+
displayPath: item.path,
|
|
471
|
+
description: item.description,
|
|
472
|
+
totalBytes: item.sizeBytes,
|
|
473
|
+
detector: item.detector,
|
|
474
|
+
itemCount: 1,
|
|
475
|
+
items: [item]
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function selectTopGroups(groups, topN) {
|
|
479
|
+
const sorted = [...groups].sort((a, b) => b.totalBytes - a.totalBytes);
|
|
480
|
+
const shown = sorted.slice(0, topN);
|
|
481
|
+
const rest = sorted.slice(topN);
|
|
482
|
+
return {
|
|
483
|
+
shown,
|
|
484
|
+
hidden: {
|
|
485
|
+
groupCount: rest.length,
|
|
486
|
+
itemCount: rest.reduce((s, g) => s + g.itemCount, 0),
|
|
487
|
+
totalBytes: rest.reduce((s, g) => s + g.totalBytes, 0)
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/commands/scan.ts
|
|
493
|
+
var require2 = createRequire(import.meta.url);
|
|
494
|
+
var { version: SHED_VERSION } = require2("../package.json");
|
|
495
|
+
var JSON_SCHEMA_VERSION = 1;
|
|
496
|
+
var COMPACT_TOP_N = 15;
|
|
497
|
+
var DETECTOR_BREAKDOWN_TOP_N = 6;
|
|
422
498
|
var RISK_LABEL = {
|
|
423
499
|
[RiskTier2.Green]: pc4.green("\u25CF Green"),
|
|
424
500
|
[RiskTier2.Yellow]: pc4.yellow("\u25CF Yellow"),
|
|
@@ -444,6 +520,7 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
444
520
|
const spinner3 = options.json ? null : p4.spinner();
|
|
445
521
|
verbose(`scan root: ${rootDir}`);
|
|
446
522
|
spinner3?.start(`Scanning ${rootDir} \u2026`);
|
|
523
|
+
const scanStartedAt = Date.now();
|
|
447
524
|
const scanner = new Scanner2([
|
|
448
525
|
new NodeDetector2(),
|
|
449
526
|
new PythonDetector2(),
|
|
@@ -482,13 +559,41 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
482
559
|
`Found ${pc4.bold(String(allItems.length))} cleanable items across ${projects.length} project(s).`
|
|
483
560
|
);
|
|
484
561
|
if (options.json) {
|
|
562
|
+
const byRisk = { green: 0, yellow: 0, red: 0 };
|
|
563
|
+
let detectOnly = 0;
|
|
564
|
+
for (const item of allItems) {
|
|
565
|
+
byRisk[item.risk]++;
|
|
566
|
+
if (item.metadata?.detectOnly === true) detectOnly++;
|
|
567
|
+
}
|
|
568
|
+
const projectsOut = projects.map((proj) => ({
|
|
569
|
+
root: proj.root,
|
|
570
|
+
detectors: [...new Set(proj.items.map((i) => i.detector))],
|
|
571
|
+
itemCount: proj.items.length,
|
|
572
|
+
totalBytes: proj.items.reduce((s, i) => s + i.sizeBytes, 0)
|
|
573
|
+
}));
|
|
485
574
|
console.log(
|
|
486
575
|
JSON.stringify(
|
|
487
576
|
{
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
577
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
578
|
+
shedVersion: SHED_VERSION,
|
|
579
|
+
timestamp: new Date(scanStartedAt).toISOString(),
|
|
580
|
+
host: {
|
|
581
|
+
hostname: hostname(),
|
|
582
|
+
platform: process.platform,
|
|
583
|
+
arch: process.arch
|
|
584
|
+
},
|
|
585
|
+
scan: {
|
|
586
|
+
root: rootDir,
|
|
587
|
+
durationMs: Date.now() - scanStartedAt
|
|
588
|
+
},
|
|
589
|
+
summary: {
|
|
590
|
+
totalBytes,
|
|
591
|
+
totalItems: allItems.length,
|
|
592
|
+
byRisk,
|
|
593
|
+
detectOnly
|
|
594
|
+
},
|
|
595
|
+
projects: projectsOut,
|
|
596
|
+
items: allItems
|
|
492
597
|
},
|
|
493
598
|
null,
|
|
494
599
|
2
|
|
@@ -501,6 +606,52 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
501
606
|
p4.outro(pc4.dim("All clear!"));
|
|
502
607
|
return;
|
|
503
608
|
}
|
|
609
|
+
if (options.all) {
|
|
610
|
+
renderFull(allItems);
|
|
611
|
+
} else {
|
|
612
|
+
renderCompact(allItems);
|
|
613
|
+
}
|
|
614
|
+
console.log();
|
|
615
|
+
p4.outro(
|
|
616
|
+
`Total recoverable: ${pc4.bold(pc4.green(formatBytes2(totalBytes)))} \u2014 run ${pc4.cyan("shed clean")} to proceed.`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
function renderCompact(allItems) {
|
|
620
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
621
|
+
const byRisk = { green: 0, yellow: 0, red: 0 };
|
|
622
|
+
let detectOnly = 0;
|
|
623
|
+
const byDetector = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const item of allItems) {
|
|
625
|
+
byRisk[item.risk]++;
|
|
626
|
+
if (item.metadata?.detectOnly === true) detectOnly++;
|
|
627
|
+
byDetector.set(item.detector, (byDetector.get(item.detector) ?? 0) + item.sizeBytes);
|
|
628
|
+
}
|
|
629
|
+
const riskLine = `${pc4.green(`\u25CF ${byRisk.green} Green`)} ${pc4.yellow(`\u25CF ${byRisk.yellow} Yellow`)} ${pc4.red(`\u25CF ${byRisk.red} Red`)}${detectOnly > 0 ? pc4.dim(` (${detectOnly} detect-only)`) : ""}`;
|
|
630
|
+
const detectorLine = [...byDetector.entries()].sort(([, a], [, b]) => b - a).slice(0, DETECTOR_BREAKDOWN_TOP_N).map(([d, b]) => `${d} ${formatBytes2(b)}`).join(" \xB7 ");
|
|
631
|
+
console.log();
|
|
632
|
+
console.log(` By risk: ${riskLine}`);
|
|
633
|
+
console.log(` By detector: ${pc4.dim(detectorLine)}`);
|
|
634
|
+
const groups = aggregateForDisplay(allItems);
|
|
635
|
+
const { shown, hidden } = selectTopGroups(groups, COMPACT_TOP_N);
|
|
636
|
+
console.log();
|
|
637
|
+
console.log(` ${pc4.bold(`Top ${shown.length} items:`)}`);
|
|
638
|
+
for (const g of shown) {
|
|
639
|
+
const path = home ? g.displayPath.replace(home, "~") : g.displayPath;
|
|
640
|
+
const tag = g.type === "aggregate" ? pc4.dim(` (${g.itemCount} ${g.detector} items)`) : "";
|
|
641
|
+
const size = g.totalBytes > 0 ? pc4.dim(` ${formatBytes2(g.totalBytes)}`) : "";
|
|
642
|
+
console.log(` ${RISK_LABEL[g.risk]} ${path}${tag}${size}`);
|
|
643
|
+
}
|
|
644
|
+
if (hidden.groupCount > 0) {
|
|
645
|
+
console.log();
|
|
646
|
+
console.log(
|
|
647
|
+
pc4.dim(
|
|
648
|
+
` \u2026 ${hidden.groupCount} more groups (${hidden.itemCount} items, ${formatBytes2(hidden.totalBytes)}) \u2014 use --all to see everything`
|
|
649
|
+
)
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function renderFull(allItems) {
|
|
654
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
504
655
|
const byProject = /* @__PURE__ */ new Map();
|
|
505
656
|
for (const item of allItems) {
|
|
506
657
|
const key = item.projectRoot ?? "(global)";
|
|
@@ -509,7 +660,6 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
509
660
|
byProject.set(key, group);
|
|
510
661
|
}
|
|
511
662
|
for (const [projectRoot, items] of byProject.entries()) {
|
|
512
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
513
663
|
const projectLabel = projectRoot === "(global)" ? pc4.dim("global caches") : pc4.cyan(home ? projectRoot.replace(home, "~") : projectRoot);
|
|
514
664
|
const groupTotal = items.reduce((s, i) => s + i.sizeBytes, 0);
|
|
515
665
|
console.log(`
|
|
@@ -522,10 +672,6 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
522
672
|
console.log(` ${pc4.dim(` ${item.description}`)}`);
|
|
523
673
|
}
|
|
524
674
|
}
|
|
525
|
-
console.log();
|
|
526
|
-
p4.outro(
|
|
527
|
-
`Total recoverable: ${pc4.bold(pc4.green(formatBytes2(totalBytes)))} \u2014 run ${pc4.cyan("shed clean")} to proceed.`
|
|
528
|
-
);
|
|
529
675
|
}
|
|
530
676
|
|
|
531
677
|
// src/commands/undo.ts
|
|
@@ -589,11 +735,11 @@ function printLogo(version2) {
|
|
|
589
735
|
}
|
|
590
736
|
|
|
591
737
|
// src/cli.ts
|
|
592
|
-
var
|
|
593
|
-
var { version } =
|
|
738
|
+
var require3 = createRequire2(import.meta.url);
|
|
739
|
+
var { version } = require3("../package.json");
|
|
594
740
|
var program = new Command();
|
|
595
741
|
program.name("shed").description("Safe disk cleanup for dev machines and Linux servers").version(version).option("-v, --verbose", "Enable verbose logging");
|
|
596
|
-
program.command("scan [path]").description("Scan for cleanable items without modifying anything").option("--json", "Output machine-readable JSON").option("--max-age <days>", "Only include items older than N days", "30").action(scanCommand);
|
|
742
|
+
program.command("scan [path]").description("Scan for cleanable items without modifying anything").option("--json", "Output machine-readable JSON").option("--max-age <days>", "Only include items older than N days", "30").option("--all", "Show every item (default: compact summary with top 15)").action(scanCommand);
|
|
597
743
|
program.command("clean [path]").description("Interactive cleanup of detected items").option("--dry-run", "Preview operations without executing", true).option("--execute", "Actually perform the cleanup (overrides --dry-run)").option("--hard-delete", "Skip Trash, delete permanently").option("--include-red", "Include Red-tier (high-risk) items").option("--yes", "Skip interactive confirmations (CI mode)").action(cleanCommand);
|
|
598
744
|
program.command("undo").description("List and restore items from previous cleanups").action(undoCommand);
|
|
599
745
|
program.command("doctor").description("Check environment and configuration").action(doctorCommand);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexmanh/shed-cli",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.5",
|
|
4
4
|
"description": "Safe disk cleanup CLI for dev machines and Linux servers — git-aware, cross-stack, trash-by-default",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"conf": "^13.1.0",
|
|
24
24
|
"execa": "^9.5.0",
|
|
25
25
|
"picocolors": "^1.1.1",
|
|
26
|
-
"@lexmanh/shed-core": "0.2.0-beta.
|
|
26
|
+
"@lexmanh/shed-core": "0.2.0-beta.5"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.10.0",
|