fixos 2.2.16__tar.gz → 2.2.18__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 (142) hide show
  1. {fixos-2.2.16 → fixos-2.2.18}/CHANGELOG.md +28 -0
  2. {fixos-2.2.16 → fixos-2.2.18}/PKG-INFO +5 -5
  3. {fixos-2.2.16 → fixos-2.2.18}/README.md +4 -4
  4. {fixos-2.2.16 → fixos-2.2.18}/fixos/__init__.py +1 -1
  5. {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/session_core.py +42 -5
  6. {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/session_handlers.py +40 -2
  7. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/ask_cmd.py +18 -14
  8. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/cleanup_cmd.py +106 -86
  9. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/features_cmd.py +33 -26
  10. {fixos-2.2.16 → fixos-2.2.18}/fixos/constants.py +4 -0
  11. {fixos-2.2.16 → fixos-2.2.18}/fixos/features/installer.py +17 -35
  12. {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/executor.py +19 -2
  13. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/security.py +48 -35
  14. {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/PKG-INFO +5 -5
  15. {fixos-2.2.16 → fixos-2.2.18}/pyproject.toml +1 -1
  16. {fixos-2.2.16 → fixos-2.2.18}/setup.py +1 -1
  17. {fixos-2.2.16 → fixos-2.2.18}/.env.example +0 -0
  18. {fixos-2.2.16 → fixos-2.2.18}/LICENSE +0 -0
  19. {fixos-2.2.16 → fixos-2.2.18}/MANIFEST.in +0 -0
  20. {fixos-2.2.16 → fixos-2.2.18}/docker/README.md +0 -0
  21. {fixos-2.2.16 → fixos-2.2.18}/docker/TEST_RESULTS.md +0 -0
  22. {fixos-2.2.16 → fixos-2.2.18}/docker/TEST_RESULTS_V2.md +0 -0
  23. {fixos-2.2.16 → fixos-2.2.18}/docker/alpine/Dockerfile +0 -0
  24. {fixos-2.2.16 → fixos-2.2.18}/docker/arch/Dockerfile +0 -0
  25. {fixos-2.2.16 → fixos-2.2.18}/docker/base/Dockerfile +0 -0
  26. {fixos-2.2.16 → fixos-2.2.18}/docker/broken-audio/Dockerfile +0 -0
  27. {fixos-2.2.16 → fixos-2.2.18}/docker/broken-full/Dockerfile +0 -0
  28. {fixos-2.2.16 → fixos-2.2.18}/docker/broken-network/Dockerfile +0 -0
  29. {fixos-2.2.16 → fixos-2.2.18}/docker/broken-thumbnails/Dockerfile +0 -0
  30. {fixos-2.2.16 → fixos-2.2.18}/docker/debian/Dockerfile +0 -0
  31. {fixos-2.2.16 → fixos-2.2.18}/docker/docker-compose.multi-system.yml +0 -0
  32. {fixos-2.2.16 → fixos-2.2.18}/docker/docker-compose.yml +0 -0
  33. {fixos-2.2.16 → fixos-2.2.18}/docker/fedora/Dockerfile +0 -0
  34. {fixos-2.2.16 → fixos-2.2.18}/docker/test-multi-system.sh +0 -0
  35. {fixos-2.2.16 → fixos-2.2.18}/docker/ubuntu/Dockerfile +0 -0
  36. {fixos-2.2.16 → fixos-2.2.18}/docs/examples/advanced_usage.py +0 -0
  37. {fixos-2.2.16 → fixos-2.2.18}/docs/examples/quickstart.py +0 -0
  38. {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/__init__.py +0 -0
  39. {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/autonomous.py +0 -0
  40. {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/autonomous_session.py +0 -0
  41. {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/hitl.py +0 -0
  42. {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/hitl_session.py +0 -0
  43. {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/session_io.py +0 -0
  44. {fixos-2.2.16 → fixos-2.2.18}/fixos/anonymizer.py +0 -0
  45. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/__init__.py +0 -0
  46. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/config_cmd.py +0 -0
  47. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/fix_cmd.py +0 -0
  48. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/history_cmd.py +0 -0
  49. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/main.py +0 -0
  50. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/orchestrate_cmd.py +0 -0
  51. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/profile_cmd.py +0 -0
  52. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/provider_cmd.py +0 -0
  53. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/quickfix_cmd.py +0 -0
  54. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/report_cmd.py +0 -0
  55. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/rollback_cmd.py +0 -0
  56. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/scan_cmd.py +0 -0
  57. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/shared.py +0 -0
  58. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/token_cmd.py +0 -0
  59. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/watch_cmd.py +0 -0
  60. {fixos-2.2.16 → fixos-2.2.18}/fixos/cli.py +0 -0
  61. {fixos-2.2.16 → fixos-2.2.18}/fixos/config.py +0 -0
  62. {fixos-2.2.16 → fixos-2.2.18}/fixos/config_interactive.py +0 -0
  63. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/__init__.py +0 -0
  64. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/__init__.py +0 -0
  65. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/_shared.py +0 -0
  66. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/audio.py +0 -0
  67. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/hardware.py +0 -0
  68. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/resources.py +0 -0
  69. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/security.py +0 -0
  70. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/system_core.py +0 -0
  71. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/thumbnails.py +0 -0
  72. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/dev_project_analyzer.py +0 -0
  73. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/disk_analyzer.py +0 -0
  74. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/flatpak_analyzer.py +0 -0
  75. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/service_cleanup.py +0 -0
  76. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/service_details.py +0 -0
  77. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/service_scanner.py +0 -0
  78. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/storage_analyzer.py +0 -0
  79. {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/system_checks.py +0 -0
  80. {fixos-2.2.16 → fixos-2.2.18}/fixos/features/__init__.py +0 -0
  81. {fixos-2.2.16 → fixos-2.2.18}/fixos/features/auditor.py +0 -0
  82. {fixos-2.2.16 → fixos-2.2.18}/fixos/features/catalog.py +0 -0
  83. {fixos-2.2.16 → fixos-2.2.18}/fixos/features/profiles.py +0 -0
  84. {fixos-2.2.16 → fixos-2.2.18}/fixos/features/renderer.py +0 -0
  85. {fixos-2.2.16 → fixos-2.2.18}/fixos/fixes/__init__.py +0 -0
  86. {fixos-2.2.16 → fixos-2.2.18}/fixos/interactive/__init__.py +0 -0
  87. {fixos-2.2.16 → fixos-2.2.18}/fixos/interactive/cleanup_planner.py +0 -0
  88. {fixos-2.2.16 → fixos-2.2.18}/fixos/llm_shell.py +0 -0
  89. {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/__init__.py +0 -0
  90. {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/graph.py +0 -0
  91. {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/orchestrator.py +0 -0
  92. {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/rollback.py +0 -0
  93. {fixos-2.2.16 → fixos-2.2.18}/fixos/platform_utils.py +0 -0
  94. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/__init__.py +0 -0
  95. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/base.py +0 -0
  96. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/__init__.py +0 -0
  97. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/audio.py +0 -0
  98. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/disk.py +0 -0
  99. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/hardware.py +0 -0
  100. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/resources.py +0 -0
  101. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/thumbnails.py +0 -0
  102. {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/registry.py +0 -0
  103. {fixos-2.2.16 → fixos-2.2.18}/fixos/profiles/__init__.py +0 -0
  104. {fixos-2.2.16 → fixos-2.2.18}/fixos/providers/__init__.py +0 -0
  105. {fixos-2.2.16 → fixos-2.2.18}/fixos/providers/llm.py +0 -0
  106. {fixos-2.2.16 → fixos-2.2.18}/fixos/providers/llm_analyzer.py +0 -0
  107. {fixos-2.2.16 → fixos-2.2.18}/fixos/providers/schemas.py +0 -0
  108. {fixos-2.2.16 → fixos-2.2.18}/fixos/system_checks.py +0 -0
  109. {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/__init__.py +0 -0
  110. {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/anonymizer.py +0 -0
  111. {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/terminal.py +0 -0
  112. {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/timeout.py +0 -0
  113. {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/web_search.py +0 -0
  114. {fixos-2.2.16 → fixos-2.2.18}/fixos/watch.py +0 -0
  115. {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/SOURCES.txt +0 -0
  116. {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/dependency_links.txt +0 -0
  117. {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/entry_points.txt +0 -0
  118. {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/requires.txt +0 -0
  119. {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/top_level.txt +0 -0
  120. {fixos-2.2.16 → fixos-2.2.18}/pytest.ini +0 -0
  121. {fixos-2.2.16 → fixos-2.2.18}/requirements-dev.txt +0 -0
  122. {fixos-2.2.16 → fixos-2.2.18}/requirements.txt +0 -0
  123. {fixos-2.2.16 → fixos-2.2.18}/scripts/pyqual-calibrate.py +0 -0
  124. {fixos-2.2.16 → fixos-2.2.18}/setup.cfg +0 -0
  125. {fixos-2.2.16 → fixos-2.2.18}/tests/__init__.py +0 -0
  126. {fixos-2.2.16 → fixos-2.2.18}/tests/conftest.py +0 -0
  127. {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/__init__.py +0 -0
  128. {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_anonymization_layers.py +0 -0
  129. {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_audio_broken.py +0 -0
  130. {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_cli.py +0 -0
  131. {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_executor.py +0 -0
  132. {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_multi_system.py +0 -0
  133. {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_network_broken.py +0 -0
  134. {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_thumbnails_broken.py +0 -0
  135. {fixos-2.2.16 → fixos-2.2.18}/tests/test_fixos.py +0 -0
  136. {fixos-2.2.16 → fixos-2.2.18}/tests/unit/__init__.py +0 -0
  137. {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_anonymizer.py +0 -0
  138. {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_core.py +0 -0
  139. {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_executor.py +0 -0
  140. {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_orchestrator.py +0 -0
  141. {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_service_cleanup.py +0 -0
  142. {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_service_scanner.py +0 -0
@@ -150,6 +150,34 @@ 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.18] - 2026-05-04
154
+
155
+ ### Docs
156
+ - Update README.md
157
+
158
+ ### Other
159
+ - Update uv.lock
160
+
161
+ ## [2.2.17] - 2026-05-04
162
+
163
+ ### Docs
164
+ - Update README.md
165
+ - Update project/README.md
166
+ - Update project/context.md
167
+
168
+ ### Other
169
+ - Update fixos/orchestrator/executor.py
170
+ - Update project/analysis.toon.yaml
171
+ - Update project/calls.mmd
172
+ - Update project/calls.png
173
+ - Update project/calls.toon.yaml
174
+ - Update project/calls.yaml
175
+ - Update project/compact_flow.mmd
176
+ - Update project/compact_flow.png
177
+ - Update project/duplication.toon.yaml
178
+ - Update project/evolution.toon.yaml
179
+ - ... and 8 more files
180
+
153
181
  ## [2.2.16] - 2026-05-04
154
182
 
155
183
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixos
3
- Version: 2.2.16
3
+ Version: 2.2.18
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.16-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-20.5h-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.18-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-22.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
68
68
 
69
- - 🤖 **LLM usage:** $7.5000 (113 commits)
70
- - 👤 **Human dev:** ~$2047 (20.5h @ $100/h, 30min dedup)
69
+ - 🤖 **LLM usage:** $7.5000 (117 commits)
70
+ - 👤 **Human dev:** ~$2210 (22.1h @ $100/h, 30min dedup)
71
71
 
72
72
  Generated on 2026-05-04 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.16-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-20.5h-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.18-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-22.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
24
24
 
25
- - 🤖 **LLM usage:** $7.5000 (113 commits)
26
- - 👤 **Human dev:** ~$2047 (20.5h @ $100/h, 30min dedup)
25
+ - 🤖 **LLM usage:** $7.5000 (117 commits)
26
+ - 👤 **Human dev:** ~$2210 (22.1h @ $100/h, 30min dedup)
27
27
 
28
28
  Generated on 2026-05-04 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
29
29
 
@@ -1,2 +1,2 @@
1
1
  """fixos – AI-powered Linux/Windows diagnostics and repair."""
2
- __version__ = "2.2.16"
2
+ __version__ = "2.2.18"
@@ -8,9 +8,7 @@ from dataclasses import dataclass, field
8
8
  from typing import List, Tuple
9
9
 
10
10
  from ..constants import (
11
- COMMAND_PREFIX_LENGTH,
12
- MAX_OUTPUT_LINES,
13
- CLEANUP_TIMEOUT_ESTIMATE,
11
+ MAX_SUMMARY_LENGTH,
14
12
  )
15
13
 
16
14
 
@@ -32,7 +30,7 @@ SYSTEM_PROMPT = """You are an expert Linux/Windows/macOS system diagnostics assi
32
30
  You receive anonymized diagnostic data OR a user-described problem. Your tasks:
33
31
 
34
32
  1. DIAGNOSE – identify ALL problems (🔴 critical → 🟡 important → 🟢 minor)
35
- 2. SOLUTIONS – for each problem provide a CONCRETE command with a brief explanation
33
+ 2. SOLUTIONS – for each problem provide an ACTIONABLE repair command with a brief explanation
36
34
  3. FORMAT – always use this exact format:
37
35
 
38
36
  ━━━ DIAGNOZA ━━━
@@ -44,6 +42,12 @@ You receive anonymized diagnostic data OR a user-described problem. Your tasks:
44
42
  **Komenda:** `command`
45
43
  **Co robi:** explanation
46
44
 
45
+ IMPORTANT RULES:
46
+ - Commands in the action list must change/fix something, not only inspect state.
47
+ - Do NOT use read-only diagnostics as fixes (e.g. `df -h`, `free -h`, `ls`, `cat`, `grep`, `systemctl status`).
48
+ - If needed, mention diagnostics in explanation, but propose executable repair steps in `Komenda`.
49
+ - For package upgrades and heavy operations, provide the real fix command (e.g. `dnf upgrade -y`).
50
+
47
51
  Always end with:
48
52
  ━━━ DOSTĘPNE AKCJE ━━━
49
53
  [1] Fix problem 1 – `command`
@@ -57,6 +61,36 @@ IMPORTANT: Adapt commands to the detected OS (Linux/Windows/macOS).
57
61
  """
58
62
 
59
63
 
64
+ def _is_diagnostic_only_command(cmd: str) -> bool:
65
+ """Return True if command is read-only and not a repair action."""
66
+ normalized = cmd.strip().lower()
67
+ if normalized.startswith("sudo "):
68
+ normalized = normalized[5:].strip()
69
+
70
+ diagnostic_prefixes = (
71
+ "df ",
72
+ "free ",
73
+ "ls ",
74
+ "cat ",
75
+ "grep ",
76
+ "find ",
77
+ "which ",
78
+ "whereis ",
79
+ "journalctl",
80
+ "dmesg",
81
+ "uptime",
82
+ "top ",
83
+ "ps ",
84
+ "systemctl status",
85
+ "dnf check-update",
86
+ "apt list --upgradable",
87
+ "pacman -qu",
88
+ "flatpak list",
89
+ "snap list",
90
+ )
91
+ return normalized.startswith(diagnostic_prefixes)
92
+
93
+
60
94
  def extract_fixes(reply: str) -> List[Tuple[str, str]]:
61
95
  """Extract (command, comment) pairs from LLM reply."""
62
96
  fixes: List[Tuple[str, str]] = []
@@ -76,7 +110,10 @@ def extract_fixes(reply: str) -> List[Tuple[str, str]]:
76
110
  if not fixes:
77
111
  for m in re.finditer(r"EXEC:\s*`([^`]+)`", reply, re.IGNORECASE):
78
112
  fixes.append((m.group(1).strip(), ""))
79
- return fixes
113
+
114
+ # Keep only actionable commands in menu; diagnostic-only commands are noise.
115
+ filtered = [(cmd, comment) for cmd, comment in fixes if not _is_diagnostic_only_command(cmd)]
116
+ return filtered
80
117
 
81
118
 
82
119
  def extract_search_topic(llm_reply: str) -> str:
@@ -3,9 +3,15 @@ Command handlers for HITL session.
3
3
  Each handler processes a specific user command type.
4
4
  """
5
5
 
6
+ import re
6
7
  from typing import TYPE_CHECKING, Tuple
7
8
 
8
- from ..constants import CLEANUP_TIMEOUT_ESTIMATE
9
+ from ..constants import (
10
+ CLEANUP_TIMEOUT_ESTIMATE,
11
+ DEFAULT_COMMAND_TIMEOUT,
12
+ FAST_COMMAND_TIMEOUT,
13
+ LONG_COMMAND_TIMEOUT,
14
+ )
9
15
  from ..platform_utils import (
10
16
  is_dangerous, elevate_cmd, run_command,
11
17
  )
@@ -19,6 +25,37 @@ if TYPE_CHECKING:
19
25
  from ..config import FixOsConfig
20
26
 
21
27
 
28
+ def _resolve_command_timeout(cmd: str) -> int:
29
+ """Choose timeout based on command type to avoid premature failures."""
30
+ normalized = cmd.strip().lower()
31
+
32
+ long_running_patterns = (
33
+ r"\bdnf\s+(update|upgrade|distro-sync|system-upgrade)\b",
34
+ r"\bapt(-get)?\s+(update|upgrade|dist-upgrade|full-upgrade)\b",
35
+ r"\bpacman\s+-S(y|yu|yyu)\b",
36
+ r"\bzypper\s+(refresh|update|dup)\b",
37
+ r"\bflatpak\s+update\b",
38
+ r"\bsnap\s+refresh\b",
39
+ )
40
+ if any(re.search(pattern, normalized) for pattern in long_running_patterns):
41
+ return LONG_COMMAND_TIMEOUT
42
+
43
+ fast_read_only_patterns = (
44
+ r"\bdf\b",
45
+ r"\bfree\b",
46
+ r"\bls\b",
47
+ r"\bcat\b",
48
+ r"\bgrep\b",
49
+ r"\bsystemctl\s+status\b",
50
+ )
51
+ if any(re.search(pattern, normalized) for pattern in fast_read_only_patterns):
52
+ return FAST_COMMAND_TIMEOUT
53
+
54
+ if CLEANUP_TIMEOUT_ESTIMATE > DEFAULT_COMMAND_TIMEOUT:
55
+ return CLEANUP_TIMEOUT_ESTIMATE
56
+ return DEFAULT_COMMAND_TIMEOUT
57
+
58
+
22
59
  def handle_quit() -> bool:
23
60
  """Handle [Q] Quit command. Returns False to exit loop."""
24
61
  io.print_session_ended()
@@ -167,8 +204,9 @@ def run_single_command(cmd: str, comment: str) -> CmdResult:
167
204
  return CmdResult(cmd=cmd, comment=comment, ok=False,
168
205
  stdout="", stderr="Pominięto.", returncode=-1, skipped=True)
169
206
 
207
+ timeout = _resolve_command_timeout(cmd)
170
208
  io.console.print(" [dim]⏳ Wykonuję...[/dim]", end="")
171
- ok, stdout, stderr, rc = run_command(cmd, timeout=CLEANUP_TIMEOUT_ESTIMATE)
209
+ ok, stdout, stderr, rc = run_command(cmd, timeout=timeout)
172
210
  io.console.print("\r" + " " * 30 + "\r", end="")
173
211
  result = CmdResult(cmd=cmd, comment=comment, ok=ok,
174
212
  stdout=stdout, stderr=stderr, returncode=rc)
@@ -28,10 +28,26 @@ _ACTION_KEYWORDS = {
28
28
  }
29
29
 
30
30
 
31
+ _OBJECT_KEYWORDS: list[tuple[list[str], tuple]] = [
32
+ (["docker", "kontener", "container"], ("docker", ["ps", "-aq"])),
33
+ (["audio", "dzwięk", "sound"], ("fixos", ["fix", "--modules", "audio"])),
34
+ (["siec", "network", "internet"], ("fixos", ["scan", "--modules", "system"])),
35
+ (["bezpieczenstwo", "security"], ("fixos", ["scan", "--modules", "security"])),
36
+ ]
37
+
38
+
39
+ def _object_based_match(prompt_lower: str):
40
+ """Fallback object-based matching when no action keyword is found."""
41
+ for keywords, cmd in _OBJECT_KEYWORDS:
42
+ if any(kw in prompt_lower for kw in keywords):
43
+ return cmd
44
+ return None
45
+
46
+
31
47
  def _match_heuristic_command(prompt_lower: str) -> object:
32
48
  """
33
49
  Match user prompt against heuristic keyword mappings.
34
-
50
+
35
51
  Returns:
36
52
  - str: Direct shell command
37
53
  - tuple: (program, args) for subprocess
@@ -41,21 +57,9 @@ def _match_heuristic_command(prompt_lower: str) -> object:
41
57
  if any(kw in prompt_lower for kw in keywords):
42
58
  if cmd is not None:
43
59
  return cmd
44
- # For "lista" - check if it's docker-related
45
60
  if "docker" in prompt_lower or "kontener" in prompt_lower:
46
61
  return ("docker", ["ps", "-a"])
47
-
48
- # Object-based matching (no action keyword found)
49
- if "docker" in prompt_lower or "kontener" in prompt_lower or "container" in prompt_lower:
50
- return ("docker", ["ps", "-aq"])
51
- elif "audio" in prompt_lower or "dzwięk" in prompt_lower or "sound" in prompt_lower:
52
- return ("fixos", ["fix", "--modules", "audio"])
53
- elif "siec" in prompt_lower or "network" in prompt_lower or "internet" in prompt_lower:
54
- return ("fixos", ["scan", "--modules", "system"])
55
- elif "bezpieczenstwo" in prompt_lower or "security" in prompt_lower:
56
- return ("fixos", ["scan", "--modules", "security"])
57
-
58
- return None
62
+ return _object_based_match(prompt_lower)
59
63
 
60
64
 
61
65
  def _format_command(matched_cmd) -> str:
@@ -21,6 +21,21 @@ CONSTANT_500 = 500
21
21
  CONSTANT_1024 = 1024
22
22
 
23
23
 
24
+ def _run_interactive_cleanup(plan: dict, list_only: bool, scanner) -> None:
25
+ """Offer interactive safe cleanup and display unsafe services."""
26
+ if not list_only and plan["safe_to_cleanup"]:
27
+ safe_total = sum(s['size_gb'] for s in plan["safe_to_cleanup"])
28
+ click.echo(click.style("Bezpieczne do wyczyszczenia:", fg="green"))
29
+ for svc in plan["safe_to_cleanup"]:
30
+ size_str = f"{svc['size_gb']:.2f} GB" if svc['size_gb'] >= 1 else f"{svc['size_mb']:.0f} MB"
31
+ click.echo(f" • {svc['name']}: {size_str}")
32
+ click.echo()
33
+ if click.confirm(f"Wyczyścić bezpieczne usługi? (zwolni {safe_total:.2f} GB)"):
34
+ _execute_safe_cleanup(plan["safe_to_cleanup"], scanner)
35
+ if plan["requires_review"] and not list_only:
36
+ _display_unsafe_services(plan["requires_review"])
37
+
38
+
24
39
  @click.command("cleanup")
25
40
  @click.option("--threshold", "-t", default=CONSTANT_500, type=int,
26
41
  help="Próg wielkości w MB (domyślnie 500MB)")
@@ -55,56 +70,35 @@ def cleanup_services(threshold, services, json_output, cleanup, dry_run, list_on
55
70
  fixos cleanup -c docker --dry-run # symulacja czyszczenia Dockera
56
71
  fixos cleanup --full # pełna analiza systemu
57
72
  """
58
- # Full system analysis
59
73
  if full_analysis:
60
74
  _cleanup_full_system(json_output, dry_run)
61
75
  return
62
-
76
+
63
77
  scanner = ServiceDataScanner(threshold_mb=threshold)
64
78
 
65
- # Handle single service cleanup
66
79
  if cleanup:
67
80
  if cleanup == "flatpak":
68
81
  _cleanup_flatpak_detailed(scanner, json_output, dry_run)
69
- return
70
-
71
- _cleanup_single_service(cleanup, scanner, json_output, dry_run)
82
+ else:
83
+ _cleanup_single_service(cleanup, scanner, json_output, dry_run)
72
84
  return
73
85
 
74
- # Parse services filter
75
86
  service_filter = services.split(",") if services else None
76
87
  plan = scanner.get_cleanup_plan(selected_services=service_filter)
77
88
 
78
- # JSON output mode
79
89
  if json_output:
80
90
  import json
81
91
  click.echo(json.dumps(plan, indent=2, default=str))
82
92
  return
83
93
 
84
- # Display results
85
94
  _display_cleanup_summary(plan, threshold)
86
-
87
95
  if plan["services_found"] == 0:
88
96
  return
89
97
 
90
98
  for svc in plan["services"]:
91
99
  _display_service_item(svc)
92
100
 
93
- # Interactive cleanup mode
94
- if not list_only and plan["safe_to_cleanup"]:
95
- safe_total = sum(s['size_gb'] for s in plan["safe_to_cleanup"])
96
- click.echo(click.style("Bezpieczne do wyczyszczenia:", fg="green"))
97
- for svc in plan["safe_to_cleanup"]:
98
- size_str = f"{svc['size_gb']:.2f} GB" if svc['size_gb'] >= 1 else f"{svc['size_mb']:.0f} MB"
99
- click.echo(f" • {svc['name']}: {size_str}")
100
-
101
- click.echo()
102
- if click.confirm(f"Wyczyścić bezpieczne usługi? (zwolni {safe_total:.2f} GB)"):
103
- _execute_safe_cleanup(plan["safe_to_cleanup"], scanner)
104
-
105
- # Show unsafe services
106
- if plan["requires_review"] and not list_only:
107
- _display_unsafe_services(plan["requires_review"])
101
+ _run_interactive_cleanup(plan, list_only, scanner)
108
102
 
109
103
 
110
104
  def _display_cleanup_summary(plan: dict, threshold: int) -> None:
@@ -647,19 +641,11 @@ def _snap_remove_packages(packages_to_remove: list) -> None:
647
641
  click.echo(click.style(f" ❌ Błąd: {e}", fg="red"))
648
642
 
649
643
 
650
- def _handle_snap_management(analyzer, dry_run: bool) -> None:
651
- """Handle Snap package management selection."""
652
- snap_packages = _snap_fetch_packages(analyzer)
644
+ _SNAP_CORE_PACKAGES = {'core', 'core18', 'core20', 'core22', 'snapd'}
653
645
 
654
- if not snap_packages:
655
- click.echo(click.style("\n❌ Brak zainstalowanych pakietów Snap lub snapd niedostępny.", fg="red"))
656
- return
657
-
658
- active_packages = [p for p in snap_packages if not p.get('disabled', False)]
659
- if not active_packages:
660
- click.echo(click.style("\n❌ Brak aktywnych pakietów Snap do odinstalowania.", fg="red"))
661
- return
662
646
 
647
+ def _snap_display_packages(active_packages: list) -> None:
648
+ """Print numbered list of active snap packages."""
663
649
  click.echo(f"\n{click.style('📦 ZAINSTALOWANE PAKIETY SNAP:', fg='magenta', bold=True)}")
664
650
  click.echo(click.style("Wybierz numery pakietów do odinstalowania", fg="white"))
665
651
  for i, pkg in enumerate(active_packages, 1):
@@ -667,30 +653,53 @@ def _handle_snap_management(analyzer, dry_run: bool) -> None:
667
653
  click.echo(f" [{i:3d}] {click.style(pkg['name'], fg='yellow')} (v{pkg['version']}, rev {pkg['rev']}): {size_str}")
668
654
  click.echo(f"\n 💰 Łącznie pakietów: {len(active_packages)}")
669
655
 
656
+
657
+ def _snap_select_packages(active_packages: list) -> list:
658
+ """Prompt user to select packages; return list to remove or empty."""
670
659
  nums = click.prompt(click.style("\nWybierz numery do odinstalowania (np. 1,3,5-10)", fg="cyan"), default="")
671
660
  if not nums.strip():
672
661
  click.echo(click.style("⏭️ Nie wybrano żadnych pakietów.", fg="yellow"))
673
- return
674
-
662
+ return []
675
663
  selected_indices = _parse_numeric_range_set(nums)
676
664
  packages_to_remove = [active_packages[i - 1] for i in selected_indices if 1 <= i <= len(active_packages)]
677
-
678
665
  if not packages_to_remove:
679
666
  click.echo(click.style("❌ Nie wybrano żadnych pakietów.", fg="red"))
680
- return
667
+ return packages_to_remove
681
668
 
682
- click.echo(f"\n{click.style('📦 WYBRANE PAKIETY DO ODINSTALOWANIA:', fg='red', bold=True)}")
683
- for i, pkg in enumerate(packages_to_remove, 1):
684
- click.echo(f" {i:3d}. {click.style(pkg['name'], fg='yellow')} (v{pkg['version']})")
685
669
 
686
- core_packages = ['core', 'core18', 'core20', 'core22', 'snapd']
687
- dangerous = [p for p in packages_to_remove if p['name'] in core_packages]
670
+ def _snap_warn_dangerous(packages_to_remove: list) -> None:
671
+ """Warn if any selected package is a core snap component."""
672
+ dangerous = [p for p in packages_to_remove if p['name'] in _SNAP_CORE_PACKAGES]
688
673
  if dangerous:
689
674
  click.echo(click.style("\n⚠️ UWAGA: Wybrane pakiety zawierają komponenty systemowe:", fg="red", bold=True))
690
675
  for pkg in dangerous:
691
676
  click.echo(f" • {pkg['name']}")
692
677
  click.echo(click.style("Ich usunięcie może wpłynąć na działanie innych pakietów Snap!", fg="red"))
693
678
 
679
+
680
+ def _handle_snap_management(analyzer, dry_run: bool) -> None:
681
+ """Handle Snap package management selection."""
682
+ snap_packages = _snap_fetch_packages(analyzer)
683
+ if not snap_packages:
684
+ click.echo(click.style("\n❌ Brak zainstalowanych pakietów Snap lub snapd niedostępny.", fg="red"))
685
+ return
686
+
687
+ active_packages = [p for p in snap_packages if not p.get('disabled', False)]
688
+ if not active_packages:
689
+ click.echo(click.style("\n❌ Brak aktywnych pakietów Snap do odinstalowania.", fg="red"))
690
+ return
691
+
692
+ _snap_display_packages(active_packages)
693
+ packages_to_remove = _snap_select_packages(active_packages)
694
+ if not packages_to_remove:
695
+ return
696
+
697
+ click.echo(f"\n{click.style('📦 WYBRANE PAKIETY DO ODINSTALOWANIA:', fg='red', bold=True)}")
698
+ for i, pkg in enumerate(packages_to_remove, 1):
699
+ click.echo(f" {i:3d}. {click.style(pkg['name'], fg='yellow')} (v{pkg['version']})")
700
+
701
+ _snap_warn_dangerous(packages_to_remove)
702
+
694
703
  if dry_run:
695
704
  click.echo(click.style("\n[DRY-RUN] - symulacja, nic nie zostanie usunięte", fg="cyan"))
696
705
  return
@@ -1049,32 +1058,37 @@ def _filter_by_prefix_type(selection: str, analyzer) -> list:
1049
1058
  return items
1050
1059
 
1051
1060
 
1061
+ def _filter_large(analyzer, threshold_bytes: int, label: str) -> list:
1062
+ """Filter items above threshold_bytes and print summary."""
1063
+ items = [item for item in analyzer.items if item.size_bytes > threshold_bytes]
1064
+ click.echo(click.style(f"\n🔴 {label}: {len(items)} sztuk, {_format_bytes(sum(i.size_bytes for i in items))}", fg="red"))
1065
+ return items
1066
+
1067
+
1068
+ _FILTER_PREFIX_DISPATCH = {
1069
+ 'top:': _filter_by_prefix_top,
1070
+ 'category:': _filter_by_prefix_category,
1071
+ 'type:': _filter_by_prefix_type,
1072
+ }
1073
+
1074
+
1052
1075
  def _select_cleanup_items_by_filter(selection: str, analyzer, safe_items: list) -> list:
1053
1076
  """Map a filter keyword to a list of items from the analyzer."""
1054
- # Simple keyword dispatch
1055
1077
  if selection == 'safe':
1056
1078
  return safe_items
1057
1079
  if selection == 'all':
1058
1080
  return analyzer.items
1059
1081
  if selection == 'large':
1060
- items = [item for item in analyzer.items if item.size_bytes > CONSTANT_1024 ** CONSTANT_3]
1061
- click.echo(click.style(f"\n🔴 Duże elementy (>1 GB): {len(items)} sztuk, {_format_bytes(sum(i.size_bytes for i in items))}", fg="red"))
1062
- return items
1082
+ return _filter_large(analyzer, CONSTANT_1024 ** CONSTANT_3, "Duże elementy (>1 GB)")
1063
1083
  if selection == 'huge':
1064
- items = [item for item in analyzer.items if item.size_bytes > CONSTANT_5 * CONSTANT_1024 ** CONSTANT_3]
1065
- click.echo(click.style(f"\n🔴 Bardzo duże elementy (>5 GB): {len(items)} sztuk, {_format_bytes(sum(i.size_bytes for i in items))}", fg="red"))
1066
- return items
1084
+ return _filter_large(analyzer, CONSTANT_5 * CONSTANT_1024 ** CONSTANT_3, "Bardzo duże elementy (>5 GB)")
1067
1085
  if selection == 'old':
1068
1086
  return _filter_by_age(analyzer, CONSTANT_30, "Stare elementy")
1069
1087
  if selection == 'stale':
1070
1088
  return _filter_by_age(analyzer, CONSTANT_90, "Bardzo stare elementy")
1071
- # Prefix dispatch
1072
- if selection.startswith('top:'):
1073
- return _filter_by_prefix_top(selection, analyzer)
1074
- if selection.startswith('category:'):
1075
- return _filter_by_prefix_category(selection, analyzer)
1076
- if selection.startswith('type:'):
1077
- return _filter_by_prefix_type(selection, analyzer)
1089
+ for prefix, handler in _FILTER_PREFIX_DISPATCH.items():
1090
+ if selection.startswith(prefix):
1091
+ return handler(selection, analyzer)
1078
1092
  return []
1079
1093
 
1080
1094
 
@@ -1125,6 +1139,38 @@ def _execute_full_cleanup(items_to_clean: list, dry_run: bool) -> None:
1125
1139
  click.echo(click.style("="*CONSTANT_60 + "\n", fg="cyan"))
1126
1140
 
1127
1141
 
1142
+ def _show_dep_types(analyzer) -> None:
1143
+ """Display detailed breakdown of dev project dependency types."""
1144
+ dev_items = [item for item in analyzer.items if item.category == 'dev_projects']
1145
+ dep_types = _build_dep_types(dev_items)
1146
+ click.echo(f"\n{click.style('📦 SZCZEGÓŁY TYPÓW:', fg='magenta', bold=True)}")
1147
+ for dep_type, data in sorted(dep_types.items(), key=lambda x: -x[1]["total"]):
1148
+ click.echo(f"\n{click.style(dep_type, fg='yellow', bold=True)}: {len(data['items'])} folderów, {_format_bytes(data['total'])}")
1149
+ for item in data["items"][:CONSTANT_5]:
1150
+ click.echo(f" • {item.path}: {_format_bytes(item.size_bytes)}")
1151
+ if len(data['items']) > CONSTANT_5:
1152
+ click.echo(f" ... i {len(data['items']) - CONSTANT_5} więcej")
1153
+
1154
+
1155
+ def _dispatch_system_selection(selection: str, analyzer, safe_items: list, dry_run: bool) -> None:
1156
+ """Route a system menu selection to the appropriate handler."""
1157
+ if selection == 'types':
1158
+ _show_dep_types(analyzer)
1159
+ return
1160
+ if selection == 'snap':
1161
+ _handle_snap_management(analyzer, dry_run)
1162
+ return
1163
+ if selection == 'home':
1164
+ _handle_home_analysis(analyzer, dry_run)
1165
+ return
1166
+ if selection == 'select':
1167
+ items_to_clean = _handle_interactive_select(analyzer, dry_run)
1168
+ else:
1169
+ items_to_clean = _select_cleanup_items_by_filter(selection, analyzer, safe_items)
1170
+ if items_to_clean:
1171
+ _execute_full_cleanup(items_to_clean, dry_run)
1172
+
1173
+
1128
1174
  def _cleanup_full_system(json_output: bool, dry_run: bool) -> None:
1129
1175
  """
1130
1176
  Full system storage analysis and cleanup.
@@ -1155,33 +1201,7 @@ def _cleanup_full_system(json_output: bool, dry_run: bool) -> None:
1155
1201
  click.echo(click.style("\n⏭️ Nie wybrano żadnych akcji.", fg="yellow"))
1156
1202
  return
1157
1203
 
1158
- if selection == 'types':
1159
- dev_items = [item for item in analyzer.items if item.category == 'dev_projects']
1160
- dep_types = _build_dep_types(dev_items)
1161
- click.echo(f"\n{click.style('📦 SZCZEGÓŁY TYPÓW:', fg='magenta', bold=True)}")
1162
- for dep_type, data in sorted(dep_types.items(), key=lambda x: -x[1]["total"]):
1163
- click.echo(f"\n{click.style(dep_type, fg='yellow', bold=True)}: {len(data['items'])} folderów, {_format_bytes(data['total'])}")
1164
- for item in data["items"][:CONSTANT_5]:
1165
- click.echo(f" • {item.path}: {_format_bytes(item.size_bytes)}")
1166
- if len(data['items']) > CONSTANT_5:
1167
- click.echo(f" ... i {len(data['items']) - CONSTANT_5} więcej")
1168
- return
1169
-
1170
- if selection == 'snap':
1171
- _handle_snap_management(analyzer, dry_run)
1172
- return
1173
-
1174
- if selection == 'home':
1175
- _handle_home_analysis(analyzer, dry_run)
1176
- return
1177
-
1178
- if selection == 'select':
1179
- items_to_clean = _handle_interactive_select(analyzer, dry_run)
1180
- else:
1181
- items_to_clean = _select_cleanup_items_by_filter(selection, analyzer, safe_items)
1182
-
1183
- if items_to_clean:
1184
- _execute_full_cleanup(items_to_clean, dry_run)
1204
+ _dispatch_system_selection(selection, analyzer, safe_items, dry_run)
1185
1205
 
1186
1206
 
1187
1207
  def _parse_size_to_gb(size_str: str) -> float:
@@ -50,6 +50,36 @@ def features_audit(profile: Optional[str], json_output: bool) -> None:
50
50
  FeatureRenderer.render_audit(result)
51
51
 
52
52
 
53
+ def _show_install_plan(to_install: list, yes: bool, dry_run: bool) -> bool:
54
+ """Display install plan and ask for confirmation. Returns False to abort."""
55
+ console.print()
56
+ console.print(f"[bold]Zostaną zainstalowane {len(to_install)} pakiety:[/bold]")
57
+ for pkg in to_install[:10]:
58
+ console.print(f" • {pkg.id} - {pkg.description[:40]}")
59
+ if len(to_install) > 10:
60
+ console.print(f" ... i {len(to_install) - 10} więcej")
61
+ console.print()
62
+ if not yes and not dry_run:
63
+ if not click.confirm("Kontynuować instalację?"):
64
+ console.print("[yellow]Anulowano.[/yellow]")
65
+ return False
66
+ return True
67
+
68
+
69
+ def _show_install_results(install_result: dict) -> None:
70
+ """Display installation results summary."""
71
+ console.print()
72
+ if install_result["installed"]:
73
+ console.print(f"[green]✅ Zainstalowano: {len(install_result['installed'])}[/green]")
74
+ if install_result["failed"]:
75
+ console.print(f"[red]❌ Nie udało się: {len(install_result['failed'])}[/red]")
76
+ for pkg_id in install_result["failed"][:5]:
77
+ console.print(f" - {pkg_id}")
78
+ if install_result["skipped"]:
79
+ console.print(f"[yellow]⏭️ Pominięto: {len(install_result['skipped'])}[/yellow]")
80
+ console.print()
81
+
82
+
53
83
  @features.command("install")
54
84
  @click.option("--profile", "-p", required=True, help="Profil użytkownika")
55
85
  @click.option("--dry-run", is_flag=True, help="Symulacja bez instalacji")
@@ -78,34 +108,11 @@ def features_install(profile: str, dry_run: bool, yes: bool, category: tuple) ->
78
108
  console.print("[green]✅ Wszystkie pakiety z profilu są zainstalowane![/green]")
79
109
  return
80
110
 
81
- # Show what will be installed
82
- console.print()
83
- console.print(f"[bold]Zostaną zainstalowane {len(to_install)} pakiety:[/bold]")
84
- for pkg in to_install[:10]:
85
- console.print(f" • {pkg.id} - {pkg.description[:40]}")
86
- if len(to_install) > 10:
87
- console.print(f" ... i {len(to_install) - 10} więcej")
88
- console.print()
89
-
90
- if not yes and not dry_run:
91
- if not click.confirm("Kontynuować instalację?"):
92
- console.print("[yellow]Anulowano.[/yellow]")
93
- return
111
+ if not _show_install_plan(to_install, yes, dry_run):
112
+ return
94
113
 
95
114
  installer = FeatureInstaller(system, dry_run=dry_run)
96
- install_result = installer.install(to_install)
97
-
98
- # Show results
99
- console.print()
100
- if install_result["installed"]:
101
- console.print(f"[green]✅ Zainstalowano: {len(install_result['installed'])}[/green]")
102
- if install_result["failed"]:
103
- console.print(f"[red]❌ Nie udało się: {len(install_result['failed'])}[/red]")
104
- for pkg_id in install_result["failed"][:5]:
105
- console.print(f" - {pkg_id}")
106
- if install_result["skipped"]:
107
- console.print(f"[yellow]⏭️ Pominięto: {len(install_result['skipped'])}[/yellow]")
108
- console.print()
115
+ _show_install_results(installer.install(to_install))
109
116
 
110
117
 
111
118
  @features.command("profiles")
@@ -7,6 +7,9 @@ DEFAULT_SESSION_TIMEOUT = 300 # 5 minutes
7
7
  HITL_TIMEOUT_BUFFER = 80 # Buffer time for HITL session (seconds)
8
8
  AUTONOMOU_TIMEOUT_BUFFER = 40 # Buffer time for autonomous session
9
9
  CLEANUP_TIMEOUT_ESTIMATE = 120 # Estimated cleanup timeout
10
+ DEFAULT_COMMAND_TIMEOUT = 300 # Default command timeout in HITL session
11
+ LONG_COMMAND_TIMEOUT = 1800 # Long-running operations like system updates
12
+ FAST_COMMAND_TIMEOUT = 60 # Read-only and quick diagnostic commands
10
13
 
11
14
  # String lengths / limits
12
15
  MAX_COMMAND_LENGTH = 200 # Maximum length for command display
@@ -25,3 +28,4 @@ DEFAULT_HISTORY_LIMIT = 20 # Default history items to show
25
28
 
26
29
  # Text formatting
27
30
  COMMAND_PREFIX_LENGTH = 80 # Max length for command prefix in output
31
+ MAX_SUMMARY_LENGTH = 200 # Max summary chars for derived search topic