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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sprout-cli
3
- Version: 0.2.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
- 3. Navigate to your new environment:
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
- cd $(sprout path feature-branch)
87
+ docker compose up -d
82
88
  ```
83
89
 
84
- 4. Start your services:
90
+ ### Alternative: Two-Step Process
91
+
92
+ If you prefer to see the creation output first:
85
93
  ```bash
86
- docker compose up -d
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
- ### `sprout rm <branch-name>`
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
- ### `sprout path <branch-name>`
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
- 3. Navigate to your new environment:
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
- cd $(sprout path feature-branch)
59
+ docker compose up -d
54
60
  ```
55
61
 
56
- 4. Start your services:
62
+ ### Alternative: Two-Step Process
63
+
64
+ If you prefer to see the creation output first:
57
65
  ```bash
58
- docker compose up -d
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
- ### `sprout rm <branch-name>`
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
- ### `sprout path <branch-name>`
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
+ ]
@@ -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.3.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,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(branch_name: BranchName) -> None:
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
- value = console.input(f"Enter a value for '{var_name}': ")
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