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 [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) }