@lexmanh/shed-cli 0.2.0-beta.8 → 0.3.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 +283 -15
- 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
|
|
53
|
+
const spinner4 = p.spinner();
|
|
54
54
|
verbose(`clean root: ${rootDir}, dryRun=${isDryRun}, hardDelete=${options.hardDelete ?? false}`);
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
213
|
-
p.outro(
|
|
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
|
|
605
|
+
const spinner4 = options.json ? null : p4.spinner();
|
|
606
606
|
verbose(`scan root: ${rootDir}`);
|
|
607
|
-
|
|
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
|
-
|
|
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,20 +783,281 @@ 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/
|
|
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";
|
|
788
|
-
|
|
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";
|
|
1046
|
+
var CABIN = [" \u2571\u2572 ", " \u2571\u2500\u2500\u2572 ", " \u2571\u2500\u2500\u2500\u2500\u2572 ", " \u2502 \u2588\u2588 \u2502 ", " \u2514\u2500\u2500\u2500\u2500\u2518 "];
|
|
1047
|
+
var WORDMARK = [
|
|
789
1048
|
" ____ _ _ ",
|
|
790
1049
|
" / ___|| |__ ___ __| |",
|
|
791
1050
|
" \\___ \\| '_ \\ / _ \\/ _` |",
|
|
792
1051
|
" ___) | | | | __/ (_| |",
|
|
793
1052
|
" |____/|_| |_|\\___|\\__,_|"
|
|
794
|
-
]
|
|
1053
|
+
];
|
|
795
1054
|
function printLogo(version2) {
|
|
796
|
-
|
|
797
|
-
|
|
1055
|
+
for (let i = 0; i < CABIN.length; i++) {
|
|
1056
|
+
console.log(pc8.yellow(CABIN[i]) + pc8.cyan(WORDMARK[i]));
|
|
1057
|
+
}
|
|
1058
|
+
console.log(` ${pc8.dim(`v${version2} \xB7 safe disk cleanup \xB7 dev machines & servers`)}`);
|
|
798
1059
|
console.log(
|
|
799
|
-
` ${
|
|
1060
|
+
` ${pc8.dim("by")} ${pc8.white("L\xEA Xu\xE2n M\u1EA1nh")} ${pc8.dim("\xB7 https://github.com/lexmanh/shed")}
|
|
800
1061
|
`
|
|
801
1062
|
);
|
|
802
1063
|
}
|
|
@@ -812,12 +1073,19 @@ program.command("undo").description("List and restore items from previous cleanu
|
|
|
812
1073
|
program.command("doctor").description("Check environment and configuration").action(doctorCommand);
|
|
813
1074
|
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
1075
|
program.command("completions").description("Print shell completion script").argument("<shell>", "bash | zsh | fish").action(completionsCommand);
|
|
1076
|
+
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
1077
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
816
1078
|
const opts = program.opts();
|
|
817
1079
|
setVerbose(opts.verbose ?? false);
|
|
818
1080
|
const cmdOpts = actionCommand.opts();
|
|
819
|
-
const
|
|
1081
|
+
const cmdName = actionCommand.name();
|
|
1082
|
+
const isCompletions = cmdName === "completions";
|
|
1083
|
+
const isUpgrade = cmdName === "upgrade";
|
|
820
1084
|
if (!cmdOpts.json && !isCompletions) printLogo(version);
|
|
1085
|
+
if (!cmdOpts.json && !isCompletions && !isUpgrade) {
|
|
1086
|
+
maybeNotifyOfUpdate(version);
|
|
1087
|
+
scheduleBackgroundRefresh();
|
|
1088
|
+
}
|
|
821
1089
|
});
|
|
822
1090
|
program.parseAsync(process.argv).catch((err) => {
|
|
823
1091
|
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.
|
|
3
|
+
"version": "0.3.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.3.0-beta.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.10.0",
|