diff --git a/internal/agent/prompt/prompt.go b/internal/agent/prompt/prompt.go index 070f29c18d02f220d309d5827f8d0c8879e0e5a2..7609661f31940ac9bccde40334034dc1e16191f1 100644 --- a/internal/agent/prompt/prompt.go +++ b/internal/agent/prompt/prompt.go @@ -13,6 +13,7 @@ import ( "time" "github.com/charmbracelet/crush/internal/config" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/home" "github.com/charmbracelet/crush/internal/shell" "github.com/charmbracelet/crush/internal/skills" @@ -107,10 +108,7 @@ func processFile(filePath string) *ContextFile { func processContextPath(p string, store *config.ConfigStore) []ContextFile { var contexts []ContextFile - fullPath := p - if !filepath.IsAbs(p) { - fullPath = filepath.Join(store.WorkingDir(), p) - } + fullPath := filepathext.SmartJoin(store.WorkingDir(), p) info, err := os.Stat(fullPath) if err != nil { return contexts diff --git a/internal/agent/tools/glob.go b/internal/agent/tools/glob.go index 7011d53b2067d2968982e8b689ee752d2af795a3..dd10c570e541a8bbe6405ca6a6bb33c45583f09c 100644 --- a/internal/agent/tools/glob.go +++ b/internal/agent/tools/glob.go @@ -13,6 +13,7 @@ import ( "strings" "charm.land/fantasy" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/fsext" ) @@ -96,10 +97,7 @@ func runRipgrep(cmd *exec.Cmd, searchRoot string, limit int) ([]string, error) { if len(p) == 0 { continue } - absPath := string(p) - if !filepath.IsAbs(absPath) { - absPath = filepath.Join(searchRoot, absPath) - } + absPath := filepathext.SmartJoin(searchRoot, string(p)) if fsext.SkipHidden(absPath) { continue } diff --git a/internal/config/load.go b/internal/config/load.go index d7a04e27c1e35e91c49a034e09c2e0d926ee536d..bc75422a082d0a20d081a700ae3c126bd727b132 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -21,6 +21,7 @@ import ( "github.com/charmbracelet/crush/internal/agent/hyper" "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/env" + "github.com/charmbracelet/crush/internal/filepathext" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/home" powernapConfig "github.com/charmbracelet/x/powernap/pkg/config" @@ -429,6 +430,7 @@ func (c *Config) setDefaults(workingDir, dataDir string) { c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory) } } + c.Options.DataDirectory = filepath.Clean(filepathext.SmartJoin(workingDir, c.Options.DataDirectory)) if c.Providers == nil { c.Providers = csync.NewMap[string, ProviderConfig]() } diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 991012616b33daa6d439d93c7cc266d6e83f2e5c..0a50cce5ea38c8f73bfa60111d410dc15a60fd4e 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -74,22 +74,43 @@ func testStore(cfg *Config) *ConfigStore { } func TestConfig_setDefaults(t *testing.T) { - cfg := &Config{} + t.Run("sets default data directory", func(t *testing.T) { + cfg := &Config{} + workingDir := t.TempDir() + + cfg.setDefaults(workingDir, "") + + require.NotNil(t, cfg.Options) + require.NotNil(t, cfg.Options.TUI) + require.NotNil(t, cfg.Options.ContextPaths) + require.NotNil(t, cfg.Providers) + require.NotNil(t, cfg.Models) + require.NotNil(t, cfg.LSP) + require.NotNil(t, cfg.MCP) + require.Equal(t, filepath.Join(workingDir, ".crush"), cfg.Options.DataDirectory) + require.Equal(t, "AGENTS.md", cfg.Options.InitializeAs) + for _, path := range defaultContextPaths { + require.Contains(t, cfg.Options.ContextPaths, path) + } + }) - cfg.setDefaults("/tmp", "") + t.Run("resolves relative configured data directory from working directory", func(t *testing.T) { + cfg := &Config{Options: &Options{DataDirectory: "."}} + workingDir := filepath.Join(t.TempDir(), "worktree") - require.NotNil(t, cfg.Options) - require.NotNil(t, cfg.Options.TUI) - require.NotNil(t, cfg.Options.ContextPaths) - require.NotNil(t, cfg.Providers) - require.NotNil(t, cfg.Models) - require.NotNil(t, cfg.LSP) - require.NotNil(t, cfg.MCP) - require.Equal(t, filepath.Join("/tmp", ".crush"), cfg.Options.DataDirectory) - require.Equal(t, "AGENTS.md", cfg.Options.InitializeAs) - for _, path := range defaultContextPaths { - require.Contains(t, cfg.Options.ContextPaths, path) - } + cfg.setDefaults(workingDir, "") + + require.Equal(t, workingDir, cfg.Options.DataDirectory) + }) + + t.Run("resolves relative flag data directory from working directory", func(t *testing.T) { + cfg := &Config{} + workingDir := filepath.Join(t.TempDir(), "worktree") + + cfg.setDefaults(workingDir, "./state") + + require.Equal(t, filepath.Join(workingDir, "state"), cfg.Options.DataDirectory) + }) } func TestConfig_configureProviders(t *testing.T) { diff --git a/internal/shell/dispatch.go b/internal/shell/dispatch.go index 869970639a5d3ba597ddb59e4486ea484972fe5d..2acff082e7a3fdf2f05796987ad63a4cc0e0458f 100644 --- a/internal/shell/dispatch.go +++ b/internal/shell/dispatch.go @@ -14,6 +14,7 @@ import ( "runtime" "strings" + "github.com/charmbracelet/crush/internal/filepathext" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" @@ -51,13 +52,10 @@ func scriptDispatchHandler(blockFuncs []BlockFunc) func(next interp.ExecHandlerF return next(ctx, args) } - scriptPath := args[0] // Resolve relative paths against the interpreter's cwd, not // the process cwd — hook commands are authored with the hook // Runner's cwd in mind and sub-shells can cd before an exec. - if !filepath.IsAbs(scriptPath) { - scriptPath = filepath.Join(interp.HandlerCtx(ctx).Dir, scriptPath) - } + scriptPath := filepathext.SmartJoin(interp.HandlerCtx(ctx).Dir, args[0]) probe, err := probeFile(scriptPath) if err != nil { return err