@lexmanh/shed-cli 0.2.0-beta.1 → 0.2.0-beta.10
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 +570 -87
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/clean.ts
|
|
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
|
-
|
|
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
|
|
53
|
+
const spinner4 = p.spinner();
|
|
62
54
|
verbose(`clean root: ${rootDir}, dryRun=${isDryRun}, hardDelete=${options.hardDelete ?? false}`);
|
|
63
|
-
|
|
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
|
-
|
|
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,124 @@ async function cleanCommand(path = ".", options = {}) {
|
|
|
226
209
|
}
|
|
227
210
|
}
|
|
228
211
|
console.log();
|
|
229
|
-
const
|
|
230
|
-
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
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/commands/completions.ts
|
|
217
|
+
var BASH = `# shed bash completion
|
|
218
|
+
_shed_completions() {
|
|
219
|
+
local cur prev cmds
|
|
220
|
+
COMPREPLY=()
|
|
221
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
222
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
223
|
+
cmds="scan clean undo doctor config completions"
|
|
224
|
+
|
|
225
|
+
case "\${prev}" in
|
|
226
|
+
scan)
|
|
227
|
+
COMPREPLY=( $(compgen -W "--json --max-age --all" -- "\${cur}") )
|
|
228
|
+
return 0
|
|
229
|
+
;;
|
|
230
|
+
clean)
|
|
231
|
+
COMPREPLY=( $(compgen -W "--dry-run --execute --hard-delete --include-red --yes" -- "\${cur}") )
|
|
232
|
+
return 0
|
|
233
|
+
;;
|
|
234
|
+
config)
|
|
235
|
+
COMPREPLY=( $(compgen -W "get set list reset" -- "\${cur}") )
|
|
236
|
+
return 0
|
|
237
|
+
;;
|
|
238
|
+
completions)
|
|
239
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
240
|
+
return 0
|
|
241
|
+
;;
|
|
242
|
+
esac
|
|
243
|
+
|
|
244
|
+
if [ "\${COMP_CWORD}" -eq 1 ]; then
|
|
245
|
+
COMPREPLY=( $(compgen -W "\${cmds} --version --help --verbose" -- "\${cur}") )
|
|
246
|
+
return 0
|
|
247
|
+
fi
|
|
248
|
+
}
|
|
249
|
+
complete -F _shed_completions shed
|
|
250
|
+
`;
|
|
251
|
+
var ZSH = `#compdef shed
|
|
252
|
+
# shed zsh completion
|
|
253
|
+
|
|
254
|
+
_shed() {
|
|
255
|
+
local -a commands
|
|
256
|
+
commands=(
|
|
257
|
+
'scan:Scan for cleanable items without modifying anything'
|
|
258
|
+
'clean:Interactive cleanup of detected items'
|
|
259
|
+
'undo:List and restore items from previous cleanups'
|
|
260
|
+
'doctor:Check environment and configuration'
|
|
261
|
+
'config:Manage user preferences'
|
|
262
|
+
'completions:Print shell completion script (bash | zsh | fish)'
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if (( CURRENT == 2 )); then
|
|
266
|
+
_describe -t commands 'shed command' commands
|
|
267
|
+
return
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
case "\${words[2]}" in
|
|
271
|
+
scan)
|
|
272
|
+
_arguments \\
|
|
273
|
+
'--json[Output machine-readable JSON]' \\
|
|
274
|
+
'--max-age[Only include items older than N days]:days' \\
|
|
275
|
+
'--all[Show every item (default: compact summary)]'
|
|
276
|
+
;;
|
|
277
|
+
clean)
|
|
278
|
+
_arguments \\
|
|
279
|
+
'--dry-run[Preview operations without executing]' \\
|
|
280
|
+
'--execute[Actually perform the cleanup]' \\
|
|
281
|
+
'--hard-delete[Skip Trash, delete permanently]' \\
|
|
282
|
+
'--include-red[Include Red-tier (high-risk) items]' \\
|
|
283
|
+
'--yes[Skip interactive confirmations (CI mode)]'
|
|
284
|
+
;;
|
|
285
|
+
config)
|
|
286
|
+
_values 'config action' get set list reset
|
|
287
|
+
;;
|
|
288
|
+
completions)
|
|
289
|
+
_values 'shell' bash zsh fish
|
|
290
|
+
;;
|
|
291
|
+
esac
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_shed "$@"
|
|
295
|
+
`;
|
|
296
|
+
var FISH = `# shed fish completion
|
|
297
|
+
complete -c shed -f
|
|
298
|
+
|
|
299
|
+
# subcommands
|
|
300
|
+
complete -c shed -n '__fish_use_subcommand' -a 'scan' -d 'Scan for cleanable items'
|
|
301
|
+
complete -c shed -n '__fish_use_subcommand' -a 'clean' -d 'Interactive cleanup'
|
|
302
|
+
complete -c shed -n '__fish_use_subcommand' -a 'undo' -d 'Restore from previous cleanups'
|
|
303
|
+
complete -c shed -n '__fish_use_subcommand' -a 'doctor' -d 'Check environment'
|
|
304
|
+
complete -c shed -n '__fish_use_subcommand' -a 'config' -d 'Manage preferences'
|
|
305
|
+
complete -c shed -n '__fish_use_subcommand' -a 'completions' -d 'Print shell completion script'
|
|
306
|
+
|
|
307
|
+
# scan flags
|
|
308
|
+
complete -c shed -n '__fish_seen_subcommand_from scan' -l json -d 'Output JSON'
|
|
309
|
+
complete -c shed -n '__fish_seen_subcommand_from scan' -l max-age -d 'Min age in days'
|
|
310
|
+
complete -c shed -n '__fish_seen_subcommand_from scan' -l all -d 'Show every item'
|
|
311
|
+
|
|
312
|
+
# clean flags
|
|
313
|
+
complete -c shed -n '__fish_seen_subcommand_from clean' -l dry-run -d 'Preview only'
|
|
314
|
+
complete -c shed -n '__fish_seen_subcommand_from clean' -l execute -d 'Actually delete'
|
|
315
|
+
complete -c shed -n '__fish_seen_subcommand_from clean' -l hard-delete -d 'Skip Trash'
|
|
316
|
+
complete -c shed -n '__fish_seen_subcommand_from clean' -l include-red -d 'Include Red tier'
|
|
317
|
+
complete -c shed -n '__fish_seen_subcommand_from clean' -l yes -d 'Skip confirmations'
|
|
318
|
+
|
|
319
|
+
# config + completions argument values
|
|
320
|
+
complete -c shed -n '__fish_seen_subcommand_from config' -a 'get set list reset'
|
|
321
|
+
complete -c shed -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'
|
|
322
|
+
`;
|
|
323
|
+
var SCRIPTS = { bash: BASH, zsh: ZSH, fish: FISH };
|
|
324
|
+
function completionsCommand(shell) {
|
|
325
|
+
if (shell !== "bash" && shell !== "zsh" && shell !== "fish") {
|
|
326
|
+
console.error("shed completions: shell must be one of: bash, zsh, fish");
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
process.stdout.write(SCRIPTS[shell]);
|
|
231
330
|
}
|
|
232
331
|
|
|
233
332
|
// src/commands/config.ts
|
|
@@ -395,30 +494,92 @@ async function doctorCommand() {
|
|
|
395
494
|
}
|
|
396
495
|
|
|
397
496
|
// src/commands/scan.ts
|
|
497
|
+
import { createRequire } from "module";
|
|
498
|
+
import { hostname } from "os";
|
|
398
499
|
import { resolve as resolve2 } from "path";
|
|
399
500
|
import * as p4 from "@clack/prompts";
|
|
400
501
|
import {
|
|
401
|
-
AndroidDetector as AndroidDetector2,
|
|
402
|
-
CocoaPodsDetector as CocoaPodsDetector2,
|
|
403
|
-
DatabaseDetector,
|
|
404
|
-
DockerDetector as DockerDetector2,
|
|
405
|
-
DotnetDetector,
|
|
406
|
-
FlutterDetector as FlutterDetector2,
|
|
407
|
-
GoDetector,
|
|
408
|
-
IdeDetector as IdeDetector2,
|
|
409
|
-
JavaGradleDetector,
|
|
410
|
-
JavaMavenDetector,
|
|
411
|
-
NodeDetector as NodeDetector2,
|
|
412
|
-
PythonDetector as PythonDetector2,
|
|
413
502
|
RiskTier as RiskTier2,
|
|
414
|
-
RubyDetector,
|
|
415
|
-
RustDetector as RustDetector2,
|
|
416
503
|
Scanner as Scanner2,
|
|
417
|
-
|
|
418
|
-
WebserverDetector,
|
|
419
|
-
XcodeDetector as XcodeDetector2
|
|
504
|
+
defaultDetectors as defaultDetectors2
|
|
420
505
|
} from "@lexmanh/shed-core";
|
|
421
506
|
import pc4 from "picocolors";
|
|
507
|
+
|
|
508
|
+
// src/commands/scan-aggregate.ts
|
|
509
|
+
import { dirname } from "path";
|
|
510
|
+
var AGGREGATE_THRESHOLD = 3;
|
|
511
|
+
function aggregateForDisplay(items) {
|
|
512
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
513
|
+
const singles = [];
|
|
514
|
+
for (const item of items) {
|
|
515
|
+
const kind = item.metadata?.kind ?? null;
|
|
516
|
+
if (!kind) {
|
|
517
|
+
singles.push(item);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const key = `${dirname(item.path)}::${item.detector}::${kind}`;
|
|
521
|
+
const arr = buckets.get(key) ?? [];
|
|
522
|
+
arr.push(item);
|
|
523
|
+
buckets.set(key, arr);
|
|
524
|
+
}
|
|
525
|
+
const result = [];
|
|
526
|
+
for (const arr of buckets.values()) {
|
|
527
|
+
if (arr.length >= AGGREGATE_THRESHOLD) {
|
|
528
|
+
const first = arr[0];
|
|
529
|
+
if (!first) continue;
|
|
530
|
+
const totalBytes = arr.reduce((s, i) => s + i.sizeBytes, 0);
|
|
531
|
+
const kind = first.metadata?.kind;
|
|
532
|
+
const parentDir = dirname(first.path);
|
|
533
|
+
const displayPath = parentDir === "." ? kind : parentDir;
|
|
534
|
+
result.push({
|
|
535
|
+
type: "aggregate",
|
|
536
|
+
risk: first.risk,
|
|
537
|
+
displayPath,
|
|
538
|
+
description: `${arr.length} ${kind} files`,
|
|
539
|
+
totalBytes,
|
|
540
|
+
detector: first.detector,
|
|
541
|
+
itemCount: arr.length,
|
|
542
|
+
items: arr
|
|
543
|
+
});
|
|
544
|
+
} else {
|
|
545
|
+
for (const item of arr) result.push(toSingle(item));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
for (const item of singles) result.push(toSingle(item));
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
function toSingle(item) {
|
|
552
|
+
return {
|
|
553
|
+
type: "single",
|
|
554
|
+
risk: item.risk,
|
|
555
|
+
displayPath: item.path,
|
|
556
|
+
description: item.description,
|
|
557
|
+
totalBytes: item.sizeBytes,
|
|
558
|
+
detector: item.detector,
|
|
559
|
+
itemCount: 1,
|
|
560
|
+
items: [item]
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function selectTopGroups(groups, topN) {
|
|
564
|
+
const sorted = [...groups].sort((a, b) => b.totalBytes - a.totalBytes);
|
|
565
|
+
const shown = sorted.slice(0, topN);
|
|
566
|
+
const rest = sorted.slice(topN);
|
|
567
|
+
return {
|
|
568
|
+
shown,
|
|
569
|
+
hidden: {
|
|
570
|
+
groupCount: rest.length,
|
|
571
|
+
itemCount: rest.reduce((s, g) => s + g.itemCount, 0),
|
|
572
|
+
totalBytes: rest.reduce((s, g) => s + g.totalBytes, 0)
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/commands/scan.ts
|
|
578
|
+
var require2 = createRequire(import.meta.url);
|
|
579
|
+
var { version: SHED_VERSION } = require2("../package.json");
|
|
580
|
+
var JSON_SCHEMA_VERSION = 1;
|
|
581
|
+
var COMPACT_TOP_N = 15;
|
|
582
|
+
var DETECTOR_BREAKDOWN_TOP_N = 6;
|
|
422
583
|
var RISK_LABEL = {
|
|
423
584
|
[RiskTier2.Green]: pc4.green("\u25CF Green"),
|
|
424
585
|
[RiskTier2.Yellow]: pc4.yellow("\u25CF Yellow"),
|
|
@@ -441,28 +602,11 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
441
602
|
if (!options.json) {
|
|
442
603
|
p4.intro(pc4.bgCyan(pc4.black(" shed scan ")));
|
|
443
604
|
}
|
|
444
|
-
const
|
|
605
|
+
const spinner4 = options.json ? null : p4.spinner();
|
|
445
606
|
verbose(`scan root: ${rootDir}`);
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
new PythonDetector2(),
|
|
450
|
-
new RustDetector2(),
|
|
451
|
-
new GoDetector(),
|
|
452
|
-
new JavaMavenDetector(),
|
|
453
|
-
new JavaGradleDetector(),
|
|
454
|
-
new RubyDetector(),
|
|
455
|
-
new DotnetDetector(),
|
|
456
|
-
new DockerDetector2(),
|
|
457
|
-
new XcodeDetector2(),
|
|
458
|
-
new FlutterDetector2(),
|
|
459
|
-
new AndroidDetector2(),
|
|
460
|
-
new CocoaPodsDetector2(),
|
|
461
|
-
new IdeDetector2(),
|
|
462
|
-
new SystemDetector(),
|
|
463
|
-
new WebserverDetector(),
|
|
464
|
-
new DatabaseDetector()
|
|
465
|
-
]);
|
|
607
|
+
spinner4?.start(`Scanning ${rootDir} \u2026`);
|
|
608
|
+
const scanStartedAt = Date.now();
|
|
609
|
+
const scanner = new Scanner2(defaultDetectors2());
|
|
466
610
|
const ctx = { scanRoot: rootDir, maxDepth: 8 };
|
|
467
611
|
const [projects, globalItems] = await Promise.all([
|
|
468
612
|
scanner.scan(rootDir),
|
|
@@ -478,17 +622,45 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
478
622
|
);
|
|
479
623
|
for (const item of allItems)
|
|
480
624
|
verbose(` item: ${item.risk} ${item.path} (${item.sizeBytes} bytes)`);
|
|
481
|
-
|
|
625
|
+
spinner4?.stop(
|
|
482
626
|
`Found ${pc4.bold(String(allItems.length))} cleanable items across ${projects.length} project(s).`
|
|
483
627
|
);
|
|
484
628
|
if (options.json) {
|
|
629
|
+
const byRisk = { green: 0, yellow: 0, red: 0 };
|
|
630
|
+
let detectOnly = 0;
|
|
631
|
+
for (const item of allItems) {
|
|
632
|
+
byRisk[item.risk]++;
|
|
633
|
+
if (item.metadata?.detectOnly === true) detectOnly++;
|
|
634
|
+
}
|
|
635
|
+
const projectsOut = projects.map((proj) => ({
|
|
636
|
+
root: proj.root,
|
|
637
|
+
detectors: [...new Set(proj.items.map((i) => i.detector))],
|
|
638
|
+
itemCount: proj.items.length,
|
|
639
|
+
totalBytes: proj.items.reduce((s, i) => s + i.sizeBytes, 0)
|
|
640
|
+
}));
|
|
485
641
|
console.log(
|
|
486
642
|
JSON.stringify(
|
|
487
643
|
{
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
644
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
645
|
+
shedVersion: SHED_VERSION,
|
|
646
|
+
timestamp: new Date(scanStartedAt).toISOString(),
|
|
647
|
+
host: {
|
|
648
|
+
hostname: hostname(),
|
|
649
|
+
platform: process.platform,
|
|
650
|
+
arch: process.arch
|
|
651
|
+
},
|
|
652
|
+
scan: {
|
|
653
|
+
root: rootDir,
|
|
654
|
+
durationMs: Date.now() - scanStartedAt
|
|
655
|
+
},
|
|
656
|
+
summary: {
|
|
657
|
+
totalBytes,
|
|
658
|
+
totalItems: allItems.length,
|
|
659
|
+
byRisk,
|
|
660
|
+
detectOnly
|
|
661
|
+
},
|
|
662
|
+
projects: projectsOut,
|
|
663
|
+
items: allItems
|
|
492
664
|
},
|
|
493
665
|
null,
|
|
494
666
|
2
|
|
@@ -501,6 +673,52 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
501
673
|
p4.outro(pc4.dim("All clear!"));
|
|
502
674
|
return;
|
|
503
675
|
}
|
|
676
|
+
if (options.all) {
|
|
677
|
+
renderFull(allItems);
|
|
678
|
+
} else {
|
|
679
|
+
renderCompact(allItems);
|
|
680
|
+
}
|
|
681
|
+
console.log();
|
|
682
|
+
p4.outro(
|
|
683
|
+
`Total recoverable: ${pc4.bold(pc4.green(formatBytes2(totalBytes)))} \u2014 run ${pc4.cyan("shed clean")} to proceed.`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
function renderCompact(allItems) {
|
|
687
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
688
|
+
const byRisk = { green: 0, yellow: 0, red: 0 };
|
|
689
|
+
let detectOnly = 0;
|
|
690
|
+
const byDetector = /* @__PURE__ */ new Map();
|
|
691
|
+
for (const item of allItems) {
|
|
692
|
+
byRisk[item.risk]++;
|
|
693
|
+
if (item.metadata?.detectOnly === true) detectOnly++;
|
|
694
|
+
byDetector.set(item.detector, (byDetector.get(item.detector) ?? 0) + item.sizeBytes);
|
|
695
|
+
}
|
|
696
|
+
const riskLine = `${pc4.green(`\u25CF ${byRisk.green} Green`)} ${pc4.yellow(`\u25CF ${byRisk.yellow} Yellow`)} ${pc4.red(`\u25CF ${byRisk.red} Red`)}${detectOnly > 0 ? pc4.dim(` (${detectOnly} detect-only)`) : ""}`;
|
|
697
|
+
const detectorLine = [...byDetector.entries()].sort(([, a], [, b]) => b - a).slice(0, DETECTOR_BREAKDOWN_TOP_N).map(([d, b]) => `${d} ${formatBytes2(b)}`).join(" \xB7 ");
|
|
698
|
+
console.log();
|
|
699
|
+
console.log(` By risk: ${riskLine}`);
|
|
700
|
+
console.log(` By detector: ${pc4.dim(detectorLine)}`);
|
|
701
|
+
const groups = aggregateForDisplay(allItems);
|
|
702
|
+
const { shown, hidden } = selectTopGroups(groups, COMPACT_TOP_N);
|
|
703
|
+
console.log();
|
|
704
|
+
console.log(` ${pc4.bold(`Top ${shown.length} items:`)}`);
|
|
705
|
+
for (const g of shown) {
|
|
706
|
+
const path = home ? g.displayPath.replace(home, "~") : g.displayPath;
|
|
707
|
+
const tag = g.type === "aggregate" ? pc4.dim(` (${g.itemCount} ${g.detector} items)`) : "";
|
|
708
|
+
const size = g.totalBytes > 0 ? pc4.dim(` ${formatBytes2(g.totalBytes)}`) : "";
|
|
709
|
+
console.log(` ${RISK_LABEL[g.risk]} ${path}${tag}${size}`);
|
|
710
|
+
}
|
|
711
|
+
if (hidden.groupCount > 0) {
|
|
712
|
+
console.log();
|
|
713
|
+
console.log(
|
|
714
|
+
pc4.dim(
|
|
715
|
+
` \u2026 ${hidden.groupCount} more groups (${hidden.itemCount} items, ${formatBytes2(hidden.totalBytes)}) \u2014 use --all to see everything`
|
|
716
|
+
)
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function renderFull(allItems) {
|
|
721
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
504
722
|
const byProject = /* @__PURE__ */ new Map();
|
|
505
723
|
for (const item of allItems) {
|
|
506
724
|
const key = item.projectRoot ?? "(global)";
|
|
@@ -509,7 +727,6 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
509
727
|
byProject.set(key, group);
|
|
510
728
|
}
|
|
511
729
|
for (const [projectRoot, items] of byProject.entries()) {
|
|
512
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
513
730
|
const projectLabel = projectRoot === "(global)" ? pc4.dim("global caches") : pc4.cyan(home ? projectRoot.replace(home, "~") : projectRoot);
|
|
514
731
|
const groupTotal = items.reduce((s, i) => s + i.sizeBytes, 0);
|
|
515
732
|
console.log(`
|
|
@@ -522,10 +739,6 @@ async function scanCommand(path = ".", options = {}) {
|
|
|
522
739
|
console.log(` ${pc4.dim(` ${item.description}`)}`);
|
|
523
740
|
}
|
|
524
741
|
}
|
|
525
|
-
console.log();
|
|
526
|
-
p4.outro(
|
|
527
|
-
`Total recoverable: ${pc4.bold(pc4.green(formatBytes2(totalBytes)))} \u2014 run ${pc4.cyan("shed clean")} to proceed.`
|
|
528
|
-
);
|
|
529
742
|
}
|
|
530
743
|
|
|
531
744
|
// src/commands/undo.ts
|
|
@@ -570,39 +783,309 @@ async function undoCommand() {
|
|
|
570
783
|
p5.outro(pc5.green("Nothing to do \u2014 restore items via your OS Trash."));
|
|
571
784
|
}
|
|
572
785
|
|
|
573
|
-
// src/
|
|
786
|
+
// src/commands/upgrade.ts
|
|
787
|
+
import * as p6 from "@clack/prompts";
|
|
788
|
+
import { execa as execa2 } from "execa";
|
|
574
789
|
import pc6 from "picocolors";
|
|
575
|
-
|
|
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 = [
|
|
576
1048
|
" ____ _ _ ",
|
|
577
1049
|
" / ___|| |__ ___ __| |",
|
|
578
1050
|
" \\___ \\| '_ \\ / _ \\/ _` |",
|
|
579
1051
|
" ___) | | | | __/ (_| |",
|
|
580
1052
|
" |____/|_| |_|\\___|\\__,_|"
|
|
581
|
-
]
|
|
1053
|
+
];
|
|
582
1054
|
function printLogo(version2) {
|
|
583
|
-
|
|
584
|
-
|
|
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`)}`);
|
|
585
1059
|
console.log(
|
|
586
|
-
` ${
|
|
1060
|
+
` ${pc8.dim("by")} ${pc8.white("L\xEA Xu\xE2n M\u1EA1nh")} ${pc8.dim("\xB7 https://github.com/lexmanh/shed")}
|
|
587
1061
|
`
|
|
588
1062
|
);
|
|
589
1063
|
}
|
|
590
1064
|
|
|
591
1065
|
// src/cli.ts
|
|
592
|
-
var
|
|
593
|
-
var { version } =
|
|
1066
|
+
var require3 = createRequire2(import.meta.url);
|
|
1067
|
+
var { version } = require3("../package.json");
|
|
594
1068
|
var program = new Command();
|
|
595
|
-
program.name("shed").description("Safe
|
|
596
|
-
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").action(scanCommand);
|
|
1069
|
+
program.name("shed").description("Safe disk cleanup for dev machines and Linux servers").version(version).option("-v, --verbose", "Enable verbose logging");
|
|
1070
|
+
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);
|
|
597
1071
|
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);
|
|
598
1072
|
program.command("undo").description("List and restore items from previous cleanups").action(undoCommand);
|
|
599
1073
|
program.command("doctor").description("Check environment and configuration").action(doctorCommand);
|
|
600
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);
|
|
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));
|
|
601
1077
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
602
1078
|
const opts = program.opts();
|
|
603
1079
|
setVerbose(opts.verbose ?? false);
|
|
604
1080
|
const cmdOpts = actionCommand.opts();
|
|
605
|
-
|
|
1081
|
+
const cmdName = actionCommand.name();
|
|
1082
|
+
const isCompletions = cmdName === "completions";
|
|
1083
|
+
const isUpgrade = cmdName === "upgrade";
|
|
1084
|
+
if (!cmdOpts.json && !isCompletions) printLogo(version);
|
|
1085
|
+
if (!cmdOpts.json && !isCompletions && !isUpgrade) {
|
|
1086
|
+
maybeNotifyOfUpdate(version);
|
|
1087
|
+
scheduleBackgroundRefresh();
|
|
1088
|
+
}
|
|
606
1089
|
});
|
|
607
1090
|
program.parseAsync(process.argv).catch((err) => {
|
|
608
1091
|
console.error("shed: fatal error:", err instanceof Error ? err.message : err);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexmanh/shed-cli",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
4
|
-
"description": "Safe
|
|
3
|
+
"version": "0.2.0-beta.10",
|
|
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": {
|
|
7
7
|
"shed": "./dist/cli.js"
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"package.json"
|
|
12
12
|
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/lexmanh/shed.git"
|
|
16
|
+
},
|
|
13
17
|
"publishConfig": {
|
|
14
18
|
"access": "public"
|
|
15
19
|
},
|
|
@@ -19,7 +23,7 @@
|
|
|
19
23
|
"conf": "^13.1.0",
|
|
20
24
|
"execa": "^9.5.0",
|
|
21
25
|
"picocolors": "^1.1.1",
|
|
22
|
-
"@lexmanh/shed-core": "0.2.0-beta.
|
|
26
|
+
"@lexmanh/shed-core": "0.2.0-beta.10"
|
|
23
27
|
},
|
|
24
28
|
"devDependencies": {
|
|
25
29
|
"@types/node": "^22.10.0",
|