Detailed changes
@@ -177,39 +177,39 @@ func coderAgent(r *vcr.Recorder, env fakeEnv, large, small fantasy.LanguageModel
// NOTE(@andreynering): Set a fixed config to ensure cassettes match
// independently of user config on `$HOME/.config/crush/crush.json`.
- cfg.Options.Attribution = &config.Attribution{
+ cfg.Config().Options.Attribution = &config.Attribution{
TrailerStyle: "co-authored-by",
GeneratedWith: true,
}
// Clear skills paths to ensure test reproducibility - user's skills
// would be included in prompt and break VCR cassette matching.
- cfg.Options.SkillsPaths = []string{}
+ cfg.Config().Options.SkillsPaths = []string{}
// Clear LSP config to ensure test reproducibility - user's LSP config
// would be included in prompt and break VCR cassette matching.
- cfg.LSP = nil
+ cfg.Config().LSP = nil
- systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), *cfg)
+ systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), *cfg.Config())
if err != nil {
return nil, err
}
// Get the model name for the bash tool
modelName := large.Model() // fallback to ID if Name not available
- if model := cfg.GetModel(large.Provider(), large.Model()); model != nil {
+ if model := cfg.Config().GetModel(large.Provider(), large.Model()); model != nil {
modelName = model.Name
}
allTools := []fantasy.AgentTool{
- tools.NewBashTool(env.permissions, env.workingDir, cfg.Options.Attribution, modelName),
+ tools.NewBashTool(env.permissions, env.workingDir, cfg.Config().Options.Attribution, modelName),
tools.NewDownloadTool(env.permissions, env.workingDir, r.GetDefaultClient()),
tools.NewEditTool(env.lspClients, env.permissions, env.history, *env.filetracker, env.workingDir),
tools.NewMultiEditTool(env.lspClients, env.permissions, env.history, *env.filetracker, env.workingDir),
tools.NewFetchTool(env.permissions, env.workingDir, r.GetDefaultClient()),
tools.NewGlobTool(env.workingDir),
tools.NewGrepTool(env.workingDir),
- tools.NewLsTool(env.permissions, env.workingDir, cfg.Tools.Ls),
+ tools.NewLsTool(env.permissions, env.workingDir, cfg.Config().Tools.Ls),
tools.NewSourcegraphTool(r.GetDefaultClient()),
tools.NewViewTool(env.lspClients, env.permissions, *env.filetracker, env.workingDir),
tools.NewWriteTool(env.lspClients, env.permissions, env.history, *env.filetracker, env.workingDir),
@@ -55,7 +55,7 @@ var logsCmd = &cobra.Command{
if err != nil {
return fmt.Errorf("failed to load configuration: %v", err)
}
- logsFile := filepath.Join(cfg.Options.DataDirectory, "logs", "crush.log")
+ logsFile := filepath.Join(cfg.Config().Options.DataDirectory, "logs", "crush.log")
_, err = os.Stat(logsFile)
if os.IsNotExist(err) {
log.Warn("Looks like you are not in a crush project. No logs found.")
@@ -38,7 +38,7 @@ crush models gpt5`,
return err
}
- if !cfg.IsConfigured() {
+ if !cfg.Config().IsConfigured() {
return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
}
@@ -55,7 +55,7 @@ crush models gpt5`,
var providerIDs []string
providerModels := make(map[string][]string)
- for providerID, provider := range cfg.Providers.Seq2() {
+ for providerID, provider := range cfg.Config().Providers.Seq2() {
if provider.Disable {
continue
}
@@ -195,10 +195,11 @@ func setupApp(cmd *cobra.Command) (*app.App, error) {
return nil, err
}
- cfg, err := config.Init(cwd, dataDir, debug)
+ svc, err := config.Init(cwd, dataDir, debug)
if err != nil {
return nil, err
}
+ cfg := svc.Config()
if cfg.Permissions == nil {
cfg.Permissions = &config.Permissions{}
@@ -124,11 +124,11 @@ func runStats(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
if dataDir == "" {
- cfg, err := config.Init("", "", false)
+ svc, err := config.Init("", "", false)
if err != nil {
return fmt.Errorf("failed to initialize config: %w", err)
}
- dataDir = cfg.Options.DataDirectory
+ dataDir = svc.Config().Options.DataDirectory
}
conn, err := db.Connect(ctx, dataDir)
@@ -18,12 +18,12 @@ type ProjectInitFlag struct {
Initialized bool `json:"initialized"`
}
-func Init(workingDir, dataDir string, debug bool) (*Config, error) {
- cfg, err := Load(workingDir, dataDir, debug)
+func Init(workingDir, dataDir string, debug bool) (*Service, error) {
+ svc, err := Load(workingDir, dataDir, debug)
if err != nil {
return nil, err
}
- return cfg, nil
+ return svc, nil
}
func ProjectNeedsInitialization(cfg *Config) (bool, error) {
@@ -30,7 +30,7 @@ import (
const defaultCatwalkURL = "https://catwalk.charm.sh"
// Load loads the configuration from the default paths.
-func Load(workingDir, dataDir string, debug bool) (*Config, error) {
+func Load(workingDir, dataDir string, debug bool) (*Service, error) {
configPaths := lookupConfigs(workingDir)
cfg, err := loadFromConfigPaths(configPaths)
@@ -38,8 +38,16 @@ func Load(workingDir, dataDir string, debug bool) (*Config, error) {
return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err)
}
+ store := NewFileStore(GlobalConfigData())
+
+ svc := &Service{
+ cfg: cfg,
+ store: store,
+ workingDir: workingDir,
+ }
+
+ // Keep dataConfigDir in sync for the transitional configStore() accessor.
cfg.dataConfigDir = GlobalConfigData()
- cfg.store = NewFileStore(cfg.dataConfigDir)
cfg.setDefaults(workingDir, dataDir)
@@ -73,26 +81,28 @@ func Load(workingDir, dataDir string, debug bool) (*Config, error) {
if err != nil {
return nil, err
}
+ svc.knownProviders = providers
cfg.knownProviders = providers
env := env.New()
// Configure providers
valueResolver := NewShellVariableResolver(env)
+ svc.resolver = valueResolver
cfg.resolver = valueResolver
- if err := cfg.configureProviders(env, valueResolver, cfg.knownProviders); err != nil {
+ if err := cfg.configureProviders(env, valueResolver, svc.knownProviders); err != nil {
return nil, fmt.Errorf("failed to configure providers: %w", err)
}
if !cfg.IsConfigured() {
slog.Warn("No providers configured")
- return cfg, nil
+ return svc, nil
}
- if err := cfg.configureSelectedModels(cfg.knownProviders); err != nil {
+ if err := cfg.configureSelectedModels(svc.knownProviders); err != nil {
return nil, fmt.Errorf("failed to configure selected models: %w", err)
}
cfg.SetupAgents()
- return cfg, nil
+ return svc, nil
}
func PushPopCrushEnv() func() {
@@ -0,0 +1,22 @@
+package config
+
+import "charm.land/catwalk/pkg/catwalk"
+
+// Service is the central access point for configuration. It wraps the
+// raw Config data and owns all internal state that was previously held
+// as unexported fields on Config (resolver, store, known providers,
+// working directory).
+type Service struct {
+ cfg *Config
+ store Store
+ resolver VariableResolver
+ workingDir string
+ knownProviders []catwalk.Provider
+}
+
+// Config returns the underlying Config struct. This is a temporary
+// escape hatch that will be removed once all callers migrate to
+// Service getter methods.
+func (s *Service) Config() *Config {
+ return s.cfg
+}