fixos 2.2.26__tar.gz → 2.2.28__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 (145) hide show
  1. {fixos-2.2.26 → fixos-2.2.28}/CHANGELOG.md +41 -0
  2. {fixos-2.2.26 → fixos-2.2.28}/PKG-INFO +6 -6
  3. {fixos-2.2.26 → fixos-2.2.28}/README.md +5 -5
  4. {fixos-2.2.26 → fixos-2.2.28}/docker/docker-compose.yml +25 -10
  5. fixos-2.2.28/docker/test-scenarios.sh +147 -0
  6. fixos-2.2.28/docker/validate-scenario.py +158 -0
  7. {fixos-2.2.26 → fixos-2.2.28}/fixos/__init__.py +1 -1
  8. {fixos-2.2.26 → fixos-2.2.28}/fixos/agent/session_core.py +34 -0
  9. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/fix_cmd.py +33 -16
  10. fixos-2.2.28/fixos/cli/output_formatter.py +158 -0
  11. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/report_cmd.py +13 -1
  12. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/scan_cmd.py +92 -38
  13. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/shared.py +2 -0
  14. {fixos-2.2.26 → fixos-2.2.28}/fixos.egg-info/PKG-INFO +6 -6
  15. {fixos-2.2.26 → fixos-2.2.28}/fixos.egg-info/SOURCES.txt +3 -0
  16. {fixos-2.2.26 → fixos-2.2.28}/fixos.egg-info/top_level.txt +1 -0
  17. {fixos-2.2.26 → fixos-2.2.28}/pyproject.toml +1 -1
  18. {fixos-2.2.26 → fixos-2.2.28}/setup.py +1 -1
  19. {fixos-2.2.26 → fixos-2.2.28}/tests/unit/test_core.py +87 -0
  20. {fixos-2.2.26 → fixos-2.2.28}/.env.example +0 -0
  21. {fixos-2.2.26 → fixos-2.2.28}/LICENSE +0 -0
  22. {fixos-2.2.26 → fixos-2.2.28}/MANIFEST.in +0 -0
  23. {fixos-2.2.26 → fixos-2.2.28}/docker/README.md +0 -0
  24. {fixos-2.2.26 → fixos-2.2.28}/docker/TEST_RESULTS.md +0 -0
  25. {fixos-2.2.26 → fixos-2.2.28}/docker/TEST_RESULTS_V2.md +0 -0
  26. {fixos-2.2.26 → fixos-2.2.28}/docker/alpine/Dockerfile +0 -0
  27. {fixos-2.2.26 → fixos-2.2.28}/docker/arch/Dockerfile +0 -0
  28. {fixos-2.2.26 → fixos-2.2.28}/docker/base/Dockerfile +0 -0
  29. {fixos-2.2.26 → fixos-2.2.28}/docker/broken-audio/Dockerfile +0 -0
  30. {fixos-2.2.26 → fixos-2.2.28}/docker/broken-full/Dockerfile +0 -0
  31. {fixos-2.2.26 → fixos-2.2.28}/docker/broken-network/Dockerfile +0 -0
  32. {fixos-2.2.26 → fixos-2.2.28}/docker/broken-thumbnails/Dockerfile +0 -0
  33. {fixos-2.2.26 → fixos-2.2.28}/docker/debian/Dockerfile +0 -0
  34. {fixos-2.2.26 → fixos-2.2.28}/docker/docker-compose.multi-system.yml +0 -0
  35. {fixos-2.2.26 → fixos-2.2.28}/docker/fedora/Dockerfile +0 -0
  36. {fixos-2.2.26 → fixos-2.2.28}/docker/test-multi-system.sh +0 -0
  37. {fixos-2.2.26 → fixos-2.2.28}/docker/ubuntu/Dockerfile +0 -0
  38. {fixos-2.2.26 → fixos-2.2.28}/docs/examples/advanced_usage.py +0 -0
  39. {fixos-2.2.26 → fixos-2.2.28}/docs/examples/quickstart.py +0 -0
  40. {fixos-2.2.26 → fixos-2.2.28}/fixos/agent/__init__.py +0 -0
  41. {fixos-2.2.26 → fixos-2.2.28}/fixos/agent/autonomous.py +0 -0
  42. {fixos-2.2.26 → fixos-2.2.28}/fixos/agent/autonomous_session.py +0 -0
  43. {fixos-2.2.26 → fixos-2.2.28}/fixos/agent/hitl.py +0 -0
  44. {fixos-2.2.26 → fixos-2.2.28}/fixos/agent/hitl_session.py +0 -0
  45. {fixos-2.2.26 → fixos-2.2.28}/fixos/agent/session_handlers.py +0 -0
  46. {fixos-2.2.26 → fixos-2.2.28}/fixos/agent/session_io.py +0 -0
  47. {fixos-2.2.26 → fixos-2.2.28}/fixos/anonymizer.py +0 -0
  48. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/__init__.py +0 -0
  49. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/ask_cmd.py +0 -0
  50. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/cleanup_cmd.py +0 -0
  51. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/config_cmd.py +0 -0
  52. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/features_cmd.py +0 -0
  53. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/history_cmd.py +0 -0
  54. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/main.py +0 -0
  55. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/orchestrate_cmd.py +0 -0
  56. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/profile_cmd.py +0 -0
  57. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/provider_cmd.py +0 -0
  58. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/quickfix_cmd.py +0 -0
  59. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/rollback_cmd.py +0 -0
  60. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/token_cmd.py +0 -0
  61. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli/watch_cmd.py +0 -0
  62. {fixos-2.2.26 → fixos-2.2.28}/fixos/cli.py +0 -0
  63. {fixos-2.2.26 → fixos-2.2.28}/fixos/config.py +0 -0
  64. {fixos-2.2.26 → fixos-2.2.28}/fixos/config_interactive.py +0 -0
  65. {fixos-2.2.26 → fixos-2.2.28}/fixos/constants.py +0 -0
  66. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/__init__.py +0 -0
  67. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/checks/__init__.py +0 -0
  68. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/checks/_shared.py +0 -0
  69. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/checks/audio.py +0 -0
  70. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/checks/hardware.py +0 -0
  71. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/checks/resources.py +0 -0
  72. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/checks/security.py +0 -0
  73. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/checks/system_core.py +0 -0
  74. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/checks/thumbnails.py +0 -0
  75. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  76. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/disk_analyzer.py +0 -0
  77. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  78. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/service_cleanup.py +0 -0
  79. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/service_details.py +0 -0
  80. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/service_scanner.py +0 -0
  81. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/storage_analyzer.py +0 -0
  82. {fixos-2.2.26 → fixos-2.2.28}/fixos/diagnostics/system_checks.py +0 -0
  83. {fixos-2.2.26 → fixos-2.2.28}/fixos/features/__init__.py +0 -0
  84. {fixos-2.2.26 → fixos-2.2.28}/fixos/features/auditor.py +0 -0
  85. {fixos-2.2.26 → fixos-2.2.28}/fixos/features/catalog.py +0 -0
  86. {fixos-2.2.26 → fixos-2.2.28}/fixos/features/installer.py +0 -0
  87. {fixos-2.2.26 → fixos-2.2.28}/fixos/features/profiles.py +0 -0
  88. {fixos-2.2.26 → fixos-2.2.28}/fixos/features/renderer.py +0 -0
  89. {fixos-2.2.26 → fixos-2.2.28}/fixos/fixes/__init__.py +0 -0
  90. {fixos-2.2.26 → fixos-2.2.28}/fixos/interactive/__init__.py +0 -0
  91. {fixos-2.2.26 → fixos-2.2.28}/fixos/interactive/cleanup_planner.py +0 -0
  92. {fixos-2.2.26 → fixos-2.2.28}/fixos/llm_shell.py +0 -0
  93. {fixos-2.2.26 → fixos-2.2.28}/fixos/orchestrator/__init__.py +0 -0
  94. {fixos-2.2.26 → fixos-2.2.28}/fixos/orchestrator/executor.py +0 -0
  95. {fixos-2.2.26 → fixos-2.2.28}/fixos/orchestrator/graph.py +0 -0
  96. {fixos-2.2.26 → fixos-2.2.28}/fixos/orchestrator/orchestrator.py +0 -0
  97. {fixos-2.2.26 → fixos-2.2.28}/fixos/orchestrator/rollback.py +0 -0
  98. {fixos-2.2.26 → fixos-2.2.28}/fixos/platform_utils.py +0 -0
  99. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/__init__.py +0 -0
  100. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/base.py +0 -0
  101. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/builtin/__init__.py +0 -0
  102. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/builtin/audio.py +0 -0
  103. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/builtin/disk.py +0 -0
  104. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/builtin/hardware.py +0 -0
  105. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/builtin/resources.py +0 -0
  106. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/builtin/security.py +0 -0
  107. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/builtin/thumbnails.py +0 -0
  108. {fixos-2.2.26 → fixos-2.2.28}/fixos/plugins/registry.py +0 -0
  109. {fixos-2.2.26 → fixos-2.2.28}/fixos/profiles/__init__.py +0 -0
  110. {fixos-2.2.26 → fixos-2.2.28}/fixos/providers/__init__.py +0 -0
  111. {fixos-2.2.26 → fixos-2.2.28}/fixos/providers/llm.py +0 -0
  112. {fixos-2.2.26 → fixos-2.2.28}/fixos/providers/llm_analyzer.py +0 -0
  113. {fixos-2.2.26 → fixos-2.2.28}/fixos/providers/schemas.py +0 -0
  114. {fixos-2.2.26 → fixos-2.2.28}/fixos/system_checks.py +0 -0
  115. {fixos-2.2.26 → fixos-2.2.28}/fixos/utils/__init__.py +0 -0
  116. {fixos-2.2.26 → fixos-2.2.28}/fixos/utils/anonymizer.py +0 -0
  117. {fixos-2.2.26 → fixos-2.2.28}/fixos/utils/terminal.py +0 -0
  118. {fixos-2.2.26 → fixos-2.2.28}/fixos/utils/timeout.py +0 -0
  119. {fixos-2.2.26 → fixos-2.2.28}/fixos/utils/web_search.py +0 -0
  120. {fixos-2.2.26 → fixos-2.2.28}/fixos/watch.py +0 -0
  121. {fixos-2.2.26 → fixos-2.2.28}/fixos.egg-info/dependency_links.txt +0 -0
  122. {fixos-2.2.26 → fixos-2.2.28}/fixos.egg-info/entry_points.txt +0 -0
  123. {fixos-2.2.26 → fixos-2.2.28}/fixos.egg-info/requires.txt +0 -0
  124. {fixos-2.2.26 → fixos-2.2.28}/pytest.ini +0 -0
  125. {fixos-2.2.26 → fixos-2.2.28}/requirements-dev.txt +0 -0
  126. {fixos-2.2.26 → fixos-2.2.28}/requirements.txt +0 -0
  127. {fixos-2.2.26 → fixos-2.2.28}/scripts/pyqual-calibrate.py +0 -0
  128. {fixos-2.2.26 → fixos-2.2.28}/setup.cfg +0 -0
  129. {fixos-2.2.26 → fixos-2.2.28}/tests/__init__.py +0 -0
  130. {fixos-2.2.26 → fixos-2.2.28}/tests/conftest.py +0 -0
  131. {fixos-2.2.26 → fixos-2.2.28}/tests/e2e/__init__.py +0 -0
  132. {fixos-2.2.26 → fixos-2.2.28}/tests/e2e/test_anonymization_layers.py +0 -0
  133. {fixos-2.2.26 → fixos-2.2.28}/tests/e2e/test_audio_broken.py +0 -0
  134. {fixos-2.2.26 → fixos-2.2.28}/tests/e2e/test_cli.py +0 -0
  135. {fixos-2.2.26 → fixos-2.2.28}/tests/e2e/test_executor.py +0 -0
  136. {fixos-2.2.26 → fixos-2.2.28}/tests/e2e/test_multi_system.py +0 -0
  137. {fixos-2.2.26 → fixos-2.2.28}/tests/e2e/test_network_broken.py +0 -0
  138. {fixos-2.2.26 → fixos-2.2.28}/tests/e2e/test_thumbnails_broken.py +0 -0
  139. {fixos-2.2.26 → fixos-2.2.28}/tests/test_fixos.py +0 -0
  140. {fixos-2.2.26 → fixos-2.2.28}/tests/unit/__init__.py +0 -0
  141. {fixos-2.2.26 → fixos-2.2.28}/tests/unit/test_anonymizer.py +0 -0
  142. {fixos-2.2.26 → fixos-2.2.28}/tests/unit/test_executor.py +0 -0
  143. {fixos-2.2.26 → fixos-2.2.28}/tests/unit/test_orchestrator.py +0 -0
  144. {fixos-2.2.26 → fixos-2.2.28}/tests/unit/test_service_cleanup.py +0 -0
  145. {fixos-2.2.26 → fixos-2.2.28}/tests/unit/test_service_scanner.py +0 -0
@@ -150,6 +150,47 @@ fix(goal): code analysis engine
150
150
  - **refactor(cli):** Usunięto zduplikowany kod ujednolicając funkcje analizy dysku do wspólnego helpera `_run_disk_analysis`.
151
151
  - **refactor(ui):** Usunięto ikony Unicode z CLI i sformatowano wyjście `stderr` oraz standardowego logowania na czysty kod Markdown dla poprawy czytelności w oknach terminalowych.
152
152
 
153
+ ## [2.2.28] - 2026-05-06
154
+
155
+ ### Docs
156
+ - Update README.md
157
+ - Update SUMD.md
158
+ - Update SUMR.md
159
+ - Update project/README.md
160
+ - Update project/context.md
161
+
162
+ ### Test
163
+ - Update tests/unit/test_core.py
164
+
165
+ ### Other
166
+ - Update app.doql.less
167
+ - Update docker/docker-compose.yml
168
+ - Update fixos/agent/session_core.py
169
+ - Update project/analysis.toon.yaml
170
+ - Update project/calls.mmd
171
+ - Update project/calls.png
172
+ - Update project/calls.toon.yaml
173
+ - Update project/calls.yaml
174
+ - Update project/compact_flow.mmd
175
+ - Update project/compact_flow.png
176
+ - ... and 10 more files
177
+
178
+ ## [2.2.27] - 2026-05-04
179
+
180
+ ### Docs
181
+ - Update README.md
182
+
183
+ ### Other
184
+ - Update docker/docker-compose.yml
185
+ - Update docker/test-scenarios.sh
186
+ - Update docker/validate-scenario.py
187
+ - Update fixos/cli/fix_cmd.py
188
+ - Update fixos/cli/output_formatter.py
189
+ - Update fixos/cli/report_cmd.py
190
+ - Update fixos/cli/scan_cmd.py
191
+ - Update fixos/cli/shared.py
192
+ - Update uv.lock
193
+
153
194
  ## [2.2.26] - 2026-05-04
154
195
 
155
196
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.26
3
+ Version: 2.2.28
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,13 +63,13 @@ 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.26-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-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.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.28-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-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
68
68
 
69
- - 🤖 **LLM usage:** $7.5000 (125 commits)
70
- - 👤 **Human dev:** ~$2603 (26.0h @ $100/h, 30min dedup)
69
+ - 🤖 **LLM usage:** $7.5000 (127 commits)
70
+ - 👤 **Human dev:** ~$2647 (26.5h @ $100/h, 30min dedup)
71
71
 
72
- Generated on 2026-05-04 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
72
+ Generated on 2026-05-06 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
73
73
 
74
74
  ---
75
75
 
@@ -19,13 +19,13 @@ 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.26-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-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.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.28-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-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
24
24
 
25
- - 🤖 **LLM usage:** $7.5000 (125 commits)
26
- - 👤 **Human dev:** ~$2603 (26.0h @ $100/h, 30min dedup)
25
+ - 🤖 **LLM usage:** $7.5000 (127 commits)
26
+ - 👤 **Human dev:** ~$2647 (26.5h @ $100/h, 30min dedup)
27
27
 
28
- Generated on 2026-05-04 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
28
+ Generated on 2026-05-06 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
29
29
 
30
30
  ---
31
31
 
@@ -1,4 +1,4 @@
1
- version: "3.9"
1
+ # docker-compose.yml – fixos test environments
2
2
 
3
3
  # ═══════════════════════════════════════════════════════════
4
4
  # fixos – środowiska testowe (Docker)
@@ -32,7 +32,6 @@ services:
32
32
  context: ..
33
33
  dockerfile: docker/base/Dockerfile
34
34
  image: fixos-base:latest
35
- profiles: ["build-only"]
36
35
  command: echo "base image built"
37
36
 
38
37
  # ── Uszkodzony dźwięk ──────────────────────────────────
@@ -49,9 +48,9 @@ services:
49
48
  - FIXOS_TEST_MODE=1
50
49
  command: >
51
50
  sh -c "
52
- echo '=== Scenariusz: broken-audio ===' &&
53
- verify-scenario.sh &&
54
- python3 -m fixos.diagnostics.system_checks
51
+ echo '=== Scenariusz: broken-audio ===' >&2 &&
52
+ verify-scenario.sh >&2 &&
53
+ python3 -m fixos.cli.main scan --yaml -M audio --no-banner
55
54
  "
56
55
 
57
56
  # ── Uszkodzone thumbnails ──────────────────────────────
@@ -83,9 +82,9 @@ services:
83
82
  - FIXOS_TEST_MODE=1
84
83
  command: >
85
84
  sh -c "
86
- echo '=== Scenariusz: broken-network ===' &&
87
- verify-scenario.sh &&
88
- python3 /usr/local/bin/simulate-network-diag.py
85
+ echo '=== Scenariusz: broken-network ===' >&2 &&
86
+ verify-scenario.sh >&2 &&
87
+ python3 -m fixos.cli.main scan --yaml -M system --no-banner
89
88
  "
90
89
 
91
90
  # ── Pełne uszkodzenie ─────────────────────────────────
@@ -123,8 +122,7 @@ services:
123
122
  --cov-report=html:/reports/coverage
124
123
  -m 'not real_api'
125
124
  "
126
- depends_on:
127
- - base
125
+
128
126
 
129
127
  # ── Unit testy (bez Dockera środowisk) ────────────────
130
128
  unit-tests:
@@ -153,6 +151,23 @@ services:
153
151
  python -m pytest tests/e2e/test_cli.py tests/e2e/test_executor.py -v --tb=short
154
152
  "
155
153
 
154
+ # ── Testy scenariuszy (YAML validation) ────────────────
155
+ scenario-tests:
156
+ <<: *common
157
+ build:
158
+ context: ..
159
+ dockerfile: docker/base/Dockerfile
160
+ container_name: fixos-scenario-tests
161
+ environment:
162
+ - FIXOS_TEST_MODE=1
163
+ command: >
164
+ sh -c "
165
+ echo 'Running scenario validation...' &&
166
+ python3 -m fixos.cli.main scan --yaml -M audio --no-banner |
167
+ python3 docker/validate-scenario.py broken-audio &&
168
+ echo 'All scenario tests passed!'
169
+ "
170
+
156
171
  networks:
157
172
  fixos-test:
158
173
  driver: bridge
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # fixos – Docker Scenario Test Harness
4
+ #
5
+ # Builds and runs each broken-* scenario container,
6
+ # captures YAML output from `fixos scan --yaml`,
7
+ # and validates results with validate-scenario.py.
8
+ #
9
+ # Usage:
10
+ # ./test-scenarios.sh # all scenarios
11
+ # ./test-scenarios.sh broken-audio # single scenario
12
+ # ═══════════════════════════════════════════════════════════
13
+
14
+ set -euo pipefail
15
+
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
18
+ VALIDATOR="$SCRIPT_DIR/validate-scenario.py"
19
+ REPORTS_DIR="${REPORTS_DIR:-/tmp/fixos-test-reports}"
20
+
21
+ # Colors
22
+ RED='\033[0;31m'
23
+ GREEN='\033[0;32m'
24
+ YELLOW='\033[1;33m'
25
+ CYAN='\033[0;36m'
26
+ NC='\033[0m'
27
+
28
+ # Scenarios: service name → modules to scan
29
+ declare -A SCENARIO_MODULES=(
30
+ ["broken-audio"]="audio"
31
+ ["broken-thumbnails"]="thumbnails"
32
+ ["broken-network"]="system"
33
+ ["broken-full"]="audio,thumbnails,system"
34
+ )
35
+
36
+ PASSED=0
37
+ FAILED=0
38
+ ERRORS=()
39
+
40
+ log() { echo -e "${CYAN}[fixos-test]${NC} $*"; }
41
+ ok() { echo -e " ${GREEN}✓${NC} $*"; }
42
+ err() { echo -e " ${RED}✗${NC} $*"; }
43
+ warn() { echo -e " ${YELLOW}⚠${NC} $*"; }
44
+
45
+ # ── Build base image ──────────────────────────────────────
46
+ build_base() {
47
+ log "Building base image..."
48
+ docker compose -f "$SCRIPT_DIR/docker-compose.yml" build base 2>&1 | tail -5
49
+ ok "fixos-base:latest built"
50
+ }
51
+
52
+ # ── Run scenario ──────────────────────────────────────────
53
+ run_scenario() {
54
+ local scenario="$1"
55
+ local modules="${SCENARIO_MODULES[$scenario]:-system}"
56
+ local output_file="$REPORTS_DIR/${scenario}.yml"
57
+
58
+ log "━━━ Scenario: ${YELLOW}${scenario}${NC} (modules: $modules) ━━━"
59
+
60
+ # Build scenario image
61
+ log " Building $scenario..."
62
+ if ! docker compose -f "$SCRIPT_DIR/docker-compose.yml" build "$scenario" 2>&1 | tail -3; then
63
+ err "Build failed for $scenario"
64
+ ERRORS+=("$scenario: build failed")
65
+ ((FAILED++))
66
+ return 1
67
+ fi
68
+
69
+ # Run fixos scan --yaml inside container
70
+ log " Running fixos scan --yaml -M $modules..."
71
+ mkdir -p "$REPORTS_DIR"
72
+
73
+ local exit_code=0
74
+ docker compose -f "$SCRIPT_DIR/docker-compose.yml" run --rm \
75
+ --no-deps \
76
+ -e FIXOS_TEST_MODE=1 \
77
+ "$scenario" \
78
+ python3 -m fixos.cli.main scan --yaml -M "$modules" --no-banner \
79
+ > "$output_file" 2>/dev/null || exit_code=$?
80
+
81
+ # Check we got output
82
+ if [ ! -s "$output_file" ]; then
83
+ err "No YAML output from $scenario"
84
+ ERRORS+=("$scenario: no output")
85
+ ((FAILED++))
86
+ return 1
87
+ fi
88
+
89
+ ok "YAML captured: $output_file ($(wc -l < "$output_file") lines)"
90
+
91
+ # Validate with validate-scenario.py
92
+ if python3 "$VALIDATOR" "$scenario" < "$output_file"; then
93
+ ok "$scenario PASSED"
94
+ ((PASSED++))
95
+ else
96
+ err "$scenario FAILED validation"
97
+ ERRORS+=("$scenario: validation failed")
98
+ ((FAILED++))
99
+ fi
100
+
101
+ echo ""
102
+ }
103
+
104
+ # ── Main ──────────────────────────────────────────────────
105
+ main() {
106
+ log "fixOS Docker Scenario Test Harness"
107
+ log "═══════════════════════════════════"
108
+
109
+ # Build base first
110
+ build_base
111
+
112
+ # Determine which scenarios to run
113
+ local scenarios=()
114
+ if [ $# -gt 0 ]; then
115
+ scenarios=("$@")
116
+ else
117
+ scenarios=("broken-audio" "broken-thumbnails" "broken-network" "broken-full")
118
+ fi
119
+
120
+ # Run each scenario
121
+ for scenario in "${scenarios[@]}"; do
122
+ if [ -z "${SCENARIO_MODULES[$scenario]+x}" ]; then
123
+ warn "Unknown scenario: $scenario (skipping)"
124
+ continue
125
+ fi
126
+ run_scenario "$scenario" || true
127
+ done
128
+
129
+ # Summary
130
+ echo ""
131
+ log "═══════════════════════════════════"
132
+ log " SUMMARY"
133
+ log "═══════════════════════════════════"
134
+ ok "Passed: $PASSED"
135
+ if [ $FAILED -gt 0 ]; then
136
+ err "Failed: $FAILED"
137
+ for e in "${ERRORS[@]}"; do
138
+ err " $e"
139
+ done
140
+ exit 1
141
+ else
142
+ ok "All scenarios passed!"
143
+ exit 0
144
+ fi
145
+ }
146
+
147
+ main "$@"
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validates fixOS scan YAML output against expected scenario conditions.
4
+
5
+ Usage:
6
+ fixos scan --yaml --modules audio | python3 validate-scenario.py broken-audio
7
+ cat scan-output.yml | python3 validate-scenario.py broken-full
8
+
9
+ Exit codes:
10
+ 0 – all expectations met
11
+ 1 – validation failures
12
+ 2 – invalid input / unknown scenario
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+ from typing import Any
19
+
20
+ import yaml
21
+
22
+
23
+ # ── Scenario expectations ─────────────────────────────────
24
+
25
+ SCENARIOS: dict[str, list[dict[str, Any]]] = {
26
+ "broken-audio": [
27
+ {
28
+ "path": "diagnostics.audio.alsa_cards",
29
+ "contains": "no soundcards",
30
+ "desc": "ALSA powinno zgłaszać brak kart dźwiękowych",
31
+ },
32
+ {
33
+ "path": "diagnostics.audio.sof_firmware",
34
+ "contains_any": ["No such file", "not installed", "ERR"],
35
+ "desc": "SOF firmware powinno być niedostępne",
36
+ },
37
+ ],
38
+ "broken-thumbnails": [
39
+ {
40
+ "path": "diagnostics.thumbnails.thumbnail_cache_count",
41
+ "equals": "0",
42
+ "desc": "Cache miniaturek powinien być pusty",
43
+ },
44
+ {
45
+ "path": "diagnostics.thumbnails.ffmpegthumbnailer",
46
+ "contains": "nie zainstalowany",
47
+ "desc": "ffmpegthumbnailer powinien być niezainstalowany",
48
+ },
49
+ ],
50
+ "broken-network": [
51
+ {
52
+ "path": "diagnostics.system.systemctl_failed",
53
+ "contains": "NetworkManager",
54
+ "desc": "NetworkManager powinien być w stanie failed",
55
+ },
56
+ ],
57
+ "broken-full": [
58
+ {
59
+ "path": "diagnostics.audio.alsa_cards",
60
+ "contains": "no soundcards",
61
+ "desc": "ALSA: brak kart dźwiękowych",
62
+ },
63
+ {
64
+ "path": "diagnostics.thumbnails.ffmpegthumbnailer",
65
+ "contains": "nie zainstalowany",
66
+ "desc": "ffmpegthumbnailer niezainstalowany",
67
+ },
68
+ ],
69
+ }
70
+
71
+
72
+ def _get_nested(data: dict, path: str) -> Any:
73
+ """Get a nested value from a dict using dot notation."""
74
+ keys = path.split(".")
75
+ current = data
76
+ for key in keys:
77
+ if isinstance(current, dict) and key in current:
78
+ current = current[key]
79
+ else:
80
+ return None
81
+ return current
82
+
83
+
84
+ def validate(data: dict, scenario: str) -> list[str]:
85
+ """Validate data against scenario expectations. Returns list of failures."""
86
+ expectations = SCENARIOS.get(scenario)
87
+ if expectations is None:
88
+ return [f"Nieznany scenariusz: '{scenario}'. Dostępne: {', '.join(SCENARIOS.keys())}"]
89
+
90
+ failures = []
91
+ for exp in expectations:
92
+ path = exp["path"]
93
+ desc = exp.get("desc", path)
94
+ value = _get_nested(data, path)
95
+
96
+ if value is None:
97
+ failures.append(f" ✗ BRAK KLUCZA: {path} — {desc}")
98
+ continue
99
+
100
+ value_str = str(value)
101
+
102
+ if "equals" in exp:
103
+ if value_str.strip() != str(exp["equals"]).strip():
104
+ failures.append(f" ✗ {desc}: oczekiwano '{exp['equals']}', otrzymano '{value_str[:80]}'")
105
+
106
+ if "contains" in exp:
107
+ if exp["contains"].lower() not in value_str.lower():
108
+ failures.append(f" ✗ {desc}: brak '{exp['contains']}' w '{value_str[:80]}'")
109
+
110
+ if "contains_any" in exp:
111
+ if not any(term.lower() in value_str.lower() for term in exp["contains_any"]):
112
+ failures.append(
113
+ f" ✗ {desc}: brak żadnego z {exp['contains_any']} w '{value_str[:80]}'"
114
+ )
115
+
116
+ return failures
117
+
118
+
119
+ def main() -> None:
120
+ if len(sys.argv) < 2:
121
+ print(f"Użycie: {sys.argv[0]} <scenario>", file=sys.stderr)
122
+ print(f"Dostępne scenariusze: {', '.join(SCENARIOS.keys())}", file=sys.stderr)
123
+ sys.exit(2)
124
+
125
+ scenario = sys.argv[1]
126
+ if scenario not in SCENARIOS:
127
+ print(f"✗ Nieznany scenariusz: '{scenario}'", file=sys.stderr)
128
+ print(f" Dostępne: {', '.join(SCENARIOS.keys())}", file=sys.stderr)
129
+ sys.exit(2)
130
+
131
+ # Read YAML from stdin
132
+ try:
133
+ raw = sys.stdin.read()
134
+ data = yaml.safe_load(raw)
135
+ except yaml.YAMLError as e:
136
+ print(f"✗ Błąd parsowania YAML: {e}", file=sys.stderr)
137
+ sys.exit(2)
138
+
139
+ if not isinstance(data, dict):
140
+ print(f"✗ Oczekiwano dict, otrzymano: {type(data).__name__}", file=sys.stderr)
141
+ sys.exit(2)
142
+
143
+ # Validate
144
+ failures = validate(data, scenario)
145
+
146
+ if failures:
147
+ print(f"✗ Scenariusz '{scenario}' — {len(failures)} błędów:", file=sys.stderr)
148
+ for f in failures:
149
+ print(f, file=sys.stderr)
150
+ sys.exit(1)
151
+ else:
152
+ expectations_count = len(SCENARIOS[scenario])
153
+ print(f"✓ Scenariusz '{scenario}' — {expectations_count}/{expectations_count} OK", file=sys.stderr)
154
+ sys.exit(0)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ main()
@@ -1,2 +1,2 @@
1
1
  """fixos – AI-powered Linux/Windows diagnostics and repair."""
2
- __version__ = "2.2.26"
2
+ __version__ = "2.2.28"
@@ -111,9 +111,17 @@ def _is_part_diagnostic_only(part: str) -> bool:
111
111
  return normalized.startswith(diagnostic_prefixes)
112
112
 
113
113
 
114
+ def _extract_co_robi(text: str) -> str:
115
+ """Extract 'Co robi:' comment from text following a command match."""
116
+ m = re.search(r"\s*\*{0,2}Co robi:\*{0,2}\s*(.+?)(?:\n|$)", text, re.IGNORECASE)
117
+ return m.group(1).strip() if m else ""
118
+
119
+
114
120
  def extract_fixes(reply: str) -> List[Tuple[str, str]]:
115
121
  """Extract (command, comment) pairs from LLM reply."""
116
122
  fixes: List[Tuple[str, str]] = []
123
+
124
+ # Pattern 1: **Komenda:** `command` (strict: bold + backticks)
117
125
  for m in re.finditer(
118
126
  r"\*\*Komenda:\*\*\s*`([^`]+)`(?:[^\n]*?\*\*Co robi:\*\*\s*(.+?))?(?=\n|$)",
119
127
  reply, re.IGNORECASE,
@@ -121,12 +129,38 @@ def extract_fixes(reply: str) -> List[Tuple[str, str]]:
121
129
  cmd = m.group(1).strip()
122
130
  if cmd:
123
131
  fixes.append((cmd, (m.group(2) or "").strip()))
132
+
133
+ # Pattern 2: Komenda: `command` (backticks, optional bold)
134
+ if not fixes:
135
+ for m in re.finditer(
136
+ r"\*{0,2}Komenda:\*{0,2}\s*`([^`]+)`",
137
+ reply, re.IGNORECASE,
138
+ ):
139
+ cmd = m.group(1).strip()
140
+ if cmd:
141
+ fixes.append((cmd, _extract_co_robi(reply[m.end():])))
142
+
143
+ # Pattern 3: Komenda: command (no backticks — command until Co robi:/next problem/section)
144
+ if not fixes:
145
+ for m in re.finditer(
146
+ r"\*{0,2}Komenda:\*{0,2}\s*"
147
+ r"(.+?)"
148
+ r"(?=\n\s*\*{0,2}Co robi:|\n[🔴🟡🟢]|\n━|\n─|\n\[[\dA-Z]|\Z)",
149
+ reply, re.IGNORECASE | re.DOTALL,
150
+ ):
151
+ cmd = re.sub(r"\s*\n\s*", " ", m.group(1)).strip()
152
+ if cmd:
153
+ fixes.append((cmd, _extract_co_robi(reply[m.end():])))
154
+
155
+ # Fallback: → Fix: `command`
124
156
  if not fixes:
125
157
  for m in re.finditer(r"→\s*Fix:\s*`([^`]+)`", reply, re.IGNORECASE):
126
158
  fixes.append((m.group(1).strip(), ""))
159
+ # Fallback: [N] ... `command`
127
160
  if not fixes:
128
161
  for m in re.finditer(r"\[(\d+)\][^`\n]+`([^`]+)`", reply):
129
162
  fixes.append((m.group(2).strip(), f"Fix #{m.group(1)}"))
163
+ # Fallback: EXEC: `command`
130
164
  if not fixes:
131
165
  for m in re.finditer(r"EXEC:\s*`([^`]+)`", reply, re.IGNORECASE):
132
166
  fixes.append((m.group(1).strip(), ""))
@@ -9,6 +9,7 @@ from typing import Dict, Any, List
9
9
  import click
10
10
 
11
11
  from fixos.cli.shared import add_common_options, add_shared_options, BANNER
12
+ from fixos.cli.output_formatter import OutputFormatter
12
13
  from fixos.config import FixOsConfig, interactive_provider_setup
13
14
  from fixos.agent.hitl import run_hitl_session
14
15
  from fixos.agent.autonomous import run_autonomous_session
@@ -19,22 +20,20 @@ from fixos.constants import (
19
20
  )
20
21
 
21
22
 
22
- def _collect_diagnostics(modules: str, disc: bool, json_output: bool, output: str) -> dict:
23
+ def _collect_diagnostics(modules: str, disc: bool, fmt: OutputFormatter, output: str) -> dict:
23
24
  """Run diagnostics collection and optionally disk analysis. Returns data dict."""
24
25
  selected_modules = modules.split(",") if modules else None
25
26
 
26
27
  if disc and not modules:
27
28
  data: dict = {}
28
29
  else:
29
- click.echo(click.style("\nZbieranie diagnostyki...", fg="yellow"))
30
- def progress(name, desc) -> None:
31
- click.echo(f" → {desc}...")
30
+ fmt.status("\nZbieranie diagnostyki...", fg="yellow")
32
31
  from fixos.diagnostics import get_full_diagnostics
33
- data = get_full_diagnostics(selected_modules, progress_callback=progress)
32
+ data = get_full_diagnostics(selected_modules, progress_callback=fmt.progress)
34
33
 
35
34
  if disc:
36
35
  from fixos.cli.scan_cmd import _run_disk_analysis
37
- _run_disk_analysis(data, json_output=json_output, is_fix_mode=True)
36
+ _run_disk_analysis(data, fmt=fmt, is_fix_mode=True)
38
37
 
39
38
  if output:
40
39
  from fixos.utils.anonymizer import anonymize
@@ -44,9 +43,9 @@ def _collect_diagnostics(modules: str, disc: bool, json_output: bool, output: st
44
43
  json.dumps({"anonymized": anon_str, "raw": data}, ensure_ascii=False, indent=2, default=str),
45
44
  encoding="utf-8"
46
45
  )
47
- click.echo(click.style(f"Raport: {output}", fg="green"))
46
+ fmt.status(f"Raport: {output}", fg="green")
48
47
  except Exception as e:
49
- click.echo(f"Błąd zapisu: {e}")
48
+ fmt.status(f"Błąd zapisu: {e}")
50
49
 
51
50
  return data
52
51
 
@@ -83,7 +82,7 @@ def _run_agent_session(cfg, data: dict, max_fixes: int) -> None:
83
82
  help="Maksymalna liczba napraw w sesji")
84
83
  @add_shared_options
85
84
  def fix(provider, token, model, no_banner, mode, timeout, modules, no_show_data, output, max_fixes,
86
- disc, dry_run, interactive, json_output, llm_fallback, show_raw) -> None:
85
+ disc, dry_run, interactive, json_output, yaml_output, llm_fallback, show_raw) -> None:
87
86
  """
88
87
  Przeprowadza pełną diagnostykę i uruchamia sesję naprawczą z LLM.
89
88
 
@@ -92,12 +91,17 @@ def fix(provider, token, model, no_banner, mode, timeout, modules, no_show_data,
92
91
  hitl – Human-in-the-Loop (pyta o każdą akcję) [domyślny]
93
92
  autonomous – Agent sam wykonuje komendy (UWAGA: wymaga potwierdzenia)
94
93
 
94
+ \b
95
+ Pipeline (synchroniczny, bez interakcji):
96
+ fixos fix --yaml --no-interactive # diagnostyka → YAML
97
+ fixos fix --yaml --no-interactive --disc # diagnostyka + dysk → YAML
98
+
95
99
  \b
96
100
  Opcje dyskowe:
97
101
  --disc – Analiza zajętości dysku + grupowanie przyczyn
98
102
  --dry-run – Symulacja bez wykonywania akcji
99
- --interactive – Tryb interaktywny (domyślnie włączony)
100
103
  --json – Wyjście w formacie JSON
104
+ --yaml – Wyjście w formacie YAML (pipe-safe)
101
105
  --llm-fallback – Użyj LLM gdy heurystyki nie wystarczą
102
106
 
103
107
  \b
@@ -107,11 +111,24 @@ def fix(provider, token, model, no_banner, mode, timeout, modules, no_show_data,
107
111
  fixos fix --disc --dry-run # analiza dysku bez wykonywania
108
112
  fixos fix --mode autonomous # tryb autonomiczny
109
113
  fixos fix --modules audio,thumbnails # tylko audio i thumbnails
114
+ fixos fix --yaml --no-interactive # pipeline mode → YAML
110
115
  fixos fix --provider openai --token sk-...
111
116
  """
117
+ fmt = OutputFormatter.from_flags(yaml_output=yaml_output, json_output=json_output)
118
+
112
119
  if not no_banner:
113
- click.echo(click.style(BANNER, fg="cyan"))
120
+ fmt.banner(BANNER)
121
+
122
+ # ── Pipeline mode: --yaml/--json + --no-interactive ──────
123
+ if fmt.is_machine and not interactive:
124
+ fmt.status("Pipeline mode: diagnostyka → structured output", fg="cyan")
125
+ data = _collect_diagnostics(modules, disc, fmt, output)
126
+ fmt.status("Diagnostyka gotowa.", fg="green")
127
+ content = fmt.format_diagnostics(data)
128
+ click.echo(content)
129
+ return
114
130
 
131
+ # ── Interactive / LLM mode ────────────────────────────────
115
132
  cfg = FixOsConfig.load(
116
133
  provider=provider,
117
134
  api_key=token,
@@ -138,15 +155,15 @@ def fix(provider, token, model, no_banner, mode, timeout, modules, no_show_data,
138
155
  click.echo(click.style(f"{err}", fg="red"))
139
156
  sys.exit(1)
140
157
 
141
- click.echo(click.style("\nKonfiguracja:", fg="cyan"))
158
+ fmt.status("\nKonfiguracja:", fg="cyan")
142
159
  click.echo(cfg.summary())
143
160
  if dry_run:
144
- click.echo(click.style(" Tryb: DRY-RUN (komendy nie będą wykonywane)", fg="yellow"))
161
+ fmt.status(" Tryb: DRY-RUN (komendy nie będą wykonywane)", fg="yellow")
145
162
  if disc:
146
- click.echo(click.style(" Analiza dysku: Włączona", fg="blue"))
163
+ fmt.status(" Analiza dysku: Włączona", fg="blue")
147
164
 
148
- data = _collect_diagnostics(modules, disc, json_output, output)
149
- click.echo(click.style("Diagnostyka gotowa.\n", fg="green"))
165
+ data = _collect_diagnostics(modules, disc, fmt, output)
166
+ fmt.status("Diagnostyka gotowa.\n", fg="green")
150
167
 
151
168
  if disc and "disk_analysis" in data:
152
169
  return handle_disk_cleanup_mode(data["disk_analysis"], cfg, dry_run, interactive, json_output, llm_fallback)