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. When // CTASK_PROJECT_ROOT is set, adds that (unless it duplicates CTASK_ROOT). // When CTASK_PROJECT_ROOT is unset, adds $CTASK_ROOT/projects/ so that // projects created under the default category are discoverable from any // shell without per-session env var setup. func SearchRoots() []string { taskRoot := ResolveRoot() roots := []string{taskRoot} seen := map[string]struct{}{searchRootKey(taskRoot): {}} add := func(p string) { if p == "" { return } key := searchRootKey(p) if _, dup := seen[key]; dup { return } seen[key] = struct{}{} roots = append(roots, p) } projRoot := ResolveProjectRoot() if projRoot != "" { add(projRoot) } else { // Default fallback: projects live under $CTASK_ROOT/projects/ when // no override is set. Explicit search root makes discovery work // from any shell. add(filepath.Join(taskRoot, "projects")) } return roots } // searchRootKey returns the dedup key for a root path: cleaned, and // lower-cased on Windows for case-insensitive comparison. func searchRootKey(p string) string { c := filepath.Clean(p) if runtime.GOOS == "windows" { return strings.ToLower(c) } return c } // 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". // launchDir is the workspace-relative project subdirectory for projects, // or empty for tasks and pre-v0.5 projects. func EnvVars(slug, mode, root, workspace, category, taskType, launchDir 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, "CTASK_LAUNCH_DIR": launchDir, } } // 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 }