Files
sherlock/internal/installer/helpers.go
T
amacocian 53b0d025f5
Release / release (push) Successful in 24s
Shared infra for installer
2026-06-13 14:25:40 +02:00

172 lines
4.6 KiB
Go

package installer
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
)
// versionFile reads and trims dir/VERSION, returning "" if absent/empty.
func versionFile(dir string) string {
b, err := os.ReadFile(filepath.Join(dir, "VERSION"))
if err != nil {
return ""
}
return strings.TrimSpace(string(b))
}
// latestLocalTag returns the highest vX.Y.Z tag in the git checkout at
// dir, or "" if there are none / git fails.
func latestLocalTag(dir string) string {
cmd := exec.CommandContext(context.Background(), "git", "-C", dir,
"tag", "-l", "v*", "--sort=-v:refname")
out, err := cmd.Output()
if err != nil {
return ""
}
for _, line := range strings.Split(string(out), "\n") {
if t := strings.TrimSpace(line); t != "" {
return t
}
}
return ""
}
// seedConfig copies example → cfgPath when cfgPath does not exist,
// creating parent dirs (0700) and the file (0600). An existing config is
// left untouched.
func seedConfig(opts *Options, example, cfgPath string) error {
if _, err := os.Stat(cfgPath); err == nil {
opts.info("Existing config found at %s — editing it in place (not overwriting).", cfgPath)
return nil
}
opts.info("Creating %s from config.example.toml", cfgPath)
if err := os.MkdirAll(filepath.Dir(cfgPath), 0o700); err != nil {
return err
}
data, err := os.ReadFile(example)
if err != nil {
return err
}
return os.WriteFile(cfgPath, data, 0o600)
}
// pickEditor resolves the editor command: $VISUAL, then $EDITOR, then
// the first of sensible-editor/nano/vi found on PATH. Returns "" if none.
func pickEditor() string {
if v := os.Getenv("VISUAL"); v != "" {
return v
}
if e := os.Getenv("EDITOR"); e != "" {
return e
}
for _, e := range []string{"sensible-editor", "nano", "vi"} {
if _, err := exec.LookPath(e); err == nil {
return e
}
}
return ""
}
// openEditor opens cfgPath in the resolved editor and waits. A missing
// editor is a warning, not an error (the operator can edit by hand).
func openEditor(opts *Options, cfgPath string) error {
ed := pickEditor()
if ed == "" {
opts.warn("no editor found (set $EDITOR). Edit %s by hand, then re-run with -y.", cfgPath)
return nil
}
opts.info("Opening %s in: %s", cfgPath, ed)
// The editor command may include args (e.g. "code --wait"); split on
// spaces like a shell would for the simple cases.
fields := strings.Fields(ed)
cmd := exec.Command(fields[0], append(fields[1:], cfgPath)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func containsPlaceholders(cfgPath string) bool {
b, err := os.ReadFile(cfgPath)
if err != nil {
return false
}
return strings.Contains(string(b), "REPLACE_")
}
// fishCompletionsDir returns the fish completions directory if the
// operator has a fish config dir, else "". We never create a fish config
// for non-fish users.
func fishCompletionsDir() string {
var base string
if d := os.Getenv("__fish_config_dir"); d != "" {
base = d
} else if x := os.Getenv("XDG_CONFIG_HOME"); x != "" {
base = filepath.Join(x, "fish")
} else if home, err := os.UserHomeDir(); err == nil {
base = filepath.Join(home, ".config", "fish")
}
if base == "" {
return ""
}
if fi, err := os.Stat(base); err != nil || !fi.IsDir() {
return ""
}
return filepath.Join(base, "completions")
}
// installCompletions copies the shipped shell completions into the
// operator's shell config when present. Currently fish only.
func installCompletions(opts *Options, source string) error {
dir := fishCompletionsDir()
if dir == "" {
return nil // not a fish user
}
src := filepath.Join(source, "completions", "sherlock.fish")
data, err := os.ReadFile(src)
if err != nil {
return err
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
dst := filepath.Join(dir, "sherlock.fish")
if err := os.WriteFile(dst, data, 0o644); err != nil {
return err
}
opts.info("Installed fish completion to %s", dst)
return nil
}
// reportInstallDir prints where binaries landed and a note if that dir
// is not on PATH (standard Go setup, not something we work around).
func reportInstallDir(opts *Options) {
bindir := goEnv("GOBIN")
if bindir == "" {
if gp := goEnv("GOPATH"); gp != "" {
bindir = filepath.Join(gp, "bin")
}
}
if bindir == "" {
return
}
opts.info("Installed to %s", bindir)
for _, p := range filepath.SplitList(os.Getenv("PATH")) {
if p == bindir {
return
}
}
opts.info("Note: %s is the Go install dir — make sure it's on your PATH.", bindir)
}
func goEnv(key string) string {
out, err := exec.Command("go", "env", key).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}