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.
Files changed (58) hide show
  1. {devgen_cli-0.2.8/devgen_cli.egg-info → devgen_cli-0.2.9}/PKG-INFO +1 -2
  2. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/commit.py +1 -1
  3. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/config.py +6 -0
  4. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/setup.py +10 -7
  5. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/changelog_generator.py +8 -4
  6. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/commit_generator.py +15 -25
  7. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/diff_builder.py +3 -6
  8. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/git_ops.py +2 -2
  9. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/gitignore_generator.py +9 -0
  10. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/anthropic.py +2 -0
  11. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/huggingface.py +3 -1
  12. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/ollama.py +13 -9
  13. devgen_cli-0.2.9/devgen/providers/openai.py +25 -0
  14. devgen_cli-0.2.9/devgen/providers/openrouter.py +30 -0
  15. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/utils.py +78 -24
  16. {devgen_cli-0.2.8 → devgen_cli-0.2.9/devgen_cli.egg-info}/PKG-INFO +1 -2
  17. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/requires.txt +0 -1
  18. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/pyproject.toml +1 -2
  19. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/setup.py +1 -1
  20. devgen_cli-0.2.8/devgen/providers/openai.py +0 -20
  21. devgen_cli-0.2.8/devgen/providers/openrouter.py +0 -25
  22. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/LICENSE +0 -0
  23. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/MANIFEST.in +0 -0
  24. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/README.md +0 -0
  25. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/__init__.py +0 -0
  26. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/ai.py +0 -0
  27. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/__init__.py +0 -0
  28. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/changelog.py +0 -0
  29. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/gitignore.py +0 -0
  30. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/license.py +0 -0
  31. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/main.py +0 -0
  32. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/cli/release.py +0 -0
  33. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/__init__.py +0 -0
  34. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/changelog_sections.py +0 -0
  35. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/license_generator.py +0 -0
  36. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/modules/release_note_generator.py +0 -0
  37. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/__init__.py +0 -0
  38. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/base.py +0 -0
  39. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/providers/gemini.py +0 -0
  40. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/commit/commit_message.tpl +0 -0
  41. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/agpl-3.0.json +0 -0
  42. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/apache-2.0.json +0 -0
  43. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/bsd-2-clause.json +0 -0
  44. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/bsd-3-clause.json +0 -0
  45. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/bsl-1.0.json +0 -0
  46. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/cc0-1.0.json +0 -0
  47. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/epl-2.0.json +0 -0
  48. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/gpl-2.0.json +0 -0
  49. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/gpl-3.0.json +0 -0
  50. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/lgpl-2.1.json +0 -0
  51. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/mit.json +0 -0
  52. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/mpl-2.0.json +0 -0
  53. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen/templates/licenses/unlicense.json +0 -0
  54. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/SOURCES.txt +0 -0
  55. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/dependency_links.txt +0 -0
  56. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/entry_points.txt +0 -0
  57. {devgen_cli-0.2.8 → devgen_cli-0.2.9}/devgen_cli.egg-info/top_level.txt +0 -0
  58. {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.8
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
@@ -120,7 +120,7 @@ def validate() -> None:
120
120
  for f in staged:
121
121
  typer.echo(f"- {f}")
122
122
  else:
123
- typer.secho("[i] No staged files.", fg=typer.colors.RED)
123
+ typer.secho("[i] No staged files.", fg=typer.colors.YELLOW)
124
124
 
125
125
 
126
126
  @app.command("undo")
@@ -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 = {**current_config, **{
90
- "provider": provider,
91
- "model": model,
92
- "api_key": api_key,
93
- "emoji": emoji,
94
- "ollama_host": ollama_host,
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
- return run_git_command(cmd).split("\n")
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
- load_template_env,
37
- render_custom_template,
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
- template_env,
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.template_env = template_env
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
- if custom_template:
101
- prompt = render_custom_template(
102
- custom_template,
103
- group_name=group,
104
- diff_text=diff,
105
- use_emoji=use_emoji,
106
- context=manifest_context,
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.template_env = load_template_env("commit")
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.template_env,
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
- if new_key in groups:
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:
@@ -16,4 +16,6 @@ class AnthropicProvider(BaseProvider):
16
16
  max_tokens=1024,
17
17
  messages=[{"role": "user", "content": prompt}],
18
18
  )
19
+ if not message.content:
20
+ raise RuntimeError(f"Anthropic returned no content for model {model!r}.")
19
21
  return message.content[0].text or ""
@@ -20,7 +20,9 @@ class HuggingfaceProvider(BaseProvider):
20
20
  }
21
21
 
22
22
  try:
23
- response = requests.post(api_url, headers=headers, json=payload)
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
- kwargs.pop("debug", None)
25
- kwargs.pop("ollama_host", None)
26
-
27
- client = OpenAI(base_url=base_url, api_key=api_key or "ollama")
28
- response = client.chat.completions.create(
29
- model=model,
30
- messages=[{"role": "user", "content": prompt}],
31
- **kwargs,
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
- path = Path(file_path)
28
- if not path.exists():
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
- path = Path(filepath)
197
- if path.exists():
198
- return path.read_text(encoding="utf-8")
199
- return None
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
- path = Path(filepath)
204
- if path.exists():
205
- path.unlink()
211
+ try:
212
+ Path(filepath).unlink()
206
213
  return True
207
- return False
214
+ except FileNotFoundError:
215
+ return False
208
216
 
209
217
 
210
- def load_template_env(sub_dir: str) -> Environment:
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
- if not template_dir.is_dir():
221
+ template_path = template_dir / name
222
+ if not template_path.is_file():
213
223
  raise RuntimeError(
214
- f"Template directory not found: {template_dir}. "
215
- "The installation may be corrupted."
224
+ f"Template not found: {template_path}. The installation may be corrupted."
216
225
  )
217
- return Environment(loader=FileSystemLoader(template_dir))
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
- def render_custom_template(template_str: str, **context) -> str:
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
- return yaml.safe_load(f) or {}
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
- "load_template_env",
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.8
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
@@ -1,6 +1,5 @@
1
1
  anthropic>=0.75.0
2
2
  google-genai>=0.1.0
3
- jinja2>=3.1.6
4
3
  openai>=2.11.0
5
4
  pyyaml>=6.0.3
6
5
  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.8"
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",
@@ -6,7 +6,7 @@ from setuptools import find_packages, setup
6
6
 
7
7
  setup(
8
8
  name="devgen-cli",
9
- version="0.2.8",
9
+ version="0.2.9",
10
10
  packages=find_packages(),
11
11
  include_package_data=True,
12
12
  install_requires=[
@@ -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