fa646f25a6
Release / release (push) Successful in 49s
- **Aliases**: Generate shell alias files (`aliases.sh` for bash/zsh, `netssh.fish` for fish) from cached hosts. Regenerate on each shell startup and cache refresh to keep aliases updated. - **Hooks**: Extend shell hook functionality to include alias file support. Install and uninstall commands updated for bash, zsh, and fish. - **Tests**: Add unit tests to verify alias file generation, path resolution, and idempotent hook installation. - **Docs**: Update README with instructions for alias file usage, installation, and relation to hooks.
129 lines
4.2 KiB
Go
129 lines
4.2 KiB
Go
package hook
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// Marker is the unique string used to detect an existing hook line.
|
|
Marker = "netssh shell-init"
|
|
// Line is the full line written into the shell profile.
|
|
Line = "netssh shell-init # netssh cache auto-refresh"
|
|
|
|
// AliasesMarker is the unique string used to detect the aliases source line.
|
|
AliasesMarker = "netssh/aliases.sh"
|
|
// AliasesSourceLine is the bash/zsh line that sources the generated aliases file.
|
|
AliasesSourceLine = "source ~/.cache/netssh/aliases.sh 2>/dev/null # netssh aliases"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// InstallAliasesSource appends the aliases source line to a bash/zsh profile.
|
|
// This is a no-op for fish (which uses conf.d auto-sourcing instead).
|
|
// Returns (true, nil) when newly added, (false, nil) when already present.
|
|
func InstallAliasesSource(profilePath string) (installed bool, err error) {
|
|
data, err := os.ReadFile(profilePath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return false, fmt.Errorf("reading profile: %w", err)
|
|
}
|
|
if strings.Contains(string(data), AliasesMarker) {
|
|
return false, nil
|
|
}
|
|
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, "%s\n", AliasesSourceLine); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Uninstall removes the hook line and the aliases source line from the profile.
|
|
// Returns (true, nil) when something was removed, (false, nil) when nothing was 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) && !strings.Contains(content, AliasesMarker) {
|
|
return false, nil
|
|
}
|
|
|
|
var kept []string
|
|
for _, line := range strings.Split(content, "\n") {
|
|
if strings.Contains(line, Marker) || strings.Contains(line, AliasesMarker) {
|
|
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
|
|
}
|