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:
2025-10-05 23:47:13 +03:00
parent 0c9e314d7c
commit 39a1d4482a
5 changed files with 159 additions and 1 deletions

View File

@@ -38,6 +38,7 @@ func (c *KVSClient) handleProfile(args []string) {
BaseURL: baseURL,
}
fmt.Println(green("Profile added:"), args[1])
c.saveProfiles()
case "use":
if len(args) < 2 {
@@ -54,6 +55,7 @@ func (c *KVSClient) handleProfile(args []string) {
c.baseURL = profile.BaseURL
c.activeProfile = args[1]
fmt.Println(green("Switched to profile:"), args[1])
c.saveProfiles()
case "remove":
if len(args) < 2 {
@@ -65,5 +67,29 @@ func (c *KVSClient) handleProfile(args []string) {
c.activeProfile = ""
}
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")
}
}

View File

@@ -27,10 +27,12 @@ KVS Interactive Shell - Available Commands:
Connection & Authentication:
connect <url> - Connect to KVS server
auth <token> - Set authentication token
profile - List all profiles
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
profile save - Save profiles to ~/.kvs/config.json
profile load - Load profiles from ~/.kvs/config.json
Key-Value Operations:
get <key> - Retrieve value for key

120
config.go Normal file
View 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
}

View File

@@ -11,6 +11,11 @@ import (
func main() {
client := NewKVSClient("http://localhost:8090")
// Load saved profiles
if config, err := LoadConfig(); err == nil {
client.syncConfigToClient(config)
}
// Setup readline
rl, err := readline.NewEx(&readline.Config{
Prompt: cyan("kvs> "),
@@ -26,6 +31,9 @@ func main() {
fmt.Println(magenta("KVS Interactive Shell"))
fmt.Println("Type 'help' for available commands")
if client.activeProfile != "" {
fmt.Println(green("Active profile:"), client.activeProfile)
}
fmt.Println()
for {

View File

@@ -99,6 +99,8 @@ var completer = readline.NewPrefixCompleter(
readline.PcItem("add"),
readline.PcItem("use"),
readline.PcItem("remove"),
readline.PcItem("save"),
readline.PcItem("load"),
),
readline.PcItem("get"),
readline.PcItem("put"),