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 }