@grifhinz/logics-manager 2.5.1 → 2.6.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/layout.css +7 -0
- package/clients/viewer/browser-host.js +524 -61
- package/clients/viewer/index.html +39 -19
- package/clients/viewer/viewer.css +471 -8
- package/logics_manager/viewer.py +286 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/logics_manager/viewer.py
CHANGED
|
@@ -346,6 +346,10 @@ def viewer_data_payload(
|
|
|
346
346
|
return {
|
|
347
347
|
"root": str(repo_root.resolve()),
|
|
348
348
|
"repoName": repo_root.resolve().name,
|
|
349
|
+
"repository": {
|
|
350
|
+
"root": str(repo_root.resolve()),
|
|
351
|
+
"githubUrl": github_repo_url(repo_root),
|
|
352
|
+
},
|
|
349
353
|
"autoRefreshIntervalSeconds": auto_refresh_interval_seconds,
|
|
350
354
|
"items": collect_viewer_items(repo_root),
|
|
351
355
|
"updateInfo": get_update_info(_current_version()).to_payload(),
|
|
@@ -392,6 +396,17 @@ def edit_doc_payload(repo_root: Path, rel_path: str, *, launcher: Any | None = N
|
|
|
392
396
|
}
|
|
393
397
|
|
|
394
398
|
|
|
399
|
+
def open_repo_folder_payload(repo_root: Path, *, launcher: Any | None = None) -> dict[str, str]:
|
|
400
|
+
root = repo_root.resolve()
|
|
401
|
+
command = _system_editor_command(root)
|
|
402
|
+
runner = launcher or subprocess.Popen
|
|
403
|
+
runner(command)
|
|
404
|
+
return {
|
|
405
|
+
"path": str(root),
|
|
406
|
+
"command": command[0],
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
395
410
|
def _system_editor_command(path: Path) -> list[str]:
|
|
396
411
|
if sys.platform == "darwin":
|
|
397
412
|
return ["open", str(path)]
|
|
@@ -412,6 +427,12 @@ def _run_read_only_cdx(repo_root: Path, args: list[str], *, runner: Any | None =
|
|
|
412
427
|
return cdx_runner(command, cwd=repo_root, text=True, capture_output=True, timeout=5)
|
|
413
428
|
|
|
414
429
|
|
|
430
|
+
def _run_read_only_gh(repo_root: Path, args: list[str], *, runner: Any | None = None) -> subprocess.CompletedProcess[str]:
|
|
431
|
+
command = ["gh", *args]
|
|
432
|
+
gh_runner = runner or subprocess.run
|
|
433
|
+
return gh_runner(command, cwd=repo_root, text=True, capture_output=True, timeout=8)
|
|
434
|
+
|
|
435
|
+
|
|
415
436
|
def _logics_doc_type(rel_path: str) -> str:
|
|
416
437
|
normalized = rel_path.replace("\\", "/").lstrip("/")
|
|
417
438
|
for family in DOC_FAMILIES:
|
|
@@ -427,6 +448,62 @@ def _sanitize_git_ref(value: str) -> str:
|
|
|
427
448
|
return ref[:200]
|
|
428
449
|
|
|
429
450
|
|
|
451
|
+
def _github_web_url_from_remote(value: str) -> str:
|
|
452
|
+
remote = value.strip()
|
|
453
|
+
if not remote:
|
|
454
|
+
return ""
|
|
455
|
+
remote = re.sub(r"^git\+", "", remote)
|
|
456
|
+
match = re.match(r"^(?:https://|http://)(?:[^/@\s]+@)?github\.com[:/]+([^/\s]+)/([^/\s]+?)(?:\.git)?/?$", remote)
|
|
457
|
+
if not match:
|
|
458
|
+
match = re.match(r"^(?:ssh://)?git@github\.com[:/]+([^/\s]+)/([^/\s]+?)(?:\.git)?/?$", remote)
|
|
459
|
+
if not match:
|
|
460
|
+
return ""
|
|
461
|
+
owner, repo = match.groups()
|
|
462
|
+
if not owner or not repo:
|
|
463
|
+
return ""
|
|
464
|
+
return f"https://github.com/{owner}/{repo}"
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _github_owner_repo_from_web_url(value: str) -> tuple[str, str] | None:
|
|
468
|
+
match = re.match(r"^https://github\.com/([^/\s]+)/([^/\s]+?)/?$", value.strip())
|
|
469
|
+
if not match:
|
|
470
|
+
return None
|
|
471
|
+
owner, repo = match.groups()
|
|
472
|
+
return owner, repo
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def github_repo_url(repo_root: Path, *, runner: Any | None = None, which: Any | None = None) -> str:
|
|
476
|
+
git_which = which or shutil.which
|
|
477
|
+
if not git_which("git"):
|
|
478
|
+
return ""
|
|
479
|
+
try:
|
|
480
|
+
remotes = _run_read_only_git(repo_root, ["remote", "-v"], runner=runner)
|
|
481
|
+
except (OSError, subprocess.SubprocessError):
|
|
482
|
+
return ""
|
|
483
|
+
if remotes.returncode != 0:
|
|
484
|
+
return ""
|
|
485
|
+
|
|
486
|
+
candidates: list[tuple[int, str]] = []
|
|
487
|
+
for line in remotes.stdout.splitlines():
|
|
488
|
+
parts = line.split()
|
|
489
|
+
if len(parts) < 2:
|
|
490
|
+
continue
|
|
491
|
+
name, url = parts[0], parts[1]
|
|
492
|
+
web_url = _github_web_url_from_remote(url)
|
|
493
|
+
if web_url:
|
|
494
|
+
candidates.append((0 if name == "origin" else 1, web_url))
|
|
495
|
+
if not candidates:
|
|
496
|
+
return ""
|
|
497
|
+
return sorted(candidates, key=lambda entry: entry[0])[0][1]
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _has_github_actions_workflows(repo_root: Path) -> bool:
|
|
501
|
+
workflows_dir = repo_root / ".github" / "workflows"
|
|
502
|
+
if not workflows_dir.is_dir():
|
|
503
|
+
return False
|
|
504
|
+
return any(path.is_file() and path.suffix.lower() in {".yml", ".yaml"} for path in workflows_dir.iterdir())
|
|
505
|
+
|
|
506
|
+
|
|
430
507
|
def _classify_porcelain_entry(line: str) -> tuple[str, dict[str, str]] | None:
|
|
431
508
|
if not line or line.startswith("## "):
|
|
432
509
|
return None
|
|
@@ -496,7 +573,38 @@ def _parse_recent_git_commits(output: str) -> list[dict[str, str]]:
|
|
|
496
573
|
return commits
|
|
497
574
|
|
|
498
575
|
|
|
499
|
-
def
|
|
576
|
+
def _parse_git_numstat(output: str) -> dict[str, dict[str, int]]:
|
|
577
|
+
stats: dict[str, dict[str, int]] = {}
|
|
578
|
+
for line in output.splitlines():
|
|
579
|
+
parts = line.split("\t")
|
|
580
|
+
if len(parts) < 3:
|
|
581
|
+
continue
|
|
582
|
+
raw_additions, raw_deletions, raw_path = parts[:3]
|
|
583
|
+
try:
|
|
584
|
+
additions = int(raw_additions)
|
|
585
|
+
deletions = int(raw_deletions)
|
|
586
|
+
except ValueError:
|
|
587
|
+
continue
|
|
588
|
+
path = raw_path.strip()
|
|
589
|
+
if " => " in path:
|
|
590
|
+
path = path.split(" => ", 1)[1].strip("{}")
|
|
591
|
+
if path:
|
|
592
|
+
stats[path] = {"additions": additions, "deletions": deletions}
|
|
593
|
+
return stats
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
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:
|
|
597
|
+
for key, entries in groups.items():
|
|
598
|
+
stats_source = staged_stats if key == "staged" else worktree_stats
|
|
599
|
+
for entry in entries:
|
|
600
|
+
path = str(entry.get("path", ""))
|
|
601
|
+
stats = stats_source.get(path) or staged_stats.get(path) or worktree_stats.get(path)
|
|
602
|
+
if stats:
|
|
603
|
+
entry["additions"] = stats["additions"]
|
|
604
|
+
entry["deletions"] = stats["deletions"]
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _count_unique_git_status_paths(groups: dict[str, list[dict[str, Any]]]) -> int:
|
|
500
608
|
paths: set[str] = set()
|
|
501
609
|
for entries in groups.values():
|
|
502
610
|
for entry in entries:
|
|
@@ -543,10 +651,12 @@ def git_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any
|
|
|
543
651
|
|
|
544
652
|
try:
|
|
545
653
|
status = _run_read_only_git(repo_root, ["status", "--porcelain=v1", "-b"], runner=runner)
|
|
654
|
+
staged_numstat = _run_read_only_git(repo_root, ["diff", "--no-ext-diff", "--numstat", "--cached"], runner=runner)
|
|
655
|
+
worktree_numstat = _run_read_only_git(repo_root, ["diff", "--no-ext-diff", "--numstat"], runner=runner)
|
|
546
656
|
commit = _run_read_only_git(repo_root, ["log", "-1", "--pretty=format:%h %s"], runner=runner)
|
|
547
657
|
recent_commits = _run_read_only_git(
|
|
548
658
|
repo_root,
|
|
549
|
-
["log", "-
|
|
659
|
+
["log", "-50", "--date=short", "--pretty=format:%h%x1f%s%x1f%an%x1f%ad%x1f%D"],
|
|
550
660
|
runner=runner,
|
|
551
661
|
)
|
|
552
662
|
unpushed = _git_unpushed_commit_count(repo_root, runner=runner)
|
|
@@ -558,12 +668,18 @@ def git_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any
|
|
|
558
668
|
|
|
559
669
|
lines = status.stdout.splitlines()
|
|
560
670
|
branch_info = _parse_git_branch_line(lines[0]) if lines else {"branch": "HEAD", "tracking": "", "ahead": 0, "behind": 0}
|
|
561
|
-
groups: dict[str, list[dict[str,
|
|
671
|
+
groups: dict[str, list[dict[str, Any]]] = {key: [] for key in ("staged", "modified", "deleted", "renamed", "untracked")}
|
|
562
672
|
for line in lines[1:]:
|
|
563
673
|
classified = _classify_porcelain_entry(line)
|
|
564
674
|
if classified:
|
|
565
675
|
group, entry = classified
|
|
566
676
|
groups[group].append(entry)
|
|
677
|
+
if staged_numstat.returncode == 0 or worktree_numstat.returncode == 0:
|
|
678
|
+
_attach_git_change_stats(
|
|
679
|
+
groups,
|
|
680
|
+
_parse_git_numstat(staged_numstat.stdout if staged_numstat.returncode == 0 else ""),
|
|
681
|
+
_parse_git_numstat(worktree_numstat.stdout if worktree_numstat.returncode == 0 else ""),
|
|
682
|
+
)
|
|
567
683
|
counts = {key: len(value) for key, value in groups.items()}
|
|
568
684
|
uncommitted_files = _count_unique_git_status_paths(groups)
|
|
569
685
|
dirty = any(counts.values())
|
|
@@ -639,6 +755,164 @@ def git_diff_payload(
|
|
|
639
755
|
}
|
|
640
756
|
|
|
641
757
|
|
|
758
|
+
def _current_git_ci_context(repo_root: Path, *, runner: Any | None = None) -> dict[str, str]:
|
|
759
|
+
context = {"branch": "", "headSha": "", "subject": "", "author": ""}
|
|
760
|
+
commands = {
|
|
761
|
+
"branch": ["rev-parse", "--abbrev-ref", "HEAD"],
|
|
762
|
+
"headSha": ["rev-parse", "HEAD"],
|
|
763
|
+
"subject": ["log", "-1", "--pretty=format:%s"],
|
|
764
|
+
"author": ["log", "-1", "--pretty=format:%an"],
|
|
765
|
+
}
|
|
766
|
+
for key, args in commands.items():
|
|
767
|
+
try:
|
|
768
|
+
result = _run_read_only_git(repo_root, args, runner=runner)
|
|
769
|
+
except (OSError, subprocess.SubprocessError):
|
|
770
|
+
continue
|
|
771
|
+
if result.returncode == 0:
|
|
772
|
+
context[key] = result.stdout.strip()[:240]
|
|
773
|
+
if context["branch"] == "HEAD":
|
|
774
|
+
context["branch"] = ""
|
|
775
|
+
return context
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _ci_badge_state(status: str, conclusion: str) -> str:
|
|
779
|
+
normalized_status = status.strip().lower()
|
|
780
|
+
normalized_conclusion = conclusion.strip().lower()
|
|
781
|
+
if normalized_status in {"queued", "in_progress", "waiting", "requested", "pending"}:
|
|
782
|
+
return "running" if normalized_status == "in_progress" else "queued"
|
|
783
|
+
if normalized_conclusion == "success":
|
|
784
|
+
return "passing"
|
|
785
|
+
if normalized_conclusion in {"failure", "timed_out", "action_required"}:
|
|
786
|
+
return "failing"
|
|
787
|
+
if normalized_conclusion == "cancelled":
|
|
788
|
+
return "cancelled"
|
|
789
|
+
return "unknown"
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _parse_github_actions_run(run: dict[str, Any], *, match_source: str) -> dict[str, Any]:
|
|
793
|
+
status = str(run.get("status") or "")
|
|
794
|
+
conclusion = str(run.get("conclusion") or "")
|
|
795
|
+
commit = run.get("head_commit") if isinstance(run.get("head_commit"), dict) else {}
|
|
796
|
+
author = commit.get("author") if isinstance(commit.get("author"), dict) else {}
|
|
797
|
+
return {
|
|
798
|
+
"id": run.get("id"),
|
|
799
|
+
"name": str(run.get("name") or run.get("display_title") or "GitHub Actions"),
|
|
800
|
+
"workflowName": str(run.get("name") or "GitHub Actions"),
|
|
801
|
+
"status": status,
|
|
802
|
+
"conclusion": conclusion,
|
|
803
|
+
"badgeState": _ci_badge_state(status, conclusion),
|
|
804
|
+
"branch": str(run.get("head_branch") or ""),
|
|
805
|
+
"headSha": str(run.get("head_sha") or ""),
|
|
806
|
+
"event": str(run.get("event") or ""),
|
|
807
|
+
"htmlUrl": str(run.get("html_url") or ""),
|
|
808
|
+
"createdAt": str(run.get("created_at") or ""),
|
|
809
|
+
"updatedAt": str(run.get("updated_at") or ""),
|
|
810
|
+
"runStartedAt": str(run.get("run_started_at") or ""),
|
|
811
|
+
"commitMessage": str(commit.get("message") or run.get("display_title") or "").splitlines()[0][:240],
|
|
812
|
+
"author": str(author.get("name") or ""),
|
|
813
|
+
"matchSource": match_source,
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _parse_github_actions_jobs(output: str) -> list[dict[str, str]]:
|
|
818
|
+
try:
|
|
819
|
+
parsed = json.loads(output or "{}")
|
|
820
|
+
except json.JSONDecodeError:
|
|
821
|
+
return []
|
|
822
|
+
if not isinstance(parsed, dict):
|
|
823
|
+
return []
|
|
824
|
+
jobs = parsed.get("jobs")
|
|
825
|
+
if not isinstance(jobs, list):
|
|
826
|
+
return []
|
|
827
|
+
rows: list[dict[str, str]] = []
|
|
828
|
+
for job in jobs[:30]:
|
|
829
|
+
if not isinstance(job, dict):
|
|
830
|
+
continue
|
|
831
|
+
rows.append(
|
|
832
|
+
{
|
|
833
|
+
"name": str(job.get("name") or "Job"),
|
|
834
|
+
"status": str(job.get("status") or ""),
|
|
835
|
+
"conclusion": str(job.get("conclusion") or ""),
|
|
836
|
+
"htmlUrl": str(job.get("html_url") or ""),
|
|
837
|
+
"startedAt": str(job.get("started_at") or ""),
|
|
838
|
+
"completedAt": str(job.get("completed_at") or ""),
|
|
839
|
+
}
|
|
840
|
+
)
|
|
841
|
+
return rows
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def ci_status_payload(repo_root: Path, *, git_runner: Any | None = None, gh_runner: Any | None = None, which: Any | None = None) -> dict[str, Any]:
|
|
845
|
+
git_which = which or shutil.which
|
|
846
|
+
github_url = github_repo_url(repo_root, runner=git_runner, which=git_which)
|
|
847
|
+
if not github_url:
|
|
848
|
+
return {"state": "hidden", "visible": False, "message": "No GitHub remote detected."}
|
|
849
|
+
owner_repo = _github_owner_repo_from_web_url(github_url)
|
|
850
|
+
if not owner_repo:
|
|
851
|
+
return {"state": "hidden", "visible": False, "message": "GitHub remote could not be parsed."}
|
|
852
|
+
if not _has_github_actions_workflows(repo_root):
|
|
853
|
+
return {"state": "hidden", "visible": False, "message": "No GitHub Actions workflows detected."}
|
|
854
|
+
if not git_which("gh"):
|
|
855
|
+
return {
|
|
856
|
+
"state": "unavailable",
|
|
857
|
+
"visible": True,
|
|
858
|
+
"message": "GitHub CLI is not available on PATH.",
|
|
859
|
+
"repositoryUrl": github_url,
|
|
860
|
+
"badgeState": "unavailable",
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
owner, repo = owner_repo
|
|
864
|
+
context = _current_git_ci_context(repo_root, runner=git_runner)
|
|
865
|
+
branch = context.get("branch", "")
|
|
866
|
+
head_sha = context.get("headSha", "")
|
|
867
|
+
endpoint = f"repos/{owner}/{repo}/actions/runs?per_page=10"
|
|
868
|
+
if branch:
|
|
869
|
+
endpoint = f"{endpoint}&branch={quote(branch, safe='')}"
|
|
870
|
+
try:
|
|
871
|
+
runs_result = _run_read_only_gh(repo_root, ["api", endpoint], runner=gh_runner)
|
|
872
|
+
except subprocess.TimeoutExpired:
|
|
873
|
+
return {"state": "timeout", "visible": True, "message": "GitHub Actions status timed out.", "repositoryUrl": github_url, **context, "badgeState": "unavailable"}
|
|
874
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
875
|
+
return {"state": "error", "visible": True, "message": f"Unable to collect GitHub Actions status: {exc}", "repositoryUrl": github_url, **context, "badgeState": "unavailable"}
|
|
876
|
+
if runs_result.returncode != 0:
|
|
877
|
+
message = (runs_result.stderr or runs_result.stdout or "GitHub Actions status failed.").strip().splitlines()[0]
|
|
878
|
+
return {"state": "unavailable", "visible": True, "message": message, "repositoryUrl": github_url, **context, "badgeState": "unavailable"}
|
|
879
|
+
|
|
880
|
+
try:
|
|
881
|
+
parsed = json.loads(runs_result.stdout or "{}")
|
|
882
|
+
except json.JSONDecodeError:
|
|
883
|
+
return {"state": "invalid-json", "visible": True, "message": "GitHub Actions status returned invalid JSON.", "repositoryUrl": github_url, **context, "badgeState": "unavailable"}
|
|
884
|
+
workflow_runs = parsed.get("workflow_runs") if isinstance(parsed, dict) else None
|
|
885
|
+
runs = [run for run in workflow_runs if isinstance(run, dict)] if isinstance(workflow_runs, list) else []
|
|
886
|
+
if not runs:
|
|
887
|
+
return {"state": "ok", "visible": True, "message": "No GitHub Actions runs found for the current branch.", "repositoryUrl": github_url, **context, "badgeState": "unknown", "run": None, "jobs": []}
|
|
888
|
+
|
|
889
|
+
selected = next((run for run in runs if head_sha and str(run.get("head_sha") or "") == head_sha), None)
|
|
890
|
+
match_source = "head" if selected else "branch-latest"
|
|
891
|
+
if selected is None:
|
|
892
|
+
selected = runs[0]
|
|
893
|
+
run_payload = _parse_github_actions_run(selected, match_source=match_source)
|
|
894
|
+
jobs: list[dict[str, str]] = []
|
|
895
|
+
run_id = run_payload.get("id")
|
|
896
|
+
if run_id:
|
|
897
|
+
try:
|
|
898
|
+
jobs_result = _run_read_only_gh(repo_root, ["api", f"repos/{owner}/{repo}/actions/runs/{run_id}/jobs?per_page=100"], runner=gh_runner)
|
|
899
|
+
except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
|
|
900
|
+
jobs_result = None
|
|
901
|
+
if jobs_result is not None and jobs_result.returncode == 0:
|
|
902
|
+
jobs = _parse_github_actions_jobs(jobs_result.stdout)
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
"state": "ok",
|
|
906
|
+
"visible": True,
|
|
907
|
+
"message": "",
|
|
908
|
+
"repositoryUrl": github_url,
|
|
909
|
+
**context,
|
|
910
|
+
"badgeState": run_payload["badgeState"],
|
|
911
|
+
"run": run_payload,
|
|
912
|
+
"jobs": jobs,
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
|
|
642
916
|
def cdx_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any | None = None) -> dict[str, Any]:
|
|
643
917
|
cdx_which = which or shutil.which
|
|
644
918
|
if not cdx_which("cdx"):
|
|
@@ -767,6 +1041,9 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
767
1041
|
if route == "/api/git-status":
|
|
768
1042
|
self._send_json({"ok": True, "payload": git_status_payload(self.server.repo_root)})
|
|
769
1043
|
return
|
|
1044
|
+
if route == "/api/ci-status":
|
|
1045
|
+
self._send_json({"ok": True, "payload": ci_status_payload(self.server.repo_root)})
|
|
1046
|
+
return
|
|
770
1047
|
if route == "/api/cdx-status":
|
|
771
1048
|
self._send_json({"ok": True, "payload": cdx_status_payload(self.server.repo_root)})
|
|
772
1049
|
return
|
|
@@ -800,6 +1077,12 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
800
1077
|
except OSError as exc:
|
|
801
1078
|
self._send_error_json(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
|
|
802
1079
|
return
|
|
1080
|
+
if parsed.path == "/api/open-repo-folder":
|
|
1081
|
+
try:
|
|
1082
|
+
self._send_json({"ok": True, "payload": open_repo_folder_payload(self.server.repo_root)})
|
|
1083
|
+
except OSError as exc:
|
|
1084
|
+
self._send_error_json(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
|
|
1085
|
+
return
|
|
803
1086
|
self._send_error_json(HTTPStatus.NOT_FOUND, "Not found")
|
|
804
1087
|
|
|
805
1088
|
|
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.6.0",
|
|
6
6
|
"publisher": "cdx-logics",
|
|
7
7
|
"icon": "clients/shared-web/media/icon.png",
|
|
8
8
|
"repository": {
|