feat: add alias file generation and management for shell hooks
Release / release (push) Successful in 49s
Release / release (push) Successful in 49s
- **Aliases**: Generate shell alias files (`aliases.sh` for bash/zsh, `netssh.fish` for fish) from cached hosts. Regenerate on each shell startup and cache refresh to keep aliases updated. - **Hooks**: Extend shell hook functionality to include alias file support. Install and uninstall commands updated for bash, zsh, and fish. - **Tests**: Add unit tests to verify alias file generation, path resolution, and idempotent hook installation. - **Docs**: Update README with instructions for alias file usage, installation, and relation to hooks.
This commit is contained in:
+75
-38
@@ -521,6 +521,11 @@ func cacheRefreshCmd() *cobra.Command {
|
||||
if err := c.SetRefreshed(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: could not update refresh timestamp: %v\n", err)
|
||||
}
|
||||
if aliasEntries := buildAliasEntries(c, *cfg); len(aliasEntries) > 0 {
|
||||
if err := hook.WriteAliasFiles(aliasEntries); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: could not write alias files: %v\n", err)
|
||||
}
|
||||
}
|
||||
fmt.Printf("%d entries written to cache.\n", len(entries))
|
||||
return nil
|
||||
},
|
||||
@@ -530,6 +535,27 @@ func cacheRefreshCmd() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// buildAliasEntries computes the sorted, deduplicated alias list from the cache.
|
||||
func buildAliasEntries(c *cache.Cache, cfg config.Config) []hook.AliasEntry {
|
||||
all := c.All()
|
||||
sort.Slice(all, func(i, j int) bool { return all[i].Name < all[j].Name })
|
||||
|
||||
seen := make(map[string]string)
|
||||
var out []hook.AliasEntry
|
||||
for _, e := range all {
|
||||
name := shortcuts.AliasName(e.Name, cfg.Shortcuts)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[name]; dup {
|
||||
continue
|
||||
}
|
||||
seen[name] = e.Name
|
||||
out = append(out, hook.AliasEntry{Name: name, Host: e.Name})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func aliasCmd() *cobra.Command {
|
||||
var shell string
|
||||
cmd := &cobra.Command{
|
||||
@@ -558,46 +584,23 @@ Or use in a script:
|
||||
shell = filepath.Base(os.Getenv("SHELL"))
|
||||
}
|
||||
|
||||
entries := c.All()
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].Name < entries[j].Name
|
||||
})
|
||||
|
||||
// Deduplicate: first host wins when two normalize to the same alias.
|
||||
seen := make(map[string]string) // alias name → canonical name
|
||||
var lines []string
|
||||
for _, e := range entries {
|
||||
aliasName := shortcuts.AliasName(e.Name, cfg.Shortcuts)
|
||||
if aliasName == "" {
|
||||
continue
|
||||
}
|
||||
if prev, exists := seen[aliasName]; exists {
|
||||
fmt.Fprintf(os.Stderr, "netssh: alias %q conflict: %s and %s — skipping %s\n", aliasName, prev, e.Name, e.Name)
|
||||
continue
|
||||
}
|
||||
seen[aliasName] = e.Name
|
||||
|
||||
switch shell {
|
||||
case "fish":
|
||||
lines = append(lines, fmt.Sprintf("alias %s 'netssh %s'", aliasName, e.Name))
|
||||
default:
|
||||
lines = append(lines, fmt.Sprintf("alias %s='netssh %s'", aliasName, e.Name))
|
||||
}
|
||||
}
|
||||
|
||||
if len(lines) == 0 {
|
||||
entries := buildAliasEntries(c, *cfg)
|
||||
if len(entries) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "netssh: cache is empty — run 'netssh cache refresh' first")
|
||||
return nil
|
||||
}
|
||||
|
||||
switch shell {
|
||||
case "fish":
|
||||
fmt.Printf("# netssh aliases (%d hosts) — source with: netssh alias --shell fish | source\n", len(lines))
|
||||
fmt.Printf("# netssh aliases (%d hosts) — source with: netssh alias --shell fish | source\n", len(entries))
|
||||
for _, e := range entries {
|
||||
fmt.Printf("alias %s 'netssh %s'\n", e.Name, e.Host)
|
||||
}
|
||||
default:
|
||||
fmt.Printf("# netssh aliases (%d hosts) — source with: eval \"$(netssh alias)\"\n", len(lines))
|
||||
}
|
||||
for _, l := range lines {
|
||||
fmt.Println(l)
|
||||
fmt.Printf("# netssh aliases (%d hosts) — source with: eval \"$(netssh alias)\"\n", len(entries))
|
||||
for _, e := range entries {
|
||||
fmt.Printf("alias %s='netssh %s'\n", e.Name, e.Host)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -606,12 +609,12 @@ Or use in a script:
|
||||
return cmd
|
||||
}
|
||||
|
||||
// shellInitCmd is called at shell startup to trigger a background cache refresh when stale.
|
||||
// It is intentionally silent — no output on success so it never disrupts shell startup.
|
||||
// shellInitCmd is called at shell startup to update alias files and trigger a
|
||||
// background cache refresh when stale. Intentionally silent — never disrupts the prompt.
|
||||
func shellInitCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "shell-init",
|
||||
Short: "Trigger a background cache refresh if stale (add to shell profile via 'hook install')",
|
||||
Short: "Regenerate alias files and trigger a background cache refresh if stale",
|
||||
Hidden: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
@@ -619,6 +622,14 @@ func shellInitCmd() *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
_ = c.Load()
|
||||
|
||||
// Regenerate alias files from the current cache every shell start
|
||||
// so aliases are always up-to-date even before a refresh completes.
|
||||
if entries := buildAliasEntries(c, *cfg); len(entries) > 0 {
|
||||
_ = hook.WriteAliasFiles(entries)
|
||||
}
|
||||
|
||||
maybeBackgroundRefresh(cfg, c)
|
||||
return nil
|
||||
},
|
||||
@@ -652,15 +663,41 @@ After installation, reload your profile or open a new shell.`,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installed, err := hook.Install(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// For bash/zsh: also add the source line so aliases are loaded on startup.
|
||||
// Fish uses conf.d auto-sourcing — no explicit source line needed.
|
||||
if shell != "fish" {
|
||||
if _, err := hook.InstallAliasesSource(profile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !installed {
|
||||
fmt.Printf("Hook already installed in %s\n", profile)
|
||||
return nil
|
||||
} else {
|
||||
fmt.Printf("Hook installed → %s\n%s\n", profile, hook.ReloadNote(profile))
|
||||
}
|
||||
|
||||
// Generate the aliases file immediately so it is ready on the next shell start.
|
||||
cfg, cfgErr := config.Load()
|
||||
if cfgErr == nil && cfg.NetBox.URL != "" {
|
||||
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
|
||||
if _ = c.Load(); true {
|
||||
if entries := buildAliasEntries(c, *cfg); len(entries) > 0 {
|
||||
if err := hook.WriteAliasFiles(entries); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: could not write alias files: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("%d aliases written to %s\n", len(entries), hook.AliasesPath())
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Cache is empty — run 'netssh cache refresh' to populate aliases.")
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("Hook installed → %s\n%s\n", profile, hook.ReloadNote(profile))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user