git-juggler 0.1.3__tar.gz → 0.1.4__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.
Files changed (101) hide show
  1. {git_juggler-0.1.3 → git_juggler-0.1.4}/PKG-INFO +2 -1
  2. {git_juggler-0.1.3 → git_juggler-0.1.4}/pyproject.toml +1 -0
  3. git_juggler-0.1.4/src/backend/git_juggler/terminal.py +166 -0
  4. git_juggler-0.1.3/src/backend/git_juggler/terminal.py +0 -89
  5. {git_juggler-0.1.3 → git_juggler-0.1.4}/.github/workflows/build-package.yml +0 -0
  6. {git_juggler-0.1.3 → git_juggler-0.1.4}/.gitignore +0 -0
  7. {git_juggler-0.1.3 → git_juggler-0.1.4}/CLAUDE.md +0 -0
  8. {git_juggler-0.1.3 → git_juggler-0.1.4}/LICENSE +0 -0
  9. {git_juggler-0.1.3 → git_juggler-0.1.4}/README.md +0 -0
  10. {git_juggler-0.1.3 → git_juggler-0.1.4}/notes.txt +0 -0
  11. {git_juggler-0.1.3 → git_juggler-0.1.4}/plans/github-actions-settings-and-build-status.md +0 -0
  12. {git_juggler-0.1.3 → git_juggler-0.1.4}/plans/jenkins-settings-and-build-status.md +0 -0
  13. {git_juggler-0.1.3 → git_juggler-0.1.4}/scripts/build-package.sh +0 -0
  14. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/__init__.py +0 -0
  15. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/__init__.py +0 -0
  16. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/activity_tracker.py +0 -0
  17. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/agent_activity_adapter.py +0 -0
  18. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/agent_process_discovery.py +0 -0
  19. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/agent_repository_tracker.py +0 -0
  20. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/create_process_inspector.py +0 -0
  21. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/git_resolver.py +0 -0
  22. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/linux_process_inspector.py +0 -0
  23. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/mac_process_inspector.py +0 -0
  24. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/paths.py +0 -0
  25. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/process_info.py +0 -0
  26. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/process_inspector.py +0 -0
  27. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/agent_tracking/worktree_evidence.py +0 -0
  28. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/app.py +0 -0
  29. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/browse.py +0 -0
  30. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/ci.py +0 -0
  31. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/commit_detail.py +0 -0
  32. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/config.py +0 -0
  33. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/dev_app.py +0 -0
  34. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/frontend_dist/assets/index-DrLGhTYA.css +0 -0
  35. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/frontend_dist/assets/index-DxTmj1Il.js +0 -0
  36. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/frontend_dist/favicon.svg +0 -0
  37. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/frontend_dist/icons.svg +0 -0
  38. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/frontend_dist/index.html +0 -0
  39. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/git_data.py +0 -0
  40. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/git_utils.py +0 -0
  41. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/github_actions.py +0 -0
  42. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/jenkins.py +0 -0
  43. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/main.py +0 -0
  44. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/repos.py +0 -0
  45. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/git_juggler/schemas.py +0 -0
  46. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/tests/test_agent_tracking.py +0 -0
  47. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/tests/test_git_data.py +0 -0
  48. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/tests/test_github_actions.py +0 -0
  49. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/tests/test_jenkins.py +0 -0
  50. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/backend/tests/test_repos.py +0 -0
  51. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/.gitignore +0 -0
  52. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/README.md +0 -0
  53. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/index.html +0 -0
  54. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/package-lock.json +0 -0
  55. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/package.json +0 -0
  56. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/public/favicon.svg +0 -0
  57. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/public/icons.svg +0 -0
  58. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/App.css +0 -0
  59. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/App.tsx +0 -0
  60. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/api/client.ts +0 -0
  61. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/api/types.ts +0 -0
  62. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Agents/AgentActivityPanel.tsx +0 -0
  63. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Badges/BranchBadge.tsx +0 -0
  64. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
  65. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Badges/StashBadge.tsx +0 -0
  66. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Badges/TagBadge.tsx +0 -0
  67. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Badges/icons.tsx +0 -0
  68. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
  69. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
  70. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
  71. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
  72. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
  73. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +0 -0
  74. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +0 -0
  75. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
  76. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -0
  77. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
  78. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Graph/GraphPanel.tsx +0 -0
  79. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Graph/branchColor.ts +0 -0
  80. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
  81. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
  82. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Menu/MainMenu.tsx +0 -0
  83. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Search/MultiSelect.tsx +0 -0
  84. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Search/SearchBox.tsx +0 -0
  85. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Sidebar/RepoList.tsx +0 -0
  86. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Tabs/TabsBar.tsx +0 -0
  87. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
  88. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
  89. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/index.css +0 -0
  90. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/index.tsx +0 -0
  91. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
  92. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/lib/flip.ts +0 -0
  93. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/lib/formatDate.ts +0 -0
  94. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/lib/scrollToCommit.ts +0 -0
  95. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/lib/stripAnsi.ts +0 -0
  96. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/lib/terminalTheme.ts +0 -0
  97. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/src/state/store.ts +0 -0
  98. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/tsconfig.app.json +0 -0
  99. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/tsconfig.json +0 -0
  100. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/tsconfig.node.json +0 -0
  101. {git_juggler-0.1.3 → git_juggler-0.1.4}/src/frontend/vite.config.ts +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: git-juggler
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Browse multiple local git repos' commit graphs and a shell, from one local web UI.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.10
8
8
  Requires-Dist: fastapi>=0.110
9
9
  Requires-Dist: gitpython>=3.1
10
+ Requires-Dist: pywinpty>=2.0; sys_platform == 'win32'
10
11
  Requires-Dist: uvicorn[standard]>=0.29
@@ -8,6 +8,7 @@ dependencies = [
8
8
  "fastapi>=0.110",
9
9
  "uvicorn[standard]>=0.29",
10
10
  "gitpython>=3.1",
11
+ "pywinpty>=2.0; sys_platform == 'win32'",
11
12
  ]
12
13
 
13
14
  [project.scripts]
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sys
7
+ import threading
8
+ from pathlib import Path
9
+
10
+ from fastapi import WebSocket, WebSocketDisconnect
11
+
12
+ # Wire protocol (JSON text frames both ways), shared by both platform
13
+ # implementations below:
14
+ # client -> server: {"type": "input", "data": "<keystrokes>"}
15
+ # {"type": "resize", "cols": n, "rows": n}
16
+ # server -> client: {"type": "output", "data": "<shell output>"}
17
+
18
+
19
+ async def run_terminal_session(websocket: WebSocket, cwd: Path) -> None:
20
+ """Spawn a real shell in a pseudo-terminal rooted at `cwd` and relay it
21
+ over the socket. POSIX and Windows need genuinely different pty APIs
22
+ (openpty/fork vs ConPTY), so this just dispatches to whichever this
23
+ process is running on; the platform-only imports (fcntl/pty/termios,
24
+ winpty) happen inside each implementation so neither is ever required
25
+ on the other platform.
26
+ """
27
+ if sys.platform == "win32":
28
+ await _run_windows_session(websocket, cwd)
29
+ else:
30
+ await _run_posix_session(websocket, cwd)
31
+
32
+
33
+ async def _run_posix_session(websocket: WebSocket, cwd: Path) -> None:
34
+ import fcntl
35
+ import pty
36
+ import signal
37
+ import struct
38
+ import termios
39
+
40
+ def _set_winsize(fd: int, rows: int, cols: int) -> None:
41
+ winsize = struct.pack("HHHH", rows, cols, 0, 0)
42
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
43
+
44
+ shell = os.environ.get("SHELL", "/bin/bash")
45
+ pid, master_fd = pty.fork()
46
+
47
+ if pid == 0: # child
48
+ os.chdir(cwd)
49
+ os.execvp(shell, [shell])
50
+ os._exit(1) # pragma: no cover - unreachable
51
+
52
+ loop = asyncio.get_running_loop()
53
+ output_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
54
+
55
+ def _on_readable() -> None:
56
+ try:
57
+ data = os.read(master_fd, 65536)
58
+ except OSError:
59
+ data = b""
60
+ if not data:
61
+ loop.remove_reader(master_fd)
62
+ output_queue.put_nowait(None)
63
+ return
64
+ output_queue.put_nowait(data)
65
+
66
+ loop.add_reader(master_fd, _on_readable)
67
+
68
+ async def pump_output() -> None:
69
+ while True:
70
+ chunk = await output_queue.get()
71
+ if chunk is None:
72
+ break
73
+ await websocket.send_text(json.dumps({"type": "output", "data": chunk.decode(errors="replace")}))
74
+
75
+ output_task = asyncio.create_task(pump_output())
76
+
77
+ try:
78
+ while True:
79
+ text = await websocket.receive_text()
80
+ try:
81
+ payload = json.loads(text)
82
+ except ValueError:
83
+ continue
84
+ msg_type = payload.get("type")
85
+ if msg_type == "input":
86
+ os.write(master_fd, payload.get("data", "").encode())
87
+ elif msg_type == "resize":
88
+ _set_winsize(master_fd, int(payload.get("rows", 24)), int(payload.get("cols", 80)))
89
+ except WebSocketDisconnect:
90
+ pass
91
+ finally:
92
+ try:
93
+ loop.remove_reader(master_fd)
94
+ except (ValueError, OSError):
95
+ pass
96
+ output_task.cancel()
97
+ try:
98
+ os.kill(pid, signal.SIGHUP)
99
+ except ProcessLookupError:
100
+ pass
101
+ try:
102
+ os.close(master_fd)
103
+ except OSError:
104
+ pass
105
+
106
+
107
+ async def _run_windows_session(websocket: WebSocket, cwd: Path) -> None:
108
+ # Imported lazily so POSIX systems never need this dependency installed,
109
+ # and so a missing install fails inside one terminal connection instead
110
+ # of at process startup (see pyproject.toml: pywinpty is a Windows-only
111
+ # extra, `sys_platform == "win32"`).
112
+ from winpty import PtyProcess
113
+
114
+ shell = os.environ.get("COMSPEC", "cmd.exe")
115
+ process = PtyProcess.spawn(shell, cwd=str(cwd), dimensions=(24, 80))
116
+
117
+ loop = asyncio.get_running_loop()
118
+ output_queue: asyncio.Queue[str | None] = asyncio.Queue()
119
+
120
+ # winpty's read() is a blocking call (there's no fd to hand to the
121
+ # asyncio selector on Windows), so it has to live on its own thread;
122
+ # results are handed back to the event loop via call_soon_threadsafe.
123
+ def _read_loop() -> None:
124
+ while True:
125
+ try:
126
+ data = process.read(65536)
127
+ except EOFError:
128
+ loop.call_soon_threadsafe(output_queue.put_nowait, None)
129
+ return
130
+ if not data:
131
+ loop.call_soon_threadsafe(output_queue.put_nowait, None)
132
+ return
133
+ loop.call_soon_threadsafe(output_queue.put_nowait, data)
134
+
135
+ reader_thread = threading.Thread(target=_read_loop, daemon=True)
136
+ reader_thread.start()
137
+
138
+ async def pump_output() -> None:
139
+ while True:
140
+ chunk = await output_queue.get()
141
+ if chunk is None:
142
+ break
143
+ await websocket.send_text(json.dumps({"type": "output", "data": chunk}))
144
+
145
+ output_task = asyncio.create_task(pump_output())
146
+
147
+ try:
148
+ while True:
149
+ text = await websocket.receive_text()
150
+ try:
151
+ payload = json.loads(text)
152
+ except ValueError:
153
+ continue
154
+ msg_type = payload.get("type")
155
+ if msg_type == "input":
156
+ process.write(payload.get("data", ""))
157
+ elif msg_type == "resize":
158
+ process.setwinsize(int(payload.get("rows", 24)), int(payload.get("cols", 80)))
159
+ except WebSocketDisconnect:
160
+ pass
161
+ finally:
162
+ output_task.cancel()
163
+ try:
164
+ process.terminate(force=True)
165
+ except Exception:
166
+ pass
@@ -1,89 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import fcntl
5
- import json
6
- import os
7
- import pty
8
- import signal
9
- import struct
10
- import termios
11
- from pathlib import Path
12
-
13
- from fastapi import WebSocket, WebSocketDisconnect
14
-
15
-
16
- def _set_winsize(fd: int, rows: int, cols: int) -> None:
17
- winsize = struct.pack("HHHH", rows, cols, 0, 0)
18
- fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
19
-
20
-
21
- async def run_terminal_session(websocket: WebSocket, cwd: Path) -> None:
22
- """Spawn a real shell in a PTY rooted at `cwd` and relay it over the socket.
23
-
24
- Wire protocol (JSON text frames both ways):
25
- client -> server: {"type": "input", "data": "<keystrokes>"}
26
- {"type": "resize", "cols": n, "rows": n}
27
- server -> client: {"type": "output", "data": "<shell output>"}
28
- """
29
- shell = os.environ.get("SHELL", "/bin/bash")
30
- pid, master_fd = pty.fork()
31
-
32
- if pid == 0: # child
33
- os.chdir(cwd)
34
- os.execvp(shell, [shell])
35
- os._exit(1) # pragma: no cover - unreachable
36
-
37
- loop = asyncio.get_running_loop()
38
- output_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
39
-
40
- def _on_readable() -> None:
41
- try:
42
- data = os.read(master_fd, 65536)
43
- except OSError:
44
- data = b""
45
- if not data:
46
- loop.remove_reader(master_fd)
47
- output_queue.put_nowait(None)
48
- return
49
- output_queue.put_nowait(data)
50
-
51
- loop.add_reader(master_fd, _on_readable)
52
-
53
- async def pump_output() -> None:
54
- while True:
55
- chunk = await output_queue.get()
56
- if chunk is None:
57
- break
58
- await websocket.send_text(json.dumps({"type": "output", "data": chunk.decode(errors="replace")}))
59
-
60
- output_task = asyncio.create_task(pump_output())
61
-
62
- try:
63
- while True:
64
- text = await websocket.receive_text()
65
- try:
66
- payload = json.loads(text)
67
- except ValueError:
68
- continue
69
- msg_type = payload.get("type")
70
- if msg_type == "input":
71
- os.write(master_fd, payload.get("data", "").encode())
72
- elif msg_type == "resize":
73
- _set_winsize(master_fd, int(payload.get("rows", 24)), int(payload.get("cols", 80)))
74
- except WebSocketDisconnect:
75
- pass
76
- finally:
77
- try:
78
- loop.remove_reader(master_fd)
79
- except (ValueError, OSError):
80
- pass
81
- output_task.cancel()
82
- try:
83
- os.kill(pid, signal.SIGHUP)
84
- except ProcessLookupError:
85
- pass
86
- try:
87
- os.close(master_fd)
88
- except OSError:
89
- pass
File without changes
File without changes
File without changes
File without changes
File without changes