commitstash 0.3.0__tar.gz → 0.4.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 (37) hide show
  1. {commitstash-0.3.0 → commitstash-0.4.0}/CHANGELOG.md +10 -0
  2. {commitstash-0.3.0 → commitstash-0.4.0}/PKG-INFO +2 -1
  3. commitstash-0.4.0/autocommit/__init__.py +1 -0
  4. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/cli.py +13 -1
  5. commitstash-0.4.0/autocommit/onboarding.py +139 -0
  6. {commitstash-0.3.0 → commitstash-0.4.0}/pyproject.toml +2 -1
  7. commitstash-0.4.0/tests/test_onboarding.py +53 -0
  8. commitstash-0.3.0/autocommit/__init__.py +0 -1
  9. {commitstash-0.3.0 → commitstash-0.4.0}/.github/workflows/ci.yml +0 -0
  10. {commitstash-0.3.0 → commitstash-0.4.0}/.github/workflows/publish.yml +0 -0
  11. {commitstash-0.3.0 → commitstash-0.4.0}/.gitignore +0 -0
  12. {commitstash-0.3.0 → commitstash-0.4.0}/.pre-commit-config.yaml +0 -0
  13. {commitstash-0.3.0 → commitstash-0.4.0}/AGENTS.md +0 -0
  14. {commitstash-0.3.0 → commitstash-0.4.0}/LICENSE +0 -0
  15. {commitstash-0.3.0 → commitstash-0.4.0}/README.md +0 -0
  16. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/changelog.py +0 -0
  17. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/config.py +0 -0
  18. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/explain.py +0 -0
  19. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/git.py +0 -0
  20. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/llm.py +0 -0
  21. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/pr.py +0 -0
  22. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/providers.py +0 -0
  23. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/review.py +0 -0
  24. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/secrets.py +0 -0
  25. {commitstash-0.3.0 → commitstash-0.4.0}/autocommit/split.py +0 -0
  26. {commitstash-0.3.0 → commitstash-0.4.0}/tests/__init__.py +0 -0
  27. {commitstash-0.3.0 → commitstash-0.4.0}/tests/conftest.py +0 -0
  28. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_changelog.py +0 -0
  29. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_cli.py +0 -0
  30. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_config.py +0 -0
  31. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_explain.py +0 -0
  32. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_git.py +0 -0
  33. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_llm.py +0 -0
  34. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_pr.py +0 -0
  35. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_review.py +0 -0
  36. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_secrets.py +0 -0
  37. {commitstash-0.3.0 → commitstash-0.4.0}/tests/test_split.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.0 (2026-07-19)
4
+
5
+ ### Features
6
+
7
+ - **onboarding:** add first-run setup flow
8
+
9
+ ### Chores
10
+
11
+ - add author metadata to pyproject
12
+
3
13
  ## v0.3.0 (2026-07-18)
4
14
 
5
15
  ### Features
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commitstash
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: AI-powered git commit message generator — reads your staged diff, writes the commit for you
5
5
  Project-URL: Homepage, https://github.com/suryaSPS/autocommit
6
6
  Project-URL: Issues, https://github.com/suryaSPS/autocommit/issues
7
+ Author-email: Surya Pratap Singh <suryasps20@gmail.com>
7
8
  License: MIT
8
9
  License-File: LICENSE
9
10
  Keywords: ai,cli,commit,developer-tools,git
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -6,7 +6,7 @@ from rich.panel import Panel
6
6
  from rich.text import Text
7
7
 
8
8
  from .changelog import build_changelog
9
- from .config import load_config, save_config
9
+ from .config import CONFIG_PATH, load_config, save_config
10
10
  from .explain import explain as _explain
11
11
  from .git import (
12
12
  get_branch_commits,
@@ -26,6 +26,7 @@ from .git import (
26
26
  unstage_all,
27
27
  )
28
28
  from .llm import generate
29
+ from .onboarding import is_first_run, run_onboarding
29
30
  from .pr import write_pr
30
31
  from .review import offline_review
31
32
  from .review import review as _review
@@ -185,6 +186,17 @@ def cli(ctx, style, emoji, body, provider, no_ai, stage_all_files, yes):
185
186
 
186
187
  config = load_config()
187
188
 
189
+ # First-run onboarding: only when interactive and the user hasn't already
190
+ # said what they want. Never in the git hook (--yes) or a non-TTY pipe.
191
+ if (
192
+ is_first_run(CONFIG_PATH)
193
+ and sys.stdin.isatty()
194
+ and not yes
195
+ and not provider
196
+ and not no_ai
197
+ ):
198
+ config = run_onboarding(console, config)
199
+
188
200
  # CLI flag overrides
189
201
  if style:
190
202
  config["style"] = style
@@ -0,0 +1,139 @@
1
+ """First-run onboarding.
2
+
3
+ Triggers the first time someone runs `autocommit` with no saved config.
4
+ Walks them through picking a provider and getting a key in place.
5
+
6
+ Security: this flow NEVER writes an API key to disk. A key entered here is
7
+ used for the current process only (set in os.environ); to persist it we
8
+ print the exact shell command for the user to add themselves. This mirrors
9
+ the guarantee load_config/save_config already make.
10
+ """
11
+
12
+ import os
13
+
14
+ import click
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+
18
+ from .config import save_config
19
+
20
+ # provider -> (env var name, sample key prefix)
21
+ PROVIDER_ENV = {
22
+ "anthropic": ("ANTHROPIC_API_KEY", "sk-ant-..."),
23
+ "openai": ("OPENAI_API_KEY", "sk-..."),
24
+ }
25
+
26
+ PROVIDER_CHOICES = ["anthropic", "openai", "ollama", "local"]
27
+
28
+ PROVIDER_BLURB = {
29
+ "anthropic": "Anthropic Claude — best quality (needs an API key)",
30
+ "openai": "OpenAI GPT — needs an API key",
31
+ "ollama": "Ollama — a local model on your machine, free, no key",
32
+ "local": "Offline heuristic — no AI, no key, works anywhere",
33
+ }
34
+
35
+
36
+ def is_first_run(config_path) -> bool:
37
+ """True when no config file exists yet."""
38
+ return not config_path.exists()
39
+
40
+
41
+ def _shell_rc_hint(env_name: str) -> str:
42
+ shell = os.environ.get("SHELL", "")
43
+ if shell.endswith("zsh"):
44
+ return "~/.zshrc"
45
+ if shell.endswith("bash"):
46
+ return "~/.bashrc"
47
+ return "your shell profile"
48
+
49
+
50
+ def _setup_key(console: Console, provider: str) -> bool:
51
+ """Ensure a usable key for provider. Returns True if AI is ready this run."""
52
+ env_name, sample = PROVIDER_ENV[provider]
53
+
54
+ if os.getenv(env_name):
55
+ console.print(f"[green]✓ {env_name} is already set in your environment.[/green]")
56
+ return True
57
+
58
+ console.print(f"\n[bold]{provider.title()} needs an API key[/bold] ([dim]{env_name}[/dim]).")
59
+ key = click.prompt(
60
+ f" Paste your key ({sample}), or press Enter to skip for now",
61
+ hide_input=True,
62
+ default="",
63
+ show_default=False,
64
+ ).strip()
65
+
66
+ if not key:
67
+ console.print("[yellow]Skipped.[/yellow] You can set it later:")
68
+ console.print(f" [dim]export {env_name}={sample}[/dim]")
69
+ return False
70
+
71
+ # Session-only: used by this process, never persisted by us.
72
+ os.environ[env_name] = key
73
+ rc = _shell_rc_hint(env_name)
74
+ console.print("[green]✓ Key accepted for this session.[/green] [dim](not written to disk)[/dim]")
75
+ console.print(f"[dim]To keep it for next time, add this line to {rc}:[/dim]")
76
+ console.print(f" [dim]export {env_name}=<the key you just entered>[/dim]")
77
+ return True
78
+
79
+
80
+ def _setup_ollama(console: Console, config: dict) -> None:
81
+ model = click.prompt(" Ollama model", default=config.get("ollama_model", "llama3.2"))
82
+ config["ollama_model"] = model
83
+ console.print("[dim]Make sure Ollama is running before you commit:[/dim]")
84
+ console.print(" [dim]ollama serve[/dim]")
85
+ console.print(f" [dim]ollama pull {model}[/dim]")
86
+
87
+
88
+ def run_onboarding(console: Console, config: dict) -> dict:
89
+ """Interactive first-run setup. Returns the config to use for this run.
90
+
91
+ Persists non-secret preferences (creating the config file, so this won't
92
+ trigger again). If the chosen AI provider has no key available, falls back
93
+ to offline mode for THIS run only so the first commit still succeeds — the
94
+ chosen provider is still saved for next time.
95
+ """
96
+ console.print(
97
+ Panel(
98
+ "Welcome to [bold]autocommit[/bold] 👋\n\n"
99
+ "Let's set up how your commit messages get generated.\n"
100
+ "[dim]Takes about 20 seconds. Change anything later with "
101
+ "[bold]autocommit configure[/bold].[/dim]",
102
+ title="[bold]First-time setup[/bold]",
103
+ border_style="cyan",
104
+ )
105
+ )
106
+
107
+ console.print("\n[bold]Providers[/bold]")
108
+ for name in PROVIDER_CHOICES:
109
+ console.print(f" [cyan]{name}[/cyan] — {PROVIDER_BLURB[name]}")
110
+ console.print()
111
+
112
+ provider = click.prompt(
113
+ "Which provider?",
114
+ type=click.Choice(PROVIDER_CHOICES),
115
+ default="anthropic",
116
+ )
117
+ config["provider"] = provider
118
+
119
+ ready = True
120
+ if provider in PROVIDER_ENV:
121
+ ready = _setup_key(console, provider)
122
+ elif provider == "ollama":
123
+ _setup_ollama(console, config)
124
+ else: # local
125
+ console.print("[green]✓ Offline mode — no key needed.[/green]")
126
+
127
+ path = save_config(config)
128
+ console.print(f"\n[green]✓ Setup saved to {path}[/green]")
129
+
130
+ run_config = dict(config)
131
+ if provider in PROVIDER_ENV and not ready:
132
+ console.print(
133
+ "\n[yellow]No key yet — using offline mode for this commit.[/yellow] "
134
+ f"[dim]Next time, with {PROVIDER_ENV[provider][0]} set, it'll use {provider}.[/dim]"
135
+ )
136
+ run_config["provider"] = "local"
137
+
138
+ console.print()
139
+ return run_config
@@ -4,11 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "commitstash"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "AI-powered git commit message generator — reads your staged diff, writes the commit for you"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = { text = "MIT" }
12
+ authors = [{ name = "Surya Pratap Singh", email = "suryasps20@gmail.com" }]
12
13
  keywords = ["git", "ai", "commit", "cli", "developer-tools"]
13
14
  classifiers = [
14
15
  "Development Status :: 4 - Beta",
@@ -0,0 +1,53 @@
1
+ from rich.console import Console
2
+
3
+ from autocommit import onboarding
4
+
5
+
6
+ def test_is_first_run(tmp_path):
7
+ cfg = tmp_path / "config.json"
8
+ assert onboarding.is_first_run(cfg) is True
9
+ cfg.write_text("{}")
10
+ assert onboarding.is_first_run(cfg) is False
11
+
12
+
13
+ def _patch_config_path(monkeypatch, tmp_path):
14
+ """Point save_config at a temp file so tests never touch the real ~/.autocommit."""
15
+ monkeypatch.setattr("autocommit.config.CONFIG_PATH", tmp_path / "config.json")
16
+
17
+
18
+ def test_onboarding_local_provider(monkeypatch, tmp_path):
19
+ _patch_config_path(monkeypatch, tmp_path)
20
+ monkeypatch.setattr("click.prompt", lambda *a, **k: "local")
21
+
22
+ run_config = onboarding.run_onboarding(Console(), {})
23
+ assert run_config["provider"] == "local"
24
+ assert (tmp_path / "config.json").exists()
25
+
26
+
27
+ def test_onboarding_key_used_for_session_not_persisted(monkeypatch, tmp_path):
28
+ _patch_config_path(monkeypatch, tmp_path)
29
+ monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
30
+ answers = iter(["anthropic", "sk-ant-secret"])
31
+ monkeypatch.setattr("click.prompt", lambda *a, **k: next(answers))
32
+
33
+ run_config = onboarding.run_onboarding(Console(), {})
34
+
35
+ assert run_config["provider"] == "anthropic" # real choice kept
36
+ assert onboarding.os.environ["ANTHROPIC_API_KEY"] == "sk-ant-secret" # session only
37
+ saved = (tmp_path / "config.json").read_text()
38
+ assert "sk-ant-secret" not in saved # never written to disk
39
+
40
+
41
+ def test_onboarding_no_key_falls_back_to_local_this_run(monkeypatch, tmp_path):
42
+ _patch_config_path(monkeypatch, tmp_path)
43
+ monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
44
+ answers = iter(["anthropic", ""]) # choose anthropic, skip the key
45
+ monkeypatch.setattr("click.prompt", lambda *a, **k: next(answers))
46
+
47
+ run_config = onboarding.run_onboarding(Console(), {})
48
+
49
+ assert run_config["provider"] == "local" # this run falls back
50
+ import json
51
+
52
+ saved = json.loads((tmp_path / "config.json").read_text())
53
+ assert saved["provider"] == "anthropic" # but the choice is remembered
@@ -1 +0,0 @@
1
- __version__ = "0.3.0"
File without changes
File without changes
File without changes
File without changes