load.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. package config
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "maps"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "runtime"
  13. "slices"
  14. "strconv"
  15. "strings"
  16. "testing"
  17. "github.com/charmbracelet/catwalk/pkg/catwalk"
  18. "github.com/charmbracelet/crush/internal/csync"
  19. "github.com/charmbracelet/crush/internal/env"
  20. "github.com/charmbracelet/crush/internal/fsext"
  21. "github.com/charmbracelet/crush/internal/home"
  22. "github.com/charmbracelet/crush/internal/log"
  23. powernapConfig "github.com/charmbracelet/x/powernap/pkg/config"
  24. )
  25. const defaultCatwalkURL = "https://catwalk.charm.sh"
  26. // LoadReader config via io.Reader.
  27. func LoadReader(fd io.Reader) (*Config, error) {
  28. data, err := io.ReadAll(fd)
  29. if err != nil {
  30. return nil, err
  31. }
  32. var config Config
  33. err = json.Unmarshal(data, &config)
  34. if err != nil {
  35. return nil, err
  36. }
  37. return &config, err
  38. }
  39. // Load loads the configuration from the default paths.
  40. func Load(workingDir, dataDir string, debug bool) (*Config, error) {
  41. configPaths := lookupConfigs(workingDir)
  42. cfg, err := loadFromConfigPaths(configPaths)
  43. if err != nil {
  44. return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err)
  45. }
  46. cfg.dataConfigDir = GlobalConfigData()
  47. cfg.setDefaults(workingDir, dataDir)
  48. if debug {
  49. cfg.Options.Debug = true
  50. }
  51. // Setup logs
  52. log.Setup(
  53. filepath.Join(cfg.Options.DataDirectory, "logs", fmt.Sprintf("%s.log", appName)),
  54. cfg.Options.Debug,
  55. )
  56. if !isInsideWorktree() {
  57. const depth = 2
  58. const items = 100
  59. slog.Warn("No git repository detected in working directory, will limit file walk operations", "depth", depth, "items", items)
  60. assignIfNil(&cfg.Tools.Ls.MaxDepth, depth)
  61. assignIfNil(&cfg.Tools.Ls.MaxItems, items)
  62. assignIfNil(&cfg.Options.TUI.Completions.MaxDepth, depth)
  63. assignIfNil(&cfg.Options.TUI.Completions.MaxItems, items)
  64. }
  65. // Load known providers, this loads the config from catwalk
  66. providers, err := Providers(cfg)
  67. if err != nil {
  68. return nil, err
  69. }
  70. cfg.knownProviders = providers
  71. env := env.New()
  72. // Configure providers
  73. valueResolver := NewShellVariableResolver(env)
  74. cfg.resolver = valueResolver
  75. if err := cfg.configureProviders(env, valueResolver, cfg.knownProviders); err != nil {
  76. return nil, fmt.Errorf("failed to configure providers: %w", err)
  77. }
  78. if !cfg.IsConfigured() {
  79. slog.Warn("No providers configured")
  80. return cfg, nil
  81. }
  82. if err := cfg.configureSelectedModels(cfg.knownProviders); err != nil {
  83. return nil, fmt.Errorf("failed to configure selected models: %w", err)
  84. }
  85. cfg.SetupAgents()
  86. return cfg, nil
  87. }
  88. func PushPopCrushEnv() func() {
  89. found := []string{}
  90. for _, ev := range os.Environ() {
  91. if strings.HasPrefix(ev, "CRUSH_") {
  92. pair := strings.SplitN(ev, "=", 2)
  93. if len(pair) != 2 {
  94. continue
  95. }
  96. found = append(found, strings.TrimPrefix(pair[0], "CRUSH_"))
  97. }
  98. }
  99. backups := make(map[string]string)
  100. for _, ev := range found {
  101. backups[ev] = os.Getenv(ev)
  102. }
  103. for _, ev := range found {
  104. os.Setenv(ev, os.Getenv("CRUSH_"+ev))
  105. }
  106. restore := func() {
  107. for k, v := range backups {
  108. os.Setenv(k, v)
  109. }
  110. }
  111. return restore
  112. }
  113. func (c *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []catwalk.Provider) error {
  114. knownProviderNames := make(map[string]bool)
  115. restore := PushPopCrushEnv()
  116. defer restore()
  117. for _, p := range knownProviders {
  118. knownProviderNames[string(p.ID)] = true
  119. config, configExists := c.Providers.Get(string(p.ID))
  120. // if the user configured a known provider we need to allow it to override a couple of parameters
  121. if configExists {
  122. if config.BaseURL != "" {
  123. p.APIEndpoint = config.BaseURL
  124. }
  125. if config.APIKey != "" {
  126. p.APIKey = config.APIKey
  127. }
  128. if len(config.Models) > 0 {
  129. models := []catwalk.Model{}
  130. seen := make(map[string]bool)
  131. for _, model := range config.Models {
  132. if seen[model.ID] {
  133. continue
  134. }
  135. seen[model.ID] = true
  136. if model.Name == "" {
  137. model.Name = model.ID
  138. }
  139. models = append(models, model)
  140. }
  141. for _, model := range p.Models {
  142. if seen[model.ID] {
  143. continue
  144. }
  145. seen[model.ID] = true
  146. if model.Name == "" {
  147. model.Name = model.ID
  148. }
  149. models = append(models, model)
  150. }
  151. p.Models = models
  152. }
  153. }
  154. headers := map[string]string{}
  155. if len(p.DefaultHeaders) > 0 {
  156. maps.Copy(headers, p.DefaultHeaders)
  157. }
  158. if len(config.ExtraHeaders) > 0 {
  159. maps.Copy(headers, config.ExtraHeaders)
  160. }
  161. prepared := ProviderConfig{
  162. ID: string(p.ID),
  163. Name: p.Name,
  164. BaseURL: p.APIEndpoint,
  165. APIKey: p.APIKey,
  166. APIKeyTemplate: p.APIKey, // Store original template for re-resolution
  167. OAuthToken: config.OAuthToken,
  168. Type: p.Type,
  169. Disable: config.Disable,
  170. SystemPromptPrefix: config.SystemPromptPrefix,
  171. ExtraHeaders: headers,
  172. ExtraBody: config.ExtraBody,
  173. ExtraParams: make(map[string]string),
  174. Models: p.Models,
  175. }
  176. if p.ID == catwalk.InferenceProviderAnthropic && config.OAuthToken != nil {
  177. prepared.SetupClaudeCode()
  178. }
  179. switch p.ID {
  180. // Handle specific providers that require additional configuration
  181. case catwalk.InferenceProviderVertexAI:
  182. if !hasVertexCredentials(env) {
  183. if configExists {
  184. slog.Warn("Skipping Vertex AI provider due to missing credentials")
  185. c.Providers.Del(string(p.ID))
  186. }
  187. continue
  188. }
  189. prepared.ExtraParams["project"] = env.Get("VERTEXAI_PROJECT")
  190. prepared.ExtraParams["location"] = env.Get("VERTEXAI_LOCATION")
  191. case catwalk.InferenceProviderAzure:
  192. endpoint, err := resolver.ResolveValue(p.APIEndpoint)
  193. if err != nil || endpoint == "" {
  194. if configExists {
  195. slog.Warn("Skipping Azure provider due to missing API endpoint", "provider", p.ID, "error", err)
  196. c.Providers.Del(string(p.ID))
  197. }
  198. continue
  199. }
  200. prepared.BaseURL = endpoint
  201. prepared.ExtraParams["apiVersion"] = env.Get("AZURE_OPENAI_API_VERSION")
  202. case catwalk.InferenceProviderBedrock:
  203. if !hasAWSCredentials(env) {
  204. if configExists {
  205. slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
  206. c.Providers.Del(string(p.ID))
  207. }
  208. continue
  209. }
  210. prepared.ExtraParams["region"] = env.Get("AWS_REGION")
  211. if prepared.ExtraParams["region"] == "" {
  212. prepared.ExtraParams["region"] = env.Get("AWS_DEFAULT_REGION")
  213. }
  214. for _, model := range p.Models {
  215. if !strings.HasPrefix(model.ID, "anthropic.") {
  216. return fmt.Errorf("bedrock provider only supports anthropic models for now, found: %s", model.ID)
  217. }
  218. }
  219. default:
  220. // if the provider api or endpoint are missing we skip them
  221. v, err := resolver.ResolveValue(p.APIKey)
  222. if v == "" || err != nil {
  223. if configExists {
  224. slog.Warn("Skipping provider due to missing API key", "provider", p.ID)
  225. c.Providers.Del(string(p.ID))
  226. }
  227. continue
  228. }
  229. }
  230. c.Providers.Set(string(p.ID), prepared)
  231. }
  232. // validate the custom providers
  233. for id, providerConfig := range c.Providers.Seq2() {
  234. if knownProviderNames[id] {
  235. continue
  236. }
  237. // Make sure the provider ID is set
  238. providerConfig.ID = id
  239. if providerConfig.Name == "" {
  240. providerConfig.Name = id // Use ID as name if not set
  241. }
  242. // default to OpenAI if not set
  243. if providerConfig.Type == "" {
  244. providerConfig.Type = catwalk.TypeOpenAICompat
  245. }
  246. if !slices.Contains(catwalk.KnownProviderTypes(), providerConfig.Type) {
  247. slog.Warn("Skipping custom provider due to unsupported provider type", "provider", id)
  248. c.Providers.Del(id)
  249. continue
  250. }
  251. if providerConfig.Disable {
  252. slog.Debug("Skipping custom provider due to disable flag", "provider", id)
  253. c.Providers.Del(id)
  254. continue
  255. }
  256. if providerConfig.APIKey == "" {
  257. slog.Warn("Provider is missing API key, this might be OK for local providers", "provider", id)
  258. }
  259. if providerConfig.BaseURL == "" {
  260. slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
  261. c.Providers.Del(id)
  262. continue
  263. }
  264. if len(providerConfig.Models) == 0 {
  265. slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
  266. c.Providers.Del(id)
  267. continue
  268. }
  269. apiKey, err := resolver.ResolveValue(providerConfig.APIKey)
  270. if apiKey == "" || err != nil {
  271. slog.Warn("Provider is missing API key, this might be OK for local providers", "provider", id)
  272. }
  273. baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
  274. if baseURL == "" || err != nil {
  275. slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
  276. c.Providers.Del(id)
  277. continue
  278. }
  279. c.Providers.Set(id, providerConfig)
  280. }
  281. return nil
  282. }
  283. func (c *Config) setDefaults(workingDir, dataDir string) {
  284. c.workingDir = workingDir
  285. if c.Options == nil {
  286. c.Options = &Options{}
  287. }
  288. if c.Options.TUI == nil {
  289. c.Options.TUI = &TUIOptions{}
  290. }
  291. if c.Options.ContextPaths == nil {
  292. c.Options.ContextPaths = []string{}
  293. }
  294. if dataDir != "" {
  295. c.Options.DataDirectory = dataDir
  296. } else if c.Options.DataDirectory == "" {
  297. if path, ok := fsext.LookupClosest(workingDir, defaultDataDirectory); ok {
  298. c.Options.DataDirectory = path
  299. } else {
  300. c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
  301. }
  302. }
  303. if c.Providers == nil {
  304. c.Providers = csync.NewMap[string, ProviderConfig]()
  305. }
  306. if c.Models == nil {
  307. c.Models = make(map[SelectedModelType]SelectedModel)
  308. }
  309. if c.RecentModels == nil {
  310. c.RecentModels = make(map[SelectedModelType][]SelectedModel)
  311. }
  312. if c.MCP == nil {
  313. c.MCP = make(map[string]MCPConfig)
  314. }
  315. if c.LSP == nil {
  316. c.LSP = make(map[string]LSPConfig)
  317. }
  318. // Apply defaults to LSP configurations
  319. c.applyLSPDefaults()
  320. // Add the default context paths if they are not already present
  321. c.Options.ContextPaths = append(defaultContextPaths, c.Options.ContextPaths...)
  322. slices.Sort(c.Options.ContextPaths)
  323. c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths)
  324. if str, ok := os.LookupEnv("CRUSH_DISABLE_PROVIDER_AUTO_UPDATE"); ok {
  325. c.Options.DisableProviderAutoUpdate, _ = strconv.ParseBool(str)
  326. }
  327. if c.Options.Attribution == nil {
  328. c.Options.Attribution = &Attribution{
  329. TrailerStyle: TrailerStyleAssistedBy,
  330. GeneratedWith: true,
  331. }
  332. } else if c.Options.Attribution.TrailerStyle == "" {
  333. // Migrate deprecated co_authored_by or apply default
  334. if c.Options.Attribution.CoAuthoredBy != nil {
  335. if *c.Options.Attribution.CoAuthoredBy {
  336. c.Options.Attribution.TrailerStyle = TrailerStyleCoAuthoredBy
  337. } else {
  338. c.Options.Attribution.TrailerStyle = TrailerStyleNone
  339. }
  340. } else {
  341. c.Options.Attribution.TrailerStyle = TrailerStyleAssistedBy
  342. }
  343. }
  344. if c.Options.InitializeAs == "" {
  345. c.Options.InitializeAs = defaultInitializeAs
  346. }
  347. }
  348. // applyLSPDefaults applies default values from powernap to LSP configurations
  349. func (c *Config) applyLSPDefaults() {
  350. // Get powernap's default configuration
  351. configManager := powernapConfig.NewManager()
  352. configManager.LoadDefaults()
  353. // Apply defaults to each LSP configuration
  354. for name, cfg := range c.LSP {
  355. // Try to get defaults from powernap based on name or command name.
  356. base, ok := configManager.GetServer(name)
  357. if !ok {
  358. base, ok = configManager.GetServer(cfg.Command)
  359. if !ok {
  360. continue
  361. }
  362. }
  363. if cfg.Options == nil {
  364. cfg.Options = base.Settings
  365. }
  366. if cfg.InitOptions == nil {
  367. cfg.InitOptions = base.InitOptions
  368. }
  369. if len(cfg.FileTypes) == 0 {
  370. cfg.FileTypes = base.FileTypes
  371. }
  372. if len(cfg.RootMarkers) == 0 {
  373. cfg.RootMarkers = base.RootMarkers
  374. }
  375. if cfg.Command == "" {
  376. cfg.Command = base.Command
  377. }
  378. if len(cfg.Args) == 0 {
  379. cfg.Args = base.Args
  380. }
  381. if len(cfg.Env) == 0 {
  382. cfg.Env = base.Environment
  383. }
  384. // Update the config in the map
  385. c.LSP[name] = cfg
  386. }
  387. }
  388. func (c *Config) defaultModelSelection(knownProviders []catwalk.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
  389. if len(knownProviders) == 0 && c.Providers.Len() == 0 {
  390. err = fmt.Errorf("no providers configured, please configure at least one provider")
  391. return largeModel, smallModel, err
  392. }
  393. // Use the first provider enabled based on the known providers order
  394. // if no provider found that is known use the first provider configured
  395. for _, p := range knownProviders {
  396. providerConfig, ok := c.Providers.Get(string(p.ID))
  397. if !ok || providerConfig.Disable {
  398. continue
  399. }
  400. defaultLargeModel := c.GetModel(string(p.ID), p.DefaultLargeModelID)
  401. if defaultLargeModel == nil {
  402. err = fmt.Errorf("default large model %s not found for provider %s", p.DefaultLargeModelID, p.ID)
  403. return largeModel, smallModel, err
  404. }
  405. largeModel = SelectedModel{
  406. Provider: string(p.ID),
  407. Model: defaultLargeModel.ID,
  408. MaxTokens: defaultLargeModel.DefaultMaxTokens,
  409. ReasoningEffort: defaultLargeModel.DefaultReasoningEffort,
  410. }
  411. defaultSmallModel := c.GetModel(string(p.ID), p.DefaultSmallModelID)
  412. if defaultSmallModel == nil {
  413. err = fmt.Errorf("default small model %s not found for provider %s", p.DefaultSmallModelID, p.ID)
  414. return largeModel, smallModel, err
  415. }
  416. smallModel = SelectedModel{
  417. Provider: string(p.ID),
  418. Model: defaultSmallModel.ID,
  419. MaxTokens: defaultSmallModel.DefaultMaxTokens,
  420. ReasoningEffort: defaultSmallModel.DefaultReasoningEffort,
  421. }
  422. return largeModel, smallModel, err
  423. }
  424. enabledProviders := c.EnabledProviders()
  425. slices.SortFunc(enabledProviders, func(a, b ProviderConfig) int {
  426. return strings.Compare(a.ID, b.ID)
  427. })
  428. if len(enabledProviders) == 0 {
  429. err = fmt.Errorf("no providers configured, please configure at least one provider")
  430. return largeModel, smallModel, err
  431. }
  432. providerConfig := enabledProviders[0]
  433. if len(providerConfig.Models) == 0 {
  434. err = fmt.Errorf("provider %s has no models configured", providerConfig.ID)
  435. return largeModel, smallModel, err
  436. }
  437. defaultLargeModel := c.GetModel(providerConfig.ID, providerConfig.Models[0].ID)
  438. largeModel = SelectedModel{
  439. Provider: providerConfig.ID,
  440. Model: defaultLargeModel.ID,
  441. MaxTokens: defaultLargeModel.DefaultMaxTokens,
  442. }
  443. defaultSmallModel := c.GetModel(providerConfig.ID, providerConfig.Models[0].ID)
  444. smallModel = SelectedModel{
  445. Provider: providerConfig.ID,
  446. Model: defaultSmallModel.ID,
  447. MaxTokens: defaultSmallModel.DefaultMaxTokens,
  448. }
  449. return largeModel, smallModel, err
  450. }
  451. func (c *Config) configureSelectedModels(knownProviders []catwalk.Provider) error {
  452. defaultLarge, defaultSmall, err := c.defaultModelSelection(knownProviders)
  453. if err != nil {
  454. return fmt.Errorf("failed to select default models: %w", err)
  455. }
  456. large, small := defaultLarge, defaultSmall
  457. largeModelSelected, largeModelConfigured := c.Models[SelectedModelTypeLarge]
  458. if largeModelConfigured {
  459. if largeModelSelected.Model != "" {
  460. large.Model = largeModelSelected.Model
  461. }
  462. if largeModelSelected.Provider != "" {
  463. large.Provider = largeModelSelected.Provider
  464. }
  465. model := c.GetModel(large.Provider, large.Model)
  466. if model == nil {
  467. large = defaultLarge
  468. // override the model type to large
  469. err := c.UpdatePreferredModel(SelectedModelTypeLarge, large)
  470. if err != nil {
  471. return fmt.Errorf("failed to update preferred large model: %w", err)
  472. }
  473. } else {
  474. if largeModelSelected.MaxTokens > 0 {
  475. large.MaxTokens = largeModelSelected.MaxTokens
  476. } else {
  477. large.MaxTokens = model.DefaultMaxTokens
  478. }
  479. if largeModelSelected.ReasoningEffort != "" {
  480. large.ReasoningEffort = largeModelSelected.ReasoningEffort
  481. }
  482. large.Think = largeModelSelected.Think
  483. if largeModelSelected.Temperature != nil {
  484. large.Temperature = largeModelSelected.Temperature
  485. }
  486. if largeModelSelected.TopP != nil {
  487. large.TopP = largeModelSelected.TopP
  488. }
  489. if largeModelSelected.TopK != nil {
  490. large.TopK = largeModelSelected.TopK
  491. }
  492. if largeModelSelected.FrequencyPenalty != nil {
  493. large.FrequencyPenalty = largeModelSelected.FrequencyPenalty
  494. }
  495. if largeModelSelected.PresencePenalty != nil {
  496. large.PresencePenalty = largeModelSelected.PresencePenalty
  497. }
  498. }
  499. }
  500. smallModelSelected, smallModelConfigured := c.Models[SelectedModelTypeSmall]
  501. if smallModelConfigured {
  502. if smallModelSelected.Model != "" {
  503. small.Model = smallModelSelected.Model
  504. }
  505. if smallModelSelected.Provider != "" {
  506. small.Provider = smallModelSelected.Provider
  507. }
  508. model := c.GetModel(small.Provider, small.Model)
  509. if model == nil {
  510. small = defaultSmall
  511. // override the model type to small
  512. err := c.UpdatePreferredModel(SelectedModelTypeSmall, small)
  513. if err != nil {
  514. return fmt.Errorf("failed to update preferred small model: %w", err)
  515. }
  516. } else {
  517. if smallModelSelected.MaxTokens > 0 {
  518. small.MaxTokens = smallModelSelected.MaxTokens
  519. } else {
  520. small.MaxTokens = model.DefaultMaxTokens
  521. }
  522. if smallModelSelected.ReasoningEffort != "" {
  523. small.ReasoningEffort = smallModelSelected.ReasoningEffort
  524. }
  525. if smallModelSelected.Temperature != nil {
  526. small.Temperature = smallModelSelected.Temperature
  527. }
  528. if smallModelSelected.TopP != nil {
  529. small.TopP = smallModelSelected.TopP
  530. }
  531. if smallModelSelected.TopK != nil {
  532. small.TopK = smallModelSelected.TopK
  533. }
  534. if smallModelSelected.FrequencyPenalty != nil {
  535. small.FrequencyPenalty = smallModelSelected.FrequencyPenalty
  536. }
  537. if smallModelSelected.PresencePenalty != nil {
  538. small.PresencePenalty = smallModelSelected.PresencePenalty
  539. }
  540. small.Think = smallModelSelected.Think
  541. }
  542. }
  543. c.Models[SelectedModelTypeLarge] = large
  544. c.Models[SelectedModelTypeSmall] = small
  545. return nil
  546. }
  547. // lookupConfigs searches config files recursively from CWD up to FS root
  548. func lookupConfigs(cwd string) []string {
  549. // prepend default config paths
  550. configPaths := []string{
  551. GlobalConfig(),
  552. GlobalConfigData(),
  553. }
  554. configNames := []string{appName + ".json", "." + appName + ".json"}
  555. foundConfigs, err := fsext.Lookup(cwd, configNames...)
  556. if err != nil {
  557. // returns at least default configs
  558. return configPaths
  559. }
  560. // reverse order so last config has more priority
  561. slices.Reverse(foundConfigs)
  562. return append(configPaths, foundConfigs...)
  563. }
  564. func loadFromConfigPaths(configPaths []string) (*Config, error) {
  565. var configs []io.Reader
  566. for _, path := range configPaths {
  567. fd, err := os.Open(path)
  568. if err != nil {
  569. if os.IsNotExist(err) {
  570. continue
  571. }
  572. return nil, fmt.Errorf("failed to open config file %s: %w", path, err)
  573. }
  574. defer fd.Close()
  575. configs = append(configs, fd)
  576. }
  577. return loadFromReaders(configs)
  578. }
  579. func loadFromReaders(readers []io.Reader) (*Config, error) {
  580. if len(readers) == 0 {
  581. return &Config{}, nil
  582. }
  583. merged, err := Merge(readers)
  584. if err != nil {
  585. return nil, fmt.Errorf("failed to merge configuration readers: %w", err)
  586. }
  587. return LoadReader(merged)
  588. }
  589. func hasVertexCredentials(env env.Env) bool {
  590. hasProject := env.Get("VERTEXAI_PROJECT") != ""
  591. hasLocation := env.Get("VERTEXAI_LOCATION") != ""
  592. return hasProject && hasLocation
  593. }
  594. func hasAWSCredentials(env env.Env) bool {
  595. if env.Get("AWS_BEARER_TOKEN_BEDROCK") != "" {
  596. return true
  597. }
  598. if env.Get("AWS_ACCESS_KEY_ID") != "" && env.Get("AWS_SECRET_ACCESS_KEY") != "" {
  599. return true
  600. }
  601. if env.Get("AWS_PROFILE") != "" || env.Get("AWS_DEFAULT_PROFILE") != "" {
  602. return true
  603. }
  604. if env.Get("AWS_REGION") != "" || env.Get("AWS_DEFAULT_REGION") != "" {
  605. return true
  606. }
  607. if env.Get("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
  608. env.Get("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
  609. return true
  610. }
  611. if _, err := os.Stat(filepath.Join(home.Dir(), ".aws/credentials")); err == nil && !testing.Testing() {
  612. return true
  613. }
  614. return false
  615. }
  616. // GlobalConfig returns the global configuration file path for the application.
  617. func GlobalConfig() string {
  618. xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
  619. if xdgConfigHome != "" {
  620. return filepath.Join(xdgConfigHome, appName, fmt.Sprintf("%s.json", appName))
  621. }
  622. return filepath.Join(home.Dir(), ".config", appName, fmt.Sprintf("%s.json", appName))
  623. }
  624. // GlobalConfigData returns the path to the main data directory for the application.
  625. // this config is used when the app overrides configurations instead of updating the global config.
  626. func GlobalConfigData() string {
  627. xdgDataHome := os.Getenv("XDG_DATA_HOME")
  628. if xdgDataHome != "" {
  629. return filepath.Join(xdgDataHome, appName, fmt.Sprintf("%s.json", appName))
  630. }
  631. // return the path to the main data directory
  632. // for windows, it should be in `%LOCALAPPDATA%/crush/`
  633. // for linux and macOS, it should be in `$HOME/.local/share/crush/`
  634. if runtime.GOOS == "windows" {
  635. localAppData := os.Getenv("LOCALAPPDATA")
  636. if localAppData == "" {
  637. localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
  638. }
  639. return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
  640. }
  641. return filepath.Join(home.Dir(), ".local", "share", appName, fmt.Sprintf("%s.json", appName))
  642. }
  643. func assignIfNil[T any](ptr **T, val T) {
  644. if *ptr == nil {
  645. *ptr = &val
  646. }
  647. }
  648. func isInsideWorktree() bool {
  649. bts, err := exec.CommandContext(
  650. context.Background(),
  651. "git", "rev-parse",
  652. "--is-inside-work-tree",
  653. ).CombinedOutput()
  654. return err == nil && strings.TrimSpace(string(bts)) == "true"
  655. }