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}