Implemented roadmap #4: Export/import functionality. Features: - export <file> <key1> [key2...] - Export specified keys to JSON - export-list <keyfile> <output> - Bulk export from key list file - import <file> - Import keys from JSON file --skip-existing (default) - Skip keys that already exist --overwrite - Overwrite existing keys Export file format (v1.0): { "version": "1.0", "entries": [ { "key": "users/alice", "uuid": "...", "timestamp": 1234567890, "data": {...} } ] } The format preserves UUIDs and timestamps for audit trails. Export shows progress with ✓/✗/⊘ symbols for success/fail/not-found. Import checks for existing keys and respects skip/overwrite flags. Use cases: - Backup: export-list keys.txt backup.json - Migration: import backup.json --overwrite - Selective sync: export dest.json key1 key2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
4.0 KiB
Go
140 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/chzyer/readline"
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
// Color helpers
|
|
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()
|
|
)
|
|
|
|
// parseCommand parses command line respecting quotes
|
|
func parseCommand(line string) []string {
|
|
var parts []string
|
|
var current strings.Builder
|
|
var quoteChar rune = 0 // Use a rune to store the active quote character (' or ")
|
|
|
|
for _, ch := range line {
|
|
switch {
|
|
// If we see a quote character
|
|
case ch == '\'' || ch == '"':
|
|
if quoteChar == 0 {
|
|
// Not currently inside a quoted string, so start one
|
|
quoteChar = ch
|
|
} else if quoteChar == ch {
|
|
// Inside a quoted string and found the matching quote, so end it
|
|
quoteChar = 0
|
|
} else {
|
|
// Inside a quoted string but found the *other* type of quote, treat it as a literal character
|
|
current.WriteRune(ch)
|
|
}
|
|
// If we see a space or tab and are NOT inside a quoted string
|
|
case (ch == ' ' || ch == '\t') && quoteChar == 0:
|
|
if current.Len() > 0 {
|
|
parts = append(parts, current.String())
|
|
current.Reset()
|
|
}
|
|
// Any other character
|
|
default:
|
|
current.WriteRune(ch)
|
|
}
|
|
}
|
|
|
|
// Add the last part if it exists
|
|
if current.Len() > 0 {
|
|
parts = append(parts, current.String())
|
|
}
|
|
|
|
return parts
|
|
}
|
|
|
|
// 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\\\"}\"")
|
|
}
|
|
}
|
|
|
|
// Auto-completion setup
|
|
var completer = readline.NewPrefixCompleter(
|
|
readline.PcItem("connect"),
|
|
readline.PcItem("auth"),
|
|
readline.PcItem("profile",
|
|
readline.PcItem("add"),
|
|
readline.PcItem("use"),
|
|
readline.PcItem("remove"),
|
|
readline.PcItem("save"),
|
|
readline.PcItem("load"),
|
|
),
|
|
readline.PcItem("get"),
|
|
readline.PcItem("put"),
|
|
readline.PcItem("delete"),
|
|
readline.PcItem("meta",
|
|
readline.PcItem("get"),
|
|
readline.PcItem("set",
|
|
readline.PcItem("--owner"),
|
|
readline.PcItem("--group"),
|
|
readline.PcItem("--permissions"),
|
|
),
|
|
),
|
|
readline.PcItem("members"),
|
|
readline.PcItem("health"),
|
|
readline.PcItem("user",
|
|
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("export"),
|
|
readline.PcItem("import"),
|
|
readline.PcItem("export-list"),
|
|
readline.PcItem("batch"),
|
|
readline.PcItem("source"),
|
|
readline.PcItem("help"),
|
|
readline.PcItem("exit"),
|
|
readline.PcItem("quit"),
|
|
readline.PcItem("clear"),
|
|
)
|