fixos 2.2.35__tar.gz → 2.2.36__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 (164) hide show
  1. {fixos-2.2.35 → fixos-2.2.36}/CHANGELOG.md +23 -0
  2. {fixos-2.2.35 → fixos-2.2.36}/PKG-INFO +6 -6
  3. {fixos-2.2.35 → fixos-2.2.36}/README.md +5 -5
  4. {fixos-2.2.35 → fixos-2.2.36}/fixos/__init__.py +1 -1
  5. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/cleanup_cmd.py +132 -21
  6. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/cache_discovery.py +6 -1
  7. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/service_cleanup.py +97 -22
  8. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/service_scanner.py +100 -4
  9. {fixos-2.2.35 → fixos-2.2.36}/fixos.egg-info/PKG-INFO +6 -6
  10. {fixos-2.2.35 → fixos-2.2.36}/pyproject.toml +1 -1
  11. {fixos-2.2.35 → fixos-2.2.36}/setup.py +1 -1
  12. {fixos-2.2.35 → fixos-2.2.36}/tests/unit/test_service_cleanup.py +37 -0
  13. fixos-2.2.36/tests/unit/test_service_scanner.py +243 -0
  14. fixos-2.2.35/tests/unit/test_service_scanner.py +0 -102
  15. {fixos-2.2.35 → fixos-2.2.36}/.env.example +0 -0
  16. {fixos-2.2.35 → fixos-2.2.36}/LICENSE +0 -0
  17. {fixos-2.2.35 → fixos-2.2.36}/MANIFEST.in +0 -0
  18. {fixos-2.2.35 → fixos-2.2.36}/docker/README.md +0 -0
  19. {fixos-2.2.35 → fixos-2.2.36}/docker/TEST_RESULTS.md +0 -0
  20. {fixos-2.2.35 → fixos-2.2.36}/docker/TEST_RESULTS_V2.md +0 -0
  21. {fixos-2.2.35 → fixos-2.2.36}/docker/alpine/Dockerfile +0 -0
  22. {fixos-2.2.35 → fixos-2.2.36}/docker/arch/Dockerfile +0 -0
  23. {fixos-2.2.35 → fixos-2.2.36}/docker/base/Dockerfile +0 -0
  24. {fixos-2.2.35 → fixos-2.2.36}/docker/broken-audio/Dockerfile +0 -0
  25. {fixos-2.2.35 → fixos-2.2.36}/docker/broken-full/Dockerfile +0 -0
  26. {fixos-2.2.35 → fixos-2.2.36}/docker/broken-network/Dockerfile +0 -0
  27. {fixos-2.2.35 → fixos-2.2.36}/docker/broken-thumbnails/Dockerfile +0 -0
  28. {fixos-2.2.35 → fixos-2.2.36}/docker/debian/Dockerfile +0 -0
  29. {fixos-2.2.35 → fixos-2.2.36}/docker/docker-compose.multi-system.yml +0 -0
  30. {fixos-2.2.35 → fixos-2.2.36}/docker/docker-compose.yml +0 -0
  31. {fixos-2.2.35 → fixos-2.2.36}/docker/fedora/Dockerfile +0 -0
  32. {fixos-2.2.35 → fixos-2.2.36}/docker/test-multi-system.sh +0 -0
  33. {fixos-2.2.35 → fixos-2.2.36}/docker/test-scenarios.sh +0 -0
  34. {fixos-2.2.35 → fixos-2.2.36}/docker/ubuntu/Dockerfile +0 -0
  35. {fixos-2.2.35 → fixos-2.2.36}/docker/validate-scenario.py +0 -0
  36. {fixos-2.2.35 → fixos-2.2.36}/docs/examples/advanced_usage.py +0 -0
  37. {fixos-2.2.35 → fixos-2.2.36}/docs/examples/quickstart.py +0 -0
  38. {fixos-2.2.35 → fixos-2.2.36}/fixos/agent/__init__.py +0 -0
  39. {fixos-2.2.35 → fixos-2.2.36}/fixos/agent/autonomous.py +0 -0
  40. {fixos-2.2.35 → fixos-2.2.36}/fixos/agent/autonomous_session.py +0 -0
  41. {fixos-2.2.35 → fixos-2.2.36}/fixos/agent/hitl.py +0 -0
  42. {fixos-2.2.35 → fixos-2.2.36}/fixos/agent/hitl_session.py +0 -0
  43. {fixos-2.2.35 → fixos-2.2.36}/fixos/agent/session_core.py +0 -0
  44. {fixos-2.2.35 → fixos-2.2.36}/fixos/agent/session_handlers.py +0 -0
  45. {fixos-2.2.35 → fixos-2.2.36}/fixos/agent/session_io.py +0 -0
  46. {fixos-2.2.35 → fixos-2.2.36}/fixos/anonymizer.py +0 -0
  47. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/__init__.py +0 -0
  48. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/_cleanup_flatpak.py +0 -0
  49. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/_cleanup_home.py +0 -0
  50. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/_cleanup_snap.py +0 -0
  51. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/_cleanup_system.py +0 -0
  52. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/_cleanup_utils.py +0 -0
  53. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/ask_cmd.py +0 -0
  54. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/config_cmd.py +0 -0
  55. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/features_cmd.py +0 -0
  56. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/fix_cmd.py +0 -0
  57. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/history_cmd.py +0 -0
  58. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/main.py +0 -0
  59. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/orchestrate_cmd.py +0 -0
  60. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/output_formatter.py +0 -0
  61. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/profile_cmd.py +0 -0
  62. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/provider_cmd.py +0 -0
  63. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/quickfix_cmd.py +0 -0
  64. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/report_cmd.py +0 -0
  65. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/rollback_cmd.py +0 -0
  66. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/scan_cmd.py +0 -0
  67. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/shared.py +0 -0
  68. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/token_cmd.py +0 -0
  69. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli/watch_cmd.py +0 -0
  70. {fixos-2.2.35 → fixos-2.2.36}/fixos/cli.py +0 -0
  71. {fixos-2.2.35 → fixos-2.2.36}/fixos/config.py +0 -0
  72. {fixos-2.2.35 → fixos-2.2.36}/fixos/config_interactive.py +0 -0
  73. {fixos-2.2.35 → fixos-2.2.36}/fixos/constants.py +0 -0
  74. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/__init__.py +0 -0
  75. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
  76. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
  77. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
  78. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/_storage_container_mixin.py +0 -0
  79. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/_storage_system_mixin.py +0 -0
  80. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/_storage_user_mixin.py +0 -0
  81. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/__init__.py +0 -0
  82. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/_shared.py +0 -0
  83. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/audio.py +0 -0
  84. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/file_analysis.py +0 -0
  85. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/hardware.py +0 -0
  86. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/packages.py +0 -0
  87. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/resources.py +0 -0
  88. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/security.py +0 -0
  89. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/storage_optimization.py +0 -0
  90. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/system_core.py +0 -0
  91. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/checks/thumbnails.py +0 -0
  92. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  93. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/disk_analyzer.py +0 -0
  94. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  95. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/service_details.py +0 -0
  96. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/storage_analyzer.py +0 -0
  97. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/system_checks.py +0 -0
  98. {fixos-2.2.35 → fixos-2.2.36}/fixos/diagnostics/utils.py +0 -0
  99. {fixos-2.2.35 → fixos-2.2.36}/fixos/features/__init__.py +0 -0
  100. {fixos-2.2.35 → fixos-2.2.36}/fixos/features/auditor.py +0 -0
  101. {fixos-2.2.35 → fixos-2.2.36}/fixos/features/catalog.py +0 -0
  102. {fixos-2.2.35 → fixos-2.2.36}/fixos/features/installer.py +0 -0
  103. {fixos-2.2.35 → fixos-2.2.36}/fixos/features/profiles.py +0 -0
  104. {fixos-2.2.35 → fixos-2.2.36}/fixos/features/renderer.py +0 -0
  105. {fixos-2.2.35 → fixos-2.2.36}/fixos/fixes/__init__.py +0 -0
  106. {fixos-2.2.35 → fixos-2.2.36}/fixos/interactive/__init__.py +0 -0
  107. {fixos-2.2.35 → fixos-2.2.36}/fixos/interactive/cleanup_planner.py +0 -0
  108. {fixos-2.2.35 → fixos-2.2.36}/fixos/llm_shell.py +0 -0
  109. {fixos-2.2.35 → fixos-2.2.36}/fixos/orchestrator/__init__.py +0 -0
  110. {fixos-2.2.35 → fixos-2.2.36}/fixos/orchestrator/executor.py +0 -0
  111. {fixos-2.2.35 → fixos-2.2.36}/fixos/orchestrator/graph.py +0 -0
  112. {fixos-2.2.35 → fixos-2.2.36}/fixos/orchestrator/orchestrator.py +0 -0
  113. {fixos-2.2.35 → fixos-2.2.36}/fixos/orchestrator/rollback.py +0 -0
  114. {fixos-2.2.35 → fixos-2.2.36}/fixos/platform_utils.py +0 -0
  115. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/__init__.py +0 -0
  116. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/base.py +0 -0
  117. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/builtin/__init__.py +0 -0
  118. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/builtin/audio.py +0 -0
  119. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/builtin/disk.py +0 -0
  120. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/builtin/hardware.py +0 -0
  121. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/builtin/resources.py +0 -0
  122. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/builtin/security.py +0 -0
  123. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/builtin/thumbnails.py +0 -0
  124. {fixos-2.2.35 → fixos-2.2.36}/fixos/plugins/registry.py +0 -0
  125. {fixos-2.2.35 → fixos-2.2.36}/fixos/profiles/__init__.py +0 -0
  126. {fixos-2.2.35 → fixos-2.2.36}/fixos/providers/__init__.py +0 -0
  127. {fixos-2.2.35 → fixos-2.2.36}/fixos/providers/llm.py +0 -0
  128. {fixos-2.2.35 → fixos-2.2.36}/fixos/providers/llm_analyzer.py +0 -0
  129. {fixos-2.2.35 → fixos-2.2.36}/fixos/providers/schemas.py +0 -0
  130. {fixos-2.2.35 → fixos-2.2.36}/fixos/system_checks.py +0 -0
  131. {fixos-2.2.35 → fixos-2.2.36}/fixos/utils/__init__.py +0 -0
  132. {fixos-2.2.35 → fixos-2.2.36}/fixos/utils/anonymizer.py +0 -0
  133. {fixos-2.2.35 → fixos-2.2.36}/fixos/utils/terminal.py +0 -0
  134. {fixos-2.2.35 → fixos-2.2.36}/fixos/utils/timeout.py +0 -0
  135. {fixos-2.2.35 → fixos-2.2.36}/fixos/utils/web_search.py +0 -0
  136. {fixos-2.2.35 → fixos-2.2.36}/fixos/watch.py +0 -0
  137. {fixos-2.2.35 → fixos-2.2.36}/fixos.egg-info/SOURCES.txt +0 -0
  138. {fixos-2.2.35 → fixos-2.2.36}/fixos.egg-info/dependency_links.txt +0 -0
  139. {fixos-2.2.35 → fixos-2.2.36}/fixos.egg-info/entry_points.txt +0 -0
  140. {fixos-2.2.35 → fixos-2.2.36}/fixos.egg-info/requires.txt +0 -0
  141. {fixos-2.2.35 → fixos-2.2.36}/fixos.egg-info/top_level.txt +0 -0
  142. {fixos-2.2.35 → fixos-2.2.36}/pytest.ini +0 -0
  143. {fixos-2.2.35 → fixos-2.2.36}/requirements-dev.txt +0 -0
  144. {fixos-2.2.35 → fixos-2.2.36}/requirements.txt +0 -0
  145. {fixos-2.2.35 → fixos-2.2.36}/scripts/pyqual-calibrate.py +0 -0
  146. {fixos-2.2.35 → fixos-2.2.36}/setup.cfg +0 -0
  147. {fixos-2.2.35 → fixos-2.2.36}/tests/__init__.py +0 -0
  148. {fixos-2.2.35 → fixos-2.2.36}/tests/conftest.py +0 -0
  149. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/__init__.py +0 -0
  150. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/test_anonymization_layers.py +0 -0
  151. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/test_audio_broken.py +0 -0
  152. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/test_cli.py +0 -0
  153. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/test_diagnostics_integration.py +0 -0
  154. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/test_executor.py +0 -0
  155. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/test_multi_system.py +0 -0
  156. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/test_network_broken.py +0 -0
  157. {fixos-2.2.35 → fixos-2.2.36}/tests/e2e/test_thumbnails_broken.py +0 -0
  158. {fixos-2.2.35 → fixos-2.2.36}/tests/test_fixos.py +0 -0
  159. {fixos-2.2.35 → fixos-2.2.36}/tests/unit/__init__.py +0 -0
  160. {fixos-2.2.35 → fixos-2.2.36}/tests/unit/test_anonymizer.py +0 -0
  161. {fixos-2.2.35 → fixos-2.2.36}/tests/unit/test_cache_discovery.py +0 -0
  162. {fixos-2.2.35 → fixos-2.2.36}/tests/unit/test_core.py +0 -0
  163. {fixos-2.2.35 → fixos-2.2.36}/tests/unit/test_executor.py +0 -0
  164. {fixos-2.2.35 → fixos-2.2.36}/tests/unit/test_orchestrator.py +0 -0
@@ -150,6 +150,29 @@ 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.36] - 2026-07-15
154
+
155
+ ### Docs
156
+ - Update project/README.md
157
+ - Update project/context.md
158
+
159
+ ### Test
160
+ - Update tests/unit/test_service_cleanup.py
161
+ - Update tests/unit/test_service_scanner.py
162
+
163
+ ### Other
164
+ - Update fixos/cli/cleanup_cmd.py
165
+ - Update fixos/diagnostics/cache_discovery.py
166
+ - Update fixos/diagnostics/service_cleanup.py
167
+ - Update fixos/diagnostics/service_scanner.py
168
+ - Update project.sh
169
+ - Update project/analysis.toon.yaml
170
+ - Update project/calls.mmd
171
+ - Update project/calls.png
172
+ - Update project/calls.toon.yaml
173
+ - Update project/calls.yaml
174
+ - ... and 13 more files
175
+
153
176
  ## [2.2.35] - 2026-06-16
154
177
 
155
178
  ### Other
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.35
3
+ Version: 2.2.36
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,13 @@ 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.35-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-$3.66-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-30.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
66
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.36-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-$4.00-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-32.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
68
68
 
69
- - 🤖 **LLM usage:** $3.6649 (131 commits)
70
- - 👤 **Human dev:** ~$3047 (30.5h @ $100/h, 30min dedup)
69
+ - 🤖 **LLM usage:** $3.9985 (137 commits)
70
+ - 👤 **Human dev:** ~$3270 (32.7h @ $100/h, 30min dedup)
71
71
 
72
- Generated on 2026-06-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
72
+ Generated on 2026-07-06 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
73
73
 
74
74
  ---
75
75
 
@@ -19,13 +19,13 @@ 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.35-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-$3.66-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-30.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
22
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.36-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-$4.00-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-32.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
24
24
 
25
- - 🤖 **LLM usage:** $3.6649 (131 commits)
26
- - 👤 **Human dev:** ~$3047 (30.5h @ $100/h, 30min dedup)
25
+ - 🤖 **LLM usage:** $3.9985 (137 commits)
26
+ - 👤 **Human dev:** ~$3270 (32.7h @ $100/h, 30min dedup)
27
27
 
28
- Generated on 2026-06-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
28
+ Generated on 2026-07-06 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
29
29
 
30
30
  ---
31
31
 
@@ -1,3 +1,3 @@
1
1
  """fixos – AI-powered Linux/Windows diagnostics and repair."""
2
2
 
3
- __version__ = "2.2.35"
3
+ __version__ = "2.2.36"
@@ -33,11 +33,34 @@ def _display_cleanup_summary(plan: dict, threshold: int) -> None:
33
33
 
34
34
  click.echo(f"Znaleziono {plan['services_found']} usług:")
35
35
  click.echo(f" Całkowity rozmiar: {plan['total_size_gb']:.2f} GB")
36
- click.echo(f" Bezpieczne do usunięcia: {plan['safe_cleanup_gb']:.2f} GB")
37
- click.echo(f" Wymaga przeglądu: {plan['requires_review_gb']:.2f} GB")
36
+ click.echo(
37
+ click.style(
38
+ f" Bezpieczne do usunięcia: {plan['safe_cleanup_gb']:.2f} GB", fg="green"
39
+ )
40
+ )
41
+ click.echo(
42
+ click.style(
43
+ f" Do rozważenia: {plan['requires_review_gb']:.2f} GB", fg="yellow"
44
+ )
45
+ )
46
+ click.echo(
47
+ click.style(
48
+ f" Niebezpieczne (rzeczywiste dane, nie cache): "
49
+ f"{plan.get('dangerous_gb', 0):.2f} GB",
50
+ fg="red",
51
+ bold=True,
52
+ )
53
+ )
38
54
  click.echo()
39
55
 
40
56
 
57
+ _RISK_LABELS = {
58
+ "safe": ("(bezpieczne)", "green"),
59
+ "review": ("(do rozważenia)", "yellow"),
60
+ "dangerous": ("(NIEBEZPIECZNE — realne dane, nie cache)", "red"),
61
+ }
62
+
63
+
41
64
  def _display_service_item(svc: dict) -> None:
42
65
  """Display a single service item with details."""
43
66
  size_str = (
@@ -45,11 +68,11 @@ def _display_service_item(svc: dict) -> None:
45
68
  if svc["size_gb"] >= 1
46
69
  else f"{svc['size_mb']:.0f} MB"
47
70
  )
48
- safe_icon = " " if svc["safe_to_cleanup"] else " "
49
- safe_text = "(bezpieczne)" if svc["safe_to_cleanup"] else "(wymaga przeglądu)"
71
+ risk = svc.get("risk_level") or ("safe" if svc["safe_to_cleanup"] else "review")
72
+ safe_text, safe_color = _RISK_LABELS.get(risk, _RISK_LABELS["review"])
50
73
 
51
74
  click.echo(
52
- f"{safe_icon} {click.style(svc['name'], fg='yellow', bold=True)} - {size_str}"
75
+ f" {click.style(svc['name'], fg='yellow', bold=True)} - {size_str}"
53
76
  )
54
77
  click.echo(f" {svc['description']}")
55
78
  paths = svc.get("details", {}).get("paths") or [svc["path"]]
@@ -59,7 +82,7 @@ def _display_service_item(svc: dict) -> None:
59
82
  click.echo(f" • {path}")
60
83
  else:
61
84
  click.echo(f" Ścieżka: {svc['path']}")
62
- click.echo(f" {safe_text}")
85
+ click.echo(f" {click.style(safe_text, fg=safe_color, bold=(risk == 'dangerous'))}")
63
86
 
64
87
  # Show details for specific services
65
88
  if svc.get("details"):
@@ -130,42 +153,113 @@ def _display_service_group(service_type: str, svcs: list, type_map: dict) -> Non
130
153
  _format_hint_line(hint)
131
154
 
132
155
 
156
+ def _group_by_service_type(services: list) -> dict:
157
+ groups: dict = {}
158
+ for svc in services:
159
+ service_type = svc.get("service_type", "unknown")
160
+ groups.setdefault(service_type, []).append(svc)
161
+ return groups
162
+
163
+
133
164
  def _display_unsafe_services(services: list) -> None:
134
- """Display services that require manual review."""
165
+ """Display services that are worth a look before deleting (reinstallable
166
+ apps, long-unused tool data, unrecognized cache directories)."""
135
167
  from fixos.diagnostics.service_scanner import ServiceType
136
168
 
137
169
  click.echo()
138
- click.echo(click.style("Usługi wymagające przeglądu:", fg="yellow"))
139
-
140
- service_groups: dict = {}
141
- for svc in services:
142
- service_type = svc.get("service_type", "unknown")
143
- if service_type not in service_groups:
144
- service_groups[service_type] = []
145
- service_groups[service_type].append(svc)
170
+ click.echo(click.style("Do rozważenia (nie usuwane automatycznie):", fg="yellow"))
146
171
 
147
172
  type_map = {
148
173
  "flatpak": ServiceType.FLATPAK,
174
+ "generic_cache": ServiceType.GENERIC_CACHE,
175
+ }
176
+
177
+ for service_type, svcs in _group_by_service_type(services).items():
178
+ _display_service_group(service_type, svcs, type_map)
179
+
180
+ click.echo()
181
+ click.echo(
182
+ click.style(
183
+ "💡 Wskazówki: Powyższe komendy są zwykle bezpieczne (usuwają nieużywane "
184
+ "wersje/runtime) i mogą odzyskać dużo miejsca — warto jednak zerknąć "
185
+ "zanim je uruchomisz",
186
+ fg="green",
187
+ )
188
+ )
189
+
190
+
191
+ def _display_dangerous_services(services: list) -> None:
192
+ """Display services holding real installed application data (models,
193
+ containers/volumes, editor extensions, VM disks) rather than a cache.
194
+
195
+ Never auto-cleaned in bulk. Shown separately with an explicit warning so
196
+ it can't be mistaken for one of the "safe" cache entries.
197
+ """
198
+ from fixos.diagnostics.service_scanner import ServiceType
199
+
200
+ if not services:
201
+ return
202
+
203
+ click.echo()
204
+ click.echo(
205
+ click.style(
206
+ "⚠️ Niebezpieczne — realne dane, nie cache (wymaga ręcznej, świadomej akcji):",
207
+ fg="red",
208
+ bold=True,
209
+ )
210
+ )
211
+ click.echo(
212
+ click.style(
213
+ " To nie jest cache do odtworzenia jednym poleceniem — to np. "
214
+ "zainstalowane modele AI, kontenery/woluminy, dyski maszyn "
215
+ "wirtualnych albo zainstalowane rozszerzenia edytora. Usunięcie "
216
+ "może oznaczać realną utratę danych lub długi re-download.",
217
+ fg="red",
218
+ )
219
+ )
220
+
221
+ type_map = {
149
222
  "docker": ServiceType.DOCKER,
223
+ "containerd": ServiceType.CONTAINERD,
224
+ "podman": ServiceType.PODMAN,
150
225
  "ollama": ServiceType.OLLAMA,
151
- "steam": ServiceType.STEAM,
152
- "minikube": ServiceType.MINIKUBE,
153
226
  "lmstudio": ServiceType.LMSTUDIO,
154
- "generic_cache": ServiceType.GENERIC_CACHE,
227
+ "huggingface": ServiceType.HUGGINGFACE,
228
+ "jupyter": ServiceType.JUPYTER,
229
+ "minikube": ServiceType.MINIKUBE,
230
+ "appimage": ServiceType.APPIMAGE,
231
+ "virtualbox": ServiceType.VBOX,
232
+ "vmware": ServiceType.VMWARE,
233
+ "steam": ServiceType.STEAM,
234
+ "cursor": ServiceType.CURSOR,
235
+ "vscode": ServiceType.VSCODE,
155
236
  }
156
237
 
157
- for service_type, svcs in service_groups.items():
238
+ for service_type, svcs in _group_by_service_type(services).items():
158
239
  _display_service_group(service_type, svcs, type_map)
159
240
 
160
241
  click.echo()
161
242
  click.echo(
162
243
  click.style(
163
- "💡 Wskazówki: Powyższe komendy są bezpieczne i często odzyskują dużo miejsca",
164
- fg="green",
244
+ " Aby usunąć pojedynczą usługę świadomie: "
245
+ "fixos cleanup -c <usluga> (poprzedź --dry-run, by zobaczyć co zrobi)",
246
+ fg="yellow",
165
247
  )
166
248
  )
167
249
 
168
250
 
251
+ def _service_has_dangerous_data(service_name: str, scanner) -> bool:
252
+ """True if any currently-scanned path for this service is 'dangerous'."""
253
+ from fixos.diagnostics.service_scanner import RiskLevel, ServiceType
254
+
255
+ try:
256
+ service_enum = ServiceType(service_name)
257
+ except ValueError:
258
+ return False
259
+ scanned = scanner.scan_service(service_enum)
260
+ return any(s.risk_level == RiskLevel.DANGEROUS.value for s in scanned)
261
+
262
+
169
263
  def _cleanup_single_service(
170
264
  service_name: str, scanner, json_output: bool, dry_run: bool
171
265
  ) -> None:
@@ -177,6 +271,21 @@ def _cleanup_single_service(
177
271
  click.echo(json.dumps(result, indent=2, default=str))
178
272
  return
179
273
 
274
+ if not dry_run and _service_has_dangerous_data(service_name, scanner):
275
+ click.echo(
276
+ click.style(
277
+ f"⚠️ {service_name} zawiera realne dane (nie tylko cache) — np. "
278
+ "zainstalowane modele/rozszerzenia/kontenery. Tej operacji nie "
279
+ "da się cofnąć.",
280
+ fg="red",
281
+ bold=True,
282
+ )
283
+ )
284
+ click.echo(f" Podgląd bez usuwania: fixos cleanup -c {service_name} --dry-run")
285
+ if not click.confirm(f"Na pewno usunąć dane usługi {service_name}?"):
286
+ click.echo("Anulowano.")
287
+ return
288
+
180
289
  click.echo(click.style(f"Czyszczenie usługi: {service_name}", fg="yellow"))
181
290
  if dry_run:
182
291
  click.echo(click.style("[TRYB DRY-RUN] - brak faktycznych zmian", fg="cyan"))
@@ -215,6 +324,8 @@ def _run_interactive_cleanup(plan: dict, list_only: bool, scanner) -> None:
215
324
  _execute_safe_cleanup(plan["safe_to_cleanup"], scanner)
216
325
  if plan["requires_review"] and not list_only:
217
326
  _display_unsafe_services(plan["requires_review"])
327
+ if plan.get("dangerous") and not list_only:
328
+ _display_dangerous_services(plan["dangerous"])
218
329
 
219
330
 
220
331
  # ── Main CLI command ──────────────────────────────────────────────────────
@@ -229,12 +229,16 @@ def _build_generic_entry(
229
229
  service_type: "ServiceType" = None,
230
230
  ) -> "ServiceDataInfo":
231
231
  from .service_cleanup import ServiceCleaner
232
- from .service_scanner import ServiceDataInfo, ServiceType
232
+ from .service_scanner import RiskLevel, ServiceDataInfo, ServiceType
233
233
 
234
234
  if service_type is None:
235
235
  service_type = ServiceType.GENERIC_CACHE
236
236
  size_gb = size_mb / 1024
237
237
  cleanup_command = ServiceCleaner.get_cleanup_command(service_type, path)
238
+ # Discovered dirs are only ever "safe" (recognized cache name pattern) or
239
+ # "review" (unrecognized, look before deleting) — never "dangerous",
240
+ # since we have no basis to claim it's real installed-app data.
241
+ risk_level = RiskLevel.SAFE.value if safe else RiskLevel.REVIEW.value
238
242
  return ServiceDataInfo(
239
243
  service_type=service_type,
240
244
  name=label,
@@ -248,4 +252,5 @@ def _build_generic_entry(
248
252
  safe_to_cleanup=safe,
249
253
  impact="high" if size_gb > 1.0 else "medium",
250
254
  details={"discovered": True, "source": "cache_discovery"},
255
+ risk_level=risk_level,
251
256
  )
@@ -20,7 +20,9 @@ class ServiceCleaner:
20
20
  self.scanner = scanner
21
21
 
22
22
  def get_cleanup_plan(self, selected_services: List[str] = None) -> Dict[str, Any]:
23
- """Generate cleanup plan for services."""
23
+ """Generate cleanup plan for services, split into 3 risk tiers."""
24
+ from .service_scanner import RiskLevel
25
+
24
26
  services = self.scanner.scan_all_services()
25
27
 
26
28
  if selected_services:
@@ -29,18 +31,25 @@ class ServiceCleaner:
29
31
  ]
30
32
 
31
33
  total_size_gb = sum(s.size_gb for s in services)
32
- safe_services = [s for s in services if s.safe_to_cleanup]
33
- unsafe_services = [s for s in services if not s.safe_to_cleanup]
34
+ safe_services = [s for s in services if s.risk_level == RiskLevel.SAFE.value]
35
+ review_services = [
36
+ s for s in services if s.risk_level == RiskLevel.REVIEW.value
37
+ ]
38
+ dangerous_services = [
39
+ s for s in services if s.risk_level == RiskLevel.DANGEROUS.value
40
+ ]
34
41
 
35
42
  plan = {
36
43
  "threshold_mb": self.scanner.threshold_mb,
37
44
  "services_found": len(services),
38
45
  "total_size_gb": round(total_size_gb, 2),
39
46
  "safe_cleanup_gb": round(sum(s.size_gb for s in safe_services), 2),
40
- "requires_review_gb": round(sum(s.size_gb for s in unsafe_services), 2),
47
+ "requires_review_gb": round(sum(s.size_gb for s in review_services), 2),
48
+ "dangerous_gb": round(sum(s.size_gb for s in dangerous_services), 2),
41
49
  "services": [self._service_to_dict(s) for s in services],
42
50
  "safe_to_cleanup": [self._service_to_dict(s) for s in safe_services],
43
- "requires_review": [self._service_to_dict(s) for s in unsafe_services],
51
+ "requires_review": [self._service_to_dict(s) for s in review_services],
52
+ "dangerous": [self._service_to_dict(s) for s in dangerous_services],
44
53
  }
45
54
 
46
55
  return plan
@@ -117,18 +126,32 @@ class ServiceCleaner:
117
126
  "cleanup_command": service.cleanup_command,
118
127
  "preview_command": service.preview_command,
119
128
  "safe_to_cleanup": service.safe_to_cleanup,
129
+ "risk_level": service.risk_level,
120
130
  "impact": service.impact,
121
131
  "items_count": service.items_count,
122
132
  "details": service.details,
123
133
  }
124
134
 
125
135
  @staticmethod
126
- def is_safe_cleanup(service_type, path: str | None = None) -> bool:
127
- """Determine if cleanup is generally safe (cache-only, not user data)."""
128
- from .service_scanner import ServiceType
136
+ def get_risk_level(service_type, path: str | None = None) -> str:
137
+ """Classify cleanup risk for a service path.
138
+
139
+ Returns one of ``RiskLevel.SAFE`` / ``REVIEW`` / ``DANGEROUS``
140
+ (as their string values):
141
+
142
+ - safe: rebuildable cache — re-downloads or regenerates itself
143
+ (package/build caches, browser caches, system package caches).
144
+ - review: recoverable but worth a look before deleting — reinstallable
145
+ apps, long-unused tool data, or unrecognized cache directories.
146
+ - dangerous: real installed application data that is not a simple
147
+ cache (AI models, containers/volumes, VM disks, editor extensions)
148
+ and may not be trivially recoverable.
149
+ """
150
+ from .service_scanner import RiskLevel, ServiceType
151
+
152
+ normalized_path = os.path.expanduser(path or "").rstrip("/")
129
153
 
130
154
  if service_type == ServiceType.CHROME:
131
- normalized_path = os.path.expanduser(path or "").rstrip("/")
132
155
  chrome_cache_root = os.path.expanduser("~/.cache/google-chrome").rstrip("/")
133
156
  chrome_cache_names = {
134
157
  "Cache",
@@ -143,34 +166,70 @@ class ServiceCleaner:
143
166
  if normalized_path == chrome_cache_root or normalized_path.startswith(
144
167
  f"{chrome_cache_root}/"
145
168
  ):
146
- return True
169
+ return RiskLevel.SAFE.value
147
170
 
148
171
  if os.path.basename(normalized_path) in chrome_cache_names:
149
- return True
172
+ return RiskLevel.SAFE.value
150
173
 
151
- return False
174
+ return RiskLevel.REVIEW.value
152
175
 
153
176
  if service_type == ServiceType.BRAVE:
154
- normalized_path = os.path.expanduser(path or "").rstrip("/")
155
177
  if "/Cache" in normalized_path or normalized_path.endswith(
156
178
  ("GPUCache", "Code Cache")
157
179
  ):
158
- return True
159
- return normalized_path.endswith("BraveSoftware") or "/BraveSoftware/" in (
180
+ return RiskLevel.SAFE.value
181
+ if normalized_path.endswith("BraveSoftware") or "/BraveSoftware/" in (
160
182
  normalized_path
161
- )
183
+ ):
184
+ return RiskLevel.SAFE.value
185
+ return RiskLevel.REVIEW.value
162
186
 
163
187
  if service_type == ServiceType.STEAM:
164
- normalized_path = os.path.expanduser(path or "").rstrip("/")
165
188
  safe_suffixes = ("shadercache", "appcache")
166
- return any(normalized_path.endswith(suffix) for suffix in safe_suffixes)
189
+ if any(normalized_path.endswith(suffix) for suffix in safe_suffixes):
190
+ return RiskLevel.SAFE.value
191
+ # Anything else is the actual Steam library/client: installed
192
+ # games and their save data, not a cache.
193
+ return RiskLevel.DANGEROUS.value
194
+
195
+ if service_type in (ServiceType.CURSOR, ServiceType.VSCODE):
196
+ # The extensions directory holds real installed extensions (and
197
+ # any local-only extension data), not a cache — reinstalling
198
+ # each one by hand is real work and some local state won't come
199
+ # back. Everything else these two scan (Cache/CachedData/logs)
200
+ # is a plain, rebuildable cache.
201
+ if os.path.basename(normalized_path) == "extensions":
202
+ return RiskLevel.DANGEROUS.value
203
+ return RiskLevel.SAFE.value
167
204
 
168
205
  if service_type in (ServiceType.GENERIC_CACHE, ServiceType.ELECTRON):
169
- base = os.path.basename(os.path.expanduser(path or "").rstrip("/")).lower()
170
- return any(
206
+ base = os.path.basename(normalized_path).lower()
207
+ if any(
171
208
  hint in base
172
209
  for hint in ("cache", "shader", "tmp", "temp", "log", "crash", "thumb")
173
- )
210
+ ):
211
+ return RiskLevel.SAFE.value
212
+ return RiskLevel.REVIEW.value
213
+
214
+ dangerous_services = {
215
+ # Bulk-wipe commands that remove every installed model/container,
216
+ # not just unused/old ones.
217
+ ServiceType.DOCKER,
218
+ ServiceType.CONTAINERD,
219
+ ServiceType.PODMAN,
220
+ ServiceType.OLLAMA,
221
+ ServiceType.LMSTUDIO,
222
+ ServiceType.HUGGINGFACE,
223
+ ServiceType.JUPYTER,
224
+ ServiceType.MINIKUBE,
225
+ # Actual installed apps, not a cache directory.
226
+ ServiceType.APPIMAGE,
227
+ # VM disks/snapshots: unique state, not redownloadable.
228
+ ServiceType.VBOX,
229
+ ServiceType.VMWARE,
230
+ }
231
+ if service_type in dangerous_services:
232
+ return RiskLevel.DANGEROUS.value
174
233
 
175
234
  safe_services = {
176
235
  # Package caches (can be re-downloaded)
@@ -178,6 +237,7 @@ class ServiceCleaner:
178
237
  ServiceType.YARN,
179
238
  ServiceType.PNPM,
180
239
  ServiceType.PIP,
240
+ ServiceType.CONDA,
181
241
  ServiceType.POETRY,
182
242
  ServiceType.GRADLE,
183
243
  ServiceType.MAVEN,
@@ -196,6 +256,8 @@ class ServiceCleaner:
196
256
  ServiceType.SLACK,
197
257
  ServiceType.SPOTIFY,
198
258
  ServiceType.BRAVE,
259
+ ServiceType.NIX,
260
+ ServiceType.BREW,
199
261
  # System caches
200
262
  ServiceType.APT,
201
263
  ServiceType.DNF,
@@ -216,7 +278,20 @@ class ServiceCleaner:
216
278
  ServiceType.TERRAFORM,
217
279
  ServiceType.PULUMI,
218
280
  }
219
- return service_type in safe_services
281
+ if service_type in safe_services:
282
+ return RiskLevel.SAFE.value
283
+
284
+ # Default: reinstallable apps / long-unused tool data / unclassified
285
+ # (Flatpak, Snap, Android SDK, JetBrains, Vagrant, Unity, ...) —
286
+ # worth a look before deleting, but not flagged as high-risk.
287
+ return RiskLevel.REVIEW.value
288
+
289
+ @staticmethod
290
+ def is_safe_cleanup(service_type, path: str | None = None) -> bool:
291
+ """Backward-compatible bool view of get_risk_level() == SAFE."""
292
+ from .service_scanner import RiskLevel
293
+
294
+ return ServiceCleaner.get_risk_level(service_type, path) == RiskLevel.SAFE.value
220
295
 
221
296
  @staticmethod
222
297
  def get_cleanup_hints(service_type, size_gb: float) -> List[str]: