package config import ( "os" "path/filepath" "runtime" "strings" ) // ResolveRoot returns the absolute workspace root path. // Reads CTASK_ROOT env var, falls back to ~/ai-workspaces (or %USERPROFILE%\ai-workspaces on Windows). func ResolveRoot() string { root := os.Getenv("CTASK_ROOT") if root == "" { home, _ := os.UserHomeDir() return filepath.Join(home, "ai-workspaces") } return expandPath(root) } // ResolveAgent returns the agent command. // Reads CTASK_AGENT env var, falls back to "claude". func ResolveAgent() string { agent := os.Getenv("CTASK_AGENT") if agent == "" { return "claude" } 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) } // SearchRoots returns the deduplicated list of workspace roots that all query // and listing operations must consult. Always includes CTASK_ROOT; also // includes CTASK_PROJECT_ROOT when set and different from CTASK_ROOT. func SearchRoots() []string { taskRoot := ResolveRoot() roots := []string{taskRoot} projRoot := ResolveProjectRoot() if projRoot == "" { return roots } if samePath(taskRoot, projRoot) { return roots } return append(roots, projRoot) } // samePath reports whether two absolute paths refer to the same directory // on the current platform. Uses case-insensitive compare on Windows. func samePath(a, b string) bool { ac := filepath.Clean(a) bc := filepath.Clean(b) if runtime.GOOS == "windows" { return strings.EqualFold(ac, bc) } return ac == bc } // EnvVars returns the environment variables to export into child sessions. // taskType must be "task" or "project"; an empty value defaults to "task". func EnvVars(slug, mode, root, workspace, category, taskType string) map[string]string { if taskType == "" { taskType = "task" } return map[string]string{ "CTASK_TASK": slug, "CTASK_MODE": mode, "CTASK_ROOT": root, "CTASK_WORKSPACE": workspace, "CTASK_CATEGORY": category, "CTASK_TYPE": taskType, } } // 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 }