Initial commit. The shell now combiles and runs.
This commit is contained in:
257
README.md
Normal file
257
README.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# KVS Shell
|
||||
|
||||
An interactive command-line client for the KVS (Key-Value Store) distributed database system.
|
||||
|
||||
## Overview
|
||||
|
||||
KVS Shell is a terminal-based client that provides an intuitive interface for interacting with KVS servers. It offers features similar to `mongosh`, `psql`, or `redis-cli`, allowing you to manage key-value data, users, groups, and cluster members through a simple command interface.
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive Shell**: Full-featured REPL with command history and auto-completion
|
||||
- **Profile Management**: Switch between multiple user accounts and servers
|
||||
- **Authentication**: JWT token-based authentication with secure credential storage
|
||||
- **Key-Value Operations**: Store, retrieve, and delete JSON data
|
||||
- **Cluster Management**: Monitor cluster members and health status
|
||||
- **User Management**: View and manage user accounts (with appropriate permissions)
|
||||
- **Colored Output**: Syntax-highlighted responses for better readability
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.21 or higher
|
||||
- Access to a running KVS server
|
||||
|
||||
### Build from Source
|
||||
|
||||
```bash
|
||||
git clone
|
||||
cd kvs-shell
|
||||
go mod tidy
|
||||
go build -o kvs-shell
|
||||
```
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
# Install to your Go bin directory
|
||||
go install
|
||||
|
||||
# Or copy the binary to your PATH
|
||||
sudo cp kvs-shell /usr/local/bin/
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Start the shell:
|
||||
```bash
|
||||
./kvs-shell
|
||||
```
|
||||
|
||||
2. Connect to your KVS server:
|
||||
```
|
||||
kvs> connect http://localhost:8090
|
||||
```
|
||||
|
||||
3. Authenticate (if required):
|
||||
```
|
||||
kvs> auth eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
4. Store and retrieve data:
|
||||
```
|
||||
kvs> put users/alice {"name": "Alice", "email": "alice@example.com"}
|
||||
kvs> get users/alice
|
||||
```
|
||||
|
||||
## Commands Reference
|
||||
|
||||
### Connection & Authentication
|
||||
|
||||
```bash
|
||||
connect <url> # Connect to KVS server
|
||||
auth <token> # Set authentication token
|
||||
```
|
||||
|
||||
### Profile Management
|
||||
|
||||
```bash
|
||||
profile # List all saved profiles
|
||||
profile add <name> <token> <user-uuid> [url] # Add a new profile
|
||||
profile use <name> # Switch to a profile
|
||||
profile remove <name> # Delete a profile
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
kvs> profile add admin eyJhbG... user-uuid-123 http://localhost:8090
|
||||
kvs> profile add dev eyJhbG... user-uuid-456 http://localhost:8091
|
||||
kvs> profile use dev
|
||||
```
|
||||
|
||||
### Key-Value Operations
|
||||
|
||||
```bash
|
||||
get <key> # Retrieve value for key
|
||||
put <key> <json> # Store JSON value at key
|
||||
delete <key> # Delete key
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
kvs> put config/app {"version": "1.0", "debug": true}
|
||||
kvs> get config/app
|
||||
kvs> delete config/old
|
||||
```
|
||||
|
||||
### Cluster Management
|
||||
|
||||
```bash
|
||||
members # List cluster members
|
||||
health # Check service health
|
||||
```
|
||||
|
||||
### User Management
|
||||
|
||||
```bash
|
||||
user get <uuid> # Get user details
|
||||
user create <nickname> # Create new user (admin only)
|
||||
```
|
||||
|
||||
### System Commands
|
||||
|
||||
```bash
|
||||
help # Show help
|
||||
clear # Clear screen
|
||||
exit, quit # Exit shell
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
### Stored Values
|
||||
|
||||
When retrieving data with `get`, you'll see:
|
||||
```
|
||||
UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||
Timestamp: 2025-10-05T12:34:56Z
|
||||
Data:
|
||||
{
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Cluster Members
|
||||
|
||||
The `members` command shows:
|
||||
```
|
||||
Cluster Members:
|
||||
• node-1 (192.168.1.10:8090) - Last seen: 2025-10-05T12:34:56Z
|
||||
• node-2 (192.168.1.11:8090) - Last seen: 2025-10-05T12:35:01Z
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### History
|
||||
|
||||
Command history is automatically saved to `~/.kvs_history` and persists across sessions.
|
||||
|
||||
### Profiles
|
||||
|
||||
Profiles are stored in memory during the session. To persist profiles across restarts, you can save them to a configuration file (feature to be added in future versions).
|
||||
|
||||
## Authentication
|
||||
|
||||
KVS Shell uses JWT tokens for authentication. Obtain a token from your KVS server administrator or through the server's authentication API:
|
||||
|
||||
```bash
|
||||
# Example: Get token from KVS server
|
||||
curl -X POST http://localhost:8090/api/tokens \
|
||||
-H "Authorization: Bearer <admin-token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_uuid": "your-uuid", "scopes": ["read", "write"]}'
|
||||
```
|
||||
|
||||
Then use the returned token in the shell:
|
||||
```
|
||||
kvs> auth eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Working with Complex JSON
|
||||
|
||||
For multi-line or complex JSON, use quotes:
|
||||
```
|
||||
kvs> put data/user '{"name": "Bob", "preferences": {"theme": "dark", "notifications": true}}'
|
||||
```
|
||||
|
||||
### Key Naming Conventions
|
||||
|
||||
Keys can use path-like structures:
|
||||
```
|
||||
users/alice
|
||||
config/app/production
|
||||
data/metrics/2025-10
|
||||
```
|
||||
|
||||
This helps organize your data hierarchically.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
If you can't connect to the server:
|
||||
1. Verify the server is running: `curl http://localhost:8090/health`
|
||||
2. Check firewall settings
|
||||
3. Ensure the URL includes the protocol (http:// or https://)
|
||||
|
||||
### Authentication Errors
|
||||
|
||||
If you get "Unauthorized" errors:
|
||||
1. Verify your token is still valid (tokens may expire)
|
||||
2. Check that your user has the required permissions
|
||||
3. Generate a new token if needed
|
||||
|
||||
### Command Not Found
|
||||
|
||||
If a command isn't recognized:
|
||||
- Type `help` to see available commands
|
||||
- Check for typos in the command name
|
||||
- Ensure you've connected to a server first
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
go build -o kvs-shell
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `github.com/chzyer/readline` - Interactive line editing and history
|
||||
- `github.com/fatih/color` - Colored terminal output
|
||||
|
||||
## Roadmap
|
||||
|
||||
Planned features:
|
||||
- [ ] Configuration file for persistent profiles
|
||||
- [ ] Management of local KVS instance configuration file
|
||||
- [ ] Batch operations from file
|
||||
- [ ] Export/import functionality
|
||||
- [ ] Group management commands
|
||||
- [ ] Metadata management
|
||||
- [ ] Query filtering and pagination
|
||||
- [ ] TLS/SSL support
|
||||
- [ ] Shell scripts execution
|
||||
- [ ] Tab completion for keys
|
||||
- [ ] Transaction support
|
||||
|
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module kvs-shell
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/chzyer/readline v1.5.1
|
||||
github.com/fatih/color v1.16.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
)
|
18
go.sum
Normal file
18
go.sum
Normal file
@@ -0,0 +1,18 @@
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
554
main.go
Normal file
554
main.go
Normal file
@@ -0,0 +1,554 @@
|
||||
// kvs-shell/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// Types matching your backend
|
||||
type StoredValue struct {
|
||||
UUID string `json:"uuid"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
JoinedTimestamp int64 `json:"joined_timestamp"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
UUID string `json:"uuid"`
|
||||
NicknameHash string `json:"nickname_hash"`
|
||||
Groups []string `json:"groups"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
UUID string `json:"uuid"`
|
||||
NameHash string `json:"name_hash"`
|
||||
Members []string `json:"members"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Client state
|
||||
type KVSClient struct {
|
||||
baseURL string
|
||||
currentToken string
|
||||
currentUser string
|
||||
httpClient *http.Client
|
||||
profiles map[string]Profile // Stored user profiles
|
||||
activeProfile string
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
UserUUID string `json:"user_uuid"`
|
||||
BaseURL string `json:"base_url"`
|
||||
}
|
||||
|
||||
// Colors
|
||||
var (
|
||||
cyan = color.New(color.FgCyan).SprintFunc()
|
||||
green = color.New(color.FgGreen).SprintFunc()
|
||||
yellow = color.New(color.FgYellow).SprintFunc()
|
||||
red = color.New(color.FgRed).SprintFunc()
|
||||
magenta = color.New(color.FgMagenta).SprintFunc()
|
||||
)
|
||||
|
||||
func NewKVSClient(baseURL string) *KVSClient {
|
||||
return &KVSClient{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
profiles: make(map[string]Profile),
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP request helper
|
||||
func (c *KVSClient) doRequest(method, path string, body interface{}) ([]byte, int, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonData, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
reqBody = bytes.NewBuffer(jsonData)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.baseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if c.currentToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.currentToken)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
return respBody, resp.StatusCode, err
|
||||
}
|
||||
|
||||
// Command handlers
|
||||
func (c *KVSClient) handleConnect(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println(red("Usage: connect <base-url>"))
|
||||
return
|
||||
}
|
||||
c.baseURL = args[0]
|
||||
fmt.Println(green("Connected to:"), c.baseURL)
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleAuth(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println(red("Usage: auth <token>"))
|
||||
return
|
||||
}
|
||||
c.currentToken = args[0]
|
||||
fmt.Println(green("Authentication token set"))
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleProfile(args []string) {
|
||||
if len(args) == 0 {
|
||||
// List profiles
|
||||
if len(c.profiles) == 0 {
|
||||
fmt.Println(yellow("No profiles configured"))
|
||||
return
|
||||
}
|
||||
fmt.Println(cyan("Available profiles:"))
|
||||
for name, profile := range c.profiles {
|
||||
marker := " "
|
||||
if name == c.activeProfile {
|
||||
marker = "*"
|
||||
}
|
||||
fmt.Printf("%s %s (user: %s, url: %s)\n", marker, name, profile.UserUUID, profile.BaseURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
subCmd := args[0]
|
||||
switch subCmd {
|
||||
case "add":
|
||||
if len(args) < 4 {
|
||||
fmt.Println(red("Usage: profile add <name> <token> <user-uuid> [base-url]"))
|
||||
return
|
||||
}
|
||||
baseURL := c.baseURL
|
||||
if len(args) >= 5 {
|
||||
baseURL = args[4]
|
||||
}
|
||||
c.profiles[args[1]] = Profile{
|
||||
Name: args[1],
|
||||
Token: args[2],
|
||||
UserUUID: args[3],
|
||||
BaseURL: baseURL,
|
||||
}
|
||||
fmt.Println(green("Profile added:"), args[1])
|
||||
|
||||
case "use":
|
||||
if len(args) < 2 {
|
||||
fmt.Println(red("Usage: profile use <name>"))
|
||||
return
|
||||
}
|
||||
profile, ok := c.profiles[args[1]]
|
||||
if !ok {
|
||||
fmt.Println(red("Profile not found:"), args[1])
|
||||
return
|
||||
}
|
||||
c.currentToken = profile.Token
|
||||
c.currentUser = profile.UserUUID
|
||||
c.baseURL = profile.BaseURL
|
||||
c.activeProfile = args[1]
|
||||
fmt.Println(green("Switched to profile:"), args[1])
|
||||
|
||||
case "remove":
|
||||
if len(args) < 2 {
|
||||
fmt.Println(red("Usage: profile remove <name>"))
|
||||
return
|
||||
}
|
||||
delete(c.profiles, args[1])
|
||||
if c.activeProfile == args[1] {
|
||||
c.activeProfile = ""
|
||||
}
|
||||
fmt.Println(green("Profile removed:"), args[1])
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleGet(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println(red("Usage: get <key>"))
|
||||
return
|
||||
}
|
||||
|
||||
respBody, status, err := c.doRequest("GET", "/kv/"+args[0], nil)
|
||||
if err != nil {
|
||||
fmt.Println(red("Error:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if status == 404 {
|
||||
fmt.Println(yellow("Key not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if status != 200 {
|
||||
fmt.Println(red("Error:"), string(respBody))
|
||||
return
|
||||
}
|
||||
|
||||
var stored StoredValue
|
||||
if err := json.Unmarshal(respBody, &stored); err != nil {
|
||||
fmt.Println(red("Error parsing response:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(cyan("UUID: "), stored.UUID)
|
||||
fmt.Println(cyan("Timestamp:"), time.UnixMilli(stored.Timestamp).Format(time.RFC3339))
|
||||
fmt.Println(cyan("Data:"))
|
||||
|
||||
var pretty bytes.Buffer
|
||||
json.Indent(&pretty, stored.Data, "", " ")
|
||||
fmt.Println(pretty.String())
|
||||
}
|
||||
|
||||
func (c *KVSClient) handlePut(args []string) {
|
||||
if len(args) < 2 {
|
||||
fmt.Println(red("Usage: put <key> <json-data>"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse JSON data
|
||||
var data json.RawMessage
|
||||
if err := json.Unmarshal([]byte(args[1]), &data); err != nil {
|
||||
fmt.Println(red("Invalid JSON:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
respBody, status, err := c.doRequest("PUT", "/kv/"+args[0], data)
|
||||
if err != nil {
|
||||
fmt.Println(red("Error:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if status != 200 && status != 201 {
|
||||
fmt.Println(red("Error:"), string(respBody))
|
||||
return
|
||||
}
|
||||
|
||||
type PutResponse struct {
|
||||
UUID string `json:"uuid"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
var resp PutResponse
|
||||
if err := json.Unmarshal(respBody, &resp); err == nil {
|
||||
fmt.Println(green("Stored successfully"))
|
||||
fmt.Println(cyan("UUID: "), resp.UUID)
|
||||
fmt.Println(cyan("Timestamp:"), time.UnixMilli(resp.Timestamp).Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleDelete(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println(red("Usage: delete <key>"))
|
||||
return
|
||||
}
|
||||
|
||||
_, status, err := c.doRequest("DELETE", "/kv/"+args[0], nil)
|
||||
if err != nil {
|
||||
fmt.Println(red("Error:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if status == 404 {
|
||||
fmt.Println(yellow("Key not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if status == 204 {
|
||||
fmt.Println(green("Deleted successfully"))
|
||||
} else {
|
||||
fmt.Println(red("Unexpected status:"), status)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleMembers(args []string) {
|
||||
respBody, status, err := c.doRequest("GET", "/members/", nil)
|
||||
if err != nil {
|
||||
fmt.Println(red("Error:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if status != 200 {
|
||||
fmt.Println(red("Error:"), string(respBody))
|
||||
return
|
||||
}
|
||||
|
||||
var members []Member
|
||||
if err := json.Unmarshal(respBody, &members); err != nil {
|
||||
fmt.Println(red("Error parsing response:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(members) == 0 {
|
||||
fmt.Println(yellow("No cluster members"))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(cyan("Cluster Members:"))
|
||||
for _, m := range members {
|
||||
lastSeen := time.UnixMilli(m.LastSeen).Format(time.RFC3339)
|
||||
fmt.Printf(" • %s (%s) - Last seen: %s\n", m.ID, m.Address, lastSeen)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleHealth(args []string) {
|
||||
respBody, status, err := c.doRequest("GET", "/health", nil)
|
||||
if err != nil {
|
||||
fmt.Println(red("Error:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if status != 200 {
|
||||
fmt.Println(red("Service unhealthy"))
|
||||
return
|
||||
}
|
||||
|
||||
var health map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &health); err != nil {
|
||||
fmt.Println(red("Error parsing response:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(green("Service Status:"))
|
||||
for k, v := range health {
|
||||
fmt.Printf(" %s: %v\n", cyan(k), v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleUserList(args []string) {
|
||||
// Note: Backend doesn't have a list-all endpoint, this would need to be added
|
||||
fmt.Println(yellow("User listing not implemented in backend API"))
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleUserGet(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println(red("Usage: user get <uuid>"))
|
||||
return
|
||||
}
|
||||
|
||||
respBody, status, err := c.doRequest("GET", "/api/users/"+args[0], nil)
|
||||
if err != nil {
|
||||
fmt.Println(red("Error:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
if status == 404 {
|
||||
fmt.Println(yellow("User not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if status != 200 {
|
||||
fmt.Println(red("Error:"), string(respBody))
|
||||
return
|
||||
}
|
||||
|
||||
var user User
|
||||
if err := json.Unmarshal(respBody, &user); err != nil {
|
||||
fmt.Println(red("Error parsing response:"), err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(cyan("User Details:"))
|
||||
fmt.Printf(" UUID: %s\n", user.UUID)
|
||||
fmt.Printf(" Nickname Hash: %s\n", user.NicknameHash)
|
||||
fmt.Printf(" Groups: %v\n", user.Groups)
|
||||
fmt.Printf(" Created: %s\n", time.Unix(user.CreatedAt, 0).Format(time.RFC3339))
|
||||
fmt.Printf(" Updated: %s\n", time.Unix(user.UpdatedAt, 0).Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleHelp(args []string) {
|
||||
help := `
|
||||
KVS Interactive Shell - Available Commands:
|
||||
|
||||
Connection & Authentication:
|
||||
connect <url> - Connect to KVS server
|
||||
auth <token> - Set authentication token
|
||||
profile add <name> <token> <user-uuid> [url] - Add user profile
|
||||
profile use <name> - Switch to user profile
|
||||
profile remove <name> - Remove user profile
|
||||
profile - List all profiles
|
||||
|
||||
Key-Value Operations:
|
||||
get <key> - Retrieve value for key
|
||||
put <key> <json> - Store JSON value at key
|
||||
delete <key> - Delete key
|
||||
|
||||
Cluster Management:
|
||||
members - List cluster members
|
||||
health - Check service health
|
||||
|
||||
User Management:
|
||||
user get <uuid> - Get user details
|
||||
user create <nickname> - Create new user (admin only)
|
||||
|
||||
System:
|
||||
help - Show this help
|
||||
exit, quit - Exit shell
|
||||
clear - Clear screen
|
||||
`
|
||||
fmt.Println(help)
|
||||
}
|
||||
|
||||
func main() {
|
||||
client := NewKVSClient("http://localhost:8090")
|
||||
|
||||
// Setup readline
|
||||
rl, err := readline.NewEx(&readline.Config{
|
||||
Prompt: cyan("kvs> "),
|
||||
HistoryFile: os.Getenv("HOME") + "/.kvs_history",
|
||||
AutoComplete: completer,
|
||||
InterruptPrompt: "^C",
|
||||
EOFPrompt: "exit",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rl.Close()
|
||||
|
||||
fmt.Println(magenta("KVS Interactive Shell"))
|
||||
fmt.Println("Type 'help' for available commands")
|
||||
fmt.Println()
|
||||
|
||||
for {
|
||||
line, err := rl.Readline()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := parseCommand(line)
|
||||
if len(parts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cmd := parts[0]
|
||||
args := parts[1:]
|
||||
|
||||
switch cmd {
|
||||
case "exit", "quit":
|
||||
fmt.Println("Goodbye!")
|
||||
return
|
||||
case "clear":
|
||||
print("\033[H\033[2J")
|
||||
case "help":
|
||||
client.handleHelp(args)
|
||||
case "connect":
|
||||
client.handleConnect(args)
|
||||
case "auth":
|
||||
client.handleAuth(args)
|
||||
case "profile":
|
||||
client.handleProfile(args)
|
||||
case "get":
|
||||
client.handleGet(args)
|
||||
case "put":
|
||||
client.handlePut(args)
|
||||
case "delete":
|
||||
client.handleDelete(args)
|
||||
case "members":
|
||||
client.handleMembers(args)
|
||||
case "health":
|
||||
client.handleHealth(args)
|
||||
case "user":
|
||||
if len(args) > 0 && args[0] == "get" {
|
||||
client.handleUserGet(args[1:])
|
||||
} else {
|
||||
client.handleUserList(args)
|
||||
}
|
||||
default:
|
||||
fmt.Println(red("Unknown command:"), cmd)
|
||||
fmt.Println("Type 'help' for available commands")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-completion
|
||||
var completer = readline.NewPrefixCompleter(
|
||||
readline.PcItem("connect"),
|
||||
readline.PcItem("auth"),
|
||||
readline.PcItem("profile",
|
||||
readline.PcItem("add"),
|
||||
readline.PcItem("use"),
|
||||
readline.PcItem("remove"),
|
||||
),
|
||||
readline.PcItem("get"),
|
||||
readline.PcItem("put"),
|
||||
readline.PcItem("delete"),
|
||||
readline.PcItem("members"),
|
||||
readline.PcItem("health"),
|
||||
readline.PcItem("user",
|
||||
readline.PcItem("get"),
|
||||
readline.PcItem("create"),
|
||||
),
|
||||
readline.PcItem("help"),
|
||||
readline.PcItem("exit"),
|
||||
readline.PcItem("quit"),
|
||||
readline.PcItem("clear"),
|
||||
)
|
||||
|
||||
// Parse command line respecting quotes
|
||||
func parseCommand(line string) []string {
|
||||
var parts []string
|
||||
var current strings.Builder
|
||||
inQuotes := false
|
||||
|
||||
for i := 0; i < len(line); i++ {
|
||||
ch := line[i]
|
||||
switch ch {
|
||||
case '"':
|
||||
inQuotes = !inQuotes
|
||||
case ' ', '\t':
|
||||
if inQuotes {
|
||||
current.WriteByte(ch)
|
||||
} else if current.Len() > 0 {
|
||||
parts = append(parts, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
default:
|
||||
current.WriteByte(ch)
|
||||
}
|
||||
}
|
||||
|
||||
if current.Len() > 0 {
|
||||
parts = append(parts, current.String())
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
Reference in New Issue
Block a user