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.
Files changed (39) hide show
  1. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/PKG-INFO +2 -1
  2. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/pyproject.toml +2 -1
  3. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/openai.py +2 -0
  4. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/prompt.py +12 -3
  5. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/cli.py +45 -23
  6. cmt_cli-1.2.0/src/cmt/core/__init__.py +9 -0
  7. cmt_cli-1.2.0/src/cmt/core/console.py +66 -0
  8. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/uv.lock +3 -1
  9. cmt_cli-1.1.2/src/cmt/core/__init__.py +0 -4
  10. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/.github/workflows/test.yml +0 -0
  11. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/.gitignore +0 -0
  12. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/.python-version +0 -0
  13. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/LICENSE +0 -0
  14. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/README.md +0 -0
  15. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/art/cmt-cli.svg +0 -0
  16. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/__init__.py +0 -0
  17. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/__init__.py +0 -0
  18. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/cache.py +0 -0
  19. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/ai/provider.py +0 -0
  20. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/analysis/__init__.py +0 -0
  21. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/analysis/analyzer.py +0 -0
  22. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/core/logging.py +0 -0
  23. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/core/settings.py +0 -0
  24. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/enums/__init__.py +0 -0
  25. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/enums/config.py +0 -0
  26. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/exceptions.py +0 -0
  27. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/git/__init__.py +0 -0
  28. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/git/repository.py +0 -0
  29. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/models/__init__.py +0 -0
  30. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/models/changes.py +0 -0
  31. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/models/suggestion.py +0 -0
  32. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/schemas/__init__.py +0 -0
  33. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/schemas/config.py +0 -0
  34. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/schemas/general.py +0 -0
  35. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/services/__init__.py +0 -0
  36. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/services/pypi.py +0 -0
  37. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/src/cmt/utils.py +0 -0
  38. {cmt_cli-1.1.2 → cmt_cli-1.2.0}/tests/__init__.py +0 -0
  39. {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.1.2
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.1.2"
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. Do not include a commit body.
35
- 14. Return ONLY the commit message.
36
- 15. Do not include markdown, quotes, explanations, or additional text.
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 settings, setup_logging
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
- typer.echo(f"cmt-cli version: {version(settings.APP_NAME)}")
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
- typer.echo(ctx.get_help())
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
- typer.echo(
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
- analysis_result = analyzer.analyze(staged_files)
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
- open_ai = OpenAIProvider()
73
+ open_ai = OpenAIProvider()
66
74
 
67
- commit = open_ai.generate_commit_message(staged_files, analysis_result)
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:\n\n%s", commit_command)
72
- typer.echo(f"Suggested commit:\n\n{commit_command}")
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("Use this message? [y]es / [e]dit / [n]o", default="y")
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
- typer.echo("Aborted.")
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
- typer.echo(f"Edited commit:\n\n{commit_command}")
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
- typer.echo(f"Commit result:\n\n{result.stdout}")
108
+ console.print_success("Code committed")
95
109
  break
96
110
 
97
111
  except CmtError as e:
98
- typer.echo(f"Error: {e}", err=True)
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
- typer.echo("\nOperation cancelled by user.", err=True)
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
- typer.echo(f"Unexpected error: {e}", err=True)
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", default=settings.OPEN_AI_DEFAULT_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
- pypi_service = PyPIService()
146
- latest_info_response = pypi_service.any_update(current_version=current_version)
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
- typer.echo(
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
- typer.echo(f"You are using the latest version of {settings.APP_NAME}")
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,9 @@
1
+ from cmt.core.console import console
2
+ from cmt.core.logging import setup_logging
3
+ from cmt.core.settings import settings
4
+
5
+ __all__ = [
6
+ "console",
7
+ "settings",
8
+ "setup_logging",
9
+ ]
@@ -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.3"
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
 
@@ -1,4 +0,0 @@
1
- from cmt.core.logging import setup_logging
2
- from cmt.core.settings import settings
3
-
4
- __all__ = ["settings", "setup_logging"]
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