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 }