devgen-cli 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devgen_cli-0.3.0/devgen_cli.egg-info → devgen_cli-0.3.1}/PKG-INFO +13 -17
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/README.md +8 -6
- devgen_cli-0.3.1/devgen/ai.py +51 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/cli/changelog.py +6 -5
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/cli/commit.py +22 -17
- devgen_cli-0.3.1/devgen/cli/config.py +127 -0
- devgen_cli-0.3.1/devgen/cli/gitignore.py +137 -0
- devgen_cli-0.3.1/devgen/cli/license.py +92 -0
- devgen_cli-0.3.1/devgen/cli/main.py +66 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/cli/release.py +13 -4
- devgen_cli-0.3.1/devgen/cli/setup.py +16 -0
- devgen_cli-0.3.1/devgen/cli/wizard.py +88 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/modules/changelog_generator.py +78 -63
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/modules/changelog_sections.py +1 -1
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/modules/commit_generator.py +123 -154
- devgen_cli-0.3.1/devgen/modules/diff_builder.py +232 -0
- devgen_cli-0.3.1/devgen/modules/git_ops.py +109 -0
- devgen_cli-0.3.1/devgen/modules/gitignore_generator.py +121 -0
- devgen_cli-0.3.1/devgen/modules/license_generator.py +65 -0
- devgen_cli-0.3.1/devgen/modules/release_note_generator.py +44 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/providers/anthropic.py +10 -2
- devgen_cli-0.3.1/devgen/providers/base.py +120 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/providers/gemini.py +9 -1
- devgen_cli-0.3.1/devgen/providers/huggingface.py +64 -0
- devgen_cli-0.3.1/devgen/providers/ollama.py +30 -0
- devgen_cli-0.3.1/devgen/providers/openai.py +8 -0
- devgen_cli-0.3.1/devgen/providers/openai_compatible.py +46 -0
- devgen_cli-0.3.1/devgen/providers/openrouter.py +15 -0
- devgen_cli-0.3.1/devgen/utils.py +357 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1/devgen_cli.egg-info}/PKG-INFO +13 -17
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen_cli.egg-info/SOURCES.txt +2 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/pyproject.toml +6 -7
- devgen_cli-0.3.1/setup.py +5 -0
- devgen_cli-0.3.0/devgen/ai.py +0 -37
- devgen_cli-0.3.0/devgen/cli/config.py +0 -228
- devgen_cli-0.3.0/devgen/cli/gitignore.py +0 -138
- devgen_cli-0.3.0/devgen/cli/license.py +0 -101
- devgen_cli-0.3.0/devgen/cli/main.py +0 -109
- devgen_cli-0.3.0/devgen/cli/setup.py +0 -107
- devgen_cli-0.3.0/devgen/modules/diff_builder.py +0 -222
- devgen_cli-0.3.0/devgen/modules/git_ops.py +0 -153
- devgen_cli-0.3.0/devgen/modules/gitignore_generator.py +0 -125
- devgen_cli-0.3.0/devgen/modules/license_generator.py +0 -78
- devgen_cli-0.3.0/devgen/modules/release_note_generator.py +0 -56
- devgen_cli-0.3.0/devgen/providers/base.py +0 -114
- devgen_cli-0.3.0/devgen/providers/huggingface.py +0 -56
- devgen_cli-0.3.0/devgen/providers/ollama.py +0 -55
- devgen_cli-0.3.0/devgen/providers/openai.py +0 -25
- devgen_cli-0.3.0/devgen/providers/openrouter.py +0 -30
- devgen_cli-0.3.0/devgen/utils.py +0 -407
- devgen_cli-0.3.0/setup.py +0 -47
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/LICENSE +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/MANIFEST.in +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/__init__.py +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/cli/__init__.py +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/modules/__init__.py +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/providers/__init__.py +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/commit/commit_message.tpl +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/agpl-3.0.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/apache-2.0.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/bsd-2-clause.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/bsd-3-clause.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/bsl-1.0.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/cc0-1.0.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/epl-2.0.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/gpl-2.0.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/gpl-3.0.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/lgpl-2.1.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/mit.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/mpl-2.0.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen/templates/licenses/unlicense.json +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen_cli.egg-info/dependency_links.txt +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen_cli.egg-info/entry_points.txt +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen_cli.egg-info/requires.txt +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/devgen_cli.egg-info/top_level.txt +0 -0
- {devgen_cli-0.3.0 → devgen_cli-0.3.1}/setup.cfg +0 -0
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devgen-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A collection of developer tools
|
|
5
|
-
Home-page: https://github.com/S4NKALP/devgen
|
|
6
|
-
Author: Sankalp Tharu
|
|
7
5
|
Author-email: Sankalp Tharu <sankalptharu50028@gmail.com>
|
|
8
|
-
License: GPL-3.0-or-later
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
9
7
|
Project-URL: Homepage, https://github.com/S4NKALP/devgen
|
|
10
8
|
Keywords: devgen,cli,git,changelog,gitignore,license,commit-generator,commit
|
|
11
9
|
Classifier: Development Status :: 3 - Alpha
|
|
12
10
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
11
|
Classifier: Operating System :: OS Independent
|
|
15
12
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
17
|
Requires-Python: >=3.10
|
|
21
18
|
Description-Content-Type: text/markdown
|
|
22
19
|
License-File: LICENSE
|
|
@@ -29,10 +26,7 @@ Requires-Dist: requests>=2.34.2
|
|
|
29
26
|
Requires-Dist: rich>=15.0.0
|
|
30
27
|
Requires-Dist: toml>=0.10.2
|
|
31
28
|
Requires-Dist: typer>=0.26.8
|
|
32
|
-
Dynamic: author
|
|
33
|
-
Dynamic: home-page
|
|
34
29
|
Dynamic: license-file
|
|
35
|
-
Dynamic: requires-python
|
|
36
30
|
|
|
37
31
|
# DevGen
|
|
38
32
|
|
|
@@ -64,7 +58,7 @@ DevGen is a CLI for the parts of development that should be invisible. It writes
|
|
|
64
58
|
- **Conventional Changelogs** — `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, `chore`, `style`, and a `BREAKING CHANGES` section.
|
|
65
59
|
- **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, …).
|
|
66
60
|
- **Custom Templates** — Override the commit-message prompt via a `.tpl` file in your config.
|
|
67
|
-
- **Interactive Setup** — `devgen setup` walks you through provider + API key + options.
|
|
61
|
+
- **Interactive Setup** — `devgen setup config` walks you through provider + API key + options.
|
|
68
62
|
- **Undo Support** — `devgen commit undo` rolls back the last AI commit while keeping changes staged.
|
|
69
63
|
- **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.
|
|
70
64
|
|
|
@@ -119,14 +113,16 @@ devgen commit run --push
|
|
|
119
113
|
| Command | Description |
|
|
120
114
|
| ------------------------------- | ----------------------------------------------------------------------------- |
|
|
121
115
|
| `devgen setup config` | Interactive provider / API key / options wizard |
|
|
116
|
+
| `devgen config set` | Same wizard, available under `config` |
|
|
117
|
+
| `devgen config list` | Print the current configuration |
|
|
118
|
+
| `devgen config edit <key> [v]` | Update a single configuration value |
|
|
122
119
|
| `devgen commit run` | Stage, generate message, commit (optionally `--push`, `--check`, `--dry-run`) |
|
|
123
120
|
| `devgen commit undo` | Undo the last AI commit, keep changes staged |
|
|
124
|
-
| `devgen changelog`
|
|
125
|
-
| `devgen release
|
|
121
|
+
| `devgen changelog generate` | Generate a Conventional Commits changelog from recent history |
|
|
122
|
+
| `devgen release notes` | Generate release notes for a version range |
|
|
126
123
|
| `devgen gitignore list` | List available GitHub `.gitignore` templates |
|
|
127
|
-
| `devgen gitignore
|
|
128
|
-
| `devgen license
|
|
129
|
-
| `devgen license add <spdx>` | Add a LICENSE file to the current project |
|
|
124
|
+
| `devgen gitignore generate …` | Add `.gitignore` entries to the current project |
|
|
125
|
+
| `devgen license generate` | Interactively add a LICENSE file to the current project |
|
|
130
126
|
|
|
131
127
|
Run `devgen <command> --help` for full options on any subcommand.
|
|
132
128
|
|
|
@@ -28,7 +28,7 @@ DevGen is a CLI for the parts of development that should be invisible. It writes
|
|
|
28
28
|
- **Conventional Changelogs** — `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, `chore`, `style`, and a `BREAKING CHANGES` section.
|
|
29
29
|
- **Project Scaffolding** — Pull `.gitignore` from GitHub's collection and drop in SPDX license files (MIT, Apache-2.0, GPL-3.0, AGPL-3.0, BSD, MPL-2.0, …).
|
|
30
30
|
- **Custom Templates** — Override the commit-message prompt via a `.tpl` file in your config.
|
|
31
|
-
- **Interactive Setup** — `devgen setup` walks you through provider + API key + options.
|
|
31
|
+
- **Interactive Setup** — `devgen setup config` walks you through provider + API key + options.
|
|
32
32
|
- **Undo Support** — `devgen commit undo` rolls back the last AI commit while keeping changes staged.
|
|
33
33
|
- **Token-Limit Aware** — When a diff exceeds a model's context window, you get a single actionable error with `--max-groups` and `--max-diff-size` hints.
|
|
34
34
|
|
|
@@ -83,14 +83,16 @@ devgen commit run --push
|
|
|
83
83
|
| Command | Description |
|
|
84
84
|
| ------------------------------- | ----------------------------------------------------------------------------- |
|
|
85
85
|
| `devgen setup config` | Interactive provider / API key / options wizard |
|
|
86
|
+
| `devgen config set` | Same wizard, available under `config` |
|
|
87
|
+
| `devgen config list` | Print the current configuration |
|
|
88
|
+
| `devgen config edit <key> [v]` | Update a single configuration value |
|
|
86
89
|
| `devgen commit run` | Stage, generate message, commit (optionally `--push`, `--check`, `--dry-run`) |
|
|
87
90
|
| `devgen commit undo` | Undo the last AI commit, keep changes staged |
|
|
88
|
-
| `devgen changelog`
|
|
89
|
-
| `devgen release
|
|
91
|
+
| `devgen changelog generate` | Generate a Conventional Commits changelog from recent history |
|
|
92
|
+
| `devgen release notes` | Generate release notes for a version range |
|
|
90
93
|
| `devgen gitignore list` | List available GitHub `.gitignore` templates |
|
|
91
|
-
| `devgen gitignore
|
|
92
|
-
| `devgen license
|
|
93
|
-
| `devgen license add <spdx>` | Add a LICENSE file to the current project |
|
|
94
|
+
| `devgen gitignore generate …` | Add `.gitignore` entries to the current project |
|
|
95
|
+
| `devgen license generate` | Interactively add a LICENSE file to the current project |
|
|
94
96
|
|
|
95
97
|
Run `devgen <command> --help` for full options on any subcommand.
|
|
96
98
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Provider registry and dispatch for AI generation."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from devgen.providers.base import BaseProvider
|
|
7
|
+
|
|
8
|
+
# Provider name -> (module, class). Keeping the registry explicit makes
|
|
9
|
+
# adding a provider a one-line change and avoids guessing class names.
|
|
10
|
+
PROVIDERS: dict[str, tuple[str, str]] = {
|
|
11
|
+
"gemini": ("devgen.providers.gemini", "GeminiProvider"),
|
|
12
|
+
"openai": ("devgen.providers.openai", "OpenaiProvider"),
|
|
13
|
+
"anthropic": ("devgen.providers.anthropic", "AnthropicProvider"),
|
|
14
|
+
"openrouter": ("devgen.providers.openrouter", "OpenrouterProvider"),
|
|
15
|
+
"huggingface": ("devgen.providers.huggingface", "HuggingfaceProvider"),
|
|
16
|
+
"ollama": ("devgen.providers.ollama", "OllamaProvider"),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _provider_class(name: str) -> type[BaseProvider]:
|
|
21
|
+
try:
|
|
22
|
+
module_name, class_name = PROVIDERS[name.lower()]
|
|
23
|
+
except KeyError as error:
|
|
24
|
+
available = ", ".join(sorted(PROVIDERS))
|
|
25
|
+
raise ValueError(
|
|
26
|
+
f"Unknown provider `{name}`. Available providers: {available}."
|
|
27
|
+
) from error
|
|
28
|
+
|
|
29
|
+
return getattr(import_module(module_name), class_name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_with_ai(
|
|
33
|
+
prompt: str,
|
|
34
|
+
provider: str = "gemini",
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
model: str | None = None,
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Generate text with the requested provider.
|
|
40
|
+
|
|
41
|
+
``model=None`` lets the provider pick its own default model.
|
|
42
|
+
"""
|
|
43
|
+
return _provider_class(provider)().generate(
|
|
44
|
+
prompt,
|
|
45
|
+
api_key=api_key,
|
|
46
|
+
model=model,
|
|
47
|
+
**kwargs,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["PROVIDERS", "generate_with_ai"]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
1
4
|
import typer
|
|
2
|
-
from typing_extensions import Annotated
|
|
3
5
|
|
|
4
6
|
from devgen.modules.changelog_generator import ChangelogGenerator
|
|
5
7
|
|
|
@@ -30,9 +32,8 @@ def generate_changelog(
|
|
|
30
32
|
),
|
|
31
33
|
] = "",
|
|
32
34
|
) -> None:
|
|
33
|
-
generator = ChangelogGenerator()
|
|
34
35
|
try:
|
|
35
|
-
|
|
36
|
-
except
|
|
37
|
-
typer.secho(f"Error generating changelog: {
|
|
36
|
+
ChangelogGenerator().run(output_file=output, from_ref=from_ref)
|
|
37
|
+
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
|
|
38
|
+
typer.secho(f"Error generating changelog: {error}", fg=typer.colors.RED)
|
|
38
39
|
raise typer.Exit(code=1)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import subprocess
|
|
1
2
|
from typing import Annotated
|
|
2
3
|
|
|
3
4
|
import questionary
|
|
@@ -11,6 +12,7 @@ from devgen.utils import (
|
|
|
11
12
|
get_git_staged_files,
|
|
12
13
|
get_main_log_path,
|
|
13
14
|
read_file_content,
|
|
15
|
+
run_git_command,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
app = typer.Typer(
|
|
@@ -78,8 +80,9 @@ def run_commit(
|
|
|
78
80
|
logger = configure_logger("devgen.cli.commit", log_file, console=debug)
|
|
79
81
|
logger.info(f"Log file: {log_file}")
|
|
80
82
|
logger.info(
|
|
81
|
-
f"Options: dry_run={dry_run}, push={push}, debug={debug},
|
|
82
|
-
f"check={check}, max_groups={max_groups},
|
|
83
|
+
f"Options: dry_run={dry_run}, push={push}, debug={debug}, "
|
|
84
|
+
f"force={force_rebuild}, check={check}, max_groups={max_groups}, "
|
|
85
|
+
f"max_diff_size={max_diff_size}"
|
|
83
86
|
)
|
|
84
87
|
|
|
85
88
|
run_commit_engine(
|
|
@@ -117,32 +120,34 @@ def validate() -> None:
|
|
|
117
120
|
staged = get_git_staged_files()
|
|
118
121
|
if staged:
|
|
119
122
|
typer.secho(f"{len(staged)} staged file(s):", fg=typer.colors.GREEN)
|
|
120
|
-
for
|
|
121
|
-
typer.echo(f"- {
|
|
123
|
+
for file in staged:
|
|
124
|
+
typer.echo(f"- {file}")
|
|
122
125
|
else:
|
|
123
126
|
typer.secho("[i] No staged files.", fg=typer.colors.YELLOW)
|
|
124
127
|
|
|
125
128
|
|
|
126
129
|
@app.command("undo")
|
|
127
130
|
def undo_commit() -> None:
|
|
128
|
-
"""
|
|
129
|
-
from devgen.utils import run_git_command
|
|
130
|
-
|
|
131
|
+
"""Undo the last commit but keep the changes staged."""
|
|
131
132
|
try:
|
|
132
|
-
# Check if there's at least one commit
|
|
133
133
|
run_git_command(["git", "rev-parse", "HEAD"])
|
|
134
|
-
except
|
|
134
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
135
135
|
typer.secho("No commits found to undo.", fg=typer.colors.RED)
|
|
136
136
|
return
|
|
137
137
|
|
|
138
|
-
if questionary.confirm(
|
|
138
|
+
if not questionary.confirm(
|
|
139
139
|
"Are you sure you want to undo the last commit? (Changes will remain staged)",
|
|
140
140
|
default=False,
|
|
141
141
|
).ask():
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
run_git_command(["git", "reset", "--soft", "HEAD~1"])
|
|
146
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as error:
|
|
147
|
+
typer.secho(f"Failed to undo commit: {error}", fg=typer.colors.RED)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
typer.secho(
|
|
151
|
+
"Last commit undone. Changes are still staged.",
|
|
152
|
+
fg=typer.colors.GREEN,
|
|
153
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from typing import Annotated, Any
|
|
2
|
+
|
|
3
|
+
import questionary
|
|
4
|
+
import typer
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from devgen.ai import PROVIDERS
|
|
8
|
+
from devgen.cli.wizard import ask, run_config_wizard
|
|
9
|
+
from devgen.utils import get_questionary_style, load_config, save_config
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
name="config",
|
|
13
|
+
help="🔧 Manage configuration values.",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
rich_markup_mode="markdown",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_TRUE_VALUES = {"true", "yes", "1"}
|
|
19
|
+
_FALSE_VALUES = {"false", "no", "0"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _coerce(value: str) -> Any:
|
|
23
|
+
"""Convert common string values to bool/int; leave everything else alone."""
|
|
24
|
+
normalized = value.strip().lower()
|
|
25
|
+
if normalized in _TRUE_VALUES:
|
|
26
|
+
return True
|
|
27
|
+
if normalized in _FALSE_VALUES:
|
|
28
|
+
return False
|
|
29
|
+
if normalized.lstrip("-").isdigit():
|
|
30
|
+
return int(normalized)
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("list")
|
|
35
|
+
def list_config() -> None:
|
|
36
|
+
"""Show all configuration values."""
|
|
37
|
+
typer.echo(yaml.safe_dump(load_config(), sort_keys=False))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command("edit")
|
|
41
|
+
def edit_config(
|
|
42
|
+
key: Annotated[str | None, typer.Argument(help="Configuration key.")] = None,
|
|
43
|
+
value: Annotated[str | None, typer.Argument(help="New value.")] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Edit or add a single configuration value."""
|
|
46
|
+
config = load_config()
|
|
47
|
+
style = get_questionary_style()
|
|
48
|
+
|
|
49
|
+
if not key:
|
|
50
|
+
key = ask(
|
|
51
|
+
questionary.select(
|
|
52
|
+
"Select key to update:",
|
|
53
|
+
choices=list(config),
|
|
54
|
+
style=style,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if value is None:
|
|
59
|
+
current = config.get(key)
|
|
60
|
+
if key == "provider":
|
|
61
|
+
value = ask(
|
|
62
|
+
questionary.select(
|
|
63
|
+
"Select AI Provider:",
|
|
64
|
+
choices=sorted(PROVIDERS),
|
|
65
|
+
default=str(current or "gemini"),
|
|
66
|
+
style=style,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
elif isinstance(current, bool):
|
|
70
|
+
value = ask(
|
|
71
|
+
questionary.select(
|
|
72
|
+
f"Select value for '{key}':",
|
|
73
|
+
choices=["True", "False"],
|
|
74
|
+
default=str(current),
|
|
75
|
+
style=style,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
value = ask(
|
|
80
|
+
questionary.text(
|
|
81
|
+
f"Enter value for '{key}':",
|
|
82
|
+
default=str(current) if current is not None else "",
|
|
83
|
+
style=style,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
config[key] = _coerce(value)
|
|
88
|
+
try:
|
|
89
|
+
save_config(config)
|
|
90
|
+
except OSError as error:
|
|
91
|
+
typer.secho(f"❌ Failed to save config: {error}", fg=typer.colors.RED)
|
|
92
|
+
raise typer.Exit(code=1)
|
|
93
|
+
typer.secho(f" Set '{key}' to '{config[key]}'", fg=typer.colors.GREEN)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("set")
|
|
97
|
+
def set_config() -> None:
|
|
98
|
+
"""Run the interactive configuration wizard."""
|
|
99
|
+
run_config_wizard()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command("info")
|
|
103
|
+
def config_info() -> None:
|
|
104
|
+
"""Show information about configuration options and templates."""
|
|
105
|
+
from rich.console import Console
|
|
106
|
+
from rich.table import Table
|
|
107
|
+
|
|
108
|
+
console = Console()
|
|
109
|
+
table = Table(title="Custom Template Variables", box=None)
|
|
110
|
+
table.add_column("Variable", style="cyan")
|
|
111
|
+
table.add_column("Description", style="white")
|
|
112
|
+
|
|
113
|
+
table.add_row("{{ group_name }}", "The folder name being committed (or 'root').")
|
|
114
|
+
table.add_row("{{ diff_text }}", "The git diff of the changes.")
|
|
115
|
+
table.add_row("{{ context }}", "Project context (manifest files content).")
|
|
116
|
+
|
|
117
|
+
console.print(table)
|
|
118
|
+
console.print(
|
|
119
|
+
"\n[bold]Tip:[/bold] If you hardcode emojis in your template, the AI will "
|
|
120
|
+
"likely include them regardless of the 'emoji' setting.",
|
|
121
|
+
style="yellow",
|
|
122
|
+
)
|
|
123
|
+
console.print("\n[bold]Example Template:[/bold]")
|
|
124
|
+
console.print(
|
|
125
|
+
"custom_template: |\n [type]: [desc]\n \n Diff: {{ diff_text }}\n",
|
|
126
|
+
style="dim",
|
|
127
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import questionary
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from devgen.cli.wizard import ask
|
|
7
|
+
from devgen.modules.gitignore_generator import GitignoreGenerator
|
|
8
|
+
from devgen.utils import get_questionary_style
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="gitignore",
|
|
12
|
+
help="🙈 Generate .gitignore files from GitHub templates.",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
rich_markup_mode="markdown",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _select_templates(
|
|
19
|
+
generator: GitignoreGenerator,
|
|
20
|
+
offline: bool,
|
|
21
|
+
) -> list[str]:
|
|
22
|
+
"""Interactively build a list of templates to include."""
|
|
23
|
+
if offline:
|
|
24
|
+
available = generator.list_cached_templates()
|
|
25
|
+
else:
|
|
26
|
+
typer.secho("Fetching templates list...", fg=typer.colors.YELLOW)
|
|
27
|
+
available = generator.list_available_templates()
|
|
28
|
+
|
|
29
|
+
if not available:
|
|
30
|
+
typer.secho(" No templates available.", fg=typer.colors.RED)
|
|
31
|
+
raise typer.Exit(code=1)
|
|
32
|
+
|
|
33
|
+
selected: list[str] = []
|
|
34
|
+
style = get_questionary_style()
|
|
35
|
+
|
|
36
|
+
while True:
|
|
37
|
+
choices = [
|
|
38
|
+
"Done (Finish selection)",
|
|
39
|
+
*(name for name in available if name not in selected),
|
|
40
|
+
]
|
|
41
|
+
choice = ask(
|
|
42
|
+
questionary.autocomplete(
|
|
43
|
+
"Search for a template (type to search, select 'Done' to finish):",
|
|
44
|
+
choices=choices,
|
|
45
|
+
ignore_case=True,
|
|
46
|
+
match_middle=True,
|
|
47
|
+
validate=lambda value, options=choices: value in options or value == "",
|
|
48
|
+
style=style,
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if not choice or choice.startswith("Done"):
|
|
53
|
+
return selected
|
|
54
|
+
|
|
55
|
+
selected.append(choice)
|
|
56
|
+
typer.secho(f"➕ Added '{choice}'", fg=typer.colors.GREEN)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("list")
|
|
60
|
+
def list_templates(
|
|
61
|
+
cached: Annotated[
|
|
62
|
+
bool,
|
|
63
|
+
typer.Option("--cached", "-c", help="List only cached templates."),
|
|
64
|
+
] = False,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""List available gitignore templates."""
|
|
67
|
+
generator = GitignoreGenerator()
|
|
68
|
+
try:
|
|
69
|
+
if cached:
|
|
70
|
+
templates = generator.list_cached_templates()
|
|
71
|
+
typer.secho(" Cached Templates:", fg=typer.colors.CYAN, bold=True)
|
|
72
|
+
else:
|
|
73
|
+
typer.secho("Fetching available templates...", fg=typer.colors.YELLOW)
|
|
74
|
+
templates = generator.list_available_templates()
|
|
75
|
+
typer.secho(" Available Templates:", fg=typer.colors.CYAN, bold=True)
|
|
76
|
+
except RuntimeError as error:
|
|
77
|
+
typer.secho(f" Error: {error}", fg=typer.colors.RED)
|
|
78
|
+
raise typer.Exit(code=1)
|
|
79
|
+
|
|
80
|
+
typer.echo(", ".join(templates) if templates else "No templates found.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command("generate")
|
|
84
|
+
def generate_gitignore(
|
|
85
|
+
templates: Annotated[
|
|
86
|
+
list[str] | None,
|
|
87
|
+
typer.Argument(
|
|
88
|
+
help="Names of templates to include (e.g. Python Node). "
|
|
89
|
+
"If empty, interactive mode is used."
|
|
90
|
+
),
|
|
91
|
+
] = None,
|
|
92
|
+
output: Annotated[
|
|
93
|
+
str,
|
|
94
|
+
typer.Option("--output", "-o", help="Output file path."),
|
|
95
|
+
] = ".gitignore",
|
|
96
|
+
append: Annotated[
|
|
97
|
+
bool,
|
|
98
|
+
typer.Option(
|
|
99
|
+
"--append/--overwrite",
|
|
100
|
+
"-a/-w",
|
|
101
|
+
help="Append to existing file or overwrite.",
|
|
102
|
+
),
|
|
103
|
+
] = True,
|
|
104
|
+
offline: Annotated[
|
|
105
|
+
bool,
|
|
106
|
+
typer.Option("--offline", help="Use only cached templates."),
|
|
107
|
+
] = False,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Generate a .gitignore file."""
|
|
110
|
+
generator = GitignoreGenerator()
|
|
111
|
+
|
|
112
|
+
if not templates:
|
|
113
|
+
try:
|
|
114
|
+
templates = _select_templates(generator, offline)
|
|
115
|
+
except typer.Exit:
|
|
116
|
+
raise
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
typer.secho("\nCancelled by user", fg=typer.colors.YELLOW)
|
|
119
|
+
raise typer.Exit()
|
|
120
|
+
except (OSError, RuntimeError) as error:
|
|
121
|
+
typer.secho(f"❌ Error fetching templates: {error}", fg=typer.colors.RED)
|
|
122
|
+
raise typer.Exit(code=1)
|
|
123
|
+
|
|
124
|
+
if not templates:
|
|
125
|
+
typer.secho("No templates selected.", fg=typer.colors.YELLOW)
|
|
126
|
+
raise typer.Exit()
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
generator.generate(
|
|
130
|
+
templates,
|
|
131
|
+
output_file=output,
|
|
132
|
+
append=append,
|
|
133
|
+
offline=offline,
|
|
134
|
+
)
|
|
135
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
136
|
+
typer.secho(f"❌ Error generating .gitignore: {error}", fg=typer.colors.RED)
|
|
137
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from devgen.cli.wizard import ask
|
|
9
|
+
from devgen.modules.license_generator import LicenseGenerator
|
|
10
|
+
from devgen.utils import get_questionary_style
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="license",
|
|
14
|
+
help="📄 Generate open source licenses.",
|
|
15
|
+
no_args_is_help=True,
|
|
16
|
+
rich_markup_mode="markdown",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("generate")
|
|
21
|
+
def generate_license(
|
|
22
|
+
output: Annotated[
|
|
23
|
+
str,
|
|
24
|
+
typer.Option(
|
|
25
|
+
"--output",
|
|
26
|
+
"-o",
|
|
27
|
+
help="Output file path.",
|
|
28
|
+
),
|
|
29
|
+
] = "LICENSE",
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Interactively generate a license file."""
|
|
32
|
+
generator = LicenseGenerator()
|
|
33
|
+
licenses = generator.list_licenses()
|
|
34
|
+
|
|
35
|
+
if not licenses:
|
|
36
|
+
typer.secho(" No license templates found.", fg=typer.colors.RED)
|
|
37
|
+
raise typer.Exit(code=1)
|
|
38
|
+
|
|
39
|
+
style = get_questionary_style()
|
|
40
|
+
choices = [
|
|
41
|
+
questionary.Choice(
|
|
42
|
+
title=f"{license_info['name']} ({license_info['key']})",
|
|
43
|
+
value=license_info["key"],
|
|
44
|
+
description=license_info["description"],
|
|
45
|
+
)
|
|
46
|
+
for license_info in licenses
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
license_key = ask(
|
|
50
|
+
questionary.select(
|
|
51
|
+
"Select a License:",
|
|
52
|
+
choices=choices,
|
|
53
|
+
use_indicator=True,
|
|
54
|
+
use_shortcuts=True,
|
|
55
|
+
style=style,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
author = ask(questionary.text("Enter Author Name:", style=style)).strip()
|
|
60
|
+
if not author:
|
|
61
|
+
typer.secho(" Author name is required.", fg=typer.colors.RED)
|
|
62
|
+
raise typer.Exit(code=1)
|
|
63
|
+
|
|
64
|
+
year = ask(
|
|
65
|
+
questionary.text(
|
|
66
|
+
"Enter Year:",
|
|
67
|
+
default=str(datetime.now().year),
|
|
68
|
+
style=style,
|
|
69
|
+
)
|
|
70
|
+
).strip()
|
|
71
|
+
|
|
72
|
+
output_path = Path(output)
|
|
73
|
+
if output_path.exists():
|
|
74
|
+
overwrite = ask(
|
|
75
|
+
questionary.confirm(
|
|
76
|
+
f"File {output} already exists. Overwrite?",
|
|
77
|
+
default=False,
|
|
78
|
+
style=style,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
if not overwrite:
|
|
82
|
+
typer.secho("Operation cancelled.", fg=typer.colors.YELLOW)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
content = generator.render_license(license_key, author, year)
|
|
87
|
+
output_path.write_text(content, encoding="utf-8")
|
|
88
|
+
except (OSError, ValueError) as error:
|
|
89
|
+
typer.secho(f"\n Failed to generate license: {error}", fg=typer.colors.RED)
|
|
90
|
+
raise typer.Exit(code=1)
|
|
91
|
+
|
|
92
|
+
typer.secho(f"\n License generated at {output_path}", fg=typer.colors.GREEN)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from devgen.cli.changelog import app as changelog_app
|
|
7
|
+
from devgen.cli.commit import app as commit_app
|
|
8
|
+
from devgen.cli.config import app as config_app
|
|
9
|
+
from devgen.cli.gitignore import app as gitignore_app
|
|
10
|
+
from devgen.cli.license import app as license_app
|
|
11
|
+
from devgen.cli.release import app as release_app
|
|
12
|
+
from devgen.cli.setup import app as setup_app
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="devgen",
|
|
16
|
+
help="devgen-py: AI-Powered Git Commit & Release Automation.",
|
|
17
|
+
add_completion=True,
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
rich_markup_mode="markdown",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _version_callback(value: bool) -> None:
|
|
24
|
+
"""Print the installed package version and exit."""
|
|
25
|
+
if not value:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
package_version = version("devgen-cli")
|
|
30
|
+
except PackageNotFoundError:
|
|
31
|
+
package_version = "unknown"
|
|
32
|
+
|
|
33
|
+
typer.echo(f"devgen version: {package_version}")
|
|
34
|
+
raise typer.Exit()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.callback()
|
|
38
|
+
def main_callback(
|
|
39
|
+
version: Annotated[
|
|
40
|
+
bool | None,
|
|
41
|
+
typer.Option(
|
|
42
|
+
"--version",
|
|
43
|
+
"-v",
|
|
44
|
+
callback=_version_callback,
|
|
45
|
+
is_eager=True,
|
|
46
|
+
help="Show the application version and exit.",
|
|
47
|
+
),
|
|
48
|
+
] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""DevGen command-line entry point."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
app.add_typer(commit_app, name="commit")
|
|
54
|
+
app.add_typer(setup_app, name="setup")
|
|
55
|
+
app.add_typer(changelog_app, name="changelog")
|
|
56
|
+
app.add_typer(license_app, name="license")
|
|
57
|
+
app.add_typer(gitignore_app, name="gitignore")
|
|
58
|
+
app.add_typer(release_app, name="release")
|
|
59
|
+
app.add_typer(config_app, name="config")
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
try:
|
|
63
|
+
app()
|
|
64
|
+
except KeyboardInterrupt:
|
|
65
|
+
print("\n\n Operation cancelled by user.")
|
|
66
|
+
raise typer.Exit(code=130)
|