@lexmanh/shed-cli 0.2.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.
Files changed (2) hide show
  1. package/dist/cli.js +610 -0
  2. package/package.json +37 -0
package/dist/cli.js ADDED
@@ -0,0 +1,610 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { createRequire } from "module";
5
+ import { Command } from "commander";
6
+
7
+ // src/commands/clean.ts
8
+ import { resolve } from "path";
9
+ import * as p from "@clack/prompts";
10
+ import {
11
+ AndroidDetector,
12
+ CocoaPodsDetector,
13
+ DockerDetector,
14
+ FlutterDetector,
15
+ IdeDetector,
16
+ NodeDetector,
17
+ PythonDetector,
18
+ RiskTier,
19
+ RustDetector,
20
+ SafetyChecker,
21
+ Scanner,
22
+ XcodeDetector
23
+ } from "@lexmanh/shed-core";
24
+ import pc from "picocolors";
25
+
26
+ // src/verbose.ts
27
+ var _verbose = false;
28
+ function setVerbose(v) {
29
+ _verbose = v;
30
+ }
31
+ function verbose(msg) {
32
+ if (_verbose) {
33
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
34
+ process.stderr.write(`[${ts}] ${msg}
35
+ `);
36
+ }
37
+ }
38
+
39
+ // src/commands/clean.ts
40
+ function formatBytes(bytes) {
41
+ if (bytes === 0) return "0 B";
42
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
43
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
44
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
45
+ }
46
+ var RISK_BADGE = {
47
+ [RiskTier.Green]: pc.green("Green "),
48
+ [RiskTier.Yellow]: pc.yellow("Yellow"),
49
+ [RiskTier.Red]: pc.red("Red ")
50
+ };
51
+ async function cleanCommand(path = ".", options = {}) {
52
+ const rootDir = resolve(path);
53
+ const isDryRun = !options.execute;
54
+ p.intro(pc.bgYellow(pc.black(" shed clean ")));
55
+ if (isDryRun) {
56
+ p.note(
57
+ "DRY-RUN mode \u2014 no files will be deleted.\nPass --execute to perform actual cleanup.",
58
+ "Safe mode"
59
+ );
60
+ }
61
+ const spinner3 = p.spinner();
62
+ verbose(`clean root: ${rootDir}, dryRun=${isDryRun}, hardDelete=${options.hardDelete ?? false}`);
63
+ spinner3.start(`Scanning ${rootDir} \u2026`);
64
+ const scanner = new Scanner([
65
+ new NodeDetector(),
66
+ new PythonDetector(),
67
+ new RustDetector(),
68
+ new DockerDetector(),
69
+ new XcodeDetector(),
70
+ new FlutterDetector(),
71
+ new AndroidDetector(),
72
+ new CocoaPodsDetector(),
73
+ new IdeDetector()
74
+ ]);
75
+ const ctx = { scanRoot: rootDir, maxDepth: 8 };
76
+ const [projects, globalItems] = await Promise.all([
77
+ scanner.scan(rootDir),
78
+ scanner.scanGlobal(ctx)
79
+ ]);
80
+ const allItems = [
81
+ ...projects.flatMap((proj) => proj.items),
82
+ ...globalItems
83
+ ].filter((i) => options.includeRed || i.risk !== RiskTier.Red);
84
+ verbose(`scan complete: ${allItems.length} cleanable items`);
85
+ spinner3.stop(`Found ${pc.bold(String(allItems.length))} cleanable items.`);
86
+ if (allItems.length === 0) {
87
+ p.outro(pc.dim("Nothing to clean."));
88
+ return;
89
+ }
90
+ const checker = new SafetyChecker();
91
+ const checkResults = await Promise.all(allItems.map((item) => checker.check(item)));
92
+ const eligibleItems = allItems.filter((_, i) => {
93
+ const result2 = checkResults[i];
94
+ return result2?.allowed ?? false;
95
+ });
96
+ const blockedItems = allItems.filter((_, i) => {
97
+ const result2 = checkResults[i];
98
+ return !(result2?.allowed ?? false);
99
+ });
100
+ if (blockedItems.length > 0) {
101
+ p.note(
102
+ blockedItems.map((item) => {
103
+ const reasons = checkResults[allItems.indexOf(item)]?.reasons ?? [];
104
+ const blockReason = reasons.find((r) => r.severity === "block");
105
+ return `${pc.dim(item.path)}
106
+ ${pc.red("\u2717")} ${blockReason?.message ?? "blocked"}`;
107
+ }).join("\n\n"),
108
+ `${blockedItems.length} item(s) blocked by safety checks`
109
+ );
110
+ }
111
+ if (eligibleItems.length === 0) {
112
+ p.outro(pc.yellow("All items were blocked by safety checks."));
113
+ return;
114
+ }
115
+ let selectedItems = eligibleItems;
116
+ if (!options.yes) {
117
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
118
+ const greenItems = eligibleItems.filter((i) => i.risk === RiskTier.Green);
119
+ const yellowItems = eligibleItems.filter((i) => i.risk === RiskTier.Yellow);
120
+ const greenBytes = greenItems.reduce((s, i) => s + i.sizeBytes, 0);
121
+ const yellowBytes = yellowItems.reduce((s, i) => s + i.sizeBytes, 0);
122
+ const allBytes = eligibleItems.reduce((s, i) => s + i.sizeBytes, 0);
123
+ const preset = await p.select({
124
+ message: "What would you like to clean?",
125
+ options: [
126
+ {
127
+ value: "all",
128
+ label: `All ${pc.dim(`${eligibleItems.length} items \xB7 ${formatBytes(allBytes)}`)}`
129
+ },
130
+ ...greenItems.length > 0 ? [
131
+ {
132
+ value: "green",
133
+ label: `${pc.green("Green only")} ${pc.dim(`${greenItems.length} items \xB7 ${formatBytes(greenBytes)} \xB7 safest`)}`
134
+ }
135
+ ] : [],
136
+ ...yellowItems.length > 0 ? [
137
+ {
138
+ value: "yellow",
139
+ label: `${pc.yellow("Yellow only")} ${pc.dim(`${yellowItems.length} items \xB7 ${formatBytes(yellowBytes)}`)}`
140
+ }
141
+ ] : [],
142
+ { value: "custom", label: "Custom (pick individual items)" }
143
+ ]
144
+ });
145
+ if (p.isCancel(preset)) {
146
+ p.cancel("Cleanup cancelled.");
147
+ return;
148
+ }
149
+ if (preset === "all") {
150
+ selectedItems = eligibleItems;
151
+ } else if (preset === "green") {
152
+ selectedItems = greenItems;
153
+ } else if (preset === "yellow") {
154
+ selectedItems = yellowItems;
155
+ } else {
156
+ const choices = eligibleItems.map((item) => {
157
+ const displayPath = home ? item.path.replace(home, "~") : item.path;
158
+ const warnings = checkResults[allItems.indexOf(item)]?.reasons.filter(
159
+ (r) => r.severity === "warning"
160
+ ) ?? [];
161
+ const warnStr = warnings.length > 0 ? pc.yellow(` \u26A0 ${warnings.map((w) => w.message).join("; ")}`) : "";
162
+ return {
163
+ value: item,
164
+ label: `${RISK_BADGE[item.risk]} ${displayPath} ${pc.dim(formatBytes(item.sizeBytes))}${warnStr}`
165
+ };
166
+ });
167
+ const selection = await p.multiselect({
168
+ message: "Select items to clean (space to toggle, enter to confirm):",
169
+ options: choices,
170
+ required: false
171
+ });
172
+ if (p.isCancel(selection)) {
173
+ p.cancel("Cleanup cancelled.");
174
+ return;
175
+ }
176
+ selectedItems = selection;
177
+ }
178
+ }
179
+ if (selectedItems.length === 0) {
180
+ p.outro(pc.dim("Nothing selected."));
181
+ return;
182
+ }
183
+ const totalBytes = selectedItems.reduce((s, i) => s + i.sizeBytes, 0);
184
+ if (!isDryRun && !options.yes) {
185
+ const action = options.hardDelete ? pc.red("PERMANENTLY DELETE") : "move to Trash";
186
+ const confirmed = await p.confirm({
187
+ message: `${action} ${selectedItems.length} item(s) (${formatBytes(totalBytes)})?`,
188
+ initialValue: false
189
+ });
190
+ if (p.isCancel(confirmed) || !confirmed) {
191
+ p.cancel("Cleanup cancelled.");
192
+ return;
193
+ }
194
+ }
195
+ verbose(
196
+ `executing ${selectedItems.length} items, dryRun=${isDryRun}, hardDelete=${options.hardDelete ?? false}`
197
+ );
198
+ for (const item of selectedItems) verbose(` \u2192 ${item.path}`);
199
+ const execSpinner = p.spinner();
200
+ execSpinner.start(isDryRun ? "Simulating cleanup \u2026" : "Cleaning up \u2026");
201
+ const result = await checker.execute(selectedItems, {
202
+ dryRun: isDryRun,
203
+ hardDelete: options.hardDelete ?? false,
204
+ includeRed: options.includeRed ?? false
205
+ });
206
+ execSpinner.stop(isDryRun ? "Dry-run complete." : "Cleanup complete.");
207
+ if (result.succeeded.length > 0) {
208
+ const verb = isDryRun ? "Would free" : "Freed";
209
+ console.log(
210
+ `
211
+ ${pc.green("\u2713")} ${verb} ${pc.bold(pc.green(formatBytes(result.totalBytesFreed)))} across ${result.succeeded.length} item(s).`
212
+ );
213
+ }
214
+ if (result.skipped.length > 0) {
215
+ console.log(` ${pc.yellow("\u26A0")} ${result.skipped.length} item(s) skipped.`);
216
+ for (const s of result.skipped) {
217
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
218
+ const displayPath = home ? s.item.path.replace(home, "~") : s.item.path;
219
+ console.log(` ${pc.dim(displayPath)}: ${s.reason}`);
220
+ }
221
+ }
222
+ if (result.failed.length > 0) {
223
+ console.log(` ${pc.red("\u2717")} ${result.failed.length} item(s) failed:`);
224
+ for (const f of result.failed) {
225
+ console.log(` ${pc.dim(f.item.path)}: ${f.error}`);
226
+ }
227
+ }
228
+ console.log();
229
+ const outro6 = isDryRun ? `Dry-run complete. Run with ${pc.cyan("--execute")} to perform actual cleanup.` : result.failed.length > 0 ? `Completed with ${result.failed.length} failure(s).` : "All done!";
230
+ p.outro(outro6);
231
+ }
232
+
233
+ // src/commands/config.ts
234
+ import * as p2 from "@clack/prompts";
235
+ import Conf from "conf";
236
+ import pc2 from "picocolors";
237
+ var DEFAULTS = {
238
+ scan: { maxDepth: 8, maxAgeDays: 30 },
239
+ clean: { hardDelete: false },
240
+ ai: { provider: "anthropic", model: "claude-opus-4-7" }
241
+ };
242
+ var KEY_DEFS = {
243
+ "scan.maxDepth": { get: (s) => s.scan.maxDepth, description: "Max filesystem depth for scan" },
244
+ "scan.maxAgeDays": {
245
+ get: (s) => s.scan.maxAgeDays,
246
+ description: "Min age (days) before item is surfaced"
247
+ },
248
+ "clean.hardDelete": {
249
+ get: (s) => s.clean.hardDelete,
250
+ description: "Permanently delete instead of Trash"
251
+ },
252
+ "ai.provider": {
253
+ get: (s) => s.ai.provider,
254
+ description: "AI provider (anthropic|openai|ollama)"
255
+ },
256
+ "ai.model": { get: (s) => s.ai.model, description: "AI model name" }
257
+ };
258
+ function getStore() {
259
+ return new Conf({ projectName: "shed", defaults: DEFAULTS });
260
+ }
261
+ function flatGet(store, key) {
262
+ const def = KEY_DEFS[key];
263
+ if (!def) return void 0;
264
+ const saved = store.store;
265
+ const merged = {
266
+ scan: { ...DEFAULTS.scan, ...saved.scan ?? {} },
267
+ clean: { ...DEFAULTS.clean, ...saved.clean ?? {} },
268
+ ai: { ...DEFAULTS.ai, ...saved.ai ?? {} }
269
+ };
270
+ return def.get(merged);
271
+ }
272
+ function flatSet(store, key, raw) {
273
+ const parts = key.split(".");
274
+ if (parts.length !== 2) return false;
275
+ const [section, field] = parts;
276
+ const current = store.get(section);
277
+ if (typeof current !== "object" || current === null) return false;
278
+ const existing = current[field];
279
+ let parsed;
280
+ if (typeof existing === "boolean") {
281
+ if (raw === "true") parsed = true;
282
+ else if (raw === "false") parsed = false;
283
+ else return false;
284
+ } else if (typeof existing === "number") {
285
+ const n = Number(raw);
286
+ if (Number.isNaN(n)) return false;
287
+ parsed = n;
288
+ } else {
289
+ parsed = raw;
290
+ }
291
+ store.set(
292
+ section,
293
+ { ...current, [field]: parsed }
294
+ );
295
+ return true;
296
+ }
297
+ async function configCommand(action, key, value) {
298
+ p2.intro(pc2.bgBlue(pc2.black(" shed config ")));
299
+ const store = getStore();
300
+ switch (action) {
301
+ case "list":
302
+ case void 0: {
303
+ const lines = Object.entries(KEY_DEFS).map(([k, def]) => {
304
+ const val = flatGet(store, k);
305
+ const isDefault = String(val) === String(def.get(DEFAULTS));
306
+ const valStr = isDefault ? pc2.dim(String(val)) : pc2.cyan(String(val));
307
+ return ` ${k.padEnd(22)} ${valStr}${isDefault ? pc2.dim(" (default)") : ""}`;
308
+ });
309
+ p2.note(lines.join("\n"), "Current configuration");
310
+ p2.note(pc2.dim(store.path), "Config file");
311
+ break;
312
+ }
313
+ case "get": {
314
+ if (!key || !(key in KEY_DEFS)) {
315
+ p2.cancel(
316
+ !key ? "Usage: shed config get <key>" : `Unknown key: ${key}
317
+ Valid: ${Object.keys(KEY_DEFS).join(", ")}`
318
+ );
319
+ process.exit(1);
320
+ }
321
+ console.log(flatGet(store, key));
322
+ break;
323
+ }
324
+ case "set": {
325
+ if (!key || value === void 0) {
326
+ p2.cancel("Usage: shed config set <key> <value>");
327
+ process.exit(1);
328
+ }
329
+ if (!(key in KEY_DEFS)) {
330
+ p2.cancel(`Unknown key: ${key}
331
+ Valid: ${Object.keys(KEY_DEFS).join(", ")}`);
332
+ process.exit(1);
333
+ }
334
+ if (!flatSet(store, key, value)) {
335
+ p2.cancel(`Invalid value "${value}" for key "${key}"`);
336
+ process.exit(1);
337
+ }
338
+ p2.outro(`${pc2.cyan(key)} = ${pc2.green(value)}`);
339
+ return;
340
+ }
341
+ case "reset": {
342
+ if (key) {
343
+ if (!(key in KEY_DEFS)) {
344
+ p2.cancel(`Unknown key: ${key}`);
345
+ process.exit(1);
346
+ }
347
+ const [section] = key.split(".");
348
+ store.set(section, DEFAULTS[section]);
349
+ p2.outro(`${pc2.cyan(key)} reset to default.`);
350
+ } else {
351
+ const confirmed = await p2.confirm({
352
+ message: "Reset ALL settings to defaults?",
353
+ initialValue: false
354
+ });
355
+ if (p2.isCancel(confirmed) || !confirmed) {
356
+ p2.cancel("Cancelled.");
357
+ return;
358
+ }
359
+ store.clear();
360
+ p2.outro("All settings reset to defaults.");
361
+ }
362
+ return;
363
+ }
364
+ default:
365
+ p2.cancel(`Unknown action: ${action}
366
+ Usage: shed config [list|get|set|reset]`);
367
+ process.exit(1);
368
+ }
369
+ p2.outro(pc2.dim(`Edit directly: ${store.path}`));
370
+ }
371
+
372
+ // src/commands/doctor.ts
373
+ import { arch, homedir, platform, release } from "os";
374
+ import * as p3 from "@clack/prompts";
375
+ import { execa } from "execa";
376
+ import pc3 from "picocolors";
377
+ async function doctorCommand() {
378
+ p3.intro(pc3.bgGreen(pc3.black(" shed doctor ")));
379
+ const checks = [];
380
+ checks.push({ name: "OS", value: `${platform()} ${release()} (${arch()})` });
381
+ checks.push({ name: "Home", value: homedir() });
382
+ checks.push({ name: "Node", value: process.version });
383
+ const tools = ["git", "npm", "pnpm", "yarn", "docker"];
384
+ for (const tool of tools) {
385
+ try {
386
+ const { stdout } = await execa(tool, ["--version"], { reject: false });
387
+ checks.push({ name: tool, value: stdout.split("\n")[0] ?? "unknown" });
388
+ } catch {
389
+ checks.push({ name: tool, value: pc3.dim("not installed") });
390
+ }
391
+ }
392
+ const body = checks.map((c) => ` ${pc3.cyan(c.name.padEnd(10))} ${c.value}`).join("\n");
393
+ p3.note(body, "Environment");
394
+ p3.outro(pc3.green("Environment check complete."));
395
+ }
396
+
397
+ // src/commands/scan.ts
398
+ import { resolve as resolve2 } from "path";
399
+ import * as p4 from "@clack/prompts";
400
+ 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
+ RiskTier as RiskTier2,
414
+ RubyDetector,
415
+ RustDetector as RustDetector2,
416
+ Scanner as Scanner2,
417
+ SystemDetector,
418
+ WebserverDetector,
419
+ XcodeDetector as XcodeDetector2
420
+ } from "@lexmanh/shed-core";
421
+ import pc4 from "picocolors";
422
+ var RISK_LABEL = {
423
+ [RiskTier2.Green]: pc4.green("\u25CF Green"),
424
+ [RiskTier2.Yellow]: pc4.yellow("\u25CF Yellow"),
425
+ [RiskTier2.Red]: pc4.red("\u25CF Red")
426
+ };
427
+ var RISK_ORDER = {
428
+ [RiskTier2.Red]: 0,
429
+ [RiskTier2.Yellow]: 1,
430
+ [RiskTier2.Green]: 2
431
+ };
432
+ function formatBytes2(bytes) {
433
+ if (bytes === 0) return "0 B";
434
+ if (bytes < 1024) return `${bytes} B`;
435
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
436
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
437
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
438
+ }
439
+ async function scanCommand(path = ".", options = {}) {
440
+ const rootDir = resolve2(path);
441
+ if (!options.json) {
442
+ p4.intro(pc4.bgCyan(pc4.black(" shed scan ")));
443
+ }
444
+ const spinner3 = options.json ? null : p4.spinner();
445
+ verbose(`scan root: ${rootDir}`);
446
+ spinner3?.start(`Scanning ${rootDir} \u2026`);
447
+ const scanner = new Scanner2([
448
+ new NodeDetector2(),
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
+ ]);
466
+ const ctx = { scanRoot: rootDir, maxDepth: 8 };
467
+ const [projects, globalItems] = await Promise.all([
468
+ scanner.scan(rootDir),
469
+ scanner.scanGlobal(ctx)
470
+ ]);
471
+ const allItems = [
472
+ ...projects.flatMap((proj) => proj.items),
473
+ ...globalItems
474
+ ].sort((a, b) => RISK_ORDER[a.risk] - RISK_ORDER[b.risk]);
475
+ const totalBytes = allItems.reduce((sum, i) => sum + i.sizeBytes, 0);
476
+ verbose(
477
+ `scan complete: ${projects.length} projects, ${globalItems.length} global items, ${allItems.length} total`
478
+ );
479
+ for (const item of allItems)
480
+ verbose(` item: ${item.risk} ${item.path} (${item.sizeBytes} bytes)`);
481
+ spinner3?.stop(
482
+ `Found ${pc4.bold(String(allItems.length))} cleanable items across ${projects.length} project(s).`
483
+ );
484
+ if (options.json) {
485
+ console.log(
486
+ JSON.stringify(
487
+ {
488
+ root: rootDir,
489
+ projects: projects.length,
490
+ items: allItems,
491
+ totalBytes
492
+ },
493
+ null,
494
+ 2
495
+ )
496
+ );
497
+ return;
498
+ }
499
+ if (allItems.length === 0) {
500
+ p4.note("Nothing found to clean in this directory.", "Result");
501
+ p4.outro(pc4.dim("All clear!"));
502
+ return;
503
+ }
504
+ const byProject = /* @__PURE__ */ new Map();
505
+ for (const item of allItems) {
506
+ const key = item.projectRoot ?? "(global)";
507
+ const group = byProject.get(key) ?? [];
508
+ group.push(item);
509
+ byProject.set(key, group);
510
+ }
511
+ for (const [projectRoot, items] of byProject.entries()) {
512
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
513
+ const projectLabel = projectRoot === "(global)" ? pc4.dim("global caches") : pc4.cyan(home ? projectRoot.replace(home, "~") : projectRoot);
514
+ const groupTotal = items.reduce((s, i) => s + i.sizeBytes, 0);
515
+ console.log(`
516
+ ${projectLabel} ${pc4.dim(formatBytes2(groupTotal))}`);
517
+ for (const item of items) {
518
+ const size = item.sizeBytes > 0 ? pc4.dim(` ${formatBytes2(item.sizeBytes)}`) : "";
519
+ const displayPath = home ? item.path.replace(home, "~") : item.path;
520
+ const shortPath = projectRoot !== "(global)" ? displayPath.replace(home ? projectRoot.replace(home, "~") : projectRoot, "").replace(/^\//, "") || displayPath : displayPath;
521
+ console.log(` ${RISK_LABEL[item.risk]} ${shortPath}${size}`);
522
+ console.log(` ${pc4.dim(` ${item.description}`)}`);
523
+ }
524
+ }
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
+ }
530
+
531
+ // src/commands/undo.ts
532
+ import { platform as platform2 } from "os";
533
+ import * as p5 from "@clack/prompts";
534
+ import pc5 from "picocolors";
535
+ function trashPath() {
536
+ switch (platform2()) {
537
+ case "darwin":
538
+ return "~/.Trash";
539
+ case "win32":
540
+ return "Recycle Bin";
541
+ default:
542
+ return "~/.local/share/Trash";
543
+ }
544
+ }
545
+ function trashOpenHint() {
546
+ switch (platform2()) {
547
+ case "darwin":
548
+ return "open ~/.Trash (or click Trash in Dock \u2192 right-click \u2192 Put Back)";
549
+ case "win32":
550
+ return "Open Recycle Bin on desktop \u2192 right-click item \u2192 Restore";
551
+ default:
552
+ return "nautilus trash:/// or gio trash --list / gio trash --restore";
553
+ }
554
+ }
555
+ async function undoCommand() {
556
+ p5.intro(pc5.bgMagenta(pc5.black(" shed undo ")));
557
+ p5.note(
558
+ [
559
+ "shed clean moves items to your OS Trash by default.",
560
+ "",
561
+ ` Trash location: ${pc5.cyan(trashPath())}`,
562
+ "",
563
+ ` To restore: ${pc5.dim(trashOpenHint())}`,
564
+ "",
565
+ pc5.dim("Tip: shed clean --hard-delete bypasses Trash (no undo possible)."),
566
+ pc5.dim(" shed clean --dry-run to preview before any real deletion.")
567
+ ].join("\n"),
568
+ "How to undo a cleanup"
569
+ );
570
+ p5.outro(pc5.green("Nothing to do \u2014 restore items via your OS Trash."));
571
+ }
572
+
573
+ // src/logo.ts
574
+ import pc6 from "picocolors";
575
+ var ART = [
576
+ " ____ _ _ ",
577
+ " / ___|| |__ ___ __| |",
578
+ " \\___ \\| '_ \\ / _ \\/ _` |",
579
+ " ___) | | | | __/ (_| |",
580
+ " |____/|_| |_|\\___|\\__,_|"
581
+ ].join("\n");
582
+ function printLogo(version2) {
583
+ console.log(pc6.cyan(ART));
584
+ console.log(` ${pc6.dim(`v${version2} \xB7 safe disk cleanup for developers`)}`);
585
+ console.log(
586
+ ` ${pc6.dim("by")} ${pc6.white("L\xEA Xu\xE2n M\u1EA1nh")} ${pc6.dim("\xB7 https://github.com/lexmanh/shed")}
587
+ `
588
+ );
589
+ }
590
+
591
+ // src/cli.ts
592
+ var require2 = createRequire(import.meta.url);
593
+ var { version } = require2("../package.json");
594
+ var program = new Command();
595
+ program.name("shed").description("Safe, cross-platform disk cleanup for developers").version(version).option("-v, --verbose", "Enable verbose logging");
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);
597
+ 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
+ program.command("undo").description("List and restore items from previous cleanups").action(undoCommand);
599
+ program.command("doctor").description("Check environment and configuration").action(doctorCommand);
600
+ 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);
601
+ program.hook("preAction", (_thisCommand, actionCommand) => {
602
+ const opts = program.opts();
603
+ setVerbose(opts.verbose ?? false);
604
+ const cmdOpts = actionCommand.opts();
605
+ if (!cmdOpts.json) printLogo(version);
606
+ });
607
+ program.parseAsync(process.argv).catch((err) => {
608
+ console.error("shed: fatal error:", err instanceof Error ? err.message : err);
609
+ process.exit(1);
610
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@lexmanh/shed-cli",
3
+ "version": "0.2.0-beta.1",
4
+ "description": "Safe, cross-platform disk cleanup CLI for developers — reclaim space from dev caches without breaking active work",
5
+ "type": "module",
6
+ "bin": {
7
+ "shed": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "package.json"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "dependencies": {
17
+ "@clack/prompts": "^0.8.2",
18
+ "commander": "^12.1.0",
19
+ "conf": "^13.1.0",
20
+ "execa": "^9.5.0",
21
+ "picocolors": "^1.1.1",
22
+ "@lexmanh/shed-core": "0.2.0-beta.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.10.0",
26
+ "tsup": "^8.3.0",
27
+ "typescript": "^5.7.0",
28
+ "vitest": "^2.1.0"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup src/cli.ts --format esm --clean",
32
+ "dev": "tsup src/cli.ts --format esm --watch",
33
+ "test": "vitest run --passWithNoTests",
34
+ "test:watch": "vitest",
35
+ "typecheck": "tsc --noEmit"
36
+ }
37
+ }