@grifhinz/logics-manager 2.3.3 → 2.4.0
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.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/clients/shared-web/media/css/toolbar.css +41 -3
- package/clients/shared-web/media/webviewChrome.js +31 -8
- package/clients/shared-web/media/webviewSelectors.js +8 -7
- package/clients/viewer/browser-host.js +463 -44
- package/clients/viewer/index.html +14 -28
- package/clients/viewer/viewer.css +141 -0
- package/logics_manager/viewer.py +205 -11
- package/package.json +4 -2
- package/pyproject.toml +1 -1
|
@@ -16,13 +16,21 @@
|
|
|
16
16
|
<body class="viewer-shell">
|
|
17
17
|
<header class="viewer-topbar" aria-label="Viewer status">
|
|
18
18
|
<div>
|
|
19
|
-
<div class="viewer-
|
|
19
|
+
<div class="viewer-topbar__identity">
|
|
20
|
+
<div class="viewer-topbar__title">Logics Viewer</div>
|
|
21
|
+
<span class="viewer-topbar__repo" id="viewer-repo-pill" title="">repository</span>
|
|
22
|
+
</div>
|
|
20
23
|
<div class="viewer-topbar__meta" id="viewer-meta">Read-only local viewer</div>
|
|
21
24
|
</div>
|
|
22
25
|
<div class="viewer-topbar__actions">
|
|
26
|
+
<label class="viewer-auto-refresh" title="Toggle automatic refresh">
|
|
27
|
+
<input id="viewer-auto-refresh" type="checkbox" checked />
|
|
28
|
+
<span>Auto</span>
|
|
29
|
+
</label>
|
|
30
|
+
<button class="btn" data-action="refresh" type="button" title="Refresh viewer data">Refresh</button>
|
|
31
|
+
<button class="btn" id="viewer-git" type="button" title="Show Git status">Git</button>
|
|
23
32
|
<button class="btn" id="viewer-insights" type="button" title="Show corpus insights">Insights</button>
|
|
24
33
|
<button class="btn" id="viewer-health" type="button" title="Show lint and audit health">Health</button>
|
|
25
|
-
<button class="btn" data-action="refresh" type="button" title="Refresh viewer data">Refresh</button>
|
|
26
34
|
</div>
|
|
27
35
|
</header>
|
|
28
36
|
|
|
@@ -34,28 +42,6 @@
|
|
|
34
42
|
<path d="M4 6h16l-6 7v5l-4 2v-7z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
|
|
35
43
|
</svg>
|
|
36
44
|
</button>
|
|
37
|
-
<div class="toolbar__tools">
|
|
38
|
-
<button class="toolbar__filter" id="tools-toggle" aria-label="Open tools menu" aria-haspopup="menu" aria-expanded="false" aria-controls="tools-panel" title="Open tools menu">
|
|
39
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
40
|
-
<rect x="5" y="6" width="14" height="2" rx="1" fill="currentColor" />
|
|
41
|
-
<rect x="5" y="11" width="14" height="2" rx="1" fill="currentColor" />
|
|
42
|
-
<rect x="5" y="16" width="14" height="2" rx="1" fill="currentColor" />
|
|
43
|
-
</svg>
|
|
44
|
-
</button>
|
|
45
|
-
<div class="tools-panel" id="tools-panel" aria-hidden="true" role="menu">
|
|
46
|
-
<div class="tools-panel__header">
|
|
47
|
-
<div class="tools-panel__header-label">Tools</div>
|
|
48
|
-
<button class="tools-panel__close" type="button" data-tools-panel-close aria-label="Close tools menu" title="Close tools menu">x</button>
|
|
49
|
-
</div>
|
|
50
|
-
<div class="tools-panel__section" data-tools-section="recommended">
|
|
51
|
-
<div class="tools-panel__section-label">Read-only</div>
|
|
52
|
-
<div class="tools-panel__section-body" data-tools-body="recommended">
|
|
53
|
-
<button class="tools-panel__item" type="button" role="menuitem" data-action="refresh" title="Refresh viewer data">Refresh</button>
|
|
54
|
-
<button class="tools-panel__item" type="button" role="menuitem" data-action="about" title="Open project page">About</button>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
45
|
<button class="toolbar__filter toolbar__filter--view" data-action="toggle-view-mode" aria-label="Switch display mode" title="Switch display mode">
|
|
60
46
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
61
47
|
<rect x="4" y="5" width="6" height="6" rx="1.5" fill="none" stroke="currentColor" stroke-width="2" />
|
|
@@ -67,11 +53,11 @@
|
|
|
67
53
|
<button class="toolbar__filter" id="activity-toggle" aria-label="Toggle activity" aria-pressed="false" title="Toggle activity">
|
|
68
54
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 12h4l2.2-5 3.6 10 2.2-5H20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
69
55
|
</button>
|
|
70
|
-
<button class="toolbar__filter" id="
|
|
71
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="
|
|
56
|
+
<button class="toolbar__filter" id="activity-clear" aria-label="Clear local activity history" title="Clear local activity history">
|
|
57
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 7h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><path d="M9 7V5h6v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><path d="M8 10v8m4-8v8m4-8v8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
|
|
72
58
|
</button>
|
|
73
|
-
<button class="toolbar__filter" id="
|
|
74
|
-
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><
|
|
59
|
+
<button class="toolbar__filter" id="attention-toggle" aria-label="Show blocked, orphaned, unprocessed, or inconsistent items" aria-pressed="false" title="Show blocked, orphaned, unprocessed, or inconsistent items">
|
|
60
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4l8 14H4z" fill="none" stroke="currentColor" stroke-width="2" /><path d="M12 9v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><circle cx="12" cy="17" r="1" fill="currentColor" /></svg>
|
|
75
61
|
</button>
|
|
76
62
|
</div>
|
|
77
63
|
<div class="toolbar__search">
|
|
@@ -16,6 +16,27 @@
|
|
|
16
16
|
line-height: 1.25;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
.viewer-topbar__identity {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 8px;
|
|
23
|
+
min-width: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.viewer-topbar__repo {
|
|
27
|
+
max-width: 220px;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
text-overflow: ellipsis;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
border: 1px solid var(--vscode-panel-border, #333333);
|
|
32
|
+
border-radius: 999px;
|
|
33
|
+
padding: 2px 8px;
|
|
34
|
+
background: var(--vscode-badge-background, #4d4d4d);
|
|
35
|
+
color: var(--vscode-badge-foreground, #ffffff);
|
|
36
|
+
font-size: 11px;
|
|
37
|
+
line-height: 1.3;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
.viewer-topbar__meta {
|
|
20
41
|
margin-top: 2px;
|
|
21
42
|
font-size: 12px;
|
|
@@ -30,6 +51,24 @@
|
|
|
30
51
|
flex: 0 0 auto;
|
|
31
52
|
}
|
|
32
53
|
|
|
54
|
+
.viewer-auto-refresh {
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 6px;
|
|
58
|
+
min-height: 28px;
|
|
59
|
+
border: 1px solid var(--vscode-button-border, var(--vscode-panel-border, #333333));
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
padding: 0 8px;
|
|
62
|
+
background: var(--vscode-button-secondaryBackground, transparent);
|
|
63
|
+
color: var(--vscode-button-secondaryForeground, var(--vscode-foreground, #d4d4d4));
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
user-select: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.viewer-auto-refresh input {
|
|
69
|
+
margin: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
33
72
|
.viewer-update {
|
|
34
73
|
flex: 0 0 auto;
|
|
35
74
|
display: flex;
|
|
@@ -220,6 +259,103 @@
|
|
|
220
259
|
background: var(--vscode-textCodeBlock-background, #111111);
|
|
221
260
|
}
|
|
222
261
|
|
|
262
|
+
.viewer-insights__rows,
|
|
263
|
+
.viewer-git__files {
|
|
264
|
+
display: grid;
|
|
265
|
+
gap: 6px;
|
|
266
|
+
margin: 10px 0 0;
|
|
267
|
+
padding: 0;
|
|
268
|
+
list-style: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.viewer-insights__row {
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
justify-content: space-between;
|
|
275
|
+
gap: 10px;
|
|
276
|
+
min-width: 0;
|
|
277
|
+
padding: 7px 9px;
|
|
278
|
+
border: 1px solid var(--vscode-panel-border, #333333);
|
|
279
|
+
border-radius: 6px;
|
|
280
|
+
background: color-mix(in srgb, var(--vscode-editorWidget-background, #202020) 84%, transparent);
|
|
281
|
+
font-size: 12px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.viewer-insights__row[hidden] {
|
|
285
|
+
display: none !important;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.viewer-insights__doc,
|
|
289
|
+
.viewer-insights__action,
|
|
290
|
+
.viewer-insights__reveal,
|
|
291
|
+
.viewer-health__path {
|
|
292
|
+
min-width: 0;
|
|
293
|
+
border: 0;
|
|
294
|
+
padding: 0;
|
|
295
|
+
background: transparent;
|
|
296
|
+
color: var(--vscode-textLink-foreground, #4ea1ff);
|
|
297
|
+
font: inherit;
|
|
298
|
+
text-align: left;
|
|
299
|
+
overflow-wrap: anywhere;
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.viewer-insights__doc:hover,
|
|
304
|
+
.viewer-insights__action:hover,
|
|
305
|
+
.viewer-insights__reveal:hover,
|
|
306
|
+
.viewer-health__path:hover {
|
|
307
|
+
text-decoration: underline;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.viewer-git,
|
|
311
|
+
.viewer-insights,
|
|
312
|
+
.viewer-health {
|
|
313
|
+
display: grid;
|
|
314
|
+
gap: 14px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.viewer-git__summary,
|
|
318
|
+
.viewer-insights__summary,
|
|
319
|
+
.viewer-health__summary {
|
|
320
|
+
display: grid;
|
|
321
|
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
322
|
+
gap: 8px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.viewer-git__section h2 {
|
|
326
|
+
margin-bottom: 8px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.viewer-git__files li {
|
|
330
|
+
min-width: 0;
|
|
331
|
+
padding: 7px 9px;
|
|
332
|
+
border: 1px solid var(--vscode-panel-border, #333333);
|
|
333
|
+
border-radius: 6px;
|
|
334
|
+
background: var(--vscode-editorWidget-background, #202020);
|
|
335
|
+
overflow-wrap: anywhere;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.viewer-git__commit,
|
|
339
|
+
.viewer-git__state {
|
|
340
|
+
margin: 0;
|
|
341
|
+
overflow-wrap: anywhere;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@media (max-width: 700px) {
|
|
345
|
+
.viewer-topbar {
|
|
346
|
+
align-items: stretch;
|
|
347
|
+
flex-direction: column;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.viewer-topbar__actions {
|
|
351
|
+
flex-wrap: wrap;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.viewer-document {
|
|
355
|
+
inset: 8px;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
223
359
|
.viewer-document__content a {
|
|
224
360
|
color: var(--vscode-textLink-foreground, #4ea1ff);
|
|
225
361
|
}
|
|
@@ -412,12 +548,17 @@
|
|
|
412
548
|
|
|
413
549
|
.viewer-topbar__actions {
|
|
414
550
|
width: 100%;
|
|
551
|
+
flex-wrap: wrap;
|
|
415
552
|
}
|
|
416
553
|
|
|
417
554
|
.viewer-topbar__actions .btn {
|
|
418
555
|
flex: 1 1 0;
|
|
419
556
|
}
|
|
420
557
|
|
|
558
|
+
.viewer-auto-refresh {
|
|
559
|
+
flex: 0 0 auto;
|
|
560
|
+
}
|
|
561
|
+
|
|
421
562
|
.viewer-document {
|
|
422
563
|
inset: 10px;
|
|
423
564
|
}
|
package/logics_manager/viewer.py
CHANGED
|
@@ -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
|
|
@@ -328,9 +330,16 @@ def collect_viewer_items(repo_root: Path) -> list[dict[str, Any]]:
|
|
|
328
330
|
return items
|
|
329
331
|
|
|
330
332
|
|
|
331
|
-
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]:
|
|
332
339
|
return {
|
|
333
340
|
"root": str(repo_root.resolve()),
|
|
341
|
+
"repoName": repo_root.resolve().name,
|
|
342
|
+
"autoRefreshIntervalSeconds": auto_refresh_interval_seconds,
|
|
334
343
|
"items": collect_viewer_items(repo_root),
|
|
335
344
|
"updateInfo": get_update_info(_current_version()).to_payload(),
|
|
336
345
|
"selectedId": selected_id,
|
|
@@ -384,13 +393,122 @@ def _system_editor_command(path: Path) -> list[str]:
|
|
|
384
393
|
return ["xdg-open", str(path)]
|
|
385
394
|
|
|
386
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
|
+
|
|
387
498
|
def _json_bytes(payload: Any) -> bytes:
|
|
388
499
|
return json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
|
|
389
500
|
|
|
390
501
|
|
|
391
502
|
class LogicsViewerServer(ThreadingHTTPServer):
|
|
392
|
-
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
|
+
):
|
|
393
510
|
self.repo_root = repo_root.resolve()
|
|
511
|
+
self.auto_refresh_interval_seconds = auto_refresh_interval_seconds
|
|
394
512
|
super().__init__(server_address, LogicsViewerRequestHandler)
|
|
395
513
|
|
|
396
514
|
|
|
@@ -453,7 +571,15 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
453
571
|
self._serve_file(media_path)
|
|
454
572
|
return
|
|
455
573
|
if route == "/api/items":
|
|
456
|
-
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
|
+
)
|
|
457
583
|
return
|
|
458
584
|
if route == "/api/doc":
|
|
459
585
|
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
@@ -468,12 +594,23 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
468
594
|
if route == "/api/audit":
|
|
469
595
|
self._send_json({"ok": True, "payload": audit_payload(self.server.repo_root)})
|
|
470
596
|
return
|
|
597
|
+
if route == "/api/git-status":
|
|
598
|
+
self._send_json({"ok": True, "payload": git_status_payload(self.server.repo_root)})
|
|
599
|
+
return
|
|
471
600
|
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
472
601
|
|
|
473
602
|
def do_POST(self) -> None:
|
|
474
603
|
parsed = urlparse(self.path)
|
|
475
604
|
if parsed.path == "/api/refresh":
|
|
476
|
-
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
|
+
)
|
|
477
614
|
return
|
|
478
615
|
if parsed.path == "/api/edit":
|
|
479
616
|
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
@@ -487,19 +624,52 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
487
624
|
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
488
625
|
|
|
489
626
|
|
|
490
|
-
def create_viewer_server(
|
|
491
|
-
|
|
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
|
+
)
|
|
492
639
|
|
|
493
640
|
|
|
494
|
-
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:
|
|
495
662
|
lines = [
|
|
496
663
|
"Logics viewer running:",
|
|
497
|
-
url,
|
|
664
|
+
f"Local: {url}",
|
|
498
665
|
"",
|
|
499
666
|
f"Repo: {repo_root.name}",
|
|
500
667
|
"Mode: read-only",
|
|
501
|
-
"Bind:
|
|
668
|
+
f"Bind: {bind_host}",
|
|
669
|
+
f"Auto refresh: {auto_refresh_interval_seconds}s",
|
|
502
670
|
]
|
|
671
|
+
if network_url:
|
|
672
|
+
lines.insert(2, f"Network: {network_url}")
|
|
503
673
|
if focus:
|
|
504
674
|
lines.append(f"Focus: {focus}")
|
|
505
675
|
return "\n".join(lines)
|
|
@@ -509,6 +679,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
509
679
|
parser = argparse.ArgumentParser(prog="logics-manager view", description="Start the local read-only Logics browser viewer.")
|
|
510
680
|
parser.add_argument("--host", default="127.0.0.1", help="Bind host. Defaults to 127.0.0.1.")
|
|
511
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
|
+
)
|
|
512
688
|
parser.add_argument("--focus", help="Open the viewer focused on a workflow ref or repo-relative Logics Markdown path.")
|
|
513
689
|
parser.add_argument("--read", action="store_true", help="Open the focused item in the read preview. Requires --focus.")
|
|
514
690
|
parser.add_argument("--open", action="store_true", help="Open the viewer in the default browser.")
|
|
@@ -519,16 +695,34 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
519
695
|
def main(argv: list[str]) -> int:
|
|
520
696
|
args = build_parser().parse_args(argv)
|
|
521
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.")
|
|
522
700
|
if args.read and not args.focus:
|
|
523
701
|
raise SystemExit("--read requires --focus.")
|
|
524
702
|
try:
|
|
525
703
|
focus = normalize_viewer_focus_target(repo_root, args.focus) if args.focus else None
|
|
526
704
|
except ValueError as exc:
|
|
527
705
|
raise SystemExit(str(exc)) from exc
|
|
528
|
-
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
|
+
)
|
|
529
712
|
host, port = server.server_address[:2]
|
|
530
713
|
url = build_viewer_url(str(host), int(port), focus=focus, read=bool(args.read))
|
|
531
|
-
|
|
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
|
+
)
|
|
532
726
|
if args.open and not args.no_open:
|
|
533
727
|
webbrowser.open(url)
|
|
534
728
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@grifhinz/logics-manager",
|
|
3
3
|
"displayName": "Logics Orchestrator",
|
|
4
4
|
"description": "Visual orchestration for Logics workflows inside VS Code.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.4.0",
|
|
6
6
|
"publisher": "cdx-logics",
|
|
7
7
|
"icon": "clients/shared-web/media/icon.png",
|
|
8
8
|
"repository": {
|
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
"test:coverage:media": "node scripts/run-plugin-coverage.mjs media",
|
|
128
128
|
"test:coverage": "npm run test:coverage:src && npm run test:coverage:media",
|
|
129
129
|
"test:smoke": "node tests/run_extension_smoke_checks.mjs",
|
|
130
|
+
"test:viewer-smoke": "node tests/run_local_viewer_visual_smoke.mjs",
|
|
130
131
|
"test:npm-cli": "node scripts/npm/logics-manager.mjs --help",
|
|
131
132
|
"test:lifecycle": "node tests/run_plugin_lifecycle_checks.mjs",
|
|
132
133
|
"test:watch": "vitest",
|
|
@@ -155,14 +156,15 @@
|
|
|
155
156
|
"@types/vscode": "^1.86.0",
|
|
156
157
|
"@typescript-eslint/eslint-plugin": "^8.58.1",
|
|
157
158
|
"@typescript-eslint/parser": "^8.58.1",
|
|
158
|
-
"@vscode/vsce": "^3.9.1",
|
|
159
159
|
"@vitest/coverage-v8": "^4.1.2",
|
|
160
|
+
"@vscode/vsce": "^3.9.1",
|
|
160
161
|
"esbuild": "^0.25.10",
|
|
161
162
|
"eslint": "^10.2.0",
|
|
162
163
|
"jsdom": "^25.0.1",
|
|
163
164
|
"mermaid": "^11.14.0",
|
|
164
165
|
"typescript": "^5.3.3",
|
|
165
166
|
"vitest": "^4.1.2",
|
|
167
|
+
"ws": "8.21.0",
|
|
166
168
|
"yaml": "^2.8.3",
|
|
167
169
|
"yauzl": "^3.2.0"
|
|
168
170
|
},
|