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:
Sebastian UnterschützandClaude Opus 5.5 committed 2026-10-28 11:23:28 +01:00
1 parent 786768ec53
commit c2e590bf52
6 files changed
+445 -1

No files matched your search

+43
View File
@@ -114,3 +114,46 @@ complete --command %s --wraps netssh
`, 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")
}
+30
View File
@@ -166,6 +166,16 @@ func isLegacyLine(line string) bool {
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.
// Missing profiles are a no-op; malformed blocks leave the file unchanged.
func Uninstall(profilePath string) (removed bool, err error) {
@@ -248,3 +258,23 @@ func writeProfile(profilePath, content string) error {
}
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"))
}