fixos 2.2.53__tar.gz → 2.2.54__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 (235) hide show
  1. {fixos-2.2.53 → fixos-2.2.54}/PKG-INFO +1 -1
  2. fixos-2.2.54/VERSION +1 -0
  3. fixos-2.2.54/fixos/cli/_cleanup_docker_volumes.py +221 -0
  4. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/cleanup_cmd.py +38 -2
  5. {fixos-2.2.53 → fixos-2.2.54}/fixos/config.py +1 -1
  6. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_storage_container_mixin.py +8 -4
  7. {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/PKG-INFO +1 -1
  8. {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/SOURCES.txt +2 -0
  9. {fixos-2.2.53 → fixos-2.2.54}/pyproject.toml +1 -1
  10. fixos-2.2.54/tests/unit/test_cleanup_docker_volumes.py +161 -0
  11. fixos-2.2.53/VERSION +0 -1
  12. {fixos-2.2.53 → fixos-2.2.54}/.env.example +0 -0
  13. {fixos-2.2.53 → fixos-2.2.54}/CHANGELOG.md +0 -0
  14. {fixos-2.2.53 → fixos-2.2.54}/LICENSE +0 -0
  15. {fixos-2.2.53 → fixos-2.2.54}/MANIFEST.in +0 -0
  16. {fixos-2.2.53 → fixos-2.2.54}/README.md +0 -0
  17. {fixos-2.2.53 → fixos-2.2.54}/docker/README.md +0 -0
  18. {fixos-2.2.53 → fixos-2.2.54}/docker/TEST_RESULTS.md +0 -0
  19. {fixos-2.2.53 → fixos-2.2.54}/docker/TEST_RESULTS_V2.md +0 -0
  20. {fixos-2.2.53 → fixos-2.2.54}/docker/alpine/Dockerfile +0 -0
  21. {fixos-2.2.53 → fixos-2.2.54}/docker/arch/Dockerfile +0 -0
  22. {fixos-2.2.53 → fixos-2.2.54}/docker/base/Dockerfile +0 -0
  23. {fixos-2.2.53 → fixos-2.2.54}/docker/broken-audio/Dockerfile +0 -0
  24. {fixos-2.2.53 → fixos-2.2.54}/docker/broken-full/Dockerfile +0 -0
  25. {fixos-2.2.53 → fixos-2.2.54}/docker/broken-network/Dockerfile +0 -0
  26. {fixos-2.2.53 → fixos-2.2.54}/docker/broken-thumbnails/Dockerfile +0 -0
  27. {fixos-2.2.53 → fixos-2.2.54}/docker/debian/Dockerfile +0 -0
  28. {fixos-2.2.53 → fixos-2.2.54}/docker/docker-compose.multi-system.yml +0 -0
  29. {fixos-2.2.53 → fixos-2.2.54}/docker/docker-compose.yml +0 -0
  30. {fixos-2.2.53 → fixos-2.2.54}/docker/fedora/Dockerfile +0 -0
  31. {fixos-2.2.53 → fixos-2.2.54}/docker/test-multi-system.sh +0 -0
  32. {fixos-2.2.53 → fixos-2.2.54}/docker/test-scenarios.sh +0 -0
  33. {fixos-2.2.53 → fixos-2.2.54}/docker/ubuntu/Dockerfile +0 -0
  34. {fixos-2.2.53 → fixos-2.2.54}/docker/validate-scenario.py +0 -0
  35. {fixos-2.2.53 → fixos-2.2.54}/docs/examples/advanced_usage.py +0 -0
  36. {fixos-2.2.53 → fixos-2.2.54}/docs/examples/quickstart.py +0 -0
  37. {fixos-2.2.53 → fixos-2.2.54}/fixos/__init__.py +0 -0
  38. {fixos-2.2.53 → fixos-2.2.54}/fixos/__main__.py +0 -0
  39. {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/__init__.py +0 -0
  40. {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/autonomous.py +0 -0
  41. {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/autonomous_session.py +0 -0
  42. {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/hitl.py +0 -0
  43. {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/hitl_session.py +0 -0
  44. {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/session_core.py +0 -0
  45. {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/session_handlers.py +0 -0
  46. {fixos-2.2.53 → fixos-2.2.54}/fixos/agent/session_io.py +0 -0
  47. {fixos-2.2.53 → fixos-2.2.54}/fixos/anonymizer.py +0 -0
  48. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/__init__.py +0 -0
  49. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/__main__.py +0 -0
  50. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_flatpak.py +0 -0
  51. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_home.py +0 -0
  52. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_snap.py +0 -0
  53. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_space.py +0 -0
  54. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_system.py +0 -0
  55. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_utils.py +0 -0
  56. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/_cleanup_venvs.py +0 -0
  57. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/ask_cmd.py +0 -0
  58. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/config_cmd.py +0 -0
  59. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/dashboard.py +0 -0
  60. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/features_cmd.py +0 -0
  61. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/fix_cmd.py +0 -0
  62. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/history_cmd.py +0 -0
  63. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/jetbrains_cmd.py +0 -0
  64. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/main.py +0 -0
  65. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/orchestrate_cmd.py +0 -0
  66. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/output_formatter.py +0 -0
  67. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/profile_cmd.py +0 -0
  68. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/projects_cmd.py +0 -0
  69. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/provider_cmd.py +0 -0
  70. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/quick_cmd.py +0 -0
  71. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/quickfix_cmd.py +0 -0
  72. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/report_cmd.py +0 -0
  73. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/rollback_cmd.py +0 -0
  74. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/scan_cmd.py +0 -0
  75. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/shared.py +0 -0
  76. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/shell_cmd.py +0 -0
  77. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/token_cmd.py +0 -0
  78. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli/watch_cmd.py +0 -0
  79. {fixos-2.2.53 → fixos-2.2.54}/fixos/cli.py +0 -0
  80. {fixos-2.2.53 → fixos-2.2.54}/fixos/config_interactive.py +0 -0
  81. {fixos-2.2.53 → fixos-2.2.54}/fixos/constants.py +0 -0
  82. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/__init__.py +0 -0
  83. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
  84. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
  85. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
  86. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_storage_system_mixin.py +0 -0
  87. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/_storage_user_mixin.py +0 -0
  88. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/cache_discovery.py +0 -0
  89. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/__init__.py +0 -0
  90. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/_shared.py +0 -0
  91. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/audio.py +0 -0
  92. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/file_analysis.py +0 -0
  93. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/hardware.py +0 -0
  94. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/packages.py +0 -0
  95. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/resources.py +0 -0
  96. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/security.py +0 -0
  97. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/storage_optimization.py +0 -0
  98. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/system_core.py +0 -0
  99. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/checks/thumbnails.py +0 -0
  100. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  101. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/disk_analyzer.py +0 -0
  102. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/docker_network_cleanup.py +0 -0
  103. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
  104. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  105. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/jetbrains_ai.py +0 -0
  106. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/jetbrains_recovery.py +0 -0
  107. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/native_scan.py +0 -0
  108. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/orphaned_workloads.py +0 -0
  109. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/process_chains.py +0 -0
  110. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/project_scanner.py +0 -0
  111. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/quick_snapshot.py +0 -0
  112. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/service_cleanup.py +0 -0
  113. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/service_details.py +0 -0
  114. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/service_scanner.py +0 -0
  115. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/storage_analyzer.py +0 -0
  116. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/system_checks.py +0 -0
  117. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/utils.py +0 -0
  118. {fixos-2.2.53 → fixos-2.2.54}/fixos/diagnostics/venv_cleanup.py +0 -0
  119. {fixos-2.2.53 → fixos-2.2.54}/fixos/endpoint_refresh.py +0 -0
  120. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/__init__.py +0 -0
  121. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/auditor.py +0 -0
  122. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/catalog.py +0 -0
  123. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/data/packages.yaml +0 -0
  124. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/data/profiles/developer.yaml +0 -0
  125. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/data/profiles/office.yaml +0 -0
  126. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/data/profiles/sysadmin.yaml +0 -0
  127. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/installer.py +0 -0
  128. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/profiles.py +0 -0
  129. {fixos-2.2.53 → fixos-2.2.54}/fixos/features/renderer.py +0 -0
  130. {fixos-2.2.53 → fixos-2.2.54}/fixos/fixes/__init__.py +0 -0
  131. {fixos-2.2.53 → fixos-2.2.54}/fixos/interactive/__init__.py +0 -0
  132. {fixos-2.2.53 → fixos-2.2.54}/fixos/interactive/cleanup_planner.py +0 -0
  133. {fixos-2.2.53 → fixos-2.2.54}/fixos/llm_shell.py +0 -0
  134. {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/__init__.py +0 -0
  135. {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/executor.py +0 -0
  136. {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/graph.py +0 -0
  137. {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/orchestrator.py +0 -0
  138. {fixos-2.2.53 → fixos-2.2.54}/fixos/orchestrator/rollback.py +0 -0
  139. {fixos-2.2.53 → fixos-2.2.54}/fixos/orphan_pins.py +0 -0
  140. {fixos-2.2.53 → fixos-2.2.54}/fixos/platform_utils.py +0 -0
  141. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/__init__.py +0 -0
  142. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/base.py +0 -0
  143. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/__init__.py +0 -0
  144. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/audio.py +0 -0
  145. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/disk.py +0 -0
  146. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/hardware.py +0 -0
  147. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/resources.py +0 -0
  148. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/security.py +0 -0
  149. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/builtin/thumbnails.py +0 -0
  150. {fixos-2.2.53 → fixos-2.2.54}/fixos/plugins/registry.py +0 -0
  151. {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/__init__.py +0 -0
  152. {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/desktop.yaml +0 -0
  153. {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/developer.yaml +0 -0
  154. {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/minimal.yaml +0 -0
  155. {fixos-2.2.53 → fixos-2.2.54}/fixos/profiles/server.yaml +0 -0
  156. {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/__init__.py +0 -0
  157. {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/gemini_native.py +0 -0
  158. {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/llm.py +0 -0
  159. {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/llm_analyzer.py +0 -0
  160. {fixos-2.2.53 → fixos-2.2.54}/fixos/providers/schemas.py +0 -0
  161. {fixos-2.2.53 → fixos-2.2.54}/fixos/system_checks.py +0 -0
  162. {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/__init__.py +0 -0
  163. {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/anonymizer.py +0 -0
  164. {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/terminal.py +0 -0
  165. {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/timeout.py +0 -0
  166. {fixos-2.2.53 → fixos-2.2.54}/fixos/utils/web_search.py +0 -0
  167. {fixos-2.2.53 → fixos-2.2.54}/fixos/watch.py +0 -0
  168. {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/dependency_links.txt +0 -0
  169. {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/entry_points.txt +0 -0
  170. {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/requires.txt +0 -0
  171. {fixos-2.2.53 → fixos-2.2.54}/fixos.egg-info/top_level.txt +0 -0
  172. {fixos-2.2.53 → fixos-2.2.54}/pytest.ini +0 -0
  173. {fixos-2.2.53 → fixos-2.2.54}/requirements-dev.txt +0 -0
  174. {fixos-2.2.53 → fixos-2.2.54}/requirements.txt +0 -0
  175. {fixos-2.2.53 → fixos-2.2.54}/scripts/pyqual-calibrate.py +0 -0
  176. {fixos-2.2.53 → fixos-2.2.54}/setup.cfg +0 -0
  177. {fixos-2.2.53 → fixos-2.2.54}/setup.py +0 -0
  178. {fixos-2.2.53 → fixos-2.2.54}/tests/__init__.py +0 -0
  179. {fixos-2.2.53 → fixos-2.2.54}/tests/conftest.py +0 -0
  180. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/__init__.py +0 -0
  181. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_anonymization_layers.py +0 -0
  182. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_audio_broken.py +0 -0
  183. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_cli.py +0 -0
  184. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_diagnostics_integration.py +0 -0
  185. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_executor.py +0 -0
  186. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_multi_system.py +0 -0
  187. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_network_broken.py +0 -0
  188. {fixos-2.2.53 → fixos-2.2.54}/tests/e2e/test_thumbnails_broken.py +0 -0
  189. {fixos-2.2.53 → fixos-2.2.54}/tests/test_fixos.py +0 -0
  190. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/__init__.py +0 -0
  191. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_anonymizer.py +0 -0
  192. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_ask_and_nl_group.py +0 -0
  193. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cache_discovery.py +0 -0
  194. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_cmd.py +0 -0
  195. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_effectiveness.py +0 -0
  196. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_planner.py +0 -0
  197. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_retention.py +0 -0
  198. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_safety.py +0 -0
  199. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_space.py +0 -0
  200. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_tmp.py +0 -0
  201. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cleanup_utils.py +0 -0
  202. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_cli_main.py +0 -0
  203. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_core.py +0 -0
  204. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_dashboard.py +0 -0
  205. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_dev_project_analyzer.py +0 -0
  206. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_disk_analyzer.py +0 -0
  207. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_docker_network_cleanup.py +0 -0
  208. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_docker_startup_optimizer.py +0 -0
  209. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_endpoint_refresh.py +0 -0
  210. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_executor.py +0 -0
  211. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_gemini_native_client.py +0 -0
  212. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_jetbrains_cmd.py +0 -0
  213. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_jetbrains_recovery.py +0 -0
  214. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_llm_auth_stop.py +0 -0
  215. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_llm_provider.py +0 -0
  216. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_native_scan.py +0 -0
  217. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_ollama_endpoint.py +0 -0
  218. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_orchestrator.py +0 -0
  219. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_orphan_pins.py +0 -0
  220. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_orphaned_workloads.py +0 -0
  221. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_packaging_data.py +0 -0
  222. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_process_chains.py +0 -0
  223. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_project_scanner.py +0 -0
  224. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_projects_cmd.py +0 -0
  225. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_quick_cmd.py +0 -0
  226. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_quick_snapshot.py +0 -0
  227. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_resources_quick.py +0 -0
  228. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_security_quick.py +0 -0
  229. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_service_cleanup.py +0 -0
  230. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_service_scanner.py +0 -0
  231. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_shell_ux.py +0 -0
  232. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_storage_analyzer.py +0 -0
  233. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_system_checks_parallel.py +0 -0
  234. {fixos-2.2.53 → fixos-2.2.54}/tests/unit/test_venv_cleanup.py +0 -0
  235. {fixos-2.2.53 → fixos-2.2.54}/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.53
3
+ Version: 2.2.54
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.54/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.2.54
@@ -0,0 +1,221 @@
1
+ """Selective removal of unused (dangling) Docker volumes.
2
+
3
+ Volumes hold real data, so nothing here is bulk: candidates are listed with
4
+ name, Compose project and age, the user picks them, and each one is checked
5
+ again right before ``docker volume rm``. ``--yes`` never selects volumes whose
6
+ name looks like stored data (databases, git mirrors, backups, ...).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import re
14
+ import subprocess
15
+ from concurrent.futures import ThreadPoolExecutor
16
+ from datetime import datetime, timezone
17
+
18
+ import click
19
+
20
+ from fixos.cli import _cleanup_space as space
21
+ from fixos.cli._cleanup_utils import _format_bytes
22
+
23
+ # Name tokens that usually mean irreplaceable data rather than a cache.
24
+ _DATA_TOKENS = re.compile(
25
+ r"(?:^|[_.-])(mirror|git|gitea|onedev|repo|repos|postgres|postgresql|pg|"
26
+ r"mysql|mariadb|mongo|mongodb|redis|db|database|backup|backups|minio|s3|"
27
+ r"vault|secrets?|keycloak|grafana|prometheus|influx|elastic|opensearch|"
28
+ r"nextcloud|uploads|media|webui|openwebui|open-webui|n8n|storage|certs|"
29
+ r"letsencrypt|wordpress|mattermost|synapse|chat-history)(?:$|[_.-])",
30
+ re.IGNORECASE,
31
+ )
32
+ _INSPECT_BATCH = 100
33
+
34
+
35
+ def _docker_lines(args: list[str], timeout: int = 30) -> list[str] | None:
36
+ try:
37
+ result = space._run_cmd(["docker", *args], timeout=timeout)
38
+ except (OSError, subprocess.TimeoutExpired):
39
+ return None
40
+ if result.returncode != 0:
41
+ return None
42
+ return [line for line in result.stdout.splitlines() if line.strip()]
43
+
44
+
45
+ def _is_unused(name: str) -> bool:
46
+ """Re-check right before removal: still dangling and no container (even stopped)."""
47
+ dangling = _docker_lines(["volume", "ls", "-q", "-f", "dangling=true", "-f", f"name=^{name}$"])
48
+ containers = _docker_lines(["ps", "-aq", "--filter", f"volume={name}"])
49
+ return dangling is not None and name in dangling and containers == []
50
+
51
+
52
+ def _age_days(created_at: str, now: datetime) -> float | None:
53
+ try:
54
+ created = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
55
+ except ValueError:
56
+ return None
57
+ if created.tzinfo is None:
58
+ created = created.replace(tzinfo=timezone.utc)
59
+ return (now - created).total_seconds() / 86400
60
+
61
+
62
+ _SIZE_WORKERS = 8
63
+
64
+
65
+ def _du_batch(command: list[str], paths: list[str]) -> dict[str, int]:
66
+ sizes: dict[str, int] = {}
67
+ try:
68
+ result = space._run_cmd([*command, "-sbl0", "--", *paths], timeout=300)
69
+ except (OSError, subprocess.TimeoutExpired):
70
+ return sizes
71
+ for record in result.stdout.split("\0"):
72
+ size, sep, path = record.partition("\t")
73
+ if sep and size.isdigit():
74
+ sizes[path] = int(size)
75
+ return sizes
76
+
77
+
78
+ def _volume_sizes(mountpoints: list[str]) -> dict[str, int]:
79
+ """Sizes via ``du`` in parallel batches; root-owned volumes use ``sudo -n``.
80
+
81
+ ``sudo -n`` never prompts: without passwordless sudo the size stays unknown.
82
+ Unreadable mountpoints go straight to ``sudo -n``, so a partial size of an
83
+ unreadable directory is never reported.
84
+ """
85
+ readable = [m for m in mountpoints if os.access(m, os.R_OK | os.X_OK)]
86
+ privileged = [m for m in mountpoints if m not in set(readable)]
87
+ jobs = [(["du"], readable[i::_SIZE_WORKERS]) for i in range(_SIZE_WORKERS)]
88
+ jobs += [(["sudo", "-n", "du"], privileged[i::_SIZE_WORKERS]) for i in range(_SIZE_WORKERS)]
89
+ jobs = [(command, paths) for command, paths in jobs if paths]
90
+ sizes: dict[str, int] = {}
91
+ if not jobs:
92
+ return sizes
93
+ with ThreadPoolExecutor(max_workers=min(_SIZE_WORKERS, len(jobs))) as pool:
94
+ for result in pool.map(lambda job: _du_batch(*job), jobs):
95
+ sizes.update(result)
96
+ return sizes
97
+
98
+
99
+ def _unused_volumes(
100
+ days: float | None, match: list[str], now: datetime | None = None
101
+ ) -> list[dict] | None:
102
+ names = _docker_lines(["volume", "ls", "-q", "-f", "dangling=true"])
103
+ if names is None:
104
+ return None
105
+ now = now or datetime.now(timezone.utc)
106
+ records: list[dict] = []
107
+ for start in range(0, len(names), _INSPECT_BATCH):
108
+ raw = _docker_lines(["volume", "inspect", *names[start : start + _INSPECT_BATCH]], timeout=60)
109
+ if raw is None:
110
+ return None
111
+ records.extend(json.loads("\n".join(raw)))
112
+ candidates = []
113
+ for record in records:
114
+ name = record.get("Name", "")
115
+ if match and not any(token.lower() in name.lower() for token in match):
116
+ continue
117
+ age = _age_days(record.get("CreatedAt", ""), now)
118
+ if days is not None and (age is None or age < days):
119
+ continue
120
+ labels = record.get("Labels") or {}
121
+ project = labels.get("com.docker.compose.project") or "(bez projektu)"
122
+ protected = bool(_DATA_TOKENS.search(name))
123
+ candidates.append(
124
+ {
125
+ "name": name,
126
+ "description": name,
127
+ "project": project,
128
+ "age_days": age,
129
+ "mountpoint": record.get("Mountpoint", ""),
130
+ "protected": protected,
131
+ "size_bytes": 0,
132
+ "size_known": False,
133
+ }
134
+ )
135
+ sizes = _volume_sizes([c["mountpoint"] for c in candidates if c["mountpoint"]])
136
+ for candidate in candidates:
137
+ if candidate["mountpoint"] in sizes:
138
+ candidate["size_bytes"] = sizes[candidate["mountpoint"]]
139
+ candidate["size_known"] = True
140
+ age = candidate["age_days"]
141
+ size = _format_bytes(candidate["size_bytes"]) if candidate["size_known"] else "rozmiar nieznany"
142
+ candidate["note"] = (
143
+ f"{candidate['project']}, {age:.0f} dni" if age is not None else candidate["project"]
144
+ ) + (" — wygląda na DANE, tylko ręczny wybór" if candidate["protected"] else "")
145
+ candidate["size_label"] = size
146
+ candidates.sort(key=lambda c: (c["size_known"], c["size_bytes"]), reverse=True)
147
+ return candidates
148
+
149
+
150
+ def _cleanup_docker_volumes(
151
+ json_output: bool,
152
+ dry_run: bool,
153
+ list_only: bool,
154
+ yes: bool,
155
+ days: float | None = None,
156
+ match: list[str] | None = None,
157
+ ) -> None:
158
+ """List unused Docker volumes and remove the selected ones."""
159
+ candidates = _unused_volumes(days, match or [])
160
+ if json_output:
161
+ space._echo_json(
162
+ {
163
+ "error": None if candidates is not None else "docker volume ls failed",
164
+ "candidates": [
165
+ {k: c[k] for k in ("name", "project", "age_days", "size_bytes", "size_known", "protected")}
166
+ for c in candidates or []
167
+ ],
168
+ }
169
+ )
170
+ return
171
+ if candidates is None:
172
+ click.echo(click.style("Błąd: nie udało się odczytać wolumenów Dockera.", fg="red"))
173
+ return
174
+ if not candidates:
175
+ click.echo(click.style("Brak nieużywanych wolumenów Dockera dla podanych filtrów.", fg="green"))
176
+ return
177
+ known = sum(c["size_bytes"] for c in candidates if c["size_known"])
178
+ click.echo(
179
+ f"\n{click.style('DOCKER — nieużywane wolumeny (dane!):', fg='magenta', bold=True)} "
180
+ f"{len(candidates)}, znany rozmiar {_format_bytes(known)}"
181
+ )
182
+ for index, item in enumerate(candidates, 1):
183
+ color = "red" if item["protected"] else "yellow"
184
+ click.echo(
185
+ f" [{index:3d}] {click.style(item['name'], fg=color)}: {item['size_label']} — {item['note']}"
186
+ )
187
+ if list_only:
188
+ return
189
+ if yes:
190
+ selected = [i for i, c in enumerate(candidates) if not c["protected"]]
191
+ skipped = len(candidates) - len(selected)
192
+ if skipped:
193
+ click.echo(f" --yes pomija {skipped} wolumenów wyglądających na dane; wybierz je ręcznie.")
194
+ else:
195
+ selected = space._choose(candidates, "Numery wolumenów do usunięcia (np. 1,2, all)")
196
+ if not selected:
197
+ click.echo("Pominięto.")
198
+ return
199
+ if dry_run:
200
+ space._show_dry_run_commands([["docker", "volume", "rm", candidates[i]["name"]] for i in selected])
201
+ return
202
+ if not yes and not space._confirm_remove():
203
+ click.echo("Anulowano.")
204
+ return
205
+ freed = 0
206
+ for index in selected:
207
+ name = candidates[index]["name"]
208
+ if not _is_unused(name):
209
+ click.echo(f" ⏭️ {name}: jest już używany albo nie istnieje — pominięto")
210
+ continue
211
+ try:
212
+ result = space._run_cmd(["docker", "volume", "rm", name], timeout=120)
213
+ except (OSError, subprocess.TimeoutExpired) as exc:
214
+ click.echo(click.style(f" ❌ {name}: {exc}", fg="red"))
215
+ continue
216
+ if result.returncode == 0:
217
+ freed += candidates[index]["size_bytes"]
218
+ click.echo(click.style(f" ✅ {name}", fg="green"))
219
+ else:
220
+ click.echo(click.style(f" ❌ {name}: {result.stderr.strip()[:120]}", fg="red"))
221
+ click.echo(f"Usunięto wolumeny o znanym rozmiarze łącznie {_format_bytes(freed)}.")
@@ -14,6 +14,8 @@ import re
14
14
 
15
15
  import click
16
16
 
17
+ from fixos.cli._cleanup_docker_volumes import _cleanup_docker_volumes
18
+
17
19
  # Re-export public symbols used by fixos.cli (backward-compat)
18
20
  from fixos.cli._cleanup_flatpak import _cleanup_flatpak_detailed
19
21
  from fixos.cli._cleanup_space import (
@@ -1611,7 +1613,7 @@ def _execute_yes_cleanup(plan: dict, scanner, dry_run: bool = False) -> None:
1611
1613
  "Wyczyść konkretną usługę "
1612
1614
  "(docker, docker-all, docker-old, docker-containers, docker-networks, "
1613
1615
  "docker-stale-services, orphaned-projects, ollama-old, snap-old, "
1614
- "journal, user-cache, jetbrains, libvirt, gitive, docker-buildcache, tmp, venvs-old, "
1616
+ "journal, user-cache, jetbrains, libvirt, gitive, docker-buildcache, docker-volumes, tmp, venvs-old, "
1615
1617
  "npm, ...)"
1616
1618
  ),
1617
1619
  )
@@ -1719,6 +1721,23 @@ def _execute_yes_cleanup(plan: dict, scanner, dry_run: bool = False) -> None:
1719
1721
  f"(domyślnie {DEFAULT_OLLAMA_OLD_UNUSED_DAYS}); pomija modele aktualnie uruchomione"
1720
1722
  ),
1721
1723
  )
1724
+ @click.option(
1725
+ "--docker-volumes",
1726
+ "docker_volumes",
1727
+ is_flag=True,
1728
+ default=False,
1729
+ help=(
1730
+ "Wybierz i usuń nieużywane wolumeny Dockera (nazwa, projekt Compose, wiek, "
1731
+ "rozmiar przez du/sudo -n); filtry --days i --match; --yes pomija "
1732
+ "wolumeny wyglądające na dane"
1733
+ ),
1734
+ )
1735
+ @click.option(
1736
+ "--match",
1737
+ "match",
1738
+ default=None,
1739
+ help="Dla --docker-volumes: fragmenty nazw rozdzielone przecinkami, np. ollama,huggingface",
1740
+ )
1722
1741
  @click.option(
1723
1742
  "--snap-old",
1724
1743
  "snap_old",
@@ -1893,6 +1912,8 @@ def cleanup_services(
1893
1912
  list_orphan_pins,
1894
1913
  process_hours,
1895
1914
  ollama_old,
1915
+ docker_volumes,
1916
+ match,
1896
1917
  snap_old,
1897
1918
  journal_cleanup,
1898
1919
  user_cache,
@@ -1951,6 +1972,8 @@ def cleanup_services(
1951
1972
  fixos cleanup --unpin-orphan-project /absolute/missing/project
1952
1973
  # usuwa ochronę, ale nie zmienia Dockera ani procesów
1953
1974
  fixos cleanup --ollama-old --days 90 --dry-run
1975
+ fixos cleanup --docker-volumes --list # nieużywane wolumeny: nazwa, projekt, wiek, rozmiar
1976
+ fixos cleanup --docker-volumes --match ollama,huggingface --days 90
1954
1977
  # modele Ollama niezmieniane od 90+ dni
1955
1978
  fixos cleanup -c ollama-old
1956
1979
  fixos cleanup --snap-old # wyłączone rewizje snapów
@@ -2004,6 +2027,7 @@ def cleanup_services(
2004
2027
  list_orphan_pins,
2005
2028
  ollama_old,
2006
2029
  snap_old,
2030
+ docker_volumes,
2007
2031
  journal_cleanup,
2008
2032
  user_cache,
2009
2033
  jetbrains,
@@ -2053,6 +2077,7 @@ def cleanup_services(
2053
2077
  ollama_old,
2054
2078
  docker_buildcache,
2055
2079
  snap_old,
2080
+ docker_volumes,
2056
2081
  journal_cleanup,
2057
2082
  user_cache,
2058
2083
  jetbrains,
@@ -2106,6 +2131,17 @@ def cleanup_services(
2106
2131
  "libvirt": (libvirt, _cleanup_libvirt),
2107
2132
  "gitive": (gitive, _cleanup_gitive),
2108
2133
  "docker-buildcache": (docker_buildcache, _cleanup_docker_buildcache),
2134
+ "docker-volumes": (
2135
+ docker_volumes,
2136
+ lambda j, d, lst, y: _cleanup_docker_volumes(
2137
+ j,
2138
+ d,
2139
+ lst,
2140
+ y,
2141
+ days=effective_days,
2142
+ match=[t.strip() for t in (match or "").split(",") if t.strip()],
2143
+ ),
2144
+ ),
2109
2145
  "tmp": (
2110
2146
  tmp_cleanup,
2111
2147
  lambda j, d, lst, y: _cleanup_tmp(
@@ -2168,7 +2204,7 @@ def cleanup_services(
2168
2204
  click.echo(
2169
2205
  click.style(
2170
2206
  "Akcje odzyskiwania miejsca (snap/journal/cache/JetBrains/"
2171
- "libvirt/gitive/docker-buildcache/tmp/trash/thumbnails) uruchom jako osobną, pojedynczą akcję.",
2207
+ "libvirt/gitive/docker-buildcache/docker-volumes/tmp/trash/thumbnails) uruchom jako osobną, pojedynczą akcję.",
2172
2208
  fg="red",
2173
2209
  )
2174
2210
  )
@@ -143,7 +143,7 @@ _OPENROUTER_MODEL_MIGRATIONS = {
143
143
  }
144
144
 
145
145
 
146
- DEFAULT_OLLAMA_HOST = "127.0.0.1:11434"
146
+ DEFAULT_OLLAMA_HOST = "127.0.0.1:11434" # nohardcode: allow NOHARDCODE-006 -- fallback when OLLAMA_HOST is unset or empty
147
147
 
148
148
 
149
149
  def ollama_host_url() -> str:
@@ -3,15 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ from typing import ClassVar
6
7
 
7
- from fixos.diagnostics.storage_analyzer import StorageItem
8
8
  from fixos.constants import MIN_DOCKER_DANGLING_MB, MIN_PODMAN_MB
9
+ from fixos.diagnostics.storage_analyzer import StorageItem
9
10
 
10
11
 
11
12
  class _ContainerAnalyzerMixin:
12
13
  """Mixin providing container _analyze_* methods for StorageAnalyzer."""
13
14
 
14
- _DOCKER_DF_SECTIONS = {
15
+ _DOCKER_DF_SECTIONS: ClassVar[dict[str, str]] = {
15
16
  "Images space usage:": "images",
16
17
  "Containers space usage:": "containers",
17
18
  "Local Volumes space usage:": "volumes",
@@ -119,11 +120,14 @@ class _ContainerAnalyzerMixin:
119
120
  size_bytes=total_docker,
120
121
  category="containers",
121
122
  risk="medium",
122
- cleanup_command="docker system prune -a --volumes",
123
+ # Never a bulk volume prune: volumes are data and are
124
+ # chosen one by one with `fixos cleanup --docker-volumes`.
125
+ cleanup_command="docker builder prune --force --filter until=168h",
123
126
  description=(
124
127
  f"Docker łącznie: {StorageItem._format_size(total_docker)}. "
125
128
  f"Obrazy: {total_images}, dangling: {dangling_images}. "
126
- "Uwaga: usunie wszystkie nieużywane zasoby."
129
+ "Czyści cache buildów starszy niż 7 dni; wolumeny wybierz "
130
+ "ręcznie przez `fixos cleanup --docker-volumes`."
127
131
  ),
128
132
  )
129
133
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.53
3
+ Version: 2.2.54
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
@@ -58,6 +58,7 @@ fixos/agent/session_handlers.py
58
58
  fixos/agent/session_io.py
59
59
  fixos/cli/__init__.py
60
60
  fixos/cli/__main__.py
61
+ fixos/cli/_cleanup_docker_volumes.py
61
62
  fixos/cli/_cleanup_flatpak.py
62
63
  fixos/cli/_cleanup_home.py
63
64
  fixos/cli/_cleanup_snap.py
@@ -187,6 +188,7 @@ tests/unit/test_anonymizer.py
187
188
  tests/unit/test_ask_and_nl_group.py
188
189
  tests/unit/test_cache_discovery.py
189
190
  tests/unit/test_cleanup_cmd.py
191
+ tests/unit/test_cleanup_docker_volumes.py
190
192
  tests/unit/test_cleanup_effectiveness.py
191
193
  tests/unit/test_cleanup_planner.py
192
194
  tests/unit/test_cleanup_retention.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fixos"
7
- version = "2.2.53"
7
+ version = "2.2.54"
8
8
  description = "AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,161 @@
1
+ """fixos cleanup --docker-volumes: listing, selection safety and re-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from types import SimpleNamespace
8
+
9
+ import click
10
+ from click.testing import CliRunner
11
+
12
+ from fixos.cli import _cleanup_docker_volumes as volumes
13
+ from fixos.cli import _cleanup_space as space
14
+ from fixos.diagnostics.storage_analyzer import (
15
+ StorageAnalyzer, # loads the mixins in order
16
+ )
17
+
18
+ NOW = datetime(2026, 10, 13, tzinfo=timezone.utc)
19
+ INSPECT = [
20
+ {"Name": "llmass_huggingface-cache", "CreatedAt": "2025-10-06T10:00:00Z",
21
+ "Labels": {"com.docker.compose.project": "llmass"}, "Mountpoint": "/v/hf"},
22
+ {"Name": "ifuri-onedev_mirror-data", "CreatedAt": "2026-07-20T10:00:00Z",
23
+ "Labels": {"com.docker.compose.project": "ifuri-onedev"}, "Mountpoint": "/v/mirror"},
24
+ {"Name": "chat_ollama_data", "CreatedAt": "2026-10-10T10:00:00Z",
25
+ "Labels": None, "Mountpoint": "/v/chat"},
26
+ ]
27
+
28
+
29
+ class FakeDocker:
30
+ """Answers the docker/du/sudo commands the module issues."""
31
+
32
+ def __init__(self, dangling=None, in_use=()):
33
+ self.dangling = list(dangling or [v["Name"] for v in INSPECT])
34
+ self.in_use = set(in_use)
35
+ self.calls: list[list[str]] = []
36
+
37
+ def __call__(self, argv, timeout=60):
38
+ self.calls.append(argv)
39
+ out, code = "", 0
40
+ if argv[:5] == ["docker", "volume", "ls", "-q", "-f"]:
41
+ names = self.dangling
42
+ if len(argv) > 6:
43
+ wanted = argv[-1].split("=", 1)[1].strip("^$")
44
+ names = [n for n in names if n == wanted]
45
+ out = "\n".join(names)
46
+ elif argv[:3] == ["docker", "volume", "inspect"]:
47
+ out = json.dumps([v for v in INSPECT if v["Name"] in argv[3:]])
48
+ elif argv[:3] == ["docker", "ps", "-aq"]:
49
+ name = argv[-1].split("=", 1)[1]
50
+ out = "c1" if name in self.in_use else ""
51
+ elif argv[:2] == ["du", "-sbl0"] or argv[:3] == ["sudo", "-n", "du"]:
52
+ sizes = {"/v/hf": 23_500_000_000, "/v/mirror": 7_400_000_000}
53
+ out = "".join(f"{sizes[p]}\t{p}\0" for p in argv if p in sizes)
54
+ elif argv[:3] == ["docker", "volume", "rm"]:
55
+ self.dangling.remove(argv[3])
56
+ return SimpleNamespace(returncode=code, stdout=out, stderr="")
57
+
58
+
59
+ def _invoke(fn, *args, input_text="", **kwargs):
60
+ @click.command()
61
+ def command():
62
+ fn(*args, **kwargs)
63
+
64
+ return CliRunner().invoke(command, input=input_text)
65
+
66
+
67
+ def test_lists_name_project_age_and_sorts_by_size(monkeypatch):
68
+ monkeypatch.setattr(space, "_run_cmd", FakeDocker())
69
+
70
+ found = volumes._unused_volumes(None, [], now=NOW)
71
+
72
+ assert [c["name"] for c in found] == [
73
+ "llmass_huggingface-cache", "ifuri-onedev_mirror-data", "chat_ollama_data",
74
+ ]
75
+ assert found[0]["project"] == "llmass" and round(found[0]["age_days"]) == 372
76
+ assert found[2]["size_known"] is False
77
+ assert [c["protected"] for c in found] == [False, True, False]
78
+
79
+
80
+ def test_match_and_days_filter(monkeypatch):
81
+ monkeypatch.setattr(space, "_run_cmd", FakeDocker())
82
+
83
+ found = volumes._unused_volumes(30, ["ollama", "huggingface"], now=NOW)
84
+
85
+ assert [c["name"] for c in found] == ["llmass_huggingface-cache"]
86
+
87
+
88
+ def test_yes_never_removes_data_looking_volumes(monkeypatch):
89
+ docker = FakeDocker()
90
+ monkeypatch.setattr(space, "_run_cmd", docker)
91
+
92
+ result = _invoke(volumes._cleanup_docker_volumes, False, False, False, True)
93
+
94
+ removed = [c[3] for c in docker.calls if c[:3] == ["docker", "volume", "rm"]]
95
+ assert result.exit_code == 0
96
+ assert "ifuri-onedev_mirror-data" not in removed
97
+ assert set(removed) == {"llmass_huggingface-cache", "chat_ollama_data"}
98
+
99
+
100
+ def test_volume_in_use_at_removal_time_is_skipped(monkeypatch):
101
+ docker = FakeDocker(in_use={"chat_ollama_data"})
102
+ monkeypatch.setattr(space, "_run_cmd", docker)
103
+
104
+ _invoke(volumes._cleanup_docker_volumes, False, False, False, False, input_text="3\ny\n")
105
+
106
+ assert not any(c[:3] == ["docker", "volume", "rm"] for c in docker.calls)
107
+
108
+
109
+ def test_dry_run_and_list_do_not_remove(monkeypatch):
110
+ docker = FakeDocker()
111
+ monkeypatch.setattr(space, "_run_cmd", docker)
112
+
113
+ _invoke(volumes._cleanup_docker_volumes, False, True, False, True)
114
+ _invoke(volumes._cleanup_docker_volumes, False, False, True, False)
115
+
116
+ assert not any(c[:3] == ["docker", "volume", "rm"] for c in docker.calls)
117
+
118
+
119
+ def test_full_analysis_never_prunes_volumes(monkeypatch):
120
+ from fixos.diagnostics._storage_container_mixin import _ContainerAnalyzerMixin
121
+
122
+ assert issubclass(StorageAnalyzer, _ContainerAnalyzerMixin)
123
+
124
+ class Analyzer(_ContainerAnalyzerMixin):
125
+ def __init__(self):
126
+ self.items = []
127
+
128
+ def _get_dir_size(self, path):
129
+ return 300 * 1024**3
130
+
131
+ analyzer = Analyzer()
132
+ analyzer._add_docker_total_item(10, 2)
133
+
134
+ command = analyzer.items[0].cleanup_command
135
+ assert "--volumes" not in command and "system prune" not in command
136
+ assert command == "docker builder prune --force --filter until=168h"
137
+
138
+
139
+ def test_app_data_volumes_are_protected():
140
+ for name in ("mcp_openwebui-data", "proxy_open-webui-data", "n8n_storage", "mcp_git-repo-storage"):
141
+ assert volumes._DATA_TOKENS.search(name), name
142
+ for name in ("llmass_huggingface-cache", "chat_ollama_data", "if-uri_trivy-cache"):
143
+ assert not volumes._DATA_TOKENS.search(name), name
144
+
145
+
146
+ def test_sizes_split_into_parallel_batches_without_unreadable_partial_sizes(monkeypatch):
147
+ calls = []
148
+
149
+ def fake_run(argv, timeout=60):
150
+ calls.append(argv)
151
+ paths = argv[argv.index("--") + 1 :]
152
+ return SimpleNamespace(returncode=0, stdout="".join(f"10\t{p}\0" for p in paths), stderr="")
153
+
154
+ monkeypatch.setattr(space, "_run_cmd", fake_run)
155
+ mountpoints = [f"/nonexistent/v{i}" for i in range(20)]
156
+
157
+ sizes = volumes._volume_sizes(mountpoints)
158
+
159
+ assert sizes == dict.fromkeys(mountpoints, 10)
160
+ assert len(calls) == volumes._SIZE_WORKERS
161
+ assert all(call[:3] == ["sudo", "-n", "du"] for call in calls)
fixos-2.2.53/VERSION DELETED
@@ -1 +0,0 @@
1
- 2.2.53
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes