From fa6942037745665e182e96d2225400c77a39660f Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Fri, 6 Feb 2026 12:31:29 +0100 Subject: [PATCH] refactor: introduce config.Service, Load/Init now return *Service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Service wraps *Config and owns internal state (store, resolver, knownProviders, workingDir). A temporary Config() escape hatch lets callers access the underlying struct during the migration. 🐾 Generated with Crush Assisted-by: Claude Opus 4.6 via Crush --- internal/agent/common_test.go | 14 +++++++------- internal/cmd/logs.go | 2 +- internal/cmd/models.go | 4 ++-- internal/cmd/root.go | 3 ++- internal/cmd/stats.go | 4 ++-- internal/config/init.go | 6 +++--- internal/config/load.go | 22 ++++++++++++++++------ internal/config/service.go | 22 ++++++++++++++++++++++ 8 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 internal/config/service.go diff --git a/internal/agent/common_test.go b/internal/agent/common_test.go index 4f96c3cfbb1728f533c71a7c05b7e1ab85975b45..9778c4fe6fd112b6af19d7e246aee028c406cae1 100644 --- a/internal/agent/common_test.go +++ b/internal/agent/common_test.go @@ -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), diff --git a/internal/cmd/logs.go b/internal/cmd/logs.go index 4c66d499a08393972ec1ad740ddb4c29293b88d9..3de9a88a96600aa6f14c76c0be4f11b24bc226dd 100644 --- a/internal/cmd/logs.go +++ b/internal/cmd/logs.go @@ -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.") diff --git a/internal/cmd/models.go b/internal/cmd/models.go index e2aa5c991d5cf49ba78dbff9d3f79c4f6493523d..f4fa559ebe41d93bee54ed5e2272f8fb0b8dc9ad 100644 --- a/internal/cmd/models.go +++ b/internal/cmd/models.go @@ -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 } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index cf6fd0909ebfdf1643e2ad4fc2de868a8b1e1c1a..f88f8ed6fce3cad33080a6585399cd08ab93e27f 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -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{} diff --git a/internal/cmd/stats.go b/internal/cmd/stats.go index 5dc971d1229350f35f93d5cf772239fa83e9206e..4f655f9b755223fc22b3914d3f5504d04275002c 100644 --- a/internal/cmd/stats.go +++ b/internal/cmd/stats.go @@ -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) diff --git a/internal/config/init.go b/internal/config/init.go index 5a4683f77485f54409d4372a33d1933b47abd33f..f2d96bd292ab6e1194dbc1b299079ef6cd6f9657 100644 --- a/internal/config/init.go +++ b/internal/config/init.go @@ -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) { diff --git a/internal/config/load.go b/internal/config/load.go index 15b597a05a3d0a58b6ecba33250a24a74a447894..02095b5839822e05eef31ff1d9917418a66898a8 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -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() { diff --git a/internal/config/service.go b/internal/config/service.go new file mode 100644 index 0000000000000000000000000000000000000000..5fd98bcb91b7586f44773fdde8b2da498db7f86b --- /dev/null +++ b/internal/config/service.go @@ -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 +}