Compare commits

..
Author SHA1 Message Date
dependabot[bot] 6330c2221c build(deps): bump ruff from 0.15.20 to 0.16.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.20 to 0.16.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.20...0.16.9)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-02 01:13:56 +00:00
sankalp e69de1d99a Merge pull request #63 from S4NKALP/dependabot/github_actions/main/actions/checkout-7
build(deps): bump actions/checkout from 6 to 7
2026-07-03 19:07:59 +05:45
dependabot[bot] e5f22bca85 build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-03 12:45:02 +00:00
sankalp 277f197a71 fix(.github/workflows): change fail-on-scopes to runtime
Update CI to enforce runtime dependency reviews, improving coverage of actual runtime packages.
2026-07-03 18:26:35 +05:45
sankalp 2edbf95ed5 feat(provider): refactor Gemini provider to use struct schema
Improves validation and aligns with latest API changes.
2026-07-03 18:24:48 +05:45
sankalp 109fcd584f feat(root): bump version to 0.3.0 and update dependencies
Update metadata and pin production dependencies for reproducible builds.
2026-07-03 18:23:37 +05:45
sankalp 61e51b5187 chore(deps): bump dependency versions
Upgrade to latest stable releases for security and compatibility.
2026-07-03 18:21:18 +05:45
sankalp d4b694e77d fix(devgen): handle missing git binary gracefully
Ensure the function returns an empty list when git is not found, avoiding crashes.
2026-07-03 18:08:00 +05:45
sankalp 261d04c56f refactor(devgen/providers): centralize retry and quota handling in BaseProvider
Move retry logic and quota detection from provider-specific code to BaseProvider to reduce duplication and simplify error handling.
2026-07-03 18:07:43 +05:45
sankalp f7001d4243 feat(devgen/modules): streamline provider guard and enhance ManifestInspector flexibility
Add token‑limit error handling and switch git diff to null‑delimited output for reliable file detection.
2026-07-03 18:07:26 +05:45
sankalp 11a9db7986 feat(devgen): extend header regex to include deps and wip scopes and ignore markdown fences
Prevent false positives on markdown fences and enable parsing of dependency-related commit types.
2026-07-03 17:02:18 +05:45
sankalp 34f5d090ce chore(devgen): extend commit message regex to include perf and revert
Update the regex in devgen/utils.py to recognize `perf` and `revert` as valid Conventional Commit types, ensuring alignment with the specification and preventing false negatives in automated analysis.
2026-07-03 16:46:05 +05:45
sankalp d20a31bf38 feat(devgen/templates/commit): update commit_message.tpl with expanded conventional commit format
Add detailed format description, examples, and diff handling to improve commit message clarity.
2026-07-03 16:45:47 +05:45
sankalp a43e09e924 feat(.github/workflows): enable auto-merge for dependabot
Allow automatic merging of Dependabot PRs to keep the repository up‑to‑date without manual intervention.
2026-06-16 15:43:46 +05:45
sankalp fca0e34422 chore(root): bump version to 0.2.9
Updated version and dependency constraints for release.
2026-06-16 15:12:54 +05:45
sankalp c4992cce57 formating 2026-06-16 15:05:07 +05:45
sankalp 957dda9d49 refactor: remove jinja2 dependency, use simple template engine
- Replace jinja2 with custom render_template() helper
- Support {{ var }} substitution and {% if %}...{% endif %} conditionals
- Remove jinja2 from pyproject.toml and requirements.txt
- Simplifies dependency tree and reduces install size
2026-06-16 14:58:25 +05:45
sankalp 869aa37f0c update 2026-06-16 14:45:55 +05:45
sankalp 51ede1a7da fix: address 7 security and reliability issues
- Prevent path traversal in gitignore template names (Critical)
- Fix TOCTOU races in file operations with try/except pattern (High)
- Add timeout to HuggingFace API requests (Medium)
- Add missing %x00 separator in changelog fallback format (Medium)
- Filter unsanitized kwargs before passing to OpenAI SDK (High)
- Use context managers for proper client cleanup in providers (High)
- Exit with non-zero code on commit engine failure (Low)
2026-06-16 14:43:52 +05:45
sankalp ede489c219 fix: resolve 8 bugs affecting real users
- Fix KeyError 'root' in FileGrouper when merging groups with no root-level files
- Fix changelog commit parsing breaking on multiline bodies (use %x00 separator)
- Add empty choices/content checks in OpenAI, Anthropic, OpenRouter providers
- Fix config edit crash when config is empty
- Fix validate command using wrong color for 'no staged files'
- Add type check for corrupted YAML config (non-dict returns empty dict)
2026-06-16 14:36:19 +05:45
sankalp a56fe0a6ee update 2026-06-13 15:17:42 +05:45
sankalp e13e763cf2 chore(.github/workflows): upgrade dependency-review-action to v4.6.0
Upgrade to the latest stable version to receive security fixes and new features.
2026-06-13 15:16:22 +05:45
sankalp 79294f1df0 Merge pull request #61 from S4NKALP/dependabot/pip/main/openai-2.41.1
build(deps): bump openai from 2.41.0 to 2.41.1
2026-06-13 15:15:43 +05:45
sankalp 187ad00e99 Merge branch 'main' into dependabot/pip/main/openai-2.41.1 2026-06-13 15:15:19 +05:45
sankalp 35e02ddf7d Merge pull request #60 from S4NKALP/dependabot/pip/main/pre-commit-4.6.0
build(deps): bump pre-commit from 4.5.0 to 4.6.0
2026-06-13 15:14:51 +05:45
sankalp 93f09c9d74 Merge pull request #62 from S4NKALP/dependabot/pip/main/anthropic-0.109.1
build(deps): bump anthropic from 0.105.2 to 0.109.1
2026-06-13 15:14:34 +05:45
sankalp ab9527df33 Merge pull request #59 from S4NKALP/dependabot/pip/main/ruff-0.15.17
build(deps): bump ruff from 0.15.16 to 0.15.17
2026-06-13 15:14:13 +05:45
sankalp 186d952fda Merge pull request #58 from S4NKALP/dependabot/github_actions/main/github/codeql-action-4
build(deps): bump github/codeql-action from 3 to 4
2026-06-13 15:13:59 +05:45
sankalp deba25b055 Merge pull request #57 from S4NKALP/dependabot/github_actions/main/actions/dependency-review-action-5
build(deps): bump actions/dependency-review-action from 4 to 5
2026-06-13 15:13:40 +05:45
dependabot[bot] 9e6c846790 build(deps): bump actions/dependency-review-action from 4 to 5
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4 to 5.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-13 09:28:12 +00:00
sankalp f007c7c579 chore(.github/workflows):switch fail-on-scopes from changes to direct 2026-06-13 15:11:43 +05:45
dependabot[bot] 363b8ec398 build(deps): bump pre-commit from 4.5.0 to 4.6.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.5.0...v4.6.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-13 09:25:52 +00:00
dependabot[bot] 0c5ec5da0a build(deps): bump openai from 2.41.0 to 2.41.1
Bumps [openai](https://github.com/openai/openai-python) from 2.41.0 to 2.41.1.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v2.41.0...v2.41.1)

---
updated-dependencies:
- dependency-name: openai
  dependency-version: 2.41.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-13 09:25:29 +00:00
dependabot[bot] 4f679bc90d build(deps): bump anthropic from 0.105.2 to 0.109.1
Bumps [anthropic](https://github.com/anthropics/anthropic-sdk-python) from 0.105.2 to 0.109.1.
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-python/compare/v0.105.2...v0.109.1)

---
updated-dependencies:
- dependency-name: anthropic
  dependency-version: 0.109.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-13 09:25:28 +00:00
dependabot[bot] f21b090b56 build(deps): bump ruff from 0.15.16 to 0.15.17
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.16 to 0.15.17.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.16...0.15.17)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-13 09:24:55 +00:00
dependabot[bot] 8c63406126 build(deps): bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-13 09:24:51 +00:00
sankalp 91500f634a chore(.github/workflows): remove vuln-check-non-vulnerable-refs flag
Removed the flag that disabled vulnerability checks for non‑vulnerable refs; allow default security behavior.
2026-06-13 15:07:07 +05:45
sankalp cb3a1662d9 refactor(devgen/cli): reorder config merge to preserve key order
Preserves insertion order and prevents accidental overwriting of existing keys
2026-06-06 21:36:37 +05:45
sankalp 70e9068082 fix: None crash in providers, _merge_deepest loop, lock-file false positives, stage() race, cache TTL, config merge, changelog indent, template check 2026-06-06 21:33:51 +05:45
sankalp fb87bfa146 fix: handle deleted files in stage() — use git rm for missing paths 2026-06-06 21:21:10 +05:45
sankalp 69bd79e0fa bump version to 0.2.6 2026-06-06 20:57:00 +05:45
sankalp a86dcc93ee update readme 2026-06-06 17:43:22 +05:45
sankalp 456040ed2b feat(root): revamp README and add gitignore/license commands
Updated project overview, removed emojis, and introduced new CLI subcommands for managing .gitignore and license templates. Added architecture and custom template documentation.

New Features
- Added `devgen gitignore list` and `devgen gitignore add <name> …` commands.
- Added `devgen license list` and `devgen license add <spdx>` commands.
- Introduced support for custom commit templates via `.tpl` files.

Enhancements
- Rewrote README with clearer description and updated badges.
- Removed emojis and streamlined formatting throughout documentation.
- Added sections for custom templates, architecture overview, and contribution guide.
- Clarified dependency installation and CI workflow references.

Bug Fixes
- Fixed formatting issues in badge links and stray hyphens.
- Resolved truncated documentation sections and improved readability.
2026-06-06 17:39:25 +05:45
sankalp df1d1765d1 ci: drop pyright typecheck job in favour of ruff
The project uses ruff for lint+format, and pyright was redundant and
slow. The previous pyright run surfaced ~30 false positives from running
outside the venv plus 5 real-but-minor typing issues that don't justify
the extra CI minutes on a 0.2.x CLI.

CI now runs only ruff check, ruff format --check, and the import/CLI
smoke matrix on 3.10/3.11/3.12/3.13.
2026-06-06 17:38:01 +05:45
sankalp 09ba5e9644 ci: add CodeQL, lint/typecheck/smoke CI, and dependency review
- ci.yml: ruff check + format check, pyright typecheck, and import/CLI
  smoke test on Python 3.10/3.11/3.12/3.13 matrix. Uses uv like the
  existing pre-commit workflow for consistency.
- codeql.yml: weekly + push/PR Python security-and-quality analysis.
- dependency-review.yml: fail PRs adding high+ severity CVEs or copyleft
  licenses that would taint GPL-3.0 (AGPL, SSPL, BUSL).
2026-06-06 17:31:31 +05:45
sankalp 850e88972e ci(dependabot): merge directly via squash instead of use-github-auto-merge
The previous config required branch protection with required status checks,
but main has no protection rules configured, so auto-merge never fired and
PRs had to be merged manually.

Switch to direct squash merge in the action itself, which works regardless
of branch protection settings.
2026-06-06 17:24:10 +05:45
sankalp 96e776f7cc chore(root):ignore .tpl files in Linguist language stats to prevent inflated language statistics. 2026-06-06 17:10:34 +05:45
sankalp f70b5208ec refactor(devgen/providers): shared provider base class
Abstract out generate validation and error handling into BaseProvider, eliminating duplicated logic across Anthropic, OpenRouter, and OpenAI providers.
2026-06-06 17:10:30 +05:45
sankalp e2ec269f16 refactor(modules): restructure changelog and release note generation
Refactor to improve readability and separate concerns,
use Section enum and ordered traversal for consistent output.
2026-06-06 17:10:30 +05:45
sankalp 90ca0c0f59 refactor(devgen/modules): rename commit_message template from .j2 to .tpl
Bug Fixes:
- Updated template reference to use .tpl extension, resolving missing template error.
2026-06-06 16:56:14 +05:45
sankalp 9781523e8e refactor(devgen/modules): streamline manifest context generation
Enhancements:
- Replaced verbose scanning of many manifest files with targeted parsing of key project manifests (pyproject.toml, package.json, requirements.txt).
- Generated a compact one‑line‑per‑file summary, reducing token usage.
- Improved clarity and readability of the AI‑provided context.
2026-06-06 16:48:28 +05:45
sankalp 96b2499ecf chore(devgen/templates/commit): streamline commit template to Conventional Commits format
Clarify output format and remove redundant guidance.

New Features:
- Added explicit format and type list

Enhancements:
- Simplified instructions, removed unnecessary guidance

Bug Fixes:
- None
2026-06-06 16:48:21 +05:45
sankalp 4f8e358a60 feat(devgen/modules): expose max_groups and max_diff_size as CLI override flags
- Add CLI flags to override `max_groups` and `max_diff_size` limits
- Update file grouping logic to respect override values
- Improve truncation logging with configurable size and guidance
- Remove unused imports and streamline rich console setup
2026-06-06 16:45:18 +05:45
sankalp 72c57f0d33 feat(devgen):add token limit error detection and friendly formatting utilities 2026-06-06 16:45:13 +05:45
sankalp 0b023482d7 feat(devgen/cli): introduce --max-groups and --max-diff-size options for commit command
Add limits to prevent token‑limit errors during commit generation.
- New `--max-groups` option to cap the number of commit groups.
- New `--max-diff-size` option to set maximum diff size per group (default 8000 chars).
Update logging to reflect the new options and pass them to the engine.
2026-06-06 16:45:07 +05:45
sankalp 8069895bc7 feat(devgen/providers): normalize token limit error handling
Add token limit error detection and formatting across all provider modules to surface clear diagnostics when context windows are exceeded, improving error messages and preventing silent failures.

New Features
- Introduce `is_token_limit_error` checks in Anthropic, Gemini, Hugging Face, Ollama, OpenAI, and OpenRouter providers.
- Use `format_token_limit_error` to generate user‑friendly error messages for token limit overs.

Enhancements
- Centralize token limit error handling for consistent diagnostics across providers.
- Explicitly re‑raise `RuntimeError` in Hugging Face to preserve original behavior.

Bug Fixes
- None
2026-06-06 16:45:05 +05:45
sankalp d991af9c47 feat(devgen/cli): introduce Ollamaprovider support and interactive host configuration
- Add "ollama" to AI provider choices in config prompts
- Prompt user for Ollama server URL when selecting the Ollama provider
- Store the configured Ollama host in the config under the "ollama_host" key
- Update config saving logic in both edit_config and setup_config functions
2026-06-06 16:31:09 +05:45
sankalp d8fcf0849a feat(root): add local Ollama AI model support
- New Features
  - Added Ollama as a supported AI model source.
  - Updated README and project manifests to include Ollama.

- Enhancements
  - Expanded AI model selection to handle local Ollama endpoints.
  - Improved dependency resolution for Ollama packages.

- Bug Fixes
  - None
2026-06-06 16:31:06 +05:45
sankalp aad3703ea3 feat(devgen/modules): add configurable Ollama host- New Features
- Added `ollama_host` configuration option.
- Enhancements
  - Use the configured host when calling the Ollama provider.
2026-06-06 16:31:02 +05:45
sankalp a8b48e9f07 feat(devgen/providers): add OllamaProvider for local Ollama integration
Add a new provider that wraps the local Ollama server using an OpenAI‑compatible client, enabling generation via `ollama_host` configuration. This introduces the first provider for local LLM inference.

New Features
- Implement OllamaProvider with a `generate()` method, supporting custom host, model selection, and robust error handling.
2026-06-06 16:30:56 +05:45
sankalp 7016cff3a7 feat(devgen): add Ollama provider and host configuration
Add support for the Ollama provider in the AI generation pipeline and expose a configurable host address.

New Features:
- Introduce `ollama` as a selectable provider.
- Add `ollama_host` configuration option with default `http://localhost:11434`.

Enhancements:
- Extend provider list in error message to include Ollama.
2026-06-06 16:30:52 +05:45
sankalp 55ebbaef15 feat(devgen/cli): improve version callback error messages
- Enhanced error messages when version cannot be determined, providing clearer guidance.
- Added explicit handling for missing pyproject.toml and TOML parsing errors.
- Improved user feedback for installation corruption issues.
2026-06-06 16:27:20 +05:45
sankalp 80f68a6ccd feat(devgen): enhance provider import error handling
Enhancements:
- Split ModuleNotFoundError and AttributeError handling to deliver clearer diagnostics
- Explicitly list available providers and flag missing provider classes as packaging bugs

Bug Fixes:
- Provide specific guidance when a provider module is not a built‑in module
- Clarify that a missing provider class indicates a packaging issue rather than user error
2026-06-06 16:27:17 +05:45
sankalp fd3ce03484 feat(devgen/modules): categorize committypes and improve changelog/release note formattingAdd separate categories for Performance, Refactor, Tests, Style, and Chore; update ordering and emojis; improve remote push detection and fetch strategy; enhance error logging.
- New Features  - Introduced distinct sections for Performance, Refactor, Tests, Style, and Chore in changelog generation.
  - Added dedicated emojis for each new category.
  - Updated section ordering to reflect the new categories.
- Enhancements
  - Enhanced push logic to fetch upstream before push, avoiding non‑fast‑forward rejections.
  - Refactored `is_ahead_of_remote` with a fetch flag to reduce network calls and TOCTOU windows.
  - Improved error logging with full traceback for group processing failures.
- Bug Fixes
  - Fixed emoji mappings for "Style" and "Other Changes".
  - Corrected handling of empty upstream ref to prevent errors.
  - Ensured proper exception handling when remote branch is missing.
2026-06-06 16:27:12 +05:45
sankalp 51d2b7549f feat(devgen/providers):improve provider error handling and response parsing 2026-06-06 16:27:03 +05:45
sankalp 5660e40eb4 chore(root): tidy README and bump dependencies
New Features
- None

Enhancements
- Updated dependency versions; fixed pre‑commit typo.
- Normalized Rich version across config files.
- Cleaned up README formatting and spacing.

Bug Fixes
- Fixed misspelling "pre-comit" → "pre-commit".
- Removed redundant whitespace in README badges and title.
2026-06-06 16:26:58 +05:45
sankalp c6914e2139 Merge pull request #53 from S4NKALP/dependabot/pip/main/openai-2.41.0
build(deps): bump openai from 2.29.0 to 2.41.0
2026-06-06 16:11:32 +05:45
dependabot[bot] 4f0a055216 build(deps): bump openai from 2.29.0 to 2.41.0
Bumps [openai](https://github.com/openai/openai-python) from 2.29.0 to 2.41.0.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v2.29.0...v2.41.0)

---
updated-dependencies:
- dependency-name: openai
  dependency-version: 2.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 10:26:23 +00:00
sankalp 16c6f70057 Merge pull request #56 from S4NKALP/dependabot/pip/main/typer-0.26.7
build(deps): bump typer from 0.24.1 to 0.26.7
2026-06-06 16:09:48 +05:45
sankalp 3daf798dcd Merge pull request #54 from S4NKALP/dependabot/pip/main/ruff-0.15.16
build(deps): bump ruff from 0.15.7 to 0.15.16
2026-06-06 16:09:33 +05:45
sankalp d6f2452d21 Merge pull request #55 from S4NKALP/dependabot/pip/main/google-genai-2.8.0
build(deps): bump google-genai from 1.68.0 to 2.8.0
2026-06-06 16:09:14 +05:45
dependabot[bot] 59aa40bdcf build(deps): bump google-genai from 1.68.0 to 2.8.0
Bumps [google-genai](https://github.com/googleapis/python-genai) from 1.68.0 to 2.8.0.
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-genai/compare/v1.68.0...v2.8.0)

---
updated-dependencies:
- dependency-name: google-genai
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 10:22:19 +00:00
sankalp 8fdc346913 Merge pull request #50 from S4NKALP/dependabot/pip/main/anthropic-0.105.2
build(deps): bump anthropic from 0.86.0 to 0.105.2
2026-06-06 16:05:29 +05:45
dependabot[bot] cdca283cd2 build(deps): bump typer from 0.24.1 to 0.26.7
Bumps [typer](https://github.com/fastapi/typer) from 0.24.1 to 0.26.7.
- [Release notes](https://github.com/fastapi/typer/releases)
- [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md)
- [Commits](https://github.com/fastapi/typer/compare/0.24.1...0.26.7)

---
updated-dependencies:
- dependency-name: typer
  dependency-version: 0.26.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 10:20:21 +00:00
dependabot[bot] e30d2d854d build(deps): bump ruff from 0.15.7 to 0.15.16
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.7 to 0.15.16.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.7...0.15.16)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 10:20:20 +00:00
sankalp e77472fb0f Merge pull request #44 from S4NKALP/dependabot/pip/main/requests-2.34.2
build(deps): bump requests from 2.32.5 to 2.34.2
2026-06-06 16:03:59 +05:45
sankalp e3dc6729dc Merge branch 'main' into dependabot/pip/main/requests-2.34.2 2026-06-06 16:03:44 +05:45
sankalp c7c1da8459 Merge pull request #28 from S4NKALP/dependabot/pip/main/rich-15.0.0
build(deps): bump rich from 14.3.3 to 15.0.0
2026-06-06 16:03:14 +05:45
dependabot[bot] b0ad273f87 build(deps): bump anthropic from 0.86.0 to 0.105.2
Bumps [anthropic](https://github.com/anthropics/anthropic-sdk-python) from 0.86.0 to 0.105.2.
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-python/compare/v0.86.0...v0.105.2)

---
updated-dependencies:
- dependency-name: anthropic
  dependency-version: 0.105.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-29 01:19:02 +00:00
dependabot[bot] dcecfec258 build(deps): bump requests from 2.32.5 to 2.34.2
Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.34.2.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.5...v2.34.2)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.34.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-15 01:17:54 +00:00
dependabot[bot] f21ba55d6d build(deps): bump rich from 14.3.3 to 15.0.0
Bumps [rich](https://github.com/Textualize/rich) from 14.3.3 to 15.0.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.3.3...v15.0.0)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 01:15:55 +00:00
sankalp 48ae3be0d4 chore: auto-update README [skip ci] 2026-03-27 07:01:11 +05:45
sankalp 5682a51d97 chore: auto-update README [skip ci] 2026-03-20 08:38:34 +05:45
sankalp 3007623f4c docs(root): update README withproject description and badges
Added a concise AI‑focused description and a centered badge section to the README, improving the project overview.

New Features- Introduced an AI‑centric tagline that highlights automation of commits, changelogs, and project management tasks.

Enhancements
- Centered and styled the title block for consistent visual presentation.
- Integrated project badges for PyPI version, Python compatibility, license, build status, and pre‑commit.

Bug Fixes
- None.
2026-03-20 08:38:24 +05:45
sankalp f2e5097e7f chore: auto-update README [skip ci] 2026-03-20 08:34:44 +05:45
sankalp d95c88b240 Merge pull request #14 from S4NKALP/dependabot/pip/main/openai-2.29.0 2026-03-20 08:34:37 +05:45
sankalp dc2b00651f chore: auto-update README [skip ci] 2026-03-20 08:34:18 +05:45
sankalp 2c9c5ed22d Merge pull request #15 from S4NKALP/dependabot/pip/main/ruff-0.15.7 2026-03-20 08:34:11 +05:45
dependabot[bot] 8a42b547ec build(deps): bump openai from 2.26.0 to 2.29.0
Bumps [openai](https://github.com/openai/openai-python) from 2.26.0 to 2.29.0.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v2.26.0...v2.29.0)

---
updated-dependencies:
- dependency-name: openai
  dependency-version: 2.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 02:49:10 +00:00
sankalp d7fd658b98 Merge pull request #13 from S4NKALP/dependabot/pip/main/anthropic-0.86.0 2026-03-20 08:33:57 +05:45
sankalp 34a7635a6d chore: auto-update README [skip ci] 2026-03-20 08:33:53 +05:45
sankalp fdd38d5a49 Merge branch 'main' into dependabot/pip/main/anthropic-0.86.0 2026-03-20 08:33:44 +05:45
sankalp aa4fc8018e chore: auto-update README [skip ci] 2026-03-20 08:33:44 +05:45
sankalp 763537a324 Merge pull request #12 from S4NKALP/dependabot/pip/main/google-genai-1.68.0 2026-03-20 08:33:05 +05:45
sankalp 867156b65a chore: auto-update README [skip ci] 2026-03-20 07:01:09 +05:45
dependabot[bot] 0e50e5f5b0 build(deps): bump ruff from 0.15.5 to 0.15.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.5 to 0.15.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.5...0.15.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 01:15:20 +00:00
dependabot[bot] 3fde0941eb build(deps): bump anthropic from 0.84.0 to 0.86.0
Bumps [anthropic](https://github.com/anthropics/anthropic-sdk-python) from 0.84.0 to 0.86.0.
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-python/compare/v0.84.0...v0.86.0)

---
updated-dependencies:
- dependency-name: anthropic
  dependency-version: 0.86.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 01:15:04 +00:00
dependabot[bot] 5b7303c2a6 build(deps): bump google-genai from 1.66.0 to 1.68.0
Bumps [google-genai](https://github.com/googleapis/python-genai) from 1.66.0 to 1.68.0.
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-genai/compare/v1.66.0...v1.68.0)

---
updated-dependencies:
- dependency-name: google-genai
  dependency-version: 1.68.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 01:15:01 +00:00
sankalp 6a8e92b4fc chore: auto-update README [skip ci] 2026-03-13 07:01:35 +05:45
sankalp 89bfe85002 docs(root): enrich README with detailed description and badges
Add expanded project overview and formatting updates.

- New Features: Expanded overview and richer description of AI-powered commits and customizable templates.
- Enhancements: Added extra badge images and improved formatting across sections.
- Bug Fixes: None.
2026-03-06 20:38:21 +05:45
sankalp 5515ca5633 Merge pull request #5 from S4NKALP/dependabot/pip/main/google-genai-1.66.0
build(deps): bump google-genai from 0.1.0 to 1.66.0
2026-03-06 20:36:10 +05:45
sankalp e7385c55fb chore: auto-update README [skip ci] 2026-03-06 20:36:03 +05:45
dependabot[bot] 52cf0d4a7b build(deps): bump google-genai from 0.1.0 to 1.66.0
Bumps [google-genai](https://github.com/googleapis/python-genai) from 0.1.0 to 1.66.0.
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-genai/commits/v1.66.0)

---
updated-dependencies:
- dependency-name: google-genai
  dependency-version: 1.66.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:50:55 +00:00
sankalp ceebf728ca Merge pull request #8 from S4NKALP/dependabot/pip/main/rich-14.3.3
build(deps): bump rich from 14.2.0 to 14.3.3
2026-03-06 20:35:44 +05:45
dependabot[bot] 277cb324ba build(deps): bump rich from 14.2.0 to 14.3.3
Bumps [rich](https://github.com/Textualize/rich) from 14.2.0 to 14.3.3.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.2.0...v14.3.3)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 14.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:50:01 +00:00
sankalp 8c98b64495 chore: auto-update README [skip ci] 2026-03-06 20:34:33 +05:45
sankalp b9899a774f Merge pull request #7 from S4NKALP/dependabot/pip/main/typer-0.24.1
build(deps): bump typer from 0.21.1 to 0.24.1
2026-03-06 20:34:32 +05:45
sankalp 96c8dd83b1 Merge pull request #6 from S4NKALP/dependabot/pip/main/ruff-0.15.5
build(deps): bump ruff from 0.14.13 to 0.15.5
2026-03-06 20:34:24 +05:45
sankalp 92e9f5ac19 chore: auto-update README [skip ci] 2026-03-06 20:34:08 +05:45
sankalp 75eb20fab3 Merge pull request #4 from S4NKALP/dependabot/pip/main/anthropic-0.84.0
build(deps): bump anthropic from 0.76.0 to 0.84.0
2026-03-06 20:34:00 +05:45
sankalp 5b7bd55ccd chore: auto-update README [skip ci] 2026-03-06 20:33:55 +05:45
sankalp 386e0ee72d Merge pull request #9 from S4NKALP/dependabot/pip/main/openai-2.26.0
build(deps): bump openai from 2.15.0 to 2.26.0
2026-03-06 20:33:48 +05:45
sankalp b31b06eb11 chore: auto-update README [skip ci] 2026-03-06 20:33:03 +05:45
sankalp cf637f34e8 Merge pull request #3 from S4NKALP/dependabot/github_actions/main/actions/checkout-6
build(deps): bump actions/checkout from 4 to 6
2026-03-06 20:32:30 +05:45
sankalp cb99c2fa56 Merge pull request #2 from S4NKALP/dependabot/github_actions/main/astral-sh/setup-uv-7
build(deps): bump astral-sh/setup-uv from 4 to 7
2026-03-06 20:32:19 +05:45
sankalp a67220cfb7 chore: auto-update README [skip ci] 2026-03-06 19:45:40 +05:45
dependabot[bot] f9914fa482 build(deps): bump openai from 2.15.0 to 2.26.0
Bumps [openai](https://github.com/openai/openai-python) from 2.15.0 to 2.26.0.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v2.15.0...v2.26.0)

---
updated-dependencies:
- dependency-name: openai
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:00:39 +00:00
dependabot[bot] fab6465c69 build(deps): bump typer from 0.21.1 to 0.24.1
Bumps [typer](https://github.com/fastapi/typer) from 0.21.1 to 0.24.1.
- [Release notes](https://github.com/fastapi/typer/releases)
- [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md)
- [Commits](https://github.com/fastapi/typer/compare/0.21.1...0.24.1)

---
updated-dependencies:
- dependency-name: typer
  dependency-version: 0.24.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:00:33 +00:00
dependabot[bot] b94e8502c8 build(deps): bump ruff from 0.14.13 to 0.15.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.13 to 0.15.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.13...0.15.5)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:00:24 +00:00
dependabot[bot] 4fe5ef0d34 build(deps): bump anthropic from 0.76.0 to 0.84.0
Bumps [anthropic](https://github.com/anthropics/anthropic-sdk-python) from 0.76.0 to 0.84.0.
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-python/compare/v0.76.0...v0.84.0)

---
updated-dependencies:
- dependency-name: anthropic
  dependency-version: 0.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:00:13 +00:00
sankalp 9e9c42ced7 chore: auto-update README [skip ci] 2026-03-06 19:45:12 +05:45
dependabot[bot] 81da7f1846 build(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 14:00:04 +00:00
dependabot[bot] a01bdac09f build(deps): bump astral-sh/setup-uv from 4 to 7
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 4 to 7.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v4...v7)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 13:59:59 +00:00
sankalp b74fb8071a chore(.github/workflows): add dependabot auto-merge workflow
New Features:
- Add dependabot auto-merge workflow (dependabot-auto-merge.yml)
2026-03-06 19:44:27 +05:45
sankalp d135381ad5 chore(.github): add dependabot.yml for weekly dependency updates
Add dependabot configuration to automate weekly updates of GitHub Actions and pip packages.

New Features
- Add Dependabot configuration for GitHub Actions and pip ecosystems with weekly schedule.

Enhancements
-

Bug Fixes
-
2026-03-06 19:44:23 +05:45
sankalp daa6f203c3 refactor(devgen/modules): reformat time.sleep block for readability 2026-03-06 19:43:19 +05:45
sankalp 1b36c3f2d7 feat(devgen/providers): improve error detection for rate‑limiting and quota exceeded responses
Enhancements:
- Extend error message parsing to capture additional throttling indicators such as "RESOURCE_EXHAUSTED", "QUOTA_EXCEEDED", and "THROTTLED".
- Refactor the conditional logic for better readability and maintainability.
- Keep existing retry behavior while increasing resilience to API rate‑limit errors.
2026-03-06 19:43:16 +05:45
sankalp 9554f7af2a feat(devgen/providers): bump default Gemini model to gemini-2.5-flash and add retry handling for quota errors
The update changes the default model, refactors imports, and implements robust retry logic with exponential back‑off and clearer error messages. This improves resilience and clarifies usage.

New Features
- Exponential back‑off retry on 429/quota exhausted responses.
- Configurable max_retries and retry_delay parameters.

Enhancements
- Default model set to gemini-2.5-flash.
- Improved error output with actionable troubleshooting steps.
2026-01-30 22:36:31 +05:45
sankalp 06f25af35f chore(deps): update all python dependencies 2026-01-16 23:30:08 +05:45
sankalp cc94b53d48 chore(root): bump version to 0.2.4
- Updated version in `pyproject.toml` and `setup.py` to `0.2.4`
- Lockfile (`uv.lock`) updated to reflect dependency changes
2025-12-30 14:40:57 +05:45
sankalp b09de402cc feat(devgen): add max_groups config option 2025-12-25 09:36:05 +05:45
sankalp c121a7fa59 feat(devgen/modules): add interactive commit review and smart file grouping
- New Features:
  - Added `check` parameter to enable interactive commit message review
  - Implemented user confirmation/editing/abort workflow using questionary
  - Added smart file grouping with configurable max_groups limit
  - Files are now merged into parent directories when group count exceeds limit

- Enhancements:
  - Improved `group_files()` method with hierarchical merging logic
  - Added graceful KeyboardInterrupt handling during user review
  - Enhanced logging for group merging operations
```
2025-12-25 09:35:33 +05:45
sankalp 525aebadf4 feat(devgen/modules): enhance commit generation with manifest context and custom templates
- New Features:
  - Added `_get_manifest_context()` to extract project metadata from manifest files (pyproject.toml, package.json, etc.) for richer commit message context.
  - Added support for custom commit message templates via `custom_template` config.
  - Added emoji instruction toggle based on global `use_emoji` setting.

- Enhancements:
  - Improved error handling for git push failures with specific hints for "no upstream branch" scenarios.
  - Optimized manifest context extraction by truncating large files to avoid token bloat.
  - Refactored prompt generation to dynamically include manifest context and custom templates.
```
2025-12-25 00:04:54 +05:45
sankalp 1219c1ad81 feat(devgen/templates/commit): add context to commit message template
- New Features:
  - Added `{{ context }}` variable to the commit message template to provide additional context.
- Enhancements:
  - Removed the emoji usage line from the template to simplify the output.
```
2025-12-25 00:04:50 +05:45
sankalp cfa891dcde feat(devgen/cli): add config info command and improve undo commit message formatting 2025-12-25 00:04:48 +05:45
sankalp 286c46d2b5 feat(root): enhance commit generation with context-aware features
- Add shell completion support for bash/zsh/fish via `devgen --install-completion`
- Introduce `devgen commit undo` to revert the last AI-generated commit while preserving staged changes
- Implement context-aware commit generation by analyzing project manifests (`package.json`, `pyproject.toml`, etc.) and lock files for accurate dependency updates
- Add support for custom Jinja2 templates in `~/.devgen.yaml` with `custom_template` configuration
- Update documentation to reflect new features and provide template variable reference

This release significantly improves commit accuracy by leveraging project context and offers greater customization through templates and shell integration.
```
2025-12-25 00:04:44 +05:45
sankalp f2ff3327c3 feat(devgen): add custom template rendering function
- New Features:
  - Introduced `render_custom_template()` function to render Jinja2 templates from strings.
  - Enables dynamic template rendering without requiring file-based templates.
```
2025-12-25 00:04:40 +05:45
sankalp 37e5d94373 feat(devgen): enhance sanitize_ai_commit_message to handle bolding and labels
- Improve header detection by stripping bolding and cleaning labels like "1. Title:", "### Message:", etc.
- Stop processing lines when encountering a sponsor section
```
2025-12-24 23:49:37 +05:45
sankalp 40c9b3ca57 refactor(devgen/templates/commit): update commit message template guidelines
- Simplify and clarify the structure of commit message generation guidelines.
- Replace "Format" with "Guidelines" for better readability.
- Remove redundant section numbering.
- Streamline emoji usage instruction into a single line.
- Clarify that the output should exclude labels like "Title:" or "Format:".

**Enhancements:**
- Improved template structure for better maintainability.
- More concise and direct instructions for users.
2025-12-24 23:49:34 +05:45
sankalp 4d309c313d 1. Title: feat(root): add TODO.md for project roadmap and tasks 2025-12-24 23:44:23 +05:45
sankalp ba747c727c feat(devgen): improve sanitize_ai_commit_message function
The `sanitize_ai_commit_message` function in `devgen/utils.py` has been enhanced for better readability and maintainability. The changes include minor formatting improvements and a more consistent regex pattern definition.

**Enhancements & Refinements:**
- Improved code formatting by removing trailing whitespace and adding a comma after `re.IGNORECASE` in the regex pattern for better readability.
- Enhanced comment clarity by fixing a minor typo in the regex examples section.
```
2025-12-24 23:34:35 +05:45
sankalp 3635887a85 feat(devgen/modules): enhance changelog and license generation with improved file handling
This update introduces improvements to the changelog generator's file handling logic, including proper prepending of new entries and automatic header management. It also refines the license generator's template directory structure and cleans up minor formatting issues in the commit generator.

**New Features**
- Added intelligent changelog prepending logic that preserves existing headers and maintains proper formatting when updating files
- Implemented automatic `# CHANGELOG` header insertion for new changelog files

**Enhancements & Refinements**
- Changed changelog version headers from `#` to `##` for better markdown hierarchy
- Updated license generator to use `templates/licenses` directory instead of `prompt_templates/licenses`
- Removed unnecessary whitespace and improved code formatting in commit generator
- Enhanced changelog file writing logic to handle both new and existing files more robustly
```
2025-12-24 23:34:32 +05:45
sankalp a7507255c7 feat(root): update devgen-cli to version 0.2.3
The dependency `devgen-cli` has been upgraded from version 0.2.2 to 0.2.3 in the project's lockfile.

- **Enhancements & Refinements**
  - Updated `devgen-cli` dependency to version 0.2.3 in `uv.lock`
```
2025-12-24 23:29:47 +05:45
sankalp d373f95388 refactor(devgen): enhance AI commit message sanitization
The `sanitize_ai_commit_message` function has been significantly improved to better handle AI-generated commit messages, including support for bolded headers, more robust header detection, and a fallback mechanism for non-conventional messages.

**Enhancements & Refinements:**
- Added comprehensive docstring explaining the function's purpose and behavior
- Improved regex pattern to match conventional commit headers with optional bolding (e.g., **feat: ...**)
- Added case-insensitive matching for commit types
- Implemented a state-based approach with `found_header` flag for clearer logic
- Added automatic removal of bold markers (**) from headers
- Introduced fallback behavior to return the first non-empty line if no conventional header is found
- Improved code structure with better variable naming and comments
- Added early return for empty input strings

**Bug Fixes:**
- Fixed potential issue where the function could return empty strings for non-conventional messages
- Resolved edge case handling for messages without proper headers
```
2025-12-24 23:29:45 +05:45
sankalp fe7da77ed6 chore(root): Bump version to 0.2.3 2025-12-14 07:58:09 +05:45
sankalp 407458149b fix: remove invalid debug argument from OpenAI and OpenRouter providers
The debug argument is used internally by devgen but is not accepted by the OpenAI API client. This change filters it out from kwargs before the API call to prevent TypeError.
2025-12-14 07:57:16 +05:45
sankalp e9a45cf140 chore(root): Bump version to 0.2.2
This commit updates the project's version from 0.2.1 to 0.2.2 across the `pyproject.toml`, `setup.py`, and `uv.lock` files.
2025-12-14 00:55:19 +05:45
sankalp 70e150611b refactor(docs): Overhaul README for improved clarity and quick start
The `README.md` has undergone a significant rewrite and restructuring to enhance readability, streamline the onboarding process, and better communicate the value proposition of DevGen. This includes a new introduction, a consolidated "Why DevGen?" section, simplified quick start guides, and a dedicated feature deep dive.

*   **Enhancements & Refinements**
    *   **Improved Introduction**: Added a concise tagline and refined the introductory paragraph, positioning DevGen as an AI co-pilot for Git and project management.
    *   **Streamlined Feature Presentation**: Replaced the previous feature list with a "Why DevGen?" section, highlighting key benefits such as AI Brain (multi-provider support), battle-tested compliance (Conventional Commits, Semantic Versioning), lightning-fast performance, and zero-friction setup.
    *   **Simplified Quick Start**: Refined the initial setup and commit generation steps for a quicker onboarding experience.
    *   **Enhanced Feature Deep Dive**: Introduced a dedicated section to provide detailed usage examples for AI-powered commits, changelogs, release notes, gitignore, and license generation.
    *   **Improved Installation Instructions**: Emphasized `pipx` and `uv` as recommended installation methods for isolated and fast CLI management.
    *   **Concise Configuration Table**: Replaced verbose configuration details with a clear markdown table for easier reference.
    *   **Updated PyPI Link**: Corrected the PyPI link to `devgen-cli`.
    *   **Minor Visual Updates**: Refined badge presentation and added a centered footer with author attribution.

*   **Refactoring**
    *   **Major Content Restructuring**: Completely reordered and rewrote large sections, removing redundant information and consolidating related content to improve overall flow and conciseness.
    *   **Removed Command Reference**: Integrated command examples directly into relevant usage sections, eliminating a separate, lengthy command reference.
    *   **Removed Redundant Sections**: Consolidated information previously found in "Use Cases" and "Requirements" into other sections, streamlining the document.
2025-12-13 22:26:20 +05:45
sankalp f85da0461f feat(dry-run): Display dry run messages on console
**Enhancements & Refinements**
* Dry run commit messages are now displayed directly on the console during execution.
* Utilizes rich `Panel` and `Markdown` to provide a structured and readable console output for dry run messages.
* Offers immediate visual feedback for generated commits when performing a dry run.
2025-12-13 21:42:33 +05:45
sankalp 7c15e2183b refactor(docs): Overhaul README for improved clarity and quick start
The `README.md` has undergone a significant rewrite and restructuring to enhance readability, streamline the onboarding process, and better communicate the value proposition of DevGen. This includes a new introduction, a consolidated "Why DevGen?" section, simplified quick start guides, and a dedicated feature deep dive.

*   **Enhancements & Refinements**
    *   **Improved Introduction**: Added a concise tagline and refined the introductory paragraph, positioning DevGen as an AI co-pilot for Git and project management.
    *   **Streamlined Feature Presentation**: Replaced the previous feature list with a "Why DevGen?" section, highlighting key benefits such as AI Brain (multi-provider support), battle-tested compliance (Conventional Commits, Semantic Versioning), lightning-fast performance, and zero-friction setup.
    *   **Simplified Quick Start**: Refined the initial setup and commit generation steps for a quicker onboarding experience.
    *   **Enhanced Feature Deep Dive**: Introduced a dedicated section to provide detailed usage examples for AI-powered commits, changelogs, release notes, gitignore, and license generation.
    *   **Improved Installation Instructions**: Emphasized `pipx` and `uv` as recommended installation methods for isolated and fast CLI management.
    *   **Concise Configuration Table**: Replaced verbose configuration details with a clear markdown table for easier reference.
    *   **Updated PyPI Link**: Corrected the PyPI link to `devgen-cli`.
    *   **Minor Visual Updates**: Refined badge presentation and added a centered footer with author attribution.

*   **Refactoring**
    *   **Major Content Restructuring**: Completely reordered and rewrote large sections, removing redundant information and consolidating related content to improve overall flow and conciseness.
    *   **Removed Command Reference**: Integrated command examples directly into relevant usage sections, eliminating a separate, lengthy command reference.
    *   **Removed Redundant Sections**: Consolidated information previously found in "Use Cases" and "Requirements" into other sections, streamlining the document.
2025-12-13 21:42:33 +05:45
sankalp a2f3657269 feat(git): Introduce a robust utility for running git commands
This commit introduces a new `run_git_command` utility function within `devgen.utils` to centralize and standardize the execution of Git commands. This improves code reusability and error handling for Git-related operations.

*   **New Features**
    *   Add `run_git_command` to `devgen/utils.py` to provide a standardized, robust way to execute Git commands, capture output, and handle errors.
    *   Expose `run_git_command` in `__all__` for public use.
*   **Enhancements & Refinements**
    *   Refactor `get_git_staged_files` to utilize the new `run_git_command` utility, reducing code duplication and improving consistency.
2025-12-13 21:19:18 +05:45
sankalp c50f8a4b89 refactor(modules): Standardize git command execution
This commit refactors the way git commands are executed across the `changelog_generator` and `commit_generator` modules, leveraging a shared utility function and enhancing error handling.

*   **Enhancements & Refinements**
    *   Replaced the internal `_exec_git` method in both `ChangelogGenerator` and `CommitEngine` with the common `run_git_command` utility from `devgen.utils`.
    *   Standardized and improved error handling for git command failures by wrapping `run_git_command` calls in `try-except subprocess.CalledProcessError` blocks, providing more precise logging and raising `CommitEngineError` where applicable.
    *   Adjusted error handling for `git reset HEAD` operations to gracefully ignore potential failures, ensuring smoother execution for non-critical resets.
    *   Refined the `is_ahead_of_remote` check in `CommitEngine` to correctly handle scenarios with no upstream configured by using `check=False` for `run_git_command` and broadening error catch.
2025-12-13 21:19:18 +05:45
sankalp a488d5e4ac fix(root): Update PyPI information and correct documentation links
This commit clarifies the project's name on PyPI and ensures all external links in the README are accurate.

*   **Enhancements & Refinements**
    *   Added a note to the README clarifying the project's name on PyPI as `devgen-cli`.
    *   Refined the project description for improved readability by adjusting punctuation.
*   **Bug Fixes**
    *   Corrected the repository case in the license badge URL.
    *   Updated the PyPI project URL to `https://pypi.org/project/devgen0cli` for accuracy.
2025-12-13 21:19:18 +05:45
sankalp 48836797b9 build(packaging): Remove .egg-info build artifacts
This commit removes the entire `devgen_cli.egg-info` directory and its contents from the repository. This cleanup is part of streamlining the project's packaging and build process, as `.egg-info` directories typically contain transient metadata generated during package installation or building, and are generally not intended to be version controlled.

*   **Enhancements & Refinements**
    *   Deleted the `devgen_cli.egg-info` directory, which contained build-time metadata.
    *   Removed `PKG-INFO`, `SOURCES.txt`, `dependency_links.txt`, `entry_points.txt`, `requires.txt`, and `top_level.txt`.
    *   Improved project hygiene by no longer tracking generated packaging metadata in version control.
    *   Streamlined project build artifacts, potentially reflecting a modernization of the packaging setup.
2025-12-12 19:11:51 +05:45
sankalp 09151decad feat(root): Prepare for new devgen_cli component
*   **New Features**
    *   Add `devgen_cli.egg-info/` to `.gitignore` to exclude build artifacts for the new `devgen_cli` component from version control.
2025-12-12 19:11:43 +05:45
sankalp 24dc7f57b4 build(packaging): Remove custom dependency link declarations
This commit updates the package metadata by removing declarations of custom dependency links and applies a minor file formatting correction.

*   **Enhancements & Refinements**
    *   Removed all declarations from `devgen_cli.egg-info/dependency_links.txt`, simplifying the package's metadata and indicating that no custom dependency links are used.
    *   Corrected the file format of `devgen_cli.egg-info/SOURCES.txt` by adding a missing newline at the end of the file, improving file integrity.
2025-12-12 19:09:24 +05:45
sankalp 449a955d15 refactor(commit_generator): Improve code readability and styling
Minor code cleanup to enhance readability and adhere to styling conventions.

*   **Enhancements & Refinements**:
    *   Wrapped long lines for better readability in logger configuration, console theme definition, commit message panel printing, and dry run output.
    *   Removed an unnecessary f-string from the commit generation status message.
2025-12-12 19:07:55 +05:45
sankalp 87ed8bf878 docs(root): Update PyPI badge URL
**Enhancements & Refinements**
*   Corrected the PyPI badge URL in `README.md` from `devgen` to `devgen-cli` to reflect the accurate package name.
2025-12-12 19:04:42 +05:45
sankalp 6e4b177fe3 feat(commit-engine): Enhance console output with rich library
The commit engine now leverages the `rich` library to provide a more visually appealing and informative console experience during various stages of the commit generation and execution process.

*   **New Features**
    *   Integrate the `rich` library for enhanced and structured console output.
    *   Introduce a custom `rich` theme for consistent styling of info, warning, and danger messages.

*   **Enhancements & Refinements**
    *   Display visual feedback with `rich` status spinners during AI message generation and remote push operations.
    *   Present generated commit messages in a formatted `rich` Panel with Markdown rendering for improved readability.
    *   Provide clearer, color-coded `rich` console messages for staging files, dry run completion, overall process completion, and identifying failed groups.
    *   Propagate the `debug` flag to the logger to control console output during debugging sessions, aligning logger and console behavior.
2025-12-12 18:57:46 +05:45
sankalp 8db30f14bc fix(devgen-cli): Update devgen-cli to v0.2.1
- **Bug Fixes**
    - `devgen-cli`: Updated the internal CLI tool to version `0.2.1`, addressing minor issues or improving stability.
2025-12-12 18:56:37 +05:45
sankalp d502849daf feat(commit-engine): Enhance console output with rich library
The commit engine now leverages the `rich` library to provide a more visually appealing and informative console experience during various stages of the commit generation and execution process.

*   **New Features**
    *   Integrate the `rich` library for enhanced and structured console output.
    *   Introduce a custom `rich` theme for consistent styling of info, warning, and danger messages.

*   **Enhancements & Refinements**
    *   Display visual feedback with `rich` status spinners during AI message generation and remote push operations.
    *   Present generated commit messages in a formatted `rich` Panel with Markdown rendering for improved readability.
    *   Provide clearer, color-coded `rich` console messages for staging files, dry run completion, overall process completion, and identifying failed groups.
    *   Propagate the `debug` flag to the logger to control console output during debugging sessions, aligning logger and console behavior.
2025-12-12 18:56:26 +05:45
sankalp f3c68aaf29 ci(.github/workflows): Update GitHub Actions and Python version
This update refines the `publish.yml` workflow by bumping the versions of several GitHub Actions and adjusting the Python version used for publishing.

*   **Enhancements & Refinements**
    *   Updated `actions/checkout` from v4 to v6.
    *   Updated `actions/setup-python` from v4 to v6.
    *   Changed the target Python version for the workflow from 3.12 to 3.10.
2025-12-12 18:56:25 +05:45
sankalp 7f6b247b40 feat(cli): Introduce AI-powered developer toolkit
Release v0.2.1 of DevGen CLI, a comprehensive toolkit automating common Git and project management tasks through AI. This initial packaging defines the project's metadata and a wide array of developer-centric features.

*   **New Features**
    *   **AI-Powered Commit Messages:** Automatically generate semantic commit messages based on staged changes, adhering to Conventional Commits format.
    *   **Changelog Generation:** Create detailed changelogs from Git history, with categories for BREAKING CHANGES, Features, Bug Fixes, Documentation, and Other Changes.
    *   **Release Note Generation:** Produce structured release notes from Git history, ready for publication.
    *   **Gitignore File Generation:** Generate `.gitignore` files by selecting from GitHub's official templates, supporting interactive selection, multiple templates, and offline mode.
    *   **License File Generation:** Interactively generate various open-source license files (e.g., MIT, Apache-2.0, GPL-3.0), customizable with author name and year.
    *   **Multi-Provider AI Support:** Seamless integration with leading AI providers including Gemini, OpenAI, Anthropic, HuggingFace, and OpenRouter.

*   **Enhancements & Refinements**
    *   **Unified CLI:** Provides a single command-line interface for all development automation tools.
    *   **Interactive Configuration:** Simple setup process for AI providers, models, and API keys via `devgen setup config`, stored persistently in `~/.devgen.yaml`.
    *   **Flexible Commit Workflow:** Offers options for dry runs, direct commits, automatic pushes, and force rebuilding of AI-generated commit messages.
    *   **Commit Caching:** Includes commands to list and clear cached commit messages for improved workflow.
    *   **Detailed Command Reference:** Comprehensive documentation for all `devgen` commands and their specific options.
    *   **Project Metadata:** Updated `PKG-INFO` with project summary, homepage, author details, license, keywords, classifiers, and Python requirements.
    *   **Dependency Management:** Specifies required Python packages including `anthropic`, `google-generativeai`, `openai`, `typer`, and `rich`.
    *   **Python 3.10+ Compatibility:** Ensures support for modern Python environments.
2025-12-12 18:56:25 +05:45
38 changed files with 2638 additions and 1619 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# Tell GitHub Linguist to skip *.tpl files entirely so they don't
# inflate language stats (Linguist otherwise maps .tpl -> Go).
*.tpl linguist-vendored
+16
View File
@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
target-branch: "main"
rebase-strategy: "auto"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
target-branch: "main"
rebase-strategy: "auto"
open-pull-requests-limit: 10
+66
View File
@@ -0,0 +1,66 @@
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint & format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Sync dependencies
run: uv sync --all-extras --dev
- name: ruff check
run: uv run ruff check .
- name: ruff format --check
run: uv run ruff format --check .
smoke:
name: Import & CLI smoke (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Sync dependencies
run: uv sync --all-extras --dev
- name: Import devgen
run: uv run python -c "import devgen, devgen.ai, devgen.utils, devgen.cli.main; print('imports OK')"
- name: CLI --help
run: uv run devgen --help
- name: CLI commit --help
run: uv run devgen commit --help
+49
View File
@@ -0,0 +1,49 @@
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "17 6 * * 1"
permissions:
actions: read
contents: read
security-events: write
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [python]
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Sync dependencies
run: uv sync --all-extras --dev
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
@@ -0,0 +1,23 @@
name: Dependabot Auto-Merge
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: write
pull-requests: write
jobs:
dependabot:
runs-on: ubuntu-latest
# Check that it's actually dependabot
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Merge PR from Dependabot
uses: fastify/github-action-merge-dependabot@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
target: minor
use-github-auto-merge: true
merge-method: squash
+26
View File
@@ -0,0 +1,26 @@
name: Dependency Review
on:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
name: Audit dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Dependency Review
uses: actions/dependency-review-action@v5
with:
fail-on-severity: high
fail-on-scopes: runtime
license-check: true
# Project is GPL-3.0-or-later; flag licenses that would taint it.
deny-licenses: AGPL-1.0-only, AGPL-1.0-or-later, AGPL-3.0-only, AGPL-3.0-or-later, SSPL-1.0, BUSL-1.1
comment-summary-in-pr: always
+2 -2
View File
@@ -6,12 +6,12 @@ jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
+3 -3
View File
@@ -12,11 +12,11 @@ jobs:
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.12'
python-version: '3.10'
cache: 'pip'
- name: Install dependencies
run: |
+1
View File
@@ -47,3 +47,4 @@ logs/
devgen.egg-info/
build/
devgen_utils.egg-info/
devgen_cli.egg-info/
+108 -220
View File
@@ -2,261 +2,149 @@
<div align="center">
<img src="https://img.shields.io/pypi/v/devgen?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI">
<img src="https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white" alt="Python">
<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>
**AI-Powered Git & Project Workflows in One CLI**
Stop wasting time on repetitive tasks. DevGen automates commits, changelogs, `.gitignore`, and license files using AI — from cloud providers or a local Ollama model.
> PyPI didn't allow the original name, so you'll find it as **devgen-cli** on PyPI
> <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>
> <img src="https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white" alt="Python">
> <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>
> <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>
> <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>
</div>
**DevGen** is an AI-powered developer toolkit that automates common Git and project management tasks. Generate semantic commit messages, changelogs, release notes, `.gitignore` files, and licenses—all from a single unified CLI.
## Why DevGen
## ✨ Features
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.
- 🚀 **AI-Powered Commit Messages** - Automatically generate semantic commit messages from your staged changes using AI
- 📝 **Changelog Generation** - Create changelogs from git history using conventional commits
- 🚀 **Release Notes** - Generate clean, emoji-enhanced release notes for your releases
- 🙈 **Gitignore Templates** - Generate `.gitignore` files from GitHub's official templates
- 📄 **License Generation** - Quickly generate open source licenses with interactive setup
- ⚙️ **Multi-Provider AI Support** - Works with Gemini, OpenAI, Anthropic, HuggingFace, and OpenRouter
## Features
## 📦 Installation
- **AI-Powered Commits** — Conventional Commit messages generated from your staged diff, grouped by directory, with optional emoji prefixes.
- **Multiple Providers** — Google Gemini, OpenAI, Anthropic, OpenRouter, HuggingFace, and local Ollama, all behind one CLI.
- **Smart Caching** — `.gitignore` and license templates are cached for offline use; AI responses are de-duplicated.
- **Conventional Changelogs** — `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, `chore`, `style`, and a `BREAKING CHANGES` section.
- **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, …).
- **Custom Templates** — Override the commit-message prompt via a `.tpl` file in your config.
- **Interactive Setup** — `devgen setup` walks you through provider + API key + options.
- **Undo Support** — `devgen commit undo` rolls back the last AI commit while keeping changes staged.
- **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.
## Supported AI Providers
| Provider | Notes |
| ----------------- | ---------------------------------------------------- |
| **Google Gemini** | Default-friendly, generous free tier |
| **OpenAI** | GPT-4o, GPT-4.1, o-series |
| **Anthropic** | Claude 3.5 / 3.7 / 4 |
| **OpenRouter** | Single key, many models |
| **HuggingFace** | Inference API |
| **Ollama** | Fully local, no API key, no data leaves your machine |
## Installation
```bash
# Stable release from PyPI
pip install devgen-cli
# Using pipx (recommended for isolated CLI)
# Recommended: isolated environment
pipx install devgen-cli
# Using uv (fast Python package manager)
# Or use uv for speed
uv tool install devgen-cli
# Or plain pip
pip install devgen-cli
# Shell completion (bash/zsh/fish)
devgen --install-completion
```
## 🚀 Quick Start
Requires **Python 3.10 or newer**.
### 1. Initial Setup
Configure your AI provider and API key:
## Quick Start
```bash
# 1. Configure a provider and API key
devgen setup config
```
This will create a configuration file at `~/.devgen.yaml` with your preferred AI provider, model, and API key.
# 2. Stage your work as usual
git add .
### 2. Generate Commit Messages
Stage your changes and let DevGen generate semantic commit messages:
```bash
# Dry run to preview commit messages
devgen commit run --dry-run
# Actually commit with AI-generated messages
# 3. Let DevGen write the commit message
devgen commit run
# Commit and automatically push
# 4. Or preview first, then commit
devgen commit run --dry-run
devgen commit run --push
# Force regeneration of commit messages
devgen commit run --force-rebuild
```
**How it works:**
## Commands
- DevGen detects all staged and unstaged changes
- Groups files by directory
- Generates semantic commit messages for each group using AI
- Follows [Conventional Commits](https://www.conventionalcommits.org/) format
| Command | Description |
| ------------------------------- | ----------------------------------------------------------------------------- |
| `devgen setup config` | Interactive provider / API key / options wizard |
| `devgen commit run` | Stage, generate message, commit (optionally `--push`, `--check`, `--dry-run`) |
| `devgen commit undo` | Undo the last AI commit, keep changes staged |
| `devgen changelog` | Generate a Conventional Commits changelog from recent history |
| `devgen release-notes` | Generate release notes for a version range |
| `devgen gitignore list` | List available GitHub `.gitignore` templates |
| `devgen gitignore add <name> …` | Add `.gitignore` entries to the current project |
| `devgen license list` | List available SPDX licenses |
| `devgen license add <spdx>` | Add a LICENSE file to the current project |
**Additional commit commands:**
Run `devgen <command> --help` for full options on any subcommand.
## Custom Templates
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.
Example minimal template:
```
{{ diff_text }}
Summarize the change above in one Conventional Commit line.
```
## Architecture
DevGen is built around a small set of composable components:
- **`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.
- **`GitOperator`** — all `subprocess` calls to `git` live here, behind a `GitError` exception.
- **`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.
- **`Section` enum** — single source of truth for changelog ordering and emoji, shared by `ChangelogGenerator` and `ReleaseNotesGenerator`.
The CLI entry point is `devgen.cli.main:app` (Typer), exposed as the `devgen` script.
## Development
```bash
# View cached commit messages
devgen commit list-cached
# Clear the commit cache
devgen commit clear-cache
# Check what files are staged
devgen commit validate
git clone https://github.com/S4NKALP/DevGen.git
cd DevGen
uv sync --all-extras --dev # install runtime + dev deps
uv run pre-commit run --all-files
uv run devgen --help
```
### 3. Generate Changelogs
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`).
Create changelogs from your git history:
## Contributing
```bash
# Generate changelog from last tag to HEAD
devgen changelog generate
Issues and PRs welcome. Please run `uv run pre-commit run --all-files` before opening a PR so CI stays green.
# Generate from a specific reference
devgen changelog generate --from v1.0.0
## License
# Output to a custom file
devgen changelog generate --output CHANGES.md
```
GPL-3.0-or-later. See [LICENSE](LICENSE).
The changelog follows Semantic Release conventions, categorizing commits into:
## Acknowledgments
- **BREAKING CHANGES**
- **Features**
- **Bug Fixes**
- **Documentation**
- **Other Changes**
Built on the shoulders of:
### 4. Generate Release Notes
- **Typer** & **Rich** — CLI and terminal UI
- **Questionary** — interactive prompts
- **Google Gemini**, **OpenAI**, **Anthropic**, **OpenRouter**, **HuggingFace**, and **Ollama** — AI providers
- **Ruff** — lint and format
Create release notes for your releases:
```bash
# Generate release notes
devgen release notes --version 1.4.0
# Custom output file
devgen release notes --version 1.4.0 --output RELEASE.md
# From a specific reference
devgen release notes --version 1.4.0 --from v1.3.0
```
### 5. Generate .gitignore Files
Create `.gitignore` files from GitHub templates:
```bash
# Interactive mode (search and select templates)
devgen gitignore generate
# Specify templates directly
devgen gitignore generate Python Node Docker
# List available templates
devgen gitignore list
# Use cached templates (offline mode)
devgen gitignore generate --offline Python
```
**Options:**
- `--append` / `--overwrite` - Append to or overwrite existing `.gitignore`
- `--output` - Specify output file path (default: `.gitignore`)
- `--offline` - Use only cached templates
### 6. Generate Licenses
Generate open source licenses interactively:
```bash
devgen license generate
```
This will:
1. Show available licenses (MIT, Apache-2.0, GPL-3.0, etc.)
2. Prompt for author name
3. Prompt for year (defaults to current year)
4. Generate the license file
**Available licenses:**
- MIT
- Apache-2.0
- GPL-2.0, GPL-3.0
- LGPL-2.1
- AGPL-3.0
- BSD-2-Clause, BSD-3-Clause
- MPL-2.0
- EPL-2.0
- BSL-1.0
- CC0-1.0
- Unlicense
## ⚙️ Configuration
Configuration is stored in `~/.devgen.yaml`. You can edit it manually or use the interactive setup:
```bash
devgen setup config
```
**Configuration options:**
- `provider` - AI provider: `gemini`, `openai`, `anthropic`, `huggingface`, or `openrouter`
- `model` - Model name (e.g., `gemini-2.5-flash`, `gpt-4`, `claude-3-opus`)
- `api_key` - Your API key
- `emoji` - Enable/disable emojis in commit messages (default: `true`)
## 📋 Command Reference
### Commit Commands
```bash
devgen commit run [--dry-run] [--push] [--debug] [--force-rebuild]
devgen commit validate
devgen commit list-cached
devgen commit clear-cache
```
### Changelog Commands
```bash
devgen changelog generate [--output FILE] [--from REF]
```
### Release Commands
```bash
devgen release notes [--version VERSION] [--output FILE] [--from REF]
```
### Gitignore Commands
```bash
devgen gitignore generate [TEMPLATES...] [--output FILE] [--append/--overwrite] [--offline]
devgen gitignore list [--cached]
```
### License Commands
```bash
devgen license generate [--output FILE]
```
### Setup Commands
```bash
devgen setup config
```
## 🎯 Use Cases
- **Automated Commit Workflow**: Let AI analyze your changes and generate meaningful commit messages
- **Release Preparation**: Automatically generate changelogs and release notes from git history
- **Project Setup**: Quickly add `.gitignore` and license files to new projects
- **CI/CD Integration**: Use in scripts to automate documentation generation
## 🔧 Requirements
- Python 3.10 or higher
- Git repository (for commit/changelog/release features)
- API key for your chosen AI provider
## 📝 License
This project is licensed under the GPL-3.0-or-later License - see the [LICENSE](LICENSE) file for details.
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## 📚 Links
- **Homepage**: [https://github.com/S4NKALP/devgen](https://github.com/S4NKALP/devgen)
- **PyPI**: [https://pypi.org/project/devgen](https://pypi.org/project/devgen)
## 🙏 Acknowledgments
- Uses GitHub's official [gitignore templates](https://github.com/github/gitignore)
- Follows [Conventional Commits](https://www.conventionalcommits.org/) specification
<div align="center">
Made with ❤️ by <a href="https://github.com/S4NKALP">Sankalp</a>
</div>
+11 -2
View File
@@ -21,8 +21,17 @@ def generate_with_ai(
provider_module = import_module(f"devgen.providers.{provider}")
class_name = "".join([x.capitalize() for x in provider.split("_")]) + "Provider"
provider_class = getattr(provider_module, class_name)
except (ModuleNotFoundError, AttributeError) as e:
raise ImportError(f"Provider `{provider}` not found or invalid: {e}") from e
except ModuleNotFoundError as e:
raise ImportError(
f"Provider `{provider}` is not a built-in module: {e}. "
f"Available providers: gemini, openai, anthropic, huggingface, "
f"openrouter, ollama."
) from e
except AttributeError as e:
raise ImportError(
f"Provider `{provider}` exists but does not expose a "
f"`{class_name}` class. This is a packaging bug."
) from e
provider_instance = provider_class()
return provider_instance.generate(prompt, api_key=api_key, model=model, **kwargs)
+55 -3
View File
@@ -1,5 +1,6 @@
from typing import Annotated
import questionary
import typer
from devgen.modules.commit_generator import run_commit_engine
@@ -50,12 +51,35 @@ def run_commit(
help="Force regeneration of commit messages.",
),
] = False,
check: Annotated[
bool,
typer.Option(
"--check",
help="Review/edit commit message before committing.",
),
] = False,
max_groups: Annotated[
int | None,
typer.Option(
"--max-groups",
help="Maximum number of commit groups. Lower this if you hit token-limit errors.",
),
] = None,
max_diff_size: Annotated[
int | None,
typer.Option(
"--max-diff-size",
help="Maximum diff size in chars per group (default 8000). "
"Lower this if you hit token-limit errors.",
),
] = None,
) -> None:
log_file = get_main_log_path()
logger = configure_logger("devgen.cli.commit", log_file)
logger = configure_logger("devgen.cli.commit", log_file, console=debug)
logger.info(f"Log file: {log_file}")
logger.info(
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}"
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}, "
f"check={check}, max_groups={max_groups}, max_diff_size={max_diff_size}"
)
run_commit_engine(
@@ -63,7 +87,10 @@ def run_commit(
push=push,
debug=debug,
force_rebuild=force_rebuild,
check=check,
logger=logger,
max_groups=max_groups,
max_diff_size=max_diff_size,
)
@@ -93,4 +120,29 @@ def validate() -> None:
for f in staged:
typer.echo(f"- {f}")
else:
typer.secho("[i] No staged files.", fg=typer.colors.RED)
typer.secho("[i] No staged files.", fg=typer.colors.YELLOW)
@app.command("undo")
def undo_commit() -> None:
"""Undoes the last commit but keeps changes staged."""
from devgen.utils import run_git_command
try:
# Check if there's at least one commit
run_git_command(["git", "rev-parse", "HEAD"])
except Exception:
typer.secho("No commits found to undo.", fg=typer.colors.RED)
return
if questionary.confirm(
"Are you sure you want to undo the last commit? (Changes will remain staged)",
default=False,
).ask():
try:
run_git_command(["git", "reset", "--soft", "HEAD~1"])
typer.secho(
"Last commit undone. Changes are still staged.", fg=typer.colors.GREEN
)
except Exception as e:
typer.secho(f"Failed to undo commit: {e}", fg=typer.colors.RED)
+61 -2
View File
@@ -48,6 +48,12 @@ def edit_config(
if not key:
choices = list(config.keys())
if not choices:
typer.secho(
"No configuration keys found. Run `devgen setup config` first.",
fg=typer.colors.YELLOW,
)
raise typer.Exit()
key = questionary.select(
"Select key to update:", choices=choices, style=style
).ask()
@@ -62,7 +68,14 @@ def edit_config(
if key == "provider":
value = questionary.select(
"Select AI Provider:",
choices=["gemini", "openai", "huggingface", "openrouter", "anthropic"],
choices=[
"gemini",
"openai",
"huggingface",
"openrouter",
"anthropic",
"ollama",
],
default=str(current_val) if current_val else "gemini",
style=style,
).ask()
@@ -117,7 +130,14 @@ def set_config() -> None:
# Questions
provider = questionary.select(
"Select AI Provider:",
choices=["gemini", "openai", "huggingface", "openrouter", "anthropic"],
choices=[
"gemini",
"openai",
"huggingface",
"openrouter",
"anthropic",
"ollama",
],
default=config.get("provider", "gemini"),
style=style,
).ask()
@@ -151,12 +171,24 @@ def set_config() -> None:
raise typer.Exit(code=130)
emoji = emoji_choice == "Yes"
ollama_host = config.get("ollama_host", "http://localhost:11434")
if provider == "ollama":
ollama_host_input = questionary.text(
"Ollama server URL:",
default=ollama_host,
style=style,
).ask()
if ollama_host_input is None:
raise typer.Exit(code=130)
ollama_host = ollama_host_input.strip() or ollama_host
# Save Config
new_config = {
"provider": provider,
"model": model,
"api_key": api_key,
"emoji": emoji,
"ollama_host": ollama_host,
}
# Merge with existing config to preserve other keys?
@@ -167,3 +199,30 @@ def set_config() -> None:
_save_config(config)
typer.secho("\nConfiguration saved.", fg=typer.colors.GREEN)
typer.echo(yaml.dump(new_config, default_flow_style=False))
@app.command("info")
def config_info() -> None:
"""Show information about configuration options and templates."""
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Custom Template Variables", box=None)
table.add_column("Variable", style="cyan")
table.add_column("Description", style="white")
table.add_row("{{ group_name }}", "The folder name being committed (or 'root').")
table.add_row("{{ diff_text }}", "The git diff of the changes.")
table.add_row("{{ context }}", "Project context (manifest files content).")
console.print(table)
console.print(
"\n[bold]Tip:[/bold] If you hardcode emojis in your template, the AI will likely include them regardless of the 'emoji' setting.",
style="yellow",
)
console.print("\n[bold]Example Template:[/bold]")
console.print(
"custom_template: |\n [type]: [desc]\n \n Diff: {{ diff_text }}\n",
style="dim",
)
+12 -4
View File
@@ -15,7 +15,7 @@ from devgen.cli.setup import app as setup_app
app = typer.Typer(
name="devgen",
help="devgen-py: AI-Powered Git Commit & Release Automation.",
add_completion=False,
add_completion=True,
no_args_is_help=True,
rich_markup_mode="markdown",
)
@@ -48,13 +48,21 @@ def _version_callback(value: bool) -> None:
typer.echo(f"devgen version: {version}")
else:
typer.secho(
"Error: Version not found in pyproject.toml",
"Could not determine version: no `project.version` or "
"`tool.poetry.version` found in pyproject.toml.",
fg=typer.colors.RED,
err=True,
)
except (FileNotFoundError, KeyError, Exception) as e:
except FileNotFoundError:
typer.secho(
f"Error: Could not determine version: {e}",
f"pyproject.toml not found at {pyproject_path}. "
"The installation may be corrupt.",
fg=typer.colors.RED,
err=True,
)
except toml.TomlDecodeError as e:
typer.secho(
f"pyproject.toml is not valid TOML: {e}",
fg=typer.colors.RED,
err=True,
)
+28 -6
View File
@@ -33,7 +33,14 @@ def setup_config() -> None:
# Questions
provider = questionary.select(
"Select AI Provider:",
choices=["gemini", "openai", "huggingface", "openrouter", "anthropic"],
choices=[
"gemini",
"openai",
"huggingface",
"openrouter",
"anthropic",
"ollama",
],
default=current_config.get("provider", "gemini"),
style=style,
).ask()
@@ -67,12 +74,27 @@ def setup_config() -> None:
raise typer.Exit(code=130)
emoji = emoji_choice == "Yes"
# Save Config
ollama_host = current_config.get("ollama_host", "http://localhost:11434")
if provider == "ollama":
ollama_host_input = questionary.text(
"Ollama server URL:",
default=ollama_host,
style=style,
).ask()
if ollama_host_input is None:
raise typer.Exit(code=130)
ollama_host = ollama_host_input.strip() or ollama_host
# Save Config — merge with existing to preserve extra keys
new_config = {
"provider": provider,
"model": model,
"api_key": api_key,
"emoji": emoji,
**current_config,
**{
"provider": provider,
"model": model,
"api_key": api_key,
"emoji": emoji,
"ollama_host": ollama_host,
},
}
try:
+128 -159
View File
@@ -5,186 +5,155 @@ from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from devgen.utils import configure_logger
from devgen.modules.changelog_sections import (
DEFAULT_STYLES,
Section,
section_for_type,
)
from devgen.utils import configure_logger, run_git_command
class ChangelogGenerator:
"""Generates a changelog from git history using Semantic Release style."""
#: Conventional commit header: ``type(scope)!: subject``
_CC_PATTERN = re.compile(r"^(\w+)(?:\(([^)]+)\))?(!?):\s+(.*)")
def __init__(self, logger=None):
self.logger = logger or configure_logger("devgen.changelog")
def _exec_git(self, args: List[str]) -> str:
"""Executes a git command."""
try:
res = subprocess.run(
args,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=True,
)
return res.stdout.strip()
except subprocess.CalledProcessError as e:
self.logger.error(f"Git command failed: {e}")
raise RuntimeError(f"Git command failed: {e}")
# ------------------------------------------------------------------ commits
def get_commits(self, from_ref: str = "", to_ref: str = "HEAD") -> List[str]:
"""Fetches commit messages in the specified range."""
range_spec = f"{from_ref}..{to_ref}" if from_ref else to_ref
# Format: hash|author|date|subject|body
fmt = "%H|%an|%ad|%s|%b"
cmd = ["git", "log", f"--format={fmt}", "--date=short", range_spec]
"""Fetch commit lines in the requested range."""
if from_ref:
range_spec = f"{from_ref}..{to_ref}"
cmd = self._log_cmd(range_spec)
else:
cmd = self._resolve_range(to_ref)
try:
output = run_git_command(cmd)
# Use %x00 as separator to avoid splitting on newlines in commit bodies
# Then filter out empty strings
return [c for c in output.split("\0") if c.strip()]
except subprocess.CalledProcessError as e:
self.logger.error(f"Git command failed: {e}")
raise RuntimeError(f"Git command failed: {e}") from e
if not from_ref:
# If no start ref, try to find the last tag
try:
last_tag = self._exec_git(["git", "describe", "--tags", "--abbrev=0"])
cmd = [
"git",
"log",
f"--format={fmt}",
"--date=short",
f"{last_tag}..HEAD",
]
self.logger.info(f"Generating changelog from last tag: {last_tag}")
except RuntimeError:
self.logger.info("No tags found, generating for all commits.")
cmd = ["git", "log", f"--format={fmt}", "--date=short"]
return self._exec_git(cmd).split("\n")
def parse_commits(self, raw_commits: List[str]) -> Dict[str, List[Dict]]:
"""Parses raw commit strings into structured data."""
groups = defaultdict(list)
# Conventional Commit Regex: type(scope)!: subject
cc_pattern = re.compile(r"^(\w+)(?:\(([^)]+)\))?(!?):\s+(.*)")
for line in raw_commits:
if not line.strip():
continue
parts = line.split("|", 4)
if len(parts) < 4:
continue
commit_hash, author, date, subject, body = (
parts[0],
parts[1],
parts[2],
parts[3],
parts[4] if len(parts) > 4 else "",
)
match = cc_pattern.match(subject)
if match:
c_type, c_scope, breaking, c_subject = match.groups()
entry = {
"hash": commit_hash,
"author": author,
"date": date,
"scope": c_scope,
"subject": c_subject,
"body": body,
"breaking": bool(breaking),
}
if breaking:
groups["BREAKING CHANGES"].append(entry)
if c_type in ["feat", "feature"]:
groups["Features"].append(entry)
elif c_type in ["fix", "bug"]:
groups["Bug Fixes"].append(entry)
elif c_type in ["docs"]:
groups["Documentation"].append(entry)
elif c_type in [
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
]:
groups["Other Changes"].append(entry)
else:
groups["Other Changes"].append(entry)
else:
# Non-conventional commits
groups["Other Changes"].append(
{
"hash": commit_hash,
"author": author,
"date": date,
"scope": None,
"subject": subject,
"body": body,
"breaking": False,
}
)
return groups
def generate_markdown(
self, groups: Dict[str, List[Dict]], version: str = "Unreleased"
) -> str:
"""Generates markdown changelog from parsed commits."""
date_str = datetime.now().strftime("%Y-%m-%d")
md = [f"# {version} ({date_str})\n"]
# Order: Breaking, Features, Fixes, Docs, Others
order = [
"BREAKING CHANGES",
"Features",
"Bug Fixes",
"Documentation",
"Other Changes",
def _log_cmd(self, range_spec: str) -> List[str]:
# Format: hash|author|date|subject|body (separated by %x00 to avoid
# splitting on newlines within commit bodies)
return [
"git",
"log",
"--format=%H|%an|%ad|%s|%b%x00",
"--date=short",
range_spec,
]
emoji_map = {
"BREAKING CHANGES": "💥 BREAKING CHANGES",
"Features": "✨ Features",
"Bug Fixes": "🐛 Bug Fixes",
"Documentation": "📚 Documentation",
"Other Changes": "🔨 Other Changes",
def _resolve_range(self, to_ref: str) -> List[str]:
try:
last_tag = run_git_command(["git", "describe", "--tags", "--abbrev=0"])
self.logger.info(f"Generating changelog from last tag: {last_tag}")
return self._log_cmd(f"{last_tag}..HEAD")
except (RuntimeError, subprocess.CalledProcessError):
self.logger.info("No tags found, generating for all commits.")
return ["git", "log", "--format=%H|%an|%ad|%s|%b%x00", "--date=short"]
# ----------------------------------------------------------------- parsing
def parse_commits(self, raw_commits: List[str]) -> Dict[Section, List[Dict]]:
"""Group raw commit lines into :class:`Section` buckets."""
groups: Dict[Section, List[Dict]] = defaultdict(list)
for line in raw_commits:
entry = self._parse_line(line)
if entry is None:
continue
section = section_for_type(entry["type"])
entry["section"] = section
if entry["breaking"]:
groups[Section.BREAKING].append(entry)
groups[section].append(entry)
return dict(groups)
def _parse_line(self, line: str) -> Dict | None:
if not line.strip():
return None
parts = line.split("|", 4)
if len(parts) < 4:
return None
commit_hash, author, date, subject = parts[:4]
body = parts[4] if len(parts) > 4 else ""
match = self._CC_PATTERN.match(subject)
if match:
c_type, c_scope, breaking, c_subject = match.groups()
return {
"type": c_type,
"hash": commit_hash,
"author": author,
"date": date,
"scope": c_scope,
"subject": c_subject,
"body": body,
"breaking": bool(breaking),
}
return {
"type": "",
"hash": commit_hash,
"author": author,
"date": date,
"scope": None,
"subject": subject,
"body": body,
"breaking": False,
}
for section in order:
commits = groups.get(section)
if commits:
header = emoji_map.get(section, section)
md.append(f"## {header}\n")
for c in commits:
scope = f"**{c['scope']}**: " if c["scope"] else ""
md.append(f"- {scope}{c['subject']} ({c['hash'][:7]})")
md.append("")
# ------------------------------------------------------------------ output
return "\n".join(md)
def generate_markdown(
self,
groups: Dict[Section, List[Dict]],
version: str = "Unreleased",
) -> str:
"""Render groups as a markdown changelog block."""
date_str = datetime.now().strftime("%Y-%m-%d")
out = [f"## {version} ({date_str})\n"]
for section in Section.ordered():
commits = groups.get(section)
if not commits:
continue
style = DEFAULT_STYLES[section]
out.append(f"## {style.emoji} {style.heading}\n")
for c in commits:
scope = f"**{c['scope']}**: " if c.get("scope") else ""
out.append(f"- {scope}{c['subject']} ({c['hash'][:7]})")
out.append("")
return "\n".join(out)
def run(self, output_file: Optional[str] = "CHANGELOG.md", from_ref: str = ""):
"""Main execution method."""
"""Fetch → parse → write (or print) the changelog."""
raw_commits = self.get_commits(from_ref)
if not raw_commits or not raw_commits[0]:
self.logger.warning("No commits found.")
return
groups = self.parse_commits(raw_commits)
md_content = self.generate_markdown(groups)
parsed = self.parse_commits(raw_commits)
md_content = self.generate_markdown(parsed)
if output_file:
path = Path(output_file)
# If file exists, prepend? For now, just overwrite or append logic could be complex.
# Let's implement prepend logic if file exists.
if path.exists():
old_content = path.read_text(encoding="utf-8")
new_content = old_content + "\n\n" + md_content
path.write_text(new_content, encoding="utf-8")
else:
path.write_text(md_content, encoding="utf-8")
self.logger.info(f"Changelog written to {output_file}")
print(f" Changelog updated: {output_file}")
else:
if not output_file:
print(md_content)
return
path = Path(output_file)
if path.exists():
old = path.read_text(encoding="utf-8")
if old.strip().startswith("# CHANGELOG"):
header, _, rest = old.partition("\n")
new_content = f"{header}\n\n{md_content}\n{rest}"
else:
new_content = f"# CHANGELOG\n\n{md_content}\n\n{old}"
else:
new_content = f"# CHANGELOG\n\n{md_content}"
path.write_text(new_content, encoding="utf-8")
self.logger.info(f"Changelog written to {output_file}")
print(f" Changelog updated: {output_file}")
+79
View File
@@ -0,0 +1,79 @@
"""Shared section definitions for changelog and release-note generators.
Both generators render the same set of Conventional Commit groups; this
module is the single source of truth for the section name, the emoji,
and the display order.
"""
from dataclasses import dataclass
from enum import Enum
from typing import Mapping
class Section(str, Enum):
"""Sections in the order they should be rendered."""
BREAKING = "BREAKING CHANGES"
FEATURES = "Features"
BUG_FIXES = "Bug Fixes"
PERFORMANCE = "Performance"
DOCUMENTATION = "Documentation"
REFACTOR = "Refactor"
TESTS = "Tests"
STYLE = "Style"
CHORE = "Chore"
OTHER = "Other Changes"
@classmethod
def ordered(cls) -> tuple["Section", ...]:
return tuple(cls)
@dataclass(frozen=True)
class SectionStyle:
"""Visual style for a section (emoji + name for the heading)."""
emoji: str
heading: str
# Default style for each section. Generators can override per output
# format (changelog vs. release notes).
DEFAULT_STYLES: Mapping[Section, SectionStyle] = {
Section.BREAKING: SectionStyle("💥", "BREAKING CHANGES"),
Section.FEATURES: SectionStyle("✨", "Features"),
Section.BUG_FIXES: SectionStyle("🐛", "Bug Fixes"),
Section.PERFORMANCE: SectionStyle("⚡", "Performance"),
Section.DOCUMENTATION: SectionStyle("📚", "Documentation"),
Section.REFACTOR: SectionStyle("♻️", "Refactor"),
Section.TESTS: SectionStyle("✅", "Tests"),
Section.STYLE: SectionStyle("💄", "Style"),
Section.CHORE: SectionStyle("🔧", "Chore"),
Section.OTHER: SectionStyle("🧹", "Other Changes"),
}
# Conventional commit type → Section mapping. Long-form aliases ("feature",
# "bug") are also accepted.
TYPE_TO_SECTION: Mapping[str, Section] = {
"feat": Section.FEATURES,
"feature": Section.FEATURES,
"fix": Section.BUG_FIXES,
"bug": Section.BUG_FIXES,
"perf": Section.PERFORMANCE,
"docs": Section.DOCUMENTATION,
"refactor": Section.REFACTOR,
"test": Section.TESTS,
"style": Section.STYLE,
"build": Section.CHORE,
"ci": Section.CHORE,
"chore": Section.CHORE,
}
def section_for_type(commit_type: str) -> Section:
"""Return the Section for a Conventional Commit ``type``.
Unknown types fall back to :attr:`Section.OTHER`.
"""
return TYPE_TO_SECTION.get(commit_type.lower(), Section.OTHER)
+319 -185
View File
@@ -1,30 +1,138 @@
import subprocess
from collections import defaultdict
"""Orchestrates the AI-powered commit flow.
The :class:`CommitEngine` is a thin coordinator. The real work lives in:
* :class:`devgen.modules.git_ops.GitOperator` — all git side-effects
* :class:`devgen.modules.diff_builder.FileGrouper` — files → commit groups
* :class:`devgen.modules.diff_builder.DiffBuilder` — staged diff + truncation
* :class:`devgen.modules.diff_builder.ManifestInspector` — project context
* :class:`devgen.ai.generate_with_ai` — provider-agnostic AI call
"""
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
import questionary
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.theme import Theme
from devgen.ai import generate_with_ai
from devgen.modules.diff_builder import (
DEFAULT_MAX_DIFF_SIZE,
DiffBuilder,
FileGrouper,
ManifestInspector,
)
from devgen.modules.git_ops import GitOperator
from devgen.utils import (
configure_logger,
extract_commit_messages,
get_commit_dry_run_path,
is_file_recent,
load_template_env,
is_token_limit_error,
format_token_limit_error,
load_template,
render_template,
sanitize_ai_commit_message,
)
class CommitEngineError(Exception):
"""Exception raised for errors in the commit engine."""
"""Raised when the commit flow cannot proceed."""
pass
class CacheManager:
"""Manages the dry-run cache file at ``~/.cache/devgen/commit_dry_run.md``."""
def __init__(self, path: Path, max_age_minutes: int = 120) -> None:
self.path = path
self.max_age_minutes = max_age_minutes
def init_dry_run(self) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S (%Z)")
with self.path.open("w", encoding="utf-8") as f:
f.write(f"# Dry Run: Commit Messages\n_Generated: {ts}_\n\n")
def load(self, *, dry_run: bool, force_rebuild: bool) -> Dict[str, str]:
if dry_run:
self.init_dry_run()
return {}
if not force_rebuild and is_file_recent(self.path, self.max_age_minutes):
return extract_commit_messages(self.path)
return {}
def append(self, group: str, message: str) -> None:
with self.path.open("a", encoding="utf-8") as f:
f.write(f"## Group: `{group}`\n\n```md\n{message}\n```\n\n---\n\n")
class CommitMessageBuilder:
"""Renders the prompt and calls the AI provider."""
def __init__(
self,
template_str: str,
config: Dict[str, Any],
provider: str,
model: str,
debug: bool = False,
) -> None:
self.template_str = template_str
self.config = config
self.provider = provider
self.model = model
self.debug = debug
def build_message(
self,
group: str,
diff: str,
manifest_context: str,
extra_kwargs: Dict[str, Any],
) -> str:
custom_template = self.config.get("custom_template")
use_emoji = self.config.get("emoji", True)
ollama_host = self.config.get("ollama_host")
template = custom_template or self.template_str
prompt = render_template(
template,
group_name=group,
diff_text=diff,
use_emoji=use_emoji,
context=manifest_context,
).strip()
provider = (
extra_kwargs.get("provider") or self.config.get("provider") or self.provider
)
model = extra_kwargs.get("model") or self.config.get("model") or self.model
api_key = extra_kwargs.get("api_key") or self.config.get("api_key")
call_kwargs = dict(extra_kwargs)
if ollama_host and provider == "ollama":
call_kwargs.setdefault("ollama_host", ollama_host)
return generate_with_ai(
prompt,
provider=provider,
model=model,
api_key=api_key,
debug=self.debug,
**call_kwargs,
)
class CommitEngine:
"""
Engine for generating AI-powered commit messages.
Handles detection of changes, grouping, AI generation, and git operations.
"""Coordinates the AI-powered commit flow.
The constructor signature is stable (used by ``cli/commit.py``) but
most logic now lives in composed objects. The public ``execute()``
method remains the single entry point.
"""
def __init__(
@@ -33,225 +141,251 @@ class CommitEngine:
push: bool = False,
debug: bool = False,
force_rebuild: bool = False,
check: bool = False,
provider: str = "gemini",
model: str = "gemini-2.5-flash",
logger: Any | None = None,
**kwargs,
):
max_groups: int | None = None,
max_diff_size: int | None = None,
**kwargs: Any,
) -> None:
self.dry_run = dry_run
self.push = push
self.debug = debug
self.force_rebuild = force_rebuild
self.check = check
self.provider = provider
self.model = model
self.logger = logger or configure_logger(
"devgen.commit", Path.home() / ".cache" / "devgen" / "commit.log"
"devgen.commit",
Path.home() / ".cache" / "devgen" / "commit.log",
console=debug,
)
self.kwargs = kwargs
self.dry_run_path = get_commit_dry_run_path()
self.template_env = load_template_env("commit")
# Load config from ~/.devgen.yaml
# CLI flags win over config; config wins over default.
from devgen.utils import load_config
self.config = load_config()
def _exec_git(self, args: List[str], allow_error: bool = False) -> str:
"""Executes a git command."""
try:
res = subprocess.run(
args,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=not allow_error,
self.console = Console(
theme=Theme(
{"info": "dim cyan", "warning": "magenta", "danger": "bold red"}
)
return res.stdout.strip()
except subprocess.CalledProcessError as e:
msg = f"Git command failed: {' '.join(e.cmd)}\nError: {e.stderr.strip()}"
self.logger.error(msg)
raise CommitEngineError(msg) from e
def detect_changes(self) -> List[str]:
"""Detects changed, deleted, or untracked files."""
out = self._exec_git(
[
"git",
"ls-files",
"--deleted",
"--modified",
"--others",
"--exclude-standard",
]
)
return [f.strip() for f in out.split("\n") if f.strip()]
def group_files(self, files: List[str]) -> Dict[str, List[str]]:
"""Groups files by their parent directory."""
groups = defaultdict(list)
for f in files:
parent = str(Path(f).parent)
key = "root" if parent == "." else parent
groups[key].append(f)
return groups
def generate_diff(self, files: List[str]) -> str:
"""Generates diff for specific files."""
return self._exec_git(["git", "--no-pager", "diff", "--staged", "--", *files])
def _init_dry_run(self):
"""Initializes the dry-run file."""
self.dry_run_path.parent.mkdir(parents=True, exist_ok=True)
with self.dry_run_path.open("w", encoding="utf-8") as f:
ts = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S (%Z)")
f.write(f"# Dry Run: Commit Messages\n_Generated: {ts}_\n\n")
def _log_dry_run(self, group: str, msg: str):
"""Appends a dry-run entry."""
with self.dry_run_path.open("a", encoding="utf-8") as f:
f.write(f"## Group: `{group}`\n\n```md\n{msg}\n```\n\n---\n\n")
def stage_files(self, files: List[str]):
"""Stages files in git."""
if not files:
return
self.logger.info(f"Staging: {files}")
self._exec_git(["git", "add", *files])
def commit_staged(self, msg: str):
"""Commits staged changes."""
self.logger.info(f"Committing:\n{msg}")
self._exec_git(["git", "commit", "-m", msg])
def push_commits(self):
"""Pushes commits to remote."""
self.logger.info("Pushing to remote...")
self._exec_git(["git", "push"])
self.logger.info("Push successful.")
def generate_message(self, group: str, diff: str, cache: Dict[str, str]) -> str:
"""Generates a commit message using AI or cache."""
if not self.force_rebuild and group in cache:
self.logger.info(f"Using cached message for {group}")
return cache[group]
# Get settings from config or kwargs
provider = (
self.kwargs.get("provider") or self.config.get("provider") or self.provider
self.git = GitOperator(logger=self.logger)
self.grouper = FileGrouper(
max_groups=(
max_groups
if max_groups is not None
else self.config.get("max_groups", 5)
)
)
model = self.kwargs.get("model") or self.config.get("model") or self.model
api_key = self.kwargs.get("api_key") or self.config.get("api_key")
use_emoji = self.config.get("emoji", True)
template = self.template_env.get_template("commit_message.j2")
prompt = template.render(group_name=group, diff_text=diff, use_emoji=use_emoji)
raw = generate_with_ai(
prompt,
provider=provider,
model=model,
api_key=api_key,
self.diff_builder = DiffBuilder(
max_size=(
max_diff_size
if max_diff_size is not None
else self.config.get("max_diff_size", DEFAULT_MAX_DIFF_SIZE)
),
logger=self.logger,
)
self.template_str = load_template("commit", "commit_message.tpl")
self.cache = CacheManager(get_commit_dry_run_path())
self.message_builder = CommitMessageBuilder(
self.template_str,
self.config,
provider=self.provider,
model=self.model,
debug=self.debug,
**self.kwargs,
)
return sanitize_ai_commit_message(raw)
def is_ahead_of_remote(self) -> bool:
"""Checks if local branch has unpushed commits."""
try:
self._exec_git(["git", "fetch", "origin"])
count = self._exec_git(
["git", "rev-list", "--count", "@{u}..HEAD"], allow_error=True
)
if count and int(count) > 0:
return True
except CommitEngineError:
# Maybe no upstream
return bool(self._exec_git(["git", "rev-parse", "HEAD"], allow_error=True))
return False
# ------------------------------------------------------------------ public
def load_cache(self) -> Dict[str, str]:
"""Loads dry-run cache."""
if self.dry_run:
self._init_dry_run()
return {}
if not self.force_rebuild and is_file_recent(self.dry_run_path):
self.logger.info(f"Loading cache from {self.dry_run_path}")
return extract_commit_messages(self.dry_run_path)
return {}
def process_group(
self, group: str, files: List[str], cache: Dict[str, str]
) -> bool:
"""Processes a single file group."""
self.stage_files(files)
diff = self.generate_diff(files)
if not diff.strip():
self.logger.info(f"Skipping empty diff for {group}")
self._exec_git(["git", "reset", "HEAD", "--", *files])
return True
msg = self.generate_message(group, diff, cache)
if not msg:
self.logger.error(f"Empty message for {group}")
return False
if self.dry_run:
self._log_dry_run(group, msg)
self._exec_git(["git", "reset", "HEAD", "--", *files])
else:
self.commit_staged(msg)
return True
def execute(self):
"""Main execution method."""
files = self.detect_changes()
ahead = self.is_ahead_of_remote()
def execute(self) -> None:
files = self.git.detect_changed()
# Only consult the remote state if the user wants to push —
# avoids a network round-trip on every run.
ahead = self.git.is_ahead_of_remote(fetch=self.push) if self.push else False
if not files and not ahead:
self.logger.info("Nothing to commit or push.")
return
failed = []
failed: list[str] = []
if files:
groups = self.group_files(files)
cache = self.load_cache()
for group, group_files in groups.items():
try:
if not self.process_group(group, group_files, cache):
failed.append(group)
except KeyboardInterrupt:
self.logger.warning("\nOperation interrupted by user.")
raise
else:
self.logger.info("No changes to commit, checking push...")
failed = self._process_groups(files)
if self.push and not self.dry_run:
if not failed:
self.push_commits()
self.git.push()
self.console.print("[bold green]Push successful.[/bold green]")
else:
self.logger.error("Push aborted due to failed commits.")
if self.dry_run:
self.logger.info(f"Dry run done. See {self.dry_run_path}")
self.console.print(
f"[bold green]Dry run done.[/bold green] See {self.cache.path}"
)
else:
self.logger.info("Done.")
self.console.print("[bold green]Done.[/bold green]")
if failed:
self.logger.warning(f"Failed groups: {failed}")
self.console.print(f"[bold red]Failed groups: {failed}[/bold red]")
# ------------------------------------------------------------------ groups
def _process_groups(self, files: List[str]) -> list[str]:
groups = self.grouper.group(files)
cache_map = self.cache.load(
dry_run=self.dry_run, force_rebuild=self.force_rebuild
)
failed: list[str] = []
for i, (group, group_files) in enumerate(groups.items()):
if not self._process_group(group, group_files, cache_map):
failed.append(group)
if len(groups) > 1 and i < len(groups) - 1:
if self.provider != "ollama":
import time
time.sleep(15) # Conservative 4 RPM guard for free-tier
return failed
def _process_group(
self,
group: str,
files: List[str],
cache: Dict[str, str],
) -> bool:
self.git.stage(files)
diff = self.diff_builder.build(files)
if not diff.strip():
self.logger.info(f"Skipping empty diff for {group}")
self.git.reset(files)
return True
if not self.force_rebuild and group in cache:
self.logger.info(f"Using cached message for {group}")
return self._apply_message(group, cache[group], files)
try:
manifest_context = ManifestInspector.summary(cwd=self.git.cwd)
if manifest_context:
self.logger.info("Including manifest context in prompt")
with self.console.status(
"[bold blue]Generating commit message...[/bold blue]"
):
raw = self.message_builder.build_message(
group=group,
diff=diff,
manifest_context=manifest_context,
extra_kwargs=self.kwargs,
)
message = sanitize_ai_commit_message(raw)
except Exception as e:
if is_token_limit_error(e):
msg = format_token_limit_error(self.provider, e, group=group)
self.console.print(f"[bold red]{msg}[/bold red]")
self.logger.error(msg)
else:
self.logger.error(
f"Failed to generate message for group {group!r}: {e}",
exc_info=True,
)
self.git.reset(files)
return False
if not message:
self.logger.error(f"Empty message for {group}")
self.git.reset(files)
return False
return self._apply_message(group, message, files)
def _apply_message(
self,
group: str,
message: str,
files: List[str],
) -> bool:
if self.dry_run:
self.console.print(
Panel(
Markdown(message),
title=f"Dry Run: {group}",
border_style="yellow",
expand=False,
)
)
self.cache.append(group, message)
self.git.reset(files)
return True
if self.check:
message = self._review_with_user(group, message, files)
if message is None:
return False
try:
self.console.print(
Panel(Markdown(message), title="Commit Message", border_style="green")
)
self.git.commit(message)
except Exception as e:
self.logger.error(f"Failed to process group {group}: {e}", exc_info=True)
self.git.reset(files)
return False
return True
def _review_with_user(
self,
group: str,
message: str,
files: List[str],
) -> Optional[str]:
"""Present the message; return the final message, or None to abort."""
self.console.print(
Panel(
Markdown(message),
title=f"Proposed Commit Message [group: {group}]",
border_style="cyan",
)
)
choice = questionary.select(
"How would you like to proceed?",
choices=["Confirm", "Edit", "Abort"],
default="Confirm",
).ask()
if not choice or choice == "Abort":
self.logger.info(f"Commit aborted by user at group {group}")
self.git.reset(files)
return None
if choice == "Edit":
edited = questionary.text(
"Edit commit message:",
multiline=True,
default=message,
).ask()
if not edited:
self.logger.info(f"Empty edit, commit cancelled for {group}")
self.git.reset(files)
return None
return edited
return message
def run_commit_engine(**kwargs):
"""Entry point for the commit engine."""
logger = configure_logger("devgen.commit")
def run_commit_engine(**kwargs: Any) -> None:
"""Entry point used by the CLI."""
debug = kwargs.get("debug", False)
logger = configure_logger("devgen.cli.commit", console=debug)
try:
engine = CommitEngine(**kwargs)
engine.execute()
CommitEngine(**kwargs).execute()
except KeyboardInterrupt:
logger.warning("Interrupted by user. Exiting...")
sys.exit(0)
except Exception as e:
logger.error(f"Commit engine failed: {e}", exc_info=True)
sys.exit(1)
__all__ = ["run_commit_engine"]
__all__ = ["CommitEngine", "CommitEngineError", "run_commit_engine"]
+222
View File
@@ -0,0 +1,222 @@
"""Builds the prompt inputs for the commit engine.
Three small composable classes:
* :class:`FileGrouper` — groups a flat file list into per-commit folders
* :class:`DiffBuilder` — produces a (possibly truncated) staged diff
* :class:`ManifestInspector` — extracts a one-line-per-file project summary
"""
import json
import re
import subprocess
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Optional
from devgen.utils import get_git_root, run_git_command
# Lock files and other metadata blobs whose full diff would just bloat the
# prompt. We mark them as updated and skip the actual diff.
IGNORED_DIFF_PATTERNS = (
"uv.lock",
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"composer.lock",
"Gemfile.lock",
"poetry.lock",
)
# Default cap for a single group diff. Real-world prompts above this size
# are likely to hit the model's context window; the cap is overridable
# via ``max_diff_size`` in the config.
DEFAULT_MAX_DIFF_SIZE = 8000
# Manifests we read for project context, in priority order. Lock files
# are excluded — they're noisy and rarely change in a way that informs
# a commit message.
_MANIFEST_FILES = (
"pyproject.toml",
"package.json",
"requirements.txt",
)
class DiffBuilder:
"""Produces a staged diff for a list of paths with truncation."""
def __init__(
self,
max_size: int = DEFAULT_MAX_DIFF_SIZE,
cwd: Optional[Path] = None,
logger=None,
) -> None:
self.max_size = max_size
self.cwd = cwd or get_git_root()
self.logger = logger
def build(self, files: List[str]) -> str:
summary, to_diff = self._partition(files)
diff = self._run_staged_diff(to_diff)
full = "\n".join(summary + [diff]).strip()
if len(full) > self.max_size:
return self._truncate(full)
return full
# --- internals --------------------------------------------------------
def _partition(self, files: List[str]) -> tuple[list[str], list[str]]:
summary, to_diff = [], []
for f in files:
if Path(f).name in IGNORED_DIFF_PATTERNS:
summary.append(f"[METADATA UPDATED] {f}")
else:
to_diff.append(f)
return summary, to_diff
def _run_staged_diff(self, files: List[str]) -> str:
if not files:
return ""
try:
return run_git_command(
["git", "--no-pager", "diff", "--staged", "--", *files],
cwd=self.cwd,
)
except subprocess.CalledProcessError as e:
raise RuntimeError(
f"Git command failed: {' '.join(e.cmd)}\nError: {e.stderr.strip()}"
) from e
def _truncate(self, full: str) -> str:
if self.logger is not None:
self.logger.info(
f"Truncating diff from {len(full)} to {self.max_size} chars "
f"(config: max_diff_size). If you still hit token-limit errors, "
f"lower this further or split the commit into more groups."
)
half = self.max_size // 2
return (
full[:half]
+ "\n\n... [DIFF TRUNCATED FOR TOKEN OPTIMIZATION] ...\n\n"
+ full[-half:]
)
class FileGrouper:
"""Groups files by parent directory, merging down to ``max_groups``."""
def __init__(self, max_groups: int = 5) -> None:
self.max_groups = max(1, max_groups)
def group(self, files: List[str]) -> Dict[str, List[str]]:
groups: Dict[str, List[str]] = defaultdict(list)
for f in files:
parent = str(Path(f).parent)
key = "root" if parent == "." else parent
groups[key].append(f)
if len(groups) <= self.max_groups:
return dict(groups)
return self._merge_deepest(dict(groups))
def _merge_deepest(self, groups: Dict[str, List[str]]) -> Dict[str, List[str]]:
while len(groups) > self.max_groups:
candidates = [k for k in groups if k != "root"]
if not candidates:
break
deepest = max(candidates, key=lambda p: len(Path(p).parts))
parent_path = str(Path(deepest).parent)
new_key = "root" if parent_path == "." else parent_path
files = groups.pop(deepest)
groups.setdefault(new_key, []).extend(files)
return groups
class ManifestInspector:
"""Extracts a compact one-line-per-file project summary.
Dumping the first 100 lines of ``pyproject.toml``/``requirements.txt``
is extremely wasteful — most of it is build/tool config the model does
not need. We extract just the project identity and the package list
so the model can write context-aware commit messages.
"""
@staticmethod
def summary(cwd: Optional[Path] = None) -> str:
cwd = cwd or get_git_root() or Path(".")
lines = [
s
for s in (
ManifestInspector._pyproject(cwd),
ManifestInspector._package_json(cwd),
ManifestInspector._requirements(cwd),
)
if s
]
if not lines:
return ""
return "\nProject: " + " | ".join(lines)
# --- extractors -------------------------------------------------------
@staticmethod
def _pkg_name(spec: str) -> str:
return re.split(r"[><=!~\[]", spec, 1)[0].strip()
@staticmethod
def _pyproject(cwd: Path) -> Optional[str]:
path = cwd / "pyproject.toml"
if not path.exists():
return None
try:
import toml
data = toml.load(path)
except Exception:
return None
proj = data.get("project", {}) if isinstance(data, dict) else {}
parts = [
f"{k}={proj[k]}" for k in ("name", "version", "description") if proj.get(k)
]
deps = proj.get("dependencies") or []
if deps:
names = [n for n in (ManifestInspector._pkg_name(d) for d in deps) if n]
if names:
parts.append(f"deps={','.join(names)}")
return f"[pyproject.toml] {' '.join(parts)}" if parts else None
@staticmethod
def _package_json(cwd: Path) -> Optional[str]:
path = cwd / "package.json"
if not path.exists():
return None
try:
data = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
parts = [
f"{k}={data[k]}" for k in ("name", "version", "description") if data.get(k)
]
deps = data.get("dependencies") or {}
if isinstance(deps, dict) and deps:
parts.append(f"deps={','.join(deps.keys())}")
return f"[package.json] {' '.join(parts)}" if parts else None
@staticmethod
def _requirements(cwd: Path) -> Optional[str]:
path = cwd / "requirements.txt"
if not path.exists():
return None
try:
names = [
n
for n in (
ManifestInspector._pkg_name(line)
for line in path.read_text(encoding="utf-8").splitlines()
)
if n and not n.startswith("#")
]
except Exception:
return None
return f"[requirements.txt] deps={','.join(names)}" if names else None
+153
View File
@@ -0,0 +1,153 @@
"""Encapsulates every ``git`` invocation the commit engine makes.
Wraps :func:`devgen.utils.run_git_command` with the project's preferred
error handling and turns raw CLI output into typed results. Higher-level
classes (:class:`CommitEngine`) depend on this instead of calling git
themselves, so the engine code stays focused on orchestration.
"""
import subprocess
from pathlib import Path
from typing import Optional
from devgen.utils import configure_logger, get_git_root, run_git_command
class GitError(RuntimeError):
"""Raised when a git command exits non-zero."""
class GitOperator:
"""All git side-effects in one place."""
def __init__(
self,
cwd: Optional[Path] = None,
logger=None,
quiet_commands: bool = True,
) -> None:
self.cwd = cwd or get_git_root()
self.logger = logger or configure_logger("devgen.git")
self.quiet_commands = quiet_commands
# ------------------------------------------------------------------ helpers
def _run(self, args, *, check: bool = True) -> str:
try:
return run_git_command(args, check=check, cwd=self.cwd)
except subprocess.CalledProcessError as e:
cmd_str = " ".join(e.cmd) if e.cmd else "git"
stderr = (e.stderr or "").strip()
raise GitError(f"Git command failed: {cmd_str}\nError: {stderr}") from e
def _format_error(self, e: subprocess.CalledProcessError) -> str:
cmd_str = " ".join(e.cmd) if e.cmd else "git"
return f"Git command failed: {cmd_str}\nError: {(e.stderr or '').strip()}"
# ------------------------------------------------------------------ queries
def has_upstream(self) -> bool:
"""Return True if the current branch tracks an upstream."""
return bool(self.upstream())
def upstream(self) -> str:
"""Return the upstream branch name, or empty string if none."""
return self._run(
[
"git",
"rev-parse",
"--abbrev-ref",
"--symbolic-full-name",
"@{u}",
],
check=False,
)
def is_ahead_of_remote(self, fetch: bool = False) -> bool:
"""Return True if local HEAD has unpushed commits.
When ``fetch`` is True, refresh the upstream ref first — but only
do that when the caller is about to push. Skipping the fetch by
default avoids a network round-trip on every run and shrinks the
TOCTOU window between fetch and rev-list.
"""
upstream = self.upstream()
if not upstream:
return False
if fetch:
try:
self._run(["git", "fetch", "origin", upstream], check=False)
except GitError:
pass
try:
count = self._run(["git", "rev-list", "--count", "@{u}..HEAD"], check=False)
return bool(count) and int(count) > 0
except (GitError, ValueError):
return False
def detect_changed(self) -> list[str]:
"""Return modified, deleted, and untracked files (deduplicated)."""
out = self._run(
[
"git",
"ls-files",
"-z",
"--deleted",
"--modified",
"--others",
"--exclude-standard",
]
)
files = {f for f in out.split("\0") if f}
staged_out = self._run(["git", "diff", "-z", "--name-only", "--cached"])
files.update(f for f in staged_out.split("\0") if f)
return sorted(files)
# ------------------------------------------------------------------ actions
def stage(self, files: list[str]) -> None:
"""Stage the given paths — ``git add`` for existing files, ``git rm`` for deleted."""
if not files:
return
existing = [f for f in files if Path(self.cwd or ".").joinpath(f).exists()]
deleted = [f for f in files if f not in existing]
try:
if existing:
self._run(["git", "add", *existing])
if deleted:
self._run(["git", "rm", *deleted])
except GitError:
# Fallback: file state may have changed between check and command.
# Try a single git add --all for the whole set; git will handle
# both additions and removals correctly.
self._run(["git", "add", "--all", "--", *files])
def commit(self, message: str) -> None:
"""``git commit -m`` with the given message."""
self._run(["git", "commit", "-m", message])
def reset(self, files: list[str]) -> None:
"""Unstage the given paths. Errors are swallowed — best-effort."""
if not files:
return
try:
self._run(["git", "reset", "HEAD", "--", *files], check=False)
except GitError:
pass
def push(self) -> None:
"""Fetch upstream then push. Falls back to a no-upstream warning."""
upstream = self.upstream()
if upstream:
try:
self._run(["git", "fetch", "origin", upstream], check=False)
except GitError:
pass
try:
self._run(["git", "push"])
except GitError as e:
if "no upstream branch" in str(e).lower():
if not self.quiet_commands:
print("[warning]No upstream branch. Skipping push.[/warning]")
return
raise
+9
View File
@@ -1,3 +1,4 @@
import re
from pathlib import Path
from typing import List
@@ -5,6 +6,9 @@ import requests
from devgen.utils import configure_logger
# Only allow safe characters in template names to prevent path traversal
_TEMPLATE_NAME_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
class GitignoreGenerator:
"""Fetches and manages .gitignore templates from GitHub."""
@@ -41,6 +45,11 @@ class GitignoreGenerator:
self, name: str, use_cache: bool = True, offline: bool = False
) -> str:
"""Fetches template content from cache or GitHub."""
if not _TEMPLATE_NAME_RE.match(name):
raise ValueError(
f"Invalid template name '{name}'. "
"Names must contain only alphanumeric characters, hyphens, dots, or underscores."
)
cache_file = self.cache_dir / f"{name}.gitignore"
if offline:
+1 -3
View File
@@ -8,9 +8,7 @@ class LicenseGenerator:
"""Generates license files from templates."""
def __init__(self):
self.templates_dir = (
Path(__file__).parent.parent / "prompt_templates" / "licenses"
)
self.templates_dir = Path(__file__).parent.parent / "templates" / "licenses"
def list_licenses(self) -> List[Dict[str, str]]:
"""Lists available license templates."""
+28 -38
View File
@@ -2,65 +2,55 @@ from datetime import datetime
from pathlib import Path
from devgen.modules.changelog_generator import ChangelogGenerator
from devgen.modules.changelog_sections import Section
from devgen.utils import configure_logger
# Release-note-specific heading styles (use the same emoji, simpler heading).
RELEASE_NOTE_STYLES = {
Section.BREAKING: ("⚠️", "BREAKING CHANGES"),
Section.FEATURES: ("✨", "Features"),
Section.BUG_FIXES: ("🐛", "Bug Fixes"),
Section.PERFORMANCE: ("⚡", "Performance"),
Section.DOCUMENTATION: ("📚", "Documentation"),
Section.REFACTOR: ("♻️", "Refactor"),
Section.TESTS: ("✅", "Tests"),
Section.STYLE: ("💄", "Style"),
Section.CHORE: ("🔧", "Chore"),
Section.OTHER: ("🧹", "Other Changes"),
}
class ReleaseNotesGenerator(ChangelogGenerator):
"""Generate short, clean release notes from git history."""
SECTION_EMOJIS = {
"BREAKING CHANGES": "⚠️",
"Features": "✨",
"Bug Fixes": "🐛",
"Documentation": "📚",
"Refactor": "♻️",
"Tests": "✅",
"Chore": "🔧",
"Style": "✨",
"Other Changes": "🧹",
}
def __init__(self, logger=None):
super().__init__(logger or configure_logger("devgen.releasenotes"))
def generate_release_markdown(self, groups, version="Unreleased"):
"""Generate human-friendly release notes."""
"""Render a human-friendly release-notes block."""
date_str = datetime.now().strftime("%Y-%m-%d")
md = [f"## 🚀 Release {version} — {date_str}\n"]
order = [
"BREAKING CHANGES",
"Features",
"Bug Fixes",
"Documentation",
"Other Changes",
]
for section in order:
out = [f"## 🚀 Release {version} — {date_str}\n"]
for section in Section.ordered():
commits = groups.get(section)
if not commits:
continue
emoji = self.SECTION_EMOJIS.get(section, "")
md.append(f"### {emoji} {section}")
emoji, heading = RELEASE_NOTE_STYLES[section]
out.append(f"### {emoji} {heading}")
for c in commits:
scope = f"**{c['scope']}**: " if c["scope"] else ""
md.append(f"- {scope}{c['subject']}")
md.append("")
return "\n".join(md)
scope = f"**{c['scope']}**: " if c.get("scope") else ""
out.append(f"- {scope}{c['subject']}")
out.append("")
return "\n".join(out)
def run(self, output_file="RELEASE-NOTES.md", version="Unreleased", from_ref=""):
"""Main logic: get commits → parse → generate → write."""
"""Fetch → parse → write release notes."""
raw_commits = self.get_commits(from_ref)
if not raw_commits or not raw_commits[0]:
print("❗ No commits found for release notes.")
return
parsed = self.parse_commits(raw_commits)
md = self.generate_release_markdown(parsed, version=version)
groups = self.parse_commits(raw_commits)
md = self.generate_release_markdown(groups, version=version)
Path(output_file).write_text(md, encoding="utf-8")
print(f" Release notes written to {output_file}")
-21
View File
@@ -1,21 +0,0 @@
from devgen.providers.anthropic import AnthropicProvider
from devgen.providers.gemini import GeminiProvider
from devgen.providers.huggingface import HuggingfaceProvider
from devgen.providers.openai import OpenaiProvider
from devgen.providers.openrouter import OpenrouterProvider
def get_provider(name):
name_lower = name.lower()
if name_lower == "gemini":
return GeminiProvider()
elif name_lower == "openai":
return OpenaiProvider()
elif name_lower == "huggingface":
return HuggingfaceProvider()
elif name_lower == "openrouter":
return OpenrouterProvider()
elif name_lower == "anthropic":
return AnthropicProvider()
raise NotImplementedError(f"Provider '{name}' is not implemented yet.")
+15 -17
View File
@@ -1,23 +1,21 @@
import anthropic
from devgen.providers.base import BaseProvider
class AnthropicProvider:
class AnthropicProvider(BaseProvider):
"""Generates content using Anthropic's Claude models."""
def generate(
self, prompt: str, api_key: str, model: str = "claude-3-opus-20240229", **kwargs
) -> str:
"""Generates a response using the Anthropic API."""
if not api_key:
raise ValueError("Anthropic API key is required.")
DISPLAY_NAME = "Anthropic"
DEFAULT_MODEL = "claude-3-opus-20240229"
try:
client = anthropic.Anthropic(api_key=api_key)
message = client.messages.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return message.content[0].text.strip()
except Exception as e:
raise RuntimeError(f"Anthropic generation failed: {e}")
def _generate(self, prompt, api_key, model, **kwargs):
client = anthropic.Anthropic(api_key=api_key)
message = client.messages.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
if not message.content:
raise RuntimeError(f"Anthropic returned no content for model {model!r}.")
return message.content[0].text or ""
+114
View File
@@ -0,0 +1,114 @@
"""Base class for AI providers.
Every provider implements :meth:`_generate` and inherits the standard
contract: API-key validation, token-limit detection, and friendly
RuntimeError wrapping. Providers with provider-specific error shapes
(eg. Hugging Face's HTTPError / RequestException split) override
:meth:`_handle_error`.
"""
from abc import ABC, abstractmethod
from typing import Any
from devgen.utils import format_token_limit_error, is_token_limit_error
class BaseProvider(ABC):
"""Abstract base for all AI providers."""
#: Human-readable name used in error messages.
DISPLAY_NAME: str = ""
#: Whether the provider requires an API key.
REQUIRES_API_KEY: bool = True
#: Default model id when the user doesn't specify one.
DEFAULT_MODEL: str = ""
def generate(
self,
prompt: str,
api_key: str | None = None,
model: str | None = None,
**kwargs: Any,
) -> str:
"""Public entry point. Validates input, dispatches, and wraps errors."""
import time
if self.REQUIRES_API_KEY and not api_key:
raise ValueError(
f"{self.DISPLAY_NAME} API key is missing. "
"Set it via `devgen setup config` or pass --api-key."
)
chosen_model = model or self.DEFAULT_MODEL
max_retries = kwargs.get("max_retries", 5)
base_delay = kwargs.get("retry_delay", 10)
for attempt in range(max_retries + 1):
try:
text = self._generate(prompt, api_key, chosen_model, **kwargs)
return (text or "").strip()
except Exception as e:
if is_token_limit_error(e):
self._handle_error(e)
if self._is_quota_error(e) and attempt < max_retries:
delay = base_delay * (2**attempt)
print(
f"\n[{self.DISPLAY_NAME}] Quota/Rate limit hit (429). "
f"Waiting {delay}s and retrying... "
f"(Attempt {attempt + 1}/{max_retries})"
)
time.sleep(delay)
continue
if self._is_quota_error(e) and attempt >= max_retries:
raise RuntimeError(
f"{self.DISPLAY_NAME} API quota exhausted (429) after multiple retries."
) from e
self._handle_error(e)
return ""
@staticmethod
def _is_quota_error(error: Exception) -> bool:
text = str(error).upper()
return any(
marker in text
for marker in (
"429",
"RESOURCE_EXHAUSTED",
"QUOTA_EXCEEDED",
"THROTTLED",
"RATE_LIMIT_EXCEEDED",
"TOO_MANY_REQUESTS",
)
)
def _handle_error(self, error: Exception) -> None:
"""Translate an internal exception into a user-facing RuntimeError.
Token-limit / context-window errors are reformatted with
:func:`format_token_limit_error`; everything else is wrapped in a
generic provider message. Subclasses override this to add
provider-specific error shapes (eg. HTTPError vs RequestException).
"""
if is_token_limit_error(error):
raise RuntimeError(
format_token_limit_error(self.DISPLAY_NAME, error)
) from error
raise RuntimeError(
f"{self.DISPLAY_NAME} request failed: {error}. "
"Check the model name, API key permissions, and your account quota."
) from error
@abstractmethod
def _generate(
self,
prompt: str,
api_key: str | None,
model: str,
**kwargs: Any,
) -> str:
"""Provider-specific generation. Return the raw generated text.
Implementations may raise any exception; :meth:`_handle_error` will
translate it. Re-raise an exception to defer to default handling.
"""
raise NotImplementedError
+20 -19
View File
@@ -1,24 +1,25 @@
import google.generativeai as genai
from google import genai
from google.genai import types
from devgen.providers.base import BaseProvider
class GeminiProvider:
class GeminiProvider(BaseProvider):
"""Generates content using Google's Gemini models."""
def generate(
self, prompt: str, api_key: str, model: str = "gemini-pro", **kwargs
) -> str:
"""Generates a response using the Gemini API."""
if not api_key:
raise ValueError("Gemini API key is required.")
DISPLAY_NAME = "Gemini"
DEFAULT_MODEL = "gemini-2.5-flash"
genai.configure(api_key=api_key)
# Handle model name mapping if needed, or trust user input
# gemini-pro is a common default
try:
model_instance = genai.GenerativeModel(model)
response = model_instance.generate_content(prompt)
return response.text.strip()
except Exception as e:
raise RuntimeError(f"Gemini generation failed: {e}")
def _generate(self, prompt, api_key, model, **kwargs):
client = genai.Client(api_key=api_key)
response = client.models.generate_content(
model=model,
contents=prompt,
config=types.GenerateContentConfig(
temperature=kwargs.get("temperature", 0.7),
top_p=kwargs.get("top_p", 0.95),
top_k=kwargs.get("top_k", 40),
max_output_tokens=kwargs.get("max_output_tokens", 2048),
),
)
return response.text or ""
+38 -27
View File
@@ -1,45 +1,56 @@
import requests
from devgen.providers.base import BaseProvider
from devgen.utils import format_token_limit_error, is_token_limit_error
class HuggingfaceProvider:
class HuggingfaceProvider(BaseProvider):
"""Generates content using Hugging Face Inference API."""
DISPLAY_NAME = "Hugging Face"
API_URL_TEMPLATE = "https://api-inference.huggingface.co/models/{model}"
DEFAULT_MODEL = "mistralai/Mistral-7B-Instruct-v0.2"
def generate(
self,
prompt: str,
api_key: str,
model: str = "mistralai/Mistral-7B-Instruct-v0.2",
**kwargs,
) -> str:
"""Generates a response using Hugging Face API."""
if not api_key:
raise ValueError("Hugging Face API token is required.")
def _generate(self, prompt, api_key, model, **kwargs):
api_url = self.API_URL_TEMPLATE.format(model=model)
headers = {"Authorization": f"Bearer {api_key}"}
# HF models often expect specific prompting formats, but we'll send raw prompt
# Some models are text-generation, some are conversational.
# Assuming text-generation for generic usage.
payload = {
"inputs": prompt,
"parameters": {"max_new_tokens": 500, "return_full_text": False},
}
try:
response = requests.post(api_url, headers=headers, json=payload)
response = requests.post(
api_url, headers=headers, json=payload, timeout=120
)
response.raise_for_status()
result = response.json()
except requests.HTTPError as e:
if is_token_limit_error(e):
raise RuntimeError(
format_token_limit_error(self.DISPLAY_NAME, e)
) from e
status = e.response.status_code if e.response is not None else "?"
raise RuntimeError(
f"Hugging Face request failed (HTTP {status}): {e}. "
"Check the model id and your token permissions."
) from e
except requests.RequestException as e:
raise RuntimeError(
f"Hugging Face network error: {e}. Check your connection."
) from e
if isinstance(result, list) and "generated_text" in result[0]:
return result[0]["generated_text"].strip()
elif isinstance(result, dict) and "error" in result:
raise RuntimeError(f"Hugging Face API error: {result['error']}")
else:
return str(result)
except Exception as e:
raise RuntimeError(f"Hugging Face generation failed: {e}")
if isinstance(result, list) and result and "generated_text" in result[0]:
return result[0]["generated_text"]
if isinstance(result, dict) and "error" in result:
err = result["error"]
if "is currently loading" in str(err).lower():
raise RuntimeError(
f"Hugging Face model is loading: {err}. "
"Wait a minute and retry, or pick a different model."
)
raise RuntimeError(f"Hugging Face API error: {err}")
raise RuntimeError(
"Hugging Face returned an unexpected response shape. "
"The model may not support text-generation."
)
+55
View File
@@ -0,0 +1,55 @@
from openai import OpenAI
from devgen.providers.base import BaseProvider
from devgen.utils import format_token_limit_error, is_token_limit_error
class OllamaProvider(BaseProvider):
"""Generates content using a local Ollama server.
Ollama exposes an OpenAI-compatible endpoint at /v1/chat/completions,
so we reuse the ``openai`` client. The host defaults to
http://localhost:11434 but can be overridden via the ``ollama_host``
kwarg (or ``ollama_host`` in ~/.devgen.yaml).
"""
DISPLAY_NAME = "Ollama"
REQUIRES_API_KEY = False
DEFAULT_HOST = "http://localhost:11434"
DEFAULT_MODEL = "llama3.2"
# Keys consumed by devgen itself — never forwarded to the SDK.
_DEVGEN_KEYS = frozenset({"debug", "ollama_host", "max_retries", "retry_delay"})
def _generate(self, prompt, api_key, model, **kwargs):
host = kwargs.get("ollama_host") or self.DEFAULT_HOST
base_url = f"{host.rstrip('/')}/v1"
# Only forward known OpenAI-compatible parameters
safe_kwargs = {k: v for k, v in kwargs.items() if k not in self._DEVGEN_KEYS}
with OpenAI(base_url=base_url, api_key=api_key or "ollama") as client:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
**safe_kwargs,
)
if not response.choices:
raise RuntimeError(
f"Ollama returned no choices for model {model!r}. "
"The model may not be installed locally."
)
return response.choices[0].message.content or ""
def _handle_error(self, error):
"""Override default wrapping — Ollama has no API key to check."""
if is_token_limit_error(error):
raise RuntimeError(
format_token_limit_error(self.DISPLAY_NAME, error)
) from error
raise RuntimeError(
f"{self.DISPLAY_NAME} request failed: {error}. "
"Is the Ollama server running? Start it with `ollama serve` and "
"pull a model with `ollama pull <model>`."
) from error
+17 -40
View File
@@ -1,48 +1,25 @@
from openai import OpenAI
from devgen.providers.base import BaseProvider
class OpenaiProvider:
"""Generates a response string using the OpenAI ChatCompletion API based on the provided prompt and parameters. This method initializes an OpenAI client with the given API key, sends a chat completion request with specified model and additional parameters, and returns the content of the generated message.
Args:
prompt (str): The input prompt to generate a response for.
api_key (str): The API key used to authenticate with the OpenAI service.
model (str, optional): The model to use for generation; defaults to "gpt-4o".
**kwargs: Additional keyword arguments to customize the API request (e.g., temperature).
Returns:
str: The content of the generated response message.
"""
class OpenaiProvider(BaseProvider):
"""Generates content using OpenAI's Chat Completions API."""
DISPLAY_NAME = "OpenAI"
DEFAULT_MODEL = "gpt-4o"
def generate(
self, prompt: str, api_key: str, model: str | None = None, **kwargs
) -> str:
"""Generates a response from the OpenAI ChatCompletion API based on the provided prompt and parameters.
# Keys consumed by devgen itself — never forwarded to the SDK.
_DEVGEN_KEYS = frozenset({"debug", "ollama_host", "max_retries", "retry_delay"})
Creates a client instance with the specified API key, sends a chat completion request using the selected model and additional parameters, and returns the generated message content as a string.
Args:
prompt (str): Prompt input.
api_key (str): OpenAI API key.
model (str, optional): Model to use (default: gpt-4o).
**kwargs: Additional OpenAI ChatCompletion parameters (e.g., temperature).
Returns:
str: Generated response content.
"""
# 1. Create a client instance with the API key.
try:
client = OpenAI(api_key=api_key)
except Exception as e:
# Add error handling if the client fails to initialize
raise RuntimeError(f"Failed to initialize OpenAI client: {e}")
# 2. Use the modern API syntax: client.chat.completions.create
response = client.chat.completions.create(
model=model or self.DEFAULT_MODEL,
messages=[{"role": "user", "content": prompt}],
**kwargs,
)
return response.choices[0].message.content.strip()
def _generate(self, prompt, api_key, model, **kwargs):
safe_kwargs = {k: v for k, v in kwargs.items() if k not in self._DEVGEN_KEYS}
with OpenAI(api_key=api_key) as client:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
**safe_kwargs,
)
if not response.choices:
raise RuntimeError(f"OpenAI returned no choices for model {model!r}.")
return response.choices[0].message.content or ""
+16 -19
View File
@@ -1,33 +1,30 @@
from openai import OpenAI
from devgen.providers.base import BaseProvider
class OpenrouterProvider:
class OpenrouterProvider(BaseProvider):
"""Generates content using OpenRouter (OpenAI-compatible API)."""
DISPLAY_NAME = "OpenRouter"
BASE_URL = "https://openrouter.ai/api/v1"
DEFAULT_MODEL = "openai/gpt-3.5-turbo"
def generate(
self, prompt: str, api_key: str, model: str = "openai/gpt-3.5-turbo", **kwargs
) -> str:
"""Generates a response using OpenRouter."""
if not api_key:
raise ValueError("OpenRouter API key is required.")
try:
client = OpenAI(
base_url=self.BASE_URL,
api_key=api_key,
)
# Keys consumed by devgen itself — never forwarded to the SDK.
_DEVGEN_KEYS = frozenset({"debug", "ollama_host", "max_retries", "retry_delay"})
def _generate(self, prompt, api_key, model, **kwargs):
safe_kwargs = {k: v for k, v in kwargs.items() if k not in self._DEVGEN_KEYS}
with OpenAI(base_url=self.BASE_URL, api_key=api_key) as client:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
extra_headers={
"HTTP-Referer": "https://github.com/S4NKALP/devgen", # Optional
"X-Title": "devgen CLI", # Optional
"HTTP-Referer": "https://github.com/S4NKALP/devgen",
"X-Title": "devgen CLI",
},
**kwargs,
**safe_kwargs,
)
return response.choices[0].message.content.strip()
except Exception as e:
raise RuntimeError(f"OpenRouter generation failed: {e}")
if not response.choices:
raise RuntimeError(f"OpenRouter returned no choices for model {model!r}.")
return response.choices[0].message.content or ""
-15
View File
@@ -1,15 +0,0 @@
Analyze the following git diff for the directory `{{ group_name }}` and generate a detailed semantic release commit message4. Format the output as:
1. A summary title in semantic release format (type(scope): summary) — scope can be the directory or 'root'.
2. A short summary paragraph if needed.
3. Bullet points listing the main changes, grouped as 'New Features', 'Enhancements & Refinements', and 'Bug Fixes' if relevant. Use bold for feature/section names.
{% if use_emoji %}
4. Use appropriate emojis for the commit type and sections (e.g., 🚀 for features, 🐛 for bugs).
{% else %}
4. Do NOT use any emojis in the commit message.
{% endif %}
Output only the commit message, don't include explanations or extra text.
Here is the git diff for `{{ group_name }}`:
{{ diff_text }}
+29
View File
@@ -0,0 +1,29 @@
You write Conventional Commits. Output ONLY the message — no preamble, no labels, no quotes, no markdown.
Format:
<type>[optional scope][optional !]: <description>
[optional blank line]
[optional body]
[optional blank line]
[optional footer(s)]
types: feat fix refactor perf docs test build ci chore style
scope: {% if group_name and group_name != 'root' %}({{ group_name }}){% else %}optional{% endif %}{% if use_emoji %} (prefix description with one matching emoji){% endif %}
Examples:
feat: add email notifications on new direct messages
feat(shopping cart): add the amazing button
feat!: remove ticket list endpoint
refers to JIRA-1337
BREAKING CHANGE: ticket endpoints no longer supports list all entities.
fix(shopping-cart): prevent order an empty shopping cart
fix(api): fix wrong calculation of request body checksum
fix: add missing parameter to service call
The error occurred due to <reasons>.
perf: decrease memory footprint for determine unique visitors by using HyperLogLog
build: update dependencies
build(release): bump version to 1.0.0
refactor: implement fibonacci number calculation as recursion
style: remove empty line
{{ diff_text }}{{ context }}
+244 -35
View File
@@ -6,7 +6,6 @@ from pathlib import Path
from typing import Any, Dict, Optional
import yaml
from jinja2 import Environment, FileSystemLoader
def ensure_log_directory() -> Path:
@@ -24,32 +23,73 @@ def get_commit_dry_run_path() -> Path:
def is_file_recent(file_path: Path | str, max_age_minutes: int = 120) -> bool:
path = Path(file_path)
if not path.exists():
try:
return (time.time() - Path(file_path).stat().st_mtime) <= max_age_minutes * 60
except FileNotFoundError:
return False
return (time.time() - path.stat().st_mtime) <= max_age_minutes * 60
def sanitize_ai_commit_message(raw_text: str) -> str:
"""
Cleans up the AI-generated commit message.
It looks for a conventional commit header and extracts everything from there.
It handles bolded headers and common AI prefixes.
"""
if not raw_text:
return ""
lines = raw_text.strip().split("\n")
cleaned_lines = []
in_block = False
# Regex for conventional commit header
# Regex for conventional commit header (including optional bolding)
# Examples:
# - feat(root): summary
# - **fix: bug fix**
# - chore(deps)!: breaking change
header_pattern = re.compile(
r"^(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*"
r"^(\*\*)?(feat|fix|chore|refactor|docs|style|test|build|ci|perf|revert|deps|wip)(\(.*\))?!?: .*",
re.IGNORECASE,
)
cleaned_lines = []
found_header = False
for line in lines:
stripped = line.strip()
if in_block:
if header_pattern.match(stripped) or "**Sponsor**" in line:
if not found_header:
# First, strip bolding if it wraps the whole line or parts of it
# This is simpler than handling it in the regex
candidate = stripped.replace("**", "").strip()
# Clean the line by removing labels like "1. Title:", "### Message:", etc.
clean_line = re.sub(
r"^(#+\s+)?(\d+\.?|\*|-)?\s*(Title|Commit Message|Message):\s*",
"",
candidate,
flags=re.IGNORECASE,
).strip()
if header_pattern.match(clean_line):
found_header = True
cleaned_lines.append(clean_line)
else:
# If we hit another header or a known separator, we stop
if "**Sponsor**" in line:
break
cleaned_lines.append(line)
elif header_pattern.match(stripped):
in_block = True
# Ignore trailing markdown code block markers
if line.strip() in ("```", "```md", "```markdown"):
continue
cleaned_lines.append(line)
return "\n".join(cleaned_lines).strip() if cleaned_lines else ""
if cleaned_lines:
return "\n".join(cleaned_lines).strip()
# Fallback: if no conventional commit header found, just take the first non-empty line
# to avoid failing completely, but log a warning if possible.
for line in lines:
if line.strip():
return line.strip()
return ""
def parse_markdown_sections(
@@ -75,7 +115,7 @@ def extract_commit_messages(filepath: Path | str) -> dict[str, str]:
def configure_logger(
name: str = "devgen", log_file: Optional[Path | str] = None
name: str = "devgen", log_file: Optional[Path | str] = None, console: bool = True
) -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
@@ -88,9 +128,10 @@ def configure_logger(
)
# Console handler
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
if console:
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
# File handler
if log_file:
@@ -103,37 +144,134 @@ def configure_logger(
return logger
def get_git_staged_files() -> list[str]:
def get_git_root(cwd: Optional[Path] = None) -> Optional[Path]:
"""Return the root directory of the current git repository, or None."""
try:
output = run_git_command(["git", "rev-parse", "--show-toplevel"], cwd=cwd)
return Path(output) if output else None
except (subprocess.CalledProcessError, FileNotFoundError):
return None
def run_git_command(
args: list[str],
check: bool = True,
cwd: Optional[Path] = None,
encoding: str = "utf-8",
errors: str = "replace",
) -> str:
"""
Executes a git command and returns the output.
Args:
args: List of command arguments (e.g., ["git", "status"]).
check: Whether to raise an exception on non-zero exit code.
cwd: Current working directory for the command.
encoding: Output encoding.
errors: Error handling strategy for encoding.
Returns:
The standard output of the command, stripped of leading/trailing whitespace.
Raises:
subprocess.CalledProcessError: If the command fails and check is True.
"""
try:
res = subprocess.run(
["git", "diff", "--name-only", "--cached"],
args,
capture_output=True,
text=True,
check=True,
encoding=encoding,
errors=errors,
check=check,
cwd=cwd,
)
return [f for f in res.stdout.splitlines() if f.strip()]
except subprocess.CalledProcessError:
return res.stdout.strip()
except subprocess.CalledProcessError as e:
# Log the error if a logger is configured, but re-raise
# We don't have access to a specific logger here easily without passing it in,
# so we rely on the caller to handle logging if needed, or we could log to a default one.
# For now, just re-raise as the caller expects.
raise e
def get_git_staged_files() -> list[str]:
try:
output = run_git_command(["git", "diff", "--name-only", "--cached"])
return [f for f in output.splitlines() if f.strip()]
except (subprocess.CalledProcessError, FileNotFoundError):
return []
def read_file_content(filepath: Path | str) -> Optional[str]:
path = Path(filepath)
if path.exists():
return path.read_text(encoding="utf-8")
return None
try:
return Path(filepath).read_text(encoding="utf-8")
except FileNotFoundError:
return None
def delete_file(filepath: Path | str) -> bool:
path = Path(filepath)
if path.exists():
path.unlink()
try:
Path(filepath).unlink()
return True
return False
except FileNotFoundError:
return False
def load_template_env(sub_dir: str) -> Environment:
def load_template(sub_dir: str, name: str) -> str:
"""Load a template file from the templates directory."""
template_dir = Path(__file__).parent / "templates" / sub_dir
return Environment(loader=FileSystemLoader(template_dir))
template_path = template_dir / name
if not template_path.is_file():
raise RuntimeError(
f"Template not found: {template_path}. The installation may be corrupted."
)
return template_path.read_text(encoding="utf-8")
def render_template(template_str: str, **context) -> str:
"""Render a simple template with variable substitution and conditionals.
Supports:
- {{ variable }} - variable substitution
- {% if condition %}...{% endif %} - conditional blocks
- {% if condition %}...{% else %}...{% endif %} - conditional with else
"""
result = template_str
# Process conditional blocks: {% if var %}...{% endif %}
# and {% if var %}...{% else %}...{% endif %}
if_pattern = re.compile(
r"\{%\s*if\s+(\w+)\s*%\}(.*?)\{%\s*endif\s*%\}",
re.DOTALL,
)
def replace_if(match):
var_name = match.group(1)
content = match.group(2)
value = context.get(var_name)
if value:
# Check for else block
else_pattern = re.compile(r"\{%\s*else\s*%\}", re.DOTALL)
parts = else_pattern.split(content, 1)
return parts[0]
else:
else_pattern = re.compile(r"\{%\s*else\s*%\}", re.DOTALL)
parts = else_pattern.split(content, 1)
return parts[1] if len(parts) > 1 else ""
result = if_pattern.sub(replace_if, result)
# Process variable substitutions: {{ variable }}
var_pattern = re.compile(r"\{\{\s*(\w+)\s*\}\}")
def replace_var(match):
var_name = match.group(1)
return str(context.get(var_name, ""))
result = var_pattern.sub(replace_var, result)
return result
def load_config() -> Dict[str, Any]:
@@ -145,6 +283,8 @@ def load_config() -> Dict[str, Any]:
"model": "gemini-2.5-flash",
"api_key": "",
"emoji": True,
"max_groups": 5,
"ollama_host": "http://localhost:11434",
}
try:
with config_path.open("w", encoding="utf-8") as f:
@@ -156,12 +296,77 @@ def load_config() -> Dict[str, Any]:
try:
with config_path.open("r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
data = yaml.safe_load(f)
if not isinstance(data, dict):
print(
f"Warning: Config at {config_path} is not a dict, using defaults."
)
return {}
return data
except Exception as e:
print(f"Warning: Failed to load config from {config_path}: {e}")
return {}
# Substrings used to identify token / context-length errors from AI providers.
# Matched case-insensitively against the full stringified exception, so we
# catch the wide variety of error formats each provider returns.
_TOKEN_LIMIT_PATTERNS = (
"context length",
"context_length",
"context_length_exceeded",
"context window",
"context window exceeded",
"context_size",
"maximum context",
"max context",
"max_tokens",
"max tokens",
"prompt is too long",
"too many tokens",
"token limit",
"tokens in the prompt",
"input is too long",
"request too large",
"request was too large",
"string too long",
"too long for the model",
"reduce the length of the messages",
"tokens_exceeded",
)
def is_token_limit_error(error: BaseException | str) -> bool:
"""Return True if the error looks like a model context/token overflow.
Different SDKs phrase the same condition differently. We look for the
common substrings so we can give a single, actionable message instead of
the raw provider text.
"""
text = str(error).lower()
return any(p in text for p in _TOKEN_LIMIT_PATTERNS)
def format_token_limit_error(
provider: str,
error: BaseException | str,
*,
group: str | None = None,
) -> str:
"""Build a friendly message for a context-window overflow.
Kept short so it doesn't drown the log line. The original provider
error is included so the user can still see the exact cause.
"""
scope = f" ({group})" if group else ""
return (
f"{provider}: diff exceeds context window{scope}. "
f"Cut token usage: --max-groups N, --max-diff-size N, "
f"or a larger-context model (gemini-2.5-flash, gpt-4o, claude-3-5-sonnet). "
f"Details: {error}"
)
__all__ = [
"ensure_log_directory",
"get_main_log_path",
@@ -170,12 +375,16 @@ __all__ = [
"sanitize_ai_commit_message",
"extract_commit_messages",
"configure_logger",
"run_git_command",
"get_git_staged_files",
"read_file_content",
"delete_file",
"load_template_env",
"load_template",
"render_template",
"load_config",
"get_questionary_style",
"is_token_limit_error",
"format_token_limit_error",
]
+9 -10
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "devgen-cli"
version = "0.2.1"
version = "0.3.0"
description = "A collection of developer tools"
readme = "README.md"
requires-python = ">=3.10"
@@ -25,16 +25,15 @@ classifiers = [
"Programming Language :: Python :: 3.10",
]
dependencies = [
"anthropic>=0.75.0",
"google-generativeai>=0.8.5",
"jinja2>=3.1.6",
"openai>=2.11.0",
"anthropic>=0.116.0",
"google-genai>=2.10.0",
"openai>=2.44.0",
"pyyaml>=6.0.3",
"questionary>=2.1.1",
"requests>=2.32.5",
"rich>=14.2.0",
"requests>=2.34.2",
"rich>=15.0.0",
"toml>=0.10.2",
"typer>=0.20.0",
"typer>=0.26.8",
]
[project.urls]
Homepage = "https://github.com/S4NKALP/devgen"
@@ -50,6 +49,6 @@ include = ["devgen*"]
[dependency-groups]
dev = [
"pre-commit>=4.5.0",
"ruff>=0.14.9",
"pre-commit>=4.6.0",
"ruff>=0.15.20",
]
+11 -12
View File
@@ -1,12 +1,11 @@
anthropic>=0.75.0
google-generativeai>=0.8.5
jinja2>=3.1.6
openai>=2.11.0
pyyaml>=6.0.3
questionary>=2.1.1
requests>=2.32.5
rich>=14.2.0
toml>=0.10.2
typer>=0.20.2
pre-commit>=4.5.0
ruff>=0.14.9
anthropic==0.116.0
google-genai==2.10.0
openai==2.44.0
pre-commit==4.6.0
pyyaml==6.0.3
questionary==2.1.1
requests==2.34.2
rich==15.0.0
ruff==0.16.9
toml==0.10.2
typer==0.26.8
+1 -1
View File
@@ -6,7 +6,7 @@ from setuptools import find_packages, setup
setup(
name="devgen-cli",
version="0.2.1",
version="0.3.0",
packages=find_packages(),
include_package_data=True,
install_requires=[
Generated
+666 -776
View File
File diff suppressed because it is too large. Load diff