package hook import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/shortcuts" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/storage" ) // AliasEntry holds a single shell alias mapping. type AliasEntry struct { Name string // short alias name (e.g. "web01") Host string // canonical NetBox hostname (e.g. "web01.example.com") } // AliasesPath returns the path for the bash/zsh aliases file. func AliasesPath() string { cacheDir := os.Getenv("XDG_CACHE_HOME") if cacheDir == "" { var err error cacheDir, err = os.UserCacheDir() if err != nil { cacheDir = filepath.Join(os.Getenv("HOME"), ".cache") } } return filepath.Join(cacheDir, "netssh", "aliases.sh") } // FishAliasesPath returns the path for the fish conf.d aliases file. // Fish auto-sources every file in conf.d, so no explicit source line is needed. func FishAliasesPath() string { return filepath.Join(FishConfigDir(), "conf.d", "netssh.fish") } // WriteAliasFiles writes alias definitions to disk for all supported shells. // // The bash/zsh file (~/.cache/netssh/aliases.sh) is always written. // The fish file (~/.config/fish/conf.d/netssh.fish) is written only when // ~/.config/fish/ already exists (i.e. fish is configured on this system). func WriteAliasFiles(entries []AliasEntry) error { if err := writeShAliasFile(entries); err != nil { return err } writeFishAliasFile(entries) // best-effort — never blocks the caller on failure alias := "" for _, entry := range entries { if entry.Host == "" { alias = entry.Name break } } return SyncCommandCompletion(alias) } func writeShAliasFile(entries []AliasEntry) error { p := AliasesPath() if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { return fmt.Errorf("creating aliases dir: %w", err) } var b strings.Builder b.WriteString("# netssh aliases — generated automatically, do not edit\n") b.WriteString("# Regenerated on every shell start and after 'netssh cache refresh'.\n") for _, e := range entries { b.WriteString(e.Definition("bash") + "\n") } return storage.WriteFile(p, []byte(b.String()), 0o600) } func writeFishAliasFile(entries []AliasEntry) { // Only write if fish is configured on this system. if _, err := os.Stat(FishConfigDir()); err != nil { return } p := FishAliasesPath() if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { return } var b strings.Builder b.WriteString("# netssh aliases — generated automatically, do not edit\n") b.WriteString("# Regenerated on every shell start and after 'netssh cache refresh'.\n") for _, e := range entries { b.WriteString(e.Definition("fish") + "\n") } storage.WriteFile(p, []byte(b.String()), 0o600) //nolint:errcheck } // Empty Host represents an alias for the netssh command itself. func (e AliasEntry) Definition(shell string) string { command := "netssh" if e.Host != "" { host := e.Host if !regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`).MatchString(host) { host = shellQuote(host, shell) } command += " " + host } if shell == "fish" { return fmt.Sprintf("alias %s %s", e.Name, shellQuote(command, shell)) } return fmt.Sprintf("alias %s=%s", e.Name, shellQuote(command, shell)) } func shellQuote(value, shell string) string { if shell == "fish" { value = strings.ReplaceAll(value, `\`, `\\`) return "'" + strings.ReplaceAll(value, "'", `\'`) + "'" } return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'" } // BuildAliasEntries gives the configured command alias priority over host aliases. func BuildAliasEntries(entries []cache.Entry, cfg config.Config) []AliasEntry { sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name }) seen := map[string]bool{"netssh": true} var out []AliasEntry if cfg.Shell.Alias != "" && config.ValidateCommandAlias(cfg.Shell.Alias) == nil { seen[cfg.Shell.Alias] = true out = append(out, AliasEntry{Name: cfg.Shell.Alias}) } for _, e := range entries { name := shortcuts.AliasName(e.Name, cfg.Shortcuts) if seen[name] || name == "" || !regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_-]*$`).MatchString(name) { continue } seen[name] = true out = append(out, AliasEntry{Name: name, Host: e.Name}) } return out } // FishConfigDir follows fish's XDG layout on both Linux and macOS. func FishConfigDir() string { dir := os.Getenv("XDG_CONFIG_HOME") if dir == "" { dir = filepath.Join(os.Getenv("HOME"), ".config") } return filepath.Join(dir, "fish") }