gitgotchi 0.3.1__tar.gz → 0.4.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.
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/CHANGELOG.md +27 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/PKG-INFO +54 -3
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/README.md +51 -2
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/pyproject.toml +4 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/__init__.py +1 -1
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/cli.py +120 -7
- gitgotchi-0.4.0/src/gitgotchi/doctor.py +190 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/hooks/post_commit.py +11 -2
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/renderer.py +30 -4
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/__init__.py +2 -1
- gitgotchi-0.4.0/src/gitgotchi/render/badge.py +99 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/card.py +19 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/forms.py +25 -9
- gitgotchi-0.4.0/src/gitgotchi/render/raster.py +341 -0
- gitgotchi-0.4.0/src/gitgotchi/statusline.py +191 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/PKG-INFO +54 -3
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/SOURCES.txt +5 -0
- gitgotchi-0.4.0/src/gitgotchi.egg-info/entry_points.txt +3 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/requires.txt +3 -0
- gitgotchi-0.4.0/tests/test_features.py +78 -0
- gitgotchi-0.4.0/tests/test_statusline.py +60 -0
- gitgotchi-0.3.1/src/gitgotchi/render/raster.py +0 -217
- gitgotchi-0.3.1/src/gitgotchi.egg-info/entry_points.txt +0 -2
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/INSTALL.md +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/LICENSE +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/MANIFEST.in +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/setup.cfg +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/__main__.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/db/__init__.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/db/models.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/db/state_manager.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/demo.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/haunt.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/history.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/hooks/__init__.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/hooks/installer.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/paths.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/__init__.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/possession.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/quality.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/states.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/theme.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/effects.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/font.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/scene.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/terminal.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/seance/__init__.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/seance/hauntings.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/seance/story_gen.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/dependency_links.txt +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/top_level.txt +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/conftest.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_cli.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_haunt.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_hauntings.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_history.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_hooks.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_possession.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_quality.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_render.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_state_manager.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_states.py +0 -0
- {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_story.py +0 -0
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### New
|
|
6
|
+
|
|
7
|
+
- `gitgotchi-statusline`: one line about your companion for Claude Code's status line, tmux or
|
|
8
|
+
Starship. It uses only the standard library, so it runs in about 50 ms, and prints nothing outside
|
|
9
|
+
a haunted repo. `gitgotchi statusline --setup` shows the config snippets.
|
|
10
|
+
- `gitgotchi badge`: an SVG README badge with a pixel portrait of your companion and its form.
|
|
11
|
+
- `gitgotchi config full|line|off [--global]`: choose how much the hook prints after each commit.
|
|
12
|
+
- `gitgotchi doctor`: checks git, the hook and its Python, the database, your terminal and the
|
|
13
|
+
API key, with a fix for each problem. It never prints the key.
|
|
14
|
+
- `gitgotchi card --gif`: an animated card (install `gitgotchi[gif]` for Pillow).
|
|
15
|
+
|
|
16
|
+
## 0.3.2
|
|
17
|
+
|
|
18
|
+
### Sharper sprites
|
|
19
|
+
|
|
20
|
+
- Every form is framed bigger, so it gets about 60% more pixels in the same space.
|
|
21
|
+
- Shading uses hue-shifted color ramps (cool shadows, warm highlights) lit from the upper front, with
|
|
22
|
+
soft contact shadows, so forms look round instead of striped.
|
|
23
|
+
- Glossy highlights on the pumpkin, the Demon and the Poltergeist's keycaps.
|
|
24
|
+
- Outlines pick up a little of the color they border, and dark inner lines separate parts that
|
|
25
|
+
overlap (arms, horns, keycaps).
|
|
26
|
+
- A cleanup pass removes one-pixel spurs, notches and stray shading pixels.
|
|
27
|
+
- The pumpkin's shadows are warm instead of muddy, the Demon has bigger curved horns, and the Angel
|
|
28
|
+
has feathered wings and a halo that stays in frame.
|
|
29
|
+
|
|
3
30
|
## 0.3.1
|
|
4
31
|
|
|
5
32
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitgotchi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A 3D pixel-art Halloween spirit that haunts your git repo and reacts as you (and your AI agents) commit
|
|
5
5
|
Author-email: M K Sai Srikar <Srikarmks@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,6 +31,8 @@ Requires-Dist: sqlalchemy>=2.0.36
|
|
|
31
31
|
Requires-Dist: typer>=0.15.4
|
|
32
32
|
Requires-Dist: python-dotenv>=1.0.0
|
|
33
33
|
Requires-Dist: numpy>=1.24
|
|
34
|
+
Provides-Extra: gif
|
|
35
|
+
Requires-Dist: pillow>=10.0; extra == "gif"
|
|
34
36
|
Provides-Extra: dev
|
|
35
37
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
36
38
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -95,10 +97,55 @@ single line instead of the full pet, so it doesn't flood the agent's context.
|
|
|
95
97
|
gitgotchi card
|
|
96
98
|
```
|
|
97
99
|
|
|
98
|
-
Saves a 1200×630 card, sized for LinkedIn and other link previews
|
|
100
|
+
Saves a 1200×630 card, sized for LinkedIn and other link previews. Add `--gif` for one where your companion
|
|
101
|
+
moves (needs `pip install "gitgotchi[gif]"`).
|
|
99
102
|
|
|
100
103
|

|
|
101
104
|
|
|
105
|
+
For your README, `gitgotchi badge` saves a small SVG with your companion's portrait and form, and prints the
|
|
106
|
+
Markdown to paste in. Run it again whenever your companion evolves.
|
|
107
|
+
|
|
108
|
+
## 📟 In your status bar
|
|
109
|
+
|
|
110
|
+
`gitgotchi-statusline` prints one line about the companion in the current repo, for example
|
|
111
|
+
`👾 Boo the Poltergeist · 63% possessed · hungry`. It starts in about 50 ms and prints nothing outside a
|
|
112
|
+
haunted repo, so it's safe to leave on everywhere.
|
|
113
|
+
|
|
114
|
+
Claude Code: add this to `~/.claude/settings.json`
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
"statusLine": {"type": "command", "command": "gitgotchi-statusline"}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
tmux: add this to `~/.tmux.conf`
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
set -g status-right '#(cd #{pane_current_path} && gitgotchi-statusline --plain)'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`gitgotchi-statusline --setup` prints these snippets, plus one for Starship.
|
|
127
|
+
|
|
128
|
+
## 🔕 How much the hook says
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
gitgotchi config line # one line per commit in this repo
|
|
132
|
+
gitgotchi config off --global # silent everywhere
|
|
133
|
+
gitgotchi config full # the whole pet (the default)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
It's stored in git config as `gitgotchi.output`, so a repo's setting wins over the global one. When an agent
|
|
137
|
+
runs `git commit` without a terminal, the hook always keeps it to one line.
|
|
138
|
+
|
|
139
|
+
## 🩺 Something not working?
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
gitgotchi doctor
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Checks git, the hook and the Python it points at, the pet database, your terminal's colors and size, and
|
|
146
|
+
whether stories can use Claude, with a fix for anything that's wrong. Include its output when you open an
|
|
147
|
+
issue; it never shows your API key.
|
|
148
|
+
|
|
102
149
|
## 🔮 Séance
|
|
103
150
|
|
|
104
151
|
```bash
|
|
@@ -122,7 +169,11 @@ story. GitGotchi reads only `ANTHROPIC_API_KEY` from `.env` and ignores everythi
|
|
|
122
169
|
| `gitgotchi haunt` | Live pane that reacts to every commit |
|
|
123
170
|
| `gitgotchi status` | Your companion and its stats |
|
|
124
171
|
| `gitgotchi seance` | Ghost stories from your repo's history |
|
|
125
|
-
| `gitgotchi card` | Save a shareable
|
|
172
|
+
| `gitgotchi card` | Save a shareable card (`--gif` for an animated one) |
|
|
173
|
+
| `gitgotchi badge` | Save a README badge with your companion's portrait |
|
|
174
|
+
| `gitgotchi-statusline` | One line for Claude Code, tmux or Starship |
|
|
175
|
+
| `gitgotchi config` | How much the hook prints: `full`, `line` or `off` |
|
|
176
|
+
| `gitgotchi doctor` | Check your setup and get fixes |
|
|
126
177
|
| `gitgotchi evolve` | Evolution paths and progress |
|
|
127
178
|
| `gitgotchi pet` | Give it some affection |
|
|
128
179
|
| `gitgotchi name <name>` | Name your companion |
|
|
@@ -54,10 +54,55 @@ single line instead of the full pet, so it doesn't flood the agent's context.
|
|
|
54
54
|
gitgotchi card
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Saves a 1200×630 card, sized for LinkedIn and other link previews
|
|
57
|
+
Saves a 1200×630 card, sized for LinkedIn and other link previews. Add `--gif` for one where your companion
|
|
58
|
+
moves (needs `pip install "gitgotchi[gif]"`).
|
|
58
59
|
|
|
59
60
|

|
|
60
61
|
|
|
62
|
+
For your README, `gitgotchi badge` saves a small SVG with your companion's portrait and form, and prints the
|
|
63
|
+
Markdown to paste in. Run it again whenever your companion evolves.
|
|
64
|
+
|
|
65
|
+
## 📟 In your status bar
|
|
66
|
+
|
|
67
|
+
`gitgotchi-statusline` prints one line about the companion in the current repo, for example
|
|
68
|
+
`👾 Boo the Poltergeist · 63% possessed · hungry`. It starts in about 50 ms and prints nothing outside a
|
|
69
|
+
haunted repo, so it's safe to leave on everywhere.
|
|
70
|
+
|
|
71
|
+
Claude Code: add this to `~/.claude/settings.json`
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
"statusLine": {"type": "command", "command": "gitgotchi-statusline"}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
tmux: add this to `~/.tmux.conf`
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
set -g status-right '#(cd #{pane_current_path} && gitgotchi-statusline --plain)'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`gitgotchi-statusline --setup` prints these snippets, plus one for Starship.
|
|
84
|
+
|
|
85
|
+
## 🔕 How much the hook says
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
gitgotchi config line # one line per commit in this repo
|
|
89
|
+
gitgotchi config off --global # silent everywhere
|
|
90
|
+
gitgotchi config full # the whole pet (the default)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
It's stored in git config as `gitgotchi.output`, so a repo's setting wins over the global one. When an agent
|
|
94
|
+
runs `git commit` without a terminal, the hook always keeps it to one line.
|
|
95
|
+
|
|
96
|
+
## 🩺 Something not working?
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
gitgotchi doctor
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Checks git, the hook and the Python it points at, the pet database, your terminal's colors and size, and
|
|
103
|
+
whether stories can use Claude, with a fix for anything that's wrong. Include its output when you open an
|
|
104
|
+
issue; it never shows your API key.
|
|
105
|
+
|
|
61
106
|
## 🔮 Séance
|
|
62
107
|
|
|
63
108
|
```bash
|
|
@@ -81,7 +126,11 @@ story. GitGotchi reads only `ANTHROPIC_API_KEY` from `.env` and ignores everythi
|
|
|
81
126
|
| `gitgotchi haunt` | Live pane that reacts to every commit |
|
|
82
127
|
| `gitgotchi status` | Your companion and its stats |
|
|
83
128
|
| `gitgotchi seance` | Ghost stories from your repo's history |
|
|
84
|
-
| `gitgotchi card` | Save a shareable
|
|
129
|
+
| `gitgotchi card` | Save a shareable card (`--gif` for an animated one) |
|
|
130
|
+
| `gitgotchi badge` | Save a README badge with your companion's portrait |
|
|
131
|
+
| `gitgotchi-statusline` | One line for Claude Code, tmux or Starship |
|
|
132
|
+
| `gitgotchi config` | How much the hook prints: `full`, `line` or `off` |
|
|
133
|
+
| `gitgotchi doctor` | Check your setup and get fixes |
|
|
85
134
|
| `gitgotchi evolve` | Evolution paths and progress |
|
|
86
135
|
| `gitgotchi pet` | Give it some affection |
|
|
87
136
|
| `gitgotchi name <name>` | Name your companion |
|
|
@@ -50,6 +50,9 @@ dependencies = [
|
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
[project.optional-dependencies]
|
|
53
|
+
gif = [
|
|
54
|
+
"pillow>=10.0",
|
|
55
|
+
]
|
|
53
56
|
dev = [
|
|
54
57
|
"pytest>=8.0",
|
|
55
58
|
"pytest-cov>=5.0",
|
|
@@ -60,6 +63,7 @@ dev = [
|
|
|
60
63
|
|
|
61
64
|
[project.scripts]
|
|
62
65
|
gitgotchi = "gitgotchi.cli:cli"
|
|
66
|
+
gitgotchi-statusline = "gitgotchi.statusline:main"
|
|
63
67
|
|
|
64
68
|
[project.urls]
|
|
65
69
|
Homepage = "https://github.com/Srikarmk/Gitgotchi"
|
|
@@ -317,31 +317,64 @@ def adopt(
|
|
|
317
317
|
|
|
318
318
|
@app.command()
|
|
319
319
|
def card(
|
|
320
|
-
output: Path = typer.Option(
|
|
320
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Where to save it (default gitgotchi-card.png, or .gif with --gif)"),
|
|
321
321
|
scale: int = typer.Option(5, help="Pixel size; 5 gives 1200x630"),
|
|
322
|
+
gif: bool = typer.Option(False, "--gif", help="Save an animated GIF (needs: pip install 'gitgotchi[gif]')"),
|
|
322
323
|
):
|
|
323
|
-
"""Save a shareable card of your companion
|
|
324
|
+
"""Save a shareable card of your companion."""
|
|
324
325
|
from gitgotchi.paths import require_repo
|
|
325
|
-
from gitgotchi.render.card import make_card
|
|
326
|
+
from gitgotchi.render.card import make_card, make_card_gif
|
|
326
327
|
from gitgotchi.render.scene import encode_png, upscale
|
|
327
328
|
from gitgotchi.render.terminal import graphics_protocol, inline_image
|
|
328
329
|
|
|
329
330
|
state_manager = load_state()
|
|
330
331
|
stats = state_manager.get_pet_state()
|
|
331
332
|
repo_name = Path(require_repo().working_tree_dir).name
|
|
332
|
-
|
|
333
|
-
image = make_card(stats, repo_name, state_manager.agent_counts())
|
|
333
|
+
agents = state_manager.agent_counts()
|
|
334
334
|
scale = max(1, min(12, scale))
|
|
335
|
-
output.
|
|
335
|
+
output = output or Path("gitgotchi-card.gif" if gif else "gitgotchi-card.png")
|
|
336
|
+
|
|
337
|
+
image = make_card(stats, repo_name, agents)
|
|
338
|
+
if gif:
|
|
339
|
+
try:
|
|
340
|
+
data = make_card_gif(stats, repo_name, agents, scale=scale)
|
|
341
|
+
except ImportError:
|
|
342
|
+
render_error("Animated cards need Pillow: pip install 'gitgotchi[gif]'")
|
|
343
|
+
raise typer.Exit(1)
|
|
344
|
+
output.write_bytes(data)
|
|
345
|
+
else:
|
|
346
|
+
output.write_bytes(encode_png(upscale(image, scale)))
|
|
336
347
|
|
|
337
348
|
protocol = graphics_protocol()
|
|
338
349
|
if protocol and console.is_terminal:
|
|
339
350
|
console.file.write(inline_image(image, min(console.width - 2, 96), protocol))
|
|
340
351
|
console.file.flush()
|
|
341
|
-
|
|
352
|
+
size = f"{image.shape[1] * scale}x{image.shape[0] * scale}"
|
|
353
|
+
console.print(Text(f"\n🖼️ Saved {output} ({size})", style=theme.ORANGE))
|
|
342
354
|
console.print(Text("Share it, and tag the spirits that wrote your code.\n", style=theme.DIM))
|
|
343
355
|
|
|
344
356
|
|
|
357
|
+
@app.command()
|
|
358
|
+
def badge(
|
|
359
|
+
output: Path = typer.Option(Path("gitgotchi-badge.svg"), "--output", "-o", help="Where to save the SVG"),
|
|
360
|
+
):
|
|
361
|
+
"""Save a README badge with your companion's portrait and form."""
|
|
362
|
+
from gitgotchi.render.badge import make_badge
|
|
363
|
+
|
|
364
|
+
from gitgotchi.paths import require_repo
|
|
365
|
+
|
|
366
|
+
stats = load_state().get_pet_state()
|
|
367
|
+
output.write_text(make_badge(stats), encoding="utf-8")
|
|
368
|
+
root = Path(require_repo().working_tree_dir).resolve()
|
|
369
|
+
try:
|
|
370
|
+
link = output.resolve().relative_to(root).as_posix()
|
|
371
|
+
except ValueError: # saved outside the repo
|
|
372
|
+
link = output.as_posix()
|
|
373
|
+
console.print(Text(f"\n🏷️ Saved {output}. Commit it and add this to your README:", style=theme.ORANGE))
|
|
374
|
+
console.print(Text(f"[](https://github.com/Srikarmk/Gitgotchi)\n", style=theme.BONE))
|
|
375
|
+
console.print(Text("Run `gitgotchi badge` again whenever your companion evolves.\n", style=theme.DIM))
|
|
376
|
+
|
|
377
|
+
|
|
345
378
|
@app.command()
|
|
346
379
|
def haunt(fps: int = typer.Option(12, help="Frames per second")):
|
|
347
380
|
"""Open a live pane where your companion reacts to every commit.
|
|
@@ -372,6 +405,86 @@ def demo():
|
|
|
372
405
|
console.print(Text("🎃 That's GitGotchi. Run `gitgotchi install` in a real repo to adopt your own.", style=theme.ORANGE))
|
|
373
406
|
|
|
374
407
|
|
|
408
|
+
@app.command()
|
|
409
|
+
def config(
|
|
410
|
+
output: Optional[str] = typer.Argument(None, help="full, line or off"),
|
|
411
|
+
global_: bool = typer.Option(False, "--global", help="Set it for every repo instead of this one"),
|
|
412
|
+
):
|
|
413
|
+
"""Choose how much the hook prints after each commit.
|
|
414
|
+
|
|
415
|
+
full: the whole pet (default). line: one line. off: nothing.
|
|
416
|
+
Stored in git config as gitgotchi.output.
|
|
417
|
+
"""
|
|
418
|
+
from gitgotchi.hooks.post_commit import configured_output
|
|
419
|
+
from gitgotchi.paths import find_repo
|
|
420
|
+
from gitgotchi.pet.renderer import OUTPUT_STYLES
|
|
421
|
+
|
|
422
|
+
repo = find_repo()
|
|
423
|
+
if output is None:
|
|
424
|
+
if repo is None:
|
|
425
|
+
render_error("No git repository found here. Run `git init` first.")
|
|
426
|
+
raise typer.Exit(1)
|
|
427
|
+
console.print(Text(f"gitgotchi.output = {configured_output(repo)}", style=theme.ORANGE))
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
output = output.strip().lower()
|
|
431
|
+
if output not in OUTPUT_STYLES:
|
|
432
|
+
render_error(f"Pick one of: {', '.join(OUTPUT_STYLES)}")
|
|
433
|
+
raise typer.Exit(1)
|
|
434
|
+
if not global_ and repo is None:
|
|
435
|
+
render_error("No git repository here. Use --global to set it everywhere.")
|
|
436
|
+
raise typer.Exit(1)
|
|
437
|
+
|
|
438
|
+
if global_:
|
|
439
|
+
from git import Git
|
|
440
|
+
|
|
441
|
+
Git().config("--global", "gitgotchi.output", output)
|
|
442
|
+
else:
|
|
443
|
+
repo.git.config("gitgotchi.output", output)
|
|
444
|
+
where = "every repo" if global_ else "this repo"
|
|
445
|
+
console.print(Text(f"🕯️ Hook output set to {output} for {where}.", style=theme.ORANGE))
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@app.command()
|
|
449
|
+
def doctor():
|
|
450
|
+
"""Check your setup and explain how to fix anything that's wrong."""
|
|
451
|
+
from gitgotchi.doctor import FAIL, WARN, run_checks
|
|
452
|
+
|
|
453
|
+
checks = run_checks(console)
|
|
454
|
+
marks = {"ok": ("✓", theme.GREEN), WARN: ("!", theme.ORANGE), FAIL: ("✗", theme.BLOOD)}
|
|
455
|
+
table = Table.grid(padding=(0, 2))
|
|
456
|
+
table.add_column(no_wrap=True)
|
|
457
|
+
table.add_column(no_wrap=True, style="bold")
|
|
458
|
+
table.add_column()
|
|
459
|
+
for check in checks:
|
|
460
|
+
mark, color = marks[check.status]
|
|
461
|
+
detail = Text(check.detail, overflow="fold")
|
|
462
|
+
if check.fix and check.status != "ok":
|
|
463
|
+
detail.append(f"\n→ {check.fix}", style=color)
|
|
464
|
+
table.add_row(Text(mark, style=f"bold {color}"), check.name, detail)
|
|
465
|
+
console.print(Panel(table, title="🩺 GitGotchi doctor", border_style=theme.PURPLE, expand=False))
|
|
466
|
+
|
|
467
|
+
failures = [c for c in checks if c.status == FAIL]
|
|
468
|
+
if failures:
|
|
469
|
+
console.print(Text(f"{len(failures)} problem{'s' if len(failures) > 1 else ''} to fix.", style=theme.BLOOD))
|
|
470
|
+
raise typer.Exit(1)
|
|
471
|
+
console.print(Text("Everything needed is working.", style=theme.GREEN))
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@app.command()
|
|
475
|
+
def statusline(
|
|
476
|
+
plain: bool = typer.Option(False, "--plain", help="No colors (for tmux or Starship)"),
|
|
477
|
+
setup: bool = typer.Option(False, "--setup", help="Show how to add it to Claude Code, tmux and Starship"),
|
|
478
|
+
):
|
|
479
|
+
"""Print one line about your companion, for status bars and prompts.
|
|
480
|
+
|
|
481
|
+
For status bars, use the faster `gitgotchi-statusline` command instead.
|
|
482
|
+
"""
|
|
483
|
+
from gitgotchi.statusline import main
|
|
484
|
+
|
|
485
|
+
raise typer.Exit(main((["--plain"] if plain else []) + (["--setup"] if setup else [])))
|
|
486
|
+
|
|
487
|
+
|
|
375
488
|
@app.command()
|
|
376
489
|
def version():
|
|
377
490
|
"""Show GitGotchi version."""
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""`gitgotchi doctor`: check every place setup can go wrong, with a fix for each."""
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from gitgotchi import __version__
|
|
13
|
+
|
|
14
|
+
OK, WARN, FAIL = "ok", "warn", "fail"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Check:
|
|
19
|
+
status: str
|
|
20
|
+
name: str
|
|
21
|
+
detail: str
|
|
22
|
+
fix: str = ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_checks(console) -> List[Check]:
|
|
26
|
+
from gitgotchi.paths import find_repo
|
|
27
|
+
|
|
28
|
+
checks = [
|
|
29
|
+
Check(OK, "GitGotchi", f"{__version__} on Python {platform.python_version()} ({sys.executable})"),
|
|
30
|
+
_git_check(),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
repo = find_repo()
|
|
34
|
+
if repo is None:
|
|
35
|
+
checks.append(Check(FAIL, "Repository", "not inside a git repository", "cd into a repo, or run `git init`"))
|
|
36
|
+
checks.extend(_terminal_checks(console))
|
|
37
|
+
checks.append(_api_key_check())
|
|
38
|
+
return checks
|
|
39
|
+
|
|
40
|
+
checks.append(Check(OK, "Repository", repo.working_tree_dir))
|
|
41
|
+
checks.append(_pet_check())
|
|
42
|
+
checks.extend(_hook_checks(repo))
|
|
43
|
+
checks.append(_output_check(repo))
|
|
44
|
+
checks.extend(_terminal_checks(console))
|
|
45
|
+
checks.append(_api_key_check())
|
|
46
|
+
return checks
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _git_check() -> Check:
|
|
50
|
+
git = shutil.which("git")
|
|
51
|
+
if not git:
|
|
52
|
+
return Check(FAIL, "git", "not found on PATH", "install git from https://git-scm.com")
|
|
53
|
+
try:
|
|
54
|
+
version = subprocess.run([git, "--version"], capture_output=True, text=True, timeout=10).stdout.strip()
|
|
55
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
56
|
+
return Check(FAIL, "git", str(e), "check that git runs from this terminal")
|
|
57
|
+
return Check(OK, "git", version)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _pet_check() -> Check:
|
|
61
|
+
from gitgotchi.db.state_manager import StateManager
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
manager = StateManager()
|
|
65
|
+
stats = manager.get_pet_state()
|
|
66
|
+
path = manager.db_path
|
|
67
|
+
manager.close()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
return Check(FAIL, "Pet", f"can't open the pet database: {e}", "check the permissions of .git/gitgotchi/")
|
|
70
|
+
if ".gitgotchi" in Path(path).parts:
|
|
71
|
+
return Check(WARN, "Pet", f"still in the old location {path}", "close any running `gitgotchi haunt`, then run any gitgotchi command to move it")
|
|
72
|
+
return Check(OK, "Pet", f"{stats.pet_name}, {stats.total_commits} commits ({path})")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _hook_checks(repo) -> List[Check]:
|
|
76
|
+
from gitgotchi.hooks.installer import HOOK_START, LEGACY_MARKER, hooks_dir
|
|
77
|
+
|
|
78
|
+
hook = hooks_dir(repo) / "post-commit"
|
|
79
|
+
if not hook.exists():
|
|
80
|
+
return [Check(WARN, "Hook", "not installed, so commits don't feed your pet", "run `gitgotchi install`")]
|
|
81
|
+
|
|
82
|
+
content = hook.read_text(errors="replace")
|
|
83
|
+
if HOOK_START not in content:
|
|
84
|
+
if LEGACY_MARKER in content:
|
|
85
|
+
return [Check(FAIL, "Hook", "left over from an older GitGotchi", "run `gitgotchi install` to update it")]
|
|
86
|
+
return [Check(WARN, "Hook", f"{hook} exists but has no GitGotchi block", "run `gitgotchi install`")]
|
|
87
|
+
|
|
88
|
+
checks = [Check(OK, "Hook", str(hook))]
|
|
89
|
+
before = content.split(HOOK_START, 1)[0]
|
|
90
|
+
if re.search(r"^\s*(exit|exec)\b", before, re.MULTILINE):
|
|
91
|
+
checks.append(Check(FAIL, "Hook order", "an `exit` or `exec` runs before GitGotchi's block", "run `gitgotchi install` to move the block up"))
|
|
92
|
+
|
|
93
|
+
if os.name != "nt" and not os.access(hook, os.X_OK):
|
|
94
|
+
checks.append(Check(FAIL, "Hook permissions", "the hook isn't executable", f"chmod +x {hook}"))
|
|
95
|
+
|
|
96
|
+
python = _hook_python(content)
|
|
97
|
+
if python is None:
|
|
98
|
+
checks.append(Check(WARN, "Hook Python", "couldn't read which Python the hook uses", "run `gitgotchi install`"))
|
|
99
|
+
elif not Path(python).exists():
|
|
100
|
+
checks.append(Check(FAIL, "Hook Python", f"{python} no longer exists", "run `gitgotchi install` from the environment GitGotchi is installed in"))
|
|
101
|
+
else:
|
|
102
|
+
probe = _probe(python)
|
|
103
|
+
if probe is None:
|
|
104
|
+
checks.append(Check(FAIL, "Hook Python", f"{python} can't import gitgotchi", "run `gitgotchi install` from the environment GitGotchi is installed in"))
|
|
105
|
+
elif probe != __version__:
|
|
106
|
+
checks.append(Check(WARN, "Hook Python", f"{python} has GitGotchi {probe}, this is {__version__}", "run `gitgotchi install` to point the hook here"))
|
|
107
|
+
else:
|
|
108
|
+
checks.append(Check(OK, "Hook Python", python))
|
|
109
|
+
|
|
110
|
+
hooks_path = _git_config(repo, "core.hooksPath")
|
|
111
|
+
if hooks_path:
|
|
112
|
+
checks.append(Check(WARN, "core.hooksPath", f"hooks come from {hooks_path} (husky or a similar tool)", "if another tool rewrites that folder, re-run `gitgotchi install` after it does"))
|
|
113
|
+
return checks
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _output_check(repo) -> Check:
|
|
117
|
+
from gitgotchi.hooks.post_commit import configured_output
|
|
118
|
+
|
|
119
|
+
style = configured_output(repo)
|
|
120
|
+
if style not in ("full", "line", "off"):
|
|
121
|
+
return Check(WARN, "Hook output", f"gitgotchi.output is '{style}', so full is used", "run `gitgotchi config line` (or full, or off)")
|
|
122
|
+
if style == "off":
|
|
123
|
+
return Check(WARN, "Hook output", "off: commits feed your pet silently", "run `gitgotchi config full` to see it again")
|
|
124
|
+
return Check(OK, "Hook output", style)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _terminal_checks(console) -> List[Check]:
|
|
128
|
+
from gitgotchi.render.terminal import graphics_protocol
|
|
129
|
+
|
|
130
|
+
checks = []
|
|
131
|
+
if not console.is_terminal:
|
|
132
|
+
checks.append(Check(WARN, "Terminal", "output isn't a terminal, so the pet is drawn as one line", "run this in a terminal to see the full pet"))
|
|
133
|
+
return checks
|
|
134
|
+
|
|
135
|
+
colors = console.color_system or "none"
|
|
136
|
+
if colors == "truecolor":
|
|
137
|
+
checks.append(Check(OK, "Colors", "true color"))
|
|
138
|
+
else:
|
|
139
|
+
checks.append(Check(WARN, "Colors", f"{colors}: the pet's colors are approximated", "use a true-color terminal (iTerm2, Kitty, WezTerm, Ghostty, VS Code, Windows Terminal), or set COLORTERM=truecolor if yours supports it"))
|
|
140
|
+
|
|
141
|
+
encoding = (getattr(sys.stdout, "encoding", "") or "").lower()
|
|
142
|
+
if "utf" not in encoding:
|
|
143
|
+
checks.append(Check(WARN, "Encoding", f"{encoding or 'unknown'}", "set PYTHONIOENCODING=utf-8"))
|
|
144
|
+
|
|
145
|
+
width = console.width
|
|
146
|
+
if width < 68:
|
|
147
|
+
checks.append(Check(WARN, "Width", f"{width} columns: the pet is drawn smaller", "widen the window to at least 68 columns"))
|
|
148
|
+
else:
|
|
149
|
+
checks.append(Check(OK, "Width", f"{width} columns"))
|
|
150
|
+
|
|
151
|
+
protocol = graphics_protocol()
|
|
152
|
+
checks.append(Check(OK, "Inline images", protocol or "not supported here; `card` saves a PNG instead"))
|
|
153
|
+
return checks
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _api_key_check() -> Check:
|
|
157
|
+
# Never print any part of the key: this output gets pasted into issues
|
|
158
|
+
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
159
|
+
return Check(OK, "Claude stories", "no ANTHROPIC_API_KEY, so the séance tells its own stories")
|
|
160
|
+
return Check(OK, "Claude stories", "ANTHROPIC_API_KEY is set")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _hook_python(content: str) -> Optional[str]:
|
|
164
|
+
match = re.search(r"^GITGOTCHI_PYTHON=(.+)$", content, re.MULTILINE)
|
|
165
|
+
if not match:
|
|
166
|
+
return None
|
|
167
|
+
value = match.group(1).strip()
|
|
168
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
|
|
169
|
+
value = value[1:-1].replace("'\"'\"'", "'")
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _probe(python: str) -> Optional[str]:
|
|
174
|
+
try:
|
|
175
|
+
result = subprocess.run(
|
|
176
|
+
[python, "-c", "import gitgotchi; print(gitgotchi.__version__)"],
|
|
177
|
+
capture_output=True,
|
|
178
|
+
text=True,
|
|
179
|
+
timeout=20,
|
|
180
|
+
)
|
|
181
|
+
except (OSError, subprocess.SubprocessError):
|
|
182
|
+
return None
|
|
183
|
+
return result.stdout.strip() if result.returncode == 0 else None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _git_config(repo, key: str) -> str:
|
|
187
|
+
try:
|
|
188
|
+
return repo.git.config("--get", key).strip()
|
|
189
|
+
except Exception:
|
|
190
|
+
return ""
|
|
@@ -68,6 +68,14 @@ def amended_from(repo: Repo) -> Optional[str]:
|
|
|
68
68
|
return None
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def configured_output(repo: Repo) -> str:
|
|
72
|
+
"""`git config gitgotchi.output`: full, line or off (repo setting wins over global)."""
|
|
73
|
+
try:
|
|
74
|
+
return repo.git.config("--get", "gitgotchi.output").strip().lower() or "full"
|
|
75
|
+
except GitCommandError: # not set
|
|
76
|
+
return "full"
|
|
77
|
+
|
|
78
|
+
|
|
71
79
|
def handle_post_commit() -> None:
|
|
72
80
|
"""Handle post-commit hook."""
|
|
73
81
|
try:
|
|
@@ -84,11 +92,12 @@ def handle_post_commit() -> None:
|
|
|
84
92
|
if feeding is None:
|
|
85
93
|
return
|
|
86
94
|
|
|
95
|
+
style = configured_output(repo)
|
|
87
96
|
if feeding.evolved:
|
|
88
|
-
render_evolution(feeding.stats, feeding.old_form, feeding.stats.current_form)
|
|
97
|
+
render_evolution(feeding.stats, feeding.old_form, feeding.stats.current_form, style)
|
|
89
98
|
else:
|
|
90
99
|
render_commit_response(
|
|
91
|
-
feeding.stats, feeding.lines_added, feeding.lines_deleted, feeding.agent
|
|
100
|
+
feeding.stats, feeding.lines_added, feeding.lines_deleted, feeding.agent, style
|
|
92
101
|
)
|
|
93
102
|
|
|
94
103
|
except Exception as e:
|
|
@@ -132,8 +132,30 @@ def evolution_frames(stats: PetStats, old_form: PetForm, new_form: PetForm, seco
|
|
|
132
132
|
yield pet_view(new_stats, t=p, show_stats=False, flash=max(0.0, 1 - k * 1.6), sparkles=int((1 - k) * 22) + 4, seed=i)
|
|
133
133
|
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
OUTPUT_STYLES = ("full", "line", "off")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def output_style(requested: str = "full") -> str:
|
|
139
|
+
"""How much the hook prints: the user's setting, but never the full pet
|
|
140
|
+
when nobody is watching a terminal (an AI agent running git, CI)."""
|
|
141
|
+
style = requested if requested in OUTPUT_STYLES else "full"
|
|
142
|
+
if style == "full" and not console.is_terminal:
|
|
143
|
+
return "line"
|
|
144
|
+
return style
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def render_evolution(stats: PetStats, old_form: PetForm, new_form: PetForm, style: str = "full") -> None:
|
|
148
|
+
style = output_style(style)
|
|
149
|
+
if style == "off":
|
|
150
|
+
return
|
|
136
151
|
old, new = theme.form_info(old_form), theme.form_info(new_form)
|
|
152
|
+
if style == "line":
|
|
153
|
+
console.print(
|
|
154
|
+
f"{new.icon} gitgotchi: {stats.pet_name} evolved: {old.title} → {new.title}",
|
|
155
|
+
markup=False,
|
|
156
|
+
highlight=False,
|
|
157
|
+
)
|
|
158
|
+
return
|
|
137
159
|
if console.is_terminal:
|
|
138
160
|
console.print(Text("\n Something is happening in the repo...", style=f"italic {theme.PURPLE}"))
|
|
139
161
|
animate(evolution_frames(stats, old_form, new_form))
|
|
@@ -151,19 +173,23 @@ def render_commit_response(
|
|
|
151
173
|
lines_added: int,
|
|
152
174
|
lines_deleted: int,
|
|
153
175
|
agent: Optional[str] = None,
|
|
176
|
+
style: str = "full",
|
|
154
177
|
) -> None:
|
|
155
|
-
"""React to a commit.
|
|
178
|
+
"""React to a commit, as loudly as `style` (full, line or off) allows.
|
|
156
179
|
|
|
157
|
-
Outside a real terminal (an AI agent running git, CI)
|
|
180
|
+
Outside a real terminal (an AI agent running git, CI) "full" becomes a
|
|
158
181
|
single line so it doesn't flood the agent's context.
|
|
159
182
|
"""
|
|
183
|
+
style = output_style(style)
|
|
184
|
+
if style == "off":
|
|
185
|
+
return
|
|
160
186
|
if agent:
|
|
161
187
|
line = random.choice(theme.AGENT_REACTIONS).format(name=stats.pet_name, agent=agent)
|
|
162
188
|
else:
|
|
163
189
|
line = random.choice(theme.COMMIT_REACTIONS).format(name=stats.pet_name)
|
|
164
190
|
icon = theme.form_info(stats.current_form).icon
|
|
165
191
|
|
|
166
|
-
if
|
|
192
|
+
if style == "line":
|
|
167
193
|
console.print(f"{icon} gitgotchi: {line} (+{lines_added} -{lines_deleted})", markup=False, highlight=False)
|
|
168
194
|
return
|
|
169
195
|
|
|
@@ -30,7 +30,8 @@ def render_scene(
|
|
|
30
30
|
pose = Pose(yaw=0.0, bob=0.015 * np.sin(t * 2 * np.pi), squash=0.97, tilt=0.0)
|
|
31
31
|
form.eyes_closed = True
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# Framed tight so the pet gets as many pixels as the canvas allows
|
|
34
|
+
frame = render(form, t, width, height, span=1.2, offset=(0.0, 0.07), pose=pose)
|
|
34
35
|
frame.rgba = effects.flash(frame.rgba, flash)
|
|
35
36
|
if not stage:
|
|
36
37
|
return compose(frame)
|