@lexmanh/shed-cli 0.2.0-beta.7 → 0.2.0-beta.9

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 +285 -71
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -8,18 +8,10 @@ import { Command } from "commander";
8
8
  import { resolve } from "path";
9
9
  import * as p from "@clack/prompts";
10
10
  import {
11
- AndroidDetector,
12
- CocoaPodsDetector,
13
- DockerDetector,
14
- FlutterDetector,
15
- IdeDetector,
16
- NodeDetector,
17
- PythonDetector,
18
11
  RiskTier,
19
- RustDetector,
20
12
  SafetyChecker,
21
13
  Scanner,
22
- XcodeDetector
14
+ defaultDetectors
23
15
  } from "@lexmanh/shed-core";
24
16
  import pc from "picocolors";
25
17
 
@@ -58,20 +50,10 @@ async function cleanCommand(path = ".", options = {}) {
58
50
  "Safe mode"
59
51
  );
60
52
  }
61
- const spinner3 = p.spinner();
53
+ const spinner4 = p.spinner();
62
54
  verbose(`clean root: ${rootDir}, dryRun=${isDryRun}, hardDelete=${options.hardDelete ?? false}`);
63
- spinner3.start(`Scanning ${rootDir} \u2026`);
64
- const scanner = new Scanner([
65
- new NodeDetector(),
66
- new PythonDetector(),
67
- new RustDetector(),
68
- new DockerDetector(),
69
- new XcodeDetector(),
70
- new FlutterDetector(),
71
- new AndroidDetector(),
72
- new CocoaPodsDetector(),
73
- new IdeDetector()
74
- ]);
55
+ spinner4.start(`Scanning ${rootDir} \u2026`);
56
+ const scanner = new Scanner(defaultDetectors());
75
57
  const ctx = { scanRoot: rootDir, maxDepth: 8 };
76
58
  const [projects, globalItems] = await Promise.all([
77
59
  scanner.scan(rootDir),
@@ -82,7 +64,7 @@ async function cleanCommand(path = ".", options = {}) {
82
64
  ...globalItems
83
65
  ].filter((i) => options.includeRed || i.risk !== RiskTier.Red);
84
66
  verbose(`scan complete: ${allItems.length} cleanable items`);
85
- spinner3.stop(`Found ${pc.bold(String(allItems.length))} cleanable items.`);
67
+ spinner4.stop(`Found ${pc.bold(String(allItems.length))} cleanable items.`);
86
68
  if (allItems.length === 0) {
87
69
  p.outro(pc.dim("Nothing to clean."));
88
70
  return;
@@ -139,10 +121,11 @@ async function cleanCommand(path = ".", options = {}) {
139
121
  label: `${pc.yellow("Yellow only")} ${pc.dim(`${yellowItems.length} items \xB7 ${formatBytes(yellowBytes)}`)}`
140
122
  }
141
123
  ] : [],
142
- { value: "custom", label: "Custom (pick individual items)" }
124
+ { value: "custom", label: "Custom (pick individual items)" },
125
+ { value: "cancel", label: pc.dim("Cancel (do nothing, exit)") }
143
126
  ]
144
127
  });
145
- if (p.isCancel(preset)) {
128
+ if (p.isCancel(preset) || preset === "cancel") {
146
129
  p.cancel("Cleanup cancelled.");
147
130
  return;
148
131
  }
@@ -226,8 +209,8 @@ async function cleanCommand(path = ".", options = {}) {
226
209
  }
227
210
  }
228
211
  console.log();
229
- const outro6 = isDryRun ? `Dry-run complete. Run with ${pc.cyan("--execute")} to perform actual cleanup.` : result.failed.length > 0 ? `Completed with ${result.failed.length} failure(s).` : "All done!";
230
- p.outro(outro6);
212
+ const outro7 = isDryRun ? `Dry-run complete. Run with ${pc.cyan("--execute")} to perform actual cleanup.` : result.failed.length > 0 ? `Completed with ${result.failed.length} failure(s).` : "All done!";
213
+ p.outro(outro7);
231
214
  }
232
215
 
233
216
  // src/commands/completions.ts
@@ -516,25 +499,9 @@ import { hostname } from "os";
516
499
  import { resolve as resolve2 } from "path";
517
500
  import * as p4 from "@clack/prompts";
518
501
  import {
519
- AndroidDetector as AndroidDetector2,
520
- CocoaPodsDetector as CocoaPodsDetector2,
521
- DatabaseDetector,
522
- DockerDetector as DockerDetector2,
523
- DotnetDetector,
524
- FlutterDetector as FlutterDetector2,
525
- GoDetector,
526
- IdeDetector as IdeDetector2,
527
- JavaGradleDetector,
528
- JavaMavenDetector,
529
- NodeDetector as NodeDetector2,
530
- PythonDetector as PythonDetector2,
531
502
  RiskTier as RiskTier2,
532
- RubyDetector,
533
- RustDetector as RustDetector2,
534
503
  Scanner as Scanner2,
535
- SystemDetector,
536
- WebserverDetector,
537
- XcodeDetector as XcodeDetector2
504
+ defaultDetectors as defaultDetectors2
538
505
  } from "@lexmanh/shed-core";
539
506
  import pc4 from "picocolors";
540
507
 
@@ -635,29 +602,11 @@ async function scanCommand(path = ".", options = {}) {
635
602
  if (!options.json) {
636
603
  p4.intro(pc4.bgCyan(pc4.black(" shed scan ")));
637
604
  }
638
- const spinner3 = options.json ? null : p4.spinner();
605
+ const spinner4 = options.json ? null : p4.spinner();
639
606
  verbose(`scan root: ${rootDir}`);
640
- spinner3?.start(`Scanning ${rootDir} \u2026`);
607
+ spinner4?.start(`Scanning ${rootDir} \u2026`);
641
608
  const scanStartedAt = Date.now();
642
- const scanner = new Scanner2([
643
- new NodeDetector2(),
644
- new PythonDetector2(),
645
- new RustDetector2(),
646
- new GoDetector(),
647
- new JavaMavenDetector(),
648
- new JavaGradleDetector(),
649
- new RubyDetector(),
650
- new DotnetDetector(),
651
- new DockerDetector2(),
652
- new XcodeDetector2(),
653
- new FlutterDetector2(),
654
- new AndroidDetector2(),
655
- new CocoaPodsDetector2(),
656
- new IdeDetector2(),
657
- new SystemDetector(),
658
- new WebserverDetector(),
659
- new DatabaseDetector()
660
- ]);
609
+ const scanner = new Scanner2(defaultDetectors2());
661
610
  const ctx = { scanRoot: rootDir, maxDepth: 8 };
662
611
  const [projects, globalItems] = await Promise.all([
663
612
  scanner.scan(rootDir),
@@ -673,7 +622,7 @@ async function scanCommand(path = ".", options = {}) {
673
622
  );
674
623
  for (const item of allItems)
675
624
  verbose(` item: ${item.risk} ${item.path} (${item.sizeBytes} bytes)`);
676
- spinner3?.stop(
625
+ spinner4?.stop(
677
626
  `Found ${pc4.bold(String(allItems.length))} cleanable items across ${projects.length} project(s).`
678
627
  );
679
628
  if (options.json) {
@@ -834,8 +783,266 @@ async function undoCommand() {
834
783
  p5.outro(pc5.green("Nothing to do \u2014 restore items via your OS Trash."));
835
784
  }
836
785
 
837
- // src/logo.ts
786
+ // src/commands/upgrade.ts
787
+ import * as p6 from "@clack/prompts";
788
+ import { execa as execa2 } from "execa";
838
789
  import pc6 from "picocolors";
790
+
791
+ // src/update/detect-install.ts
792
+ import { realpathSync } from "fs";
793
+ import { constants, access } from "fs/promises";
794
+ import { dirname as dirname2 } from "path";
795
+ var PACKAGE_NAME = "@lexmanh/shed-cli";
796
+ function classifyInstall(resolvedPath) {
797
+ const p7 = resolvedPath.replace(/\\/g, "/").toLowerCase();
798
+ if (p7.includes("/_npx/") || p7.includes("/npx-cache/")) {
799
+ return {
800
+ kind: "npx",
801
+ note: "Running via npx (ephemeral). Re-run with `npx @lexmanh/shed-cli@latest`."
802
+ };
803
+ }
804
+ if (p7.includes("/bun/install/cache/") || p7.includes("/.bun/install/cache/")) {
805
+ return {
806
+ kind: "bunx",
807
+ note: "Running via bunx (ephemeral). Re-run with `bunx @lexmanh/shed-cli@latest`."
808
+ };
809
+ }
810
+ if (p7.includes("/.volta/") || p7.includes("/volta/tools/")) {
811
+ return { kind: "volta" };
812
+ }
813
+ if (p7.includes("/pnpm/global/") || p7.includes("/library/pnpm/") || p7.includes("/.local/share/pnpm/")) {
814
+ return { kind: "pnpm-global" };
815
+ }
816
+ if (p7.includes("/yarn/global/") || p7.includes("/.config/yarn/global/")) {
817
+ return { kind: "yarn-global" };
818
+ }
819
+ if (p7.includes("/.bun/install/global/")) {
820
+ return { kind: "bun-global" };
821
+ }
822
+ if (p7.includes("/node_modules/")) {
823
+ return { kind: "npm-global" };
824
+ }
825
+ return { kind: "unknown" };
826
+ }
827
+ function buildUpgradeCommand(kind, pkg = PACKAGE_NAME) {
828
+ switch (kind) {
829
+ case "npm-global":
830
+ return `npm i -g ${pkg}@latest`;
831
+ case "pnpm-global":
832
+ return `pnpm add -g ${pkg}@latest`;
833
+ case "yarn-global":
834
+ return `yarn global add ${pkg}@latest`;
835
+ case "bun-global":
836
+ return `bun add -g ${pkg}@latest`;
837
+ case "volta":
838
+ return `volta install ${pkg}@latest`;
839
+ case "npx":
840
+ case "bunx":
841
+ case "unknown":
842
+ return null;
843
+ }
844
+ }
845
+ function detectInstall(binPath) {
846
+ let resolvedPath;
847
+ try {
848
+ resolvedPath = realpathSync(binPath);
849
+ } catch {
850
+ resolvedPath = binPath;
851
+ }
852
+ const { kind, note: note7 } = classifyInstall(resolvedPath);
853
+ return {
854
+ kind,
855
+ upgradeCommand: buildUpgradeCommand(kind),
856
+ resolvedPath,
857
+ note: note7
858
+ };
859
+ }
860
+ async function needsElevation(resolvedPath) {
861
+ if (process.platform === "win32") return false;
862
+ try {
863
+ await access(dirname2(resolvedPath), constants.W_OK);
864
+ return false;
865
+ } catch {
866
+ return true;
867
+ }
868
+ }
869
+
870
+ // src/update/registry.ts
871
+ import Conf2 from "conf";
872
+ var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
873
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
874
+ var DEFAULT_TIMEOUT_MS = 3e3;
875
+ function getCache() {
876
+ return new Conf2({ projectName: "shed", configName: "update-cache" });
877
+ }
878
+ function readCachedLatest() {
879
+ const last = getCache().get("lastCheck");
880
+ if (!last) return null;
881
+ if (Date.now() - last.checkedAt >= CACHE_TTL_MS) return null;
882
+ return last.version;
883
+ }
884
+ async function fetchLatestVersion(opts = {}) {
885
+ if (!opts.force) {
886
+ const cached = readCachedLatest();
887
+ if (cached) return cached;
888
+ }
889
+ try {
890
+ const ctrl = new AbortController();
891
+ const t = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
892
+ const res = await fetch(REGISTRY_URL, {
893
+ signal: ctrl.signal,
894
+ headers: { Accept: "application/json" }
895
+ });
896
+ clearTimeout(t);
897
+ if (!res.ok) return null;
898
+ const json = await res.json();
899
+ if (!json.version) return null;
900
+ getCache().set("lastCheck", { version: json.version, checkedAt: Date.now() });
901
+ return json.version;
902
+ } catch {
903
+ return null;
904
+ }
905
+ }
906
+ function compareSemver(a, b) {
907
+ const [aMain, aPre = ""] = a.split("-");
908
+ const [bMain, bPre = ""] = b.split("-");
909
+ const aParts = (aMain ?? "").split(".").map((n2) => Number(n2) || 0);
910
+ const bParts = (bMain ?? "").split(".").map((n2) => Number(n2) || 0);
911
+ for (let i = 0; i < 3; i++) {
912
+ const x = aParts[i] ?? 0;
913
+ const y = bParts[i] ?? 0;
914
+ if (x !== y) return x - y;
915
+ }
916
+ if (aPre === "" && bPre === "") return 0;
917
+ if (aPre === "") return 1;
918
+ if (bPre === "") return -1;
919
+ const aP = aPre.split(".");
920
+ const bP = bPre.split(".");
921
+ const n = Math.max(aP.length, bP.length);
922
+ for (let i = 0; i < n; i++) {
923
+ const x = aP[i];
924
+ const y = bP[i];
925
+ if (x === void 0) return -1;
926
+ if (y === void 0) return 1;
927
+ const xIsNum = /^\d+$/.test(x);
928
+ const yIsNum = /^\d+$/.test(y);
929
+ if (xIsNum && yIsNum) {
930
+ const diff = Number(x) - Number(y);
931
+ if (diff !== 0) return diff;
932
+ } else if (xIsNum) {
933
+ return -1;
934
+ } else if (yIsNum) {
935
+ return 1;
936
+ } else if (x !== y) {
937
+ return x > y ? 1 : -1;
938
+ }
939
+ }
940
+ return 0;
941
+ }
942
+ function isNewer(latest, current) {
943
+ return compareSemver(latest, current) > 0;
944
+ }
945
+
946
+ // src/commands/upgrade.ts
947
+ async function upgradeCommand(opts, currentVersion) {
948
+ p6.intro(pc6.bgMagenta(pc6.black(" shed upgrade ")));
949
+ const install = detectInstall(process.argv[1] ?? "");
950
+ const spin = p6.spinner();
951
+ spin.start("Checking npm registry\u2026");
952
+ const latest = await fetchLatestVersion({ force: true });
953
+ spin.stop(latest ? `Latest: v${latest}` : "Could not reach registry");
954
+ if (!latest) {
955
+ p6.outro(pc6.yellow("No upgrade information available. Check your network and try again."));
956
+ process.exit(1);
957
+ }
958
+ if (!isNewer(latest, currentVersion)) {
959
+ p6.note(
960
+ `Installed: ${pc6.cyan(`v${currentVersion}`)}
961
+ Latest: ${pc6.cyan(`v${latest}`)}`,
962
+ "Already up to date"
963
+ );
964
+ p6.outro(pc6.green("Nothing to do."));
965
+ return;
966
+ }
967
+ p6.note(
968
+ [
969
+ `Installed: ${pc6.dim(`v${currentVersion}`)}`,
970
+ `Latest: ${pc6.green(`v${latest}`)}`,
971
+ `Source: ${pc6.cyan(install.kind)}`,
972
+ `Path: ${pc6.dim(install.resolvedPath)}`
973
+ ].join("\n"),
974
+ "Upgrade available"
975
+ );
976
+ if (!install.upgradeCommand) {
977
+ p6.note(
978
+ install.note ?? "Could not detect how shed was installed.",
979
+ pc6.yellow("Cannot self-upgrade")
980
+ );
981
+ p6.outro(pc6.dim("Re-install manually using your preferred package manager."));
982
+ return;
983
+ }
984
+ const elevate = await needsElevation(install.resolvedPath);
985
+ const finalCommand = elevate ? `sudo ${install.upgradeCommand}` : install.upgradeCommand;
986
+ if (opts.check) {
987
+ p6.note(finalCommand, "Run this to upgrade");
988
+ p6.outro(pc6.dim("(--check mode: nothing executed)"));
989
+ return;
990
+ }
991
+ if (elevate) {
992
+ p6.note(finalCommand, pc6.yellow("Install dir is not writable \u2014 run this manually"));
993
+ p6.outro(pc6.dim("Re-run `shed upgrade` after the install completes to verify."));
994
+ return;
995
+ }
996
+ if (!opts.yes) {
997
+ const ok = await p6.confirm({ message: `Run \`${finalCommand}\` now?`, initialValue: true });
998
+ if (p6.isCancel(ok) || !ok) {
999
+ p6.cancel("Upgrade cancelled.");
1000
+ return;
1001
+ }
1002
+ }
1003
+ const runSpin = p6.spinner();
1004
+ runSpin.start(`Running ${finalCommand}\u2026`);
1005
+ try {
1006
+ const [bin, ...args] = finalCommand.split(" ");
1007
+ if (!bin) throw new Error("Empty upgrade command");
1008
+ await execa2(bin, args, { stdio: "pipe" });
1009
+ runSpin.stop(pc6.green(`Upgraded to v${latest}.`));
1010
+ p6.outro(pc6.green("Done. Re-run `shed --version` to confirm."));
1011
+ } catch (err) {
1012
+ runSpin.stop(pc6.red("Upgrade failed."));
1013
+ const message = err instanceof Error ? err.message : String(err);
1014
+ p6.note(message, pc6.red("Error"));
1015
+ p6.outro(pc6.dim(`You can retry manually: ${finalCommand}`));
1016
+ process.exit(1);
1017
+ }
1018
+ }
1019
+
1020
+ // src/update/notifier.ts
1021
+ import pc7 from "picocolors";
1022
+ function maybeNotifyOfUpdate(currentVersion) {
1023
+ const cached = readCachedLatest();
1024
+ if (!cached || !isNewer(cached, currentVersion)) return;
1025
+ const install = detectInstall(process.argv[1] ?? "");
1026
+ const cmd = install.upgradeCommand ?? "shed upgrade";
1027
+ const banner = [
1028
+ pc7.yellow("\u25B2"),
1029
+ pc7.dim(`shed v${currentVersion} \u2192`),
1030
+ pc7.green(`v${cached}`),
1031
+ pc7.dim("available."),
1032
+ pc7.dim("Run"),
1033
+ pc7.cyan("`shed upgrade`"),
1034
+ pc7.dim(`(or \`${cmd}\`).`)
1035
+ ].join(" ");
1036
+ console.log(banner);
1037
+ }
1038
+ function scheduleBackgroundRefresh() {
1039
+ if (readCachedLatest() !== null) return;
1040
+ void fetchLatestVersion({ force: false, timeoutMs: 1500 }).catch(() => {
1041
+ });
1042
+ }
1043
+
1044
+ // src/logo.ts
1045
+ import pc8 from "picocolors";
839
1046
  var ART = [
840
1047
  " ____ _ _ ",
841
1048
  " / ___|| |__ ___ __| |",
@@ -844,10 +1051,10 @@ var ART = [
844
1051
  " |____/|_| |_|\\___|\\__,_|"
845
1052
  ].join("\n");
846
1053
  function printLogo(version2) {
847
- console.log(pc6.cyan(ART));
848
- console.log(` ${pc6.dim(`v${version2} \xB7 safe disk cleanup \xB7 dev machines & servers`)}`);
1054
+ console.log(pc8.cyan(ART));
1055
+ console.log(` ${pc8.dim(`v${version2} \xB7 safe disk cleanup \xB7 dev machines & servers`)}`);
849
1056
  console.log(
850
- ` ${pc6.dim("by")} ${pc6.white("L\xEA Xu\xE2n M\u1EA1nh")} ${pc6.dim("\xB7 https://github.com/lexmanh/shed")}
1057
+ ` ${pc8.dim("by")} ${pc8.white("L\xEA Xu\xE2n M\u1EA1nh")} ${pc8.dim("\xB7 https://github.com/lexmanh/shed")}
851
1058
  `
852
1059
  );
853
1060
  }
@@ -863,12 +1070,19 @@ program.command("undo").description("List and restore items from previous cleanu
863
1070
  program.command("doctor").description("Check environment and configuration").action(doctorCommand);
864
1071
  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);
865
1072
  program.command("completions").description("Print shell completion script").argument("<shell>", "bash | zsh | fish").action(completionsCommand);
1073
+ program.command("upgrade").alias("update").description("Check for and install the latest version of shed").option("--check", "Only check; print the upgrade command without running it").option("--yes", "Skip the confirmation prompt").action((opts) => upgradeCommand(opts, version));
866
1074
  program.hook("preAction", (_thisCommand, actionCommand) => {
867
1075
  const opts = program.opts();
868
1076
  setVerbose(opts.verbose ?? false);
869
1077
  const cmdOpts = actionCommand.opts();
870
- const isCompletions = actionCommand.name() === "completions";
1078
+ const cmdName = actionCommand.name();
1079
+ const isCompletions = cmdName === "completions";
1080
+ const isUpgrade = cmdName === "upgrade";
871
1081
  if (!cmdOpts.json && !isCompletions) printLogo(version);
1082
+ if (!cmdOpts.json && !isCompletions && !isUpgrade) {
1083
+ maybeNotifyOfUpdate(version);
1084
+ scheduleBackgroundRefresh();
1085
+ }
872
1086
  });
873
1087
  program.parseAsync(process.argv).catch((err) => {
874
1088
  console.error("shed: fatal error:", err instanceof Error ? err.message : err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexmanh/shed-cli",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.9",
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.7"
26
+ "@lexmanh/shed-core": "0.2.0-beta.9"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.10.0",