collab-runtime 0.6.2__tar.gz → 0.7.0__tar.gz
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.
- {collab_runtime-0.6.2/collab_runtime.egg-info → collab_runtime-0.7.0}/PKG-INFO +1 -1
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/main.py +197 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0/collab_runtime.egg-info}/PKG-INFO +1 -1
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/pyproject.toml +1 -1
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/LICENSE +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/README.md +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/__init__.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/__main__.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/agent_hooks.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/agent_identity.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/dashboard/dashboard-format.js +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/dashboard/index.html +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/dashboard_server.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/errors.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/githooks.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/hook_templates/commit-msg +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/hook_templates/post-commit +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/hook_templates/pre-commit +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/hook_templates/pre-push +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/live_locks_watcher.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/lock_client.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/logging_config.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/platform_probe.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/safe_subprocess.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab/subprocess_bridge.py +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab_runtime.egg-info/SOURCES.txt +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab_runtime.egg-info/dependency_links.txt +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab_runtime.egg-info/entry_points.txt +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab_runtime.egg-info/requires.txt +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/collab_runtime.egg-info/top_level.txt +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/docs/pypi/README.md +0 -0
- {collab_runtime-0.6.2 → collab_runtime-0.7.0}/setup.cfg +0 -0
|
@@ -15,6 +15,8 @@ import traceback as _tb
|
|
|
15
15
|
from argparse import ArgumentParser
|
|
16
16
|
from contextlib import nullcontext
|
|
17
17
|
|
|
18
|
+
from . import __version__
|
|
19
|
+
|
|
18
20
|
logger = logging.getLogger("collab.lock_client")
|
|
19
21
|
|
|
20
22
|
|
|
@@ -109,6 +111,11 @@ def _run_cli() -> None:
|
|
|
109
111
|
description="Collaborative File Lock Manager (Supabase)",
|
|
110
112
|
parents=[common],
|
|
111
113
|
)
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--version",
|
|
116
|
+
action="version",
|
|
117
|
+
version=f"collab-runtime {__version__}",
|
|
118
|
+
)
|
|
112
119
|
sub = parser.add_subparsers(dest="command")
|
|
113
120
|
|
|
114
121
|
# acquire
|
|
@@ -200,6 +207,20 @@ def _run_cli() -> None:
|
|
|
200
207
|
# daemon-status
|
|
201
208
|
sub.add_parser("daemon-status", help="Check watcher daemon status")
|
|
202
209
|
|
|
210
|
+
# restart
|
|
211
|
+
rst = sub.add_parser(
|
|
212
|
+
"restart",
|
|
213
|
+
help="Restart the watcher daemon (stop + start)",
|
|
214
|
+
)
|
|
215
|
+
rst.add_argument("--interval", type=int, default=5, help="Poll interval (seconds)")
|
|
216
|
+
rst.add_argument(
|
|
217
|
+
"--timeout",
|
|
218
|
+
type=int,
|
|
219
|
+
default=0,
|
|
220
|
+
help="Idle timeout in minutes (0 = disabled)",
|
|
221
|
+
)
|
|
222
|
+
rst.add_argument("--open-dashboard", action="store_true")
|
|
223
|
+
|
|
203
224
|
# cleanup - kill orphaned processes
|
|
204
225
|
sub.add_parser(
|
|
205
226
|
"cleanup", help="Kill all orphaned lock_client processes (preserves locks)"
|
|
@@ -246,6 +267,27 @@ def _run_cli() -> None:
|
|
|
246
267
|
hpr = sub.add_parser("history-prune", help="Delete lock history older than N days")
|
|
247
268
|
hpr.add_argument("--days", type=int, default=30, help="Retention window in days")
|
|
248
269
|
|
|
270
|
+
# ping
|
|
271
|
+
sub.add_parser("ping", help="Check Supabase connectivity")
|
|
272
|
+
|
|
273
|
+
# info
|
|
274
|
+
sub.add_parser("info", help="Show comprehensive status overview")
|
|
275
|
+
|
|
276
|
+
# logs
|
|
277
|
+
lg = sub.add_parser("logs", help="Show recent collab log entries")
|
|
278
|
+
lg.add_argument(
|
|
279
|
+
"--lines",
|
|
280
|
+
type=int,
|
|
281
|
+
default=50,
|
|
282
|
+
help="Number of recent log lines to display (default: 50)",
|
|
283
|
+
)
|
|
284
|
+
lg.add_argument(
|
|
285
|
+
"--follow",
|
|
286
|
+
"-f",
|
|
287
|
+
action="store_true",
|
|
288
|
+
help="Follow the log file (tail -f mode, Ctrl+C to stop)",
|
|
289
|
+
)
|
|
290
|
+
|
|
249
291
|
# watch (internal, called by daemon-start)
|
|
250
292
|
wp = sub.add_parser("watch", help="Run watcher in foreground")
|
|
251
293
|
wp.add_argument("--interval", type=int, default=5)
|
|
@@ -374,6 +416,9 @@ def _run_cli() -> None:
|
|
|
374
416
|
"history",
|
|
375
417
|
"history-prune",
|
|
376
418
|
"whoami",
|
|
419
|
+
"ping",
|
|
420
|
+
"info",
|
|
421
|
+
"logs",
|
|
377
422
|
}
|
|
378
423
|
quiet_ctx = (
|
|
379
424
|
_quiet_console_loggers() if args.command in quiet_commands else nullcontext()
|
|
@@ -540,6 +585,20 @@ def _run_cli() -> None:
|
|
|
540
585
|
running = client.daemon_status()
|
|
541
586
|
sys.exit(0 if running else 1)
|
|
542
587
|
|
|
588
|
+
elif args.command == "restart":
|
|
589
|
+
open_flag = getattr(args, "open_dashboard", False)
|
|
590
|
+
auto_env = os.getenv("AUTO_OPEN_DASHBOARD", "0").lower() in (
|
|
591
|
+
"1",
|
|
592
|
+
"true",
|
|
593
|
+
"yes",
|
|
594
|
+
)
|
|
595
|
+
client.daemon_stop()
|
|
596
|
+
client.daemon_start(
|
|
597
|
+
interval=getattr(args, "interval", 5),
|
|
598
|
+
timeout_mins=getattr(args, "timeout", 0),
|
|
599
|
+
open_dashboard=(open_flag or auto_env),
|
|
600
|
+
)
|
|
601
|
+
|
|
543
602
|
elif args.command == "cleanup":
|
|
544
603
|
client.cleanup_orphaned_processes()
|
|
545
604
|
|
|
@@ -599,6 +658,144 @@ def _run_cli() -> None:
|
|
|
599
658
|
print(f"✗ Failed to prune lock history: {msg}")
|
|
600
659
|
sys.exit(1)
|
|
601
660
|
|
|
661
|
+
elif args.command == "ping":
|
|
662
|
+
import time as _t
|
|
663
|
+
from socket import create_connection as _connect
|
|
664
|
+
from urllib.parse import urlparse as _urlparse
|
|
665
|
+
|
|
666
|
+
url = os.getenv("SUPABASE_URL", "")
|
|
667
|
+
if not url:
|
|
668
|
+
print("✗ SUPABASE_URL is not configured (check .env)")
|
|
669
|
+
sys.exit(1)
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
host = _urlparse(url).hostname or ""
|
|
673
|
+
except Exception:
|
|
674
|
+
print(f"✗ Invalid SUPABASE_URL: {url!r}")
|
|
675
|
+
sys.exit(1)
|
|
676
|
+
|
|
677
|
+
if not host:
|
|
678
|
+
print(f"✗ Could not parse hostname from SUPABASE_URL: {url!r}")
|
|
679
|
+
sys.exit(1)
|
|
680
|
+
|
|
681
|
+
try:
|
|
682
|
+
start = _t.monotonic()
|
|
683
|
+
with _connect((host, 443), timeout=5.0):
|
|
684
|
+
pass
|
|
685
|
+
elapsed = _t.monotonic() - start
|
|
686
|
+
print(f"✓ Supabase is reachable " f"({host}, {elapsed*1000:.0f}ms)")
|
|
687
|
+
sys.exit(0)
|
|
688
|
+
except OSError as exc:
|
|
689
|
+
print(f"✗ Cannot reach {host}: {exc}")
|
|
690
|
+
print(" Verify network/VPN and that the Supabase project is active.")
|
|
691
|
+
sys.exit(1)
|
|
692
|
+
|
|
693
|
+
elif args.command == "info":
|
|
694
|
+
# Gather daemon status (suppress daemon_status()'s own print output)
|
|
695
|
+
import io as _io
|
|
696
|
+
|
|
697
|
+
from . import __version__ as _ver
|
|
698
|
+
from . import agent_identity
|
|
699
|
+
from .lock_client import _COLLAB_ROOT as _root
|
|
700
|
+
from .lock_client import _PROJECT_ROOT as _p_root
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
_old_stdout = sys.stdout
|
|
704
|
+
sys.stdout = _io.StringIO()
|
|
705
|
+
try:
|
|
706
|
+
daemon_running = client.daemon_status()
|
|
707
|
+
finally:
|
|
708
|
+
sys.stdout = _old_stdout
|
|
709
|
+
except Exception:
|
|
710
|
+
daemon_running = False
|
|
711
|
+
daemon_state = "RUNNING" if daemon_running else "stopped"
|
|
712
|
+
|
|
713
|
+
# Count active locks
|
|
714
|
+
lock_count: int | str
|
|
715
|
+
my_locks: int | str
|
|
716
|
+
try:
|
|
717
|
+
active_locks = client.active()
|
|
718
|
+
lock_count = len(active_locks)
|
|
719
|
+
my_locks = sum(1 for lk in active_locks if client._lock_owned_by_me(lk))
|
|
720
|
+
except Exception:
|
|
721
|
+
lock_count = "?"
|
|
722
|
+
my_locks = "?"
|
|
723
|
+
|
|
724
|
+
# Identity summary
|
|
725
|
+
ident = agent_identity.identity_summary(
|
|
726
|
+
client.developer_id,
|
|
727
|
+
client.agent_id,
|
|
728
|
+
client.agent_label,
|
|
729
|
+
getattr(client, "agent_kind", None),
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
state_dir = os.path.join(_root, ".collab")
|
|
733
|
+
|
|
734
|
+
print(f"collab-runtime v{_ver}")
|
|
735
|
+
print()
|
|
736
|
+
print("── Daemon ──")
|
|
737
|
+
print(f" Status: {daemon_state}")
|
|
738
|
+
print(f" PID file: {os.path.join(state_dir, '.daemon.pid')}")
|
|
739
|
+
print()
|
|
740
|
+
print("── Identity ──")
|
|
741
|
+
print(f" Developer: {ident['developer_id']}")
|
|
742
|
+
print(f" Mode: {ident['mode']}")
|
|
743
|
+
if ident.get("agent_id"):
|
|
744
|
+
print(f" Agent ID: {ident['agent_id']}")
|
|
745
|
+
if ident.get("agent_label"):
|
|
746
|
+
print(f" Agent label: {ident['agent_label']}")
|
|
747
|
+
if ident.get("agent_kind"):
|
|
748
|
+
print(f" Agent kind: {ident['agent_kind']}")
|
|
749
|
+
print()
|
|
750
|
+
print("── Locks ──")
|
|
751
|
+
print(f" Active: {lock_count}")
|
|
752
|
+
print(f" Mine: {my_locks}")
|
|
753
|
+
print(f" Admin: {'yes' if client.is_admin else 'no'}")
|
|
754
|
+
print()
|
|
755
|
+
print("── Paths ──")
|
|
756
|
+
print(f" Runtime: {_root}")
|
|
757
|
+
print(f" Project: {_p_root}")
|
|
758
|
+
print(f" State dir: {state_dir}")
|
|
759
|
+
print(f" Log file: {os.path.join(_root, 'logs', 'collab.log')}")
|
|
760
|
+
sys.exit(0)
|
|
761
|
+
|
|
762
|
+
elif args.command == "logs":
|
|
763
|
+
from .lock_client import _COLLAB_ROOT as _root
|
|
764
|
+
|
|
765
|
+
log_path = os.path.join(_root, "logs", "collab.log")
|
|
766
|
+
lines = int(getattr(args, "lines", 50))
|
|
767
|
+
follow = getattr(args, "follow", False)
|
|
768
|
+
|
|
769
|
+
if not os.path.isfile(log_path):
|
|
770
|
+
print(f"No log file found at {log_path}")
|
|
771
|
+
sys.exit(1)
|
|
772
|
+
|
|
773
|
+
if follow:
|
|
774
|
+
# tail -f mode
|
|
775
|
+
print(f"Following {log_path} (Ctrl+C to stop)...")
|
|
776
|
+
try:
|
|
777
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
778
|
+
fh.seek(0, 2) # seek to end
|
|
779
|
+
while True:
|
|
780
|
+
line = fh.readline()
|
|
781
|
+
if line:
|
|
782
|
+
print(line, end="")
|
|
783
|
+
else:
|
|
784
|
+
time.sleep(0.25)
|
|
785
|
+
except KeyboardInterrupt:
|
|
786
|
+
print()
|
|
787
|
+
sys.exit(0)
|
|
788
|
+
else:
|
|
789
|
+
try:
|
|
790
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
791
|
+
all_lines = fh.readlines()
|
|
792
|
+
for line in all_lines[-lines:]:
|
|
793
|
+
print(line, end="")
|
|
794
|
+
except OSError as exc:
|
|
795
|
+
print(f"✗ Cannot read log file: {exc}")
|
|
796
|
+
sys.exit(1)
|
|
797
|
+
sys.exit(0)
|
|
798
|
+
|
|
602
799
|
elif args.command == "watch":
|
|
603
800
|
# Ensure watcher child process uses the explicit PID namespace passed by
|
|
604
801
|
# daemon-start. This prevents cross-instance status/stop interference.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|