// SPDX-FileCopyrightText: Amolith // // SPDX-License-Identifier: AGPL-3.0-or-later package main import ( "bufio" "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" ) var installCmd = &cobra.Command{ Use: "install", Short: "Create symlinks for git formatted-commit and git formatted-tag", Long: `Creates symlinks in ~/.local/bin so you can use: git formatted-commit ... git formatted-tag ... The symlinks point to this binary and dispatch based on the invocation name.`, RunE: func(cmd *cobra.Command, args []string) error { return runInstall() }, } func runInstall() error { execPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to determine executable path: %w", err) } execPath, err = filepath.EvalSymlinks(execPath) if err != nil { return fmt.Errorf("failed to resolve executable path: %w", err) } homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to determine home directory: %w", err) } binDir := filepath.Join(homeDir, ".local", "bin") if err := os.MkdirAll(binDir, 0o755); err != nil { return fmt.Errorf("failed to create %s: %w", binDir, err) } symlinks := []struct { name string path string }{ {"git-formatted-commit", filepath.Join(binDir, "git-formatted-commit")}, {"git-formatted-tag", filepath.Join(binDir, "git-formatted-tag")}, } for _, link := range symlinks { if err := createSymlink(execPath, link.path, link.name); err != nil { return err } } checkPath(binDir) return nil } func createSymlink(target, linkPath, name string) error { info, err := os.Lstat(linkPath) if err == nil { if info.Mode()&os.ModeSymlink == 0 { fmt.Fprintf(os.Stderr, "Error: %s exists and is not a symlink\n", linkPath) fmt.Fprintln(os.Stderr, " Refusing to overwrite. Remove it manually if intended.") return fmt.Errorf("cannot overwrite non-symlink at %s", linkPath) } existingTarget, err := os.Readlink(linkPath) if err != nil { return fmt.Errorf("failed to read existing symlink %s: %w", linkPath, err) } if existingTarget == target { fmt.Printf(" %s → %s (already exists, same target)\n", name, target) return nil } fmt.Printf(" %s exists, points to %s\n", linkPath, existingTarget) fmt.Printf(" Overwrite to point to %s? [y/N] ", target) reader := bufio.NewReader(os.Stdin) response, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("failed to read response: %w", err) } response = strings.TrimSpace(strings.ToLower(response)) if response != "y" && response != "yes" { fmt.Printf(" Skipping %s\n", name) return nil } if err := os.Remove(linkPath); err != nil { return fmt.Errorf("failed to remove existing symlink: %w", err) } } else if !os.IsNotExist(err) { return fmt.Errorf("failed to check %s: %w", linkPath, err) } if err := os.Symlink(target, linkPath); err != nil { return fmt.Errorf("failed to create symlink %s: %w", linkPath, err) } fmt.Printf(" Created %s → %s\n", name, target) return nil } func checkPath(binDir string) { pathEnv := os.Getenv("PATH") paths := filepath.SplitList(pathEnv) for _, p := range paths { if p == binDir { return } } fmt.Println() fmt.Printf("⚠ Warning: %s is not in your PATH\n", binDir) fmt.Println(" Add this to your shell config:") fmt.Printf(" export PATH=\"$HOME/.local/bin:$PATH\"\n") }