sprout-cli 0.2.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 (26) hide show
  1. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/PKG-INFO +130 -12
  2. sprout_cli-0.4.0/README.md +272 -0
  3. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/pyproject.toml +7 -1
  4. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/__init__.py +1 -1
  5. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/cli.py +12 -7
  6. sprout_cli-0.4.0/src/sprout/commands/create.py +153 -0
  7. sprout_cli-0.4.0/src/sprout/commands/ls.py +48 -0
  8. sprout_cli-0.4.0/src/sprout/commands/path.py +45 -0
  9. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/commands/rm.py +16 -3
  10. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/utils.py +123 -7
  11. sprout_cli-0.4.0/tests/test_commands.py +455 -0
  12. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/tests/test_integration.py +9 -1
  13. sprout_cli-0.4.0/tests/test_multi_env.py +190 -0
  14. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/tests/test_utils.py +84 -0
  15. sprout_cli-0.2.0/README.md +0 -154
  16. sprout_cli-0.2.0/src/sprout/commands/create.py +0 -89
  17. sprout_cli-0.2.0/src/sprout/commands/ls.py +0 -93
  18. sprout_cli-0.2.0/src/sprout/commands/path.py +0 -28
  19. sprout_cli-0.2.0/tests/test_commands.py +0 -196
  20. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/.gitignore +0 -0
  21. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/LICENSE +0 -0
  22. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/__main__.py +0 -0
  23. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/commands/__init__.py +0 -0
  24. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/exceptions.py +0 -0
  25. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/types.py +0 -0
  26. {sprout_cli-0.2.0 → sprout_cli-0.4.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sprout-cli
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: CLI tool to automate git worktree and Docker Compose development workflows
5
5
  Author: SecDevLab Inc.
6
6
  License: MIT
@@ -56,7 +56,7 @@ pip install -e ".[dev]"
56
56
 
57
57
  ## Quick Start
58
58
 
59
- 1. Create a `.env.example` template in your project root:
59
+ 1. Create a `.env.example` template in your project root (and optionally in subdirectories):
60
60
  ```env
61
61
  # API Configuration
62
62
  API_KEY={{ API_KEY }}
@@ -71,35 +71,104 @@ DB_PORT={{ auto_port() }}
71
71
  # DB_NAME=${DB_NAME}
72
72
  ```
73
73
 
74
- 2. Create a new development environment:
75
- ```bash
76
- sprout create feature-branch
74
+ For monorepo or multi-service projects, you can create `.env.example` files in subdirectories:
75
+ ```
76
+ repo/
77
+ .env.example # Root configuration
78
+ service-a/
79
+ .env.example # Service A specific config
80
+ service-b/
81
+ .env.example # Service B specific config
77
82
  ```
78
83
 
79
- 3. Navigate to your new environment:
84
+ 2. Create and navigate to a new development environment in one command:
80
85
  ```bash
81
- cd $(sprout path feature-branch)
86
+ cd $(sprout create feature-branch --path)
82
87
  ```
83
88
 
84
- 4. Start your services:
89
+ This single command:
90
+ - Creates a new git worktree for `feature-branch`
91
+ - Generates a `.env` file from your template
92
+ - Outputs the path to the new environment
93
+ - Changes to that directory when wrapped in `cd $(...)`
94
+
95
+ 3. Start your services:
85
96
  ```bash
86
97
  docker compose up -d
87
98
  ```
88
99
 
100
+ ### Alternative: Two-Step Process
101
+
102
+ If you prefer to see the creation output first:
103
+ ```bash
104
+ # Create the environment
105
+ sprout create feature-branch
106
+
107
+ # Then navigate to it
108
+ cd $(sprout path feature-branch)
109
+ ```
110
+
89
111
  ## Commands
90
112
 
91
- ### `sprout create <branch-name>`
113
+ ### `sprout create <branch-name> [--path]`
92
114
  Create a new development environment with automated setup.
93
115
 
116
+ Options:
117
+ - `--path`: Output only the worktree path (useful for shell command substitution)
118
+
119
+ Examples:
120
+ ```bash
121
+ # Create and see progress messages
122
+ sprout create feature-xyz
123
+
124
+ # Create and navigate in one command
125
+ cd $(sprout create feature-xyz --path)
126
+ ```
127
+
94
128
  ### `sprout ls`
95
129
  List all managed development environments with their status.
96
130
 
97
- ### `sprout rm <branch-name>`
131
+ The output includes index numbers that can be used with other commands:
132
+ ```bash
133
+ sprout ls
134
+ # Output:
135
+ # ┏━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
136
+ # ┃ No. ┃ Branch ┃ Path ┃ Status ┃ Last Modified ┃
137
+ # ┡━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
138
+ # │ 1 │ feature-auth │ .sprout/feat... │ │ 2025-06-27 14:30 │
139
+ # │ 2 │ bugfix-api │ .sprout/bugf... │ │ 2025-06-27 15:45 │
140
+ # └──────┴─────────────────┴─────────────────┴────────┴──────────────────┘
141
+ ```
142
+
143
+ ### `sprout rm <branch-name-or-index>`
98
144
  Remove a development environment (with confirmation prompts).
99
145
 
100
- ### `sprout path <branch-name>`
146
+ You can use either the branch name or the index number from `sprout ls`:
147
+ ```bash
148
+ # Remove by branch name
149
+ sprout rm feature-auth
150
+
151
+ # Remove by index number
152
+ sprout rm 1
153
+ ```
154
+
155
+ ### `sprout path <branch-name-or-index>`
101
156
  Get the filesystem path of a development environment.
102
157
 
158
+ You can use either the branch name or the index number from `sprout ls`:
159
+ ```bash
160
+ # Get path by branch name
161
+ sprout path feature-auth
162
+ # Output: /path/to/project/.sprout/feature-auth
163
+
164
+ # Get path by index number
165
+ sprout path 1
166
+ # Output: /path/to/project/.sprout/feature-auth
167
+
168
+ # Use with cd command
169
+ cd $(sprout path 2)
170
+ ```
171
+
103
172
  ### `sprout --version`
104
173
  Show the version of sprout.
105
174
 
@@ -114,8 +183,9 @@ sprout supports two types of placeholders in `.env.example`:
114
183
 
115
184
  2. **Auto Port Assignment**: `{{ auto_port() }}`
116
185
  - Automatically assigns available ports
117
- - Avoids conflicts with other sprout environments
186
+ - Avoids conflicts across ALL services in ALL sprout environments
118
187
  - Checks system port availability
188
+ - Ensures global uniqueness even in monorepo setups
119
189
 
120
190
  3. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
121
191
  - NOT processed by sprout - passed through as-is
@@ -137,6 +207,54 @@ sprout create another-branch
137
207
  # → Enter a value for 'DATABASE_URL': [user input required]
138
208
  ```
139
209
 
210
+ ## Monorepo Tutorial
211
+
212
+ Try out the monorepo functionality with the included sample:
213
+
214
+ 1. **Navigate to the sample monorepo**:
215
+ ```bash
216
+ cd sample/monorepo
217
+ ```
218
+
219
+ 2. **Set required environment variables**:
220
+ ```bash
221
+ export API_KEY="your-api-key"
222
+ export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp"
223
+ export REACT_APP_API_KEY="your-frontend-api-key"
224
+ export JWT_SECRET="your-jwt-secret"
225
+ export SMTP_USER="your-smtp-username"
226
+ export SMTP_PASS="your-smtp-password"
227
+ ```
228
+
229
+ 3. **Create a development environment**:
230
+ ```bash
231
+ sprout create monorepo-feature
232
+ ```
233
+
234
+ 4. **Navigate to the created environment**:
235
+ ```bash
236
+ cd .sprout/monorepo-feature
237
+ ```
238
+
239
+ 5. **Verify all services have unique ports**:
240
+ ```bash
241
+ find . -name "*.env" -exec echo "=== {} ===" \; -exec cat {} \;
242
+ ```
243
+
244
+ 6. **Start all services**:
245
+ ```bash
246
+ cd sample/monorepo
247
+ docker-compose up -d
248
+ ```
249
+
250
+ The sample includes:
251
+ - **Root service**: Database and Redis with shared configuration
252
+ - **Frontend**: React app with API integration
253
+ - **Backend**: REST API with authentication
254
+ - **Shared**: Utilities with message queue and monitoring
255
+
256
+ Each service gets unique, conflict-free ports automatically!
257
+
140
258
  ## Documentation
141
259
 
142
260
  - [Architecture Overview](docs/sprout-cli/overview.md) - Design philosophy, architecture, and implementation details
@@ -0,0 +1,272 @@
1
+ # sprout
2
+
3
+ A CLI tool to automate git worktree and Docker Compose development workflows.
4
+
5
+ ## Features
6
+
7
+ - 🌱 Create isolated development environments using git worktrees
8
+ - 🔧 Automatic `.env` file generation from templates
9
+ - 🚢 Smart port allocation to avoid conflicts
10
+ - 📁 Centralized worktree management in `.sprout/` directory
11
+ - 🎨 Beautiful CLI interface with colors and tables
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install sprout
17
+ ```
18
+
19
+ For development:
20
+ ```bash
21
+ # Clone the repository
22
+ git clone https://github.com/SecDev-Lab/sprout.git
23
+ cd sprout
24
+
25
+ # Install in development mode
26
+ pip install -e ".[dev]"
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ 1. Create a `.env.example` template in your project root (and optionally in subdirectories):
32
+ ```env
33
+ # API Configuration
34
+ API_KEY={{ API_KEY }}
35
+ API_PORT={{ auto_port() }}
36
+
37
+ # Database Configuration
38
+ DB_HOST=localhost
39
+ DB_PORT={{ auto_port() }}
40
+
41
+ # Example: Docker Compose variables (preserved as-is)
42
+ # sprout will NOT process ${...} syntax - it's passed through unchanged
43
+ # DB_NAME=${DB_NAME}
44
+ ```
45
+
46
+ For monorepo or multi-service projects, you can create `.env.example` files in subdirectories:
47
+ ```
48
+ repo/
49
+ .env.example # Root configuration
50
+ service-a/
51
+ .env.example # Service A specific config
52
+ service-b/
53
+ .env.example # Service B specific config
54
+ ```
55
+
56
+ 2. Create and navigate to a new development environment in one command:
57
+ ```bash
58
+ cd $(sprout create feature-branch --path)
59
+ ```
60
+
61
+ This single command:
62
+ - Creates a new git worktree for `feature-branch`
63
+ - Generates a `.env` file from your template
64
+ - Outputs the path to the new environment
65
+ - Changes to that directory when wrapped in `cd $(...)`
66
+
67
+ 3. Start your services:
68
+ ```bash
69
+ docker compose up -d
70
+ ```
71
+
72
+ ### Alternative: Two-Step Process
73
+
74
+ If you prefer to see the creation output first:
75
+ ```bash
76
+ # Create the environment
77
+ sprout create feature-branch
78
+
79
+ # Then navigate to it
80
+ cd $(sprout path feature-branch)
81
+ ```
82
+
83
+ ## Commands
84
+
85
+ ### `sprout create <branch-name> [--path]`
86
+ Create a new development environment with automated setup.
87
+
88
+ Options:
89
+ - `--path`: Output only the worktree path (useful for shell command substitution)
90
+
91
+ Examples:
92
+ ```bash
93
+ # Create and see progress messages
94
+ sprout create feature-xyz
95
+
96
+ # Create and navigate in one command
97
+ cd $(sprout create feature-xyz --path)
98
+ ```
99
+
100
+ ### `sprout ls`
101
+ List all managed development environments with their status.
102
+
103
+ The output includes index numbers that can be used with other commands:
104
+ ```bash
105
+ sprout ls
106
+ # Output:
107
+ # ┏━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
108
+ # ┃ No. ┃ Branch ┃ Path ┃ Status ┃ Last Modified ┃
109
+ # ┡━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
110
+ # │ 1 │ feature-auth │ .sprout/feat... │ │ 2025-06-27 14:30 │
111
+ # │ 2 │ bugfix-api │ .sprout/bugf... │ │ 2025-06-27 15:45 │
112
+ # └──────┴─────────────────┴─────────────────┴────────┴──────────────────┘
113
+ ```
114
+
115
+ ### `sprout rm <branch-name-or-index>`
116
+ Remove a development environment (with confirmation prompts).
117
+
118
+ You can use either the branch name or the index number from `sprout ls`:
119
+ ```bash
120
+ # Remove by branch name
121
+ sprout rm feature-auth
122
+
123
+ # Remove by index number
124
+ sprout rm 1
125
+ ```
126
+
127
+ ### `sprout path <branch-name-or-index>`
128
+ Get the filesystem path of a development environment.
129
+
130
+ You can use either the branch name or the index number from `sprout ls`:
131
+ ```bash
132
+ # Get path by branch name
133
+ sprout path feature-auth
134
+ # Output: /path/to/project/.sprout/feature-auth
135
+
136
+ # Get path by index number
137
+ sprout path 1
138
+ # Output: /path/to/project/.sprout/feature-auth
139
+
140
+ # Use with cd command
141
+ cd $(sprout path 2)
142
+ ```
143
+
144
+ ### `sprout --version`
145
+ Show the version of sprout.
146
+
147
+ ## Template Syntax
148
+
149
+ sprout supports two types of placeholders in `.env.example`:
150
+
151
+ 1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
152
+ - **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
153
+ - **Then**: If not found in environment, prompts for user input
154
+ - Example: `{{ API_KEY }}` will use `$API_KEY` if set, otherwise asks you to enter it
155
+
156
+ 2. **Auto Port Assignment**: `{{ auto_port() }}`
157
+ - Automatically assigns available ports
158
+ - Avoids conflicts across ALL services in ALL sprout environments
159
+ - Checks system port availability
160
+ - Ensures global uniqueness even in monorepo setups
161
+
162
+ 3. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
163
+ - NOT processed by sprout - passed through as-is
164
+ - Useful for Docker Compose variable substitution
165
+ - Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
166
+
167
+ ### Environment Variable Resolution Example
168
+
169
+ ```bash
170
+ # Set environment variable
171
+ export API_KEY="my-secret-key"
172
+
173
+ # Create sprout environment - API_KEY will be automatically used
174
+ sprout create feature-branch
175
+ # → API_KEY in .env will be set to "my-secret-key" without prompting
176
+
177
+ # For unset variables, sprout will prompt
178
+ sprout create another-branch
179
+ # → Enter a value for 'DATABASE_URL': [user input required]
180
+ ```
181
+
182
+ ## Monorepo Tutorial
183
+
184
+ Try out the monorepo functionality with the included sample:
185
+
186
+ 1. **Navigate to the sample monorepo**:
187
+ ```bash
188
+ cd sample/monorepo
189
+ ```
190
+
191
+ 2. **Set required environment variables**:
192
+ ```bash
193
+ export API_KEY="your-api-key"
194
+ export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp"
195
+ export REACT_APP_API_KEY="your-frontend-api-key"
196
+ export JWT_SECRET="your-jwt-secret"
197
+ export SMTP_USER="your-smtp-username"
198
+ export SMTP_PASS="your-smtp-password"
199
+ ```
200
+
201
+ 3. **Create a development environment**:
202
+ ```bash
203
+ sprout create monorepo-feature
204
+ ```
205
+
206
+ 4. **Navigate to the created environment**:
207
+ ```bash
208
+ cd .sprout/monorepo-feature
209
+ ```
210
+
211
+ 5. **Verify all services have unique ports**:
212
+ ```bash
213
+ find . -name "*.env" -exec echo "=== {} ===" \; -exec cat {} \;
214
+ ```
215
+
216
+ 6. **Start all services**:
217
+ ```bash
218
+ cd sample/monorepo
219
+ docker-compose up -d
220
+ ```
221
+
222
+ The sample includes:
223
+ - **Root service**: Database and Redis with shared configuration
224
+ - **Frontend**: React app with API integration
225
+ - **Backend**: REST API with authentication
226
+ - **Shared**: Utilities with message queue and monitoring
227
+
228
+ Each service gets unique, conflict-free ports automatically!
229
+
230
+ ## Documentation
231
+
232
+ - [Architecture Overview](docs/sprout-cli/overview.md) - Design philosophy, architecture, and implementation details
233
+ - [Detailed Usage Guide](docs/sprout-cli/usage.md) - Comprehensive usage examples and troubleshooting
234
+
235
+ ## Development
236
+
237
+ ### Setup
238
+ ```bash
239
+ # Install development dependencies
240
+ make setup
241
+ ```
242
+
243
+ ### Testing
244
+ ```bash
245
+ # Run tests
246
+ make test
247
+
248
+ # Run tests with coverage
249
+ make test-cov
250
+ ```
251
+
252
+ ### Code Quality
253
+ ```bash
254
+ # Run linter
255
+ make lint
256
+
257
+ # Format code
258
+ make format
259
+
260
+ # Run type checking
261
+ make typecheck
262
+ ```
263
+
264
+ ## Requirements
265
+
266
+ - Python 3.11+
267
+ - Git
268
+ - Docker Compose (optional, for Docker-based workflows)
269
+
270
+ ## License
271
+
272
+ See LICENSE file.
@@ -122,4 +122,10 @@ ignore_missing_imports = true
122
122
  [[tool.mypy.overrides]]
123
123
  module = "tests.*"
124
124
  disallow_untyped_defs = false
125
- disallow_incomplete_defs = false
125
+ disallow_incomplete_defs = false
126
+
127
+ [dependency-groups]
128
+ dev = [
129
+ "pytest-mock>=3.14.1",
130
+ "ruff>=0.12.1",
131
+ ]
@@ -1,3 +1,3 @@
1
1
  """sprout - CLI tool to automate git worktree and Docker Compose development workflows."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"
@@ -46,9 +46,14 @@ def create(
46
46
  ...,
47
47
  help="Name of the branch to create worktree for",
48
48
  ),
49
+ path: bool = typer.Option(
50
+ False,
51
+ "--path",
52
+ help="Output only the worktree path (for use with shell command substitution)",
53
+ ),
49
54
  ) -> None:
50
55
  """Create a new development environment."""
51
- create_worktree(branch_name)
56
+ create_worktree(branch_name, path_only=path)
52
57
 
53
58
 
54
59
  @app.command()
@@ -59,24 +64,24 @@ def ls() -> None:
59
64
 
60
65
  @app.command()
61
66
  def rm(
62
- branch_name: BranchName = typer.Argument(
67
+ identifier: str = typer.Argument(
63
68
  ...,
64
- help="Name of the branch to remove",
69
+ help="Branch name or index number to remove",
65
70
  ),
66
71
  ) -> None:
67
72
  """Remove a development environment."""
68
- remove_worktree(branch_name)
73
+ remove_worktree(identifier)
69
74
 
70
75
 
71
76
  @app.command()
72
77
  def path(
73
- branch_name: BranchName = typer.Argument(
78
+ identifier: str = typer.Argument(
74
79
  ...,
75
- help="Name of the branch to get path for",
80
+ help="Branch name or index number to get path for",
76
81
  ),
77
82
  ) -> None:
78
83
  """Get the path of a development environment."""
79
- get_worktree_path(branch_name)
84
+ get_worktree_path(identifier)
80
85
 
81
86
 
82
87
  if __name__ == "__main__":
@@ -0,0 +1,153 @@
1
+ """Implementation of the create command."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Never
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from sprout.exceptions import SproutError
11
+ from sprout.types import BranchName
12
+ from sprout.utils import (
13
+ branch_exists,
14
+ ensure_sprout_dir,
15
+ get_git_root,
16
+ get_used_ports,
17
+ is_git_repository,
18
+ parse_env_template,
19
+ run_command,
20
+ worktree_exists,
21
+ )
22
+
23
+ console = Console()
24
+
25
+
26
+ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
27
+ """Create a new worktree with development environment."""
28
+ # Check prerequisites
29
+ if not is_git_repository():
30
+ if not path_only:
31
+ console.print("[red]Error: Not in a git repository[/red]")
32
+ console.print("Please run this command from the root of a git repository.")
33
+ else:
34
+ typer.echo("Error: Not in a git repository", err=True)
35
+ raise typer.Exit(1)
36
+
37
+ git_root = get_git_root()
38
+
39
+ # Find all .env.example files that are tracked by git
40
+ result = run_command(["git", "ls-files", "*.env.example", "**/*.env.example"])
41
+ env_examples = []
42
+ if result.stdout.strip():
43
+ for file_path in result.stdout.strip().split("\n"):
44
+ full_path = git_root / file_path
45
+ if full_path.exists():
46
+ env_examples.append(full_path)
47
+
48
+ if not env_examples:
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)
55
+
56
+ # Check if worktree already exists
57
+ if worktree_exists(branch_name):
58
+ if not path_only:
59
+ console.print(f"[red]Error: Worktree for branch '{branch_name}' already exists[/red]")
60
+ else:
61
+ typer.echo(f"Error: Worktree for branch '{branch_name}' already exists", err=True)
62
+ raise typer.Exit(1)
63
+
64
+ # Ensure .sprout directory exists
65
+ sprout_dir = ensure_sprout_dir()
66
+ worktree_path = sprout_dir / branch_name
67
+
68
+ # Create the worktree
69
+ if not path_only:
70
+ console.print(f"Creating worktree for branch [cyan]{branch_name}[/cyan]...")
71
+
72
+ # Check if branch exists, create if it doesn't
73
+ if not branch_exists(branch_name):
74
+ if not path_only:
75
+ console.print(f"Branch '{branch_name}' doesn't exist. Creating new branch...")
76
+ # Create branch with -b flag
77
+ cmd = ["git", "worktree", "add", "-b", branch_name, str(worktree_path)]
78
+ else:
79
+ cmd = ["git", "worktree", "add", str(worktree_path), branch_name]
80
+
81
+ try:
82
+ run_command(cmd)
83
+ except SproutError as e:
84
+ if not path_only:
85
+ console.print(f"[red]Error creating worktree: {e}[/red]")
86
+ else:
87
+ typer.echo(f"Error creating worktree: {e}", err=True)
88
+ raise typer.Exit(1) from e
89
+
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:
135
+ 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
142
+
143
+ # Success message or path output
144
+ if path_only:
145
+ # Output only the path for shell command substitution
146
+ print(str(worktree_path))
147
+ else:
148
+ console.print(f"\n[green]✅ Workspace '{branch_name}' created successfully![/green]\n")
149
+ console.print("Navigate to your new environment with:")
150
+ console.print(f" [cyan]cd {worktree_path.relative_to(Path.cwd())}[/cyan]")
151
+
152
+ # Exit successfully
153
+ raise typer.Exit(0)