logics-manager 2.3.2__tar.gz → 2.4.0__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.
- {logics_manager-2.3.2 → logics_manager-2.4.0}/PKG-INFO +1 -1
- {logics_manager-2.3.2 → logics_manager-2.4.0}/README.md +1 -1
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/viewer.py +213 -11
- logics_manager-2.4.0/logics_manager/viewer_assets/media/css/board.css +658 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/css/details.css +457 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/css/layout.css +123 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/css/toolbar.css +614 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/harnessApi.js +324 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/hostApi.js +213 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/hostApiContract.js +55 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/icon.png +0 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/layoutController.js +246 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/logics.svg +7 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/logicsModel.js +910 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/main.css +112 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/main.js +3 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/mainApp.js +1005 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/mainCore.js +604 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/mainInteractionHandlers.js +324 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/mainInteractions.js +378 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/renderBoard.js +3 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/renderBoardApp.js +1339 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/renderDetails.js +685 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/renderMarkdown.js +449 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/toolsPanelLayout.js +172 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/uiStatus.js +54 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/webviewChrome.js +428 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/webviewPersistence.js +116 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/media/webviewSelectors.js +492 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/vendor/mermaid.min.js +3405 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/viewer/browser-host.js +1266 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/viewer/index.html +223 -0
- logics_manager-2.4.0/logics_manager/viewer_assets/viewer/viewer.css +574 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager.egg-info/PKG-INFO +1 -1
- logics_manager-2.4.0/logics_manager.egg-info/SOURCES.txt +60 -0
- logics_manager-2.4.0/pyproject.toml +30 -0
- logics_manager-2.3.2/logics_manager.egg-info/SOURCES.txt +0 -30
- logics_manager-2.3.2/pyproject.toml +0 -15
- {logics_manager-2.3.2 → logics_manager-2.4.0}/LICENSE +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/__init__.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/__main__.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/assist.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/assist_handoff.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/assist_surface.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/audit.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/bootstrap.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/cli.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/cli_output.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/config.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/doctor.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/flow.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/flow_evidence.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/index.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/insights.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/lint.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/mcp.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/path_utils.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/sync.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/termstyle.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager/update_check.py +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager.egg-info/dependency_links.txt +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager.egg-info/entry_points.txt +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/logics_manager.egg-info/top_level.txt +0 -0
- {logics_manager-2.3.2 → logics_manager-2.4.0}/setup.cfg +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/AlexAgo83/logics-manager/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|

|
|
@@ -5,6 +5,8 @@ import json
|
|
|
5
5
|
import mimetypes
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import socket
|
|
8
10
|
import subprocess
|
|
9
11
|
import sys
|
|
10
12
|
import webbrowser
|
|
@@ -40,9 +42,15 @@ DOC_FAMILIES = (
|
|
|
40
42
|
|
|
41
43
|
STAGE_ORDER = {family.stage: index for index, family in enumerate(DOC_FAMILIES)}
|
|
42
44
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
45
|
+
PACKAGE_VIEWER_ASSETS_ROOT = Path(__file__).resolve().parent / "viewer_assets"
|
|
43
46
|
VIEWER_ROOT = REPO_ROOT / "clients" / "viewer"
|
|
47
|
+
if not (VIEWER_ROOT / "index.html").is_file():
|
|
48
|
+
VIEWER_ROOT = PACKAGE_VIEWER_ASSETS_ROOT / "viewer"
|
|
44
49
|
SHARED_MEDIA_ROOT = REPO_ROOT / "clients" / "shared-web" / "media"
|
|
50
|
+
if not SHARED_MEDIA_ROOT.is_dir():
|
|
51
|
+
SHARED_MEDIA_ROOT = PACKAGE_VIEWER_ASSETS_ROOT / "media"
|
|
45
52
|
DIST_VENDOR_ROOT = REPO_ROOT / "dist" / "vendor"
|
|
53
|
+
PACKAGE_VENDOR_ROOT = PACKAGE_VIEWER_ASSETS_ROOT / "vendor"
|
|
46
54
|
NODE_MERMAID_ROOT = REPO_ROOT / "node_modules" / "mermaid" / "dist"
|
|
47
55
|
|
|
48
56
|
|
|
@@ -322,9 +330,16 @@ def collect_viewer_items(repo_root: Path) -> list[dict[str, Any]]:
|
|
|
322
330
|
return items
|
|
323
331
|
|
|
324
332
|
|
|
325
|
-
def viewer_data_payload(
|
|
333
|
+
def viewer_data_payload(
|
|
334
|
+
repo_root: Path,
|
|
335
|
+
selected_id: str | None = None,
|
|
336
|
+
*,
|
|
337
|
+
auto_refresh_interval_seconds: int = 60,
|
|
338
|
+
) -> dict[str, Any]:
|
|
326
339
|
return {
|
|
327
340
|
"root": str(repo_root.resolve()),
|
|
341
|
+
"repoName": repo_root.resolve().name,
|
|
342
|
+
"autoRefreshIntervalSeconds": auto_refresh_interval_seconds,
|
|
328
343
|
"items": collect_viewer_items(repo_root),
|
|
329
344
|
"updateInfo": get_update_info(_current_version()).to_payload(),
|
|
330
345
|
"selectedId": selected_id,
|
|
@@ -378,13 +393,122 @@ def _system_editor_command(path: Path) -> list[str]:
|
|
|
378
393
|
return ["xdg-open", str(path)]
|
|
379
394
|
|
|
380
395
|
|
|
396
|
+
def _run_read_only_git(repo_root: Path, args: list[str], *, runner: Any | None = None) -> subprocess.CompletedProcess[str]:
|
|
397
|
+
command = ["git", *args]
|
|
398
|
+
git_runner = runner or subprocess.run
|
|
399
|
+
return git_runner(command, cwd=repo_root, text=True, capture_output=True, timeout=5)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _sanitize_git_ref(value: str) -> str:
|
|
403
|
+
ref = value.strip()
|
|
404
|
+
ref = re.sub(r"://[^/@\s]+@", "://", ref)
|
|
405
|
+
ref = re.sub(r"^[^/@\s]+@", "", ref)
|
|
406
|
+
return ref[:200]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _classify_porcelain_entry(line: str) -> tuple[str, dict[str, str]] | None:
|
|
410
|
+
if not line or line.startswith("## "):
|
|
411
|
+
return None
|
|
412
|
+
if line.startswith("?? "):
|
|
413
|
+
return "untracked", {"path": line[3:].strip()}
|
|
414
|
+
if len(line) < 4:
|
|
415
|
+
return None
|
|
416
|
+
staged = line[0]
|
|
417
|
+
worktree = line[1]
|
|
418
|
+
raw_path = line[3:].strip()
|
|
419
|
+
if " -> " in raw_path:
|
|
420
|
+
before, after = raw_path.split(" -> ", 1)
|
|
421
|
+
return "renamed", {"path": after.strip(), "from": before.strip()}
|
|
422
|
+
if staged == "R":
|
|
423
|
+
return "renamed", {"path": raw_path}
|
|
424
|
+
if staged not in {" ", "?", "!"}:
|
|
425
|
+
return "staged", {"path": raw_path, "code": staged}
|
|
426
|
+
if worktree == "D":
|
|
427
|
+
return "deleted", {"path": raw_path, "code": worktree}
|
|
428
|
+
if worktree not in {" ", "?", "!"}:
|
|
429
|
+
return "modified", {"path": raw_path, "code": worktree}
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _parse_git_branch_line(line: str) -> dict[str, Any]:
|
|
434
|
+
branch = line[3:].strip() if line.startswith("## ") else ""
|
|
435
|
+
tracking = ""
|
|
436
|
+
ahead = 0
|
|
437
|
+
behind = 0
|
|
438
|
+
if "..." in branch:
|
|
439
|
+
branch, tracking_part = branch.split("...", 1)
|
|
440
|
+
if " [" in tracking_part:
|
|
441
|
+
tracking, details = tracking_part.split(" [", 1)
|
|
442
|
+
for detail in details.rstrip("]").split(", "):
|
|
443
|
+
if detail.startswith("ahead "):
|
|
444
|
+
ahead = int(detail.removeprefix("ahead ") or "0")
|
|
445
|
+
if detail.startswith("behind "):
|
|
446
|
+
behind = int(detail.removeprefix("behind ") or "0")
|
|
447
|
+
else:
|
|
448
|
+
tracking = tracking_part
|
|
449
|
+
return {
|
|
450
|
+
"branch": _sanitize_git_ref(branch or "HEAD"),
|
|
451
|
+
"tracking": _sanitize_git_ref(tracking),
|
|
452
|
+
"ahead": ahead,
|
|
453
|
+
"behind": behind,
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def git_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any | None = None) -> dict[str, Any]:
|
|
458
|
+
git_which = which or shutil.which
|
|
459
|
+
if not git_which("git"):
|
|
460
|
+
return {"state": "unavailable", "message": "Git is not available on PATH."}
|
|
461
|
+
try:
|
|
462
|
+
inside = _run_read_only_git(repo_root, ["rev-parse", "--is-inside-work-tree"], runner=runner)
|
|
463
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
464
|
+
return {"state": "error", "message": f"Unable to run Git status: {exc}"}
|
|
465
|
+
if inside.returncode != 0 or inside.stdout.strip().lower() != "true":
|
|
466
|
+
return {"state": "not-repository", "message": "This folder is not inside a Git worktree."}
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
status = _run_read_only_git(repo_root, ["status", "--porcelain=v1", "-b"], runner=runner)
|
|
470
|
+
commit = _run_read_only_git(repo_root, ["log", "-1", "--pretty=format:%h %s"], runner=runner)
|
|
471
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
472
|
+
return {"state": "error", "message": f"Unable to collect Git status: {exc}"}
|
|
473
|
+
if status.returncode != 0:
|
|
474
|
+
message = (status.stderr or status.stdout or "Git status failed.").strip().splitlines()[0]
|
|
475
|
+
return {"state": "error", "message": message}
|
|
476
|
+
|
|
477
|
+
lines = status.stdout.splitlines()
|
|
478
|
+
branch_info = _parse_git_branch_line(lines[0]) if lines else {"branch": "HEAD", "tracking": "", "ahead": 0, "behind": 0}
|
|
479
|
+
groups: dict[str, list[dict[str, str]]] = {key: [] for key in ("staged", "modified", "deleted", "renamed", "untracked")}
|
|
480
|
+
for line in lines[1:]:
|
|
481
|
+
classified = _classify_porcelain_entry(line)
|
|
482
|
+
if classified:
|
|
483
|
+
group, entry = classified
|
|
484
|
+
groups[group].append(entry)
|
|
485
|
+
counts = {key: len(value) for key, value in groups.items()}
|
|
486
|
+
dirty = any(counts.values())
|
|
487
|
+
return {
|
|
488
|
+
"state": "ok",
|
|
489
|
+
**branch_info,
|
|
490
|
+
"clean": not dirty,
|
|
491
|
+
"dirty": dirty,
|
|
492
|
+
"counts": counts,
|
|
493
|
+
"groups": groups,
|
|
494
|
+
"latestCommit": (commit.stdout.strip() if commit.returncode == 0 else "")[:300],
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
|
|
381
498
|
def _json_bytes(payload: Any) -> bytes:
|
|
382
499
|
return json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
|
|
383
500
|
|
|
384
501
|
|
|
385
502
|
class LogicsViewerServer(ThreadingHTTPServer):
|
|
386
|
-
def __init__(
|
|
503
|
+
def __init__(
|
|
504
|
+
self,
|
|
505
|
+
server_address: tuple[str, int],
|
|
506
|
+
repo_root: Path,
|
|
507
|
+
*,
|
|
508
|
+
auto_refresh_interval_seconds: int = 60,
|
|
509
|
+
):
|
|
387
510
|
self.repo_root = repo_root.resolve()
|
|
511
|
+
self.auto_refresh_interval_seconds = auto_refresh_interval_seconds
|
|
388
512
|
super().__init__(server_address, LogicsViewerRequestHandler)
|
|
389
513
|
|
|
390
514
|
|
|
@@ -435,6 +559,8 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
435
559
|
vendor_path = DIST_VENDOR_ROOT / "mermaid.min.js"
|
|
436
560
|
if not vendor_path.is_file():
|
|
437
561
|
vendor_path = NODE_MERMAID_ROOT / "mermaid.min.js"
|
|
562
|
+
if not vendor_path.is_file():
|
|
563
|
+
vendor_path = PACKAGE_VENDOR_ROOT / "mermaid.min.js"
|
|
438
564
|
self._serve_file(vendor_path)
|
|
439
565
|
return
|
|
440
566
|
if route.startswith("/media/"):
|
|
@@ -445,7 +571,15 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
445
571
|
self._serve_file(media_path)
|
|
446
572
|
return
|
|
447
573
|
if route == "/api/items":
|
|
448
|
-
self._send_json(
|
|
574
|
+
self._send_json(
|
|
575
|
+
{
|
|
576
|
+
"ok": True,
|
|
577
|
+
"payload": viewer_data_payload(
|
|
578
|
+
self.server.repo_root,
|
|
579
|
+
auto_refresh_interval_seconds=self.server.auto_refresh_interval_seconds,
|
|
580
|
+
),
|
|
581
|
+
}
|
|
582
|
+
)
|
|
449
583
|
return
|
|
450
584
|
if route == "/api/doc":
|
|
451
585
|
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
@@ -460,12 +594,23 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
460
594
|
if route == "/api/audit":
|
|
461
595
|
self._send_json({"ok": True, "payload": audit_payload(self.server.repo_root)})
|
|
462
596
|
return
|
|
597
|
+
if route == "/api/git-status":
|
|
598
|
+
self._send_json({"ok": True, "payload": git_status_payload(self.server.repo_root)})
|
|
599
|
+
return
|
|
463
600
|
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
464
601
|
|
|
465
602
|
def do_POST(self) -> None:
|
|
466
603
|
parsed = urlparse(self.path)
|
|
467
604
|
if parsed.path == "/api/refresh":
|
|
468
|
-
self._send_json(
|
|
605
|
+
self._send_json(
|
|
606
|
+
{
|
|
607
|
+
"ok": True,
|
|
608
|
+
"payload": viewer_data_payload(
|
|
609
|
+
self.server.repo_root,
|
|
610
|
+
auto_refresh_interval_seconds=self.server.auto_refresh_interval_seconds,
|
|
611
|
+
),
|
|
612
|
+
}
|
|
613
|
+
)
|
|
469
614
|
return
|
|
470
615
|
if parsed.path == "/api/edit":
|
|
471
616
|
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
@@ -479,19 +624,52 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
479
624
|
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
480
625
|
|
|
481
626
|
|
|
482
|
-
def create_viewer_server(
|
|
483
|
-
|
|
627
|
+
def create_viewer_server(
|
|
628
|
+
repo_root: Path,
|
|
629
|
+
host: str = "127.0.0.1",
|
|
630
|
+
port: int = 8765,
|
|
631
|
+
*,
|
|
632
|
+
auto_refresh_interval_seconds: int = 60,
|
|
633
|
+
) -> LogicsViewerServer:
|
|
634
|
+
return LogicsViewerServer(
|
|
635
|
+
(host, port),
|
|
636
|
+
repo_root,
|
|
637
|
+
auto_refresh_interval_seconds=auto_refresh_interval_seconds,
|
|
638
|
+
)
|
|
484
639
|
|
|
485
640
|
|
|
486
|
-
def
|
|
641
|
+
def _network_viewer_url(host: str, port: int, *, focus: str | None = None, read: bool = False) -> str | None:
|
|
642
|
+
if host not in {"0.0.0.0", "::", ""}:
|
|
643
|
+
return None
|
|
644
|
+
try:
|
|
645
|
+
candidate = socket.gethostbyname(socket.gethostname())
|
|
646
|
+
except OSError:
|
|
647
|
+
return None
|
|
648
|
+
if not candidate or candidate.startswith("127."):
|
|
649
|
+
return None
|
|
650
|
+
return build_viewer_url(candidate, port, focus=focus, read=read)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def render_start_status(
|
|
654
|
+
url: str,
|
|
655
|
+
repo_root: Path,
|
|
656
|
+
*,
|
|
657
|
+
focus: str | None = None,
|
|
658
|
+
network_url: str | None = None,
|
|
659
|
+
bind_host: str = "localhost",
|
|
660
|
+
auto_refresh_interval_seconds: int = 60,
|
|
661
|
+
) -> str:
|
|
487
662
|
lines = [
|
|
488
663
|
"Logics viewer running:",
|
|
489
|
-
url,
|
|
664
|
+
f"Local: {url}",
|
|
490
665
|
"",
|
|
491
666
|
f"Repo: {repo_root.name}",
|
|
492
667
|
"Mode: read-only",
|
|
493
|
-
"Bind:
|
|
668
|
+
f"Bind: {bind_host}",
|
|
669
|
+
f"Auto refresh: {auto_refresh_interval_seconds}s",
|
|
494
670
|
]
|
|
671
|
+
if network_url:
|
|
672
|
+
lines.insert(2, f"Network: {network_url}")
|
|
495
673
|
if focus:
|
|
496
674
|
lines.append(f"Focus: {focus}")
|
|
497
675
|
return "\n".join(lines)
|
|
@@ -501,6 +679,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
501
679
|
parser = argparse.ArgumentParser(prog="logics-manager view", description="Start the local read-only Logics browser viewer.")
|
|
502
680
|
parser.add_argument("--host", default="127.0.0.1", help="Bind host. Defaults to 127.0.0.1.")
|
|
503
681
|
parser.add_argument("--port", type=int, default=8765, help="Bind port. Use 0 to select an available port.")
|
|
682
|
+
parser.add_argument(
|
|
683
|
+
"--refresh-interval",
|
|
684
|
+
type=int,
|
|
685
|
+
default=60,
|
|
686
|
+
help="Automatic refresh interval in seconds. Defaults to 60; positive shorter intervals are allowed.",
|
|
687
|
+
)
|
|
504
688
|
parser.add_argument("--focus", help="Open the viewer focused on a workflow ref or repo-relative Logics Markdown path.")
|
|
505
689
|
parser.add_argument("--read", action="store_true", help="Open the focused item in the read preview. Requires --focus.")
|
|
506
690
|
parser.add_argument("--open", action="store_true", help="Open the viewer in the default browser.")
|
|
@@ -511,16 +695,34 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
511
695
|
def main(argv: list[str]) -> int:
|
|
512
696
|
args = build_parser().parse_args(argv)
|
|
513
697
|
repo_root = find_repo_root(Path.cwd())
|
|
698
|
+
if args.refresh_interval <= 0:
|
|
699
|
+
raise SystemExit("--refresh-interval must be a positive number of seconds.")
|
|
514
700
|
if args.read and not args.focus:
|
|
515
701
|
raise SystemExit("--read requires --focus.")
|
|
516
702
|
try:
|
|
517
703
|
focus = normalize_viewer_focus_target(repo_root, args.focus) if args.focus else None
|
|
518
704
|
except ValueError as exc:
|
|
519
705
|
raise SystemExit(str(exc)) from exc
|
|
520
|
-
server = create_viewer_server(
|
|
706
|
+
server = create_viewer_server(
|
|
707
|
+
repo_root,
|
|
708
|
+
host=args.host,
|
|
709
|
+
port=args.port,
|
|
710
|
+
auto_refresh_interval_seconds=args.refresh_interval,
|
|
711
|
+
)
|
|
521
712
|
host, port = server.server_address[:2]
|
|
522
713
|
url = build_viewer_url(str(host), int(port), focus=focus, read=bool(args.read))
|
|
523
|
-
|
|
714
|
+
network_url = _network_viewer_url(str(host), int(port), focus=focus, read=bool(args.read))
|
|
715
|
+
print(
|
|
716
|
+
render_start_status(
|
|
717
|
+
url,
|
|
718
|
+
repo_root,
|
|
719
|
+
focus=focus,
|
|
720
|
+
network_url=network_url,
|
|
721
|
+
bind_host=str(host),
|
|
722
|
+
auto_refresh_interval_seconds=args.refresh_interval,
|
|
723
|
+
),
|
|
724
|
+
flush=True,
|
|
725
|
+
)
|
|
524
726
|
if args.open and not args.no_open:
|
|
525
727
|
webbrowser.open(url)
|
|
526
728
|
|