|
37 | 37 | tweaks_for_profile, |
38 | 38 | ) |
39 | 39 |
|
| 40 | +__all__ = ["main"] |
| 41 | + |
40 | 42 |
|
41 | 43 | def _confirm(prompt: str) -> bool: |
42 | 44 | try: |
@@ -311,6 +313,96 @@ def _split_root_for_reg(path: str) -> tuple[int, str]: |
311 | 313 | raise ValueError(f"Unsupported registry path: {path}") |
312 | 314 |
|
313 | 315 |
|
| 316 | +def _run_doctor() -> int: |
| 317 | + """Comprehensive system health check — prints a report and returns exit code.""" |
| 318 | + import platform |
| 319 | + |
| 320 | + checks: list[tuple[str, bool, str]] = [] # (label, passed, detail) |
| 321 | + |
| 322 | + # 1. Python version |
| 323 | + vi = sys.version_info |
| 324 | + py_ok = (vi.major, vi.minor) >= (3, 9) |
| 325 | + checks.append(("Python >= 3.9", py_ok, f"{vi.major}.{vi.minor}.{vi.micro}")) |
| 326 | + |
| 327 | + # 2. winreg availability |
| 328 | + win_ok = is_windows() |
| 329 | + checks.append(("Windows / winreg", win_ok, platform.system())) |
| 330 | + |
| 331 | + # 3. Admin status |
| 332 | + from .elevation import is_admin |
| 333 | + |
| 334 | + admin_ok = is_admin() |
| 335 | + checks.append(("Running as admin", admin_ok, "yes" if admin_ok else "no (some tweaks unavailable)")) |
| 336 | + |
| 337 | + # 4. Config file validity |
| 338 | + config_detail = "OK" |
| 339 | + try: |
| 340 | + from .config import load_config |
| 341 | + |
| 342 | + load_config(None) |
| 343 | + cfg_ok = True |
| 344 | + except Exception as exc: |
| 345 | + cfg_ok = False |
| 346 | + config_detail = str(exc)[:80] |
| 347 | + checks.append(("Config file", cfg_ok, config_detail)) |
| 348 | + |
| 349 | + # 5. Tweaks load cleanly |
| 350 | + tweak_detail = "OK" |
| 351 | + try: |
| 352 | + from .tweaks import all_tweaks |
| 353 | + |
| 354 | + all_tweaks_list = all_tweaks() |
| 355 | + ids = [td.id for td in all_tweaks_list] |
| 356 | + dup_ids = {tid for tid in ids if ids.count(tid) > 1} |
| 357 | + tweaks_ok = len(dup_ids) == 0 |
| 358 | + tweak_detail = f"{len(all_tweaks_list)} tweaks loaded" if tweaks_ok else f"Duplicate IDs: {', '.join(sorted(dup_ids))}" |
| 359 | + except Exception as exc: |
| 360 | + tweaks_ok = False |
| 361 | + tweak_detail = str(exc)[:80] |
| 362 | + checks.append(("Tweaks registry", tweaks_ok, tweak_detail)) |
| 363 | + |
| 364 | + # 6. Corporate guard |
| 365 | + corp_detail = "not detected" |
| 366 | + try: |
| 367 | + from .corpguard import corp_guard_status, is_corporate_network |
| 368 | + |
| 369 | + if is_corporate_network(): |
| 370 | + corp_detail = corp_guard_status() or "corporate network detected" |
| 371 | + corp_ok = True # detecting is not a failure; just informational |
| 372 | + except Exception as exc: |
| 373 | + corp_ok = False |
| 374 | + corp_detail = str(exc)[:80] |
| 375 | + checks.append(("Corp guard", corp_ok, corp_detail)) |
| 376 | + |
| 377 | + # 7. Session log writable |
| 378 | + try: |
| 379 | + SESSION.log_path.parent.mkdir(parents=True, exist_ok=True) |
| 380 | + log_ok = True |
| 381 | + log_detail = str(SESSION.log_path) |
| 382 | + except Exception as exc: |
| 383 | + log_ok = False |
| 384 | + log_detail = str(exc)[:80] |
| 385 | + checks.append(("Log path writable", log_ok, log_detail)) |
| 386 | + |
| 387 | + # ── Report ──────────────────────────────────────────────────────────────── |
| 388 | + _W = 30 |
| 389 | + print(f"\n {'RegiLattice Doctor':^{_W + 20}}") |
| 390 | + print(f" {platform_summary()}") |
| 391 | + print() |
| 392 | + all_ok = True |
| 393 | + for label, passed, detail in checks: |
| 394 | + icon = "\u2705" if passed else "\u274c" |
| 395 | + all_ok = all_ok and passed |
| 396 | + print(f" {icon} {label:<{_W}} {detail}") |
| 397 | + print() |
| 398 | + if all_ok: |
| 399 | + print(" All checks passed \u2014 RegiLattice is healthy.") |
| 400 | + else: |
| 401 | + print(" \u26a0\ufe0f Some checks failed. Review the items marked with \u274c above.") |
| 402 | + print() |
| 403 | + return 0 if all_ok else 1 |
| 404 | + |
| 405 | + |
314 | 406 | def _build_parser() -> argparse.ArgumentParser: |
315 | 407 | parser = argparse.ArgumentParser( |
316 | 408 | prog="regilattice", |
@@ -499,6 +591,11 @@ def _build_parser() -> argparse.ArgumentParser: |
499 | 591 | dest="needs_admin", |
500 | 592 | help="Show only tweaks that require administrator rights (use with --list/--search).", |
501 | 593 | ) |
| 594 | + parser.add_argument( |
| 595 | + "--doctor", |
| 596 | + action="store_true", |
| 597 | + help="Run a comprehensive system health check: Python version, winreg, admin, config, tweaks.", |
| 598 | + ) |
502 | 599 | return parser |
503 | 600 |
|
504 | 601 |
|
@@ -542,6 +639,9 @@ def _handle_shutdown(signum: int, frame: object) -> None: |
542 | 639 | print(f" \u274c {pkg} \u2014 could not install") |
543 | 640 | return 0 |
544 | 641 |
|
| 642 | + if args.doctor: |
| 643 | + return _run_doctor() |
| 644 | + |
545 | 645 | if args.hwinfo: |
546 | 646 | from .hwinfo import detect_hardware, hardware_summary, suggest_profile |
547 | 647 |
|
@@ -716,12 +816,15 @@ def _handle_shutdown(signum: int, frame: object) -> None: |
716 | 816 | if getattr(args, "output", "table") == "json": |
717 | 817 | import json as _j |
718 | 818 |
|
719 | | - print(_j.dumps( |
720 | | - [{"id": td.id, "label": td.label, "category": td.category, |
721 | | - "needs_admin": td.needs_admin, "corp_safe": td.corp_safe} |
722 | | - for td in tweaks], |
723 | | - indent=2, |
724 | | - )) |
| 819 | + print( |
| 820 | + _j.dumps( |
| 821 | + [ |
| 822 | + {"id": td.id, "label": td.label, "category": td.category, "needs_admin": td.needs_admin, "corp_safe": td.corp_safe} |
| 823 | + for td in tweaks |
| 824 | + ], |
| 825 | + indent=2, |
| 826 | + ) |
| 827 | + ) |
725 | 828 | else: |
726 | 829 | print(f"{'ID':<30} {'Category':<14} {'Status':<14} Label") |
727 | 830 | print("-" * 80) |
|
0 commit comments