@grifhinz/logics-manager 2.3.2 → 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 +213 -11
- package/package.json +4 -2
- package/pyproject.toml +17 -2
|
@@ -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
|
|
@@ -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
|
|
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
|
},
|
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "logics-manager"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.0"
|
|
8
8
|
description = "Canonical Logics CLI"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
|
|
@@ -12,4 +12,19 @@ requires-python = ">=3.10"
|
|
|
12
12
|
logics-manager = "logics_manager.cli:main"
|
|
13
13
|
|
|
14
14
|
[tool.setuptools]
|
|
15
|
-
packages = [
|
|
15
|
+
packages = [
|
|
16
|
+
"logics_manager",
|
|
17
|
+
"logics_manager.viewer_assets",
|
|
18
|
+
"logics_manager.viewer_assets.media",
|
|
19
|
+
"logics_manager.viewer_assets.media.css",
|
|
20
|
+
"logics_manager.viewer_assets.vendor",
|
|
21
|
+
"logics_manager.viewer_assets.viewer",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.package-data]
|
|
25
|
+
logics_manager = [
|
|
26
|
+
"viewer_assets/media/*",
|
|
27
|
+
"viewer_assets/media/css/*",
|
|
28
|
+
"viewer_assets/vendor/*",
|
|
29
|
+
"viewer_assets/viewer/*",
|
|
30
|
+
]
|