devgen-cli 0.2.0__py3-none-any.whl
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/__init__.py +0 -0
- devgen/ai.py +28 -0
- devgen/cli/__init__.py +0 -0
- devgen/cli/changelog.py +38 -0
- devgen/cli/commit.py +96 -0
- devgen/cli/config.py +169 -0
- devgen/cli/gitignore.py +138 -0
- devgen/cli/license.py +101 -0
- devgen/cli/main.py +101 -0
- devgen/cli/release.py +30 -0
- devgen/cli/setup.py +85 -0
- devgen/modules/__init__.py +0 -0
- devgen/modules/changelog_generator.py +190 -0
- devgen/modules/commit_generator.py +257 -0
- devgen/modules/gitignore_generator.py +116 -0
- devgen/modules/license_generator.py +80 -0
- devgen/modules/release_note_generator.py +66 -0
- devgen/providers/__init__.py +21 -0
- devgen/providers/anthropic.py +23 -0
- devgen/providers/gemini.py +24 -0
- devgen/providers/huggingface.py +45 -0
- devgen/providers/openai.py +48 -0
- devgen/providers/openrouter.py +33 -0
- devgen/utils.py +198 -0
- devgen_cli-0.2.0.dist-info/METADATA +287 -0
- devgen_cli-0.2.0.dist-info/RECORD +30 -0
- devgen_cli-0.2.0.dist-info/WHEEL +5 -0
- devgen_cli-0.2.0.dist-info/entry_points.txt +2 -0
- devgen_cli-0.2.0.dist-info/licenses/LICENSE +675 -0
- devgen_cli-0.2.0.dist-info/top_level.txt +1 -0
devgen/__init__.py
ADDED
|
File without changes
|
devgen/ai.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from importlib import import_module
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def generate_with_ai(
|
|
5
|
+
prompt, provider="gemini", api_key=None, model="gemini-2.5-flash", **kwargs
|
|
6
|
+
):
|
|
7
|
+
"""Generates AI-based content such as commit messages using the specified provider and model.
|
|
8
|
+
This function dynamically loads and initializes the provider class, sets the API key, and invokes the provider's generate method with the provided prompt and additional parameters.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
prompt (str): The input prompt used to generate content.
|
|
12
|
+
provider (str): The name of the provider module and class to use; defaults to "gemini".
|
|
13
|
+
api_key (str, optional): The API key for authenticating with the provider. Should be provided via config file or kwargs.
|
|
14
|
+
model (str): The name of the model to use with the provider; defaults to "gemini".
|
|
15
|
+
**kwargs: Additional keyword arguments to pass to the provider's generate method.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: The generated content produced by the AI provider.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
provider_module = import_module(f"devgen.providers.{provider}")
|
|
22
|
+
class_name = "".join([x.capitalize() for x in provider.split("_")]) + "Provider"
|
|
23
|
+
provider_class = getattr(provider_module, class_name)
|
|
24
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
|
25
|
+
raise ImportError(f"Provider `{provider}` not found or invalid: {e}") from e
|
|
26
|
+
|
|
27
|
+
provider_instance = provider_class()
|
|
28
|
+
return provider_instance.generate(prompt, api_key=api_key, model=model, **kwargs)
|
devgen/cli/__init__.py
ADDED
|
File without changes
|
devgen/cli/changelog.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing_extensions import Annotated
|
|
3
|
+
|
|
4
|
+
from devgen.modules.changelog_generator import ChangelogGenerator
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(
|
|
7
|
+
name="changelog",
|
|
8
|
+
help="📝 Generate changelogs from git history.",
|
|
9
|
+
no_args_is_help=True,
|
|
10
|
+
rich_markup_mode="markdown",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("generate")
|
|
15
|
+
def generate_changelog(
|
|
16
|
+
output: Annotated[
|
|
17
|
+
str,
|
|
18
|
+
typer.Option(
|
|
19
|
+
"--output",
|
|
20
|
+
"-o",
|
|
21
|
+
help="Output file path.",
|
|
22
|
+
),
|
|
23
|
+
] = "CHANGELOG.md",
|
|
24
|
+
from_ref: Annotated[
|
|
25
|
+
str,
|
|
26
|
+
typer.Option(
|
|
27
|
+
"--from",
|
|
28
|
+
"-f",
|
|
29
|
+
help="Starting git reference (tag or hash). Defaults to last tag.",
|
|
30
|
+
),
|
|
31
|
+
] = "",
|
|
32
|
+
) -> None:
|
|
33
|
+
generator = ChangelogGenerator()
|
|
34
|
+
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)
|
|
38
|
+
raise typer.Exit(code=1)
|
devgen/cli/commit.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from devgen.modules.commit_generator import run_commit_engine
|
|
6
|
+
from devgen.utils import (
|
|
7
|
+
configure_logger,
|
|
8
|
+
delete_file,
|
|
9
|
+
get_commit_dry_run_path,
|
|
10
|
+
get_git_staged_files,
|
|
11
|
+
get_main_log_path,
|
|
12
|
+
read_file_content,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="commit",
|
|
17
|
+
help="🚀 AI-powered semantic commit message generator.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
rich_markup_mode="markdown",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("run")
|
|
24
|
+
def run_commit(
|
|
25
|
+
dry_run: Annotated[
|
|
26
|
+
bool,
|
|
27
|
+
typer.Option(
|
|
28
|
+
"--dry-run",
|
|
29
|
+
help="Simulate the commit process without making changes.",
|
|
30
|
+
),
|
|
31
|
+
] = False,
|
|
32
|
+
push: Annotated[
|
|
33
|
+
bool,
|
|
34
|
+
typer.Option(
|
|
35
|
+
"--push",
|
|
36
|
+
help="Automatically push changes to the remote repository.",
|
|
37
|
+
),
|
|
38
|
+
] = False,
|
|
39
|
+
debug: Annotated[
|
|
40
|
+
bool,
|
|
41
|
+
typer.Option(
|
|
42
|
+
"--debug",
|
|
43
|
+
help="Enable debug mode for detailed logging.",
|
|
44
|
+
),
|
|
45
|
+
] = False,
|
|
46
|
+
force_rebuild: Annotated[
|
|
47
|
+
bool,
|
|
48
|
+
typer.Option(
|
|
49
|
+
"--force-rebuild",
|
|
50
|
+
help="Force regeneration of commit messages.",
|
|
51
|
+
),
|
|
52
|
+
] = False,
|
|
53
|
+
) -> None:
|
|
54
|
+
log_file = get_main_log_path()
|
|
55
|
+
logger = configure_logger("devgen.cli.commit", log_file)
|
|
56
|
+
logger.info(f"Log file: {log_file}")
|
|
57
|
+
logger.info(
|
|
58
|
+
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
run_commit_engine(
|
|
62
|
+
dry_run=dry_run,
|
|
63
|
+
push=push,
|
|
64
|
+
debug=debug,
|
|
65
|
+
force_rebuild=force_rebuild,
|
|
66
|
+
logger=logger,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("clear-cache")
|
|
71
|
+
def clear_cache() -> None:
|
|
72
|
+
if delete_file(get_commit_dry_run_path()):
|
|
73
|
+
typer.secho("Cache cleared.", fg=typer.colors.GREEN)
|
|
74
|
+
else:
|
|
75
|
+
typer.secho("[i] No cache found.", fg=typer.colors.YELLOW)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("list-cached")
|
|
79
|
+
def list_cached() -> None:
|
|
80
|
+
content = read_file_content(get_commit_dry_run_path())
|
|
81
|
+
if content:
|
|
82
|
+
typer.secho("--- Cached Dry-Run ---", fg=typer.colors.CYAN)
|
|
83
|
+
typer.echo(content)
|
|
84
|
+
else:
|
|
85
|
+
typer.secho("[i] No cache found.", fg=typer.colors.YELLOW)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.command("validate")
|
|
89
|
+
def validate() -> None:
|
|
90
|
+
staged = get_git_staged_files()
|
|
91
|
+
if staged:
|
|
92
|
+
typer.secho(f"{len(staged)} staged file(s):", fg=typer.colors.GREEN)
|
|
93
|
+
for f in staged:
|
|
94
|
+
typer.echo(f"- {f}")
|
|
95
|
+
else:
|
|
96
|
+
typer.secho("[i] No staged files.", fg=typer.colors.RED)
|
devgen/cli/config.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
import questionary
|
|
5
|
+
import typer
|
|
6
|
+
import yaml
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
|
+
|
|
9
|
+
from devgen.utils import get_questionary_style, load_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
|
+
|
|
19
|
+
def _save_config(config: dict[str, Any]) -> None:
|
|
20
|
+
config_path = Path.home() / ".devgen.yaml"
|
|
21
|
+
try:
|
|
22
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
23
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
typer.secho(f"❌ Failed to save config: {e}", fg=typer.colors.RED)
|
|
26
|
+
raise typer.Exit(code=1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("list")
|
|
30
|
+
def list_config() -> None:
|
|
31
|
+
"""Show all configuration values."""
|
|
32
|
+
config = load_config()
|
|
33
|
+
if not config:
|
|
34
|
+
typer.echo("Configuration is empty.")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
typer.echo(yaml.dump(config, default_flow_style=False))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command("edit")
|
|
41
|
+
def edit_config(
|
|
42
|
+
key: Annotated[Optional[str], typer.Argument(help="Configuration key.")] = None,
|
|
43
|
+
value: Annotated[Optional[str], typer.Argument(help="New value.")] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Edit a specific configuration value."""
|
|
46
|
+
config = load_config()
|
|
47
|
+
style = get_questionary_style()
|
|
48
|
+
|
|
49
|
+
if not key:
|
|
50
|
+
choices = list(config.keys())
|
|
51
|
+
key = questionary.select(
|
|
52
|
+
"Select key to update:", choices=choices, style=style
|
|
53
|
+
).ask()
|
|
54
|
+
|
|
55
|
+
if not key:
|
|
56
|
+
raise typer.Exit()
|
|
57
|
+
|
|
58
|
+
if value is None:
|
|
59
|
+
# Check if existing value is boolean to offer select
|
|
60
|
+
current_val = config.get(key)
|
|
61
|
+
|
|
62
|
+
if key == "provider":
|
|
63
|
+
value = questionary.select(
|
|
64
|
+
"Select AI Provider:",
|
|
65
|
+
choices=["gemini", "openai", "huggingface", "openrouter", "anthropic"],
|
|
66
|
+
default=str(current_val) if current_val else "gemini",
|
|
67
|
+
style=style,
|
|
68
|
+
).ask()
|
|
69
|
+
if value is None:
|
|
70
|
+
raise typer.Exit(code=130)
|
|
71
|
+
elif isinstance(current_val, bool):
|
|
72
|
+
val_choice = questionary.select(
|
|
73
|
+
f"Select value for '{key}':",
|
|
74
|
+
choices=["True", "False"],
|
|
75
|
+
default=str(current_val),
|
|
76
|
+
style=style,
|
|
77
|
+
).ask()
|
|
78
|
+
if val_choice is None:
|
|
79
|
+
raise typer.Exit(code=130)
|
|
80
|
+
value = val_choice
|
|
81
|
+
else:
|
|
82
|
+
value = questionary.text(
|
|
83
|
+
f"Enter value for '{key}':",
|
|
84
|
+
default=str(current_val) if current_val is not None else "",
|
|
85
|
+
style=style,
|
|
86
|
+
).ask()
|
|
87
|
+
|
|
88
|
+
if value is None:
|
|
89
|
+
raise typer.Exit(code=130)
|
|
90
|
+
|
|
91
|
+
# Basic type inference
|
|
92
|
+
if isinstance(value, str):
|
|
93
|
+
if value.lower() == "true":
|
|
94
|
+
val = True
|
|
95
|
+
elif value.lower() == "false":
|
|
96
|
+
val = False
|
|
97
|
+
elif value.isdigit():
|
|
98
|
+
val = int(value)
|
|
99
|
+
else:
|
|
100
|
+
val = value
|
|
101
|
+
else:
|
|
102
|
+
val = value
|
|
103
|
+
|
|
104
|
+
config[key] = val
|
|
105
|
+
_save_config(config)
|
|
106
|
+
typer.secho(f" Set '{key}' to '{val}'", fg=typer.colors.GREEN)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command("set")
|
|
110
|
+
def set_config() -> None:
|
|
111
|
+
"""Run the interactive configuration wizard."""
|
|
112
|
+
typer.secho("🛠️ Interactive Configuration Setup", fg=typer.colors.CYAN, bold=True)
|
|
113
|
+
|
|
114
|
+
config = load_config()
|
|
115
|
+
style = get_questionary_style()
|
|
116
|
+
|
|
117
|
+
# Questions
|
|
118
|
+
provider = questionary.select(
|
|
119
|
+
"Select AI Provider:",
|
|
120
|
+
choices=["gemini", "openai", "huggingface", "openrouter", "anthropic"],
|
|
121
|
+
default=config.get("provider", "gemini"),
|
|
122
|
+
style=style,
|
|
123
|
+
).ask()
|
|
124
|
+
if provider is None:
|
|
125
|
+
raise typer.Exit(code=130)
|
|
126
|
+
|
|
127
|
+
model_default = config.get("model", "gemini-2.5-flash")
|
|
128
|
+
|
|
129
|
+
model = questionary.text(
|
|
130
|
+
"Enter Model Name:", default=model_default, style=style
|
|
131
|
+
).ask()
|
|
132
|
+
if model is None:
|
|
133
|
+
raise typer.Exit(code=130)
|
|
134
|
+
|
|
135
|
+
api_key = questionary.password(
|
|
136
|
+
"Enter API Key (leave empty to keep existing or none):", style=style
|
|
137
|
+
).ask()
|
|
138
|
+
if api_key is None:
|
|
139
|
+
raise typer.Exit(code=130)
|
|
140
|
+
|
|
141
|
+
if not api_key:
|
|
142
|
+
api_key = config.get("api_key", "")
|
|
143
|
+
|
|
144
|
+
emoji_choice = questionary.select(
|
|
145
|
+
"Use Emojis in Commit Messages?",
|
|
146
|
+
choices=["Yes", "No"],
|
|
147
|
+
default="Yes" if config.get("emoji", True) else "No",
|
|
148
|
+
style=style,
|
|
149
|
+
).ask()
|
|
150
|
+
if emoji_choice is None:
|
|
151
|
+
raise typer.Exit(code=130)
|
|
152
|
+
emoji = emoji_choice == "Yes"
|
|
153
|
+
|
|
154
|
+
# Save Config
|
|
155
|
+
new_config = {
|
|
156
|
+
"provider": provider,
|
|
157
|
+
"model": model,
|
|
158
|
+
"api_key": api_key,
|
|
159
|
+
"emoji": emoji,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Merge with existing config to preserve other keys?
|
|
163
|
+
# The setup wizard usually overwrites or defines the core keys.
|
|
164
|
+
# Let's preserve other keys.
|
|
165
|
+
config.update(new_config)
|
|
166
|
+
|
|
167
|
+
_save_config(config)
|
|
168
|
+
typer.secho("\nConfiguration saved.", fg=typer.colors.GREEN)
|
|
169
|
+
typer.echo(yaml.dump(new_config, default_flow_style=False))
|
devgen/cli/gitignore.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import questionary
|
|
4
|
+
import typer
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
|
|
7
|
+
from devgen.modules.gitignore_generator import GitignoreGenerator
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="gitignore",
|
|
11
|
+
help="🙈 Generate .gitignore files from GitHub templates.",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
rich_markup_mode="markdown",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("list")
|
|
18
|
+
def list_templates(
|
|
19
|
+
cached: Annotated[
|
|
20
|
+
bool, typer.Option("--cached", "-c", help="List only cached templates.")
|
|
21
|
+
] = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""List available gitignore templates."""
|
|
24
|
+
generator = GitignoreGenerator()
|
|
25
|
+
try:
|
|
26
|
+
if cached:
|
|
27
|
+
templates = generator.list_cached_templates()
|
|
28
|
+
typer.secho(" Cached Templates:", fg=typer.colors.CYAN, bold=True)
|
|
29
|
+
else:
|
|
30
|
+
typer.secho("Fetching available templates...", fg=typer.colors.YELLOW)
|
|
31
|
+
templates = generator.list_available_templates()
|
|
32
|
+
typer.secho(" Available Templates:", fg=typer.colors.CYAN, bold=True)
|
|
33
|
+
|
|
34
|
+
if not templates:
|
|
35
|
+
typer.echo("No templates found.")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Print in columns or just list
|
|
39
|
+
typer.echo(", ".join(templates))
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
typer.secho(f" Error: {e}", fg=typer.colors.RED)
|
|
43
|
+
raise typer.Exit(code=1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("generate")
|
|
47
|
+
def generate_gitignore(
|
|
48
|
+
templates: Annotated[
|
|
49
|
+
Optional[List[str]],
|
|
50
|
+
typer.Argument(
|
|
51
|
+
help="Names of templates to include (e.g. Python Node). If empty, interactive mode is used."
|
|
52
|
+
),
|
|
53
|
+
] = None,
|
|
54
|
+
output: Annotated[
|
|
55
|
+
str,
|
|
56
|
+
typer.Option("--output", "-o", help="Output file path."),
|
|
57
|
+
] = ".gitignore",
|
|
58
|
+
append: Annotated[
|
|
59
|
+
bool,
|
|
60
|
+
typer.Option(
|
|
61
|
+
"--append/--overwrite",
|
|
62
|
+
"-a/-w",
|
|
63
|
+
help="Append to existing file or overwrite.",
|
|
64
|
+
),
|
|
65
|
+
] = True,
|
|
66
|
+
offline: Annotated[
|
|
67
|
+
bool,
|
|
68
|
+
typer.Option("--offline", help="Use only cached templates."),
|
|
69
|
+
] = False,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Generate a .gitignore file."""
|
|
72
|
+
generator = GitignoreGenerator()
|
|
73
|
+
|
|
74
|
+
if not templates:
|
|
75
|
+
# Interactive mode
|
|
76
|
+
try:
|
|
77
|
+
if offline:
|
|
78
|
+
available = generator.list_cached_templates()
|
|
79
|
+
else:
|
|
80
|
+
typer.secho("Fetching templates list...", fg=typer.colors.YELLOW)
|
|
81
|
+
available = generator.list_available_templates()
|
|
82
|
+
|
|
83
|
+
if not available:
|
|
84
|
+
typer.secho(" No templates available.", fg=typer.colors.RED)
|
|
85
|
+
raise typer.Exit(code=1)
|
|
86
|
+
|
|
87
|
+
from devgen.utils import get_questionary_style
|
|
88
|
+
|
|
89
|
+
style = get_questionary_style()
|
|
90
|
+
|
|
91
|
+
# Interactive search loop
|
|
92
|
+
selected_templates = []
|
|
93
|
+
while True:
|
|
94
|
+
# Filter out already selected
|
|
95
|
+
choices = [t for t in available if t not in selected_templates]
|
|
96
|
+
# Add "Done" option at the top
|
|
97
|
+
choices.insert(0, "Done (Finish selection)")
|
|
98
|
+
|
|
99
|
+
choice = questionary.autocomplete(
|
|
100
|
+
"Search for a template (type to search, select 'Done' to finish):",
|
|
101
|
+
choices=choices,
|
|
102
|
+
ignore_case=True,
|
|
103
|
+
match_middle=True,
|
|
104
|
+
validate=lambda x: x in choices or x == "",
|
|
105
|
+
style=style,
|
|
106
|
+
).ask()
|
|
107
|
+
|
|
108
|
+
if choice is None:
|
|
109
|
+
raise typer.Exit(code=130)
|
|
110
|
+
|
|
111
|
+
if not choice or choice.startswith("Done"):
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
selected_templates.append(choice)
|
|
115
|
+
typer.secho(f"➕ Added '{choice}'", fg=typer.colors.GREEN)
|
|
116
|
+
|
|
117
|
+
templates = selected_templates
|
|
118
|
+
|
|
119
|
+
if not templates:
|
|
120
|
+
typer.secho("No templates selected.", fg=typer.colors.YELLOW)
|
|
121
|
+
raise typer.Exit()
|
|
122
|
+
|
|
123
|
+
except typer.Exit:
|
|
124
|
+
raise
|
|
125
|
+
except KeyboardInterrupt:
|
|
126
|
+
typer.secho("\nCancelled by user", fg=typer.colors.YELLOW)
|
|
127
|
+
raise typer.Exit()
|
|
128
|
+
except Exception as e:
|
|
129
|
+
typer.secho(f"❌ Error fetching templates: {e}", fg=typer.colors.RED)
|
|
130
|
+
raise typer.Exit(code=1)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
generator.generate(
|
|
134
|
+
templates, output_file=output, append=append, offline=offline
|
|
135
|
+
)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
typer.secho(f"❌ Error generating .gitignore: {e}", fg=typer.colors.RED)
|
|
138
|
+
raise typer.Exit(code=1)
|
devgen/cli/license.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import questionary
|
|
5
|
+
import typer
|
|
6
|
+
from typing_extensions import Annotated
|
|
7
|
+
|
|
8
|
+
from devgen.modules.license_generator import LicenseGenerator
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="license",
|
|
12
|
+
help="📄 Generate open source licenses.",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
rich_markup_mode="markdown",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("generate")
|
|
19
|
+
def generate_license(
|
|
20
|
+
output: Annotated[
|
|
21
|
+
str,
|
|
22
|
+
typer.Option(
|
|
23
|
+
"--output",
|
|
24
|
+
"-o",
|
|
25
|
+
help="Output file path.",
|
|
26
|
+
),
|
|
27
|
+
] = "LICENSE",
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Interactively generate a license file."""
|
|
30
|
+
generator = LicenseGenerator()
|
|
31
|
+
licenses = generator.list_licenses()
|
|
32
|
+
|
|
33
|
+
if not licenses:
|
|
34
|
+
typer.secho(" No license templates found.", fg=typer.colors.RED)
|
|
35
|
+
raise typer.Exit(code=1)
|
|
36
|
+
|
|
37
|
+
from devgen.utils import get_questionary_style
|
|
38
|
+
|
|
39
|
+
style = get_questionary_style()
|
|
40
|
+
|
|
41
|
+
# 1. Select License
|
|
42
|
+
choices = [
|
|
43
|
+
questionary.Choice(
|
|
44
|
+
title=f"{lic['name']} ({lic['key']})",
|
|
45
|
+
value=lic["key"],
|
|
46
|
+
description=lic["description"],
|
|
47
|
+
)
|
|
48
|
+
for lic in licenses
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
license_key = questionary.select(
|
|
52
|
+
"Select a License:",
|
|
53
|
+
choices=choices,
|
|
54
|
+
use_indicator=True,
|
|
55
|
+
use_shortcuts=True,
|
|
56
|
+
style=style,
|
|
57
|
+
).ask()
|
|
58
|
+
|
|
59
|
+
if license_key is None:
|
|
60
|
+
raise typer.Exit(code=130)
|
|
61
|
+
|
|
62
|
+
if not license_key:
|
|
63
|
+
raise typer.Exit()
|
|
64
|
+
|
|
65
|
+
# 2. Enter Author Name
|
|
66
|
+
# Try to guess from git config if possible, but for now just empty default
|
|
67
|
+
author = questionary.text("Enter Author Name:", style=style).ask()
|
|
68
|
+
if author is None:
|
|
69
|
+
raise typer.Exit(code=130)
|
|
70
|
+
|
|
71
|
+
if not author:
|
|
72
|
+
typer.secho(" Author name is required.", fg=typer.colors.RED)
|
|
73
|
+
raise typer.Exit(code=1)
|
|
74
|
+
|
|
75
|
+
# 3. Enter Year
|
|
76
|
+
current_year = str(datetime.now().year)
|
|
77
|
+
year = questionary.text("Enter Year:", default=current_year, style=style).ask()
|
|
78
|
+
if year is None:
|
|
79
|
+
raise typer.Exit(code=130)
|
|
80
|
+
|
|
81
|
+
# Generate
|
|
82
|
+
try:
|
|
83
|
+
content = generator.render_license(license_key, author, year)
|
|
84
|
+
|
|
85
|
+
output_path = Path(output)
|
|
86
|
+
if output_path.exists():
|
|
87
|
+
overwrite = questionary.confirm(
|
|
88
|
+
f"File {output} already exists. Overwrite?", default=False, style=style
|
|
89
|
+
).ask()
|
|
90
|
+
if overwrite is None:
|
|
91
|
+
raise typer.Exit(code=130)
|
|
92
|
+
if not overwrite:
|
|
93
|
+
typer.secho("Operation cancelled.", fg=typer.colors.YELLOW)
|
|
94
|
+
raise typer.Exit()
|
|
95
|
+
|
|
96
|
+
output_path.write_text(content, encoding="utf-8")
|
|
97
|
+
typer.secho(f"\n License generated at {output_path}", fg=typer.colors.GREEN)
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
typer.secho(f"\n Failed to generate license: {e}", fg=typer.colors.RED)
|
|
101
|
+
raise typer.Exit(code=1)
|
devgen/cli/main.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import toml
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from devgen.cli.changelog import app as changelog_app
|
|
8
|
+
from devgen.cli.commit import app as commit_app
|
|
9
|
+
from devgen.cli.config import app as config_app
|
|
10
|
+
from devgen.cli.gitignore import app as gitignore_app
|
|
11
|
+
from devgen.cli.license import app as license_app
|
|
12
|
+
from devgen.cli.release import app as release_app
|
|
13
|
+
from devgen.cli.setup import app as setup_app
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="devgen",
|
|
17
|
+
help="devgen-py: AI-Powered Git Commit & Release Automation.",
|
|
18
|
+
add_completion=False,
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
rich_markup_mode="markdown",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _version_callback(value: bool) -> None:
|
|
25
|
+
"""Displays the application version retrieved from pyproject.toml if the input value is True, then exits the program. If the version information cannot be found or the file is missing or malformed, outputs an error message before exiting.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
value (bool): A boolean flag indicating whether to display the version and exit.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
None
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
typer.Exit: Exits the program after attempting to display the version or an error message.
|
|
35
|
+
"""
|
|
36
|
+
if value:
|
|
37
|
+
try:
|
|
38
|
+
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
|
|
39
|
+
pyproject_data = toml.load(pyproject_path)
|
|
40
|
+
# Try standard PEP 621 first, then fallback
|
|
41
|
+
version = pyproject_data.get("project", {}).get("version")
|
|
42
|
+
if not version:
|
|
43
|
+
version = (
|
|
44
|
+
pyproject_data.get("tool", {}).get("poetry", {}).get("version")
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if version:
|
|
48
|
+
typer.echo(f"devgen version: {version}")
|
|
49
|
+
else:
|
|
50
|
+
typer.secho(
|
|
51
|
+
"Error: Version not found in pyproject.toml",
|
|
52
|
+
fg=typer.colors.RED,
|
|
53
|
+
err=True,
|
|
54
|
+
)
|
|
55
|
+
except (FileNotFoundError, KeyError, Exception) as e:
|
|
56
|
+
typer.secho(
|
|
57
|
+
f"Error: Could not determine version: {e}",
|
|
58
|
+
fg=typer.colors.RED,
|
|
59
|
+
err=True,
|
|
60
|
+
)
|
|
61
|
+
raise typer.Exit()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.callback()
|
|
65
|
+
def main_callback(
|
|
66
|
+
version: Annotated[
|
|
67
|
+
bool | None,
|
|
68
|
+
typer.Option(
|
|
69
|
+
"--version",
|
|
70
|
+
"-v",
|
|
71
|
+
callback=_version_callback,
|
|
72
|
+
is_eager=True,
|
|
73
|
+
help="Show the application version and exit.",
|
|
74
|
+
),
|
|
75
|
+
] = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Displays the application version information when the --version or -v option is used. If the flag is set, shows the version and exits; otherwise, provides guidance on using other commands with devgen.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
version (bool or None, optional): A flag indicating whether to display the application version, set via command-line options --version or -v. Defaults to None.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
None
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
app.add_typer(commit_app, name="commit")
|
|
89
|
+
app.add_typer(setup_app, name="setup")
|
|
90
|
+
app.add_typer(changelog_app, name="changelog")
|
|
91
|
+
app.add_typer(license_app, name="license")
|
|
92
|
+
app.add_typer(gitignore_app, name="gitignore")
|
|
93
|
+
app.add_typer(release_app, name="release")
|
|
94
|
+
app.add_typer(config_app, name="config")
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
try:
|
|
98
|
+
app()
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
print("\n\n Operation cancelled by user.")
|
|
101
|
+
raise typer.Exit(code=130)
|