feat: interactive setup wizard for first-run and netssh configure
Release / release (push) Successful in 1m36s

- Auto-detects missing config (netbox.url empty) and launches wizard
- `netssh configure` re-runs the wizard anytime to change settings
- 3-page huh form: NetBox connection, SSH defaults, resolver & cache
- Saves to ~/.config/netssh.yaml (permissions 0600)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sebastian Unterschütz
2026-05-23 13:04:55 +02:00
parent 11f5319eae
commit ff9c61c087
5 changed files with 235 additions and 7 deletions
+36 -7
View File
@@ -16,12 +16,14 @@ import (
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/resolver"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/setup"
internalssh "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/ssh"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/tui"
)
// managedSubcommands are dispatched to cobra; everything else is treated as SSH wrapper mode.
var managedSubcommands = map[string]bool{
"configure": true,
"search": true,
"cache": true,
"completion": true,
@@ -47,12 +49,28 @@ func main() {
runSSHWrapper(args)
}
// runSSHWrapper resolves the target hostname via NetBox and execs the native ssh binary.
func runSSHWrapper(args []string) {
// loadConfigOrSetup loads the config and runs the setup wizard if NetBox is not configured.
func loadConfigOrSetup() *config.Config {
cfg, err := config.Load()
if err != nil {
fatalf("config: %v", err)
}
if cfg.NetBox.URL == "" {
fmt.Fprintln(os.Stderr, "No configuration found. Starting setup…")
if err := setup.RunWizard(cfg); err != nil {
fatalf("setup: %v", err)
}
cfg, err = config.Load()
if err != nil {
fatalf("config: %v", err)
}
}
return cfg
}
// runSSHWrapper resolves the target hostname via NetBox and execs the native ssh binary.
func runSSHWrapper(args []string) {
cfg := loadConfigOrSetup()
parsed := internalssh.Parse(args)
if parsed == nil {
@@ -127,10 +145,7 @@ func connect(ip string, parsed *internalssh.ParsedArgs, originalArgs []string) {
// runTUI starts the interactive host search.
func runTUI() {
cfg, err := config.Load()
if err != nil {
fatalf("config: %v", err)
}
cfg := loadConfigOrSetup()
c := cache.New(cfg.Cache.Path, cfg.Cache.TTL)
_ = c.Load()
@@ -193,10 +208,24 @@ func rootCmd() *cobra.Command {
}
// cobra automatically adds a "completion" subcommand
root.AddCommand(searchCmd(), cacheCmd())
root.AddCommand(configureCmd(), searchCmd(), cacheCmd())
return root
}
func configureCmd() *cobra.Command {
return &cobra.Command{
Use: "configure",
Short: "Interactively configure netssh",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _ := config.Load()
if cfg == nil {
cfg = &config.Config{}
}
return setup.RunWizard(cfg)
},
}
}
func searchCmd() *cobra.Command {
return &cobra.Command{
Use: "search <query>",