Files
HostKeeper/v1/cmd/hostkeeper/add.go
T
swanadiva 847989df75 refactor: move V1 code into v1/ subdirectory
- git mv cmd/ internal/ pkg/ test/ go.mod go.sum Makefile build.sh docs/ v1/
- Create v1/README.md with V1 documentation
- Update root README for V1 + V2 structure
- V1 still builds (cd v1 && go build ./cmd/hostkeeper) and 105 tests pass
- Root is now clean for V2 development
2026-07-07 11:56:27 +07:00

295 lines
7.2 KiB
Go

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"
)
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 := newStorage(cfg)
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 := newStorage(cfg)
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
}