I shipped my first Python CLI to PyPI this week. Here's what I learned building **gitpulse** — a tool that reads your staged git diff and generates Conventional Commit messages using AI.
## The Problem
Every developer has lazy commits in their history:
`git commit -m "fix"`
`git commit -m "update"`
`git commit -m "stuff"`
Clean commit histories matter — for changelogs, code reviews, and your future self trying to understand why you changed that line 6 months ago. But writing `feat(auth): add JWT token validation with refresh support` for every single commit is mental overhead nobody needs.
## The Solution
gitpulse reads your staged diff and generates the message for you:
`pip install gitpulse-commit`
`git add src/auth.py`
`git-pulse`
`# → feat(auth): add JWT token validation`
You confirm, edit, or abort. One key. Done.
## What I Learned
### 1. PyPI packaging is simpler than it looks
`pyproject.toml` + `python -m build` + `twine upload` is all you need. No `setup.py`, no `setup.cfg`. The hardest part was finding an available package name (hint: check PyPI before you start coding).
### 2. ABC + factory pattern for pluggable providers
The core `AIClient` is abstract. Each provider (OpenCode, OpenAI, Ollama) implements `generate(diff) -> str`. The factory function just does `return OpenAIClient(...)` based on a string. Adding a new provider is ~20 lines of code.
### 3. Prompt engineering is API design
The system prompt is the most important file in the project. It enforces:
- Conventional Commits format (`type(scope): description`)
- 72-character max
- No markdown, no explanations, no fluff
- Imperative mood, no period at end
If the AI ignores any of these, the CLI still strips markdown and truncates — defense in depth.
### 4. Git hooks are easy but fragile
`git-pulse init` writes a prepare-commit-msg hook. It works great, but detecting an existing hook without breaking it required a sentinel marker (`## GITPULSE_HOOK_V0.1`) rather than just searching for a comment string.
### 5. GitHub Actions CI/CD is a superpower
yaml
on:
push:
tags:
- 'v*'
Now `git tag v0.2.0 && git push --tags` automatically publishes to PyPI. Zero manual steps.
## The Code
7 modules, ~300 lines of Python:
- `cli.py` — argparse with subcommands
- `ai_client.py` — ABC + 3 providers
- `git_ops.py` — subprocess wrappers
- `hook.py` — git hook installer
- `config.py` — env var getters
- `prompts.py` — the system prompt
- `__init__.py` — version
Only dependency: `requests`. Python 3.8+. MIT license.
## Try It
`pip install gitpulse-commit`
GitHub: [erico964-blip/gitpulse](https://github.com/erico964-blip/gitpulse)
PyPI: [gitpulse-commit](https://pypi.org/project/gitpulse-commit/)
Built this over a weekend. Feedback welcome — especially on the prompt design and hook workflow!
Top comments (0)