jung 0.4.0__py3-none-any.whl
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.
- jung/__init__.py +3 -0
- jung/__main__.py +4 -0
- jung/_daemonize.py +137 -0
- jung/adapter/__init__.py +31 -0
- jung/adapter/antigravity.py +554 -0
- jung/adapter/cursor_adapter.py +242 -0
- jung/adapter/loader.py +153 -0
- jung/adapter/plugin.py +52 -0
- jung/adapter/protocol.py +31 -0
- jung/blame.py +288 -0
- jung/cli/__init__.py +25 -0
- jung/cli/adapter_cmd.py +63 -0
- jung/cli/blame_cmd.py +107 -0
- jung/cli/branch_cmd.py +99 -0
- jung/cli/commit_cmd.py +44 -0
- jung/cli/config_cmd.py +178 -0
- jung/cli/cost_cmd.py +156 -0
- jung/cli/dashboard_cmd.py +66 -0
- jung/cli/diff_cmd.py +172 -0
- jung/cli/export_cmd.py +71 -0
- jung/cli/format.py +99 -0
- jung/cli/gc_cmd.py +78 -0
- jung/cli/git_cmd.py +88 -0
- jung/cli/init_cmd.py +93 -0
- jung/cli/interactive.py +352 -0
- jung/cli/interactive_cmd.py +40 -0
- jung/cli/log_cmd.py +287 -0
- jung/cli/merge_cmd.py +43 -0
- jung/cli/remote_cmd.py +235 -0
- jung/cli/report_cmd.py +201 -0
- jung/cli/revert_cmd.py +46 -0
- jung/cli/search_cmd.py +156 -0
- jung/cli/show_cmd.py +193 -0
- jung/cli/status_cmd.py +90 -0
- jung/cli/testdata_cmd.py +55 -0
- jung/cli/timeline_cmd.py +115 -0
- jung/cli/watch_cmd.py +160 -0
- jung/config.py +113 -0
- jung/correlation/__init__.py +146 -0
- jung/correlation/engine.py +146 -0
- jung/cost.py +148 -0
- jung/daemon.py +670 -0
- jung/exceptions.py +29 -0
- jung/export.py +88 -0
- jung/git_hooks.py +118 -0
- jung/git_integration.py +129 -0
- jung/idgen.py +39 -0
- jung/log_setup.py +58 -0
- jung/main.py +112 -0
- jung/models.py +215 -0
- jung/remote/__init__.py +1 -0
- jung/remote/client.py +132 -0
- jung/remote/server.py +72 -0
- jung/remote/sync.py +255 -0
- jung/report.py +211 -0
- jung/snapshot.py +269 -0
- jung/store/__init__.py +7 -0
- jung/store/blob.py +150 -0
- jung/store/database.py +575 -0
- jung/store/migrations.py +28 -0
- jung/testdata/generator.py +236 -0
- jung/watcher.py +225 -0
- jung/web/__init__.py +1 -0
- jung/web/app.py +583 -0
- jung-0.4.0.dist-info/METADATA +2211 -0
- jung-0.4.0.dist-info/RECORD +70 -0
- jung-0.4.0.dist-info/WHEEL +5 -0
- jung-0.4.0.dist-info/entry_points.txt +5 -0
- jung-0.4.0.dist-info/licenses/LICENSE +21 -0
- jung-0.4.0.dist-info/top_level.txt +1 -0
jung/__init__.py
ADDED
jung/__main__.py
ADDED
jung/_daemonize.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Daemonize support for Windows and POSIX.
|
|
2
|
+
|
|
3
|
+
Windows: spawns a background subprocess with CREATE_NO_WINDOW flag.
|
|
4
|
+
POSIX: double-fork pattern with /dev/null redirection.
|
|
5
|
+
|
|
6
|
+
This module provides a single daemonize() function that starts the
|
|
7
|
+
daemon entrypoint in the background and writes a PID file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import signal
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
JUNG_DIR_NAME = ".jung"
|
|
20
|
+
PID_FILENAME = "daemon.pid"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def daemonize(chronicle_dir: Path, extra_args: list[str] | None = None) -> int:
|
|
24
|
+
"""Launch the jung daemon in the background.
|
|
25
|
+
|
|
26
|
+
Returns the PID of the background process.
|
|
27
|
+
"""
|
|
28
|
+
chronicle_dir.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
pid_path = chronicle_dir / PID_FILENAME
|
|
30
|
+
|
|
31
|
+
if os.name == "nt":
|
|
32
|
+
return _daemonize_windows(chronicle_dir, pid_path, extra_args)
|
|
33
|
+
else:
|
|
34
|
+
return _daemonize_posix(chronicle_dir, pid_path, extra_args)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_running(chronicle_dir: Path) -> bool:
|
|
38
|
+
"""Check if the daemon is running by reading PID file."""
|
|
39
|
+
pid_path = chronicle_dir / PID_FILENAME
|
|
40
|
+
if not pid_path.exists():
|
|
41
|
+
return False
|
|
42
|
+
pid_str = pid_path.read_text().strip()
|
|
43
|
+
if not pid_str:
|
|
44
|
+
return False
|
|
45
|
+
try:
|
|
46
|
+
pid = int(pid_str)
|
|
47
|
+
except ValueError:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
if os.name == "nt":
|
|
51
|
+
return _is_pid_alive_windows(pid)
|
|
52
|
+
else:
|
|
53
|
+
return _is_pid_alive_posix(pid)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def stop_daemon(chronicle_dir: Path) -> bool:
|
|
57
|
+
"""Stop the running daemon. Returns True if stopped, False if not running."""
|
|
58
|
+
pid_path = chronicle_dir / PID_FILENAME
|
|
59
|
+
if not pid_path.exists():
|
|
60
|
+
return False
|
|
61
|
+
pid_str = pid_path.read_text().strip()
|
|
62
|
+
if not pid_str:
|
|
63
|
+
return False
|
|
64
|
+
try:
|
|
65
|
+
pid = int(pid_str)
|
|
66
|
+
except ValueError:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
if os.name == "nt":
|
|
71
|
+
subprocess.run(["taskkill", "/F", "/PID", str(pid)], capture_output=True)
|
|
72
|
+
else:
|
|
73
|
+
os.kill(pid, signal.SIGTERM)
|
|
74
|
+
except (OSError, subprocess.SubprocessError):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
pid_path.unlink(missing_ok=True)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _daemonize_windows(chronicle_dir: Path, pid_path: Path, extra_args: list[str] | None = None) -> int:
|
|
82
|
+
"""Spawn the daemon as a background subprocess."""
|
|
83
|
+
args = extra_args or []
|
|
84
|
+
proc = subprocess.Popen(
|
|
85
|
+
[sys.executable, "-m", "jung.daemon", "--daemon"] + args,
|
|
86
|
+
cwd=str(chronicle_dir.parent),
|
|
87
|
+
creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS,
|
|
88
|
+
stdout=subprocess.DEVNULL,
|
|
89
|
+
stderr=subprocess.DEVNULL,
|
|
90
|
+
stdin=subprocess.DEVNULL,
|
|
91
|
+
)
|
|
92
|
+
pid_path.write_text(str(proc.pid))
|
|
93
|
+
return proc.pid
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _daemonize_posix(chronicle_dir: Path, pid_path: Path, extra_args: list[str] | None = None) -> int:
|
|
97
|
+
"""Double-fork daemonize for POSIX systems."""
|
|
98
|
+
pid = os.fork()
|
|
99
|
+
if pid > 0:
|
|
100
|
+
return pid
|
|
101
|
+
|
|
102
|
+
os.setsid()
|
|
103
|
+
pid2 = os.fork()
|
|
104
|
+
if pid2 > 0:
|
|
105
|
+
os._exit(0)
|
|
106
|
+
|
|
107
|
+
devnull = os.open(os.devnull, os.O_RDWR)
|
|
108
|
+
os.dup2(devnull, sys.stdin.fileno())
|
|
109
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
110
|
+
os.dup2(devnull, sys.stderr.fileno())
|
|
111
|
+
os.close(devnull)
|
|
112
|
+
|
|
113
|
+
pid_path.write_text(str(os.getpid()))
|
|
114
|
+
|
|
115
|
+
from jung.daemon import run_daemon
|
|
116
|
+
run_daemon(chronicle_dir=str(chronicle_dir.parent))
|
|
117
|
+
|
|
118
|
+
return os.getpid()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _is_pid_alive_windows(pid: int) -> bool:
|
|
122
|
+
try:
|
|
123
|
+
result = subprocess.run(
|
|
124
|
+
["tasklist", "/FI", f"PID eq {pid}", "/NH"],
|
|
125
|
+
capture_output=True, text=True, timeout=5,
|
|
126
|
+
)
|
|
127
|
+
return str(pid) in result.stdout
|
|
128
|
+
except (subprocess.SubprocessError, OSError):
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _is_pid_alive_posix(pid: int) -> bool:
|
|
133
|
+
try:
|
|
134
|
+
os.kill(pid, 0)
|
|
135
|
+
return True
|
|
136
|
+
except OSError:
|
|
137
|
+
return False
|
jung/adapter/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""IngestionAdapter protocol — contract all IDE adapters must satisfy."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterator, Protocol
|
|
4
|
+
|
|
5
|
+
from jung.models import NormalizedEvent, RawEvent, SessionRef
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IngestionAdapter(Protocol):
|
|
9
|
+
"""Interface for IDE-specific ingestion adapters.
|
|
10
|
+
|
|
11
|
+
Every adapter must implement these three methods.
|
|
12
|
+
Nothing downstream of this layer knows or cares
|
|
13
|
+
which IDE produced the data.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
ide_name: str
|
|
17
|
+
|
|
18
|
+
def discover_sessions(self, workspace_path: str) -> list[SessionRef]:
|
|
19
|
+
"""Find IDE-native sessions relevant to this workspace."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def stream_events(self, session_ref: SessionRef) -> Iterator[RawEvent]:
|
|
23
|
+
"""Yield events from the IDE's native storage in real-time."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def parse_event(self, raw: RawEvent) -> NormalizedEvent | None:
|
|
27
|
+
"""Convert an IDE-specific raw event into jung's canonical schema.
|
|
28
|
+
|
|
29
|
+
Returns None for unparseable entries (logged but skipped).
|
|
30
|
+
"""
|
|
31
|
+
...
|