devgen-cli 0.2.9__py3-none-any.whl → 0.3.1__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/ai.py +46 -32
- devgen/cli/changelog.py +6 -5
- devgen/cli/commit.py +22 -17
- devgen/cli/config.py +62 -163
- devgen/cli/gitignore.py +66 -67
- devgen/cli/license.py +41 -50
- devgen/cli/main.py +11 -54
- devgen/cli/release.py +13 -4
- devgen/cli/setup.py +4 -95
- devgen/cli/wizard.py +88 -0
- devgen/modules/changelog_generator.py +78 -63
- devgen/modules/changelog_sections.py +1 -1
- devgen/modules/commit_generator.py +125 -148
- devgen/modules/diff_builder.py +119 -108
- devgen/modules/git_ops.py +33 -76
- devgen/modules/gitignore_generator.py +63 -67
- devgen/modules/license_generator.py +44 -57
- devgen/modules/release_note_generator.py +19 -31
- devgen/providers/anthropic.py +10 -2
- devgen/providers/base.py +69 -40
- devgen/providers/gemini.py +18 -48
- devgen/providers/huggingface.py +35 -27
- devgen/providers/ollama.py +16 -41
- devgen/providers/openai.py +2 -19
- devgen/providers/openai_compatible.py +46 -0
- devgen/providers/openrouter.py +7 -22
- devgen/templates/commit/commit_message.tpl +21 -6
- devgen/utils.py +188 -235
- {devgen_cli-0.2.9.dist-info → devgen_cli-0.3.1.dist-info}/METADATA +19 -23
- devgen_cli-0.3.1.dist-info/RECORD +51 -0
- {devgen_cli-0.2.9.dist-info → devgen_cli-0.3.1.dist-info}/WHEEL +1 -1
- devgen_cli-0.2.9.dist-info/RECORD +0 -49
- {devgen_cli-0.2.9.dist-info → devgen_cli-0.3.1.dist-info}/entry_points.txt +0 -0
- {devgen_cli-0.2.9.dist-info → devgen_cli-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {devgen_cli-0.2.9.dist-info → devgen_cli-0.3.1.dist-info}/top_level.txt +0 -0
devgen/ai.py
CHANGED
|
@@ -1,37 +1,51 @@
|
|
|
1
|
+
"""Provider registry and dispatch for AI generation."""
|
|
2
|
+
|
|
1
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)
|
|
2
30
|
|
|
3
31
|
|
|
4
32
|
def generate_with_ai(
|
|
5
|
-
prompt
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
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.
|
|
19
42
|
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
) from e
|
|
30
|
-
except AttributeError as e:
|
|
31
|
-
raise ImportError(
|
|
32
|
-
f"Provider `{provider}` exists but does not expose a "
|
|
33
|
-
f"`{class_name}` class. This is a packaging bug."
|
|
34
|
-
) from e
|
|
35
|
-
|
|
36
|
-
provider_instance = provider_class()
|
|
37
|
-
return provider_instance.generate(prompt, api_key=api_key, model=model, **kwargs)
|
|
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"]
|
devgen/cli/changelog.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
1
4
|
import typer
|
|
2
|
-
from typing_extensions import Annotated
|
|
3
5
|
|
|
4
6
|
from devgen.modules.changelog_generator import ChangelogGenerator
|
|
5
7
|
|
|
@@ -30,9 +32,8 @@ def generate_changelog(
|
|
|
30
32
|
),
|
|
31
33
|
] = "",
|
|
32
34
|
) -> None:
|
|
33
|
-
generator = ChangelogGenerator()
|
|
34
35
|
try:
|
|
35
|
-
|
|
36
|
-
except
|
|
37
|
-
typer.secho(f"Error generating changelog: {
|
|
36
|
+
ChangelogGenerator().run(output_file=output, from_ref=from_ref)
|
|
37
|
+
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
|
|
38
|
+
typer.secho(f"Error generating changelog: {error}", fg=typer.colors.RED)
|
|
38
39
|
raise typer.Exit(code=1)
|
devgen/cli/commit.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import subprocess
|
|
1
2
|
from typing import Annotated
|
|
2
3
|
|
|
3
4
|
import questionary
|
|
@@ -11,6 +12,7 @@ from devgen.utils import (
|
|
|
11
12
|
get_git_staged_files,
|
|
12
13
|
get_main_log_path,
|
|
13
14
|
read_file_content,
|
|
15
|
+
run_git_command,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
app = typer.Typer(
|
|
@@ -78,8 +80,9 @@ def run_commit(
|
|
|
78
80
|
logger = configure_logger("devgen.cli.commit", log_file, console=debug)
|
|
79
81
|
logger.info(f"Log file: {log_file}")
|
|
80
82
|
logger.info(
|
|
81
|
-
f"Options: dry_run={dry_run}, push={push}, debug={debug},
|
|
82
|
-
f"check={check}, max_groups={max_groups},
|
|
83
|
+
f"Options: dry_run={dry_run}, push={push}, debug={debug}, "
|
|
84
|
+
f"force={force_rebuild}, check={check}, max_groups={max_groups}, "
|
|
85
|
+
f"max_diff_size={max_diff_size}"
|
|
83
86
|
)
|
|
84
87
|
|
|
85
88
|
run_commit_engine(
|
|
@@ -117,32 +120,34 @@ def validate() -> None:
|
|
|
117
120
|
staged = get_git_staged_files()
|
|
118
121
|
if staged:
|
|
119
122
|
typer.secho(f"{len(staged)} staged file(s):", fg=typer.colors.GREEN)
|
|
120
|
-
for
|
|
121
|
-
typer.echo(f"- {
|
|
123
|
+
for file in staged:
|
|
124
|
+
typer.echo(f"- {file}")
|
|
122
125
|
else:
|
|
123
126
|
typer.secho("[i] No staged files.", fg=typer.colors.YELLOW)
|
|
124
127
|
|
|
125
128
|
|
|
126
129
|
@app.command("undo")
|
|
127
130
|
def undo_commit() -> None:
|
|
128
|
-
"""
|
|
129
|
-
from devgen.utils import run_git_command
|
|
130
|
-
|
|
131
|
+
"""Undo the last commit but keep the changes staged."""
|
|
131
132
|
try:
|
|
132
|
-
# Check if there's at least one commit
|
|
133
133
|
run_git_command(["git", "rev-parse", "HEAD"])
|
|
134
|
-
except
|
|
134
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
135
135
|
typer.secho("No commits found to undo.", fg=typer.colors.RED)
|
|
136
136
|
return
|
|
137
137
|
|
|
138
|
-
if questionary.confirm(
|
|
138
|
+
if not questionary.confirm(
|
|
139
139
|
"Are you sure you want to undo the last commit? (Changes will remain staged)",
|
|
140
140
|
default=False,
|
|
141
141
|
).ask():
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
run_git_command(["git", "reset", "--soft", "HEAD~1"])
|
|
146
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as error:
|
|
147
|
+
typer.secho(f"Failed to undo commit: {error}", fg=typer.colors.RED)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
typer.secho(
|
|
151
|
+
"Last commit undone. Changes are still staged.",
|
|
152
|
+
fg=typer.colors.GREEN,
|
|
153
|
+
)
|
devgen/cli/config.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import Any, Optional
|
|
1
|
+
from typing import Annotated, Any
|
|
3
2
|
|
|
4
3
|
import questionary
|
|
5
4
|
import typer
|
|
6
5
|
import yaml
|
|
7
|
-
from typing_extensions import Annotated
|
|
8
6
|
|
|
9
|
-
from devgen.
|
|
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
10
|
|
|
11
11
|
app = typer.Typer(
|
|
12
12
|
name="config",
|
|
@@ -15,190 +15,88 @@ app = typer.Typer(
|
|
|
15
15
|
rich_markup_mode="markdown",
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
+
_TRUE_VALUES = {"true", "yes", "1"}
|
|
19
|
+
_FALSE_VALUES = {"false", "no", "0"}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
@app.command("list")
|
|
30
35
|
def list_config() -> None:
|
|
31
36
|
"""Show all configuration values."""
|
|
32
|
-
|
|
33
|
-
if not config:
|
|
34
|
-
typer.echo("Configuration is empty.")
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
typer.echo(yaml.dump(config, default_flow_style=False))
|
|
37
|
+
typer.echo(yaml.safe_dump(load_config(), sort_keys=False))
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
@app.command("edit")
|
|
41
41
|
def edit_config(
|
|
42
|
-
key: Annotated[
|
|
43
|
-
value: Annotated[
|
|
42
|
+
key: Annotated[str | None, typer.Argument(help="Configuration key.")] = None,
|
|
43
|
+
value: Annotated[str | None, typer.Argument(help="New value.")] = None,
|
|
44
44
|
) -> None:
|
|
45
|
-
"""Edit a
|
|
45
|
+
"""Edit or add a single configuration value."""
|
|
46
46
|
config = load_config()
|
|
47
47
|
style = get_questionary_style()
|
|
48
48
|
|
|
49
49
|
if not key:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
key = ask(
|
|
51
|
+
questionary.select(
|
|
52
|
+
"Select key to update:",
|
|
53
|
+
choices=list(config),
|
|
54
|
+
style=style,
|
|
55
55
|
)
|
|
56
|
-
|
|
57
|
-
key = questionary.select(
|
|
58
|
-
"Select key to update:", choices=choices, style=style
|
|
59
|
-
).ask()
|
|
60
|
-
|
|
61
|
-
if not key:
|
|
62
|
-
raise typer.Exit()
|
|
56
|
+
)
|
|
63
57
|
|
|
64
58
|
if value is None:
|
|
65
|
-
|
|
66
|
-
current_val = config.get(key)
|
|
67
|
-
|
|
59
|
+
current = config.get(key)
|
|
68
60
|
if key == "provider":
|
|
69
|
-
value =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
f"Select value for '{key}':",
|
|
87
|
-
choices=["True", "False"],
|
|
88
|
-
default=str(current_val),
|
|
89
|
-
style=style,
|
|
90
|
-
).ask()
|
|
91
|
-
if val_choice is None:
|
|
92
|
-
raise typer.Exit(code=130)
|
|
93
|
-
value = val_choice
|
|
94
|
-
else:
|
|
95
|
-
value = questionary.text(
|
|
96
|
-
f"Enter value for '{key}':",
|
|
97
|
-
default=str(current_val) if current_val is not None else "",
|
|
98
|
-
style=style,
|
|
99
|
-
).ask()
|
|
100
|
-
|
|
101
|
-
if value is None:
|
|
102
|
-
raise typer.Exit(code=130)
|
|
103
|
-
|
|
104
|
-
# Basic type inference
|
|
105
|
-
if isinstance(value, str):
|
|
106
|
-
if value.lower() == "true":
|
|
107
|
-
val = True
|
|
108
|
-
elif value.lower() == "false":
|
|
109
|
-
val = False
|
|
110
|
-
elif value.isdigit():
|
|
111
|
-
val = int(value)
|
|
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
|
+
)
|
|
112
78
|
else:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
)
|
|
116
86
|
|
|
117
|
-
config[key] =
|
|
118
|
-
|
|
119
|
-
|
|
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)
|
|
120
94
|
|
|
121
95
|
|
|
122
96
|
@app.command("set")
|
|
123
97
|
def set_config() -> None:
|
|
124
98
|
"""Run the interactive configuration wizard."""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
config = load_config()
|
|
128
|
-
style = get_questionary_style()
|
|
129
|
-
|
|
130
|
-
# Questions
|
|
131
|
-
provider = questionary.select(
|
|
132
|
-
"Select AI Provider:",
|
|
133
|
-
choices=[
|
|
134
|
-
"gemini",
|
|
135
|
-
"openai",
|
|
136
|
-
"huggingface",
|
|
137
|
-
"openrouter",
|
|
138
|
-
"anthropic",
|
|
139
|
-
"ollama",
|
|
140
|
-
],
|
|
141
|
-
default=config.get("provider", "gemini"),
|
|
142
|
-
style=style,
|
|
143
|
-
).ask()
|
|
144
|
-
if provider is None:
|
|
145
|
-
raise typer.Exit(code=130)
|
|
146
|
-
|
|
147
|
-
model_default = config.get("model", "gemini-2.5-flash")
|
|
148
|
-
|
|
149
|
-
model = questionary.text(
|
|
150
|
-
"Enter Model Name:", default=model_default, style=style
|
|
151
|
-
).ask()
|
|
152
|
-
if model is None:
|
|
153
|
-
raise typer.Exit(code=130)
|
|
154
|
-
|
|
155
|
-
api_key = questionary.password(
|
|
156
|
-
"Enter API Key (leave empty to keep existing or none):", style=style
|
|
157
|
-
).ask()
|
|
158
|
-
if api_key is None:
|
|
159
|
-
raise typer.Exit(code=130)
|
|
160
|
-
|
|
161
|
-
if not api_key:
|
|
162
|
-
api_key = config.get("api_key", "")
|
|
163
|
-
|
|
164
|
-
emoji_choice = questionary.select(
|
|
165
|
-
"Use Emojis in Commit Messages?",
|
|
166
|
-
choices=["Yes", "No"],
|
|
167
|
-
default="Yes" if config.get("emoji", True) else "No",
|
|
168
|
-
style=style,
|
|
169
|
-
).ask()
|
|
170
|
-
if emoji_choice is None:
|
|
171
|
-
raise typer.Exit(code=130)
|
|
172
|
-
emoji = emoji_choice == "Yes"
|
|
173
|
-
|
|
174
|
-
ollama_host = config.get("ollama_host", "http://localhost:11434")
|
|
175
|
-
if provider == "ollama":
|
|
176
|
-
ollama_host_input = questionary.text(
|
|
177
|
-
"Ollama server URL:",
|
|
178
|
-
default=ollama_host,
|
|
179
|
-
style=style,
|
|
180
|
-
).ask()
|
|
181
|
-
if ollama_host_input is None:
|
|
182
|
-
raise typer.Exit(code=130)
|
|
183
|
-
ollama_host = ollama_host_input.strip() or ollama_host
|
|
184
|
-
|
|
185
|
-
# Save Config
|
|
186
|
-
new_config = {
|
|
187
|
-
"provider": provider,
|
|
188
|
-
"model": model,
|
|
189
|
-
"api_key": api_key,
|
|
190
|
-
"emoji": emoji,
|
|
191
|
-
"ollama_host": ollama_host,
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
# Merge with existing config to preserve other keys?
|
|
195
|
-
# The setup wizard usually overwrites or defines the core keys.
|
|
196
|
-
# Let's preserve other keys.
|
|
197
|
-
config.update(new_config)
|
|
198
|
-
|
|
199
|
-
_save_config(config)
|
|
200
|
-
typer.secho("\nConfiguration saved.", fg=typer.colors.GREEN)
|
|
201
|
-
typer.echo(yaml.dump(new_config, default_flow_style=False))
|
|
99
|
+
run_config_wizard()
|
|
202
100
|
|
|
203
101
|
|
|
204
102
|
@app.command("info")
|
|
@@ -218,7 +116,8 @@ def config_info() -> None:
|
|
|
218
116
|
|
|
219
117
|
console.print(table)
|
|
220
118
|
console.print(
|
|
221
|
-
"\n[bold]Tip:[/bold] If you hardcode emojis in your template, the AI will
|
|
119
|
+
"\n[bold]Tip:[/bold] If you hardcode emojis in your template, the AI will "
|
|
120
|
+
"likely include them regardless of the 'emoji' setting.",
|
|
222
121
|
style="yellow",
|
|
223
122
|
)
|
|
224
123
|
console.print("\n[bold]Example Template:[/bold]")
|
devgen/cli/gitignore.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Annotated
|
|
2
2
|
|
|
3
3
|
import questionary
|
|
4
4
|
import typer
|
|
5
|
-
from typing_extensions import Annotated
|
|
6
5
|
|
|
6
|
+
from devgen.cli.wizard import ask
|
|
7
7
|
from devgen.modules.gitignore_generator import GitignoreGenerator
|
|
8
|
+
from devgen.utils import get_questionary_style
|
|
8
9
|
|
|
9
10
|
app = typer.Typer(
|
|
10
11
|
name="gitignore",
|
|
@@ -14,10 +15,52 @@ app = typer.Typer(
|
|
|
14
15
|
)
|
|
15
16
|
|
|
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
|
+
|
|
17
59
|
@app.command("list")
|
|
18
60
|
def list_templates(
|
|
19
61
|
cached: Annotated[
|
|
20
|
-
bool,
|
|
62
|
+
bool,
|
|
63
|
+
typer.Option("--cached", "-c", help="List only cached templates."),
|
|
21
64
|
] = False,
|
|
22
65
|
) -> None:
|
|
23
66
|
"""List available gitignore templates."""
|
|
@@ -30,25 +73,20 @@ def list_templates(
|
|
|
30
73
|
typer.secho("Fetching available templates...", fg=typer.colors.YELLOW)
|
|
31
74
|
templates = generator.list_available_templates()
|
|
32
75
|
typer.secho(" Available Templates:", fg=typer.colors.CYAN, bold=True)
|
|
33
|
-
|
|
34
|
-
|
|
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)
|
|
76
|
+
except RuntimeError as error:
|
|
77
|
+
typer.secho(f" Error: {error}", fg=typer.colors.RED)
|
|
43
78
|
raise typer.Exit(code=1)
|
|
44
79
|
|
|
80
|
+
typer.echo(", ".join(templates) if templates else "No templates found.")
|
|
81
|
+
|
|
45
82
|
|
|
46
83
|
@app.command("generate")
|
|
47
84
|
def generate_gitignore(
|
|
48
85
|
templates: Annotated[
|
|
49
|
-
|
|
86
|
+
list[str] | None,
|
|
50
87
|
typer.Argument(
|
|
51
|
-
help="Names of templates to include (e.g. Python Node).
|
|
88
|
+
help="Names of templates to include (e.g. Python Node). "
|
|
89
|
+
"If empty, interactive mode is used."
|
|
52
90
|
),
|
|
53
91
|
] = None,
|
|
54
92
|
output: Annotated[
|
|
@@ -72,67 +110,28 @@ def generate_gitignore(
|
|
|
72
110
|
generator = GitignoreGenerator()
|
|
73
111
|
|
|
74
112
|
if not templates:
|
|
75
|
-
# Interactive mode
|
|
76
113
|
try:
|
|
77
|
-
|
|
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
|
-
|
|
114
|
+
templates = _select_templates(generator, offline)
|
|
123
115
|
except typer.Exit:
|
|
124
116
|
raise
|
|
125
117
|
except KeyboardInterrupt:
|
|
126
118
|
typer.secho("\nCancelled by user", fg=typer.colors.YELLOW)
|
|
127
119
|
raise typer.Exit()
|
|
128
|
-
except
|
|
129
|
-
typer.secho(f"❌ Error fetching templates: {
|
|
120
|
+
except (OSError, RuntimeError) as error:
|
|
121
|
+
typer.secho(f"❌ Error fetching templates: {error}", fg=typer.colors.RED)
|
|
130
122
|
raise typer.Exit(code=1)
|
|
131
123
|
|
|
124
|
+
if not templates:
|
|
125
|
+
typer.secho("No templates selected.", fg=typer.colors.YELLOW)
|
|
126
|
+
raise typer.Exit()
|
|
127
|
+
|
|
132
128
|
try:
|
|
133
129
|
generator.generate(
|
|
134
|
-
templates,
|
|
130
|
+
templates,
|
|
131
|
+
output_file=output,
|
|
132
|
+
append=append,
|
|
133
|
+
offline=offline,
|
|
135
134
|
)
|
|
136
|
-
except
|
|
137
|
-
typer.secho(f"❌ Error generating .gitignore: {
|
|
135
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
136
|
+
typer.secho(f"❌ Error generating .gitignore: {error}", fg=typer.colors.RED)
|
|
138
137
|
raise typer.Exit(code=1)
|