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/cmd/root.go ADDED
@@ -0,0 +1,74 @@
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 rootCmd = &cobra.Command{
15
+ Use: "pf",
16
+ Short: "pf - git worktree utility",
17
+ Long: `pf is a git worktree utility for managing worktrees efficiently.`,
18
+ Run: func(cmd *cobra.Command, args []string) {
19
+ if v, _ := cmd.Flags().GetBool("version"); v {
20
+ fmt.Printf("pf %s\n", Version)
21
+ return
22
+ }
23
+ printCurrentContext()
24
+ fmt.Println()
25
+ cmd.Help()
26
+ },
27
+ }
28
+
29
+ func printCurrentContext() {
30
+ // Get current branch
31
+ branchCmd := exec.Command("git", "branch", "--show-current")
32
+ branchOutput, err := branchCmd.Output()
33
+ if err != nil {
34
+ fmt.Println("Not in a git repository")
35
+ return
36
+ }
37
+ branch := strings.TrimSpace(string(branchOutput))
38
+
39
+ // Get current worktree path
40
+ cwd, err := os.Getwd()
41
+ if err != nil {
42
+ return
43
+ }
44
+
45
+ // Check if we're in a pf worktree
46
+ s, _ := store.Load()
47
+ var worktreeName string
48
+ if s != nil {
49
+ for _, wt := range s.Worktrees {
50
+ absPath, _ := filepath.Abs(wt.Path)
51
+ if strings.HasPrefix(cwd, absPath) {
52
+ worktreeName = wt.Name
53
+ break
54
+ }
55
+ }
56
+ }
57
+
58
+ if worktreeName != "" {
59
+ fmt.Printf("worktree: %s git:(%s)\n", worktreeName, branch)
60
+ } else {
61
+ fmt.Printf("main git:(%s)\n", branch)
62
+ }
63
+ }
64
+
65
+ func init() {
66
+ rootCmd.Flags().BoolP("version", "v", false, "Print version information")
67
+ }
68
+
69
+ func Execute() {
70
+ if err := rootCmd.Execute(); err != nil {
71
+ fmt.Fprintln(os.Stderr, err)
72
+ os.Exit(1)
73
+ }
74
+ }
@@ -0,0 +1,9 @@
1
+ package cmd
2
+
3
+ import "github.com/charmbracelet/lipgloss"
4
+
5
+ var (
6
+ success = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true)
7
+ dim = lipgloss.NewStyle().Faint(true)
8
+ bold = lipgloss.NewStyle().Bold(true)
9
+ )
@@ -0,0 +1,27 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/spf13/cobra"
7
+ )
8
+
9
+ var (
10
+ Version = "dev"
11
+ GitCommit = "none"
12
+ BuildDate = "unknown"
13
+ )
14
+
15
+ var versionCmd = &cobra.Command{
16
+ Use: "version",
17
+ Short: "Print version information",
18
+ Run: func(cmd *cobra.Command, args []string) {
19
+ fmt.Printf("pf %s\n", Version)
20
+ fmt.Printf(" Git commit: %s\n", GitCommit)
21
+ fmt.Printf(" Built: %s\n", BuildDate)
22
+ },
23
+ }
24
+
25
+ func init() {
26
+ rootCmd.AddCommand(versionCmd)
27
+ }
@@ -0,0 +1,57 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "os/exec"
7
+ "strings"
8
+
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ var whereCmd = &cobra.Command{
13
+ Use: "where",
14
+ Short: "Show current worktree and branch",
15
+ Hidden: true,
16
+ RunE: runWhere,
17
+ }
18
+
19
+ func init() {
20
+ rootCmd.AddCommand(whereCmd)
21
+ }
22
+
23
+ func runWhere(cmd *cobra.Command, args []string) error {
24
+ // Get current branch
25
+ branchCmd := exec.Command("git", "branch", "--show-current")
26
+ branchOutput, err := branchCmd.Output()
27
+ if err != nil {
28
+ return fmt.Errorf("failed to get current branch: %w", err)
29
+ }
30
+ branch := strings.TrimSpace(string(branchOutput))
31
+
32
+ // Get current worktree path
33
+ cwd, err := os.Getwd()
34
+ if err != nil {
35
+ return fmt.Errorf("failed to get working directory: %w", err)
36
+ }
37
+
38
+ // Check if we're in a worktree
39
+ gitDirCmd := exec.Command("git", "rev-parse", "--git-dir")
40
+ gitDirOutput, err := gitDirCmd.Output()
41
+ if err != nil {
42
+ return fmt.Errorf("not in a git repository: %w", err)
43
+ }
44
+ gitDir := strings.TrimSpace(string(gitDirOutput))
45
+
46
+ isWorktree := strings.Contains(gitDir, "worktrees")
47
+
48
+ fmt.Printf("Branch: %s\n", branch)
49
+ fmt.Printf("Path: %s\n", cwd)
50
+ if isWorktree {
51
+ fmt.Println("Type: worktree")
52
+ } else {
53
+ fmt.Println("Type: main repository")
54
+ }
55
+
56
+ return nil
57
+ }
package/go/go.mod ADDED
@@ -0,0 +1,37 @@
1
+ module github.com/pullfrog/cli
2
+
3
+ go 1.25
4
+
5
+ require github.com/spf13/cobra v1.10.2
6
+
7
+ require (
8
+ github.com/atotto/clipboard v0.1.4 // indirect
9
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
10
+ github.com/catppuccin/go v0.3.0 // indirect
11
+ github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
12
+ github.com/charmbracelet/bubbletea v1.3.6 // indirect
13
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
14
+ github.com/charmbracelet/huh v0.8.0 // indirect
15
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
16
+ github.com/charmbracelet/x/ansi v0.9.3 // indirect
17
+ github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
18
+ github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
19
+ github.com/charmbracelet/x/term v0.2.1 // indirect
20
+ github.com/dustin/go-humanize v1.0.1 // indirect
21
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
22
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
23
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
24
+ github.com/mattn/go-isatty v0.0.20 // indirect
25
+ github.com/mattn/go-localereader v0.0.1 // indirect
26
+ github.com/mattn/go-runewidth v0.0.16 // indirect
27
+ github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
28
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
29
+ github.com/muesli/cancelreader v0.2.2 // indirect
30
+ github.com/muesli/termenv v0.16.0 // indirect
31
+ github.com/rivo/uniseg v0.4.7 // indirect
32
+ github.com/spf13/pflag v1.0.9 // indirect
33
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
34
+ golang.org/x/sync v0.15.0 // indirect
35
+ golang.org/x/sys v0.33.0 // indirect
36
+ golang.org/x/text v0.23.0 // indirect
37
+ )
package/go/go.sum ADDED
@@ -0,0 +1,73 @@
1
+ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2
+ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5
+ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
6
+ github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
7
+ github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
8
+ github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
9
+ github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
10
+ github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
11
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
12
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
13
+ github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
14
+ github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
15
+ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
16
+ github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
17
+ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
18
+ github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
19
+ github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
20
+ github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
21
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
22
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
23
+ github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
24
+ github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
25
+ github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
26
+ github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
27
+ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
28
+ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
29
+ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
30
+ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
31
+ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
32
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
33
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
34
+ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
35
+ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
36
+ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
37
+ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
38
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
39
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
40
+ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
41
+ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
42
+ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
43
+ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
44
+ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
45
+ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
46
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
47
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
48
+ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
49
+ github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
50
+ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
51
+ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
52
+ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
53
+ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
54
+ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
55
+ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
56
+ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
57
+ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
58
+ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
59
+ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
60
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
61
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
62
+ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
63
+ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
64
+ golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
65
+ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
66
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
67
+ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
68
+ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
69
+ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
70
+ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
71
+ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
72
+ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
73
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -0,0 +1,124 @@
1
+ package store
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "os/exec"
7
+ "path/filepath"
8
+ "strings"
9
+ "time"
10
+ )
11
+
12
+ const worktreesFile = "worktrees.json"
13
+
14
+ type Worktree struct {
15
+ Name string `json:"name"`
16
+ Path string `json:"path"`
17
+ CreatedAt time.Time `json:"created_at"`
18
+ }
19
+
20
+ type Store struct {
21
+ Worktrees []Worktree `json:"worktrees"`
22
+ }
23
+
24
+ func GetGitRoot() (string, error) {
25
+ cmd := exec.Command("git", "rev-parse", "--show-toplevel")
26
+ output, err := cmd.Output()
27
+ if err != nil {
28
+ return "", err
29
+ }
30
+ return strings.TrimSpace(string(output)), nil
31
+ }
32
+
33
+ func GetGitCommonDir() (string, error) {
34
+ cmd := exec.Command("git", "rev-parse", "--git-common-dir")
35
+ output, err := cmd.Output()
36
+ if err != nil {
37
+ return "", err
38
+ }
39
+ path := strings.TrimSpace(string(output))
40
+ if !filepath.IsAbs(path) {
41
+ cwd, err := os.Getwd()
42
+ if err != nil {
43
+ return "", err
44
+ }
45
+ path = filepath.Join(cwd, path)
46
+ }
47
+ return path, nil
48
+ }
49
+
50
+ func getStorePath() (string, error) {
51
+ gitDir, err := GetGitCommonDir()
52
+ if err != nil {
53
+ return "", err
54
+ }
55
+ return filepath.Join(gitDir, "pf"), nil
56
+ }
57
+
58
+ func Load() (*Store, error) {
59
+ storePath, err := getStorePath()
60
+ if err != nil {
61
+ return nil, err
62
+ }
63
+
64
+ filePath := filepath.Join(storePath, worktreesFile)
65
+ data, err := os.ReadFile(filePath)
66
+ if err != nil {
67
+ if os.IsNotExist(err) {
68
+ return &Store{Worktrees: []Worktree{}}, nil
69
+ }
70
+ return nil, err
71
+ }
72
+
73
+ var store Store
74
+ if err := json.Unmarshal(data, &store); err != nil {
75
+ return nil, err
76
+ }
77
+ return &store, nil
78
+ }
79
+
80
+ func (s *Store) Save() error {
81
+ storePath, err := getStorePath()
82
+ if err != nil {
83
+ return err
84
+ }
85
+
86
+ if err := os.MkdirAll(storePath, 0755); err != nil {
87
+ return err
88
+ }
89
+
90
+ filePath := filepath.Join(storePath, worktreesFile)
91
+ data, err := json.MarshalIndent(s, "", " ")
92
+ if err != nil {
93
+ return err
94
+ }
95
+
96
+ return os.WriteFile(filePath, data, 0644)
97
+ }
98
+
99
+ func (s *Store) Add(name, path string) {
100
+ s.Worktrees = append(s.Worktrees, Worktree{
101
+ Name: name,
102
+ Path: path,
103
+ CreatedAt: time.Now(),
104
+ })
105
+ }
106
+
107
+ func (s *Store) FindByName(name string) *Worktree {
108
+ for i := range s.Worktrees {
109
+ if s.Worktrees[i].Name == name {
110
+ return &s.Worktrees[i]
111
+ }
112
+ }
113
+ return nil
114
+ }
115
+
116
+ func (s *Store) RemoveByName(name string) {
117
+ filtered := []Worktree{}
118
+ for _, w := range s.Worktrees {
119
+ if w.Name != name {
120
+ filtered = append(filtered, w)
121
+ }
122
+ }
123
+ s.Worktrees = filtered
124
+ }
package/go/main.go ADDED
@@ -0,0 +1,7 @@
1
+ package main
2
+
3
+ import "github.com/pullfrog/cli/cmd"
4
+
5
+ func main() {
6
+ cmd.Execute()
7
+ }
package/index.ts ADDED
@@ -0,0 +1,107 @@
1
+ import pc from "picocolors";
2
+ import { newCommand } from "./commands/new.js";
3
+ import { openCommand } from "./commands/open.js";
4
+ import { lsCommand } from "./commands/ls.js";
5
+ import { rmCommand } from "./commands/rm.js";
6
+ // import { completionCommand } from "./commands/completion.js";
7
+
8
+ const VERSION = "0.0.2";
9
+
10
+ function printHelp() {
11
+ const dim = pc.dim;
12
+ const cyan = pc.cyan;
13
+ const green = pc.green;
14
+
15
+ console.log(`${pc.bold("pf")} ${dim(`v${VERSION}`)} - A humane utility for git worktrees
16
+
17
+ ${pc.bold("Usage:")} ${cyan("pf")} ${dim("<command>")} ${dim("[options]")}
18
+
19
+ ${pc.bold("Commands:")}
20
+ ${green("new")} ${dim("[name]")} Create a branch and worktree with the specified name ${dim('(default: "pf-[hash]")')}
21
+ ${green("open")} ${dim("<name>")} Open a worktree in a subshell
22
+ ${green("ls")} List all tracked worktrees
23
+ ${green("rm")} ${dim("<name>")} Remove a worktree
24
+
25
+ ${pc.bold("Options:")}
26
+ ${cyan("--help")}, ${cyan("-h")} Show help
27
+ ${cyan("--version")}, ${cyan("-v")} Show version
28
+
29
+ ${dim("─".repeat(60))}
30
+
31
+ ${pc.bold("Why pf?")}
32
+
33
+ In agentic development, you often want to spin up isolated workspaces for
34
+ specific tasks—without stashing or committing your current changes. pf makes
35
+ this effortless.
36
+
37
+ Create a new worktree for a task:
38
+
39
+ ${dim("$")} ${cyan("pf new add-login-button")}
40
+
41
+ This creates a branch and worktree, then drops you into a subshell. Open it
42
+ in your editor or point an AI coding agent at it. Your changes are completely
43
+ independent of any other work on your machine.
44
+
45
+ When you're done, push to GitHub and create a PR:
46
+
47
+ ${dim("$")} ${cyan("git push -u origin add-login-button")}
48
+
49
+ Or merge directly back into main:
50
+
51
+ ${dim("$")} ${cyan("git checkout main && git merge add-login-button")}
52
+
53
+ Then just ${cyan("exit")} to pop out of the worktree and back to your main repo.
54
+ Clean up when you're done:
55
+
56
+ ${dim("$")} ${cyan("pf rm add-login-button")}
57
+
58
+ Use ${cyan("pf ls")} to see your active worktrees, and ${cyan("pf open <name>")} to
59
+ return to one later.`);
60
+ }
61
+
62
+ function printVersion() {
63
+ console.log(`pf v${VERSION}`);
64
+ }
65
+
66
+ const args = process.argv.slice(2);
67
+ const cmd = args[0];
68
+
69
+ if (!cmd || cmd === "--help" || cmd === "-h") {
70
+ printHelp();
71
+ process.exit(0);
72
+ }
73
+
74
+ if (cmd === "--version" || cmd === "-v") {
75
+ printVersion();
76
+ process.exit(0);
77
+ }
78
+
79
+ switch (cmd) {
80
+ case "new":
81
+ newCommand(args[1]);
82
+ break;
83
+ case "open":
84
+ if (!args[1]) {
85
+ console.error("Usage: pf open <name>");
86
+ process.exit(1);
87
+ }
88
+ openCommand(args[1]);
89
+ break;
90
+ case "ls":
91
+ lsCommand(args.includes("--plain"));
92
+ break;
93
+ case "rm":
94
+ if (!args[1]) {
95
+ console.error("Usage: pf rm <name>");
96
+ process.exit(1);
97
+ }
98
+ rmCommand(args[1], args.includes("--delete-branch"));
99
+ break;
100
+ // case "completion":
101
+ // completionCommand(args[1]);
102
+ // break;
103
+ default:
104
+ console.error(`Unknown command: ${cmd}`);
105
+ console.error("Run 'pf --help' for usage.");
106
+ process.exit(1);
107
+ }
package/package.json CHANGED
@@ -1,30 +1,28 @@
1
1
  {
2
2
  "name": "pf",
3
- "version": "0.0.1",
4
- "description": "Point free style!",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "tap test"
3
+ "version": "0.0.2",
4
+ "description": "A humane utility for git worktrees",
5
+ "type": "module",
6
+ "bin": {
7
+ "pf": "dist/index.js"
8
8
  },
9
- "repository": {
10
- "type": "git",
11
- "url": "git://github.com/Submersible/node-pf.git"
9
+ "scripts": {
10
+ "build": "tsc && node build.mjs",
11
+ "dev": "node build.mjs --watch",
12
+ "prepare": "node build.mjs",
13
+ "typecheck": "tsc"
12
14
  },
13
- "keywords": [
14
- "point",
15
- "free",
16
- "functional",
17
- "pure",
18
- "algorithm",
19
- "higher",
20
- "order"
21
- ],
22
- "author": "Ryan Munro <munro.github@gmail.com>",
23
- "license": "MIT",
24
15
  "dependencies": {
25
- "lodash": "~0.9.2"
16
+ "picocolors": "^1.0.0",
17
+ "cli-table3": "^0.6.5"
26
18
  },
27
19
  "devDependencies": {
28
- "tap": "~0.3.2"
29
- }
20
+ "@types/node": "^22.0.0",
21
+ "esbuild": "^0.24.0",
22
+ "typescript": "^5.7.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=24"
26
+ },
27
+ "license": "MIT"
30
28
  }
package/store.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { dirname, isAbsolute, join } from "path";
4
+
5
+ const WORKTREES_FILE = "worktrees.json";
6
+
7
+ export interface Worktree {
8
+ name: string;
9
+ path: string;
10
+ created_at: string;
11
+ }
12
+
13
+ export interface StoreData {
14
+ worktrees: Worktree[];
15
+ }
16
+
17
+ export function getGitRoot(): string {
18
+ return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
19
+ }
20
+
21
+ export function getGitCommonDir(): string {
22
+ let path = execSync("git rev-parse --git-common-dir", { encoding: "utf-8" }).trim();
23
+ if (!isAbsolute(path)) {
24
+ path = join(process.cwd(), path);
25
+ }
26
+ return path;
27
+ }
28
+
29
+ function getStorePath(): string {
30
+ return join(getGitCommonDir(), "pf");
31
+ }
32
+
33
+ export function loadStore(): StoreData {
34
+ const storePath = getStorePath();
35
+ const filePath = join(storePath, WORKTREES_FILE);
36
+
37
+ if (!existsSync(filePath)) {
38
+ return { worktrees: [] };
39
+ }
40
+
41
+ const data = readFileSync(filePath, "utf-8");
42
+ return JSON.parse(data);
43
+ }
44
+
45
+ export function saveStore(store: StoreData): void {
46
+ const storePath = getStorePath();
47
+ mkdirSync(storePath, { recursive: true });
48
+
49
+ const filePath = join(storePath, WORKTREES_FILE);
50
+ writeFileSync(filePath, JSON.stringify(store, null, 2));
51
+ }
52
+
53
+ export function addWorktree(store: StoreData, name: string, path: string): void {
54
+ store.worktrees.push({
55
+ name,
56
+ path,
57
+ created_at: new Date().toISOString(),
58
+ });
59
+ }
60
+
61
+ export function findWorktree(store: StoreData, name: string): Worktree | undefined {
62
+ return store.worktrees.find((w) => w.name === name);
63
+ }
64
+
65
+ export function removeWorktree(store: StoreData, name: string): void {
66
+ store.worktrees = store.worktrees.filter((w) => w.name !== name);
67
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": ".",
11
+ "declaration": false,
12
+ "noEmit": true
13
+ },
14
+ "include": ["*.ts", "commands/*.ts"],
15
+ "exclude": ["node_modules", "dist", "go"]
16
+ }