feat: add alias file generation and management for shell hooks
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.
This commit is contained in:
Sebastian Unterschütz
2026-05-27 23:08:30 +02:00
parent 7c902cab3a
commit fa646f25a6
5 changed files with 450 additions and 58 deletions
+32 -5
View File
@@ -8,10 +8,15 @@ import (
)
const (
// Marker is the unique string used to detect an existing hook installation.
// 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.
@@ -68,8 +73,30 @@ func Install(profilePath string) (installed bool, err error) {
return true, nil
}
// Uninstall removes the hook line from the profile.
// Returns (true, nil) when removed, (false, nil) when the hook was not found.
// 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)
@@ -80,13 +107,13 @@ func Uninstall(profilePath string) (removed bool, err error) {
return false, fmt.Errorf("reading profile: %w", err)
}
content := string(data)
if !strings.Contains(content, Marker) {
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) {
if strings.Contains(line, Marker) || strings.Contains(line, AliasesMarker) {
continue
}
kept = append(kept, line)