sprout-cli 0.2.0__tar.gz → 0.3.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.3.0}/PKG-INFO +69 -10
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/README.md +68 -9
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/pyproject.toml +7 -1
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/src/sprout/__init__.py +1 -1
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/src/sprout/cli.py +12 -7
- sprout_cli-0.3.0/src/sprout/commands/create.py +114 -0
- sprout_cli-0.3.0/src/sprout/commands/ls.py +48 -0
- sprout_cli-0.3.0/src/sprout/commands/path.py +45 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/src/sprout/commands/rm.py +16 -3
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/src/sprout/utils.py +97 -3
- sprout_cli-0.3.0/tests/test_commands.py +431 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/tests/test_utils.py +84 -0
- 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.3.0}/.gitignore +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/LICENSE +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/src/sprout/__main__.py +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/src/sprout/commands/__init__.py +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/src/sprout/exceptions.py +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/src/sprout/types.py +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/tests/__init__.py +0 -0
- {sprout_cli-0.2.0 → sprout_cli-0.3.0}/tests/test_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sprout-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: CLI tool to automate git worktree and Docker Compose development workflows
|
|
5
5
|
Author: SecDevLab Inc.
|
|
6
6
|
License: MIT
|
|
@@ -71,35 +71,94 @@ DB_PORT={{ auto_port() }}
|
|
|
71
71
|
# DB_NAME=${DB_NAME}
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
2. Create a new development environment:
|
|
74
|
+
2. Create and navigate to a new development environment in one command:
|
|
75
75
|
```bash
|
|
76
|
-
sprout create feature-branch
|
|
76
|
+
cd $(sprout create feature-branch --path)
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
This single command:
|
|
80
|
+
- Creates a new git worktree for `feature-branch`
|
|
81
|
+
- Generates a `.env` file from your template
|
|
82
|
+
- Outputs the path to the new environment
|
|
83
|
+
- Changes to that directory when wrapped in `cd $(...)`
|
|
84
|
+
|
|
85
|
+
3. Start your services:
|
|
80
86
|
```bash
|
|
81
|
-
|
|
87
|
+
docker compose up -d
|
|
82
88
|
```
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
### Alternative: Two-Step Process
|
|
91
|
+
|
|
92
|
+
If you prefer to see the creation output first:
|
|
85
93
|
```bash
|
|
86
|
-
|
|
94
|
+
# Create the environment
|
|
95
|
+
sprout create feature-branch
|
|
96
|
+
|
|
97
|
+
# Then navigate to it
|
|
98
|
+
cd $(sprout path feature-branch)
|
|
87
99
|
```
|
|
88
100
|
|
|
89
101
|
## Commands
|
|
90
102
|
|
|
91
|
-
### `sprout create <branch-name
|
|
103
|
+
### `sprout create <branch-name> [--path]`
|
|
92
104
|
Create a new development environment with automated setup.
|
|
93
105
|
|
|
106
|
+
Options:
|
|
107
|
+
- `--path`: Output only the worktree path (useful for shell command substitution)
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
```bash
|
|
111
|
+
# Create and see progress messages
|
|
112
|
+
sprout create feature-xyz
|
|
113
|
+
|
|
114
|
+
# Create and navigate in one command
|
|
115
|
+
cd $(sprout create feature-xyz --path)
|
|
116
|
+
```
|
|
117
|
+
|
|
94
118
|
### `sprout ls`
|
|
95
119
|
List all managed development environments with their status.
|
|
96
120
|
|
|
97
|
-
|
|
121
|
+
The output includes index numbers that can be used with other commands:
|
|
122
|
+
```bash
|
|
123
|
+
sprout ls
|
|
124
|
+
# Output:
|
|
125
|
+
# ┏━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
|
|
126
|
+
# ┃ No. ┃ Branch ┃ Path ┃ Status ┃ Last Modified ┃
|
|
127
|
+
# ┡━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
|
|
128
|
+
# │ 1 │ feature-auth │ .sprout/feat... │ │ 2025-06-27 14:30 │
|
|
129
|
+
# │ 2 │ bugfix-api │ .sprout/bugf... │ │ 2025-06-27 15:45 │
|
|
130
|
+
# └──────┴─────────────────┴─────────────────┴────────┴──────────────────┘
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `sprout rm <branch-name-or-index>`
|
|
98
134
|
Remove a development environment (with confirmation prompts).
|
|
99
135
|
|
|
100
|
-
|
|
136
|
+
You can use either the branch name or the index number from `sprout ls`:
|
|
137
|
+
```bash
|
|
138
|
+
# Remove by branch name
|
|
139
|
+
sprout rm feature-auth
|
|
140
|
+
|
|
141
|
+
# Remove by index number
|
|
142
|
+
sprout rm 1
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `sprout path <branch-name-or-index>`
|
|
101
146
|
Get the filesystem path of a development environment.
|
|
102
147
|
|
|
148
|
+
You can use either the branch name or the index number from `sprout ls`:
|
|
149
|
+
```bash
|
|
150
|
+
# Get path by branch name
|
|
151
|
+
sprout path feature-auth
|
|
152
|
+
# Output: /path/to/project/.sprout/feature-auth
|
|
153
|
+
|
|
154
|
+
# Get path by index number
|
|
155
|
+
sprout path 1
|
|
156
|
+
# Output: /path/to/project/.sprout/feature-auth
|
|
157
|
+
|
|
158
|
+
# Use with cd command
|
|
159
|
+
cd $(sprout path 2)
|
|
160
|
+
```
|
|
161
|
+
|
|
103
162
|
### `sprout --version`
|
|
104
163
|
Show the version of sprout.
|
|
105
164
|
|
|
@@ -43,35 +43,94 @@ DB_PORT={{ auto_port() }}
|
|
|
43
43
|
# DB_NAME=${DB_NAME}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
2. Create a new development environment:
|
|
46
|
+
2. Create and navigate to a new development environment in one command:
|
|
47
47
|
```bash
|
|
48
|
-
sprout create feature-branch
|
|
48
|
+
cd $(sprout create feature-branch --path)
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
This single command:
|
|
52
|
+
- Creates a new git worktree for `feature-branch`
|
|
53
|
+
- Generates a `.env` file from your template
|
|
54
|
+
- Outputs the path to the new environment
|
|
55
|
+
- Changes to that directory when wrapped in `cd $(...)`
|
|
56
|
+
|
|
57
|
+
3. Start your services:
|
|
52
58
|
```bash
|
|
53
|
-
|
|
59
|
+
docker compose up -d
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
### Alternative: Two-Step Process
|
|
63
|
+
|
|
64
|
+
If you prefer to see the creation output first:
|
|
57
65
|
```bash
|
|
58
|
-
|
|
66
|
+
# Create the environment
|
|
67
|
+
sprout create feature-branch
|
|
68
|
+
|
|
69
|
+
# Then navigate to it
|
|
70
|
+
cd $(sprout path feature-branch)
|
|
59
71
|
```
|
|
60
72
|
|
|
61
73
|
## Commands
|
|
62
74
|
|
|
63
|
-
### `sprout create <branch-name
|
|
75
|
+
### `sprout create <branch-name> [--path]`
|
|
64
76
|
Create a new development environment with automated setup.
|
|
65
77
|
|
|
78
|
+
Options:
|
|
79
|
+
- `--path`: Output only the worktree path (useful for shell command substitution)
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
```bash
|
|
83
|
+
# Create and see progress messages
|
|
84
|
+
sprout create feature-xyz
|
|
85
|
+
|
|
86
|
+
# Create and navigate in one command
|
|
87
|
+
cd $(sprout create feature-xyz --path)
|
|
88
|
+
```
|
|
89
|
+
|
|
66
90
|
### `sprout ls`
|
|
67
91
|
List all managed development environments with their status.
|
|
68
92
|
|
|
69
|
-
|
|
93
|
+
The output includes index numbers that can be used with other commands:
|
|
94
|
+
```bash
|
|
95
|
+
sprout ls
|
|
96
|
+
# Output:
|
|
97
|
+
# ┏━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
|
|
98
|
+
# ┃ No. ┃ Branch ┃ Path ┃ Status ┃ Last Modified ┃
|
|
99
|
+
# ┡━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
|
|
100
|
+
# │ 1 │ feature-auth │ .sprout/feat... │ │ 2025-06-27 14:30 │
|
|
101
|
+
# │ 2 │ bugfix-api │ .sprout/bugf... │ │ 2025-06-27 15:45 │
|
|
102
|
+
# └──────┴─────────────────┴─────────────────┴────────┴──────────────────┘
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `sprout rm <branch-name-or-index>`
|
|
70
106
|
Remove a development environment (with confirmation prompts).
|
|
71
107
|
|
|
72
|
-
|
|
108
|
+
You can use either the branch name or the index number from `sprout ls`:
|
|
109
|
+
```bash
|
|
110
|
+
# Remove by branch name
|
|
111
|
+
sprout rm feature-auth
|
|
112
|
+
|
|
113
|
+
# Remove by index number
|
|
114
|
+
sprout rm 1
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `sprout path <branch-name-or-index>`
|
|
73
118
|
Get the filesystem path of a development environment.
|
|
74
119
|
|
|
120
|
+
You can use either the branch name or the index number from `sprout ls`:
|
|
121
|
+
```bash
|
|
122
|
+
# Get path by branch name
|
|
123
|
+
sprout path feature-auth
|
|
124
|
+
# Output: /path/to/project/.sprout/feature-auth
|
|
125
|
+
|
|
126
|
+
# Get path by index number
|
|
127
|
+
sprout path 1
|
|
128
|
+
# Output: /path/to/project/.sprout/feature-auth
|
|
129
|
+
|
|
130
|
+
# Use with cd command
|
|
131
|
+
cd $(sprout path 2)
|
|
132
|
+
```
|
|
133
|
+
|
|
75
134
|
### `sprout --version`
|
|
76
135
|
Show the version of sprout.
|
|
77
136
|
|
|
@@ -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,114 @@
|
|
|
1
|
+
"""Implementation of the create command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Never
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from sprout.exceptions import SproutError
|
|
10
|
+
from sprout.types import BranchName
|
|
11
|
+
from sprout.utils import (
|
|
12
|
+
branch_exists,
|
|
13
|
+
ensure_sprout_dir,
|
|
14
|
+
get_git_root,
|
|
15
|
+
is_git_repository,
|
|
16
|
+
parse_env_template,
|
|
17
|
+
run_command,
|
|
18
|
+
worktree_exists,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
25
|
+
"""Create a new worktree with development environment."""
|
|
26
|
+
# Check prerequisites
|
|
27
|
+
if not is_git_repository():
|
|
28
|
+
if not path_only:
|
|
29
|
+
console.print("[red]Error: Not in a git repository[/red]")
|
|
30
|
+
console.print("Please run this command from the root of a git repository.")
|
|
31
|
+
else:
|
|
32
|
+
typer.echo("Error: Not in a git repository", err=True)
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
|
|
35
|
+
git_root = get_git_root()
|
|
36
|
+
env_example = git_root / ".env.example"
|
|
37
|
+
|
|
38
|
+
if not env_example.exists():
|
|
39
|
+
if not path_only:
|
|
40
|
+
console.print("[red]Error: .env.example file not found[/red]")
|
|
41
|
+
console.print(f"Expected at: {env_example}")
|
|
42
|
+
else:
|
|
43
|
+
typer.echo(f"Error: .env.example file not found at {env_example}", err=True)
|
|
44
|
+
raise typer.Exit(1)
|
|
45
|
+
|
|
46
|
+
# Check if worktree already exists
|
|
47
|
+
if worktree_exists(branch_name):
|
|
48
|
+
if not path_only:
|
|
49
|
+
console.print(f"[red]Error: Worktree for branch '{branch_name}' already exists[/red]")
|
|
50
|
+
else:
|
|
51
|
+
typer.echo(f"Error: Worktree for branch '{branch_name}' already exists", err=True)
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
# Ensure .sprout directory exists
|
|
55
|
+
sprout_dir = ensure_sprout_dir()
|
|
56
|
+
worktree_path = sprout_dir / branch_name
|
|
57
|
+
|
|
58
|
+
# Create the worktree
|
|
59
|
+
if not path_only:
|
|
60
|
+
console.print(f"Creating worktree for branch [cyan]{branch_name}[/cyan]...")
|
|
61
|
+
|
|
62
|
+
# Check if branch exists, create if it doesn't
|
|
63
|
+
if not branch_exists(branch_name):
|
|
64
|
+
if not path_only:
|
|
65
|
+
console.print(f"Branch '{branch_name}' doesn't exist. Creating new branch...")
|
|
66
|
+
# Create branch with -b flag
|
|
67
|
+
cmd = ["git", "worktree", "add", "-b", branch_name, str(worktree_path)]
|
|
68
|
+
else:
|
|
69
|
+
cmd = ["git", "worktree", "add", str(worktree_path), branch_name]
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
run_command(cmd)
|
|
73
|
+
except SproutError as e:
|
|
74
|
+
if not path_only:
|
|
75
|
+
console.print(f"[red]Error creating worktree: {e}[/red]")
|
|
76
|
+
else:
|
|
77
|
+
typer.echo(f"Error creating worktree: {e}", err=True)
|
|
78
|
+
raise typer.Exit(1) from e
|
|
79
|
+
|
|
80
|
+
# Generate .env file
|
|
81
|
+
if not path_only:
|
|
82
|
+
console.print("Generating .env file...")
|
|
83
|
+
try:
|
|
84
|
+
env_content = parse_env_template(env_example, silent=path_only)
|
|
85
|
+
env_file = worktree_path / ".env"
|
|
86
|
+
env_file.write_text(env_content)
|
|
87
|
+
except SproutError as e:
|
|
88
|
+
if not path_only:
|
|
89
|
+
console.print(f"[red]Error generating .env file: {e}[/red]")
|
|
90
|
+
else:
|
|
91
|
+
typer.echo(f"Error generating .env file: {e}", err=True)
|
|
92
|
+
# Clean up worktree on failure
|
|
93
|
+
run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
|
|
94
|
+
raise typer.Exit(1) from e
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
if not path_only:
|
|
97
|
+
console.print("\n[yellow]Cancelled by user[/yellow]")
|
|
98
|
+
else:
|
|
99
|
+
typer.echo("Cancelled by user", err=True)
|
|
100
|
+
# Clean up worktree on cancellation
|
|
101
|
+
run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
|
|
102
|
+
raise typer.Exit(130) from None
|
|
103
|
+
|
|
104
|
+
# Success message or path output
|
|
105
|
+
if path_only:
|
|
106
|
+
# Output only the path for shell command substitution
|
|
107
|
+
print(str(worktree_path))
|
|
108
|
+
else:
|
|
109
|
+
console.print(f"\n[green]✅ Workspace '{branch_name}' created successfully![/green]\n")
|
|
110
|
+
console.print("Navigate to your new environment with:")
|
|
111
|
+
console.print(f" [cyan]cd {worktree_path.relative_to(Path.cwd())}[/cyan]")
|
|
112
|
+
|
|
113
|
+
# Exit successfully
|
|
114
|
+
raise typer.Exit(0)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Implementation of the ls command."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from sprout.utils import get_indexed_worktrees, is_git_repository
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def list_worktrees() -> None:
|
|
13
|
+
"""List all managed development environments."""
|
|
14
|
+
if not is_git_repository():
|
|
15
|
+
console.print("[red]Error: Not in a git repository[/red]")
|
|
16
|
+
raise typer.Exit(1)
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
sprout_worktrees = get_indexed_worktrees()
|
|
20
|
+
except Exception as e:
|
|
21
|
+
console.print(f"[red]Error listing worktrees: {e}[/red]")
|
|
22
|
+
raise typer.Exit(1) from e
|
|
23
|
+
|
|
24
|
+
if not sprout_worktrees:
|
|
25
|
+
console.print("[yellow]No sprout-managed worktrees found.[/yellow]")
|
|
26
|
+
console.print("Use 'sprout create <branch-name>' to create one.")
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
# Create table with index column
|
|
30
|
+
table = Table(title="Sprout Worktrees", show_lines=True)
|
|
31
|
+
table.add_column("No.", style="bright_white", no_wrap=True, width=4)
|
|
32
|
+
table.add_column("Branch", style="cyan", no_wrap=True)
|
|
33
|
+
table.add_column("Path", style="blue")
|
|
34
|
+
table.add_column("Status", style="green")
|
|
35
|
+
table.add_column("Last Modified", style="yellow")
|
|
36
|
+
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
for idx, wt in enumerate(sprout_worktrees, 1):
|
|
40
|
+
branch = wt.get("branch", wt.get("head", "detached"))
|
|
41
|
+
path = str(wt["path"].relative_to(Path.cwd()))
|
|
42
|
+
status = "[green]● current[/green]" if wt.get("is_current", False) else ""
|
|
43
|
+
modified_dt = wt.get("modified")
|
|
44
|
+
modified = modified_dt.strftime("%Y-%m-%d %H:%M") if modified_dt else "N/A"
|
|
45
|
+
|
|
46
|
+
table.add_row(str(idx), branch, path, status, modified)
|
|
47
|
+
|
|
48
|
+
console.print(table)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Implementation of the path command."""
|
|
2
|
+
|
|
3
|
+
from typing import Never, TextIO
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from sprout.utils import (
|
|
8
|
+
get_sprout_dir,
|
|
9
|
+
is_git_repository,
|
|
10
|
+
resolve_branch_identifier,
|
|
11
|
+
worktree_exists,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
console: TextIO = typer.get_text_stream("stdout")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_worktree_path(identifier: str) -> Never:
|
|
18
|
+
"""Get the path of a development environment by branch name or index."""
|
|
19
|
+
if not is_git_repository():
|
|
20
|
+
typer.echo("Error: Not in a git repository", err=True)
|
|
21
|
+
raise typer.Exit(1)
|
|
22
|
+
|
|
23
|
+
# Resolve identifier to branch name
|
|
24
|
+
branch_name = resolve_branch_identifier(identifier)
|
|
25
|
+
|
|
26
|
+
if branch_name is None:
|
|
27
|
+
if identifier.isdigit():
|
|
28
|
+
typer.echo(
|
|
29
|
+
f"Error: Invalid index '{identifier}'. Use 'sprout ls' to see valid indices.",
|
|
30
|
+
err=True,
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
typer.echo(f"Error: Worktree for branch '{identifier}' does not exist", err=True)
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
|
|
36
|
+
# Check if worktree exists
|
|
37
|
+
if not worktree_exists(branch_name):
|
|
38
|
+
typer.echo(f"Error: Worktree for branch '{branch_name}' does not exist", err=True)
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
worktree_path = get_sprout_dir() / branch_name
|
|
42
|
+
|
|
43
|
+
# Output only the path, no extra formatting
|
|
44
|
+
print(str(worktree_path))
|
|
45
|
+
raise typer.Exit(0)
|
|
@@ -4,10 +4,10 @@ import typer
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
|
|
6
6
|
from sprout.exceptions import SproutError
|
|
7
|
-
from sprout.types import BranchName
|
|
8
7
|
from sprout.utils import (
|
|
9
8
|
get_sprout_dir,
|
|
10
9
|
is_git_repository,
|
|
10
|
+
resolve_branch_identifier,
|
|
11
11
|
run_command,
|
|
12
12
|
worktree_exists,
|
|
13
13
|
)
|
|
@@ -15,12 +15,25 @@ from sprout.utils import (
|
|
|
15
15
|
console = Console()
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def remove_worktree(
|
|
19
|
-
"""Remove a development environment."""
|
|
18
|
+
def remove_worktree(identifier: str) -> None:
|
|
19
|
+
"""Remove a development environment by branch name or index."""
|
|
20
20
|
if not is_git_repository():
|
|
21
21
|
console.print("[red]Error: Not in a git repository[/red]")
|
|
22
22
|
raise typer.Exit(1)
|
|
23
23
|
|
|
24
|
+
# Resolve identifier to branch name
|
|
25
|
+
branch_name = resolve_branch_identifier(identifier)
|
|
26
|
+
|
|
27
|
+
if branch_name is None:
|
|
28
|
+
if identifier.isdigit():
|
|
29
|
+
console.print(
|
|
30
|
+
f"[red]Error: Invalid index '{identifier}'. "
|
|
31
|
+
"Use 'sprout ls' to see valid indices.[/red]"
|
|
32
|
+
)
|
|
33
|
+
else:
|
|
34
|
+
console.print(f"[red]Error: Worktree for branch '{identifier}' does not exist[/red]")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
|
|
24
37
|
# Check if worktree exists
|
|
25
38
|
if not worktree_exists(branch_name):
|
|
26
39
|
console.print(f"[red]Error: Worktree for branch '{branch_name}' does not exist[/red]")
|
|
@@ -5,13 +5,15 @@ import random
|
|
|
5
5
|
import re
|
|
6
6
|
import socket
|
|
7
7
|
import subprocess
|
|
8
|
+
from datetime import datetime
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import TypeAlias
|
|
10
11
|
|
|
12
|
+
import typer
|
|
11
13
|
from rich.console import Console
|
|
12
14
|
|
|
13
15
|
from sprout.exceptions import SproutError
|
|
14
|
-
from sprout.types import BranchName
|
|
16
|
+
from sprout.types import BranchName, WorktreeInfo
|
|
15
17
|
|
|
16
18
|
# Type aliases
|
|
17
19
|
PortNumber: TypeAlias = int
|
|
@@ -123,7 +125,7 @@ def find_available_port() -> PortNumber:
|
|
|
123
125
|
raise SproutError("Could not find an available port after 1000 attempts")
|
|
124
126
|
|
|
125
127
|
|
|
126
|
-
def parse_env_template(template_path: Path) -> str:
|
|
128
|
+
def parse_env_template(template_path: Path, silent: bool = False) -> str:
|
|
127
129
|
"""Parse .env.example template and process placeholders."""
|
|
128
130
|
if not template_path.exists():
|
|
129
131
|
raise SproutError(f".env.example file not found at {template_path}")
|
|
@@ -155,7 +157,12 @@ def parse_env_template(template_path: Path) -> str:
|
|
|
155
157
|
value = os.environ.get(var_name)
|
|
156
158
|
if value is None:
|
|
157
159
|
# Prompt user for value
|
|
158
|
-
|
|
160
|
+
if silent:
|
|
161
|
+
# Use stderr for prompts in silent mode to keep stdout clean
|
|
162
|
+
typer.echo(f"Enter a value for '{var_name}': ", err=True, nl=False)
|
|
163
|
+
value = input()
|
|
164
|
+
else:
|
|
165
|
+
value = console.input(f"Enter a value for '{var_name}': ")
|
|
159
166
|
return value
|
|
160
167
|
|
|
161
168
|
line = re.sub(r"{{\s*([^}]+)\s*}}", replace_variable, line)
|
|
@@ -175,3 +182,90 @@ def branch_exists(branch_name: BranchName) -> bool:
|
|
|
175
182
|
"""Check if a git branch exists."""
|
|
176
183
|
result = run_command(["git", "rev-parse", "--verify", f"refs/heads/{branch_name}"], check=False)
|
|
177
184
|
return result.returncode == 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_indexed_worktrees() -> list[WorktreeInfo]:
|
|
188
|
+
"""Get a list of sprout-managed worktrees with consistent ordering.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
List of WorktreeInfo dicts, sorted by branch name for consistent indexing.
|
|
192
|
+
"""
|
|
193
|
+
if not is_git_repository():
|
|
194
|
+
raise SproutError("Not in a git repository")
|
|
195
|
+
|
|
196
|
+
sprout_dir = get_sprout_dir()
|
|
197
|
+
|
|
198
|
+
# Get worktree list from git
|
|
199
|
+
result = run_command(["git", "worktree", "list", "--porcelain"])
|
|
200
|
+
|
|
201
|
+
# Parse worktree output
|
|
202
|
+
worktrees: list[WorktreeInfo] = []
|
|
203
|
+
current_worktree: WorktreeInfo = {}
|
|
204
|
+
|
|
205
|
+
for line in result.stdout.strip().split("\n"):
|
|
206
|
+
if not line:
|
|
207
|
+
if current_worktree:
|
|
208
|
+
worktrees.append(current_worktree)
|
|
209
|
+
current_worktree = {}
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if line.startswith("worktree "):
|
|
213
|
+
current_worktree["path"] = Path(line[9:])
|
|
214
|
+
elif line.startswith("branch "):
|
|
215
|
+
branch_ref = line[7:]
|
|
216
|
+
# Strip refs/heads/ prefix if present
|
|
217
|
+
if branch_ref.startswith("refs/heads/"):
|
|
218
|
+
current_worktree["branch"] = branch_ref[11:]
|
|
219
|
+
else:
|
|
220
|
+
current_worktree["branch"] = branch_ref
|
|
221
|
+
elif line.startswith("HEAD "):
|
|
222
|
+
current_worktree["head"] = line[5:]
|
|
223
|
+
|
|
224
|
+
if current_worktree:
|
|
225
|
+
worktrees.append(current_worktree)
|
|
226
|
+
|
|
227
|
+
# Filter for sprout-managed worktrees
|
|
228
|
+
sprout_worktrees: list[WorktreeInfo] = []
|
|
229
|
+
current_path = Path.cwd().resolve()
|
|
230
|
+
|
|
231
|
+
for wt in worktrees:
|
|
232
|
+
wt_path = wt["path"].resolve()
|
|
233
|
+
if wt_path.parent == sprout_dir:
|
|
234
|
+
# Check if we're currently in this worktree
|
|
235
|
+
wt["is_current"] = current_path == wt_path or current_path.is_relative_to(wt_path)
|
|
236
|
+
|
|
237
|
+
# Get last modified time
|
|
238
|
+
if wt_path.exists():
|
|
239
|
+
stat = wt_path.stat()
|
|
240
|
+
wt["modified"] = datetime.fromtimestamp(stat.st_mtime)
|
|
241
|
+
else:
|
|
242
|
+
wt["modified"] = None
|
|
243
|
+
|
|
244
|
+
sprout_worktrees.append(wt)
|
|
245
|
+
|
|
246
|
+
# Sort by branch name for consistent indexing
|
|
247
|
+
sprout_worktrees.sort(key=lambda wt: wt.get("branch") or wt.get("head") or "")
|
|
248
|
+
|
|
249
|
+
return sprout_worktrees
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def resolve_branch_identifier(identifier: str) -> BranchName | None:
|
|
253
|
+
"""Resolve a branch identifier (name or index) to a branch name.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
identifier: Either a branch name or a 1-based index number
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Branch name if found, None otherwise
|
|
260
|
+
"""
|
|
261
|
+
# Check if identifier is a number
|
|
262
|
+
if identifier.isdigit():
|
|
263
|
+
index = int(identifier)
|
|
264
|
+
worktrees = get_indexed_worktrees()
|
|
265
|
+
|
|
266
|
+
if 1 <= index <= len(worktrees):
|
|
267
|
+
return worktrees[index - 1].get("branch", worktrees[index - 1].get("head", ""))
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
# Otherwise treat as branch name
|
|
271
|
+
return identifier
|