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.
- {fixos-2.2.50 → fixos-2.2.51}/PKG-INFO +1 -1
- fixos-2.2.51/fixos/__init__.py +18 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/ask_cmd.py +31 -2
- fixos-2.2.51/fixos/cli/dashboard.py +583 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/main.py +41 -41
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/shared.py +47 -1
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/shell_cmd.py +107 -37
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/cache_discovery.py +46 -11
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/disk_analyzer.py +32 -7
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/native_scan.py +100 -7
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/project_scanner.py +14 -1
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/quick_snapshot.py +27 -4
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/service_scanner.py +18 -2
- {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/PKG-INFO +1 -1
- {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/SOURCES.txt +5 -1
- {fixos-2.2.50 → fixos-2.2.51}/pyproject.toml +1 -1
- {fixos-2.2.50 → fixos-2.2.51}/tests/conftest.py +11 -2
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_ask_and_nl_group.py +63 -2
- fixos-2.2.51/tests/unit/test_dashboard.py +350 -0
- fixos-2.2.51/tests/unit/test_disk_analyzer.py +126 -0
- fixos-2.2.51/tests/unit/test_native_scan.py +186 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_project_scanner.py +33 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_quick_snapshot.py +31 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_shell_ux.py +37 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_venv_cleanup.py +2 -2
- fixos-2.2.51/tests/unit/test_version_parity.py +38 -0
- fixos-2.2.50/fixos/__init__.py +0 -3
- fixos-2.2.50/tests/unit/test_disk_analyzer.py +0 -60
- {fixos-2.2.50 → fixos-2.2.51}/.env.example +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/CHANGELOG.md +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/LICENSE +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/MANIFEST.in +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/README.md +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/README.md +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/TEST_RESULTS.md +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/TEST_RESULTS_V2.md +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/alpine/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/arch/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/base/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/broken-audio/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/broken-full/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/broken-network/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/broken-thumbnails/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/debian/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/docker-compose.multi-system.yml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/docker-compose.yml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/fedora/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/test-multi-system.sh +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/test-scenarios.sh +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/ubuntu/Dockerfile +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docker/validate-scenario.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docs/examples/advanced_usage.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/docs/examples/quickstart.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/__main__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/autonomous.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/autonomous_session.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/hitl.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/hitl_session.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/session_core.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/session_handlers.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/agent/session_io.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/anonymizer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_flatpak.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_home.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_snap.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_space.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_system.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_utils.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/_cleanup_venvs.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/cleanup_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/config_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/features_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/fix_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/history_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/jetbrains_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/orchestrate_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/output_formatter.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/profile_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/projects_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/provider_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/quick_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/quickfix_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/report_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/rollback_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/scan_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/token_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli/watch_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/cli.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/config.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/config_interactive.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/constants.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_storage_container_mixin.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_storage_system_mixin.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/_storage_user_mixin.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/_shared.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/audio.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/file_analysis.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/hardware.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/packages.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/resources.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/security.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/storage_optimization.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/system_core.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/checks/thumbnails.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/dev_project_analyzer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/docker_network_cleanup.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/flatpak_analyzer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/jetbrains_ai.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/jetbrains_recovery.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/orphaned_workloads.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/process_chains.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/service_cleanup.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/service_details.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/storage_analyzer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/system_checks.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/utils.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/diagnostics/venv_cleanup.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/endpoint_refresh.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/auditor.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/catalog.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/data/packages.yaml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/data/profiles/developer.yaml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/data/profiles/office.yaml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/data/profiles/sysadmin.yaml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/installer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/profiles.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/features/renderer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/fixes/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/interactive/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/interactive/cleanup_planner.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/llm_shell.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/executor.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/graph.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/orchestrator.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/orchestrator/rollback.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/orphan_pins.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/platform_utils.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/base.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/audio.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/disk.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/hardware.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/resources.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/security.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/builtin/thumbnails.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/plugins/registry.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/desktop.yaml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/developer.yaml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/minimal.yaml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/profiles/server.yaml +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/gemini_native.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/llm.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/llm_analyzer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/providers/schemas.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/system_checks.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/anonymizer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/terminal.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/timeout.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/utils/web_search.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos/watch.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/dependency_links.txt +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/entry_points.txt +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/requires.txt +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/fixos.egg-info/top_level.txt +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/pytest.ini +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/requirements-dev.txt +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/requirements.txt +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/scripts/pyqual-calibrate.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/setup.cfg +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/setup.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_anonymization_layers.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_audio_broken.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_cli.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_diagnostics_integration.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_executor.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_multi_system.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_network_broken.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/e2e/test_thumbnails_broken.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/test_fixos.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/__init__.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_anonymizer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cache_discovery.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_effectiveness.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_retention.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_safety.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_space.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_tmp.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_cleanup_utils.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_core.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_dev_project_analyzer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_docker_network_cleanup.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_docker_startup_optimizer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_endpoint_refresh.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_executor.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_gemini_native_client.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_jetbrains_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_jetbrains_recovery.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_llm_auth_stop.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_llm_provider.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_orchestrator.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_orphan_pins.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_orphaned_workloads.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_packaging_data.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_process_chains.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_projects_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_quick_cmd.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_resources_quick.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_security_quick.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_service_cleanup.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_service_scanner.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_storage_analyzer.py +0 -0
- {fixos-2.2.50 → fixos-2.2.51}/tests/unit/test_system_checks_parallel.py +0 -0
|
@@ -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":
|
|
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":
|
|
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)
|