Files
ssh-netbox-wrapper/internal/hook/hook.go
T
Sebastian Unterschütz 7c902cab3a
Release / release (push) Successful in 50s
feat: introduce shortcuts and shell hook support
- **Shortcuts**: Add hostname normalization with domain stripping and hyphen folding. Include alias generation for cached hosts.
- **Shell Hook**: Automate 24h cache refresh trigger with shell startup hook. Add install/uninstall commands for bash, zsh, and fish.
- **Wizard**: Extend setup wizard to configure shortcuts (domains, hyphen stripping) and default SSH port.
- **Cache**: Add `GetByShortcut` for resolving hosts via normalized shortcuts. Implement `NeedsRefresh` / `SetRefreshed` logic for refresh timestamps.
- **Tests**: Comprehensive unit tests for shortcuts, hook installation, cache refresh, and alias generation.
- **Docs**: Update README with shortcuts, shell hook, and default SSH port configuration.
2026-05-27 22:53:24 +02:00

102 lines
3.0 KiB
Go

package hook
import (
"fmt"
"os"
"path/filepath"
"strings"
)
const (
// Marker is the unique string used to detect an existing hook installation.
Marker = "netssh shell-init"
// Line is the full line written into the shell profile.
Line = "netssh shell-init # netssh cache auto-refresh"
)
// ProfilePath returns the canonical shell profile path for the given shell name.
func ProfilePath(shell string) (string, error) {
switch shell {
case "bash":
return filepath.Join(os.Getenv("HOME"), ".bashrc"), nil
case "zsh":
return filepath.Join(os.Getenv("HOME"), ".zshrc"), nil
case "fish":
configDir, err := os.UserConfigDir()
if err != nil {
configDir = filepath.Join(os.Getenv("HOME"), ".config")
}
return filepath.Join(configDir, "fish", "config.fish"), nil
default:
return "", fmt.Errorf("unsupported shell %q — supported: bash, zsh, fish", shell)
}
}
// ReloadNote returns the shell-specific hint for reloading the profile.
func ReloadNote(profilePath string) string {
return "Reload with: source " + profilePath
}
// IsInstalled reports whether the hook marker is present in the profile file.
// Returns false if the file does not exist or cannot be read.
func IsInstalled(profilePath string) bool {
data, err := os.ReadFile(profilePath)
if err != nil {
return false
}
return strings.Contains(string(data), Marker)
}
// Install appends the hook line to the profile if it is not already present.
// Returns (true, nil) when newly installed, (false, nil) when already present.
// Creates the profile file (and any parent directories) if they do not exist.
func Install(profilePath string) (installed bool, err error) {
if IsInstalled(profilePath) {
return false, nil
}
if err := os.MkdirAll(filepath.Dir(profilePath), 0o755); err != nil {
return false, fmt.Errorf("creating directory: %w", err)
}
f, err := os.OpenFile(profilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return false, fmt.Errorf("opening profile: %w", err)
}
defer f.Close()
if _, err := fmt.Fprintf(f, "\n%s\n", Line); err != nil {
return false, err
}
return true, nil
}
// Uninstall removes the hook line from the profile.
// Returns (true, nil) when removed, (false, nil) when the hook was not found.
// Returns (false, nil) — not an error — when the file does not exist.
func Uninstall(profilePath string) (removed bool, err error) {
data, err := os.ReadFile(profilePath)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("reading profile: %w", err)
}
content := string(data)
if !strings.Contains(content, Marker) {
return false, nil
}
var kept []string
for _, line := range strings.Split(content, "\n") {
if strings.Contains(line, Marker) {
continue
}
kept = append(kept, line)
}
// Collapse triple blank lines that the removal may leave.
cleaned := strings.ReplaceAll(strings.Join(kept, "\n"), "\n\n\n", "\n\n")
if err := os.WriteFile(profilePath, []byte(cleaned), 0o644); err != nil {
return false, fmt.Errorf("writing profile: %w", err)
}
return true, nil
}