@grifhinz/logics-manager 2.5.0 → 2.5.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/AlexAgo83/logics-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/AlexAgo83/logics-manager/actions/workflows/ci.yml)
4
4
  [![License](https://img.shields.io/github/license/AlexAgo83/logics-manager)](LICENSE)
5
- ![Version](https://img.shields.io/badge/version-v2.5.0-4C8BF5)
5
+ ![Version](https://img.shields.io/badge/version-v2.5.2-4C8BF5)
6
6
  ![VS Code](https://img.shields.io/badge/VS%20Code-1.86.0-007ACC?logo=visualstudiocode&logoColor=white)
7
7
  ![TypeScript](https://img.shields.io/badge/TypeScript-5.3.3-3178C6?logo=typescript&logoColor=white)
8
8
  ![Vitest](https://img.shields.io/badge/Vitest-2.1.8-6E9F18?logo=vitest&logoColor=white)
@@ -230,13 +230,15 @@ npm install -g @grifhinz/logics-manager@latest
230
230
  If npm reports a successful update but `logics-manager --version` still shows an older version, another installation is earlier on `PATH`. Diagnose it with:
231
231
 
232
232
  ```bash
233
- command -v -a logics-manager
233
+ type -a logics-manager
234
+ whence -a logics-manager # zsh
235
+ pipx list
234
236
  npm prefix -g
235
237
  npm list -g @grifhinz/logics-manager --depth=0
236
238
  "$(npm prefix -g)/bin/logics-manager" --version
237
239
  ```
238
240
 
239
- If the direct npm binary shows the expected version, remove the older Python install or move the npm global `bin` directory earlier on `PATH`.
241
+ 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.
240
242
 
241
243
  ## VS Code Extension
242
244
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.5.0
1
+ 2.5.2
@@ -107,6 +107,7 @@
107
107
  }
108
108
 
109
109
  .layout--stacked .details {
110
+ width: 100%;
110
111
  min-height: 220px;
111
112
  border-top: none;
112
113
  position: absolute;
@@ -1751,6 +1751,14 @@
1751
1751
  <span class="viewer-git__domain-label">${escapeHtml(label)}${key === "changes" ? gitBadgeHtml("changes") : ""}${key === "history" ? gitBadgeHtml("history") : ""}</span><strong>${escapeHtml(count)}</strong>
1752
1752
  </button>
1753
1753
  `).join("");
1754
+ const renderChangeStats = (entry) => {
1755
+ const additions = Number(entry?.additions);
1756
+ const deletions = Number(entry?.deletions);
1757
+ if (!Number.isFinite(additions) || !Number.isFinite(deletions)) {
1758
+ return "";
1759
+ }
1760
+ return `<span class="viewer-git__file-changes" title="Line changes"><span class="viewer-git__file-additions">+${escapeHtml(additions)}</span><span class="viewer-git__file-deletions">-${escapeHtml(deletions)}</span></span>`;
1761
+ };
1754
1762
  const renderFileSections = (allowedKeys) => groupDefs.filter(([key]) => allowedKeys.includes(key)).map(([key, label]) => {
1755
1763
  const entries = Array.isArray(payload.groups?.[key]) ? payload.groups[key] : [];
1756
1764
  if (!entries.length) {
@@ -1763,6 +1771,7 @@
1763
1771
  <li>
1764
1772
  <button class="viewer-git__file" type="button" data-viewer-git-file="${escapeHtml(entry.path)}" data-viewer-git-cached="${key === "staged" ? "1" : "0"}">
1765
1773
  <span class="viewer-git__file-path">${escapeHtml(entry.from ? `${entry.from} -> ${entry.path}` : entry.path)}</span>
1774
+ ${renderChangeStats(entry)}
1766
1775
  ${entry.logicsType ? `<span class="viewer-git__file-kind">${escapeHtml(entry.logicsType)}</span>` : ""}
1767
1776
  </button>
1768
1777
  </li>
@@ -1852,6 +1861,29 @@
1852
1861
  });
1853
1862
  }
1854
1863
 
1864
+ function gitDiffLineKind(line) {
1865
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1866
+ return "add";
1867
+ }
1868
+ if (line.startsWith("-") && !line.startsWith("---")) {
1869
+ return "delete";
1870
+ }
1871
+ if (line.startsWith("@@")) {
1872
+ return "hunk";
1873
+ }
1874
+ if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("+++") || line.startsWith("---")) {
1875
+ return "meta";
1876
+ }
1877
+ return "context";
1878
+ }
1879
+
1880
+ function renderGitDiffPreview(content) {
1881
+ return String(content)
1882
+ .split("\n")
1883
+ .map((line) => `<span class="viewer-git__diff-line viewer-git__diff-line--${gitDiffLineKind(line)}">${escapeHtml(line || " ")}</span>`)
1884
+ .join("");
1885
+ }
1886
+
1855
1887
  async function loadGitDiff(path, cached, button = null) {
1856
1888
  const diffPanel = document.querySelector("[data-viewer-git-diff]");
1857
1889
  if (!(diffPanel instanceof HTMLElement) || !path) {
@@ -1873,7 +1905,7 @@
1873
1905
  return;
1874
1906
  }
1875
1907
  const content = payload.diff || payload.message || "No diff is available for this file.";
1876
- diffPanel.innerHTML = `<div class="viewer-git__diff-meta">${escapeHtml(payload.path || path)} · ${escapeHtml(payload.mode || "worktree")}${payload.truncated ? " · truncated" : ""}</div><pre><code>${escapeHtml(content)}</code></pre>`;
1908
+ diffPanel.innerHTML = `<div class="viewer-git__diff-meta">${escapeHtml(payload.path || path)} · ${escapeHtml(payload.mode || "worktree")}${payload.truncated ? " · truncated" : ""}</div><pre><code>${renderGitDiffPreview(content)}</code></pre>`;
1877
1909
  }
1878
1910
 
1879
1911
  function applyGitDomain(domain) {
@@ -829,6 +829,23 @@
829
829
  overflow-wrap: anywhere;
830
830
  }
831
831
 
832
+ .viewer-git__file-changes {
833
+ display: inline-flex;
834
+ align-items: center;
835
+ gap: 5px;
836
+ flex: 0 0 auto;
837
+ font-family: var(--vscode-editor-font-family, ui-monospace, SFMono-Regular, Menlo, monospace);
838
+ font-size: 11px;
839
+ }
840
+
841
+ .viewer-git__file-additions {
842
+ color: #7ee787;
843
+ }
844
+
845
+ .viewer-git__file-deletions {
846
+ color: #ffaaa5;
847
+ }
848
+
832
849
  .viewer-git__file-kind {
833
850
  flex: 0 0 auto;
834
851
  padding: 2px 6px;
@@ -871,6 +888,35 @@
871
888
  white-space: pre;
872
889
  }
873
890
 
891
+ .viewer-git__diff-line {
892
+ display: block;
893
+ min-height: 1.35em;
894
+ padding: 0 8px;
895
+ border-left: 3px solid transparent;
896
+ }
897
+
898
+ .viewer-git__diff-line--add {
899
+ border-left-color: #2ea043;
900
+ background: rgba(46, 160, 67, 0.16);
901
+ color: #7ee787;
902
+ }
903
+
904
+ .viewer-git__diff-line--delete {
905
+ border-left-color: #f85149;
906
+ background: rgba(248, 81, 73, 0.16);
907
+ color: #ffaaa5;
908
+ }
909
+
910
+ .viewer-git__diff-line--hunk {
911
+ border-left-color: var(--vscode-textLink-foreground, #4ea1ff);
912
+ background: color-mix(in srgb, var(--vscode-textLink-foreground, #4ea1ff) 14%, transparent);
913
+ color: var(--vscode-textLink-foreground, #4ea1ff);
914
+ }
915
+
916
+ .viewer-git__diff-line--meta {
917
+ color: var(--vscode-descriptionForeground, #aaaaaa);
918
+ }
919
+
874
920
  .viewer-git__commit,
875
921
  .viewer-git__state {
876
922
  margin: 0;
@@ -195,15 +195,21 @@ def _find_executable_paths(command: str) -> list[str]:
195
195
  def _print_path_conflict_guidance(paths: list[str]) -> None:
196
196
  if len(paths) <= 1:
197
197
  return
198
+ path_lines = [f" - {path}" for path in paths]
198
199
  print(
199
200
  "\n".join(
200
201
  [
201
202
  "",
202
203
  "Multiple logics-manager executables are on PATH. If --version still shows an older release, an earlier install is taking precedence.",
204
+ "Detected executables:",
205
+ *path_lines,
203
206
  "Diagnose with:",
204
- " command -v -a logics-manager",
207
+ " type -a logics-manager",
208
+ " whence -a logics-manager # zsh",
205
209
  " pipx list",
206
210
  " npm list -g @grifhinz/logics-manager --depth=0",
211
+ "",
212
+ "If you recently changed installs in zsh, run `rehash` or open a new terminal before retrying.",
207
213
  ]
208
214
  )
209
215
  )
@@ -427,7 +427,7 @@ def _markdown_file_path(repo_root: Path, raw_path: str, allowed_dirs: tuple[str,
427
427
 
428
428
  def _run_command(repo_root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
429
429
  command = [sys.executable, "-m", "logics_manager", *args]
430
- result = subprocess.run(command, cwd=repo_root, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
430
+ result = subprocess.run(command, cwd=repo_root, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=_subprocess_env())
431
431
  if result.returncode != 0:
432
432
  raise McpToolError(
433
433
  "command_failed",
@@ -439,7 +439,7 @@ def _run_command(repo_root: Path, args: list[str]) -> subprocess.CompletedProces
439
439
 
440
440
  def _run_json_command(repo_root: Path, args: list[str]) -> dict[str, Any]:
441
441
  command = [sys.executable, "-m", "logics_manager", *args]
442
- result = subprocess.run(command, cwd=repo_root, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
442
+ result = subprocess.run(command, cwd=repo_root, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=_subprocess_env())
443
443
  payload = _json_from_stdout_or_none(result.stdout)
444
444
  if payload is None:
445
445
  raise McpToolError(
@@ -450,6 +450,14 @@ def _run_json_command(repo_root: Path, args: list[str]) -> dict[str, Any]:
450
450
  return payload
451
451
 
452
452
 
453
+ def _subprocess_env() -> dict[str, str]:
454
+ env = os.environ.copy()
455
+ source_root = str(Path(__file__).resolve().parents[1])
456
+ existing = env.get("PYTHONPATH")
457
+ env["PYTHONPATH"] = source_root if not existing else os.pathsep.join([source_root, existing])
458
+ return env
459
+
460
+
453
461
  def _json_from_stdout(stdout: str) -> dict[str, Any]:
454
462
  start = stdout.find("{")
455
463
  end = stdout.rfind("}")
@@ -14,6 +14,7 @@ from dataclasses import dataclass
14
14
  from datetime import datetime
15
15
  from http import HTTPStatus
16
16
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
17
+ from importlib import metadata
17
18
  from pathlib import Path
18
19
  from typing import Any
19
20
  from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse
@@ -56,8 +57,14 @@ NODE_MERMAID_ROOT = REPO_ROOT / "node_modules" / "mermaid" / "dist"
56
57
 
57
58
  def _current_version() -> str:
58
59
  try:
59
- return (REPO_ROOT / "VERSION").read_text(encoding="utf-8").strip() or "0.0.0"
60
+ version = (REPO_ROOT / "VERSION").read_text(encoding="utf-8").strip()
60
61
  except OSError:
62
+ version = ""
63
+ if version:
64
+ return version
65
+ try:
66
+ return metadata.version("logics-manager")
67
+ except metadata.PackageNotFoundError:
61
68
  return "0.0.0"
62
69
 
63
70
 
@@ -489,7 +496,38 @@ def _parse_recent_git_commits(output: str) -> list[dict[str, str]]:
489
496
  return commits
490
497
 
491
498
 
492
- def _count_unique_git_status_paths(groups: dict[str, list[dict[str, str]]]) -> int:
499
+ def _parse_git_numstat(output: str) -> dict[str, dict[str, int]]:
500
+ stats: dict[str, dict[str, int]] = {}
501
+ for line in output.splitlines():
502
+ parts = line.split("\t")
503
+ if len(parts) < 3:
504
+ continue
505
+ raw_additions, raw_deletions, raw_path = parts[:3]
506
+ try:
507
+ additions = int(raw_additions)
508
+ deletions = int(raw_deletions)
509
+ except ValueError:
510
+ continue
511
+ path = raw_path.strip()
512
+ if " => " in path:
513
+ path = path.split(" => ", 1)[1].strip("{}")
514
+ if path:
515
+ stats[path] = {"additions": additions, "deletions": deletions}
516
+ return stats
517
+
518
+
519
+ def _attach_git_change_stats(groups: dict[str, list[dict[str, Any]]], staged_stats: dict[str, dict[str, int]], worktree_stats: dict[str, dict[str, int]]) -> None:
520
+ for key, entries in groups.items():
521
+ stats_source = staged_stats if key == "staged" else worktree_stats
522
+ for entry in entries:
523
+ path = str(entry.get("path", ""))
524
+ stats = stats_source.get(path) or staged_stats.get(path) or worktree_stats.get(path)
525
+ if stats:
526
+ entry["additions"] = stats["additions"]
527
+ entry["deletions"] = stats["deletions"]
528
+
529
+
530
+ def _count_unique_git_status_paths(groups: dict[str, list[dict[str, Any]]]) -> int:
493
531
  paths: set[str] = set()
494
532
  for entries in groups.values():
495
533
  for entry in entries:
@@ -536,6 +574,8 @@ def git_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any
536
574
 
537
575
  try:
538
576
  status = _run_read_only_git(repo_root, ["status", "--porcelain=v1", "-b"], runner=runner)
577
+ staged_numstat = _run_read_only_git(repo_root, ["diff", "--no-ext-diff", "--numstat", "--cached"], runner=runner)
578
+ worktree_numstat = _run_read_only_git(repo_root, ["diff", "--no-ext-diff", "--numstat"], runner=runner)
539
579
  commit = _run_read_only_git(repo_root, ["log", "-1", "--pretty=format:%h %s"], runner=runner)
540
580
  recent_commits = _run_read_only_git(
541
581
  repo_root,
@@ -551,12 +591,18 @@ def git_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any
551
591
 
552
592
  lines = status.stdout.splitlines()
553
593
  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")}
594
+ groups: dict[str, list[dict[str, Any]]] = {key: [] for key in ("staged", "modified", "deleted", "renamed", "untracked")}
555
595
  for line in lines[1:]:
556
596
  classified = _classify_porcelain_entry(line)
557
597
  if classified:
558
598
  group, entry = classified
559
599
  groups[group].append(entry)
600
+ if staged_numstat.returncode == 0 or worktree_numstat.returncode == 0:
601
+ _attach_git_change_stats(
602
+ groups,
603
+ _parse_git_numstat(staged_numstat.stdout if staged_numstat.returncode == 0 else ""),
604
+ _parse_git_numstat(worktree_numstat.stdout if worktree_numstat.returncode == 0 else ""),
605
+ )
560
606
  counts = {key: len(value) for key, value in groups.items()}
561
607
  uncommitted_files = _count_unique_git_status_paths(groups)
562
608
  dirty = any(counts.values())
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.0",
5
+ "version": "2.5.2",
6
6
  "publisher": "cdx-logics",
7
7
  "icon": "clients/shared-web/media/icon.png",
8
8
  "repository": {
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.5.0"
7
+ version = "2.5.2"
8
8
  description = "Canonical Logics CLI"
9
9
  requires-python = ">=3.10"
10
10