fixos 2.2.52__tar.gz → 2.2.53__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 (233) hide show
  1. {fixos-2.2.52 → fixos-2.2.53}/PKG-INFO +1 -1
  2. fixos-2.2.53/VERSION +1 -0
  3. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/_cleanup_space.py +36 -4
  4. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/cleanup_cmd.py +23 -5
  5. {fixos-2.2.52 → fixos-2.2.53}/fixos/config.py +24 -2
  6. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/native_scan.py +15 -3
  7. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/quick_snapshot.py +31 -2
  8. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/service_cleanup.py +51 -6
  9. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/service_scanner.py +61 -2
  10. {fixos-2.2.52 → fixos-2.2.53}/fixos/interactive/cleanup_planner.py +26 -28
  11. {fixos-2.2.52 → fixos-2.2.53}/fixos.egg-info/PKG-INFO +1 -1
  12. {fixos-2.2.52 → fixos-2.2.53}/fixos.egg-info/SOURCES.txt +2 -0
  13. {fixos-2.2.52 → fixos-2.2.53}/pyproject.toml +1 -1
  14. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cleanup_effectiveness.py +93 -0
  15. fixos-2.2.53/tests/unit/test_cleanup_planner.py +41 -0
  16. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cleanup_tmp.py +43 -0
  17. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_native_scan.py +26 -0
  18. fixos-2.2.53/tests/unit/test_ollama_endpoint.py +46 -0
  19. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_quick_snapshot.py +33 -0
  20. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_service_cleanup.py +75 -0
  21. fixos-2.2.52/VERSION +0 -1
  22. {fixos-2.2.52 → fixos-2.2.53}/.env.example +0 -0
  23. {fixos-2.2.52 → fixos-2.2.53}/CHANGELOG.md +0 -0
  24. {fixos-2.2.52 → fixos-2.2.53}/LICENSE +0 -0
  25. {fixos-2.2.52 → fixos-2.2.53}/MANIFEST.in +0 -0
  26. {fixos-2.2.52 → fixos-2.2.53}/README.md +0 -0
  27. {fixos-2.2.52 → fixos-2.2.53}/docker/README.md +0 -0
  28. {fixos-2.2.52 → fixos-2.2.53}/docker/TEST_RESULTS.md +0 -0
  29. {fixos-2.2.52 → fixos-2.2.53}/docker/TEST_RESULTS_V2.md +0 -0
  30. {fixos-2.2.52 → fixos-2.2.53}/docker/alpine/Dockerfile +0 -0
  31. {fixos-2.2.52 → fixos-2.2.53}/docker/arch/Dockerfile +0 -0
  32. {fixos-2.2.52 → fixos-2.2.53}/docker/base/Dockerfile +0 -0
  33. {fixos-2.2.52 → fixos-2.2.53}/docker/broken-audio/Dockerfile +0 -0
  34. {fixos-2.2.52 → fixos-2.2.53}/docker/broken-full/Dockerfile +0 -0
  35. {fixos-2.2.52 → fixos-2.2.53}/docker/broken-network/Dockerfile +0 -0
  36. {fixos-2.2.52 → fixos-2.2.53}/docker/broken-thumbnails/Dockerfile +0 -0
  37. {fixos-2.2.52 → fixos-2.2.53}/docker/debian/Dockerfile +0 -0
  38. {fixos-2.2.52 → fixos-2.2.53}/docker/docker-compose.multi-system.yml +0 -0
  39. {fixos-2.2.52 → fixos-2.2.53}/docker/docker-compose.yml +0 -0
  40. {fixos-2.2.52 → fixos-2.2.53}/docker/fedora/Dockerfile +0 -0
  41. {fixos-2.2.52 → fixos-2.2.53}/docker/test-multi-system.sh +0 -0
  42. {fixos-2.2.52 → fixos-2.2.53}/docker/test-scenarios.sh +0 -0
  43. {fixos-2.2.52 → fixos-2.2.53}/docker/ubuntu/Dockerfile +0 -0
  44. {fixos-2.2.52 → fixos-2.2.53}/docker/validate-scenario.py +0 -0
  45. {fixos-2.2.52 → fixos-2.2.53}/docs/examples/advanced_usage.py +0 -0
  46. {fixos-2.2.52 → fixos-2.2.53}/docs/examples/quickstart.py +0 -0
  47. {fixos-2.2.52 → fixos-2.2.53}/fixos/__init__.py +0 -0
  48. {fixos-2.2.52 → fixos-2.2.53}/fixos/__main__.py +0 -0
  49. {fixos-2.2.52 → fixos-2.2.53}/fixos/agent/__init__.py +0 -0
  50. {fixos-2.2.52 → fixos-2.2.53}/fixos/agent/autonomous.py +0 -0
  51. {fixos-2.2.52 → fixos-2.2.53}/fixos/agent/autonomous_session.py +0 -0
  52. {fixos-2.2.52 → fixos-2.2.53}/fixos/agent/hitl.py +0 -0
  53. {fixos-2.2.52 → fixos-2.2.53}/fixos/agent/hitl_session.py +0 -0
  54. {fixos-2.2.52 → fixos-2.2.53}/fixos/agent/session_core.py +0 -0
  55. {fixos-2.2.52 → fixos-2.2.53}/fixos/agent/session_handlers.py +0 -0
  56. {fixos-2.2.52 → fixos-2.2.53}/fixos/agent/session_io.py +0 -0
  57. {fixos-2.2.52 → fixos-2.2.53}/fixos/anonymizer.py +0 -0
  58. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/__init__.py +0 -0
  59. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/__main__.py +0 -0
  60. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/_cleanup_flatpak.py +0 -0
  61. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/_cleanup_home.py +0 -0
  62. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/_cleanup_snap.py +0 -0
  63. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/_cleanup_system.py +0 -0
  64. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/_cleanup_utils.py +0 -0
  65. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/_cleanup_venvs.py +0 -0
  66. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/ask_cmd.py +0 -0
  67. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/config_cmd.py +0 -0
  68. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/dashboard.py +0 -0
  69. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/features_cmd.py +0 -0
  70. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/fix_cmd.py +0 -0
  71. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/history_cmd.py +0 -0
  72. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/jetbrains_cmd.py +0 -0
  73. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/main.py +0 -0
  74. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/orchestrate_cmd.py +0 -0
  75. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/output_formatter.py +0 -0
  76. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/profile_cmd.py +0 -0
  77. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/projects_cmd.py +0 -0
  78. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/provider_cmd.py +0 -0
  79. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/quick_cmd.py +0 -0
  80. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/quickfix_cmd.py +0 -0
  81. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/report_cmd.py +0 -0
  82. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/rollback_cmd.py +0 -0
  83. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/scan_cmd.py +0 -0
  84. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/shared.py +0 -0
  85. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/shell_cmd.py +0 -0
  86. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/token_cmd.py +0 -0
  87. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli/watch_cmd.py +0 -0
  88. {fixos-2.2.52 → fixos-2.2.53}/fixos/cli.py +0 -0
  89. {fixos-2.2.52 → fixos-2.2.53}/fixos/config_interactive.py +0 -0
  90. {fixos-2.2.52 → fixos-2.2.53}/fixos/constants.py +0 -0
  91. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/__init__.py +0 -0
  92. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
  93. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
  94. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
  95. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/_storage_container_mixin.py +0 -0
  96. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/_storage_system_mixin.py +0 -0
  97. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/_storage_user_mixin.py +0 -0
  98. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/cache_discovery.py +0 -0
  99. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/__init__.py +0 -0
  100. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/_shared.py +0 -0
  101. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/audio.py +0 -0
  102. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/file_analysis.py +0 -0
  103. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/hardware.py +0 -0
  104. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/packages.py +0 -0
  105. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/resources.py +0 -0
  106. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/security.py +0 -0
  107. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/storage_optimization.py +0 -0
  108. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/system_core.py +0 -0
  109. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/checks/thumbnails.py +0 -0
  110. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  111. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/disk_analyzer.py +0 -0
  112. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/docker_network_cleanup.py +0 -0
  113. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
  114. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  115. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/jetbrains_ai.py +0 -0
  116. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/jetbrains_recovery.py +0 -0
  117. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/orphaned_workloads.py +0 -0
  118. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/process_chains.py +0 -0
  119. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/project_scanner.py +0 -0
  120. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/service_details.py +0 -0
  121. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/storage_analyzer.py +0 -0
  122. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/system_checks.py +0 -0
  123. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/utils.py +0 -0
  124. {fixos-2.2.52 → fixos-2.2.53}/fixos/diagnostics/venv_cleanup.py +0 -0
  125. {fixos-2.2.52 → fixos-2.2.53}/fixos/endpoint_refresh.py +0 -0
  126. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/__init__.py +0 -0
  127. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/auditor.py +0 -0
  128. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/catalog.py +0 -0
  129. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/data/packages.yaml +0 -0
  130. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/data/profiles/developer.yaml +0 -0
  131. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/data/profiles/office.yaml +0 -0
  132. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/data/profiles/sysadmin.yaml +0 -0
  133. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/installer.py +0 -0
  134. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/profiles.py +0 -0
  135. {fixos-2.2.52 → fixos-2.2.53}/fixos/features/renderer.py +0 -0
  136. {fixos-2.2.52 → fixos-2.2.53}/fixos/fixes/__init__.py +0 -0
  137. {fixos-2.2.52 → fixos-2.2.53}/fixos/interactive/__init__.py +0 -0
  138. {fixos-2.2.52 → fixos-2.2.53}/fixos/llm_shell.py +0 -0
  139. {fixos-2.2.52 → fixos-2.2.53}/fixos/orchestrator/__init__.py +0 -0
  140. {fixos-2.2.52 → fixos-2.2.53}/fixos/orchestrator/executor.py +0 -0
  141. {fixos-2.2.52 → fixos-2.2.53}/fixos/orchestrator/graph.py +0 -0
  142. {fixos-2.2.52 → fixos-2.2.53}/fixos/orchestrator/orchestrator.py +0 -0
  143. {fixos-2.2.52 → fixos-2.2.53}/fixos/orchestrator/rollback.py +0 -0
  144. {fixos-2.2.52 → fixos-2.2.53}/fixos/orphan_pins.py +0 -0
  145. {fixos-2.2.52 → fixos-2.2.53}/fixos/platform_utils.py +0 -0
  146. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/__init__.py +0 -0
  147. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/base.py +0 -0
  148. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/builtin/__init__.py +0 -0
  149. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/builtin/audio.py +0 -0
  150. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/builtin/disk.py +0 -0
  151. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/builtin/hardware.py +0 -0
  152. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/builtin/resources.py +0 -0
  153. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/builtin/security.py +0 -0
  154. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/builtin/thumbnails.py +0 -0
  155. {fixos-2.2.52 → fixos-2.2.53}/fixos/plugins/registry.py +0 -0
  156. {fixos-2.2.52 → fixos-2.2.53}/fixos/profiles/__init__.py +0 -0
  157. {fixos-2.2.52 → fixos-2.2.53}/fixos/profiles/desktop.yaml +0 -0
  158. {fixos-2.2.52 → fixos-2.2.53}/fixos/profiles/developer.yaml +0 -0
  159. {fixos-2.2.52 → fixos-2.2.53}/fixos/profiles/minimal.yaml +0 -0
  160. {fixos-2.2.52 → fixos-2.2.53}/fixos/profiles/server.yaml +0 -0
  161. {fixos-2.2.52 → fixos-2.2.53}/fixos/providers/__init__.py +0 -0
  162. {fixos-2.2.52 → fixos-2.2.53}/fixos/providers/gemini_native.py +0 -0
  163. {fixos-2.2.52 → fixos-2.2.53}/fixos/providers/llm.py +0 -0
  164. {fixos-2.2.52 → fixos-2.2.53}/fixos/providers/llm_analyzer.py +0 -0
  165. {fixos-2.2.52 → fixos-2.2.53}/fixos/providers/schemas.py +0 -0
  166. {fixos-2.2.52 → fixos-2.2.53}/fixos/system_checks.py +0 -0
  167. {fixos-2.2.52 → fixos-2.2.53}/fixos/utils/__init__.py +0 -0
  168. {fixos-2.2.52 → fixos-2.2.53}/fixos/utils/anonymizer.py +0 -0
  169. {fixos-2.2.52 → fixos-2.2.53}/fixos/utils/terminal.py +0 -0
  170. {fixos-2.2.52 → fixos-2.2.53}/fixos/utils/timeout.py +0 -0
  171. {fixos-2.2.52 → fixos-2.2.53}/fixos/utils/web_search.py +0 -0
  172. {fixos-2.2.52 → fixos-2.2.53}/fixos/watch.py +0 -0
  173. {fixos-2.2.52 → fixos-2.2.53}/fixos.egg-info/dependency_links.txt +0 -0
  174. {fixos-2.2.52 → fixos-2.2.53}/fixos.egg-info/entry_points.txt +0 -0
  175. {fixos-2.2.52 → fixos-2.2.53}/fixos.egg-info/requires.txt +0 -0
  176. {fixos-2.2.52 → fixos-2.2.53}/fixos.egg-info/top_level.txt +0 -0
  177. {fixos-2.2.52 → fixos-2.2.53}/pytest.ini +0 -0
  178. {fixos-2.2.52 → fixos-2.2.53}/requirements-dev.txt +0 -0
  179. {fixos-2.2.52 → fixos-2.2.53}/requirements.txt +0 -0
  180. {fixos-2.2.52 → fixos-2.2.53}/scripts/pyqual-calibrate.py +0 -0
  181. {fixos-2.2.52 → fixos-2.2.53}/setup.cfg +0 -0
  182. {fixos-2.2.52 → fixos-2.2.53}/setup.py +0 -0
  183. {fixos-2.2.52 → fixos-2.2.53}/tests/__init__.py +0 -0
  184. {fixos-2.2.52 → fixos-2.2.53}/tests/conftest.py +0 -0
  185. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/__init__.py +0 -0
  186. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/test_anonymization_layers.py +0 -0
  187. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/test_audio_broken.py +0 -0
  188. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/test_cli.py +0 -0
  189. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/test_diagnostics_integration.py +0 -0
  190. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/test_executor.py +0 -0
  191. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/test_multi_system.py +0 -0
  192. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/test_network_broken.py +0 -0
  193. {fixos-2.2.52 → fixos-2.2.53}/tests/e2e/test_thumbnails_broken.py +0 -0
  194. {fixos-2.2.52 → fixos-2.2.53}/tests/test_fixos.py +0 -0
  195. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/__init__.py +0 -0
  196. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_anonymizer.py +0 -0
  197. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_ask_and_nl_group.py +0 -0
  198. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cache_discovery.py +0 -0
  199. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cleanup_cmd.py +0 -0
  200. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cleanup_retention.py +0 -0
  201. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cleanup_safety.py +0 -0
  202. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cleanup_space.py +0 -0
  203. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cleanup_utils.py +0 -0
  204. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_cli_main.py +0 -0
  205. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_core.py +0 -0
  206. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_dashboard.py +0 -0
  207. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_dev_project_analyzer.py +0 -0
  208. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_disk_analyzer.py +0 -0
  209. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_docker_network_cleanup.py +0 -0
  210. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_docker_startup_optimizer.py +0 -0
  211. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_endpoint_refresh.py +0 -0
  212. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_executor.py +0 -0
  213. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_gemini_native_client.py +0 -0
  214. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_jetbrains_cmd.py +0 -0
  215. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_jetbrains_recovery.py +0 -0
  216. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_llm_auth_stop.py +0 -0
  217. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_llm_provider.py +0 -0
  218. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_orchestrator.py +0 -0
  219. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_orphan_pins.py +0 -0
  220. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_orphaned_workloads.py +0 -0
  221. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_packaging_data.py +0 -0
  222. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_process_chains.py +0 -0
  223. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_project_scanner.py +0 -0
  224. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_projects_cmd.py +0 -0
  225. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_quick_cmd.py +0 -0
  226. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_resources_quick.py +0 -0
  227. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_security_quick.py +0 -0
  228. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_service_scanner.py +0 -0
  229. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_shell_ux.py +0 -0
  230. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_storage_analyzer.py +0 -0
  231. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_system_checks_parallel.py +0 -0
  232. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_venv_cleanup.py +0 -0
  233. {fixos-2.2.52 → fixos-2.2.53}/tests/unit/test_version_parity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.52
3
+ Version: 2.2.53
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
fixos-2.2.53/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.2.53
@@ -52,6 +52,31 @@ def _run_cmd(argv: list[str], timeout: int = 60) -> subprocess.CompletedProcess:
52
52
  return subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
53
53
 
54
54
 
55
+ _DU_BATCH = 512
56
+
57
+
58
+ def _dir_sizes(paths: list[Path]) -> dict[str, int]:
59
+ """Sizes in bytes for many directories with one ``du`` per batch.
60
+
61
+ ``--count-links`` keeps each directory independent of the others, as a
62
+ separate ``du`` would be; NUL-terminated output survives any file name.
63
+ Paths missing from the result must be measured individually.
64
+ """
65
+ sizes: dict[str, int] = {}
66
+ for start in range(0, len(paths), _DU_BATCH):
67
+ batch = [str(path) for path in paths[start : start + _DU_BATCH]]
68
+ try:
69
+ result = _run_cmd(["du", "-sbl0", "--", *batch], timeout=300)
70
+ except (OSError, subprocess.TimeoutExpired):
71
+ continue
72
+ # du exits 1 when some subtree is unreadable but still prints totals.
73
+ for record in result.stdout.split("\0"):
74
+ size, sep, name = record.partition("\t")
75
+ if sep and size.isdigit():
76
+ sizes[name] = int(size)
77
+ return sizes
78
+
79
+
55
80
  def _dir_size(path: Path) -> int:
56
81
  """Return directory size in bytes (du -sb with a Python fallback)."""
57
82
  try:
@@ -876,6 +901,7 @@ def _scan_tmp(
876
901
  cutoff_seconds = days * 86400
877
902
  tmp_path = Path(tmp_dir) if tmp_dir is not None else Path("/tmp")
878
903
  candidates = []
904
+ directories = []
879
905
 
880
906
  if tmp_path.is_dir():
881
907
  try:
@@ -893,23 +919,29 @@ def _scan_tmp(
893
919
  mtime = stat_val.st_mtime
894
920
  age_seconds = now - mtime
895
921
  if age_seconds >= cutoff_seconds:
896
- size = (
897
- stat_val.st_size if not item.is_dir() else _dir_size(item)
898
- )
922
+ is_dir = stat_module.S_ISDIR(stat_val.st_mode)
899
923
  age_days = age_seconds / 86400
900
924
  candidates.append({
901
925
  "description": f"{name} (wiek: {age_days:.1f} dni)",
902
926
  "path": item,
903
927
  "name": name,
904
- "size_bytes": size,
928
+ "size_bytes": 0 if is_dir else stat_val.st_size,
905
929
  "note": f"{age_days:.1f} dni",
906
930
  "age_days": age_days,
907
931
  })
932
+ if is_dir:
933
+ directories.append(candidates[-1])
908
934
  except (OSError, PermissionError):
909
935
  continue
910
936
  except (OSError, PermissionError):
911
937
  pass
912
938
 
939
+ # One `du` per directory meant thousands of processes on a busy /tmp.
940
+ sizes = _dir_sizes([c["path"] for c in directories])
941
+ for candidate in directories:
942
+ size = sizes.get(str(candidate["path"]))
943
+ candidate["size_bytes"] = size if size is not None else _dir_size(candidate["path"])
944
+
913
945
  return candidates
914
946
 
915
947
 
@@ -129,11 +129,7 @@ def _clean_terminal_input(value: str) -> str:
129
129
 
130
130
  def _display_service_item(svc: dict) -> None:
131
131
  """Display a single service item with details."""
132
- size_str = (
133
- f"{svc['size_gb']:.2f} GB"
134
- if svc["size_gb"] >= 1
135
- else f"{svc['size_mb']:.0f} MB"
136
- )
132
+ size_str = _size_str(svc)
137
133
  risk = svc.get("risk_level") or ("safe" if svc["safe_to_cleanup"] else "review")
138
134
  safe_text, safe_color = _RISK_LABELS.get(risk, _RISK_LABELS["review"])
139
135
 
@@ -164,6 +160,8 @@ def _display_service_item(svc: dict) -> None:
164
160
  )
165
161
  else:
166
162
  click.echo(f" Komponenty: {svc['details']['components']}")
163
+ elif svc["service_type"] == "docker" and svc["details"].get("fast_inventory"):
164
+ _display_docker_inventory(svc["details"]["fast_inventory"])
167
165
  elif svc["service_type"] == "ollama" and svc["details"].get("models"):
168
166
  models = svc["details"]["models"]
169
167
  if models:
@@ -173,6 +171,26 @@ def _display_service_item(svc: dict) -> None:
173
171
  click.echo()
174
172
 
175
173
 
174
+ def _display_docker_inventory(inventory: dict) -> None:
175
+ """Show what a timed-out `docker system df` could not size."""
176
+ click.echo(" Docker — szybki spis (bez rozmiarów wolumenów):")
177
+ pairs = (
178
+ ("obrazy", "images", "dangling_images", "wiszących"),
179
+ ("kontenery", "containers", "stopped_containers", "zatrzymanych"),
180
+ ("wolumeny", "volumes", "dangling_volumes", "nieużywanych — dane, tylko ręcznie"),
181
+ )
182
+ for label, total_key, part_key, part_label in pairs:
183
+ if total_key in inventory:
184
+ part = inventory.get(part_key)
185
+ suffix = f" ({part} {part_label})" if part is not None else ""
186
+ click.echo(f" • {label}: {inventory[total_key]}{suffix}")
187
+ if "build_cache_total_gb" in inventory:
188
+ click.echo(
189
+ f" • cache buildów: {inventory['build_cache_total_gb']:.2f} GB "
190
+ f"(odzyskiwalne {inventory.get('build_cache_reclaimable_gb', 0):.2f} GB)"
191
+ )
192
+
193
+
176
194
  def _error_message(result: dict) -> str:
177
195
  """Human-readable failure reason, even when the command's own stderr was
178
196
  empty (e.g. suppressed with 2>/dev/null inside the cleanup command
@@ -128,7 +128,8 @@ PROVIDER_DEFAULTS = {
128
128
  "description": "Cerebras – najszybsze wnioskowanie na świecie, darmowy tier",
129
129
  },
130
130
  "ollama": {
131
- "base_url": "http://localhost:11434/v1",
131
+ # Resolved from OLLAMA_HOST at load time, see ollama_host_url().
132
+ "base_url": None,
132
133
  "model": "llama3.2",
133
134
  "key_env": None,
134
135
  "key_url": "https://ollama.com/download",
@@ -142,6 +143,22 @@ _OPENROUTER_MODEL_MIGRATIONS = {
142
143
  }
143
144
 
144
145
 
146
+ DEFAULT_OLLAMA_HOST = "127.0.0.1:11434"
147
+
148
+
149
+ def ollama_host_url() -> str:
150
+ """Ollama server URL from ``OLLAMA_HOST`` (the variable Ollama itself reads).
151
+
152
+ Single source for every fixOS component talking to Ollama: the LLM
153
+ provider and model cleanup previously disagreed when OLLAMA_HOST pointed
154
+ to another host.
155
+ """
156
+ host = os.environ.get("OLLAMA_HOST", "").strip() or DEFAULT_OLLAMA_HOST
157
+ if "://" not in host:
158
+ host = f"http://{host}"
159
+ return host.rstrip("/")
160
+
161
+
145
162
  def _normalize_model_id(provider: str, model: str) -> str:
146
163
  """Normalize provider-specific model IDs without changing valid choices."""
147
164
  normalized = model.strip()
@@ -293,7 +310,12 @@ class FixOsConfig:
293
310
 
294
311
  # Base URL
295
312
  url_env_key = f"{cfg.provider.upper()}_BASE_URL"
296
- cfg.base_url = base_url or os.environ.get(url_env_key) or pdef["base_url"]
313
+ cfg.base_url = (
314
+ base_url
315
+ or os.environ.get(url_env_key)
316
+ or pdef["base_url"]
317
+ or (f"{ollama_host_url()}/v1" if cfg.provider == "ollama" else None)
318
+ )
297
319
 
298
320
  # Transport Gemini: natywne API Google albo endpoint OpenAI-compatible
299
321
  cfg.gemini_transport = os.environ.get("GEMINI_TRANSPORT", "native").lower()
@@ -69,9 +69,19 @@ def measure_tree(
69
69
 
70
70
 
71
71
  def measure_batch(
72
- paths: list[Path], exclude: tuple[str, ...] = (), *, native: bool = True
72
+ paths: list[Path],
73
+ exclude: tuple[str, ...] = (),
74
+ *,
75
+ native: bool = True,
76
+ timeout: float = 120,
77
+ python_fallback: bool = True,
73
78
  ) -> dict[str, dict]:
74
- """Measure multiple paths, accelerating with Rust batch measurement if available."""
79
+ """Measure multiple paths, accelerating with Rust batch measurement if available.
80
+
81
+ Results are keyed by the resolved path. ``python_fallback=False`` returns
82
+ ``{}`` instead of an unbounded Python walk when the native batch is
83
+ missing, fails or exceeds ``timeout``; time-budgeted callers use it.
84
+ """
75
85
  if not paths:
76
86
  return {}
77
87
  if native:
@@ -84,7 +94,7 @@ def measure_batch(
84
94
  [str(executable), "batch-measure", *abs_paths],
85
95
  capture_output=True,
86
96
  text=True,
87
- timeout=120,
97
+ timeout=timeout,
88
98
  check=True,
89
99
  )
90
100
  data = json.loads(result.stdout)
@@ -103,6 +113,8 @@ def measure_batch(
103
113
  return res
104
114
  except (OSError, ValueError, subprocess.SubprocessError):
105
115
  pass
116
+ if not python_fallback:
117
+ return {}
106
118
  return {str(p.resolve()): measure_python(p, exclude) for p in paths}
107
119
 
108
120
 
@@ -348,11 +348,15 @@ def _measure_caches(
348
348
  from fixos.diagnostics.native_scan import measure_batch
349
349
 
350
350
  unique_paths = list({path for _, path in jobs})
351
- batch_result = measure_batch(unique_paths)
351
+ # The native batch had a 120 s limit and an unbounded Python
352
+ # fallback; on a busy disk that turned "quick" into minutes.
353
+ batch_result = measure_batch(
354
+ unique_paths, timeout=timeout * 2, python_fallback=False
355
+ )
352
356
  if isinstance(batch_result, dict) and batch_result:
353
357
  unresolved_jobs: list[tuple[CacheRule, Path]] = []
354
358
  for rule, path in jobs:
355
- m = batch_result.get(str(path))
359
+ m = batch_result.get(str(path.resolve()))
356
360
  if isinstance(m, dict) and "bytes" in m:
357
361
  kib = (m["bytes"] + 1023) // 1024
358
362
  measured.setdefault(rule.id, []).append((path, kib))
@@ -569,6 +573,28 @@ def _history_row(
569
573
  }
570
574
 
571
575
 
576
+ def _fill_from_history(
577
+ items: list[dict[str, Any]], history: list[dict[str, Any]]
578
+ ) -> None:
579
+ """Show the last complete size for caches that missed the time budget.
580
+
581
+ The item stays ``complete: False`` so growth reports never compare it.
582
+ """
583
+ for item in items:
584
+ if item.get("complete", True):
585
+ continue
586
+ for row in reversed(history):
587
+ if row.get("cache_complete", {}).get(item["id"]) and item["id"] in row.get(
588
+ "cache_sizes", {}
589
+ ):
590
+ size = int(row["cache_sizes"][item["id"]])
591
+ if size > item["size_bytes"]:
592
+ item["size_bytes"] = size
593
+ item["size_gb"] = round(size / 1024**3, 3)
594
+ item["last_complete_at"] = row.get("generated_at")
595
+ break
596
+
597
+
572
598
  def _parse_timestamp(row: dict[str, Any]) -> datetime | None:
573
599
  try:
574
600
  parsed = datetime.fromisoformat(str(row["generated_at"]))
@@ -836,6 +862,9 @@ def collect_quick_snapshot(
836
862
  history_path = _state_dir(state_dir) / "quick-history.json"
837
863
  history = _load_history(history_path)
838
864
  current_row = _history_row(generated_at, resources, safe_items, review_items)
865
+ _fill_from_history(safe_items + review_items, history)
866
+ safe_items.sort(key=lambda item: item["size_bytes"], reverse=True)
867
+ review_items.sort(key=lambda item: item["size_bytes"], reverse=True)
839
868
  growth = _growth_report(history, current_row, now, max(1, hours))
840
869
  alerts = _alerts(resources, growth, thresholds)
841
870
 
@@ -8,6 +8,7 @@ import os
8
8
  import re
9
9
  import shlex
10
10
  import shutil
11
+ import stat
11
12
  import subprocess
12
13
  import urllib.error
13
14
  import urllib.request
@@ -341,10 +342,9 @@ class ServiceCleaner:
341
342
 
342
343
  @staticmethod
343
344
  def _ollama_base_url() -> str:
344
- host = os.environ.get("OLLAMA_HOST", "127.0.0.1:11434").strip()
345
- if "://" not in host:
346
- host = f"http://{host}"
347
- return host.rstrip("/")
345
+ from ..config import ollama_host_url
346
+
347
+ return ollama_host_url()
348
348
 
349
349
  @staticmethod
350
350
  def _parse_ollama_modified_at(value: str) -> datetime:
@@ -1480,6 +1480,16 @@ class ServiceCleaner:
1480
1480
  result["estimated_max_gb"] = estimated_gb
1481
1481
  return result
1482
1482
 
1483
+ planned_paths = service.details.get("paths") or [service.path]
1484
+ # uv/pnpm/conda caches hardlink files into environments; deleting
1485
+ # the cache link frees nothing while another link survives.
1486
+ measure_shared = service_enum != ServiceType.DOCKER
1487
+ shared_before_gb = (
1488
+ self._shared_hardlink_bytes(planned_paths) / 1024**3
1489
+ if measure_shared
1490
+ else 0.0
1491
+ )
1492
+
1483
1493
  # Execute cleanup
1484
1494
  cleanup_result = subprocess.run(
1485
1495
  service.cleanup_command,
@@ -1492,7 +1502,6 @@ class ServiceCleaner:
1492
1502
 
1493
1503
  # Check new size
1494
1504
  measure = getattr(self.scanner, "measure_service_size_mb", None)
1495
- planned_paths = service.details.get("paths") or [service.path]
1496
1505
  if callable(measure):
1497
1506
  new_size_mb = sum(
1498
1507
  measure(service_enum, path, refresh=True) for path in planned_paths
@@ -1502,7 +1511,14 @@ class ServiceCleaner:
1502
1511
  self.scanner._get_path_size_mb(path) for path in planned_paths
1503
1512
  )
1504
1513
  new_size_gb = new_size_mb / 1024
1505
- freed_gb = max(0, initial_size - new_size_gb)
1514
+ shared_after_gb = (
1515
+ self._shared_hardlink_bytes(planned_paths) / 1024**3
1516
+ if measure_shared
1517
+ else 0.0
1518
+ )
1519
+ freed_gb = max(
1520
+ 0, (initial_size - shared_before_gb) - (new_size_gb - shared_after_gb)
1521
+ )
1506
1522
 
1507
1523
  result["success"] = cleanup_result.returncode == 0
1508
1524
  result["output"] = cleanup_result.stdout
@@ -1511,6 +1527,9 @@ class ServiceCleaner:
1511
1527
  result["space_freed_gb"] = round(freed_gb, 3)
1512
1528
  result["initial_size_gb"] = initial_size
1513
1529
  result["remaining_size_gb"] = round(new_size_gb, 3)
1530
+ result["shared_hardlink_gb"] = round(
1531
+ max(0.0, shared_before_gb - shared_after_gb), 3
1532
+ )
1514
1533
 
1515
1534
  except Exception as e: # noqa: BLE001 - stored in result["error"] for the caller to display
1516
1535
  result["error"] = str(e)
@@ -1528,6 +1547,32 @@ class ServiceCleaner:
1528
1547
  == "docker builder prune --force --filter until=168h"
1529
1548
  )
1530
1549
 
1550
+ @staticmethod
1551
+ def _shared_hardlink_bytes(paths: list[str]) -> int:
1552
+ """Allocated bytes under ``paths`` whose inode also has links elsewhere.
1553
+
1554
+ Removing such a link does not return blocks to the filesystem, so it
1555
+ must not count as freed space. Links inside the measured trees are
1556
+ counted together; only an inode with more links than seen is shared.
1557
+ """
1558
+ inodes: dict[tuple[int, int], list[int]] = {}
1559
+ for root in paths:
1560
+ if not root or os.path.islink(root) or not os.path.isdir(root):
1561
+ continue
1562
+ for dirpath, _dirnames, filenames in os.walk(root):
1563
+ for name in filenames:
1564
+ try:
1565
+ st = os.lstat(os.path.join(dirpath, name))
1566
+ except OSError:
1567
+ continue
1568
+ if st.st_nlink < 2 or not stat.S_ISREG(st.st_mode):
1569
+ continue
1570
+ entry = inodes.setdefault(
1571
+ (st.st_dev, st.st_ino), [st.st_nlink, 0, st.st_blocks * 512]
1572
+ )
1573
+ entry[1] += 1
1574
+ return sum(size for nlink, seen, size in inodes.values() if seen < nlink)
1575
+
1531
1576
  @staticmethod
1532
1577
  def _estimated_cleanup_gb(service) -> float:
1533
1578
  """Best available upper bound for a dry-run cleanup.
@@ -330,7 +330,12 @@ class ServiceDataScanner:
330
330
  for path in matching_paths:
331
331
  if os.path.exists(path):
332
332
  info = self._analyze_service_path(service_type, path)
333
- if info and info.size_mb >= self.threshold_mb:
333
+ # An unmeasurable entry (slow Docker daemon) has no size to
334
+ # compare; dropping it hid hundreds of GB from the report.
335
+ if info and (
336
+ info.size_mb >= self.threshold_mb
337
+ or info.details.get("size_unknown")
338
+ ):
334
339
  results.append(info)
335
340
  if len(results) > 1:
336
341
  return self._merge_by_risk(results)
@@ -543,10 +548,64 @@ class ServiceDataScanner:
543
548
  preview_command=ServiceCleaner.get_preview_command(ServiceType.DOCKER, path),
544
549
  safe_to_cleanup=False,
545
550
  impact="high",
546
- details={"size_unknown": True, "reason": "docker_system_df_timeout"},
551
+ details={
552
+ "size_unknown": True,
553
+ "reason": "docker_system_df_timeout",
554
+ "fast_inventory": self._docker_fast_inventory(),
555
+ },
547
556
  risk_level=ServiceCleaner.get_risk_level(ServiceType.DOCKER, path),
548
557
  )
549
558
 
559
+ @staticmethod
560
+ def _docker_fast_inventory(timeout_s: float = 10.0) -> dict[str, Any]:
561
+ """Counts and build-cache size from Docker commands that stay fast.
562
+
563
+ ``docker system df`` walks every volume to size it; with hundreds of
564
+ volumes it can take minutes even on an idle host. Listing objects and
565
+ ``docker builder du`` answer in well under a second, so a timed-out
566
+ scan can still show where Docker holds data. Volume sizes stay
567
+ unknown; orphaned volumes are data and are only counted.
568
+ """
569
+
570
+ def lines(*args: str) -> list[str] | None:
571
+ try:
572
+ result = subprocess.run(
573
+ ["docker", *args],
574
+ capture_output=True,
575
+ text=True,
576
+ timeout=timeout_s,
577
+ check=False,
578
+ )
579
+ except (OSError, subprocess.TimeoutExpired):
580
+ return None
581
+ if result.returncode != 0:
582
+ return None
583
+ return [line for line in result.stdout.splitlines() if line.strip()]
584
+
585
+ inventory: dict[str, Any] = {}
586
+ queries = {
587
+ "images": ("image", "ls", "-q", "--no-trunc"),
588
+ "dangling_images": ("image", "ls", "-q", "--no-trunc", "-f", "dangling=true"),
589
+ "containers": ("ps", "-aq"),
590
+ "stopped_containers": ("ps", "-aq", "-f", "status=exited"),
591
+ "volumes": ("volume", "ls", "-q"),
592
+ "dangling_volumes": ("volume", "ls", "-q", "-f", "dangling=true"),
593
+ }
594
+ for key, args in queries.items():
595
+ found = lines(*args)
596
+ if found is not None:
597
+ inventory[key] = len(set(found))
598
+
599
+ builder = lines("builder", "du")
600
+ if builder is not None:
601
+ for line in builder:
602
+ label, _, value = line.partition(":")
603
+ label = label.strip().lower()
604
+ if label in ("total", "reclaimable") and value.strip():
605
+ size_mb = ServiceDataScanner._parse_human_size_to_mb(value.strip())
606
+ inventory[f"build_cache_{label}_gb"] = round(size_mb / 1024, 3)
607
+ return inventory
608
+
550
609
  def _get_docker_daemon_size_mb(self, *, refresh: bool = False) -> float | None:
551
610
  """Total Docker disk usage as reported by the daemon itself."""
552
611
  usage = self._get_docker_daemon_usage(refresh=refresh)
@@ -1,13 +1,13 @@
1
- #!/usr/bin/env python3
2
1
  """
3
2
  Interactive Cleanup Planner for fixOS
4
3
  Groups cleanup actions and provides interactive selection
5
4
  """
6
5
 
7
6
  import json
8
- from typing import Dict, List, Any
9
7
  from dataclasses import dataclass
10
8
  from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any
11
11
 
12
12
 
13
13
  class Priority(Enum):
@@ -42,7 +42,7 @@ class CleanupAction:
42
42
  impact: str
43
43
  category: str = ""
44
44
  estimated_time: str = ""
45
- dependencies: List[str] = None
45
+ dependencies: list[str] = None
46
46
  preview_command: str = ""
47
47
 
48
48
  def __post_init__(self):
@@ -116,8 +116,8 @@ class CleanupPlanner:
116
116
  }
117
117
 
118
118
  def group_by_category(
119
- self, suggestions: List[Dict]
120
- ) -> Dict[str, List[CleanupAction]]:
119
+ self, suggestions: list[dict]
120
+ ) -> dict[str, list[CleanupAction]]:
121
121
  """Group cleanup suggestions by category"""
122
122
  grouped = {}
123
123
 
@@ -133,13 +133,12 @@ class CleanupPlanner:
133
133
  grouped[category] = []
134
134
  grouped[category].append(action)
135
135
 
136
- except Exception:
137
- # Skip invalid suggestions
136
+ except Exception: # noqa: BLE001, S112 - one malformed suggestion must not drop the whole plan
138
137
  continue
139
138
 
140
139
  # Sort actions within each category by priority and size
141
- for category in grouped:
142
- grouped[category].sort(
140
+ for actions in grouped.values():
141
+ actions.sort(
143
142
  key=lambda x: (self._priority_score(x.priority), x.size_gb),
144
143
  reverse=True,
145
144
  )
@@ -147,25 +146,25 @@ class CleanupPlanner:
147
146
  return grouped
148
147
 
149
148
  def prioritize_actions(
150
- self, grouped_actions: Dict[str, List[CleanupAction]]
151
- ) -> List[CleanupAction]:
149
+ self, grouped_actions: dict[str, list[CleanupAction]]
150
+ ) -> list[CleanupAction]:
152
151
  """Create prioritized list of all actions"""
153
152
  all_actions = []
154
153
 
155
154
  # Add high priority actions first
156
- for category, actions in grouped_actions.items():
155
+ for actions in grouped_actions.values():
157
156
  high_priority = [
158
157
  a for a in actions if a.priority in [Priority.CRITICAL, Priority.HIGH]
159
158
  ]
160
159
  all_actions.extend(high_priority)
161
160
 
162
161
  # Add medium priority actions
163
- for category, actions in grouped_actions.items():
162
+ for actions in grouped_actions.values():
164
163
  medium_priority = [a for a in actions if a.priority == Priority.MEDIUM]
165
164
  all_actions.extend(medium_priority)
166
165
 
167
166
  # Add low priority actions
168
- for category, actions in grouped_actions.items():
167
+ for actions in grouped_actions.values():
169
168
  low_priority = [a for a in actions if a.priority == Priority.LOW]
170
169
  all_actions.extend(low_priority)
171
170
 
@@ -181,7 +180,7 @@ class CleanupPlanner:
181
180
 
182
181
  return all_actions
183
182
 
184
- def create_cleanup_plan(self, suggestions: List[Dict]) -> Dict[str, Any]:
183
+ def create_cleanup_plan(self, suggestions: list[dict]) -> dict[str, Any]:
185
184
  """Create comprehensive cleanup plan"""
186
185
  grouped = self.group_by_category(suggestions)
187
186
  prioritized = self.prioritize_actions(grouped)
@@ -237,7 +236,7 @@ class CleanupPlanner:
237
236
 
238
237
  return plan
239
238
 
240
- def interactive_selection(self, plan: Dict[str, Any]) -> Dict[str, Any]:
239
+ def interactive_selection(self, plan: dict[str, Any]) -> dict[str, Any]:
241
240
  """Interactive selection process (simulated for now)"""
242
241
  selected_actions = []
243
242
 
@@ -255,7 +254,7 @@ class CleanupPlanner:
255
254
  "selection_method": "auto_safe_high_impact",
256
255
  }
257
256
 
258
- def _dict_to_action(self, suggestion: Dict) -> CleanupAction:
257
+ def _dict_to_action(self, suggestion: dict) -> CleanupAction:
259
258
  """Convert dictionary suggestion to CleanupAction"""
260
259
  try:
261
260
  # Need to handle strings if suggestion payload comes in as string not Enum
@@ -285,8 +284,7 @@ class CleanupPlanner:
285
284
  dependencies=suggestion.get("dependencies", []),
286
285
  preview_command=suggestion.get("preview_command", ""),
287
286
  )
288
- except Exception:
289
- # Create default action for invalid suggestions
287
+ except Exception: # noqa: BLE001 - invalid suggestions become a low-priority default action
290
288
  return CleanupAction(
291
289
  type=CleanupType.LARGE_FILE,
292
290
  priority=Priority.LOW,
@@ -298,7 +296,7 @@ class CleanupPlanner:
298
296
  impact="none",
299
297
  )
300
298
 
301
- def _action_to_dict(self, action: CleanupAction) -> Dict[str, Any]:
299
+ def _action_to_dict(self, action: CleanupAction) -> dict[str, Any]:
302
300
  """Convert CleanupAction to dictionary"""
303
301
  return {
304
302
  "type": action.type.value,
@@ -345,7 +343,7 @@ class CleanupPlanner:
345
343
  return scores.get(priority, 1)
346
344
 
347
345
  @staticmethod
348
- def _rec_safe_high_impact(prioritized: List[CleanupAction]) -> List[Dict]:
346
+ def _rec_safe_high_impact(prioritized: list[CleanupAction]) -> list[dict]:
349
347
  actions = [
350
348
  a
351
349
  for a in prioritized
@@ -368,7 +366,7 @@ class CleanupPlanner:
368
366
  ]
369
367
 
370
368
  @staticmethod
371
- def _rec_cache(grouped: Dict[str, List[CleanupAction]]) -> List[Dict]:
369
+ def _rec_cache(grouped: dict[str, list[CleanupAction]]) -> list[dict]:
372
370
  actions = grouped.get("cache", [])
373
371
  if not actions:
374
372
  return []
@@ -387,7 +385,7 @@ class CleanupPlanner:
387
385
  ]
388
386
 
389
387
  @staticmethod
390
- def _rec_logs(grouped: Dict[str, List[CleanupAction]]) -> List[Dict]:
388
+ def _rec_logs(grouped: dict[str, list[CleanupAction]]) -> list[dict]:
391
389
  actions = grouped.get("logs", [])
392
390
  if not actions:
393
391
  return []
@@ -406,7 +404,7 @@ class CleanupPlanner:
406
404
  ]
407
405
 
408
406
  @staticmethod
409
- def _rec_manual(prioritized: List[CleanupAction]) -> List[Dict]:
407
+ def _rec_manual(prioritized: list[CleanupAction]) -> list[dict]:
410
408
  actions = [a for a in prioritized if not a.safe and a.size_gb > 1.0]
411
409
  if not actions:
412
410
  return []
@@ -423,10 +421,10 @@ class CleanupPlanner:
423
421
  ]
424
422
 
425
423
  def _generate_recommendations(
426
- self, grouped: Dict[str, List[CleanupAction]], prioritized: List[CleanupAction]
427
- ) -> List[Dict]:
424
+ self, grouped: dict[str, list[CleanupAction]], prioritized: list[CleanupAction]
425
+ ) -> list[dict]:
428
426
  """Generate cleanup recommendations"""
429
- recs: List[Dict] = []
427
+ recs: list[dict] = []
430
428
  recs.extend(self._rec_safe_high_impact(prioritized))
431
429
  recs.extend(self._rec_cache(grouped))
432
430
  recs.extend(self._rec_logs(grouped))
@@ -441,7 +439,7 @@ def main():
441
439
  {
442
440
  "type": "cache_cleanup",
443
441
  "priority": "high",
444
- "path": "/home/user/.cache/npm",
442
+ "path": str(Path.home() / ".cache" / "npm"),
445
443
  "size_gb": 2.5,
446
444
  "description": "Clear npm cache",
447
445
  "command": "npm cache clean --force",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.52
3
+ Version: 2.2.53
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
@@ -188,6 +188,7 @@ tests/unit/test_ask_and_nl_group.py
188
188
  tests/unit/test_cache_discovery.py
189
189
  tests/unit/test_cleanup_cmd.py
190
190
  tests/unit/test_cleanup_effectiveness.py
191
+ tests/unit/test_cleanup_planner.py
191
192
  tests/unit/test_cleanup_retention.py
192
193
  tests/unit/test_cleanup_safety.py
193
194
  tests/unit/test_cleanup_space.py
@@ -208,6 +209,7 @@ tests/unit/test_jetbrains_recovery.py
208
209
  tests/unit/test_llm_auth_stop.py
209
210
  tests/unit/test_llm_provider.py
210
211
  tests/unit/test_native_scan.py
212
+ tests/unit/test_ollama_endpoint.py
211
213
  tests/unit/test_orchestrator.py
212
214
  tests/unit/test_orphan_pins.py
213
215
  tests/unit/test_orphaned_workloads.py