devgen-cli 0.2.9__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.
Files changed (79) hide show
  1. {devgen_cli-0.2.9/devgen_cli.egg-info → devgen_cli-0.3.1}/PKG-INFO +19 -23
  2. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/README.md +8 -6
  3. devgen_cli-0.3.1/devgen/ai.py +51 -0
  4. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/cli/changelog.py +6 -5
  5. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/cli/commit.py +22 -17
  6. devgen_cli-0.3.1/devgen/cli/config.py +127 -0
  7. devgen_cli-0.3.1/devgen/cli/gitignore.py +137 -0
  8. devgen_cli-0.3.1/devgen/cli/license.py +92 -0
  9. devgen_cli-0.3.1/devgen/cli/main.py +66 -0
  10. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/cli/release.py +13 -4
  11. devgen_cli-0.3.1/devgen/cli/setup.py +16 -0
  12. devgen_cli-0.3.1/devgen/cli/wizard.py +88 -0
  13. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/modules/changelog_generator.py +78 -63
  14. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/modules/changelog_sections.py +1 -1
  15. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/modules/commit_generator.py +125 -148
  16. devgen_cli-0.3.1/devgen/modules/diff_builder.py +232 -0
  17. devgen_cli-0.3.1/devgen/modules/git_ops.py +109 -0
  18. devgen_cli-0.3.1/devgen/modules/gitignore_generator.py +121 -0
  19. devgen_cli-0.3.1/devgen/modules/license_generator.py +65 -0
  20. devgen_cli-0.3.1/devgen/modules/release_note_generator.py +44 -0
  21. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/providers/anthropic.py +10 -2
  22. devgen_cli-0.3.1/devgen/providers/base.py +120 -0
  23. devgen_cli-0.3.1/devgen/providers/gemini.py +33 -0
  24. devgen_cli-0.3.1/devgen/providers/huggingface.py +64 -0
  25. devgen_cli-0.3.1/devgen/providers/ollama.py +30 -0
  26. devgen_cli-0.3.1/devgen/providers/openai.py +8 -0
  27. devgen_cli-0.3.1/devgen/providers/openai_compatible.py +46 -0
  28. devgen_cli-0.3.1/devgen/providers/openrouter.py +15 -0
  29. devgen_cli-0.3.1/devgen/templates/commit/commit_message.tpl +29 -0
  30. devgen_cli-0.3.1/devgen/utils.py +357 -0
  31. {devgen_cli-0.2.9 → devgen_cli-0.3.1/devgen_cli.egg-info}/PKG-INFO +19 -23
  32. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen_cli.egg-info/SOURCES.txt +2 -0
  33. devgen_cli-0.3.1/devgen_cli.egg-info/requires.txt +9 -0
  34. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/pyproject.toml +14 -15
  35. devgen_cli-0.3.1/setup.py +5 -0
  36. devgen_cli-0.2.9/devgen/ai.py +0 -37
  37. devgen_cli-0.2.9/devgen/cli/config.py +0 -228
  38. devgen_cli-0.2.9/devgen/cli/gitignore.py +0 -138
  39. devgen_cli-0.2.9/devgen/cli/license.py +0 -101
  40. devgen_cli-0.2.9/devgen/cli/main.py +0 -109
  41. devgen_cli-0.2.9/devgen/cli/setup.py +0 -107
  42. devgen_cli-0.2.9/devgen/modules/diff_builder.py +0 -221
  43. devgen_cli-0.2.9/devgen/modules/git_ops.py +0 -152
  44. devgen_cli-0.2.9/devgen/modules/gitignore_generator.py +0 -125
  45. devgen_cli-0.2.9/devgen/modules/license_generator.py +0 -78
  46. devgen_cli-0.2.9/devgen/modules/release_note_generator.py +0 -56
  47. devgen_cli-0.2.9/devgen/providers/base.py +0 -91
  48. devgen_cli-0.2.9/devgen/providers/gemini.py +0 -63
  49. devgen_cli-0.2.9/devgen/providers/huggingface.py +0 -56
  50. devgen_cli-0.2.9/devgen/providers/ollama.py +0 -55
  51. devgen_cli-0.2.9/devgen/providers/openai.py +0 -25
  52. devgen_cli-0.2.9/devgen/providers/openrouter.py +0 -30
  53. devgen_cli-0.2.9/devgen/templates/commit/commit_message.tpl +0 -14
  54. devgen_cli-0.2.9/devgen/utils.py +0 -404
  55. devgen_cli-0.2.9/devgen_cli.egg-info/requires.txt +0 -9
  56. devgen_cli-0.2.9/setup.py +0 -47
  57. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/LICENSE +0 -0
  58. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/MANIFEST.in +0 -0
  59. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/__init__.py +0 -0
  60. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/cli/__init__.py +0 -0
  61. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/modules/__init__.py +0 -0
  62. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/providers/__init__.py +0 -0
  63. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/agpl-3.0.json +0 -0
  64. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/apache-2.0.json +0 -0
  65. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/bsd-2-clause.json +0 -0
  66. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/bsd-3-clause.json +0 -0
  67. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/bsl-1.0.json +0 -0
  68. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/cc0-1.0.json +0 -0
  69. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/epl-2.0.json +0 -0
  70. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/gpl-2.0.json +0 -0
  71. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/gpl-3.0.json +0 -0
  72. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/lgpl-2.1.json +0 -0
  73. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/mit.json +0 -0
  74. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/mpl-2.0.json +0 -0
  75. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen/templates/licenses/unlicense.json +0 -0
  76. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen_cli.egg-info/dependency_links.txt +0 -0
  77. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen_cli.egg-info/entry_points.txt +0 -0
  78. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/devgen_cli.egg-info/top_level.txt +0 -0
  79. {devgen_cli-0.2.9 → devgen_cli-0.3.1}/setup.cfg +0 -0
@@ -1,38 +1,32 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devgen-cli
3
- Version: 0.2.9
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
23
- Requires-Dist: anthropic>=0.75.0
24
- Requires-Dist: google-genai>=0.1.0
25
- Requires-Dist: openai>=2.11.0
20
+ Requires-Dist: anthropic>=0.116.0
21
+ Requires-Dist: google-genai>=2.10.0
22
+ Requires-Dist: openai>=2.44.0
26
23
  Requires-Dist: pyyaml>=6.0.3
27
24
  Requires-Dist: questionary>=2.1.1
28
- Requires-Dist: requests>=2.32.5
29
- Requires-Dist: rich>=14.2.0
25
+ Requires-Dist: requests>=2.34.2
26
+ Requires-Dist: rich>=15.0.0
30
27
  Requires-Dist: toml>=0.10.2
31
- Requires-Dist: typer>=0.20.0
32
- Dynamic: author
33
- Dynamic: home-page
28
+ Requires-Dist: typer>=0.26.8
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` | Generate a Conventional Commits changelog from recent history |
125
- | `devgen release-notes` | Generate release notes for a version range |
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 add <name> …` | Add `.gitignore` entries to the current project |
128
- | `devgen license list` | List available SPDX licenses |
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` | Generate a Conventional Commits changelog from recent history |
89
- | `devgen release-notes` | Generate release notes for a version range |
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 add <name> …` | Add `.gitignore` entries to the current project |
92
- | `devgen license list` | List available SPDX licenses |
93
- | `devgen license add <spdx>` | Add a LICENSE file to the current project |
94
+ | `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
- generator.run(output_file=output, from_ref=from_ref)
36
- except Exception as e:
37
- typer.secho(f"Error generating changelog: {e}", fg=typer.colors.RED)
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}, force={force_rebuild}, "
82
- f"check={check}, max_groups={max_groups}, max_diff_size={max_diff_size}"
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 f in staged:
121
- typer.echo(f"- {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
- """Undoes the last commit but keeps changes staged."""
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 Exception:
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
- try:
143
- run_git_command(["git", "reset", "--soft", "HEAD~1"])
144
- typer.secho(
145
- "Last commit undone. Changes are still staged.", fg=typer.colors.GREEN
146
- )
147
- except Exception as e:
148
- typer.secho(f"Failed to undo commit: {e}", fg=typer.colors.RED)
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)