Improve installer updates and dependency checks and add command aliases
Release / release (push) Successful in 7m57s
Release / release (push) Successful in 7m57s
This commit is contained in:
1 parent
12cc64fdd9
commit
4b28d441b0
13 files changed
+973
-148
No files matched your search
@@ -4,12 +4,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Shell ShellConfig `mapstructure:"shell"`
|
||||
NetBox NetBoxConfig `mapstructure:"netbox"`
|
||||
Resolver ResolverConfig `mapstructure:"resolver"`
|
||||
Cache CacheConfig `mapstructure:"cache"`
|
||||
@@ -17,6 +19,10 @@ type Config struct {
|
||||
Shortcuts ShortcutsConfig `mapstructure:"shortcuts"`
|
||||
}
|
||||
|
||||
type ShellConfig struct {
|
||||
Alias string `mapstructure:"alias"`
|
||||
}
|
||||
|
||||
type NetBoxConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
Token string `mapstructure:"token"`
|
||||
@@ -71,7 +77,7 @@ func Load() (*Config, error) {
|
||||
v.SetEnvPrefix("NETSSH")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
for _, key := range []string{"netbox.url", "netbox.token", "netbox.token_version", "resolver.strategies", "resolver.management_subnets", "resolver.interface_name", "cache.ttl", "cache.path", "ssh.default_user", "ssh.default_port", "shortcuts.domains", "shortcuts.strip_hyphens"} {
|
||||
for _, key := range []string{"netbox.url", "netbox.token", "netbox.token_version", "resolver.strategies", "resolver.management_subnets", "resolver.interface_name", "cache.ttl", "cache.path", "ssh.default_user", "ssh.default_port", "shortcuts.domains", "shortcuts.strip_hyphens", "shell.alias"} {
|
||||
if err := v.BindEnv(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,6 +94,10 @@ func Load() (*Config, error) {
|
||||
return nil, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
|
||||
if err := ValidateCommandAlias(cfg.Shell.Alias); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.NetBox.TokenVersion == 0 && cfg.NetBox.Token != "" {
|
||||
if strings.HasPrefix(cfg.NetBox.Token, "nbt_") {
|
||||
cfg.NetBox.TokenVersion = 2
|
||||
@@ -106,3 +116,18 @@ func Load() (*Config, error) {
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// ValidateCommandAlias restricts aliases to portable shell identifiers.
|
||||
func ValidateCommandAlias(name string) error {
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
if !regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`).MatchString(name) {
|
||||
return fmt.Errorf("shell.alias must be a shell identifier (e.g. j)")
|
||||
}
|
||||
switch name {
|
||||
case "netssh", "alias", "unalias", "source", "eval", "exec", "command", "builtin", "if", "then", "else", "elif", "fi", "for", "while", "until", "do", "done", "case", "esac", "in", "function", "select", "time", "coproc", "end", "begin", "switch", "set", "export", "return", "break", "continue", "not", "and", "or":
|
||||
return fmt.Errorf("shell.alias %q is reserved; choose another name", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -157,3 +157,23 @@ func TestEnvironmentOverridesWithoutConfigKeys(t *testing.T) {
|
||||
t.Fatalf("env ignored: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandAliasEnvironmentAndValidation(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
t.Setenv("NETSSH_SHELL_ALIAS", "j")
|
||||
cfg, err := Load()
|
||||
if err != nil || cfg.Shell.Alias != "j" {
|
||||
t.Fatalf("alias env: %+v, %v", cfg, err)
|
||||
}
|
||||
for _, name := range []string{"netssh", "if", "alias", "j;touch /tmp/bad", "a b", "1bad"} {
|
||||
t.Setenv("NETSSH_SHELL_ALIAS", name)
|
||||
if _, err := Load(); err == nil {
|
||||
t.Errorf("unsafe alias accepted: %q", name)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"", "j", "jump_server", "goSSH"} {
|
||||
if err := ValidateCommandAlias(name); err != nil {
|
||||
t.Errorf("valid alias rejected: %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
-17
@@ -4,7 +4,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/shortcuts"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/storage"
|
||||
)
|
||||
|
||||
// AliasEntry holds a single shell alias mapping.
|
||||
@@ -15,9 +22,13 @@ type AliasEntry struct {
|
||||
|
||||
// AliasesPath returns the path for the bash/zsh aliases file.
|
||||
func AliasesPath() string {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
cacheDir = filepath.Join(os.Getenv("HOME"), ".cache")
|
||||
cacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||
if cacheDir == "" {
|
||||
var err error
|
||||
cacheDir, err = os.UserCacheDir()
|
||||
if err != nil {
|
||||
cacheDir = filepath.Join(os.Getenv("HOME"), ".cache")
|
||||
}
|
||||
}
|
||||
return filepath.Join(cacheDir, "netssh", "aliases.sh")
|
||||
}
|
||||
@@ -25,11 +36,7 @@ func AliasesPath() string {
|
||||
// FishAliasesPath returns the path for the fish conf.d aliases file.
|
||||
// Fish auto-sources every file in conf.d, so no explicit source line is needed.
|
||||
func FishAliasesPath() string {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
configDir = filepath.Join(os.Getenv("HOME"), ".config")
|
||||
}
|
||||
return filepath.Join(configDir, "fish", "conf.d", "netssh.fish")
|
||||
return filepath.Join(FishConfigDir(), "conf.d", "netssh.fish")
|
||||
}
|
||||
|
||||
// WriteAliasFiles writes alias definitions to disk for all supported shells.
|
||||
@@ -54,18 +61,14 @@ func writeShAliasFile(entries []AliasEntry) error {
|
||||
b.WriteString("# netssh aliases — generated automatically, do not edit\n")
|
||||
b.WriteString("# Regenerated on every shell start and after 'netssh cache refresh'.\n")
|
||||
for _, e := range entries {
|
||||
fmt.Fprintf(&b, "alias %s='netssh %s'\n", e.Name, e.Host)
|
||||
b.WriteString(e.Definition("bash") + "\n")
|
||||
}
|
||||
return os.WriteFile(p, []byte(b.String()), 0o644)
|
||||
return storage.WriteFile(p, []byte(b.String()), 0o600)
|
||||
}
|
||||
|
||||
func writeFishAliasFile(entries []AliasEntry) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Only write if fish is configured on this system.
|
||||
if _, err := os.Stat(filepath.Join(configDir, "fish")); err != nil {
|
||||
if _, err := os.Stat(FishConfigDir()); err != nil {
|
||||
return
|
||||
}
|
||||
p := FishAliasesPath()
|
||||
@@ -76,7 +79,60 @@ func writeFishAliasFile(entries []AliasEntry) {
|
||||
b.WriteString("# netssh aliases — generated automatically, do not edit\n")
|
||||
b.WriteString("# Regenerated on every shell start and after 'netssh cache refresh'.\n")
|
||||
for _, e := range entries {
|
||||
fmt.Fprintf(&b, "alias %s 'netssh %s'\n", e.Name, e.Host)
|
||||
b.WriteString(e.Definition("fish") + "\n")
|
||||
}
|
||||
os.WriteFile(p, []byte(b.String()), 0o644) //nolint:errcheck
|
||||
storage.WriteFile(p, []byte(b.String()), 0o600) //nolint:errcheck
|
||||
}
|
||||
|
||||
// Empty Host represents an alias for the netssh command itself.
|
||||
func (e AliasEntry) Definition(shell string) string {
|
||||
command := "netssh"
|
||||
if e.Host != "" {
|
||||
host := e.Host
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`).MatchString(host) {
|
||||
host = shellQuote(host, shell)
|
||||
}
|
||||
command += " " + host
|
||||
}
|
||||
if shell == "fish" {
|
||||
return fmt.Sprintf("alias %s %s", e.Name, shellQuote(command, shell))
|
||||
}
|
||||
return fmt.Sprintf("alias %s=%s", e.Name, shellQuote(command, shell))
|
||||
}
|
||||
|
||||
func shellQuote(value, shell string) string {
|
||||
if shell == "fish" {
|
||||
value = strings.ReplaceAll(value, `\`, `\\`)
|
||||
return "'" + strings.ReplaceAll(value, "'", `\'`) + "'"
|
||||
}
|
||||
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
// BuildAliasEntries gives the configured command alias priority over host aliases.
|
||||
func BuildAliasEntries(entries []cache.Entry, cfg config.Config) []AliasEntry {
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
|
||||
seen := map[string]bool{"netssh": true}
|
||||
var out []AliasEntry
|
||||
if cfg.Shell.Alias != "" && config.ValidateCommandAlias(cfg.Shell.Alias) == nil {
|
||||
seen[cfg.Shell.Alias] = true
|
||||
out = append(out, AliasEntry{Name: cfg.Shell.Alias})
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := shortcuts.AliasName(e.Name, cfg.Shortcuts)
|
||||
if seen[name] || name == "" || !regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_-]*$`).MatchString(name) {
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
out = append(out, AliasEntry{Name: name, Host: e.Name})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FishConfigDir follows fish's XDG layout on both Linux and macOS.
|
||||
func FishConfigDir() string {
|
||||
dir := os.Getenv("XDG_CONFIG_HOME")
|
||||
if dir == "" {
|
||||
dir = filepath.Join(os.Getenv("HOME"), ".config")
|
||||
}
|
||||
return filepath.Join(dir, "fish")
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/hook"
|
||||
)
|
||||
|
||||
@@ -208,3 +210,20 @@ func TestUninstall_AlsoRemovesAliasesSourceLine(t *testing.T) {
|
||||
t.Error("other content should be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandAliasGenerationAndCollision(t *testing.T) {
|
||||
cfg := config.Config{Shell: config.ShellConfig{Alias: "j"}}
|
||||
entries := hook.BuildAliasEntries([]cache.Entry{{Name: "j"}, {Name: "netssh"}, {Name: "web-01"}}, cfg)
|
||||
if len(entries) != 2 {
|
||||
t.Fatalf("entries: %+v", entries)
|
||||
}
|
||||
if got := entries[0].Definition("bash"); got != "alias j='netssh'" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
if got := entries[0].Definition("fish"); got != "alias j 'netssh'" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
if got := entries[1].Definition("bash"); got != "alias web-01='netssh web-01'" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
}
|
||||
+66
-24
@@ -4,12 +4,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
// Marker is the unique string used to detect an existing hook line.
|
||||
Marker = "netssh shell-init"
|
||||
Marker = "netssh shell-init"
|
||||
BlockStart = "# >>> netssh >>>"
|
||||
BlockEnd = "# <<< netssh <<<"
|
||||
// Line is the full line written into the shell profile.
|
||||
Line = "netssh shell-init # netssh cache auto-refresh"
|
||||
|
||||
@@ -25,13 +30,13 @@ func ProfilePath(shell string) (string, error) {
|
||||
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")
|
||||
dir := os.Getenv("ZDOTDIR")
|
||||
if dir == "" {
|
||||
dir = os.Getenv("HOME")
|
||||
}
|
||||
return filepath.Join(configDir, "fish", "config.fish"), nil
|
||||
return filepath.Join(dir, ".zshrc"), nil
|
||||
case "fish":
|
||||
return filepath.Join(FishConfigDir(), "config.fish"), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported shell %q — supported: bash, zsh, fish", shell)
|
||||
}
|
||||
@@ -49,7 +54,12 @@ func IsInstalled(profilePath string) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(data), Marker)
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.TrimSpace(line) == BlockStart || hookLinePattern.MatchString(strings.TrimSpace(line)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Install appends the hook line to the profile if it is not already present.
|
||||
@@ -81,23 +91,27 @@ func InstallAliasesSource(profilePath string) (installed bool, err error) {
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return false, fmt.Errorf("reading profile: %w", err)
|
||||
}
|
||||
if strings.Contains(string(data), AliasesMarker) {
|
||||
return false, nil
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.TrimSpace(line) == BlockStart || aliasesLinePattern.MatchString(strings.TrimSpace(line)) {
|
||||
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 {
|
||||
if _, err := fmt.Fprintf(f, "%s\n", "source "+shellQuote(AliasesPath(), "bash")+" 2>/dev/null # netssh aliases"); 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.
|
||||
var hookLinePattern = regexp.MustCompile(`^(?:[^\s]*/)?netssh\s+shell-init\s*(?:#.*)?$`)
|
||||
var aliasesLinePattern = regexp.MustCompile(`^(?:source|\.)\s+["']?[^;|&]*netssh/aliases\.sh["']?(?:\s+2>/dev/null)?\s*(?:#.*)?$`)
|
||||
|
||||
// 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) {
|
||||
data, err := os.ReadFile(profilePath)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -106,22 +120,50 @@ func Uninstall(profilePath string) (removed bool, err error) {
|
||||
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) {
|
||||
inside := false
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
value := strings.TrimSpace(line)
|
||||
switch value {
|
||||
case BlockStart:
|
||||
if inside {
|
||||
return false, fmt.Errorf("nested netssh block in %s", profilePath)
|
||||
}
|
||||
inside = true
|
||||
removed = true
|
||||
continue
|
||||
case BlockEnd:
|
||||
if !inside {
|
||||
return false, fmt.Errorf("unmatched netssh block in %s", profilePath)
|
||||
}
|
||||
inside = false
|
||||
continue
|
||||
}
|
||||
if inside {
|
||||
continue
|
||||
}
|
||||
if hookLinePattern.MatchString(value) || aliasesLinePattern.MatchString(value) {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
kept = append(kept, line)
|
||||
}
|
||||
// Collapse triple blank lines that the removal may leave.
|
||||
if inside {
|
||||
return false, fmt.Errorf("unclosed netssh block in %s", profilePath)
|
||||
}
|
||||
if !removed {
|
||||
return false, nil
|
||||
}
|
||||
cleaned := strings.ReplaceAll(strings.Join(kept, "\n"), "\n\n\n", "\n\n")
|
||||
|
||||
if err := os.WriteFile(profilePath, []byte(cleaned), 0o644); err != nil {
|
||||
target, err := filepath.EvalSymlinks(profilePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := storage.WriteFile(target, []byte(cleaned), info.Mode().Perm()); err != nil {
|
||||
return false, fmt.Errorf("writing profile: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
|
||||
@@ -261,3 +261,30 @@ func writeProfile(t *testing.T, content string) string {
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func TestManagedBlockInstallAndUninstall(t *testing.T) {
|
||||
original := "export KEEP=yes\n# netssh shell-init documentation\necho 'netssh shell-init'\n"
|
||||
profile := writeProfile(t, original+hook.BlockStart+"\nexport PATH='/custom/bin':\"$PATH\"\n'/custom/bin/netssh' shell-init\n[ ! -f '/custom/cache/netssh/aliases.sh' ] || . '/custom/cache/netssh/aliases.sh'\n"+hook.BlockEnd+"\n")
|
||||
if installed, err := hook.Install(profile); err != nil || installed {
|
||||
t.Fatalf("duplicate install: %v, %v", installed, err)
|
||||
}
|
||||
if installed, err := hook.InstallAliasesSource(profile); err != nil || installed {
|
||||
t.Fatalf("duplicate source: %v, %v", installed, err)
|
||||
}
|
||||
if removed, err := hook.Uninstall(profile); err != nil || !removed {
|
||||
t.Fatalf("uninstall: %v, %v", removed, err)
|
||||
}
|
||||
data, _ := os.ReadFile(profile)
|
||||
if string(data) != original {
|
||||
t.Fatalf("unrelated content changed: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilePathRespectsZdotdir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("ZDOTDIR", dir)
|
||||
path, err := hook.ProfilePath("zsh")
|
||||
if err != nil || path != filepath.Join(dir, ".zshrc") {
|
||||
t.Fatalf("zsh path: %s, %v", path, err)
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,11 @@ import (
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
|
||||
"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/netbox"
|
||||
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/storage"
|
||||
)
|
||||
|
||||
// RunWizard runs the interactive setup form, pre-filled with any existing cfg values.
|
||||
@@ -30,6 +33,7 @@ func RunWizard(cfg *config.Config) error {
|
||||
cacheTTL := strconv.Itoa(cfg.Cache.TTL)
|
||||
shortcutDomains := strings.Join(cfg.Shortcuts.Domains, ", ")
|
||||
stripHyphens := cfg.Shortcuts.StripHyphens
|
||||
commandAlias := cfg.Shell.Alias
|
||||
|
||||
if strategiesRaw == "" {
|
||||
strategiesRaw = "primary_ip"
|
||||
@@ -122,6 +126,12 @@ func RunWizard(cfg *config.Config) error {
|
||||
Title("Strip hyphens").
|
||||
Description("When enabled, fsn1-web01.example.com can be accessed as fsn1web01.\nOnly works for hosts already in the cache.").
|
||||
Value(&stripHyphens),
|
||||
huh.NewInput().
|
||||
Title("Command alias").
|
||||
Description("Optional alias for netssh itself, e.g. j. Leave empty to disable.").
|
||||
Placeholder("j").
|
||||
Value(&commandAlias).
|
||||
Validate(func(s string) error { return config.ValidateCommandAlias(strings.TrimSpace(s)) }),
|
||||
).Title("Shortcuts"),
|
||||
)
|
||||
|
||||
@@ -165,6 +175,7 @@ func RunWizard(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
out := config.Config{
|
||||
Shell: config.ShellConfig{Alias: strings.TrimSpace(commandAlias)},
|
||||
NetBox: config.NetBoxConfig{
|
||||
URL: strings.TrimRight(strings.TrimSpace(url), "/"),
|
||||
Token: strings.TrimSpace(token),
|
||||
@@ -180,7 +191,8 @@ func RunWizard(cfg *config.Config) error {
|
||||
InterfaceName: strings.TrimSpace(interfaceName),
|
||||
},
|
||||
Cache: config.CacheConfig{
|
||||
TTL: ttl,
|
||||
TTL: ttl,
|
||||
Path: cfg.Cache.Path,
|
||||
},
|
||||
Shortcuts: config.ShortcutsConfig{
|
||||
Domains: domainList,
|
||||
@@ -188,7 +200,38 @@ func RunWizard(cfg *config.Config) error {
|
||||
},
|
||||
}
|
||||
|
||||
return save(out)
|
||||
if err := save(out); err != nil {
|
||||
return err
|
||||
}
|
||||
loaded, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := cache.New(loaded.Cache.Path, loaded.Cache.TTL)
|
||||
if err := c.Load(); err != nil {
|
||||
return fmt.Errorf("updating aliases: %w", err)
|
||||
}
|
||||
if err := hook.WriteAliasFiles(hook.BuildAliasEntries(c.All(), *loaded)); err != nil {
|
||||
return err
|
||||
}
|
||||
if loaded.Shell.Alias != "" {
|
||||
shell := filepath.Base(os.Getenv("SHELL"))
|
||||
profile, err := hook.ProfilePath(shell)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Run 'netssh hook install --shell bash|zsh|fish' to activate your alias.\n")
|
||||
return nil
|
||||
}
|
||||
if _, err := hook.Install(profile); err != nil {
|
||||
return err
|
||||
}
|
||||
if shell != "fish" {
|
||||
if _, err := hook.InstallAliasesSource(profile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, hook.ReloadNote(profile))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var knownStrategies = map[string]bool{
|
||||
@@ -220,6 +263,9 @@ func parseStrategies(s string) []string {
|
||||
}
|
||||
|
||||
func save(cfg config.Config) error {
|
||||
if err := config.ValidateCommandAlias(cfg.Shell.Alias); err != nil {
|
||||
return err
|
||||
}
|
||||
path := config.Path()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return fmt.Errorf("creating config dir: %w", err)
|
||||
@@ -248,6 +294,9 @@ func save(cfg config.Config) error {
|
||||
|
||||
b.WriteString("\ncache:\n")
|
||||
fmt.Fprintf(&b, " ttl: %d\n", cfg.Cache.TTL)
|
||||
if cfg.Cache.Path != "" {
|
||||
fmt.Fprintf(&b, " path: %q\n", cfg.Cache.Path)
|
||||
}
|
||||
|
||||
b.WriteString("\nssh:\n")
|
||||
if cfg.SSH.DefaultUser != "" {
|
||||
@@ -270,7 +319,11 @@ func save(cfg config.Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(b.String()), 0o600); err != nil {
|
||||
if cfg.Shell.Alias != "" {
|
||||
fmt.Fprintf(&b, "\nshell:\n alias: %q\n", cfg.Shell.Alias)
|
||||
}
|
||||
|
||||
if err := storage.WriteFile(path, []byte(b.String()), 0o600); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -366,3 +366,23 @@ func TestSave_RoundtripViaLoad(t *testing.T) {
|
||||
t.Errorf("TTL: got %d, want %d", loaded.Cache.TTL, original.Cache.TTL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveCommandAliasRoundtripAndDisable(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
cfg := config.Config{Shell: config.ShellConfig{Alias: "j"}, Cache: config.CacheConfig{Path: filepath.Join(t.TempDir(), "hosts.json")}}
|
||||
if err := save(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := config.Load()
|
||||
if err != nil || loaded.Shell.Alias != "j" || loaded.Cache.Path != cfg.Cache.Path {
|
||||
t.Fatalf("roundtrip: %+v, %v", loaded, err)
|
||||
}
|
||||
cfg.Shell.Alias = ""
|
||||
if err := save(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err = config.Load()
|
||||
if err != nil || loaded.Shell.Alias != "" {
|
||||
t.Fatalf("disable: %+v, %v", loaded, err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user