install.go

  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}