@lexmanh/shed-cli 0.3.0-beta.2 → 0.4.0-beta.1
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 +270 -118
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Command } from "commander";
|
|
|
7
7
|
// src/commands/clean.ts
|
|
8
8
|
import * as p2 from "@clack/prompts";
|
|
9
9
|
import {
|
|
10
|
+
OpLog,
|
|
10
11
|
RiskTier,
|
|
11
12
|
SafetyChecker,
|
|
12
13
|
Scanner,
|
|
@@ -111,7 +112,7 @@ async function cleanCommand(path, options = {}) {
|
|
|
111
112
|
p2.outro(pc2.dim("Nothing to clean."));
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
|
-
const checker = new SafetyChecker();
|
|
115
|
+
const checker = new SafetyChecker({ oplog: new OpLog() });
|
|
115
116
|
const checkResults = await Promise.all(allItems.map((item) => checker.check(item)));
|
|
116
117
|
const eligibleItems = allItems.filter((_, i) => {
|
|
117
118
|
const result2 = checkResults[i];
|
|
@@ -250,9 +251,14 @@ async function cleanCommand(path, options = {}) {
|
|
|
250
251
|
console.log(` ${pc2.dim(f.item.path)}: ${f.error}`);
|
|
251
252
|
}
|
|
252
253
|
}
|
|
254
|
+
if (!isDryRun && !options.hardDelete && result.succeeded.length > 0) {
|
|
255
|
+
console.log();
|
|
256
|
+
console.log(` ${pc2.dim("To restore: ")}${pc2.cyan(`shed undo --session ${result.sessionId}`)}`);
|
|
257
|
+
console.log(` ${pc2.dim("History: ")}${pc2.cyan("shed history")}`);
|
|
258
|
+
}
|
|
253
259
|
console.log();
|
|
254
|
-
const
|
|
255
|
-
p2.outro(
|
|
260
|
+
const outro8 = isDryRun ? `Dry-run complete. Run with ${pc2.cyan("--execute")} to perform actual cleanup.` : result.failed.length > 0 ? `Completed with ${result.failed.length} failure(s).` : "All done!";
|
|
261
|
+
p2.outro(outro8);
|
|
256
262
|
}
|
|
257
263
|
|
|
258
264
|
// src/commands/completions.ts
|
|
@@ -535,16 +541,89 @@ async function doctorCommand() {
|
|
|
535
541
|
p4.outro(pc4.green("Environment check complete."));
|
|
536
542
|
}
|
|
537
543
|
|
|
544
|
+
// src/commands/history.ts
|
|
545
|
+
import * as p5 from "@clack/prompts";
|
|
546
|
+
import { OpLog as OpLog2 } from "@lexmanh/shed-core";
|
|
547
|
+
import pc5 from "picocolors";
|
|
548
|
+
function formatBytes2(bytes) {
|
|
549
|
+
if (bytes === 0) return "0 B";
|
|
550
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
551
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
552
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
553
|
+
}
|
|
554
|
+
function formatRelative(ts) {
|
|
555
|
+
const ms = Date.now() - new Date(ts).getTime();
|
|
556
|
+
if (Number.isNaN(ms)) return ts;
|
|
557
|
+
const sec = Math.floor(ms / 1e3);
|
|
558
|
+
if (sec < 60) return `${sec}s ago`;
|
|
559
|
+
const min = Math.floor(sec / 60);
|
|
560
|
+
if (min < 60) return `${min}m ago`;
|
|
561
|
+
const hr = Math.floor(min / 60);
|
|
562
|
+
if (hr < 24) return `${hr}h ago`;
|
|
563
|
+
const day = Math.floor(hr / 24);
|
|
564
|
+
return `${day}d ago`;
|
|
565
|
+
}
|
|
566
|
+
var ACTION_BADGE = {
|
|
567
|
+
trash: pc5.cyan("TRASH "),
|
|
568
|
+
delete: pc5.red("DELETE"),
|
|
569
|
+
revert: pc5.green("REVERT")
|
|
570
|
+
};
|
|
571
|
+
var RISK_DOT = {
|
|
572
|
+
green: pc5.green("\u25CF"),
|
|
573
|
+
yellow: pc5.yellow("\u25CF"),
|
|
574
|
+
red: pc5.red("\u25CF")
|
|
575
|
+
};
|
|
576
|
+
async function historyCommand(options = {}) {
|
|
577
|
+
const limit = options.limit ? Math.max(1, Number.parseInt(options.limit, 10) || 20) : 20;
|
|
578
|
+
const oplog = new OpLog2();
|
|
579
|
+
const entries = await oplog.read({ limit, session: options.session });
|
|
580
|
+
if (options.json) {
|
|
581
|
+
console.log(JSON.stringify({ entries }, null, 2));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
p5.intro(pc5.bgMagenta(pc5.black(" shed history ")));
|
|
585
|
+
if (entries.length === 0) {
|
|
586
|
+
p5.note(
|
|
587
|
+
[
|
|
588
|
+
"No cleanup operations have been recorded yet.",
|
|
589
|
+
"",
|
|
590
|
+
`Run ${pc5.cyan("shed clean --execute")} to perform a real cleanup.`,
|
|
591
|
+
"",
|
|
592
|
+
pc5.dim("Operations are logged to ~/.shed/ops.jsonl when shed actually deletes files.")
|
|
593
|
+
].join("\n"),
|
|
594
|
+
"Empty oplog"
|
|
595
|
+
);
|
|
596
|
+
p5.outro(pc5.dim("Nothing to show."));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
600
|
+
const lines = entries.map((e) => {
|
|
601
|
+
const path = home ? e.path.replace(home, "~") : e.path;
|
|
602
|
+
const reverted = e.reverted ? pc5.dim(" [reverted]") : "";
|
|
603
|
+
const tokenShort = e.token.slice(-8);
|
|
604
|
+
const when = formatRelative(e.ts);
|
|
605
|
+
return ` ${ACTION_BADGE[e.action]} ${RISK_DOT[e.risk]} ${pc5.dim(when.padEnd(7))} ${pc5.bold(tokenShort)} ${path} ${pc5.dim(formatBytes2(e.sizeBytes))}${reverted}`;
|
|
606
|
+
});
|
|
607
|
+
console.log();
|
|
608
|
+
console.log(` ${pc5.bold(`Last ${entries.length} operation(s):`)}`);
|
|
609
|
+
console.log();
|
|
610
|
+
for (const line of lines) console.log(line);
|
|
611
|
+
console.log();
|
|
612
|
+
p5.outro(
|
|
613
|
+
`Restore: ${pc5.cyan("shed undo <token>")} \xB7 All in session: ${pc5.cyan("shed undo --session <id>")}`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
538
617
|
// src/commands/scan.ts
|
|
539
618
|
import { createRequire } from "module";
|
|
540
619
|
import { hostname } from "os";
|
|
541
|
-
import * as
|
|
620
|
+
import * as p6 from "@clack/prompts";
|
|
542
621
|
import {
|
|
543
622
|
RiskTier as RiskTier2,
|
|
544
623
|
Scanner as Scanner2,
|
|
545
624
|
defaultDetectors as defaultDetectors2
|
|
546
625
|
} from "@lexmanh/shed-core";
|
|
547
|
-
import
|
|
626
|
+
import pc6 from "picocolors";
|
|
548
627
|
|
|
549
628
|
// src/commands/scan-aggregate.ts
|
|
550
629
|
import { dirname } from "path";
|
|
@@ -622,16 +701,16 @@ var JSON_SCHEMA_VERSION = 2;
|
|
|
622
701
|
var COMPACT_TOP_N = 15;
|
|
623
702
|
var DETECTOR_BREAKDOWN_TOP_N = 6;
|
|
624
703
|
var RISK_LABEL = {
|
|
625
|
-
[RiskTier2.Green]:
|
|
626
|
-
[RiskTier2.Yellow]:
|
|
627
|
-
[RiskTier2.Red]:
|
|
704
|
+
[RiskTier2.Green]: pc6.green("\u25CF Green"),
|
|
705
|
+
[RiskTier2.Yellow]: pc6.yellow("\u25CF Yellow"),
|
|
706
|
+
[RiskTier2.Red]: pc6.red("\u25CF Red")
|
|
628
707
|
};
|
|
629
708
|
var RISK_ORDER = {
|
|
630
709
|
[RiskTier2.Red]: 0,
|
|
631
710
|
[RiskTier2.Yellow]: 1,
|
|
632
711
|
[RiskTier2.Green]: 2
|
|
633
712
|
};
|
|
634
|
-
function
|
|
713
|
+
function formatBytes3(bytes) {
|
|
635
714
|
if (bytes === 0) return "0 B";
|
|
636
715
|
if (bytes < 1024) return `${bytes} B`;
|
|
637
716
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -640,7 +719,7 @@ function formatBytes2(bytes) {
|
|
|
640
719
|
}
|
|
641
720
|
async function scanCommand(path, options = {}) {
|
|
642
721
|
if (!options.json) {
|
|
643
|
-
|
|
722
|
+
p6.intro(pc6.bgCyan(pc6.black(" shed scan ")));
|
|
644
723
|
}
|
|
645
724
|
const scope = await resolveScanScope({ path, nonInteractive: options.json });
|
|
646
725
|
if (!scope.ok) {
|
|
@@ -649,11 +728,11 @@ async function scanCommand(path, options = {}) {
|
|
|
649
728
|
JSON.stringify({ schemaVersion: JSON_SCHEMA_VERSION, error: scope.message }, null, 2)
|
|
650
729
|
);
|
|
651
730
|
} else {
|
|
652
|
-
|
|
731
|
+
p6.cancel(scope.message);
|
|
653
732
|
}
|
|
654
733
|
return;
|
|
655
734
|
}
|
|
656
|
-
const spinner4 = options.json ? null :
|
|
735
|
+
const spinner4 = options.json ? null : p6.spinner();
|
|
657
736
|
verbose(`scan roots: ${scope.roots.join(", ")}`);
|
|
658
737
|
spinner4?.start(
|
|
659
738
|
scope.roots.length === 1 ? `Scanning ${scope.roots[0]} \u2026` : `Scanning ${scope.roots.length} roots \u2026`
|
|
@@ -675,7 +754,7 @@ async function scanCommand(path, options = {}) {
|
|
|
675
754
|
for (const item of allItems)
|
|
676
755
|
verbose(` item: ${item.risk} ${item.path} (${item.sizeBytes} bytes)`);
|
|
677
756
|
spinner4?.stop(
|
|
678
|
-
`Found ${
|
|
757
|
+
`Found ${pc6.bold(String(allItems.length))} cleanable items across ${projects.length} project(s).`
|
|
679
758
|
);
|
|
680
759
|
if (options.json) {
|
|
681
760
|
const byRisk = { green: 0, yellow: 0, red: 0 };
|
|
@@ -721,8 +800,8 @@ async function scanCommand(path, options = {}) {
|
|
|
721
800
|
return;
|
|
722
801
|
}
|
|
723
802
|
if (allItems.length === 0) {
|
|
724
|
-
|
|
725
|
-
|
|
803
|
+
p6.note("Nothing found to clean in this directory.", "Result");
|
|
804
|
+
p6.outro(pc6.dim("All clear!"));
|
|
726
805
|
return;
|
|
727
806
|
}
|
|
728
807
|
if (options.all) {
|
|
@@ -731,8 +810,8 @@ async function scanCommand(path, options = {}) {
|
|
|
731
810
|
renderCompact(allItems);
|
|
732
811
|
}
|
|
733
812
|
console.log();
|
|
734
|
-
|
|
735
|
-
`Total recoverable: ${
|
|
813
|
+
p6.outro(
|
|
814
|
+
`Total recoverable: ${pc6.bold(pc6.green(formatBytes3(totalBytes)))} \u2014 run ${pc6.cyan("shed clean")} to proceed.`
|
|
736
815
|
);
|
|
737
816
|
}
|
|
738
817
|
function renderCompact(allItems) {
|
|
@@ -745,26 +824,26 @@ function renderCompact(allItems) {
|
|
|
745
824
|
if (item.metadata?.detectOnly === true) detectOnly++;
|
|
746
825
|
byDetector.set(item.detector, (byDetector.get(item.detector) ?? 0) + item.sizeBytes);
|
|
747
826
|
}
|
|
748
|
-
const riskLine = `${
|
|
749
|
-
const detectorLine = [...byDetector.entries()].sort(([, a], [, b]) => b - a).slice(0, DETECTOR_BREAKDOWN_TOP_N).map(([d, b]) => `${d} ${
|
|
827
|
+
const riskLine = `${pc6.green(`\u25CF ${byRisk.green} Green`)} ${pc6.yellow(`\u25CF ${byRisk.yellow} Yellow`)} ${pc6.red(`\u25CF ${byRisk.red} Red`)}${detectOnly > 0 ? pc6.dim(` (${detectOnly} detect-only)`) : ""}`;
|
|
828
|
+
const detectorLine = [...byDetector.entries()].sort(([, a], [, b]) => b - a).slice(0, DETECTOR_BREAKDOWN_TOP_N).map(([d, b]) => `${d} ${formatBytes3(b)}`).join(" \xB7 ");
|
|
750
829
|
console.log();
|
|
751
830
|
console.log(` By risk: ${riskLine}`);
|
|
752
|
-
console.log(` By detector: ${
|
|
831
|
+
console.log(` By detector: ${pc6.dim(detectorLine)}`);
|
|
753
832
|
const groups = aggregateForDisplay(allItems);
|
|
754
833
|
const { shown, hidden } = selectTopGroups(groups, COMPACT_TOP_N);
|
|
755
834
|
console.log();
|
|
756
|
-
console.log(` ${
|
|
835
|
+
console.log(` ${pc6.bold(`Top ${shown.length} items:`)}`);
|
|
757
836
|
for (const g of shown) {
|
|
758
837
|
const path = home ? g.displayPath.replace(home, "~") : g.displayPath;
|
|
759
|
-
const tag = g.type === "aggregate" ?
|
|
760
|
-
const size = g.totalBytes > 0 ?
|
|
838
|
+
const tag = g.type === "aggregate" ? pc6.dim(` (${g.itemCount} ${g.detector} items)`) : "";
|
|
839
|
+
const size = g.totalBytes > 0 ? pc6.dim(` ${formatBytes3(g.totalBytes)}`) : "";
|
|
761
840
|
console.log(` ${RISK_LABEL[g.risk]} ${path}${tag}${size}`);
|
|
762
841
|
}
|
|
763
842
|
if (hidden.groupCount > 0) {
|
|
764
843
|
console.log();
|
|
765
844
|
console.log(
|
|
766
|
-
|
|
767
|
-
` \u2026 ${hidden.groupCount} more groups (${hidden.itemCount} items, ${
|
|
845
|
+
pc6.dim(
|
|
846
|
+
` \u2026 ${hidden.groupCount} more groups (${hidden.itemCount} items, ${formatBytes3(hidden.totalBytes)}) \u2014 use --all to see everything`
|
|
768
847
|
)
|
|
769
848
|
);
|
|
770
849
|
}
|
|
@@ -779,66 +858,138 @@ function renderFull(allItems) {
|
|
|
779
858
|
byProject.set(key, group);
|
|
780
859
|
}
|
|
781
860
|
for (const [projectRoot, items] of byProject.entries()) {
|
|
782
|
-
const projectLabel = projectRoot === "(global)" ?
|
|
861
|
+
const projectLabel = projectRoot === "(global)" ? pc6.dim("global caches") : pc6.cyan(home ? projectRoot.replace(home, "~") : projectRoot);
|
|
783
862
|
const groupTotal = items.reduce((s, i) => s + i.sizeBytes, 0);
|
|
784
863
|
console.log(`
|
|
785
|
-
${projectLabel} ${
|
|
864
|
+
${projectLabel} ${pc6.dim(formatBytes3(groupTotal))}`);
|
|
786
865
|
for (const item of items) {
|
|
787
|
-
const size = item.sizeBytes > 0 ?
|
|
866
|
+
const size = item.sizeBytes > 0 ? pc6.dim(` ${formatBytes3(item.sizeBytes)}`) : "";
|
|
788
867
|
const displayPath = home ? item.path.replace(home, "~") : item.path;
|
|
789
868
|
const shortPath = projectRoot !== "(global)" ? displayPath.replace(home ? projectRoot.replace(home, "~") : projectRoot, "").replace(/^\//, "") || displayPath : displayPath;
|
|
790
869
|
console.log(` ${RISK_LABEL[item.risk]} ${shortPath}${size}`);
|
|
791
|
-
console.log(` ${
|
|
870
|
+
console.log(` ${pc6.dim(` ${item.description}`)}`);
|
|
792
871
|
}
|
|
793
872
|
}
|
|
794
873
|
}
|
|
795
874
|
|
|
796
875
|
// src/commands/undo.ts
|
|
797
|
-
import
|
|
798
|
-
import
|
|
799
|
-
import
|
|
800
|
-
function
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return "Recycle Bin";
|
|
806
|
-
default:
|
|
807
|
-
return "~/.local/share/Trash";
|
|
808
|
-
}
|
|
876
|
+
import * as p7 from "@clack/prompts";
|
|
877
|
+
import { OpLog as OpLog3, restoreFromTrash } from "@lexmanh/shed-core";
|
|
878
|
+
import pc7 from "picocolors";
|
|
879
|
+
function formatBytes4(bytes) {
|
|
880
|
+
if (bytes === 0) return "0 B";
|
|
881
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
882
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
883
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
809
884
|
}
|
|
810
|
-
function
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
885
|
+
async function undoCommand(token, options = {}) {
|
|
886
|
+
p7.intro(pc7.bgMagenta(pc7.black(" shed undo ")));
|
|
887
|
+
const oplog = new OpLog3();
|
|
888
|
+
let targets;
|
|
889
|
+
if (token) {
|
|
890
|
+
const found = await oplog.findByToken(token);
|
|
891
|
+
if (!found) {
|
|
892
|
+
p7.cancel(`No operation found with token: ${token}`);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
targets = [found];
|
|
896
|
+
} else if (options.last) {
|
|
897
|
+
const recent = await oplog.read({ limit: 1 });
|
|
898
|
+
if (recent.length === 0) {
|
|
899
|
+
p7.cancel("No operations recorded yet.");
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
targets = [...recent];
|
|
903
|
+
} else if (options.session) {
|
|
904
|
+
targets = [...await oplog.read({ session: options.session })];
|
|
905
|
+
if (targets.length === 0) {
|
|
906
|
+
p7.cancel(`No operations found for session: ${options.session}`);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
} else {
|
|
910
|
+
targets = await pickInteractively(oplog);
|
|
911
|
+
if (targets.length === 0) return;
|
|
912
|
+
}
|
|
913
|
+
const restorable = targets.filter((t) => t.action === "trash" && !t.reverted);
|
|
914
|
+
const skipped = targets.length - restorable.length;
|
|
915
|
+
if (restorable.length === 0) {
|
|
916
|
+
if (skipped > 0) {
|
|
917
|
+
p7.cancel(
|
|
918
|
+
`Nothing to restore. ${skipped} op(s) were either hard-deleted or already reverted.`
|
|
919
|
+
);
|
|
920
|
+
} else {
|
|
921
|
+
p7.cancel("Nothing to restore.");
|
|
922
|
+
}
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
926
|
+
let restored = 0;
|
|
927
|
+
let failed = 0;
|
|
928
|
+
for (const op of restorable) {
|
|
929
|
+
const display = home ? op.path.replace(home, "~") : op.path;
|
|
930
|
+
const result = await restoreFromTrash(op.path);
|
|
931
|
+
if (result.ok) {
|
|
932
|
+
await oplog.markReverted(op.token);
|
|
933
|
+
console.log(
|
|
934
|
+
` ${pc7.green("\u2713")} restored ${pc7.bold(display)} ${pc7.dim(formatBytes4(op.sizeBytes))}`
|
|
935
|
+
);
|
|
936
|
+
restored++;
|
|
937
|
+
} else {
|
|
938
|
+
console.log(` ${pc7.red("\u2717")} ${display}: ${pc7.dim(result.message)}`);
|
|
939
|
+
failed++;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
console.log();
|
|
943
|
+
if (failed === 0) {
|
|
944
|
+
p7.outro(pc7.green(`Restored ${restored} item(s).`));
|
|
945
|
+
} else if (restored === 0) {
|
|
946
|
+
p7.outro(pc7.red(`Failed to restore any of ${failed} item(s).`));
|
|
947
|
+
} else {
|
|
948
|
+
p7.outro(`Restored ${restored} of ${restored + failed} item(s).`);
|
|
818
949
|
}
|
|
819
950
|
}
|
|
820
|
-
async function
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
951
|
+
async function pickInteractively(oplog) {
|
|
952
|
+
const recent = await oplog.read({ limit: 20 });
|
|
953
|
+
const restorable = recent.filter((e) => e.action === "trash" && !e.reverted);
|
|
954
|
+
if (restorable.length === 0) {
|
|
955
|
+
p7.note(
|
|
956
|
+
[
|
|
957
|
+
"No restorable operations in your oplog.",
|
|
958
|
+
"",
|
|
959
|
+
`Run ${pc7.cyan("shed history")} to see all recorded operations,`,
|
|
960
|
+
`or ${pc7.cyan("shed clean --execute")} to perform a real cleanup.`,
|
|
961
|
+
"",
|
|
962
|
+
pc7.dim("Note: hard-deletes (--hard-delete) cannot be undone.")
|
|
963
|
+
].join("\n"),
|
|
964
|
+
"Nothing to undo"
|
|
965
|
+
);
|
|
966
|
+
p7.outro(pc7.dim("Empty."));
|
|
967
|
+
return [];
|
|
968
|
+
}
|
|
969
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
970
|
+
const choice = await p7.select({
|
|
971
|
+
message: "Pick an operation to restore:",
|
|
972
|
+
options: restorable.map((e) => {
|
|
973
|
+
const display = home ? e.path.replace(home, "~") : e.path;
|
|
974
|
+
const tokenShort = e.token.slice(-8);
|
|
975
|
+
return {
|
|
976
|
+
value: e.token,
|
|
977
|
+
label: `${pc7.bold(tokenShort)} ${display} ${pc7.dim(formatBytes4(e.sizeBytes))}`
|
|
978
|
+
};
|
|
979
|
+
})
|
|
980
|
+
});
|
|
981
|
+
if (p7.isCancel(choice) || typeof choice !== "string") {
|
|
982
|
+
p7.cancel("Cancelled.");
|
|
983
|
+
return [];
|
|
984
|
+
}
|
|
985
|
+
const picked = restorable.find((e) => e.token === choice);
|
|
986
|
+
return picked ? [picked] : [];
|
|
836
987
|
}
|
|
837
988
|
|
|
838
989
|
// src/commands/upgrade.ts
|
|
839
|
-
import * as
|
|
990
|
+
import * as p8 from "@clack/prompts";
|
|
840
991
|
import { execa as execa2 } from "execa";
|
|
841
|
-
import
|
|
992
|
+
import pc8 from "picocolors";
|
|
842
993
|
|
|
843
994
|
// src/update/detect-install.ts
|
|
844
995
|
import { realpathSync } from "fs";
|
|
@@ -846,32 +997,32 @@ import { constants, access } from "fs/promises";
|
|
|
846
997
|
import { dirname as dirname2 } from "path";
|
|
847
998
|
var PACKAGE_NAME = "@lexmanh/shed-cli";
|
|
848
999
|
function classifyInstall(resolvedPath) {
|
|
849
|
-
const
|
|
850
|
-
if (
|
|
1000
|
+
const p9 = resolvedPath.replace(/\\/g, "/").toLowerCase();
|
|
1001
|
+
if (p9.includes("/_npx/") || p9.includes("/npx-cache/")) {
|
|
851
1002
|
return {
|
|
852
1003
|
kind: "npx",
|
|
853
1004
|
note: "Running via npx (ephemeral). Re-run with `npx @lexmanh/shed-cli@latest`."
|
|
854
1005
|
};
|
|
855
1006
|
}
|
|
856
|
-
if (
|
|
1007
|
+
if (p9.includes("/bun/install/cache/") || p9.includes("/.bun/install/cache/")) {
|
|
857
1008
|
return {
|
|
858
1009
|
kind: "bunx",
|
|
859
1010
|
note: "Running via bunx (ephemeral). Re-run with `bunx @lexmanh/shed-cli@latest`."
|
|
860
1011
|
};
|
|
861
1012
|
}
|
|
862
|
-
if (
|
|
1013
|
+
if (p9.includes("/.volta/") || p9.includes("/volta/tools/")) {
|
|
863
1014
|
return { kind: "volta" };
|
|
864
1015
|
}
|
|
865
|
-
if (
|
|
1016
|
+
if (p9.includes("/pnpm/global/") || p9.includes("/library/pnpm/") || p9.includes("/.local/share/pnpm/")) {
|
|
866
1017
|
return { kind: "pnpm-global" };
|
|
867
1018
|
}
|
|
868
|
-
if (
|
|
1019
|
+
if (p9.includes("/yarn/global/") || p9.includes("/.config/yarn/global/")) {
|
|
869
1020
|
return { kind: "yarn-global" };
|
|
870
1021
|
}
|
|
871
|
-
if (
|
|
1022
|
+
if (p9.includes("/.bun/install/global/")) {
|
|
872
1023
|
return { kind: "bun-global" };
|
|
873
1024
|
}
|
|
874
|
-
if (
|
|
1025
|
+
if (p9.includes("/node_modules/")) {
|
|
875
1026
|
return { kind: "npm-global" };
|
|
876
1027
|
}
|
|
877
1028
|
return { kind: "unknown" };
|
|
@@ -901,12 +1052,12 @@ function detectInstall(binPath) {
|
|
|
901
1052
|
} catch {
|
|
902
1053
|
resolvedPath = binPath;
|
|
903
1054
|
}
|
|
904
|
-
const { kind, note:
|
|
1055
|
+
const { kind, note: note9 } = classifyInstall(resolvedPath);
|
|
905
1056
|
return {
|
|
906
1057
|
kind,
|
|
907
1058
|
upgradeCommand: buildUpgradeCommand(kind),
|
|
908
1059
|
resolvedPath,
|
|
909
|
-
note:
|
|
1060
|
+
note: note9
|
|
910
1061
|
};
|
|
911
1062
|
}
|
|
912
1063
|
async function needsElevation(resolvedPath) {
|
|
@@ -997,93 +1148,93 @@ function isNewer(latest, current) {
|
|
|
997
1148
|
|
|
998
1149
|
// src/commands/upgrade.ts
|
|
999
1150
|
async function upgradeCommand(opts, currentVersion) {
|
|
1000
|
-
|
|
1151
|
+
p8.intro(pc8.bgMagenta(pc8.black(" shed upgrade ")));
|
|
1001
1152
|
const install = detectInstall(process.argv[1] ?? "");
|
|
1002
|
-
const spin =
|
|
1153
|
+
const spin = p8.spinner();
|
|
1003
1154
|
spin.start("Checking npm registry\u2026");
|
|
1004
1155
|
const latest = await fetchLatestVersion({ force: true });
|
|
1005
1156
|
spin.stop(latest ? `Latest: v${latest}` : "Could not reach registry");
|
|
1006
1157
|
if (!latest) {
|
|
1007
|
-
|
|
1158
|
+
p8.outro(pc8.yellow("No upgrade information available. Check your network and try again."));
|
|
1008
1159
|
process.exit(1);
|
|
1009
1160
|
}
|
|
1010
1161
|
if (!isNewer(latest, currentVersion)) {
|
|
1011
|
-
|
|
1012
|
-
`Installed: ${
|
|
1013
|
-
Latest: ${
|
|
1162
|
+
p8.note(
|
|
1163
|
+
`Installed: ${pc8.cyan(`v${currentVersion}`)}
|
|
1164
|
+
Latest: ${pc8.cyan(`v${latest}`)}`,
|
|
1014
1165
|
"Already up to date"
|
|
1015
1166
|
);
|
|
1016
|
-
|
|
1167
|
+
p8.outro(pc8.green("Nothing to do."));
|
|
1017
1168
|
return;
|
|
1018
1169
|
}
|
|
1019
|
-
|
|
1170
|
+
p8.note(
|
|
1020
1171
|
[
|
|
1021
|
-
`Installed: ${
|
|
1022
|
-
`Latest: ${
|
|
1023
|
-
`Source: ${
|
|
1024
|
-
`Path: ${
|
|
1172
|
+
`Installed: ${pc8.dim(`v${currentVersion}`)}`,
|
|
1173
|
+
`Latest: ${pc8.green(`v${latest}`)}`,
|
|
1174
|
+
`Source: ${pc8.cyan(install.kind)}`,
|
|
1175
|
+
`Path: ${pc8.dim(install.resolvedPath)}`
|
|
1025
1176
|
].join("\n"),
|
|
1026
1177
|
"Upgrade available"
|
|
1027
1178
|
);
|
|
1028
1179
|
if (!install.upgradeCommand) {
|
|
1029
|
-
|
|
1180
|
+
p8.note(
|
|
1030
1181
|
install.note ?? "Could not detect how shed was installed.",
|
|
1031
|
-
|
|
1182
|
+
pc8.yellow("Cannot self-upgrade")
|
|
1032
1183
|
);
|
|
1033
|
-
|
|
1184
|
+
p8.outro(pc8.dim("Re-install manually using your preferred package manager."));
|
|
1034
1185
|
return;
|
|
1035
1186
|
}
|
|
1036
1187
|
const elevate = await needsElevation(install.resolvedPath);
|
|
1037
1188
|
const finalCommand = elevate ? `sudo ${install.upgradeCommand}` : install.upgradeCommand;
|
|
1038
1189
|
if (opts.check) {
|
|
1039
|
-
|
|
1040
|
-
|
|
1190
|
+
p8.note(finalCommand, "Run this to upgrade");
|
|
1191
|
+
p8.outro(pc8.dim("(--check mode: nothing executed)"));
|
|
1041
1192
|
return;
|
|
1042
1193
|
}
|
|
1043
1194
|
if (elevate) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1195
|
+
p8.note(finalCommand, pc8.yellow("Install dir is not writable \u2014 run this manually"));
|
|
1196
|
+
p8.outro(pc8.dim("Re-run `shed upgrade` after the install completes to verify."));
|
|
1046
1197
|
return;
|
|
1047
1198
|
}
|
|
1048
1199
|
if (!opts.yes) {
|
|
1049
|
-
const ok = await
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1200
|
+
const ok = await p8.confirm({ message: `Run \`${finalCommand}\` now?`, initialValue: true });
|
|
1201
|
+
if (p8.isCancel(ok) || !ok) {
|
|
1202
|
+
p8.cancel("Upgrade cancelled.");
|
|
1052
1203
|
return;
|
|
1053
1204
|
}
|
|
1054
1205
|
}
|
|
1055
|
-
const runSpin =
|
|
1206
|
+
const runSpin = p8.spinner();
|
|
1056
1207
|
runSpin.start(`Running ${finalCommand}\u2026`);
|
|
1057
1208
|
try {
|
|
1058
1209
|
const [bin, ...args] = finalCommand.split(" ");
|
|
1059
1210
|
if (!bin) throw new Error("Empty upgrade command");
|
|
1060
1211
|
await execa2(bin, args, { stdio: "pipe" });
|
|
1061
|
-
runSpin.stop(
|
|
1062
|
-
|
|
1212
|
+
runSpin.stop(pc8.green(`Upgraded to v${latest}.`));
|
|
1213
|
+
p8.outro(pc8.green("Done. Re-run `shed --version` to confirm."));
|
|
1063
1214
|
} catch (err) {
|
|
1064
|
-
runSpin.stop(
|
|
1215
|
+
runSpin.stop(pc8.red("Upgrade failed."));
|
|
1065
1216
|
const message = err instanceof Error ? err.message : String(err);
|
|
1066
|
-
|
|
1067
|
-
|
|
1217
|
+
p8.note(message, pc8.red("Error"));
|
|
1218
|
+
p8.outro(pc8.dim(`You can retry manually: ${finalCommand}`));
|
|
1068
1219
|
process.exit(1);
|
|
1069
1220
|
}
|
|
1070
1221
|
}
|
|
1071
1222
|
|
|
1072
1223
|
// src/update/notifier.ts
|
|
1073
|
-
import
|
|
1224
|
+
import pc9 from "picocolors";
|
|
1074
1225
|
function maybeNotifyOfUpdate(currentVersion) {
|
|
1075
1226
|
const cached = readCachedLatest();
|
|
1076
1227
|
if (!cached || !isNewer(cached, currentVersion)) return;
|
|
1077
1228
|
const install = detectInstall(process.argv[1] ?? "");
|
|
1078
1229
|
const cmd = install.upgradeCommand ?? "shed upgrade";
|
|
1079
1230
|
const banner = [
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1231
|
+
pc9.yellow("\u25B2"),
|
|
1232
|
+
pc9.dim(`shed v${currentVersion} \u2192`),
|
|
1233
|
+
pc9.green(`v${cached}`),
|
|
1234
|
+
pc9.dim("available."),
|
|
1235
|
+
pc9.dim("Run"),
|
|
1236
|
+
pc9.cyan("`shed upgrade`"),
|
|
1237
|
+
pc9.dim(`(or \`${cmd}\`).`)
|
|
1087
1238
|
].join(" ");
|
|
1088
1239
|
console.log(banner);
|
|
1089
1240
|
}
|
|
@@ -1094,7 +1245,7 @@ function scheduleBackgroundRefresh() {
|
|
|
1094
1245
|
}
|
|
1095
1246
|
|
|
1096
1247
|
// src/logo.ts
|
|
1097
|
-
import
|
|
1248
|
+
import pc10 from "picocolors";
|
|
1098
1249
|
var CABIN = [" \u2571\u2572 ", " \u2571\u2500\u2500\u2572 ", " \u2571\u2500\u2500\u2500\u2500\u2572 ", " \u2502 \u2588\u2588 \u2502 ", " \u2514\u2500\u2500\u2500\u2500\u2518 "];
|
|
1099
1250
|
var WORDMARK = [
|
|
1100
1251
|
" ____ _ _ ",
|
|
@@ -1105,11 +1256,11 @@ var WORDMARK = [
|
|
|
1105
1256
|
];
|
|
1106
1257
|
function printLogo(version2) {
|
|
1107
1258
|
for (let i = 0; i < CABIN.length; i++) {
|
|
1108
|
-
console.log(
|
|
1259
|
+
console.log(pc10.yellow(CABIN[i]) + pc10.cyan(WORDMARK[i]));
|
|
1109
1260
|
}
|
|
1110
|
-
console.log(` ${
|
|
1261
|
+
console.log(` ${pc10.dim(`v${version2} \xB7 safe disk cleanup \xB7 dev machines & servers`)}`);
|
|
1111
1262
|
console.log(
|
|
1112
|
-
` ${
|
|
1263
|
+
` ${pc10.dim("by")} ${pc10.white("L\xEA Xu\xE2n M\u1EA1nh")} ${pc10.dim("\xB7 https://github.com/lexmanh/shed")}
|
|
1113
1264
|
`
|
|
1114
1265
|
);
|
|
1115
1266
|
}
|
|
@@ -1121,7 +1272,8 @@ var program = new Command();
|
|
|
1121
1272
|
program.name("shed").description("Safe disk cleanup for dev machines and Linux servers").version(version).option("-v, --verbose", "Enable verbose logging");
|
|
1122
1273
|
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);
|
|
1123
1274
|
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);
|
|
1124
|
-
program.command("undo").description("
|
|
1275
|
+
program.command("undo [token]").description("Restore a previously cleaned item from Trash").option("--last", "Restore the most recent operation").option("--session <id>", "Restore every operation from a session").action(undoCommand);
|
|
1276
|
+
program.command("history").description("List recent cleanup operations from the oplog").option("--limit <n>", "Show at most N entries", "20").option("--session <id>", "Filter to a single session ULID").option("--json", "Output machine-readable JSON").action(historyCommand);
|
|
1125
1277
|
program.command("doctor").description("Check environment and configuration").action(doctorCommand);
|
|
1126
1278
|
program.command("config").description("Manage user preferences").argument("[action]", "get | set | list | reset").argument("[key]", "Configuration key").argument("[value]", "Configuration value (for set)").action(configCommand);
|
|
1127
1279
|
program.command("completions").description("Print shell completion script").argument("<shell>", "bash | zsh | fish").action(completionsCommand);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexmanh/shed-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-beta.1",
|
|
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.
|
|
26
|
+
"@lexmanh/shed-core": "0.4.0-beta.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.10.0",
|