From 33201227ca5aebd8a266c1dd079106f1b97d233f Mon Sep 17 00:00:00 2001 From: ryyst Date: Sun, 5 Oct 2025 23:50:27 +0300 Subject: [PATCH] feat: add group management commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented roadmap #5: Group management commands with full CRUD. Features: - group create [members] - Create new group with optional initial members - group get - Display group details (uuid, name_hash, members, timestamps) - group update - Replace entire member list - group delete - Delete group - group add-member - Add single member (incremental) - group remove-member - Remove single member (incremental) The add-member and remove-member commands fetch current members, modify the list, and update atomically - providing a better UX than replacing the entire list. Backend API endpoints used: - POST /api/groups - Create group - GET /api/groups/{uuid} - Get group details - PUT /api/groups/{uuid} - Update members - DELETE /api/groups/{uuid} - Delete group 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cmd_group.go | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++ cmd_system.go | 8 ++ main.go | 2 + utils.go | 8 ++ 4 files changed, 329 insertions(+) create mode 100644 cmd_group.go diff --git a/cmd_group.go b/cmd_group.go new file mode 100644 index 0000000..73fc0ef --- /dev/null +++ b/cmd_group.go @@ -0,0 +1,311 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" +) + +// handleGroup dispatches group management sub-commands +func (c *KVSClient) handleGroup(args []string) { + if len(args) == 0 { + fmt.Println(red("Usage: group [options]")) + return + } + + subCmd := args[0] + switch subCmd { + case "create": + c.handleGroupCreate(args[1:]) + case "get": + c.handleGroupGet(args[1:]) + case "update": + c.handleGroupUpdate(args[1:]) + case "delete": + c.handleGroupDelete(args[1:]) + case "add-member": + c.handleGroupAddMember(args[1:]) + case "remove-member": + c.handleGroupRemoveMember(args[1:]) + default: + fmt.Println(red("Unknown group command:"), subCmd) + fmt.Println("Available commands: create, get, update, delete, add-member, remove-member") + } +} + +// handleGroupCreate creates a new group +func (c *KVSClient) handleGroupCreate(args []string) { + if len(args) < 1 { + fmt.Println(red("Usage: group create [member-uuid1,member-uuid2,...]")) + return + } + + groupname := args[0] + var members []string + + // Parse optional members list + if len(args) >= 2 { + members = strings.Split(args[1], ",") + } + + payload := map[string]interface{}{ + "groupname": groupname, + } + if len(members) > 0 { + payload["members"] = members + } + + respBody, status, err := c.doRequest("POST", "/api/groups", payload) + if err != nil { + fmt.Println(red("Error:"), err) + return + } + + if status != http.StatusOK { + fmt.Printf(red("Error creating group (status %d):\n"), status) + fmt.Println(string(respBody)) + return + } + + var response struct { + UUID string `json:"uuid"` + } + if err := json.Unmarshal(respBody, &response); err != nil { + fmt.Println(red("Error parsing response:"), err) + return + } + + fmt.Println(green("Group created successfully")) + fmt.Println(cyan("UUID:"), response.UUID) +} + +// handleGroupGet retrieves group details +func (c *KVSClient) handleGroupGet(args []string) { + if len(args) < 1 { + fmt.Println(red("Usage: group get ")) + return + } + + groupUUID := args[0] + + respBody, status, err := c.doRequest("GET", "/api/groups/"+groupUUID, nil) + if err != nil { + fmt.Println(red("Error:"), err) + return + } + + if status == http.StatusNotFound { + fmt.Println(yellow("Group not found")) + return + } + + if status != http.StatusOK { + fmt.Printf(red("Error getting group (status %d):\n"), status) + fmt.Println(string(respBody)) + return + } + + var group Group + if err := json.Unmarshal(respBody, &group); err != nil { + fmt.Println(red("Error parsing response:"), err) + return + } + + fmt.Println(cyan("Group Details:")) + fmt.Printf(" UUID: %s\n", group.UUID) + fmt.Printf(" Name Hash: %s\n", group.NameHash) + fmt.Printf(" Members: %v\n", group.Members) + fmt.Printf(" Created: %s\n", time.Unix(group.CreatedAt, 0).Format(time.RFC3339)) + fmt.Printf(" Updated: %s\n", time.Unix(group.UpdatedAt, 0).Format(time.RFC3339)) +} + +// handleGroupUpdate replaces all members in a group +func (c *KVSClient) handleGroupUpdate(args []string) { + if len(args) < 2 { + fmt.Println(red("Usage: group update ")) + fmt.Println("Note: This replaces ALL members. Use add-member/remove-member for incremental changes.") + return + } + + groupUUID := args[0] + members := strings.Split(args[1], ",") + + payload := map[string]interface{}{ + "members": members, + } + + respBody, status, err := c.doRequest("PUT", "/api/groups/"+groupUUID, payload) + if err != nil { + fmt.Println(red("Error:"), err) + return + } + + if status == http.StatusNotFound { + fmt.Println(yellow("Group not found")) + return + } + + if status != http.StatusOK { + fmt.Printf(red("Error updating group (status %d):\n"), status) + fmt.Println(string(respBody)) + return + } + + fmt.Println(green("Group updated successfully")) +} + +// handleGroupDelete deletes a group +func (c *KVSClient) handleGroupDelete(args []string) { + if len(args) < 1 { + fmt.Println(red("Usage: group delete ")) + return + } + + groupUUID := args[0] + + respBody, status, err := c.doRequest("DELETE", "/api/groups/"+groupUUID, nil) + if err != nil { + fmt.Println(red("Error:"), err) + return + } + + if status == http.StatusNotFound { + fmt.Println(yellow("Group not found")) + return + } + + if status != http.StatusOK { + fmt.Printf(red("Error deleting group (status %d):\n"), status) + fmt.Println(string(respBody)) + return + } + + fmt.Println(green("Group deleted successfully")) +} + +// handleGroupAddMember adds a member to an existing group +func (c *KVSClient) handleGroupAddMember(args []string) { + if len(args) < 2 { + fmt.Println(red("Usage: group add-member ")) + return + } + + groupUUID := args[0] + userUUID := args[1] + + // First, get current members + respBody, status, err := c.doRequest("GET", "/api/groups/"+groupUUID, nil) + if err != nil { + fmt.Println(red("Error:"), err) + return + } + + if status != http.StatusOK { + fmt.Printf(red("Error getting group (status %d):\n"), status) + fmt.Println(string(respBody)) + return + } + + var group Group + if err := json.Unmarshal(respBody, &group); err != nil { + fmt.Println(red("Error parsing response:"), err) + return + } + + // Check if user is already a member + for _, member := range group.Members { + if member == userUUID { + fmt.Println(yellow("User is already a member of this group")) + return + } + } + + // Add new member + group.Members = append(group.Members, userUUID) + + // Update group with new members list + payload := map[string]interface{}{ + "members": group.Members, + } + + respBody, status, err = c.doRequest("PUT", "/api/groups/"+groupUUID, payload) + if err != nil { + fmt.Println(red("Error:"), err) + return + } + + if status != http.StatusOK { + fmt.Printf(red("Error updating group (status %d):\n"), status) + fmt.Println(string(respBody)) + return + } + + fmt.Println(green("Member added successfully")) +} + +// handleGroupRemoveMember removes a member from an existing group +func (c *KVSClient) handleGroupRemoveMember(args []string) { + if len(args) < 2 { + fmt.Println(red("Usage: group remove-member ")) + return + } + + groupUUID := args[0] + userUUID := args[1] + + // First, get current members + respBody, status, err := c.doRequest("GET", "/api/groups/"+groupUUID, nil) + if err != nil { + fmt.Println(red("Error:"), err) + return + } + + if status != http.StatusOK { + fmt.Printf(red("Error getting group (status %d):\n"), status) + fmt.Println(string(respBody)) + return + } + + var group Group + if err := json.Unmarshal(respBody, &group); err != nil { + fmt.Println(red("Error parsing response:"), err) + return + } + + // Remove member from list + newMembers := []string{} + found := false + for _, member := range group.Members { + if member != userUUID { + newMembers = append(newMembers, member) + } else { + found = true + } + } + + if !found { + fmt.Println(yellow("User is not a member of this group")) + return + } + + // Update group with new members list + payload := map[string]interface{}{ + "members": newMembers, + } + + respBody, status, err = c.doRequest("PUT", "/api/groups/"+groupUUID, payload) + if err != nil { + fmt.Println(red("Error:"), err) + return + } + + if status != http.StatusOK { + fmt.Printf(red("Error updating group (status %d):\n"), status) + fmt.Println(string(respBody)) + return + } + + fmt.Println(green("Member removed successfully")) +} diff --git a/cmd_system.go b/cmd_system.go index b569671..b8a028a 100644 --- a/cmd_system.go +++ b/cmd_system.go @@ -52,6 +52,14 @@ User Management: user get - Get user details user create - Create new user (admin only) +Group Management: + group create [members] - Create new group + group get - Get group details + group update - Replace all group members + group delete - Delete group + group add-member - Add member to group + group remove-member - Remove member from group + System: help - Show this help exit, quit - Exit shell diff --git a/main.go b/main.go index 661fbae..5ac4e91 100644 --- a/main.go +++ b/main.go @@ -95,6 +95,8 @@ func main() { } else { client.handleUserList(args) } + case "group": + client.handleGroup(args) default: fmt.Println(red("Unknown command:"), cmd) fmt.Println("Type 'help' for available commands") diff --git a/utils.go b/utils.go index 2871a37..8021ddd 100644 --- a/utils.go +++ b/utils.go @@ -119,6 +119,14 @@ var completer = readline.NewPrefixCompleter( readline.PcItem("get"), readline.PcItem("create"), ), + readline.PcItem("group", + readline.PcItem("create"), + readline.PcItem("get"), + readline.PcItem("update"), + readline.PcItem("delete"), + readline.PcItem("add-member"), + readline.PcItem("remove-member"), + ), readline.PcItem("help"), readline.PcItem("exit"), readline.PcItem("quit"),