gitdirector 1.4.5__tar.gz → 1.5.2__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.
- {gitdirector-1.4.5 → gitdirector-1.5.2}/PKG-INFO +67 -3
- {gitdirector-1.4.5 → gitdirector-1.5.2}/README.md +66 -2
- {gitdirector-1.4.5 → gitdirector-1.5.2}/pyproject.toml +1 -1
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/cli.py +2 -0
- gitdirector-1.5.2/src/gitdirector/commands/autoclean.py +39 -0
- gitdirector-1.5.2/src/gitdirector/commands/gd_tmux.py +79 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/help.py +2 -1
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/__init__.py +12 -5
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app.py +49 -10
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app_panels.py +6 -1
- gitdirector-1.5.2/src/gitdirector/commands/tui/diff_renderer.py +832 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/panel_view.py +1 -1
- gitdirector-1.5.2/src/gitdirector/commands/tui/screens/__init__.py +64 -0
- gitdirector-1.5.2/src/gitdirector/commands/tui/screens/_shared.py +115 -0
- gitdirector-1.5.2/src/gitdirector/commands/tui/screens/commit.py +474 -0
- gitdirector-1.5.2/src/gitdirector/commands/tui/screens/diff.py +793 -0
- gitdirector-1.5.2/src/gitdirector/commands/tui/screens/diff_files.py +543 -0
- gitdirector-1.4.5/src/gitdirector/commands/tui/screens.py → gitdirector-1.5.2/src/gitdirector/commands/tui/screens/panels.py +146 -863
- gitdirector-1.5.2/src/gitdirector/commands/tui/screens/repos.py +549 -0
- gitdirector-1.5.2/src/gitdirector/commands/tui/screens/sessions.py +143 -0
- gitdirector-1.5.2/src/gitdirector/commands/tui/terminal_caps.py +133 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/terminal_widget.py +28 -13
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/tmux/core.py +31 -5
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/tmux/monitor.py +14 -5
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/repo.py +112 -0
- gitdirector-1.4.5/src/gitdirector/commands/autoclean.py +0 -85
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/__init__.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/__init__.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/cd.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/info.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/link.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/listt.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/pull.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/status.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app_repos.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app_sessions.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app_ui.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/constants.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/panels.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/unlink.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/config.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/info.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/__init__.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/tmux/__init__.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/tmux/panels.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/manager.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/storage.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/ui_theme.py +0 -0
- {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: gitdirector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.2
|
|
4
4
|
Summary: A terminal based control plane for developers working across multiple repositories. Launch multiple AI coding agents, multiple tmux sessions and track changes across all your repos in one place.
|
|
5
5
|
Keywords: git,repository,manager,cli,synchronization,batch
|
|
6
6
|
Author: Anito Anto
|
|
@@ -74,8 +74,9 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
|
|
|
74
74
|
| `gitdirector status` | Show repositories with staged/unstaged files |
|
|
75
75
|
| `gitdirector pull` | Pull latest changes for all tracked repositories |
|
|
76
76
|
| `gitdirector cd NAME` | Open or switch to a tmux session for a repository |
|
|
77
|
-
| `gitdirector autoclean
|
|
77
|
+
| `gitdirector autoclean` | Remove broken repository links from tracking |
|
|
78
78
|
| `gitdirector info PATH\|NAME [--full]` | Show file statistics for a repository |
|
|
79
|
+
| `gitdirector gd-tmux PATH\|NAME "command"` | Create a gd tmux session and run a command in it |
|
|
79
80
|
| `gitdirector help` | Show help |
|
|
80
81
|
|
|
81
82
|
### link
|
|
@@ -103,8 +104,9 @@ Features:
|
|
|
103
104
|
- `r` to refresh all statuses
|
|
104
105
|
- Press `enter` on any repository to open an action menu:
|
|
105
106
|
- **New tmux session** — create and attach a session for the repository
|
|
107
|
+
- **Review Diff** — open a two-pane viewer for uncommitted changes (left: file list with GitHub-style status pills and +/- counts; right: per-file unified diff with GitHub-dark syntax highlighting, line numbers, and green/red backgrounds for added/removed lines). Navigate files with `j`/`k`, `n`/`p`, `g`/`G`, or `]`/`[`. Switch the focused panel with `tab`. `r` refreshes, `esc` closes.
|
|
106
108
|
- **Attach existing session** — switch to any already-running tmux session
|
|
107
|
-
- **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, or
|
|
109
|
+
- **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, Codex, or Pi in a new tmux session
|
|
108
110
|
- **Remove session** — kill a running tmux session
|
|
109
111
|
|
|
110
112
|
### unlink
|
|
@@ -174,6 +176,68 @@ Opens a [tmux](https://github.com/tmux/tmux) session rooted at the repository di
|
|
|
174
176
|
> Debian/Ubuntu: `sudo apt install tmux`
|
|
175
177
|
> Arch: `sudo pacman -S tmux`
|
|
176
178
|
|
|
179
|
+
### gd-tmux
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
gitdirector gd-tmux my-repo "npm test"
|
|
183
|
+
gitdirector gd-tmux /path/to/my-repo "make build"
|
|
184
|
+
gitdirector gd-tmux my-repo 'echo "hello world"'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Creates a new gd tmux session rooted at the given repository, runs the supplied command inside it, and attaches the session. The session is created using the same code path the TUI's **Launch AI agent** menu uses: it is a real `gd/{repo}/{command}/{N}` session, visible in the Sessions tab of `gitdirector console`, and it self-destructs when the command exits.
|
|
188
|
+
|
|
189
|
+
- `PATH|NAME` — accept either an absolute path to a tracked repository or its basename, with the same disambiguation rules as `gitdirector unlink`.
|
|
190
|
+
- `"command"` — the shell command to run inside the new session. Pass it quoted so the shell hands the whole string to gitdirector; quotes and arguments are preserved verbatim.
|
|
191
|
+
|
|
192
|
+
The command is the same as opening the repo in the console, picking **Launch AI agent** from the action menu, and entering the command — but you can do it from any shell without entering the TUI first. The new session shows up in the Sessions tab immediately.
|
|
193
|
+
|
|
194
|
+
### autoclean
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
gitdirector autoclean
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Scans the tracked repositories in `~/.gitdirector/config.yaml` for paths that no longer exist on disk and removes them from the config. Lists any broken links for review and asks for confirmation before deleting them.
|
|
201
|
+
|
|
202
|
+
## Note for AI coding agents
|
|
203
|
+
|
|
204
|
+
If you are an AI coding agent reading this project: when the user asks you to run a long-lived process inside a tracked repository — a dev server, a frontend bundler, a backend API, a watcher, a database, a test runner with a long timeout, or anything else that does not exit on its own — **do not run it in the foreground**. A foreground process will block your shell for the rest of the session and the user will not be able to interact with you.
|
|
205
|
+
|
|
206
|
+
Use `gitdirector gd-tmux` to launch the process in a detached gd tmux session that the user can monitor and tear down themselves. The session shows up in the Sessions tab of `gitdirector console`, auto-cleans up when the process exits, and supports path-or-name lookup the same way every other gitdirector command does.
|
|
207
|
+
|
|
208
|
+
**Always prefer the absolute path over the repo name.** The by-name lookup matches the *directory name on disk* verbatim (via `Path.name`) — case-sensitive, dots and spaces included — and is ambiguous if two tracked repos share the same basename. When in doubt, look up the path with `gitdirector list` and use the absolute path. Quoting a path is fine; an absolute path is never ambiguous.
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Frontend dev server in the user's web repo — use the absolute path
|
|
214
|
+
gitdirector gd-tmux /Users/me/projects/webapp "npm run dev"
|
|
215
|
+
|
|
216
|
+
# Backend API in the api repo
|
|
217
|
+
gitdirector gd-tmux /Users/me/projects/api "uvicorn app.main:app --reload --port 8000"
|
|
218
|
+
|
|
219
|
+
# A long-running integration test with output captured to a log
|
|
220
|
+
gitdirector gd-tmux /Users/me/projects/myrepo "pytest -x -v 2>&1 | tee /tmp/test.log"
|
|
221
|
+
|
|
222
|
+
# A watcher or build loop
|
|
223
|
+
gitdirector gd-tmux /Users/me/projects/myrepo "npm run watch"
|
|
224
|
+
|
|
225
|
+
# A repo whose directory name contains a space — quote both the path and the command
|
|
226
|
+
gitdirector gd-tmux "/Users/me/projects/My Repo" "npm run dev"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The by-name form is supported for ergonomic use by humans in interactive shells, but agents should default to the absolute path to avoid any chance of matching the wrong repo.
|
|
230
|
+
|
|
231
|
+
Rules of thumb:
|
|
232
|
+
|
|
233
|
+
- **Use the absolute path.** It is unambiguous and survives the user renaming or moving the directory. The by-name form is convenient shorthand, not a slug — it is the raw directory name, case-sensitive.
|
|
234
|
+
- **Repo names with spaces, dots, or punctuation are matched verbatim** when you do use the by-name form. A repo at `/Users/me/My Repo` is matched by `"My Repo"`, not by `My` or `Repo`. Quote the name to keep the shell from splitting it.
|
|
235
|
+
- **Quoting the command matters.** The command string is handed to `sh -lc` inside the new session, so write it as a single quoted string. `'echo "hi"'` (single-quote outer, double-quote inner) is the safest pattern for embedded double quotes.
|
|
236
|
+
- **Path separators in the command are fine.** Commands like `python /path/to/script.py` or `find . -name foo/bar` work; the command is embedded verbatim into the inner shell, while the session-name label is auto-sanitized.
|
|
237
|
+
- **The session self-destructs** when the command exits (success or failure), so you do not need to clean it up.
|
|
238
|
+
- **If you need to capture output**, redirect it to a file in the command itself (`> /tmp/out 2>&1`) and read the file later — the session is gone once the command exits.
|
|
239
|
+
- **If the user asks you to stop a running process**, kill the matching session: `gitdirector console` → Sessions tab, or directly with `tmux kill-session -t =<session-name>`.
|
|
240
|
+
|
|
177
241
|
## Configuration
|
|
178
242
|
|
|
179
243
|
Config is stored at `~/.gitdirector/config.yaml`.
|
|
@@ -31,8 +31,9 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
|
|
|
31
31
|
| `gitdirector status` | Show repositories with staged/unstaged files |
|
|
32
32
|
| `gitdirector pull` | Pull latest changes for all tracked repositories |
|
|
33
33
|
| `gitdirector cd NAME` | Open or switch to a tmux session for a repository |
|
|
34
|
-
| `gitdirector autoclean
|
|
34
|
+
| `gitdirector autoclean` | Remove broken repository links from tracking |
|
|
35
35
|
| `gitdirector info PATH\|NAME [--full]` | Show file statistics for a repository |
|
|
36
|
+
| `gitdirector gd-tmux PATH\|NAME "command"` | Create a gd tmux session and run a command in it |
|
|
36
37
|
| `gitdirector help` | Show help |
|
|
37
38
|
|
|
38
39
|
### link
|
|
@@ -60,8 +61,9 @@ Features:
|
|
|
60
61
|
- `r` to refresh all statuses
|
|
61
62
|
- Press `enter` on any repository to open an action menu:
|
|
62
63
|
- **New tmux session** — create and attach a session for the repository
|
|
64
|
+
- **Review Diff** — open a two-pane viewer for uncommitted changes (left: file list with GitHub-style status pills and +/- counts; right: per-file unified diff with GitHub-dark syntax highlighting, line numbers, and green/red backgrounds for added/removed lines). Navigate files with `j`/`k`, `n`/`p`, `g`/`G`, or `]`/`[`. Switch the focused panel with `tab`. `r` refreshes, `esc` closes.
|
|
63
65
|
- **Attach existing session** — switch to any already-running tmux session
|
|
64
|
-
- **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, or
|
|
66
|
+
- **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, Codex, or Pi in a new tmux session
|
|
65
67
|
- **Remove session** — kill a running tmux session
|
|
66
68
|
|
|
67
69
|
### unlink
|
|
@@ -131,6 +133,68 @@ Opens a [tmux](https://github.com/tmux/tmux) session rooted at the repository di
|
|
|
131
133
|
> Debian/Ubuntu: `sudo apt install tmux`
|
|
132
134
|
> Arch: `sudo pacman -S tmux`
|
|
133
135
|
|
|
136
|
+
### gd-tmux
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
gitdirector gd-tmux my-repo "npm test"
|
|
140
|
+
gitdirector gd-tmux /path/to/my-repo "make build"
|
|
141
|
+
gitdirector gd-tmux my-repo 'echo "hello world"'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Creates a new gd tmux session rooted at the given repository, runs the supplied command inside it, and attaches the session. The session is created using the same code path the TUI's **Launch AI agent** menu uses: it is a real `gd/{repo}/{command}/{N}` session, visible in the Sessions tab of `gitdirector console`, and it self-destructs when the command exits.
|
|
145
|
+
|
|
146
|
+
- `PATH|NAME` — accept either an absolute path to a tracked repository or its basename, with the same disambiguation rules as `gitdirector unlink`.
|
|
147
|
+
- `"command"` — the shell command to run inside the new session. Pass it quoted so the shell hands the whole string to gitdirector; quotes and arguments are preserved verbatim.
|
|
148
|
+
|
|
149
|
+
The command is the same as opening the repo in the console, picking **Launch AI agent** from the action menu, and entering the command — but you can do it from any shell without entering the TUI first. The new session shows up in the Sessions tab immediately.
|
|
150
|
+
|
|
151
|
+
### autoclean
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
gitdirector autoclean
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Scans the tracked repositories in `~/.gitdirector/config.yaml` for paths that no longer exist on disk and removes them from the config. Lists any broken links for review and asks for confirmation before deleting them.
|
|
158
|
+
|
|
159
|
+
## Note for AI coding agents
|
|
160
|
+
|
|
161
|
+
If you are an AI coding agent reading this project: when the user asks you to run a long-lived process inside a tracked repository — a dev server, a frontend bundler, a backend API, a watcher, a database, a test runner with a long timeout, or anything else that does not exit on its own — **do not run it in the foreground**. A foreground process will block your shell for the rest of the session and the user will not be able to interact with you.
|
|
162
|
+
|
|
163
|
+
Use `gitdirector gd-tmux` to launch the process in a detached gd tmux session that the user can monitor and tear down themselves. The session shows up in the Sessions tab of `gitdirector console`, auto-cleans up when the process exits, and supports path-or-name lookup the same way every other gitdirector command does.
|
|
164
|
+
|
|
165
|
+
**Always prefer the absolute path over the repo name.** The by-name lookup matches the *directory name on disk* verbatim (via `Path.name`) — case-sensitive, dots and spaces included — and is ambiguous if two tracked repos share the same basename. When in doubt, look up the path with `gitdirector list` and use the absolute path. Quoting a path is fine; an absolute path is never ambiguous.
|
|
166
|
+
|
|
167
|
+
Examples:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Frontend dev server in the user's web repo — use the absolute path
|
|
171
|
+
gitdirector gd-tmux /Users/me/projects/webapp "npm run dev"
|
|
172
|
+
|
|
173
|
+
# Backend API in the api repo
|
|
174
|
+
gitdirector gd-tmux /Users/me/projects/api "uvicorn app.main:app --reload --port 8000"
|
|
175
|
+
|
|
176
|
+
# A long-running integration test with output captured to a log
|
|
177
|
+
gitdirector gd-tmux /Users/me/projects/myrepo "pytest -x -v 2>&1 | tee /tmp/test.log"
|
|
178
|
+
|
|
179
|
+
# A watcher or build loop
|
|
180
|
+
gitdirector gd-tmux /Users/me/projects/myrepo "npm run watch"
|
|
181
|
+
|
|
182
|
+
# A repo whose directory name contains a space — quote both the path and the command
|
|
183
|
+
gitdirector gd-tmux "/Users/me/projects/My Repo" "npm run dev"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The by-name form is supported for ergonomic use by humans in interactive shells, but agents should default to the absolute path to avoid any chance of matching the wrong repo.
|
|
187
|
+
|
|
188
|
+
Rules of thumb:
|
|
189
|
+
|
|
190
|
+
- **Use the absolute path.** It is unambiguous and survives the user renaming or moving the directory. The by-name form is convenient shorthand, not a slug — it is the raw directory name, case-sensitive.
|
|
191
|
+
- **Repo names with spaces, dots, or punctuation are matched verbatim** when you do use the by-name form. A repo at `/Users/me/My Repo` is matched by `"My Repo"`, not by `My` or `Repo`. Quote the name to keep the shell from splitting it.
|
|
192
|
+
- **Quoting the command matters.** The command string is handed to `sh -lc` inside the new session, so write it as a single quoted string. `'echo "hi"'` (single-quote outer, double-quote inner) is the safest pattern for embedded double quotes.
|
|
193
|
+
- **Path separators in the command are fine.** Commands like `python /path/to/script.py` or `find . -name foo/bar` work; the command is embedded verbatim into the inner shell, while the session-name label is auto-sanitized.
|
|
194
|
+
- **The session self-destructs** when the command exits (success or failure), so you do not need to clean it up.
|
|
195
|
+
- **If you need to capture output**, redirect it to a file in the command itself (`> /tmp/out 2>&1`) and read the file later — the session is gone once the command exits.
|
|
196
|
+
- **If the user asks you to stop a running process**, kill the matching session: `gitdirector console` → Sessions tab, or directly with `tmux kill-session -t =<session-name>`.
|
|
197
|
+
|
|
134
198
|
## Configuration
|
|
135
199
|
|
|
136
200
|
Config is stored at `~/.gitdirector/config.yaml`.
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitdirector"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.2"
|
|
8
8
|
description = "A terminal based control plane for developers working across multiple repositories. Launch multiple AI coding agents, multiple tmux sessions and track changes across all your repos in one place."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -10,6 +10,7 @@ from .commands import (
|
|
|
10
10
|
autoclean,
|
|
11
11
|
cd,
|
|
12
12
|
console,
|
|
13
|
+
gd_tmux,
|
|
13
14
|
help,
|
|
14
15
|
info,
|
|
15
16
|
link,
|
|
@@ -55,6 +56,7 @@ help.register(cli)
|
|
|
55
56
|
tui.register(cli)
|
|
56
57
|
autoclean.register(cli)
|
|
57
58
|
info.register(cli)
|
|
59
|
+
gd_tmux.register(cli)
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def main():
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from ..config import Config
|
|
4
|
+
from . import console
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(cli: click.Group):
|
|
8
|
+
@cli.command()
|
|
9
|
+
def autoclean():
|
|
10
|
+
_autoclean_links()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _autoclean_links():
|
|
14
|
+
config = Config()
|
|
15
|
+
broken = [p for p in config.repositories if not p.exists()]
|
|
16
|
+
|
|
17
|
+
if not broken:
|
|
18
|
+
console.print()
|
|
19
|
+
console.print(" [green]All links are valid.[/green]")
|
|
20
|
+
console.print()
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
console.print()
|
|
24
|
+
console.print(f" Found [yellow]{len(broken)}[/yellow] broken link(s):\n")
|
|
25
|
+
for p in broken:
|
|
26
|
+
console.print(f" [red]✕[/red] {p}", soft_wrap=True)
|
|
27
|
+
console.print()
|
|
28
|
+
|
|
29
|
+
if not click.confirm(" Remove these broken links?"):
|
|
30
|
+
console.print()
|
|
31
|
+
console.print(" [dim]Cancelled.[/dim]")
|
|
32
|
+
console.print()
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
config.remove_repositories(broken)
|
|
36
|
+
|
|
37
|
+
console.print()
|
|
38
|
+
console.print(f" [green]Removed {len(broken)} broken link(s).[/green]")
|
|
39
|
+
console.print()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..manager import RepositoryManager
|
|
6
|
+
from ..storage import normalize_repository_path
|
|
7
|
+
from . import console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _resolve_repo(target: str) -> Path | None:
|
|
11
|
+
"""Resolve *target* to a tracked repository path.
|
|
12
|
+
|
|
13
|
+
Tries the value as a filesystem path first. If no path-based match is
|
|
14
|
+
found and the value does not look path-like, falls back to looking it up
|
|
15
|
+
by repository name. Prints a rich error and returns ``None`` on missing
|
|
16
|
+
or ambiguous targets.
|
|
17
|
+
"""
|
|
18
|
+
manager = RepositoryManager()
|
|
19
|
+
candidate = Path(target)
|
|
20
|
+
is_path_like = (
|
|
21
|
+
"/" in target
|
|
22
|
+
or "\\" in target
|
|
23
|
+
or target in (".", "..")
|
|
24
|
+
or candidate.is_absolute()
|
|
25
|
+
or target.startswith("~")
|
|
26
|
+
or candidate.exists()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if is_path_like:
|
|
30
|
+
normalized = normalize_repository_path(candidate)
|
|
31
|
+
if manager.config.has_repository(normalized):
|
|
32
|
+
return normalized
|
|
33
|
+
console.print(f"\n [red]No tracked repository at path: {normalized}[/red]\n")
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
matches = [r for r in manager.config.repositories if r.name == target]
|
|
37
|
+
if not matches:
|
|
38
|
+
console.print(f"\n [red]No tracked repository named: {target}[/red]\n")
|
|
39
|
+
return None
|
|
40
|
+
if len(matches) > 1:
|
|
41
|
+
paths_list = "\n".join(f" {p}" for p in matches)
|
|
42
|
+
console.print(
|
|
43
|
+
f"\n [red]Multiple repositories named '{target}' — use the full path:[/red]\n"
|
|
44
|
+
f"{paths_list}\n"
|
|
45
|
+
)
|
|
46
|
+
return None
|
|
47
|
+
return matches[0]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register(cli: click.Group):
|
|
51
|
+
@cli.command()
|
|
52
|
+
@click.argument("target", metavar="PATH|NAME")
|
|
53
|
+
@click.argument("command")
|
|
54
|
+
def gd_tmux(target: str, command: str):
|
|
55
|
+
"""Create a gd tmux session for a repository and run a command in it."""
|
|
56
|
+
if not command.strip():
|
|
57
|
+
console.print("\n [red]Command must not be empty.[/red]\n")
|
|
58
|
+
raise SystemExit(1)
|
|
59
|
+
|
|
60
|
+
repo_path = _resolve_repo(target)
|
|
61
|
+
if repo_path is None:
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
from ..integrations.tmux import (
|
|
66
|
+
attach_tmux_session,
|
|
67
|
+
create_tmux_session,
|
|
68
|
+
launch_command_in_tmux_session,
|
|
69
|
+
)
|
|
70
|
+
except ImportError:
|
|
71
|
+
console.print(
|
|
72
|
+
"\n [red]The tmux integration is unavailable for the gd-tmux command.[/red]\n"
|
|
73
|
+
" Reinstall gitdirector or check your installation.\n"
|
|
74
|
+
)
|
|
75
|
+
raise SystemExit(1)
|
|
76
|
+
|
|
77
|
+
session_name = create_tmux_session(repo_path.name, repo_path, purpose=command)
|
|
78
|
+
launch_command_in_tmux_session(session_name, command)
|
|
79
|
+
attach_tmux_session(session_name, skip_config_sync=True)
|
|
@@ -32,8 +32,9 @@ def show_help():
|
|
|
32
32
|
("pull", "Pull latest changes for all tracked repositories"),
|
|
33
33
|
("cd NAME", "Open or switch to a tmux session for a repository"),
|
|
34
34
|
("console", "Interactive TUI for browsing and opening repositories"),
|
|
35
|
-
("autoclean
|
|
35
|
+
("autoclean", "Remove broken repository links from tracking"),
|
|
36
36
|
("info PATH|NAME [--full]", "Show file statistics for a repository"),
|
|
37
|
+
('gd-tmux PATH|NAME "cmd"', "Create a gd tmux session and run a command in it"),
|
|
37
38
|
("help", "Show this help message"),
|
|
38
39
|
]:
|
|
39
40
|
cmd_table.add_row(cmd, desc)
|
|
@@ -14,20 +14,24 @@ from .constants import (
|
|
|
14
14
|
)
|
|
15
15
|
from .panel_view import PanelViewScreen, PaneWidget
|
|
16
16
|
from .panels import Panel, PanelStore
|
|
17
|
-
from .screens import
|
|
18
|
-
|
|
17
|
+
from .screens import SortMenuScreen
|
|
18
|
+
from .screens.diff import DiffReviewScreen
|
|
19
|
+
from .screens.panels import (
|
|
19
20
|
AgentLoadingScreen,
|
|
20
21
|
ConfirmScreen,
|
|
21
22
|
CreatePanelScreen,
|
|
23
|
+
PanelActionMenuScreen,
|
|
24
|
+
RenamePanelScreen,
|
|
25
|
+
)
|
|
26
|
+
from .screens.repos import (
|
|
27
|
+
ActionMenuScreen,
|
|
22
28
|
GitCommandResultScreen,
|
|
23
29
|
GitOperationsMenuScreen,
|
|
24
30
|
PullLoadingScreen,
|
|
25
31
|
PullResultScreen,
|
|
26
|
-
RemoveSessionScreen,
|
|
27
32
|
RepoInfoScreen,
|
|
28
|
-
SelectSessionScreen,
|
|
29
|
-
SortMenuScreen,
|
|
30
33
|
)
|
|
34
|
+
from .screens.sessions import RemoveSessionScreen, SelectSessionScreen
|
|
31
35
|
from .terminal_widget import TerminalWidget
|
|
32
36
|
|
|
33
37
|
__all__ = [
|
|
@@ -35,18 +39,21 @@ __all__ = [
|
|
|
35
39
|
"AgentLoadingScreen",
|
|
36
40
|
"ConfirmScreen",
|
|
37
41
|
"CreatePanelScreen",
|
|
42
|
+
"DiffReviewScreen",
|
|
38
43
|
"GitCommandResultScreen",
|
|
39
44
|
"GitOperationsMenuScreen",
|
|
40
45
|
"_DEFAULT_PANELS_SORT_COLUMN",
|
|
41
46
|
"GitDirectorConsole",
|
|
42
47
|
"PaneWidget",
|
|
43
48
|
"Panel",
|
|
49
|
+
"PanelActionMenuScreen",
|
|
44
50
|
"PanelStore",
|
|
45
51
|
"PanelViewScreen",
|
|
46
52
|
"_PANELS_SORT_COLUMN_NAMES",
|
|
47
53
|
"PullLoadingScreen",
|
|
48
54
|
"PullResultScreen",
|
|
49
55
|
"RemoveSessionScreen",
|
|
56
|
+
"RenamePanelScreen",
|
|
50
57
|
"RepoInfoScreen",
|
|
51
58
|
"SelectSessionScreen",
|
|
52
59
|
"SortMenuScreen",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Callable
|
|
@@ -31,17 +32,17 @@ from .constants import (
|
|
|
31
32
|
_SESSION_STATUS_POLL_INTERVAL_SECS,
|
|
32
33
|
)
|
|
33
34
|
from .panels import Panel, PanelStore
|
|
34
|
-
from .screens import
|
|
35
|
+
from .screens.diff import DiffReviewScreen
|
|
36
|
+
from .screens.panels import AgentLoadingScreen, ConfirmScreen
|
|
37
|
+
from .screens.repos import (
|
|
35
38
|
ActionMenuScreen,
|
|
36
|
-
AgentLoadingScreen,
|
|
37
|
-
ConfirmScreen,
|
|
38
39
|
GitCommandResultScreen,
|
|
39
40
|
GitOperationsMenuScreen,
|
|
40
41
|
PullLoadingScreen,
|
|
41
42
|
PullResultScreen,
|
|
42
|
-
RemoveSessionScreen,
|
|
43
43
|
RepoInfoScreen,
|
|
44
44
|
)
|
|
45
|
+
from .screens.sessions import RemoveSessionScreen
|
|
45
46
|
|
|
46
47
|
_panel_row_height = _app_panels._panel_row_height
|
|
47
48
|
_render_panel_preview = _app_panels._render_panel_preview
|
|
@@ -55,6 +56,9 @@ __all__ = [
|
|
|
55
56
|
]
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
58
62
|
class GitDirectorConsole(
|
|
59
63
|
ConsolePanelsMixin,
|
|
60
64
|
ConsoleSessionsMixin,
|
|
@@ -355,25 +359,32 @@ class GitDirectorConsole(
|
|
|
355
359
|
if path is None:
|
|
356
360
|
return
|
|
357
361
|
|
|
358
|
-
from ...integrations.tmux import create_tmux_session,
|
|
362
|
+
from ...integrations.tmux import create_tmux_session, launch_command_in_tmux_session
|
|
359
363
|
|
|
360
364
|
purpose = agent_cmd if agent_cmd else "shell"
|
|
361
365
|
session_name = create_tmux_session(path.name, path, purpose=purpose)
|
|
362
366
|
|
|
363
367
|
if agent_cmd:
|
|
364
|
-
ready_marker =
|
|
368
|
+
ready_marker = launch_command_in_tmux_session(session_name, agent_cmd)
|
|
365
369
|
self.push_screen(
|
|
366
370
|
AgentLoadingScreen(agent_cmd, session_name, ready_marker),
|
|
367
371
|
callback=lambda _: self.set_timer(0.2, lambda: self._refresh_repo_for_path(path)),
|
|
368
372
|
)
|
|
369
373
|
else:
|
|
370
|
-
|
|
374
|
+
# create_tmux_session already called sync_panel_tmux_config, so skip
|
|
375
|
+
# the redundant sync in attach_tmux_session. Without this skip the
|
|
376
|
+
# sync re-lists every tmux session, rewrites gd-tmux.conf, and runs
|
|
377
|
+
# `tmux source-file` between the manual screen clear and tmux's
|
|
378
|
+
# first redraw — long enough to expose a visible empty alt-screen.
|
|
379
|
+
self._suspend_and_attach(session_name, path, skip_config_sync=True)
|
|
371
380
|
|
|
372
381
|
def _suspend_and_attach(
|
|
373
382
|
self,
|
|
374
383
|
session_name: str,
|
|
375
384
|
path: Path | None = None,
|
|
376
385
|
row_key: str | None = None,
|
|
386
|
+
*,
|
|
387
|
+
skip_config_sync: bool = False,
|
|
377
388
|
) -> None:
|
|
378
389
|
"""Suspend the TUI and attach to the tmux session."""
|
|
379
390
|
import sys
|
|
@@ -402,7 +413,7 @@ class GitDirectorConsole(
|
|
|
402
413
|
with self.suspend():
|
|
403
414
|
sys.stdout.write("\033[?1049h\033[H\033[2J\033[?25l")
|
|
404
415
|
sys.stdout.flush()
|
|
405
|
-
attach_tmux_session(session_name)
|
|
416
|
+
attach_tmux_session(session_name, skip_config_sync=skip_config_sync)
|
|
406
417
|
sys.stdout.write("\033[?25h")
|
|
407
418
|
sys.stdout.flush()
|
|
408
419
|
try:
|
|
@@ -650,10 +661,27 @@ class GitDirectorConsole(
|
|
|
650
661
|
|
|
651
662
|
from ..pull import pull_repository
|
|
652
663
|
|
|
653
|
-
|
|
664
|
+
try:
|
|
665
|
+
result = pull_repository(path)
|
|
666
|
+
except Exception as exc:
|
|
667
|
+
logger.exception("pull worker crashed")
|
|
668
|
+
error_result = (path.name, False, f"Pull failed: {exc}")
|
|
669
|
+
if self._background_shutdown_requested(worker):
|
|
670
|
+
return
|
|
671
|
+
try:
|
|
672
|
+
self.call_from_thread(
|
|
673
|
+
self._show_pull_result, loading_screen, path, command, error_result
|
|
674
|
+
)
|
|
675
|
+
except Exception:
|
|
676
|
+
logger.debug("Failed to post pull error to UI", exc_info=True)
|
|
677
|
+
return
|
|
678
|
+
|
|
654
679
|
if self._background_shutdown_requested(worker):
|
|
655
680
|
return
|
|
656
|
-
|
|
681
|
+
try:
|
|
682
|
+
self.call_from_thread(self._show_pull_result, loading_screen, path, command, result)
|
|
683
|
+
except Exception:
|
|
684
|
+
logger.debug("Failed to post pull result to UI", exc_info=True)
|
|
657
685
|
|
|
658
686
|
def _show_pull_result(
|
|
659
687
|
self,
|
|
@@ -677,6 +705,7 @@ class GitDirectorConsole(
|
|
|
677
705
|
"agent:claude": "claude",
|
|
678
706
|
"agent:copilot": "copilot",
|
|
679
707
|
"agent:codex": "codex",
|
|
708
|
+
"agent:pi": "pi",
|
|
680
709
|
}
|
|
681
710
|
|
|
682
711
|
def _handle_menu_action(self, action: str | None) -> None:
|
|
@@ -690,6 +719,8 @@ class GitDirectorConsole(
|
|
|
690
719
|
session_name = action[len("attach:") :]
|
|
691
720
|
path = self._get_selected_path()
|
|
692
721
|
self._attach_to_session(session_name, path)
|
|
722
|
+
elif action == "review_diff":
|
|
723
|
+
self._open_review_diff()
|
|
693
724
|
elif action == "remove_session":
|
|
694
725
|
path = self._get_selected_path()
|
|
695
726
|
if path:
|
|
@@ -698,6 +729,14 @@ class GitDirectorConsole(
|
|
|
698
729
|
callback=self._handle_remove_selection,
|
|
699
730
|
)
|
|
700
731
|
|
|
732
|
+
def _open_review_diff(self) -> None:
|
|
733
|
+
path = self._get_selected_path()
|
|
734
|
+
if path is None:
|
|
735
|
+
return
|
|
736
|
+
info = self._results.get(str(path))
|
|
737
|
+
branch = info.branch if info else None
|
|
738
|
+
self.push_screen(DiffReviewScreen(path.name, path, branch=branch))
|
|
739
|
+
|
|
701
740
|
def _handle_remove_selection(self, session_name: str | None) -> None:
|
|
702
741
|
if session_name is None:
|
|
703
742
|
return
|
|
@@ -12,7 +12,12 @@ from .constants import (
|
|
|
12
12
|
_PANELS_SORT_COLUMN_NAMES,
|
|
13
13
|
)
|
|
14
14
|
from .panels import Panel, render_panel_layout_preview
|
|
15
|
-
from .screens import
|
|
15
|
+
from .screens.panels import (
|
|
16
|
+
ConfirmScreen,
|
|
17
|
+
CreatePanelScreen,
|
|
18
|
+
PanelActionMenuScreen,
|
|
19
|
+
RenamePanelScreen,
|
|
20
|
+
)
|
|
16
21
|
|
|
17
22
|
_PANEL_PREVIEW_FILLED = "■"
|
|
18
23
|
_PANEL_PREVIEW_OPEN = "□"
|