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.
- {fixos-2.2.16 → fixos-2.2.18}/CHANGELOG.md +28 -0
- {fixos-2.2.16 → fixos-2.2.18}/PKG-INFO +5 -5
- {fixos-2.2.16 → fixos-2.2.18}/README.md +4 -4
- {fixos-2.2.16 → fixos-2.2.18}/fixos/__init__.py +1 -1
- {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/session_core.py +42 -5
- {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/session_handlers.py +40 -2
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/ask_cmd.py +18 -14
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/cleanup_cmd.py +106 -86
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/features_cmd.py +33 -26
- {fixos-2.2.16 → fixos-2.2.18}/fixos/constants.py +4 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/features/installer.py +17 -35
- {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/executor.py +19 -2
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/security.py +48 -35
- {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/PKG-INFO +5 -5
- {fixos-2.2.16 → fixos-2.2.18}/pyproject.toml +1 -1
- {fixos-2.2.16 → fixos-2.2.18}/setup.py +1 -1
- {fixos-2.2.16 → fixos-2.2.18}/.env.example +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/LICENSE +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/MANIFEST.in +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/README.md +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/TEST_RESULTS.md +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/TEST_RESULTS_V2.md +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/alpine/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/arch/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/base/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/broken-audio/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/broken-full/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/broken-network/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/broken-thumbnails/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/debian/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/docker-compose.multi-system.yml +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/docker-compose.yml +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/fedora/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/test-multi-system.sh +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docker/ubuntu/Dockerfile +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docs/examples/advanced_usage.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/docs/examples/quickstart.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/autonomous.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/autonomous_session.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/hitl.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/hitl_session.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/agent/session_io.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/anonymizer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/config_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/fix_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/history_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/main.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/orchestrate_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/profile_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/provider_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/quickfix_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/report_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/rollback_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/scan_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/shared.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/token_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli/watch_cmd.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/cli.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/config.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/config_interactive.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/_shared.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/audio.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/hardware.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/resources.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/security.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/system_core.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/checks/thumbnails.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/dev_project_analyzer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/disk_analyzer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/flatpak_analyzer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/service_cleanup.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/service_details.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/service_scanner.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/storage_analyzer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/diagnostics/system_checks.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/features/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/features/auditor.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/features/catalog.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/features/profiles.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/features/renderer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/fixes/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/interactive/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/interactive/cleanup_planner.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/llm_shell.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/graph.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/orchestrator.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/orchestrator/rollback.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/platform_utils.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/base.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/audio.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/disk.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/hardware.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/resources.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/builtin/thumbnails.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/plugins/registry.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/profiles/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/providers/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/providers/llm.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/providers/llm_analyzer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/providers/schemas.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/system_checks.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/anonymizer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/terminal.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/timeout.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/utils/web_search.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos/watch.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/SOURCES.txt +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/dependency_links.txt +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/entry_points.txt +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/requires.txt +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/fixos.egg-info/top_level.txt +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/pytest.ini +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/requirements-dev.txt +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/requirements.txt +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/scripts/pyqual-calibrate.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/setup.cfg +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/conftest.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_anonymization_layers.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_audio_broken.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_cli.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_executor.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_multi_system.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_network_broken.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/e2e/test_thumbnails_broken.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/test_fixos.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/unit/__init__.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_anonymizer.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_core.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_executor.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_orchestrator.py +0 -0
- {fixos-2.2.16 → fixos-2.2.18}/tests/unit/test_service_cleanup.py +0 -0
- {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.
|
|
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
|
-
     
|
|
67
|
+
  
|
|
68
68
|
|
|
69
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
70
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
     
|
|
23
|
+
  
|
|
24
24
|
|
|
25
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
26
|
-
- 👤 **Human dev:** ~$
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|