pf 0.0.1 → 0.0.2

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.
package/go/Makefile ADDED
@@ -0,0 +1,37 @@
1
+ BINARY_NAME=pf
2
+ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
3
+ GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
4
+ BUILD_DATE ?= $(shell date -u '+%Y-%m-%d_%H:%M:%S')
5
+ LDFLAGS=-ldflags "-X github.com/pullfrog/cli/cmd.Version=$(VERSION) -X github.com/pullfrog/cli/cmd.GitCommit=$(GIT_COMMIT) -X github.com/pullfrog/cli/cmd.BuildDate=$(BUILD_DATE)"
6
+
7
+ .PHONY: build install clean test build-all
8
+
9
+ # Build for current platform
10
+ build:
11
+ go build $(LDFLAGS) -o bin/$(BINARY_NAME) .
12
+
13
+ # Install to GOPATH/bin (should be in PATH)
14
+ install:
15
+ go install $(LDFLAGS) .
16
+
17
+ # Run tests
18
+ test:
19
+ go test -v ./...
20
+
21
+ # Clean build artifacts
22
+ clean:
23
+ rm -rf bin/
24
+ go clean
25
+
26
+ # Build for all platforms
27
+ build-all: clean
28
+ mkdir -p bin
29
+ GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-amd64 .
30
+ GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-arm64 .
31
+ GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-amd64 .
32
+ GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-arm64 .
33
+ GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-windows-amd64.exe .
34
+
35
+ # Development build and run
36
+ run: build
37
+ ./bin/$(BINARY_NAME)
@@ -0,0 +1,183 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bufio"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ var completionCmd = &cobra.Command{
14
+ Use: "completion [bash|zsh|fish]",
15
+ Short: "Install shell completions",
16
+ Long: `Install shell completions for pf.
17
+
18
+ Run without arguments to interactively install completions.
19
+ Run with a shell name to output the raw completion script.
20
+ `,
21
+ DisableFlagsInUseLine: true,
22
+ ValidArgs: []string{"bash", "zsh", "fish"},
23
+ Args: cobra.MaximumNArgs(1),
24
+ RunE: runCompletion,
25
+ }
26
+
27
+ func init() {
28
+ rootCmd.AddCommand(completionCmd)
29
+ }
30
+
31
+ func runCompletion(cmd *cobra.Command, args []string) error {
32
+ // If shell specified, just output the script
33
+ if len(args) == 1 {
34
+ switch args[0] {
35
+ case "bash":
36
+ return rootCmd.GenBashCompletion(os.Stdout)
37
+ case "zsh":
38
+ return rootCmd.GenZshCompletion(os.Stdout)
39
+ case "fish":
40
+ return rootCmd.GenFishCompletion(os.Stdout, true)
41
+ }
42
+ return nil
43
+ }
44
+
45
+ // Interactive mode
46
+ shell := detectShell()
47
+ if shell == "" {
48
+ return fmt.Errorf("could not detect shell, please run: pf completion [bash|zsh|fish]")
49
+ }
50
+
51
+ completionPath := getCompletionPath(shell)
52
+ rcPath, rcLines := getRcConfig(shell)
53
+
54
+ // Build description of changes
55
+ var desc strings.Builder
56
+ desc.WriteString(fmt.Sprintf("1. Write completion script to %s\n", completionPath))
57
+ if rcPath != "" && !fileContains(rcPath, rcLines[0]) {
58
+ desc.WriteString(fmt.Sprintf("2. Add to %s:\n", rcPath))
59
+ for _, line := range rcLines {
60
+ desc.WriteString(fmt.Sprintf(" %s\n", line))
61
+ }
62
+ }
63
+
64
+ fmt.Printf("Install %s completions? This will:\n", shell)
65
+ fmt.Print(desc.String())
66
+ fmt.Print("\nProceed? [y/N] ")
67
+
68
+ reader := bufio.NewReader(os.Stdin)
69
+ response, _ := reader.ReadString('\n')
70
+ response = strings.TrimSpace(strings.ToLower(response))
71
+
72
+ if response != "y" && response != "yes" {
73
+ fmt.Println("Canceled.")
74
+ return nil
75
+ }
76
+
77
+ return installCompletion(shell, completionPath, rcPath, rcLines)
78
+ }
79
+
80
+ func detectShell() string {
81
+ shell := os.Getenv("SHELL")
82
+ if strings.Contains(shell, "zsh") {
83
+ return "zsh"
84
+ } else if strings.Contains(shell, "bash") {
85
+ return "bash"
86
+ } else if strings.Contains(shell, "fish") {
87
+ return "fish"
88
+ }
89
+ return ""
90
+ }
91
+
92
+ func getCompletionPath(shell string) string {
93
+ home, _ := os.UserHomeDir()
94
+
95
+ switch shell {
96
+ case "zsh":
97
+ return filepath.Join(home, ".zsh/completions/_pf")
98
+ case "bash":
99
+ return filepath.Join(home, ".bash_completion.d/pf")
100
+ case "fish":
101
+ return filepath.Join(home, ".config/fish/completions/pf.fish")
102
+ }
103
+ return ""
104
+ }
105
+
106
+ func getRcConfig(shell string) (string, []string) {
107
+ home, _ := os.UserHomeDir()
108
+
109
+ switch shell {
110
+ case "zsh":
111
+ return filepath.Join(home, ".zshrc"), []string{
112
+ `fpath=(~/.zsh/completions $fpath)`,
113
+ `autoload -Uz compinit && compinit`,
114
+ }
115
+ case "bash":
116
+ return filepath.Join(home, ".bashrc"), []string{
117
+ `source ~/.bash_completion.d/pf`,
118
+ }
119
+ case "fish":
120
+ // Fish auto-loads from completions dir
121
+ return "", nil
122
+ }
123
+ return "", nil
124
+ }
125
+
126
+ func installCompletion(shell, completionPath, rcPath string, rcLines []string) error {
127
+ // Ensure completion directory exists
128
+ dir := filepath.Dir(completionPath)
129
+ if err := os.MkdirAll(dir, 0755); err != nil {
130
+ return fmt.Errorf("failed to create directory: %w", err)
131
+ }
132
+
133
+ // Write completion script
134
+ file, err := os.Create(completionPath)
135
+ if err != nil {
136
+ return fmt.Errorf("failed to create file: %w", err)
137
+ }
138
+ defer file.Close()
139
+
140
+ switch shell {
141
+ case "bash":
142
+ rootCmd.GenBashCompletion(file)
143
+ case "zsh":
144
+ rootCmd.GenZshCompletion(file)
145
+ case "fish":
146
+ rootCmd.GenFishCompletion(file, true)
147
+ }
148
+
149
+ fmt.Println(success.Render("✓"), "Wrote", completionPath)
150
+
151
+ // Update rc file if needed
152
+ if rcPath != "" && len(rcLines) > 0 && !fileContains(rcPath, rcLines[0]) {
153
+ f, err := os.OpenFile(rcPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
154
+ if err != nil {
155
+ return fmt.Errorf("failed to open %s: %w", rcPath, err)
156
+ }
157
+ defer f.Close()
158
+
159
+ f.WriteString("\n# pf completions\n")
160
+ for _, line := range rcLines {
161
+ f.WriteString(line + "\n")
162
+ }
163
+
164
+ fmt.Println(success.Render("✓"), "Updated", rcPath)
165
+ }
166
+
167
+ fmt.Println()
168
+ if rcPath != "" {
169
+ fmt.Println("Restart your shell or run:", dim.Render("source "+rcPath))
170
+ } else {
171
+ fmt.Println("Restart your shell to enable completions.")
172
+ }
173
+
174
+ return nil
175
+ }
176
+
177
+ func fileContains(path, substr string) bool {
178
+ data, err := os.ReadFile(path)
179
+ if err != nil {
180
+ return false
181
+ }
182
+ return strings.Contains(string(data), substr)
183
+ }
package/go/cmd/list.go ADDED
@@ -0,0 +1,126 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "os/exec"
7
+ "path/filepath"
8
+ "strings"
9
+
10
+ "github.com/charmbracelet/lipgloss"
11
+ "github.com/charmbracelet/lipgloss/table"
12
+ "github.com/pullfrog/cli/internal/store"
13
+ "github.com/spf13/cobra"
14
+ )
15
+
16
+ var listCmd = &cobra.Command{
17
+ Use: "ls",
18
+ Short: "List all tracked worktrees",
19
+ RunE: runList,
20
+ }
21
+
22
+ var plainOutput bool
23
+
24
+ var listAliasCmd = &cobra.Command{
25
+ Use: "list",
26
+ Hidden: true,
27
+ RunE: runList,
28
+ }
29
+
30
+ func init() {
31
+ listCmd.Flags().BoolVarP(&plainOutput, "plain", "p", false, "Plain output (grep-friendly)")
32
+ rootCmd.AddCommand(listCmd)
33
+ rootCmd.AddCommand(listAliasCmd)
34
+ }
35
+
36
+ func runList(cmd *cobra.Command, args []string) error {
37
+ s, err := store.Load()
38
+ if err != nil {
39
+ return fmt.Errorf("failed to load store: %w", err)
40
+ }
41
+
42
+ if len(s.Worktrees) == 0 {
43
+ fmt.Println("No worktrees. Use 'pf new <name>' to create one.")
44
+ return nil
45
+ }
46
+
47
+ // Detect current worktree
48
+ cwd, _ := os.Getwd()
49
+ currentWorktree := ""
50
+ for _, wt := range s.Worktrees {
51
+ absPath, _ := filepath.Abs(wt.Path)
52
+ if strings.HasPrefix(cwd, absPath) {
53
+ currentWorktree = wt.Name
54
+ break
55
+ }
56
+ }
57
+
58
+ if plainOutput {
59
+ for _, wt := range s.Worktrees {
60
+ branch := getWorktreeBranch(wt.Path)
61
+ status := getWorktreeStatus(wt.Path)
62
+ fmt.Printf("%s\t%s\t%s\n", wt.Name, branch, status)
63
+ }
64
+ return nil
65
+ }
66
+
67
+ rows := [][]string{}
68
+ for _, wt := range s.Worktrees {
69
+ branch := getWorktreeBranch(wt.Path)
70
+ status := getWorktreeStatus(wt.Path)
71
+ name := wt.Name
72
+ if wt.Name == currentWorktree {
73
+ name = "*" + name
74
+ }
75
+ rows = append(rows, []string{name, branch, status})
76
+ }
77
+
78
+ t := table.New().
79
+ Border(lipgloss.NormalBorder()).
80
+ BorderHeader(true).
81
+ Headers("name", "branch", "git status").
82
+ Rows(rows...)
83
+
84
+ fmt.Println(t)
85
+
86
+ return nil
87
+ }
88
+
89
+ func getWorktreeBranch(path string) string {
90
+ if _, err := os.Stat(path); os.IsNotExist(err) {
91
+ return "[missing]"
92
+ }
93
+
94
+ cmd := exec.Command("git", "-C", path, "branch", "--show-current")
95
+ output, err := cmd.Output()
96
+ if err != nil {
97
+ return "[detached]"
98
+ }
99
+
100
+ branch := strings.TrimSpace(string(output))
101
+ if branch == "" {
102
+ return "[detached]"
103
+ }
104
+ return branch
105
+ }
106
+
107
+ func getWorktreeStatus(path string) string {
108
+ if _, err := os.Stat(path); os.IsNotExist(err) {
109
+ return ""
110
+ }
111
+
112
+ cmd := exec.Command("git", "-C", path, "status", "--porcelain")
113
+ output, err := cmd.Output()
114
+ if err != nil {
115
+ return "[error]"
116
+ }
117
+
118
+ status := strings.TrimSpace(string(output))
119
+ if status == "" {
120
+ return "clean"
121
+ }
122
+
123
+ lines := strings.Split(status, "\n")
124
+ return fmt.Sprintf("%d changed", len(lines))
125
+ }
126
+
package/go/cmd/new.go ADDED
@@ -0,0 +1,146 @@
1
+ package cmd
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+ "fmt"
7
+ "os"
8
+ "os/exec"
9
+ "path/filepath"
10
+ "strings"
11
+
12
+ "github.com/pullfrog/cli/internal/store"
13
+ "github.com/spf13/cobra"
14
+ )
15
+
16
+ var newCmd = &cobra.Command{
17
+ Use: "new [name]",
18
+ Short: "Create a new worktree with a branch",
19
+ Long: `Creates a new git worktree and branch with the given name. If no name is provided, generates a random name like pf-a1b2c3d4.`,
20
+ Args: cobra.MaximumNArgs(1),
21
+ RunE: runNew,
22
+ }
23
+
24
+ func init() {
25
+ rootCmd.AddCommand(newCmd)
26
+ }
27
+
28
+ func runNew(cmd *cobra.Command, args []string) error {
29
+ var name string
30
+ if len(args) > 0 {
31
+ name = args[0]
32
+ } else {
33
+ name = generateName()
34
+ }
35
+
36
+ // Get the git root directory
37
+ gitRoot, err := store.GetGitRoot()
38
+ if err != nil {
39
+ return fmt.Errorf("not in a git repository: %w", err)
40
+ }
41
+
42
+ // Don't allow creating worktrees from within a worktree
43
+ gitDir, err := store.GetGitCommonDir()
44
+ if err != nil {
45
+ return fmt.Errorf("failed to get git directory: %w", err)
46
+ }
47
+ if gitRoot != filepath.Dir(gitDir) {
48
+ fmt.Fprintln(os.Stderr, "Error: You are inside a pf worktree.")
49
+ fmt.Fprintln(os.Stderr, "")
50
+ fmt.Fprintln(os.Stderr, "Each time you run 'pf new' or 'pf open', pf starts a subshell in the")
51
+ fmt.Fprintln(os.Stderr, "worktree directory. To keep subshells shallow and avoid nesting, pf")
52
+ fmt.Fprintln(os.Stderr, "requires you to return to the main repo before opening another worktree.")
53
+ fmt.Fprintln(os.Stderr, "")
54
+ fmt.Fprintln(os.Stderr, "To exit the current worktree:")
55
+ fmt.Fprintln(os.Stderr, " exit")
56
+ fmt.Fprintln(os.Stderr, "")
57
+ fmt.Fprintln(os.Stderr, "Then re-run:")
58
+ fmt.Fprintf(os.Stderr, " pf new %s\n", name)
59
+ os.Exit(1)
60
+ }
61
+
62
+ // Check if branch already exists - error if so
63
+ if checkBranchExists(name) {
64
+ return fmt.Errorf("branch '%s' already exists", name)
65
+ }
66
+
67
+ // Check if worktree already exists in store
68
+ s, err := store.Load()
69
+ if err != nil {
70
+ return fmt.Errorf("failed to load store: %w", err)
71
+ }
72
+ if s.FindByName(name) != nil {
73
+ return fmt.Errorf("worktree '%s' already exists - use 'pf open %s'", name, name)
74
+ }
75
+
76
+ // Get current branch to show what we're branching from
77
+ baseBranch := getCurrentBranch()
78
+
79
+ // Determine worktree path inside .git/pf/worktrees
80
+ repoName := filepath.Base(gitRoot)
81
+ worktreePath := filepath.Join(gitDir, "pf", "worktrees", name, repoName)
82
+
83
+ // Ensure directory exists
84
+ if err := os.MkdirAll(filepath.Dir(worktreePath), 0755); err != nil {
85
+ return fmt.Errorf("failed to create worktree directory: %w", err)
86
+ }
87
+
88
+ // Create new branch with worktree (suppress git noise)
89
+ gitCmd := exec.Command("git", "worktree", "add", "-b", name, worktreePath)
90
+ if err := gitCmd.Run(); err != nil {
91
+ return fmt.Errorf("failed to create worktree: %w", err)
92
+ }
93
+
94
+ // Track in store
95
+ s.Add(name, worktreePath)
96
+ if err := s.Save(); err != nil {
97
+ return fmt.Errorf("failed to save store: %w", err)
98
+ }
99
+
100
+ fmt.Println()
101
+ fmt.Println(success.Render("✓"), bold.Render(name), dim.Render("(from "+baseBranch+")"))
102
+ fmt.Println(dim.Render(" Subshell started. Type 'exit' to return."))
103
+ fmt.Println()
104
+
105
+ // Get user's shell
106
+ shell := os.Getenv("SHELL")
107
+ if shell == "" {
108
+ shell = "/bin/sh"
109
+ }
110
+
111
+ // Spawn new shell in worktree directory
112
+ shellCmd := exec.Command(shell)
113
+ shellCmd.Dir = worktreePath
114
+ shellCmd.Stdin = os.Stdin
115
+ shellCmd.Stdout = os.Stdout
116
+ shellCmd.Stderr = os.Stderr
117
+
118
+ if err := shellCmd.Run(); err != nil {
119
+ return fmt.Errorf("shell exited with error: %w", err)
120
+ }
121
+
122
+ fmt.Println()
123
+ fmt.Println(dim.Render(" Back in main worktree"))
124
+ fmt.Println()
125
+ return nil
126
+ }
127
+
128
+ func checkBranchExists(branchName string) bool {
129
+ cmd := exec.Command("git", "show-ref", "--verify", "--quiet", "refs/heads/"+branchName)
130
+ return cmd.Run() == nil
131
+ }
132
+
133
+ func getCurrentBranch() string {
134
+ cmd := exec.Command("git", "branch", "--show-current")
135
+ output, err := cmd.Output()
136
+ if err != nil {
137
+ return "HEAD"
138
+ }
139
+ return strings.TrimSpace(string(output))
140
+ }
141
+
142
+ func generateName() string {
143
+ bytes := make([]byte, 4)
144
+ rand.Read(bytes)
145
+ return "pf-" + hex.EncodeToString(bytes)
146
+ }
package/go/cmd/open.go ADDED
@@ -0,0 +1,128 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "os/exec"
7
+ "path/filepath"
8
+ "strings"
9
+
10
+ "github.com/pullfrog/cli/internal/store"
11
+ "github.com/spf13/cobra"
12
+ )
13
+
14
+ var openCmd = &cobra.Command{
15
+ Use: "open <name>",
16
+ Short: "Open a worktree",
17
+ Long: `Opens a worktree in a subshell. Type 'exit' to return.`,
18
+ Args: cobra.ExactArgs(1),
19
+ RunE: runOpen,
20
+ ValidArgsFunction: completeWorktreeNames,
21
+ }
22
+
23
+ func init() {
24
+ rootCmd.AddCommand(openCmd)
25
+ }
26
+
27
+ func completeWorktreeNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
28
+ if len(args) != 0 {
29
+ return nil, cobra.ShellCompDirectiveNoFileComp
30
+ }
31
+
32
+ s, err := store.Load()
33
+ if err != nil {
34
+ return nil, cobra.ShellCompDirectiveNoFileComp
35
+ }
36
+
37
+ var names []string
38
+ for _, wt := range s.Worktrees {
39
+ names = append(names, wt.Name)
40
+ }
41
+ return names, cobra.ShellCompDirectiveNoFileComp
42
+ }
43
+
44
+ func runOpen(cmd *cobra.Command, args []string) error {
45
+ name := args[0]
46
+
47
+ // Get git dirs
48
+ gitRoot, err := store.GetGitRoot()
49
+ if err != nil {
50
+ return fmt.Errorf("not in a git repository: %w", err)
51
+ }
52
+ gitDir, err := store.GetGitCommonDir()
53
+ if err != nil {
54
+ return fmt.Errorf("failed to get git directory: %w", err)
55
+ }
56
+
57
+ // Don't allow opening from within a worktree
58
+ if gitRoot != filepath.Dir(gitDir) {
59
+ fmt.Fprintln(os.Stderr, "Error: You are inside a pf worktree.")
60
+ fmt.Fprintln(os.Stderr, "")
61
+ fmt.Fprintln(os.Stderr, "Each time you run 'pf new' or 'pf open', pf starts a subshell in the")
62
+ fmt.Fprintln(os.Stderr, "worktree directory. To keep subshells shallow and avoid nesting, pf")
63
+ fmt.Fprintln(os.Stderr, "requires you to return to the main repo before opening another worktree.")
64
+ fmt.Fprintln(os.Stderr, "")
65
+ fmt.Fprintln(os.Stderr, "To exit the current worktree:")
66
+ fmt.Fprintln(os.Stderr, " exit")
67
+ fmt.Fprintln(os.Stderr, "")
68
+ fmt.Fprintln(os.Stderr, "Then re-run:")
69
+ fmt.Fprintf(os.Stderr, " pf open %s\n", name)
70
+ os.Exit(1)
71
+ }
72
+
73
+ // Check for uncommitted changes
74
+ statusCmd := exec.Command("git", "status", "--porcelain")
75
+ output, err := statusCmd.Output()
76
+ if err != nil {
77
+ return fmt.Errorf("failed to check git status: %w", err)
78
+ }
79
+
80
+ if strings.TrimSpace(string(output)) != "" {
81
+ return fmt.Errorf("uncommitted changes in current worktree - commit or stash first")
82
+ }
83
+
84
+ s, err := store.Load()
85
+ if err != nil {
86
+ return fmt.Errorf("failed to load store: %w", err)
87
+ }
88
+
89
+ wt := s.FindByName(name)
90
+ if wt == nil {
91
+ return fmt.Errorf("worktree '%s' not found", name)
92
+ }
93
+
94
+ // Check if directory exists
95
+ if _, err := os.Stat(wt.Path); os.IsNotExist(err) {
96
+ return fmt.Errorf("worktree directory does not exist: %s", wt.Path)
97
+ }
98
+
99
+ // Get user's shell
100
+ shell := os.Getenv("SHELL")
101
+ if shell == "" {
102
+ shell = "/bin/sh"
103
+ }
104
+
105
+ // Spawn new shell in worktree directory
106
+ shellCmd := exec.Command(shell)
107
+ shellCmd.Dir = wt.Path
108
+ shellCmd.Stdin = os.Stdin
109
+ shellCmd.Stdout = os.Stdout
110
+ shellCmd.Stderr = os.Stderr
111
+
112
+ // Get current branch of the worktree
113
+ branch := getWorktreeBranch(wt.Path)
114
+
115
+ fmt.Println()
116
+ fmt.Println(success.Render("✓"), bold.Render(name), dim.Render("("+branch+")"))
117
+ fmt.Println(dim.Render(" Subshell started. Type 'exit' to return."))
118
+ fmt.Println()
119
+
120
+ if err := shellCmd.Run(); err != nil {
121
+ return fmt.Errorf("shell exited with error: %w", err)
122
+ }
123
+
124
+ fmt.Println()
125
+ fmt.Println(dim.Render(" Back in main worktree"))
126
+ fmt.Println()
127
+ return nil
128
+ }
package/go/cmd/rm.go ADDED
@@ -0,0 +1,88 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "os/exec"
7
+ "path/filepath"
8
+ "strings"
9
+
10
+ "github.com/pullfrog/cli/internal/store"
11
+ "github.com/spf13/cobra"
12
+ )
13
+
14
+ var rmCmd = &cobra.Command{
15
+ Use: "rm <name>",
16
+ Short: "Remove a worktree",
17
+ Long: `Removes a worktree and optionally deletes the branch.`,
18
+ Args: cobra.ExactArgs(1),
19
+ RunE: runRm,
20
+ ValidArgsFunction: completeWorktreeNames,
21
+ }
22
+
23
+ var deleteBranch bool
24
+
25
+ func init() {
26
+ rmCmd.Flags().BoolVarP(&deleteBranch, "delete-branch", "d", false, "Also delete the branch")
27
+ rootCmd.AddCommand(rmCmd)
28
+ }
29
+
30
+ func runRm(cmd *cobra.Command, args []string) error {
31
+ name := args[0]
32
+
33
+ s, err := store.Load()
34
+ if err != nil {
35
+ return fmt.Errorf("failed to load store: %w", err)
36
+ }
37
+
38
+ wt := s.FindByName(name)
39
+ if wt == nil {
40
+ return fmt.Errorf("worktree '%s' not found", name)
41
+ }
42
+
43
+ // Check if we're currently in this worktree
44
+ cwd, err := os.Getwd()
45
+ if err == nil {
46
+ absWtPath, _ := filepath.Abs(wt.Path)
47
+ absCwd, _ := filepath.Abs(cwd)
48
+ if strings.HasPrefix(absCwd, absWtPath) {
49
+ return fmt.Errorf("cannot remove current worktree - exit first")
50
+ }
51
+ }
52
+
53
+ // Get branch before removing (for -d flag)
54
+ branch := getWorktreeBranch(wt.Path)
55
+
56
+ // Check if directory exists
57
+ if _, err := os.Stat(wt.Path); err == nil {
58
+ // Remove the worktree using git (suppress noise)
59
+ gitCmd := exec.Command("git", "worktree", "remove", wt.Path)
60
+ if err := gitCmd.Run(); err != nil {
61
+ return fmt.Errorf("failed to remove worktree: %w", err)
62
+ }
63
+ }
64
+
65
+ // Prune worktrees
66
+ exec.Command("git", "worktree", "prune").Run()
67
+
68
+ // Delete branch if requested
69
+ if deleteBranch && branch != "[missing]" && branch != "[detached]" {
70
+ gitCmd := exec.Command("git", "branch", "-D", branch)
71
+ if err := gitCmd.Run(); err != nil {
72
+ fmt.Fprintf(os.Stderr, "Warning: failed to delete branch %s: %v\n", branch, err)
73
+ }
74
+ }
75
+
76
+ // Remove from store
77
+ s.RemoveByName(name)
78
+ if err := s.Save(); err != nil {
79
+ return fmt.Errorf("failed to save store: %w", err)
80
+ }
81
+
82
+ if deleteBranch && branch != "[missing]" && branch != "[detached]" {
83
+ fmt.Println(success.Render("✓"), "Removed", bold.Render(name), dim.Render("(branch deleted)"))
84
+ } else {
85
+ fmt.Println(success.Render("✓"), "Removed", bold.Render(name))
86
+ }
87
+ return nil
88
+ }