diff --git a/internal/config/config.go b/internal/config/config.go index 46a2942..e7d2bae 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,6 +3,7 @@ package config import ( "os" "path/filepath" + "runtime" "strings" ) @@ -14,16 +15,7 @@ func ResolveRoot() string { home, _ := os.UserHomeDir() return filepath.Join(home, "ai-workspaces") } - // Expand leading tilde - if strings.HasPrefix(root, "~/") || root == "~" { - home, _ := os.UserHomeDir() - root = filepath.Join(home, root[2:]) - } - abs, err := filepath.Abs(root) - if err != nil { - return root - } - return abs + return expandPath(root) } // ResolveAgent returns the agent command. @@ -36,6 +28,37 @@ func ResolveAgent() string { return agent } +// ResolveSeedDir returns the user general seed directory. +// Reads CTASK_SEED_DIR; falls back to %APPDATA%\ctask\seed on Windows or +// ~/.config/ctask/seed on Unix. +func ResolveSeedDir() string { + if v := os.Getenv("CTASK_SEED_DIR"); v != "" { + return expandPath(v) + } + return defaultSeedDir("seed") +} + +// ResolveProjectSeedDir returns the user project seed directory. +// Reads CTASK_SEED_PROJECT_DIR; falls back to %APPDATA%\ctask\seed-project on Windows +// or ~/.config/ctask/seed-project on Unix. +func ResolveProjectSeedDir() string { + if v := os.Getenv("CTASK_SEED_PROJECT_DIR"); v != "" { + return expandPath(v) + } + return defaultSeedDir("seed-project") +} + +// ResolveProjectRoot returns the project workspace root override. +// Returns empty string if CTASK_PROJECT_ROOT is not set; callers should fall back +// to ResolveRoot() in that case. +func ResolveProjectRoot() string { + v := os.Getenv("CTASK_PROJECT_ROOT") + if v == "" { + return "" + } + return expandPath(v) +} + // EnvVars returns the environment variables to export into child sessions. func EnvVars(slug, mode, root, workspace, category string) map[string]string { return map[string]string{ @@ -46,3 +69,30 @@ func EnvVars(slug, mode, root, workspace, category string) map[string]string { "CTASK_CATEGORY": category, } } + +// defaultSeedDir returns the platform-default location for a seed directory leaf. +func defaultSeedDir(leaf string) string { + if runtime.GOOS == "windows" { + appData := os.Getenv("APPDATA") + if appData == "" { + home, _ := os.UserHomeDir() + return filepath.Join(home, "AppData", "Roaming", "ctask", leaf) + } + return filepath.Join(appData, "ctask", leaf) + } + home, _ := os.UserHomeDir() + return filepath.Join(home, ".config", "ctask", leaf) +} + +// expandPath expands a leading ~/ and resolves to an absolute path when possible. +func expandPath(p string) string { + if strings.HasPrefix(p, "~/") || p == "~" { + home, _ := os.UserHomeDir() + p = filepath.Join(home, p[1:]) + } + abs, err := filepath.Abs(p) + if err != nil { + return p + } + return abs +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 48b2cc8..dc8e316 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -68,6 +68,82 @@ func TestRootResolvesTilde(t *testing.T) { } } +func TestResolveSeedDirDefault(t *testing.T) { + os.Unsetenv("CTASK_SEED_DIR") + got := ResolveSeedDir() + home, _ := os.UserHomeDir() + var want string + if runtime.GOOS == "windows" { + appData := os.Getenv("APPDATA") + if appData == "" { + t.Skip("APPDATA not set on this Windows host") + } + want = filepath.Join(appData, "ctask", "seed") + } else { + want = filepath.Join(home, ".config", "ctask", "seed") + } + if got != want { + t.Errorf("ResolveSeedDir default: got %q, want %q", got, want) + } +} + +func TestResolveSeedDirOverride(t *testing.T) { + dir := t.TempDir() + os.Setenv("CTASK_SEED_DIR", dir) + defer os.Unsetenv("CTASK_SEED_DIR") + got := ResolveSeedDir() + if got != dir { + t.Errorf("ResolveSeedDir override: got %q, want %q", got, dir) + } +} + +func TestResolveProjectSeedDirDefault(t *testing.T) { + os.Unsetenv("CTASK_SEED_PROJECT_DIR") + got := ResolveProjectSeedDir() + home, _ := os.UserHomeDir() + var want string + if runtime.GOOS == "windows" { + appData := os.Getenv("APPDATA") + if appData == "" { + t.Skip("APPDATA not set on this Windows host") + } + want = filepath.Join(appData, "ctask", "seed-project") + } else { + want = filepath.Join(home, ".config", "ctask", "seed-project") + } + if got != want { + t.Errorf("ResolveProjectSeedDir default: got %q, want %q", got, want) + } +} + +func TestResolveProjectSeedDirOverride(t *testing.T) { + dir := t.TempDir() + os.Setenv("CTASK_SEED_PROJECT_DIR", dir) + defer os.Unsetenv("CTASK_SEED_PROJECT_DIR") + got := ResolveProjectSeedDir() + if got != dir { + t.Errorf("ResolveProjectSeedDir override: got %q, want %q", got, dir) + } +} + +func TestResolveProjectRootEmptyWhenUnset(t *testing.T) { + os.Unsetenv("CTASK_PROJECT_ROOT") + got := ResolveProjectRoot() + if got != "" { + t.Errorf("ResolveProjectRoot with no override should return empty, got %q", got) + } +} + +func TestResolveProjectRootOverride(t *testing.T) { + dir := t.TempDir() + os.Setenv("CTASK_PROJECT_ROOT", dir) + defer os.Unsetenv("CTASK_PROJECT_ROOT") + got := ResolveProjectRoot() + if got != dir { + t.Errorf("ResolveProjectRoot override: got %q, want %q", got, dir) + } +} + func TestEnvVars(t *testing.T) { vars := EnvVars("my-slug", "local", "/abs/root", "/abs/root/cat/ws", "general") expected := map[string]string{