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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sprout-cli
3
- Version: 0.4.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
- 1. Create a `.env.example` template in your project root (and optionally in subdirectories):
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 a `.env` file from your template
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
- 1. Create a `.env.example` template in your project root (and optionally in subdirectories):
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 a `.env` file from your template
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
 
@@ -1,3 +1,3 @@
1
1
  """sprout - CLI tool to automate git worktree and Docker Compose development workflows."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.5.0"
@@ -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("[red]Error: No .env.example files found[/red]")
51
- console.print(f"Expected at least one .env.example file in: {git_root}")
52
- else:
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 not path_only:
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("\n[yellow]Cancelled by user[/yellow]")
137
- else:
138
- typer.echo("Cancelled by user", err=True)
139
- # Clean up worktree on cancellation
140
- run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
141
- raise typer.Exit(130) from None
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
- console.print(f" [cyan]cd {worktree_path.relative_to(Path.cwd())}[/cyan]")
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 error when .env.example doesn't exist."""
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 = Path("/project")
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
- result = runner.invoke(app, ["create", "feature-branch"])
145
+ # Change to project directory to make relative path calculation work
146
+ import os
140
147
 
141
- assert result.exit_code == 1
142
- assert "No .env.example files found" in result.stdout
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 == 1
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