on main help sign in
fix(config): always resolve the data directory to an absolute path (#2883)
c853a93Christian Rocha
internal/agent/prompt/prompt.go+2−4
@@ -13,6 +13,7 @@ import (
13 "time"
13 "time"
14
14
15 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/config"
16+ "github.com/charmbracelet/crush/internal/filepathext"
16 "github.com/charmbracelet/crush/internal/home"
17 "github.com/charmbracelet/crush/internal/home"
17 "github.com/charmbracelet/crush/internal/shell"
18 "github.com/charmbracelet/crush/internal/shell"
18 "github.com/charmbracelet/crush/internal/skills"
19 "github.com/charmbracelet/crush/internal/skills"
@@ -107,10 +108,7 @@ func processFile(filePath string) *ContextFile {
107
108
108func processContextPath(p string, store *config.ConfigStore) []ContextFile {
109func processContextPath(p string, store *config.ConfigStore) []ContextFile {
109 var contexts []ContextFile
110 var contexts []ContextFile
110− fullPath := p
111− if !filepath.IsAbs(p) {
112− fullPath = filepath.Join(store.WorkingDir(), p)
111+ fullPath := filepathext.SmartJoin(store.WorkingDir(), p)
113− }
114 info, err := os.Stat(fullPath)
112 info, err := os.Stat(fullPath)
115 if err != nil {
113 if err != nil {
116 return contexts
114 return contexts
internal/agent/tools/glob.go+2−4
@@ -13,6 +13,7 @@ import (
13 "strings"
13 "strings"
14
14
15 "charm.land/fantasy"
15 "charm.land/fantasy"
16+ "github.com/charmbracelet/crush/internal/filepathext"
16 "github.com/charmbracelet/crush/internal/fsext"
17 "github.com/charmbracelet/crush/internal/fsext"
17)
18)
18
19
@@ -96,10 +97,7 @@ func runRipgrep(cmd *exec.Cmd, searchRoot string, limit int) ([]string, error) {
96 if len(p) == 0 {
97 if len(p) == 0 {
97 continue
98 continue
98 }
99 }
99− absPath := string(p)
100+ absPath := filepathext.SmartJoin(searchRoot, string(p))
100− if !filepath.IsAbs(absPath) {
101− absPath = filepath.Join(searchRoot, absPath)
102− }
103 if fsext.SkipHidden(absPath) {
101 if fsext.SkipHidden(absPath) {
104 continue
102 continue
105 }
103 }
internal/config/load.go+2
@@ -21,6 +21,7 @@ import (
21 "github.com/charmbracelet/crush/internal/agent/hyper"
21 "github.com/charmbracelet/crush/internal/agent/hyper"
22 "github.com/charmbracelet/crush/internal/csync"
22 "github.com/charmbracelet/crush/internal/csync"
23 "github.com/charmbracelet/crush/internal/env"
23 "github.com/charmbracelet/crush/internal/env"
24+ "github.com/charmbracelet/crush/internal/filepathext"
24 "github.com/charmbracelet/crush/internal/fsext"
25 "github.com/charmbracelet/crush/internal/fsext"
25 "github.com/charmbracelet/crush/internal/home"
26 "github.com/charmbracelet/crush/internal/home"
26 powernapConfig "github.com/charmbracelet/x/powernap/pkg/config"
27 powernapConfig "github.com/charmbracelet/x/powernap/pkg/config"
@@ -429,6 +430,7 @@ func (c *Config) setDefaults(workingDir, dataDir string) {
429 c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
430 c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
430 }
431 }
431 }
432 }
433+ c.Options.DataDirectory = filepath.Clean(filepathext.SmartJoin(workingDir, c.Options.DataDirectory))
432 if c.Providers == nil {
434 if c.Providers == nil {
433 c.Providers = csync.NewMap[string, ProviderConfig]()
435 c.Providers = csync.NewMap[string, ProviderConfig]()
434 }
436 }
internal/config/load_test.go+35−14
@@ -74,22 +74,43 @@ func testStore(cfg *Config) *ConfigStore {
74}
74}
75
75
76func TestConfig_setDefaults(t *testing.T) {
76func TestConfig_setDefaults(t *testing.T) {
77+ t.Run("sets default data directory", func(t *testing.T) {
77− cfg := &Config{}
78+ cfg := &Config{}
79+ workingDir := t.TempDir()
80+
81+ cfg.setDefaults(workingDir, "")
82+
83+ require.NotNil(t, cfg.Options)
84+ require.NotNil(t, cfg.Options.TUI)
85+ require.NotNil(t, cfg.Options.ContextPaths)
86+ require.NotNil(t, cfg.Providers)
87+ require.NotNil(t, cfg.Models)
88+ require.NotNil(t, cfg.LSP)
89+ require.NotNil(t, cfg.MCP)
90+ require.Equal(t, filepath.Join(workingDir, ".crush"), cfg.Options.DataDirectory)
91+ require.Equal(t, "AGENTS.md", cfg.Options.InitializeAs)
92+ for _, path := range defaultContextPaths {
93+ require.Contains(t, cfg.Options.ContextPaths, path)
94+ }
95+ })
78
96
79− cfg.setDefaults("/tmp", "")
97+ t.Run("resolves relative configured data directory from working directory", func(t *testing.T) {
98+ cfg := &Config{Options: &Options{DataDirectory: "."}}
99+ workingDir := filepath.Join(t.TempDir(), "worktree")
80
100
81− require.NotNil(t, cfg.Options)
101+ cfg.setDefaults(workingDir, "")
102+
82− require.NotNil(t, cfg.Options.TUI)
103+ require.Equal(t, workingDir, cfg.Options.DataDirectory)
83− require.NotNil(t, cfg.Options.ContextPaths)
104+ })
84− require.NotNil(t, cfg.Providers)
105+
85− require.NotNil(t, cfg.Models)
106+ t.Run("resolves relative flag data directory from working directory", func(t *testing.T) {
86− require.NotNil(t, cfg.LSP)
107+ cfg := &Config{}
87− require.NotNil(t, cfg.MCP)
108+ workingDir := filepath.Join(t.TempDir(), "worktree")
88− require.Equal(t, filepath.Join("/tmp", ".crush"), cfg.Options.DataDirectory)
109+
89− require.Equal(t, "AGENTS.md", cfg.Options.InitializeAs)
110+ cfg.setDefaults(workingDir, "./state")
90− for _, path := range defaultContextPaths {
111+
91− require.Contains(t, cfg.Options.ContextPaths, path)
112+ require.Equal(t, filepath.Join(workingDir, "state"), cfg.Options.DataDirectory)
92− }
113+ })
93}
114}
94
115
95func TestConfig_configureProviders(t *testing.T) {
116func TestConfig_configureProviders(t *testing.T) {
internal/shell/dispatch.go+2−4
@@ -14,6 +14,7 @@ import (
14 "runtime"
14 "runtime"
15 "strings"
15 "strings"
16
16
17+ "github.com/charmbracelet/crush/internal/filepathext"
17 "mvdan.cc/sh/v3/expand"
18 "mvdan.cc/sh/v3/expand"
18 "mvdan.cc/sh/v3/interp"
19 "mvdan.cc/sh/v3/interp"
19 "mvdan.cc/sh/v3/syntax"
20 "mvdan.cc/sh/v3/syntax"
@@ -51,13 +52,10 @@ func scriptDispatchHandler(blockFuncs []BlockFunc) func(next interp.ExecHandlerF
51 return next(ctx, args)
52 return next(ctx, args)
52 }
53 }
53
54
54− scriptPath := args[0]
55 // Resolve relative paths against the interpreter's cwd, not
55 // Resolve relative paths against the interpreter's cwd, not
56 // the process cwd — hook commands are authored with the hook
56 // the process cwd — hook commands are authored with the hook
57 // Runner's cwd in mind and sub-shells can cd before an exec.
57 // Runner's cwd in mind and sub-shells can cd before an exec.
58− if !filepath.IsAbs(scriptPath) {
58+ scriptPath := filepathext.SmartJoin(interp.HandlerCtx(ctx).Dir, args[0])
59− scriptPath = filepath.Join(interp.HandlerCtx(ctx).Dir, scriptPath)
60− }
61 probe, err := probeFile(scriptPath)
59 probe, err := probeFile(scriptPath)
62 if err != nil {
60 if err != nil {
63 return err
61 return err