Compare commits
4 Commits
fe45fa5254
...
f7f956de47
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f7f956de47 | ||
999deccb74 | |||
88a3b9824a | |||
8edbfacda5 |
160
CLAUDE.md
Normal file
160
CLAUDE.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**kvs-sh** is an interactive CLI shell for the KVS (Key-Value Store) distributed database. Think `mongosh`, `psql`, or `redis-cli` but for KVS. Single Go file implementation (~600 lines) with readline support for command history and auto-completion.
|
||||
|
||||
## KVS Server Reference
|
||||
|
||||
The complete KVS server source code is located at `../kvs/` relative to this repository. Reference it when:
|
||||
- Understanding API contracts and response formats
|
||||
- Verifying authentication/authorization behavior
|
||||
- Checking endpoint paths and HTTP methods
|
||||
- Understanding data structures (StoredValue, Member, User, Group types)
|
||||
- Implementing new shell commands for backend features
|
||||
|
||||
Key server locations:
|
||||
- `../kvs/server/handlers.go` - All API endpoint implementations
|
||||
- `../kvs/server/routes.go` - Route definitions and HTTP methods
|
||||
- `../kvs/types/types.go` - Canonical type definitions (StoredValue, etc.)
|
||||
- `../kvs/auth/` - JWT authentication and permission system details
|
||||
- `../kvs/README.md` - Complete API documentation with curl examples
|
||||
- `../kvs/CLAUDE.md` - Server architecture and development guidance
|
||||
|
||||
When adding new commands to kvs-sh, always verify the actual backend implementation in `../kvs/` rather than making assumptions about API behavior.
|
||||
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build -o kvs-shell
|
||||
|
||||
# Run
|
||||
./kvs-shell
|
||||
|
||||
# Install to system
|
||||
go install
|
||||
```
|
||||
|
||||
## Quick Start with Server
|
||||
|
||||
```bash
|
||||
# 1. Start KVS server (in ../kvs/)
|
||||
cd ../kvs && ./kvs config.yaml
|
||||
|
||||
# 2. Run shell
|
||||
./kvs-shell
|
||||
|
||||
# 3. In shell:
|
||||
# connect http://localhost:8080
|
||||
# auth $KVS_ROOT_TOKEN
|
||||
# put test/data '{"hello":"world"}'
|
||||
```
|
||||
|
||||
**Important:** If auth fails with "token not found", the database has old data. Either:
|
||||
- Create a new token via the API, or
|
||||
- Wipe database: `rm -rf ../kvs/data && mkdir ../kvs/data` (restart server to generate root token)
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Single-File Design
|
||||
Everything lives in `main.go` - intentionally kept simple. The shell is a stateful REPL that maintains:
|
||||
- HTTP client connection to KVS backend
|
||||
- Current JWT auth token
|
||||
- In-memory profile storage (username, token, server URL combinations)
|
||||
- Active profile selection
|
||||
|
||||
### Core Components
|
||||
|
||||
**KVSClient (main.go:49)**: Central state container holding:
|
||||
- `baseURL`: KVS server endpoint
|
||||
- `currentToken`: JWT for authentication
|
||||
- `profiles`: In-memory map of saved user profiles (NOT persisted to disk yet)
|
||||
- `httpClient`: Reusable HTTP client with 30s timeout
|
||||
|
||||
**Command Router (main.go:465)**: Main loop uses switch statement on first word of input, delegates to `handle*` methods
|
||||
|
||||
**Command Parser (main.go:527)**: Custom parser respecting quotes - critical for JSON arguments like `put key '{"foo": "bar"}'`
|
||||
|
||||
**Auto-completion (main.go:503)**: Prefix tree using readline library for tab completion of commands
|
||||
|
||||
### HTTP Communication Pattern
|
||||
All API calls go through `doRequest()` (main.go:85) which:
|
||||
1. Serializes body to JSON if present
|
||||
2. Sets `Authorization: Bearer <token>` header if token exists
|
||||
3. Returns `(body, statusCode, error)` tuple
|
||||
4. Delegates HTTP status interpretation to caller
|
||||
|
||||
### Key Backend API Mappings
|
||||
- `GET /kv/<key>` → Returns `StoredValue` with UUID, timestamp, data
|
||||
- `PUT /kv/<key>` → Accepts raw JSON, returns UUID + timestamp
|
||||
- `DELETE /kv/<key>` → Returns 204 on success
|
||||
- `GET /members/` → Returns array of cluster `Member` objects
|
||||
- `GET /api/users/<uuid>` → Returns `User` object
|
||||
- `GET /health` → Returns service status map
|
||||
|
||||
### Type Definitions (main.go:18-46)
|
||||
Backend response structures are mirrored in the client:
|
||||
- `StoredValue`: The core KV response with UUID, timestamp (int64 unix millis), and `json.RawMessage` data
|
||||
- `Member`: Cluster member info with ID, address, last_seen timestamp
|
||||
- `User`: User record with nickname hash, groups array, created/updated timestamps
|
||||
- `Group`: Group definition (defined but not yet used in commands)
|
||||
|
||||
### Profile System
|
||||
Profiles are in-memory only (see roadmap for persistence plan). When you `profile use <name>`, it swaps the client's token, userUUID, and baseURL atomically. The `*` marker in profile listings indicates active profile.
|
||||
|
||||
### Color Scheme
|
||||
Uses fatih/color package with semantic coloring:
|
||||
- Cyan: labels, headers, structured data keys
|
||||
- Green: success messages, confirmations
|
||||
- Yellow: warnings, "not found" states
|
||||
- Red: errors, invalid usage
|
||||
- Magenta: banner/title text
|
||||
|
||||
### Command Parsing - Critical Implementation Details
|
||||
|
||||
**parseCommand()** (main.go:527-570): Handles both single (`'`) and double (`"`) quotes intelligently.
|
||||
|
||||
**Key behavior:**
|
||||
- Quotes are **only stripped** when they delimit an entire argument (start and end)
|
||||
- Supports both `'` and `"` as quote delimiters
|
||||
- Tracks which quote type started the argument to handle nesting correctly
|
||||
- Preserves quotes that appear mid-argument (for unquoted JSON)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
put key '{"foo":"bar"}' # ✅ Strips outer ', preserves inner "
|
||||
put key {"foo":"bar"} # ✅ Works (no spaces, compact JSON)
|
||||
put key {"foo": "bar"} # ❌ Fails - spaces split into multiple args
|
||||
put key "{\"foo\":\"bar\"}" # ✅ Works (escaped quotes in double-quoted arg)
|
||||
```
|
||||
|
||||
**JSON Error Handling** (main.go:393-425):
|
||||
- `printJSONError()` provides user-friendly error messages with examples
|
||||
- Detects error type (invalid chars, incomplete JSON, etc.) and shows context-specific tips
|
||||
- Shows what input was received to help debug quoting issues
|
||||
|
||||
## Backend Assumptions
|
||||
|
||||
The shell expects the KVS backend to:
|
||||
1. Accept JWT tokens in `Authorization: Bearer <token>` header
|
||||
2. Return timestamps as Unix milliseconds (int64)
|
||||
3. Use standard REST semantics (404 for not found, 204 for delete, 200/201 for success)
|
||||
4. Accept raw JSON payloads for PUT operations (not wrapped in metadata)
|
||||
|
||||
## Known Limitations & Roadmap Items
|
||||
|
||||
1. **No persistent profiles** - restart loses all `profile add` data (roadmap #1)
|
||||
2. **No metadata commands** - backend supports owner/group/permissions but shell doesn't expose yet (roadmap #6)
|
||||
3. **No group management** - `Group` type defined but no CRUD operations (roadmap #5)
|
||||
4. **No TLS support** - HTTP only, no certificate handling (roadmap #8)
|
||||
5. **No tab completion for keys** - only completes command names, not dynamic keys from server (roadmap #10)
|
||||
6. **user create not implemented** - Listed in help but just shows "not implemented" message
|
45
main.go
45
main.go
@@ -230,7 +230,7 @@ func (c *KVSClient) handleGet(args []string) {
|
||||
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())
|
||||
@@ -260,9 +260,8 @@ func (c *KVSClient) handlePut(args []string) {
|
||||
|
||||
// Parse JSON data
|
||||
var data json.RawMessage
|
||||
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
|
||||
fmt.Println(red("Invalid JSON:"), err)
|
||||
fmt.Println("Tip: Use proper JSON format with escaped quotes")
|
||||
if err := json.Unmarshal([]byte(args[1]), &data); err != nil {
|
||||
printJSONError(args[1], err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -388,7 +387,7 @@ func (c *KVSClient) handleMetaSet(args []string) {
|
||||
return
|
||||
}
|
||||
key := args[0]
|
||||
|
||||
|
||||
// Use a map to build the JSON payload for partial updates
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
@@ -572,6 +571,39 @@ func (c *KVSClient) handleUserCreate(args []string) {
|
||||
|
||||
fmt.Println(green("User created successfully"))
|
||||
fmt.Println(cyan("UUID:"), response.UUID)
|
||||
|
||||
// printJSONError displays a user-friendly JSON parsing error with helpful hints
|
||||
func printJSONError(input string, err error) {
|
||||
fmt.Println(red("❌ Invalid JSON:"), err)
|
||||
fmt.Println()
|
||||
|
||||
// Show what was received
|
||||
if len(input) > 100 {
|
||||
fmt.Println(yellow("Received:"), input[:100]+"...")
|
||||
} else {
|
||||
fmt.Println(yellow("Received:"), input)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Provide helpful hints based on common errors
|
||||
errMsg := err.Error()
|
||||
|
||||
if strings.Contains(errMsg, "looking for beginning of") ||
|
||||
strings.Contains(errMsg, "invalid character") {
|
||||
fmt.Println(cyan("💡 Tip:"), "JSON with spaces needs quotes:")
|
||||
fmt.Println(green(" ✓ Correct:"), "put key '{\"hello\":\"world\"}'")
|
||||
fmt.Println(green(" ✓ Correct:"), "put key '{\"a\":1,\"b\":2}'")
|
||||
fmt.Println(green(" ✓ Compact:"), "put key {\"a\":1,\"b\":2} (no spaces)")
|
||||
fmt.Println(red(" ✗ Wrong: "), "put key {\"a\": 1, \"b\": 2} (spaces without quotes)")
|
||||
} else if strings.Contains(errMsg, "unexpected end of JSON") {
|
||||
fmt.Println(cyan("💡 Tip:"), "JSON appears incomplete or cut off")
|
||||
fmt.Println(green(" Check:"), "Are all brackets/braces matched? {}, []")
|
||||
fmt.Println(green(" Check:"), "Did spaces split your JSON into multiple arguments?")
|
||||
} else {
|
||||
fmt.Println(cyan("💡 Tip:"), "Wrap JSON in quotes for complex values:")
|
||||
fmt.Println(green(" Single quotes:"), "put key '{\"your\":\"json\"}'")
|
||||
fmt.Println(green(" Double quotes:"), "put key \"{\\\"your\\\":\\\"json\\\"}\"")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KVSClient) handleHelp(args []string) {
|
||||
@@ -761,6 +793,9 @@ func parseCommand(line string) []string {
|
||||
default:
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
|
||||
// Add all other characters (including quotes inside unquoted args)
|
||||
current.WriteByte(ch)
|
||||
}
|
||||
|
||||
// Add the last part if it exists
|
||||
|
Reference in New Issue
Block a user