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.
Files changed (212) hide show
  1. {fixos-2.2.48 → fixos-2.2.49}/PKG-INFO +1 -1
  2. {fixos-2.2.48 → fixos-2.2.49}/fixos/__init__.py +1 -1
  3. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/cleanup_cmd.py +88 -7
  4. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/shared.py +2 -6
  5. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/shell_cmd.py +72 -51
  6. {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/PKG-INFO +1 -1
  7. {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/SOURCES.txt +1 -0
  8. {fixos-2.2.48 → fixos-2.2.49}/pyproject.toml +1 -1
  9. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cleanup_cmd.py +4 -4
  10. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cleanup_safety.py +53 -14
  11. fixos-2.2.49/tests/unit/test_shell_ux.py +98 -0
  12. {fixos-2.2.48 → fixos-2.2.49}/.env.example +0 -0
  13. {fixos-2.2.48 → fixos-2.2.49}/CHANGELOG.md +0 -0
  14. {fixos-2.2.48 → fixos-2.2.49}/LICENSE +0 -0
  15. {fixos-2.2.48 → fixos-2.2.49}/MANIFEST.in +0 -0
  16. {fixos-2.2.48 → fixos-2.2.49}/README.md +0 -0
  17. {fixos-2.2.48 → fixos-2.2.49}/docker/README.md +0 -0
  18. {fixos-2.2.48 → fixos-2.2.49}/docker/TEST_RESULTS.md +0 -0
  19. {fixos-2.2.48 → fixos-2.2.49}/docker/TEST_RESULTS_V2.md +0 -0
  20. {fixos-2.2.48 → fixos-2.2.49}/docker/alpine/Dockerfile +0 -0
  21. {fixos-2.2.48 → fixos-2.2.49}/docker/arch/Dockerfile +0 -0
  22. {fixos-2.2.48 → fixos-2.2.49}/docker/base/Dockerfile +0 -0
  23. {fixos-2.2.48 → fixos-2.2.49}/docker/broken-audio/Dockerfile +0 -0
  24. {fixos-2.2.48 → fixos-2.2.49}/docker/broken-full/Dockerfile +0 -0
  25. {fixos-2.2.48 → fixos-2.2.49}/docker/broken-network/Dockerfile +0 -0
  26. {fixos-2.2.48 → fixos-2.2.49}/docker/broken-thumbnails/Dockerfile +0 -0
  27. {fixos-2.2.48 → fixos-2.2.49}/docker/debian/Dockerfile +0 -0
  28. {fixos-2.2.48 → fixos-2.2.49}/docker/docker-compose.multi-system.yml +0 -0
  29. {fixos-2.2.48 → fixos-2.2.49}/docker/docker-compose.yml +0 -0
  30. {fixos-2.2.48 → fixos-2.2.49}/docker/fedora/Dockerfile +0 -0
  31. {fixos-2.2.48 → fixos-2.2.49}/docker/test-multi-system.sh +0 -0
  32. {fixos-2.2.48 → fixos-2.2.49}/docker/test-scenarios.sh +0 -0
  33. {fixos-2.2.48 → fixos-2.2.49}/docker/ubuntu/Dockerfile +0 -0
  34. {fixos-2.2.48 → fixos-2.2.49}/docker/validate-scenario.py +0 -0
  35. {fixos-2.2.48 → fixos-2.2.49}/docs/examples/advanced_usage.py +0 -0
  36. {fixos-2.2.48 → fixos-2.2.49}/docs/examples/quickstart.py +0 -0
  37. {fixos-2.2.48 → fixos-2.2.49}/fixos/__main__.py +0 -0
  38. {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/__init__.py +0 -0
  39. {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/autonomous.py +0 -0
  40. {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/autonomous_session.py +0 -0
  41. {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/hitl.py +0 -0
  42. {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/hitl_session.py +0 -0
  43. {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/session_core.py +0 -0
  44. {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/session_handlers.py +0 -0
  45. {fixos-2.2.48 → fixos-2.2.49}/fixos/agent/session_io.py +0 -0
  46. {fixos-2.2.48 → fixos-2.2.49}/fixos/anonymizer.py +0 -0
  47. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/__init__.py +0 -0
  48. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_flatpak.py +0 -0
  49. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_home.py +0 -0
  50. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_snap.py +0 -0
  51. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_system.py +0 -0
  52. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/_cleanup_utils.py +0 -0
  53. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/ask_cmd.py +0 -0
  54. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/config_cmd.py +0 -0
  55. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/features_cmd.py +0 -0
  56. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/fix_cmd.py +0 -0
  57. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/history_cmd.py +0 -0
  58. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/jetbrains_cmd.py +0 -0
  59. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/main.py +0 -0
  60. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/orchestrate_cmd.py +0 -0
  61. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/output_formatter.py +0 -0
  62. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/profile_cmd.py +0 -0
  63. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/projects_cmd.py +0 -0
  64. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/provider_cmd.py +0 -0
  65. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/quick_cmd.py +0 -0
  66. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/quickfix_cmd.py +0 -0
  67. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/report_cmd.py +0 -0
  68. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/rollback_cmd.py +0 -0
  69. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/scan_cmd.py +0 -0
  70. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/token_cmd.py +0 -0
  71. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli/watch_cmd.py +0 -0
  72. {fixos-2.2.48 → fixos-2.2.49}/fixos/cli.py +0 -0
  73. {fixos-2.2.48 → fixos-2.2.49}/fixos/config.py +0 -0
  74. {fixos-2.2.48 → fixos-2.2.49}/fixos/config_interactive.py +0 -0
  75. {fixos-2.2.48 → fixos-2.2.49}/fixos/constants.py +0 -0
  76. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/__init__.py +0 -0
  77. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
  78. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
  79. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
  80. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_storage_container_mixin.py +0 -0
  81. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_storage_system_mixin.py +0 -0
  82. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/_storage_user_mixin.py +0 -0
  83. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/cache_discovery.py +0 -0
  84. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/__init__.py +0 -0
  85. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/_shared.py +0 -0
  86. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/audio.py +0 -0
  87. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/file_analysis.py +0 -0
  88. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/hardware.py +0 -0
  89. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/packages.py +0 -0
  90. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/resources.py +0 -0
  91. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/security.py +0 -0
  92. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/storage_optimization.py +0 -0
  93. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/system_core.py +0 -0
  94. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/checks/thumbnails.py +0 -0
  95. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  96. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/disk_analyzer.py +0 -0
  97. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/docker_network_cleanup.py +0 -0
  98. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
  99. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  100. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/jetbrains_ai.py +0 -0
  101. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/jetbrains_recovery.py +0 -0
  102. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/orphaned_workloads.py +0 -0
  103. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/process_chains.py +0 -0
  104. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/project_scanner.py +0 -0
  105. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/quick_snapshot.py +0 -0
  106. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/service_cleanup.py +0 -0
  107. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/service_details.py +0 -0
  108. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/service_scanner.py +0 -0
  109. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/storage_analyzer.py +0 -0
  110. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/system_checks.py +0 -0
  111. {fixos-2.2.48 → fixos-2.2.49}/fixos/diagnostics/utils.py +0 -0
  112. {fixos-2.2.48 → fixos-2.2.49}/fixos/endpoint_refresh.py +0 -0
  113. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/__init__.py +0 -0
  114. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/auditor.py +0 -0
  115. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/catalog.py +0 -0
  116. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/data/packages.yaml +0 -0
  117. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/data/profiles/developer.yaml +0 -0
  118. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/data/profiles/office.yaml +0 -0
  119. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/data/profiles/sysadmin.yaml +0 -0
  120. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/installer.py +0 -0
  121. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/profiles.py +0 -0
  122. {fixos-2.2.48 → fixos-2.2.49}/fixos/features/renderer.py +0 -0
  123. {fixos-2.2.48 → fixos-2.2.49}/fixos/fixes/__init__.py +0 -0
  124. {fixos-2.2.48 → fixos-2.2.49}/fixos/interactive/__init__.py +0 -0
  125. {fixos-2.2.48 → fixos-2.2.49}/fixos/interactive/cleanup_planner.py +0 -0
  126. {fixos-2.2.48 → fixos-2.2.49}/fixos/llm_shell.py +0 -0
  127. {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/__init__.py +0 -0
  128. {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/executor.py +0 -0
  129. {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/graph.py +0 -0
  130. {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/orchestrator.py +0 -0
  131. {fixos-2.2.48 → fixos-2.2.49}/fixos/orchestrator/rollback.py +0 -0
  132. {fixos-2.2.48 → fixos-2.2.49}/fixos/orphan_pins.py +0 -0
  133. {fixos-2.2.48 → fixos-2.2.49}/fixos/platform_utils.py +0 -0
  134. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/__init__.py +0 -0
  135. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/base.py +0 -0
  136. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/__init__.py +0 -0
  137. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/audio.py +0 -0
  138. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/disk.py +0 -0
  139. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/hardware.py +0 -0
  140. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/resources.py +0 -0
  141. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/security.py +0 -0
  142. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/builtin/thumbnails.py +0 -0
  143. {fixos-2.2.48 → fixos-2.2.49}/fixos/plugins/registry.py +0 -0
  144. {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/__init__.py +0 -0
  145. {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/desktop.yaml +0 -0
  146. {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/developer.yaml +0 -0
  147. {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/minimal.yaml +0 -0
  148. {fixos-2.2.48 → fixos-2.2.49}/fixos/profiles/server.yaml +0 -0
  149. {fixos-2.2.48 → fixos-2.2.49}/fixos/providers/__init__.py +0 -0
  150. {fixos-2.2.48 → fixos-2.2.49}/fixos/providers/llm.py +0 -0
  151. {fixos-2.2.48 → fixos-2.2.49}/fixos/providers/llm_analyzer.py +0 -0
  152. {fixos-2.2.48 → fixos-2.2.49}/fixos/providers/schemas.py +0 -0
  153. {fixos-2.2.48 → fixos-2.2.49}/fixos/system_checks.py +0 -0
  154. {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/__init__.py +0 -0
  155. {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/anonymizer.py +0 -0
  156. {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/terminal.py +0 -0
  157. {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/timeout.py +0 -0
  158. {fixos-2.2.48 → fixos-2.2.49}/fixos/utils/web_search.py +0 -0
  159. {fixos-2.2.48 → fixos-2.2.49}/fixos/watch.py +0 -0
  160. {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/dependency_links.txt +0 -0
  161. {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/entry_points.txt +0 -0
  162. {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/requires.txt +0 -0
  163. {fixos-2.2.48 → fixos-2.2.49}/fixos.egg-info/top_level.txt +0 -0
  164. {fixos-2.2.48 → fixos-2.2.49}/pytest.ini +0 -0
  165. {fixos-2.2.48 → fixos-2.2.49}/requirements-dev.txt +0 -0
  166. {fixos-2.2.48 → fixos-2.2.49}/requirements.txt +0 -0
  167. {fixos-2.2.48 → fixos-2.2.49}/scripts/pyqual-calibrate.py +0 -0
  168. {fixos-2.2.48 → fixos-2.2.49}/setup.cfg +0 -0
  169. {fixos-2.2.48 → fixos-2.2.49}/setup.py +0 -0
  170. {fixos-2.2.48 → fixos-2.2.49}/tests/__init__.py +0 -0
  171. {fixos-2.2.48 → fixos-2.2.49}/tests/conftest.py +0 -0
  172. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/__init__.py +0 -0
  173. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_anonymization_layers.py +0 -0
  174. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_audio_broken.py +0 -0
  175. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_cli.py +0 -0
  176. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_diagnostics_integration.py +0 -0
  177. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_executor.py +0 -0
  178. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_multi_system.py +0 -0
  179. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_network_broken.py +0 -0
  180. {fixos-2.2.48 → fixos-2.2.49}/tests/e2e/test_thumbnails_broken.py +0 -0
  181. {fixos-2.2.48 → fixos-2.2.49}/tests/test_fixos.py +0 -0
  182. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/__init__.py +0 -0
  183. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_anonymizer.py +0 -0
  184. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_ask_and_nl_group.py +0 -0
  185. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cache_discovery.py +0 -0
  186. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cleanup_effectiveness.py +0 -0
  187. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_cleanup_utils.py +0 -0
  188. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_core.py +0 -0
  189. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_dev_project_analyzer.py +0 -0
  190. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_docker_network_cleanup.py +0 -0
  191. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_docker_startup_optimizer.py +0 -0
  192. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_endpoint_refresh.py +0 -0
  193. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_executor.py +0 -0
  194. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_jetbrains_cmd.py +0 -0
  195. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_jetbrains_recovery.py +0 -0
  196. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_llm_auth_stop.py +0 -0
  197. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_llm_provider.py +0 -0
  198. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_orchestrator.py +0 -0
  199. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_orphan_pins.py +0 -0
  200. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_orphaned_workloads.py +0 -0
  201. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_packaging_data.py +0 -0
  202. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_process_chains.py +0 -0
  203. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_project_scanner.py +0 -0
  204. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_projects_cmd.py +0 -0
  205. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_quick_cmd.py +0 -0
  206. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_quick_snapshot.py +0 -0
  207. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_resources_quick.py +0 -0
  208. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_security_quick.py +0 -0
  209. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_service_cleanup.py +0 -0
  210. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_service_scanner.py +0 -0
  211. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_storage_analyzer.py +0 -0
  212. {fixos-2.2.48 → fixos-2.2.49}/tests/unit/test_system_checks_parallel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.48
3
+ Version: 2.2.49
4
4
  Summary: AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues
5
5
  Home-page: https://github.com/wronai/fixos
6
6
  Author: fixos contributors
@@ -1,3 +1,3 @@
1
1
  """fixos – AI-powered Linux/Windows diagnostics and repair."""
2
2
 
3
- __version__ = "2.2.48"
3
+ __version__ = "2.2.49"
@@ -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 kolejno spośród wszystkich możliwych usług:",
1250
+ "Wybierz usługi do wyczyszczenia:",
1226
1251
  fg="cyan",
1227
1252
  )
1228
1253
  )
1229
- selected = []
1254
+ executable = []
1230
1255
  for svc in services:
1231
- label = f" {svc['name']} ({_size_str(svc)}) [{_selection_description(svc)}]"
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
- if click.confirm(label, default=False):
1239
- selected.append(svc)
1240
- return selected
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
- raise click.exceptions.NoSuchCommand(
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
- raise click.exceptions.NoSuchCommand(
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 for all fixOS commands and common options."""
22
- return NestedCompleter.from_nested_dict({
23
- "quick": {"--hours": None, "--json": None, "--deep": None, "--help": None},
24
- "fix": {
25
- "--modules": None,
26
- "--dry-run": None,
27
- "--json": None,
28
- "--yaml": None,
29
- "--help": None,
30
- },
31
- "cleanup": {
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-7], komendę lub zapytanie naturalne.",
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
- "prompt": "#00d7af bold",
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.48
3
+ Version: 2.2.49
4
4
  Summary: AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues
5
5
  Home-page: https://github.com/wronai/fixos
6
6
  Author: fixos contributors
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fixos"
7
- version = "2.2.48"
7
+ version = "2.2.49"
8
8
  description = "AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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 yes; JetBrains no; Npm yes; Docker risk
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
- # but intentionally does not consume an answer.
51
- result = CliRunner().invoke(command, input="2\ny\nn\ny\ny\n")
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 kolejno spośród wszystkich możliwych usług:", 1
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; Docker yes; Npm yes. No protected-data confirmation
91
- # is needed because nothing is executed.
92
- result = CliRunner().invoke(command, input="2\ny\ny\n")
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(ServiceType.LOGS, path)
140
- assert ".local/state" not in ServiceCleaner.get_preview_command(ServiceType.LOGS, path)
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: calls.append(dry_run)
254
- or {"success": True, "space_freed_gb": 0.0, "output": ""},
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(cmd, 1, stdout="2048\t/data\n", stderr="denied")
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