devgen-cli 0.2.4__tar.gz → 0.2.6__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.
- devgen_cli-0.2.6/PKG-INFO +187 -0
- devgen_cli-0.2.6/README.md +150 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/ai.py +11 -2
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/commit.py +19 -1
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/config.py +28 -2
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/main.py +11 -3
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/setup.py +20 -1
- devgen_cli-0.2.6/devgen/modules/changelog_generator.py +155 -0
- devgen_cli-0.2.6/devgen/modules/changelog_sections.py +79 -0
- devgen_cli-0.2.6/devgen/modules/commit_generator.py +392 -0
- devgen_cli-0.2.6/devgen/modules/diff_builder.py +220 -0
- devgen_cli-0.2.6/devgen/modules/git_ops.py +144 -0
- devgen_cli-0.2.6/devgen/modules/release_note_generator.py +56 -0
- devgen_cli-0.2.6/devgen/providers/__init__.py +0 -0
- devgen_cli-0.2.6/devgen/providers/anthropic.py +19 -0
- devgen_cli-0.2.6/devgen/providers/base.py +90 -0
- devgen_cli-0.2.6/devgen/providers/gemini.py +63 -0
- devgen_cli-0.2.6/devgen/providers/huggingface.py +54 -0
- devgen_cli-0.2.6/devgen/providers/ollama.py +51 -0
- devgen_cli-0.2.6/devgen/providers/openai.py +20 -0
- devgen_cli-0.2.6/devgen/providers/openrouter.py +25 -0
- devgen_cli-0.2.6/devgen/templates/commit/commit_message.tpl +14 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/utils.py +62 -0
- devgen_cli-0.2.6/devgen_cli.egg-info/PKG-INFO +187 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen_cli.egg-info/SOURCES.txt +6 -1
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen_cli.egg-info/requires.txt +1 -1
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/pyproject.toml +2 -2
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/setup.py +1 -1
- devgen_cli-0.2.4/PKG-INFO +0 -208
- devgen_cli-0.2.4/README.md +0 -171
- devgen_cli-0.2.4/devgen/modules/changelog_generator.py +0 -183
- devgen_cli-0.2.4/devgen/modules/commit_generator.py +0 -509
- devgen_cli-0.2.4/devgen/modules/release_note_generator.py +0 -66
- devgen_cli-0.2.4/devgen/providers/__init__.py +0 -24
- devgen_cli-0.2.4/devgen/providers/anthropic.py +0 -23
- devgen_cli-0.2.4/devgen/providers/gemini.py +0 -24
- devgen_cli-0.2.4/devgen/providers/huggingface.py +0 -45
- devgen_cli-0.2.4/devgen/providers/openai.py +0 -51
- devgen_cli-0.2.4/devgen/providers/openrouter.py +0 -36
- devgen_cli-0.2.4/devgen/templates/commit/commit_message.j2 +0 -11
- devgen_cli-0.2.4/devgen_cli.egg-info/PKG-INFO +0 -208
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/LICENSE +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/MANIFEST.in +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/__init__.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/__init__.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/changelog.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/gitignore.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/license.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/cli/release.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/modules/__init__.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/modules/gitignore_generator.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/modules/license_generator.py +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/agpl-3.0.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/apache-2.0.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/bsd-2-clause.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/bsd-3-clause.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/bsl-1.0.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/cc0-1.0.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/epl-2.0.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/gpl-2.0.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/gpl-3.0.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/lgpl-2.1.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/mit.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/mpl-2.0.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen/templates/licenses/unlicense.json +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen_cli.egg-info/dependency_links.txt +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen_cli.egg-info/entry_points.txt +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/devgen_cli.egg-info/top_level.txt +0 -0
- {devgen_cli-0.2.4 → devgen_cli-0.2.6}/setup.cfg +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devgen-cli
|
|
3
|
+
Version: 0.2.6
|
|
4
|
+
Summary: A collection of developer tools
|
|
5
|
+
Home-page: https://github.com/S4NKALP/devgen
|
|
6
|
+
Author: Sankalp Tharu
|
|
7
|
+
Author-email: Sankalp Tharu <sankalptharu50028@gmail.com>
|
|
8
|
+
License: GPL-3.0-or-later
|
|
9
|
+
Project-URL: Homepage, https://github.com/S4NKALP/devgen
|
|
10
|
+
Keywords: devgen,cli,git,changelog,gitignore,license,commit-generator,commit
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: anthropic>=0.75.0
|
|
24
|
+
Requires-Dist: google-genai>=0.1.0
|
|
25
|
+
Requires-Dist: jinja2>=3.1.6
|
|
26
|
+
Requires-Dist: openai>=2.11.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
28
|
+
Requires-Dist: questionary>=2.1.1
|
|
29
|
+
Requires-Dist: requests>=2.32.5
|
|
30
|
+
Requires-Dist: rich>=14.2.0
|
|
31
|
+
Requires-Dist: toml>=0.10.2
|
|
32
|
+
Requires-Dist: typer>=0.20.0
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: requires-python
|
|
37
|
+
|
|
38
|
+
# DevGen
|
|
39
|
+
|
|
40
|
+
<div align="center">
|
|
41
|
+
|
|
42
|
+
**AI-Powered Git & Project Workflows in One CLI**
|
|
43
|
+
|
|
44
|
+
Stop wasting time on repetitive tasks. DevGen automates commits, changelogs, `.gitignore`, and license files using AI — from cloud providers or a local Ollama model.
|
|
45
|
+
|
|
46
|
+
> PyPI didn't allow the original name, so you'll find it as **devgen-cli** on PyPI
|
|
47
|
+
|
|
48
|
+
> <a href="https://pypi.org/project/devgen-cli"><img src="https://img.shields.io/pypi/v/devgen-cli?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI"></a>
|
|
49
|
+
> <img src="https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white" alt="Python">
|
|
50
|
+
> <a href="https://github.com/S4NKALP/DevGen/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-GPL--3.0--or--later-blue.svg" alt="License"></a>
|
|
51
|
+
> <a href="https://github.com/S4NKALP/DevGen/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/S4NKALP/DevGen/ci.yml?branch=main&label=CI" alt="CI"></a>
|
|
52
|
+
> <a href="https://github.com/S4NKALP/DevGen/actions/workflows/pre-commit.yml"><img src="https://img.shields.io/github/actions/workflow/status/S4NKALP/DevGen/pre-commit.yml?branch=main&label=Pre-Commit" alt="Pre-Commit"></a>
|
|
53
|
+
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
## Why DevGen
|
|
57
|
+
|
|
58
|
+
DevGen is a CLI for the parts of development that should be invisible. It writes Conventional Commits from your diff, drafts SemVer changelogs, fetches `.gitignore` and license templates, and routes everything through whichever AI provider you prefer — including a fully local Ollama model if you don't want to send code to the cloud.
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **AI-Powered Commits** — Conventional Commit messages generated from your staged diff, grouped by directory, with optional emoji prefixes.
|
|
63
|
+
- **Multiple Providers** — Google Gemini, OpenAI, Anthropic, OpenRouter, HuggingFace, and local Ollama, all behind one CLI.
|
|
64
|
+
- **Smart Caching** — `.gitignore` and license templates are cached for offline use; AI responses are de-duplicated.
|
|
65
|
+
- **Conventional Changelogs** — `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, `chore`, `style`, and a `BREAKING CHANGES` section.
|
|
66
|
+
- **Project Scaffolding** — Pull `.gitignore` from GitHub's collection and drop in SPDX license files (MIT, Apache-2.0, GPL-3.0, AGPL-3.0, BSD, MPL-2.0, …).
|
|
67
|
+
- **Custom Templates** — Override the commit-message prompt via a `.tpl` file in your config.
|
|
68
|
+
- **Interactive Setup** — `devgen setup` walks you through provider + API key + options.
|
|
69
|
+
- **Undo Support** — `devgen commit undo` rolls back the last AI commit while keeping changes staged.
|
|
70
|
+
- **Token-Limit Aware** — When a diff exceeds a model's context window, you get a single actionable error with `--max-groups` and `--max-diff-size` hints.
|
|
71
|
+
|
|
72
|
+
## Supported AI Providers
|
|
73
|
+
|
|
74
|
+
| Provider | Notes |
|
|
75
|
+
| ----------------- | ---------------------------------------------------- |
|
|
76
|
+
| **Google Gemini** | Default-friendly, generous free tier |
|
|
77
|
+
| **OpenAI** | GPT-4o, GPT-4.1, o-series |
|
|
78
|
+
| **Anthropic** | Claude 3.5 / 3.7 / 4 |
|
|
79
|
+
| **OpenRouter** | Single key, many models |
|
|
80
|
+
| **HuggingFace** | Inference API |
|
|
81
|
+
| **Ollama** | Fully local, no API key, no data leaves your machine |
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Recommended: isolated environment
|
|
87
|
+
pipx install devgen-cli
|
|
88
|
+
|
|
89
|
+
# Or use uv for speed
|
|
90
|
+
uv tool install devgen-cli
|
|
91
|
+
|
|
92
|
+
# Or plain pip
|
|
93
|
+
pip install devgen-cli
|
|
94
|
+
|
|
95
|
+
# Shell completion (bash/zsh/fish)
|
|
96
|
+
devgen --install-completion
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Requires **Python 3.10 or newer**.
|
|
100
|
+
|
|
101
|
+
## Quick Start
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# 1. Configure a provider and API key
|
|
105
|
+
devgen setup config
|
|
106
|
+
|
|
107
|
+
# 2. Stage your work as usual
|
|
108
|
+
git add .
|
|
109
|
+
|
|
110
|
+
# 3. Let DevGen write the commit message
|
|
111
|
+
devgen commit run
|
|
112
|
+
|
|
113
|
+
# 4. Or preview first, then commit
|
|
114
|
+
devgen commit run --dry-run
|
|
115
|
+
devgen commit run --push
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Commands
|
|
119
|
+
|
|
120
|
+
| Command | Description |
|
|
121
|
+
| ------------------------------- | ----------------------------------------------------------------------------- |
|
|
122
|
+
| `devgen setup config` | Interactive provider / API key / options wizard |
|
|
123
|
+
| `devgen commit run` | Stage, generate message, commit (optionally `--push`, `--check`, `--dry-run`) |
|
|
124
|
+
| `devgen commit undo` | Undo the last AI commit, keep changes staged |
|
|
125
|
+
| `devgen changelog` | Generate a Conventional Commits changelog from recent history |
|
|
126
|
+
| `devgen release-notes` | Generate release notes for a version range |
|
|
127
|
+
| `devgen gitignore list` | List available GitHub `.gitignore` templates |
|
|
128
|
+
| `devgen gitignore add <name> …` | Add `.gitignore` entries to the current project |
|
|
129
|
+
| `devgen license list` | List available SPDX licenses |
|
|
130
|
+
| `devgen license add <spdx>` | Add a LICENSE file to the current project |
|
|
131
|
+
|
|
132
|
+
Run `devgen <command> --help` for full options on any subcommand.
|
|
133
|
+
|
|
134
|
+
## Custom Templates
|
|
135
|
+
|
|
136
|
+
DevGen uses `.tpl` files for its commit prompt. To override the default, set `custom_template` in your config (see `devgen config info`) and point it at a `.tpl` file with `{{ diff_text }}`, `{{ context }}`, `{{ group_name }}`, and the conditional `{% if use_emoji %}` block.
|
|
137
|
+
|
|
138
|
+
Example minimal template:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
{{ diff_text }}
|
|
142
|
+
Summarize the change above in one Conventional Commit line.
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Architecture
|
|
146
|
+
|
|
147
|
+
DevGen is built around a small set of composable components:
|
|
148
|
+
|
|
149
|
+
- **`BaseProvider`** — abstract base for every AI provider; handles API-key validation, token-limit detection, and error wrapping. Adding a new provider is one subclass with a single `_generate()` method.
|
|
150
|
+
- **`GitOperator`** — all `subprocess` calls to `git` live here, behind a `GitError` exception.
|
|
151
|
+
- **`DiffBuilder` / `FileGrouper` / `ManifestInspector`** — split a staged diff into per-directory groups with compact project context, so the model sees a focused slice instead of a wall of text.
|
|
152
|
+
- **`Section` enum** — single source of truth for changelog ordering and emoji, shared by `ChangelogGenerator` and `ReleaseNotesGenerator`.
|
|
153
|
+
|
|
154
|
+
The CLI entry point is `devgen.cli.main:app` (Typer), exposed as the `devgen` script.
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
git clone https://github.com/S4NKALP/DevGen.git
|
|
160
|
+
cd DevGen
|
|
161
|
+
uv sync --all-extras --dev # install runtime + dev deps
|
|
162
|
+
uv run pre-commit run --all-files
|
|
163
|
+
uv run devgen --help
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The project uses `uv` for dependency management, `ruff` for lint + format, and `pyright` for type checking. CI runs all three on Python 3.10 through 3.13 (see `.github/workflows/ci.yml`).
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
Issues and PRs welcome. Please run `uv run pre-commit run --all-files` before opening a PR so CI stays green.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
GPL-3.0-or-later. See [LICENSE](LICENSE).
|
|
175
|
+
|
|
176
|
+
## Acknowledgments
|
|
177
|
+
|
|
178
|
+
Built on the shoulders of:
|
|
179
|
+
|
|
180
|
+
- **Typer** & **Rich** — CLI and terminal UI
|
|
181
|
+
- **Questionary** — interactive prompts
|
|
182
|
+
- **Google Gemini**, **OpenAI**, **Anthropic**, **OpenRouter**, **HuggingFace**, and **Ollama** — AI providers
|
|
183
|
+
- **Ruff** — lint and format
|
|
184
|
+
|
|
185
|
+
<div align="center">
|
|
186
|
+
Made with ❤️ by <a href="https://github.com/S4NKALP">Sankalp</a>
|
|
187
|
+
</div>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# DevGen
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**AI-Powered Git & Project Workflows in One CLI**
|
|
6
|
+
|
|
7
|
+
Stop wasting time on repetitive tasks. DevGen automates commits, changelogs, `.gitignore`, and license files using AI — from cloud providers or a local Ollama model.
|
|
8
|
+
|
|
9
|
+
> PyPI didn't allow the original name, so you'll find it as **devgen-cli** on PyPI
|
|
10
|
+
|
|
11
|
+
> <a href="https://pypi.org/project/devgen-cli"><img src="https://img.shields.io/pypi/v/devgen-cli?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI"></a>
|
|
12
|
+
> <img src="https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white" alt="Python">
|
|
13
|
+
> <a href="https://github.com/S4NKALP/DevGen/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-GPL--3.0--or--later-blue.svg" alt="License"></a>
|
|
14
|
+
> <a href="https://github.com/S4NKALP/DevGen/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/S4NKALP/DevGen/ci.yml?branch=main&label=CI" alt="CI"></a>
|
|
15
|
+
> <a href="https://github.com/S4NKALP/DevGen/actions/workflows/pre-commit.yml"><img src="https://img.shields.io/github/actions/workflow/status/S4NKALP/DevGen/pre-commit.yml?branch=main&label=Pre-Commit" alt="Pre-Commit"></a>
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
## Why DevGen
|
|
20
|
+
|
|
21
|
+
DevGen is a CLI for the parts of development that should be invisible. It writes Conventional Commits from your diff, drafts SemVer changelogs, fetches `.gitignore` and license templates, and routes everything through whichever AI provider you prefer — including a fully local Ollama model if you don't want to send code to the cloud.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **AI-Powered Commits** — Conventional Commit messages generated from your staged diff, grouped by directory, with optional emoji prefixes.
|
|
26
|
+
- **Multiple Providers** — Google Gemini, OpenAI, Anthropic, OpenRouter, HuggingFace, and local Ollama, all behind one CLI.
|
|
27
|
+
- **Smart Caching** — `.gitignore` and license templates are cached for offline use; AI responses are de-duplicated.
|
|
28
|
+
- **Conventional Changelogs** — `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, `chore`, `style`, and a `BREAKING CHANGES` section.
|
|
29
|
+
- **Project Scaffolding** — Pull `.gitignore` from GitHub's collection and drop in SPDX license files (MIT, Apache-2.0, GPL-3.0, AGPL-3.0, BSD, MPL-2.0, …).
|
|
30
|
+
- **Custom Templates** — Override the commit-message prompt via a `.tpl` file in your config.
|
|
31
|
+
- **Interactive Setup** — `devgen setup` walks you through provider + API key + options.
|
|
32
|
+
- **Undo Support** — `devgen commit undo` rolls back the last AI commit while keeping changes staged.
|
|
33
|
+
- **Token-Limit Aware** — When a diff exceeds a model's context window, you get a single actionable error with `--max-groups` and `--max-diff-size` hints.
|
|
34
|
+
|
|
35
|
+
## Supported AI Providers
|
|
36
|
+
|
|
37
|
+
| Provider | Notes |
|
|
38
|
+
| ----------------- | ---------------------------------------------------- |
|
|
39
|
+
| **Google Gemini** | Default-friendly, generous free tier |
|
|
40
|
+
| **OpenAI** | GPT-4o, GPT-4.1, o-series |
|
|
41
|
+
| **Anthropic** | Claude 3.5 / 3.7 / 4 |
|
|
42
|
+
| **OpenRouter** | Single key, many models |
|
|
43
|
+
| **HuggingFace** | Inference API |
|
|
44
|
+
| **Ollama** | Fully local, no API key, no data leaves your machine |
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Recommended: isolated environment
|
|
50
|
+
pipx install devgen-cli
|
|
51
|
+
|
|
52
|
+
# Or use uv for speed
|
|
53
|
+
uv tool install devgen-cli
|
|
54
|
+
|
|
55
|
+
# Or plain pip
|
|
56
|
+
pip install devgen-cli
|
|
57
|
+
|
|
58
|
+
# Shell completion (bash/zsh/fish)
|
|
59
|
+
devgen --install-completion
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires **Python 3.10 or newer**.
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. Configure a provider and API key
|
|
68
|
+
devgen setup config
|
|
69
|
+
|
|
70
|
+
# 2. Stage your work as usual
|
|
71
|
+
git add .
|
|
72
|
+
|
|
73
|
+
# 3. Let DevGen write the commit message
|
|
74
|
+
devgen commit run
|
|
75
|
+
|
|
76
|
+
# 4. Or preview first, then commit
|
|
77
|
+
devgen commit run --dry-run
|
|
78
|
+
devgen commit run --push
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Commands
|
|
82
|
+
|
|
83
|
+
| Command | Description |
|
|
84
|
+
| ------------------------------- | ----------------------------------------------------------------------------- |
|
|
85
|
+
| `devgen setup config` | Interactive provider / API key / options wizard |
|
|
86
|
+
| `devgen commit run` | Stage, generate message, commit (optionally `--push`, `--check`, `--dry-run`) |
|
|
87
|
+
| `devgen commit undo` | Undo the last AI commit, keep changes staged |
|
|
88
|
+
| `devgen changelog` | Generate a Conventional Commits changelog from recent history |
|
|
89
|
+
| `devgen release-notes` | Generate release notes for a version range |
|
|
90
|
+
| `devgen gitignore list` | List available GitHub `.gitignore` templates |
|
|
91
|
+
| `devgen gitignore add <name> …` | Add `.gitignore` entries to the current project |
|
|
92
|
+
| `devgen license list` | List available SPDX licenses |
|
|
93
|
+
| `devgen license add <spdx>` | Add a LICENSE file to the current project |
|
|
94
|
+
|
|
95
|
+
Run `devgen <command> --help` for full options on any subcommand.
|
|
96
|
+
|
|
97
|
+
## Custom Templates
|
|
98
|
+
|
|
99
|
+
DevGen uses `.tpl` files for its commit prompt. To override the default, set `custom_template` in your config (see `devgen config info`) and point it at a `.tpl` file with `{{ diff_text }}`, `{{ context }}`, `{{ group_name }}`, and the conditional `{% if use_emoji %}` block.
|
|
100
|
+
|
|
101
|
+
Example minimal template:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
{{ diff_text }}
|
|
105
|
+
Summarize the change above in one Conventional Commit line.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Architecture
|
|
109
|
+
|
|
110
|
+
DevGen is built around a small set of composable components:
|
|
111
|
+
|
|
112
|
+
- **`BaseProvider`** — abstract base for every AI provider; handles API-key validation, token-limit detection, and error wrapping. Adding a new provider is one subclass with a single `_generate()` method.
|
|
113
|
+
- **`GitOperator`** — all `subprocess` calls to `git` live here, behind a `GitError` exception.
|
|
114
|
+
- **`DiffBuilder` / `FileGrouper` / `ManifestInspector`** — split a staged diff into per-directory groups with compact project context, so the model sees a focused slice instead of a wall of text.
|
|
115
|
+
- **`Section` enum** — single source of truth for changelog ordering and emoji, shared by `ChangelogGenerator` and `ReleaseNotesGenerator`.
|
|
116
|
+
|
|
117
|
+
The CLI entry point is `devgen.cli.main:app` (Typer), exposed as the `devgen` script.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
git clone https://github.com/S4NKALP/DevGen.git
|
|
123
|
+
cd DevGen
|
|
124
|
+
uv sync --all-extras --dev # install runtime + dev deps
|
|
125
|
+
uv run pre-commit run --all-files
|
|
126
|
+
uv run devgen --help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The project uses `uv` for dependency management, `ruff` for lint + format, and `pyright` for type checking. CI runs all three on Python 3.10 through 3.13 (see `.github/workflows/ci.yml`).
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
Issues and PRs welcome. Please run `uv run pre-commit run --all-files` before opening a PR so CI stays green.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
GPL-3.0-or-later. See [LICENSE](LICENSE).
|
|
138
|
+
|
|
139
|
+
## Acknowledgments
|
|
140
|
+
|
|
141
|
+
Built on the shoulders of:
|
|
142
|
+
|
|
143
|
+
- **Typer** & **Rich** — CLI and terminal UI
|
|
144
|
+
- **Questionary** — interactive prompts
|
|
145
|
+
- **Google Gemini**, **OpenAI**, **Anthropic**, **OpenRouter**, **HuggingFace**, and **Ollama** — AI providers
|
|
146
|
+
- **Ruff** — lint and format
|
|
147
|
+
|
|
148
|
+
<div align="center">
|
|
149
|
+
Made with ❤️ by <a href="https://github.com/S4NKALP">Sankalp</a>
|
|
150
|
+
</div>
|
|
@@ -21,8 +21,17 @@ def generate_with_ai(
|
|
|
21
21
|
provider_module = import_module(f"devgen.providers.{provider}")
|
|
22
22
|
class_name = "".join([x.capitalize() for x in provider.split("_")]) + "Provider"
|
|
23
23
|
provider_class = getattr(provider_module, class_name)
|
|
24
|
-
except
|
|
25
|
-
raise ImportError(
|
|
24
|
+
except ModuleNotFoundError as e:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
f"Provider `{provider}` is not a built-in module: {e}. "
|
|
27
|
+
f"Available providers: gemini, openai, anthropic, huggingface, "
|
|
28
|
+
f"openrouter, ollama."
|
|
29
|
+
) from e
|
|
30
|
+
except AttributeError as e:
|
|
31
|
+
raise ImportError(
|
|
32
|
+
f"Provider `{provider}` exists but does not expose a "
|
|
33
|
+
f"`{class_name}` class. This is a packaging bug."
|
|
34
|
+
) from e
|
|
26
35
|
|
|
27
36
|
provider_instance = provider_class()
|
|
28
37
|
return provider_instance.generate(prompt, api_key=api_key, model=model, **kwargs)
|
|
@@ -58,12 +58,28 @@ def run_commit(
|
|
|
58
58
|
help="Review/edit commit message before committing.",
|
|
59
59
|
),
|
|
60
60
|
] = False,
|
|
61
|
+
max_groups: Annotated[
|
|
62
|
+
int | None,
|
|
63
|
+
typer.Option(
|
|
64
|
+
"--max-groups",
|
|
65
|
+
help="Maximum number of commit groups. Lower this if you hit token-limit errors.",
|
|
66
|
+
),
|
|
67
|
+
] = None,
|
|
68
|
+
max_diff_size: Annotated[
|
|
69
|
+
int | None,
|
|
70
|
+
typer.Option(
|
|
71
|
+
"--max-diff-size",
|
|
72
|
+
help="Maximum diff size in chars per group (default 8000). "
|
|
73
|
+
"Lower this if you hit token-limit errors.",
|
|
74
|
+
),
|
|
75
|
+
] = None,
|
|
61
76
|
) -> None:
|
|
62
77
|
log_file = get_main_log_path()
|
|
63
78
|
logger = configure_logger("devgen.cli.commit", log_file, console=debug)
|
|
64
79
|
logger.info(f"Log file: {log_file}")
|
|
65
80
|
logger.info(
|
|
66
|
-
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild},
|
|
81
|
+
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}, "
|
|
82
|
+
f"check={check}, max_groups={max_groups}, max_diff_size={max_diff_size}"
|
|
67
83
|
)
|
|
68
84
|
|
|
69
85
|
run_commit_engine(
|
|
@@ -73,6 +89,8 @@ def run_commit(
|
|
|
73
89
|
force_rebuild=force_rebuild,
|
|
74
90
|
check=check,
|
|
75
91
|
logger=logger,
|
|
92
|
+
max_groups=max_groups,
|
|
93
|
+
max_diff_size=max_diff_size,
|
|
76
94
|
)
|
|
77
95
|
|
|
78
96
|
|
|
@@ -62,7 +62,14 @@ def edit_config(
|
|
|
62
62
|
if key == "provider":
|
|
63
63
|
value = questionary.select(
|
|
64
64
|
"Select AI Provider:",
|
|
65
|
-
choices=[
|
|
65
|
+
choices=[
|
|
66
|
+
"gemini",
|
|
67
|
+
"openai",
|
|
68
|
+
"huggingface",
|
|
69
|
+
"openrouter",
|
|
70
|
+
"anthropic",
|
|
71
|
+
"ollama",
|
|
72
|
+
],
|
|
66
73
|
default=str(current_val) if current_val else "gemini",
|
|
67
74
|
style=style,
|
|
68
75
|
).ask()
|
|
@@ -117,7 +124,14 @@ def set_config() -> None:
|
|
|
117
124
|
# Questions
|
|
118
125
|
provider = questionary.select(
|
|
119
126
|
"Select AI Provider:",
|
|
120
|
-
choices=[
|
|
127
|
+
choices=[
|
|
128
|
+
"gemini",
|
|
129
|
+
"openai",
|
|
130
|
+
"huggingface",
|
|
131
|
+
"openrouter",
|
|
132
|
+
"anthropic",
|
|
133
|
+
"ollama",
|
|
134
|
+
],
|
|
121
135
|
default=config.get("provider", "gemini"),
|
|
122
136
|
style=style,
|
|
123
137
|
).ask()
|
|
@@ -151,12 +165,24 @@ def set_config() -> None:
|
|
|
151
165
|
raise typer.Exit(code=130)
|
|
152
166
|
emoji = emoji_choice == "Yes"
|
|
153
167
|
|
|
168
|
+
ollama_host = config.get("ollama_host", "http://localhost:11434")
|
|
169
|
+
if provider == "ollama":
|
|
170
|
+
ollama_host_input = questionary.text(
|
|
171
|
+
"Ollama server URL:",
|
|
172
|
+
default=ollama_host,
|
|
173
|
+
style=style,
|
|
174
|
+
).ask()
|
|
175
|
+
if ollama_host_input is None:
|
|
176
|
+
raise typer.Exit(code=130)
|
|
177
|
+
ollama_host = ollama_host_input.strip() or ollama_host
|
|
178
|
+
|
|
154
179
|
# Save Config
|
|
155
180
|
new_config = {
|
|
156
181
|
"provider": provider,
|
|
157
182
|
"model": model,
|
|
158
183
|
"api_key": api_key,
|
|
159
184
|
"emoji": emoji,
|
|
185
|
+
"ollama_host": ollama_host,
|
|
160
186
|
}
|
|
161
187
|
|
|
162
188
|
# Merge with existing config to preserve other keys?
|
|
@@ -48,13 +48,21 @@ def _version_callback(value: bool) -> None:
|
|
|
48
48
|
typer.echo(f"devgen version: {version}")
|
|
49
49
|
else:
|
|
50
50
|
typer.secho(
|
|
51
|
-
"
|
|
51
|
+
"Could not determine version: no `project.version` or "
|
|
52
|
+
"`tool.poetry.version` found in pyproject.toml.",
|
|
52
53
|
fg=typer.colors.RED,
|
|
53
54
|
err=True,
|
|
54
55
|
)
|
|
55
|
-
except
|
|
56
|
+
except FileNotFoundError:
|
|
56
57
|
typer.secho(
|
|
57
|
-
f"
|
|
58
|
+
f"pyproject.toml not found at {pyproject_path}. "
|
|
59
|
+
"The installation may be corrupt.",
|
|
60
|
+
fg=typer.colors.RED,
|
|
61
|
+
err=True,
|
|
62
|
+
)
|
|
63
|
+
except toml.TomlDecodeError as e:
|
|
64
|
+
typer.secho(
|
|
65
|
+
f"pyproject.toml is not valid TOML: {e}",
|
|
58
66
|
fg=typer.colors.RED,
|
|
59
67
|
err=True,
|
|
60
68
|
)
|
|
@@ -33,7 +33,14 @@ def setup_config() -> None:
|
|
|
33
33
|
# Questions
|
|
34
34
|
provider = questionary.select(
|
|
35
35
|
"Select AI Provider:",
|
|
36
|
-
choices=[
|
|
36
|
+
choices=[
|
|
37
|
+
"gemini",
|
|
38
|
+
"openai",
|
|
39
|
+
"huggingface",
|
|
40
|
+
"openrouter",
|
|
41
|
+
"anthropic",
|
|
42
|
+
"ollama",
|
|
43
|
+
],
|
|
37
44
|
default=current_config.get("provider", "gemini"),
|
|
38
45
|
style=style,
|
|
39
46
|
).ask()
|
|
@@ -67,12 +74,24 @@ def setup_config() -> None:
|
|
|
67
74
|
raise typer.Exit(code=130)
|
|
68
75
|
emoji = emoji_choice == "Yes"
|
|
69
76
|
|
|
77
|
+
ollama_host = current_config.get("ollama_host", "http://localhost:11434")
|
|
78
|
+
if provider == "ollama":
|
|
79
|
+
ollama_host_input = questionary.text(
|
|
80
|
+
"Ollama server URL:",
|
|
81
|
+
default=ollama_host,
|
|
82
|
+
style=style,
|
|
83
|
+
).ask()
|
|
84
|
+
if ollama_host_input is None:
|
|
85
|
+
raise typer.Exit(code=130)
|
|
86
|
+
ollama_host = ollama_host_input.strip() or ollama_host
|
|
87
|
+
|
|
70
88
|
# Save Config
|
|
71
89
|
new_config = {
|
|
72
90
|
"provider": provider,
|
|
73
91
|
"model": model,
|
|
74
92
|
"api_key": api_key,
|
|
75
93
|
"emoji": emoji,
|
|
94
|
+
"ollama_host": ollama_host,
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
try:
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from devgen.modules.changelog_sections import (
|
|
9
|
+
DEFAULT_STYLES,
|
|
10
|
+
Section,
|
|
11
|
+
section_for_type,
|
|
12
|
+
)
|
|
13
|
+
from devgen.utils import configure_logger, run_git_command
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChangelogGenerator:
|
|
17
|
+
"""Generates a changelog from git history using Semantic Release style."""
|
|
18
|
+
|
|
19
|
+
#: Conventional commit header: ``type(scope)!: subject``
|
|
20
|
+
_CC_PATTERN = re.compile(r"^(\w+)(?:\(([^)]+)\))?(!?):\s+(.*)")
|
|
21
|
+
|
|
22
|
+
def __init__(self, logger=None):
|
|
23
|
+
self.logger = logger or configure_logger("devgen.changelog")
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------ commits
|
|
26
|
+
|
|
27
|
+
def get_commits(self, from_ref: str = "", to_ref: str = "HEAD") -> List[str]:
|
|
28
|
+
"""Fetch commit lines in the requested range."""
|
|
29
|
+
if from_ref:
|
|
30
|
+
range_spec = f"{from_ref}..{to_ref}"
|
|
31
|
+
cmd = self._log_cmd(range_spec)
|
|
32
|
+
else:
|
|
33
|
+
cmd = self._resolve_range(to_ref)
|
|
34
|
+
try:
|
|
35
|
+
return run_git_command(cmd).split("\n")
|
|
36
|
+
except subprocess.CalledProcessError as e:
|
|
37
|
+
self.logger.error(f"Git command failed: {e}")
|
|
38
|
+
raise RuntimeError(f"Git command failed: {e}") from e
|
|
39
|
+
|
|
40
|
+
def _log_cmd(self, range_spec: str) -> List[str]:
|
|
41
|
+
# Format: hash|author|date|subject|body
|
|
42
|
+
return [
|
|
43
|
+
"git",
|
|
44
|
+
"log",
|
|
45
|
+
"--format=%H|%an|%ad|%s|%b",
|
|
46
|
+
"--date=short",
|
|
47
|
+
range_spec,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def _resolve_range(self, to_ref: str) -> List[str]:
|
|
51
|
+
try:
|
|
52
|
+
last_tag = run_git_command(["git", "describe", "--tags", "--abbrev=0"])
|
|
53
|
+
self.logger.info(f"Generating changelog from last tag: {last_tag}")
|
|
54
|
+
return self._log_cmd(f"{last_tag}..HEAD")
|
|
55
|
+
except (RuntimeError, subprocess.CalledProcessError):
|
|
56
|
+
self.logger.info("No tags found, generating for all commits.")
|
|
57
|
+
return ["git", "log", "--format=%H|%an|%ad|%s|%b", "--date=short"]
|
|
58
|
+
|
|
59
|
+
# ----------------------------------------------------------------- parsing
|
|
60
|
+
|
|
61
|
+
def parse_commits(self, raw_commits: List[str]) -> Dict[Section, List[Dict]]:
|
|
62
|
+
"""Group raw commit lines into :class:`Section` buckets."""
|
|
63
|
+
groups: Dict[Section, List[Dict]] = defaultdict(list)
|
|
64
|
+
for line in raw_commits:
|
|
65
|
+
entry = self._parse_line(line)
|
|
66
|
+
if entry is None:
|
|
67
|
+
continue
|
|
68
|
+
section = section_for_type(entry["type"])
|
|
69
|
+
entry["section"] = section
|
|
70
|
+
if entry["breaking"]:
|
|
71
|
+
groups[Section.BREAKING].append(entry)
|
|
72
|
+
groups[section].append(entry)
|
|
73
|
+
return dict(groups)
|
|
74
|
+
|
|
75
|
+
def _parse_line(self, line: str) -> Dict | None:
|
|
76
|
+
if not line.strip():
|
|
77
|
+
return None
|
|
78
|
+
parts = line.split("|", 4)
|
|
79
|
+
if len(parts) < 4:
|
|
80
|
+
return None
|
|
81
|
+
commit_hash, author, date, subject = parts[:4]
|
|
82
|
+
body = parts[4] if len(parts) > 4 else ""
|
|
83
|
+
|
|
84
|
+
match = self._CC_PATTERN.match(subject)
|
|
85
|
+
if match:
|
|
86
|
+
c_type, c_scope, breaking, c_subject = match.groups()
|
|
87
|
+
return {
|
|
88
|
+
"type": c_type,
|
|
89
|
+
"hash": commit_hash,
|
|
90
|
+
"author": author,
|
|
91
|
+
"date": date,
|
|
92
|
+
"scope": c_scope,
|
|
93
|
+
"subject": c_subject,
|
|
94
|
+
"body": body,
|
|
95
|
+
"breaking": bool(breaking),
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
"type": "",
|
|
99
|
+
"hash": commit_hash,
|
|
100
|
+
"author": author,
|
|
101
|
+
"date": date,
|
|
102
|
+
"scope": None,
|
|
103
|
+
"subject": subject,
|
|
104
|
+
"body": body,
|
|
105
|
+
"breaking": False,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------ output
|
|
109
|
+
|
|
110
|
+
def generate_markdown(
|
|
111
|
+
self,
|
|
112
|
+
groups: Dict[Section, List[Dict]],
|
|
113
|
+
version: str = "Unreleased",
|
|
114
|
+
) -> str:
|
|
115
|
+
"""Render groups as a markdown changelog block."""
|
|
116
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
117
|
+
out = [f"## {version} ({date_str})\n"]
|
|
118
|
+
for section in Section.ordered():
|
|
119
|
+
commits = groups.get(section)
|
|
120
|
+
if not commits:
|
|
121
|
+
continue
|
|
122
|
+
style = DEFAULT_STYLES[section]
|
|
123
|
+
out.append(f"## {style.emoji} {style.heading}\n")
|
|
124
|
+
for c in commits:
|
|
125
|
+
scope = f"**{c['scope']}**: " if c.get("scope") else ""
|
|
126
|
+
out.append(f"- {scope}{c['subject']} ({c['hash'][:7]})")
|
|
127
|
+
out.append("")
|
|
128
|
+
return "\n".join(out)
|
|
129
|
+
|
|
130
|
+
def run(self, output_file: Optional[str] = "CHANGELOG.md", from_ref: str = ""):
|
|
131
|
+
"""Fetch → parse → write (or print) the changelog."""
|
|
132
|
+
raw_commits = self.get_commits(from_ref)
|
|
133
|
+
if not raw_commits or not raw_commits[0]:
|
|
134
|
+
self.logger.warning("No commits found.")
|
|
135
|
+
return
|
|
136
|
+
groups = self.parse_commits(raw_commits)
|
|
137
|
+
md_content = self.generate_markdown(groups)
|
|
138
|
+
|
|
139
|
+
if not output_file:
|
|
140
|
+
print(md_content)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
path = Path(output_file)
|
|
144
|
+
if path.exists():
|
|
145
|
+
old = path.read_text(encoding="utf-8")
|
|
146
|
+
if old.strip().startswith("# CHANGELOG"):
|
|
147
|
+
header, _, rest = old.partition("\n")
|
|
148
|
+
new_content = f"{header}\n\n{md_content}\n{rest.lstrip()}"
|
|
149
|
+
else:
|
|
150
|
+
new_content = f"# CHANGELOG\n\n{md_content}\n\n{old}"
|
|
151
|
+
else:
|
|
152
|
+
new_content = f"# CHANGELOG\n\n{md_content}"
|
|
153
|
+
path.write_text(new_content, encoding="utf-8")
|
|
154
|
+
self.logger.info(f"Changelog written to {output_file}")
|
|
155
|
+
print(f" Changelog updated: {output_file}")
|