fixos 2.2.39__tar.gz → 2.2.40__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 (178) hide show
  1. {fixos-2.2.39 → fixos-2.2.40}/CHANGELOG.md +33 -0
  2. {fixos-2.2.39 → fixos-2.2.40}/PKG-INFO +5 -5
  3. {fixos-2.2.39 → fixos-2.2.40}/README.md +4 -4
  4. {fixos-2.2.39 → fixos-2.2.40}/fixos/__init__.py +1 -1
  5. fixos-2.2.40/fixos/__main__.py +7 -0
  6. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/cleanup_cmd.py +117 -19
  7. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/quick_snapshot.py +5 -1
  8. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/service_cleanup.py +64 -22
  9. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/service_scanner.py +19 -7
  10. {fixos-2.2.39 → fixos-2.2.40}/fixos.egg-info/PKG-INFO +5 -5
  11. {fixos-2.2.39 → fixos-2.2.40}/fixos.egg-info/SOURCES.txt +2 -0
  12. {fixos-2.2.39 → fixos-2.2.40}/pyproject.toml +1 -1
  13. {fixos-2.2.39 → fixos-2.2.40}/setup.py +1 -1
  14. fixos-2.2.40/tests/unit/test_cleanup_cmd.py +87 -0
  15. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_service_cleanup.py +84 -6
  16. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_service_scanner.py +20 -0
  17. {fixos-2.2.39 → fixos-2.2.40}/.env.example +0 -0
  18. {fixos-2.2.39 → fixos-2.2.40}/LICENSE +0 -0
  19. {fixos-2.2.39 → fixos-2.2.40}/MANIFEST.in +0 -0
  20. {fixos-2.2.39 → fixos-2.2.40}/docker/README.md +0 -0
  21. {fixos-2.2.39 → fixos-2.2.40}/docker/TEST_RESULTS.md +0 -0
  22. {fixos-2.2.39 → fixos-2.2.40}/docker/TEST_RESULTS_V2.md +0 -0
  23. {fixos-2.2.39 → fixos-2.2.40}/docker/alpine/Dockerfile +0 -0
  24. {fixos-2.2.39 → fixos-2.2.40}/docker/arch/Dockerfile +0 -0
  25. {fixos-2.2.39 → fixos-2.2.40}/docker/base/Dockerfile +0 -0
  26. {fixos-2.2.39 → fixos-2.2.40}/docker/broken-audio/Dockerfile +0 -0
  27. {fixos-2.2.39 → fixos-2.2.40}/docker/broken-full/Dockerfile +0 -0
  28. {fixos-2.2.39 → fixos-2.2.40}/docker/broken-network/Dockerfile +0 -0
  29. {fixos-2.2.39 → fixos-2.2.40}/docker/broken-thumbnails/Dockerfile +0 -0
  30. {fixos-2.2.39 → fixos-2.2.40}/docker/debian/Dockerfile +0 -0
  31. {fixos-2.2.39 → fixos-2.2.40}/docker/docker-compose.multi-system.yml +0 -0
  32. {fixos-2.2.39 → fixos-2.2.40}/docker/docker-compose.yml +0 -0
  33. {fixos-2.2.39 → fixos-2.2.40}/docker/fedora/Dockerfile +0 -0
  34. {fixos-2.2.39 → fixos-2.2.40}/docker/test-multi-system.sh +0 -0
  35. {fixos-2.2.39 → fixos-2.2.40}/docker/test-scenarios.sh +0 -0
  36. {fixos-2.2.39 → fixos-2.2.40}/docker/ubuntu/Dockerfile +0 -0
  37. {fixos-2.2.39 → fixos-2.2.40}/docker/validate-scenario.py +0 -0
  38. {fixos-2.2.39 → fixos-2.2.40}/docs/examples/advanced_usage.py +0 -0
  39. {fixos-2.2.39 → fixos-2.2.40}/docs/examples/quickstart.py +0 -0
  40. {fixos-2.2.39 → fixos-2.2.40}/fixos/agent/__init__.py +0 -0
  41. {fixos-2.2.39 → fixos-2.2.40}/fixos/agent/autonomous.py +0 -0
  42. {fixos-2.2.39 → fixos-2.2.40}/fixos/agent/autonomous_session.py +0 -0
  43. {fixos-2.2.39 → fixos-2.2.40}/fixos/agent/hitl.py +0 -0
  44. {fixos-2.2.39 → fixos-2.2.40}/fixos/agent/hitl_session.py +0 -0
  45. {fixos-2.2.39 → fixos-2.2.40}/fixos/agent/session_core.py +0 -0
  46. {fixos-2.2.39 → fixos-2.2.40}/fixos/agent/session_handlers.py +0 -0
  47. {fixos-2.2.39 → fixos-2.2.40}/fixos/agent/session_io.py +0 -0
  48. {fixos-2.2.39 → fixos-2.2.40}/fixos/anonymizer.py +0 -0
  49. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/__init__.py +0 -0
  50. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/_cleanup_flatpak.py +0 -0
  51. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/_cleanup_home.py +0 -0
  52. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/_cleanup_snap.py +0 -0
  53. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/_cleanup_system.py +0 -0
  54. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/_cleanup_utils.py +0 -0
  55. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/ask_cmd.py +0 -0
  56. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/config_cmd.py +0 -0
  57. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/features_cmd.py +0 -0
  58. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/fix_cmd.py +0 -0
  59. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/history_cmd.py +0 -0
  60. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/main.py +0 -0
  61. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/orchestrate_cmd.py +0 -0
  62. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/output_formatter.py +0 -0
  63. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/profile_cmd.py +0 -0
  64. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/projects_cmd.py +0 -0
  65. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/provider_cmd.py +0 -0
  66. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/quick_cmd.py +0 -0
  67. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/quickfix_cmd.py +0 -0
  68. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/report_cmd.py +0 -0
  69. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/rollback_cmd.py +0 -0
  70. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/scan_cmd.py +0 -0
  71. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/shared.py +0 -0
  72. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/token_cmd.py +0 -0
  73. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli/watch_cmd.py +0 -0
  74. {fixos-2.2.39 → fixos-2.2.40}/fixos/cli.py +0 -0
  75. {fixos-2.2.39 → fixos-2.2.40}/fixos/config.py +0 -0
  76. {fixos-2.2.39 → fixos-2.2.40}/fixos/config_interactive.py +0 -0
  77. {fixos-2.2.39 → fixos-2.2.40}/fixos/constants.py +0 -0
  78. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/__init__.py +0 -0
  79. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
  80. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
  81. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
  82. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/_storage_container_mixin.py +0 -0
  83. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/_storage_system_mixin.py +0 -0
  84. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/_storage_user_mixin.py +0 -0
  85. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/cache_discovery.py +0 -0
  86. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/__init__.py +0 -0
  87. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/_shared.py +0 -0
  88. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/audio.py +0 -0
  89. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/file_analysis.py +0 -0
  90. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/hardware.py +0 -0
  91. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/packages.py +0 -0
  92. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/resources.py +0 -0
  93. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/security.py +0 -0
  94. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/storage_optimization.py +0 -0
  95. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/system_core.py +0 -0
  96. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/checks/thumbnails.py +0 -0
  97. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  98. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/disk_analyzer.py +0 -0
  99. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  100. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/project_scanner.py +0 -0
  101. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/service_details.py +0 -0
  102. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/storage_analyzer.py +0 -0
  103. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/system_checks.py +0 -0
  104. {fixos-2.2.39 → fixos-2.2.40}/fixos/diagnostics/utils.py +0 -0
  105. {fixos-2.2.39 → fixos-2.2.40}/fixos/features/__init__.py +0 -0
  106. {fixos-2.2.39 → fixos-2.2.40}/fixos/features/auditor.py +0 -0
  107. {fixos-2.2.39 → fixos-2.2.40}/fixos/features/catalog.py +0 -0
  108. {fixos-2.2.39 → fixos-2.2.40}/fixos/features/installer.py +0 -0
  109. {fixos-2.2.39 → fixos-2.2.40}/fixos/features/profiles.py +0 -0
  110. {fixos-2.2.39 → fixos-2.2.40}/fixos/features/renderer.py +0 -0
  111. {fixos-2.2.39 → fixos-2.2.40}/fixos/fixes/__init__.py +0 -0
  112. {fixos-2.2.39 → fixos-2.2.40}/fixos/interactive/__init__.py +0 -0
  113. {fixos-2.2.39 → fixos-2.2.40}/fixos/interactive/cleanup_planner.py +0 -0
  114. {fixos-2.2.39 → fixos-2.2.40}/fixos/llm_shell.py +0 -0
  115. {fixos-2.2.39 → fixos-2.2.40}/fixos/orchestrator/__init__.py +0 -0
  116. {fixos-2.2.39 → fixos-2.2.40}/fixos/orchestrator/executor.py +0 -0
  117. {fixos-2.2.39 → fixos-2.2.40}/fixos/orchestrator/graph.py +0 -0
  118. {fixos-2.2.39 → fixos-2.2.40}/fixos/orchestrator/orchestrator.py +0 -0
  119. {fixos-2.2.39 → fixos-2.2.40}/fixos/orchestrator/rollback.py +0 -0
  120. {fixos-2.2.39 → fixos-2.2.40}/fixos/platform_utils.py +0 -0
  121. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/__init__.py +0 -0
  122. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/base.py +0 -0
  123. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/builtin/__init__.py +0 -0
  124. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/builtin/audio.py +0 -0
  125. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/builtin/disk.py +0 -0
  126. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/builtin/hardware.py +0 -0
  127. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/builtin/resources.py +0 -0
  128. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/builtin/security.py +0 -0
  129. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/builtin/thumbnails.py +0 -0
  130. {fixos-2.2.39 → fixos-2.2.40}/fixos/plugins/registry.py +0 -0
  131. {fixos-2.2.39 → fixos-2.2.40}/fixos/profiles/__init__.py +0 -0
  132. {fixos-2.2.39 → fixos-2.2.40}/fixos/providers/__init__.py +0 -0
  133. {fixos-2.2.39 → fixos-2.2.40}/fixos/providers/llm.py +0 -0
  134. {fixos-2.2.39 → fixos-2.2.40}/fixos/providers/llm_analyzer.py +0 -0
  135. {fixos-2.2.39 → fixos-2.2.40}/fixos/providers/schemas.py +0 -0
  136. {fixos-2.2.39 → fixos-2.2.40}/fixos/system_checks.py +0 -0
  137. {fixos-2.2.39 → fixos-2.2.40}/fixos/utils/__init__.py +0 -0
  138. {fixos-2.2.39 → fixos-2.2.40}/fixos/utils/anonymizer.py +0 -0
  139. {fixos-2.2.39 → fixos-2.2.40}/fixos/utils/terminal.py +0 -0
  140. {fixos-2.2.39 → fixos-2.2.40}/fixos/utils/timeout.py +0 -0
  141. {fixos-2.2.39 → fixos-2.2.40}/fixos/utils/web_search.py +0 -0
  142. {fixos-2.2.39 → fixos-2.2.40}/fixos/watch.py +0 -0
  143. {fixos-2.2.39 → fixos-2.2.40}/fixos.egg-info/dependency_links.txt +0 -0
  144. {fixos-2.2.39 → fixos-2.2.40}/fixos.egg-info/entry_points.txt +0 -0
  145. {fixos-2.2.39 → fixos-2.2.40}/fixos.egg-info/requires.txt +0 -0
  146. {fixos-2.2.39 → fixos-2.2.40}/fixos.egg-info/top_level.txt +0 -0
  147. {fixos-2.2.39 → fixos-2.2.40}/pytest.ini +0 -0
  148. {fixos-2.2.39 → fixos-2.2.40}/requirements-dev.txt +0 -0
  149. {fixos-2.2.39 → fixos-2.2.40}/requirements.txt +0 -0
  150. {fixos-2.2.39 → fixos-2.2.40}/scripts/pyqual-calibrate.py +0 -0
  151. {fixos-2.2.39 → fixos-2.2.40}/setup.cfg +0 -0
  152. {fixos-2.2.39 → fixos-2.2.40}/tests/__init__.py +0 -0
  153. {fixos-2.2.39 → fixos-2.2.40}/tests/conftest.py +0 -0
  154. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/__init__.py +0 -0
  155. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/test_anonymization_layers.py +0 -0
  156. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/test_audio_broken.py +0 -0
  157. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/test_cli.py +0 -0
  158. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/test_diagnostics_integration.py +0 -0
  159. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/test_executor.py +0 -0
  160. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/test_multi_system.py +0 -0
  161. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/test_network_broken.py +0 -0
  162. {fixos-2.2.39 → fixos-2.2.40}/tests/e2e/test_thumbnails_broken.py +0 -0
  163. {fixos-2.2.39 → fixos-2.2.40}/tests/test_fixos.py +0 -0
  164. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/__init__.py +0 -0
  165. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_anonymizer.py +0 -0
  166. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_cache_discovery.py +0 -0
  167. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_core.py +0 -0
  168. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_dev_project_analyzer.py +0 -0
  169. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_executor.py +0 -0
  170. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_llm_provider.py +0 -0
  171. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_orchestrator.py +0 -0
  172. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_project_scanner.py +0 -0
  173. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_projects_cmd.py +0 -0
  174. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_quick_cmd.py +0 -0
  175. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_quick_snapshot.py +0 -0
  176. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_resources_quick.py +0 -0
  177. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_security_quick.py +0 -0
  178. {fixos-2.2.39 → fixos-2.2.40}/tests/unit/test_system_checks_parallel.py +0 -0
@@ -11,6 +11,18 @@
11
11
 
12
12
  ### Fixed
13
13
 
14
+ - Tryb `fixos cleanup` → „Wybierz pojedyncze” przechodzi teraz kolejno przez
15
+ wszystkie usługi: bezpieczne, wymagające przeglądu i chronione. Pozycje bez
16
+ bezpiecznej operacji zbiorczej pokazują komendę podglądu, dane chronione
17
+ wymagają dodatkowego potwierdzenia, a wykonanie jest przypięte do dokładnie
18
+ wybranej ścieżki i poziomu ryzyka.
19
+ - Skany cache npm, pnpm, Yarn, Maven i uv obejmują teraz wyłącznie
20
+ odtwarzalne katalogi. `~/.local/share/uv` (narzędzia, interpretery i
21
+ credentials) oraz `.tools` pnpm nie są już błędnie liczone jako bezpieczne
22
+ cache; czyszczenie npm faktycznie obejmuje raportowane `_npx`.
23
+ - Naprawiono scenariusze TestQL i dodano punkt wejścia `python -m fixos`.
24
+ `cleanup -c <chroniona-usługa> --dry-run` pokazuje bezpieczną komendę
25
+ diagnostyczną zamiast kończyć się mylącym błędem.
14
26
  - Stabilny raport Docker z osobnym rozmiarem aktywnym i odzyskiwalnym oraz
15
27
  poprawnym pomiarem przed/po czyszczeniu.
16
28
  - Usunięto niebezpieczne zbiorcze komendy kasowania wolumenów Docker i
@@ -19,6 +31,27 @@
19
31
  rekurencyjnych skanów `/` oraz `/home` z szybkich kontroli.
20
32
  - Czyszczenie cache JetBrains trafia w aktualną ścieżkę i wymaga zamknięcia IDE.
21
33
 
34
+ ## [2.2.40] - 2026-07-23
35
+
36
+ ### Docs
37
+ - Update CHANGELOG.md
38
+ - Update README.md
39
+
40
+ ### Test
41
+ - Update testql-scenarios/generated-cli-tests.testql.toon.yaml
42
+ - Update testql-scenarios/generated-from-pytests.testql.toon.yaml
43
+ - Update tests/unit/test_cleanup_cmd.py
44
+ - Update tests/unit/test_service_cleanup.py
45
+ - Update tests/unit/test_service_scanner.py
46
+
47
+ ### Other
48
+ - Update fixos/__main__.py
49
+ - Update fixos/cli/cleanup_cmd.py
50
+ - Update fixos/diagnostics/quick_snapshot.py
51
+ - Update fixos/diagnostics/service_cleanup.py
52
+ - Update fixos/diagnostics/service_scanner.py
53
+ - Update uv.lock
54
+
22
55
  ## [2.2.39] - 2026-07-23
23
56
 
24
57
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.39
3
+ Version: 2.2.40
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
@@ -63,11 +63,11 @@ AI-powered OS Diagnostics
63
63
 
64
64
  ## AI Cost Tracking
65
65
 
66
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.39-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.76-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-38.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
66
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.40-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.85-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-38.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
68
68
 
69
- - 🤖 **LLM usage:** $4.7645 (146 commits)
70
- - 👤 **Human dev:** ~$3795 (38.0h @ $100/h, 30min dedup)
69
+ - 🤖 **LLM usage:** $4.8451 (147 commits)
70
+ - 👤 **Human dev:** ~$3817 (38.2h @ $100/h, 30min dedup)
71
71
 
72
72
  Generated on 2026-07-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
73
73
 
@@ -19,11 +19,11 @@ AI-powered OS Diagnostics
19
19
 
20
20
  ## AI Cost Tracking
21
21
 
22
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.39-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
23
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.76-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-38.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
22
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.40-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
23
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.85-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-38.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
24
24
 
25
- - 🤖 **LLM usage:** $4.7645 (146 commits)
26
- - 👤 **Human dev:** ~$3795 (38.0h @ $100/h, 30min dedup)
25
+ - 🤖 **LLM usage:** $4.8451 (147 commits)
26
+ - 👤 **Human dev:** ~$3817 (38.2h @ $100/h, 30min dedup)
27
27
 
28
28
  Generated on 2026-07-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
29
29
 
@@ -1,3 +1,3 @@
1
1
  """fixos – AI-powered Linux/Windows diagnostics and repair."""
2
2
 
3
- __version__ = "2.2.39"
3
+ __version__ = "2.2.40"
@@ -0,0 +1,7 @@
1
+ """Allow running fixOS as ``python -m fixos``."""
2
+
3
+ from fixos.cli import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -79,9 +79,7 @@ def _display_service_item(svc: dict) -> None:
79
79
  risk = svc.get("risk_level") or ("safe" if svc["safe_to_cleanup"] else "review")
80
80
  safe_text, safe_color = _RISK_LABELS.get(risk, _RISK_LABELS["review"])
81
81
 
82
- click.echo(
83
- f" {click.style(svc['name'], fg='yellow', bold=True)} - {size_str}"
84
- )
82
+ click.echo(f" {click.style(svc['name'], fg='yellow', bold=True)} - {size_str}")
85
83
  click.echo(f" {svc['description']}")
86
84
  paths = svc.get("details", {}).get("paths") or [svc["path"]]
87
85
  if len(paths) > 1:
@@ -90,7 +88,9 @@ def _display_service_item(svc: dict) -> None:
90
88
  click.echo(f" • {path}")
91
89
  else:
92
90
  click.echo(f" Ścieżka: {svc['path']}")
93
- click.echo(f" {click.style(safe_text, fg=safe_color, bold=(risk == 'dangerous'))}")
91
+ click.echo(
92
+ f" {click.style(safe_text, fg=safe_color, bold=(risk == 'dangerous'))}"
93
+ )
94
94
 
95
95
  # Show details for specific services
96
96
  if svc.get("details"):
@@ -134,7 +134,11 @@ def _execute_safe_cleanup(services: list, scanner) -> float:
134
134
  for svc in services:
135
135
  svc_type = svc["service_type"]
136
136
  click.echo(f"Czyszczenie {svc_type}...")
137
- result = scanner.cleanup_service(svc_type, dry_run=False)
137
+ result = scanner.cleanup_service(
138
+ svc_type,
139
+ dry_run=False,
140
+ planned_service=svc,
141
+ )
138
142
  if result["success"]:
139
143
  freed = result.get("space_freed_gb", 0)
140
144
  total_freed += freed
@@ -327,7 +331,9 @@ def _cleanup_single_service(
327
331
  bold=True,
328
332
  )
329
333
  )
330
- click.echo(f" Podgląd bez usuwania: fixos cleanup -c {service_name} --dry-run")
334
+ click.echo(
335
+ f" Podgląd bez usuwania: fixos cleanup -c {service_name} --dry-run"
336
+ )
331
337
  if not click.confirm(f"Na pewno usunąć dane usługi {service_name}?"):
332
338
  click.echo("Anulowano.")
333
339
  return
@@ -340,7 +346,9 @@ def _cleanup_single_service(
340
346
 
341
347
  if result["success"]:
342
348
  if dry_run:
343
- click.echo(click.style(f"Symulacja dla {service_name} zakończona", fg="green"))
349
+ click.echo(
350
+ click.style(f"Symulacja dla {service_name} zakończona", fg="green")
351
+ )
344
352
  if result.get("output"):
345
353
  click.echo(f" {result['output']}")
346
354
  if result["space_freed_gb"] > 0:
@@ -349,7 +357,9 @@ def _cleanup_single_service(
349
357
  f"{result['space_freed_gb']:.2f} GB"
350
358
  )
351
359
  else:
352
- click.echo(click.style(f"Zakończono czyszczenie {service_name}", fg="green"))
360
+ click.echo(
361
+ click.style(f"Zakończono czyszczenie {service_name}", fg="green")
362
+ )
353
363
  if result["space_freed_gb"] > 0:
354
364
  click.echo(f" Zwolniono: {result['space_freed_gb']:.2f} GB")
355
365
  else:
@@ -362,11 +372,15 @@ def _cleanup_single_service(
362
372
 
363
373
 
364
374
  def _size_str(svc: dict) -> str:
365
- return f"{svc['size_gb']:.2f} GB" if svc["size_gb"] >= 1 else f"{svc['size_mb']:.0f} MB"
375
+ return (
376
+ f"{svc['size_gb']:.2f} GB"
377
+ if svc["size_gb"] >= 1
378
+ else f"{svc['size_mb']:.0f} MB"
379
+ )
366
380
 
367
381
 
368
- def _select_safe_services(safe_services: list) -> list:
369
- """Ask what to clean from the safe tier: all of it, hand-picked items, or none."""
382
+ def _select_safe_services(safe_services: list) -> tuple[str, list]:
383
+ """Choose bulk-safe cleanup, full individual selection, or no cleanup."""
370
384
  click.echo(click.style("Bezpieczne do wyczyszczenia:", fg="green"))
371
385
  for svc in safe_services:
372
386
  click.echo(f" • {svc['name']}: {_size_str(svc)}")
@@ -375,28 +389,112 @@ def _select_safe_services(safe_services: list) -> list:
375
389
  click.echo()
376
390
  click.echo("Co wyczyścić?")
377
391
  click.echo(f" [1] Wszystkie bezpieczne (zwolni {safe_total:.2f} GB)")
378
- click.echo(" [2] Wybierz pojedyncze usługi")
392
+ click.echo(" [2] Wybierz pojedyncze spośród wszystkich usług")
379
393
  click.echo(" [0] Nic — pomiń")
380
394
  choice = click.prompt(
381
395
  "Wybór", type=click.Choice(["0", "1", "2"]), default="1", show_choices=False
382
396
  )
383
397
 
384
398
  if choice == "0":
385
- return []
399
+ return "none", []
386
400
  if choice == "1":
387
- return safe_services
401
+ return "safe", safe_services
402
+ return "individual", []
388
403
 
389
- click.echo(click.style("Wybierz usługi do wyczyszczenia:", fg="cyan"))
390
- return [svc for svc in safe_services if click.confirm(f" {svc['name']} ({_size_str(svc)})")]
404
+
405
+ def _selection_description(svc: dict) -> str:
406
+ risk = svc.get("risk_level", "review")
407
+ labels = {
408
+ "safe": "bezpieczne cache",
409
+ "review": "do rozważenia",
410
+ "dangerous": "CHRONIONE/mieszane",
411
+ }
412
+ description = labels.get(risk, "do rozważenia")
413
+ if risk == "dangerous" and svc["service_type"] == "docker":
414
+ reclaimable = svc.get("details", {}).get("usage", {}).get("Build Cache", {})
415
+ reclaimable_gb = float(reclaimable.get("reclaimable_gb", 0))
416
+ if reclaimable_gb:
417
+ description += f"; komenda dotyczy do {reclaimable_gb:.2f} GB cache buildów"
418
+ return description
419
+
420
+
421
+ def _select_individual_services(services: list) -> list:
422
+ """Offer every scanned, executable entry in its displayed order."""
423
+ click.echo(
424
+ click.style(
425
+ "Wybierz kolejno spośród wszystkich możliwych usług:",
426
+ fg="cyan",
427
+ )
428
+ )
429
+ selected = []
430
+ for svc in services:
431
+ label = (
432
+ f" {svc['name']} ({_size_str(svc)}) " f"[{_selection_description(svc)}]"
433
+ )
434
+ if not svc.get("can_cleanup") or not svc.get("cleanup_command", "").strip():
435
+ click.echo(f"{label} — brak bezpiecznej operacji zbiorczej")
436
+ preview = svc.get("preview_command")
437
+ if preview:
438
+ click.echo(f" Podgląd: {preview}")
439
+ continue
440
+ if click.confirm(label, default=False):
441
+ selected.append(svc)
442
+ return selected
443
+
444
+
445
+ def _execute_individual_cleanup(services: list, scanner) -> float:
446
+ """Execute hand-picked entries with an extra gate for protected data."""
447
+ total_freed = 0.0
448
+ for svc in services:
449
+ risk = svc.get("risk_level", "review")
450
+ if risk == "dangerous":
451
+ click.echo(
452
+ click.style(
453
+ f"⚠️ {svc['name']} zawiera dane chronione lub mieszane. "
454
+ "Operacji nie da się automatycznie cofnąć.",
455
+ fg="red",
456
+ bold=True,
457
+ )
458
+ )
459
+ if not click.confirm(
460
+ f"Potwierdź wykonanie dokładnie dla: {svc['name']}",
461
+ default=False,
462
+ ):
463
+ click.echo(f" Pominięto {svc['name']}.")
464
+ continue
465
+
466
+ click.echo(f"Czyszczenie {svc['name']}...")
467
+ result = scanner.cleanup_service(
468
+ svc["service_type"],
469
+ dry_run=False,
470
+ planned_service=svc,
471
+ )
472
+ if result["success"]:
473
+ freed = float(result.get("space_freed_gb", 0))
474
+ total_freed += freed
475
+ click.echo(click.style(f" Zwolniono {freed:.2f} GB", fg="green"))
476
+ else:
477
+ click.echo(click.style(f" Błąd: {_error_message(result)}", fg="red"))
478
+ return total_freed
391
479
 
392
480
 
393
481
  def _run_interactive_cleanup(plan: dict, list_only: bool, scanner) -> None:
394
482
  """Offer interactive safe cleanup and display unsafe services."""
395
- if not list_only and plan["safe_to_cleanup"]:
396
- selected = _select_safe_services(plan["safe_to_cleanup"])
483
+ if not list_only and plan["services"]:
484
+ if plan["safe_to_cleanup"]:
485
+ mode, selected = _select_safe_services(plan["safe_to_cleanup"])
486
+ else:
487
+ mode, selected = "individual", []
488
+ if mode == "individual":
489
+ selected = _select_individual_services(plan["services"])
490
+ if selected:
491
+ _execute_individual_cleanup(selected, scanner)
492
+ else:
493
+ click.echo(click.style("Pominięto czyszczenie.", fg="yellow"))
494
+ return
397
495
  if selected:
398
496
  _execute_safe_cleanup(selected, scanner)
399
- else:
497
+ elif mode == "none":
400
498
  click.echo(click.style("Pominięto czyszczenie.", fg="yellow"))
401
499
  if plan["requires_review"] and not list_only:
402
500
  _display_unsafe_services(plan["requires_review"])
@@ -61,10 +61,14 @@ CACHE_RULES: tuple[CacheRule, ...] = (
61
61
  (
62
62
  "~/.npm/_cacache",
63
63
  "~/.npm/_npx",
64
+ "~/.npm/_prebuilds",
64
65
  "~/AppData/Local/npm-cache/_cacache",
65
66
  "~/AppData/Local/npm-cache/_npx",
66
67
  ),
67
- "npm cache clean --force",
68
+ (
69
+ "npm cache clean --force 2>/dev/null || true; "
70
+ "rm -rf ~/.npm/_npx ~/.npm/_prebuilds"
71
+ ),
68
72
  stack="node",
69
73
  ),
70
74
  CacheRule(
@@ -6,6 +6,7 @@ Handles planning and execution of service data cleanup operations.
6
6
  import os
7
7
  import shlex
8
8
  import subprocess
9
+ from types import SimpleNamespace
9
10
  from typing import Dict, Any, List
10
11
  from ..constants import (
11
12
  DEFAULT_COMMAND_TIMEOUT,
@@ -47,10 +48,7 @@ class ServiceCleaner:
47
48
  "requires_review_gb": round(sum(s.size_gb for s in review_services), 2),
48
49
  "dangerous_gb": round(sum(s.size_gb for s in dangerous_services), 2),
49
50
  "manager_reported_reclaimable_gb": round(
50
- sum(
51
- float(s.details.get("reclaimable_size_gb", 0.0))
52
- for s in services
53
- ),
51
+ sum(float(s.details.get("reclaimable_size_gb", 0.0)) for s in services),
54
52
  2,
55
53
  ),
56
54
  "services": [self._service_to_dict(s) for s in services],
@@ -62,9 +60,19 @@ class ServiceCleaner:
62
60
  return plan
63
61
 
64
62
  def cleanup_service(
65
- self, service_type: str, dry_run: bool = False
63
+ self,
64
+ service_type: str,
65
+ dry_run: bool = False,
66
+ planned_service: Dict[str, Any] | None = None,
66
67
  ) -> Dict[str, Any]:
67
- """Execute cleanup for a specific service."""
68
+ """Execute cleanup for a specific service or exact planned entry.
69
+
70
+ One service type can have entries in multiple risk tiers (for example
71
+ VS Code cache and VS Code extensions). Re-scanning and blindly taking
72
+ the largest entry could therefore execute a different cleanup than the
73
+ one selected in the CLI. ``planned_service`` pins execution to the
74
+ displayed path, command and risk entry.
75
+ """
68
76
  from .service_scanner import ServiceType
69
77
 
70
78
  result = {
@@ -78,16 +86,38 @@ class ServiceCleaner:
78
86
 
79
87
  try:
80
88
  service_enum = ServiceType(service_type)
81
- services = self.scanner.scan_service(service_enum)
82
-
83
- if not services:
84
- result["error"] = f"No {service_type} data found above threshold"
85
- return result
86
-
87
- service = services[0] # Take first (largest)
89
+ if planned_service is not None:
90
+ if planned_service.get("service_type") != service_type:
91
+ result["error"] = "Selected cleanup entry does not match service"
92
+ return result
93
+ service = SimpleNamespace(
94
+ service_type=service_enum,
95
+ name=planned_service.get("name", service_type),
96
+ path=planned_service.get("path", ""),
97
+ size_gb=float(planned_service.get("size_gb", 0)),
98
+ can_cleanup=bool(planned_service.get("can_cleanup")),
99
+ cleanup_command=planned_service.get("cleanup_command", ""),
100
+ preview_command=planned_service.get("preview_command", ""),
101
+ details=planned_service.get("details") or {},
102
+ )
103
+ else:
104
+ services = self.scanner.scan_service(service_enum)
105
+ if not services:
106
+ result["error"] = f"No {service_type} data found above threshold"
107
+ return result
108
+ service = services[0] # Backward-compatible single-service mode.
88
109
  initial_size = service.size_gb
89
110
 
90
111
  if not service.can_cleanup or not service.cleanup_command.strip():
112
+ if dry_run:
113
+ preview = getattr(service, "preview_command", "")
114
+ result["success"] = True
115
+ result["requires_item_selection"] = True
116
+ result["output"] = (
117
+ "[DRY RUN] Zbiorcze czyszczenie jest wyłączone. "
118
+ f"Bezpieczny podgląd: {preview or 'brak komendy podglądu'}"
119
+ )
120
+ return result
91
121
  result["error"] = (
92
122
  f"{service.name}: wybierz konkretne elementy do usunięcia; "
93
123
  "zbiorcze czyszczenie jest wyłączone, aby chronić dane"
@@ -113,12 +143,15 @@ class ServiceCleaner:
113
143
 
114
144
  # Check new size
115
145
  measure = getattr(self.scanner, "measure_service_size_mb", None)
146
+ planned_paths = service.details.get("paths") or [service.path]
116
147
  if callable(measure):
117
- new_size_mb = measure(
118
- service_enum, service.path, refresh=True
148
+ new_size_mb = sum(
149
+ measure(service_enum, path, refresh=True) for path in planned_paths
119
150
  )
120
151
  else:
121
- new_size_mb = self.scanner._get_path_size_mb(service.path)
152
+ new_size_mb = sum(
153
+ self.scanner._get_path_size_mb(path) for path in planned_paths
154
+ )
122
155
  new_size_gb = new_size_mb / 1024
123
156
  freed_gb = max(0, initial_size - new_size_gb)
124
157
 
@@ -492,7 +525,7 @@ class ServiceCleaner:
492
525
  # Python
493
526
  ServiceType.PIP: "Python pip cache",
494
527
  ServiceType.CONDA: "Conda package cache (pkgs directories)",
495
- ServiceType.POETRY: "Poetry virtual environments and cache",
528
+ ServiceType.POETRY: "Poetry package cache",
496
529
  # Java
497
530
  ServiceType.GRADLE: "Gradle build cache",
498
531
  ServiceType.MAVEN: "Maven repository cache",
@@ -578,12 +611,15 @@ class ServiceCleaner:
578
611
  ServiceType.CONTAINERD: "sudo rm -rf /var/lib/containerd",
579
612
  ServiceType.PODMAN: "podman system prune -af --volumes",
580
613
  # JS/Node
581
- ServiceType.NPM: "npm cache clean --force",
614
+ ServiceType.NPM: (
615
+ "npm cache clean --force 2>/dev/null || true; "
616
+ "rm -rf ~/.npm/_npx ~/.npm/_prebuilds"
617
+ ),
582
618
  ServiceType.YARN: "yarn cache clean --all",
583
619
  ServiceType.PNPM: "pnpm store prune",
584
620
  # Python
585
621
  ServiceType.PIP: "pip cache purge || rm -rf ~/.cache/pip/*",
586
- ServiceType.CONDA: "conda clean --all -y || rm -rf ~/.conda/envs/*/pkgs",
622
+ ServiceType.CONDA: "conda clean --all -y",
587
623
  ServiceType.POETRY: "poetry cache clear --all pypi || rm -rf ~/.cache/pypoetry",
588
624
  # Java
589
625
  ServiceType.GRADLE: "rm -rf ~/.gradle/caches ~/.gradle/daemon ~/.gradle/wrapper",
@@ -641,7 +677,7 @@ class ServiceCleaner:
641
677
  ServiceType.TRASH: "rm -rf ~/.local/share/Trash/* ~/.Trash/*",
642
678
  ServiceType.LOGS: "find ~/.cache/log ~/.local/state -name '*.log' -mtime +7 -delete 2>/dev/null; journalctl --vacuum-time=7d 2>/dev/null || true",
643
679
  ServiceType.NVIDIA: "rm -rf ~/.cache/nvidia ~/.nv/ComputeCache ~/.cache/mesa_shader_cache",
644
- ServiceType.UV: "uv cache clean || rm -rf ~/.cache/uv ~/.local/share/uv",
680
+ ServiceType.UV: "uv cache clean",
645
681
  ServiceType.TORCH: "rm -rf ~/.cache/torch ~/.torch",
646
682
  ServiceType.BUN: "rm -rf ~/.bun/install/cache",
647
683
  ServiceType.PLAYWRIGHT: "rm -rf ~/.cache/ms-playwright ~/.cache/puppeteer",
@@ -730,9 +766,15 @@ class ServiceCleaner:
730
766
  ServiceType.CONTAINERD: "sudo ls -la /var/lib/containerd 2>/dev/null || echo 'Requires sudo access'",
731
767
  ServiceType.PODMAN: "podman system df -v || podman images",
732
768
  # JS/Node
733
- ServiceType.NPM: "npm cache ls 2>/dev/null || du -sh ~/.npm",
769
+ ServiceType.NPM: (
770
+ "npm cache ls 2>/dev/null; "
771
+ "du -sh ~/.npm/_cacache ~/.npm/_npx ~/.npm/_prebuilds 2>/dev/null"
772
+ ),
734
773
  ServiceType.YARN: "yarn cache list 2>/dev/null || du -sh ~/.cache/yarn",
735
- ServiceType.PNPM: "pnpm store status 2>/dev/null || du -sh ~/.pnpm-store",
774
+ ServiceType.PNPM: (
775
+ "pnpm store status 2>/dev/null || "
776
+ "du -sh ~/.pnpm-store ~/.local/share/pnpm/store 2>/dev/null"
777
+ ),
736
778
  # Python
737
779
  ServiceType.PIP: "pip cache dir && pip cache info 2>/dev/null || du -sh ~/.cache/pip",
738
780
  ServiceType.CONDA: "conda info --envs 2>/dev/null && conda list 2>/dev/null | head -20 || du -sh ~/miniconda3",
@@ -146,9 +146,14 @@ class ServiceDataScanner:
146
146
  ],
147
147
  ServiceType.CONTAINERD: ["/var/lib/containerd", "/run/containerd"],
148
148
  ServiceType.PODMAN: ["~/.local/share/containers", "~/.config/containers"],
149
- ServiceType.NPM: ["~/.npm", "~/.cache/npm"],
150
- ServiceType.YARN: ["~/.cache/yarn", "~/.yarn", "~/.config/yarn"],
151
- ServiceType.PNPM: ["~/.pnpm-store", "~/.local/share/pnpm"],
149
+ ServiceType.NPM: [
150
+ "~/.npm/_cacache",
151
+ "~/.npm/_npx",
152
+ "~/.npm/_prebuilds",
153
+ "~/.cache/npm",
154
+ ],
155
+ ServiceType.YARN: ["~/.cache/yarn", "~/.yarn/berry/cache"],
156
+ ServiceType.PNPM: ["~/.pnpm-store", "~/.local/share/pnpm/store"],
152
157
  ServiceType.PIP: ["~/.cache/pip"],
153
158
  ServiceType.CONDA: [
154
159
  "~/miniconda3/pkgs",
@@ -159,7 +164,7 @@ class ServiceDataScanner:
159
164
  ],
160
165
  ServiceType.POETRY: ["~/.cache/pypoetry"],
161
166
  ServiceType.GRADLE: ["~/.gradle", "~/.cache/gradle"],
162
- ServiceType.MAVEN: ["~/.m2"],
167
+ ServiceType.MAVEN: ["~/.m2/repository"],
163
168
  ServiceType.CARGO: ["~/.cargo/registry", "~/.cargo/git"],
164
169
  ServiceType.GO: ["~/go/pkg", "~/.go/pkg"],
165
170
  ServiceType.FLUTTER: ["~/.flutter-sdk", "~/flutter", "~/.pub-cache"],
@@ -224,7 +229,7 @@ class ServiceDataScanner:
224
229
  "~/.nv/ComputeCache",
225
230
  "~/.cache/mesa_shader_cache",
226
231
  ],
227
- ServiceType.UV: ["~/.cache/uv", "~/.local/share/uv"],
232
+ ServiceType.UV: ["~/.cache/uv"],
228
233
  ServiceType.TORCH: ["~/.cache/torch", "~/.torch"],
229
234
  ServiceType.BUN: ["~/.bun/install/cache"],
230
235
  ServiceType.PLAYWRIGHT: ["~/.cache/ms-playwright", "~/.cache/puppeteer"],
@@ -644,10 +649,17 @@ class ServiceDataScanner:
644
649
  return self._cleaner.get_cleanup_plan(selected_services)
645
650
 
646
651
  def cleanup_service(
647
- self, service_type: str, dry_run: bool = False
652
+ self,
653
+ service_type: str,
654
+ dry_run: bool = False,
655
+ planned_service: Dict[str, Any] | None = None,
648
656
  ) -> Dict[str, Any]:
649
657
  """Execute cleanup for a specific service."""
650
- return self._cleaner.cleanup_service(service_type, dry_run)
658
+ return self._cleaner.cleanup_service(
659
+ service_type,
660
+ dry_run,
661
+ planned_service=planned_service,
662
+ )
651
663
 
652
664
 
653
665
  def main():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.39
3
+ Version: 2.2.40
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
@@ -63,11 +63,11 @@ AI-powered OS Diagnostics
63
63
 
64
64
  ## AI Cost Tracking
65
65
 
66
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.39-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.76-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-38.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
66
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-2.2.40-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
67
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$4.85-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-38.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
68
68
 
69
- - 🤖 **LLM usage:** $4.7645 (146 commits)
70
- - 👤 **Human dev:** ~$3795 (38.0h @ $100/h, 30min dedup)
69
+ - 🤖 **LLM usage:** $4.8451 (147 commits)
70
+ - 👤 **Human dev:** ~$3817 (38.2h @ $100/h, 30min dedup)
71
71
 
72
72
  Generated on 2026-07-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
73
73
 
@@ -29,6 +29,7 @@ docker/ubuntu/Dockerfile
29
29
  docs/examples/advanced_usage.py
30
30
  docs/examples/quickstart.py
31
31
  fixos/__init__.py
32
+ fixos/__main__.py
32
33
  fixos/anonymizer.py
33
34
  fixos/cli.py
34
35
  fixos/config.py
@@ -158,6 +159,7 @@ tests/e2e/test_thumbnails_broken.py
158
159
  tests/unit/__init__.py
159
160
  tests/unit/test_anonymizer.py
160
161
  tests/unit/test_cache_discovery.py
162
+ tests/unit/test_cleanup_cmd.py
161
163
  tests/unit/test_core.py
162
164
  tests/unit/test_dev_project_analyzer.py
163
165
  tests/unit/test_executor.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fixos"
7
- version = "2.2.39"
7
+ version = "2.2.40"
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"
@@ -5,7 +5,7 @@ long_description = (Path(__file__).parent / "README.md").read_text(encoding="utf
5
5
 
6
6
  setup(
7
7
  name="fixos",
8
- version="2.2.39",
8
+ version="2.2.40",
9
9
  description="AI-powered Linux/Windows diagnostics and repair with anonymization",
10
10
  long_description=long_description,
11
11
  long_description_content_type="text/markdown",