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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: myspace-cli
3
- Version: 1.2.0
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
- ### 方法2:创建全局命令
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
- ### 方法3:通过 pip 安装(发布到 PyPI 后)
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
- sys.stdout.write(f"\r-> 正在读取: {item_path}")
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=self.args.reindex,
647
+ reindex=True, # 走到这里表示要刷新
434
648
  index_ttl_hours=self.args.index_ttl,
435
- prompt=not self.args.no_prompt,
649
+ prompt=False,
436
650
  )
437
-
438
651
  if not directories:
439
652
  print("❌ 无法分析目录大小")
440
653
  return
441
-
442
- print(f"显示前 {min(len(directories), top_n)} 个最大的目录:\n")
443
-
444
- for i, (dir_path, size) in enumerate(directories, 1):
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=self.args.reindex,
690
+ reindex=True,
465
691
  index_ttl_hours=self.args.index_ttl,
466
- prompt=not self.args.no_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
- print(f"显示前 {min(len(apps), top_n)} 个空间占用最高的应用:\n")
476
- for i, (app, size) in enumerate(apps, 1):
477
- size_str = self.analyzer.format_bytes(size)
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 = True
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
- # 默认执行全部(用户不选择,或者选择1)
790
- args.apps = True
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,,