git-cai-cli 0.1.0__py3-none-any.whl → 0.1.dev1__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.
git_cai_cli/__init__.py CHANGED
@@ -0,0 +1,5 @@
1
+ """git-cai-cli: AI-powered git commit message generator."""
2
+
3
+ from ._version import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.dev1'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev1')
23
+
24
+ __commit_id__ = commit_id = None
File without changes
git_cai_cli/cli/cli.py ADDED
@@ -0,0 +1,371 @@
1
+ """
2
+ CLI entry point for git-cai-cli.
3
+ """
4
+
5
+ import logging
6
+
7
+ import typer
8
+ from git_cai_cli.cli.helptext import print_help_and_exit
9
+ from git_cai_cli.cli.modes import resolve_mode, validate_options
10
+ from git_cai_cli.main import run
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+ app = typer.Typer(add_completion=False, help=None, no_args_is_help=False)
15
+
16
+
17
+ @app.callback(invoke_without_command=True)
18
+ def callback( # pylint: disable=too-many-arguments,too-many-positional-arguments
19
+ version: bool = typer.Option(
20
+ False, "-v", "--version", help="Show version", is_eager=True
21
+ ),
22
+ help_flag: bool = typer.Option(
23
+ False, "-h", "--help", help="Show help", is_eager=True
24
+ ),
25
+ install_completion: bool = typer.Option(
26
+ False,
27
+ "--install-completion",
28
+ "-i",
29
+ help="Install shell completion for git-cai",
30
+ is_eager=True,
31
+ ),
32
+ amend: bool = typer.Option(
33
+ False, "-A", "--amend", help="Regenerate and amend the last commit message"
34
+ ),
35
+ branch_context: bool | None = typer.Option(
36
+ None,
37
+ "--branch/--no-branch",
38
+ "-b",
39
+ help="Include current branch name as context for the LLM. Use --no-branch to explicitly disable when the persisted config has it enabled.",
40
+ ),
41
+ conventional: bool | None = typer.Option(
42
+ None,
43
+ "--conventional/--no-conventional",
44
+ "-C",
45
+ help="Use Conventional Commits format (type(scope): description). Use --no-conventional to explicitly disable when the persisted config has it enabled.",
46
+ ),
47
+ crazy: bool = typer.Option(
48
+ False, "-c", "--crazy", help="Commit immediately without opening editor"
49
+ ),
50
+ enable_debug: bool = typer.Option(
51
+ False, "--debug", "-d", help="Enable debug logging"
52
+ ),
53
+ generate_config: bool = typer.Option(
54
+ False,
55
+ "-g",
56
+ "--generate-config",
57
+ help="Generate default cai_config.yml in the current directory",
58
+ ),
59
+ generate_prompts: bool = typer.Option(
60
+ False,
61
+ "-p",
62
+ "--generate-prompts",
63
+ help="Generate default commit/squash prompt files in the current directory",
64
+ ),
65
+ list_flag: bool = typer.Option(
66
+ False,
67
+ "--list",
68
+ "-l",
69
+ help="List information (e.g. languages, styles, providers, config)",
70
+ is_flag=True,
71
+ ),
72
+ list_arg: str = typer.Argument(
73
+ None, help="Optional argument for --list or --squash (see docs)"
74
+ ),
75
+ stage_tracked: bool = typer.Option(
76
+ False, "--all", "-a", help="Stage all tracked files"
77
+ ),
78
+ squash: bool = typer.Option(
79
+ False, "--squash", "-s", help="Squash commits on this branch"
80
+ ),
81
+ pr: bool = typer.Option(
82
+ False,
83
+ "--PR",
84
+ "-r",
85
+ help="Generate a Pull Request description from the commits on this branch",
86
+ ),
87
+ base: str = typer.Option(
88
+ None,
89
+ "--base",
90
+ help="Explicit base branch for --PR (overrides auto-detection)",
91
+ ),
92
+ update: bool = typer.Option(False, "--update", "-u", help="Check for updates"),
93
+ set_config: str = typer.Option(
94
+ None,
95
+ "-S",
96
+ "--set",
97
+ help="Set a config value in repo config (key=value). Requires existing repo config.",
98
+ ),
99
+ set_home: str = typer.Option(
100
+ None,
101
+ "-H",
102
+ "--set-home",
103
+ help="Set a config value in home config (key=value). Always targets ~/.config/cai/.",
104
+ ),
105
+ provider: str = typer.Option(
106
+ None, "--provider", "-P", help="Override LLM provider for this invocation"
107
+ ),
108
+ model: str = typer.Option(
109
+ None, "--model", "-m", help="Override model (requires --provider)"
110
+ ),
111
+ context: str = typer.Option(
112
+ None,
113
+ "-x",
114
+ "--context",
115
+ help="Extra context for the LLM (e.g. ticket number, reason for change)",
116
+ ),
117
+ time_flag: bool = typer.Option(
118
+ False, "--time", "-t", help="Measure and log generation time"
119
+ ),
120
+ timeout: int = typer.Option(
121
+ None,
122
+ "-T",
123
+ "--timeout",
124
+ help="HTTP timeout in seconds for this invocation (overrides config; default 30).",
125
+ ),
126
+ full_files: bool | None = typer.Option(
127
+ None,
128
+ "--full-files/--no-full-files",
129
+ "-F",
130
+ help="Send the full contents of affected files alongside the diff. Use --no-full-files to explicitly disable when the persisted config has it enabled.",
131
+ ),
132
+ files: list[str] = typer.Option(
133
+ None,
134
+ "-f",
135
+ "--files",
136
+ help="Limit the diff (and full-file content, if enabled) to these paths. Repeat for multiple files.",
137
+ ),
138
+ sql: str = typer.Option(
139
+ None,
140
+ "--sql",
141
+ "-q",
142
+ help="Override stats writing for this run: --sql true / --sql false. Wins over the persisted `stats` config.",
143
+ ),
144
+ stats: bool = typer.Option(
145
+ False,
146
+ "--stats",
147
+ "-z",
148
+ help="Show local-only usage analytics (commits per provider, token totals, latency).",
149
+ ),
150
+ stats_since: str = typer.Option(
151
+ None,
152
+ "--since",
153
+ help="Filter --stats to events on or after this date (YYYY-MM-DD).",
154
+ ),
155
+ stats_json: bool = typer.Option(
156
+ False,
157
+ "--json",
158
+ help="Render --stats output as JSON instead of text.",
159
+ ),
160
+ stats_reset: bool = typer.Option(
161
+ False,
162
+ "--reset-stats",
163
+ help="Delete all rows from the local stats DB.",
164
+ ),
165
+ init: bool = typer.Option(
166
+ False,
167
+ "--init",
168
+ "-I",
169
+ help="Interactive setup wizard: writes ~/.config/cai/cai_config.yml and tokens.yml.",
170
+ ),
171
+ signoff: bool | None = typer.Option(
172
+ None,
173
+ "--signoff/--no-signoff",
174
+ "-o",
175
+ help="Append a `Signed-off-by:` trailer using git user.name/user.email. Use --no-signoff to disable when the persisted config has it enabled.",
176
+ ),
177
+ print_only: bool = typer.Option(
178
+ False,
179
+ "--print",
180
+ help="Generate the commit message, print it to stdout, and exit without committing.",
181
+ ),
182
+ temperature: float = typer.Option(
183
+ None,
184
+ "-E",
185
+ "--temperature",
186
+ help="Override the active provider's sampling temperature for this invocation.",
187
+ ),
188
+ style: str = typer.Option(
189
+ None,
190
+ "-y",
191
+ "--style",
192
+ help="Override the commit message style for this invocation (e.g. funny, neutral, none).",
193
+ ),
194
+ language: str = typer.Option(
195
+ None,
196
+ "-L",
197
+ "--language",
198
+ help="Override the commit message language code for this invocation (e.g. de, fr, none).",
199
+ ),
200
+ emoji: bool | None = typer.Option(
201
+ None,
202
+ "-e",
203
+ "--emoji/--no-emoji",
204
+ help="Override emoji usage for this invocation. Use --no-emoji to disable when config enables it.",
205
+ ),
206
+ check: bool = typer.Option(
207
+ False,
208
+ "-k",
209
+ "--check",
210
+ help="Run configuration diagnostics (offline). Add --ping for a live provider probe.",
211
+ ),
212
+ ping: bool = typer.Option(
213
+ False,
214
+ "-n",
215
+ "--ping",
216
+ help="With --check, also send a tiny request to the active provider to confirm reachability.",
217
+ ),
218
+ allow_secrets: bool = typer.Option(
219
+ False,
220
+ "-B",
221
+ "--allow-secrets",
222
+ help="Bypass the local secret scan and send the diff even if a likely secret is detected.",
223
+ ),
224
+ ):
225
+ """
226
+ CLI entry point for git-cai-cli.
227
+ """
228
+ if help_flag:
229
+ print_help_and_exit()
230
+
231
+ if version:
232
+ from git_cai_cli._version import __version__
233
+
234
+ typer.echo(f"cai version: {__version__}")
235
+ raise typer.Exit()
236
+
237
+ if install_completion:
238
+ from git_cai_cli.core.completion import install_completion as do_install
239
+
240
+ do_install()
241
+ raise typer.Exit()
242
+
243
+ mode = resolve_mode(
244
+ amend=amend,
245
+ check=check,
246
+ init=init,
247
+ list_flag=list_flag,
248
+ pr=pr,
249
+ squash=squash,
250
+ stats=stats,
251
+ update=update,
252
+ )
253
+
254
+ if ping and not check:
255
+ typer.echo("Error: --ping is only valid together with --check.", err=True)
256
+ raise typer.Exit(code=1)
257
+
258
+ sql_override: bool | None = None
259
+ if sql is not None:
260
+ normalized = sql.strip().lower()
261
+ if normalized in ("true", "1", "yes", "on"):
262
+ sql_override = True
263
+ elif normalized in ("false", "0", "no", "off"):
264
+ sql_override = False
265
+ else:
266
+ typer.echo(f"Error: --sql expects true/false, got {sql!r}.", err=True)
267
+ raise typer.Exit(code=1)
268
+
269
+ validate_options(
270
+ mode=mode,
271
+ stage_tracked=stage_tracked,
272
+ enable_debug=enable_debug,
273
+ help_flag=help_flag,
274
+ version_flag=version,
275
+ provider_override=provider,
276
+ model_override=model,
277
+ time_flag=time_flag,
278
+ context=context,
279
+ files=files,
280
+ print_only=print_only,
281
+ crazy=crazy,
282
+ )
283
+
284
+ if generate_config:
285
+ from git_cai_cli.core.options import CliManager
286
+
287
+ manager = CliManager(package_name="git-cai-cli")
288
+
289
+ try:
290
+ manager.generate_config_here()
291
+ typer.echo("cai_config.yml created in current directory.")
292
+ except RuntimeError as e:
293
+ typer.echo(f"Error: {e}", err=True)
294
+ raise typer.Exit(code=1)
295
+
296
+ raise typer.Exit()
297
+
298
+ if generate_prompts:
299
+ from git_cai_cli.core.options import CliManager
300
+
301
+ manager = CliManager(package_name="git-cai-cli")
302
+
303
+ try:
304
+ manager.generate_prompts_here()
305
+ typer.echo(
306
+ "commit_prompt.md, squash_prompt.md, full_files_prompt.md, "
307
+ "and pr_prompt.md created in current directory."
308
+ )
309
+ except RuntimeError as e:
310
+ typer.echo(f"Error: {e}", err=True)
311
+ raise typer.Exit(code=1)
312
+
313
+ raise typer.Exit()
314
+
315
+ if set_config or set_home:
316
+ from git_cai_cli.core.config import set_config_value
317
+ from git_cai_cli.main import configure_logging
318
+
319
+ configure_logging(enable_debug)
320
+
321
+ raw = set_home if set_home else set_config
322
+ force_home = set_home is not None
323
+
324
+ if "=" not in raw:
325
+ log.error("Invalid format: expected key=value, got '%s'", raw)
326
+ raise typer.Exit(code=1)
327
+
328
+ key, value = raw.split("=", 1)
329
+
330
+ try:
331
+ target = set_config_value(key, value, force_home=force_home)
332
+ log.info("Configuration updated: %s = %s in %s", key, value, target)
333
+ except ValueError as e:
334
+ log.error("Failed to set config: %s", e)
335
+ raise typer.Exit(code=1)
336
+
337
+ raise typer.Exit()
338
+
339
+ run(
340
+ mode=mode,
341
+ enable_debug=enable_debug,
342
+ list_arg=list_arg,
343
+ stage_tracked=stage_tracked,
344
+ conventional=conventional,
345
+ branch_context=branch_context,
346
+ crazy=crazy,
347
+ provider_override=provider,
348
+ model_override=model,
349
+ time_flag=time_flag,
350
+ context=context,
351
+ timeout_override=timeout,
352
+ full_files_override=full_files,
353
+ files_override=files,
354
+ base_override=base,
355
+ sql_override=sql_override,
356
+ stats_since=stats_since,
357
+ stats_json=stats_json,
358
+ stats_reset=stats_reset,
359
+ signoff=signoff,
360
+ print_only=print_only,
361
+ temperature_override=temperature,
362
+ style_override=style,
363
+ language_override=language,
364
+ emoji_override=emoji,
365
+ live_check=ping,
366
+ allow_secrets=allow_secrets,
367
+ )
368
+
369
+
370
+ if __name__ == "__main__":
371
+ app()
@@ -0,0 +1,75 @@
1
+ """
2
+ Help text for git-cai-cli.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ HOME = Path.home()
10
+
11
+ HELP_TEXT = f"""
12
+ Git CAI - AI-powered commit message generator
13
+
14
+ Usage:
15
+ git cai Generate commit message from staged changes
16
+
17
+ Flags:
18
+ -A, --amend Regenerate and amend the last commit message
19
+ -a, --all Stage all modified and deleted files that are already tracked by Git
20
+ -B, --allow-secrets Bypass the local secret scan and send even if a secret is detected
21
+ -b, --branch Include current branch name as context for the LLM
22
+ -C, --conventional Use Conventional Commits format (type(scope): description)
23
+ -c, --crazy Commit immediately without opening editor (trust LLM output)
24
+ -d, --debug Enable debug logging
25
+ -E, --temperature FLOAT Override the active provider's sampling temperature for this invocation
26
+ -e, --emoji Toggle emoji prefixes (use --no-emoji to disable)
27
+ -F, --full-files Send the full contents of affected files alongside the diff
28
+ -f, --files PATH Limit the diff (and full files) to PATH; repeat the flag for multiple files
29
+ -g, --generate-config Generate default cai_config.yml in the current directory
30
+ -H, --set-home KEY=VALUE Set a config value in home config (~/.config/cai/)
31
+ -h, --help Show this help message or opens manual
32
+ -I, --init Interactive setup wizard (writes home config and tokens.yml)
33
+ -i, --install-completion Install shell completion for git-cai
34
+ -k, --check Run configuration diagnostics (offline; add --ping for a live probe)
35
+ -L, --language CODE Override the commit message language (e.g. de, fr, none)
36
+ -l, --list [TYPE] List information. TYPE: config, editor, language,
37
+ model, path, provider, style
38
+ -m, --model NAME Override model for this invocation (requires --provider)
39
+ -n, --ping With --check, probe the active provider for reachability
40
+ -o, --signoff Append a `Signed-off-by:` trailer (git user.name / user.email)
41
+ -P, --provider NAME Override LLM provider for this invocation
42
+ -p, --generate-prompts Generate default commit/squash/full_files/pr prompt files
43
+ --print Print the generated commit message to stdout and exit (no commit)
44
+ -q, --sql true|false Override stats writing for this run (wins over config)
45
+ -r, --PR Generate a Pull Request description from the commits on this branch
46
+ --base BRANCH Base branch for --PR (overrides auto-detection)
47
+ -S, --set KEY=VALUE Set a config value in repo config (requires existing repo config)
48
+ -s, --squash [N|HASH] Squash commits on this branch and summarize them
49
+ No argument: squash all commits since branch checkout
50
+ Number: squash the last N commits
51
+ Hash: squash up to and including that commit
52
+ -T, --timeout SECONDS HTTP timeout in seconds (overrides config; default 30)
53
+ -t, --time Measure and log commit message generation time
54
+ -u, --update Check for updates
55
+ -v, --version Show installed version
56
+ -x, --context TEXT Provide extra context for the LLM (e.g. ticket number, reason)
57
+ -y, --style NAME Override the commit message style (e.g. funny, neutral, none)
58
+ -z, --stats Show local-only usage analytics (commits, tokens, latency)
59
+ --since YYYY-MM-DD Filter --stats to events on or after this date
60
+ --json Render --stats output as JSON instead of text
61
+ --reset-stats Delete all rows from the local stats DB
62
+
63
+ Configuration:
64
+ Tokens are loaded from {HOME}/.config/cai/tokens.yml
65
+ Reset to default config by deleting {HOME}/.config/cai/cai_config.yml
66
+ and executing 'git cai' again.
67
+ """
68
+
69
+
70
+ def print_help_and_exit() -> None:
71
+ """
72
+ Print help text and exit.
73
+ """
74
+ typer.echo(HELP_TEXT)
75
+ raise typer.Exit()
@@ -0,0 +1,145 @@
1
+ """
2
+ Module for handling operational modes and validating command-line options.
3
+ """
4
+
5
+ from enum import Enum, auto
6
+
7
+ import typer
8
+
9
+
10
+ class Mode(Enum):
11
+ """
12
+ Enum representing the different operational modes of the CLI.
13
+ """
14
+
15
+ AMEND = auto()
16
+ CHECK = auto()
17
+ COMMIT = auto()
18
+ INIT = auto()
19
+ LIST = auto()
20
+ PR = auto()
21
+ SQUASH = auto()
22
+ STATS = auto()
23
+ UPDATE = auto()
24
+
25
+
26
+ def resolve_mode(
27
+ *,
28
+ amend: bool,
29
+ check: bool = False,
30
+ init: bool = False,
31
+ list_flag: bool,
32
+ pr: bool,
33
+ squash: bool,
34
+ stats: bool = False,
35
+ update: bool,
36
+ ) -> Mode:
37
+ """
38
+ Resolves the operational mode based on the provided flags.
39
+ """
40
+ flags = [amend, check, init, list_flag, pr, squash, stats, update]
41
+ if sum(flags) > 1:
42
+ typer.echo(
43
+ "Error: --amend, --check, --init, --list, --PR, --squash, --stats, and "
44
+ "--update cannot be used together.",
45
+ err=True,
46
+ )
47
+ raise typer.Exit(code=1)
48
+
49
+ if amend:
50
+ return Mode.AMEND
51
+ if check:
52
+ return Mode.CHECK
53
+ if init:
54
+ return Mode.INIT
55
+ if list_flag:
56
+ return Mode.LIST
57
+ if pr:
58
+ return Mode.PR
59
+ if squash:
60
+ return Mode.SQUASH
61
+ if stats:
62
+ return Mode.STATS
63
+ if update:
64
+ return Mode.UPDATE
65
+
66
+ return Mode.COMMIT
67
+
68
+
69
+ def validate_options(
70
+ *,
71
+ mode: Mode,
72
+ stage_tracked: bool,
73
+ enable_debug: bool,
74
+ help_flag: bool,
75
+ version_flag: bool,
76
+ provider_override: str | None = None,
77
+ model_override: str | None = None,
78
+ time_flag: bool = False,
79
+ context: str | None = None,
80
+ files: list[str] | None = None,
81
+ print_only: bool = False,
82
+ crazy: bool = False,
83
+ ) -> None:
84
+ """
85
+ Validates the combination of command-line options provided by the user.
86
+ """
87
+ if enable_debug and (help_flag or version_flag):
88
+ typer.echo(
89
+ "Error: --debug cannot be used with --help or --version.",
90
+ err=True,
91
+ )
92
+ raise typer.Exit(code=1)
93
+
94
+ if stage_tracked and mode not in (Mode.COMMIT, Mode.AMEND):
95
+ typer.echo(
96
+ "Error: --all cannot be used with --init, --list, --update, --PR, or --squash.",
97
+ err=True,
98
+ )
99
+ raise typer.Exit(code=1)
100
+
101
+ if (provider_override or model_override) and mode in (
102
+ Mode.INIT,
103
+ Mode.LIST,
104
+ Mode.UPDATE,
105
+ ):
106
+ typer.echo(
107
+ "Error: --provider/--model cannot be used with --init, --list, or --update.",
108
+ err=True,
109
+ )
110
+ raise typer.Exit(code=1)
111
+
112
+ if time_flag and mode in (Mode.INIT, Mode.LIST, Mode.UPDATE):
113
+ typer.echo(
114
+ "Error: --time cannot be used with --init, --list, or --update.",
115
+ err=True,
116
+ )
117
+ raise typer.Exit(code=1)
118
+
119
+ if context and mode not in (Mode.COMMIT, Mode.AMEND, Mode.SQUASH, Mode.PR):
120
+ typer.echo(
121
+ "Error: --context cannot be used with --init, --list, or --update.",
122
+ err=True,
123
+ )
124
+ raise typer.Exit(code=1)
125
+
126
+ if files and mode not in (Mode.COMMIT, Mode.AMEND):
127
+ typer.echo(
128
+ "Error: --files cannot be used with --init, --list, --update, --PR, or --squash.",
129
+ err=True,
130
+ )
131
+ raise typer.Exit(code=1)
132
+
133
+ if print_only:
134
+ if mode not in (Mode.COMMIT, Mode.AMEND):
135
+ typer.echo(
136
+ "Error: --print can only be used in COMMIT or AMEND mode.",
137
+ err=True,
138
+ )
139
+ raise typer.Exit(code=1)
140
+ if crazy:
141
+ typer.echo(
142
+ "Error: --print and --crazy are mutually exclusive.",
143
+ err=True,
144
+ )
145
+ raise typer.Exit(code=1)