feat: implement edit and delete host commands
- Add edit command with flag-based and interactive update modes - Add delete command with confirmation prompt and --force flag - Include delete alias 'rm' for convenience - Add comprehensive tests for both commands - Update project state documentation
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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 (
|
||||
editHostname string
|
||||
editPort int
|
||||
editUser string
|
||||
editPassword string
|
||||
editKeyPath string
|
||||
editAuthType string
|
||||
editGroup string
|
||||
editTags []string
|
||||
editNotes string
|
||||
editName string
|
||||
)
|
||||
|
||||
// editCmd represents the edit command
|
||||
var editCmd = &cobra.Command{
|
||||
Use: "edit <host-name-or-id>",
|
||||
Short: "Edit a saved SSH host",
|
||||
Long: `Edit an existing SSH host configuration in HostKeeper.
|
||||
|
||||
You can update fields using flags or interactively.
|
||||
|
||||
Examples:
|
||||
# Edit hostname and port
|
||||
hostkeeper edit myserver --host 10.0.0.1 --port 2222
|
||||
|
||||
# Change username and auth
|
||||
hostkeeper edit myserver --user root --auth-type key
|
||||
|
||||
# Edit interactively
|
||||
hostkeeper edit myserver`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runEditHost,
|
||||
}
|
||||
|
||||
func init() {
|
||||
editCmd.Flags().StringVar(&editHostname, "host", "", "new hostname or IP address")
|
||||
editCmd.Flags().IntVar(&editPort, "port", 0, "new SSH port")
|
||||
editCmd.Flags().StringVar(&editUser, "user", "", "new SSH username")
|
||||
editCmd.Flags().StringVar(&editPassword, "password", "", "new SSH password")
|
||||
editCmd.Flags().StringVar(&editKeyPath, "key", "", "new path to SSH private key")
|
||||
editCmd.Flags().StringVar(&editAuthType, "auth-type", "", "new authentication type: password, key, or both")
|
||||
editCmd.Flags().StringVar(&editGroup, "group", "", "new host group")
|
||||
editCmd.Flags().StringSliceVar(&editTags, "tags", nil, "new tags (comma-separated)")
|
||||
editCmd.Flags().StringVar(&editNotes, "notes", "", "new notes")
|
||||
editCmd.Flags().StringVar(&editName, "name", "", "new host name")
|
||||
|
||||
rootCmd.AddCommand(editCmd)
|
||||
}
|
||||
|
||||
func runEditHost(cmd *cobra.Command, args []string) error {
|
||||
hostIdentifier := args[0]
|
||||
|
||||
cfg := appCfg
|
||||
if cfg == nil {
|
||||
var err error
|
||||
cfg, err = config.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
host, err := findHost(ctx, store, hostIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if any flags were provided
|
||||
flagProvided := cmd.Flags().Changed("host") || cmd.Flags().Changed("port") ||
|
||||
cmd.Flags().Changed("user") || cmd.Flags().Changed("password") ||
|
||||
cmd.Flags().Changed("key") || cmd.Flags().Changed("auth-type") ||
|
||||
cmd.Flags().Changed("group") || cmd.Flags().Changed("tags") ||
|
||||
cmd.Flags().Changed("notes") || cmd.Flags().Changed("name")
|
||||
|
||||
if !flagProvided {
|
||||
return editHostInteractive(cfg, store, host)
|
||||
}
|
||||
|
||||
// Apply flag-based updates
|
||||
if cmd.Flags().Changed("host") {
|
||||
host.Hostname = editHostname
|
||||
}
|
||||
if cmd.Flags().Changed("port") {
|
||||
if editPort > 0 {
|
||||
host.Port = editPort
|
||||
} else {
|
||||
host.Port = cfg.GetAppConfig().DefaultPort
|
||||
}
|
||||
}
|
||||
if cmd.Flags().Changed("user") {
|
||||
host.Username = editUser
|
||||
}
|
||||
if cmd.Flags().Changed("password") {
|
||||
host.Auth.Password = editPassword
|
||||
}
|
||||
if cmd.Flags().Changed("auth-type") {
|
||||
host.Auth.Type = editAuthType
|
||||
}
|
||||
if cmd.Flags().Changed("key") {
|
||||
data, err := os.ReadFile(editKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read key file: %w", err)
|
||||
}
|
||||
host.Auth.Password = string(data)
|
||||
}
|
||||
if cmd.Flags().Changed("group") {
|
||||
host.Group = editGroup
|
||||
}
|
||||
if cmd.Flags().Changed("tags") {
|
||||
host.Tags = editTags
|
||||
}
|
||||
if cmd.Flags().Changed("notes") {
|
||||
host.Notes = editNotes
|
||||
}
|
||||
if cmd.Flags().Changed("name") {
|
||||
host.Name = editName
|
||||
}
|
||||
|
||||
host.UpdatedAt = time.Now()
|
||||
|
||||
if err := store.SaveHost(ctx, host); err != nil {
|
||||
return fmt.Errorf("failed to update host: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Host '%s' updated successfully\n", host.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func editHostInteractive(cfg *config.Config, store *storage.JSONStorage, host *models.Host) error {
|
||||
fmt.Println("╔══════════════════════════════════════╗")
|
||||
fmt.Println("║ Edit SSH Host ║")
|
||||
fmt.Println("╚══════════════════════════════════════╝")
|
||||
fmt.Println()
|
||||
fmt.Println("Press Enter to keep the current value.")
|
||||
fmt.Println()
|
||||
|
||||
// Name
|
||||
fmt.Printf("Host Name [%s]: ", host.Name)
|
||||
var name string
|
||||
fmt.Scanln(&name)
|
||||
if name != "" {
|
||||
host.Name = name
|
||||
}
|
||||
|
||||
// Hostname
|
||||
fmt.Printf("Hostname or IP [%s]: ", host.Hostname)
|
||||
var hostname string
|
||||
fmt.Scanln(&hostname)
|
||||
if hostname != "" {
|
||||
host.Hostname = hostname
|
||||
}
|
||||
|
||||
// Port
|
||||
defaultPort := cfg.GetAppConfig().DefaultPort
|
||||
fmt.Printf("Port [%d]: ", host.Port)
|
||||
var portInput string
|
||||
fmt.Scanln(&portInput)
|
||||
if portInput != "" {
|
||||
fmt.Sscanf(portInput, "%d", &host.Port)
|
||||
} else if host.Port == 0 {
|
||||
host.Port = defaultPort
|
||||
}
|
||||
|
||||
// Username
|
||||
fmt.Printf("Username [%s]: ", host.Username)
|
||||
var username string
|
||||
fmt.Scanln(&username)
|
||||
if username != "" {
|
||||
host.Username = username
|
||||
}
|
||||
|
||||
// Auth type
|
||||
currentAuth := host.Auth.Type
|
||||
if currentAuth == "" {
|
||||
currentAuth = "password"
|
||||
}
|
||||
fmt.Printf("Auth Type (password/key/both) [%s]: ", currentAuth)
|
||||
var authType string
|
||||
fmt.Scanln(&authType)
|
||||
if authType != "" {
|
||||
host.Auth.Type = authType
|
||||
} else {
|
||||
host.Auth.Type = currentAuth
|
||||
}
|
||||
|
||||
// Password
|
||||
if host.Auth.Type == "password" || host.Auth.Type == "both" {
|
||||
prompt := "Password"
|
||||
if host.Auth.Password != "" {
|
||||
prompt += " [********]"
|
||||
}
|
||||
fmt.Printf("%s: ", prompt)
|
||||
var password string
|
||||
fmt.Scanln(&password)
|
||||
if password != "" {
|
||||
host.Auth.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
// Key path
|
||||
if host.Auth.Type == "key" || host.Auth.Type == "both" {
|
||||
fmt.Printf("Path to private key: ")
|
||||
var keyPath string
|
||||
fmt.Scanln(&keyPath)
|
||||
if keyPath != "" {
|
||||
data, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read key file: %w", err)
|
||||
}
|
||||
host.Auth.Password = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
// Group
|
||||
fmt.Printf("Group [%s]: ", host.Group)
|
||||
var group string
|
||||
fmt.Scanln(&group)
|
||||
if group != "" {
|
||||
host.Group = group
|
||||
}
|
||||
|
||||
// Tags
|
||||
currentTags := strings.Join(host.Tags, ",")
|
||||
fmt.Printf("Tags (comma-separated) [%s]: ", currentTags)
|
||||
var tagsInput string
|
||||
fmt.Scanln(&tagsInput)
|
||||
if tagsInput != "" {
|
||||
tags := strings.Split(tagsInput, ",")
|
||||
for i, t := range tags {
|
||||
tags[i] = strings.TrimSpace(t)
|
||||
}
|
||||
host.Tags = tags
|
||||
}
|
||||
|
||||
// Notes
|
||||
fmt.Printf("Notes [%s]: ", host.Notes)
|
||||
var notes string
|
||||
fmt.Scanln(¬es)
|
||||
if notes != "" {
|
||||
host.Notes = notes
|
||||
}
|
||||
|
||||
host.UpdatedAt = time.Now()
|
||||
|
||||
ctx := context.Background()
|
||||
if err := store.SaveHost(ctx, host); err != nil {
|
||||
return fmt.Errorf("failed to update host: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("✓ Host '%s' updated successfully!\n", host.Name)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user