@grifhinz/logics-manager 2.3.3 → 2.5.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.
@@ -25,8 +25,8 @@ KINDS = {
25
25
  "request": Kind("logics/request", "req", False, ("From version", "Understanding", "Confidence"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
26
26
  "backlog": Kind("logics/backlog", "item", True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
27
27
  "task": Kind("logics/tasks", "task", True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
28
- "product": Kind("logics/product", "prod", False, ("Date", "Status", "Related request", "Related backlog", "Related task", "Related architecture", "Reminder"), ("Draft", "Proposed", "Active", "Validated", "Rejected", "Superseded", "Archived")),
29
- "architecture": Kind("logics/architecture", "adr", False, ("Date", "Status", "Drivers", "Related request", "Related backlog", "Related task", "Reminder"), ("Draft", "Proposed", "Accepted", "Rejected", "Superseded", "Archived")),
28
+ "product": Kind("logics/product", "prod", False, ("Date", "Status", "Related request", "Related backlog", "Related task", "Related architecture", "Reminder"), ("Draft", "Proposed", "Active", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
29
+ "architecture": Kind("logics/architecture", "adr", False, ("Date", "Status", "Drivers", "Related request", "Related backlog", "Related task", "Reminder"), ("Draft", "Proposed", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
30
30
  }
31
31
 
32
32
  WORKFLOW_KINDS = {"request", "backlog", "task"}
@@ -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(repo_root: Path, selected_id: str | None = None) -> dict[str, Any]:
333
+ def viewer_data_payload(
334
+ repo_root: Path,
335
+ selected_id: str | None = None,
336
+ *,
337
+ auto_refresh_interval_seconds: int = 15,
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,285 @@ 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 _run_read_only_cdx(repo_root: Path, args: list[str], *, runner: Any | None = None) -> subprocess.CompletedProcess[str]:
403
+ command = ["cdx", *args]
404
+ cdx_runner = runner or subprocess.run
405
+ return cdx_runner(command, cwd=repo_root, text=True, capture_output=True, timeout=5)
406
+
407
+
408
+ def _logics_doc_type(rel_path: str) -> str:
409
+ normalized = rel_path.replace("\\", "/").lstrip("/")
410
+ for family in DOC_FAMILIES:
411
+ if normalized.startswith(f"{family.directory}/"):
412
+ return family.stage
413
+ return ""
414
+
415
+
416
+ def _sanitize_git_ref(value: str) -> str:
417
+ ref = value.strip()
418
+ ref = re.sub(r"://[^/@\s]+@", "://", ref)
419
+ ref = re.sub(r"^[^/@\s]+@", "", ref)
420
+ return ref[:200]
421
+
422
+
423
+ def _classify_porcelain_entry(line: str) -> tuple[str, dict[str, str]] | None:
424
+ if not line or line.startswith("## "):
425
+ return None
426
+ if line.startswith("?? "):
427
+ path = line[3:].strip()
428
+ return "untracked", {"path": path, "logicsType": _logics_doc_type(path)}
429
+ if len(line) < 4:
430
+ return None
431
+ staged = line[0]
432
+ worktree = line[1]
433
+ raw_path = line[3:].strip()
434
+ if " -> " in raw_path:
435
+ before, after = raw_path.split(" -> ", 1)
436
+ path = after.strip()
437
+ return "renamed", {"path": path, "from": before.strip(), "logicsType": _logics_doc_type(path)}
438
+ if staged == "R":
439
+ return "renamed", {"path": raw_path, "logicsType": _logics_doc_type(raw_path)}
440
+ if staged not in {" ", "?", "!"}:
441
+ return "staged", {"path": raw_path, "code": staged, "logicsType": _logics_doc_type(raw_path)}
442
+ if worktree == "D":
443
+ return "deleted", {"path": raw_path, "code": worktree, "logicsType": _logics_doc_type(raw_path)}
444
+ if worktree not in {" ", "?", "!"}:
445
+ return "modified", {"path": raw_path, "code": worktree, "logicsType": _logics_doc_type(raw_path)}
446
+ return None
447
+
448
+
449
+ def _parse_git_branch_line(line: str) -> dict[str, Any]:
450
+ branch = line[3:].strip() if line.startswith("## ") else ""
451
+ tracking = ""
452
+ ahead = 0
453
+ behind = 0
454
+ if "..." in branch:
455
+ branch, tracking_part = branch.split("...", 1)
456
+ if " [" in tracking_part:
457
+ tracking, details = tracking_part.split(" [", 1)
458
+ for detail in details.rstrip("]").split(", "):
459
+ if detail.startswith("ahead "):
460
+ ahead = int(detail.removeprefix("ahead ") or "0")
461
+ if detail.startswith("behind "):
462
+ behind = int(detail.removeprefix("behind ") or "0")
463
+ else:
464
+ tracking = tracking_part
465
+ return {
466
+ "branch": _sanitize_git_ref(branch or "HEAD"),
467
+ "tracking": _sanitize_git_ref(tracking),
468
+ "ahead": ahead,
469
+ "behind": behind,
470
+ }
471
+
472
+
473
+ def _parse_recent_git_commits(output: str) -> list[dict[str, str]]:
474
+ commits: list[dict[str, str]] = []
475
+ for line in output.splitlines():
476
+ parts = line.split("\x1f")
477
+ if len(parts) < 5:
478
+ continue
479
+ commit_hash, subject, author, date, refs = parts[:5]
480
+ commits.append(
481
+ {
482
+ "hash": _sanitize_git_ref(commit_hash),
483
+ "subject": subject.strip()[:240],
484
+ "author": author.strip()[:120],
485
+ "date": date.strip()[:40],
486
+ "refs": _sanitize_git_ref(refs),
487
+ }
488
+ )
489
+ return commits
490
+
491
+
492
+ def _count_unique_git_status_paths(groups: dict[str, list[dict[str, str]]]) -> int:
493
+ paths: set[str] = set()
494
+ for entries in groups.values():
495
+ for entry in entries:
496
+ path = entry.get("path", "").strip()
497
+ if path:
498
+ paths.add(path)
499
+ return len(paths)
500
+
501
+
502
+ def _git_unpushed_commit_count(repo_root: Path, *, runner: Any | None = None) -> dict[str, Any]:
503
+ try:
504
+ upstream = _run_read_only_git(repo_root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], runner=runner)
505
+ except (OSError, subprocess.SubprocessError) as exc:
506
+ return {"available": False, "count": 0, "message": f"Unable to inspect upstream: {exc}"}
507
+ if upstream.returncode != 0:
508
+ return {"available": False, "count": 0, "message": "No upstream branch detected."}
509
+
510
+ tracking = _sanitize_git_ref(upstream.stdout.strip())
511
+ try:
512
+ unpushed = _run_read_only_git(repo_root, ["rev-list", "--count", "@{u}..HEAD"], runner=runner)
513
+ except (OSError, subprocess.SubprocessError) as exc:
514
+ return {"available": False, "count": 0, "tracking": tracking, "message": f"Unable to count unpushed commits: {exc}"}
515
+ if unpushed.returncode != 0:
516
+ message = (unpushed.stderr or unpushed.stdout or "Unable to count unpushed commits.").strip().splitlines()[0]
517
+ return {"available": False, "count": 0, "tracking": tracking, "message": message}
518
+
519
+ try:
520
+ count = max(0, int(unpushed.stdout.strip() or "0"))
521
+ except ValueError:
522
+ count = 0
523
+ return {"available": True, "count": count, "tracking": tracking, "message": ""}
524
+
525
+
526
+ def git_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any | None = None) -> dict[str, Any]:
527
+ git_which = which or shutil.which
528
+ if not git_which("git"):
529
+ return {"state": "unavailable", "message": "Git is not available on PATH."}
530
+ try:
531
+ inside = _run_read_only_git(repo_root, ["rev-parse", "--is-inside-work-tree"], runner=runner)
532
+ except (OSError, subprocess.SubprocessError) as exc:
533
+ return {"state": "error", "message": f"Unable to run Git status: {exc}"}
534
+ if inside.returncode != 0 or inside.stdout.strip().lower() != "true":
535
+ return {"state": "not-repository", "message": "This folder is not inside a Git worktree."}
536
+
537
+ try:
538
+ status = _run_read_only_git(repo_root, ["status", "--porcelain=v1", "-b"], runner=runner)
539
+ commit = _run_read_only_git(repo_root, ["log", "-1", "--pretty=format:%h %s"], runner=runner)
540
+ recent_commits = _run_read_only_git(
541
+ repo_root,
542
+ ["log", "-8", "--date=short", "--pretty=format:%h%x1f%s%x1f%an%x1f%ad%x1f%D"],
543
+ runner=runner,
544
+ )
545
+ unpushed = _git_unpushed_commit_count(repo_root, runner=runner)
546
+ except (OSError, subprocess.SubprocessError) as exc:
547
+ return {"state": "error", "message": f"Unable to collect Git status: {exc}"}
548
+ if status.returncode != 0:
549
+ message = (status.stderr or status.stdout or "Git status failed.").strip().splitlines()[0]
550
+ return {"state": "error", "message": message}
551
+
552
+ lines = status.stdout.splitlines()
553
+ branch_info = _parse_git_branch_line(lines[0]) if lines else {"branch": "HEAD", "tracking": "", "ahead": 0, "behind": 0}
554
+ groups: dict[str, list[dict[str, str]]] = {key: [] for key in ("staged", "modified", "deleted", "renamed", "untracked")}
555
+ for line in lines[1:]:
556
+ classified = _classify_porcelain_entry(line)
557
+ if classified:
558
+ group, entry = classified
559
+ groups[group].append(entry)
560
+ counts = {key: len(value) for key, value in groups.items()}
561
+ uncommitted_files = _count_unique_git_status_paths(groups)
562
+ dirty = any(counts.values())
563
+ return {
564
+ "state": "ok",
565
+ **branch_info,
566
+ "clean": not dirty,
567
+ "dirty": dirty,
568
+ "counts": counts,
569
+ "badgeCounts": {
570
+ "unpushedCommits": int(unpushed.get("count", 0)),
571
+ "uncommittedFiles": uncommitted_files,
572
+ },
573
+ "badgeAvailability": {
574
+ "unpushedCommits": bool(unpushed.get("available")),
575
+ "uncommittedFiles": True,
576
+ },
577
+ "badgeMessages": {
578
+ "unpushedCommits": str(unpushed.get("message", "")),
579
+ "uncommittedFiles": "",
580
+ },
581
+ "groups": groups,
582
+ "latestCommit": (commit.stdout.strip() if commit.returncode == 0 else "")[:300],
583
+ "recentCommits": _parse_recent_git_commits(recent_commits.stdout) if recent_commits.returncode == 0 else [],
584
+ }
585
+
586
+
587
+ def git_diff_payload(
588
+ repo_root: Path,
589
+ rel_path: str,
590
+ *,
591
+ cached: bool = False,
592
+ max_chars: int = 20000,
593
+ runner: Any | None = None,
594
+ which: Any | None = None,
595
+ ) -> dict[str, Any]:
596
+ git_which = which or shutil.which
597
+ if not git_which("git"):
598
+ return {"state": "unavailable", "message": "Git is not available on PATH."}
599
+ normalized = unquote(rel_path).replace("\\", "/").lstrip("/")
600
+ if not normalized or normalized.startswith("~") or normalized.startswith("/") or ".." in normalized.split("/"):
601
+ return {"state": "error", "message": "Unsafe Git path."}
602
+ try:
603
+ inside = _run_read_only_git(repo_root, ["rev-parse", "--is-inside-work-tree"], runner=runner)
604
+ except (OSError, subprocess.SubprocessError) as exc:
605
+ return {"state": "error", "message": f"Unable to run Git diff: {exc}"}
606
+ if inside.returncode != 0 or inside.stdout.strip().lower() != "true":
607
+ return {"state": "not-repository", "message": "This folder is not inside a Git worktree."}
608
+
609
+ args = ["diff", "--no-ext-diff", "--unified=80"]
610
+ if cached:
611
+ args.append("--cached")
612
+ args.extend(["--", normalized])
613
+ try:
614
+ diff = _run_read_only_git(repo_root, args, runner=runner)
615
+ except (OSError, subprocess.SubprocessError) as exc:
616
+ return {"state": "error", "message": f"Unable to collect Git diff: {exc}"}
617
+ if diff.returncode != 0:
618
+ message = (diff.stderr or diff.stdout or "Git diff failed.").strip().splitlines()[0]
619
+ return {"state": "error", "message": message}
620
+ content = diff.stdout
621
+ truncated = len(content) > max_chars
622
+ if truncated:
623
+ content = content[:max_chars]
624
+ return {
625
+ "state": "ok",
626
+ "path": normalized,
627
+ "mode": "staged" if cached else "worktree",
628
+ "diff": content,
629
+ "truncated": truncated,
630
+ "logicsType": _logics_doc_type(normalized),
631
+ "message": "" if content else "No diff is available for this file in the selected mode.",
632
+ }
633
+
634
+
635
+ def cdx_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any | None = None) -> dict[str, Any]:
636
+ cdx_which = which or shutil.which
637
+ if not cdx_which("cdx"):
638
+ return {"state": "unavailable", "message": "CDX is not available on PATH.", "status": {}}
639
+
640
+ try:
641
+ status = _run_read_only_cdx(repo_root, ["status", "--json"], runner=runner)
642
+ except subprocess.TimeoutExpired:
643
+ return {"state": "timeout", "message": "CDX status timed out.", "status": {}}
644
+ except (OSError, subprocess.SubprocessError) as exc:
645
+ return {"state": "error", "message": f"Unable to run CDX status: {exc}", "status": {}}
646
+
647
+ if status.returncode != 0:
648
+ message = (status.stderr or status.stdout or "CDX status failed.").strip().splitlines()[0]
649
+ return {"state": "error", "message": message, "status": {}}
650
+
651
+ try:
652
+ parsed = json.loads(status.stdout or "{}")
653
+ except json.JSONDecodeError:
654
+ return {"state": "invalid-json", "message": "CDX status returned invalid JSON.", "status": {}}
655
+ if not isinstance(parsed, dict):
656
+ return {"state": "invalid-json", "message": "CDX status JSON must be an object.", "status": {}}
657
+
658
+ return {"state": "ok", "message": "", "status": parsed}
659
+
660
+
387
661
  def _json_bytes(payload: Any) -> bytes:
388
662
  return json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
389
663
 
390
664
 
391
665
  class LogicsViewerServer(ThreadingHTTPServer):
392
- def __init__(self, server_address: tuple[str, int], repo_root: Path):
666
+ def __init__(
667
+ self,
668
+ server_address: tuple[str, int],
669
+ repo_root: Path,
670
+ *,
671
+ auto_refresh_interval_seconds: int = 15,
672
+ ):
393
673
  self.repo_root = repo_root.resolve()
674
+ self.auto_refresh_interval_seconds = auto_refresh_interval_seconds
394
675
  super().__init__(server_address, LogicsViewerRequestHandler)
395
676
 
396
677
 
@@ -453,7 +734,15 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
453
734
  self._serve_file(media_path)
454
735
  return
455
736
  if route == "/api/items":
456
- self._send_json({"ok": True, "payload": viewer_data_payload(self.server.repo_root)})
737
+ self._send_json(
738
+ {
739
+ "ok": True,
740
+ "payload": viewer_data_payload(
741
+ self.server.repo_root,
742
+ auto_refresh_interval_seconds=self.server.auto_refresh_interval_seconds,
743
+ ),
744
+ }
745
+ )
457
746
  return
458
747
  if route == "/api/doc":
459
748
  rel_path = parse_qs(parsed.query).get("path", [""])[0]
@@ -468,12 +757,32 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
468
757
  if route == "/api/audit":
469
758
  self._send_json({"ok": True, "payload": audit_payload(self.server.repo_root)})
470
759
  return
760
+ if route == "/api/git-status":
761
+ self._send_json({"ok": True, "payload": git_status_payload(self.server.repo_root)})
762
+ return
763
+ if route == "/api/cdx-status":
764
+ self._send_json({"ok": True, "payload": cdx_status_payload(self.server.repo_root)})
765
+ return
766
+ if route == "/api/git-diff":
767
+ params = parse_qs(parsed.query)
768
+ rel_path = params.get("path", [""])[0]
769
+ cached = params.get("cached", [""])[0].lower() in {"1", "true", "yes"}
770
+ self._send_json({"ok": True, "payload": git_diff_payload(self.server.repo_root, rel_path, cached=cached)})
771
+ return
471
772
  self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
472
773
 
473
774
  def do_POST(self) -> None:
474
775
  parsed = urlparse(self.path)
475
776
  if parsed.path == "/api/refresh":
476
- self._send_json({"ok": True, "payload": viewer_data_payload(self.server.repo_root)})
777
+ self._send_json(
778
+ {
779
+ "ok": True,
780
+ "payload": viewer_data_payload(
781
+ self.server.repo_root,
782
+ auto_refresh_interval_seconds=self.server.auto_refresh_interval_seconds,
783
+ ),
784
+ }
785
+ )
477
786
  return
478
787
  if parsed.path == "/api/edit":
479
788
  rel_path = parse_qs(parsed.query).get("path", [""])[0]
@@ -487,19 +796,52 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
487
796
  self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
488
797
 
489
798
 
490
- def create_viewer_server(repo_root: Path, host: str = "127.0.0.1", port: int = 8765) -> LogicsViewerServer:
491
- return LogicsViewerServer((host, port), repo_root)
799
+ def create_viewer_server(
800
+ repo_root: Path,
801
+ host: str = "127.0.0.1",
802
+ port: int = 8765,
803
+ *,
804
+ auto_refresh_interval_seconds: int = 15,
805
+ ) -> LogicsViewerServer:
806
+ return LogicsViewerServer(
807
+ (host, port),
808
+ repo_root,
809
+ auto_refresh_interval_seconds=auto_refresh_interval_seconds,
810
+ )
492
811
 
493
812
 
494
- def render_start_status(url: str, repo_root: Path, *, focus: str | None = None) -> str:
813
+ def _network_viewer_url(host: str, port: int, *, focus: str | None = None, read: bool = False) -> str | None:
814
+ if host not in {"0.0.0.0", "::", ""}:
815
+ return None
816
+ try:
817
+ candidate = socket.gethostbyname(socket.gethostname())
818
+ except OSError:
819
+ return None
820
+ if not candidate or candidate.startswith("127."):
821
+ return None
822
+ return build_viewer_url(candidate, port, focus=focus, read=read)
823
+
824
+
825
+ def render_start_status(
826
+ url: str,
827
+ repo_root: Path,
828
+ *,
829
+ focus: str | None = None,
830
+ network_url: str | None = None,
831
+ bind_host: str = "localhost",
832
+ auto_refresh_interval_seconds: int = 15,
833
+ ) -> str:
495
834
  lines = [
496
835
  "Logics viewer running:",
497
- url,
836
+ f"Local: {url}",
498
837
  "",
499
838
  f"Repo: {repo_root.name}",
500
839
  "Mode: read-only",
501
- "Bind: localhost",
840
+ f"Bind: {bind_host}",
841
+ f"Auto refresh: {auto_refresh_interval_seconds}s",
502
842
  ]
843
+ if network_url:
844
+ lines.insert(2, f"Network: {network_url}")
503
845
  if focus:
504
846
  lines.append(f"Focus: {focus}")
505
847
  return "\n".join(lines)
@@ -509,6 +851,12 @@ def build_parser() -> argparse.ArgumentParser:
509
851
  parser = argparse.ArgumentParser(prog="logics-manager view", description="Start the local read-only Logics browser viewer.")
510
852
  parser.add_argument("--host", default="127.0.0.1", help="Bind host. Defaults to 127.0.0.1.")
511
853
  parser.add_argument("--port", type=int, default=8765, help="Bind port. Use 0 to select an available port.")
854
+ parser.add_argument(
855
+ "--refresh-interval",
856
+ type=int,
857
+ default=15,
858
+ help="Automatic refresh interval in seconds. Defaults to 15; positive intervals are allowed.",
859
+ )
512
860
  parser.add_argument("--focus", help="Open the viewer focused on a workflow ref or repo-relative Logics Markdown path.")
513
861
  parser.add_argument("--read", action="store_true", help="Open the focused item in the read preview. Requires --focus.")
514
862
  parser.add_argument("--open", action="store_true", help="Open the viewer in the default browser.")
@@ -519,16 +867,34 @@ def build_parser() -> argparse.ArgumentParser:
519
867
  def main(argv: list[str]) -> int:
520
868
  args = build_parser().parse_args(argv)
521
869
  repo_root = find_repo_root(Path.cwd())
870
+ if args.refresh_interval <= 0:
871
+ raise SystemExit("--refresh-interval must be a positive number of seconds.")
522
872
  if args.read and not args.focus:
523
873
  raise SystemExit("--read requires --focus.")
524
874
  try:
525
875
  focus = normalize_viewer_focus_target(repo_root, args.focus) if args.focus else None
526
876
  except ValueError as exc:
527
877
  raise SystemExit(str(exc)) from exc
528
- server = create_viewer_server(repo_root, host=args.host, port=args.port)
878
+ server = create_viewer_server(
879
+ repo_root,
880
+ host=args.host,
881
+ port=args.port,
882
+ auto_refresh_interval_seconds=args.refresh_interval,
883
+ )
529
884
  host, port = server.server_address[:2]
530
885
  url = build_viewer_url(str(host), int(port), focus=focus, read=bool(args.read))
531
- print(render_start_status(url, repo_root, focus=focus), flush=True)
886
+ network_url = _network_viewer_url(str(host), int(port), focus=focus, read=bool(args.read))
887
+ print(
888
+ render_start_status(
889
+ url,
890
+ repo_root,
891
+ focus=focus,
892
+ network_url=network_url,
893
+ bind_host=str(host),
894
+ auto_refresh_interval_seconds=args.refresh_interval,
895
+ ),
896
+ flush=True,
897
+ )
532
898
  if args.open and not args.no_open:
533
899
  webbrowser.open(url)
534
900
 
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.3.3",
5
+ "version": "2.5.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.3.3"
7
+ version = "2.5.0"
8
8
  description = "Canonical Logics CLI"
9
9
  requires-python = ">=3.10"
10
10