sweeply 0.1.0

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/cli.js +463 -0
  4. package/package.json +60 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ángel Iván López Delgado (Fora)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # sweeply
2
+
3
+ [![CI](https://github.com/Foralitos/sweeply/actions/workflows/ci.yml/badge.svg)](https://github.com/Foralitos/sweeply/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/sweeply.svg)](https://www.npmjs.com/package/sweeply)
5
+ [![license](https://img.shields.io/npm/l/sweeply.svg)](LICENSE)
6
+
7
+ TUI para encontrar y borrar el peso muerto de tus proyectos — `node_modules`, `.next`, `dist`, `venv`, `target` y demás carpetas regenerables — con un **semáforo de seguridad git** que te dice de un vistazo qué tan tranquilo puedes estar:
8
+
9
+ - 🟢 **pusheado** — todo commiteado y en el remoto. Borrar aquí es 100% recuperable.
10
+ - 🟡 **cambios sin commitear / sin push / sin remoto** — revisa antes de tocar el proyecto.
11
+ - 🔴 **sin git** — si borras el proyecto completo, no hay vuelta atrás.
12
+
13
+ sweeply **solo borra carpetas regenerables** (las recuperas con `yarn install`, `pip install`, etc.), nunca tu código. Y siempre pide confirmación.
14
+
15
+ ## Uso
16
+
17
+ Requiere **Node 22 o superior**.
18
+
19
+ ```bash
20
+ npx sweeply # escanea tu home
21
+ npx sweeply --dir ~/Sites # escanea una carpeta específica
22
+ npx sweeply -t .cache,tmp # detecta carpetas extra
23
+ ```
24
+
25
+ ### Teclas
26
+
27
+ | Tecla | Acción |
28
+ |---|---|
29
+ | `↑↓` / `j k` | navegar |
30
+ | `espacio` | seleccionar/deseleccionar |
31
+ | `a` | seleccionar todos los 🟢 |
32
+ | `enter` | borrar seleccionados (pide confirmación) |
33
+ | `g` | detalle git del proyecto |
34
+ | `s` | ordenar por tamaño |
35
+ | `q` | salir |
36
+
37
+ ## Qué detecta
38
+
39
+ `node_modules`, `.next`, `.nuxt`, `.turbo`, `dist`, `build`, `out` (si hay `package.json` al lado), `venv`/`.venv` (si hay proyecto Python al lado), `target` (Rust), `Pods` (iOS). La condición de "manifest al lado" evita falsos positivos: una carpeta `dist` suelta sin `package.json` no se toca.
40
+
41
+ ## Desarrollo
42
+
43
+ ```bash
44
+ yarn # instalar deps
45
+ yarn dev # correr desde src con tsx
46
+ yarn build # compilar a dist/ con tsup
47
+ yarn typecheck
48
+ ```
49
+
50
+ ## ¿Por qué no npkill?
51
+
52
+ [npkill](https://npkill.js.org) está muy bien y fue la inspiración. sweeply agrega dos cosas: detección multi-ecosistema (Python, Rust, iOS, builds de Next) y el semáforo git — la respuesta a "¿será seguro borrar esto?" sin tener que abrir el proyecto.
53
+
54
+ ## Contribuir
55
+
56
+ Los PRs son bienvenidos — lee [CONTRIBUTING.md](CONTRIBUTING.md) primero.
57
+ Para vulnerabilidades, usa el [canal privado](SECURITY.md), no un issue público.
58
+
59
+ ## Licencia
60
+
61
+ MIT — ver [LICENSE](LICENSE).
package/dist/cli.js ADDED
@@ -0,0 +1,463 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { render } from "ink";
5
+ import os3 from "os";
6
+
7
+ // src/ui/App.tsx
8
+ import { useEffect, useMemo, useState } from "react";
9
+ import { Box as Box3, Text as Text3, useApp, useInput, useStdout } from "ink";
10
+ import path3 from "path";
11
+ import os2 from "os";
12
+
13
+ // src/core/scanner.ts
14
+ import fs from "fs/promises";
15
+ import path from "path";
16
+ import os from "os";
17
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
18
+ "Library",
19
+ "Applications",
20
+ "Movies",
21
+ "Music",
22
+ "Pictures",
23
+ "Photos",
24
+ "Public",
25
+ ".Trash"
26
+ ]);
27
+ var MAX_DEPTH = 8;
28
+ async function* scan(rootDir, targets) {
29
+ const byName = new Map(targets.map((t) => [t.name, t]));
30
+ const isHome = path.resolve(rootDir) === os.homedir();
31
+ const queue = [
32
+ { dir: path.resolve(rootDir), depth: 0 }
33
+ ];
34
+ while (queue.length > 0) {
35
+ const { dir: dir2, depth } = queue.shift();
36
+ let entries;
37
+ try {
38
+ entries = await fs.readdir(dir2, { withFileTypes: true });
39
+ } catch {
40
+ continue;
41
+ }
42
+ const fileNames = new Set(
43
+ entries.filter((e) => e.isFile()).map((e) => e.name)
44
+ );
45
+ const found = [];
46
+ for (const entry of entries) {
47
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
48
+ const def = byName.get(entry.name);
49
+ if (def) {
50
+ const ok = !def.requiresSibling || def.requiresSibling.some((f) => fileNames.has(f));
51
+ if (ok) {
52
+ found.push({
53
+ path: path.join(dir2, entry.name),
54
+ name: entry.name,
55
+ label: def.label
56
+ });
57
+ continue;
58
+ }
59
+ }
60
+ if (entry.name === ".git" || entry.name.startsWith(".")) continue;
61
+ if (depth === 0 && isHome && SKIP_DIRS.has(entry.name)) continue;
62
+ if (depth < MAX_DEPTH) {
63
+ queue.push({ dir: path.join(dir2, entry.name), depth: depth + 1 });
64
+ }
65
+ }
66
+ if (found.length > 0) {
67
+ yield { path: dir2, name: path.basename(dir2), targets: found };
68
+ }
69
+ }
70
+ }
71
+
72
+ // src/core/size.ts
73
+ import fs2 from "fs/promises";
74
+ import path2 from "path";
75
+ async function dirSize(dir2) {
76
+ let total = 0;
77
+ const stack = [dir2];
78
+ while (stack.length > 0) {
79
+ const current = stack.pop();
80
+ let entries;
81
+ try {
82
+ entries = await fs2.readdir(current, { withFileTypes: true });
83
+ } catch {
84
+ continue;
85
+ }
86
+ for (const entry of entries) {
87
+ const full = path2.join(current, entry.name);
88
+ if (entry.isSymbolicLink()) continue;
89
+ if (entry.isDirectory()) {
90
+ stack.push(full);
91
+ } else if (entry.isFile()) {
92
+ try {
93
+ const st = await fs2.stat(full);
94
+ total += st.blocks > 0 ? st.blocks * 512 : st.size;
95
+ } catch {
96
+ }
97
+ }
98
+ }
99
+ }
100
+ return total;
101
+ }
102
+ function formatSize(bytes) {
103
+ if (bytes === void 0) return "\u2026";
104
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
105
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
106
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
107
+ }
108
+
109
+ // src/core/git.ts
110
+ import { execFile } from "child_process";
111
+ import { promisify } from "util";
112
+ import fs3 from "fs/promises";
113
+ var run = promisify(execFile);
114
+ async function git(cwd, args) {
115
+ const { stdout } = await run("git", args, { cwd, timeout: 1e4 });
116
+ return stdout.trim();
117
+ }
118
+ async function gitStatus(projectDir) {
119
+ let lastActivity = "";
120
+ try {
121
+ const st = await fs3.stat(projectDir);
122
+ lastActivity = st.mtime.toISOString().slice(0, 10);
123
+ } catch {
124
+ }
125
+ try {
126
+ await git(projectDir, ["rev-parse", "--is-inside-work-tree"]);
127
+ } catch {
128
+ return {
129
+ semaphore: "red",
130
+ summary: "sin git",
131
+ detail: "Este directorio no es un repositorio git.\nSi borras el proyecto completo no hay forma de recuperarlo.",
132
+ lastActivity
133
+ };
134
+ }
135
+ try {
136
+ lastActivity = await git(projectDir, ["log", "-1", "--format=%cs"]) || lastActivity;
137
+ } catch {
138
+ }
139
+ let branch = "";
140
+ try {
141
+ branch = await git(projectDir, ["branch", "--show-current"]);
142
+ } catch {
143
+ }
144
+ let dirtyLines = [];
145
+ try {
146
+ const porcelain = await git(projectDir, ["status", "--porcelain"]);
147
+ dirtyLines = porcelain ? porcelain.split("\n") : [];
148
+ } catch {
149
+ dirtyLines = ["(git status fall\xF3)"];
150
+ }
151
+ let ahead = null;
152
+ let hasUpstream = true;
153
+ try {
154
+ ahead = parseInt(await git(projectDir, ["rev-list", "@{u}..HEAD", "--count"]), 10);
155
+ } catch {
156
+ hasUpstream = false;
157
+ }
158
+ const detailParts = [`rama: ${branch || "(detached)"}`];
159
+ if (dirtyLines.length > 0) {
160
+ detailParts.push(
161
+ `${dirtyLines.length} archivo(s) sin commitear:`,
162
+ ...dirtyLines.slice(0, 8).map((l) => ` ${l}`)
163
+ );
164
+ if (dirtyLines.length > 8) detailParts.push(` \u2026 y ${dirtyLines.length - 8} m\xE1s`);
165
+ }
166
+ if (!hasUpstream) detailParts.push("sin rama remota configurada (no hay respaldo en un servidor)");
167
+ else if (ahead && ahead > 0) detailParts.push(`${ahead} commit(s) sin pushear`);
168
+ if (dirtyLines.length > 0) {
169
+ return {
170
+ semaphore: "yellow",
171
+ summary: `${dirtyLines.length} sin commit`,
172
+ detail: detailParts.join("\n"),
173
+ lastActivity
174
+ };
175
+ }
176
+ if (!hasUpstream) {
177
+ return { semaphore: "yellow", summary: "sin remoto", detail: detailParts.join("\n"), lastActivity };
178
+ }
179
+ if (ahead && ahead > 0) {
180
+ return { semaphore: "yellow", summary: `${ahead} sin push`, detail: detailParts.join("\n"), lastActivity };
181
+ }
182
+ return {
183
+ semaphore: "green",
184
+ summary: "pusheado",
185
+ detail: detailParts.concat("Todo commiteado y pusheado. Borrar aqu\xED es 100% recuperable.").join("\n"),
186
+ lastActivity
187
+ };
188
+ }
189
+
190
+ // src/core/remove.ts
191
+ import fs4 from "fs/promises";
192
+ async function removeTargets(targets, onProgress) {
193
+ const removed = [];
194
+ const failed = [];
195
+ let done = 0;
196
+ for (const t of targets) {
197
+ onProgress(done, targets.length, t.path);
198
+ try {
199
+ await fs4.rm(t.path, { recursive: true, force: true });
200
+ removed.push(t.path);
201
+ } catch (err) {
202
+ failed.push({ path: t.path, error: err instanceof Error ? err.message : String(err) });
203
+ }
204
+ done += 1;
205
+ onProgress(done, targets.length, t.path);
206
+ }
207
+ return { removed, failed };
208
+ }
209
+
210
+ // src/ui/ProjectRow.tsx
211
+ import { Box, Text } from "ink";
212
+ import { jsx, jsxs } from "react/jsx-runtime";
213
+ var SEM_COLOR = { green: "green", yellow: "yellow", red: "red" };
214
+ function ProjectRow({ row, isCursor }) {
215
+ const targets = row.project.targets.map((t) => t.name).join(" + ");
216
+ const sem = row.git ? SEM_COLOR[row.git.semaphore] : "gray";
217
+ return /* @__PURE__ */ jsxs(Box, { children: [
218
+ /* @__PURE__ */ jsx(Text, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F " : " " }),
219
+ /* @__PURE__ */ jsx(Text, { color: row.deleted ? "gray" : isCursor ? "cyan" : void 0, children: row.deleted ? "\u2714 " : row.selected ? "\u25A0 " : "\u25A1 " }),
220
+ /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { bold: isCursor, strikethrough: row.deleted, wrap: "truncate", children: row.project.name }) }),
221
+ /* @__PURE__ */ jsx(Box, { width: 9, justifyContent: "flex-end", marginRight: 2, children: /* @__PURE__ */ jsx(Text, { color: row.deleted ? "gray" : "magenta", children: row.deleted ? "liberado" : formatSize(row.size) }) }),
222
+ /* @__PURE__ */ jsx(Box, { width: 16, children: /* @__PURE__ */ jsxs(Text, { color: sem, children: [
223
+ "\u25CF ",
224
+ row.git ? row.git.summary : "\u2026"
225
+ ] }) }),
226
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: row.git?.lastActivity ?? "" }) }),
227
+ /* @__PURE__ */ jsx(Text, { color: "gray", wrap: "truncate", children: targets })
228
+ ] });
229
+ }
230
+
231
+ // src/ui/Footer.tsx
232
+ import { Box as Box2, Text as Text2 } from "ink";
233
+ import { jsxs as jsxs2 } from "react/jsx-runtime";
234
+ function Footer({
235
+ selectedCount,
236
+ selectedSize,
237
+ freedSize,
238
+ sortBySize
239
+ }) {
240
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
241
+ freedSize > 0 && /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
242
+ "\u2714 liberado en esta sesi\xF3n: ",
243
+ formatSize(freedSize)
244
+ ] }),
245
+ /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
246
+ "\u2191\u2193 navegar \xB7 espacio seleccionar \xB7 a seleccionar \u{1F7E2} \xB7 enter borrar",
247
+ selectedCount > 0 && /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
248
+ " (",
249
+ selectedCount,
250
+ " = ",
251
+ formatSize(selectedSize),
252
+ ")"
253
+ ] }),
254
+ " \xB7 g detalle git \xB7 s orden",
255
+ sortBySize ? " (tama\xF1o)" : "",
256
+ " \xB7 q salir"
257
+ ] })
258
+ ] });
259
+ }
260
+
261
+ // src/ui/App.tsx
262
+ import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
263
+ function App({ rootDir, targets }) {
264
+ const { exit } = useApp();
265
+ const { stdout } = useStdout();
266
+ const [rows, setRows] = useState([]);
267
+ const [scanning, setScanning] = useState(true);
268
+ const [cursor, setCursor] = useState(0);
269
+ const [mode, setMode] = useState("list");
270
+ const [showDetail, setShowDetail] = useState(false);
271
+ const [sortBySize, setSortBySize] = useState(false);
272
+ const [freedSize, setFreedSize] = useState(0);
273
+ const [deleteProgress, setDeleteProgress] = useState("");
274
+ const patchRow = (projectPath, patch) => {
275
+ setRows(
276
+ (prev) => prev.map((r) => r.project.path === projectPath ? { ...r, ...patch } : r)
277
+ );
278
+ };
279
+ useEffect(() => {
280
+ let cancelled = false;
281
+ (async () => {
282
+ for await (const project of scan(rootDir, targets)) {
283
+ if (cancelled) return;
284
+ setRows((prev) => [...prev, { project, selected: false, deleted: false }]);
285
+ void Promise.all(project.targets.map((t) => dirSize(t.path))).then((sizes) => {
286
+ if (!cancelled) patchRow(project.path, { size: sizes.reduce((a, b) => a + b, 0) });
287
+ });
288
+ void gitStatus(project.path).then((git2) => {
289
+ if (!cancelled) patchRow(project.path, { git: git2 });
290
+ });
291
+ }
292
+ if (!cancelled) setScanning(false);
293
+ })();
294
+ return () => {
295
+ cancelled = true;
296
+ };
297
+ }, [rootDir, targets]);
298
+ const visible = useMemo(() => {
299
+ const list = [...rows];
300
+ if (sortBySize) list.sort((a, b) => (b.size ?? -1) - (a.size ?? -1));
301
+ return list;
302
+ }, [rows, sortBySize]);
303
+ const current = visible[cursor];
304
+ const selected = rows.filter((r) => r.selected && !r.deleted);
305
+ const selectedSize = selected.reduce((a, r) => a + (r.size ?? 0), 0);
306
+ const doDelete = async () => {
307
+ setMode("deleting");
308
+ const toDelete = rows.filter((r) => r.selected && !r.deleted);
309
+ for (const row of toDelete) {
310
+ const { failed } = await removeTargets(row.project.targets, (done, total) => {
311
+ setDeleteProgress(`${row.project.name}: ${done}/${total}`);
312
+ });
313
+ if (failed.length === 0) {
314
+ setFreedSize((f) => f + (row.size ?? 0));
315
+ patchRow(row.project.path, { deleted: true, selected: false });
316
+ } else {
317
+ setDeleteProgress(`error en ${row.project.name}: ${failed[0].error}`);
318
+ }
319
+ }
320
+ setDeleteProgress("");
321
+ setMode("list");
322
+ };
323
+ useInput((input, key) => {
324
+ if (mode === "deleting") return;
325
+ if (mode === "confirm") {
326
+ if (input === "y" || input === "s") void doDelete();
327
+ else if (input === "n" || key.escape) setMode("list");
328
+ return;
329
+ }
330
+ if (input === "q" || key.ctrl && input === "c") exit();
331
+ else if (key.downArrow || input === "j") setCursor((c) => Math.min(c + 1, visible.length - 1));
332
+ else if (key.upArrow || input === "k") setCursor((c) => Math.max(c - 1, 0));
333
+ else if (input === " " && current && !current.deleted) {
334
+ patchRow(current.project.path, { selected: !current.selected });
335
+ } else if (input === "a") {
336
+ setRows(
337
+ (prev) => prev.map(
338
+ (r) => r.git?.semaphore === "green" && !r.deleted ? { ...r, selected: true } : r
339
+ )
340
+ );
341
+ } else if (input === "g") setShowDetail((d) => !d);
342
+ else if (input === "s") setSortBySize((s) => !s);
343
+ else if (key.return && selected.length > 0) setMode("confirm");
344
+ });
345
+ const listHeight = Math.max(5, (stdout?.rows ?? 24) - 8);
346
+ const offset = Math.max(0, Math.min(cursor - Math.floor(listHeight / 2), visible.length - listHeight));
347
+ const windowRows = visible.slice(offset, offset + listHeight);
348
+ const totalFound = rows.reduce((a, r) => a + (r.size ?? 0), 0);
349
+ const displayDir = path3.resolve(rootDir).replace(os2.homedir(), "~");
350
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
351
+ /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
352
+ /* @__PURE__ */ jsx2(Text3, { bold: true, color: "cyan", children: "sweeply" }),
353
+ /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
354
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
355
+ " \xB7 ",
356
+ displayDir
357
+ ] }),
358
+ /* @__PURE__ */ jsxs3(Text3, { children: [
359
+ " \xB7 ",
360
+ scanning ? "escaneando\u2026" : `${rows.length} proyectos`
361
+ ] }),
362
+ /* @__PURE__ */ jsxs3(Text3, { color: "magenta", children: [
363
+ " \xB7 ",
364
+ formatSize(totalFound),
365
+ " recuperables"
366
+ ] })
367
+ ] })
368
+ ] }),
369
+ rows.length === 0 && !scanning && /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
370
+ "No encontr\xE9 nada que limpiar en ",
371
+ rootDir,
372
+ "."
373
+ ] }),
374
+ windowRows.map((row, i) => /* @__PURE__ */ jsx2(ProjectRow, { row, isCursor: offset + i === cursor }, row.project.path)),
375
+ showDetail && current?.git && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, children: [
376
+ /* @__PURE__ */ jsx2(Text3, { bold: true, children: current.project.path }),
377
+ current.git.detail.split("\n").map((line, i) => /* @__PURE__ */ jsx2(Text3, { color: "gray", children: line }, i))
378
+ ] }),
379
+ mode === "confirm" && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "double", borderColor: "red", paddingX: 1, marginTop: 1, children: [
380
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "red", children: [
381
+ "\xBFBorrar ",
382
+ selected.length,
383
+ " proyecto(s) \u2014 ",
384
+ formatSize(selectedSize),
385
+ "?"
386
+ ] }),
387
+ selected.slice(0, 6).map((r) => /* @__PURE__ */ jsxs3(Text3, { children: [
388
+ " ",
389
+ r.project.name,
390
+ ": ",
391
+ r.project.targets.map((t) => t.name).join(", ")
392
+ ] }, r.project.path)),
393
+ selected.length > 6 && /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
394
+ " \u2026 y ",
395
+ selected.length - 6,
396
+ " m\xE1s"
397
+ ] }),
398
+ /* @__PURE__ */ jsx2(Text3, { color: "gray", children: "Solo se borran las carpetas regenerables, nunca tu c\xF3digo." }),
399
+ /* @__PURE__ */ jsx2(Text3, { bold: true, children: "y = s\xED, borrar \xB7 n = cancelar" })
400
+ ] }),
401
+ mode === "deleting" && /* @__PURE__ */ jsx2(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "yellow", children: [
402
+ "borrando\u2026 ",
403
+ deleteProgress
404
+ ] }) }),
405
+ /* @__PURE__ */ jsx2(
406
+ Footer,
407
+ {
408
+ selectedCount: selected.length,
409
+ selectedSize,
410
+ freedSize,
411
+ sortBySize
412
+ }
413
+ )
414
+ ] });
415
+ }
416
+
417
+ // src/core/targets.ts
418
+ var DEFAULT_TARGETS = [
419
+ { name: "node_modules", requiresSibling: ["package.json"], label: "node" },
420
+ { name: ".next", requiresSibling: ["package.json"], label: "next" },
421
+ { name: ".nuxt", requiresSibling: ["package.json"], label: "nuxt" },
422
+ { name: ".turbo", requiresSibling: ["package.json"], label: "turbo" },
423
+ { name: "dist", requiresSibling: ["package.json"], label: "build" },
424
+ { name: "build", requiresSibling: ["package.json"], label: "build" },
425
+ { name: "out", requiresSibling: ["package.json"], label: "build" },
426
+ { name: "venv", requiresSibling: ["pyproject.toml", "requirements.txt", "setup.py"], label: "python" },
427
+ { name: ".venv", requiresSibling: ["pyproject.toml", "requirements.txt", "setup.py"], label: "python" },
428
+ { name: "target", requiresSibling: ["Cargo.toml"], label: "rust" },
429
+ { name: "Pods", requiresSibling: ["Podfile"], label: "ios" }
430
+ ];
431
+ function buildTargets(extra) {
432
+ const custom = extra.filter((n) => n.trim().length > 0).map((n) => ({ name: n.trim(), label: "custom" }));
433
+ return [...DEFAULT_TARGETS, ...custom];
434
+ }
435
+
436
+ // src/cli.tsx
437
+ import { jsx as jsx3 } from "react/jsx-runtime";
438
+ function parseArgs(argv) {
439
+ let dir2 = os3.homedir();
440
+ let extraTargets2 = [];
441
+ for (let i = 0; i < argv.length; i++) {
442
+ const arg = argv[i];
443
+ if (arg === "--dir" || arg === "-d") dir2 = argv[++i] ?? dir2;
444
+ else if (arg === "--targets" || arg === "-t") extraTargets2 = (argv[++i] ?? "").split(",");
445
+ else if (arg === "--help" || arg === "-h") {
446
+ console.log(`sweeply \u2014 limpia el peso muerto de tus proyectos
447
+
448
+ Uso: sweeply [opciones]
449
+
450
+ -d, --dir <ruta> d\xF3nde buscar (default: tu home)
451
+ -t, --targets <a,b> carpetas extra a detectar, separadas por coma
452
+ -h, --help esta ayuda
453
+
454
+ Dentro de la interfaz: \u2191\u2193 navegar \xB7 espacio seleccionar \xB7 a seleccionar
455
+ todo lo \u{1F7E2} \xB7 enter borrar (pide confirmaci\xF3n) \xB7 g detalle git \xB7 s ordenar
456
+ por tama\xF1o \xB7 q salir`);
457
+ process.exit(0);
458
+ }
459
+ }
460
+ return { dir: dir2, extraTargets: extraTargets2 };
461
+ }
462
+ var { dir, extraTargets } = parseArgs(process.argv.slice(2));
463
+ render(/* @__PURE__ */ jsx3(App, { rootDir: dir, targets: buildTargets(extraTargets) }));
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "sweeply",
3
+ "version": "0.1.0",
4
+ "description": "TUI para encontrar y borrar peso muerto de tus proyectos (node_modules, .next, venv...) con semáforo de seguridad git",
5
+ "license": "MIT",
6
+ "author": "Fora (Ángel Iván López Delgado)",
7
+ "homepage": "https://github.com/Foralitos/sweeply#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Foralitos/sweeply.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Foralitos/sweeply/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "sweeply": "dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "dev": "tsx src/cli.tsx",
32
+ "build": "tsup",
33
+ "typecheck": "tsc --noEmit",
34
+ "prepublishOnly": "yarn typecheck && yarn build"
35
+ },
36
+ "keywords": [
37
+ "cli",
38
+ "tui",
39
+ "node_modules",
40
+ "cleanup",
41
+ "disk-space",
42
+ "npkill",
43
+ "monorepo",
44
+ "ink"
45
+ ],
46
+ "dependencies": {
47
+ "ink": "^7.1.1",
48
+ "react": "^19.2.8"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^26.3.0",
52
+ "@types/react": "^19.2.18",
53
+ "tsup": "^8.5.1",
54
+ "tsx": "^4.23.12",
55
+ "typescript": "^7.0.2"
56
+ },
57
+ "resolutions": {
58
+ "esbuild": "^0.28.1"
59
+ }
60
+ }