package main import ( "bufio" "context" "fmt" "os" "strings" "time" "github.com/google/uuid" "github.com/spf13/cobra" "git.tukangketik.id/swanadiva/hostkeeper/internal/models" "git.tukangketik.id/swanadiva/hostkeeper/pkg/config" "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" ) var ( addHostname string addPort int addUser string addPassword string addKeyPath string addAuthType string addGroup string addTags []string addNotes string ) // addCmd represents the add command var addCmd = &cobra.Command{ Use: "add [name]", Short: "Add a new SSH host", Long: `Add a new SSH host connection to HostKeeper. You can add hosts using flags for quick addition or interactively. Examples: # Add host with password authentication hostkeeper add myserver --host 192.168.1.10 --user admin --password mypass # Add host with key authentication hostkeeper add myserver --host 192.168.1.10 --user admin --key ~/.ssh/id_rsa # Add host with custom port and group hostkeeper add myserver --host 192.168.1.10 --port 2222 --user admin --password mypass --group production # Add host interactively hostkeeper add`, Args: cobra.MaximumNArgs(1), RunE: runAddHost, } func init() { // Flags for add command addCmd.Flags().StringVar(&addHostname, "host", "", "hostname or IP address") addCmd.Flags().IntVar(&addPort, "port", 0, "SSH port (default: 22)") addCmd.Flags().StringVar(&addUser, "user", "", "SSH username") addCmd.Flags().StringVar(&addPassword, "password", "", "SSH password") addCmd.Flags().StringVar(&addKeyPath, "key", "", "path to SSH private key") addCmd.Flags().StringVar(&addAuthType, "auth-type", "", "authentication type: password, key, or both") addCmd.Flags().StringVar(&addGroup, "group", "", "host group for categorization") addCmd.Flags().StringSliceVar(&addTags, "tags", nil, "tags for the host (comma-separated)") addCmd.Flags().StringVar(&addNotes, "notes", "", "notes about this host") rootCmd.AddCommand(addCmd) } func runAddHost(cmd *cobra.Command, args []string) error { cfg := appCfg if cfg == nil { var err error cfg, err = config.New() if err != nil { return fmt.Errorf("failed to initialize config: %w", err) } } // Determine host name var name string if len(args) > 0 { name = args[0] } // Check if we should use interactive mode interactive := name == "" && addHostname == "" if interactive { return addHostInteractive(cfg) } // Validate required fields if name == "" { return fmt.Errorf("host name is required (provide as argument or use interactive mode)") } if addHostname == "" { return fmt.Errorf("hostname is required (--host flag)") } if addUser == "" { return fmt.Errorf("username is required (--user flag)") } // Set default port from config port := addPort if port == 0 { port = cfg.GetAppConfig().DefaultPort } // Determine auth type authType := addAuthType if authType == "" { if addKeyPath != "" && addPassword != "" { authType = "both" } else if addKeyPath != "" { authType = "key" } else if addPassword != "" { authType = "password" } else { return fmt.Errorf("authentication is required: provide --password, --key, or both") } } // Read key if provided keyContent := "" if addKeyPath != "" { data, err := os.ReadFile(addKeyPath) if err != nil { return fmt.Errorf("failed to read key file: %w", err) } keyContent = string(data) } // Create host host := &models.Host{ ID: uuid.New().String(), Name: name, Hostname: addHostname, Port: port, Username: addUser, Auth: models.AuthConfig{ Type: authType, Password: addPassword, }, Group: addGroup, Tags: addTags, Notes: addNotes, CreatedAt: time.Now(), UpdatedAt: time.Now(), } // Store key content in password field if key-only auth (as reference) // In a full implementation, this would store the key securely if keyContent != "" { host.Auth.Password = keyContent // Will be moved to secure storage } // Initialize storage store, err := storage.NewJSONStorage(cfg.GetDataDir()) if err != nil { return fmt.Errorf("failed to initialize storage: %w", err) } // Save host ctx := context.Background() if err := store.SaveHost(ctx, host); err != nil { return fmt.Errorf("failed to save host: %w", err) } fmt.Printf("✓ Host '%s' added successfully\n", name) fmt.Printf(" Hostname: %s:%d\n", addHostname, port) fmt.Printf(" User: %s\n", addUser) fmt.Printf(" Auth: %s\n", authType) return nil } func addHostInteractive(cfg *config.Config) error { input := bufio.NewReader(os.Stdin) fmt.Println("╔══════════════════════════════════════╗") fmt.Println("║ Add New SSH Host ║") fmt.Println("╚══════════════════════════════════════╝") fmt.Println() readLine := func(prompt string) string { fmt.Print(prompt) line, _ := input.ReadString('\n') return strings.TrimRight(line, "\n\r") } // Get host name name := readLine("Host Name (e.g., myserver): ") if name == "" { return fmt.Errorf("host name is required") } // Get hostname hostname := readLine("Hostname or IP (e.g., 192.168.1.10): ") if hostname == "" { return fmt.Errorf("hostname is required") } // Get port defaultPort := cfg.GetAppConfig().DefaultPort portInput := readLine(fmt.Sprintf("Port [%d]: ", defaultPort)) port := defaultPort if portInput != "" { fmt.Sscanf(portInput, "%d", &port) } // Get username username := readLine("Username: ") if username == "" { return fmt.Errorf("username is required") } // Get auth type authType := readLine("Auth Type (password/key/both) [password]: ") if authType == "" { authType = "password" } // Get password var password string if authType == "password" || authType == "both" { password = readLine("Password: ") } // Get key path var keyContent string if authType == "key" || authType == "both" { keyPath := readLine("Path to private key (~/.ssh/id_rsa): ") if keyPath != "" { data, err := os.ReadFile(keyPath) if err != nil { return fmt.Errorf("failed to read key file: %w", err) } keyContent = string(data) } } // Get group group := readLine("Group (optional): ") // Get tags tagsInput := readLine("Tags (comma-separated, optional): ") var tags []string if tagsInput != "" { tags = strings.Split(tagsInput, ",") for i, t := range tags { tags[i] = strings.TrimSpace(t) } } // Get notes notes := readLine("Notes (optional): ") // Create host host := &models.Host{ ID: uuid.New().String(), Name: name, Hostname: hostname, Port: port, Username: username, Auth: models.AuthConfig{ Type: authType, Password: password, }, Group: group, Tags: tags, Notes: notes, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if keyContent != "" { host.Auth.Password = keyContent } // Initialize storage store, err := storage.NewJSONStorage(cfg.GetDataDir()) if err != nil { return fmt.Errorf("failed to initialize storage: %w", err) } // Save host ctx := context.Background() if err := store.SaveHost(ctx, host); err != nil { return fmt.Errorf("failed to save host: %w", err) } fmt.Println() fmt.Printf("✓ Host '%s' added successfully!\n", name) return nil }