fixos 2.2.50__tar.gz → 2.2.51__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 (229) hide show
  1. {fixos-2.2.50 → fixos-2.2.51}/PKG-INFO +1 -1
  2. fixos-2.2.51/fixos/__init__.py +18 -0
  3. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/ask_cmd.py +31 -2
  4. fixos-2.2.51/fixos/cli/dashboard.py +583 -0
  5. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/main.py +41 -41
  6. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/shared.py +47 -1
  7. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/shell_cmd.py +107 -37
  8. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/cache_discovery.py +46 -11
  9. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/disk_analyzer.py +32 -7
  10. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/native_scan.py +100 -7
  11. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/project_scanner.py +14 -1
  12. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/quick_snapshot.py +27 -4
  13. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/service_scanner.py +18 -2
  14. {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/PKG-INFO +1 -1
  15. {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/SOURCES.txt +5 -1
  16. {fixos-2.2.50 → fixos-2.2.51}/pyproject.toml +1 -1
  17. {fixos-2.2.50 → fixos-2.2.51}/tests/conftest.py +11 -2
  18. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_ask_and_nl_group.py +63 -2
  19. fixos-2.2.51/tests/unit/test_dashboard.py +350 -0
  20. fixos-2.2.51/tests/unit/test_disk_analyzer.py +126 -0
  21. fixos-2.2.51/tests/unit/test_native_scan.py +186 -0
  22. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_project_scanner.py +33 -0
  23. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_quick_snapshot.py +31 -0
  24. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_shell_ux.py +37 -0
  25. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_venv_cleanup.py +2 -2
  26. fixos-2.2.51/tests/unit/test_version_parity.py +38 -0
  27. fixos-2.2.50/fixos/__init__.py +0 -3
  28. fixos-2.2.50/tests/unit/test_disk_analyzer.py +0 -60
  29. {fixos-2.2.50 → fixos-2.2.51}/.env.example +0 -0
  30. {fixos-2.2.50 → fixos-2.2.51}/CHANGELOG.md +0 -0
  31. {fixos-2.2.50 → fixos-2.2.51}/LICENSE +0 -0
  32. {fixos-2.2.50 → fixos-2.2.51}/MANIFEST.in +0 -0
  33. {fixos-2.2.50 → fixos-2.2.51}/README.md +0 -0
  34. {fixos-2.2.50 → fixos-2.2.51}/docker/README.md +0 -0
  35. {fixos-2.2.50 → fixos-2.2.51}/docker/TEST_RESULTS.md +0 -0
  36. {fixos-2.2.50 → fixos-2.2.51}/docker/TEST_RESULTS_V2.md +0 -0
  37. {fixos-2.2.50 → fixos-2.2.51}/docker/alpine/Dockerfile +0 -0
  38. {fixos-2.2.50 → fixos-2.2.51}/docker/arch/Dockerfile +0 -0
  39. {fixos-2.2.50 → fixos-2.2.51}/docker/base/Dockerfile +0 -0
  40. {fixos-2.2.50 → fixos-2.2.51}/docker/broken-audio/Dockerfile +0 -0
  41. {fixos-2.2.50 → fixos-2.2.51}/docker/broken-full/Dockerfile +0 -0
  42. {fixos-2.2.50 → fixos-2.2.51}/docker/broken-network/Dockerfile +0 -0
  43. {fixos-2.2.50 → fixos-2.2.51}/docker/broken-thumbnails/Dockerfile +0 -0
  44. {fixos-2.2.50 → fixos-2.2.51}/docker/debian/Dockerfile +0 -0
  45. {fixos-2.2.50 → fixos-2.2.51}/docker/docker-compose.multi-system.yml +0 -0
  46. {fixos-2.2.50 → fixos-2.2.51}/docker/docker-compose.yml +0 -0
  47. {fixos-2.2.50 → fixos-2.2.51}/docker/fedora/Dockerfile +0 -0
  48. {fixos-2.2.50 → fixos-2.2.51}/docker/test-multi-system.sh +0 -0
  49. {fixos-2.2.50 → fixos-2.2.51}/docker/test-scenarios.sh +0 -0
  50. {fixos-2.2.50 → fixos-2.2.51}/docker/ubuntu/Dockerfile +0 -0
  51. {fixos-2.2.50 → fixos-2.2.51}/docker/validate-scenario.py +0 -0
  52. {fixos-2.2.50 → fixos-2.2.51}/docs/examples/advanced_usage.py +0 -0
  53. {fixos-2.2.50 → fixos-2.2.51}/docs/examples/quickstart.py +0 -0
  54. {fixos-2.2.50 → fixos-2.2.51}/fixos/__main__.py +0 -0
  55. {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/__init__.py +0 -0
  56. {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/autonomous.py +0 -0
  57. {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/autonomous_session.py +0 -0
  58. {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/hitl.py +0 -0
  59. {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/hitl_session.py +0 -0
  60. {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/session_core.py +0 -0
  61. {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/session_handlers.py +0 -0
  62. {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/session_io.py +0 -0
  63. {fixos-2.2.50 → fixos-2.2.51}/fixos/anonymizer.py +0 -0
  64. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/__init__.py +0 -0
  65. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_flatpak.py +0 -0
  66. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_home.py +0 -0
  67. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_snap.py +0 -0
  68. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_space.py +0 -0
  69. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_system.py +0 -0
  70. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_utils.py +0 -0
  71. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_venvs.py +0 -0
  72. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/cleanup_cmd.py +0 -0
  73. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/config_cmd.py +0 -0
  74. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/features_cmd.py +0 -0
  75. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/fix_cmd.py +0 -0
  76. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/history_cmd.py +0 -0
  77. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/jetbrains_cmd.py +0 -0
  78. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/orchestrate_cmd.py +0 -0
  79. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/output_formatter.py +0 -0
  80. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/profile_cmd.py +0 -0
  81. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/projects_cmd.py +0 -0
  82. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/provider_cmd.py +0 -0
  83. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/quick_cmd.py +0 -0
  84. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/quickfix_cmd.py +0 -0
  85. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/report_cmd.py +0 -0
  86. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/rollback_cmd.py +0 -0
  87. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/scan_cmd.py +0 -0
  88. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/token_cmd.py +0 -0
  89. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/watch_cmd.py +0 -0
  90. {fixos-2.2.50 → fixos-2.2.51}/fixos/cli.py +0 -0
  91. {fixos-2.2.50 → fixos-2.2.51}/fixos/config.py +0 -0
  92. {fixos-2.2.50 → fixos-2.2.51}/fixos/config_interactive.py +0 -0
  93. {fixos-2.2.50 → fixos-2.2.51}/fixos/constants.py +0 -0
  94. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/__init__.py +0 -0
  95. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
  96. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
  97. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
  98. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_storage_container_mixin.py +0 -0
  99. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_storage_system_mixin.py +0 -0
  100. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_storage_user_mixin.py +0 -0
  101. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/__init__.py +0 -0
  102. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/_shared.py +0 -0
  103. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/audio.py +0 -0
  104. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/file_analysis.py +0 -0
  105. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/hardware.py +0 -0
  106. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/packages.py +0 -0
  107. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/resources.py +0 -0
  108. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/security.py +0 -0
  109. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/storage_optimization.py +0 -0
  110. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/system_core.py +0 -0
  111. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/thumbnails.py +0 -0
  112. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  113. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/docker_network_cleanup.py +0 -0
  114. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
  115. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  116. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/jetbrains_ai.py +0 -0
  117. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/jetbrains_recovery.py +0 -0
  118. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/orphaned_workloads.py +0 -0
  119. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/process_chains.py +0 -0
  120. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/service_cleanup.py +0 -0
  121. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/service_details.py +0 -0
  122. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/storage_analyzer.py +0 -0
  123. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/system_checks.py +0 -0
  124. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/utils.py +0 -0
  125. {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/venv_cleanup.py +0 -0
  126. {fixos-2.2.50 → fixos-2.2.51}/fixos/endpoint_refresh.py +0 -0
  127. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/__init__.py +0 -0
  128. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/auditor.py +0 -0
  129. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/catalog.py +0 -0
  130. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/data/packages.yaml +0 -0
  131. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/data/profiles/developer.yaml +0 -0
  132. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/data/profiles/office.yaml +0 -0
  133. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/data/profiles/sysadmin.yaml +0 -0
  134. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/installer.py +0 -0
  135. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/profiles.py +0 -0
  136. {fixos-2.2.50 → fixos-2.2.51}/fixos/features/renderer.py +0 -0
  137. {fixos-2.2.50 → fixos-2.2.51}/fixos/fixes/__init__.py +0 -0
  138. {fixos-2.2.50 → fixos-2.2.51}/fixos/interactive/__init__.py +0 -0
  139. {fixos-2.2.50 → fixos-2.2.51}/fixos/interactive/cleanup_planner.py +0 -0
  140. {fixos-2.2.50 → fixos-2.2.51}/fixos/llm_shell.py +0 -0
  141. {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/__init__.py +0 -0
  142. {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/executor.py +0 -0
  143. {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/graph.py +0 -0
  144. {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/orchestrator.py +0 -0
  145. {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/rollback.py +0 -0
  146. {fixos-2.2.50 → fixos-2.2.51}/fixos/orphan_pins.py +0 -0
  147. {fixos-2.2.50 → fixos-2.2.51}/fixos/platform_utils.py +0 -0
  148. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/__init__.py +0 -0
  149. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/base.py +0 -0
  150. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/__init__.py +0 -0
  151. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/audio.py +0 -0
  152. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/disk.py +0 -0
  153. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/hardware.py +0 -0
  154. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/resources.py +0 -0
  155. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/security.py +0 -0
  156. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/thumbnails.py +0 -0
  157. {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/registry.py +0 -0
  158. {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/__init__.py +0 -0
  159. {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/desktop.yaml +0 -0
  160. {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/developer.yaml +0 -0
  161. {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/minimal.yaml +0 -0
  162. {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/server.yaml +0 -0
  163. {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/__init__.py +0 -0
  164. {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/gemini_native.py +0 -0
  165. {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/llm.py +0 -0
  166. {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/llm_analyzer.py +0 -0
  167. {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/schemas.py +0 -0
  168. {fixos-2.2.50 → fixos-2.2.51}/fixos/system_checks.py +0 -0
  169. {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/__init__.py +0 -0
  170. {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/anonymizer.py +0 -0
  171. {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/terminal.py +0 -0
  172. {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/timeout.py +0 -0
  173. {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/web_search.py +0 -0
  174. {fixos-2.2.50 → fixos-2.2.51}/fixos/watch.py +0 -0
  175. {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/dependency_links.txt +0 -0
  176. {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/entry_points.txt +0 -0
  177. {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/requires.txt +0 -0
  178. {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/top_level.txt +0 -0
  179. {fixos-2.2.50 → fixos-2.2.51}/pytest.ini +0 -0
  180. {fixos-2.2.50 → fixos-2.2.51}/requirements-dev.txt +0 -0
  181. {fixos-2.2.50 → fixos-2.2.51}/requirements.txt +0 -0
  182. {fixos-2.2.50 → fixos-2.2.51}/scripts/pyqual-calibrate.py +0 -0
  183. {fixos-2.2.50 → fixos-2.2.51}/setup.cfg +0 -0
  184. {fixos-2.2.50 → fixos-2.2.51}/setup.py +0 -0
  185. {fixos-2.2.50 → fixos-2.2.51}/tests/__init__.py +0 -0
  186. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/__init__.py +0 -0
  187. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_anonymization_layers.py +0 -0
  188. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_audio_broken.py +0 -0
  189. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_cli.py +0 -0
  190. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_diagnostics_integration.py +0 -0
  191. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_executor.py +0 -0
  192. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_multi_system.py +0 -0
  193. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_network_broken.py +0 -0
  194. {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_thumbnails_broken.py +0 -0
  195. {fixos-2.2.50 → fixos-2.2.51}/tests/test_fixos.py +0 -0
  196. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/__init__.py +0 -0
  197. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_anonymizer.py +0 -0
  198. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cache_discovery.py +0 -0
  199. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_cmd.py +0 -0
  200. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_effectiveness.py +0 -0
  201. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_retention.py +0 -0
  202. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_safety.py +0 -0
  203. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_space.py +0 -0
  204. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_tmp.py +0 -0
  205. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_utils.py +0 -0
  206. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_core.py +0 -0
  207. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_dev_project_analyzer.py +0 -0
  208. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_docker_network_cleanup.py +0 -0
  209. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_docker_startup_optimizer.py +0 -0
  210. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_endpoint_refresh.py +0 -0
  211. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_executor.py +0 -0
  212. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_gemini_native_client.py +0 -0
  213. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_jetbrains_cmd.py +0 -0
  214. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_jetbrains_recovery.py +0 -0
  215. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_llm_auth_stop.py +0 -0
  216. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_llm_provider.py +0 -0
  217. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_orchestrator.py +0 -0
  218. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_orphan_pins.py +0 -0
  219. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_orphaned_workloads.py +0 -0
  220. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_packaging_data.py +0 -0
  221. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_process_chains.py +0 -0
  222. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_projects_cmd.py +0 -0
  223. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_quick_cmd.py +0 -0
  224. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_resources_quick.py +0 -0
  225. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_security_quick.py +0 -0
  226. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_service_cleanup.py +0 -0
  227. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_service_scanner.py +0 -0
  228. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_storage_analyzer.py +0 -0
  229. {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_system_checks_parallel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.50
3
+ Version: 2.2.51
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
@@ -0,0 +1,18 @@
1
+ """fixos – AI-powered Linux/Windows diagnostics and repair."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError
6
+ from importlib.metadata import version as _get_version
7
+ from pathlib import Path
8
+
9
+ # Single Source of Truth: check VERSION file in repo root first,
10
+ # then fall back to installed package metadata.
11
+ _version_file = Path(__file__).resolve().parent.parent / "VERSION"
12
+ if _version_file.is_file():
13
+ __version__ = _version_file.read_text(encoding="utf-8").strip()
14
+ else:
15
+ try:
16
+ __version__ = _get_version("fixos")
17
+ except (PackageNotFoundError, OSError):
18
+ __version__ = "unknown"
@@ -2,6 +2,7 @@
2
2
  Natural language command (ask) for fixOS CLI
3
3
  """
4
4
 
5
+ import re
5
6
  import subprocess
6
7
  from contextlib import suppress
7
8
 
@@ -39,6 +40,7 @@ _PACKAGE_ACTION_TERMS = (
39
40
  "triage",
40
41
  )
41
42
  _PACKAGE_TRIAGE_COMMAND = ("fixos", ["cleanup", "--full", "--dry-run", "--json"])
43
+ _TMP_CLEANUP_COMMAND = ("fixos", ["cleanup", "--tmp", "--retention", "24h"])
42
44
 
43
45
 
44
46
  def _is_package_triage_request(prompt_lower: str) -> bool:
@@ -64,6 +66,13 @@ def _match_heuristic_command(prompt_lower: str) -> object | None:
64
66
  - tuple: (program, args) for subprocess
65
67
  - None: No match found
66
68
  """
69
+ if re.fullmatch(
70
+ r"(?:wyczysc|wyczyść|posprzataj|posprzątaj|clean|cleanup|clear)"
71
+ r"(?:\s+(?:katalog|directory))?\s+/tmp/?",
72
+ prompt_lower.strip(),
73
+ ):
74
+ return _TMP_CLEANUP_COMMAND
75
+
67
76
  is_docker = any(
68
77
  kw in prompt_lower
69
78
  for kw in ["docker", "kontener", "kontenery", "container", "containers"]
@@ -418,8 +427,9 @@ Przykłady:
418
427
  output = {
419
428
  "status": "error",
420
429
  "reason": "llm_error",
430
+ "prompt": prompt,
421
431
  "message": str(e),
422
- "hint": 'fixos ask "wylacz wszystkie kontenery docker"',
432
+ "hint": "fixos --help",
423
433
  }
424
434
  click.echo(yaml.dump(output, default_flow_style=False, allow_unicode=True))
425
435
 
@@ -435,6 +445,24 @@ def _handle_natural_command(prompt: str, dry_run: bool = False) -> None:
435
445
  # Stage 1: Try heuristic matching
436
446
  matched_cmd = _match_heuristic_command(prompt_lower)
437
447
 
448
+ if matched_cmd == _TMP_CLEANUP_COMMAND:
449
+ command = _format_command(matched_cmd)
450
+ if dry_run:
451
+ click.echo(yaml.dump(_build_output_dict(
452
+ status="dry_run", prompt=prompt, source="heuristics",
453
+ command=command,
454
+ ), default_flow_style=False, allow_unicode=True))
455
+ else:
456
+ from fixos.cli.cleanup_cmd import cleanup_services
457
+
458
+ click.echo(f"→ {command}")
459
+ # Invoke in this terminal: the existing cleaner owns selection,
460
+ # retention and confirmation, without shell capture or LLM calls.
461
+ click.get_current_context().invoke(
462
+ cleanup_services, tmp_cleanup=True, retention="24h", yes=False,
463
+ )
464
+ return
465
+
438
466
  if matched_cmd:
439
467
  # Heuristic match found - execute directly
440
468
  cmd_str = _format_command(matched_cmd)
@@ -448,8 +476,9 @@ def _handle_natural_command(prompt: str, dry_run: bool = False) -> None:
448
476
  output = {
449
477
  "status": "error",
450
478
  "reason": "no_api_key",
479
+ "prompt": prompt,
451
480
  "message": "Brak klucza API. Użyj: fixos token set <KLUCZ>",
452
- "hint": 'fixos ask "wylacz wszystkie kontenery docker"',
481
+ "hint": "fixos --help",
453
482
  }
454
483
  click.echo(yaml.dump(output, default_flow_style=False, allow_unicode=True))
455
484
  return
@@ -0,0 +1,583 @@
1
+ """
2
+ Interactive Graphical Shell Dashboard and Automated Safe Intervention for fixOS.
3
+ Provides rich terminal visualizations of CPU, RAM, Disk, caches, and rogue processes,
4
+ with safe, evidence-led automated intervention capabilities.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import shutil
11
+ import time
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ import click
16
+ import psutil
17
+ from rich.box import ROUNDED
18
+ from rich.console import Console
19
+ from rich.panel import Panel
20
+ from rich.progress_bar import ProgressBar
21
+ from rich.table import Table
22
+ from rich.text import Text
23
+
24
+ from fixos.diagnostics.native_scan import measure_tree, scan_dir_native
25
+ from fixos.diagnostics.process_chains import (
26
+ PRIVILEGED_ACCOUNTS,
27
+ ProcessRecord,
28
+ collect_processes,
29
+ )
30
+
31
+ # Protected application and session patterns that must NEVER be terminated
32
+ PROTECTED_PROCESS_PATTERNS = frozenset(
33
+ {
34
+ "systemd",
35
+ "init",
36
+ "dbus",
37
+ "gnome",
38
+ "kde",
39
+ "plasma",
40
+ "wayland",
41
+ "xorg",
42
+ "x11",
43
+ "pipewire",
44
+ "pulseaudio",
45
+ "wireplumber",
46
+ "login",
47
+ "sshd",
48
+ "bash",
49
+ "zsh",
50
+ "fish",
51
+ "sh",
52
+ "tmux",
53
+ "screen",
54
+ "pycharm",
55
+ "idea",
56
+ "code",
57
+ "cursor",
58
+ "vscode",
59
+ "alacritty",
60
+ "kitty",
61
+ "wezterm",
62
+ "konsole",
63
+ "gnome-terminal",
64
+ "ptyxis",
65
+ "terminal",
66
+ "fixos",
67
+ "dockerd",
68
+ "containerd",
69
+ }
70
+ )
71
+
72
+
73
+ @dataclass
74
+ class StorageItem:
75
+ name: str
76
+ path: str
77
+ size_mb: float
78
+ risk: str
79
+ safe_to_cleanup: bool
80
+ description: str
81
+
82
+
83
+ @dataclass
84
+ class ProcessItem:
85
+ pid: int
86
+ name: str
87
+ username: str
88
+ cpu_percent: float
89
+ memory_percent: float
90
+ create_time: float
91
+ runtime_str: str
92
+ cmdline_str: str
93
+ safety: str # "safe", "review", "protected"
94
+ reason: str
95
+
96
+
97
+ @dataclass
98
+ class DashboardScanData:
99
+ cpu_percent: float
100
+ cpu_cores: int
101
+ ram_percent: float
102
+ ram_used_gb: float
103
+ ram_total_gb: float
104
+ disk_percent: float
105
+ disk_used_gb: float
106
+ disk_total_gb: float
107
+ disk_free_gb: float
108
+ is_disk_critical: bool
109
+ storage_items: list[StorageItem]
110
+ safe_reclaimable_gb: float
111
+ total_scanned_storage_gb: float
112
+ heavy_processes: list[ProcessItem]
113
+ safe_terminable_pids: list[int]
114
+ warnings: list[str]
115
+
116
+
117
+ def _format_runtime(create_time: float, now: float) -> str:
118
+ elapsed = max(0, int(now - create_time))
119
+ hours, rem = divmod(elapsed, 3600)
120
+ minutes, seconds = divmod(rem, 60)
121
+ if hours > 0:
122
+ return f"{hours}h {minutes}m"
123
+ if minutes > 0:
124
+ return f"{minutes}m {seconds}s"
125
+ return f"{seconds}s"
126
+
127
+
128
+ def classify_process_safety(
129
+ proc: ProcessRecord, self_pid: int, now: float
130
+ ) -> tuple[str, str]:
131
+ """Classify whether a process is safe to terminate automatically.
132
+
133
+ Returns (safety, reason) where safety is 'safe', 'review', or 'protected'.
134
+ """
135
+ pid = proc.pid
136
+ name_lower = proc.name.lower()
137
+
138
+ if pid in (0, 1, 2) or pid == self_pid:
139
+ return "protected", "Bieżący proces lub proces systemowy PID <= 2"
140
+
141
+ if proc.username.lower() in PRIVILEGED_ACCOUNTS:
142
+ return "protected", f"Proces konta uprzywilejowanego ({proc.username})"
143
+
144
+ for pat in PROTECTED_PROCESS_PATTERNS:
145
+ if pat in name_lower:
146
+ return "protected", f"Chroniona aplikacja/środowisko ({pat})"
147
+
148
+ # Check command line arguments for IDE / development GUI markers
149
+ cmd_str = " ".join(proc.cmdline).lower()
150
+ for pat in PROTECTED_PROCESS_PATTERNS:
151
+ if pat in cmd_str and ("bin/" in cmd_str or "opt/" in cmd_str or "app" in cmd_str):
152
+ return "protected", f"Powiązany z chronionym komponentem ({pat})"
153
+
154
+ # Check for excessive CPU / memory usage
155
+ high_cpu = proc.cpu_percent >= 70.0
156
+ high_mem = proc.memory_percent >= 25.0
157
+ stale_runtime = (now - proc.create_time) >= 600 # Running > 10m
158
+
159
+ if (
160
+ (high_cpu or high_mem)
161
+ and stale_runtime
162
+ and name_lower in ("python", "python3", "node", "ruby", "perl", "worker", "php")
163
+ ):
164
+ return (
165
+ "safe",
166
+ f"Zablokowany / zapętlony worker ({proc.cpu_percent:.1f}% CPU, {proc.memory_percent:.1f}% RAM)",
167
+ )
168
+
169
+ if high_cpu:
170
+ return "review", f"Wysokie zużycie CPU ({proc.cpu_percent:.1f}%), wymaga weryfikacji"
171
+
172
+ return "review", "Standardowy proces użytkownika"
173
+
174
+
175
+ def collect_dashboard_scan() -> DashboardScanData:
176
+ """Run a fast, comprehensive system scan returning structured metrics."""
177
+ now = time.time()
178
+ warnings: list[str] = []
179
+
180
+ # 1. System CPU & Memory
181
+ cpu_percent = psutil.cpu_percent(interval=0.1)
182
+ cpu_cores = psutil.cpu_count(logical=True) or 1
183
+ vm = psutil.virtual_memory()
184
+ ram_percent = vm.percent
185
+ ram_used_gb = (vm.total - vm.available) / (1024**3)
186
+ ram_total_gb = vm.total / (1024**3)
187
+
188
+ # 2. Disk metrics
189
+ try:
190
+ du = shutil.disk_usage("/")
191
+ disk_percent = (du.used / du.total) * 100
192
+ disk_used_gb = du.used / (1024**3)
193
+ disk_total_gb = du.total / (1024**3)
194
+ disk_free_gb = du.free / (1024**3)
195
+ except OSError:
196
+ disk_percent, disk_used_gb, disk_total_gb, disk_free_gb = 0.0, 0.0, 0.0, 0.0
197
+
198
+ is_disk_critical = disk_percent >= 90.0
199
+ if is_disk_critical:
200
+ warnings.append(
201
+ f"KRYTYCZNE ZAJĘCIE DYSKU: {disk_percent:.1f}% zajęte (tylko {disk_free_gb:.1f} GB wolne)!"
202
+ )
203
+
204
+ # 3. Storage opportunities (Fast scan using Rust native where available)
205
+ storage_items: list[StorageItem] = []
206
+ cache_root = Path.home() / ".cache"
207
+ if cache_root.is_dir():
208
+ native_entries = scan_dir_native(cache_root, min_bytes=100 * 1024 * 1024)
209
+ if native_entries:
210
+ for entry in native_entries[:12]:
211
+ name = entry["name"]
212
+ size_mb = entry["bytes"] / (1024 * 1024)
213
+ # Known safe caches
214
+ is_safe = name.lower() in {
215
+ "pip", "uv", "npm", "yarn", "pnpm", "thumbnails",
216
+ "google-chrome", "mozilla", "huggingface", "mesa_shader_cache",
217
+ }
218
+ risk = "safe" if is_safe else "review"
219
+ storage_items.append(
220
+ StorageItem(
221
+ name=f"~/.cache/{name}",
222
+ path=entry["path"],
223
+ size_mb=round(size_mb, 1),
224
+ risk=risk,
225
+ safe_to_cleanup=is_safe,
226
+ description="Pamięć podręczna aplikacji / narzędzi programistycznych",
227
+ )
228
+ )
229
+
230
+ # Check temp and trash
231
+ tmp_path = Path("/tmp")
232
+ if tmp_path.is_dir():
233
+ tmp_m = measure_tree(tmp_path, native=True)
234
+ tmp_mb = tmp_m.get("bytes", 0) / (1024 * 1024)
235
+ if tmp_mb > 50:
236
+ storage_items.append(
237
+ StorageItem(
238
+ name="/tmp",
239
+ path="/tmp",
240
+ size_mb=round(tmp_mb, 1),
241
+ risk="safe",
242
+ safe_to_cleanup=True,
243
+ description="Tymczasowe pliki systemowe i procesów",
244
+ )
245
+ )
246
+
247
+ safe_reclaimable_gb = sum(
248
+ item.size_mb / 1024 for item in storage_items if item.safe_to_cleanup
249
+ )
250
+ total_scanned_storage_gb = sum(item.size_mb / 1024 for item in storage_items)
251
+
252
+ # 4. Process sampling & safety classification
253
+ self_pid = os.getpid()
254
+ all_procs = collect_processes(sample_seconds=0.1)
255
+ heavy_procs: list[ProcessItem] = []
256
+ safe_terminable_pids: list[int] = []
257
+
258
+ # Sort by CPU % descending, then Memory %
259
+ sorted_procs = sorted(
260
+ all_procs,
261
+ key=lambda p: (p.cpu_percent, p.memory_percent),
262
+ reverse=True,
263
+ )
264
+
265
+ for proc in sorted_procs:
266
+ if proc.cpu_percent >= 5.0 or proc.memory_percent >= 5.0:
267
+ safety, reason = classify_process_safety(proc, self_pid, now)
268
+ runtime_str = _format_runtime(proc.create_time, now)
269
+ cmd_preview = " ".join(proc.cmdline)[:60] if proc.cmdline else proc.name
270
+ heavy_procs.append(
271
+ ProcessItem(
272
+ pid=proc.pid,
273
+ name=proc.name,
274
+ username=proc.username,
275
+ cpu_percent=proc.cpu_percent,
276
+ memory_percent=proc.memory_percent,
277
+ create_time=proc.create_time,
278
+ runtime_str=runtime_str,
279
+ cmdline_str=cmd_preview,
280
+ safety=safety,
281
+ reason=reason,
282
+ )
283
+ )
284
+ if safety == "safe":
285
+ safe_terminable_pids.append(proc.pid)
286
+
287
+ return DashboardScanData(
288
+ cpu_percent=round(cpu_percent, 1),
289
+ cpu_cores=cpu_cores,
290
+ ram_percent=round(ram_percent, 1),
291
+ ram_used_gb=round(ram_used_gb, 2),
292
+ ram_total_gb=round(ram_total_gb, 2),
293
+ disk_percent=round(disk_percent, 1),
294
+ disk_used_gb=round(disk_used_gb, 2),
295
+ disk_total_gb=round(disk_total_gb, 2),
296
+ disk_free_gb=round(disk_free_gb, 2),
297
+ is_disk_critical=is_disk_critical,
298
+ storage_items=storage_items,
299
+ safe_reclaimable_gb=round(safe_reclaimable_gb, 2),
300
+ total_scanned_storage_gb=round(total_scanned_storage_gb, 2),
301
+ heavy_processes=heavy_procs[:10],
302
+ safe_terminable_pids=safe_terminable_pids,
303
+ warnings=warnings,
304
+ )
305
+
306
+
307
+ def render_dashboard(data: DashboardScanData, console: Console | None = None) -> None:
308
+ """Render graphical shell representation using Rich components."""
309
+ console = console or Console()
310
+
311
+ # 1. Header & Title
312
+ title = Text("FixOS • Szybki Dashboard Diagnostyczny i Optymalizacja Systemu", style="bold cyan")
313
+ console.print(Panel(title, box=ROUNDED, border_style="cyan"))
314
+
315
+ # 2. Resource Status Table
316
+ res_table = Table(box=ROUNDED, border_style="bright_blue", show_header=True, expand=True)
317
+ res_table.add_column("Zasób", style="bold white", width=14)
318
+ res_table.add_column("Wskaźnik Wykorzystania", ratio=3)
319
+ res_table.add_column("Szczegóły", style="bright_white", ratio=2)
320
+ res_table.add_column("Status", style="bold", justify="center", width=12)
321
+
322
+ def _status_badge(percent: float, crit_thresh: float = 90.0, warn_thresh: float = 75.0) -> tuple[str, str]:
323
+ if percent >= crit_thresh:
324
+ return "[red]KRYTYCZNY[/red]", "red"
325
+ if percent >= warn_thresh:
326
+ return "[yellow]OSTRZEŻENIE[/yellow]", "yellow"
327
+ return "[green]W NORMIE[/green]", "green"
328
+
329
+ # CPU bar
330
+ cpu_badge, cpu_color = _status_badge(data.cpu_percent, 85.0, 65.0)
331
+ cpu_bar = ProgressBar(total=100.0, completed=data.cpu_percent, complete_style=cpu_color)
332
+ res_table.add_row(
333
+ "CPU",
334
+ cpu_bar,
335
+ f"{data.cpu_percent}% ({data.cpu_cores} rdzeni logicznych)",
336
+ cpu_badge,
337
+ )
338
+
339
+ # RAM bar
340
+ ram_badge, ram_color = _status_badge(data.ram_percent, 90.0, 75.0)
341
+ ram_bar = ProgressBar(total=100.0, completed=data.ram_percent, complete_style=ram_color)
342
+ res_table.add_row(
343
+ "Pamięć RAM",
344
+ ram_bar,
345
+ f"{data.ram_percent}% ({data.ram_used_gb:.1f} GB / {data.ram_total_gb:.1f} GB)",
346
+ ram_badge,
347
+ )
348
+
349
+ # Disk bar
350
+ disk_badge, disk_color = _status_badge(data.disk_percent, 90.0, 80.0)
351
+ disk_bar = ProgressBar(total=100.0, completed=data.disk_percent, complete_style=disk_color)
352
+ res_table.add_row(
353
+ "Dysk (/)",
354
+ disk_bar,
355
+ f"{data.disk_percent}% (Wolne: {data.disk_free_gb:.1f} GB z {data.disk_total_gb:.1f} GB)",
356
+ disk_badge,
357
+ )
358
+
359
+ console.print(Panel(res_table, title="[bold]1. Główne Zasoby Systemowe[/bold]", box=ROUNDED, border_style="blue"))
360
+
361
+ # Warnings / Alerts
362
+ if data.warnings:
363
+ for w in data.warnings:
364
+ console.print(Panel(Text(f"⚠️ {w}", style="bold red"), border_style="red"))
365
+
366
+ # 3. Storage Table
367
+ storage_table = Table(box=ROUNDED, border_style="bright_magenta", show_header=True, expand=True)
368
+ storage_table.add_column("Lokalizacja / Cache", style="white", ratio=2)
369
+ storage_table.add_column("Rozmiar", justify="right", style="cyan", width=12)
370
+ storage_table.add_column("Kategoria Ryzyka", justify="center", width=16)
371
+ storage_table.add_column("Opis & Rekomendacja", style="bright_black", ratio=3)
372
+
373
+ if data.storage_items:
374
+ for item in data.storage_items:
375
+ size_str = f"{item.size_mb / 1024:.2f} GB" if item.size_mb >= 1024 else f"{item.size_mb:.0f} MB"
376
+ if item.safe_to_cleanup:
377
+ risk_tag = "[green]BEZPIECZNE[/green]"
378
+ elif item.risk == "review":
379
+ risk_tag = "[yellow]DO DECYZJI[/yellow]"
380
+ else:
381
+ risk_tag = "[red]CHRONIONE[/red]"
382
+ storage_table.add_row(item.name, size_str, risk_tag, item.description)
383
+ else:
384
+ storage_table.add_row("Brak dużych katalogów cache powyżej progu", "-", "-", "-")
385
+
386
+ storage_title = (
387
+ f"[bold]2. Przestrzeń Dysku & Cache (Bezpieczny odzysk: [green]{data.safe_reclaimable_gb:.2f} GB[/green] / "
388
+ f"Łącznie: {data.total_scanned_storage_gb:.2f} GB)[/bold]"
389
+ )
390
+ console.print(Panel(storage_table, title=storage_title, box=ROUNDED, border_style="magenta"))
391
+
392
+ # 4. Top Heavy Processes Table
393
+ proc_table = Table(box=ROUNDED, border_style="bright_yellow", show_header=True, expand=True)
394
+ proc_table.add_column("PID", justify="right", style="cyan", width=8)
395
+ proc_table.add_column("Nazwa", style="bold white", width=16)
396
+ proc_table.add_column("CPU %", justify="right", style="yellow", width=8)
397
+ proc_table.add_column("RAM %", justify="right", style="magenta", width=8)
398
+ proc_table.add_column("Czas", justify="right", style="white", width=10)
399
+ proc_table.add_column("Bezpieczeństwo Zatrzymania", justify="center", width=26)
400
+ proc_table.add_column("Uzasadnienie", style="bright_black", ratio=3)
401
+
402
+ if data.heavy_processes:
403
+ for p in data.heavy_processes:
404
+ if p.safety == "safe":
405
+ safety_tag = "[bold green]BEZPIECZNE ZAMKNIĘCIE[/bold green]"
406
+ elif p.safety == "review":
407
+ safety_tag = "[yellow]DO DECYZJI[/yellow]"
408
+ else:
409
+ safety_tag = "[red]CHRONIONY (SYSTEM/IDE)[/red]"
410
+ proc_table.add_row(
411
+ str(p.pid),
412
+ p.name,
413
+ f"{p.cpu_percent:.1f}%",
414
+ f"{p.memory_percent:.1f}%",
415
+ p.runtime_str,
416
+ safety_tag,
417
+ p.reason,
418
+ )
419
+ else:
420
+ proc_table.add_row("-", "Brak procesów o wysokim obciążeniu", "-", "-", "-", "-", "-")
421
+
422
+ safe_count = len(data.safe_terminable_pids)
423
+ proc_title = (
424
+ f"[bold]3. Procesy o Wysokim Obciążeniu (Bezpieczne do zakończenia: [green]{safe_count}[/green])[/bold]"
425
+ )
426
+ console.print(Panel(proc_table, title=proc_title, box=ROUNDED, border_style="yellow"))
427
+
428
+
429
+ def execute_safe_cache_cleanup(data: DashboardScanData, *, dry_run: bool = False) -> tuple[int, float]:
430
+ """Safely delete only items marked as safe_to_cleanup."""
431
+ cleaned_count = 0
432
+ reclaimed_mb = 0.0
433
+
434
+ for item in data.storage_items:
435
+ if not item.safe_to_cleanup:
436
+ continue
437
+
438
+ p = Path(item.path)
439
+ if not p.exists():
440
+ continue
441
+
442
+ if dry_run:
443
+ click.echo(f" [DRY-RUN] Usunięcie bezpiecznego cache: {item.name} ({item.size_mb:.1f} MB)")
444
+ cleaned_count += 1
445
+ reclaimed_mb += item.size_mb
446
+ continue
447
+
448
+ try:
449
+ if p.is_dir():
450
+ shutil.rmtree(p, ignore_errors=True)
451
+ p.mkdir(parents=True, exist_ok=True)
452
+ elif p.is_file():
453
+ p.unlink(missing_ok=True)
454
+ click.echo(click.style(f" ✓ Oczyszczono: {item.name} ({item.size_mb:.1f} MB)", fg="green"))
455
+ cleaned_count += 1
456
+ reclaimed_mb += item.size_mb
457
+ except OSError as exc:
458
+ click.echo(click.style(f" ✗ Błąd czyszczenia {item.name}: {exc}", fg="red"))
459
+
460
+ return cleaned_count, reclaimed_mb
461
+
462
+
463
+ def execute_safe_process_termination(data: DashboardScanData, *, dry_run: bool = False) -> list[int]:
464
+ """Terminate only processes verified as safe to terminate."""
465
+ terminated: list[int] = []
466
+
467
+ for item in data.heavy_processes:
468
+ if item.safety != "safe":
469
+ continue
470
+
471
+ pid = item.pid
472
+ if dry_run:
473
+ click.echo(f" [DRY-RUN] Bezpieczne zakończenie procesu PID {pid} ({item.name}): {item.reason}")
474
+ terminated.append(pid)
475
+ continue
476
+
477
+ try:
478
+ proc = psutil.Process(pid)
479
+ # Revalidate creation time
480
+ if abs(proc.create_time() - item.create_time) > 0.1:
481
+ click.echo(click.style(f" ⚠️ Pominięto PID {pid}: PID został ponownie użyty", fg="yellow"))
482
+ continue
483
+
484
+ click.echo(click.style(f" → Zamykanie procesu PID {pid} ({item.name})...", fg="cyan"))
485
+ proc.terminate()
486
+ try:
487
+ proc.wait(timeout=1.5)
488
+ except psutil.TimeoutExpired:
489
+ proc.kill()
490
+ click.echo(click.style(f" ✓ Zakończono PID {pid} ({item.name})", fg="green"))
491
+ terminated.append(pid)
492
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as exc:
493
+ click.echo(click.style(f" ⚠️ Proces PID {pid} nie mógł zostać zamknięty: {exc}", fg="yellow"))
494
+
495
+ return terminated
496
+
497
+
498
+ def run_interactive_dashboard(dry_run: bool = False) -> None:
499
+ """Run interactive visual dashboard loop."""
500
+ console = Console()
501
+
502
+ while True:
503
+ console.clear()
504
+ console.print("[dim cyan]Zbieranie danych diagnostycznych (Rust native scan)...[/dim cyan]")
505
+ data = collect_dashboard_scan()
506
+ render_dashboard(data, console)
507
+
508
+ console.print()
509
+ console.print("[bold cyan]Opcje automatycznej bezpiecznej interwencji:[/bold cyan]")
510
+ console.print(" [bold yellow][1][/bold yellow] Wykonaj bezpieczne czyszczenie cache (pip, uv, npm, thumbnails, /tmp)")
511
+ console.print(" [bold yellow][2][/bold yellow] Bezpiecznie zakończ zapętlone procesy o wysokim obciążeniu")
512
+ console.print(" [bold yellow][3][/bold yellow] Pełna automatyczna interwencja (1 + 2)")
513
+ console.print(" [bold yellow][r][/bold yellow] Odśwież skan")
514
+ console.print(" [bold yellow][q][/bold yellow] Wyjście do menu")
515
+ console.print()
516
+
517
+ choice = click.prompt("Wybierz opcję", default="q", show_default=True).strip().lower()
518
+
519
+ if choice in ("q", "quit", "exit"):
520
+ break
521
+ if choice in ("r", "refresh"):
522
+ continue
523
+ if choice == "1":
524
+ console.print("\n[bold]Czyszczenie bezpiecznego cache:[/bold]")
525
+ count, mb = execute_safe_cache_cleanup(data, dry_run=dry_run)
526
+ console.print(f"\n[green]Zakończono: oczyszczono {count} pozycji, odzyskano {mb:.1f} MB.[/green]")
527
+ click.pause("\nNaciśnij dowolny klawisz, aby odświeżyć...")
528
+ elif choice == "2":
529
+ console.print("\n[bold]Zamykanie bezpiecznych procesów:[/bold]")
530
+ if not data.safe_terminable_pids:
531
+ console.print("[yellow]Brak procesów zakwalifikowanych jako bezpieczne do automatycznego zamknięcia.[/yellow]")
532
+ else:
533
+ pids = execute_safe_process_termination(data, dry_run=dry_run)
534
+ console.print(f"\n[green]Zakończono: zamknięto {len(pids)} procesów.[/green]")
535
+ click.pause("\nNaciśnij dowolny klawisz, aby odświeżyć...")
536
+ elif choice == "3":
537
+ console.print("\n[bold]Pełna bezpieczna interwencja:[/bold]")
538
+ count, mb = execute_safe_cache_cleanup(data, dry_run=dry_run)
539
+ pids = execute_safe_process_termination(data, dry_run=dry_run)
540
+ console.print(f"\n[green]Podsumowanie: odzyskano {mb:.1f} MB, zakończono {len(pids)} procesów.[/green]")
541
+ click.pause("\nNaciśnij dowolny klawisz, aby odświeżyć...")
542
+
543
+
544
+ @click.command("dashboard")
545
+ @click.option("--dry-run", is_flag=True, default=False, help="Symuluj interwencję bez usuwania plików i zabijania procesów")
546
+ @click.option("--auto-fix", is_flag=True, default=False, help="Automatycznie wykonaj bezpieczną interwencję bez pytań")
547
+ @click.option("--json", "json_output", is_flag=True, default=False, help="Zwróć dane skanu w formacie JSON")
548
+ def dashboard_cmd(dry_run: bool, auto_fix: bool, json_output: bool) -> None:
549
+ """Graficzna reprezentacja stanu systemu w shellu i automatyczna interwencja."""
550
+ if json_output:
551
+ import json
552
+
553
+ data = collect_dashboard_scan()
554
+ out = {
555
+ "cpu_percent": data.cpu_percent,
556
+ "ram_percent": data.ram_percent,
557
+ "disk_percent": data.disk_percent,
558
+ "disk_free_gb": data.disk_free_gb,
559
+ "safe_reclaimable_gb": data.safe_reclaimable_gb,
560
+ "storage_items": [
561
+ {"name": i.name, "path": i.path, "size_mb": i.size_mb, "risk": i.risk, "safe": i.safe_to_cleanup}
562
+ for i in data.storage_items
563
+ ],
564
+ "heavy_processes": [
565
+ {"pid": p.pid, "name": p.name, "cpu": p.cpu_percent, "ram": p.memory_percent, "safety": p.safety}
566
+ for p in data.heavy_processes
567
+ ],
568
+ }
569
+ click.echo(json.dumps(out, indent=2))
570
+ return
571
+
572
+ if auto_fix:
573
+ console = Console()
574
+ console.print("[dim cyan]Wykonywanie automatycznej bezpiecznej interwencji...[/dim cyan]")
575
+ data = collect_dashboard_scan()
576
+ render_dashboard(data, console)
577
+ console.print("\n[bold]Uruchamianie interwencji:[/bold]")
578
+ _count, mb = execute_safe_cache_cleanup(data, dry_run=dry_run)
579
+ pids = execute_safe_process_termination(data, dry_run=dry_run)
580
+ console.print(f"\n[bold green]Interwencja zakończona: odzyskano {mb:.1f} MB, zamknięto {len(pids)} procesów.[/bold green]")
581
+ return
582
+
583
+ run_interactive_dashboard(dry_run=dry_run)