fixos 2.2.52__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.
- {fixos-2.2.52 → fixos-2.2.54}/PKG-INFO +1 -1
- fixos-2.2.54/VERSION +1 -0
- fixos-2.2.54/fixos/cli/_cleanup_docker_volumes.py +221 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/_cleanup_space.py +36 -4
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/cleanup_cmd.py +61 -7
- {fixos-2.2.52 → fixos-2.2.54}/fixos/config.py +24 -2
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/_storage_container_mixin.py +8 -4
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/native_scan.py +15 -3
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/quick_snapshot.py +31 -2
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/service_cleanup.py +51 -6
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/service_scanner.py +61 -2
- {fixos-2.2.52 → fixos-2.2.54}/fixos/interactive/cleanup_planner.py +26 -28
- {fixos-2.2.52 → fixos-2.2.54}/fixos.egg-info/PKG-INFO +1 -1
- {fixos-2.2.52 → fixos-2.2.54}/fixos.egg-info/SOURCES.txt +4 -0
- {fixos-2.2.52 → fixos-2.2.54}/pyproject.toml +1 -1
- fixos-2.2.54/tests/unit/test_cleanup_docker_volumes.py +161 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cleanup_effectiveness.py +93 -0
- fixos-2.2.54/tests/unit/test_cleanup_planner.py +41 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cleanup_tmp.py +43 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_native_scan.py +26 -0
- fixos-2.2.54/tests/unit/test_ollama_endpoint.py +46 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_quick_snapshot.py +33 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_service_cleanup.py +75 -0
- fixos-2.2.52/VERSION +0 -1
- {fixos-2.2.52 → fixos-2.2.54}/.env.example +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/CHANGELOG.md +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/LICENSE +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/MANIFEST.in +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/README.md +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/README.md +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/TEST_RESULTS.md +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/TEST_RESULTS_V2.md +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/alpine/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/arch/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/base/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/broken-audio/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/broken-full/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/broken-network/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/broken-thumbnails/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/debian/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/docker-compose.multi-system.yml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/docker-compose.yml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/fedora/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/test-multi-system.sh +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/test-scenarios.sh +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/ubuntu/Dockerfile +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docker/validate-scenario.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docs/examples/advanced_usage.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/docs/examples/quickstart.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/__main__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/agent/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/agent/autonomous.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/agent/autonomous_session.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/agent/hitl.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/agent/hitl_session.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/agent/session_core.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/agent/session_handlers.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/agent/session_io.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/anonymizer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/__main__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/_cleanup_flatpak.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/_cleanup_home.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/_cleanup_snap.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/_cleanup_system.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/_cleanup_utils.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/_cleanup_venvs.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/ask_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/config_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/dashboard.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/features_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/fix_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/history_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/jetbrains_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/main.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/orchestrate_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/output_formatter.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/profile_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/projects_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/provider_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/quick_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/quickfix_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/report_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/rollback_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/scan_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/shared.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/shell_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/token_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli/watch_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/cli.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/config_interactive.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/constants.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/_storage_system_mixin.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/_storage_user_mixin.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/cache_discovery.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/_shared.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/audio.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/file_analysis.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/hardware.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/packages.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/resources.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/security.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/storage_optimization.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/system_core.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/checks/thumbnails.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/dev_project_analyzer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/disk_analyzer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/docker_network_cleanup.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/flatpak_analyzer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/jetbrains_ai.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/jetbrains_recovery.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/orphaned_workloads.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/process_chains.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/project_scanner.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/service_details.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/storage_analyzer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/system_checks.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/utils.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/diagnostics/venv_cleanup.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/endpoint_refresh.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/auditor.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/catalog.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/data/packages.yaml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/data/profiles/developer.yaml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/data/profiles/office.yaml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/data/profiles/sysadmin.yaml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/installer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/profiles.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/features/renderer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/fixes/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/interactive/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/llm_shell.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/orchestrator/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/orchestrator/executor.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/orchestrator/graph.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/orchestrator/orchestrator.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/orchestrator/rollback.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/orphan_pins.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/platform_utils.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/base.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/builtin/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/builtin/audio.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/builtin/disk.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/builtin/hardware.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/builtin/resources.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/builtin/security.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/builtin/thumbnails.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/plugins/registry.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/profiles/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/profiles/desktop.yaml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/profiles/developer.yaml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/profiles/minimal.yaml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/profiles/server.yaml +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/providers/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/providers/gemini_native.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/providers/llm.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/providers/llm_analyzer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/providers/schemas.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/system_checks.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/utils/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/utils/anonymizer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/utils/terminal.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/utils/timeout.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/utils/web_search.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos/watch.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos.egg-info/dependency_links.txt +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos.egg-info/entry_points.txt +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos.egg-info/requires.txt +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/fixos.egg-info/top_level.txt +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/pytest.ini +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/requirements-dev.txt +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/requirements.txt +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/scripts/pyqual-calibrate.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/setup.cfg +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/setup.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/conftest.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/test_anonymization_layers.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/test_audio_broken.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/test_cli.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/test_diagnostics_integration.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/test_executor.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/test_multi_system.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/test_network_broken.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/e2e/test_thumbnails_broken.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/test_fixos.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/__init__.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_anonymizer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_ask_and_nl_group.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cache_discovery.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cleanup_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cleanup_retention.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cleanup_safety.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cleanup_space.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cleanup_utils.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_cli_main.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_core.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_dashboard.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_dev_project_analyzer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_disk_analyzer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_docker_network_cleanup.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_docker_startup_optimizer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_endpoint_refresh.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_executor.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_gemini_native_client.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_jetbrains_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_jetbrains_recovery.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_llm_auth_stop.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_llm_provider.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_orchestrator.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_orphan_pins.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_orphaned_workloads.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_packaging_data.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_process_chains.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_project_scanner.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_projects_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_quick_cmd.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_resources_quick.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_security_quick.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_service_scanner.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_shell_ux.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_storage_analyzer.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_system_checks_parallel.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_venv_cleanup.py +0 -0
- {fixos-2.2.52 → fixos-2.2.54}/tests/unit/test_version_parity.py +0 -0
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)}.")
|
|
@@ -52,6 +52,31 @@ def _run_cmd(argv: list[str], timeout: int = 60) -> subprocess.CompletedProcess:
|
|
|
52
52
|
return subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
_DU_BATCH = 512
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _dir_sizes(paths: list[Path]) -> dict[str, int]:
|
|
59
|
+
"""Sizes in bytes for many directories with one ``du`` per batch.
|
|
60
|
+
|
|
61
|
+
``--count-links`` keeps each directory independent of the others, as a
|
|
62
|
+
separate ``du`` would be; NUL-terminated output survives any file name.
|
|
63
|
+
Paths missing from the result must be measured individually.
|
|
64
|
+
"""
|
|
65
|
+
sizes: dict[str, int] = {}
|
|
66
|
+
for start in range(0, len(paths), _DU_BATCH):
|
|
67
|
+
batch = [str(path) for path in paths[start : start + _DU_BATCH]]
|
|
68
|
+
try:
|
|
69
|
+
result = _run_cmd(["du", "-sbl0", "--", *batch], timeout=300)
|
|
70
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
71
|
+
continue
|
|
72
|
+
# du exits 1 when some subtree is unreadable but still prints totals.
|
|
73
|
+
for record in result.stdout.split("\0"):
|
|
74
|
+
size, sep, name = record.partition("\t")
|
|
75
|
+
if sep and size.isdigit():
|
|
76
|
+
sizes[name] = int(size)
|
|
77
|
+
return sizes
|
|
78
|
+
|
|
79
|
+
|
|
55
80
|
def _dir_size(path: Path) -> int:
|
|
56
81
|
"""Return directory size in bytes (du -sb with a Python fallback)."""
|
|
57
82
|
try:
|
|
@@ -876,6 +901,7 @@ def _scan_tmp(
|
|
|
876
901
|
cutoff_seconds = days * 86400
|
|
877
902
|
tmp_path = Path(tmp_dir) if tmp_dir is not None else Path("/tmp")
|
|
878
903
|
candidates = []
|
|
904
|
+
directories = []
|
|
879
905
|
|
|
880
906
|
if tmp_path.is_dir():
|
|
881
907
|
try:
|
|
@@ -893,23 +919,29 @@ def _scan_tmp(
|
|
|
893
919
|
mtime = stat_val.st_mtime
|
|
894
920
|
age_seconds = now - mtime
|
|
895
921
|
if age_seconds >= cutoff_seconds:
|
|
896
|
-
|
|
897
|
-
stat_val.st_size if not item.is_dir() else _dir_size(item)
|
|
898
|
-
)
|
|
922
|
+
is_dir = stat_module.S_ISDIR(stat_val.st_mode)
|
|
899
923
|
age_days = age_seconds / 86400
|
|
900
924
|
candidates.append({
|
|
901
925
|
"description": f"{name} (wiek: {age_days:.1f} dni)",
|
|
902
926
|
"path": item,
|
|
903
927
|
"name": name,
|
|
904
|
-
"size_bytes":
|
|
928
|
+
"size_bytes": 0 if is_dir else stat_val.st_size,
|
|
905
929
|
"note": f"{age_days:.1f} dni",
|
|
906
930
|
"age_days": age_days,
|
|
907
931
|
})
|
|
932
|
+
if is_dir:
|
|
933
|
+
directories.append(candidates[-1])
|
|
908
934
|
except (OSError, PermissionError):
|
|
909
935
|
continue
|
|
910
936
|
except (OSError, PermissionError):
|
|
911
937
|
pass
|
|
912
938
|
|
|
939
|
+
# One `du` per directory meant thousands of processes on a busy /tmp.
|
|
940
|
+
sizes = _dir_sizes([c["path"] for c in directories])
|
|
941
|
+
for candidate in directories:
|
|
942
|
+
size = sizes.get(str(candidate["path"]))
|
|
943
|
+
candidate["size_bytes"] = size if size is not None else _dir_size(candidate["path"])
|
|
944
|
+
|
|
913
945
|
return candidates
|
|
914
946
|
|
|
915
947
|
|
|
@@ -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 (
|
|
@@ -129,11 +131,7 @@ def _clean_terminal_input(value: str) -> str:
|
|
|
129
131
|
|
|
130
132
|
def _display_service_item(svc: dict) -> None:
|
|
131
133
|
"""Display a single service item with details."""
|
|
132
|
-
size_str = (
|
|
133
|
-
f"{svc['size_gb']:.2f} GB"
|
|
134
|
-
if svc["size_gb"] >= 1
|
|
135
|
-
else f"{svc['size_mb']:.0f} MB"
|
|
136
|
-
)
|
|
134
|
+
size_str = _size_str(svc)
|
|
137
135
|
risk = svc.get("risk_level") or ("safe" if svc["safe_to_cleanup"] else "review")
|
|
138
136
|
safe_text, safe_color = _RISK_LABELS.get(risk, _RISK_LABELS["review"])
|
|
139
137
|
|
|
@@ -164,6 +162,8 @@ def _display_service_item(svc: dict) -> None:
|
|
|
164
162
|
)
|
|
165
163
|
else:
|
|
166
164
|
click.echo(f" Komponenty: {svc['details']['components']}")
|
|
165
|
+
elif svc["service_type"] == "docker" and svc["details"].get("fast_inventory"):
|
|
166
|
+
_display_docker_inventory(svc["details"]["fast_inventory"])
|
|
167
167
|
elif svc["service_type"] == "ollama" and svc["details"].get("models"):
|
|
168
168
|
models = svc["details"]["models"]
|
|
169
169
|
if models:
|
|
@@ -173,6 +173,26 @@ def _display_service_item(svc: dict) -> None:
|
|
|
173
173
|
click.echo()
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
def _display_docker_inventory(inventory: dict) -> None:
|
|
177
|
+
"""Show what a timed-out `docker system df` could not size."""
|
|
178
|
+
click.echo(" Docker — szybki spis (bez rozmiarów wolumenów):")
|
|
179
|
+
pairs = (
|
|
180
|
+
("obrazy", "images", "dangling_images", "wiszących"),
|
|
181
|
+
("kontenery", "containers", "stopped_containers", "zatrzymanych"),
|
|
182
|
+
("wolumeny", "volumes", "dangling_volumes", "nieużywanych — dane, tylko ręcznie"),
|
|
183
|
+
)
|
|
184
|
+
for label, total_key, part_key, part_label in pairs:
|
|
185
|
+
if total_key in inventory:
|
|
186
|
+
part = inventory.get(part_key)
|
|
187
|
+
suffix = f" ({part} {part_label})" if part is not None else ""
|
|
188
|
+
click.echo(f" • {label}: {inventory[total_key]}{suffix}")
|
|
189
|
+
if "build_cache_total_gb" in inventory:
|
|
190
|
+
click.echo(
|
|
191
|
+
f" • cache buildów: {inventory['build_cache_total_gb']:.2f} GB "
|
|
192
|
+
f"(odzyskiwalne {inventory.get('build_cache_reclaimable_gb', 0):.2f} GB)"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
176
196
|
def _error_message(result: dict) -> str:
|
|
177
197
|
"""Human-readable failure reason, even when the command's own stderr was
|
|
178
198
|
empty (e.g. suppressed with 2>/dev/null inside the cleanup command
|
|
@@ -1593,7 +1613,7 @@ def _execute_yes_cleanup(plan: dict, scanner, dry_run: bool = False) -> None:
|
|
|
1593
1613
|
"Wyczyść konkretną usługę "
|
|
1594
1614
|
"(docker, docker-all, docker-old, docker-containers, docker-networks, "
|
|
1595
1615
|
"docker-stale-services, orphaned-projects, ollama-old, snap-old, "
|
|
1596
|
-
"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, "
|
|
1597
1617
|
"npm, ...)"
|
|
1598
1618
|
),
|
|
1599
1619
|
)
|
|
@@ -1701,6 +1721,23 @@ def _execute_yes_cleanup(plan: dict, scanner, dry_run: bool = False) -> None:
|
|
|
1701
1721
|
f"(domyślnie {DEFAULT_OLLAMA_OLD_UNUSED_DAYS}); pomija modele aktualnie uruchomione"
|
|
1702
1722
|
),
|
|
1703
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
|
+
)
|
|
1704
1741
|
@click.option(
|
|
1705
1742
|
"--snap-old",
|
|
1706
1743
|
"snap_old",
|
|
@@ -1875,6 +1912,8 @@ def cleanup_services(
|
|
|
1875
1912
|
list_orphan_pins,
|
|
1876
1913
|
process_hours,
|
|
1877
1914
|
ollama_old,
|
|
1915
|
+
docker_volumes,
|
|
1916
|
+
match,
|
|
1878
1917
|
snap_old,
|
|
1879
1918
|
journal_cleanup,
|
|
1880
1919
|
user_cache,
|
|
@@ -1933,6 +1972,8 @@ def cleanup_services(
|
|
|
1933
1972
|
fixos cleanup --unpin-orphan-project /absolute/missing/project
|
|
1934
1973
|
# usuwa ochronę, ale nie zmienia Dockera ani procesów
|
|
1935
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
|
|
1936
1977
|
# modele Ollama niezmieniane od 90+ dni
|
|
1937
1978
|
fixos cleanup -c ollama-old
|
|
1938
1979
|
fixos cleanup --snap-old # wyłączone rewizje snapów
|
|
@@ -1986,6 +2027,7 @@ def cleanup_services(
|
|
|
1986
2027
|
list_orphan_pins,
|
|
1987
2028
|
ollama_old,
|
|
1988
2029
|
snap_old,
|
|
2030
|
+
docker_volumes,
|
|
1989
2031
|
journal_cleanup,
|
|
1990
2032
|
user_cache,
|
|
1991
2033
|
jetbrains,
|
|
@@ -2035,6 +2077,7 @@ def cleanup_services(
|
|
|
2035
2077
|
ollama_old,
|
|
2036
2078
|
docker_buildcache,
|
|
2037
2079
|
snap_old,
|
|
2080
|
+
docker_volumes,
|
|
2038
2081
|
journal_cleanup,
|
|
2039
2082
|
user_cache,
|
|
2040
2083
|
jetbrains,
|
|
@@ -2088,6 +2131,17 @@ def cleanup_services(
|
|
|
2088
2131
|
"libvirt": (libvirt, _cleanup_libvirt),
|
|
2089
2132
|
"gitive": (gitive, _cleanup_gitive),
|
|
2090
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
|
+
),
|
|
2091
2145
|
"tmp": (
|
|
2092
2146
|
tmp_cleanup,
|
|
2093
2147
|
lambda j, d, lst, y: _cleanup_tmp(
|
|
@@ -2150,7 +2204,7 @@ def cleanup_services(
|
|
|
2150
2204
|
click.echo(
|
|
2151
2205
|
click.style(
|
|
2152
2206
|
"Akcje odzyskiwania miejsca (snap/journal/cache/JetBrains/"
|
|
2153
|
-
"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ę.",
|
|
2154
2208
|
fg="red",
|
|
2155
2209
|
)
|
|
2156
2210
|
)
|
|
@@ -128,7 +128,8 @@ PROVIDER_DEFAULTS = {
|
|
|
128
128
|
"description": "Cerebras – najszybsze wnioskowanie na świecie, darmowy tier",
|
|
129
129
|
},
|
|
130
130
|
"ollama": {
|
|
131
|
-
|
|
131
|
+
# Resolved from OLLAMA_HOST at load time, see ollama_host_url().
|
|
132
|
+
"base_url": None,
|
|
132
133
|
"model": "llama3.2",
|
|
133
134
|
"key_env": None,
|
|
134
135
|
"key_url": "https://ollama.com/download",
|
|
@@ -142,6 +143,22 @@ _OPENROUTER_MODEL_MIGRATIONS = {
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
|
|
146
|
+
DEFAULT_OLLAMA_HOST = "127.0.0.1:11434" # nohardcode: allow NOHARDCODE-006 -- fallback when OLLAMA_HOST is unset or empty
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def ollama_host_url() -> str:
|
|
150
|
+
"""Ollama server URL from ``OLLAMA_HOST`` (the variable Ollama itself reads).
|
|
151
|
+
|
|
152
|
+
Single source for every fixOS component talking to Ollama: the LLM
|
|
153
|
+
provider and model cleanup previously disagreed when OLLAMA_HOST pointed
|
|
154
|
+
to another host.
|
|
155
|
+
"""
|
|
156
|
+
host = os.environ.get("OLLAMA_HOST", "").strip() or DEFAULT_OLLAMA_HOST
|
|
157
|
+
if "://" not in host:
|
|
158
|
+
host = f"http://{host}"
|
|
159
|
+
return host.rstrip("/")
|
|
160
|
+
|
|
161
|
+
|
|
145
162
|
def _normalize_model_id(provider: str, model: str) -> str:
|
|
146
163
|
"""Normalize provider-specific model IDs without changing valid choices."""
|
|
147
164
|
normalized = model.strip()
|
|
@@ -293,7 +310,12 @@ class FixOsConfig:
|
|
|
293
310
|
|
|
294
311
|
# Base URL
|
|
295
312
|
url_env_key = f"{cfg.provider.upper()}_BASE_URL"
|
|
296
|
-
cfg.base_url =
|
|
313
|
+
cfg.base_url = (
|
|
314
|
+
base_url
|
|
315
|
+
or os.environ.get(url_env_key)
|
|
316
|
+
or pdef["base_url"]
|
|
317
|
+
or (f"{ollama_host_url()}/v1" if cfg.provider == "ollama" else None)
|
|
318
|
+
)
|
|
297
319
|
|
|
298
320
|
# Transport Gemini: natywne API Google albo endpoint OpenAI-compatible
|
|
299
321
|
cfg.gemini_transport = os.environ.get("GEMINI_TRANSPORT", "native").lower()
|
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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
|
)
|
|
@@ -69,9 +69,19 @@ def measure_tree(
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def measure_batch(
|
|
72
|
-
paths: list[Path],
|
|
72
|
+
paths: list[Path],
|
|
73
|
+
exclude: tuple[str, ...] = (),
|
|
74
|
+
*,
|
|
75
|
+
native: bool = True,
|
|
76
|
+
timeout: float = 120,
|
|
77
|
+
python_fallback: bool = True,
|
|
73
78
|
) -> dict[str, dict]:
|
|
74
|
-
"""Measure multiple paths, accelerating with Rust batch measurement if available.
|
|
79
|
+
"""Measure multiple paths, accelerating with Rust batch measurement if available.
|
|
80
|
+
|
|
81
|
+
Results are keyed by the resolved path. ``python_fallback=False`` returns
|
|
82
|
+
``{}`` instead of an unbounded Python walk when the native batch is
|
|
83
|
+
missing, fails or exceeds ``timeout``; time-budgeted callers use it.
|
|
84
|
+
"""
|
|
75
85
|
if not paths:
|
|
76
86
|
return {}
|
|
77
87
|
if native:
|
|
@@ -84,7 +94,7 @@ def measure_batch(
|
|
|
84
94
|
[str(executable), "batch-measure", *abs_paths],
|
|
85
95
|
capture_output=True,
|
|
86
96
|
text=True,
|
|
87
|
-
timeout=
|
|
97
|
+
timeout=timeout,
|
|
88
98
|
check=True,
|
|
89
99
|
)
|
|
90
100
|
data = json.loads(result.stdout)
|
|
@@ -103,6 +113,8 @@ def measure_batch(
|
|
|
103
113
|
return res
|
|
104
114
|
except (OSError, ValueError, subprocess.SubprocessError):
|
|
105
115
|
pass
|
|
116
|
+
if not python_fallback:
|
|
117
|
+
return {}
|
|
106
118
|
return {str(p.resolve()): measure_python(p, exclude) for p in paths}
|
|
107
119
|
|
|
108
120
|
|
|
@@ -348,11 +348,15 @@ def _measure_caches(
|
|
|
348
348
|
from fixos.diagnostics.native_scan import measure_batch
|
|
349
349
|
|
|
350
350
|
unique_paths = list({path for _, path in jobs})
|
|
351
|
-
|
|
351
|
+
# The native batch had a 120 s limit and an unbounded Python
|
|
352
|
+
# fallback; on a busy disk that turned "quick" into minutes.
|
|
353
|
+
batch_result = measure_batch(
|
|
354
|
+
unique_paths, timeout=timeout * 2, python_fallback=False
|
|
355
|
+
)
|
|
352
356
|
if isinstance(batch_result, dict) and batch_result:
|
|
353
357
|
unresolved_jobs: list[tuple[CacheRule, Path]] = []
|
|
354
358
|
for rule, path in jobs:
|
|
355
|
-
m = batch_result.get(str(path))
|
|
359
|
+
m = batch_result.get(str(path.resolve()))
|
|
356
360
|
if isinstance(m, dict) and "bytes" in m:
|
|
357
361
|
kib = (m["bytes"] + 1023) // 1024
|
|
358
362
|
measured.setdefault(rule.id, []).append((path, kib))
|
|
@@ -569,6 +573,28 @@ def _history_row(
|
|
|
569
573
|
}
|
|
570
574
|
|
|
571
575
|
|
|
576
|
+
def _fill_from_history(
|
|
577
|
+
items: list[dict[str, Any]], history: list[dict[str, Any]]
|
|
578
|
+
) -> None:
|
|
579
|
+
"""Show the last complete size for caches that missed the time budget.
|
|
580
|
+
|
|
581
|
+
The item stays ``complete: False`` so growth reports never compare it.
|
|
582
|
+
"""
|
|
583
|
+
for item in items:
|
|
584
|
+
if item.get("complete", True):
|
|
585
|
+
continue
|
|
586
|
+
for row in reversed(history):
|
|
587
|
+
if row.get("cache_complete", {}).get(item["id"]) and item["id"] in row.get(
|
|
588
|
+
"cache_sizes", {}
|
|
589
|
+
):
|
|
590
|
+
size = int(row["cache_sizes"][item["id"]])
|
|
591
|
+
if size > item["size_bytes"]:
|
|
592
|
+
item["size_bytes"] = size
|
|
593
|
+
item["size_gb"] = round(size / 1024**3, 3)
|
|
594
|
+
item["last_complete_at"] = row.get("generated_at")
|
|
595
|
+
break
|
|
596
|
+
|
|
597
|
+
|
|
572
598
|
def _parse_timestamp(row: dict[str, Any]) -> datetime | None:
|
|
573
599
|
try:
|
|
574
600
|
parsed = datetime.fromisoformat(str(row["generated_at"]))
|
|
@@ -836,6 +862,9 @@ def collect_quick_snapshot(
|
|
|
836
862
|
history_path = _state_dir(state_dir) / "quick-history.json"
|
|
837
863
|
history = _load_history(history_path)
|
|
838
864
|
current_row = _history_row(generated_at, resources, safe_items, review_items)
|
|
865
|
+
_fill_from_history(safe_items + review_items, history)
|
|
866
|
+
safe_items.sort(key=lambda item: item["size_bytes"], reverse=True)
|
|
867
|
+
review_items.sort(key=lambda item: item["size_bytes"], reverse=True)
|
|
839
868
|
growth = _growth_report(history, current_row, now, max(1, hours))
|
|
840
869
|
alerts = _alerts(resources, growth, thresholds)
|
|
841
870
|
|