Files
gems/jade-depo/internal/engine/config.go
T
joakim 1d87d93172 Add multi-depository support with global config
- Implement global CLI config at ~/.config/jade/config.yml
- Add jade depo commands (add, list, remove, set-default)
- Support depository short names and context-aware detection
- Remove tag_prefix config, hardcode + syntax for consistency
- Update depository resolution: flag -> context -> default
- Auto-initialize .jade/ directory structure when adding depos
- Update documentation with new multi-depository workflow
2026-01-03 16:25:23 +01:00

295 lines
8.3 KiB
Go

package engine
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/viper"
)
// GlobalConfig represents the CLI-level configuration
type GlobalConfig struct {
// Depositories maps depository names to their filesystem paths
Depositories map[string]string `mapstructure:"depositories"`
// DefaultDepository is the name of the default depository to use
DefaultDepository string `mapstructure:"default_depository"`
}
// Config represents the runtime configuration for a specific depository
type Config struct {
// DepoPath is the path to the active jade depository
DepoPath string
// DepoName is the name of the active depository (if registered)
DepoName string
}
// getGlobalConfigPath returns the path to the global CLI config
func getGlobalConfigPath() string {
home, err := os.UserHomeDir()
if err != nil {
return "./.config/jade/config.yml"
}
return filepath.Join(home, ".config", "jade", "config.yml")
}
// loadGlobalConfig loads the global CLI configuration
func loadGlobalConfig() (*GlobalConfig, error) {
configPath := getGlobalConfigPath()
configDir := filepath.Dir(configPath)
// Ensure config directory exists
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %w", err)
}
// Create a new viper instance for global config
v := viper.New()
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
// Set defaults
v.SetDefault("depositories", make(map[string]string))
v.SetDefault("default_depository", "")
// Try to read existing config
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok || os.IsNotExist(err) {
// Config file not found, create it with defaults
if err := v.SafeWriteConfigAs(configPath); err != nil {
return nil, fmt.Errorf("failed to create global config: %w", err)
}
} else {
return nil, fmt.Errorf("error reading global config: %w", err)
}
}
// Unmarshal into struct
cfg := &GlobalConfig{}
if err := v.Unmarshal(cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal global config: %w", err)
}
// Initialize map if nil
if cfg.Depositories == nil {
cfg.Depositories = make(map[string]string)
}
return cfg, nil
}
// saveGlobalConfig saves the global CLI configuration
func saveGlobalConfig(cfg *GlobalConfig) error {
configPath := getGlobalConfigPath()
configDir := filepath.Dir(configPath)
// Ensure config directory exists
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
// Create a new viper instance
v := viper.New()
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
// Set values
v.Set("depositories", cfg.Depositories)
v.Set("default_depository", cfg.DefaultDepository)
// Write config
if err := v.WriteConfig(); err != nil {
// If file doesn't exist, use SafeWriteConfigAs
if os.IsNotExist(err) {
if err := v.SafeWriteConfigAs(configPath); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
} else {
return fmt.Errorf("failed to write config file: %w", err)
}
}
return nil
}
// resolveDepository resolves a depository name or path to an absolute path
// Priority: 1) depoInput (name or path), 2) context (PWD), 3) global default
func resolveDepository(globalCfg *GlobalConfig, depoInput string) (path string, name string, err error) {
// 1. If depoInput is provided, check if it's a name or path
if depoInput != "" {
// Check if it's a registered name
if depoPath, exists := globalCfg.Depositories[depoInput]; exists {
return depoPath, depoInput, nil
}
// Otherwise treat it as a literal path
absPath, err := filepath.Abs(depoInput)
if err != nil {
return "", "", fmt.Errorf("invalid path: %w", err)
}
// Check if this path is registered under a name
for name, path := range globalCfg.Depositories {
if path == absPath {
return absPath, name, nil
}
}
return absPath, "", nil
}
// 2. Check if current working directory is inside a registered depository
cwd, err := os.Getwd()
if err == nil {
for name, depoPath := range globalCfg.Depositories {
absDepoPath, err := filepath.Abs(depoPath)
if err != nil {
continue
}
// Check if cwd is inside this depository
relPath, err := filepath.Rel(absDepoPath, cwd)
if err == nil && !filepath.IsAbs(relPath) && len(relPath) > 0 && relPath[0] != '.' {
return absDepoPath, name, nil
}
// Check if cwd exactly matches the depository
if cwd == absDepoPath {
return absDepoPath, name, nil
}
}
}
// 3. Use default depository from global config
if globalCfg.DefaultDepository != "" {
if depoPath, exists := globalCfg.Depositories[globalCfg.DefaultDepository]; exists {
return depoPath, globalCfg.DefaultDepository, nil
}
}
// No depository found
return "", "", fmt.Errorf("no depository configured. Run 'jade depo add <name> [path]' to add one")
}
// initConfig initializes the configuration for a specific depository
func initConfig(depoInput string) (*Config, error) {
// Load global config
globalCfg, err := loadGlobalConfig()
if err != nil {
return nil, fmt.Errorf("failed to load global config: %w", err)
}
// Resolve depository
depoPath, depoName, err := resolveDepository(globalCfg, depoInput)
if err != nil {
return nil, err
}
// Ensure depository and .jade directory exist
jadeDir := filepath.Join(depoPath, ".jade")
if err := os.MkdirAll(jadeDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create .jade directory: %w", err)
}
// Create trash directory
trashDir := filepath.Join(jadeDir, "trash")
if err := os.MkdirAll(trashDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create trash directory: %w", err)
}
return &Config{
DepoPath: depoPath,
DepoName: depoName,
}, nil
}
// AddDepository adds a new depository to the global config
func AddDepository(name, path string) error {
globalCfg, err := loadGlobalConfig()
if err != nil {
return fmt.Errorf("failed to load global config: %w", err)
}
// Check if name already exists
if _, exists := globalCfg.Depositories[name]; exists {
return fmt.Errorf("depository '%s' already exists", name)
}
// Make path absolute
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("invalid path: %w", err)
}
// Initialize depository structure
jadeDir := filepath.Join(absPath, ".jade")
if err := os.MkdirAll(jadeDir, 0755); err != nil {
return fmt.Errorf("failed to create .jade directory: %w", err)
}
trashDir := filepath.Join(jadeDir, "trash")
if err := os.MkdirAll(trashDir, 0755); err != nil {
return fmt.Errorf("failed to create trash directory: %w", err)
}
// Add to config
globalCfg.Depositories[name] = absPath
// If this is the first depository, make it default
if globalCfg.DefaultDepository == "" {
globalCfg.DefaultDepository = name
}
return saveGlobalConfig(globalCfg)
}
// ListDepositories returns all depositories and the default one
func ListDepositories() (map[string]string, string, error) {
globalCfg, err := loadGlobalConfig()
if err != nil {
return nil, "", fmt.Errorf("failed to load global config: %w", err)
}
return globalCfg.Depositories, globalCfg.DefaultDepository, nil
}
// RemoveDepository removes a depository from the global config
func RemoveDepository(name string) error {
globalCfg, err := loadGlobalConfig()
if err != nil {
return fmt.Errorf("failed to load global config: %w", err)
}
// Check if depository exists
if _, exists := globalCfg.Depositories[name]; !exists {
return fmt.Errorf("depository '%s' not found", name)
}
// Remove from map
delete(globalCfg.Depositories, name)
// If this was the default, clear default
if globalCfg.DefaultDepository == name {
globalCfg.DefaultDepository = ""
// Set first remaining depository as default if any exist
for depoName := range globalCfg.Depositories {
globalCfg.DefaultDepository = depoName
break
}
}
return saveGlobalConfig(globalCfg)
}
// SetDefaultDepository sets the default depository
func SetDefaultDepository(name string) error {
globalCfg, err := loadGlobalConfig()
if err != nil {
return fmt.Errorf("failed to load global config: %w", err)
}
// Check if depository exists
if _, exists := globalCfg.Depositories[name]; !exists {
return fmt.Errorf("depository '%s' not found", name)
}
globalCfg.DefaultDepository = name
return saveGlobalConfig(globalCfg)
}