183 lines
5.2 KiB
Go
183 lines
5.2 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"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)
|
|
}
|
|
|
|
// ResolveSessionMode returns "direct" or "persistent" based on CTASK_SESSION_MODE.
|
|
// Default (unset/empty) is "direct". Any other value falls back to "direct"
|
|
// after printing a stderr warning. Used by entry commands (new, resume, last,
|
|
// open) to dispatch between the standard session.Run path and the tmux-backed
|
|
// persistent path.
|
|
func ResolveSessionMode() string {
|
|
v := os.Getenv("CTASK_SESSION_MODE")
|
|
switch v {
|
|
case "", "direct":
|
|
return "direct"
|
|
case "persistent":
|
|
return "persistent"
|
|
default:
|
|
fmt.Fprintf(os.Stderr,
|
|
"[ctask] warning: CTASK_SESSION_MODE=%q is not recognized; using direct mode\n", v)
|
|
return "direct"
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|