homeconsole-cli 0.0.4__tar.gz → 0.0.6__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.
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/PKG-INFO +1 -1
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/core.py +3 -19
- homeconsole_cli-0.0.6/hc/commands/doctor.py +128 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/env.py +235 -0
- homeconsole_cli-0.0.6/hc/commands/status.py +111 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/main.py +4 -1
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/repl.py +9 -0
- homeconsole_cli-0.0.6/hc/update_check.py +72 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/homeconsole_cli.egg-info/PKG-INFO +1 -1
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/homeconsole_cli.egg-info/SOURCES.txt +2 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/pyproject.toml +1 -1
- homeconsole_cli-0.0.4/hc/commands/status.py +0 -64
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/README.md +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/__init__.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/api.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/capabilities.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/client.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/__init__.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/_client_helpers.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/_compose_helpers.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/auth.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/connect.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/deploy.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/install.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/logs.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/marketplace.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/module.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/ping.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/plugin.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/recovery/__init__.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/recovery/compose.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/recovery/config.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/recovery/core.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/recovery/db.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/recovery/mode.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/recovery/redis.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/recovery/ui.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/remove.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/reset.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/search.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/secrets.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/setup.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/setup_wizard.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/commands/update.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/config.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/constants.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/core_ops.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/core_source.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/env_bootstrap.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/errors.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/marketplace_operation.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/native_core.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/setup_runner.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/hc/shell.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/homeconsole_cli.egg-info/dependency_links.txt +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/homeconsole_cli.egg-info/entry_points.txt +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/homeconsole_cli.egg-info/requires.txt +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/homeconsole_cli.egg-info/top_level.txt +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/setup.cfg +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/tests/test_config_roundtrip.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/tests/test_core_ops.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/tests/test_env_bootstrap.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/tests/test_main_root_callback.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/tests/test_marketplace_operation_parse.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/tests/test_native_core_helpers.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/tests/test_nav_cli.py +0 -0
- {homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/tests/test_setup_runner.py +0 -0
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
|
|
7
5
|
import typer
|
|
@@ -72,9 +70,9 @@ def register(app: typer.Typer) -> None:
|
|
|
72
70
|
src = init_core_source(console, repo_url=repo, ref=ref)
|
|
73
71
|
console.print(f"[green]✓[/green] Core исходники готовы: {src.path}")
|
|
74
72
|
|
|
75
|
-
@core_app.command("
|
|
76
|
-
def
|
|
77
|
-
"""Обновить локальную копию исходников Core (git pull).
|
|
73
|
+
@core_app.command("update")
|
|
74
|
+
def core_update() -> None:
|
|
75
|
+
"""Обновить локальную копию исходников Core (git pull --ff-only)."""
|
|
78
76
|
console = Console()
|
|
79
77
|
src = update_core_source(console)
|
|
80
78
|
console.print(f"[green]✓[/green] Обновлено: {src.path}")
|
|
@@ -259,19 +257,5 @@ def register(app: typer.Typer) -> None:
|
|
|
259
257
|
raise typer.Exit(code=1)
|
|
260
258
|
_docker_logs(console, follow=follow, tail=tail)
|
|
261
259
|
|
|
262
|
-
@core_app.command("update")
|
|
263
|
-
def core_runtime_update(
|
|
264
|
-
tag: str = typer.Option("latest", "--tag", help="Тег образа"),
|
|
265
|
-
wait: bool = typer.Option(True, "--wait/--no-wait"),
|
|
266
|
-
quiet: bool = typer.Option(False, "--quiet"),
|
|
267
|
-
) -> None:
|
|
268
|
-
"""Обновить core-runtime до образа (алиас для `hc update core`)."""
|
|
269
|
-
cmd = [sys.executable, "-m", "hc.main", "update", "core", "--tag", tag]
|
|
270
|
-
if not wait:
|
|
271
|
-
cmd.append("--no-wait")
|
|
272
|
-
if quiet:
|
|
273
|
-
cmd.append("--quiet")
|
|
274
|
-
raise SystemExit(subprocess.run(cmd, check=False).returncode)
|
|
275
|
-
|
|
276
260
|
app.add_typer(core_app, name="core")
|
|
277
261
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from hc.config import Config
|
|
13
|
+
from hc.constants import CONFIG_PATH, CORE_SRC_DIR
|
|
14
|
+
from hc.core_source import COMPOSE_MODES
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(app: typer.Typer) -> None:
|
|
18
|
+
@app.command("doctor")
|
|
19
|
+
def doctor() -> None:
|
|
20
|
+
"""Диагностика системы: Docker, конфиг, исходники, порты, диск."""
|
|
21
|
+
console = Console()
|
|
22
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
23
|
+
table.add_column(min_width=28)
|
|
24
|
+
table.add_column()
|
|
25
|
+
table.add_column(style="dim")
|
|
26
|
+
|
|
27
|
+
ok = "[green]✓[/green]"
|
|
28
|
+
warn = "[yellow]![/yellow]"
|
|
29
|
+
fail = "[red]✗[/red]"
|
|
30
|
+
issues: list[str] = []
|
|
31
|
+
|
|
32
|
+
def row(label: str, icon: str, detail: str = "") -> None:
|
|
33
|
+
table.add_row(label, icon, detail)
|
|
34
|
+
|
|
35
|
+
# ── Docker ────────────────────────────────────────────────────
|
|
36
|
+
docker_bin = shutil.which("docker")
|
|
37
|
+
if docker_bin:
|
|
38
|
+
ver = subprocess.run( # noqa: S603
|
|
39
|
+
["docker", "version", "--format", "{{.Server.Version}}"],
|
|
40
|
+
capture_output=True, text=True, check=False, timeout=5,
|
|
41
|
+
)
|
|
42
|
+
docker_ver = ver.stdout.strip() or "?"
|
|
43
|
+
row("Docker", ok, f"v{docker_ver} ({docker_bin})")
|
|
44
|
+
else:
|
|
45
|
+
row("Docker", fail, "не найден — установи Docker или OrbStack")
|
|
46
|
+
issues.append("Docker не найден")
|
|
47
|
+
|
|
48
|
+
compose_ver = subprocess.run( # noqa: S603
|
|
49
|
+
["docker", "compose", "version", "--short"],
|
|
50
|
+
capture_output=True, text=True, check=False, timeout=5,
|
|
51
|
+
)
|
|
52
|
+
if compose_ver.returncode == 0:
|
|
53
|
+
row("Docker Compose", ok, compose_ver.stdout.strip())
|
|
54
|
+
else:
|
|
55
|
+
row("Docker Compose", warn, "не определена версия")
|
|
56
|
+
|
|
57
|
+
git_bin = shutil.which("git")
|
|
58
|
+
if git_bin:
|
|
59
|
+
git_ver = subprocess.run( # noqa: S603
|
|
60
|
+
["git", "--version"], capture_output=True, text=True, check=False
|
|
61
|
+
)
|
|
62
|
+
row("git", ok, git_ver.stdout.strip())
|
|
63
|
+
else:
|
|
64
|
+
row("git", warn, "не найден — нужен для hc core init/update")
|
|
65
|
+
|
|
66
|
+
# ── Config ────────────────────────────────────────────────────
|
|
67
|
+
table.add_row("")
|
|
68
|
+
if CONFIG_PATH.exists():
|
|
69
|
+
cfg = Config.load()
|
|
70
|
+
host_ok = bool(cfg.core.host.strip())
|
|
71
|
+
token_ok = bool(cfg.core.token.strip())
|
|
72
|
+
row("Конфиг (~/.config/hc)", ok, str(CONFIG_PATH))
|
|
73
|
+
row(" Core host", ok if host_ok else warn,
|
|
74
|
+
f"{cfg.core.host}:{cfg.core.port}" if host_ok else "не задан")
|
|
75
|
+
row(" Token", ok if token_ok else warn,
|
|
76
|
+
"задан" if token_ok else "не задан — запусти hc connect")
|
|
77
|
+
else:
|
|
78
|
+
row("Конфиг (~/.config/hc)", warn, "не найден — запусти hc connect или hc setup")
|
|
79
|
+
issues.append("Конфиг не найден")
|
|
80
|
+
|
|
81
|
+
# ── Core sources ──────────────────────────────────────────────
|
|
82
|
+
table.add_row("")
|
|
83
|
+
if CORE_SRC_DIR.exists():
|
|
84
|
+
row("Core исходники", ok, str(CORE_SRC_DIR))
|
|
85
|
+
for mode, rel in COMPOSE_MODES.items():
|
|
86
|
+
cf = CORE_SRC_DIR / rel
|
|
87
|
+
row(f" compose [{mode}]", ok if cf.exists() else warn,
|
|
88
|
+
rel if cf.exists() else f"{rel} ← не найден")
|
|
89
|
+
env_file = CORE_SRC_DIR / ".env"
|
|
90
|
+
row(" .env", ok if env_file.exists() else warn,
|
|
91
|
+
".env готов" if env_file.exists() else "нет — будет создан при hc env up")
|
|
92
|
+
else:
|
|
93
|
+
row("Core исходники", warn, f"не найдены — запусти hc core init")
|
|
94
|
+
issues.append("Core исходники не найдены")
|
|
95
|
+
|
|
96
|
+
# ── Ports ─────────────────────────────────────────────────────
|
|
97
|
+
table.add_row("")
|
|
98
|
+
CHECK_PORTS = {18080: "UI (caddy)", 18000: "Core API", 5432: "PostgreSQL", 6379: "Redis"}
|
|
99
|
+
for port, label in CHECK_PORTS.items():
|
|
100
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
101
|
+
s.settimeout(0.3)
|
|
102
|
+
in_use = s.connect_ex(("127.0.0.1", port)) == 0
|
|
103
|
+
icon = ok if in_use else "[dim]—[/dim]"
|
|
104
|
+
row(f" :{port} {label}", icon, "listening" if in_use else "free")
|
|
105
|
+
|
|
106
|
+
# ── Disk ─────────────────────────────────────────────────────
|
|
107
|
+
table.add_row("")
|
|
108
|
+
try:
|
|
109
|
+
stat = shutil.disk_usage(Path.home())
|
|
110
|
+
free_gb = stat.free / 1024 ** 3
|
|
111
|
+
total_gb = stat.total / 1024 ** 3
|
|
112
|
+
used_pct = (stat.used / stat.total) * 100
|
|
113
|
+
disk_color = "red" if used_pct > 90 else "yellow" if used_pct > 75 else "green"
|
|
114
|
+
row("Диск (home)", f"[{disk_color}]{'!' if used_pct > 75 else '✓'}[/{disk_color}]",
|
|
115
|
+
f"{free_gb:.1f} GB свободно из {total_gb:.1f} GB ({used_pct:.0f}% занято)")
|
|
116
|
+
if used_pct > 90:
|
|
117
|
+
issues.append(f"Диск почти полон ({used_pct:.0f}%)")
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
console.print(table)
|
|
122
|
+
|
|
123
|
+
if issues:
|
|
124
|
+
console.print()
|
|
125
|
+
for issue in issues:
|
|
126
|
+
console.print(f"[yellow]![/yellow] {issue}")
|
|
127
|
+
else:
|
|
128
|
+
console.print("\n[green]✓ Всё в порядке[/green]")
|
|
@@ -168,6 +168,32 @@ def _run(cmd: list[str], *, cwd: Path | None = None, extra_env: dict[str, str] |
|
|
|
168
168
|
raise typer.Exit(code=p.returncode)
|
|
169
169
|
|
|
170
170
|
|
|
171
|
+
def _try_pull_source(src: CoreSource, console: Console) -> None:
|
|
172
|
+
"""git pull --ff-only if the working tree is clean. Silently skips on dirty tree or any error."""
|
|
173
|
+
try:
|
|
174
|
+
status = subprocess.run( # noqa: S603
|
|
175
|
+
["git", "status", "--porcelain"],
|
|
176
|
+
cwd=str(src.path),
|
|
177
|
+
capture_output=True,
|
|
178
|
+
text=True,
|
|
179
|
+
timeout=5,
|
|
180
|
+
)
|
|
181
|
+
if status.returncode != 0 or status.stdout.strip():
|
|
182
|
+
return # dirty or not a git repo — skip
|
|
183
|
+
|
|
184
|
+
pull = subprocess.run( # noqa: S603
|
|
185
|
+
["git", "pull", "--ff-only", "--quiet"],
|
|
186
|
+
cwd=str(src.path),
|
|
187
|
+
capture_output=True,
|
|
188
|
+
text=True,
|
|
189
|
+
timeout=15,
|
|
190
|
+
)
|
|
191
|
+
if pull.returncode == 0 and pull.stdout.strip():
|
|
192
|
+
console.print(f"[dim]↑ core-runtime-service обновлён[/dim]")
|
|
193
|
+
except Exception: # noqa: BLE001
|
|
194
|
+
pass # network unavailable, timeout, etc. — not critical
|
|
195
|
+
|
|
196
|
+
|
|
171
197
|
def _get_running_services(compose_file: Path, cwd: Path) -> set[str]:
|
|
172
198
|
try:
|
|
173
199
|
r = subprocess.run( # noqa: S603
|
|
@@ -424,6 +450,7 @@ def register(app: typer.Typer) -> None:
|
|
|
424
450
|
raise typer.Exit(code=2)
|
|
425
451
|
|
|
426
452
|
src = _resolve_source(console)
|
|
453
|
+
_try_pull_source(src, console)
|
|
427
454
|
project = compose_project_from_source(console, src, mode=mode)
|
|
428
455
|
running = _get_running_services(project.compose_file, project.cwd)
|
|
429
456
|
|
|
@@ -592,6 +619,59 @@ def register(app: typer.Typer) -> None:
|
|
|
592
619
|
console.print(f"[dim]Подсказка:[/dim] {e.hint}")
|
|
593
620
|
raise typer.Exit(code=int(e.exit_code or 1))
|
|
594
621
|
|
|
622
|
+
@env_app.command("rebuild")
|
|
623
|
+
def env_rebuild(
|
|
624
|
+
mode: str = typer.Option(_MODE_DEFAULT, "--mode", "-m", help=_MODE_HELP),
|
|
625
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help=_PROFILE_HELP),
|
|
626
|
+
no_cache: bool = typer.Option(False, "--no-cache", help="Сборка без кэша Docker"),
|
|
627
|
+
) -> None:
|
|
628
|
+
"""
|
|
629
|
+
Пересобрать образы и перезапустить сервисы (интерактивный выбор).
|
|
630
|
+
|
|
631
|
+
Примеры:
|
|
632
|
+
hc env rebuild # интерактив
|
|
633
|
+
hc env rebuild --profile base # core + caddy без вопросов
|
|
634
|
+
hc env rebuild --no-cache # интерактив, без кэша
|
|
635
|
+
"""
|
|
636
|
+
console = Console()
|
|
637
|
+
try:
|
|
638
|
+
require_docker(console)
|
|
639
|
+
mode = mode.strip().lower()
|
|
640
|
+
if mode not in _SERVICES:
|
|
641
|
+
console.print(f"[red]Ошибка:[/red] неизвестный режим {mode!r}. Допустимые: {' | '.join(_SERVICES)}")
|
|
642
|
+
raise typer.Exit(code=2)
|
|
643
|
+
|
|
644
|
+
src = _resolve_source(console)
|
|
645
|
+
project = compose_project_from_source(console, src, mode=mode)
|
|
646
|
+
running = _get_running_services(project.compose_file, project.cwd)
|
|
647
|
+
|
|
648
|
+
selected = _resolve_services(mode=mode, profile=profile, console=console, running=running)
|
|
649
|
+
service_names = [s.name for s in selected]
|
|
650
|
+
|
|
651
|
+
console.print(
|
|
652
|
+
f"\n[cyan]→[/cyan] env rebuild "
|
|
653
|
+
f"mode=[bold]{mode}[/bold] "
|
|
654
|
+
f"services=[bold]{', '.join(service_names)}[/bold]"
|
|
655
|
+
+ (" [dim]--no-cache[/dim]" if no_cache else "")
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
base_cmd = ["docker", "compose", "-f", str(project.compose_file)]
|
|
659
|
+
|
|
660
|
+
build_cmd = [*base_cmd, "build"]
|
|
661
|
+
if no_cache:
|
|
662
|
+
build_cmd.append("--no-cache")
|
|
663
|
+
build_cmd += service_names
|
|
664
|
+
_run(build_cmd, cwd=project.cwd)
|
|
665
|
+
|
|
666
|
+
_run([*base_cmd, "up", "-d", *service_names], cwd=project.cwd)
|
|
667
|
+
console.print(f"[green]✓[/green] rebuild ok")
|
|
668
|
+
|
|
669
|
+
except HcCliError as e:
|
|
670
|
+
console.print(f"[red]Ошибка:[/red] {e.message}")
|
|
671
|
+
if e.hint:
|
|
672
|
+
console.print(f"[dim]Подсказка:[/dim] {e.hint}")
|
|
673
|
+
raise typer.Exit(code=int(e.exit_code or 1))
|
|
674
|
+
|
|
595
675
|
@env_app.command("status")
|
|
596
676
|
def env_status(
|
|
597
677
|
mode: str = typer.Option(_MODE_DEFAULT, "--mode", "-m", help=_MODE_HELP),
|
|
@@ -617,4 +697,159 @@ def register(app: typer.Typer) -> None:
|
|
|
617
697
|
console.print(f"[dim]Подсказка:[/dim] {e.hint}")
|
|
618
698
|
raise typer.Exit(code=int(e.exit_code or 1))
|
|
619
699
|
|
|
700
|
+
@env_app.command("stats")
|
|
701
|
+
def env_stats(
|
|
702
|
+
mode: str = typer.Option(_MODE_DEFAULT, "--mode", "-m", help=_MODE_HELP),
|
|
703
|
+
watch: bool = typer.Option(False, "--watch", "-w", help="Обновлять каждые N секунд"),
|
|
704
|
+
interval: float = typer.Option(3.0, "--interval", "-n", help="Интервал обновления (сек)"),
|
|
705
|
+
) -> None:
|
|
706
|
+
"""CPU%, RAM, NET I/O контейнеров dev-окружения."""
|
|
707
|
+
import json
|
|
708
|
+
import time
|
|
709
|
+
from rich.live import Live
|
|
710
|
+
from rich.table import Table
|
|
711
|
+
|
|
712
|
+
console = Console()
|
|
713
|
+
try:
|
|
714
|
+
require_docker(console)
|
|
715
|
+
mode = mode.strip().lower()
|
|
716
|
+
src = _resolve_source(console)
|
|
717
|
+
project = compose_project_from_source(console, src, mode=mode)
|
|
718
|
+
|
|
719
|
+
def _stats_table() -> Table:
|
|
720
|
+
r = subprocess.run( # noqa: S603
|
|
721
|
+
["docker", "compose", "-f", str(project.compose_file),
|
|
722
|
+
"stats", "--no-stream", "--format", "{{json .}}"],
|
|
723
|
+
cwd=str(project.cwd),
|
|
724
|
+
capture_output=True, text=True, check=False,
|
|
725
|
+
)
|
|
726
|
+
table = Table(show_header=True, header_style="bold cyan", box=None, padding=(0, 1))
|
|
727
|
+
table.add_column("Сервис")
|
|
728
|
+
table.add_column("CPU%", justify="right")
|
|
729
|
+
table.add_column("RAM", justify="right")
|
|
730
|
+
table.add_column("RAM%", justify="right")
|
|
731
|
+
table.add_column("NET I/O", justify="right")
|
|
732
|
+
table.add_column("BLOCK I/O",justify="right")
|
|
733
|
+
table.add_column("PIDs", justify="right")
|
|
734
|
+
|
|
735
|
+
for line in r.stdout.strip().splitlines():
|
|
736
|
+
try:
|
|
737
|
+
d = json.loads(line)
|
|
738
|
+
cpu_s = d.get("CPUPerc", "0%")
|
|
739
|
+
try:
|
|
740
|
+
cpu_f = float(cpu_s.rstrip("%"))
|
|
741
|
+
cpu_color = "red" if cpu_f > 80 else "yellow" if cpu_f > 40 else "green"
|
|
742
|
+
except ValueError:
|
|
743
|
+
cpu_color = "white"
|
|
744
|
+
table.add_row(
|
|
745
|
+
d.get("Name", "?"),
|
|
746
|
+
f"[{cpu_color}]{cpu_s}[/{cpu_color}]",
|
|
747
|
+
d.get("MemUsage", "?"),
|
|
748
|
+
d.get("MemPerc", "?"),
|
|
749
|
+
d.get("NetIO", "?"),
|
|
750
|
+
d.get("BlockIO", "?"),
|
|
751
|
+
d.get("PIDs", "?"),
|
|
752
|
+
)
|
|
753
|
+
except (json.JSONDecodeError, KeyError):
|
|
754
|
+
pass
|
|
755
|
+
return table
|
|
756
|
+
|
|
757
|
+
if watch:
|
|
758
|
+
with Live(refresh_per_second=1, screen=False) as live:
|
|
759
|
+
while True:
|
|
760
|
+
live.update(_stats_table())
|
|
761
|
+
time.sleep(interval)
|
|
762
|
+
else:
|
|
763
|
+
console.print(_stats_table())
|
|
764
|
+
|
|
765
|
+
except (KeyboardInterrupt, typer.Abort):
|
|
766
|
+
pass
|
|
767
|
+
except HcCliError as e:
|
|
768
|
+
console.print(f"[red]Ошибка:[/red] {e.message}")
|
|
769
|
+
if e.hint:
|
|
770
|
+
console.print(f"[dim]Подсказка:[/dim] {e.hint}")
|
|
771
|
+
raise typer.Exit(code=int(e.exit_code or 1))
|
|
772
|
+
|
|
773
|
+
@env_app.command("health")
|
|
774
|
+
def env_health(
|
|
775
|
+
mode: str = typer.Option(_MODE_DEFAULT, "--mode", "-m", help=_MODE_HELP),
|
|
776
|
+
) -> None:
|
|
777
|
+
"""Healthcheck статус каждого сервиса окружения."""
|
|
778
|
+
import json
|
|
779
|
+
from rich.table import Table
|
|
780
|
+
|
|
781
|
+
console = Console()
|
|
782
|
+
try:
|
|
783
|
+
require_docker(console)
|
|
784
|
+
mode = mode.strip().lower()
|
|
785
|
+
src = _resolve_source(console)
|
|
786
|
+
project = compose_project_from_source(console, src, mode=mode)
|
|
787
|
+
|
|
788
|
+
r = subprocess.run( # noqa: S603
|
|
789
|
+
["docker", "compose", "-f", str(project.compose_file),
|
|
790
|
+
"ps", "--format", "json"],
|
|
791
|
+
cwd=str(project.cwd),
|
|
792
|
+
capture_output=True, text=True, check=False,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
table = Table(show_header=True, header_style="bold cyan", box=None, padding=(0, 1))
|
|
796
|
+
table.add_column("Сервис")
|
|
797
|
+
table.add_column("Статус")
|
|
798
|
+
table.add_column("Health")
|
|
799
|
+
table.add_column("Порты")
|
|
800
|
+
|
|
801
|
+
_STATUS_COLOR = {"running": "green", "exited": "red", "paused": "yellow"}
|
|
802
|
+
_HEALTH_COLOR = {"healthy": "green", "unhealthy": "red",
|
|
803
|
+
"starting": "yellow", "none": "dim"}
|
|
804
|
+
|
|
805
|
+
rows: list[dict] = []
|
|
806
|
+
raw = r.stdout.strip()
|
|
807
|
+
if raw:
|
|
808
|
+
try:
|
|
809
|
+
parsed = json.loads(raw)
|
|
810
|
+
rows = parsed if isinstance(parsed, list) else [parsed]
|
|
811
|
+
except json.JSONDecodeError:
|
|
812
|
+
for line in raw.splitlines():
|
|
813
|
+
try:
|
|
814
|
+
rows.append(json.loads(line))
|
|
815
|
+
except json.JSONDecodeError:
|
|
816
|
+
pass
|
|
817
|
+
|
|
818
|
+
if not rows:
|
|
819
|
+
console.print("[yellow]Нет запущенных контейнеров.[/yellow]")
|
|
820
|
+
console.print(f"[dim]compose:[/dim] {project.compose_file}")
|
|
821
|
+
return
|
|
822
|
+
|
|
823
|
+
for row in rows:
|
|
824
|
+
name = row.get("Service") or row.get("Name") or "?"
|
|
825
|
+
state = str(row.get("State") or row.get("Status") or "?").lower()
|
|
826
|
+
health = str(row.get("Health") or "none").lower()
|
|
827
|
+
ports = row.get("Publishers") or row.get("Ports") or ""
|
|
828
|
+
if isinstance(ports, list):
|
|
829
|
+
ports = ", ".join(
|
|
830
|
+
f"{p.get('PublishedPort', '')}→{p.get('TargetPort', '')}"
|
|
831
|
+
for p in ports if p.get("PublishedPort")
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
sc = _STATUS_COLOR.get(state, "white")
|
|
835
|
+
hc_color = _HEALTH_COLOR.get(health, "white")
|
|
836
|
+
health_icon = {"healthy": "✓", "unhealthy": "✗",
|
|
837
|
+
"starting": "…", "none": "—"}.get(health, health)
|
|
838
|
+
|
|
839
|
+
table.add_row(
|
|
840
|
+
f"[bold]{name}[/bold]",
|
|
841
|
+
f"[{sc}]{state}[/{sc}]",
|
|
842
|
+
f"[{hc_color}]{health_icon} {health}[/{hc_color}]",
|
|
843
|
+
str(ports),
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
console.print(table)
|
|
847
|
+
console.print(f"\n[dim]compose:[/dim] {project.compose_file}")
|
|
848
|
+
|
|
849
|
+
except HcCliError as e:
|
|
850
|
+
console.print(f"[red]Ошибка:[/red] {e.message}")
|
|
851
|
+
if e.hint:
|
|
852
|
+
console.print(f"[dim]Подсказка:[/dim] {e.hint}")
|
|
853
|
+
raise typer.Exit(code=int(e.exit_code or 1))
|
|
854
|
+
|
|
620
855
|
app.add_typer(env_app, name="env")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import anyio
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.live import Live
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from hc.commands._client_helpers import require_client
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register(app: typer.Typer) -> None:
|
|
16
|
+
@app.command("status")
|
|
17
|
+
def status(
|
|
18
|
+
watch: bool = typer.Option(False, "--watch", "-w", help="Live-мониторинг (Ctrl+C для выхода)"),
|
|
19
|
+
interval: float = typer.Option(5.0, "--interval", "-n", help="Интервал обновления в секундах"),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Статус Core: версия, uptime, плагины, модули. С --watch — live-мониторинг."""
|
|
22
|
+
console = Console()
|
|
23
|
+
client = require_client(console)
|
|
24
|
+
|
|
25
|
+
latencies: list[float] = []
|
|
26
|
+
|
|
27
|
+
async def _fetch() -> tuple[dict | None, int | None, tuple[int, int] | None, float]:
|
|
28
|
+
t0 = time.monotonic()
|
|
29
|
+
health = await client.admin_status()
|
|
30
|
+
if not health:
|
|
31
|
+
health = await client.health()
|
|
32
|
+
latency_ms = (time.monotonic() - t0) * 1000
|
|
33
|
+
|
|
34
|
+
plugins = await client.get_plugins()
|
|
35
|
+
modules = await client.get_modules()
|
|
36
|
+
|
|
37
|
+
active_plugins = None
|
|
38
|
+
if isinstance(plugins, list):
|
|
39
|
+
active_plugins = sum(1 for p in plugins if str(p.get("status", "")).lower() == "running")
|
|
40
|
+
|
|
41
|
+
modules_stat = None
|
|
42
|
+
if isinstance(modules, list):
|
|
43
|
+
total = len(modules)
|
|
44
|
+
ok = sum(1 for m in modules if str(m.get("status", "")).lower() in {"running", "ok"})
|
|
45
|
+
modules_stat = (ok, total)
|
|
46
|
+
|
|
47
|
+
return health, active_plugins, modules_stat, latency_ms
|
|
48
|
+
|
|
49
|
+
def _build_panel(
|
|
50
|
+
health: dict | None,
|
|
51
|
+
active_plugins: int | None,
|
|
52
|
+
modules_stat: tuple[int, int] | None,
|
|
53
|
+
latency_ms: float,
|
|
54
|
+
) -> Panel:
|
|
55
|
+
if not health:
|
|
56
|
+
return Panel(Text("Core недоступен", style="red"), title="HomeConsole", border_style="red")
|
|
57
|
+
|
|
58
|
+
latencies.append(latency_ms)
|
|
59
|
+
if len(latencies) > 10:
|
|
60
|
+
latencies.pop(0)
|
|
61
|
+
|
|
62
|
+
version = str(health.get("version", "unknown"))
|
|
63
|
+
status_value = str(health.get("status", "running"))
|
|
64
|
+
uptime = str(health.get("uptime", "unknown"))
|
|
65
|
+
|
|
66
|
+
status_text = Text(status_value)
|
|
67
|
+
if status_value.lower() in {"running", "ok"}:
|
|
68
|
+
status_text.stylize("green")
|
|
69
|
+
status_text = Text("✓ ") + status_text
|
|
70
|
+
else:
|
|
71
|
+
status_text.stylize("red")
|
|
72
|
+
|
|
73
|
+
lat_color = "green" if latency_ms < 100 else "yellow" if latency_ms < 500 else "red"
|
|
74
|
+
lat_avg = sum(latencies) / len(latencies)
|
|
75
|
+
lat_line = f"{latency_ms:.0f}ms [dim](avg {lat_avg:.0f}ms)[/dim]"
|
|
76
|
+
|
|
77
|
+
lines: list[Text] = [
|
|
78
|
+
Text.assemble(("Версия: ", "bold"), (version, "")),
|
|
79
|
+
Text.assemble(("Статус: ", "bold"), status_text),
|
|
80
|
+
Text.assemble(("Latency: ", "bold"), Text(lat_line, style=lat_color)),
|
|
81
|
+
]
|
|
82
|
+
if active_plugins is not None:
|
|
83
|
+
lines.append(Text.assemble(("Плагинов: ", "bold"), (f"{active_plugins} активных", "")))
|
|
84
|
+
if modules_stat is not None:
|
|
85
|
+
ok, total = modules_stat
|
|
86
|
+
col = "green" if ok == total else "yellow"
|
|
87
|
+
lines.append(Text.assemble(("Модулей: ", "bold"), Text(f"{ok} / {total}", style=col)))
|
|
88
|
+
lines.append(Text.assemble(("Uptime: ", "bold"), (uptime, "")))
|
|
89
|
+
|
|
90
|
+
title = "HomeConsole"
|
|
91
|
+
if watch:
|
|
92
|
+
ts = time.strftime("%H:%M:%S")
|
|
93
|
+
title += f" [dim]{ts}[/dim]"
|
|
94
|
+
|
|
95
|
+
return Panel(Text("\n").join(lines), title=title, border_style="cyan")
|
|
96
|
+
|
|
97
|
+
if not watch:
|
|
98
|
+
health, active_plugins, modules_stat, latency_ms = anyio.run(_fetch)
|
|
99
|
+
console.print(_build_panel(health, active_plugins, modules_stat, latency_ms))
|
|
100
|
+
if not health:
|
|
101
|
+
raise typer.Exit(code=1)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with Live(refresh_per_second=1, screen=False) as live:
|
|
106
|
+
while True:
|
|
107
|
+
health, active_plugins, modules_stat, latency_ms = anyio.run(_fetch)
|
|
108
|
+
live.update(_build_panel(health, active_plugins, modules_stat, latency_ms))
|
|
109
|
+
time.sleep(interval)
|
|
110
|
+
except (KeyboardInterrupt, typer.Abort):
|
|
111
|
+
pass
|
|
@@ -23,6 +23,7 @@ from hc.commands.status import register as register_status
|
|
|
23
23
|
from hc.commands.deploy import register as register_deploy
|
|
24
24
|
from hc.commands.update import register as register_update
|
|
25
25
|
from hc.commands.ping import register as register_ping
|
|
26
|
+
from hc.commands.doctor import register as register_doctor
|
|
26
27
|
from hc.commands.marketplace import register as register_marketplace
|
|
27
28
|
from hc.commands.secrets import register as register_secrets
|
|
28
29
|
from hc.shell import run_shell
|
|
@@ -70,7 +71,8 @@ _NAV_TREE: dict[str, dict[str, object]] = {
|
|
|
70
71
|
"secrets": {"desc": "Управление секретами", "children": {}},
|
|
71
72
|
"recovery": {"desc": "Recovery сценарии", "children": {}},
|
|
72
73
|
"reset": {"desc": "Сброс состояний", "children": {}},
|
|
73
|
-
"ping":
|
|
74
|
+
"ping": {"desc": "Проверка доступности", "children": {}},
|
|
75
|
+
"doctor": {"desc": "Диагностика системы (Docker, конфиг, порты, диск)", "children": {}},
|
|
74
76
|
"marketplace": {"desc": "Маркетплейс", "children": {}},
|
|
75
77
|
"repl": {"desc": "Интерактивный режим", "children": {}},
|
|
76
78
|
"shell": {"desc": "Алиас для repl", "children": {}},
|
|
@@ -189,6 +191,7 @@ def _register_all() -> None:
|
|
|
189
191
|
register_update(app)
|
|
190
192
|
register_ping(app)
|
|
191
193
|
register_marketplace(app)
|
|
194
|
+
register_doctor(app)
|
|
192
195
|
register_secrets(app)
|
|
193
196
|
|
|
194
197
|
|
|
@@ -18,6 +18,7 @@ from hc import __version__
|
|
|
18
18
|
from hc.config import Config
|
|
19
19
|
from hc.constants import APP_NAME, HISTORY_PATH
|
|
20
20
|
from hc.commands._client_helpers import require_client
|
|
21
|
+
from hc.update_check import get_update_notification
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
_GROUPS = {"core", "auth", "setup", "plugin", "module", "reset", "recovery", "deploy", "update"}
|
|
@@ -298,6 +299,14 @@ def run_repl(app: typer.Typer) -> None:
|
|
|
298
299
|
console.print(f"{APP_NAME} {__version__} | {status}")
|
|
299
300
|
console.print("Type 'help' or '?' for commands, 'exit' to quit")
|
|
300
301
|
|
|
302
|
+
latest = get_update_notification(__version__)
|
|
303
|
+
if latest:
|
|
304
|
+
console.print(
|
|
305
|
+
f"[yellow]→ Доступна новая версия [bold]{latest}[/bold] "
|
|
306
|
+
f"(текущая {__version__})[/yellow]"
|
|
307
|
+
)
|
|
308
|
+
console.print("[dim] pipx upgrade homeconsole-cli | pip install --upgrade homeconsole-cli[/dim]")
|
|
309
|
+
|
|
301
310
|
HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
302
311
|
session = PromptSession(_prompt(None), history=FileHistory(str(HISTORY_PATH)), completer=completer)
|
|
303
312
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from hc.constants import STATE_DIR
|
|
8
|
+
|
|
9
|
+
_CACHE_FILE = STATE_DIR / "version_check.json"
|
|
10
|
+
_TTL = 86400 # 24 hours
|
|
11
|
+
_PYPI_URL = "https://pypi.org/pypi/homeconsole-cli/json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_ver(v: str) -> tuple[int, ...]:
|
|
15
|
+
try:
|
|
16
|
+
return tuple(int(x) for x in v.split(".") if x.isdigit())
|
|
17
|
+
except Exception:
|
|
18
|
+
return (0,)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_newer(latest: str, current: str) -> bool:
|
|
22
|
+
return _parse_ver(latest) > _parse_ver(current)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_cache() -> dict:
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(_CACHE_FILE.read_text())
|
|
28
|
+
except Exception:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _write_cache(latest: str) -> None:
|
|
33
|
+
try:
|
|
34
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
_CACHE_FILE.write_text(json.dumps({"ts": time.time(), "latest": latest}))
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fetch_and_cache() -> None:
|
|
41
|
+
"""Background thread: fetch latest version from PyPI and cache it."""
|
|
42
|
+
try:
|
|
43
|
+
import httpx
|
|
44
|
+
with httpx.Client(timeout=5.0) as client:
|
|
45
|
+
resp = client.get(_PYPI_URL)
|
|
46
|
+
if resp.status_code == 200:
|
|
47
|
+
latest = resp.json()["info"]["version"]
|
|
48
|
+
_write_cache(latest)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_update_notification(current: str) -> str | None:
|
|
54
|
+
"""
|
|
55
|
+
Returns a notification string if a newer version is available, else None.
|
|
56
|
+
Uses a 24-hour cache — never blocks startup.
|
|
57
|
+
If cache is missing or expired, starts a background refresh for next session.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
data = _read_cache()
|
|
61
|
+
ts = data.get("ts", 0)
|
|
62
|
+
latest = data.get("latest", "")
|
|
63
|
+
|
|
64
|
+
if not latest or time.time() - ts > _TTL:
|
|
65
|
+
# Refresh in background — result visible next session
|
|
66
|
+
threading.Thread(target=_fetch_and_cache, daemon=True).start()
|
|
67
|
+
|
|
68
|
+
if latest and _is_newer(latest, current):
|
|
69
|
+
return latest
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
return None
|
|
@@ -16,6 +16,7 @@ hc/native_core.py
|
|
|
16
16
|
hc/repl.py
|
|
17
17
|
hc/setup_runner.py
|
|
18
18
|
hc/shell.py
|
|
19
|
+
hc/update_check.py
|
|
19
20
|
hc/commands/__init__.py
|
|
20
21
|
hc/commands/_client_helpers.py
|
|
21
22
|
hc/commands/_compose_helpers.py
|
|
@@ -23,6 +24,7 @@ hc/commands/auth.py
|
|
|
23
24
|
hc/commands/connect.py
|
|
24
25
|
hc/commands/core.py
|
|
25
26
|
hc/commands/deploy.py
|
|
27
|
+
hc/commands/doctor.py
|
|
26
28
|
hc/commands/env.py
|
|
27
29
|
hc/commands/install.py
|
|
28
30
|
hc/commands/logs.py
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
import anyio
|
|
6
|
-
import typer
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
from rich.panel import Panel
|
|
9
|
-
from rich.text import Text
|
|
10
|
-
|
|
11
|
-
from hc.commands._client_helpers import require_client
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def register(app: typer.Typer) -> None:
|
|
15
|
-
@app.command("status")
|
|
16
|
-
def status() -> None:
|
|
17
|
-
console = Console()
|
|
18
|
-
client = require_client(console)
|
|
19
|
-
|
|
20
|
-
async def _run() -> tuple[dict | None, int | None, tuple[int, int] | None]:
|
|
21
|
-
health = await client.admin_status()
|
|
22
|
-
if not health:
|
|
23
|
-
health = await client.health()
|
|
24
|
-
plugins = await client.get_plugins()
|
|
25
|
-
modules = await client.get_modules()
|
|
26
|
-
active_plugins = None
|
|
27
|
-
if isinstance(plugins, list):
|
|
28
|
-
active_plugins = sum(1 for p in plugins if str(p.get("status", "")).lower() == "running")
|
|
29
|
-
modules_stat = None
|
|
30
|
-
if isinstance(modules, list):
|
|
31
|
-
total = len(modules)
|
|
32
|
-
ok = sum(1 for m in modules if str(m.get("status", "")).lower() in {"running", "ok"})
|
|
33
|
-
modules_stat = (ok, total)
|
|
34
|
-
return health, active_plugins, modules_stat
|
|
35
|
-
|
|
36
|
-
health, active_plugins, modules_stat = anyio.run(_run)
|
|
37
|
-
if not health:
|
|
38
|
-
raise typer.Exit(code=1)
|
|
39
|
-
|
|
40
|
-
version = str(health.get("version", "unknown"))
|
|
41
|
-
status_value = str(health.get("status", "running"))
|
|
42
|
-
uptime = str(health.get("uptime", "unknown"))
|
|
43
|
-
|
|
44
|
-
status_text = Text(status_value)
|
|
45
|
-
if status_value.lower() in {"running", "ok"}:
|
|
46
|
-
status_text.stylize("green")
|
|
47
|
-
status_text = Text("✓ ") + status_text
|
|
48
|
-
else:
|
|
49
|
-
status_text.stylize("red")
|
|
50
|
-
|
|
51
|
-
lines: list[Text] = [
|
|
52
|
-
Text.assemble(("Версия: ", "bold"), (version, "")),
|
|
53
|
-
Text.assemble(("Статус: ", "bold"), status_text),
|
|
54
|
-
]
|
|
55
|
-
if active_plugins is not None:
|
|
56
|
-
lines.append(Text.assemble(("Плагинов: ", "bold"), (f"{active_plugins} активных", "")))
|
|
57
|
-
if modules_stat is not None:
|
|
58
|
-
ok, total = modules_stat
|
|
59
|
-
lines.append(Text.assemble(("Модулей: ", "bold"), (f"{ok} / {total}", "")))
|
|
60
|
-
lines.append(Text.assemble(("Uptime: ", "bold"), (uptime, "")))
|
|
61
|
-
|
|
62
|
-
body = Text("\n").join(lines)
|
|
63
|
-
console.print(Panel(body, title="HomeConsole", border_style="cyan"))
|
|
64
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{homeconsole_cli-0.0.4 → homeconsole_cli-0.0.6}/homeconsole_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|