@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.
Files changed (2) hide show
  1. package/dist/cli.js +270 -118
  2. 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 outro7 = 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!";
255
- p2.outro(outro7);
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 p5 from "@clack/prompts";
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 pc5 from "picocolors";
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]: pc5.green("\u25CF Green"),
626
- [RiskTier2.Yellow]: pc5.yellow("\u25CF Yellow"),
627
- [RiskTier2.Red]: pc5.red("\u25CF 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 formatBytes2(bytes) {
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
- p5.intro(pc5.bgCyan(pc5.black(" shed scan ")));
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
- p5.cancel(scope.message);
731
+ p6.cancel(scope.message);
653
732
  }
654
733
  return;
655
734
  }
656
- const spinner4 = options.json ? null : p5.spinner();
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 ${pc5.bold(String(allItems.length))} cleanable items across ${projects.length} project(s).`
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
- p5.note("Nothing found to clean in this directory.", "Result");
725
- p5.outro(pc5.dim("All clear!"));
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
- p5.outro(
735
- `Total recoverable: ${pc5.bold(pc5.green(formatBytes2(totalBytes)))} \u2014 run ${pc5.cyan("shed clean")} to proceed.`
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 = `${pc5.green(`\u25CF ${byRisk.green} Green`)} ${pc5.yellow(`\u25CF ${byRisk.yellow} Yellow`)} ${pc5.red(`\u25CF ${byRisk.red} Red`)}${detectOnly > 0 ? pc5.dim(` (${detectOnly} detect-only)`) : ""}`;
749
- const detectorLine = [...byDetector.entries()].sort(([, a], [, b]) => b - a).slice(0, DETECTOR_BREAKDOWN_TOP_N).map(([d, b]) => `${d} ${formatBytes2(b)}`).join(" \xB7 ");
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: ${pc5.dim(detectorLine)}`);
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(` ${pc5.bold(`Top ${shown.length} items:`)}`);
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" ? pc5.dim(` (${g.itemCount} ${g.detector} items)`) : "";
760
- const size = g.totalBytes > 0 ? pc5.dim(` ${formatBytes2(g.totalBytes)}`) : "";
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
- pc5.dim(
767
- ` \u2026 ${hidden.groupCount} more groups (${hidden.itemCount} items, ${formatBytes2(hidden.totalBytes)}) \u2014 use --all to see everything`
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)" ? pc5.dim("global caches") : pc5.cyan(home ? projectRoot.replace(home, "~") : projectRoot);
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} ${pc5.dim(formatBytes2(groupTotal))}`);
864
+ ${projectLabel} ${pc6.dim(formatBytes3(groupTotal))}`);
786
865
  for (const item of items) {
787
- const size = item.sizeBytes > 0 ? pc5.dim(` ${formatBytes2(item.sizeBytes)}`) : "";
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(` ${pc5.dim(` ${item.description}`)}`);
870
+ console.log(` ${pc6.dim(` ${item.description}`)}`);
792
871
  }
793
872
  }
794
873
  }
795
874
 
796
875
  // src/commands/undo.ts
797
- import { platform as platform2 } from "os";
798
- import * as p6 from "@clack/prompts";
799
- import pc6 from "picocolors";
800
- function trashPath() {
801
- switch (platform2()) {
802
- case "darwin":
803
- return "~/.Trash";
804
- case "win32":
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 trashOpenHint() {
811
- switch (platform2()) {
812
- case "darwin":
813
- return "open ~/.Trash (or click Trash in Dock \u2192 right-click \u2192 Put Back)";
814
- case "win32":
815
- return "Open Recycle Bin on desktop \u2192 right-click item \u2192 Restore";
816
- default:
817
- return "nautilus trash:/// or gio trash --list / gio trash --restore";
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 undoCommand() {
821
- p6.intro(pc6.bgMagenta(pc6.black(" shed undo ")));
822
- p6.note(
823
- [
824
- "shed clean moves items to your OS Trash by default.",
825
- "",
826
- ` Trash location: ${pc6.cyan(trashPath())}`,
827
- "",
828
- ` To restore: ${pc6.dim(trashOpenHint())}`,
829
- "",
830
- pc6.dim("Tip: shed clean --hard-delete bypasses Trash (no undo possible)."),
831
- pc6.dim(" shed clean --dry-run to preview before any real deletion.")
832
- ].join("\n"),
833
- "How to undo a cleanup"
834
- );
835
- p6.outro(pc6.green("Nothing to do \u2014 restore items via your OS Trash."));
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 p7 from "@clack/prompts";
990
+ import * as p8 from "@clack/prompts";
840
991
  import { execa as execa2 } from "execa";
841
- import pc7 from "picocolors";
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 p8 = resolvedPath.replace(/\\/g, "/").toLowerCase();
850
- if (p8.includes("/_npx/") || p8.includes("/npx-cache/")) {
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 (p8.includes("/bun/install/cache/") || p8.includes("/.bun/install/cache/")) {
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 (p8.includes("/.volta/") || p8.includes("/volta/tools/")) {
1013
+ if (p9.includes("/.volta/") || p9.includes("/volta/tools/")) {
863
1014
  return { kind: "volta" };
864
1015
  }
865
- if (p8.includes("/pnpm/global/") || p8.includes("/library/pnpm/") || p8.includes("/.local/share/pnpm/")) {
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 (p8.includes("/yarn/global/") || p8.includes("/.config/yarn/global/")) {
1019
+ if (p9.includes("/yarn/global/") || p9.includes("/.config/yarn/global/")) {
869
1020
  return { kind: "yarn-global" };
870
1021
  }
871
- if (p8.includes("/.bun/install/global/")) {
1022
+ if (p9.includes("/.bun/install/global/")) {
872
1023
  return { kind: "bun-global" };
873
1024
  }
874
- if (p8.includes("/node_modules/")) {
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: note8 } = classifyInstall(resolvedPath);
1055
+ const { kind, note: note9 } = classifyInstall(resolvedPath);
905
1056
  return {
906
1057
  kind,
907
1058
  upgradeCommand: buildUpgradeCommand(kind),
908
1059
  resolvedPath,
909
- note: note8
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
- p7.intro(pc7.bgMagenta(pc7.black(" shed upgrade ")));
1151
+ p8.intro(pc8.bgMagenta(pc8.black(" shed upgrade ")));
1001
1152
  const install = detectInstall(process.argv[1] ?? "");
1002
- const spin = p7.spinner();
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
- p7.outro(pc7.yellow("No upgrade information available. Check your network and try again."));
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
- p7.note(
1012
- `Installed: ${pc7.cyan(`v${currentVersion}`)}
1013
- Latest: ${pc7.cyan(`v${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
- p7.outro(pc7.green("Nothing to do."));
1167
+ p8.outro(pc8.green("Nothing to do."));
1017
1168
  return;
1018
1169
  }
1019
- p7.note(
1170
+ p8.note(
1020
1171
  [
1021
- `Installed: ${pc7.dim(`v${currentVersion}`)}`,
1022
- `Latest: ${pc7.green(`v${latest}`)}`,
1023
- `Source: ${pc7.cyan(install.kind)}`,
1024
- `Path: ${pc7.dim(install.resolvedPath)}`
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
- p7.note(
1180
+ p8.note(
1030
1181
  install.note ?? "Could not detect how shed was installed.",
1031
- pc7.yellow("Cannot self-upgrade")
1182
+ pc8.yellow("Cannot self-upgrade")
1032
1183
  );
1033
- p7.outro(pc7.dim("Re-install manually using your preferred package manager."));
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
- p7.note(finalCommand, "Run this to upgrade");
1040
- p7.outro(pc7.dim("(--check mode: nothing executed)"));
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
- p7.note(finalCommand, pc7.yellow("Install dir is not writable \u2014 run this manually"));
1045
- p7.outro(pc7.dim("Re-run `shed upgrade` after the install completes to verify."));
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 p7.confirm({ message: `Run \`${finalCommand}\` now?`, initialValue: true });
1050
- if (p7.isCancel(ok) || !ok) {
1051
- p7.cancel("Upgrade cancelled.");
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 = p7.spinner();
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(pc7.green(`Upgraded to v${latest}.`));
1062
- p7.outro(pc7.green("Done. Re-run `shed --version` to confirm."));
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(pc7.red("Upgrade failed."));
1215
+ runSpin.stop(pc8.red("Upgrade failed."));
1065
1216
  const message = err instanceof Error ? err.message : String(err);
1066
- p7.note(message, pc7.red("Error"));
1067
- p7.outro(pc7.dim(`You can retry manually: ${finalCommand}`));
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 pc8 from "picocolors";
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
- pc8.yellow("\u25B2"),
1081
- pc8.dim(`shed v${currentVersion} \u2192`),
1082
- pc8.green(`v${cached}`),
1083
- pc8.dim("available."),
1084
- pc8.dim("Run"),
1085
- pc8.cyan("`shed upgrade`"),
1086
- pc8.dim(`(or \`${cmd}\`).`)
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 pc9 from "picocolors";
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(pc9.yellow(CABIN[i]) + pc9.cyan(WORDMARK[i]));
1259
+ console.log(pc10.yellow(CABIN[i]) + pc10.cyan(WORDMARK[i]));
1109
1260
  }
1110
- console.log(` ${pc9.dim(`v${version2} \xB7 safe disk cleanup \xB7 dev machines & servers`)}`);
1261
+ console.log(` ${pc10.dim(`v${version2} \xB7 safe disk cleanup \xB7 dev machines & servers`)}`);
1111
1262
  console.log(
1112
- ` ${pc9.dim("by")} ${pc9.white("L\xEA Xu\xE2n M\u1EA1nh")} ${pc9.dim("\xB7 https://github.com/lexmanh/shed")}
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("List and restore items from previous cleanups").action(undoCommand);
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.0-beta.2",
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.3.0-beta.2"
26
+ "@lexmanh/shed-core": "0.4.0-beta.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.10.0",