@lexmanh/shed-cli 0.3.0-beta.1 → 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 +406 -202
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,15 +5,51 @@ import { createRequire as createRequire2 } from "module";
|
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/clean.ts
|
|
8
|
-
import
|
|
9
|
-
import * as p from "@clack/prompts";
|
|
8
|
+
import * as p2 from "@clack/prompts";
|
|
10
9
|
import {
|
|
10
|
+
OpLog,
|
|
11
11
|
RiskTier,
|
|
12
12
|
SafetyChecker,
|
|
13
13
|
Scanner,
|
|
14
14
|
defaultDetectors
|
|
15
15
|
} from "@lexmanh/shed-core";
|
|
16
|
+
import pc2 from "picocolors";
|
|
17
|
+
|
|
18
|
+
// src/scope-resolver.ts
|
|
19
|
+
import { resolve } from "path";
|
|
20
|
+
import * as p from "@clack/prompts";
|
|
21
|
+
import { resolveScopeRoots } from "@lexmanh/shed-core";
|
|
16
22
|
import pc from "picocolors";
|
|
23
|
+
async function resolveScanScope(opts = {}) {
|
|
24
|
+
const explicit = opts.path !== void 0 ? resolve(opts.path) : void 0;
|
|
25
|
+
const result = await resolveScopeRoots({ explicit });
|
|
26
|
+
if (result.ok) return { ok: true, roots: result.roots };
|
|
27
|
+
if (result.reason !== "no-default-roots" || opts.nonInteractive) {
|
|
28
|
+
return { ok: false, message: result.message };
|
|
29
|
+
}
|
|
30
|
+
const examples = result.suggestedExamples ?? [];
|
|
31
|
+
p.note(
|
|
32
|
+
[
|
|
33
|
+
"shed could not find a default dev directory under your home folder.",
|
|
34
|
+
"",
|
|
35
|
+
"Pass an explicit path to `shed scan` / `shed clean`, or create one of:",
|
|
36
|
+
...examples.map((e) => ` ${pc.dim(e)}`)
|
|
37
|
+
].join("\n"),
|
|
38
|
+
"No scan target"
|
|
39
|
+
);
|
|
40
|
+
const answer = await p.text({
|
|
41
|
+
message: "Path to scan (leave empty to cancel):",
|
|
42
|
+
placeholder: "~/projects"
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(answer) || typeof answer !== "string" || answer.trim() === "") {
|
|
45
|
+
return { ok: false, message: "Cancelled \u2014 no path provided." };
|
|
46
|
+
}
|
|
47
|
+
const trimmed = answer.trim();
|
|
48
|
+
const expanded = trimmed.startsWith("~") ? trimmed.replace(/^~/, process.env.HOME ?? process.env.USERPROFILE ?? "") : trimmed;
|
|
49
|
+
const second = await resolveScopeRoots({ explicit: resolve(expanded) });
|
|
50
|
+
if (second.ok) return { ok: true, roots: second.roots };
|
|
51
|
+
return { ok: false, message: second.message };
|
|
52
|
+
}
|
|
17
53
|
|
|
18
54
|
// src/verbose.ts
|
|
19
55
|
var _verbose = false;
|
|
@@ -36,40 +72,47 @@ function formatBytes(bytes) {
|
|
|
36
72
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
37
73
|
}
|
|
38
74
|
var RISK_BADGE = {
|
|
39
|
-
[RiskTier.Green]:
|
|
40
|
-
[RiskTier.Yellow]:
|
|
41
|
-
[RiskTier.Red]:
|
|
75
|
+
[RiskTier.Green]: pc2.green("Green "),
|
|
76
|
+
[RiskTier.Yellow]: pc2.yellow("Yellow"),
|
|
77
|
+
[RiskTier.Red]: pc2.red("Red ")
|
|
42
78
|
};
|
|
43
|
-
async function cleanCommand(path
|
|
44
|
-
const rootDir = resolve(path);
|
|
79
|
+
async function cleanCommand(path, options = {}) {
|
|
45
80
|
const isDryRun = !options.execute;
|
|
46
|
-
|
|
81
|
+
p2.intro(pc2.bgYellow(pc2.black(" shed clean ")));
|
|
82
|
+
const scope = await resolveScanScope({ path });
|
|
83
|
+
if (!scope.ok) {
|
|
84
|
+
p2.cancel(scope.message);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
47
87
|
if (isDryRun) {
|
|
48
|
-
|
|
88
|
+
p2.note(
|
|
49
89
|
"DRY-RUN mode \u2014 no files will be deleted.\nPass --execute to perform actual cleanup.",
|
|
50
90
|
"Safe mode"
|
|
51
91
|
);
|
|
52
92
|
}
|
|
53
|
-
const spinner4 =
|
|
54
|
-
verbose(
|
|
55
|
-
|
|
93
|
+
const spinner4 = p2.spinner();
|
|
94
|
+
verbose(
|
|
95
|
+
`clean roots: ${scope.roots.join(", ")}, dryRun=${isDryRun}, hardDelete=${options.hardDelete ?? false}`
|
|
96
|
+
);
|
|
97
|
+
spinner4.start(
|
|
98
|
+
scope.roots.length === 1 ? `Scanning ${scope.roots[0]} \u2026` : `Scanning ${scope.roots.length} roots \u2026`
|
|
99
|
+
);
|
|
56
100
|
const scanner = new Scanner(defaultDetectors());
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
]);
|
|
101
|
+
const projectsByRoot = await Promise.all(scope.roots.map((root) => scanner.scan(root)));
|
|
102
|
+
const projects = projectsByRoot.flat();
|
|
103
|
+
const ctx = { scanRoot: scope.roots[0] ?? "/", maxDepth: 8 };
|
|
104
|
+
const globalItems = await scanner.scanGlobal(ctx);
|
|
62
105
|
const allItems = [
|
|
63
106
|
...projects.flatMap((proj) => proj.items),
|
|
64
107
|
...globalItems
|
|
65
108
|
].filter((i) => options.includeRed || i.risk !== RiskTier.Red);
|
|
66
109
|
verbose(`scan complete: ${allItems.length} cleanable items`);
|
|
67
|
-
spinner4.stop(`Found ${
|
|
110
|
+
spinner4.stop(`Found ${pc2.bold(String(allItems.length))} cleanable items.`);
|
|
68
111
|
if (allItems.length === 0) {
|
|
69
|
-
|
|
112
|
+
p2.outro(pc2.dim("Nothing to clean."));
|
|
70
113
|
return;
|
|
71
114
|
}
|
|
72
|
-
const checker = new SafetyChecker();
|
|
115
|
+
const checker = new SafetyChecker({ oplog: new OpLog() });
|
|
73
116
|
const checkResults = await Promise.all(allItems.map((item) => checker.check(item)));
|
|
74
117
|
const eligibleItems = allItems.filter((_, i) => {
|
|
75
118
|
const result2 = checkResults[i];
|
|
@@ -80,18 +123,18 @@ async function cleanCommand(path = ".", options = {}) {
|
|
|
80
123
|
return !(result2?.allowed ?? false);
|
|
81
124
|
});
|
|
82
125
|
if (blockedItems.length > 0) {
|
|
83
|
-
|
|
126
|
+
p2.note(
|
|
84
127
|
blockedItems.map((item) => {
|
|
85
128
|
const reasons = checkResults[allItems.indexOf(item)]?.reasons ?? [];
|
|
86
129
|
const blockReason = reasons.find((r) => r.severity === "block");
|
|
87
|
-
return `${
|
|
88
|
-
${
|
|
130
|
+
return `${pc2.dim(item.path)}
|
|
131
|
+
${pc2.red("\u2717")} ${blockReason?.message ?? "blocked"}`;
|
|
89
132
|
}).join("\n\n"),
|
|
90
133
|
`${blockedItems.length} item(s) blocked by safety checks`
|
|
91
134
|
);
|
|
92
135
|
}
|
|
93
136
|
if (eligibleItems.length === 0) {
|
|
94
|
-
|
|
137
|
+
p2.outro(pc2.yellow("All items were blocked by safety checks."));
|
|
95
138
|
return;
|
|
96
139
|
}
|
|
97
140
|
let selectedItems = eligibleItems;
|
|
@@ -102,31 +145,31 @@ async function cleanCommand(path = ".", options = {}) {
|
|
|
102
145
|
const greenBytes = greenItems.reduce((s, i) => s + i.sizeBytes, 0);
|
|
103
146
|
const yellowBytes = yellowItems.reduce((s, i) => s + i.sizeBytes, 0);
|
|
104
147
|
const allBytes = eligibleItems.reduce((s, i) => s + i.sizeBytes, 0);
|
|
105
|
-
const preset = await
|
|
148
|
+
const preset = await p2.select({
|
|
106
149
|
message: "What would you like to clean?",
|
|
107
150
|
options: [
|
|
108
151
|
{
|
|
109
152
|
value: "all",
|
|
110
|
-
label: `All ${
|
|
153
|
+
label: `All ${pc2.dim(`${eligibleItems.length} items \xB7 ${formatBytes(allBytes)}`)}`
|
|
111
154
|
},
|
|
112
155
|
...greenItems.length > 0 ? [
|
|
113
156
|
{
|
|
114
157
|
value: "green",
|
|
115
|
-
label: `${
|
|
158
|
+
label: `${pc2.green("Green only")} ${pc2.dim(`${greenItems.length} items \xB7 ${formatBytes(greenBytes)} \xB7 safest`)}`
|
|
116
159
|
}
|
|
117
160
|
] : [],
|
|
118
161
|
...yellowItems.length > 0 ? [
|
|
119
162
|
{
|
|
120
163
|
value: "yellow",
|
|
121
|
-
label: `${
|
|
164
|
+
label: `${pc2.yellow("Yellow only")} ${pc2.dim(`${yellowItems.length} items \xB7 ${formatBytes(yellowBytes)}`)}`
|
|
122
165
|
}
|
|
123
166
|
] : [],
|
|
124
167
|
{ value: "custom", label: "Custom (pick individual items)" },
|
|
125
|
-
{ value: "cancel", label:
|
|
168
|
+
{ value: "cancel", label: pc2.dim("Cancel (do nothing, exit)") }
|
|
126
169
|
]
|
|
127
170
|
});
|
|
128
|
-
if (
|
|
129
|
-
|
|
171
|
+
if (p2.isCancel(preset) || preset === "cancel") {
|
|
172
|
+
p2.cancel("Cleanup cancelled.");
|
|
130
173
|
return;
|
|
131
174
|
}
|
|
132
175
|
if (preset === "all") {
|
|
@@ -141,37 +184,37 @@ async function cleanCommand(path = ".", options = {}) {
|
|
|
141
184
|
const warnings = checkResults[allItems.indexOf(item)]?.reasons.filter(
|
|
142
185
|
(r) => r.severity === "warning"
|
|
143
186
|
) ?? [];
|
|
144
|
-
const warnStr = warnings.length > 0 ?
|
|
187
|
+
const warnStr = warnings.length > 0 ? pc2.yellow(` \u26A0 ${warnings.map((w) => w.message).join("; ")}`) : "";
|
|
145
188
|
return {
|
|
146
189
|
value: item,
|
|
147
|
-
label: `${RISK_BADGE[item.risk]} ${displayPath} ${
|
|
190
|
+
label: `${RISK_BADGE[item.risk]} ${displayPath} ${pc2.dim(formatBytes(item.sizeBytes))}${warnStr}`
|
|
148
191
|
};
|
|
149
192
|
});
|
|
150
|
-
const selection = await
|
|
193
|
+
const selection = await p2.multiselect({
|
|
151
194
|
message: "Select items to clean (space to toggle, enter to confirm):",
|
|
152
195
|
options: choices,
|
|
153
196
|
required: false
|
|
154
197
|
});
|
|
155
|
-
if (
|
|
156
|
-
|
|
198
|
+
if (p2.isCancel(selection)) {
|
|
199
|
+
p2.cancel("Cleanup cancelled.");
|
|
157
200
|
return;
|
|
158
201
|
}
|
|
159
202
|
selectedItems = selection;
|
|
160
203
|
}
|
|
161
204
|
}
|
|
162
205
|
if (selectedItems.length === 0) {
|
|
163
|
-
|
|
206
|
+
p2.outro(pc2.dim("Nothing selected."));
|
|
164
207
|
return;
|
|
165
208
|
}
|
|
166
209
|
const totalBytes = selectedItems.reduce((s, i) => s + i.sizeBytes, 0);
|
|
167
210
|
if (!isDryRun && !options.yes) {
|
|
168
|
-
const action = options.hardDelete ?
|
|
169
|
-
const confirmed = await
|
|
211
|
+
const action = options.hardDelete ? pc2.red("PERMANENTLY DELETE") : "move to Trash";
|
|
212
|
+
const confirmed = await p2.confirm({
|
|
170
213
|
message: `${action} ${selectedItems.length} item(s) (${formatBytes(totalBytes)})?`,
|
|
171
214
|
initialValue: false
|
|
172
215
|
});
|
|
173
|
-
if (
|
|
174
|
-
|
|
216
|
+
if (p2.isCancel(confirmed) || !confirmed) {
|
|
217
|
+
p2.cancel("Cleanup cancelled.");
|
|
175
218
|
return;
|
|
176
219
|
}
|
|
177
220
|
}
|
|
@@ -179,7 +222,7 @@ async function cleanCommand(path = ".", options = {}) {
|
|
|
179
222
|
`executing ${selectedItems.length} items, dryRun=${isDryRun}, hardDelete=${options.hardDelete ?? false}`
|
|
180
223
|
);
|
|
181
224
|
for (const item of selectedItems) verbose(` \u2192 ${item.path}`);
|
|
182
|
-
const execSpinner =
|
|
225
|
+
const execSpinner = p2.spinner();
|
|
183
226
|
execSpinner.start(isDryRun ? "Simulating cleanup \u2026" : "Cleaning up \u2026");
|
|
184
227
|
const result = await checker.execute(selectedItems, {
|
|
185
228
|
dryRun: isDryRun,
|
|
@@ -191,26 +234,31 @@ async function cleanCommand(path = ".", options = {}) {
|
|
|
191
234
|
const verb = isDryRun ? "Would free" : "Freed";
|
|
192
235
|
console.log(
|
|
193
236
|
`
|
|
194
|
-
${
|
|
237
|
+
${pc2.green("\u2713")} ${verb} ${pc2.bold(pc2.green(formatBytes(result.totalBytesFreed)))} across ${result.succeeded.length} item(s).`
|
|
195
238
|
);
|
|
196
239
|
}
|
|
197
240
|
if (result.skipped.length > 0) {
|
|
198
|
-
console.log(` ${
|
|
241
|
+
console.log(` ${pc2.yellow("\u26A0")} ${result.skipped.length} item(s) skipped.`);
|
|
199
242
|
for (const s of result.skipped) {
|
|
200
243
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
201
244
|
const displayPath = home ? s.item.path.replace(home, "~") : s.item.path;
|
|
202
|
-
console.log(` ${
|
|
245
|
+
console.log(` ${pc2.dim(displayPath)}: ${s.reason}`);
|
|
203
246
|
}
|
|
204
247
|
}
|
|
205
248
|
if (result.failed.length > 0) {
|
|
206
|
-
console.log(` ${
|
|
249
|
+
console.log(` ${pc2.red("\u2717")} ${result.failed.length} item(s) failed:`);
|
|
207
250
|
for (const f of result.failed) {
|
|
208
|
-
console.log(` ${
|
|
251
|
+
console.log(` ${pc2.dim(f.item.path)}: ${f.error}`);
|
|
209
252
|
}
|
|
210
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
|
+
}
|
|
211
259
|
console.log();
|
|
212
|
-
const
|
|
213
|
-
|
|
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);
|
|
214
262
|
}
|
|
215
263
|
|
|
216
264
|
// src/commands/completions.ts
|
|
@@ -330,9 +378,9 @@ function completionsCommand(shell) {
|
|
|
330
378
|
}
|
|
331
379
|
|
|
332
380
|
// src/commands/config.ts
|
|
333
|
-
import * as
|
|
381
|
+
import * as p3 from "@clack/prompts";
|
|
334
382
|
import Conf from "conf";
|
|
335
|
-
import
|
|
383
|
+
import pc3 from "picocolors";
|
|
336
384
|
var DEFAULTS = {
|
|
337
385
|
scan: { maxDepth: 8, maxAgeDays: 30 },
|
|
338
386
|
clean: { hardDelete: false },
|
|
@@ -394,7 +442,7 @@ function flatSet(store, key, raw) {
|
|
|
394
442
|
return true;
|
|
395
443
|
}
|
|
396
444
|
async function configCommand(action, key, value) {
|
|
397
|
-
|
|
445
|
+
p3.intro(pc3.bgBlue(pc3.black(" shed config ")));
|
|
398
446
|
const store = getStore();
|
|
399
447
|
switch (action) {
|
|
400
448
|
case "list":
|
|
@@ -402,16 +450,16 @@ async function configCommand(action, key, value) {
|
|
|
402
450
|
const lines = Object.entries(KEY_DEFS).map(([k, def]) => {
|
|
403
451
|
const val = flatGet(store, k);
|
|
404
452
|
const isDefault = String(val) === String(def.get(DEFAULTS));
|
|
405
|
-
const valStr = isDefault ?
|
|
406
|
-
return ` ${k.padEnd(22)} ${valStr}${isDefault ?
|
|
453
|
+
const valStr = isDefault ? pc3.dim(String(val)) : pc3.cyan(String(val));
|
|
454
|
+
return ` ${k.padEnd(22)} ${valStr}${isDefault ? pc3.dim(" (default)") : ""}`;
|
|
407
455
|
});
|
|
408
|
-
|
|
409
|
-
|
|
456
|
+
p3.note(lines.join("\n"), "Current configuration");
|
|
457
|
+
p3.note(pc3.dim(store.path), "Config file");
|
|
410
458
|
break;
|
|
411
459
|
}
|
|
412
460
|
case "get": {
|
|
413
461
|
if (!key || !(key in KEY_DEFS)) {
|
|
414
|
-
|
|
462
|
+
p3.cancel(
|
|
415
463
|
!key ? "Usage: shed config get <key>" : `Unknown key: ${key}
|
|
416
464
|
Valid: ${Object.keys(KEY_DEFS).join(", ")}`
|
|
417
465
|
);
|
|
@@ -422,59 +470,59 @@ Valid: ${Object.keys(KEY_DEFS).join(", ")}`
|
|
|
422
470
|
}
|
|
423
471
|
case "set": {
|
|
424
472
|
if (!key || value === void 0) {
|
|
425
|
-
|
|
473
|
+
p3.cancel("Usage: shed config set <key> <value>");
|
|
426
474
|
process.exit(1);
|
|
427
475
|
}
|
|
428
476
|
if (!(key in KEY_DEFS)) {
|
|
429
|
-
|
|
477
|
+
p3.cancel(`Unknown key: ${key}
|
|
430
478
|
Valid: ${Object.keys(KEY_DEFS).join(", ")}`);
|
|
431
479
|
process.exit(1);
|
|
432
480
|
}
|
|
433
481
|
if (!flatSet(store, key, value)) {
|
|
434
|
-
|
|
482
|
+
p3.cancel(`Invalid value "${value}" for key "${key}"`);
|
|
435
483
|
process.exit(1);
|
|
436
484
|
}
|
|
437
|
-
|
|
485
|
+
p3.outro(`${pc3.cyan(key)} = ${pc3.green(value)}`);
|
|
438
486
|
return;
|
|
439
487
|
}
|
|
440
488
|
case "reset": {
|
|
441
489
|
if (key) {
|
|
442
490
|
if (!(key in KEY_DEFS)) {
|
|
443
|
-
|
|
491
|
+
p3.cancel(`Unknown key: ${key}`);
|
|
444
492
|
process.exit(1);
|
|
445
493
|
}
|
|
446
494
|
const [section] = key.split(".");
|
|
447
495
|
store.set(section, DEFAULTS[section]);
|
|
448
|
-
|
|
496
|
+
p3.outro(`${pc3.cyan(key)} reset to default.`);
|
|
449
497
|
} else {
|
|
450
|
-
const confirmed = await
|
|
498
|
+
const confirmed = await p3.confirm({
|
|
451
499
|
message: "Reset ALL settings to defaults?",
|
|
452
500
|
initialValue: false
|
|
453
501
|
});
|
|
454
|
-
if (
|
|
455
|
-
|
|
502
|
+
if (p3.isCancel(confirmed) || !confirmed) {
|
|
503
|
+
p3.cancel("Cancelled.");
|
|
456
504
|
return;
|
|
457
505
|
}
|
|
458
506
|
store.clear();
|
|
459
|
-
|
|
507
|
+
p3.outro("All settings reset to defaults.");
|
|
460
508
|
}
|
|
461
509
|
return;
|
|
462
510
|
}
|
|
463
511
|
default:
|
|
464
|
-
|
|
512
|
+
p3.cancel(`Unknown action: ${action}
|
|
465
513
|
Usage: shed config [list|get|set|reset]`);
|
|
466
514
|
process.exit(1);
|
|
467
515
|
}
|
|
468
|
-
|
|
516
|
+
p3.outro(pc3.dim(`Edit directly: ${store.path}`));
|
|
469
517
|
}
|
|
470
518
|
|
|
471
519
|
// src/commands/doctor.ts
|
|
472
520
|
import { arch, homedir, platform, release } from "os";
|
|
473
|
-
import * as
|
|
521
|
+
import * as p4 from "@clack/prompts";
|
|
474
522
|
import { execa } from "execa";
|
|
475
|
-
import
|
|
523
|
+
import pc4 from "picocolors";
|
|
476
524
|
async function doctorCommand() {
|
|
477
|
-
|
|
525
|
+
p4.intro(pc4.bgGreen(pc4.black(" shed doctor ")));
|
|
478
526
|
const checks = [];
|
|
479
527
|
checks.push({ name: "OS", value: `${platform()} ${release()} (${arch()})` });
|
|
480
528
|
checks.push({ name: "Home", value: homedir() });
|
|
@@ -485,25 +533,97 @@ async function doctorCommand() {
|
|
|
485
533
|
const { stdout } = await execa(tool, ["--version"], { reject: false });
|
|
486
534
|
checks.push({ name: tool, value: stdout.split("\n")[0] ?? "unknown" });
|
|
487
535
|
} catch {
|
|
488
|
-
checks.push({ name: tool, value:
|
|
536
|
+
checks.push({ name: tool, value: pc4.dim("not installed") });
|
|
489
537
|
}
|
|
490
538
|
}
|
|
491
|
-
const body = checks.map((c) => ` ${
|
|
492
|
-
|
|
493
|
-
|
|
539
|
+
const body = checks.map((c) => ` ${pc4.cyan(c.name.padEnd(10))} ${c.value}`).join("\n");
|
|
540
|
+
p4.note(body, "Environment");
|
|
541
|
+
p4.outro(pc4.green("Environment check complete."));
|
|
542
|
+
}
|
|
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
|
+
);
|
|
494
615
|
}
|
|
495
616
|
|
|
496
617
|
// src/commands/scan.ts
|
|
497
618
|
import { createRequire } from "module";
|
|
498
619
|
import { hostname } from "os";
|
|
499
|
-
import
|
|
500
|
-
import * as p4 from "@clack/prompts";
|
|
620
|
+
import * as p6 from "@clack/prompts";
|
|
501
621
|
import {
|
|
502
622
|
RiskTier as RiskTier2,
|
|
503
623
|
Scanner as Scanner2,
|
|
504
624
|
defaultDetectors as defaultDetectors2
|
|
505
625
|
} from "@lexmanh/shed-core";
|
|
506
|
-
import
|
|
626
|
+
import pc6 from "picocolors";
|
|
507
627
|
|
|
508
628
|
// src/commands/scan-aggregate.ts
|
|
509
629
|
import { dirname } from "path";
|
|
@@ -577,41 +697,52 @@ function selectTopGroups(groups, topN) {
|
|
|
577
697
|
// src/commands/scan.ts
|
|
578
698
|
var require2 = createRequire(import.meta.url);
|
|
579
699
|
var { version: SHED_VERSION } = require2("../package.json");
|
|
580
|
-
var JSON_SCHEMA_VERSION =
|
|
700
|
+
var JSON_SCHEMA_VERSION = 2;
|
|
581
701
|
var COMPACT_TOP_N = 15;
|
|
582
702
|
var DETECTOR_BREAKDOWN_TOP_N = 6;
|
|
583
703
|
var RISK_LABEL = {
|
|
584
|
-
[RiskTier2.Green]:
|
|
585
|
-
[RiskTier2.Yellow]:
|
|
586
|
-
[RiskTier2.Red]:
|
|
704
|
+
[RiskTier2.Green]: pc6.green("\u25CF Green"),
|
|
705
|
+
[RiskTier2.Yellow]: pc6.yellow("\u25CF Yellow"),
|
|
706
|
+
[RiskTier2.Red]: pc6.red("\u25CF Red")
|
|
587
707
|
};
|
|
588
708
|
var RISK_ORDER = {
|
|
589
709
|
[RiskTier2.Red]: 0,
|
|
590
710
|
[RiskTier2.Yellow]: 1,
|
|
591
711
|
[RiskTier2.Green]: 2
|
|
592
712
|
};
|
|
593
|
-
function
|
|
713
|
+
function formatBytes3(bytes) {
|
|
594
714
|
if (bytes === 0) return "0 B";
|
|
595
715
|
if (bytes < 1024) return `${bytes} B`;
|
|
596
716
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
597
717
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
598
718
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
599
719
|
}
|
|
600
|
-
async function scanCommand(path
|
|
601
|
-
const rootDir = resolve2(path);
|
|
720
|
+
async function scanCommand(path, options = {}) {
|
|
602
721
|
if (!options.json) {
|
|
603
|
-
|
|
722
|
+
p6.intro(pc6.bgCyan(pc6.black(" shed scan ")));
|
|
723
|
+
}
|
|
724
|
+
const scope = await resolveScanScope({ path, nonInteractive: options.json });
|
|
725
|
+
if (!scope.ok) {
|
|
726
|
+
if (options.json) {
|
|
727
|
+
console.log(
|
|
728
|
+
JSON.stringify({ schemaVersion: JSON_SCHEMA_VERSION, error: scope.message }, null, 2)
|
|
729
|
+
);
|
|
730
|
+
} else {
|
|
731
|
+
p6.cancel(scope.message);
|
|
732
|
+
}
|
|
733
|
+
return;
|
|
604
734
|
}
|
|
605
|
-
const spinner4 = options.json ? null :
|
|
606
|
-
verbose(`scan
|
|
607
|
-
spinner4?.start(
|
|
735
|
+
const spinner4 = options.json ? null : p6.spinner();
|
|
736
|
+
verbose(`scan roots: ${scope.roots.join(", ")}`);
|
|
737
|
+
spinner4?.start(
|
|
738
|
+
scope.roots.length === 1 ? `Scanning ${scope.roots[0]} \u2026` : `Scanning ${scope.roots.length} roots \u2026`
|
|
739
|
+
);
|
|
608
740
|
const scanStartedAt = Date.now();
|
|
609
741
|
const scanner = new Scanner2(defaultDetectors2());
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
]);
|
|
742
|
+
const projectsByRoot = await Promise.all(scope.roots.map((root) => scanner.scan(root)));
|
|
743
|
+
const projects = projectsByRoot.flat();
|
|
744
|
+
const ctx = { scanRoot: scope.roots[0] ?? "/", maxDepth: 8 };
|
|
745
|
+
const globalItems = await scanner.scanGlobal(ctx);
|
|
615
746
|
const allItems = [
|
|
616
747
|
...projects.flatMap((proj) => proj.items),
|
|
617
748
|
...globalItems
|
|
@@ -623,7 +754,7 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
623
754
|
for (const item of allItems)
|
|
624
755
|
verbose(` item: ${item.risk} ${item.path} (${item.sizeBytes} bytes)`);
|
|
625
756
|
spinner4?.stop(
|
|
626
|
-
`Found ${
|
|
757
|
+
`Found ${pc6.bold(String(allItems.length))} cleanable items across ${projects.length} project(s).`
|
|
627
758
|
);
|
|
628
759
|
if (options.json) {
|
|
629
760
|
const byRisk = { green: 0, yellow: 0, red: 0 };
|
|
@@ -650,7 +781,7 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
650
781
|
arch: process.arch
|
|
651
782
|
},
|
|
652
783
|
scan: {
|
|
653
|
-
|
|
784
|
+
roots: scope.roots,
|
|
654
785
|
durationMs: Date.now() - scanStartedAt
|
|
655
786
|
},
|
|
656
787
|
summary: {
|
|
@@ -669,8 +800,8 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
669
800
|
return;
|
|
670
801
|
}
|
|
671
802
|
if (allItems.length === 0) {
|
|
672
|
-
|
|
673
|
-
|
|
803
|
+
p6.note("Nothing found to clean in this directory.", "Result");
|
|
804
|
+
p6.outro(pc6.dim("All clear!"));
|
|
674
805
|
return;
|
|
675
806
|
}
|
|
676
807
|
if (options.all) {
|
|
@@ -679,8 +810,8 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
679
810
|
renderCompact(allItems);
|
|
680
811
|
}
|
|
681
812
|
console.log();
|
|
682
|
-
|
|
683
|
-
`Total recoverable: ${
|
|
813
|
+
p6.outro(
|
|
814
|
+
`Total recoverable: ${pc6.bold(pc6.green(formatBytes3(totalBytes)))} \u2014 run ${pc6.cyan("shed clean")} to proceed.`
|
|
684
815
|
);
|
|
685
816
|
}
|
|
686
817
|
function renderCompact(allItems) {
|
|
@@ -693,26 +824,26 @@ function renderCompact(allItems) {
|
|
|
693
824
|
if (item.metadata?.detectOnly === true) detectOnly++;
|
|
694
825
|
byDetector.set(item.detector, (byDetector.get(item.detector) ?? 0) + item.sizeBytes);
|
|
695
826
|
}
|
|
696
|
-
const riskLine = `${
|
|
697
|
-
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 ");
|
|
698
829
|
console.log();
|
|
699
830
|
console.log(` By risk: ${riskLine}`);
|
|
700
|
-
console.log(` By detector: ${
|
|
831
|
+
console.log(` By detector: ${pc6.dim(detectorLine)}`);
|
|
701
832
|
const groups = aggregateForDisplay(allItems);
|
|
702
833
|
const { shown, hidden } = selectTopGroups(groups, COMPACT_TOP_N);
|
|
703
834
|
console.log();
|
|
704
|
-
console.log(` ${
|
|
835
|
+
console.log(` ${pc6.bold(`Top ${shown.length} items:`)}`);
|
|
705
836
|
for (const g of shown) {
|
|
706
837
|
const path = home ? g.displayPath.replace(home, "~") : g.displayPath;
|
|
707
|
-
const tag = g.type === "aggregate" ?
|
|
708
|
-
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)}`) : "";
|
|
709
840
|
console.log(` ${RISK_LABEL[g.risk]} ${path}${tag}${size}`);
|
|
710
841
|
}
|
|
711
842
|
if (hidden.groupCount > 0) {
|
|
712
843
|
console.log();
|
|
713
844
|
console.log(
|
|
714
|
-
|
|
715
|
-
` \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`
|
|
716
847
|
)
|
|
717
848
|
);
|
|
718
849
|
}
|
|
@@ -727,66 +858,138 @@ function renderFull(allItems) {
|
|
|
727
858
|
byProject.set(key, group);
|
|
728
859
|
}
|
|
729
860
|
for (const [projectRoot, items] of byProject.entries()) {
|
|
730
|
-
const projectLabel = projectRoot === "(global)" ?
|
|
861
|
+
const projectLabel = projectRoot === "(global)" ? pc6.dim("global caches") : pc6.cyan(home ? projectRoot.replace(home, "~") : projectRoot);
|
|
731
862
|
const groupTotal = items.reduce((s, i) => s + i.sizeBytes, 0);
|
|
732
863
|
console.log(`
|
|
733
|
-
${projectLabel} ${
|
|
864
|
+
${projectLabel} ${pc6.dim(formatBytes3(groupTotal))}`);
|
|
734
865
|
for (const item of items) {
|
|
735
|
-
const size = item.sizeBytes > 0 ?
|
|
866
|
+
const size = item.sizeBytes > 0 ? pc6.dim(` ${formatBytes3(item.sizeBytes)}`) : "";
|
|
736
867
|
const displayPath = home ? item.path.replace(home, "~") : item.path;
|
|
737
868
|
const shortPath = projectRoot !== "(global)" ? displayPath.replace(home ? projectRoot.replace(home, "~") : projectRoot, "").replace(/^\//, "") || displayPath : displayPath;
|
|
738
869
|
console.log(` ${RISK_LABEL[item.risk]} ${shortPath}${size}`);
|
|
739
|
-
console.log(` ${
|
|
870
|
+
console.log(` ${pc6.dim(` ${item.description}`)}`);
|
|
740
871
|
}
|
|
741
872
|
}
|
|
742
873
|
}
|
|
743
874
|
|
|
744
875
|
// src/commands/undo.ts
|
|
745
|
-
import
|
|
746
|
-
import
|
|
747
|
-
import
|
|
748
|
-
function
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
return "Recycle Bin";
|
|
754
|
-
default:
|
|
755
|
-
return "~/.local/share/Trash";
|
|
756
|
-
}
|
|
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`;
|
|
757
884
|
}
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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).`);
|
|
766
949
|
}
|
|
767
950
|
}
|
|
768
|
-
async function
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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] : [];
|
|
784
987
|
}
|
|
785
988
|
|
|
786
989
|
// src/commands/upgrade.ts
|
|
787
|
-
import * as
|
|
990
|
+
import * as p8 from "@clack/prompts";
|
|
788
991
|
import { execa as execa2 } from "execa";
|
|
789
|
-
import
|
|
992
|
+
import pc8 from "picocolors";
|
|
790
993
|
|
|
791
994
|
// src/update/detect-install.ts
|
|
792
995
|
import { realpathSync } from "fs";
|
|
@@ -794,32 +997,32 @@ import { constants, access } from "fs/promises";
|
|
|
794
997
|
import { dirname as dirname2 } from "path";
|
|
795
998
|
var PACKAGE_NAME = "@lexmanh/shed-cli";
|
|
796
999
|
function classifyInstall(resolvedPath) {
|
|
797
|
-
const
|
|
798
|
-
if (
|
|
1000
|
+
const p9 = resolvedPath.replace(/\\/g, "/").toLowerCase();
|
|
1001
|
+
if (p9.includes("/_npx/") || p9.includes("/npx-cache/")) {
|
|
799
1002
|
return {
|
|
800
1003
|
kind: "npx",
|
|
801
1004
|
note: "Running via npx (ephemeral). Re-run with `npx @lexmanh/shed-cli@latest`."
|
|
802
1005
|
};
|
|
803
1006
|
}
|
|
804
|
-
if (
|
|
1007
|
+
if (p9.includes("/bun/install/cache/") || p9.includes("/.bun/install/cache/")) {
|
|
805
1008
|
return {
|
|
806
1009
|
kind: "bunx",
|
|
807
1010
|
note: "Running via bunx (ephemeral). Re-run with `bunx @lexmanh/shed-cli@latest`."
|
|
808
1011
|
};
|
|
809
1012
|
}
|
|
810
|
-
if (
|
|
1013
|
+
if (p9.includes("/.volta/") || p9.includes("/volta/tools/")) {
|
|
811
1014
|
return { kind: "volta" };
|
|
812
1015
|
}
|
|
813
|
-
if (
|
|
1016
|
+
if (p9.includes("/pnpm/global/") || p9.includes("/library/pnpm/") || p9.includes("/.local/share/pnpm/")) {
|
|
814
1017
|
return { kind: "pnpm-global" };
|
|
815
1018
|
}
|
|
816
|
-
if (
|
|
1019
|
+
if (p9.includes("/yarn/global/") || p9.includes("/.config/yarn/global/")) {
|
|
817
1020
|
return { kind: "yarn-global" };
|
|
818
1021
|
}
|
|
819
|
-
if (
|
|
1022
|
+
if (p9.includes("/.bun/install/global/")) {
|
|
820
1023
|
return { kind: "bun-global" };
|
|
821
1024
|
}
|
|
822
|
-
if (
|
|
1025
|
+
if (p9.includes("/node_modules/")) {
|
|
823
1026
|
return { kind: "npm-global" };
|
|
824
1027
|
}
|
|
825
1028
|
return { kind: "unknown" };
|
|
@@ -849,12 +1052,12 @@ function detectInstall(binPath) {
|
|
|
849
1052
|
} catch {
|
|
850
1053
|
resolvedPath = binPath;
|
|
851
1054
|
}
|
|
852
|
-
const { kind, note:
|
|
1055
|
+
const { kind, note: note9 } = classifyInstall(resolvedPath);
|
|
853
1056
|
return {
|
|
854
1057
|
kind,
|
|
855
1058
|
upgradeCommand: buildUpgradeCommand(kind),
|
|
856
1059
|
resolvedPath,
|
|
857
|
-
note:
|
|
1060
|
+
note: note9
|
|
858
1061
|
};
|
|
859
1062
|
}
|
|
860
1063
|
async function needsElevation(resolvedPath) {
|
|
@@ -945,93 +1148,93 @@ function isNewer(latest, current) {
|
|
|
945
1148
|
|
|
946
1149
|
// src/commands/upgrade.ts
|
|
947
1150
|
async function upgradeCommand(opts, currentVersion) {
|
|
948
|
-
|
|
1151
|
+
p8.intro(pc8.bgMagenta(pc8.black(" shed upgrade ")));
|
|
949
1152
|
const install = detectInstall(process.argv[1] ?? "");
|
|
950
|
-
const spin =
|
|
1153
|
+
const spin = p8.spinner();
|
|
951
1154
|
spin.start("Checking npm registry\u2026");
|
|
952
1155
|
const latest = await fetchLatestVersion({ force: true });
|
|
953
1156
|
spin.stop(latest ? `Latest: v${latest}` : "Could not reach registry");
|
|
954
1157
|
if (!latest) {
|
|
955
|
-
|
|
1158
|
+
p8.outro(pc8.yellow("No upgrade information available. Check your network and try again."));
|
|
956
1159
|
process.exit(1);
|
|
957
1160
|
}
|
|
958
1161
|
if (!isNewer(latest, currentVersion)) {
|
|
959
|
-
|
|
960
|
-
`Installed: ${
|
|
961
|
-
Latest: ${
|
|
1162
|
+
p8.note(
|
|
1163
|
+
`Installed: ${pc8.cyan(`v${currentVersion}`)}
|
|
1164
|
+
Latest: ${pc8.cyan(`v${latest}`)}`,
|
|
962
1165
|
"Already up to date"
|
|
963
1166
|
);
|
|
964
|
-
|
|
1167
|
+
p8.outro(pc8.green("Nothing to do."));
|
|
965
1168
|
return;
|
|
966
1169
|
}
|
|
967
|
-
|
|
1170
|
+
p8.note(
|
|
968
1171
|
[
|
|
969
|
-
`Installed: ${
|
|
970
|
-
`Latest: ${
|
|
971
|
-
`Source: ${
|
|
972
|
-
`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)}`
|
|
973
1176
|
].join("\n"),
|
|
974
1177
|
"Upgrade available"
|
|
975
1178
|
);
|
|
976
1179
|
if (!install.upgradeCommand) {
|
|
977
|
-
|
|
1180
|
+
p8.note(
|
|
978
1181
|
install.note ?? "Could not detect how shed was installed.",
|
|
979
|
-
|
|
1182
|
+
pc8.yellow("Cannot self-upgrade")
|
|
980
1183
|
);
|
|
981
|
-
|
|
1184
|
+
p8.outro(pc8.dim("Re-install manually using your preferred package manager."));
|
|
982
1185
|
return;
|
|
983
1186
|
}
|
|
984
1187
|
const elevate = await needsElevation(install.resolvedPath);
|
|
985
1188
|
const finalCommand = elevate ? `sudo ${install.upgradeCommand}` : install.upgradeCommand;
|
|
986
1189
|
if (opts.check) {
|
|
987
|
-
|
|
988
|
-
|
|
1190
|
+
p8.note(finalCommand, "Run this to upgrade");
|
|
1191
|
+
p8.outro(pc8.dim("(--check mode: nothing executed)"));
|
|
989
1192
|
return;
|
|
990
1193
|
}
|
|
991
1194
|
if (elevate) {
|
|
992
|
-
|
|
993
|
-
|
|
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."));
|
|
994
1197
|
return;
|
|
995
1198
|
}
|
|
996
1199
|
if (!opts.yes) {
|
|
997
|
-
const ok = await
|
|
998
|
-
if (
|
|
999
|
-
|
|
1200
|
+
const ok = await p8.confirm({ message: `Run \`${finalCommand}\` now?`, initialValue: true });
|
|
1201
|
+
if (p8.isCancel(ok) || !ok) {
|
|
1202
|
+
p8.cancel("Upgrade cancelled.");
|
|
1000
1203
|
return;
|
|
1001
1204
|
}
|
|
1002
1205
|
}
|
|
1003
|
-
const runSpin =
|
|
1206
|
+
const runSpin = p8.spinner();
|
|
1004
1207
|
runSpin.start(`Running ${finalCommand}\u2026`);
|
|
1005
1208
|
try {
|
|
1006
1209
|
const [bin, ...args] = finalCommand.split(" ");
|
|
1007
1210
|
if (!bin) throw new Error("Empty upgrade command");
|
|
1008
1211
|
await execa2(bin, args, { stdio: "pipe" });
|
|
1009
|
-
runSpin.stop(
|
|
1010
|
-
|
|
1212
|
+
runSpin.stop(pc8.green(`Upgraded to v${latest}.`));
|
|
1213
|
+
p8.outro(pc8.green("Done. Re-run `shed --version` to confirm."));
|
|
1011
1214
|
} catch (err) {
|
|
1012
|
-
runSpin.stop(
|
|
1215
|
+
runSpin.stop(pc8.red("Upgrade failed."));
|
|
1013
1216
|
const message = err instanceof Error ? err.message : String(err);
|
|
1014
|
-
|
|
1015
|
-
|
|
1217
|
+
p8.note(message, pc8.red("Error"));
|
|
1218
|
+
p8.outro(pc8.dim(`You can retry manually: ${finalCommand}`));
|
|
1016
1219
|
process.exit(1);
|
|
1017
1220
|
}
|
|
1018
1221
|
}
|
|
1019
1222
|
|
|
1020
1223
|
// src/update/notifier.ts
|
|
1021
|
-
import
|
|
1224
|
+
import pc9 from "picocolors";
|
|
1022
1225
|
function maybeNotifyOfUpdate(currentVersion) {
|
|
1023
1226
|
const cached = readCachedLatest();
|
|
1024
1227
|
if (!cached || !isNewer(cached, currentVersion)) return;
|
|
1025
1228
|
const install = detectInstall(process.argv[1] ?? "");
|
|
1026
1229
|
const cmd = install.upgradeCommand ?? "shed upgrade";
|
|
1027
1230
|
const banner = [
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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}\`).`)
|
|
1035
1238
|
].join(" ");
|
|
1036
1239
|
console.log(banner);
|
|
1037
1240
|
}
|
|
@@ -1042,7 +1245,7 @@ function scheduleBackgroundRefresh() {
|
|
|
1042
1245
|
}
|
|
1043
1246
|
|
|
1044
1247
|
// src/logo.ts
|
|
1045
|
-
import
|
|
1248
|
+
import pc10 from "picocolors";
|
|
1046
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 "];
|
|
1047
1250
|
var WORDMARK = [
|
|
1048
1251
|
" ____ _ _ ",
|
|
@@ -1053,11 +1256,11 @@ var WORDMARK = [
|
|
|
1053
1256
|
];
|
|
1054
1257
|
function printLogo(version2) {
|
|
1055
1258
|
for (let i = 0; i < CABIN.length; i++) {
|
|
1056
|
-
console.log(
|
|
1259
|
+
console.log(pc10.yellow(CABIN[i]) + pc10.cyan(WORDMARK[i]));
|
|
1057
1260
|
}
|
|
1058
|
-
console.log(` ${
|
|
1261
|
+
console.log(` ${pc10.dim(`v${version2} \xB7 safe disk cleanup \xB7 dev machines & servers`)}`);
|
|
1059
1262
|
console.log(
|
|
1060
|
-
` ${
|
|
1263
|
+
` ${pc10.dim("by")} ${pc10.white("L\xEA Xu\xE2n M\u1EA1nh")} ${pc10.dim("\xB7 https://github.com/lexmanh/shed")}
|
|
1061
1264
|
`
|
|
1062
1265
|
);
|
|
1063
1266
|
}
|
|
@@ -1069,7 +1272,8 @@ var program = new Command();
|
|
|
1069
1272
|
program.name("shed").description("Safe disk cleanup for dev machines and Linux servers").version(version).option("-v, --verbose", "Enable verbose logging");
|
|
1070
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);
|
|
1071
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);
|
|
1072
|
-
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);
|
|
1073
1277
|
program.command("doctor").description("Check environment and configuration").action(doctorCommand);
|
|
1074
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);
|
|
1075
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",
|