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.
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/PKG-INFO +130 -12
- sprout_cli-0.4.0/README.md +272 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/pyproject.toml +7 -1
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/__init__.py +1 -1
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/cli.py +12 -7
- sprout_cli-0.4.0/src/sprout/commands/create.py +153 -0
- sprout_cli-0.4.0/src/sprout/commands/ls.py +48 -0
- sprout_cli-0.4.0/src/sprout/commands/path.py +45 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/commands/rm.py +16 -3
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/utils.py +123 -7
- sprout_cli-0.4.0/tests/test_commands.py +455 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/tests/test_integration.py +9 -1
- sprout_cli-0.4.0/tests/test_multi_env.py +190 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/tests/test_utils.py +84 -0
- sprout_cli-0.2.0/README.md +0 -154
- sprout_cli-0.2.0/src/sprout/commands/create.py +0 -89
- sprout_cli-0.2.0/src/sprout/commands/ls.py +0 -93
- sprout_cli-0.2.0/src/sprout/commands/path.py +0 -28
- sprout_cli-0.2.0/tests/test_commands.py +0 -196
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/.gitignore +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/LICENSE +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/__main__.py +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/commands/__init__.py +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/exceptions.py +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.4.0}/src/sprout/types.py +0 -0
- {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.
|
|
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
|
-
|
|
75
|
-
```
|
|
76
|
-
|
|
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
|
-
|
|
84
|
+
2. Create and navigate to a new development environment in one command:
|
|
80
85
|
```bash
|
|
81
|
-
cd $(sprout
|
|
86
|
+
cd $(sprout create feature-branch --path)
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
]
|
|
@@ -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
|
-
|
|
67
|
+
identifier: str = typer.Argument(
|
|
63
68
|
...,
|
|
64
|
-
help="
|
|
69
|
+
help="Branch name or index number to remove",
|
|
65
70
|
),
|
|
66
71
|
) -> None:
|
|
67
72
|
"""Remove a development environment."""
|
|
68
|
-
remove_worktree(
|
|
73
|
+
remove_worktree(identifier)
|
|
69
74
|
|
|
70
75
|
|
|
71
76
|
@app.command()
|
|
72
77
|
def path(
|
|
73
|
-
|
|
78
|
+
identifier: str = typer.Argument(
|
|
74
79
|
...,
|
|
75
|
-
help="
|
|
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(
|
|
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)
|