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.
Files changed (63) hide show
  1. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/CHANGELOG.md +27 -0
  2. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/PKG-INFO +54 -3
  3. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/README.md +51 -2
  4. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/pyproject.toml +4 -0
  5. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/__init__.py +1 -1
  6. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/cli.py +120 -7
  7. gitgotchi-0.4.0/src/gitgotchi/doctor.py +190 -0
  8. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/hooks/post_commit.py +11 -2
  9. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/renderer.py +30 -4
  10. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/__init__.py +2 -1
  11. gitgotchi-0.4.0/src/gitgotchi/render/badge.py +99 -0
  12. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/card.py +19 -0
  13. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/forms.py +25 -9
  14. gitgotchi-0.4.0/src/gitgotchi/render/raster.py +341 -0
  15. gitgotchi-0.4.0/src/gitgotchi/statusline.py +191 -0
  16. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/PKG-INFO +54 -3
  17. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/SOURCES.txt +5 -0
  18. gitgotchi-0.4.0/src/gitgotchi.egg-info/entry_points.txt +3 -0
  19. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/requires.txt +3 -0
  20. gitgotchi-0.4.0/tests/test_features.py +78 -0
  21. gitgotchi-0.4.0/tests/test_statusline.py +60 -0
  22. gitgotchi-0.3.1/src/gitgotchi/render/raster.py +0 -217
  23. gitgotchi-0.3.1/src/gitgotchi.egg-info/entry_points.txt +0 -2
  24. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/INSTALL.md +0 -0
  25. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/LICENSE +0 -0
  26. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/MANIFEST.in +0 -0
  27. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/setup.cfg +0 -0
  28. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/__main__.py +0 -0
  29. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/db/__init__.py +0 -0
  30. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/db/models.py +0 -0
  31. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/db/state_manager.py +0 -0
  32. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/demo.py +0 -0
  33. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/haunt.py +0 -0
  34. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/history.py +0 -0
  35. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/hooks/__init__.py +0 -0
  36. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/hooks/installer.py +0 -0
  37. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/paths.py +0 -0
  38. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/__init__.py +0 -0
  39. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/possession.py +0 -0
  40. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/quality.py +0 -0
  41. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/states.py +0 -0
  42. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/pet/theme.py +0 -0
  43. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/effects.py +0 -0
  44. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/font.py +0 -0
  45. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/scene.py +0 -0
  46. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/render/terminal.py +0 -0
  47. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/seance/__init__.py +0 -0
  48. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/seance/hauntings.py +0 -0
  49. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi/seance/story_gen.py +0 -0
  50. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/dependency_links.txt +0 -0
  51. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/src/gitgotchi.egg-info/top_level.txt +0 -0
  52. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/conftest.py +0 -0
  53. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_cli.py +0 -0
  54. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_haunt.py +0 -0
  55. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_hauntings.py +0 -0
  56. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_history.py +0 -0
  57. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_hooks.py +0 -0
  58. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_possession.py +0 -0
  59. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_quality.py +0 -0
  60. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_render.py +0 -0
  61. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_state_manager.py +0 -0
  62. {gitgotchi-0.3.1 → gitgotchi-0.4.0}/tests/test_states.py +0 -0
  63. {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.1
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
  ![A sample card](https://raw.githubusercontent.com/Srikarmk/Gitgotchi/main/docs/card.png)
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 PNG |
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
  ![A sample card](https://raw.githubusercontent.com/Srikarmk/Gitgotchi/main/docs/card.png)
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 PNG |
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"
@@ -1,3 +1,3 @@
1
1
  """GitGotchi - A Halloween-inspired terminal companion for your git repository."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.4.0"
@@ -317,31 +317,64 @@ def adopt(
317
317
 
318
318
  @app.command()
319
319
  def card(
320
- output: Path = typer.Option(Path("gitgotchi-card.png"), "--output", "-o", help="Where to save the PNG"),
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 as a PNG."""
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.write_bytes(encode_png(upscale(image, scale)))
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
- console.print(Text(f"\n🖼️ Saved {output} ({image.shape[1] * scale}x{image.shape[0] * scale})", style=theme.ORANGE))
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"[![GitGotchi]({link})](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
- def render_evolution(stats: PetStats, old_form: PetForm, new_form: PetForm) -> None:
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) this prints a
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 not console.is_terminal:
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
- frame = render(form, t, width, height, offset=(0.0, 0.12), pose=pose)
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)