feat: add netssh uninstall
Remove everything netssh and install.sh set up, also leftovers of older versions: - netssh blocks and unmarked legacy lines in bash, zsh, and fish profiles - completion files for bash, zsh (incl. the oh-my-zsh cache), fish, and the command alias; files not generated by netssh are kept - the cache directory with hosts, aliases, and per-instance caches - the config file and local notes; the config is first copied to a private netssh.yaml.backup-<timestamp> because it holds API tokens - the netssh binary; for protected paths the sudo command is printed --keep-config keeps config and notes, --dry-run only lists the plan, and --yes skips the confirmation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
786768ec53
commit
c2e590bf52
6 files changed
+445
-1
No files matched your search
+2
-1
@@ -41,6 +41,7 @@ var managedSubcommands = map[string]bool{
|
|||||||
"__complete": true,
|
"__complete": true,
|
||||||
"help": true,
|
"help": true,
|
||||||
"version": true,
|
"version": true,
|
||||||
|
"uninstall": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -357,7 +358,7 @@ func rootCmd() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
root.AddCommand(notesCmd(), configureCmd(), searchCmd(), cacheCmd(), aliasCmd(), hookCmd(), shellInitCmd(), versionCmd())
|
root.AddCommand(notesCmd(), configureCmd(), searchCmd(), cacheCmd(), aliasCmd(), hookCmd(), shellInitCmd(), versionCmd(), uninstallCmd())
|
||||||
|
|
||||||
// cobra builds the "completion" command lazily; force init so we can extend it.
|
// cobra builds the "completion" command lazily; force init so we can extend it.
|
||||||
root.InitDefaultCompletionCmd()
|
root.InitDefaultCompletionCmd()
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
||||||
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/hook"
|
||||||
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// uninstallPlan lists everything netssh has written to this machine.
|
||||||
|
type uninstallPlan struct {
|
||||||
|
Profiles []string // shell profiles with a netssh block or legacy lines
|
||||||
|
Paths []string // files and directories to delete
|
||||||
|
Config string // config file to back up and delete; empty when kept
|
||||||
|
Binaries []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildUninstallPlan(cfg *config.Config, keepConfig bool) uninstallPlan {
|
||||||
|
var plan uninstallPlan
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
addPath := func(path string) {
|
||||||
|
if path == "" || seen[path] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(path); err == nil {
|
||||||
|
seen[path] = true
|
||||||
|
plan.Paths = append(plan.Paths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, profile := range hook.ProfileCandidates() {
|
||||||
|
target, err := filepath.EvalSymlinks(profile)
|
||||||
|
if err != nil || seen[target] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[target] = true
|
||||||
|
if hook.HasEntries(profile) {
|
||||||
|
plan.Profiles = append(plan.Profiles, profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases := []string{hook.CompletionAliasState()}
|
||||||
|
if cfg != nil {
|
||||||
|
aliases = append(aliases, cfg.Shell.Alias)
|
||||||
|
}
|
||||||
|
for _, path := range hook.CompletionFiles(aliases...) {
|
||||||
|
if hook.IsGenerated(path) {
|
||||||
|
addPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cache directory holds hosts, aliases, refresh stamps, and instance caches.
|
||||||
|
cacheDir := filepath.Dir(hook.AliasesPath())
|
||||||
|
addPath(cacheDir)
|
||||||
|
if cfg != nil {
|
||||||
|
for _, path := range []string{cfg.Cache.Path, cfg.CachePath()} {
|
||||||
|
if path == "" || strings.HasPrefix(path, cacheDir+string(filepath.Separator)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addPath(path)
|
||||||
|
addPath(path + ".lock")
|
||||||
|
addPath(filepath.Join(filepath.Dir(path), "last_refresh"))
|
||||||
|
if strings.HasPrefix(filepath.Base(filepath.Dir(path)), "instances-") {
|
||||||
|
addPath(filepath.Dir(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !keepConfig {
|
||||||
|
if _, err := os.Stat(config.Path()); err == nil {
|
||||||
|
plan.Config = config.Path()
|
||||||
|
}
|
||||||
|
// Local server notes live next to the config file.
|
||||||
|
addPath(filepath.Join(filepath.Dir(config.Path()), "netssh"))
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.Binaries = installedBinaries()
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
// installedBinaries finds the running binary and any other netssh in PATH.
|
||||||
|
func installedBinaries() []string {
|
||||||
|
var out []string
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
add := func(path string) {
|
||||||
|
real, err := filepath.EvalSymlinks(path)
|
||||||
|
if err != nil || seen[real] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info, err := os.Stat(real); err != nil || info.IsDir() || filepath.Base(real) != "netssh" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[real] = true
|
||||||
|
out = append(out, real)
|
||||||
|
}
|
||||||
|
if self, err := os.Executable(); err == nil {
|
||||||
|
add(self)
|
||||||
|
}
|
||||||
|
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
||||||
|
if dir != "" {
|
||||||
|
add(filepath.Join(dir, "netssh"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p uninstallPlan) empty() bool {
|
||||||
|
return len(p.Profiles) == 0 && len(p.Paths) == 0 && p.Config == "" && len(p.Binaries) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p uninstallPlan) print(w io.Writer) {
|
||||||
|
for _, profile := range p.Profiles {
|
||||||
|
fmt.Fprintf(w, " edit %s (remove netssh entries)\n", profile)
|
||||||
|
}
|
||||||
|
for _, path := range p.Paths {
|
||||||
|
fmt.Fprintf(w, " delete %s\n", path)
|
||||||
|
}
|
||||||
|
if p.Config != "" {
|
||||||
|
fmt.Fprintf(w, " delete %s (a backup is kept)\n", p.Config)
|
||||||
|
}
|
||||||
|
for _, binary := range p.Binaries {
|
||||||
|
fmt.Fprintf(w, " delete %s\n", binary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute applies the plan. It continues after errors so that one protected
|
||||||
|
// file does not leave the rest behind, and returns commands that need sudo.
|
||||||
|
func (p uninstallPlan) execute(w io.Writer) (manual []string, err error) {
|
||||||
|
var errs []error
|
||||||
|
for _, profile := range p.Profiles {
|
||||||
|
if _, err := hook.Uninstall(profile); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "cleaned %s\n", profile)
|
||||||
|
}
|
||||||
|
for _, path := range p.Paths {
|
||||||
|
if err := os.RemoveAll(path); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "removed %s\n", path)
|
||||||
|
}
|
||||||
|
if p.Config != "" {
|
||||||
|
backup, err := backupConfig(p.Config)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("config not removed: %w", err))
|
||||||
|
} else if err := os.Remove(p.Config); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "removed %s (backup: %s)\n", p.Config, backup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, binary := range p.Binaries {
|
||||||
|
if err := os.Remove(binary); err != nil {
|
||||||
|
if errors.Is(err, fs.ErrPermission) {
|
||||||
|
manual = append(manual, "sudo rm -f "+shellQuote(binary))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "removed %s\n", binary)
|
||||||
|
}
|
||||||
|
return manual, errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// backupConfig keeps a private copy, because the config holds API tokens.
|
||||||
|
func backupConfig(path string) (string, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
backup := path + ".backup-" + time.Now().Format("20060102-150405")
|
||||||
|
if err := storage.WriteFile(backup, data, 0o600); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return backup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shellQuote(s string) string {
|
||||||
|
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
func uninstallCmd() *cobra.Command {
|
||||||
|
var keepConfig, yes, dryRun bool
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "uninstall",
|
||||||
|
Short: "Remove netssh, its shell integration, cache, and config",
|
||||||
|
Long: `Removes everything netssh and install.sh have set up:
|
||||||
|
|
||||||
|
- netssh blocks and unmarked entries of older versions in bash, zsh, and fish profiles
|
||||||
|
- shell completion files (bash, zsh, fish, oh-my-zsh cache, command alias)
|
||||||
|
- the cache directory with hosts, aliases, and per-instance caches
|
||||||
|
- the config file (a private backup is kept) and local server notes
|
||||||
|
- the netssh binary
|
||||||
|
|
||||||
|
Use --keep-config to keep the config file and notes for a reinstall.`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
// A broken config must not block the uninstall.
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: %v\n", err)
|
||||||
|
cfg = nil
|
||||||
|
}
|
||||||
|
plan := buildUninstallPlan(cfg, keepConfig)
|
||||||
|
out := cmd.OutOrStdout()
|
||||||
|
if plan.empty() {
|
||||||
|
fmt.Fprintln(out, "Nothing to remove.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Fprintln(out, "netssh uninstall will:")
|
||||||
|
plan.print(out)
|
||||||
|
if dryRun {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !yes {
|
||||||
|
fmt.Fprint(out, "Proceed? [y/N] ")
|
||||||
|
answer, _ := bufio.NewReader(cmd.InOrStdin()).ReadString('\n')
|
||||||
|
if a := strings.ToLower(strings.TrimSpace(answer)); a != "y" && a != "yes" {
|
||||||
|
return fmt.Errorf("aborted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manual, err := plan.execute(out)
|
||||||
|
for _, command := range manual {
|
||||||
|
fmt.Fprintf(out, "Permission denied. Run: %s\n", command)
|
||||||
|
}
|
||||||
|
if len(plan.Profiles) > 0 {
|
||||||
|
fmt.Fprintln(out, "Open a new terminal. For zsh, also run: rm -f ~/.zcompdump*")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().BoolVar(&keepConfig, "keep-config", false, "Keep the config file and local notes")
|
||||||
|
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Do not ask for confirmation")
|
||||||
|
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Only show what would be removed")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/hook"
|
||||||
|
)
|
||||||
|
|
||||||
|
func uninstallFixture(t *testing.T) (home string) {
|
||||||
|
t.Helper()
|
||||||
|
home = t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("ZDOTDIR", home)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
||||||
|
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
||||||
|
t.Setenv("ZSH", "")
|
||||||
|
t.Setenv("ZSH_CACHE_DIR", "")
|
||||||
|
t.Setenv("NETSSH_SHELL_ALIAS", "")
|
||||||
|
t.Setenv("NETSSH_CACHE_PATH", "")
|
||||||
|
t.Setenv("PATH", filepath.Join(home, "nobin"))
|
||||||
|
files := map[string]string{
|
||||||
|
".bashrc": "export KEEP=1\n# >>> netssh >>>\nnetssh shell-init\n# <<< netssh <<<\n",
|
||||||
|
".zshrc": "plugins=(git)\nnetssh shell-init # netssh cache auto-refresh\nsource ~/.cache/netssh/aliases.sh 2>/dev/null # netssh aliases\n",
|
||||||
|
".profile": "export UNRELATED=1\n",
|
||||||
|
".config/netssh.yaml": "netbox:\n url: \"https://netbox.example.com\"\n token: \"secret\"\nshell:\n alias: \"j\"\n",
|
||||||
|
".config/netssh/notes/abc.txt": "note",
|
||||||
|
".cache/netssh/hosts.json": "{}",
|
||||||
|
".cache/netssh/instances-0011/hosts.json": "{}",
|
||||||
|
".local/share/bash-completion/completions/netssh": "# bash completion for netssh",
|
||||||
|
".zfunc/_netssh": hook.ZshCompletionScript,
|
||||||
|
".zfunc/_j": "#compdef j\n# netssh command alias completion — generated automatically\n",
|
||||||
|
".zfunc/_other": "#compdef other\n",
|
||||||
|
}
|
||||||
|
for name, content := range files {
|
||||||
|
path := filepath.Join(home, name)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUninstall(t *testing.T, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
cmd := rootCmd()
|
||||||
|
var out strings.Builder
|
||||||
|
cmd.SetOut(&out)
|
||||||
|
cmd.SetArgs(append([]string{"uninstall"}, args...))
|
||||||
|
if err := cmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("uninstall: %v\n%s", err, out.String())
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUninstallRemovesEverythingAndBacksUpConfig(t *testing.T) {
|
||||||
|
home := uninstallFixture(t)
|
||||||
|
if out := runUninstall(t, "--dry-run"); !strings.Contains(out, ".zshrc") || strings.Contains(out, ".profile") {
|
||||||
|
t.Fatalf("plan:\n%s", out)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(home, ".cache", "netssh")); err != nil {
|
||||||
|
t.Fatal("dry run removed files")
|
||||||
|
}
|
||||||
|
runUninstall(t, "--yes")
|
||||||
|
|
||||||
|
for name, want := range map[string]string{".bashrc": "export KEEP=1\n", ".zshrc": "plugins=(git)\n", ".profile": "export UNRELATED=1\n"} {
|
||||||
|
data, _ := os.ReadFile(filepath.Join(home, name))
|
||||||
|
if string(data) != want {
|
||||||
|
t.Errorf("%s: %q", name, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, gone := range []string{".cache/netssh", ".config/netssh.yaml", ".config/netssh", ".zfunc/_netssh", ".zfunc/_j", ".local/share/bash-completion/completions/netssh"} {
|
||||||
|
if _, err := os.Stat(filepath.Join(home, gone)); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("%s not removed: %v", gone, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(home, ".zfunc", "_other")); err != nil {
|
||||||
|
t.Error("unrelated completion removed")
|
||||||
|
}
|
||||||
|
backups, _ := filepath.Glob(filepath.Join(home, ".config", "netssh.yaml.backup-*"))
|
||||||
|
if len(backups) != 1 {
|
||||||
|
t.Fatalf("backups: %v", backups)
|
||||||
|
}
|
||||||
|
if info, _ := os.Stat(backups[0]); info.Mode().Perm() != 0o600 {
|
||||||
|
t.Error("backup with token is not private")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUninstallKeepConfig(t *testing.T) {
|
||||||
|
home := uninstallFixture(t)
|
||||||
|
runUninstall(t, "--yes", "--keep-config")
|
||||||
|
for _, kept := range []string{".config/netssh.yaml", ".config/netssh/notes/abc.txt"} {
|
||||||
|
if _, err := os.Stat(filepath.Join(home, kept)); err != nil {
|
||||||
|
t.Errorf("%s removed", kept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(home, ".cache", "netssh")); !os.IsNotExist(err) {
|
||||||
|
t.Error("cache kept")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUninstallAbortsWithoutConfirmation(t *testing.T) {
|
||||||
|
home := uninstallFixture(t)
|
||||||
|
cmd := rootCmd()
|
||||||
|
cmd.SetOut(&strings.Builder{})
|
||||||
|
cmd.SetIn(strings.NewReader("n\n"))
|
||||||
|
cmd.SetArgs([]string{"uninstall"})
|
||||||
|
if err := cmd.Execute(); err == nil {
|
||||||
|
t.Fatal("no confirmation required")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(home, ".config", "netssh.yaml")); err != nil {
|
||||||
|
t.Fatal("config removed without confirmation")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -258,3 +258,4 @@ for kind in "${COMPLETIONS[@]}"; do
|
|||||||
info "zsh: if <Tab> still shows files, run: rm -f $(quote_sh "$ZSH_DIR")/.zcompdump* && exec zsh"
|
info "zsh: if <Tab> still shows files, run: rm -f $(quote_sh "$ZSH_DIR")/.zcompdump* && exec zsh"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
info "Remove netssh again with: netssh uninstall"
|
||||||
@@ -114,3 +114,46 @@ complete --command %s --wraps netssh
|
|||||||
`, completionMarker, alias)
|
`, completionMarker, alias)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompletionFiles lists the completion and alias files netssh may have
|
||||||
|
// installed for itself and for the given command aliases.
|
||||||
|
func CompletionFiles(aliases ...string) []string {
|
||||||
|
files := []string{
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".local", "share", "bash-completion", "completions", "netssh"),
|
||||||
|
filepath.Join(ZshFuncDir(), "_netssh"),
|
||||||
|
filepath.Join(FishConfigDir(), "completions", "netssh.fish"),
|
||||||
|
FishAliasesPath(),
|
||||||
|
}
|
||||||
|
// oh-my-zsh caches generated completions separately.
|
||||||
|
for _, dir := range []string{os.Getenv("ZSH_CACHE_DIR"), filepath.Join(os.Getenv("ZSH"), "cache"), filepath.Join(os.Getenv("HOME"), ".oh-my-zsh", "cache")} {
|
||||||
|
if dir != "" && dir != "cache" {
|
||||||
|
files = append(files, filepath.Join(dir, "completions", "_netssh"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, alias := range aliases {
|
||||||
|
if config.ValidateCommandAlias(alias) != nil || alias == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, shell := range []string{"bash", "zsh", "fish"} {
|
||||||
|
files = append(files, commandCompletionPath(shell, alias))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompletionAliasState returns the command alias whose completion files were
|
||||||
|
// last generated, or "" when there is none.
|
||||||
|
func CompletionAliasState() string {
|
||||||
|
data, err := os.ReadFile(filepath.Join(filepath.Dir(AliasesPath()), "completion_alias"))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGenerated reports whether a completion file was written by netssh,
|
||||||
|
// so uninstall never removes a user's own file of the same name.
|
||||||
|
func IsGenerated(path string) bool {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
return err == nil && strings.Contains(string(data), "netssh")
|
||||||
|
}
|
||||||
@@ -166,6 +166,16 @@ func isLegacyLine(line string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasEntries reports whether the profile contains a managed block or legacy lines.
|
||||||
|
func HasEntries(profilePath string) bool {
|
||||||
|
data, err := os.ReadFile(profilePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, removed, err := clean(string(data), profilePath)
|
||||||
|
return removed || err != nil
|
||||||
|
}
|
||||||
|
|
||||||
// Uninstall removes legacy hook lines and complete installer-managed blocks.
|
// Uninstall removes legacy hook lines and complete installer-managed blocks.
|
||||||
// Missing profiles are a no-op; malformed blocks leave the file unchanged.
|
// Missing profiles are a no-op; malformed blocks leave the file unchanged.
|
||||||
func Uninstall(profilePath string) (removed bool, err error) {
|
func Uninstall(profilePath string) (removed bool, err error) {
|
||||||
@@ -248,3 +258,23 @@ func writeProfile(profilePath, content string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfileCandidates lists every shell profile netssh or install.sh may have
|
||||||
|
// written to, in the same order as the installer.
|
||||||
|
func ProfileCandidates() []string {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
zdot := zshDir()
|
||||||
|
paths := []string{
|
||||||
|
filepath.Join(home, ".bashrc"),
|
||||||
|
filepath.Join(home, ".bash_profile"),
|
||||||
|
filepath.Join(home, ".bash_login"),
|
||||||
|
filepath.Join(home, ".profile"),
|
||||||
|
filepath.Join(zdot, ".zshrc"),
|
||||||
|
filepath.Join(zdot, ".zprofile"),
|
||||||
|
filepath.Join(zdot, ".zshenv"),
|
||||||
|
}
|
||||||
|
if zdot != home {
|
||||||
|
paths = append(paths, filepath.Join(home, ".zshrc"), filepath.Join(home, ".zprofile"), filepath.Join(home, ".zshenv"))
|
||||||
|
}
|
||||||
|
return append(paths, filepath.Join(FishConfigDir(), "config.fish"))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user