|
|
|
@@ -8,91 +8,287 @@ import (
|
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Config struct {
|
|
|
|
|
// DepoPath is the path to the jade depository. Default ~/jade-depository
|
|
|
|
|
DepoPath string `mapstructure:"depo_path"`
|
|
|
|
|
// TagPrefix is the prefix used for tags in notes. Default "+"
|
|
|
|
|
TagPrefix string `mapstructure:"tag_prefix"`
|
|
|
|
|
// 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"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getDefaultDepoPath returns the default depository path
|
|
|
|
|
func getDefaultDepoPath() string {
|
|
|
|
|
// 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 "./jade-depository"
|
|
|
|
|
return "./.config/jade/config.yml"
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(home, "jade-depository")
|
|
|
|
|
return filepath.Join(home, ".config", "jade", "config.yml")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getConfigPath returns the config file path within the depository
|
|
|
|
|
func getConfigPath(depoPath string) string {
|
|
|
|
|
return filepath.Join(depoPath, ".jade", "config.yml")
|
|
|
|
|
}
|
|
|
|
|
// loadGlobalConfig loads the global CLI configuration
|
|
|
|
|
func loadGlobalConfig() (*GlobalConfig, error) {
|
|
|
|
|
configPath := getGlobalConfigPath()
|
|
|
|
|
configDir := filepath.Dir(configPath)
|
|
|
|
|
|
|
|
|
|
// initConfig initializes the configuration using Viper
|
|
|
|
|
// Precedence order: flags > environment variables > config file > defaults
|
|
|
|
|
func initConfig(cfgPath, depoPath string) (*Config, error) {
|
|
|
|
|
// Set up environment variable support
|
|
|
|
|
viper.SetEnvPrefix("JADE")
|
|
|
|
|
viper.AutomaticEnv()
|
|
|
|
|
|
|
|
|
|
// Determine depository path (from flag, env var, or default)
|
|
|
|
|
actualDepoPath := depoPath
|
|
|
|
|
if actualDepoPath == "" {
|
|
|
|
|
actualDepoPath = viper.GetString("depo_path")
|
|
|
|
|
if actualDepoPath == "" {
|
|
|
|
|
actualDepoPath = getDefaultDepoPath()
|
|
|
|
|
}
|
|
|
|
|
// 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
|
|
|
|
|
viper.SetDefault("depo_path", actualDepoPath)
|
|
|
|
|
viper.SetDefault("tag_prefix", "+")
|
|
|
|
|
v.SetDefault("depositories", make(map[string]string))
|
|
|
|
|
v.SetDefault("default_depository", "")
|
|
|
|
|
|
|
|
|
|
// Ensure depository and .jade directory exist
|
|
|
|
|
jadeDir := filepath.Join(actualDepoPath, ".jade")
|
|
|
|
|
if err := os.MkdirAll(jadeDir, 0755); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create .jade directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine config file location
|
|
|
|
|
configFilePath := cfgPath
|
|
|
|
|
if configFilePath == "" {
|
|
|
|
|
configFilePath = getConfigPath(actualDepoPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set up config file
|
|
|
|
|
viper.SetConfigFile(configFilePath)
|
|
|
|
|
|
|
|
|
|
// Try to read existing config file
|
|
|
|
|
if err := viper.ReadInConfig(); err != nil {
|
|
|
|
|
// Check if file doesn't exist (handles both viper.ConfigFileNotFoundError and os.IsNotExist)
|
|
|
|
|
// 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 := viper.SafeWriteConfigAs(configFilePath); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create config file: %w", err)
|
|
|
|
|
if err := v.SafeWriteConfigAs(configPath); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create global config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Config file was found but another error occurred
|
|
|
|
|
return nil, fmt.Errorf("error reading config file: %w", err)
|
|
|
|
|
return nil, fmt.Errorf("error reading global config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Override with flags if provided (highest precedence)
|
|
|
|
|
if depoPath != "" {
|
|
|
|
|
viper.Set("depo_path", depoPath)
|
|
|
|
|
// Unmarshal into struct
|
|
|
|
|
cfg := &GlobalConfig{}
|
|
|
|
|
if err := v.Unmarshal(cfg); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to unmarshal global config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unmarshal config into struct
|
|
|
|
|
cfg := &Config{}
|
|
|
|
|
if err := viper.Unmarshal(cfg); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write config back to persist any changes (e.g., from flags or env vars)
|
|
|
|
|
if err := viper.WriteConfig(); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to write 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)
|
|
|
|
|
}
|
|
|
|
|