Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions pkg/path/expand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package path

import (
"os"
"path/filepath"
"strings"
)

// ExpandPath expands shell-like patterns in a file path:
// - ~ or ~/ at the start is replaced with the user's home directory
// - Environment variables like ${HOME} or $HOME are expanded
func ExpandPath(p string) string {
if p == "" {
return p
}

// Expand environment variables
p = os.ExpandEnv(p)

// Expand tilde to home directory
if p == "~" || strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~"+string(filepath.Separator)) {
if home, err := os.UserHomeDir(); err == nil {
if p == "~" {
return home
}
return filepath.Join(home, p[2:])
}
}

return p
}
76 changes: 76 additions & 0 deletions pkg/path/expand_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package path

import (
"os"
"path/filepath"
"testing"
)

func TestExpandPath(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
input string
envSetup map[string]string
expected string
}{
{
name: "empty path",
input: "",
expected: "",
},
{
name: "tilde only",
input: "~",
expected: home,
},
{
name: "tilde with subpath",
input: "~/data/memory.db",
expected: filepath.Join(home, "data", "memory.db"),
},
{
name: "env var",
input: "${HOME}/.data/memory.db",
expected: filepath.Join(home, ".data", "memory.db"),
},
{
name: "custom env var",
input: "${MY_TEST_DATA_DIR}/memory.db",
envSetup: map[string]string{"MY_TEST_DATA_DIR": "/tmp/testdata"},
expected: "/tmp/testdata/memory.db",
},
{
name: "absolute path unchanged",
input: "/absolute/path/memory.db",
expected: "/absolute/path/memory.db",
},
{
name: "relative path unchanged",
input: "relative/path/memory.db",
expected: "relative/path/memory.db",
},
{
name: "tilde and env var combined",
input: "~/${MY_TEST_SUBDIR}/memory.db",
envSetup: map[string]string{"MY_TEST_SUBDIR": "data"},
expected: filepath.Join(home, "data", "memory.db"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.envSetup {
t.Setenv(k, v)
}
result := ExpandPath(tt.input)
if result != tt.expected {
t.Errorf("ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
40 changes: 19 additions & 21 deletions pkg/teamloader/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ func NewDefaultToolsetRegistry() *ToolsetRegistry {
return r
}

// resolveToolsetPath expands shell patterns (~, env vars) in the given path,
// then validates it relative to the working directory or parent directory.
func resolveToolsetPath(toolsetPath, parentDir string, runConfig *config.RuntimeConfig) (string, error) {
toolsetPath = path.ExpandPath(toolsetPath)

var basePath string
if filepath.IsAbs(toolsetPath) {
basePath = ""
} else if wd := runConfig.WorkingDir; wd != "" {
basePath = wd
} else {
basePath = parentDir
}

return path.ValidatePathInDirectory(toolsetPath, basePath)
}

func createTodoTool(_ context.Context, toolset latest.Toolset, _ string, _ *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
if toolset.Shared {
return builtin.NewSharedTodoTool(), nil
Expand All @@ -98,16 +115,7 @@ func createTasksTool(_ context.Context, toolset latest.Toolset, parentDir string
toolsetPath = "tasks.json"
}

var basePath string
if filepath.IsAbs(toolsetPath) {
basePath = ""
} else if wd := runConfig.WorkingDir; wd != "" {
basePath = wd
} else {
basePath = parentDir
}

validatedPath, err := path.ValidatePathInDirectory(toolsetPath, basePath)
validatedPath, err := resolveToolsetPath(toolsetPath, parentDir, runConfig)
if err != nil {
return nil, fmt.Errorf("invalid tasks storage path: %w", err)
}
Expand All @@ -122,18 +130,8 @@ func createMemoryTool(_ context.Context, toolset latest.Toolset, parentDir strin
var validatedMemoryPath string

if toolset.Path != "" {
// Explicit path provided - resolve relative to working dir or parent dir
var basePath string
if filepath.IsAbs(toolset.Path) {
basePath = ""
} else if wd := runConfig.WorkingDir; wd != "" {
basePath = wd
} else {
basePath = parentDir
}

var err error
validatedMemoryPath, err = path.ValidatePathInDirectory(toolset.Path, basePath)
validatedMemoryPath, err = resolveToolsetPath(toolset.Path, parentDir, runConfig)
if err != nil {
return nil, fmt.Errorf("invalid memory database path: %w", err)
}
Expand Down
Loading