cmt-cli 1.1.2__tar.gz → 1.2.0__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.
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/PKG-INFO +2 -1
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/pyproject.toml +2 -1
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/openai.py +2 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/prompt.py +12 -3
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/cli.py +45 -23
- cmt_cli-1.2.0/src/cmt/core/__init__.py +9 -0
- cmt_cli-1.2.0/src/cmt/core/console.py +66 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/uv.lock +3 -1
- cmt_cli-1.1.2/src/cmt/core/__init__.py +0 -4
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/.github/workflows/test.yml +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/.gitignore +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/.python-version +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/LICENSE +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/README.md +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/art/cmt-cli.svg +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/cache.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/provider.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/analysis/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/analysis/analyzer.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/core/logging.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/core/settings.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/enums/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/enums/config.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/exceptions.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/git/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/git/repository.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/models/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/models/changes.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/models/suggestion.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/schemas/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/schemas/config.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/schemas/general.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/services/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/services/pypi.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/utils.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/tests/__init__.py +0 -0
- {cmt_cli-1.1.2 → cmt_cli-1.2.0}/tests/unit/test_repository.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cmt-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: AI-powered Git commit message generator
|
|
5
5
|
Project-URL: Homepage, https://github.com/bijaydas/cmt
|
|
6
6
|
Project-URL: Documentation, https://github.com/bijaydas/cmt#readme
|
|
@@ -19,6 +19,7 @@ Requires-Python: >=3.12
|
|
|
19
19
|
Requires-Dist: langchain-openai>=1.6.0
|
|
20
20
|
Requires-Dist: langchain>=1.3.18
|
|
21
21
|
Requires-Dist: pydantic>=2.13.4
|
|
22
|
+
Requires-Dist: rich>=15.0.0
|
|
22
23
|
Requires-Dist: typer>=0.27.1
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cmt-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "AI-powered Git commit message generator"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -11,6 +11,7 @@ dependencies = [
|
|
|
11
11
|
"langchain>=1.3.18",
|
|
12
12
|
"langchain-openai>=1.6.0",
|
|
13
13
|
"pydantic>=2.13.4",
|
|
14
|
+
"rich>=15.0.0",
|
|
14
15
|
"typer>=0.27.1",
|
|
15
16
|
]
|
|
16
17
|
classifiers = [
|
|
@@ -95,4 +95,6 @@ class OpenAIProvider(AIProvider):
|
|
|
95
95
|
|
|
96
96
|
@staticmethod
|
|
97
97
|
def commit_command(commit_suggestion: CommitSuggestion) -> str:
|
|
98
|
+
if not commit_suggestion.description:
|
|
99
|
+
return commit_suggestion.message
|
|
98
100
|
return f"{commit_suggestion.message} \n\n{commit_suggestion.description}"
|
|
@@ -31,13 +31,22 @@ such as `chore`.
|
|
|
31
31
|
10. Use imperative mood.
|
|
32
32
|
11. Do not mention individual files unless necessary.
|
|
33
33
|
12. Do not generate multiple commit messages.
|
|
34
|
-
13.
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
13. Only add a description if the change is complex enough that the short description alone is
|
|
35
|
+
insufficient to understand it (e.g., multiple distinct changes, non-obvious rationale, or
|
|
36
|
+
breaking changes).
|
|
37
|
+
14. When a description is required, format it as a bullet list, with one point per notable
|
|
38
|
+
change, placed after a blank line following the short description.
|
|
39
|
+
15. Keep each bullet point concise and in imperative mood.
|
|
40
|
+
16. Return ONLY the commit message.
|
|
41
|
+
17. Do not include markdown formatting (headers, bold, code blocks, etc.), quotes, explanations,
|
|
42
|
+
or additional text, other than the hyphen-prefixed bullet list described above.
|
|
37
43
|
|
|
38
44
|
## Commit message format
|
|
39
45
|
|
|
40
46
|
<type>[optional scope]: <short description>
|
|
47
|
+
|
|
48
|
+
- <description bullet point 1>
|
|
49
|
+
- <description bullet point 2>
|
|
41
50
|
"""
|
|
42
51
|
|
|
43
52
|
COMMIT_DATA_PROMPT = """## Change statistics
|
|
@@ -5,7 +5,11 @@ import typer
|
|
|
5
5
|
|
|
6
6
|
from cmt.ai.openai import OpenAIProvider
|
|
7
7
|
from cmt.analysis.analyzer import Analyzer
|
|
8
|
-
from cmt.core import
|
|
8
|
+
from cmt.core import (
|
|
9
|
+
console,
|
|
10
|
+
settings,
|
|
11
|
+
setup_logging,
|
|
12
|
+
)
|
|
9
13
|
from cmt.enums.config import ConfigTask
|
|
10
14
|
from cmt.exceptions import CmtError
|
|
11
15
|
from cmt.git.repository import Repository
|
|
@@ -22,7 +26,8 @@ app = typer.Typer(
|
|
|
22
26
|
|
|
23
27
|
def version_callback(value: bool) -> None:
|
|
24
28
|
if value:
|
|
25
|
-
|
|
29
|
+
console.print(f"[brand]cmt-cli[/brand] [muted]version {version(settings.APP_NAME)}[/muted]")
|
|
30
|
+
console.print("Run 'cmt update' to check for updates.")
|
|
26
31
|
raise typer.Exit()
|
|
27
32
|
|
|
28
33
|
|
|
@@ -34,7 +39,7 @@ def main(
|
|
|
34
39
|
setup_logging()
|
|
35
40
|
if ctx.invoked_subcommand is None:
|
|
36
41
|
logger.info("No subcommand invoked, displaying help.")
|
|
37
|
-
|
|
42
|
+
console.print(ctx.get_help())
|
|
38
43
|
raise typer.Exit(code=0)
|
|
39
44
|
|
|
40
45
|
|
|
@@ -55,57 +60,66 @@ def suggest() -> None:
|
|
|
55
60
|
logger.warning(
|
|
56
61
|
"No staged files found. Please stage your changes before running this command."
|
|
57
62
|
)
|
|
58
|
-
|
|
63
|
+
console.print_warning(
|
|
59
64
|
"No staged files found. Please stage your changes before running this command."
|
|
60
65
|
)
|
|
61
66
|
raise typer.Exit(code=0)
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
with console.get_console().status(
|
|
69
|
+
"[brand]Analyzing staged changes...[/brand]", spinner="dots"
|
|
70
|
+
):
|
|
71
|
+
analysis_result = analyzer.analyze(staged_files)
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
open_ai = OpenAIProvider()
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
with console.get_console().status(
|
|
76
|
+
"[brand]Generating commit message...[/brand]", spinner="dots"
|
|
77
|
+
):
|
|
78
|
+
commit = open_ai.generate_commit_message(staged_files, analysis_result)
|
|
68
79
|
|
|
69
80
|
commit_command = OpenAIProvider.commit_command(commit)
|
|
70
81
|
|
|
71
|
-
logger.info("Suggested commit
|
|
72
|
-
|
|
82
|
+
logger.info("Suggested commit:%s", commit_command)
|
|
83
|
+
console.print_suggested_commit(commit_command)
|
|
73
84
|
|
|
74
85
|
while True:
|
|
75
86
|
action = (
|
|
76
|
-
typer.prompt(
|
|
87
|
+
typer.prompt(
|
|
88
|
+
"Would you like to use this commit message? [y]es / [e]dit / [n]o",
|
|
89
|
+
default="y",
|
|
90
|
+
)
|
|
77
91
|
.strip()
|
|
78
92
|
.lower()[0]
|
|
79
93
|
)
|
|
80
94
|
|
|
81
95
|
if action == "n":
|
|
82
|
-
|
|
96
|
+
console.print_warning("Aborted.")
|
|
83
97
|
break
|
|
84
98
|
|
|
85
99
|
if action == "e":
|
|
86
100
|
commit_command = edit_with_vim(commit_command)
|
|
87
|
-
|
|
101
|
+
console.print_suggested_commit(commit_command)
|
|
88
102
|
|
|
89
103
|
if action == "y":
|
|
90
104
|
logger.info("Committing with message:\n\n%s", commit_command)
|
|
91
105
|
result = repository.commit(commit_command)
|
|
92
106
|
|
|
93
107
|
logger.info("Commit result:\n\n%s", result.stdout)
|
|
94
|
-
|
|
108
|
+
console.print_success("Code committed")
|
|
95
109
|
break
|
|
96
110
|
|
|
97
111
|
except CmtError as e:
|
|
98
|
-
|
|
112
|
+
console.print_error(str(e))
|
|
99
113
|
raise typer.Exit(code=1) from None
|
|
100
114
|
except typer.Exit:
|
|
101
115
|
raise
|
|
102
116
|
except KeyboardInterrupt:
|
|
103
117
|
logger.info("Operation cancelled by user.")
|
|
104
|
-
|
|
118
|
+
console.print_warning("Operation cancelled by user.")
|
|
105
119
|
raise typer.Exit(code=1) from None
|
|
106
120
|
except Exception as e:
|
|
107
121
|
logger.error("Unexpected error: %s", e)
|
|
108
|
-
|
|
122
|
+
console.print_error(f"\nUnexpected error: {e}")
|
|
109
123
|
raise typer.Exit(code=1) from None
|
|
110
124
|
|
|
111
125
|
|
|
@@ -115,11 +129,14 @@ def config(task: ConfigTask) -> None:
|
|
|
115
129
|
if task == ConfigTask.set:
|
|
116
130
|
open_ai_key = typer.prompt("Enter your OpenAI API key", default=None)
|
|
117
131
|
open_ai_model = typer.prompt(
|
|
118
|
-
"Enter your OpenAI model",
|
|
132
|
+
"Enter your OpenAI model",
|
|
133
|
+
default=settings.OPEN_AI_DEFAULT_MODEL,
|
|
119
134
|
)
|
|
120
135
|
logger.info("Configuration updated with OpenAI API key and model.")
|
|
121
136
|
settings.set(AIConfig(api_key=open_ai_key, model=open_ai_model))
|
|
122
137
|
|
|
138
|
+
console.print_success("Configuration updated.")
|
|
139
|
+
|
|
123
140
|
if task == ConfigTask.get:
|
|
124
141
|
setting_values = settings.get()
|
|
125
142
|
|
|
@@ -129,6 +146,8 @@ def config(task: ConfigTask) -> None:
|
|
|
129
146
|
if setting_values.model:
|
|
130
147
|
typer.echo(f"OpenAI model: {setting_values.model}")
|
|
131
148
|
|
|
149
|
+
typer.echo("Run `cmt config set` to update the configuration.")
|
|
150
|
+
|
|
132
151
|
logger.info("Retrieved current configuration.")
|
|
133
152
|
|
|
134
153
|
raise typer.Exit(code=0)
|
|
@@ -142,8 +161,9 @@ def update() -> None:
|
|
|
142
161
|
current_version = version(settings.APP_NAME)
|
|
143
162
|
logger.info("Current %s version: %s", settings.APP_NAME, current_version)
|
|
144
163
|
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
with console.get_console().status("[brand]Checking for updates...[/brand]", spinner="dots"):
|
|
165
|
+
pypi_service = PyPIService()
|
|
166
|
+
latest_info_response = pypi_service.any_update(current_version=current_version)
|
|
147
167
|
|
|
148
168
|
if latest_info_response.updated_required:
|
|
149
169
|
logger.info(
|
|
@@ -151,17 +171,19 @@ def update() -> None:
|
|
|
151
171
|
latest_info_response.current_version,
|
|
152
172
|
latest_info_response.latest_version,
|
|
153
173
|
)
|
|
154
|
-
|
|
174
|
+
console.print(
|
|
155
175
|
f"A new version of {settings.APP_NAME} is available: "
|
|
156
|
-
f"{latest_info_response.latest_version}.\n\n"
|
|
157
|
-
f"upgrade {settings.APP_NAME} using your package manager."
|
|
176
|
+
f"[brand]{latest_info_response.latest_version}[/brand].\n\n"
|
|
177
|
+
f"upgrade {settings.APP_NAME} using your package manager."
|
|
158
178
|
)
|
|
159
179
|
else:
|
|
160
180
|
logger.info(
|
|
161
181
|
"No update required. You are using the latest version: %s",
|
|
162
182
|
latest_info_response.current_version,
|
|
163
183
|
)
|
|
164
|
-
|
|
184
|
+
console.print_success(
|
|
185
|
+
f"You are using the latest version {latest_info_response.current_version}"
|
|
186
|
+
)
|
|
165
187
|
|
|
166
188
|
raise typer.Exit(code=0)
|
|
167
189
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from rich.console import Console as RichConsoleBase
|
|
2
|
+
from rich.panel import Panel
|
|
3
|
+
from rich.theme import Theme
|
|
4
|
+
|
|
5
|
+
CMT_THEME = Theme(
|
|
6
|
+
{
|
|
7
|
+
"brand": "bold magenta",
|
|
8
|
+
"muted": "dim",
|
|
9
|
+
"info": "cyan",
|
|
10
|
+
"success": "bold green",
|
|
11
|
+
"warning": "bold yellow",
|
|
12
|
+
"error": "bold red",
|
|
13
|
+
"prompt.choices": "bold cyan",
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RichConsole:
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._console = RichConsoleBase(theme=CMT_THEME, highlight=False)
|
|
21
|
+
self._error_console = RichConsoleBase(theme=CMT_THEME, highlight=False, stderr=True)
|
|
22
|
+
|
|
23
|
+
def get_console(self) -> RichConsoleBase:
|
|
24
|
+
return self._console
|
|
25
|
+
|
|
26
|
+
def get_error_console(self) -> RichConsoleBase:
|
|
27
|
+
return self._error_console
|
|
28
|
+
|
|
29
|
+
def print(self, *args, **kwargs) -> None:
|
|
30
|
+
self._console.print(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
def print_banner(self, app_name: str, version: str | None = None) -> None:
|
|
33
|
+
label = f"[brand]✦ {app_name}[/brand]"
|
|
34
|
+
if version:
|
|
35
|
+
label += f" [muted]v{version}[/muted]"
|
|
36
|
+
self._console.print(label)
|
|
37
|
+
|
|
38
|
+
def print_success(self, message: str) -> None:
|
|
39
|
+
self._console.print(f"[success]✔[/success] {message}")
|
|
40
|
+
|
|
41
|
+
def print_warning(self, message: str) -> None:
|
|
42
|
+
self._console.print(f"[warning]⚠[/warning] {message}")
|
|
43
|
+
|
|
44
|
+
def print_info(self, message: str) -> None:
|
|
45
|
+
self._console.print(f"[info]ℹ[/info] {message}")
|
|
46
|
+
|
|
47
|
+
def print_error(self, message: str) -> None:
|
|
48
|
+
self._error_console.print(f"[error]✖ {message}[/error]")
|
|
49
|
+
|
|
50
|
+
def _render_commit_panel(self, commit_message: str, title: str = "Suggested commit") -> Panel:
|
|
51
|
+
return Panel(
|
|
52
|
+
commit_message,
|
|
53
|
+
title=f"[brand]{title}[/brand]",
|
|
54
|
+
title_align="left",
|
|
55
|
+
border_style="brand",
|
|
56
|
+
padding=(1, 2),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def print_suggested_commit(self, commit_message: str, title: str = "Suggested commit") -> None:
|
|
60
|
+
self._console.print(self._render_commit_panel(commit_message, title=title))
|
|
61
|
+
|
|
62
|
+
def print_loading(self, message: str) -> None:
|
|
63
|
+
self._console.status(f"[info]{message}[/info]", spinner="dots")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
console = RichConsole()
|
|
@@ -175,12 +175,13 @@ wheels = [
|
|
|
175
175
|
|
|
176
176
|
[[package]]
|
|
177
177
|
name = "cmt-cli"
|
|
178
|
-
version = "1.0
|
|
178
|
+
version = "1.2.0"
|
|
179
179
|
source = { editable = "." }
|
|
180
180
|
dependencies = [
|
|
181
181
|
{ name = "langchain" },
|
|
182
182
|
{ name = "langchain-openai" },
|
|
183
183
|
{ name = "pydantic" },
|
|
184
|
+
{ name = "rich" },
|
|
184
185
|
{ name = "typer" },
|
|
185
186
|
]
|
|
186
187
|
|
|
@@ -195,6 +196,7 @@ requires-dist = [
|
|
|
195
196
|
{ name = "langchain", specifier = ">=1.3.18" },
|
|
196
197
|
{ name = "langchain-openai", specifier = ">=1.6.0" },
|
|
197
198
|
{ name = "pydantic", specifier = ">=2.13.4" },
|
|
199
|
+
{ name = "rich", specifier = ">=15.0.0" },
|
|
198
200
|
{ name = "typer", specifier = ">=0.27.1" },
|
|
199
201
|
]
|
|
200
202
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|