fixos 2.2.28__tar.gz → 2.2.29__tar.gz

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 (165) hide show
  1. {fixos-2.2.28 → fixos-2.2.29}/CHANGELOG.md +26 -0
  2. {fixos-2.2.28 → fixos-2.2.29}/PKG-INFO +4 -6
  3. {fixos-2.2.28 → fixos-2.2.29}/README.md +3 -5
  4. {fixos-2.2.28 → fixos-2.2.29}/fixos/__init__.py +1 -1
  5. {fixos-2.2.28 → fixos-2.2.29}/fixos/agent/session_core.py +84 -34
  6. fixos-2.2.29/fixos/cli/_cleanup_flatpak.py +229 -0
  7. fixos-2.2.29/fixos/cli/_cleanup_home.py +132 -0
  8. fixos-2.2.29/fixos/cli/_cleanup_snap.py +116 -0
  9. fixos-2.2.29/fixos/cli/_cleanup_system.py +444 -0
  10. fixos-2.2.29/fixos/cli/_cleanup_utils.py +114 -0
  11. fixos-2.2.29/fixos/cli/cleanup_cmd.py +250 -0
  12. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/config_cmd.py +37 -23
  13. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/scan_cmd.py +6 -1
  14. {fixos-2.2.28 → fixos-2.2.29}/fixos/constants.py +13 -0
  15. fixos-2.2.29/fixos/diagnostics/_flatpak_analysis_mixin.py +298 -0
  16. fixos-2.2.29/fixos/diagnostics/_flatpak_execution_mixin.py +232 -0
  17. fixos-2.2.29/fixos/diagnostics/_flatpak_recommendations_mixin.py +259 -0
  18. fixos-2.2.29/fixos/diagnostics/_storage_container_mixin.py +155 -0
  19. fixos-2.2.29/fixos/diagnostics/_storage_system_mixin.py +259 -0
  20. fixos-2.2.29/fixos/diagnostics/_storage_user_mixin.py +348 -0
  21. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/checks/__init__.py +6 -0
  22. fixos-2.2.29/fixos/diagnostics/checks/file_analysis.py +334 -0
  23. fixos-2.2.29/fixos/diagnostics/checks/packages.py +220 -0
  24. fixos-2.2.29/fixos/diagnostics/checks/storage_optimization.py +224 -0
  25. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/dev_project_analyzer.py +3 -5
  26. fixos-2.2.29/fixos/diagnostics/flatpak_analyzer.py +161 -0
  27. fixos-2.2.29/fixos/diagnostics/storage_analyzer.py +274 -0
  28. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/system_checks.py +9 -0
  29. fixos-2.2.29/fixos/diagnostics/utils.py +12 -0
  30. fixos-2.2.29/fixos/fixes/__init__.py +2 -0
  31. {fixos-2.2.28 → fixos-2.2.29}/fixos/utils/terminal.py +10 -15
  32. {fixos-2.2.28 → fixos-2.2.29}/fixos.egg-info/PKG-INFO +4 -6
  33. {fixos-2.2.28 → fixos-2.2.29}/fixos.egg-info/SOURCES.txt +16 -0
  34. {fixos-2.2.28 → fixos-2.2.29}/pyproject.toml +1 -1
  35. {fixos-2.2.28 → fixos-2.2.29}/pytest.ini +1 -1
  36. {fixos-2.2.28 → fixos-2.2.29}/setup.py +1 -1
  37. fixos-2.2.29/tests/e2e/test_diagnostics_integration.py +132 -0
  38. {fixos-2.2.28 → fixos-2.2.29}/tests/unit/test_core.py +31 -0
  39. fixos-2.2.28/fixos/cli/cleanup_cmd.py +0 -1228
  40. fixos-2.2.28/fixos/diagnostics/flatpak_analyzer.py +0 -937
  41. fixos-2.2.28/fixos/diagnostics/storage_analyzer.py +0 -1091
  42. fixos-2.2.28/fixos/fixes/__init__.py +0 -4
  43. {fixos-2.2.28 → fixos-2.2.29}/.env.example +0 -0
  44. {fixos-2.2.28 → fixos-2.2.29}/LICENSE +0 -0
  45. {fixos-2.2.28 → fixos-2.2.29}/MANIFEST.in +0 -0
  46. {fixos-2.2.28 → fixos-2.2.29}/docker/README.md +0 -0
  47. {fixos-2.2.28 → fixos-2.2.29}/docker/TEST_RESULTS.md +0 -0
  48. {fixos-2.2.28 → fixos-2.2.29}/docker/TEST_RESULTS_V2.md +0 -0
  49. {fixos-2.2.28 → fixos-2.2.29}/docker/alpine/Dockerfile +0 -0
  50. {fixos-2.2.28 → fixos-2.2.29}/docker/arch/Dockerfile +0 -0
  51. {fixos-2.2.28 → fixos-2.2.29}/docker/base/Dockerfile +0 -0
  52. {fixos-2.2.28 → fixos-2.2.29}/docker/broken-audio/Dockerfile +0 -0
  53. {fixos-2.2.28 → fixos-2.2.29}/docker/broken-full/Dockerfile +0 -0
  54. {fixos-2.2.28 → fixos-2.2.29}/docker/broken-network/Dockerfile +0 -0
  55. {fixos-2.2.28 → fixos-2.2.29}/docker/broken-thumbnails/Dockerfile +0 -0
  56. {fixos-2.2.28 → fixos-2.2.29}/docker/debian/Dockerfile +0 -0
  57. {fixos-2.2.28 → fixos-2.2.29}/docker/docker-compose.multi-system.yml +0 -0
  58. {fixos-2.2.28 → fixos-2.2.29}/docker/docker-compose.yml +0 -0
  59. {fixos-2.2.28 → fixos-2.2.29}/docker/fedora/Dockerfile +0 -0
  60. {fixos-2.2.28 → fixos-2.2.29}/docker/test-multi-system.sh +0 -0
  61. {fixos-2.2.28 → fixos-2.2.29}/docker/test-scenarios.sh +0 -0
  62. {fixos-2.2.28 → fixos-2.2.29}/docker/ubuntu/Dockerfile +0 -0
  63. {fixos-2.2.28 → fixos-2.2.29}/docker/validate-scenario.py +0 -0
  64. {fixos-2.2.28 → fixos-2.2.29}/docs/examples/advanced_usage.py +0 -0
  65. {fixos-2.2.28 → fixos-2.2.29}/docs/examples/quickstart.py +0 -0
  66. {fixos-2.2.28 → fixos-2.2.29}/fixos/agent/__init__.py +0 -0
  67. {fixos-2.2.28 → fixos-2.2.29}/fixos/agent/autonomous.py +0 -0
  68. {fixos-2.2.28 → fixos-2.2.29}/fixos/agent/autonomous_session.py +0 -0
  69. {fixos-2.2.28 → fixos-2.2.29}/fixos/agent/hitl.py +0 -0
  70. {fixos-2.2.28 → fixos-2.2.29}/fixos/agent/hitl_session.py +0 -0
  71. {fixos-2.2.28 → fixos-2.2.29}/fixos/agent/session_handlers.py +0 -0
  72. {fixos-2.2.28 → fixos-2.2.29}/fixos/agent/session_io.py +0 -0
  73. {fixos-2.2.28 → fixos-2.2.29}/fixos/anonymizer.py +0 -0
  74. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/__init__.py +0 -0
  75. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/ask_cmd.py +0 -0
  76. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/features_cmd.py +0 -0
  77. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/fix_cmd.py +0 -0
  78. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/history_cmd.py +0 -0
  79. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/main.py +0 -0
  80. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/orchestrate_cmd.py +0 -0
  81. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/output_formatter.py +0 -0
  82. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/profile_cmd.py +0 -0
  83. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/provider_cmd.py +0 -0
  84. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/quickfix_cmd.py +0 -0
  85. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/report_cmd.py +0 -0
  86. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/rollback_cmd.py +0 -0
  87. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/shared.py +0 -0
  88. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/token_cmd.py +0 -0
  89. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli/watch_cmd.py +0 -0
  90. {fixos-2.2.28 → fixos-2.2.29}/fixos/cli.py +0 -0
  91. {fixos-2.2.28 → fixos-2.2.29}/fixos/config.py +0 -0
  92. {fixos-2.2.28 → fixos-2.2.29}/fixos/config_interactive.py +0 -0
  93. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/__init__.py +0 -0
  94. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/checks/_shared.py +0 -0
  95. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/checks/audio.py +0 -0
  96. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/checks/hardware.py +0 -0
  97. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/checks/resources.py +0 -0
  98. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/checks/security.py +0 -0
  99. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/checks/system_core.py +0 -0
  100. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/checks/thumbnails.py +0 -0
  101. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/disk_analyzer.py +0 -0
  102. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/service_cleanup.py +0 -0
  103. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/service_details.py +0 -0
  104. {fixos-2.2.28 → fixos-2.2.29}/fixos/diagnostics/service_scanner.py +0 -0
  105. {fixos-2.2.28 → fixos-2.2.29}/fixos/features/__init__.py +0 -0
  106. {fixos-2.2.28 → fixos-2.2.29}/fixos/features/auditor.py +0 -0
  107. {fixos-2.2.28 → fixos-2.2.29}/fixos/features/catalog.py +0 -0
  108. {fixos-2.2.28 → fixos-2.2.29}/fixos/features/installer.py +0 -0
  109. {fixos-2.2.28 → fixos-2.2.29}/fixos/features/profiles.py +0 -0
  110. {fixos-2.2.28 → fixos-2.2.29}/fixos/features/renderer.py +0 -0
  111. {fixos-2.2.28 → fixos-2.2.29}/fixos/interactive/__init__.py +0 -0
  112. {fixos-2.2.28 → fixos-2.2.29}/fixos/interactive/cleanup_planner.py +0 -0
  113. {fixos-2.2.28 → fixos-2.2.29}/fixos/llm_shell.py +0 -0
  114. {fixos-2.2.28 → fixos-2.2.29}/fixos/orchestrator/__init__.py +0 -0
  115. {fixos-2.2.28 → fixos-2.2.29}/fixos/orchestrator/executor.py +0 -0
  116. {fixos-2.2.28 → fixos-2.2.29}/fixos/orchestrator/graph.py +0 -0
  117. {fixos-2.2.28 → fixos-2.2.29}/fixos/orchestrator/orchestrator.py +0 -0
  118. {fixos-2.2.28 → fixos-2.2.29}/fixos/orchestrator/rollback.py +0 -0
  119. {fixos-2.2.28 → fixos-2.2.29}/fixos/platform_utils.py +0 -0
  120. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/__init__.py +0 -0
  121. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/base.py +0 -0
  122. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/builtin/__init__.py +0 -0
  123. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/builtin/audio.py +0 -0
  124. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/builtin/disk.py +0 -0
  125. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/builtin/hardware.py +0 -0
  126. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/builtin/resources.py +0 -0
  127. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/builtin/security.py +0 -0
  128. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/builtin/thumbnails.py +0 -0
  129. {fixos-2.2.28 → fixos-2.2.29}/fixos/plugins/registry.py +0 -0
  130. {fixos-2.2.28 → fixos-2.2.29}/fixos/profiles/__init__.py +0 -0
  131. {fixos-2.2.28 → fixos-2.2.29}/fixos/providers/__init__.py +0 -0
  132. {fixos-2.2.28 → fixos-2.2.29}/fixos/providers/llm.py +0 -0
  133. {fixos-2.2.28 → fixos-2.2.29}/fixos/providers/llm_analyzer.py +0 -0
  134. {fixos-2.2.28 → fixos-2.2.29}/fixos/providers/schemas.py +0 -0
  135. {fixos-2.2.28 → fixos-2.2.29}/fixos/system_checks.py +0 -0
  136. {fixos-2.2.28 → fixos-2.2.29}/fixos/utils/__init__.py +0 -0
  137. {fixos-2.2.28 → fixos-2.2.29}/fixos/utils/anonymizer.py +0 -0
  138. {fixos-2.2.28 → fixos-2.2.29}/fixos/utils/timeout.py +0 -0
  139. {fixos-2.2.28 → fixos-2.2.29}/fixos/utils/web_search.py +0 -0
  140. {fixos-2.2.28 → fixos-2.2.29}/fixos/watch.py +0 -0
  141. {fixos-2.2.28 → fixos-2.2.29}/fixos.egg-info/dependency_links.txt +0 -0
  142. {fixos-2.2.28 → fixos-2.2.29}/fixos.egg-info/entry_points.txt +0 -0
  143. {fixos-2.2.28 → fixos-2.2.29}/fixos.egg-info/requires.txt +0 -0
  144. {fixos-2.2.28 → fixos-2.2.29}/fixos.egg-info/top_level.txt +0 -0
  145. {fixos-2.2.28 → fixos-2.2.29}/requirements-dev.txt +0 -0
  146. {fixos-2.2.28 → fixos-2.2.29}/requirements.txt +0 -0
  147. {fixos-2.2.28 → fixos-2.2.29}/scripts/pyqual-calibrate.py +0 -0
  148. {fixos-2.2.28 → fixos-2.2.29}/setup.cfg +0 -0
  149. {fixos-2.2.28 → fixos-2.2.29}/tests/__init__.py +0 -0
  150. {fixos-2.2.28 → fixos-2.2.29}/tests/conftest.py +0 -0
  151. {fixos-2.2.28 → fixos-2.2.29}/tests/e2e/__init__.py +0 -0
  152. {fixos-2.2.28 → fixos-2.2.29}/tests/e2e/test_anonymization_layers.py +0 -0
  153. {fixos-2.2.28 → fixos-2.2.29}/tests/e2e/test_audio_broken.py +0 -0
  154. {fixos-2.2.28 → fixos-2.2.29}/tests/e2e/test_cli.py +0 -0
  155. {fixos-2.2.28 → fixos-2.2.29}/tests/e2e/test_executor.py +0 -0
  156. {fixos-2.2.28 → fixos-2.2.29}/tests/e2e/test_multi_system.py +0 -0
  157. {fixos-2.2.28 → fixos-2.2.29}/tests/e2e/test_network_broken.py +0 -0
  158. {fixos-2.2.28 → fixos-2.2.29}/tests/e2e/test_thumbnails_broken.py +0 -0
  159. {fixos-2.2.28 → fixos-2.2.29}/tests/test_fixos.py +0 -0
  160. {fixos-2.2.28 → fixos-2.2.29}/tests/unit/__init__.py +0 -0
  161. {fixos-2.2.28 → fixos-2.2.29}/tests/unit/test_anonymizer.py +0 -0
  162. {fixos-2.2.28 → fixos-2.2.29}/tests/unit/test_executor.py +0 -0
  163. {fixos-2.2.28 → fixos-2.2.29}/tests/unit/test_orchestrator.py +0 -0
  164. {fixos-2.2.28 → fixos-2.2.29}/tests/unit/test_service_cleanup.py +0 -0
  165. {fixos-2.2.28 → fixos-2.2.29}/tests/unit/test_service_scanner.py +0 -0
@@ -150,6 +150,32 @@ fix(goal): code analysis engine
150
150
  - **refactor(cli):** Usunięto zduplikowany kod ujednolicając funkcje analizy dysku do wspólnego helpera `_run_disk_analysis`.
151
151
  - **refactor(ui):** Usunięto ikony Unicode z CLI i sformatowano wyjście `stderr` oraz standardowego logowania na czysty kod Markdown dla poprawy czytelności w oknach terminalowych.
152
152
 
153
+ ## [2.2.29] - 2026-05-08
154
+
155
+ ### Docs
156
+ - Update README.md
157
+ - Update SUMD.md
158
+ - Update SUMR.md
159
+ - Update project/README.md
160
+ - Update project/context.md
161
+
162
+ ### Test
163
+ - Update tests/e2e/test_diagnostics_integration.py
164
+ - Update tests/unit/test_core.py
165
+
166
+ ### Other
167
+ - Update app.doql.less
168
+ - Update fixos/agent/session_core.py
169
+ - Update fixos/cli/_cleanup_flatpak.py
170
+ - Update fixos/cli/_cleanup_home.py
171
+ - Update fixos/cli/_cleanup_snap.py
172
+ - Update fixos/cli/_cleanup_system.py
173
+ - Update fixos/cli/_cleanup_utils.py
174
+ - Update fixos/cli/cleanup_cmd.py
175
+ - Update fixos/cli/config_cmd.py
176
+ - Update fixos/cli/scan_cmd.py
177
+ - ... and 36 more files
178
+
153
179
  ## [2.2.28] - 2026-05-06
154
180
 
155
181
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.28
3
+ Version: 2.2.29
4
4
  Summary: AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues
5
5
  Home-page: https://github.com/wronai/fixos
6
6
  Author: fixos contributors
@@ -63,13 +63,11 @@ AI-powered OS Diagnostics
63
63
 
64
64
  ## AI Cost Tracking
65
65
 
66
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
66
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-yellow) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
68
67
 
69
- - 🤖 **LLM usage:** $7.5000 (127 commits)
70
- - 👤 **Human dev:** ~$2647 (26.5h @ $100/h, 30min dedup)
68
+ This project uses AI-generated code. Total cost: **$7.5000** with **128** AI commits.
71
69
 
72
- Generated on 2026-05-06 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
70
+ Generated on 2026-05-08 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
73
71
 
74
72
  ---
75
73
 
@@ -19,13 +19,11 @@ AI-powered OS Diagnostics
19
19
 
20
20
  ## AI Cost Tracking
21
21
 
22
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
23
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
22
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-yellow) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
24
23
 
25
- - 🤖 **LLM usage:** $7.5000 (127 commits)
26
- - 👤 **Human dev:** ~$2647 (26.5h @ $100/h, 30min dedup)
24
+ This project uses AI-generated code. Total cost: **$7.5000** with **128** AI commits.
27
25
 
28
- Generated on 2026-05-06 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
26
+ Generated on 2026-05-08 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
29
27
 
30
28
  ---
31
29
 
@@ -1,2 +1,2 @@
1
1
  """fixos – AI-powered Linux/Windows diagnostics and repair."""
2
- __version__ = "2.2.28"
2
+ __version__ = "2.2.29"
@@ -51,6 +51,31 @@ IMPORTANT RULES:
51
51
  - When disk usage is critically high (>90%), ALWAYS propose cleanup commands FIRST.
52
52
  - NEVER suggest package upgrades or installations BEFORE cleanup has freed sufficient space and been verified.
53
53
 
54
+ PACKAGE ANALYSIS rules (when "packages" data is present):
55
+ - For orphaned packages: propose `sudo dnf autoremove` or specific `sudo dnf remove <pkg>`.
56
+ - For debug/devel packages on desktop: propose `sudo dnf remove '*-debuginfo*'` or specific removals.
57
+ - For duplicate RPM+Flatpak apps: propose removing one version (prefer Flatpak for GUI apps).
58
+ - For unused Flatpak runtimes: propose `flatpak uninstall --unused`.
59
+ - For leaf packages not used in 90+ days: propose specific `sudo dnf remove <pkg>`.
60
+ - Always warn user about dependencies that will be removed.
61
+
62
+ STORAGE OPTIMIZATION rules (when "storage" data is present):
63
+ - If unallocated disk space exists: propose `sudo growpart` or `sudo lvextend + resize2fs/xfs_growfs`.
64
+ - For btrfs without compression: propose adding `compress=zstd:1` to fstab.
65
+ - For old btrfs snapshots: propose `sudo snapper delete <id>` for specific old snapshots.
66
+ - For swap/zram optimization: propose tuning swappiness or enabling zram.
67
+ - If fstrim.timer is disabled on SSD: propose `sudo systemctl enable --now fstrim.timer`.
68
+
69
+ FILE ANALYSIS rules (when "files" data is present):
70
+ - For large files >200MB: list them and propose review/deletion commands.
71
+ - For duplicate files: propose `fdupes -d` or `rdfind` commands for interactive dedup.
72
+ - For media files (ebooks, mp3, mp4, images): propose organizing/archiving commands:
73
+ - `mkdir -p ~/Archive/{ebooks,muzyka,wideo,obrazy}` + `mv` commands.
74
+ - For compression: `tar czf ~/Archive/ebooks.tar.gz ~/path/to/ebooks/`.
75
+ - For old downloads (>30 days): propose cleanup of ~/Downloads.
76
+ - For trash: propose `rm -rf ~/.local/share/Trash/files/*`.
77
+ - Always group suggestions by category (video, music, ebooks, archives, etc.).
78
+
54
79
  Always end with:
55
80
  ━━━ DOSTĘPNE AKCJE ━━━
56
81
  [1] Fix problem 1 – `command`
@@ -117,11 +142,9 @@ def _extract_co_robi(text: str) -> str:
117
142
  return m.group(1).strip() if m else ""
118
143
 
119
144
 
120
- def extract_fixes(reply: str) -> List[Tuple[str, str]]:
121
- """Extract (command, comment) pairs from LLM reply."""
145
+ def _pattern_strict_bold(reply: str) -> List[Tuple[str, str]]:
146
+ """Pattern 1: **Komenda:** `command` (strict: bold + backticks)."""
122
147
  fixes: List[Tuple[str, str]] = []
123
-
124
- # Pattern 1: **Komenda:** `command` (strict: bold + backticks)
125
148
  for m in re.finditer(
126
149
  r"\*\*Komenda:\*\*\s*`([^`]+)`(?:[^\n]*?\*\*Co robi:\*\*\s*(.+?))?(?=\n|$)",
127
150
  reply, re.IGNORECASE,
@@ -129,45 +152,72 @@ def extract_fixes(reply: str) -> List[Tuple[str, str]]:
129
152
  cmd = m.group(1).strip()
130
153
  if cmd:
131
154
  fixes.append((cmd, (m.group(2) or "").strip()))
155
+ return fixes
132
156
 
133
- # Pattern 2: Komenda: `command` (backticks, optional bold)
134
- if not fixes:
135
- for m in re.finditer(
136
- r"\*{0,2}Komenda:\*{0,2}\s*`([^`]+)`",
137
- reply, re.IGNORECASE,
138
- ):
139
- cmd = m.group(1).strip()
140
- if cmd:
141
- fixes.append((cmd, _extract_co_robi(reply[m.end():])))
142
-
143
- # Pattern 3: Komenda: command (no backticks — command until Co robi:/next problem/section)
144
- if not fixes:
145
- for m in re.finditer(
146
- r"\*{0,2}Komenda:\*{0,2}\s*"
147
- r"(.+?)"
148
- r"(?=\n\s*\*{0,2}Co robi:|\n[🔴🟡🟢]|\n━|\n─|\n\[[\dA-Z]|\Z)",
149
- reply, re.IGNORECASE | re.DOTALL,
150
- ):
151
- cmd = re.sub(r"\s*\n\s*", " ", m.group(1)).strip()
152
- if cmd:
153
- fixes.append((cmd, _extract_co_robi(reply[m.end():])))
154
-
155
- # Fallback: → Fix: `command`
156
- if not fixes:
157
- for m in re.finditer(r"→\s*Fix:\s*`([^`]+)`", reply, re.IGNORECASE):
158
- fixes.append((m.group(1).strip(), ""))
159
- # Fallback: [N] ... `command`
157
+
158
+ def _pattern_backticks(reply: str) -> List[Tuple[str, str]]:
159
+ """Pattern 2: Komenda: `command` (backticks, optional bold)."""
160
+ fixes: List[Tuple[str, str]] = []
161
+ for m in re.finditer(
162
+ r"\*{0,2}Komenda:\*{0,2}\s*`([^`]+)`",
163
+ reply, re.IGNORECASE,
164
+ ):
165
+ cmd = m.group(1).strip()
166
+ if cmd:
167
+ fixes.append((cmd, _extract_co_robi(reply[m.end():])))
168
+ return fixes
169
+
170
+
171
+ def _pattern_no_backticks(reply: str) -> List[Tuple[str, str]]:
172
+ """Pattern 3: Komenda: command (no backticks — until next section)."""
173
+ fixes: List[Tuple[str, str]] = []
174
+ for m in re.finditer(
175
+ r"\*{0,2}Komenda:\*{0,2}\s*"
176
+ r"(.+?)"
177
+ r"(?=\n\s*\*{0,2}Co robi:|\n[🔴🟡🟢]|\n━|\n─|\n\[[\dA-Z]|\Z)",
178
+ reply, re.IGNORECASE | re.DOTALL,
179
+ ):
180
+ cmd = re.sub(r"\s*\n\s*", " ", m.group(1)).strip()
181
+ if cmd:
182
+ fixes.append((cmd, _extract_co_robi(reply[m.end():])))
183
+ return fixes
184
+
185
+
186
+ def _pattern_fallbacks(reply: str) -> List[Tuple[str, str]]:
187
+ """Fallback patterns: → Fix, [N] command, EXEC."""
188
+ fixes: List[Tuple[str, str]] = []
189
+ for m in re.finditer(r"→\s*Fix:\s*`([^`]+)`", reply, re.IGNORECASE):
190
+ fixes.append((m.group(1).strip(), ""))
160
191
  if not fixes:
161
192
  for m in re.finditer(r"\[(\d+)\][^`\n]+`([^`]+)`", reply):
162
193
  fixes.append((m.group(2).strip(), f"Fix #{m.group(1)}"))
163
- # Fallback: EXEC: `command`
164
194
  if not fixes:
165
195
  for m in re.finditer(r"EXEC:\s*`([^`]+)`", reply, re.IGNORECASE):
166
196
  fixes.append((m.group(1).strip(), ""))
197
+ return fixes
198
+
167
199
 
168
- # Keep only actionable commands in menu; diagnostic-only commands are noise.
200
+ def _deduplicate(fixes: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
201
+ """Remove diagnostic-only commands and deduplicate."""
169
202
  filtered = [(cmd, comment) for cmd, comment in fixes if not _is_diagnostic_only_command(cmd)]
170
- return filtered
203
+ seen: set[str] = set()
204
+ unique: List[Tuple[str, str]] = []
205
+ for cmd, comment in filtered:
206
+ if cmd not in seen:
207
+ seen.add(cmd)
208
+ unique.append((cmd, comment))
209
+ return unique
210
+
211
+
212
+ def extract_fixes(reply: str) -> List[Tuple[str, str]]:
213
+ """Extract (command, comment) pairs from LLM reply."""
214
+ fixes = (
215
+ _pattern_strict_bold(reply)
216
+ or _pattern_backticks(reply)
217
+ or _pattern_no_backticks(reply)
218
+ or _pattern_fallbacks(reply)
219
+ )
220
+ return _deduplicate(fixes)
171
221
 
172
222
 
173
223
  def extract_search_topic(llm_reply: str) -> str:
@@ -0,0 +1,229 @@
1
+ """Flatpak-specific cleanup CLI handlers."""
2
+ from __future__ import annotations
3
+
4
+ import click
5
+
6
+ from fixos.cli._cleanup_utils import (
7
+ _format_bytes,
8
+ _parse_selection,
9
+ _parse_size_to_bytes,
10
+ )
11
+
12
+
13
+ def _cleanup_flatpak_detailed(scanner, json_output: bool, dry_run: bool) -> None:
14
+ """
15
+ Detailed interactive Flatpak cleanup with menu selection.
16
+
17
+ Wyświetla menu z opcjami i pokazuje korzyści na bazie rzeczywistych danych.
18
+ """
19
+ from fixos.diagnostics.flatpak_analyzer import FlatpakAnalyzer
20
+
21
+ analyzer = FlatpakAnalyzer()
22
+
23
+ # Najpierw wykonaj pełną analizę
24
+ click.echo(click.style("\n🔍 Analizuję Flatpak...", fg="cyan"))
25
+ analysis = analyzer.analyze()
26
+
27
+ # JSON output mode
28
+ if json_output:
29
+ import json
30
+ click.echo(json.dumps(analysis, indent=2, default=str))
31
+ return
32
+
33
+ # Wyświetl podsumowanie stanu
34
+ _display_flatpak_status(analysis)
35
+
36
+ # Pobierz rekomendacje
37
+ recommendations = analyzer.get_cleanup_recommendations()
38
+
39
+ if not recommendations:
40
+ click.echo(click.style("\n✅ Brak rekomendacji czyszczenia - Flatpak jest w dobrym stanie.", fg="green"))
41
+ return
42
+
43
+ # Wyświetl menu z opcjami
44
+ click.echo(f"\n{click.style('='*60, fg='cyan')}")
45
+ click.echo(click.style("📋 WYBIERZ OPCJE DO WYKONANIA", fg="cyan", bold=True))
46
+ click.echo(click.style(f"{'='*60}", fg="cyan"))
47
+
48
+ if dry_run:
49
+ click.echo(click.style("\n[TRYB DRY-RUN] - brak faktycznych zmian\n", fg="yellow"))
50
+
51
+ # Wyświetl każdą opcję z korzyściami
52
+ total_potential_savings = 0
53
+ for i, rec in enumerate(recommendations, 1):
54
+ priority_icon = {"critical": "💣", "high": "🔥", "medium": "⚠️", "low": "💡"}.get(rec['priority'], "•")
55
+ risk_color = {"none": "green", "low": "green", "medium": "yellow", "high": "red"}.get(rec['risk'], "white")
56
+
57
+ click.echo(f"\n{click.style(f'[{i}]', fg='cyan', bold=True)} {priority_icon} {click.style(rec['description'], fg='yellow', bold=True)}")
58
+ click.echo(f" Komenda: {click.style(rec['action'], fg='white', dim=True)}")
59
+
60
+ # Korzyść na bazie rzeczywistych danych
61
+ savings = rec.get('estimated_savings', '0 B')
62
+ click.echo(f" {click.style('Korzyść:', fg='green')} ~{savings}")
63
+ total_potential_savings += _parse_size_to_bytes(savings)
64
+
65
+ click.echo(f" {click.style('Ryzyko:', fg='white')} {click.style(rec['risk'].upper(), fg=risk_color)}")
66
+
67
+ # Dodatkowe info
68
+ if rec.get('items'):
69
+ n_items = len(rec['items'])
70
+ click.echo(f" {click.style(f'Elementów: {n_items}', fg='white', dim=True)}")
71
+
72
+ # Podsumowanie potencjalnych korzyści
73
+ click.echo(f"\n{click.style('-'*60, fg='cyan')}")
74
+ click.echo(f"💰 {click.style('ŁĄCZNA POTENCJALNA KORZYŚĆ:', fg='green', bold=True)} ~{_format_bytes(total_potential_savings)}")
75
+ click.echo(click.style(f"{'-'*60}", fg="cyan"))
76
+
77
+ # Menu wyboru
78
+ click.echo(f"\n{click.style('Dostępne opcje:', fg='white', bold=True)}")
79
+ click.echo(f" {click.style('1,2,3...', fg='cyan')} - wybierz konkretne opcje (np. '1,3')")
80
+ click.echo(f" {click.style('all', fg='green')} - wykonaj wszystkie bezpieczne (ryzyko low/none)")
81
+ click.echo(f" {click.style('critical', fg='red')} - wykonaj tylko krytyczne")
82
+ click.echo(f" {click.style('none', fg='yellow')} - pomiń wszystko")
83
+ click.echo(f" {click.style('?', fg='white')} - pokaż szczegóły każdej opcji")
84
+
85
+ # Pytaj o wybór
86
+ click.echo()
87
+ selection = click.prompt(
88
+ click.style("Twój wybór", fg="cyan"),
89
+ default="none",
90
+ show_default=False
91
+ )
92
+
93
+ # Obsłuż '?'
94
+ if selection.strip() == '?':
95
+ _display_detailed_recommendations(recommendations)
96
+ selection = click.prompt(
97
+ click.style("\nTwój wybór", fg="cyan"),
98
+ default="none",
99
+ show_default=False
100
+ )
101
+
102
+ # Parsuj wybór
103
+ selected_indices = _parse_selection(selection, len(recommendations))
104
+
105
+ if not selected_indices:
106
+ click.echo(click.style("\n⏭️ Nie wybrano żadnych akcji.", fg="yellow"))
107
+ return
108
+
109
+ # Wykonaj wybrane akcje
110
+ results = {
111
+ "executed": [],
112
+ "skipped": [],
113
+ "failed": [],
114
+ "space_reclaimed": 0,
115
+ }
116
+
117
+ click.echo(f"\n{click.style('='*60, fg='cyan')}")
118
+ click.echo(click.style("🚀 WYKONYWANIE WYBRANYCH AKCJI", fg="cyan", bold=True))
119
+ click.echo(f"{click.style('='*60, fg='cyan')}\n")
120
+
121
+ for idx in selected_indices:
122
+ rec = recommendations[idx]
123
+ click.echo(f"\n[{idx+1}/{len(recommendations)}] {rec['description']}")
124
+
125
+ if dry_run:
126
+ click.echo(click.style(f" [DRY-RUN] Wykonano by: {rec['action']}", fg="cyan"))
127
+ results['executed'].append({"action": rec['action'], "dry_run": True})
128
+ savings_bytes = _parse_size_to_bytes(rec.get('estimated_savings', '0 B'))
129
+ results['space_reclaimed'] += savings_bytes
130
+ else:
131
+ click.echo(f" 🚀 Wykonuję: {rec['action']}")
132
+ result = analyzer._execute_cleanup_action(rec)
133
+
134
+ if result['success']:
135
+ results['executed'].append({
136
+ "action": rec['action'],
137
+ "output": result.get('output', ''),
138
+ })
139
+ results['space_reclaimed'] += result.get('bytes_reclaimed', 0)
140
+ click.echo(click.style(" ✅ Sukces", fg="green"))
141
+ else:
142
+ results['failed'].append({
143
+ "action": rec['action'],
144
+ "error": result.get('error', 'Unknown error'),
145
+ })
146
+ click.echo(click.style(f" ❌ Błąd: {result.get('error', 'Unknown error')}", fg="red"))
147
+
148
+ # Podsumowanie końcowe
149
+ click.echo(f"\n{click.style('='*60, fg='cyan')}")
150
+ click.echo(click.style("📊 PODSUMOWANIE", fg="cyan", bold=True))
151
+ click.echo(click.style(f"{'='*60}", fg="cyan"))
152
+ click.echo(f" ✅ Wykonano: {len(results['executed'])}")
153
+ click.echo(f" ⏭️ Pominięto: {len(results['skipped'])}")
154
+ click.echo(f" ❌ Błędy: {len(results['failed'])}")
155
+
156
+ freed_gb = results['space_reclaimed'] / (1024**3)
157
+ if dry_run:
158
+ click.echo(click.style(f"\n 💰 [DRY-RUN] Zwolniono by: {freed_gb:.2f} GB", fg="cyan"))
159
+ else:
160
+ click.echo(click.style(f"\n 💰 Odzyskano: {freed_gb:.2f} GB", fg="green"))
161
+
162
+ click.echo(click.style("="*60 + "\n", fg="cyan"))
163
+
164
+
165
+ def _display_flatpak_status(analysis: dict) -> None:
166
+ """Wyświetl status Flatpak z rzeczywistymi danymi."""
167
+ click.echo(f"\n{click.style('='*60, fg='cyan')}")
168
+ click.echo(click.style("📊 STATUS FLATPAK", fg="cyan", bold=True))
169
+ click.echo(click.style(f"{'='*60}", fg="cyan"))
170
+
171
+ # Aplikacje
172
+ apps_count = len(analysis.get('installed_apps', []))
173
+ apps_size = sum(a.get('size_bytes', 0) for a in analysis.get('installed_apps', []))
174
+ click.echo(f"\n📱 Aplikacje: {click.style(str(apps_count), fg='yellow')} ({_format_bytes(apps_size)})")
175
+
176
+ # Runtimes
177
+ runtimes_count = len(analysis.get('installed_runtimes', []))
178
+ runtimes_size = sum(r.get('size_bytes', 0) for r in analysis.get('installed_runtimes', []))
179
+ click.echo(f"🔧 Runtime'y: {click.style(str(runtimes_count), fg='yellow')} ({_format_bytes(runtimes_size)})")
180
+
181
+ # Repo bloat
182
+ repo_bloat = analysis.get('repo_bloat', {})
183
+ if repo_bloat.get('bloat_detected'):
184
+ click.echo(f"\n{click.style('⚠️ PROBLEM: Repo zajmuje za dużo miejsca!', fg='red', bold=True)}")
185
+ click.echo(f" Repo: {_format_bytes(repo_bloat.get('repo_total_size', 0))}")
186
+ click.echo(f" Aplikacje+Runtime'y: {_format_bytes(repo_bloat.get('installed_size', 0))}")
187
+ click.echo(f" Ratio: {repo_bloat.get('ratio', 0):.1f}x (powinno być ~1-1.5x)")
188
+ click.echo(f" {click.style('Marnotrawstwo:', fg='red')} {_format_bytes(repo_bloat.get('wasted_size', 0))}")
189
+
190
+ # Duplikaty
191
+ duplicates = analysis.get('duplicate_apps', [])
192
+ if duplicates:
193
+ dup_size = sum(d.get('total_size', 0) for d in duplicates)
194
+ click.echo(f"\n🔄 Duplikaty aplikacji: {click.style(str(len(duplicates)), fg='yellow')} ({_format_bytes(dup_size)})")
195
+ for dup in duplicates[:3]:
196
+ click.echo(f" • {dup.get('name', '?')} ({dup.get('count', 0)} wersje)")
197
+
198
+ # Nieużywane runtime'y
199
+ unused = analysis.get('unused_runtimes', [])
200
+ if unused:
201
+ unused_size = analysis.get('total_size_unused', 0)
202
+ click.echo(f"\n🗑️ Nieużywane runtime'y: {click.style(str(len(unused)), fg='yellow')} ({_format_bytes(unused_size)})")
203
+
204
+ # Leftover data
205
+ leftover = analysis.get('leftover_data', [])
206
+ if leftover:
207
+ leftover_size = analysis.get('total_size_leftover', 0)
208
+ click.echo(f"\n📁 Dane po odinstalowanych: {click.style(str(len(leftover)), fg='yellow')} ({_format_bytes(leftover_size)})")
209
+
210
+
211
+ def _display_detailed_recommendations(recommendations: list) -> None:
212
+ """Wyświetl szczegółowe informacje o każdej rekomendacji."""
213
+ click.echo(f"\n{click.style('='*60, fg='cyan')}")
214
+ click.echo(click.style("📖 SZCZEGÓŁY REKOMENDACJI", fg="cyan", bold=True))
215
+ click.echo(click.style(f"{'='*60}", fg="cyan"))
216
+
217
+ for i, rec in enumerate(recommendations, 1):
218
+ click.echo(f"\n{click.style(f'[{i}]', fg='cyan', bold=True)} {rec['description']}")
219
+ click.echo(click.style(f"{'-'*50}", fg="white", dim=True))
220
+ click.echo(f"\n{rec['explanation']}")
221
+
222
+ if rec.get('items'):
223
+ click.echo(f"\nElementy do usunięcia ({len(rec['items'])}):")
224
+ for item in rec['items'][:10]:
225
+ name = item.get('name', '?')
226
+ size = item.get('size_human', '?')
227
+ click.echo(f" • {name} ({size})")
228
+ if len(rec['items']) > 10:
229
+ click.echo(f" ... i {len(rec['items']) - 10} więcej")
@@ -0,0 +1,132 @@
1
+ """Home directory analysis and cleanup CLI handlers."""
2
+ from __future__ import annotations
3
+
4
+ import click
5
+
6
+ from fixos.cli._cleanup_utils import _format_bytes, _parse_numeric_range_set
7
+ from fixos.constants import MAX_HOME_LARGE_FILES_DISPLAY, MAX_HOME_LARGE_DIRS_DISPLAY
8
+
9
+
10
+ def _display_home_items(large_files: list, large_dirs: list) -> None:
11
+ """Print large files and directories found in home."""
12
+ if large_files:
13
+ click.echo(f"\n{click.style('📄 DUŻE PLIKI (>200MB):', fg='red', bold=True)}")
14
+ for i, f in enumerate(large_files[:MAX_HOME_LARGE_FILES_DISPLAY], 1):
15
+ click.echo(f" [{i:3d}] 📄 {click.style(f['path'], fg='cyan')}: {f['size_human']}")
16
+ if len(large_files) > MAX_HOME_LARGE_FILES_DISPLAY:
17
+ click.echo(f" ... i {len(large_files) - MAX_HOME_LARGE_FILES_DISPLAY} więcej")
18
+ if large_dirs:
19
+ click.echo(f"\n{click.style('📁 DUŻE FOLDERY (>500MB):', fg='magenta', bold=True)}")
20
+ offset = len(large_files)
21
+ for i, d in enumerate(large_dirs[:MAX_HOME_LARGE_DIRS_DISPLAY], 1):
22
+ click.echo(f" [{offset + i:3d}] 📁 {click.style(d['path'], fg='yellow')}: {d['size_human']}")
23
+ if len(large_dirs) > MAX_HOME_LARGE_DIRS_DISPLAY:
24
+ click.echo(f" ... i {len(large_dirs) - MAX_HOME_LARGE_DIRS_DISPLAY} więcej")
25
+
26
+
27
+ def _resolve_home_selection(nums: str, large_files: list, large_dirs: list) -> list:
28
+ """Convert index string into the list of selected home items."""
29
+ total_items = len(large_files) + len(large_dirs)
30
+ selected_indices = _parse_numeric_range_set(nums)
31
+ items = []
32
+ for i in selected_indices:
33
+ if 1 <= i <= len(large_files):
34
+ items.append(large_files[i - 1])
35
+ elif len(large_files) < i <= total_items:
36
+ items.append(large_dirs[i - len(large_files) - 1])
37
+ return items
38
+
39
+
40
+ def _remove_home_items(items_to_remove: list) -> None:
41
+ """Delete files/directories and report result."""
42
+ import os
43
+ import shutil
44
+ click.echo(f"\n{click.style('🚀 USUWANIE ELEMENTÓW:', fg='cyan', bold=True)}")
45
+ for item in items_to_remove:
46
+ click.echo(f"\n• {item['path']}")
47
+ try:
48
+ if item.get('type') == 'file':
49
+ os.remove(item['path'])
50
+ else:
51
+ shutil.rmtree(item['path'])
52
+ click.echo(click.style(" ✅ Usunięto", fg="green"))
53
+ except Exception as e:
54
+ click.echo(click.style(f" ❌ Błąd: {e}", fg="red"))
55
+
56
+
57
+ def _show_home_item_info(nums: str, large_files: list, large_dirs: list, total_items: int) -> None:
58
+ """Display info about a single home item by index."""
59
+ import os
60
+ import mimetypes
61
+ try:
62
+ idx = int(nums[5:])
63
+ if 1 <= idx <= len(large_files):
64
+ f = large_files[idx - 1]
65
+ click.echo(f"\n{click.style('📦 SZCZEGÓŁY PLIKU:', fg='yellow', bold=True)}")
66
+ click.echo(f" Ścieżka: {f['path']}")
67
+ click.echo(f" Rozmiar: {f['size_human']}")
68
+ if os.path.exists(f['path']):
69
+ click.echo(click.style(" ✅ Istnieje", fg="green"))
70
+ mime_type, _ = mimetypes.guess_type(f['path'])
71
+ if mime_type:
72
+ click.echo(f" Typ: {mime_type}")
73
+ else:
74
+ click.echo(click.style(" ⚠️ Nie istnieje", fg="yellow"))
75
+ elif len(large_files) < idx <= total_items:
76
+ d = large_dirs[idx - len(large_files) - 1]
77
+ click.echo(f"\n{click.style('📦 SZCZEGÓŁY FOLDERU:', fg='yellow', bold=True)}")
78
+ click.echo(f" Ścieżka: {d['path']}")
79
+ click.echo(f" Rozmiar: {d['size_human']}")
80
+ else:
81
+ click.echo(click.style(f"❌ Nieprawidłowy numer: {idx}", fg="red"))
82
+ except ValueError:
83
+ click.echo(click.style("❌ Format: info:N (np. info:5)", fg="red"))
84
+
85
+
86
+ def _handle_home_analysis(analyzer, dry_run: bool) -> None:
87
+ """Handle home directory large-file analysis and removal."""
88
+ large_files = getattr(analyzer, 'home_large_files', [])
89
+ large_dirs = getattr(analyzer, 'home_large_dirs', [])
90
+
91
+ if not large_files and not large_dirs:
92
+ click.echo(click.style("\n❌ Brak dużych plików w home do analizy.", fg="red"))
93
+ return
94
+
95
+ click.echo(f"\n{click.style('🏠 ANALIZA HOME DIRECTORY:', fg='yellow', bold=True)}")
96
+ click.echo(click.style(f"Znaleziono {len(large_files)} dużych plików (>200MB) i {len(large_dirs)} folderów (>500MB)", fg="white"))
97
+ _display_home_items(large_files, large_dirs)
98
+
99
+ total_items = len(large_files) + len(large_dirs)
100
+ click.echo(f"\n{click.style('💡 Wybierz numery plików/folderów do usunięcia:', fg='cyan')}")
101
+ click.echo(click.style(" • Pliki: 1-{}, Foldery: {}-{}".format(len(large_files), len(large_files)+1, total_items), fg="white"))
102
+
103
+ nums = click.prompt(click.style("\nWybierz numery (np. 1,3,5-10) lub 'info:N' dla szczegółów", fg="cyan"), default="")
104
+ if not nums.strip():
105
+ click.echo(click.style("⏭️ Nie wybrano żadnych elementów.", fg="yellow"))
106
+ return
107
+
108
+ nums = nums.strip().lower()
109
+ if nums.startswith('info:'):
110
+ _show_home_item_info(nums, large_files, large_dirs, total_items)
111
+ return
112
+
113
+ items_to_remove = _resolve_home_selection(nums, large_files, large_dirs)
114
+ if not items_to_remove:
115
+ click.echo(click.style("❌ Nie wybrano żadnych elementów.", fg="red"))
116
+ return
117
+
118
+ click.echo(f"\n{click.style('📦 WYBRANE ELEMENTY DO USUNIĘCIA:', fg='red', bold=True)}")
119
+ for i, item in enumerate(items_to_remove, 1):
120
+ icon = "📄" if item.get('type') == 'file' else "📁"
121
+ click.echo(f" {i:3d}. {icon} {click.style(item['path'], fg='cyan')}: {item['size_human']}")
122
+ total_size = sum(item['size'] for item in items_to_remove)
123
+ click.echo(f"\n{click.style('💰 Łącznie do usunięcia:', fg='red', bold=True)} {_format_bytes(total_size)}")
124
+
125
+ if dry_run:
126
+ click.echo(click.style("\n[DRY-RUN] - symulacja, nic nie zostanie usunięte", fg="cyan"))
127
+ return
128
+ if not click.confirm(click.style("\n⚠️ Potwierdzasz usunięcie tych elementów?", fg="yellow"), default=False):
129
+ click.echo(click.style("⏭️ Anulowano.", fg="yellow"))
130
+ return
131
+
132
+ _remove_home_items(items_to_remove)