feat: add persistent profile storage to ~/.kvs/config.json
Implemented roadmap #1: Configuration file for persistent profiles. Features: - Auto-saves profiles to ~/.kvs/config.json on add/use/remove - Auto-loads profiles on shell startup - File created with 0600 permissions for token security - Shows active profile in welcome message - Added 'profile save' and 'profile load' commands for manual control Technical details: - Created config.go with LoadConfig/SaveConfig functions - Profile changes automatically trigger persistence - ~/.kvs directory created with 0700 permissions if missing - Gracefully handles missing config file on first run 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ func (c *KVSClient) handleProfile(args []string) {
|
|||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
}
|
}
|
||||||
fmt.Println(green("Profile added:"), args[1])
|
fmt.Println(green("Profile added:"), args[1])
|
||||||
|
c.saveProfiles()
|
||||||
|
|
||||||
case "use":
|
case "use":
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
@@ -54,6 +55,7 @@ func (c *KVSClient) handleProfile(args []string) {
|
|||||||
c.baseURL = profile.BaseURL
|
c.baseURL = profile.BaseURL
|
||||||
c.activeProfile = args[1]
|
c.activeProfile = args[1]
|
||||||
fmt.Println(green("Switched to profile:"), args[1])
|
fmt.Println(green("Switched to profile:"), args[1])
|
||||||
|
c.saveProfiles()
|
||||||
|
|
||||||
case "remove":
|
case "remove":
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
@@ -65,5 +67,29 @@ func (c *KVSClient) handleProfile(args []string) {
|
|||||||
c.activeProfile = ""
|
c.activeProfile = ""
|
||||||
}
|
}
|
||||||
fmt.Println(green("Profile removed:"), args[1])
|
fmt.Println(green("Profile removed:"), args[1])
|
||||||
|
c.saveProfiles()
|
||||||
|
|
||||||
|
case "save":
|
||||||
|
if err := c.saveProfiles(); err != nil {
|
||||||
|
fmt.Println(red("Error saving profiles:"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(green("Profiles saved to ~/.kvs/config.json"))
|
||||||
|
|
||||||
|
case "load":
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(red("Error loading profiles:"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.syncConfigToClient(config)
|
||||||
|
fmt.Println(green("Profiles loaded from ~/.kvs/config.json"))
|
||||||
|
if c.activeProfile != "" {
|
||||||
|
fmt.Println(cyan("Active profile:"), c.activeProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Println(red("Unknown profile command:"), subCmd)
|
||||||
|
fmt.Println("Available commands: add, use, remove, save, load")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,10 +27,12 @@ KVS Interactive Shell - Available Commands:
|
|||||||
Connection & Authentication:
|
Connection & Authentication:
|
||||||
connect <url> - Connect to KVS server
|
connect <url> - Connect to KVS server
|
||||||
auth <token> - Set authentication token
|
auth <token> - Set authentication token
|
||||||
|
profile - List all profiles
|
||||||
profile add <name> <token> <user-uuid> [url] - Add user profile
|
profile add <name> <token> <user-uuid> [url] - Add user profile
|
||||||
profile use <name> - Switch to user profile
|
profile use <name> - Switch to user profile
|
||||||
profile remove <name> - Remove user profile
|
profile remove <name> - Remove user profile
|
||||||
profile - List all profiles
|
profile save - Save profiles to ~/.kvs/config.json
|
||||||
|
profile load - Load profiles from ~/.kvs/config.json
|
||||||
|
|
||||||
Key-Value Operations:
|
Key-Value Operations:
|
||||||
get <key> - Retrieve value for key
|
get <key> - Retrieve value for key
|
||||||
|
120
config.go
Normal file
120
config.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds the persistent configuration
|
||||||
|
type Config struct {
|
||||||
|
Profiles map[string]Profile `json:"profiles"`
|
||||||
|
ActiveProfile string `json:"active_profile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigPath returns the path to the config file
|
||||||
|
func getConfigPath() (string, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
kvsDir := filepath.Join(homeDir, ".kvs")
|
||||||
|
// Create .kvs directory if it doesn't exist
|
||||||
|
if err := os.MkdirAll(kvsDir, 0700); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(kvsDir, "config.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads the configuration from disk
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
configPath, err := getConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If config doesn't exist, return empty config
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
return &Config{
|
||||||
|
Profiles: make(map[string]Profile),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Profiles == nil {
|
||||||
|
config.Profiles = make(map[string]Profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveConfig saves the configuration to disk with 0600 permissions
|
||||||
|
func SaveConfig(config *Config) error {
|
||||||
|
configPath, err := getConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write with restrictive permissions (0600) to protect tokens
|
||||||
|
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncConfigToClient updates client state from config
|
||||||
|
func (c *KVSClient) syncConfigToClient(config *Config) {
|
||||||
|
c.profiles = config.Profiles
|
||||||
|
c.activeProfile = config.ActiveProfile
|
||||||
|
|
||||||
|
// If there's an active profile, apply it
|
||||||
|
if c.activeProfile != "" {
|
||||||
|
if profile, ok := c.profiles[c.activeProfile]; ok {
|
||||||
|
c.currentToken = profile.Token
|
||||||
|
c.currentUser = profile.UserUUID
|
||||||
|
c.baseURL = profile.BaseURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncClientToConfig updates config from client state
|
||||||
|
func (c *KVSClient) syncClientToConfig(config *Config) {
|
||||||
|
config.Profiles = c.profiles
|
||||||
|
config.ActiveProfile = c.activeProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveProfiles is a convenience method to save current client state
|
||||||
|
func (c *KVSClient) saveProfiles() error {
|
||||||
|
config, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
// If we can't load, create new config
|
||||||
|
config = &Config{Profiles: make(map[string]Profile)}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.syncClientToConfig(config)
|
||||||
|
|
||||||
|
if err := SaveConfig(config); err != nil {
|
||||||
|
fmt.Println(yellow("Warning: Failed to save profiles:"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
8
main.go
8
main.go
@@ -11,6 +11,11 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
client := NewKVSClient("http://localhost:8090")
|
client := NewKVSClient("http://localhost:8090")
|
||||||
|
|
||||||
|
// Load saved profiles
|
||||||
|
if config, err := LoadConfig(); err == nil {
|
||||||
|
client.syncConfigToClient(config)
|
||||||
|
}
|
||||||
|
|
||||||
// Setup readline
|
// Setup readline
|
||||||
rl, err := readline.NewEx(&readline.Config{
|
rl, err := readline.NewEx(&readline.Config{
|
||||||
Prompt: cyan("kvs> "),
|
Prompt: cyan("kvs> "),
|
||||||
@@ -26,6 +31,9 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println(magenta("KVS Interactive Shell"))
|
fmt.Println(magenta("KVS Interactive Shell"))
|
||||||
fmt.Println("Type 'help' for available commands")
|
fmt.Println("Type 'help' for available commands")
|
||||||
|
if client.activeProfile != "" {
|
||||||
|
fmt.Println(green("Active profile:"), client.activeProfile)
|
||||||
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
2
utils.go
2
utils.go
@@ -99,6 +99,8 @@ var completer = readline.NewPrefixCompleter(
|
|||||||
readline.PcItem("add"),
|
readline.PcItem("add"),
|
||||||
readline.PcItem("use"),
|
readline.PcItem("use"),
|
||||||
readline.PcItem("remove"),
|
readline.PcItem("remove"),
|
||||||
|
readline.PcItem("save"),
|
||||||
|
readline.PcItem("load"),
|
||||||
),
|
),
|
||||||
readline.PcItem("get"),
|
readline.PcItem("get"),
|
||||||
readline.PcItem("put"),
|
readline.PcItem("put"),
|
||||||
|
Reference in New Issue
Block a user