sprout-cli 0.4.0__tar.gz → 0.5.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.
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/PKG-INFO +10 -4
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/README.md +9 -3
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/__init__.py +1 -1
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/commands/create.py +65 -57
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/tests/test_commands.py +46 -7
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/tests/test_integration.py +4 -3
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/.gitignore +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/LICENSE +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/pyproject.toml +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/__main__.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/cli.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/commands/__init__.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/commands/ls.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/commands/path.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/commands/rm.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/exceptions.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/types.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/src/sprout/utils.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/tests/__init__.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/tests/test_multi_env.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.5.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sprout-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: CLI tool to automate git worktree and Docker Compose development workflows
|
|
5
5
|
Author: SecDevLab Inc.
|
|
6
6
|
License: MIT
|
|
@@ -41,7 +41,7 @@ A CLI tool to automate git worktree and Docker Compose development workflows.
|
|
|
41
41
|
## Installation
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
pip install sprout
|
|
44
|
+
pip install sprout-cli
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
For development:
|
|
@@ -56,7 +56,9 @@ pip install -e ".[dev]"
|
|
|
56
56
|
|
|
57
57
|
## Quick Start
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
**Note**: Sprout works in any git repository. `.env.example` files are optional - if you don't have them, sprout will simply create worktrees without `.env` generation.
|
|
60
|
+
|
|
61
|
+
1. (Optional) Create a `.env.example` template in your project root (and optionally in subdirectories) for automatic `.env` generation:
|
|
60
62
|
```env
|
|
61
63
|
# API Configuration
|
|
62
64
|
API_KEY={{ API_KEY }}
|
|
@@ -86,9 +88,13 @@ repo/
|
|
|
86
88
|
cd $(sprout create feature-branch --path)
|
|
87
89
|
```
|
|
88
90
|
|
|
91
|
+
**What happens when you run `sprout create`:**
|
|
92
|
+
- If `.env.example` files exist: Sprout will generate corresponding `.env` files with populated variables and unique port assignments
|
|
93
|
+
- If no `.env.example` files exist: Sprout will show a warning and create the worktree without `.env` generation
|
|
94
|
+
|
|
89
95
|
This single command:
|
|
90
96
|
- Creates a new git worktree for `feature-branch`
|
|
91
|
-
- Generates
|
|
97
|
+
- Generates `.env` files from your templates (if `.env.example` files exist)
|
|
92
98
|
- Outputs the path to the new environment
|
|
93
99
|
- Changes to that directory when wrapped in `cd $(...)`
|
|
94
100
|
|
|
@@ -13,7 +13,7 @@ A CLI tool to automate git worktree and Docker Compose development workflows.
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
pip install sprout
|
|
16
|
+
pip install sprout-cli
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
For development:
|
|
@@ -28,7 +28,9 @@ pip install -e ".[dev]"
|
|
|
28
28
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
**Note**: Sprout works in any git repository. `.env.example` files are optional - if you don't have them, sprout will simply create worktrees without `.env` generation.
|
|
32
|
+
|
|
33
|
+
1. (Optional) Create a `.env.example` template in your project root (and optionally in subdirectories) for automatic `.env` generation:
|
|
32
34
|
```env
|
|
33
35
|
# API Configuration
|
|
34
36
|
API_KEY={{ API_KEY }}
|
|
@@ -58,9 +60,13 @@ repo/
|
|
|
58
60
|
cd $(sprout create feature-branch --path)
|
|
59
61
|
```
|
|
60
62
|
|
|
63
|
+
**What happens when you run `sprout create`:**
|
|
64
|
+
- If `.env.example` files exist: Sprout will generate corresponding `.env` files with populated variables and unique port assignments
|
|
65
|
+
- If no `.env.example` files exist: Sprout will show a warning and create the worktree without `.env` generation
|
|
66
|
+
|
|
61
67
|
This single command:
|
|
62
68
|
- Creates a new git worktree for `feature-branch`
|
|
63
|
-
- Generates
|
|
69
|
+
- Generates `.env` files from your templates (if `.env.example` files exist)
|
|
64
70
|
- Outputs the path to the new environment
|
|
65
71
|
- Changes to that directory when wrapped in `cd $(...)`
|
|
66
72
|
|
|
@@ -47,11 +47,9 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
|
47
47
|
|
|
48
48
|
if not env_examples:
|
|
49
49
|
if not path_only:
|
|
50
|
-
console.print("[
|
|
51
|
-
console.print(f"
|
|
52
|
-
|
|
53
|
-
typer.echo(f"Error: No .env.example files found in {git_root}", err=True)
|
|
54
|
-
raise typer.Exit(1)
|
|
50
|
+
console.print("[yellow]Warning: No .env.example files found[/yellow]")
|
|
51
|
+
console.print(f"Proceeding without .env generation in: {git_root}")
|
|
52
|
+
# Continue execution without exiting
|
|
55
53
|
|
|
56
54
|
# Check if worktree already exists
|
|
57
55
|
if worktree_exists(branch_name):
|
|
@@ -87,58 +85,59 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
|
87
85
|
typer.echo(f"Error creating worktree: {e}", err=True)
|
|
88
86
|
raise typer.Exit(1) from e
|
|
89
87
|
|
|
90
|
-
# Generate .env files
|
|
91
|
-
if
|
|
92
|
-
console.print(f"Generating .env files from {len(env_examples)} template(s)...")
|
|
93
|
-
|
|
94
|
-
# Get all currently used ports to avoid conflicts
|
|
95
|
-
all_used_ports = get_used_ports()
|
|
96
|
-
session_ports: set[int] = set()
|
|
97
|
-
|
|
98
|
-
try:
|
|
99
|
-
for env_example in env_examples:
|
|
100
|
-
# Calculate relative path from git root
|
|
101
|
-
relative_dir = env_example.parent.relative_to(git_root)
|
|
102
|
-
|
|
103
|
-
# Create target directory in worktree if needed
|
|
104
|
-
if relative_dir != Path("."):
|
|
105
|
-
target_dir = worktree_path / relative_dir
|
|
106
|
-
target_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
-
env_file = target_dir / ".env"
|
|
108
|
-
else:
|
|
109
|
-
env_file = worktree_path / ".env"
|
|
110
|
-
|
|
111
|
-
# Parse template with combined used ports
|
|
112
|
-
env_content = parse_env_template(
|
|
113
|
-
env_example, silent=path_only, used_ports=all_used_ports | session_ports
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
# Extract ports from generated content and add to session_ports
|
|
117
|
-
port_matches = re.findall(r"=(\d{4,5})\b", env_content)
|
|
118
|
-
for port_str in port_matches:
|
|
119
|
-
port = int(port_str)
|
|
120
|
-
if 1024 <= port <= 65535:
|
|
121
|
-
session_ports.add(port)
|
|
122
|
-
|
|
123
|
-
# Write the .env file
|
|
124
|
-
env_file.write_text(env_content)
|
|
125
|
-
|
|
126
|
-
except SproutError as e:
|
|
127
|
-
if not path_only:
|
|
128
|
-
console.print(f"[red]Error generating .env file: {e}[/red]")
|
|
129
|
-
else:
|
|
130
|
-
typer.echo(f"Error generating .env file: {e}", err=True)
|
|
131
|
-
# Clean up worktree on failure
|
|
132
|
-
run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
|
|
133
|
-
raise typer.Exit(1) from e
|
|
134
|
-
except KeyboardInterrupt:
|
|
88
|
+
# Generate .env files only if .env.example files exist
|
|
89
|
+
if env_examples:
|
|
135
90
|
if not path_only:
|
|
136
|
-
console.print("
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
91
|
+
console.print(f"Generating .env files from {len(env_examples)} template(s)...")
|
|
92
|
+
|
|
93
|
+
# Get all currently used ports to avoid conflicts
|
|
94
|
+
all_used_ports = get_used_ports()
|
|
95
|
+
session_ports: set[int] = set()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
for env_example in env_examples:
|
|
99
|
+
# Calculate relative path from git root
|
|
100
|
+
relative_dir = env_example.parent.relative_to(git_root)
|
|
101
|
+
|
|
102
|
+
# Create target directory in worktree if needed
|
|
103
|
+
if relative_dir != Path("."):
|
|
104
|
+
target_dir = worktree_path / relative_dir
|
|
105
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
env_file = target_dir / ".env"
|
|
107
|
+
else:
|
|
108
|
+
env_file = worktree_path / ".env"
|
|
109
|
+
|
|
110
|
+
# Parse template with combined used ports
|
|
111
|
+
env_content = parse_env_template(
|
|
112
|
+
env_example, silent=path_only, used_ports=all_used_ports | session_ports
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Extract ports from generated content and add to session_ports
|
|
116
|
+
port_matches = re.findall(r"=(\d{4,5})\b", env_content)
|
|
117
|
+
for port_str in port_matches:
|
|
118
|
+
port = int(port_str)
|
|
119
|
+
if 1024 <= port <= 65535:
|
|
120
|
+
session_ports.add(port)
|
|
121
|
+
|
|
122
|
+
# Write the .env file
|
|
123
|
+
env_file.write_text(env_content)
|
|
124
|
+
|
|
125
|
+
except SproutError as e:
|
|
126
|
+
if not path_only:
|
|
127
|
+
console.print(f"[red]Error generating .env file: {e}[/red]")
|
|
128
|
+
else:
|
|
129
|
+
typer.echo(f"Error generating .env file: {e}", err=True)
|
|
130
|
+
# Clean up worktree on failure
|
|
131
|
+
run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
|
|
132
|
+
raise typer.Exit(1) from e
|
|
133
|
+
except KeyboardInterrupt:
|
|
134
|
+
if not path_only:
|
|
135
|
+
console.print("\n[yellow]Cancelled by user[/yellow]")
|
|
136
|
+
else:
|
|
137
|
+
typer.echo("Cancelled by user", err=True)
|
|
138
|
+
# Clean up worktree on cancellation
|
|
139
|
+
run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
|
|
140
|
+
raise typer.Exit(130) from None
|
|
142
141
|
|
|
143
142
|
# Success message or path output
|
|
144
143
|
if path_only:
|
|
@@ -146,8 +145,17 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
|
146
145
|
print(str(worktree_path))
|
|
147
146
|
else:
|
|
148
147
|
console.print(f"\n[green]✅ Workspace '{branch_name}' created successfully![/green]\n")
|
|
148
|
+
if env_examples:
|
|
149
|
+
console.print(f"Generated .env files from {len(env_examples)} template(s)")
|
|
150
|
+
else:
|
|
151
|
+
console.print("No .env files generated (no .env.example templates found)")
|
|
149
152
|
console.print("Navigate to your new environment with:")
|
|
150
|
-
|
|
153
|
+
try:
|
|
154
|
+
relative_path = worktree_path.relative_to(Path.cwd())
|
|
155
|
+
console.print(f" [cyan]cd {relative_path}[/cyan]")
|
|
156
|
+
except ValueError:
|
|
157
|
+
# If worktree_path is not relative to current directory, show absolute path
|
|
158
|
+
console.print(f" [cyan]cd {worktree_path}[/cyan]")
|
|
151
159
|
|
|
152
160
|
# Exit successfully
|
|
153
161
|
raise typer.Exit(0)
|
|
@@ -126,20 +126,37 @@ class TestCreateCommand:
|
|
|
126
126
|
# stdout should be empty
|
|
127
127
|
assert result.stdout == ""
|
|
128
128
|
|
|
129
|
-
def test_create_no_env_example(self, mocker):
|
|
130
|
-
"""Test
|
|
129
|
+
def test_create_no_env_example(self, mocker, tmp_path):
|
|
130
|
+
"""Test success when .env.example doesn't exist."""
|
|
131
131
|
mocker.patch("sprout.commands.create.is_git_repository", return_value=True)
|
|
132
|
-
mock_git_root =
|
|
132
|
+
mock_git_root = tmp_path / "project"
|
|
133
|
+
mock_git_root.mkdir()
|
|
133
134
|
mocker.patch("sprout.commands.create.get_git_root", return_value=mock_git_root)
|
|
135
|
+
mocker.patch("sprout.commands.create.worktree_exists", return_value=False)
|
|
136
|
+
sprout_dir = tmp_path / ".sprout"
|
|
137
|
+
sprout_dir.mkdir()
|
|
138
|
+
mocker.patch("sprout.commands.create.ensure_sprout_dir", return_value=sprout_dir)
|
|
139
|
+
mocker.patch("sprout.commands.create.branch_exists", return_value=False)
|
|
134
140
|
|
|
135
|
-
# Mock git ls-files to return empty list
|
|
141
|
+
# Mock git ls-files to return empty list (no .env.example files)
|
|
136
142
|
mock_run = mocker.patch("sprout.commands.create.run_command")
|
|
137
143
|
mock_run.return_value = Mock(stdout="", returncode=0)
|
|
138
144
|
|
|
139
|
-
|
|
145
|
+
# Change to project directory to make relative path calculation work
|
|
146
|
+
import os
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
old_cwd = os.getcwd()
|
|
149
|
+
os.chdir(str(mock_git_root))
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = runner.invoke(app, ["create", "feature-branch"])
|
|
153
|
+
|
|
154
|
+
assert result.exit_code == 0
|
|
155
|
+
assert "Warning: No .env.example files found" in result.stdout
|
|
156
|
+
assert "Workspace 'feature-branch' created successfully!" in result.stdout
|
|
157
|
+
assert "No .env files generated (no .env.example templates found)" in result.stdout
|
|
158
|
+
finally:
|
|
159
|
+
os.chdir(old_cwd)
|
|
143
160
|
|
|
144
161
|
def test_create_worktree_exists(self, mocker):
|
|
145
162
|
"""Test error when worktree already exists."""
|
|
@@ -161,6 +178,28 @@ class TestCreateCommand:
|
|
|
161
178
|
assert result.exit_code == 1
|
|
162
179
|
assert "Worktree for branch 'feature-branch' already exists" in result.stdout
|
|
163
180
|
|
|
181
|
+
def test_create_without_env_example_path_mode(self, mocker, tmp_path):
|
|
182
|
+
"""Test path mode with no .env.example files."""
|
|
183
|
+
mocker.patch("sprout.commands.create.is_git_repository", return_value=True)
|
|
184
|
+
mock_git_root = tmp_path / "project"
|
|
185
|
+
mock_git_root.mkdir()
|
|
186
|
+
mocker.patch("sprout.commands.create.get_git_root", return_value=mock_git_root)
|
|
187
|
+
mocker.patch("sprout.commands.create.worktree_exists", return_value=False)
|
|
188
|
+
sprout_dir = tmp_path / ".sprout"
|
|
189
|
+
sprout_dir.mkdir()
|
|
190
|
+
mocker.patch("sprout.commands.create.ensure_sprout_dir", return_value=sprout_dir)
|
|
191
|
+
mocker.patch("sprout.commands.create.branch_exists", return_value=False)
|
|
192
|
+
|
|
193
|
+
# Mock git ls-files to return empty list (no .env.example files)
|
|
194
|
+
mock_run = mocker.patch("sprout.commands.create.run_command")
|
|
195
|
+
mock_run.return_value = Mock(stdout="", returncode=0)
|
|
196
|
+
|
|
197
|
+
result = runner.invoke(app, ["create", "feature-branch", "--path"])
|
|
198
|
+
|
|
199
|
+
assert result.exit_code == 0
|
|
200
|
+
# In path mode, only the path should be printed to stdout
|
|
201
|
+
assert result.stdout.strip() == str(sprout_dir / "feature-branch")
|
|
202
|
+
|
|
164
203
|
|
|
165
204
|
class TestLsCommand:
|
|
166
205
|
"""Test sprout ls command."""
|
|
@@ -223,11 +223,12 @@ class TestIntegrationWorkflow:
|
|
|
223
223
|
result = runner.invoke(app, ["path", "nonexistent"])
|
|
224
224
|
assert result.exit_code == 1
|
|
225
225
|
|
|
226
|
-
# Remove .env.example and try to create
|
|
226
|
+
# Remove .env.example and try to create (should succeed now)
|
|
227
227
|
(git_repo / ".env.example").unlink()
|
|
228
228
|
result = runner.invoke(app, ["create", "another-branch"])
|
|
229
|
-
assert result.exit_code ==
|
|
230
|
-
assert "No .env.example files found" in result.stdout
|
|
229
|
+
assert result.exit_code == 0
|
|
230
|
+
assert "Warning: No .env.example files found" in result.stdout
|
|
231
|
+
assert "Workspace 'another-branch' created successfully!" in result.stdout
|
|
231
232
|
|
|
232
233
|
# Test outside git repo using a separate temp directory
|
|
233
234
|
import tempfile
|
|
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
|