devgen-cli 0.2.8__tar.gz → 0.2.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devgen_cli-0.2.8/devgen_cli.egg-info → devgen_cli-0.2.9}/PKG-INFO +1 -2
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/commit.py +1 -1
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/config.py +6 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/setup.py +10 -7
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/changelog_generator.py +8 -4
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/commit_generator.py +15 -25
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/diff_builder.py +3 -6
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/git_ops.py +2 -2
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/gitignore_generator.py +9 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/anthropic.py +2 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/huggingface.py +3 -1
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/ollama.py +13 -9
- devgen_cli-0.2.9/devgen/providers/openai.py +25 -0
- devgen_cli-0.2.9/devgen/providers/openrouter.py +30 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/utils.py +78 -24
- {devgen_cli-0.2.8 → devgen_cli-0.2.9/devgen_cli.egg-info}/PKG-INFO +1 -2
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/requires.txt +0 -1
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/pyproject.toml +1 -2
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/setup.py +1 -1
- devgen_cli-0.2.8/devgen/providers/openai.py +0 -20
- devgen_cli-0.2.8/devgen/providers/openrouter.py +0 -25
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/LICENSE +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/MANIFEST.in +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/README.md +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/__init__.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/ai.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/__init__.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/changelog.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/gitignore.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/license.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/main.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/release.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/__init__.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/changelog_sections.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/license_generator.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/release_note_generator.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/__init__.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/base.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/gemini.py +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/commit/commit_message.tpl +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/agpl-3.0.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/apache-2.0.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/bsd-2-clause.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/bsd-3-clause.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/bsl-1.0.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/cc0-1.0.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/epl-2.0.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/gpl-2.0.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/gpl-3.0.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/lgpl-2.1.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/mit.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/mpl-2.0.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/unlicense.json +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/SOURCES.txt +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/dependency_links.txt +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/entry_points.txt +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/top_level.txt +0 -0
- {devgen_cli-0.2.8 → devgen_cli-0.2.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devgen-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: A collection of developer tools
|
|
5
5
|
Home-page: https://github.com/S4NKALP/devgen
|
|
6
6
|
Author: Sankalp Tharu
|
|
@@ -22,7 +22,6 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: anthropic>=0.75.0
|
|
24
24
|
Requires-Dist: google-genai>=0.1.0
|
|
25
|
-
Requires-Dist: jinja2>=3.1.6
|
|
26
25
|
Requires-Dist: openai>=2.11.0
|
|
27
26
|
Requires-Dist: pyyaml>=6.0.3
|
|
28
27
|
Requires-Dist: questionary>=2.1.1
|
|
@@ -48,6 +48,12 @@ def edit_config(
|
|
|
48
48
|
|
|
49
49
|
if not key:
|
|
50
50
|
choices = list(config.keys())
|
|
51
|
+
if not choices:
|
|
52
|
+
typer.secho(
|
|
53
|
+
"No configuration keys found. Run `devgen setup config` first.",
|
|
54
|
+
fg=typer.colors.YELLOW,
|
|
55
|
+
)
|
|
56
|
+
raise typer.Exit()
|
|
51
57
|
key = questionary.select(
|
|
52
58
|
"Select key to update:", choices=choices, style=style
|
|
53
59
|
).ask()
|
|
@@ -86,13 +86,16 @@ def setup_config() -> None:
|
|
|
86
86
|
ollama_host = ollama_host_input.strip() or ollama_host
|
|
87
87
|
|
|
88
88
|
# Save Config — merge with existing to preserve extra keys
|
|
89
|
-
new_config = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
new_config = {
|
|
90
|
+
**current_config,
|
|
91
|
+
**{
|
|
92
|
+
"provider": provider,
|
|
93
|
+
"model": model,
|
|
94
|
+
"api_key": api_key,
|
|
95
|
+
"emoji": emoji,
|
|
96
|
+
"ollama_host": ollama_host,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
96
99
|
|
|
97
100
|
try:
|
|
98
101
|
with config_path.open("w", encoding="utf-8") as f:
|
|
@@ -32,17 +32,21 @@ class ChangelogGenerator:
|
|
|
32
32
|
else:
|
|
33
33
|
cmd = self._resolve_range(to_ref)
|
|
34
34
|
try:
|
|
35
|
-
|
|
35
|
+
output = run_git_command(cmd)
|
|
36
|
+
# Use %x00 as separator to avoid splitting on newlines in commit bodies
|
|
37
|
+
# Then filter out empty strings
|
|
38
|
+
return [c for c in output.split("\0") if c.strip()]
|
|
36
39
|
except subprocess.CalledProcessError as e:
|
|
37
40
|
self.logger.error(f"Git command failed: {e}")
|
|
38
41
|
raise RuntimeError(f"Git command failed: {e}") from e
|
|
39
42
|
|
|
40
43
|
def _log_cmd(self, range_spec: str) -> List[str]:
|
|
41
|
-
# Format: hash|author|date|subject|body
|
|
44
|
+
# Format: hash|author|date|subject|body (separated by %x00 to avoid
|
|
45
|
+
# splitting on newlines within commit bodies)
|
|
42
46
|
return [
|
|
43
47
|
"git",
|
|
44
48
|
"log",
|
|
45
|
-
"--format=%H|%an|%ad|%s|%b",
|
|
49
|
+
"--format=%H|%an|%ad|%s|%b%x00",
|
|
46
50
|
"--date=short",
|
|
47
51
|
range_spec,
|
|
48
52
|
]
|
|
@@ -54,7 +58,7 @@ class ChangelogGenerator:
|
|
|
54
58
|
return self._log_cmd(f"{last_tag}..HEAD")
|
|
55
59
|
except (RuntimeError, subprocess.CalledProcessError):
|
|
56
60
|
self.logger.info("No tags found, generating for all commits.")
|
|
57
|
-
return ["git", "log", "--format=%H|%an|%ad|%s|%b", "--date=short"]
|
|
61
|
+
return ["git", "log", "--format=%H|%an|%ad|%s|%b%x00", "--date=short"]
|
|
58
62
|
|
|
59
63
|
# ----------------------------------------------------------------- parsing
|
|
60
64
|
|
|
@@ -33,8 +33,8 @@ from devgen.utils import (
|
|
|
33
33
|
extract_commit_messages,
|
|
34
34
|
get_commit_dry_run_path,
|
|
35
35
|
is_file_recent,
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
load_template,
|
|
37
|
+
render_template,
|
|
38
38
|
sanitize_ai_commit_message,
|
|
39
39
|
)
|
|
40
40
|
|
|
@@ -74,13 +74,13 @@ class CommitMessageBuilder:
|
|
|
74
74
|
|
|
75
75
|
def __init__(
|
|
76
76
|
self,
|
|
77
|
-
|
|
77
|
+
template_str: str,
|
|
78
78
|
config: Dict[str, Any],
|
|
79
79
|
provider: str,
|
|
80
80
|
model: str,
|
|
81
81
|
debug: bool = False,
|
|
82
82
|
) -> None:
|
|
83
|
-
self.
|
|
83
|
+
self.template_str = template_str
|
|
84
84
|
self.config = config
|
|
85
85
|
self.provider = provider
|
|
86
86
|
self.model = model
|
|
@@ -97,25 +97,14 @@ class CommitMessageBuilder:
|
|
|
97
97
|
use_emoji = self.config.get("emoji", True)
|
|
98
98
|
ollama_host = self.config.get("ollama_host")
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
else:
|
|
109
|
-
prompt = (
|
|
110
|
-
self.template_env.get_template("commit_message.tpl")
|
|
111
|
-
.render(
|
|
112
|
-
group_name=group,
|
|
113
|
-
diff_text=diff,
|
|
114
|
-
use_emoji=use_emoji,
|
|
115
|
-
context=manifest_context,
|
|
116
|
-
)
|
|
117
|
-
.strip()
|
|
118
|
-
)
|
|
100
|
+
template = custom_template or self.template_str
|
|
101
|
+
prompt = render_template(
|
|
102
|
+
template,
|
|
103
|
+
group_name=group,
|
|
104
|
+
diff_text=diff,
|
|
105
|
+
use_emoji=use_emoji,
|
|
106
|
+
context=manifest_context,
|
|
107
|
+
).strip()
|
|
119
108
|
|
|
120
109
|
provider = (
|
|
121
110
|
extra_kwargs.get("provider") or self.config.get("provider") or self.provider
|
|
@@ -198,10 +187,10 @@ class CommitEngine:
|
|
|
198
187
|
),
|
|
199
188
|
logger=self.logger,
|
|
200
189
|
)
|
|
201
|
-
self.
|
|
190
|
+
self.template_str = load_template("commit", "commit_message.tpl")
|
|
202
191
|
self.cache = CacheManager(get_commit_dry_run_path())
|
|
203
192
|
self.message_builder = CommitMessageBuilder(
|
|
204
|
-
self.
|
|
193
|
+
self.template_str,
|
|
205
194
|
self.config,
|
|
206
195
|
provider=self.provider,
|
|
207
196
|
model=self.model,
|
|
@@ -388,6 +377,7 @@ def run_commit_engine(**kwargs: Any) -> None:
|
|
|
388
377
|
sys.exit(0)
|
|
389
378
|
except Exception as e:
|
|
390
379
|
logger.error(f"Commit engine failed: {e}", exc_info=True)
|
|
380
|
+
sys.exit(1)
|
|
391
381
|
|
|
392
382
|
|
|
393
383
|
__all__ = ["CommitEngine", "CommitEngineError", "run_commit_engine"]
|
|
@@ -14,7 +14,7 @@ from collections import defaultdict
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Dict, List, Optional
|
|
16
16
|
|
|
17
|
-
from devgen.utils import run_git_command
|
|
17
|
+
from devgen.utils import get_git_root, run_git_command
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
# Lock files and other metadata blobs whose full diff would just bloat the
|
|
@@ -54,7 +54,7 @@ class DiffBuilder:
|
|
|
54
54
|
logger=None,
|
|
55
55
|
) -> None:
|
|
56
56
|
self.max_size = max_size
|
|
57
|
-
self.cwd = cwd
|
|
57
|
+
self.cwd = cwd or get_git_root()
|
|
58
58
|
self.logger = logger
|
|
59
59
|
|
|
60
60
|
def build(self, files: List[str]) -> str:
|
|
@@ -129,10 +129,7 @@ class FileGrouper:
|
|
|
129
129
|
parent_path = str(Path(deepest).parent)
|
|
130
130
|
new_key = "root" if parent_path == "." else parent_path
|
|
131
131
|
files = groups.pop(deepest)
|
|
132
|
-
|
|
133
|
-
groups[new_key].extend(files)
|
|
134
|
-
else:
|
|
135
|
-
groups["root"].extend(files)
|
|
132
|
+
groups.setdefault(new_key, []).extend(files)
|
|
136
133
|
return groups
|
|
137
134
|
|
|
138
135
|
|
|
@@ -10,7 +10,7 @@ import subprocess
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Optional
|
|
12
12
|
|
|
13
|
-
from devgen.utils import configure_logger, run_git_command
|
|
13
|
+
from devgen.utils import configure_logger, get_git_root, run_git_command
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class GitError(RuntimeError):
|
|
@@ -26,7 +26,7 @@ class GitOperator:
|
|
|
26
26
|
logger=None,
|
|
27
27
|
quiet_commands: bool = True,
|
|
28
28
|
) -> None:
|
|
29
|
-
self.cwd = cwd
|
|
29
|
+
self.cwd = cwd or get_git_root()
|
|
30
30
|
self.logger = logger or configure_logger("devgen.git")
|
|
31
31
|
self.quiet_commands = quiet_commands
|
|
32
32
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import List
|
|
3
4
|
|
|
@@ -5,6 +6,9 @@ import requests
|
|
|
5
6
|
|
|
6
7
|
from devgen.utils import configure_logger
|
|
7
8
|
|
|
9
|
+
# Only allow safe characters in template names to prevent path traversal
|
|
10
|
+
_TEMPLATE_NAME_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
|
|
11
|
+
|
|
8
12
|
|
|
9
13
|
class GitignoreGenerator:
|
|
10
14
|
"""Fetches and manages .gitignore templates from GitHub."""
|
|
@@ -41,6 +45,11 @@ class GitignoreGenerator:
|
|
|
41
45
|
self, name: str, use_cache: bool = True, offline: bool = False
|
|
42
46
|
) -> str:
|
|
43
47
|
"""Fetches template content from cache or GitHub."""
|
|
48
|
+
if not _TEMPLATE_NAME_RE.match(name):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Invalid template name '{name}'. "
|
|
51
|
+
"Names must contain only alphanumeric characters, hyphens, dots, or underscores."
|
|
52
|
+
)
|
|
44
53
|
cache_file = self.cache_dir / f"{name}.gitignore"
|
|
45
54
|
|
|
46
55
|
if offline:
|
|
@@ -20,7 +20,9 @@ class HuggingfaceProvider(BaseProvider):
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
|
-
response = requests.post(
|
|
23
|
+
response = requests.post(
|
|
24
|
+
api_url, headers=headers, json=payload, timeout=120
|
|
25
|
+
)
|
|
24
26
|
response.raise_for_status()
|
|
25
27
|
result = response.json()
|
|
26
28
|
except requests.HTTPError as e:
|
|
@@ -18,18 +18,22 @@ class OllamaProvider(BaseProvider):
|
|
|
18
18
|
DEFAULT_HOST = "http://localhost:11434"
|
|
19
19
|
DEFAULT_MODEL = "llama3.2"
|
|
20
20
|
|
|
21
|
+
# Keys consumed by devgen itself — never forwarded to the SDK.
|
|
22
|
+
_DEVGEN_KEYS = frozenset({"debug", "ollama_host", "max_retries", "retry_delay"})
|
|
23
|
+
|
|
21
24
|
def _generate(self, prompt, api_key, model, **kwargs):
|
|
22
25
|
host = kwargs.get("ollama_host") or self.DEFAULT_HOST
|
|
23
26
|
base_url = f"{host.rstrip('/')}/v1"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
|
|
28
|
+
# Only forward known OpenAI-compatible parameters
|
|
29
|
+
safe_kwargs = {k: v for k, v in kwargs.items() if k not in self._DEVGEN_KEYS}
|
|
30
|
+
|
|
31
|
+
with OpenAI(base_url=base_url, api_key=api_key or "ollama") as client:
|
|
32
|
+
response = client.chat.completions.create(
|
|
33
|
+
model=model,
|
|
34
|
+
messages=[{"role": "user", "content": prompt}],
|
|
35
|
+
**safe_kwargs,
|
|
36
|
+
)
|
|
33
37
|
|
|
34
38
|
if not response.choices:
|
|
35
39
|
raise RuntimeError(
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
|
|
3
|
+
from devgen.providers.base import BaseProvider
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OpenaiProvider(BaseProvider):
|
|
7
|
+
"""Generates content using OpenAI's Chat Completions API."""
|
|
8
|
+
|
|
9
|
+
DISPLAY_NAME = "OpenAI"
|
|
10
|
+
DEFAULT_MODEL = "gpt-4o"
|
|
11
|
+
|
|
12
|
+
# Keys consumed by devgen itself — never forwarded to the SDK.
|
|
13
|
+
_DEVGEN_KEYS = frozenset({"debug", "ollama_host", "max_retries", "retry_delay"})
|
|
14
|
+
|
|
15
|
+
def _generate(self, prompt, api_key, model, **kwargs):
|
|
16
|
+
safe_kwargs = {k: v for k, v in kwargs.items() if k not in self._DEVGEN_KEYS}
|
|
17
|
+
with OpenAI(api_key=api_key) as client:
|
|
18
|
+
response = client.chat.completions.create(
|
|
19
|
+
model=model,
|
|
20
|
+
messages=[{"role": "user", "content": prompt}],
|
|
21
|
+
**safe_kwargs,
|
|
22
|
+
)
|
|
23
|
+
if not response.choices:
|
|
24
|
+
raise RuntimeError(f"OpenAI returned no choices for model {model!r}.")
|
|
25
|
+
return response.choices[0].message.content or ""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
|
|
3
|
+
from devgen.providers.base import BaseProvider
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OpenrouterProvider(BaseProvider):
|
|
7
|
+
"""Generates content using OpenRouter (OpenAI-compatible API)."""
|
|
8
|
+
|
|
9
|
+
DISPLAY_NAME = "OpenRouter"
|
|
10
|
+
BASE_URL = "https://openrouter.ai/api/v1"
|
|
11
|
+
DEFAULT_MODEL = "openai/gpt-3.5-turbo"
|
|
12
|
+
|
|
13
|
+
# Keys consumed by devgen itself — never forwarded to the SDK.
|
|
14
|
+
_DEVGEN_KEYS = frozenset({"debug", "ollama_host", "max_retries", "retry_delay"})
|
|
15
|
+
|
|
16
|
+
def _generate(self, prompt, api_key, model, **kwargs):
|
|
17
|
+
safe_kwargs = {k: v for k, v in kwargs.items() if k not in self._DEVGEN_KEYS}
|
|
18
|
+
with OpenAI(base_url=self.BASE_URL, api_key=api_key) as client:
|
|
19
|
+
response = client.chat.completions.create(
|
|
20
|
+
model=model,
|
|
21
|
+
messages=[{"role": "user", "content": prompt}],
|
|
22
|
+
extra_headers={
|
|
23
|
+
"HTTP-Referer": "https://github.com/S4NKALP/devgen",
|
|
24
|
+
"X-Title": "devgen CLI",
|
|
25
|
+
},
|
|
26
|
+
**safe_kwargs,
|
|
27
|
+
)
|
|
28
|
+
if not response.choices:
|
|
29
|
+
raise RuntimeError(f"OpenRouter returned no choices for model {model!r}.")
|
|
30
|
+
return response.choices[0].message.content or ""
|
|
@@ -6,7 +6,6 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any, Dict, Optional
|
|
7
7
|
|
|
8
8
|
import yaml
|
|
9
|
-
from jinja2 import Environment, FileSystemLoader
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
def ensure_log_directory() -> Path:
|
|
@@ -24,10 +23,10 @@ def get_commit_dry_run_path() -> Path:
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
def is_file_recent(file_path: Path | str, max_age_minutes: int = 120) -> bool:
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
try:
|
|
27
|
+
return (time.time() - Path(file_path).stat().st_mtime) <= max_age_minutes * 60
|
|
28
|
+
except FileNotFoundError:
|
|
29
29
|
return False
|
|
30
|
-
return (time.time() - path.stat().st_mtime) <= max_age_minutes * 60
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
def sanitize_ai_commit_message(raw_text: str) -> str:
|
|
@@ -142,6 +141,15 @@ def configure_logger(
|
|
|
142
141
|
return logger
|
|
143
142
|
|
|
144
143
|
|
|
144
|
+
def get_git_root(cwd: Optional[Path] = None) -> Optional[Path]:
|
|
145
|
+
"""Return the root directory of the current git repository, or None."""
|
|
146
|
+
try:
|
|
147
|
+
output = run_git_command(["git", "rev-parse", "--show-toplevel"], cwd=cwd)
|
|
148
|
+
return Path(output) if output else None
|
|
149
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
145
153
|
def run_git_command(
|
|
146
154
|
args: list[str],
|
|
147
155
|
check: bool = True,
|
|
@@ -193,35 +201,74 @@ def get_git_staged_files() -> list[str]:
|
|
|
193
201
|
|
|
194
202
|
|
|
195
203
|
def read_file_content(filepath: Path | str) -> Optional[str]:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
try:
|
|
205
|
+
return Path(filepath).read_text(encoding="utf-8")
|
|
206
|
+
except FileNotFoundError:
|
|
207
|
+
return None
|
|
200
208
|
|
|
201
209
|
|
|
202
210
|
def delete_file(filepath: Path | str) -> bool:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
path.unlink()
|
|
211
|
+
try:
|
|
212
|
+
Path(filepath).unlink()
|
|
206
213
|
return True
|
|
207
|
-
|
|
214
|
+
except FileNotFoundError:
|
|
215
|
+
return False
|
|
208
216
|
|
|
209
217
|
|
|
210
|
-
def
|
|
218
|
+
def load_template(sub_dir: str, name: str) -> str:
|
|
219
|
+
"""Load a template file from the templates directory."""
|
|
211
220
|
template_dir = Path(__file__).parent / "templates" / sub_dir
|
|
212
|
-
|
|
221
|
+
template_path = template_dir / name
|
|
222
|
+
if not template_path.is_file():
|
|
213
223
|
raise RuntimeError(
|
|
214
|
-
f"Template
|
|
215
|
-
"The installation may be corrupted."
|
|
224
|
+
f"Template not found: {template_path}. The installation may be corrupted."
|
|
216
225
|
)
|
|
217
|
-
return
|
|
226
|
+
return template_path.read_text(encoding="utf-8")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def render_template(template_str: str, **context) -> str:
|
|
230
|
+
"""Render a simple template with variable substitution and conditionals.
|
|
231
|
+
|
|
232
|
+
Supports:
|
|
233
|
+
- {{ variable }} - variable substitution
|
|
234
|
+
- {% if condition %}...{% endif %} - conditional blocks
|
|
235
|
+
- {% if condition %}...{% else %}...{% endif %} - conditional with else
|
|
236
|
+
"""
|
|
237
|
+
result = template_str
|
|
238
|
+
|
|
239
|
+
# Process conditional blocks: {% if var %}...{% endif %}
|
|
240
|
+
# and {% if var %}...{% else %}...{% endif %}
|
|
241
|
+
if_pattern = re.compile(
|
|
242
|
+
r"\{%\s*if\s+(\w+)\s*%\}(.*?)\{%\s*endif\s*%\}",
|
|
243
|
+
re.DOTALL,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def replace_if(match):
|
|
247
|
+
var_name = match.group(1)
|
|
248
|
+
content = match.group(2)
|
|
249
|
+
value = context.get(var_name)
|
|
250
|
+
if value:
|
|
251
|
+
# Check for else block
|
|
252
|
+
else_pattern = re.compile(r"\{%\s*else\s*%\}", re.DOTALL)
|
|
253
|
+
parts = else_pattern.split(content, 1)
|
|
254
|
+
return parts[0]
|
|
255
|
+
else:
|
|
256
|
+
else_pattern = re.compile(r"\{%\s*else\s*%\}", re.DOTALL)
|
|
257
|
+
parts = else_pattern.split(content, 1)
|
|
258
|
+
return parts[1] if len(parts) > 1 else ""
|
|
259
|
+
|
|
260
|
+
result = if_pattern.sub(replace_if, result)
|
|
261
|
+
|
|
262
|
+
# Process variable substitutions: {{ variable }}
|
|
263
|
+
var_pattern = re.compile(r"\{\{\s*(\w+)\s*\}\}")
|
|
264
|
+
|
|
265
|
+
def replace_var(match):
|
|
266
|
+
var_name = match.group(1)
|
|
267
|
+
return str(context.get(var_name, ""))
|
|
218
268
|
|
|
269
|
+
result = var_pattern.sub(replace_var, result)
|
|
219
270
|
|
|
220
|
-
|
|
221
|
-
"""Renders a Jinja2 template from a string."""
|
|
222
|
-
env = Environment()
|
|
223
|
-
template = env.from_string(template_str)
|
|
224
|
-
return template.render(**context)
|
|
271
|
+
return result
|
|
225
272
|
|
|
226
273
|
|
|
227
274
|
def load_config() -> Dict[str, Any]:
|
|
@@ -246,7 +293,13 @@ def load_config() -> Dict[str, Any]:
|
|
|
246
293
|
|
|
247
294
|
try:
|
|
248
295
|
with config_path.open("r", encoding="utf-8") as f:
|
|
249
|
-
|
|
296
|
+
data = yaml.safe_load(f)
|
|
297
|
+
if not isinstance(data, dict):
|
|
298
|
+
print(
|
|
299
|
+
f"Warning: Config at {config_path} is not a dict, using defaults."
|
|
300
|
+
)
|
|
301
|
+
return {}
|
|
302
|
+
return data
|
|
250
303
|
except Exception as e:
|
|
251
304
|
print(f"Warning: Failed to load config from {config_path}: {e}")
|
|
252
305
|
return {}
|
|
@@ -323,7 +376,8 @@ __all__ = [
|
|
|
323
376
|
"get_git_staged_files",
|
|
324
377
|
"read_file_content",
|
|
325
378
|
"delete_file",
|
|
326
|
-
"
|
|
379
|
+
"load_template",
|
|
380
|
+
"render_template",
|
|
327
381
|
"load_config",
|
|
328
382
|
"get_questionary_style",
|
|
329
383
|
"is_token_limit_error",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devgen-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: A collection of developer tools
|
|
5
5
|
Home-page: https://github.com/S4NKALP/devgen
|
|
6
6
|
Author: Sankalp Tharu
|
|
@@ -22,7 +22,6 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: anthropic>=0.75.0
|
|
24
24
|
Requires-Dist: google-genai>=0.1.0
|
|
25
|
-
Requires-Dist: jinja2>=3.1.6
|
|
26
25
|
Requires-Dist: openai>=2.11.0
|
|
27
26
|
Requires-Dist: pyyaml>=6.0.3
|
|
28
27
|
Requires-Dist: questionary>=2.1.1
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devgen-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.9"
|
|
8
8
|
description = "A collection of developer tools"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -27,7 +27,6 @@ classifiers = [
|
|
|
27
27
|
dependencies = [
|
|
28
28
|
"anthropic>=0.75.0",
|
|
29
29
|
"google-genai>=0.1.0",
|
|
30
|
-
"jinja2>=3.1.6",
|
|
31
30
|
"openai>=2.11.0",
|
|
32
31
|
"pyyaml>=6.0.3",
|
|
33
32
|
"questionary>=2.1.1",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from openai import OpenAI
|
|
2
|
-
|
|
3
|
-
from devgen.providers.base import BaseProvider
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class OpenaiProvider(BaseProvider):
|
|
7
|
-
"""Generates content using OpenAI's Chat Completions API."""
|
|
8
|
-
|
|
9
|
-
DISPLAY_NAME = "OpenAI"
|
|
10
|
-
DEFAULT_MODEL = "gpt-4o"
|
|
11
|
-
|
|
12
|
-
def _generate(self, prompt, api_key, model, **kwargs):
|
|
13
|
-
kwargs.pop("debug", None)
|
|
14
|
-
client = OpenAI(api_key=api_key)
|
|
15
|
-
response = client.chat.completions.create(
|
|
16
|
-
model=model,
|
|
17
|
-
messages=[{"role": "user", "content": prompt}],
|
|
18
|
-
**kwargs,
|
|
19
|
-
)
|
|
20
|
-
return response.choices[0].message.content or ""
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from openai import OpenAI
|
|
2
|
-
|
|
3
|
-
from devgen.providers.base import BaseProvider
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class OpenrouterProvider(BaseProvider):
|
|
7
|
-
"""Generates content using OpenRouter (OpenAI-compatible API)."""
|
|
8
|
-
|
|
9
|
-
DISPLAY_NAME = "OpenRouter"
|
|
10
|
-
BASE_URL = "https://openrouter.ai/api/v1"
|
|
11
|
-
DEFAULT_MODEL = "openai/gpt-3.5-turbo"
|
|
12
|
-
|
|
13
|
-
def _generate(self, prompt, api_key, model, **kwargs):
|
|
14
|
-
kwargs.pop("debug", None)
|
|
15
|
-
client = OpenAI(base_url=self.BASE_URL, api_key=api_key)
|
|
16
|
-
response = client.chat.completions.create(
|
|
17
|
-
model=model,
|
|
18
|
-
messages=[{"role": "user", "content": prompt}],
|
|
19
|
-
extra_headers={
|
|
20
|
-
"HTTP-Referer": "https://github.com/S4NKALP/devgen",
|
|
21
|
-
"X-Title": "devgen CLI",
|
|
22
|
-
},
|
|
23
|
-
**kwargs,
|
|
24
|
-
)
|
|
25
|
-
return response.choices[0].message.content
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|