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>
160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
package hook
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/storage"
|
|
)
|
|
|
|
const completionMarker = "# netssh command alias completion — generated automatically"
|
|
|
|
// SyncCommandCompletion installs lazy loaders for the configured command alias.
|
|
// The loaders obtain the canonical completion from the current netssh binary,
|
|
// so aliases configured by the wizard work without a separate completion install.
|
|
func SyncCommandCompletion(alias string) error {
|
|
if err := config.ValidateCommandAlias(alias); err != nil {
|
|
return err
|
|
}
|
|
statePath := filepath.Join(filepath.Dir(AliasesPath()), "completion_alias")
|
|
data, err := os.ReadFile(statePath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
previous := strings.TrimSpace(string(data))
|
|
if err := config.ValidateCommandAlias(previous); err != nil {
|
|
return fmt.Errorf("invalid completion alias state: %w", err)
|
|
}
|
|
if alias == "" && previous == "" {
|
|
return nil
|
|
}
|
|
|
|
for _, shell := range []string{"bash", "zsh", "fish"} {
|
|
if previous != "" && previous != alias {
|
|
path := commandCompletionPath(shell, previous)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
// Only delete files owned by netssh; preserve any user replacements.
|
|
if strings.HasPrefix(string(data), completionMarker+"\n") || strings.HasPrefix(string(data), "#compdef "+previous+"\n"+completionMarker+"\n") {
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if alias == "" {
|
|
continue
|
|
}
|
|
if shell == "fish" {
|
|
if _, err := os.Stat(FishConfigDir()); os.IsNotExist(err) {
|
|
continue
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
path := commandCompletionPath(shell, alias)
|
|
script := commandCompletionScript(shell, alias)
|
|
existing, err := os.ReadFile(path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if string(existing) == script {
|
|
continue
|
|
}
|
|
if err := storage.WriteFile(path, []byte(script), 0o644); err != nil {
|
|
return fmt.Errorf("writing %s alias completion: %w", shell, err)
|
|
}
|
|
}
|
|
if alias == "" {
|
|
if err := os.Remove(statePath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
return storage.WriteFile(statePath, []byte(alias+"\n"), 0o600)
|
|
}
|
|
|
|
func commandCompletionPath(shell, alias string) string {
|
|
switch shell {
|
|
case "bash":
|
|
return filepath.Join(os.Getenv("HOME"), ".local", "share", "bash-completion", "completions", alias)
|
|
case "zsh":
|
|
return filepath.Join(ZshFuncDir(), "_"+alias)
|
|
default:
|
|
return filepath.Join(FishConfigDir(), "completions", alias+".fish")
|
|
}
|
|
}
|
|
|
|
func commandCompletionScript(shell, alias string) string {
|
|
switch shell {
|
|
case "bash":
|
|
return fmt.Sprintf(`%s
|
|
source <(netssh completion bash)
|
|
if [[ $(type -t compopt) == builtin ]]; then
|
|
complete -o default -F __start_netssh %s
|
|
else
|
|
complete -o default -o nospace -F __start_netssh %s
|
|
fi
|
|
`, completionMarker, alias, alias)
|
|
case "zsh":
|
|
return fmt.Sprintf(`#compdef %s
|
|
%s
|
|
source <(netssh completion zsh)
|
|
compdef _netssh %s
|
|
_netssh "$@"
|
|
`, alias, completionMarker, alias)
|
|
default:
|
|
return fmt.Sprintf(`%s
|
|
netssh completion fish | source
|
|
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")
|
|
}
|