feat(v0.6): config file parser + resolver + source attribution
Phase 1 foundation. Adds: - internal/config/configfile.go — strict-key YAML parser for the global config file. Unknown top-level keys invalidate the entire file (no partial apply); future schema versions are rejected with an upgrade message. Platform-appropriate path (XDG_CONFIG_HOME or ~/.config on Unix, %APPDATA% on native Windows). - internal/config/resolver.go — Resolver / ResolvedSetting / SettingSource (Builtin, ConfigFileSrc, EnvVar, CLIFlag, PlatformOverride). Each setting carries provenance plus an Override chain so callers can render "env var (overrides config file: X)" lines. session_mode applies the native-Windows platform override at the resolver layer with the configured value chained as Override. Exports SetConfigPathForTest and SetIsNativeWindowsForTest so cross-package tests can isolate themselves from host config or simulate the override. - internal/config/config.go — legacy ResolveRoot / ResolveAgent / ResolveSeedDir / ResolveProjectRoot / ResolveSessionMode now delegate to LoadResolver so config-file values take effect for entry commands. ResolveProjectRoot preserves the "" sentinel for built-in source so SearchRoots and doctor checkProjectRoot keep their existing fallback semantics. ResolveSessionMode preserves the v0.5.3 unknown-value stderr warning, distinguishing env-var and config-file sources in the message. - Tests: 6 LoadConfigFile cases (missing, basic, partial, unknown key, future schema, malformed YAML), 3 ConfigFilePath cases (XDG / ~/.config / %APPDATA%), 11 resolver cases including layering, override chains, path expansion, platform override, and SettingSource string rendering. - Test isolation: vulnerable tests in config_test.go and config_roots_test.go that asserted Builtin-source defaults now call SetConfigPathForTest to point at a nonexistent path, insulating them from developer-host config files. Three cmd tests that asserted persistent session_mode behavior now call SetIsNativeWindowsForTest to disable the platform override (Phase 1 introduces the override; the layering-only behavior these tests cover is tested separately in resolver_test.go). No new dependencies (gopkg.in/yaml.v3 was already in go.mod). No version bump (lands at the end of Phase 3 per the v0.6 spec).
This commit is contained in:
+49
-39
@@ -9,39 +9,34 @@ import (
|
||||
)
|
||||
|
||||
// ResolveRoot returns the absolute workspace root path.
|
||||
// Reads CTASK_ROOT env var, falls back to ~/ai-workspaces (or %USERPROFILE%\ai-workspaces on Windows).
|
||||
// v0.6: resolution order is CLI flag > CTASK_ROOT env var > config file
|
||||
// (ctask_root key) > built-in default (~/ai-workspaces or
|
||||
// %USERPROFILE%\ai-workspaces). No CLI flag participates in Phase 1.
|
||||
func ResolveRoot() string {
|
||||
root := os.Getenv("CTASK_ROOT")
|
||||
if root == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, "ai-workspaces")
|
||||
}
|
||||
return expandPath(root)
|
||||
return LoadResolver().CtaskRoot().Value
|
||||
}
|
||||
|
||||
// ResolveAgent returns the agent command.
|
||||
// Reads CTASK_AGENT env var, falls back to "claude".
|
||||
// v0.6: resolution order is CLI flag > CTASK_AGENT env var > config file
|
||||
// (default_agent key) > built-in default ("claude"). No CLI flag
|
||||
// participates in Phase 1.
|
||||
func ResolveAgent() string {
|
||||
agent := os.Getenv("CTASK_AGENT")
|
||||
if agent == "" {
|
||||
return "claude"
|
||||
}
|
||||
return agent
|
||||
return LoadResolver().DefaultAgent().Value
|
||||
}
|
||||
|
||||
// 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.
|
||||
// v0.6: resolution order is CTASK_SEED_DIR env var > config file
|
||||
// (seed_dir key) > built-in default (%APPDATA%\ctask\seed on Windows,
|
||||
// ~/.config/ctask/seed on Unix).
|
||||
func ResolveSeedDir() string {
|
||||
if v := os.Getenv("CTASK_SEED_DIR"); v != "" {
|
||||
return expandPath(v)
|
||||
}
|
||||
return defaultSeedDir("seed")
|
||||
return LoadResolver().SeedDir().Value
|
||||
}
|
||||
|
||||
// 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.
|
||||
// v0.6 Phase 1 does not give this setting a config-file equivalent
|
||||
// (spec lists only seed_dir, singular).
|
||||
func ResolveProjectSeedDir() string {
|
||||
if v := os.Getenv("CTASK_SEED_PROJECT_DIR"); v != "" {
|
||||
return expandPath(v)
|
||||
@@ -50,14 +45,20 @@ func ResolveProjectSeedDir() string {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Returns empty string when no user-supplied value is set (config file
|
||||
// or env var). The "" sentinel is load-bearing for SearchRoots() and
|
||||
// doctor's checkProjectRoot, both of which apply their own fallback
|
||||
// (the v0.5 $CTASK_ROOT/projects/ default).
|
||||
//
|
||||
// v0.6: a project_root value supplied via the config file or
|
||||
// CTASK_PROJECT_ROOT env var is returned verbatim; a missing/empty
|
||||
// value still returns "".
|
||||
func ResolveProjectRoot() string {
|
||||
v := os.Getenv("CTASK_PROJECT_ROOT")
|
||||
if v == "" {
|
||||
s := LoadResolver().ProjectRoot()
|
||||
if s.Source == Builtin {
|
||||
return ""
|
||||
}
|
||||
return expandPath(v)
|
||||
return s.Value
|
||||
}
|
||||
|
||||
// SearchRoots returns the deduplicated list of workspace roots that all query
|
||||
@@ -149,23 +150,32 @@ func defaultSeedDir(leaf string) string {
|
||||
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.
|
||||
// ResolveSessionMode returns "direct" or "persistent" — the effective
|
||||
// launch session mode after resolving CLI flag > env var > config file
|
||||
// > built-in default ("direct"), with the platform-override rule
|
||||
// applied (persistent → direct on native Windows). Unknown values from
|
||||
// the env var or config file are coerced to "direct" with a one-line
|
||||
// stderr warning, preserving the v0.5.3 behavior.
|
||||
//
|
||||
// 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"
|
||||
r := LoadResolver()
|
||||
s := r.SessionMode()
|
||||
if s.Value == "direct" || s.Value == "persistent" {
|
||||
return s.Value
|
||||
}
|
||||
// Unknown value path — preserve the v0.5.3 stderr warning. Source
|
||||
// of the bad value is reported so the user can find where to fix it.
|
||||
switch s.Source {
|
||||
case EnvVar:
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"[ctask] warning: CTASK_SESSION_MODE=%q is not recognized; using direct mode\n", s.Value)
|
||||
case ConfigFileSrc:
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"[ctask] warning: config session_mode=%q is not recognized; using direct mode\n", s.Value)
|
||||
}
|
||||
return "direct"
|
||||
}
|
||||
|
||||
// expandPath expands a leading ~/ and resolves to an absolute path when possible.
|
||||
|
||||
Reference in New Issue
Block a user