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.
Files changed (70) hide show
  1. jung/__init__.py +3 -0
  2. jung/__main__.py +4 -0
  3. jung/_daemonize.py +137 -0
  4. jung/adapter/__init__.py +31 -0
  5. jung/adapter/antigravity.py +554 -0
  6. jung/adapter/cursor_adapter.py +242 -0
  7. jung/adapter/loader.py +153 -0
  8. jung/adapter/plugin.py +52 -0
  9. jung/adapter/protocol.py +31 -0
  10. jung/blame.py +288 -0
  11. jung/cli/__init__.py +25 -0
  12. jung/cli/adapter_cmd.py +63 -0
  13. jung/cli/blame_cmd.py +107 -0
  14. jung/cli/branch_cmd.py +99 -0
  15. jung/cli/commit_cmd.py +44 -0
  16. jung/cli/config_cmd.py +178 -0
  17. jung/cli/cost_cmd.py +156 -0
  18. jung/cli/dashboard_cmd.py +66 -0
  19. jung/cli/diff_cmd.py +172 -0
  20. jung/cli/export_cmd.py +71 -0
  21. jung/cli/format.py +99 -0
  22. jung/cli/gc_cmd.py +78 -0
  23. jung/cli/git_cmd.py +88 -0
  24. jung/cli/init_cmd.py +93 -0
  25. jung/cli/interactive.py +352 -0
  26. jung/cli/interactive_cmd.py +40 -0
  27. jung/cli/log_cmd.py +287 -0
  28. jung/cli/merge_cmd.py +43 -0
  29. jung/cli/remote_cmd.py +235 -0
  30. jung/cli/report_cmd.py +201 -0
  31. jung/cli/revert_cmd.py +46 -0
  32. jung/cli/search_cmd.py +156 -0
  33. jung/cli/show_cmd.py +193 -0
  34. jung/cli/status_cmd.py +90 -0
  35. jung/cli/testdata_cmd.py +55 -0
  36. jung/cli/timeline_cmd.py +115 -0
  37. jung/cli/watch_cmd.py +160 -0
  38. jung/config.py +113 -0
  39. jung/correlation/__init__.py +146 -0
  40. jung/correlation/engine.py +146 -0
  41. jung/cost.py +148 -0
  42. jung/daemon.py +670 -0
  43. jung/exceptions.py +29 -0
  44. jung/export.py +88 -0
  45. jung/git_hooks.py +118 -0
  46. jung/git_integration.py +129 -0
  47. jung/idgen.py +39 -0
  48. jung/log_setup.py +58 -0
  49. jung/main.py +112 -0
  50. jung/models.py +215 -0
  51. jung/remote/__init__.py +1 -0
  52. jung/remote/client.py +132 -0
  53. jung/remote/server.py +72 -0
  54. jung/remote/sync.py +255 -0
  55. jung/report.py +211 -0
  56. jung/snapshot.py +269 -0
  57. jung/store/__init__.py +7 -0
  58. jung/store/blob.py +150 -0
  59. jung/store/database.py +575 -0
  60. jung/store/migrations.py +28 -0
  61. jung/testdata/generator.py +236 -0
  62. jung/watcher.py +225 -0
  63. jung/web/__init__.py +1 -0
  64. jung/web/app.py +583 -0
  65. jung-0.4.0.dist-info/METADATA +2211 -0
  66. jung-0.4.0.dist-info/RECORD +70 -0
  67. jung-0.4.0.dist-info/WHEEL +5 -0
  68. jung-0.4.0.dist-info/entry_points.txt +5 -0
  69. jung-0.4.0.dist-info/licenses/LICENSE +21 -0
  70. jung-0.4.0.dist-info/top_level.txt +1 -0
jung/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """jung — version control context for AI-assisted development."""
2
+
3
+ __version__ = "0.4.0"
jung/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from jung.main import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
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
@@ -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
+ ...