1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package main
6
7import (
8 "bufio"
9 "fmt"
10 "os"
11 "path/filepath"
12 "strings"
13
14 "github.com/spf13/cobra"
15)
16
17var installCmd = &cobra.Command{
18 Use: "install",
19 Short: "Create symlinks for git formatted-commit and git formatted-tag",
20 Long: `Creates symlinks in ~/.local/bin so you can use:
21 git formatted-commit ...
22 git formatted-tag ...
23
24The symlinks point to this binary and dispatch based on the invocation name.`,
25 RunE: func(cmd *cobra.Command, args []string) error {
26 return runInstall()
27 },
28}
29
30func runInstall() error {
31 execPath, err := os.Executable()
32 if err != nil {
33 return fmt.Errorf("failed to determine executable path: %w", err)
34 }
35 execPath, err = filepath.EvalSymlinks(execPath)
36 if err != nil {
37 return fmt.Errorf("failed to resolve executable path: %w", err)
38 }
39
40 homeDir, err := os.UserHomeDir()
41 if err != nil {
42 return fmt.Errorf("failed to determine home directory: %w", err)
43 }
44
45 binDir := filepath.Join(homeDir, ".local", "bin")
46 if err := os.MkdirAll(binDir, 0o755); err != nil {
47 return fmt.Errorf("failed to create %s: %w", binDir, err)
48 }
49
50 symlinks := []struct {
51 name string
52 path string
53 }{
54 {"git-formatted-commit", filepath.Join(binDir, "git-formatted-commit")},
55 {"git-formatted-tag", filepath.Join(binDir, "git-formatted-tag")},
56 }
57
58 for _, link := range symlinks {
59 if err := createSymlink(execPath, link.path, link.name); err != nil {
60 return err
61 }
62 }
63
64 checkPath(binDir)
65 return nil
66}
67
68func createSymlink(target, linkPath, name string) error {
69 info, err := os.Lstat(linkPath)
70 if err == nil {
71 if info.Mode()&os.ModeSymlink == 0 {
72 fmt.Fprintf(os.Stderr, "Error: %s exists and is not a symlink\n", linkPath)
73 fmt.Fprintln(os.Stderr, " Refusing to overwrite. Remove it manually if intended.")
74 return fmt.Errorf("cannot overwrite non-symlink at %s", linkPath)
75 }
76
77 existingTarget, err := os.Readlink(linkPath)
78 if err != nil {
79 return fmt.Errorf("failed to read existing symlink %s: %w", linkPath, err)
80 }
81
82 if existingTarget == target {
83 fmt.Printf(" %s → %s (already exists, same target)\n", name, target)
84 return nil
85 }
86
87 fmt.Printf(" %s exists, points to %s\n", linkPath, existingTarget)
88 fmt.Printf(" Overwrite to point to %s? [y/N] ", target)
89
90 reader := bufio.NewReader(os.Stdin)
91 response, err := reader.ReadString('\n')
92 if err != nil {
93 return fmt.Errorf("failed to read response: %w", err)
94 }
95
96 response = strings.TrimSpace(strings.ToLower(response))
97 if response != "y" && response != "yes" {
98 fmt.Printf(" Skipping %s\n", name)
99 return nil
100 }
101
102 if err := os.Remove(linkPath); err != nil {
103 return fmt.Errorf("failed to remove existing symlink: %w", err)
104 }
105 } else if !os.IsNotExist(err) {
106 return fmt.Errorf("failed to check %s: %w", linkPath, err)
107 }
108
109 if err := os.Symlink(target, linkPath); err != nil {
110 return fmt.Errorf("failed to create symlink %s: %w", linkPath, err)
111 }
112
113 fmt.Printf(" Created %s → %s\n", name, target)
114 return nil
115}
116
117func checkPath(binDir string) {
118 pathEnv := os.Getenv("PATH")
119 paths := filepath.SplitList(pathEnv)
120
121 for _, p := range paths {
122 if p == binDir {
123 return
124 }
125 }
126
127 fmt.Println()
128 fmt.Printf("⚠ Warning: %s is not in your PATH\n", binDir)
129 fmt.Println(" Add this to your shell config:")
130 fmt.Printf(" export PATH=\"$HOME/.local/bin:$PATH\"\n")
131}