fixos 2.2.48__tar.gz → 2.2.49__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.48 → fixos-2.2.49}/PKG-INFO +1 -1
- {fixos-2.2.48 → fixos-2.2.49}/fixos/__init__.py +1 -1
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/cleanup_cmd.py +88 -7
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/shared.py +2 -6
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/shell_cmd.py +72 -51
- {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/PKG-INFO +1 -1
- {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/SOURCES.txt +1 -0
- {fixos-2.2.48 → fixos-2.2.49}/pyproject.toml +1 -1
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cleanup_cmd.py +4 -4
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cleanup_safety.py +53 -14
- fixos-2.2.49/tests/unit/test_shell_ux.py +98 -0
- {fixos-2.2.48 → fixos-2.2.49}/.env.example +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/CHANGELOG.md +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/LICENSE +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/MANIFEST.in +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/README.md +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/README.md +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/TEST_RESULTS.md +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/TEST_RESULTS_V2.md +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/alpine/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/arch/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/base/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/broken-audio/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/broken-full/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/broken-network/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/broken-thumbnails/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/debian/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/docker-compose.multi-system.yml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/docker-compose.yml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/fedora/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/test-multi-system.sh +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/test-scenarios.sh +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/ubuntu/Dockerfile +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docker/validate-scenario.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docs/examples/advanced_usage.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/docs/examples/quickstart.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/__main__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/autonomous.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/autonomous_session.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/hitl.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/hitl_session.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/session_core.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/session_handlers.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/session_io.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/anonymizer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_flatpak.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_home.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_snap.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_system.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_utils.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/ask_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/config_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/features_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/fix_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/history_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/jetbrains_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/main.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/orchestrate_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/output_formatter.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/profile_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/projects_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/provider_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/quick_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/quickfix_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/report_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/rollback_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/scan_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/token_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/watch_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/cli.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/config.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/config_interactive.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/constants.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_storage_container_mixin.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_storage_system_mixin.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_storage_user_mixin.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/cache_discovery.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/_shared.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/audio.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/file_analysis.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/hardware.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/packages.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/resources.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/security.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/storage_optimization.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/system_core.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/thumbnails.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/dev_project_analyzer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/disk_analyzer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/docker_network_cleanup.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/flatpak_analyzer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/jetbrains_ai.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/jetbrains_recovery.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/orphaned_workloads.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/process_chains.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/project_scanner.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/quick_snapshot.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/service_cleanup.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/service_details.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/service_scanner.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/storage_analyzer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/system_checks.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/utils.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/endpoint_refresh.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/auditor.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/catalog.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/data/packages.yaml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/data/profiles/developer.yaml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/data/profiles/office.yaml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/data/profiles/sysadmin.yaml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/installer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/profiles.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/features/renderer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/fixes/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/interactive/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/interactive/cleanup_planner.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/llm_shell.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/executor.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/graph.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/orchestrator.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/rollback.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/orphan_pins.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/platform_utils.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/base.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/audio.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/disk.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/hardware.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/resources.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/security.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/thumbnails.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/registry.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/desktop.yaml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/developer.yaml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/minimal.yaml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/server.yaml +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/providers/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/providers/llm.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/providers/llm_analyzer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/providers/schemas.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/system_checks.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/anonymizer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/terminal.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/timeout.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/web_search.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos/watch.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/dependency_links.txt +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/entry_points.txt +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/requires.txt +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/top_level.txt +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/pytest.ini +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/requirements-dev.txt +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/requirements.txt +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/scripts/pyqual-calibrate.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/setup.cfg +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/setup.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/conftest.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_anonymization_layers.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_audio_broken.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_cli.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_diagnostics_integration.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_executor.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_multi_system.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_network_broken.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_thumbnails_broken.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/test_fixos.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/__init__.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_anonymizer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_ask_and_nl_group.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cache_discovery.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cleanup_effectiveness.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cleanup_utils.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_core.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_dev_project_analyzer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_docker_network_cleanup.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_docker_startup_optimizer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_endpoint_refresh.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_executor.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_jetbrains_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_jetbrains_recovery.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_llm_auth_stop.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_llm_provider.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_orchestrator.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_orphan_pins.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_orphaned_workloads.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_packaging_data.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_process_chains.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_project_scanner.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_projects_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_quick_cmd.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_quick_snapshot.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_resources_quick.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_security_quick.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_service_cleanup.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_service_scanner.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_storage_analyzer.py +0 -0
- {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_system_checks_parallel.py +0 -0
|
@@ -1218,26 +1218,58 @@ def _selection_description(svc: dict) -> str:
|
|
|
1218
1218
|
return description
|
|
1219
1219
|
|
|
1220
1220
|
|
|
1221
|
+
def _parse_selection(raw: str, count: int) -> set[int]:
|
|
1222
|
+
"""Parse '1,3,7-9', 'all' or 'none' into 0-based indexes."""
|
|
1223
|
+
tokens = raw.strip().lower().replace(" ", "")
|
|
1224
|
+
if tokens in ("", "none", "0"):
|
|
1225
|
+
return set()
|
|
1226
|
+
if tokens == "all":
|
|
1227
|
+
return set(range(count))
|
|
1228
|
+
selected = set()
|
|
1229
|
+
for part in tokens.split(","):
|
|
1230
|
+
if not part:
|
|
1231
|
+
continue
|
|
1232
|
+
bounds = part.split("-", 1)
|
|
1233
|
+
try:
|
|
1234
|
+
if len(bounds) == 2:
|
|
1235
|
+
numbers = range(int(bounds[0]), int(bounds[1]) + 1)
|
|
1236
|
+
else:
|
|
1237
|
+
numbers = (int(bounds[0]),)
|
|
1238
|
+
except ValueError:
|
|
1239
|
+
continue
|
|
1240
|
+
for number in numbers:
|
|
1241
|
+
if 1 <= number <= count:
|
|
1242
|
+
selected.add(number - 1)
|
|
1243
|
+
return selected
|
|
1244
|
+
|
|
1245
|
+
|
|
1221
1246
|
def _select_individual_services(services: list) -> list:
|
|
1222
1247
|
"""Offer every scanned, executable entry in its displayed order."""
|
|
1223
1248
|
click.echo(
|
|
1224
1249
|
click.style(
|
|
1225
|
-
"Wybierz
|
|
1250
|
+
"Wybierz usługi do wyczyszczenia:",
|
|
1226
1251
|
fg="cyan",
|
|
1227
1252
|
)
|
|
1228
1253
|
)
|
|
1229
|
-
|
|
1254
|
+
executable = []
|
|
1230
1255
|
for svc in services:
|
|
1231
|
-
label = f"
|
|
1256
|
+
label = f"{svc['name']} ({_size_str(svc)}) [{_selection_description(svc)}]"
|
|
1232
1257
|
if not svc.get("can_cleanup") or not svc.get("cleanup_command", "").strip():
|
|
1233
|
-
click.echo(f"{label} — brak bezpiecznej operacji zbiorczej")
|
|
1258
|
+
click.echo(f" {label} — brak bezpiecznej operacji zbiorczej")
|
|
1234
1259
|
preview = svc.get("preview_command")
|
|
1235
1260
|
if preview:
|
|
1236
1261
|
click.echo(f" Podgląd: {preview}")
|
|
1237
1262
|
continue
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1263
|
+
executable.append(svc)
|
|
1264
|
+
click.echo(f" [{len(executable)}] {label}")
|
|
1265
|
+
if not executable:
|
|
1266
|
+
return []
|
|
1267
|
+
raw = click.prompt(
|
|
1268
|
+
"Numery do wyczyszczenia (np. 1,3,7-9; all = wszystkie; none = żadne)",
|
|
1269
|
+
default="none",
|
|
1270
|
+
show_default=True,
|
|
1271
|
+
)
|
|
1272
|
+
return [executable[i] for i in sorted(_parse_selection(raw, len(executable)))]
|
|
1241
1273
|
|
|
1242
1274
|
|
|
1243
1275
|
def _execute_individual_cleanup(
|
|
@@ -1326,6 +1358,41 @@ def _run_interactive_cleanup(
|
|
|
1326
1358
|
_display_dangerous_services(plan["dangerous"])
|
|
1327
1359
|
|
|
1328
1360
|
|
|
1361
|
+
def _execute_yes_cleanup(plan: dict, scanner, dry_run: bool = False) -> None:
|
|
1362
|
+
"""Execute the whole safe plan without prompting (--yes)."""
|
|
1363
|
+
safe = plan["safe_to_cleanup"]
|
|
1364
|
+
if not safe:
|
|
1365
|
+
click.echo(
|
|
1366
|
+
click.style(
|
|
1367
|
+
"Brak bezpiecznych pozycji do automatycznego czyszczenia.",
|
|
1368
|
+
fg="yellow",
|
|
1369
|
+
)
|
|
1370
|
+
)
|
|
1371
|
+
else:
|
|
1372
|
+
if dry_run:
|
|
1373
|
+
click.echo(
|
|
1374
|
+
click.style(
|
|
1375
|
+
"Tryb symulacji (--dry-run): wybrane pozycje nie zostaną usunięte.",
|
|
1376
|
+
fg="cyan",
|
|
1377
|
+
bold=True,
|
|
1378
|
+
)
|
|
1379
|
+
)
|
|
1380
|
+
_execute_safe_cleanup(safe, scanner, dry_run=dry_run)
|
|
1381
|
+
if not dry_run:
|
|
1382
|
+
click.echo()
|
|
1383
|
+
click.echo(
|
|
1384
|
+
click.style(
|
|
1385
|
+
"Uwaga: listy poniżej pochodzą ze skanu sprzed czyszczenia — "
|
|
1386
|
+
"uruchom ponownie `fixos cleanup --list`, by zobaczyć aktualny stan.",
|
|
1387
|
+
fg="cyan",
|
|
1388
|
+
)
|
|
1389
|
+
)
|
|
1390
|
+
if plan["requires_review"]:
|
|
1391
|
+
_display_unsafe_services(plan["requires_review"])
|
|
1392
|
+
if plan.get("dangerous"):
|
|
1393
|
+
_display_dangerous_services(plan["dangerous"])
|
|
1394
|
+
|
|
1395
|
+
|
|
1329
1396
|
# ── Main CLI command ──────────────────────────────────────────────────────
|
|
1330
1397
|
|
|
1331
1398
|
|
|
@@ -1479,6 +1546,14 @@ def _run_interactive_cleanup(
|
|
|
1479
1546
|
default=False,
|
|
1480
1547
|
help="Tylko wyświetl listę bez interakcji",
|
|
1481
1548
|
)
|
|
1549
|
+
@click.option(
|
|
1550
|
+
"--yes",
|
|
1551
|
+
"-y",
|
|
1552
|
+
"yes",
|
|
1553
|
+
is_flag=True,
|
|
1554
|
+
default=False,
|
|
1555
|
+
help="Bez pytań — wykonaj wszystkie bezpieczne pozycje (SSH/cron/CI)",
|
|
1556
|
+
)
|
|
1482
1557
|
@click.option(
|
|
1483
1558
|
"--full",
|
|
1484
1559
|
"-f",
|
|
@@ -1505,6 +1580,7 @@ def cleanup_services(
|
|
|
1505
1580
|
days,
|
|
1506
1581
|
dry_run,
|
|
1507
1582
|
list_only,
|
|
1583
|
+
yes,
|
|
1508
1584
|
full_analysis,
|
|
1509
1585
|
) -> None:
|
|
1510
1586
|
"""
|
|
@@ -1522,6 +1598,7 @@ def cleanup_services(
|
|
|
1522
1598
|
fixos cleanup -t 1000 # próg 1000MB (1GB)
|
|
1523
1599
|
fixos cleanup -s docker,ollama # tylko Docker i Ollama
|
|
1524
1600
|
fixos cleanup --list # tylko lista, bez czyszczenia
|
|
1601
|
+
fixos cleanup --yes # bez pytań: wszystkie bezpieczne
|
|
1525
1602
|
fixos cleanup -c docker --dry-run # tylko cache buildów >7 dni
|
|
1526
1603
|
fixos cleanup --docker-all --dry-run
|
|
1527
1604
|
# wszystkie unused images/cache + osierocone sieci
|
|
@@ -1737,4 +1814,8 @@ def cleanup_services(
|
|
|
1737
1814
|
for svc in plan["services"]:
|
|
1738
1815
|
_display_service_item(svc)
|
|
1739
1816
|
|
|
1817
|
+
if yes:
|
|
1818
|
+
_execute_yes_cleanup(plan, scanner, dry_run=dry_run)
|
|
1819
|
+
return
|
|
1820
|
+
|
|
1740
1821
|
_run_interactive_cleanup(plan, list_only, scanner, dry_run=dry_run)
|
|
@@ -178,9 +178,7 @@ class NaturalLanguageGroup(click.Group):
|
|
|
178
178
|
except (click.Abort, EOFError):
|
|
179
179
|
pass
|
|
180
180
|
# In non-interactive mode or if rejected, fail cleanly with typo suggestion
|
|
181
|
-
|
|
182
|
-
cmd_name, possibilities=all_commands, ctx=ctx
|
|
183
|
-
)
|
|
181
|
+
ctx.fail(f"No such command '{cmd_name}'. Did you mean '{best_match}'?")
|
|
184
182
|
|
|
185
183
|
# 2. Check if this is a genuine natural language command:
|
|
186
184
|
# - multiple arguments: e.g. `fixos wylacz kontenery`
|
|
@@ -196,7 +194,5 @@ class NaturalLanguageGroup(click.Group):
|
|
|
196
194
|
return super().resolve_command(ctx, ["ask"] + args)
|
|
197
195
|
|
|
198
196
|
# 3. Otherwise, unrecognized single token that is not NL nor a close match
|
|
199
|
-
|
|
200
|
-
cmd_name, possibilities=all_commands, ctx=ctx
|
|
201
|
-
)
|
|
197
|
+
ctx.fail(f"No such command '{cmd_name}'.")
|
|
202
198
|
|
|
@@ -7,7 +7,6 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import shlex
|
|
10
|
-
import sys
|
|
11
10
|
from typing import Any
|
|
12
11
|
|
|
13
12
|
import click
|
|
@@ -17,53 +16,32 @@ from prompt_toolkit.history import FileHistory
|
|
|
17
16
|
from prompt_toolkit.styles import Style
|
|
18
17
|
|
|
19
18
|
|
|
19
|
+
def _command_completion_words(command: click.Command) -> dict[str, Any] | None:
|
|
20
|
+
"""Collect subcommand and option words for one click command."""
|
|
21
|
+
words: dict[str, Any] = {}
|
|
22
|
+
if isinstance(command, click.Group):
|
|
23
|
+
for name in sorted(command.commands):
|
|
24
|
+
words[name] = _command_completion_words(command.commands[name])
|
|
25
|
+
for param in getattr(command, "params", []):
|
|
26
|
+
for opt in list(getattr(param, "opts", [])) + list(
|
|
27
|
+
getattr(param, "secondary_opts", [])
|
|
28
|
+
):
|
|
29
|
+
words.setdefault(opt, None)
|
|
30
|
+
return words or None
|
|
31
|
+
|
|
32
|
+
|
|
20
33
|
def get_command_completer() -> NestedCompleter:
|
|
21
|
-
"""Build a nested completer
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"--docker-all": None,
|
|
33
|
-
"--docker-old": None,
|
|
34
|
-
"--docker-networks": None,
|
|
35
|
-
"--docker-stale-services": None,
|
|
36
|
-
"--orphaned-projects": None,
|
|
37
|
-
"--ollama-old": None,
|
|
38
|
-
"--dry-run": None,
|
|
39
|
-
"--threshold": None,
|
|
40
|
-
"-t": None,
|
|
41
|
-
"--list": None,
|
|
42
|
-
"--full": None,
|
|
43
|
-
"--help": None,
|
|
44
|
-
},
|
|
45
|
-
"scan": {"--modules": None, "--show-raw": None, "--json": None, "--help": None},
|
|
46
|
-
"quickfix": {"--help": None},
|
|
47
|
-
"jetbrains": {"doctor": None, "--help": None},
|
|
48
|
-
"projects": {"--help": None},
|
|
49
|
-
"orchestrate": {"--dry-run": None, "--help": None},
|
|
50
|
-
"watch": {"--help": None},
|
|
51
|
-
"report": {"--help": None},
|
|
52
|
-
"history": {"--help": None},
|
|
53
|
-
"rollback": {"--help": None},
|
|
54
|
-
"profile": {"--help": None},
|
|
55
|
-
"llm": {"--free": None, "--help": None},
|
|
56
|
-
"providers": {"--help": None},
|
|
57
|
-
"token": {"set": None, "show": None, "clear": None, "--help": None},
|
|
58
|
-
"config": {"show": None, "set": None, "init": None, "--help": None},
|
|
59
|
-
"test-llm": {"--help": None},
|
|
60
|
-
"ask": {"--dry-run": None, "--help": None},
|
|
61
|
-
"help": None,
|
|
62
|
-
"clear": None,
|
|
63
|
-
"menu": None,
|
|
64
|
-
"exit": None,
|
|
65
|
-
"quit": None,
|
|
66
|
-
})
|
|
34
|
+
"""Build a nested completer from the live click command tree."""
|
|
35
|
+
from fixos.cli.main import cli
|
|
36
|
+
|
|
37
|
+
words: dict[str, Any] = {
|
|
38
|
+
name: _command_completion_words(command)
|
|
39
|
+
for name, command in sorted(cli.commands.items())
|
|
40
|
+
}
|
|
41
|
+
words.update(
|
|
42
|
+
{"commands": None, "clear": None, "menu": None, "exit": None, "quit": None}
|
|
43
|
+
)
|
|
44
|
+
return NestedCompleter.from_nested_dict(words)
|
|
67
45
|
|
|
68
46
|
|
|
69
47
|
def print_interactive_menu() -> None:
|
|
@@ -81,6 +59,7 @@ def print_interactive_menu() -> None:
|
|
|
81
59
|
("5", "jetbrains doctor", "Diagnoza PyCharm, WebStorm, IDEA"),
|
|
82
60
|
("6", "ask", "Zadaj pytanie / polecenie w języku naturalnym"),
|
|
83
61
|
("7", "config show", "Podgląd konfiguracji i aktywny model"),
|
|
62
|
+
("8", "commands", "Pełna lista wszystkich komend i opcji"),
|
|
84
63
|
]
|
|
85
64
|
for num, cmd, desc in menu_items:
|
|
86
65
|
num_styled = click.style(f"[{num}]", fg="yellow", bold=True)
|
|
@@ -92,7 +71,7 @@ def print_interactive_menu() -> None:
|
|
|
92
71
|
click.echo()
|
|
93
72
|
click.echo(
|
|
94
73
|
click.style(
|
|
95
|
-
" 💡 Wskazówka: wpisz numer [1-
|
|
74
|
+
" 💡 Wskazówka: wpisz numer [1-8], komendę lub zapytanie naturalne.",
|
|
96
75
|
fg="bright_black",
|
|
97
76
|
)
|
|
98
77
|
)
|
|
@@ -106,6 +85,38 @@ def print_interactive_menu() -> None:
|
|
|
106
85
|
click.echo()
|
|
107
86
|
|
|
108
87
|
|
|
88
|
+
def print_command_catalog() -> None:
|
|
89
|
+
"""Print every registered command with its one-line help."""
|
|
90
|
+
from fixos.cli.main import cli
|
|
91
|
+
|
|
92
|
+
click.echo(click.style(" 📚 Wszystkie komendy fixos:", fg="cyan", bold=True))
|
|
93
|
+
click.echo()
|
|
94
|
+
for name in sorted(cli.commands):
|
|
95
|
+
command = cli.commands[name]
|
|
96
|
+
try:
|
|
97
|
+
short = command.get_short_help_str().splitlines()[0]
|
|
98
|
+
except Exception:
|
|
99
|
+
short = ""
|
|
100
|
+
name_styled = click.style(f"{name:<14}", fg="green")
|
|
101
|
+
click.echo(f" {name_styled} {short}")
|
|
102
|
+
if isinstance(command, click.Group):
|
|
103
|
+
for sub_name in sorted(command.commands):
|
|
104
|
+
sub = command.commands[sub_name]
|
|
105
|
+
try:
|
|
106
|
+
sub_short = sub.get_short_help_str().splitlines()[0]
|
|
107
|
+
except Exception:
|
|
108
|
+
sub_short = ""
|
|
109
|
+
click.echo(f" {sub_name:<12} {sub_short}")
|
|
110
|
+
click.echo()
|
|
111
|
+
click.echo(
|
|
112
|
+
click.style(
|
|
113
|
+
" 💡 Każdą komendę możesz uruchomić z flagą --help, np. `cleanup --help`.",
|
|
114
|
+
fg="bright_black",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
click.echo()
|
|
118
|
+
|
|
119
|
+
|
|
109
120
|
MENU_SHORTCUTS = {
|
|
110
121
|
"1": "quick",
|
|
111
122
|
"2": "fix",
|
|
@@ -114,6 +125,7 @@ MENU_SHORTCUTS = {
|
|
|
114
125
|
"5": "jetbrains doctor",
|
|
115
126
|
"6": "ask",
|
|
116
127
|
"7": "config show",
|
|
128
|
+
"8": "commands",
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
|
|
@@ -134,9 +146,11 @@ def run_interactive_shell(ctx: click.Context | None = None) -> None:
|
|
|
134
146
|
history = FileHistory(history_file)
|
|
135
147
|
completer = get_command_completer()
|
|
136
148
|
|
|
137
|
-
style = Style.from_dict(
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
style = Style.from_dict(
|
|
150
|
+
{
|
|
151
|
+
"prompt": "#00d7af bold",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
140
154
|
|
|
141
155
|
session = PromptSession(
|
|
142
156
|
history=history,
|
|
@@ -168,6 +182,10 @@ def run_interactive_shell(ctx: click.Context | None = None) -> None:
|
|
|
168
182
|
click.clear()
|
|
169
183
|
continue
|
|
170
184
|
|
|
185
|
+
if user_input.lower() == "commands":
|
|
186
|
+
print_command_catalog()
|
|
187
|
+
continue
|
|
188
|
+
|
|
171
189
|
# Check menu shortcuts
|
|
172
190
|
if user_input in MENU_SHORTCUTS:
|
|
173
191
|
if user_input == "6":
|
|
@@ -179,6 +197,9 @@ def run_interactive_shell(ctx: click.Context | None = None) -> None:
|
|
|
179
197
|
continue
|
|
180
198
|
except (KeyboardInterrupt, EOFError):
|
|
181
199
|
continue
|
|
200
|
+
elif MENU_SHORTCUTS[user_input] == "commands":
|
|
201
|
+
print_command_catalog()
|
|
202
|
+
continue
|
|
182
203
|
else:
|
|
183
204
|
user_input = MENU_SHORTCUTS[user_input]
|
|
184
205
|
|
|
@@ -205,5 +205,6 @@ tests/unit/test_resources_quick.py
|
|
|
205
205
|
tests/unit/test_security_quick.py
|
|
206
206
|
tests/unit/test_service_cleanup.py
|
|
207
207
|
tests/unit/test_service_scanner.py
|
|
208
|
+
tests/unit/test_shell_ux.py
|
|
208
209
|
tests/unit/test_storage_analyzer.py
|
|
209
210
|
tests/unit/test_system_checks_parallel.py
|
|
@@ -45,14 +45,14 @@ def test_individual_mode_walks_all_risk_tiers_in_display_order():
|
|
|
45
45
|
def command():
|
|
46
46
|
cleanup_cmd._run_interactive_cleanup(plan, False, Scanner())
|
|
47
47
|
|
|
48
|
-
# 2 = individual; Docker
|
|
48
|
+
# 2 = individual; pick 1 (Docker) and 3 (Npm) in one input; Docker risk
|
|
49
49
|
# confirmation yes. Ollama has no executable bulk action, so it is shown
|
|
50
|
-
#
|
|
51
|
-
result = CliRunner().invoke(command, input="2\
|
|
50
|
+
# without a number and intentionally consumes no input.
|
|
51
|
+
result = CliRunner().invoke(command, input="2\n1,3\ny\n")
|
|
52
52
|
|
|
53
53
|
assert result.exit_code == 0
|
|
54
54
|
individual_output = result.output.split(
|
|
55
|
-
"Wybierz
|
|
55
|
+
"Wybierz usługi do wyczyszczenia:", 1
|
|
56
56
|
)[1]
|
|
57
57
|
assert individual_output.index("Docker") < individual_output.index("Jetbrains")
|
|
58
58
|
assert individual_output.index("Jetbrains") < individual_output.index("Npm")
|
|
@@ -87,9 +87,9 @@ class TestInteractiveDryRun:
|
|
|
87
87
|
def command():
|
|
88
88
|
cleanup_cmd._run_interactive_cleanup(plan, False, scanner, dry_run=True)
|
|
89
89
|
|
|
90
|
-
# 2 = individual;
|
|
91
|
-
# is needed because nothing is executed.
|
|
92
|
-
result = CliRunner().invoke(command, input="2\
|
|
90
|
+
# 2 = individual; 'all' picks both entries in one input. No
|
|
91
|
+
# protected-data confirmation is needed because nothing is executed.
|
|
92
|
+
result = CliRunner().invoke(command, input="2\nall\n")
|
|
93
93
|
|
|
94
94
|
assert result.exit_code == 0, result.output
|
|
95
95
|
assert scanner.calls == [("docker", True), ("npm", True)]
|
|
@@ -132,12 +132,51 @@ class TestInteractiveDryRun:
|
|
|
132
132
|
assert "Symulacja" not in result.output
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
class TestYesFlag:
|
|
136
|
+
"""`--yes` runs the safe plan with zero interactive input."""
|
|
137
|
+
|
|
138
|
+
def _command_with_plan(self, plan, scanner, monkeypatch, argv):
|
|
139
|
+
scanner.get_cleanup_plan = lambda selected_services=None: plan
|
|
140
|
+
monkeypatch.setattr(cleanup_cmd, "ServiceDataScanner", lambda **_: scanner)
|
|
141
|
+
return CliRunner().invoke(cleanup_cmd.cleanup_services, argv)
|
|
142
|
+
|
|
143
|
+
def test_yes_runs_all_safe_without_input(self, monkeypatch):
|
|
144
|
+
scanner = RecordingScanner()
|
|
145
|
+
plan = _plan(_service("Npm", "safe"), _service("Pip", "safe"))
|
|
146
|
+
result = self._command_with_plan(plan, scanner, monkeypatch, ["--yes"])
|
|
147
|
+
assert result.exit_code == 0, result.output
|
|
148
|
+
assert scanner.calls == [("npm", False), ("pip", False)]
|
|
149
|
+
|
|
150
|
+
def test_yes_dry_run_simulates_everything(self, monkeypatch):
|
|
151
|
+
scanner = RecordingScanner()
|
|
152
|
+
plan = _plan(_service("Npm", "safe"), _service("Pip", "safe"))
|
|
153
|
+
result = self._command_with_plan(
|
|
154
|
+
plan, scanner, monkeypatch, ["--yes", "--dry-run"]
|
|
155
|
+
)
|
|
156
|
+
assert result.exit_code == 0, result.output
|
|
157
|
+
assert scanner.calls == [("npm", True), ("pip", True)]
|
|
158
|
+
assert "Tryb symulacji (--dry-run)" in result.output
|
|
159
|
+
assert "Zwolniono" not in result.output
|
|
160
|
+
|
|
161
|
+
def test_yes_with_no_safe_services_cleans_nothing(self, monkeypatch):
|
|
162
|
+
scanner = RecordingScanner()
|
|
163
|
+
plan = _plan(_service("Docker", "dangerous"))
|
|
164
|
+
result = self._command_with_plan(plan, scanner, monkeypatch, ["--yes"])
|
|
165
|
+
assert result.exit_code == 0, result.output
|
|
166
|
+
assert scanner.calls == []
|
|
167
|
+
assert "Brak bezpiecznych pozycji" in result.output
|
|
168
|
+
|
|
169
|
+
|
|
135
170
|
class TestCleanupCommandSafety:
|
|
136
171
|
def test_logs_service_leaves_xdg_state_alone(self):
|
|
137
172
|
path = "~/.cache/log"
|
|
138
173
|
assert ServiceDataScanner.SERVICE_PATHS[ServiceType.LOGS] == [path]
|
|
139
|
-
assert ".local/state" not in ServiceCleaner.get_cleanup_command(
|
|
140
|
-
|
|
174
|
+
assert ".local/state" not in ServiceCleaner.get_cleanup_command(
|
|
175
|
+
ServiceType.LOGS, path
|
|
176
|
+
)
|
|
177
|
+
assert ".local/state" not in ServiceCleaner.get_preview_command(
|
|
178
|
+
ServiceType.LOGS, path
|
|
179
|
+
)
|
|
141
180
|
|
|
142
181
|
def test_multi_word_cache_paths_stay_single_rm_arguments(self):
|
|
143
182
|
for service_type in (ServiceType.DISCORD, ServiceType.SLACK):
|
|
@@ -183,9 +222,7 @@ class TestDockerDefaultProposal:
|
|
|
183
222
|
)
|
|
184
223
|
return cleaner
|
|
185
224
|
|
|
186
|
-
def test_default_plan_proposes_stopped_containers_and_images(
|
|
187
|
-
self, monkeypatch
|
|
188
|
-
):
|
|
225
|
+
def test_default_plan_proposes_stopped_containers_and_images(self, monkeypatch):
|
|
189
226
|
usage = {
|
|
190
227
|
"Containers": {"size_gb": 3.0, "reclaimable_gb": 2.5},
|
|
191
228
|
"Images": {"size_gb": 40.0, "reclaimable_gb": 10.0},
|
|
@@ -217,9 +254,7 @@ class TestDockerDefaultProposal:
|
|
|
217
254
|
}
|
|
218
255
|
cleaner = self._cleaner(self._scanner(usage), monkeypatch)
|
|
219
256
|
|
|
220
|
-
kinds = {
|
|
221
|
-
action["cleanup_kind"] for action in cleaner.build_safe_age_actions()
|
|
222
|
-
}
|
|
257
|
+
kinds = {action["cleanup_kind"] for action in cleaner.build_safe_age_actions()}
|
|
223
258
|
assert "docker-containers" not in kinds
|
|
224
259
|
assert "docker-unused" not in kinds
|
|
225
260
|
|
|
@@ -250,8 +285,10 @@ class TestDockerDefaultProposal:
|
|
|
250
285
|
monkeypatch.setattr(
|
|
251
286
|
ServiceCleaner,
|
|
252
287
|
"cleanup_docker_containers",
|
|
253
|
-
lambda self, dry_run=False:
|
|
254
|
-
|
|
288
|
+
lambda self, dry_run=False: (
|
|
289
|
+
calls.append(dry_run)
|
|
290
|
+
or {"success": True, "space_freed_gb": 0.0, "output": ""}
|
|
291
|
+
),
|
|
255
292
|
)
|
|
256
293
|
svc = _service("Docker (zatrzymane kontenery)", "safe")
|
|
257
294
|
svc["cleanup_kind"] = "docker-containers"
|
|
@@ -265,7 +302,9 @@ class TestDockerDefaultProposal:
|
|
|
265
302
|
class TestScanMeasurement:
|
|
266
303
|
def test_du_total_is_kept_when_a_subdirectory_is_unreadable(self, monkeypatch):
|
|
267
304
|
def fake_run(cmd, **kwargs):
|
|
268
|
-
return subprocess.CompletedProcess(
|
|
305
|
+
return subprocess.CompletedProcess(
|
|
306
|
+
cmd, 1, stdout="2048\t/data\n", stderr="denied"
|
|
307
|
+
)
|
|
269
308
|
|
|
270
309
|
def forbidden_walk(*args, **kwargs):
|
|
271
310
|
raise AssertionError("du output must not be discarded")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Interactive UX contracts: shell completion coverage and cleanup selection."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from fixos.cli.cleanup_cmd import _parse_selection
|
|
7
|
+
from fixos.cli.main import cli
|
|
8
|
+
from fixos.cli.shell_cmd import (
|
|
9
|
+
MENU_SHORTCUTS,
|
|
10
|
+
get_command_completer,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _words(node):
|
|
15
|
+
"""Unwrap a NestedCompleter node into its option names."""
|
|
16
|
+
return set(getattr(node, "options", {}) or {})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestCompleterCoverage:
|
|
20
|
+
"""The shell completer must expose every registered command and option."""
|
|
21
|
+
|
|
22
|
+
def test_all_commands_complete(self):
|
|
23
|
+
options = _words(get_command_completer())
|
|
24
|
+
for name in cli.commands:
|
|
25
|
+
assert name in options, f"command '{name}' missing from completer"
|
|
26
|
+
|
|
27
|
+
def test_all_options_complete(self):
|
|
28
|
+
options = get_command_completer().options
|
|
29
|
+
for name, command in cli.commands.items():
|
|
30
|
+
words = _words(options.get(name))
|
|
31
|
+
for param in getattr(command, "params", []):
|
|
32
|
+
for opt in param.opts + param.secondary_opts:
|
|
33
|
+
assert opt in words, f"{name} {opt} missing from completer"
|
|
34
|
+
|
|
35
|
+
def test_group_subcommands_complete(self):
|
|
36
|
+
options = get_command_completer().options
|
|
37
|
+
for name, command in cli.commands.items():
|
|
38
|
+
if not isinstance(command, click.Group):
|
|
39
|
+
continue
|
|
40
|
+
words = _words(options.get(name))
|
|
41
|
+
for sub_name in command.commands:
|
|
42
|
+
assert sub_name in words, f"{name} {sub_name} missing"
|
|
43
|
+
|
|
44
|
+
def test_shell_meta_words_present(self):
|
|
45
|
+
options = _words(get_command_completer())
|
|
46
|
+
for word in ("commands", "clear", "menu", "exit", "quit"):
|
|
47
|
+
assert word in options
|
|
48
|
+
|
|
49
|
+
def test_completer_survives_new_options(self):
|
|
50
|
+
"""Options added after this test (e.g. cleanup -c) complete automatically."""
|
|
51
|
+
words = _words(get_command_completer().options["cleanup"])
|
|
52
|
+
assert "-c" in words or "--category" in words
|
|
53
|
+
assert "--docker-all" in words
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestMenuShortcuts:
|
|
57
|
+
"""Menu shortcuts must resolve to real commands or handled meta words."""
|
|
58
|
+
|
|
59
|
+
def test_every_shortcut_resolves(self):
|
|
60
|
+
for shortcut, target in MENU_SHORTCUTS.items():
|
|
61
|
+
if target == "commands":
|
|
62
|
+
continue
|
|
63
|
+
head = target.split()[0]
|
|
64
|
+
assert head in cli.commands, f"shortcut [{shortcut}] -> '{target}'"
|
|
65
|
+
|
|
66
|
+
def test_catalog_covers_every_command(self, capsys):
|
|
67
|
+
from fixos.cli.shell_cmd import print_command_catalog
|
|
68
|
+
|
|
69
|
+
print_command_catalog()
|
|
70
|
+
out = capsys.readouterr().out
|
|
71
|
+
for name in cli.commands:
|
|
72
|
+
assert name in out, f"'{name}' absent from printed catalog"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestParseSelection:
|
|
76
|
+
@pytest.mark.parametrize(
|
|
77
|
+
"raw,expected",
|
|
78
|
+
[
|
|
79
|
+
("", set()),
|
|
80
|
+
("none", set()),
|
|
81
|
+
("0", set()),
|
|
82
|
+
("all", {0, 1, 2, 3, 4}),
|
|
83
|
+
("1", {0}),
|
|
84
|
+
("1,3,5", {0, 2, 4}),
|
|
85
|
+
("2-4", {1, 2, 3}),
|
|
86
|
+
("1,3-4", {0, 2, 3}),
|
|
87
|
+
(" 1 , 3 ", {0, 2}),
|
|
88
|
+
("9", set()), # out of range ignored
|
|
89
|
+
("abc", set()), # garbage ignored
|
|
90
|
+
("1,,2", {0, 1}),
|
|
91
|
+
("4-2", set()), # reversed range yields nothing
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
def test_parse(self, raw, expected):
|
|
95
|
+
assert _parse_selection(raw, 5) == expected
|
|
96
|
+
|
|
97
|
+
def test_range_clamps_to_count(self):
|
|
98
|
+
assert _parse_selection("3-99", 5) == {2, 3, 4}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|