@lexmanh/shed-cli 0.2.0-beta.8 → 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 +278 -13
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -50,9 +50,9 @@ async function cleanCommand(path = ".", options = {}) {
50
50
  "Safe mode"
51
51
  );
52
52
  }
53
- const spinner3 = p.spinner();
53
+ const spinner4 = p.spinner();
54
54
  verbose(`clean root: ${rootDir}, dryRun=${isDryRun}, hardDelete=${options.hardDelete ?? false}`);
55
- spinner3.start(`Scanning ${rootDir} \u2026`);
55
+ spinner4.start(`Scanning ${rootDir} \u2026`);
56
56
  const scanner = new Scanner(defaultDetectors());
57
57
  const ctx = { scanRoot: rootDir, maxDepth: 8 };
58
58
  const [projects, globalItems] = await Promise.all([
@@ -64,7 +64,7 @@ async function cleanCommand(path = ".", options = {}) {
64
64
  ...globalItems
65
65
  ].filter((i) => options.includeRed || i.risk !== RiskTier.Red);
66
66
  verbose(`scan complete: ${allItems.length} cleanable items`);
67
- spinner3.stop(`Found ${pc.bold(String(allItems.length))} cleanable items.`);
67
+ spinner4.stop(`Found ${pc.bold(String(allItems.length))} cleanable items.`);
68
68
  if (allItems.length === 0) {
69
69
  p.outro(pc.dim("Nothing to clean."));
70
70
  return;
@@ -209,8 +209,8 @@ async function cleanCommand(path = ".", options = {}) {
209
209
  }
210
210
  }
211
211
  console.log();
212
- 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!";
213
- 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);
214
214
  }
215
215
 
216
216
  // src/commands/completions.ts
@@ -602,9 +602,9 @@ async function scanCommand(path = ".", options = {}) {
602
602
  if (!options.json) {
603
603
  p4.intro(pc4.bgCyan(pc4.black(" shed scan ")));
604
604
  }
605
- const spinner3 = options.json ? null : p4.spinner();
605
+ const spinner4 = options.json ? null : p4.spinner();
606
606
  verbose(`scan root: ${rootDir}`);
607
- spinner3?.start(`Scanning ${rootDir} \u2026`);
607
+ spinner4?.start(`Scanning ${rootDir} \u2026`);
608
608
  const scanStartedAt = Date.now();
609
609
  const scanner = new Scanner2(defaultDetectors2());
610
610
  const ctx = { scanRoot: rootDir, maxDepth: 8 };
@@ -622,7 +622,7 @@ async function scanCommand(path = ".", options = {}) {
622
622
  );
623
623
  for (const item of allItems)
624
624
  verbose(` item: ${item.risk} ${item.path} (${item.sizeBytes} bytes)`);
625
- spinner3?.stop(
625
+ spinner4?.stop(
626
626
  `Found ${pc4.bold(String(allItems.length))} cleanable items across ${projects.length} project(s).`
627
627
  );
628
628
  if (options.json) {
@@ -783,8 +783,266 @@ async function undoCommand() {
783
783
  p5.outro(pc5.green("Nothing to do \u2014 restore items via your OS Trash."));
784
784
  }
785
785
 
786
- // src/logo.ts
786
+ // src/commands/upgrade.ts
787
+ import * as p6 from "@clack/prompts";
788
+ import { execa as execa2 } from "execa";
787
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";
788
1046
  var ART = [
789
1047
  " ____ _ _ ",
790
1048
  " / ___|| |__ ___ __| |",
@@ -793,10 +1051,10 @@ var ART = [
793
1051
  " |____/|_| |_|\\___|\\__,_|"
794
1052
  ].join("\n");
795
1053
  function printLogo(version2) {
796
- console.log(pc6.cyan(ART));
797
- 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`)}`);
798
1056
  console.log(
799
- ` ${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")}
800
1058
  `
801
1059
  );
802
1060
  }
@@ -812,12 +1070,19 @@ program.command("undo").description("List and restore items from previous cleanu
812
1070
  program.command("doctor").description("Check environment and configuration").action(doctorCommand);
813
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);
814
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));
815
1074
  program.hook("preAction", (_thisCommand, actionCommand) => {
816
1075
  const opts = program.opts();
817
1076
  setVerbose(opts.verbose ?? false);
818
1077
  const cmdOpts = actionCommand.opts();
819
- const isCompletions = actionCommand.name() === "completions";
1078
+ const cmdName = actionCommand.name();
1079
+ const isCompletions = cmdName === "completions";
1080
+ const isUpgrade = cmdName === "upgrade";
820
1081
  if (!cmdOpts.json && !isCompletions) printLogo(version);
1082
+ if (!cmdOpts.json && !isCompletions && !isUpgrade) {
1083
+ maybeNotifyOfUpdate(version);
1084
+ scheduleBackgroundRefresh();
1085
+ }
821
1086
  });
822
1087
  program.parseAsync(process.argv).catch((err) => {
823
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.8",
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.8"
26
+ "@lexmanh/shed-core": "0.2.0-beta.9"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.10.0",