@grifhinz/logics-manager 2.9.2 → 2.9.3
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 +60 -3
- package/VERSION +1 -1
- package/logics_manager/sync.py +50 -32
- package/logics_manager/viewer.py +67 -25
- package/package.json +2 -2
- package/pyproject.toml +1 -1
package/README.md
CHANGED
|
@@ -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
|

|
|
@@ -51,7 +51,21 @@ The CLI owns the behavior. The extension and MCP server call into it instead of
|
|
|
51
51
|
|
|
52
52
|
## Quick Start
|
|
53
53
|
|
|
54
|
-
Install the CLI from
|
|
54
|
+
Install the CLI from PyPI:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python3.11 -m pip install logics-manager
|
|
58
|
+
logics-manager --help
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For an isolated user-level install, use `pipx`:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pipx install logics-manager
|
|
65
|
+
logics-manager --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Install the CLI from this repository when developing locally:
|
|
55
69
|
|
|
56
70
|
```bash
|
|
57
71
|
python3.11 -m pip install .
|
|
@@ -67,7 +81,7 @@ pipx ensurepath
|
|
|
67
81
|
pipx install logics-manager
|
|
68
82
|
```
|
|
69
83
|
|
|
70
|
-
Or install the npm package:
|
|
84
|
+
Or install the npm package from npm:
|
|
71
85
|
|
|
72
86
|
```bash
|
|
73
87
|
npm install -g @grifhinz/logics-manager
|
|
@@ -151,6 +165,17 @@ logics-manager view --open
|
|
|
151
165
|
|
|
152
166
|
The viewer starts a localhost-only browser UI on `127.0.0.1:8765` by default. It shows the same workflow board/list experience as the extension, with search, filters, document previews, corpus insights, lint/audit health, Mermaid rendering, auto-refresh, and an edit shortcut that opens the selected Markdown file in the system editor.
|
|
153
167
|
|
|
168
|
+
The topbar includes focused operational views:
|
|
169
|
+
|
|
170
|
+
| View | Purpose |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| Explorer | Read-only repository tree with bounded previews for text, images, directories, oversized files, and unsupported binary files. |
|
|
173
|
+
| Workshop | Local terminals and command runs. Terminals use the vendored xterm.js frontend; commands are discovered from `package.json` and `pyproject.toml` scripts and stream output over SSE. |
|
|
174
|
+
| Git | Repository status, changed files, and diffs for review-oriented inspection. |
|
|
175
|
+
| CI | Local/remote validation status surfaced for release and handoff checks. |
|
|
176
|
+
| CDX | Guarded assistant workflows for audits, release reviews, corpus planning, and pre-release preparation. |
|
|
177
|
+
| Settings | Viewer preferences, display controls, refresh behavior, and local UI state. |
|
|
178
|
+
|
|
154
179
|
Viewer preferences are stored locally in the browser profile. Auto-refresh
|
|
155
180
|
restores the interval chosen in the viewer unless the launch command explicitly
|
|
156
181
|
sets `--refresh-interval`, in which case that launch value controls only the
|
|
@@ -161,6 +186,24 @@ When workspace inspection is available, the topbar shows an `Explorer` view
|
|
|
161
186
|
before `Git`; it provides a read-only file tree and bounded previews for text,
|
|
162
187
|
directories, images, oversized files, and unsupported binary files.
|
|
163
188
|
|
|
189
|
+
The Workshop view is local-machine only. Terminal sessions and command runs are
|
|
190
|
+
created on the machine running `logics-manager view`, appear with running-count
|
|
191
|
+
badges in the topbar, and can be stopped from the UI. Terminal sessions are
|
|
192
|
+
cleaned up after the browser disconnects, while quick reloads can reattach
|
|
193
|
+
without leaving duplicate sessions behind.
|
|
194
|
+
|
|
195
|
+
For phone or tablet inspection on the same trusted network, launch with `--lan`:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
logics-manager view --lan --open
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
LAN mode binds to `0.0.0.0`, computes a reachable local-network URL, and adds a
|
|
202
|
+
per-session bearer token for non-loopback requests. The browser receives a
|
|
203
|
+
shareable URL and, when the optional `segno` package is installed, a QR code.
|
|
204
|
+
LAN mode is still read-only at the HTTP layer; workflow mutations continue to go
|
|
205
|
+
through canonical CLI commands.
|
|
206
|
+
|
|
164
207
|
The CDX missions panel includes guarded workflows for audits, release reviews,
|
|
165
208
|
turning a free-form wish into a structured Logics request, preparing a corpus
|
|
166
209
|
plan, and preparing a guarded pre-release from an editable `vX.X.X` version.
|
|
@@ -177,6 +220,7 @@ Useful options:
|
|
|
177
220
|
|
|
178
221
|
```bash
|
|
179
222
|
logics-manager view --port 0 --open
|
|
223
|
+
logics-manager view --lan --port 0 --open
|
|
180
224
|
logics-manager view --host 127.0.0.1 --port 9876
|
|
181
225
|
logics-manager view --focus req_001_example --open
|
|
182
226
|
logics-manager view --focus logics/tasks/task_001_example.md --read --open
|
|
@@ -284,6 +328,12 @@ npm list -g @grifhinz/logics-manager --depth=0
|
|
|
284
328
|
|
|
285
329
|
If the direct npm binary shows the expected version, remove the older Python install or move the npm global `bin` directory earlier on `PATH`. In zsh, run `rehash` or open a new terminal after changing installs so the shell forgets any cached command location.
|
|
286
330
|
|
|
331
|
+
When installed with `pipx` from a local path, `self-update` reports that original
|
|
332
|
+
spec, for example `from spec '/path/to/logics-manager'`. That installation is
|
|
333
|
+
updated from the local working tree, not from the PyPI artifact. Use
|
|
334
|
+
`pipx uninstall logics-manager && pipx install logics-manager` when you want to
|
|
335
|
+
switch back to the published PyPI package.
|
|
336
|
+
|
|
287
337
|
## VS Code Extension
|
|
288
338
|
|
|
289
339
|
The VS Code extension is the human cockpit around the same runtime. It helps you:
|
|
@@ -377,6 +427,13 @@ python3 -m logics_manager mcp connect --repo-root . --public-url https://example
|
|
|
377
427
|
|
|
378
428
|
The connector plan prints the bearer token when used, server command, tunnel target, assistant connector URL, auth mode, auth header, smoke checks, warnings, and cleanup steps.
|
|
379
429
|
|
|
430
|
+
## Security
|
|
431
|
+
|
|
432
|
+
See [SECURITY.md](SECURITY.md) for supported versions and vulnerability
|
|
433
|
+
reporting guidance. Do not publish suspected vulnerabilities in public issues
|
|
434
|
+
until they are triaged; use GitHub's private vulnerability reporting or a
|
|
435
|
+
private security advisory draft for this repository.
|
|
436
|
+
|
|
380
437
|
## Assistant Model
|
|
381
438
|
|
|
382
439
|
The project is local-first:
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.9.
|
|
1
|
+
2.9.3
|
package/logics_manager/sync.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
7
|
from copy import deepcopy
|
|
7
8
|
from dataclasses import dataclass
|
|
@@ -43,12 +44,28 @@ APPROVED_WORKFLOW_INDICATORS = ("Status", "Progress", "Understanding", "Confiden
|
|
|
43
44
|
MAX_MUTATION_TEXT_CHARS = 2000
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def _read_text(path: Path) -> str:
|
|
47
|
-
|
|
47
|
+
def _read_text(repo_root: Path, path: Path) -> str:
|
|
48
|
+
root = os.path.realpath(repo_root)
|
|
49
|
+
absolute_name = os.path.realpath(path)
|
|
50
|
+
try:
|
|
51
|
+
common = os.path.commonpath([root, absolute_name])
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
raise SystemExit(f"Unsupported workflow doc path `{path}`.") from exc
|
|
54
|
+
if common != root:
|
|
55
|
+
raise SystemExit(f"Unsupported workflow doc path `{path}`.")
|
|
56
|
+
absolute = Path(absolute_name)
|
|
57
|
+
approved_dirs = {Path(os.path.realpath(repo_root / kind["directory"])) for kind in DOC_KINDS.values()}
|
|
58
|
+
if absolute.parent not in approved_dirs:
|
|
59
|
+
raise SystemExit(f"Unsupported workflow doc path `{path}`.")
|
|
60
|
+
return absolute.read_text(encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _read_lines(repo_root: Path, path: Path) -> list[str]:
|
|
64
|
+
return _read_text(repo_root, path).splitlines()
|
|
48
65
|
|
|
49
66
|
|
|
50
|
-
def
|
|
51
|
-
return
|
|
67
|
+
def _is_relative_path(path: Path) -> bool:
|
|
68
|
+
return not path.is_absolute() and ".." not in path.parts
|
|
52
69
|
|
|
53
70
|
|
|
54
71
|
def _indicator_value(lines: list[str], key: str) -> str | None:
|
|
@@ -128,7 +145,7 @@ def _detect_workflow_kind(path: Path) -> str:
|
|
|
128
145
|
|
|
129
146
|
|
|
130
147
|
def parse_workflow_doc(path: Path, *, repo_root: Path | None = None) -> WorkflowDocModel:
|
|
131
|
-
text = _read_text(path)
|
|
148
|
+
text = _read_text(repo_root or path.parent.parent.parent, path)
|
|
132
149
|
lines = text.splitlines()
|
|
133
150
|
sections = _extract_sections(text)
|
|
134
151
|
indicators = {key: value for key in ("From version", "Schema version", "Status", "Understanding", "Confidence", "Progress", "Complexity", "Theme", "Date", "Drivers", "Related request", "Related backlog", "Related task", "Reminder") if (value := _indicator_value(lines, key)) is not None}
|
|
@@ -307,6 +324,7 @@ def _build_context_pack(
|
|
|
307
324
|
|
|
308
325
|
|
|
309
326
|
def _resolve_target_docs(repo_root: Path, sources: list[str]) -> list[tuple[str, Path]]:
|
|
327
|
+
candidates: dict[str, tuple[str, Path]] = {}
|
|
310
328
|
if not sources:
|
|
311
329
|
targets: list[tuple[str, Path]] = []
|
|
312
330
|
for kind_name, kind in DOC_KINDS.items():
|
|
@@ -317,25 +335,25 @@ def _resolve_target_docs(repo_root: Path, sources: list[str]) -> list[tuple[str,
|
|
|
317
335
|
targets.append((kind_name, path))
|
|
318
336
|
return targets
|
|
319
337
|
|
|
338
|
+
for kind_name, kind in DOC_KINDS.items():
|
|
339
|
+
directory = repo_root / kind["directory"]
|
|
340
|
+
if not directory.is_dir():
|
|
341
|
+
continue
|
|
342
|
+
for path in sorted(directory.glob(f"{kind['prefix']}_*.md")):
|
|
343
|
+
candidates[path.relative_to(repo_root).as_posix()] = (kind_name, path)
|
|
344
|
+
candidates[path.stem] = (kind_name, path)
|
|
345
|
+
|
|
320
346
|
resolved: list[tuple[str, Path]] = []
|
|
321
347
|
for source in sources:
|
|
322
348
|
raw_source = Path(source)
|
|
323
|
-
if
|
|
349
|
+
if not _is_relative_path(raw_source):
|
|
324
350
|
raise SystemExit(f"Unsupported workflow doc target `{source}`.")
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
resolved.append((kind_name, candidate))
|
|
330
|
-
break
|
|
351
|
+
normalized = raw_source.as_posix()
|
|
352
|
+
target = candidates.get(normalized) or candidates.get(Path(source).stem if "/" not in normalized else "")
|
|
353
|
+
if target is not None:
|
|
354
|
+
resolved.append(target)
|
|
331
355
|
continue
|
|
332
|
-
|
|
333
|
-
path = repo_root / kind["directory"] / f"{source}.md"
|
|
334
|
-
if path.is_file():
|
|
335
|
-
resolved.append((kind_name, path))
|
|
336
|
-
break
|
|
337
|
-
else:
|
|
338
|
-
raise SystemExit(f"Could not resolve workflow doc target `{source}`.")
|
|
356
|
+
raise SystemExit(f"Could not resolve workflow doc target `{source}`.")
|
|
339
357
|
return resolved
|
|
340
358
|
|
|
341
359
|
|
|
@@ -385,7 +403,7 @@ def read_logics_doc_payload(repo_root: Path, source: str, *, max_chars: int = 40
|
|
|
385
403
|
for heading in requested_sections
|
|
386
404
|
if heading in doc.sections
|
|
387
405
|
}
|
|
388
|
-
text = _read_text(path)
|
|
406
|
+
text = _read_text(repo_root, path)
|
|
389
407
|
return {
|
|
390
408
|
"ref": doc.ref,
|
|
391
409
|
"kind": doc.kind,
|
|
@@ -464,7 +482,7 @@ def search_logics_docs_payload(
|
|
|
464
482
|
doc = docs_by_ref.get(ref)
|
|
465
483
|
if doc is None:
|
|
466
484
|
continue
|
|
467
|
-
text = _strip_mermaid_blocks(_read_text(repo_root / doc.path))
|
|
485
|
+
text = _strip_mermaid_blocks(_read_text(repo_root, repo_root / doc.path))
|
|
468
486
|
lines = text.splitlines()
|
|
469
487
|
for idx, line in enumerate(lines):
|
|
470
488
|
if normalized_query in line.lower():
|
|
@@ -531,7 +549,7 @@ def update_workflow_indicators_payload(repo_root: Path, source: str, indicators:
|
|
|
531
549
|
if len(targets) != 1:
|
|
532
550
|
raise SystemExit(f"Expected one workflow doc target for `{source}`.")
|
|
533
551
|
kind, path = targets[0]
|
|
534
|
-
lines = _read_lines(path)
|
|
552
|
+
lines = _read_lines(repo_root, path)
|
|
535
553
|
changed = False
|
|
536
554
|
for key in APPROVED_WORKFLOW_INDICATORS:
|
|
537
555
|
if key not in cleaned:
|
|
@@ -573,7 +591,7 @@ def append_workflow_note_payload(repo_root: Path, source: str, *, note_kind: str
|
|
|
573
591
|
section = _section_for_note(kind, note_kind)
|
|
574
592
|
cleaned = _clean_mutation_text(text, field="text")
|
|
575
593
|
bullet = f"- {cleaned}"
|
|
576
|
-
lines = _read_lines(path)
|
|
594
|
+
lines = _read_lines(repo_root, path)
|
|
577
595
|
insert_at = None
|
|
578
596
|
for idx, line in enumerate(lines):
|
|
579
597
|
if line.startswith("# ") and line[2:].strip().lower() == section.lower():
|
|
@@ -631,13 +649,13 @@ def _collect_docs_linking_ref(repo_root: Path, kind: str, ref: str) -> list[Path
|
|
|
631
649
|
directory = repo_root / DOC_KINDS[kind]["directory"]
|
|
632
650
|
linked: list[Path] = []
|
|
633
651
|
for path in sorted(directory.glob("*.md")):
|
|
634
|
-
if ref in _read_text(path):
|
|
652
|
+
if ref in _read_text(repo_root, path):
|
|
635
653
|
linked.append(path)
|
|
636
654
|
return linked
|
|
637
655
|
|
|
638
656
|
|
|
639
|
-
def _is_doc_done(path: Path, kind: str) -> bool:
|
|
640
|
-
lines = _read_lines(path)
|
|
657
|
+
def _is_doc_done(repo_root: Path, path: Path, kind: str) -> bool:
|
|
658
|
+
lines = _read_lines(repo_root, path)
|
|
641
659
|
status_value = _indicator_value(lines, "Status")
|
|
642
660
|
if status_value is not None and " ".join(status_value.split()).lower() in {"done", "archived"}:
|
|
643
661
|
return True
|
|
@@ -648,10 +666,10 @@ def _is_doc_done(path: Path, kind: str) -> bool:
|
|
|
648
666
|
return False
|
|
649
667
|
|
|
650
668
|
|
|
651
|
-
def _close_doc(path: Path, kind: str, dry_run: bool) -> None:
|
|
669
|
+
def _close_doc(repo_root: Path, path: Path, kind: str, dry_run: bool) -> None:
|
|
652
670
|
if dry_run:
|
|
653
671
|
return
|
|
654
|
-
lines = _read_lines(path)
|
|
672
|
+
lines = _read_lines(repo_root, path)
|
|
655
673
|
updated = []
|
|
656
674
|
saw_status = False
|
|
657
675
|
saw_progress = False
|
|
@@ -696,7 +714,7 @@ def _refresh_workflow_mermaid_signature_text(text: str, kind: str, *, repo_root:
|
|
|
696
714
|
|
|
697
715
|
|
|
698
716
|
def refresh_workflow_mermaid_signature_file(path: Path, kind: str, dry_run: bool, *, repo_root: Path | None = None) -> bool:
|
|
699
|
-
original = _read_text(path)
|
|
717
|
+
original = _read_text(repo_root or path.parent.parent.parent, path)
|
|
700
718
|
refreshed, changed = _refresh_workflow_mermaid_signature_text(original, kind, repo_root=repo_root, dry_run=dry_run)
|
|
701
719
|
if not changed:
|
|
702
720
|
return False
|
|
@@ -711,14 +729,14 @@ def _close_eligible_requests(repo_root: Path, dry_run: bool, *, quiet: bool = Fa
|
|
|
711
729
|
scanned = 0
|
|
712
730
|
for request_path in sorted(request_dir.glob("req_*.md")):
|
|
713
731
|
scanned += 1
|
|
714
|
-
if _is_doc_done(request_path, "request"):
|
|
732
|
+
if _is_doc_done(repo_root, request_path, "request"):
|
|
715
733
|
continue
|
|
716
734
|
request_ref = request_path.stem
|
|
717
735
|
linked_items = _collect_docs_linking_ref(repo_root, "backlog", request_ref)
|
|
718
736
|
if not linked_items:
|
|
719
737
|
continue
|
|
720
|
-
if all(_is_doc_done(item_path, "backlog") for item_path in linked_items):
|
|
721
|
-
_close_doc(request_path, "request", dry_run)
|
|
738
|
+
if all(_is_doc_done(repo_root, item_path, "backlog") for item_path in linked_items):
|
|
739
|
+
_close_doc(repo_root, request_path, "request", dry_run)
|
|
722
740
|
if not quiet:
|
|
723
741
|
print(f"Auto-closed request {request_ref} (all linked backlog items are done).")
|
|
724
742
|
closed += 1
|
package/logics_manager/viewer.py
CHANGED
|
@@ -692,8 +692,10 @@ def _resolve_repo_doc_path(repo_root: Path, rel_path: str) -> tuple[str, Path]:
|
|
|
692
692
|
def edit_doc_payload(repo_root: Path, rel_path: str, *, launcher: Any | None = None) -> dict[str, str]:
|
|
693
693
|
normalized, absolute = _resolve_repo_doc_path(repo_root, rel_path)
|
|
694
694
|
command = _system_editor_command(absolute)
|
|
695
|
-
|
|
696
|
-
|
|
695
|
+
if launcher is not None:
|
|
696
|
+
launcher(command)
|
|
697
|
+
else:
|
|
698
|
+
webbrowser.open(absolute.as_uri())
|
|
697
699
|
return {
|
|
698
700
|
"path": normalized,
|
|
699
701
|
"command": command[0],
|
|
@@ -701,13 +703,25 @@ def edit_doc_payload(repo_root: Path, rel_path: str, *, launcher: Any | None = N
|
|
|
701
703
|
|
|
702
704
|
|
|
703
705
|
def _resolve_openable_file_path(repo_root: Path, file_path: str) -> Path:
|
|
704
|
-
|
|
705
|
-
if
|
|
706
|
+
raw_value = unquote(file_path).strip()
|
|
707
|
+
if raw_value.startswith(("/", "\\")) or re.match(r"^[A-Za-z]:", raw_value):
|
|
708
|
+
raise ValueError("File path escapes repository root.")
|
|
709
|
+
normalized = raw_value.replace("\\", "/").lstrip("/")
|
|
710
|
+
if not normalized:
|
|
706
711
|
raise ValueError("Missing file path.")
|
|
707
|
-
|
|
708
|
-
if
|
|
709
|
-
|
|
710
|
-
|
|
712
|
+
raw_parts = tuple(part for part in normalized.split("/") if part)
|
|
713
|
+
if any(part == ".." for part in raw_parts):
|
|
714
|
+
raise ValueError("File path escapes repository root.")
|
|
715
|
+
candidate = repo_root.joinpath(*raw_parts)
|
|
716
|
+
root = os.path.realpath(repo_root)
|
|
717
|
+
absolute_name = os.path.realpath(candidate)
|
|
718
|
+
try:
|
|
719
|
+
common = os.path.commonpath([root, absolute_name])
|
|
720
|
+
except ValueError as exc:
|
|
721
|
+
raise ValueError("File path escapes repository root.") from exc
|
|
722
|
+
if common != root:
|
|
723
|
+
raise ValueError("File path escapes repository root.")
|
|
724
|
+
absolute = Path(absolute_name)
|
|
711
725
|
if not absolute.is_file():
|
|
712
726
|
raise FileNotFoundError(str(candidate))
|
|
713
727
|
return absolute
|
|
@@ -716,8 +730,10 @@ def _resolve_openable_file_path(repo_root: Path, file_path: str) -> Path:
|
|
|
716
730
|
def open_file_payload(repo_root: Path, file_path: str, *, launcher: Any | None = None) -> dict[str, str]:
|
|
717
731
|
absolute = _resolve_openable_file_path(repo_root, file_path)
|
|
718
732
|
command = _system_editor_command(absolute)
|
|
719
|
-
|
|
720
|
-
|
|
733
|
+
if launcher is not None:
|
|
734
|
+
launcher(command)
|
|
735
|
+
else:
|
|
736
|
+
webbrowser.open(absolute.as_uri())
|
|
721
737
|
return {
|
|
722
738
|
"path": str(absolute),
|
|
723
739
|
"command": command[0],
|
|
@@ -751,8 +767,10 @@ def file_preview_payload(
|
|
|
751
767
|
def open_repo_folder_payload(repo_root: Path, *, launcher: Any | None = None) -> dict[str, str]:
|
|
752
768
|
root = repo_root.resolve()
|
|
753
769
|
command = _system_editor_command(root)
|
|
754
|
-
|
|
755
|
-
|
|
770
|
+
if launcher is not None:
|
|
771
|
+
launcher(command)
|
|
772
|
+
else:
|
|
773
|
+
webbrowser.open(root.as_uri())
|
|
756
774
|
return {
|
|
757
775
|
"path": str(root),
|
|
758
776
|
"command": command[0],
|
|
@@ -763,10 +781,22 @@ def _system_editor_command(path: Path) -> list[str]:
|
|
|
763
781
|
if sys.platform == "darwin":
|
|
764
782
|
return ["open", str(path)]
|
|
765
783
|
if os.name == "nt":
|
|
766
|
-
return ["
|
|
784
|
+
return ["explorer.exe", str(path)]
|
|
767
785
|
return ["xdg-open", str(path)]
|
|
768
786
|
|
|
769
787
|
|
|
788
|
+
STATIC_CONTENT_TYPES = {
|
|
789
|
+
".css": "text/css; charset=utf-8",
|
|
790
|
+
".html": "text/html; charset=utf-8",
|
|
791
|
+
".js": "text/javascript; charset=utf-8",
|
|
792
|
+
".json": "application/json; charset=utf-8",
|
|
793
|
+
".map": "application/json; charset=utf-8",
|
|
794
|
+
".png": "image/png",
|
|
795
|
+
".svg": "image/svg+xml",
|
|
796
|
+
".wasm": "application/wasm",
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
|
|
770
800
|
def _run_read_only_git(repo_root: Path, args: list[str], *, runner: Any | None = None) -> subprocess.CompletedProcess[str]:
|
|
771
801
|
command = ["git", *args]
|
|
772
802
|
git_runner = runner or subprocess.run
|
|
@@ -3074,14 +3104,23 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
3074
3104
|
def _send_error_json(self, status: HTTPStatus, message: str) -> None:
|
|
3075
3105
|
self._send_json({"ok": False, "error": message}, status=status.value)
|
|
3076
3106
|
|
|
3077
|
-
def _serve_file(self, path: Path) -> None:
|
|
3078
|
-
|
|
3107
|
+
def _serve_file(self, path: Path, *, root: Path) -> None:
|
|
3108
|
+
root_name = os.path.realpath(root)
|
|
3109
|
+
absolute_name = os.path.realpath(path)
|
|
3110
|
+
try:
|
|
3111
|
+
common = os.path.commonpath([root_name, absolute_name])
|
|
3112
|
+
except ValueError:
|
|
3113
|
+
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
3114
|
+
return
|
|
3115
|
+
if common != root_name:
|
|
3116
|
+
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
3117
|
+
return
|
|
3118
|
+
absolute = Path(absolute_name)
|
|
3119
|
+
if not absolute.is_file(): # lgtm [py/path-injection]
|
|
3079
3120
|
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
3080
3121
|
return
|
|
3081
|
-
content_type =
|
|
3082
|
-
|
|
3083
|
-
content_type = f"{content_type}; charset=utf-8"
|
|
3084
|
-
self._send_bytes(path.read_bytes(), content_type=content_type)
|
|
3122
|
+
content_type = STATIC_CONTENT_TYPES.get(absolute.suffix.lower(), "application/octet-stream")
|
|
3123
|
+
self._send_bytes(absolute.read_bytes(), content_type=content_type) # lgtm [py/path-injection]
|
|
3085
3124
|
|
|
3086
3125
|
def _stream_workshop_terminal(self, session: "WorkshopTerminalSession", parsed: Any) -> None:
|
|
3087
3126
|
import time as _time
|
|
@@ -3254,28 +3293,31 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
3254
3293
|
return
|
|
3255
3294
|
route = parsed.path
|
|
3256
3295
|
if route == "/":
|
|
3257
|
-
self._serve_file(VIEWER_ROOT / "index.html")
|
|
3296
|
+
self._serve_file(VIEWER_ROOT / "index.html", root=VIEWER_ROOT)
|
|
3258
3297
|
return
|
|
3259
3298
|
if route == "/browser-host.js":
|
|
3260
|
-
self._serve_file(VIEWER_ROOT / "browser-host.js")
|
|
3299
|
+
self._serve_file(VIEWER_ROOT / "browser-host.js", root=VIEWER_ROOT)
|
|
3261
3300
|
return
|
|
3262
3301
|
if route == "/viewer.css":
|
|
3263
|
-
self._serve_file(VIEWER_ROOT / "viewer.css")
|
|
3302
|
+
self._serve_file(VIEWER_ROOT / "viewer.css", root=VIEWER_ROOT)
|
|
3264
3303
|
return
|
|
3265
3304
|
if route == "/vendor/mermaid.min.js":
|
|
3266
3305
|
vendor_path = DIST_VENDOR_ROOT / "mermaid.min.js"
|
|
3306
|
+
vendor_root = DIST_VENDOR_ROOT
|
|
3267
3307
|
if not vendor_path.is_file():
|
|
3268
3308
|
vendor_path = NODE_MERMAID_ROOT / "mermaid.min.js"
|
|
3309
|
+
vendor_root = NODE_MERMAID_ROOT
|
|
3269
3310
|
if not vendor_path.is_file():
|
|
3270
3311
|
vendor_path = PACKAGE_VENDOR_ROOT / "mermaid.min.js"
|
|
3271
|
-
|
|
3312
|
+
vendor_root = PACKAGE_VENDOR_ROOT
|
|
3313
|
+
self._serve_file(vendor_path, root=vendor_root)
|
|
3272
3314
|
return
|
|
3273
3315
|
if route.startswith("/media/"):
|
|
3274
3316
|
media_path = (SHARED_MEDIA_ROOT / route.removeprefix("/media/")).resolve()
|
|
3275
3317
|
if SHARED_MEDIA_ROOT.resolve() != media_path and SHARED_MEDIA_ROOT.resolve() not in media_path.parents:
|
|
3276
3318
|
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
3277
3319
|
return
|
|
3278
|
-
self._serve_file(media_path)
|
|
3320
|
+
self._serve_file(media_path, root=SHARED_MEDIA_ROOT)
|
|
3279
3321
|
return
|
|
3280
3322
|
if route == "/api/items":
|
|
3281
3323
|
self._send_json(
|
|
@@ -3399,7 +3441,7 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
3399
3441
|
self._send_error_json(HTTPStatus.BAD_REQUEST, "Workspace file is not an image preview.")
|
|
3400
3442
|
return
|
|
3401
3443
|
_normalized, absolute = _resolve_workspace_path(self.server.repo_root, rel_path)
|
|
3402
|
-
self._serve_file(absolute)
|
|
3444
|
+
self._serve_file(absolute, root=self.server.repo_root)
|
|
3403
3445
|
except (FileNotFoundError, ValueError) as exc:
|
|
3404
3446
|
self._send_error_json(HTTPStatus.NOT_FOUND, str(exc))
|
|
3405
3447
|
return
|
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.9.
|
|
5
|
+
"version": "2.9.3",
|
|
6
6
|
"publisher": "cdx-logics",
|
|
7
7
|
"icon": "clients/shared-web/media/icon.png",
|
|
8
8
|
"repository": {
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"@typescript-eslint/parser": "^8.58.1",
|
|
160
160
|
"@vitest/coverage-v8": "^4.1.2",
|
|
161
161
|
"@vscode/vsce": "^3.9.1",
|
|
162
|
-
"esbuild": "^0.
|
|
162
|
+
"esbuild": "^0.28.1",
|
|
163
163
|
"eslint": "^10.2.0",
|
|
164
164
|
"jsdom": "^25.0.1",
|
|
165
165
|
"mermaid": "^11.14.0",
|