shortcake 0.1.0__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.
shortcake/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Shortcake CLI package."""
2
+
3
+ __version__ = "0.1.0"
shortcake/cli.py ADDED
@@ -0,0 +1,266 @@
1
+ """CLI module for shortcake."""
2
+
3
+ import re
4
+ import subprocess
5
+ import time
6
+
7
+ import typer
8
+
9
+ from shortcake import config
10
+
11
+ app = typer.Typer(help="Shortcake CLI - A CLI built with typer and uv")
12
+
13
+
14
+ @app.command()
15
+ def version():
16
+ """Show the version."""
17
+ from shortcake import __version__
18
+
19
+ typer.echo(f"Shortcake version {__version__}")
20
+
21
+
22
+ def _generate_branch_name(commit_message: str, keep_emoji: bool = False) -> str:
23
+ """Generate a branch name from a commit message.
24
+
25
+ Converts the commit message to lowercase, replaces spaces with hyphens,
26
+ and removes special characters. Optionally keeps emojis.
27
+
28
+ Args:
29
+ commit_message: The commit message to convert
30
+ keep_emoji: If True, keeps emojis in the branch name. Default is False.
31
+
32
+ Note: Future enhancement - support for gitmoji conventions
33
+ """
34
+ # Convert to lowercase and replace spaces with hyphens
35
+ branch_name = commit_message.lower().strip()
36
+ # Replace multiple spaces with single hyphen
37
+ branch_name = re.sub(r"\s+", "-", branch_name)
38
+
39
+ if keep_emoji:
40
+ # Keep emojis, alphanumeric (including unicode), and hyphens
41
+ # Comprehensive Unicode ranges for emojis:
42
+ # - \U0001F300-\U0001F9FF: Miscellaneous Symbols and Pictographs, Emoticons, etc.
43
+ # - \U0001F600-\U0001F64F: Emoticons
44
+ # - \U0001F680-\U0001F6FF: Transport and Map Symbols
45
+ # - \U00002600-\U000027BF: Miscellaneous Symbols
46
+ # - \U00002B00-\U00002BFF: Miscellaneous Symbols and Arrows (includes ⭐)
47
+ # - \U0001F1E0-\U0001F1FF: Regional Indicator Symbols (flags)
48
+ branch_name = re.sub(
49
+ r"[^\w\-\U0001F300-\U0001F9FF\U0001F600-\U0001F64F\U0001F680-\U0001F6FF\U00002600-\U000027BF\U00002B00-\U00002BFF\U0001F1E0-\U0001F1FF]",
50
+ "",
51
+ branch_name,
52
+ )
53
+ else:
54
+ # Remove special characters including emojis, keep hyphens and word chars (including unicode)
55
+ branch_name = re.sub(r"[^\w-]", "", branch_name)
56
+
57
+ # Collapse consecutive hyphens into a single hyphen
58
+ branch_name = re.sub(r"-+", "-", branch_name)
59
+
60
+ # Remove leading/trailing hyphens
61
+ branch_name = branch_name.strip("-")
62
+ # Limit length to 50 characters
63
+ branch_name = branch_name[:50].rstrip("-")
64
+
65
+ return branch_name
66
+
67
+
68
+ @app.command()
69
+ def create():
70
+ """Create a stack with a new branch and commit.
71
+
72
+ Stage your changes first with 'git add', then run this command.
73
+ Opens your configured editor to compose the commit message.
74
+ The branch name is automatically generated from the commit message.
75
+
76
+ Emoji handling in branch names is controlled by the keep_emoji configuration setting
77
+ (use 'shortcake config set keep_emoji true/false').
78
+
79
+ Note: Future enhancement will include gitmoji integration.
80
+ """
81
+ # Get keep_emoji setting from config
82
+ keep_emoji = config.get_keep_emoji()
83
+
84
+ # Get the original branch to restore on error
85
+ original_branch_result = subprocess.run(
86
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
87
+ capture_output=True,
88
+ text=True,
89
+ )
90
+ original_branch = (
91
+ original_branch_result.stdout.strip() if original_branch_result.returncode == 0 else None
92
+ )
93
+
94
+ temp_branch_name = None
95
+
96
+ try:
97
+ # Create a temporary branch name using timestamp
98
+ temp_branch_name = f"temp-shortcake-{int(time.time() * 1000)}"
99
+
100
+ # Create and switch to temporary branch
101
+ subprocess.run(
102
+ ["git", "checkout", "-b", temp_branch_name],
103
+ capture_output=True,
104
+ text=True,
105
+ check=True,
106
+ )
107
+
108
+ # Create commit using git's normal flow (opens editor)
109
+ subprocess.run(["git", "commit"], check=True)
110
+
111
+ # Get the commit message that was just created
112
+ result = subprocess.run(
113
+ ["git", "log", "-1", "--pretty=%s"], capture_output=True, text=True, check=True
114
+ )
115
+ commit_message = result.stdout.strip()
116
+
117
+ if not commit_message:
118
+ typer.echo("Error: Commit message cannot be empty", err=True)
119
+ raise typer.Exit(1)
120
+
121
+ # Generate branch name from commit message
122
+ branch_name = _generate_branch_name(commit_message, keep_emoji=keep_emoji)
123
+
124
+ if not branch_name:
125
+ typer.echo(
126
+ "Error: Could not generate a valid branch name from the commit message", err=True
127
+ )
128
+ raise typer.Exit(1)
129
+
130
+ # Rename the temporary branch to the final name
131
+ subprocess.run(
132
+ ["git", "branch", "-m", temp_branch_name, branch_name],
133
+ capture_output=True,
134
+ text=True,
135
+ check=True,
136
+ )
137
+
138
+ typer.echo(f"Created and switched to branch: {branch_name}")
139
+ typer.echo(f"Created commit: {commit_message}")
140
+
141
+ except subprocess.CalledProcessError as e:
142
+ # Clean up: switch back to original branch and delete temp branch if it was created
143
+ if temp_branch_name and original_branch:
144
+ subprocess.run(
145
+ ["git", "checkout", original_branch],
146
+ capture_output=True,
147
+ text=True,
148
+ )
149
+ subprocess.run(
150
+ ["git", "branch", "-D", temp_branch_name],
151
+ capture_output=True,
152
+ text=True,
153
+ )
154
+
155
+ typer.echo(f"Error: {e.stderr.strip() if e.stderr else 'Command failed'}", err=True)
156
+ raise typer.Exit(1) from None
157
+
158
+
159
+ @app.command()
160
+ def edit():
161
+ """Edit the current stack by amending the commit.
162
+
163
+ Stage your changes first with 'git add', then run this command.
164
+ Amends the previous commit without opening an editor.
165
+ """
166
+ try:
167
+ # Check if there are staged changes
168
+ diff_result = subprocess.run(
169
+ ["git", "diff", "--cached", "--quiet"],
170
+ capture_output=True,
171
+ text=True,
172
+ )
173
+
174
+ # git diff --quiet returns 0 if no changes, 1 if there are changes
175
+ if diff_result.returncode == 0:
176
+ typer.echo("Error: No staged changes to amend. Use 'git add' first.", err=True)
177
+ raise typer.Exit(1)
178
+
179
+ # Amend the commit without opening editor (reuse previous message)
180
+ subprocess.run(
181
+ ["git", "commit", "--amend", "--no-edit"], capture_output=True, text=True, check=True
182
+ )
183
+ typer.echo("Successfully amended the commit")
184
+
185
+ except subprocess.CalledProcessError as e:
186
+ # Only catch errors from the actual commit command, not from diff check above
187
+ if "git commit" in str(e.cmd):
188
+ typer.echo("Error: Failed to amend commit", err=True)
189
+ raise typer.Exit(1) from None
190
+ raise
191
+
192
+
193
+ # Create alias for edit command
194
+ @app.command(name="modify")
195
+ def modify():
196
+ """Alias for edit - Edit the current stack by amending the commit.
197
+
198
+ Stage your changes first with 'git add', then run this command.
199
+ Amends the previous commit without opening an editor.
200
+ """
201
+ edit()
202
+
203
+
204
+ @app.command(name="config")
205
+ def config_cmd(
206
+ action: str = typer.Argument(..., help="Action to perform: 'get', 'set', or 'list'"),
207
+ key: str = typer.Argument(None, help="Configuration key (e.g., 'keep_emoji')"),
208
+ value: str = typer.Argument(None, help="Configuration value (for 'set' action)"),
209
+ ):
210
+ """Manage shortcake configuration.
211
+
212
+ Examples:
213
+ shortcake config list - List all configuration settings
214
+ shortcake config get keep_emoji - Get a specific setting
215
+ shortcake config set keep_emoji true - Set a configuration value
216
+ """
217
+ if action == "list":
218
+ # List all configuration settings
219
+ cfg = config.load_config()
220
+ typer.echo("Current configuration:")
221
+ for field_name, field_value in cfg.model_dump().items():
222
+ typer.echo(f" {field_name} = {field_value}")
223
+ typer.echo(f"\nConfiguration file: {config.get_config_path()}")
224
+
225
+ elif action == "get":
226
+ if not key:
227
+ typer.echo("Error: Key is required for 'get' action", err=True)
228
+ raise typer.Exit(1)
229
+
230
+ cfg = config.load_config()
231
+ cfg_dict = cfg.model_dump()
232
+ if key in cfg_dict:
233
+ typer.echo(f"{key} = {cfg_dict[key]}")
234
+ else:
235
+ typer.echo(f"Configuration key '{key}' not found")
236
+ typer.echo(f"Available keys: {', '.join(cfg_dict.keys())}")
237
+
238
+ elif action == "set":
239
+ if not key or value is None:
240
+ typer.echo("Error: Both key and value are required for 'set' action", err=True)
241
+ raise typer.Exit(1)
242
+
243
+ # Handle boolean values
244
+ if key == "keep_emoji":
245
+ if value.lower() in ("true", "1", "yes"):
246
+ config.set_keep_emoji(True)
247
+ typer.echo(f"Set {key} = true")
248
+ elif value.lower() in ("false", "0", "no"):
249
+ config.set_keep_emoji(False)
250
+ typer.echo(f"Set {key} = false")
251
+ else:
252
+ typer.echo(f"Error: Invalid value for {key}. Use 'true' or 'false'", err=True)
253
+ raise typer.Exit(1)
254
+ else:
255
+ typer.echo(f"Error: Unknown configuration key '{key}'", err=True)
256
+ cfg = config.load_config()
257
+ typer.echo(f"Available keys: {', '.join(cfg.model_dump().keys())}")
258
+ raise typer.Exit(1)
259
+
260
+ else:
261
+ typer.echo(f"Error: Unknown action '{action}'. Use 'list', 'get', or 'set'", err=True)
262
+ raise typer.Exit(1)
263
+
264
+
265
+ if __name__ == "__main__":
266
+ app()
shortcake/config.py ADDED
@@ -0,0 +1,75 @@
1
+ """Configuration management for shortcake."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import rtoml
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ShortcakeConfig(BaseModel):
11
+ """Configuration model for shortcake."""
12
+
13
+ keep_emoji: bool = Field(default=False, description="Whether to keep emojis in branch names")
14
+
15
+
16
+ def get_config_path() -> Path:
17
+ """Get the path to the configuration file following XDG Base Directory spec."""
18
+ xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
19
+
20
+ if xdg_config_home:
21
+ config_dir = Path(xdg_config_home) / "shortcake"
22
+ else:
23
+ config_dir = Path.home() / ".config" / "shortcake"
24
+
25
+ config_dir.mkdir(parents=True, exist_ok=True)
26
+ return config_dir / "config.toml"
27
+
28
+
29
+ def load_config() -> ShortcakeConfig:
30
+ """Load configuration from the user's config file.
31
+
32
+ Returns:
33
+ ShortcakeConfig instance with configuration values.
34
+ Returns default config if file doesn't exist.
35
+ """
36
+ config_path = get_config_path()
37
+ if not config_path.exists():
38
+ return ShortcakeConfig()
39
+
40
+ try:
41
+ config_data = rtoml.load(config_path)
42
+ return ShortcakeConfig.model_validate(config_data)
43
+ except Exception:
44
+ return ShortcakeConfig()
45
+
46
+
47
+ def save_config(config: ShortcakeConfig) -> None:
48
+ """Save configuration to the user's config file.
49
+
50
+ Args:
51
+ config: ShortcakeConfig instance containing configuration values to save.
52
+ """
53
+ config_path = get_config_path()
54
+ rtoml.dump(config.model_dump(), config_path, pretty=True)
55
+
56
+
57
+ def get_keep_emoji() -> bool:
58
+ """Get the keep_emoji configuration value.
59
+
60
+ Returns:
61
+ True if emojis should be kept in branch names, False otherwise.
62
+ """
63
+ config = load_config()
64
+ return config.keep_emoji
65
+
66
+
67
+ def set_keep_emoji(value: bool) -> None:
68
+ """Set the keep_emoji configuration value.
69
+
70
+ Args:
71
+ value: True to keep emojis in branch names, False to remove them.
72
+ """
73
+ config = load_config()
74
+ config.keep_emoji = value
75
+ save_config(config)
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: shortcake
3
+ Version: 0.1.0
4
+ Summary: A CLI built with typer and uv
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.14
7
+ Requires-Dist: pydantic>=2.0.0
8
+ Requires-Dist: rtoml>=0.11.0
9
+ Requires-Dist: typer>=0.9.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # shortcake
13
+
14
+ A CLI application built with [typer](https://typer.tiangolo.com/) and [uv](https://docs.astral.sh/uv/), supporting only Python 3.14.
15
+
16
+ ## Requirements
17
+
18
+ - Python 3.14+
19
+ - uv package manager
20
+
21
+ ## Installation
22
+
23
+ Install dependencies using uv:
24
+
25
+ ```bash
26
+ uv sync
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ Run the CLI using uv:
32
+
33
+ ```bash
34
+ # Show help
35
+ uv run shortcake --help
36
+
37
+ # Say hello with default greeting
38
+ uv run shortcake hello
39
+
40
+ # Say hello with custom name
41
+ uv run shortcake hello --name "Patrick"
42
+
43
+ # Show version
44
+ uv run shortcake version
45
+ ```
46
+
47
+ ## Commands
48
+
49
+ ### `hello`
50
+ Say hello to someone.
51
+
52
+ Options:
53
+ - `--name TEXT`: Name to greet (default: "World")
54
+
55
+ ### `version`
56
+ Show the current version of shortcake.
57
+
58
+ ### `create`
59
+ Create a stack with a new branch and commit.
60
+
61
+ This command helps you create stacked PRs by:
62
+ 1. Creating a temporary branch
63
+ 2. Opening your configured editor ($EDITOR) to compose the commit message (emojis are fully supported! 🎉)
64
+ 3. Creating the commit
65
+ 4. Generating a branch name from the commit message (lowercase, hyphenated, alphanumeric only)
66
+ 5. Renaming the temporary branch to the final branch name
67
+
68
+ **Important:** Stage your changes with `git add` before running this command. Only staged changes will be committed.
69
+
70
+ **Emoji Support:**
71
+ - Commit messages fully support emojis
72
+ - Emoji handling in branch names is controlled by the `keep_emoji` configuration setting
73
+ - Use `shortcake config set keep_emoji true` to preserve emojis in branch names
74
+ - Use `shortcake config set keep_emoji false` to remove emojis from branch names (default)
75
+
76
+ **Note:** Future enhancement will include gitmoji integration for conventional emoji commits.
77
+
78
+ Example:
79
+ ```bash
80
+ # Stage your changes first
81
+ git add .
82
+
83
+ # Basic usage (emojis removed from branch name by default)
84
+ uv run shortcake create
85
+ # Opens your editor to compose commit message
86
+ # Type: 🚀 Add rocket feature
87
+ # Save and close
88
+ # Creates commit: 🚀 Add rocket feature
89
+ # Creates branch: add-rocket-feature
90
+
91
+ # Configure to keep emojis in branch names
92
+ uv run shortcake config set keep_emoji true
93
+
94
+ # Stage changes and create
95
+ git add .
96
+ uv run shortcake create
97
+ # Opens your editor to compose commit message
98
+ # Type: 🔥 Add fire feature
99
+ # Save and close
100
+ # Creates commit: 🔥 Add fire feature
101
+ # Creates branch: 🔥-add-fire-feature
102
+ ```
103
+
104
+ ### `edit` / `modify`
105
+ Edit the current stack by amending the commit.
106
+
107
+ This command helps you modify the current stack by amending the previous commit without opening an editor.
108
+
109
+ **Important:** Stage your changes with `git add` before running this command. Only staged changes will be amended.
110
+
111
+ Example:
112
+ ```bash
113
+ # Make some changes to your files
114
+ echo "more content" >> file.txt
115
+
116
+ # Stage your changes first
117
+ git add .
118
+
119
+ # Amend the previous commit
120
+ uv run shortcake edit
121
+ # Successfully amended the commit (reuses previous commit message)
122
+
123
+ # Or use the modify alias
124
+ uv run shortcake modify
125
+ ```
126
+
127
+ ### `config`
128
+ Manage shortcake configuration settings.
129
+
130
+ Configuration is stored in `~/.shortcake/config.json` in your home directory.
131
+
132
+ Available settings:
133
+ - `keep_emoji`: Whether to keep emojis in branch names (true/false, default: false)
134
+
135
+ **Actions:**
136
+ - `list` - List all configuration settings
137
+ - `get <key>` - Get a specific configuration value
138
+ - `set <key> <value>` - Set a configuration value
139
+
140
+ Example:
141
+ ```bash
142
+ # List all configuration
143
+ uv run shortcake config list
144
+
145
+ # Get a specific setting
146
+ uv run shortcake config get keep_emoji
147
+
148
+ # Set keep_emoji to true
149
+ uv run shortcake config set keep_emoji true
150
+
151
+ # Set keep_emoji to false
152
+ uv run shortcake config set keep_emoji false
153
+ ```
154
+
155
+ ## Development
156
+
157
+ This project uses uv for dependency management and requires Python 3.14 or higher.
@@ -0,0 +1,8 @@
1
+ shortcake/__init__.py,sha256=wMhdtPPRECMOYHAZ24AQzxtFHHiJstljsPkwETWc-Kg,52
2
+ shortcake/cli.py,sha256=aLd4TjBxEAB4vxjddyZJLClHLEmyDquh2AyCFUoepfQ,9424
3
+ shortcake/config.py,sha256=xLM4XIYZBnOBM4lKswol7YV_wL09JKKw7EBl-WHjrPg,2027
4
+ shortcake-0.1.0.dist-info/METADATA,sha256=G_tlZsqqybdRC6EsXmrdhuqt0VgopjlIXwfF25bmGNI,3913
5
+ shortcake-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ shortcake-0.1.0.dist-info/entry_points.txt,sha256=t8Wxen9tlumoUnxmSYdyU-YTN7eSOf5czD7KyTvfzLQ,48
7
+ shortcake-0.1.0.dist-info/licenses/LICENSE,sha256=oYA1Ia1s3TIvpFLy7uhdMlanjFx4Mn-MErxK0rQk5lY,1072
8
+ shortcake-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ shortcake = shortcake.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Patrick Arminio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.