1package config
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"slices"
  8	"strings"
  9	"sync/atomic"
 10
 11	"github.com/charmbracelet/crush/internal/fsext"
 12)
 13
 14const (
 15	InitFlagFilename = "init"
 16)
 17
 18type ProjectInitFlag struct {
 19	Initialized bool `json:"initialized"`
 20}
 21
 22// TODO: we need to remove the global config instance keeping it now just until everything is migrated
 23var instance atomic.Pointer[Config]
 24
 25func Init(workingDir, dataDir string, debug bool) (*Config, error) {
 26	cfg, err := Load(workingDir, dataDir, debug)
 27	if err != nil {
 28		return nil, err
 29	}
 30	instance.Store(cfg)
 31	return instance.Load(), nil
 32}
 33
 34func Get() *Config {
 35	cfg := instance.Load()
 36	return cfg
 37}
 38
 39func ProjectNeedsInitialization() (bool, error) {
 40	cfg := Get()
 41	if cfg == nil {
 42		return false, fmt.Errorf("config not loaded")
 43	}
 44
 45	flagFilePath := filepath.Join(cfg.Options.DataDirectory, InitFlagFilename)
 46
 47	_, err := os.Stat(flagFilePath)
 48	if err == nil {
 49		return false, nil
 50	}
 51
 52	if !os.IsNotExist(err) {
 53		return false, fmt.Errorf("failed to check init flag file: %w", err)
 54	}
 55
 56	someContextFileExists, err := contextPathsExist(cfg.WorkingDir())
 57	if err != nil {
 58		return false, fmt.Errorf("failed to check for context files: %w", err)
 59	}
 60	if someContextFileExists {
 61		return false, nil
 62	}
 63
 64	// If the working directory has no non-ignored files, skip initialization step
 65	empty, err := dirHasNoVisibleFiles(cfg.WorkingDir())
 66	if err != nil {
 67		return false, fmt.Errorf("failed to check if directory is empty: %w", err)
 68	}
 69	if empty {
 70		return false, nil
 71	}
 72
 73	return true, nil
 74}
 75
 76func contextPathsExist(dir string) (bool, error) {
 77	entries, err := os.ReadDir(dir)
 78	if err != nil {
 79		return false, err
 80	}
 81
 82	// Create a slice of lowercase filenames for lookup with slices.Contains
 83	var files []string
 84	for _, entry := range entries {
 85		if !entry.IsDir() {
 86			files = append(files, strings.ToLower(entry.Name()))
 87		}
 88	}
 89
 90	// Check if any of the default context paths exist in the directory
 91	for _, path := range defaultContextPaths {
 92		// Extract just the filename from the path
 93		_, filename := filepath.Split(path)
 94		filename = strings.ToLower(filename)
 95
 96		if slices.Contains(files, filename) {
 97			return true, nil
 98		}
 99	}
100
101	return false, nil
102}
103
104// dirHasNoVisibleFiles returns true if the directory has no files/dirs after applying ignore rules
105func dirHasNoVisibleFiles(dir string) (bool, error) {
106	files, _, err := fsext.ListDirectory(dir, nil, 1, 1)
107	if err != nil {
108		return false, err
109	}
110	return len(files) == 0, nil
111}
112
113func MarkProjectInitialized() error {
114	cfg := Get()
115	if cfg == nil {
116		return fmt.Errorf("config not loaded")
117	}
118	flagFilePath := filepath.Join(cfg.Options.DataDirectory, InitFlagFilename)
119
120	file, err := os.Create(flagFilePath)
121	if err != nil {
122		return fmt.Errorf("failed to create init flag file: %w", err)
123	}
124	defer file.Close()
125
126	return nil
127}
128
129func HasInitialDataConfig() bool {
130	cfgPath := GlobalConfigData()
131	if _, err := os.Stat(cfgPath); err != nil {
132		return false
133	}
134	return Get().IsConfigured()
135}