package setup import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" "github.com/charmbracelet/huh" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" ) // RunWizard runs the interactive setup form, pre-filled with any existing cfg values. // It saves the result to the config file on success. func RunWizard(cfg *config.Config) error { url := cfg.NetBox.URL token := cfg.NetBox.Token defaultUser := cfg.SSH.DefaultUser strategies := cfg.Resolver.Strategies subnets := strings.Join(cfg.Resolver.ManagementSubnets, ", ") interfaceName := cfg.Resolver.InterfaceName cacheTTL := strconv.Itoa(cfg.Cache.TTL) if len(strategies) == 0 { strategies = []string{"primary_ip"} } if cacheTTL == "0" { cacheTTL = "3600" } form := huh.NewForm( huh.NewGroup( huh.NewInput(). Title("NetBox URL"). Description("e.g. https://netbox.example.com"). Placeholder("https://"). Value(&url). Validate(func(s string) error { if strings.TrimSpace(s) == "" { return errors.New("required") } return nil }), huh.NewInput(). Title("NetBox API token"). EchoMode(huh.EchoModePassword). Value(&token). Validate(func(s string) error { if strings.TrimSpace(s) == "" { return errors.New("required") } return nil }), ).Title("NetBox connection"), huh.NewGroup( huh.NewInput(). Title("Default SSH user"). Description("Leave empty to use your system user ($USER)."). Value(&defaultUser), ).Title("SSH defaults"), huh.NewGroup( huh.NewMultiSelect[string](). Title("Resolver strategies"). Description("Order matters: first match wins."). Options( huh.NewOption("primary_ip — NetBox primary IPv4/IPv6", "primary_ip"), huh.NewOption("management_subnet — first IP inside a subnet", "management_subnet"), huh.NewOption("interface_name — IP on a named interface", "interface_name"), ). Value(&strategies), huh.NewInput(). Title("Management subnets"). Description("Comma-separated CIDRs, e.g. 10.0.0.0/8, 192.168.0.0/16\nOnly used when management_subnet strategy is active."). Value(&subnets), huh.NewInput(). Title("Interface name"). Description("Only used when interface_name strategy is active."). Placeholder("eth0"). Value(&interfaceName), huh.NewInput(). Title("Cache TTL (seconds)"). Value(&cacheTTL). Validate(func(s string) error { if _, err := strconv.Atoi(s); err != nil { return errors.New("must be a number") } return nil }), ).Title("Resolver & cache"), ) if err := form.Run(); err != nil { if errors.Is(err, huh.ErrUserAborted) { fmt.Fprintln(os.Stderr, "Setup cancelled.") os.Exit(0) } return err } ttl, _ := strconv.Atoi(cacheTTL) var subnetList []string for _, s := range strings.Split(subnets, ",") { if s = strings.TrimSpace(s); s != "" { subnetList = append(subnetList, s) } } out := config.Config{ NetBox: config.NetBoxConfig{ URL: strings.TrimRight(strings.TrimSpace(url), "/"), Token: strings.TrimSpace(token), }, SSH: config.SSHConfig{ DefaultUser: strings.TrimSpace(defaultUser), }, Resolver: config.ResolverConfig{ Strategies: strategies, ManagementSubnets: subnetList, InterfaceName: strings.TrimSpace(interfaceName), }, Cache: config.CacheConfig{ TTL: ttl, }, } return save(out) } func save(cfg config.Config) error { path := config.Path() if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return fmt.Errorf("creating config dir: %w", err) } var b strings.Builder b.WriteString("netbox:\n") b.WriteString(fmt.Sprintf(" url: %q\n", cfg.NetBox.URL)) b.WriteString(fmt.Sprintf(" token: %q\n", cfg.NetBox.Token)) b.WriteString("\nresolver:\n") b.WriteString(" strategies:\n") for _, s := range cfg.Resolver.Strategies { fmt.Fprintf(&b, " - %s\n", s) } if len(cfg.Resolver.ManagementSubnets) > 0 { b.WriteString(" management_subnets:\n") for _, s := range cfg.Resolver.ManagementSubnets { fmt.Fprintf(&b, " - %s\n", s) } } if cfg.Resolver.InterfaceName != "" { fmt.Fprintf(&b, " interface_name: %q\n", cfg.Resolver.InterfaceName) } b.WriteString("\ncache:\n") fmt.Fprintf(&b, " ttl: %d\n", cfg.Cache.TTL) b.WriteString("\nssh:\n") if cfg.SSH.DefaultUser != "" { fmt.Fprintf(&b, " default_user: %q\n", cfg.SSH.DefaultUser) } if err := os.WriteFile(path, []byte(b.String()), 0o600); err != nil { return fmt.Errorf("writing config: %w", err) } fmt.Printf("\nConfig saved → %s\n", path) return nil }