@grifhinz/logics-manager 2.1.2 → 2.3.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 +106 -4
- package/VERSION +1 -1
- package/clients/README.md +9 -0
- package/clients/shared-web/media/css/board.css +658 -0
- package/clients/shared-web/media/css/details.css +457 -0
- package/clients/shared-web/media/css/layout.css +123 -0
- package/clients/shared-web/media/css/toolbar.css +576 -0
- package/clients/shared-web/media/harnessApi.js +324 -0
- package/clients/shared-web/media/hostApi.js +213 -0
- package/clients/shared-web/media/hostApiContract.js +55 -0
- package/clients/shared-web/media/icon.png +0 -0
- package/clients/shared-web/media/layoutController.js +246 -0
- package/clients/shared-web/media/logics.svg +7 -0
- package/clients/shared-web/media/logicsModel.js +910 -0
- package/clients/shared-web/media/main.css +112 -0
- package/clients/shared-web/media/main.js +3 -0
- package/clients/shared-web/media/mainApp.js +1005 -0
- package/clients/shared-web/media/mainCore.js +604 -0
- package/clients/shared-web/media/mainInteractionHandlers.js +324 -0
- package/clients/shared-web/media/mainInteractions.js +378 -0
- package/clients/shared-web/media/renderBoard.js +3 -0
- package/clients/shared-web/media/renderBoardApp.js +1339 -0
- package/clients/shared-web/media/renderDetails.js +685 -0
- package/clients/shared-web/media/renderMarkdown.js +449 -0
- package/clients/shared-web/media/toolsPanelLayout.js +172 -0
- package/clients/shared-web/media/uiStatus.js +54 -0
- package/clients/shared-web/media/webviewChrome.js +405 -0
- package/clients/shared-web/media/webviewPersistence.js +116 -0
- package/clients/shared-web/media/webviewSelectors.js +491 -0
- package/clients/viewer/README.md +5 -0
- package/clients/viewer/browser-host.js +847 -0
- package/clients/viewer/index.html +237 -0
- package/clients/viewer/viewer.css +433 -0
- package/logics_manager/assist.py +94 -63
- package/logics_manager/assist_handoff.py +132 -0
- package/logics_manager/assist_surface.py +38 -0
- package/logics_manager/cli.py +152 -12
- package/logics_manager/cli_output.py +18 -0
- package/logics_manager/flow.py +1360 -84
- package/logics_manager/flow_evidence.py +63 -0
- package/logics_manager/index.py +3 -7
- package/logics_manager/insights.py +418 -0
- package/logics_manager/mcp.py +50 -0
- package/logics_manager/path_utils.py +31 -0
- package/logics_manager/sync.py +24 -12
- package/logics_manager/update_check.py +138 -0
- package/logics_manager/viewer.py +533 -0
- package/package.json +12 -6
- package/pyproject.toml +1 -1
package/logics_manager/sync.py
CHANGED
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
from .config import find_repo_root
|
|
12
12
|
from .lint import expected_workflow_mermaid_signature
|
|
13
|
+
from .path_utils import resolve_repo_output_path
|
|
13
14
|
from .termstyle import colorize_help
|
|
14
15
|
|
|
15
16
|
|
|
@@ -576,9 +577,10 @@ def append_workflow_note_payload(repo_root: Path, source: str, *, note_kind: str
|
|
|
576
577
|
changed = False
|
|
577
578
|
else:
|
|
578
579
|
lines.insert(insert_at, bullet)
|
|
580
|
+
mermaid_signature_refreshed = False
|
|
579
581
|
if changed and not dry_run:
|
|
580
582
|
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
581
|
-
refresh_workflow_mermaid_signature_file(path, kind, dry_run=False, repo_root=repo_root)
|
|
583
|
+
mermaid_signature_refreshed = refresh_workflow_mermaid_signature_file(path, kind, dry_run=False, repo_root=repo_root)
|
|
582
584
|
return {
|
|
583
585
|
"path": path.relative_to(repo_root).as_posix(),
|
|
584
586
|
"ref": path.stem,
|
|
@@ -586,6 +588,7 @@ def append_workflow_note_payload(repo_root: Path, source: str, *, note_kind: str
|
|
|
586
588
|
"section": section,
|
|
587
589
|
"text": cleaned,
|
|
588
590
|
"changed": changed,
|
|
591
|
+
"mermaid_signature_refreshed": mermaid_signature_refreshed,
|
|
589
592
|
"dry_run": dry_run,
|
|
590
593
|
}
|
|
591
594
|
|
|
@@ -689,7 +692,7 @@ def refresh_workflow_mermaid_signature_file(path: Path, kind: str, dry_run: bool
|
|
|
689
692
|
return True
|
|
690
693
|
|
|
691
694
|
|
|
692
|
-
def _close_eligible_requests(repo_root: Path, dry_run: bool) -> tuple[int, int]:
|
|
695
|
+
def _close_eligible_requests(repo_root: Path, dry_run: bool, *, quiet: bool = False) -> tuple[int, int]:
|
|
693
696
|
request_dir = repo_root / DOC_KINDS["request"]["directory"]
|
|
694
697
|
closed = 0
|
|
695
698
|
scanned = 0
|
|
@@ -703,7 +706,8 @@ def _close_eligible_requests(repo_root: Path, dry_run: bool) -> tuple[int, int]:
|
|
|
703
706
|
continue
|
|
704
707
|
if all(_is_doc_done(item_path, "backlog") for item_path in linked_items):
|
|
705
708
|
_close_doc(request_path, "request", dry_run)
|
|
706
|
-
|
|
709
|
+
if not quiet:
|
|
710
|
+
print(f"Auto-closed request {request_ref} (all linked backlog items are done).")
|
|
707
711
|
closed += 1
|
|
708
712
|
return scanned, closed
|
|
709
713
|
|
|
@@ -1030,7 +1034,7 @@ def _print_help(text: str) -> None:
|
|
|
1030
1034
|
|
|
1031
1035
|
def cmd_close_eligible_requests(args: argparse.Namespace) -> dict[str, object]:
|
|
1032
1036
|
repo_root = _find_repo_root(Path.cwd())
|
|
1033
|
-
scanned, closed = _close_eligible_requests(repo_root, args.dry_run)
|
|
1037
|
+
scanned, closed = _close_eligible_requests(repo_root, args.dry_run, quiet=args.format == "json")
|
|
1034
1038
|
payload = {
|
|
1035
1039
|
"command": "sync",
|
|
1036
1040
|
"kind": "close-eligible-requests",
|
|
@@ -1161,6 +1165,8 @@ def cmd_append_note(args: argparse.Namespace) -> dict[str, object]:
|
|
|
1161
1165
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1162
1166
|
else:
|
|
1163
1167
|
print(f"Appended {args.section} note to {payload['path']} (changed: {payload['changed']}).")
|
|
1168
|
+
if payload.get("mermaid_signature_refreshed"):
|
|
1169
|
+
print("- Mermaid signature refreshed.")
|
|
1164
1170
|
return {"command": "sync", "kind": "append-note", "repo_root": repo_root.as_posix(), **payload}
|
|
1165
1171
|
|
|
1166
1172
|
|
|
@@ -1168,13 +1174,16 @@ def cmd_context_pack(args: argparse.Namespace) -> dict[str, object]:
|
|
|
1168
1174
|
repo_root = _find_repo_root(Path.cwd())
|
|
1169
1175
|
payload = _build_context_pack(repo_root, args.ref, mode=args.mode, profile=args.profile, config=None)
|
|
1170
1176
|
if args.out:
|
|
1171
|
-
out_path = (repo_root
|
|
1177
|
+
out_path, output_path = resolve_repo_output_path(repo_root, args.out)
|
|
1172
1178
|
serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
|
1173
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1174
1179
|
if not args.dry_run:
|
|
1180
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1175
1181
|
out_path.write_text(serialized, encoding="utf-8")
|
|
1176
|
-
|
|
1177
|
-
|
|
1182
|
+
payload["output_path"] = output_path
|
|
1183
|
+
if args.format == "json":
|
|
1184
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1185
|
+
else:
|
|
1186
|
+
print(f"Wrote {output_path}")
|
|
1178
1187
|
else:
|
|
1179
1188
|
if args.format == "json":
|
|
1180
1189
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
@@ -1189,13 +1198,16 @@ def cmd_export_graph(args: argparse.Namespace) -> dict[str, object]:
|
|
|
1189
1198
|
payload = _graph_payload(repo_root, config=None)
|
|
1190
1199
|
payload["repo_root"] = repo_root.as_posix()
|
|
1191
1200
|
if args.out:
|
|
1192
|
-
out_path = (repo_root
|
|
1201
|
+
out_path, output_path = resolve_repo_output_path(repo_root, args.out)
|
|
1193
1202
|
serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
|
1194
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1195
1203
|
if not args.dry_run:
|
|
1204
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1196
1205
|
out_path.write_text(serialized, encoding="utf-8")
|
|
1197
|
-
|
|
1198
|
-
|
|
1206
|
+
payload["output_path"] = output_path
|
|
1207
|
+
if args.format == "json":
|
|
1208
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1209
|
+
else:
|
|
1210
|
+
print(f"Wrote {output_path}")
|
|
1199
1211
|
else:
|
|
1200
1212
|
if args.format == "json":
|
|
1201
1213
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
from urllib.error import URLError
|
|
10
|
+
from urllib.request import urlopen
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
NPM_LATEST_URL = "https://registry.npmjs.org/@grifhinz%2Flogics-manager/latest"
|
|
14
|
+
DISABLE_ENV = "LOGICS_MANAGER_NO_UPDATE_CHECK"
|
|
15
|
+
CHECK_INTERVAL_SECONDS = 24 * 60 * 60
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class UpdateInfo:
|
|
20
|
+
current_version: str
|
|
21
|
+
latest_version: str | None
|
|
22
|
+
update_available: bool
|
|
23
|
+
checked_at: int | None
|
|
24
|
+
update_command: str
|
|
25
|
+
source: str
|
|
26
|
+
|
|
27
|
+
def to_payload(self) -> dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"currentVersion": self.current_version,
|
|
30
|
+
"latestVersion": self.latest_version,
|
|
31
|
+
"updateAvailable": self.update_available,
|
|
32
|
+
"checkedAt": self.checked_at,
|
|
33
|
+
"updateCommand": self.update_command,
|
|
34
|
+
"source": self.source,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_version(value: str | None) -> tuple[int, int, int, str]:
|
|
39
|
+
raw = (value or "").strip().lstrip("v")
|
|
40
|
+
parts = raw.split(".", 3)
|
|
41
|
+
numeric: list[int] = []
|
|
42
|
+
suffix = ""
|
|
43
|
+
for index, part in enumerate(parts[:3]):
|
|
44
|
+
digits = ""
|
|
45
|
+
rest = ""
|
|
46
|
+
for char in part:
|
|
47
|
+
if char.isdigit() and not rest:
|
|
48
|
+
digits += char
|
|
49
|
+
else:
|
|
50
|
+
rest += char
|
|
51
|
+
numeric.append(int(digits or "0"))
|
|
52
|
+
if rest:
|
|
53
|
+
suffix = ".".join([rest, *parts[index + 1 :]])
|
|
54
|
+
break
|
|
55
|
+
while len(numeric) < 3:
|
|
56
|
+
numeric.append(0)
|
|
57
|
+
return numeric[0], numeric[1], numeric[2], suffix
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_newer_version(latest: str | None, current: str | None) -> bool:
|
|
61
|
+
latest_tuple = _parse_version(latest)
|
|
62
|
+
current_tuple = _parse_version(current)
|
|
63
|
+
return latest_tuple[:3] > current_tuple[:3]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def update_cache_path() -> Path:
|
|
67
|
+
override = os.environ.get("LOGICS_MANAGER_UPDATE_CACHE")
|
|
68
|
+
if override:
|
|
69
|
+
return Path(override)
|
|
70
|
+
cache_root = os.environ.get("XDG_CACHE_HOME")
|
|
71
|
+
if cache_root:
|
|
72
|
+
return Path(cache_root) / "logics-manager" / "update-check.json"
|
|
73
|
+
return Path.home() / ".cache" / "logics-manager" / "update-check.json"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_cache(path: Path, now: int) -> dict[str, Any] | None:
|
|
77
|
+
try:
|
|
78
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
79
|
+
except (OSError, json.JSONDecodeError):
|
|
80
|
+
return None
|
|
81
|
+
checked_at = int(payload.get("checked_at") or 0)
|
|
82
|
+
if checked_at <= 0 or now - checked_at > CHECK_INTERVAL_SECONDS:
|
|
83
|
+
return None
|
|
84
|
+
return payload if isinstance(payload, dict) else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _write_cache(path: Path, payload: dict[str, Any]) -> None:
|
|
88
|
+
try:
|
|
89
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
path.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8")
|
|
91
|
+
except OSError:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def fetch_latest_npm_version(*, timeout: float = 0.75, opener: Callable[..., Any] = urlopen) -> str | None:
|
|
96
|
+
try:
|
|
97
|
+
with opener(NPM_LATEST_URL, timeout=timeout) as response:
|
|
98
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
99
|
+
except (OSError, URLError, TimeoutError, json.JSONDecodeError, ValueError):
|
|
100
|
+
return None
|
|
101
|
+
version = payload.get("version") if isinstance(payload, dict) else None
|
|
102
|
+
return version.strip() if isinstance(version, str) and version.strip() else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_update_info(
|
|
106
|
+
current_version: str,
|
|
107
|
+
*,
|
|
108
|
+
cache_path: Path | None = None,
|
|
109
|
+
now: int | None = None,
|
|
110
|
+
fetch_latest: Callable[[], str | None] | None = None,
|
|
111
|
+
) -> UpdateInfo:
|
|
112
|
+
now_value = int(time.time() if now is None else now)
|
|
113
|
+
path = cache_path or update_cache_path()
|
|
114
|
+
cached = _read_cache(path, now_value)
|
|
115
|
+
latest = str(cached.get("latest_version") or "") if cached else ""
|
|
116
|
+
if not latest:
|
|
117
|
+
latest = (fetch_latest or fetch_latest_npm_version)() or ""
|
|
118
|
+
_write_cache(path, {"checked_at": now_value, "latest_version": latest})
|
|
119
|
+
return UpdateInfo(
|
|
120
|
+
current_version=current_version,
|
|
121
|
+
latest_version=latest or None,
|
|
122
|
+
update_available=is_newer_version(latest, current_version),
|
|
123
|
+
checked_at=now_value,
|
|
124
|
+
update_command="logics-manager self-update",
|
|
125
|
+
source="npm",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_update_notice(current_version: str) -> str | None:
|
|
130
|
+
if os.environ.get(DISABLE_ENV):
|
|
131
|
+
return None
|
|
132
|
+
info = get_update_info(current_version)
|
|
133
|
+
if not info.update_available or not info.latest_version:
|
|
134
|
+
return None
|
|
135
|
+
return (
|
|
136
|
+
f"logics-manager {info.latest_version} is available "
|
|
137
|
+
f"(current {info.current_version}). Run `{info.update_command}` to update."
|
|
138
|
+
)
|