myspace-cli 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
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.
- {myspace_cli-1.2.0.dist-info → myspace_cli-1.3.0.dist-info}/METADATA +58 -15
- myspace_cli-1.3.0.dist-info/RECORD +6 -0
- space_cli.py +263 -43
- myspace_cli-1.2.0.dist-info/RECORD +0 -6
- {myspace_cli-1.2.0.dist-info → myspace_cli-1.3.0.dist-info}/WHEEL +0 -0
- {myspace_cli-1.2.0.dist-info → myspace_cli-1.3.0.dist-info}/entry_points.txt +0 -0
- {myspace_cli-1.2.0.dist-info → myspace_cli-1.3.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: myspace-cli
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.3.0
|
4
4
|
Summary: A macOS disk space analysis CLI: health, index, app usage, big files.
|
5
5
|
Author-email: Your Name <you@example.com>
|
6
6
|
License: MIT
|
@@ -23,7 +23,7 @@ Requires-Dist: mcp<2,>=1; extra == "mcp"
|
|
23
23
|
|
24
24
|
space-cli是一个开源的Mac OS命令行小工具,用于分析磁盘空间健康度并找出占用空间最大的目录。
|
25
25
|
|
26
|
-
|
26
|
+
本软件采用**最严安全原则**,所有分析操作采用只读模式,未经允许不会尝试改写和破坏用户电脑的任何数据,也不会上传任何数据到外网,严格保护用户的隐私。
|
27
27
|
|
28
28
|
## 功能特性
|
29
29
|
|
@@ -35,13 +35,29 @@ space-cli是一个开源的Mac OS命令行小工具,用于分析磁盘空间
|
|
35
35
|
- 🎯 **灵活配置** - 支持自定义分析路径和显示数量
|
36
36
|
- 🗂️ **索引缓存** - 目录大小结果本地索引缓存(`~/.spacecli/index.json`),支持TTL与重建提示
|
37
37
|
- 🧩 **应用分析** - 汇总 `Applications`、`Library`、`Caches`、`Logs` 等路径估算应用占用,给出卸载建议
|
38
|
+
- 🗑️ **一键删除应用** - 在应用分析列表中输入序号即可一键删除所选应用及其缓存(含二次确认)
|
38
39
|
- 🏠 **用户目录深度分析** - 针对 `~/Library`、`~/Downloads`、`~/Documents` 分别下探并展示Top N目录
|
39
40
|
- 🗄️ **大文件分析** - 扫描并列出指定路径下最大的文件,支持数量和最小体积阈值
|
40
41
|
- ⏱️ **支持MCP调用** - 支持你自己的AI Agent无缝调用磁盘空间信息
|
41
42
|
|
42
43
|
## 安装
|
43
44
|
|
44
|
-
### 方法1
|
45
|
+
### 方法1:通过 pip 安装(推荐)
|
46
|
+
|
47
|
+
```bash
|
48
|
+
python3 -m pip install --upgrade myspace-cli
|
49
|
+
|
50
|
+
# 支持Pip安装
|
51
|
+
pip install myspace-cli
|
52
|
+
|
53
|
+
# 安装完成后直接使用
|
54
|
+
space-cli --help
|
55
|
+
|
56
|
+
# 或以模块方式
|
57
|
+
python3 -m space_cli --help
|
58
|
+
```
|
59
|
+
|
60
|
+
### 方法2:直接使用
|
45
61
|
|
46
62
|
```bash
|
47
63
|
# 克隆或下载项目
|
@@ -55,7 +71,7 @@ chmod +x space_cli.py
|
|
55
71
|
python3 space_cli.py
|
56
72
|
```
|
57
73
|
|
58
|
-
### 方法
|
74
|
+
### 方法3:创建全局命令
|
59
75
|
|
60
76
|
```bash
|
61
77
|
# 复制到系统路径
|
@@ -66,17 +82,7 @@ sudo chmod +x /usr/local/bin/space-cli
|
|
66
82
|
space-cli
|
67
83
|
```
|
68
84
|
|
69
|
-
|
70
|
-
|
71
|
-
```bash
|
72
|
-
python3 -m pip install --upgrade spacecli
|
73
|
-
|
74
|
-
# 直接使用命令
|
75
|
-
space-cli --help
|
76
|
-
|
77
|
-
# 或者作为模块调用
|
78
|
-
python3 -m space_cli --help
|
79
|
-
```
|
85
|
+
注:若你更倾向于使用 PyPI 包名 `spacecli`,也可执行 `python3 -m pip install --upgrade spacecli`,命令入口同为 `space-cli`。
|
80
86
|
|
81
87
|
## 使用方法
|
82
88
|
|
@@ -126,6 +132,8 @@ python3 space_cli.py --no-prompt
|
|
126
132
|
# 分析应用目录占用并给出卸载建议(按应用归并)
|
127
133
|
python3 space_cli.py --apps -n 20
|
128
134
|
|
135
|
+
# 在应用分析输出后,按提示输入序号一键删除应用(会二次确认)
|
136
|
+
# 例如:输入 3 即删除列表中的第3个应用及其相关缓存
|
129
137
|
# 大文件分析(显示前20个,阈值2G)
|
130
138
|
python3 space_cli.py --big-files --big-files-top 20 --big-files-min 2G
|
131
139
|
|
@@ -200,6 +208,29 @@ python3 space_cli.py --big-files --export report.json
|
|
200
208
|
2. /Users/username/Movies/clip.mov -- 大小: 3.1 GB (0.62%)
|
201
209
|
```
|
202
210
|
|
211
|
+
### 应用分析与一键删除
|
212
|
+
```
|
213
|
+
============================================================
|
214
|
+
🧩 应用目录空间分析与卸载建议
|
215
|
+
============================================================
|
216
|
+
1. Docker Desktop -- 占用: 9.1 GB (1.80%) — 建议卸载或清理缓存
|
217
|
+
2. Xcode -- 占用: 6.2 GB (1.23%) — 建议卸载或清理缓存
|
218
|
+
3. WeChat -- 占用: 2.4 GB (0.47%) — 可保留,定期清理缓存
|
219
|
+
|
220
|
+
是否要一键删除某个应用?输入序号或回车跳过: 1
|
221
|
+
确认删除应用及相关缓存: Docker Desktop (约 9.1 GB)?[y/N]: y
|
222
|
+
将尝试删除以下路径:
|
223
|
+
- /Applications/Docker.app
|
224
|
+
- ~/Library/Application Support/Docker
|
225
|
+
- ~/Library/Caches/com.docker.docker
|
226
|
+
...(略)
|
227
|
+
✅ 删除完成,预计释放空间: 8.7 GB
|
228
|
+
```
|
229
|
+
|
230
|
+
说明:
|
231
|
+
- 删除动作包含二次确认,并会列出将删除的路径清单。
|
232
|
+
- 系统级目录可能因权限/SIP 受保护而无法完全删除,此时工具会尽量清理可删部分并给出失败项与原因。
|
233
|
+
|
203
234
|
|
204
235
|
## MCP Server(可选)
|
205
236
|
|
@@ -231,6 +262,7 @@ python3 mcp_server.py
|
|
231
262
|
- 支持中断操作(Ctrl+C)
|
232
263
|
- 内存优化的文件遍历
|
233
264
|
- 单行滚动进度避免输出刷屏
|
265
|
+
- 进度刷新使用 ANSI 清行(\r\033[K),避免长行残留
|
234
266
|
|
235
267
|
## 故障排除
|
236
268
|
|
@@ -244,6 +276,11 @@ sudo python3 space_cli.py
|
|
244
276
|
python3 space_cli.py -p /Users/$(whoami)
|
245
277
|
```
|
246
278
|
|
279
|
+
此外,针对“Operation not permitted”等提示:
|
280
|
+
- 退出相关应用后再试(例如删除 Docker 前先退出 Docker Desktop)。
|
281
|
+
- 在“系统设置 → 隐私与安全性”中为终端授予“完全磁盘访问权限”。
|
282
|
+
- 遇到容器元数据或受 SIP 保护的系统级文件(如 `~/Library/Containers/com.docker.docker/... .plist`),可能无法删除,建议仅清理用户级缓存目录。
|
283
|
+
|
247
284
|
### 性能问题
|
248
285
|
对于大型文件系统,分析可能需要较长时间:
|
249
286
|
- 使用 `--directories-only` 跳过健康检查
|
@@ -282,3 +319,9 @@ MIT License
|
|
282
319
|
- 新增大文件分析 `--big-files`/`--big-files-top`/`--big-files-min`
|
283
320
|
- 导出报告在启用大文件分析时包含 `largest_files`
|
284
321
|
- 单行滚动进度显示
|
322
|
+
|
323
|
+
### v1.2.0
|
324
|
+
- 应用分析支持“按序号一键删除应用”,并显示将删除的路径清单与预计释放空间
|
325
|
+
- 删除过程增加权限修复与降级清理策略(chflags nouchg / chmod 0777 / 逐项清理)
|
326
|
+
- 针对 "Operation not permitted" 增加友好提示(SIP、完全磁盘访问、退出相关应用)
|
327
|
+
- 单行覆盖输出加入 ANSI 清行,避免长行残留
|
@@ -0,0 +1,6 @@
|
|
1
|
+
space_cli.py,sha256=ihUdZL8ojzxfK4Z81F-fJ6V-8Tn8AprP1mUAW2zp3iY,42738
|
2
|
+
myspace_cli-1.3.0.dist-info/METADATA,sha256=_d8mzS3A3a3XDpgzDCbRbXeE8HX9LdDQEy25g0Tek8k,10674
|
3
|
+
myspace_cli-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
4
|
+
myspace_cli-1.3.0.dist-info/entry_points.txt,sha256=sIVEmPf6W8aNa7KiqOwAI6JrsgHlQciO5xH2G29tKPQ,45
|
5
|
+
myspace_cli-1.3.0.dist-info/top_level.txt,sha256=lnUfbQX9h-9ZjecMVCOWucS2kx69FwTndlrb48S20Sc,10
|
6
|
+
myspace_cli-1.3.0.dist-info/RECORD,,
|
space_cli.py
CHANGED
@@ -182,7 +182,7 @@ class SpaceAnalyzer:
|
|
182
182
|
dirpath_display = dirpath[-80:] # 截取最后50个字符
|
183
183
|
if dirpath_display == "":
|
184
184
|
dirpath_display = dirpath
|
185
|
-
sys.stdout.write(f"\r-> 正在读取: \033[36m{dirpath_display}\033[0m")
|
185
|
+
sys.stdout.write(f"\r\033[K-> 正在读取: \033[36m{dirpath_display}\033[0m")
|
186
186
|
sys.stdout.flush()
|
187
187
|
for filename in filenames:
|
188
188
|
filepath = os.path.join(dirpath, filename)
|
@@ -203,7 +203,7 @@ class SpaceAnalyzer:
|
|
203
203
|
if dirpath_display == "":
|
204
204
|
dirpath_display = dirpath
|
205
205
|
# 间隔性进度输出(单行覆盖)
|
206
|
-
sys.stdout.write(f"\r-> 正在读取: \033[36m{dirpath_display}\033[0m 已扫描文件数: \033[32m{scanned}\033[0m")
|
206
|
+
sys.stdout.write(f"\r\033[K-> 正在读取: \033[36m{dirpath_display}\033[0m 已扫描文件数: \033[32m{scanned}\033[0m")
|
207
207
|
sys.stdout.flush()
|
208
208
|
except KeyboardInterrupt:
|
209
209
|
print("\n用户中断扫描,返回当前结果...")
|
@@ -256,7 +256,7 @@ class SpaceAnalyzer:
|
|
256
256
|
if os.path.isdir(item_path):
|
257
257
|
try:
|
258
258
|
# 进度提示:当前正在读取的目录(单行覆盖)
|
259
|
-
sys.stdout.write(f"\r-> 正在读取: \033[36m{item_path}\033[0m")
|
259
|
+
sys.stdout.write(f"\r\033[K-> 正在读取: \033[36m{item_path}\033[0m")
|
260
260
|
sys.stdout.flush()
|
261
261
|
size = self.get_directory_size(item_path)
|
262
262
|
directory_sizes.append((item_path, size))
|
@@ -311,6 +311,200 @@ class SpaceCli:
|
|
311
311
|
os.makedirs(app_cache_dir, exist_ok=True)
|
312
312
|
self.app_index = IndexStore(index_file=os.path.join(app_cache_dir, "apps.json"))
|
313
313
|
|
314
|
+
# —— 应用删除相关 ——
|
315
|
+
def _candidate_app_paths(self, app_name: str) -> List[str]:
|
316
|
+
"""根据应用名推导可能占用空间的相关目录/文件路径列表。"""
|
317
|
+
home = str(Path.home())
|
318
|
+
candidates: List[str] = []
|
319
|
+
possible_bases = [
|
320
|
+
("/Applications", f"{app_name}.app"),
|
321
|
+
(os.path.join(home, "Applications"), f"{app_name}.app"),
|
322
|
+
("/Library/Application Support", app_name),
|
323
|
+
(os.path.join(home, "Library", "Application Support"), app_name),
|
324
|
+
("/Library/Caches", app_name),
|
325
|
+
(os.path.join(home, "Library", "Caches"), app_name),
|
326
|
+
("/Library/Logs", app_name),
|
327
|
+
(os.path.join(home, "Library", "Logs"), app_name),
|
328
|
+
(os.path.join(home, "Library", "Containers"), app_name),
|
329
|
+
]
|
330
|
+
# 直接拼接命中
|
331
|
+
for base, tail in possible_bases:
|
332
|
+
path = os.path.join(base, tail)
|
333
|
+
if os.path.exists(path):
|
334
|
+
candidates.append(path)
|
335
|
+
# 模糊扫描:包含应用名的目录
|
336
|
+
scan_dirs = [
|
337
|
+
"/Applications",
|
338
|
+
os.path.join(home, "Applications"),
|
339
|
+
"/Library/Application Support",
|
340
|
+
os.path.join(home, "Library", "Application Support"),
|
341
|
+
"/Library/Caches",
|
342
|
+
os.path.join(home, "Library", "Caches"),
|
343
|
+
"/Library/Logs",
|
344
|
+
os.path.join(home, "Library", "Logs"),
|
345
|
+
os.path.join(home, "Library", "Containers"),
|
346
|
+
]
|
347
|
+
app_lower = app_name.lower()
|
348
|
+
for base in scan_dirs:
|
349
|
+
if not os.path.exists(base):
|
350
|
+
continue
|
351
|
+
try:
|
352
|
+
for item in os.listdir(base):
|
353
|
+
item_path = os.path.join(base, item)
|
354
|
+
# 只收集目录或 .app 包
|
355
|
+
if not os.path.isdir(item_path):
|
356
|
+
continue
|
357
|
+
name_lower = item.lower()
|
358
|
+
if app_lower in name_lower:
|
359
|
+
candidates.append(item_path)
|
360
|
+
except (PermissionError, OSError):
|
361
|
+
continue
|
362
|
+
# 去重并按路径长度降序(先删更深层,避免空目录残留)
|
363
|
+
uniq: List[str] = []
|
364
|
+
seen = set()
|
365
|
+
for p in sorted(set(candidates), key=lambda x: len(x), reverse=True):
|
366
|
+
if p not in seen:
|
367
|
+
uniq.append(p)
|
368
|
+
seen.add(p)
|
369
|
+
return uniq
|
370
|
+
|
371
|
+
def _delete_paths_and_sum(self, paths: List[str]) -> Tuple[int, List[Tuple[str, str]]]:
|
372
|
+
"""删除给定路径列表,返回释放的总字节数与失败列表(路径, 原因)。"""
|
373
|
+
total_freed = 0
|
374
|
+
failures: List[Tuple[str, str]] = []
|
375
|
+
|
376
|
+
def _try_fix_permissions(path: str) -> None:
|
377
|
+
"""尝试修复权限与不可变标记以便删除。"""
|
378
|
+
try:
|
379
|
+
# 去除不可变标记(普通用户能去除的场景)
|
380
|
+
subprocess.run(["chflags", "-R", "nouchg", path], capture_output=True)
|
381
|
+
except Exception:
|
382
|
+
pass
|
383
|
+
try:
|
384
|
+
os.chmod(path, 0o777)
|
385
|
+
except Exception:
|
386
|
+
pass
|
387
|
+
|
388
|
+
def _onerror(func, path, exc_info):
|
389
|
+
# 当 rmtree 无法删除时,尝试修复权限并重试一次
|
390
|
+
_try_fix_permissions(path)
|
391
|
+
try:
|
392
|
+
func(path)
|
393
|
+
except Exception:
|
394
|
+
# 让上层捕获
|
395
|
+
raise
|
396
|
+
for p in paths:
|
397
|
+
try:
|
398
|
+
size_before = 0
|
399
|
+
try:
|
400
|
+
if os.path.isdir(p):
|
401
|
+
size_before = self.analyzer.get_directory_size(p)
|
402
|
+
elif os.path.isfile(p):
|
403
|
+
size_before = os.path.getsize(p)
|
404
|
+
except Exception:
|
405
|
+
size_before = 0
|
406
|
+
if os.path.isdir(p) and not os.path.islink(p):
|
407
|
+
try:
|
408
|
+
shutil.rmtree(p, ignore_errors=False, onerror=_onerror)
|
409
|
+
except Exception:
|
410
|
+
# 目录删除失败,降级为逐项尝试删除(尽量清理可删部分)
|
411
|
+
for dirpath, dirnames, filenames in os.walk(p, topdown=False):
|
412
|
+
for name in filenames:
|
413
|
+
fpath = os.path.join(dirpath, name)
|
414
|
+
try:
|
415
|
+
_try_fix_permissions(fpath)
|
416
|
+
os.remove(fpath)
|
417
|
+
except Exception:
|
418
|
+
continue
|
419
|
+
for name in dirnames:
|
420
|
+
dpath = os.path.join(dirpath, name)
|
421
|
+
try:
|
422
|
+
_try_fix_permissions(dpath)
|
423
|
+
os.rmdir(dpath)
|
424
|
+
except Exception:
|
425
|
+
continue
|
426
|
+
# 最后尝试删除顶层目录
|
427
|
+
_try_fix_permissions(p)
|
428
|
+
os.rmdir(p)
|
429
|
+
else:
|
430
|
+
os.remove(p)
|
431
|
+
total_freed += size_before
|
432
|
+
except Exception as e:
|
433
|
+
failures.append((p, str(e)))
|
434
|
+
return total_freed, failures
|
435
|
+
|
436
|
+
def _offer_app_delete(self, apps: List[Tuple[str, int]]) -> None:
|
437
|
+
"""在已打印的应用列表后,提供按序号一键删除功能。"""
|
438
|
+
if not sys.stdin.isatty() or getattr(self.args, 'no_prompt', False):
|
439
|
+
return
|
440
|
+
try:
|
441
|
+
ans = input("是否要一键删除某个应用?输入序号或回车跳过: ").strip()
|
442
|
+
except EOFError:
|
443
|
+
ans = ""
|
444
|
+
if not ans:
|
445
|
+
return
|
446
|
+
try:
|
447
|
+
idx = int(ans)
|
448
|
+
except ValueError:
|
449
|
+
print("❌ 无效的输入(应为数字序号)")
|
450
|
+
return
|
451
|
+
if idx < 1 or idx > len(apps):
|
452
|
+
print("❌ 序号超出范围")
|
453
|
+
return
|
454
|
+
app_name, app_size = apps[idx - 1]
|
455
|
+
size_str = self.analyzer.format_bytes(app_size)
|
456
|
+
try:
|
457
|
+
confirm = input(f"确认删除应用及相关缓存: {app_name} (约 {size_str})?[y/N]: ").strip().lower()
|
458
|
+
except EOFError:
|
459
|
+
confirm = ""
|
460
|
+
if confirm not in ("y", "yes"):
|
461
|
+
print("已取消删除")
|
462
|
+
return
|
463
|
+
related_paths = self._candidate_app_paths(app_name)
|
464
|
+
if not related_paths:
|
465
|
+
print("未找到可删除的相关目录/文件")
|
466
|
+
return
|
467
|
+
print("将尝试删除以下路径:")
|
468
|
+
for p in related_paths:
|
469
|
+
print(f" - {p}")
|
470
|
+
try:
|
471
|
+
confirm2 = input("再次确认删除以上路径?[y/N]: ").strip().lower()
|
472
|
+
except EOFError:
|
473
|
+
confirm2 = ""
|
474
|
+
if confirm2 not in ("y", "yes"):
|
475
|
+
print("已取消删除")
|
476
|
+
return
|
477
|
+
freed, failures = self._delete_paths_and_sum(related_paths)
|
478
|
+
print(f"✅ 删除完成,预计释放空间: {self.analyzer.format_bytes(freed)}")
|
479
|
+
if failures:
|
480
|
+
print("以下路径删除失败,可能需要手动或管理员权限:")
|
481
|
+
for p, reason in failures:
|
482
|
+
print(f" - {p} -> {reason}")
|
483
|
+
# 常见提示:Operation not permitted(SIP/容器元数据等)
|
484
|
+
if any("Operation not permitted" in r for _, r in failures):
|
485
|
+
print("提示:部分系统受保护或容器元数据文件无法删除。可尝试:")
|
486
|
+
print(" - 先退出相关应用(如 Docker)再重试")
|
487
|
+
print(" - 给予当前终端“完全磁盘访问权限”(系统设置 → 隐私与安全性)")
|
488
|
+
print(" - 仅删除用户目录下缓存,保留系统级容器元数据")
|
489
|
+
|
490
|
+
# 通用渲染:目录与应用(减少重复)
|
491
|
+
def _render_dirs(self, entries: List[Tuple[str, int]], total_bytes: int) -> None:
|
492
|
+
for i, (dir_path, size) in enumerate(entries, 1):
|
493
|
+
size_str = self.analyzer.format_bytes(size)
|
494
|
+
percentage = (size / total_bytes) * 100 if total_bytes else 0
|
495
|
+
# 1G 以上红色,否则绿色
|
496
|
+
color = "\033[31m" if size >= 1024**3 else "\033[32m"
|
497
|
+
print(f"{i:2d}. \033[36m{dir_path}\033[0m -- 大小: {color}{size_str}\033[0m (\033[33m{percentage:.2f}%\033[0m)")
|
498
|
+
|
499
|
+
def _render_apps(self, entries: List[Tuple[str, int]], disk_total: int) -> None:
|
500
|
+
for i, (app, size) in enumerate(entries, 1):
|
501
|
+
size_str = self.analyzer.format_bytes(size)
|
502
|
+
pct = (size / disk_total) * 100 if disk_total else 0
|
503
|
+
suggestion = "建议卸载或清理缓存" if size >= 5 * 1024**3 else "可保留,定期清理缓存"
|
504
|
+
# 3G 以上红色,否则绿色
|
505
|
+
color = "\033[31m" if size >= 3 * 1024**3 else "\033[32m"
|
506
|
+
print(f"{i:2d}. \033[36m{app}\033[0m -- 占用: {color}{size_str}\033[0m ({pct:.2f}%) — {suggestion}")
|
507
|
+
|
314
508
|
def analyze_app_directories(self, top_n: int = 20,
|
315
509
|
index: IndexStore = None,
|
316
510
|
use_index: bool = True,
|
@@ -369,7 +563,8 @@ class SpaceCli:
|
|
369
563
|
continue
|
370
564
|
key = app_key_from_path(item_path)
|
371
565
|
# 进度提示:当前应用相关目录(单行覆盖)
|
372
|
-
|
566
|
+
item_path = item_path[:100]
|
567
|
+
sys.stdout.write(f"\r\033[K-> 正在读取: \033[36m{item_path}\033[0m")
|
373
568
|
sys.stdout.flush()
|
374
569
|
size = self.analyzer.get_directory_size(item_path)
|
375
570
|
scanned_dirs.append(item_path)
|
@@ -424,32 +619,42 @@ class SpaceCli:
|
|
424
619
|
print("=" * 60)
|
425
620
|
print("📊 占用空间最大的目录")
|
426
621
|
print("=" * 60)
|
427
|
-
|
622
|
+
|
623
|
+
# 若有缓存:直接显示缓存,然后再询问是否重新分析
|
624
|
+
if self.args.use_index:
|
625
|
+
cached = self.index.get(path)
|
626
|
+
if cached and cached.get("entries"):
|
627
|
+
cached_entries = [(e["path"], int(e["size"])) for e in cached["entries"]][:top_n]
|
628
|
+
total_info = self.analyzer.get_disk_usage(path)
|
629
|
+
total_bytes = total_info['total'] if total_info else 1
|
630
|
+
print(f"(来自索引) 显示前 {min(len(cached_entries), top_n)} 个最大的目录:\n")
|
631
|
+
self._render_dirs(cached_entries, total_bytes)
|
632
|
+
if sys.stdin.isatty() and not self.args.no_prompt:
|
633
|
+
try:
|
634
|
+
ans = input("是否重新分析以刷新索引?[y/N]: ").strip().lower()
|
635
|
+
except EOFError:
|
636
|
+
ans = ""
|
637
|
+
if ans not in ("y", "yes"):
|
638
|
+
return
|
639
|
+
else:
|
640
|
+
return
|
641
|
+
|
428
642
|
directories = self.analyzer.analyze_largest_directories(
|
429
643
|
path,
|
430
644
|
top_n=top_n,
|
431
645
|
index=self.index,
|
432
646
|
use_index=self.args.use_index,
|
433
|
-
reindex=
|
647
|
+
reindex=True, # 走到这里表示要刷新
|
434
648
|
index_ttl_hours=self.args.index_ttl,
|
435
|
-
prompt=
|
649
|
+
prompt=False,
|
436
650
|
)
|
437
|
-
|
438
651
|
if not directories:
|
439
652
|
print("❌ 无法分析目录大小")
|
440
653
|
return
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
size_str = self.analyzer.format_bytes(size)
|
446
|
-
percentage = (size / self.analyzer.get_disk_usage(path)['total']) * 100 if self.analyzer.get_disk_usage(path) else 0
|
447
|
-
# 目录大小大于1G采用红色显示
|
448
|
-
color = "\033[31m" if size >= 1024**3 else "\033[32m"
|
449
|
-
print(f"{i:2d}. \033[36m{dir_path}\033[0m -- 大小: {color}{size_str}\033[0m (\033[33m{percentage:.2f}%\033[0m)")
|
450
|
-
##print(f"{i:2d}. {dir_path}")
|
451
|
-
##print(f" 大小: {size_str} ({percentage:.2f}%)")
|
452
|
-
##print()
|
654
|
+
total_info = self.analyzer.get_disk_usage(path)
|
655
|
+
total_bytes = total_info['total'] if total_info else 1
|
656
|
+
print("\n已重新分析,最新结果:\n")
|
657
|
+
self._render_dirs(directories, total_bytes)
|
453
658
|
|
454
659
|
def print_app_analysis(self, top_n: int = 20):
|
455
660
|
"""打印应用目录占用分析,并给出卸载建议"""
|
@@ -457,29 +662,44 @@ class SpaceCli:
|
|
457
662
|
print("🧩 应用目录空间分析与卸载建议")
|
458
663
|
print("=" * 60)
|
459
664
|
|
665
|
+
# 先显示缓存,再决定是否刷新
|
666
|
+
if self.args.use_index:
|
667
|
+
cached = self.app_index.get_named("apps_aggregate")
|
668
|
+
if cached and cached.get("entries"):
|
669
|
+
cached_entries = [(e["name"], int(e["size"])) for e in cached["entries"]][:top_n]
|
670
|
+
total = self.analyzer.get_disk_usage("/")
|
671
|
+
disk_total = total['total'] if total else 1
|
672
|
+
print(f"(来自索引) 显示前 {min(len(cached_entries), top_n)} 个空间占用最高的应用:\n")
|
673
|
+
self._render_apps(cached_entries, disk_total)
|
674
|
+
# 提供一键删除
|
675
|
+
self._offer_app_delete(cached_entries)
|
676
|
+
if sys.stdin.isatty() and not self.args.no_prompt:
|
677
|
+
try:
|
678
|
+
ans = input("是否重新分析应用以刷新索引?[y/N]: ").strip().lower()
|
679
|
+
except EOFError:
|
680
|
+
ans = ""
|
681
|
+
if ans not in ("y", "yes"):
|
682
|
+
return
|
683
|
+
else:
|
684
|
+
return
|
685
|
+
|
460
686
|
apps = self.analyze_app_directories(
|
461
687
|
top_n=top_n,
|
462
688
|
index=self.app_index,
|
463
689
|
use_index=self.args.use_index,
|
464
|
-
reindex=
|
690
|
+
reindex=True,
|
465
691
|
index_ttl_hours=self.args.index_ttl,
|
466
|
-
prompt=
|
692
|
+
prompt=False,
|
467
693
|
)
|
468
694
|
if not apps:
|
469
695
|
print("❌ 未发现可分析的应用目录")
|
470
696
|
return
|
471
|
-
|
472
697
|
total = self.analyzer.get_disk_usage("/")
|
473
698
|
disk_total = total['total'] if total else 1
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
pct = (size / disk_total) * 100
|
479
|
-
suggestion = "建议卸载或清理缓存" if size >= 5 * 1024**3 else "可保留,定期清理缓存"
|
480
|
-
print(f"{i:2d}. \033[36m{app}\033[0m -- 占用: {size_str} ({pct:.2f}%) — {suggestion}")
|
481
|
-
##print(f" 占用: {size_str} ({pct:.2f}%) — {suggestion}")
|
482
|
-
#print()
|
699
|
+
print("\n已重新分析,最新应用占用结果:\n")
|
700
|
+
self._render_apps(apps, disk_total)
|
701
|
+
# 提供一键删除
|
702
|
+
self._offer_app_delete(apps)
|
483
703
|
|
484
704
|
def print_home_deep_analysis(self, top_n: int = 20):
|
485
705
|
"""对用户目录的 Library / Downloads / Documents 分别下探分析"""
|
@@ -757,37 +977,37 @@ def main():
|
|
757
977
|
except EOFError:
|
758
978
|
choice = ""
|
759
979
|
|
760
|
-
if choice == "0":
|
980
|
+
if choice == "0": # 退出
|
761
981
|
sys.exit(0)
|
762
|
-
elif choice == "2":
|
982
|
+
elif choice == "2": # 仅显示当前用户目录分析
|
763
983
|
args.path = home_path
|
764
984
|
args.apps = False
|
765
985
|
args.health_only = False
|
766
986
|
args.directories_only = False
|
767
|
-
elif choice == "3":
|
987
|
+
elif choice == "3": # 仅显示系统信息
|
768
988
|
args.health_only = True
|
769
989
|
args.directories_only = False
|
770
990
|
args.apps = False
|
771
|
-
elif choice == "4":
|
991
|
+
elif choice == "4": # 仅显示磁盘健康状态
|
772
992
|
args.health_only = False
|
773
993
|
args.directories_only = True
|
774
994
|
args.apps = False
|
775
|
-
elif choice == "5":
|
995
|
+
elif choice == "5": # 仅显示最大目录列表
|
776
996
|
args.health_only = False
|
777
997
|
args.directories_only = False
|
778
998
|
args.apps = False
|
779
|
-
elif choice == "6":
|
999
|
+
elif choice == "6": # 仅显示应用目录分析与建议
|
780
1000
|
args.health_only = False
|
781
|
-
args.directories_only =
|
1001
|
+
args.directories_only = False
|
782
1002
|
args.apps = True
|
783
|
-
elif choice == "7":
|
1003
|
+
elif choice == "7": # 仅显示大文件分析
|
784
1004
|
args.health_only = False
|
785
1005
|
args.directories_only = True
|
786
1006
|
args.apps = False
|
787
1007
|
args.big_files = True
|
788
|
-
else:
|
789
|
-
|
790
|
-
|
1008
|
+
else: # 默认执行全部(用户不选择,或者选择1)
|
1009
|
+
args.apps = True
|
1010
|
+
|
791
1011
|
|
792
1012
|
# --home 优先设置路径
|
793
1013
|
if getattr(args, 'home', False):
|
@@ -1,6 +0,0 @@
|
|
1
|
-
space_cli.py,sha256=cQR64qsQyjGkkOskkm3EH59dx-T2idfv_YYuI6VPk9A,32260
|
2
|
-
myspace_cli-1.2.0.dist-info/METADATA,sha256=AKVi6Os8zDw6h8z_St0DAgaYy3PpCT3HUOzhCe9cUHY,8284
|
3
|
-
myspace_cli-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
4
|
-
myspace_cli-1.2.0.dist-info/entry_points.txt,sha256=sIVEmPf6W8aNa7KiqOwAI6JrsgHlQciO5xH2G29tKPQ,45
|
5
|
-
myspace_cli-1.2.0.dist-info/top_level.txt,sha256=lnUfbQX9h-9ZjecMVCOWucS2kx69FwTndlrb48S20Sc,10
|
6
|
-
myspace_cli-1.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|